%% socket_server.pl — Unix domain socket listener for Talmudic Engine. %% %% Construction §3.3: "JSON over Unix domain socket" %% Step 14 (Architecture §4): "Connect components" %% %% Listens on a Unix domain socket, accepts JSON requests, %% dispatches to api:process_request/2 via json_bridge, %% returns JSON responses. %% %% Protocol: One JSON object per line (same as Haskell server). %% Connection model: accept, handle one or more requests per %% connection, close when client disconnects. :- module(socket_server, [ serve/0, serve/1, stop_server/0 ]). :- use_module(library(socket)). :- use_module(library(json)). :- use_module(library(readutil)). :- use_module('../interface/api'). :- use_module('../interface/json_bridge'). %% Default socket path — dev path; production uses /var/run/shorashim/talmudic.sock default_socket_path('/tmp/shorashim/talmudic.sock'). %% Dynamic flag to control server loop :- dynamic server_running/1. %% ============================================================ %% SERVER ENTRY POINTS %% ============================================================ %% serve/0 — Start server on default socket path serve :- default_socket_path(Path), serve(Path). %% serve/1 — Start server on given socket path serve(Path) :- %% Clean up any stale socket file ( exists_file(Path) -> delete_file(Path) ; true ), %% Ensure parent directory exists file_directory_name(Path, Dir), ( exists_directory(Dir) -> true ; make_directory_path(Dir) ), %% Create Unix domain socket unix_domain_socket(ServerSocket), tcp_bind(ServerSocket, Path), tcp_listen(ServerSocket, 5), format('Talmudic Engine listening on ~w~n', [Path]), flush_output, %% Set running flag assert(server_running(true)), %% Accept loop accept_loop(ServerSocket, Path). %% Stop the server gracefully stop_server :- retractall(server_running(_)). %% ============================================================ %% CONNECTION HANDLING %% ============================================================ %% Main accept loop — waits for connections accept_loop(ServerSocket, Path) :- ( server_running(true) -> catch( ( tcp_accept(ServerSocket, ClientSocket, _Peer), tcp_open_socket(ClientSocket, InStream, OutStream), %% Handle this client's requests catch( handle_connection(InStream, OutStream), Error, ( format(user_error, 'Connection error: ~w~n', [Error]), flush_output(user_error) ) ), close(InStream, [force(true)]), close(OutStream, [force(true)]), accept_loop(ServerSocket, Path) ), AcceptError, ( format(user_error, 'Accept error: ~w~n', [AcceptError]), flush_output(user_error), %% Clean up tcp_close_socket(ServerSocket), ( exists_file(Path) -> delete_file(Path) ; true ), fail ) ) ; %% Server stopped — clean up tcp_close_socket(ServerSocket), ( exists_file(Path) -> delete_file(Path) ; true ), format('Talmudic Engine stopped.~n'), flush_output ). %% Handle a single connection — read lines until EOF handle_connection(InStream, OutStream) :- read_line_to_string(InStream, Line), ( Line == end_of_file -> true ; process_json_line(Line, ResponseJson), %% Write JSON response on a single line json_write_dict(OutStream, ResponseJson), nl(OutStream), flush_output(OutStream), %% Continue reading from this connection handle_connection(InStream, OutStream) ). %% ============================================================ %% JSON REQUEST PROCESSING %% ============================================================ %% Process a JSON line string and produce a JSON response string process_json_line(Line, ResponseJson) :- catch( ( %% Parse JSON string to dict atom_json_dict(Line, RequestDict, []), %% Convert JSON to Prolog request json_to_request(RequestDict, Request), %% Dispatch to API process_request(Request, Response), %% Convert response to JSON response_to_json(Response, ResponseJson) ), Error, ( term_string(Error, ErrorStr), ResponseJson = json{status: "error", error: json{code: "PROCESSING_ERROR", message: ErrorStr}} ) ).