I’m writing some Node code for a Visual Studio Code extension that needs to communicate with a swipl process without blocking the terminal or “polluting” it, and thanks to suggestions by Jan I managed to achieve this quite easily through a TCP connection.
I have some questions though about how to best dispose of resources (both for sockets and their streams), plus some other things about the overall pattern.
Code
The file creating the TCP server is loaded on running the process (swipl -f init.pl
). As it is, the prolog top level is blocked/not shown until a TCP client connects, but that’s fine with me as I would have prevented the terminal from being shown to the user until the TCP connection is set up anyway.
(The code is without resource disposal as that is part of the questions below.)
:- initialization(enable_tcp_reply(_)).
enable_tcp_reply(Port) :-
% Set the server up and start listening
tcp_socket(Server),
tcp_bind(Server, localhost:Port),
tcp_listen(Server, 1),
% Get the connecting client
tcp_accept(Server, Client, _),
tcp_open_socket(Client, In, Out),
% Start the main query loop (in a thread)
thread_create(read_and_reply(In, Out), _, [detached(true)]).
read_and_reply(In, Out) :-
% Read the query sent by the client
read_term(In, Goal, [variable_names(Vars)]),
% Call it once and return the bound variables in JSON form (as per my requisites)
once(Goal),
write_bindings_as_json(Out, Vars),
flush_output(Out),
% Redo
read_and_reply(In, Out).
For those interested, here are the parts in the Node script I’m using for testing related to the TCP client (actually the script is different and uses the readline
module to test queries interactively, but that’s unneeded).
Note that using "localhost"
as the host for createConnection()
(which is also its default) has the connection fail because of some IPv6 assumptions (found related issues on GitHub). The address must be specified directly.
const net = require("net");
// Create and connect the client
const client = net.createConnection({ host: "127.0.0.1", port: "..." }); // Add port
// Receive answers from the server (event-driven pattern; others are possible as sockets are also streams)
client.on("data", data => {
console.log(`data from server: ${data}`); // Actually call JSON.parse() and do stuff
});
// Test a query (the \.\s+$ pattern is required by read_term)
client.write("A=2. ");
// Logs 'data from server: {"A":2}'
Aside from a general code review for the Prolog part, my questions are below.
1) tcp_accept/3 signature and overall pattern
Currently I’m calling tcp_accept right after tcp_listen (as also mentioned in tcp_bind/2) and this seems to work fine, but I see that it deviates from the docs’ example, where tcp_accept is called with the server socket’s input stream (after opening it with tcp_open_socket) rather than with the server socket itself.
What’s the correct pattern and does it mean that tcp_accept/3 accepts both a socket and an input stream as its first argument? (possibly alluded to in tcp_open_socket/3)
Maybe the pattern with tcp_open_socket on the server is just to be able to close its streams and both are valid?
2) tcp_listen/2 with 1 max pending connections
I went this way as I only expect one connection to the TCP server (the one from the VSC extension), but I’m not sure it’s a sensible choice.
3) tcp_setopts(Socket, reuseaddr)
Found this in some implementations and I don’t know about its use cases (and if it fits mine).
4) Resource disposal / error handling
Currently I would only add the closing of opened client’s streams (using a stream pair rather than separate streams) and I don’t know if the server socket and its streams would be correctly disposed of on the swipl
process being terminated (either by the extension or by users closing the swipl terminal).
(For what concerns the spun thread, I guess I should be fine.)
Regarding errors, I’m thinking of handling both improper queries from the client (something that should never happen anyway, as queries in my case are in a precise form) and call errors in the same catch block (for now).
I’m less sure about handling the thread fail error that occurs on the client disconnecting, as I don’t see how a disconnection could occur on the extension part (but I think I should probably handle that anyway).
Anyway, here are the edits for code review:
:- initialization(enable_tcp_reply(_)).
enable_tcp_reply(Port) :-
...
thread_create(
setup_call_cleanup(
tcp_open_socket(Client, InOut), % Using a stream pair for one-time closing
(
stream_pair(InOut, In, Out),
read_and_reply(In, Out)
),
close(InOut)
),
_,
[detached(true)]
).
read_and_reply(In, Out) :-
% Try to read the query, execute it, and return its results all at once
catch(
(
read_term(In, Goal, [variable_names(Vars)]),
once(Goal),
write_bindings_as_json(Out, Vars)
),
_,
write(Out, '\"error\"') % Error notification (send anything)
),
% Flush either the JSON result or an error
flush_output(Out),
% Redo
read_and_reply(In, Out).