Next: , Previous: , Up: The Prolog Library   [Contents][Index]


10.22 Simpler use of JSON mediated process communication—library('jsonrpc/simple_jsonrpc_server')

This module provides a simplified interface to the JSON-RPC library (see JSON-RPC Server library).

This library should be considered pre-release, in that it may evolve, also incompatibly, as we gain more experience with how it is used.

Please note: You should always take the security implications into account if exposing any service to untrusted clients, e.g. on Internet.

This module simplifies calling of the underlying server library and adds some features for easier debugging and troubleshooting. It provides utilities for attaching the server process to a SPIDER debugger session, making it possible to do interactive debugging of a server. It also contains a simple logging facility.

Several of the options can be set using environment variables (or system properties), which is especially useful when the server is launched as a sub-process of some other program that you do not directly control. For instance, this makes it possible to have a C program that runs the Prolog server as a sub-process, but still interactively debug the Prolog sub-process using SPIDER.

The names of these environment variables can be set as an option, so the environment variables used for different servers do not conflict, even if launched in the same shell environment.

Exported predicates:

simple_jsonrpc_server_entrypoint(RequestHook, CallHook, EntrypointOptions)
simple_jsonrpc_server_entrypoint(RequestHook, EntrypointOptions)

The RequestHook and optional CallHook are passed to jsonrpc_server:jsonrpc_server_main/[4,5] when (if) the server is started, see its documentation for details.

The EntrypointOptions is a list of options:

environment(V)

V can be one of false (default), true, or true(Prefix), where Prefix is an atom.

If V is false no environment variables or system properties will be used for setting default option values.

If V is true or true(Prefix), some default options can be set using environment variables, as follows:

For each such option, a “base name” is documented, e.g. SERVER_LOGGING for the logging/1 option. If the environment variable is set in the environment, its value will be used for setting the default for the corresponding option (i.e. options in EntrypointOptions always takes precedence).

If a Prefix is specified, it is prefixed to the base name to obtain the name of the environment variable. For instance, with the option environment(true('MY_ENGINE_')), the default value for the logging/1 option will be taken from the environment variable MY_ENGINE_SERVER_LOGGING, if set.

Note that the boolean option values true and false corresponds to yes and no when specified using an environment variable. As usual, it is possible to use system properties instead of environment variables.

state(StateIn)

This is passed as the input state to the server.

start(Boolean)

Boolean is true (default) or false. If Boolean is true the server is started, otherwise the arguments are recorded and the server can be started later with simple_jsonrpc_server_start_from_saved_options/0. The latter is useful when doing interactive debugging, e.g. using the attach_spider(true) option, below.

The environment variable base name for this option is SERVER_AUTOSTART and setting it to no changes the default to start(false).

logging(Boolean)

Boolean is true or false (default). If true, logging is enabled, both for the exported simple_jsonrpc_server_log/2 and for the server.

The environment variable base name for this option is SERVER_LOGGING and setting it to yes changes the default to logging(true).

halt(Boolean)

Boolean is true (default) or false. If true the server will call halt/0 on exit, ending the Prolog process. Halting automatically is often useful when invoked as a sub-process, but may be inconvenient when doing interactive debugging of the server code.

The environment variable base name for this option is SERVER_HALT and setting it to no changes the default to halt(false).

attach_spider(How)

How is one of true, try or false (default). If true or try an attempt is made to attach to a SPIDER session (SPIDER should be waiting to “Attach to Prolog Process"), before the server is started. If How is try, failure to attach is silently ignored (but logged, if logging is enabled).

The environment variable base name for this option is SERVER_ATTACH_SPIDER and setting it to yes or try changes the default to attach_spider(true) and attach_spider(try), respectively.

If attaching to SPIDER it is often useful to also specify start(false) and halt(false), possibly using environment variables, as described above.

simple_jsonrpc_server_start_from_saved_options

Start the server using the recorded options and state from the entry point (simple_jsonrpc_server_entrypoint/[2,3]). Fails if there is no saved information.

This is useful for interactive debugging of the server, e.g. when specifying the options attach_spider(true) and start(false). In this case, once the server has attached to SPIDER, you can start the server in the debugger using trace, simple_jsonrpc_server_start_from_saved_options..

simple_jsonrpc_server_saved_options(-RequestHook, -CallHook, -EntrypointOptions, -Options)

The entry point options and jsonrpc_server options that was used in, and recorded by, simple_jsonrpc_server_entrypoint/[2,3]. This is useful while debugging, e.g. to manually (re-) start the server with the original options. See simple_jsonrpc_server_start_from_saved_options/0.

simple_jsonrpc_server_log(+FormatControl, +FormatArgs)

This is like format(user_error, FormatControl, FormatArgs), but adds a prefix (‘SSERVER’) before, and a newline after the logged text. Does nothing unless logging is on.

The FormatControl should produce one line of output, e.g. it should not contain ~n or ~N.

simple_jsonrpc_server_try_attach_spider

Attempt to attach to a SPIDER session (that should be trying to ‘Attach to Prolog Process’). Fails if no connection could be made, succeeds also if already connected. This can be useful to call from a JSON-RPC request while debugging.

If it was possible to attach to SPIDER, the prolog flag informational is set to on, to ensure the debugger and toplevel interaction works properly.

Please note: This should not be called before the JSON-RPC server has been started since it may change the active current input/output streams in unexpected ways. See the attach_spider/1 option to simple_jsonrpc_server_entrypoint/[2,3].

simple_jsonrpc_server_detach_spider

Detach from a SPIDER session. Does nothing if not attached.

10.23 Examples

The following two examples showcase two very simple but complete servers, the first one using just “plain” requests (i.e. only a request hook, the second one using both a request hook and a call hook, where the call hook is used for backtracking over a data structure.

In these simple examples the state is a ground term that can directly be used as JSON data. This is not a requirement, and is probably rare in practice. More realistic code would use a more elaborate state, containing Prolog data that cannot directly be used as JSON data, and may contain variables. Among the example files is a program that maintain a collection of variables where the variables are constrained using library(clpfd).

10.23.1 Minimal Request Hook

This example implements a request hook that can manipulate a state consisting of a single integer.

% counter_server.pl
:- module(counter_server, [counter_server_entrypoint/0]).
:- use_module(simple_jsonrpc_server, []).

counter_server_entrypoint :-
   simple_jsonrpc_server_entrypoint(request_hook, [state(0)]).

request_hook(Message, ResultDescription, State1, State) :-
    Message = request(Method,_Id,_Params,_RPC),
    request_hook1(Method, ResultDescription, State1, State).

% Requests:
% 'current' (return current counter)
% 'increment' (increment and return new value)
% 'quit' (exit the server).
request_hook1('current', ResultDescription, StateIn, StateOut) :-
    ResultDescription = result(StateIn),
    StateOut = StateIn.
request_hook1('increment', ResultDescription, StateIn, StateOut) :-
    StateOut is StateIn + 1,
    ResultDescription = result(StateOut).
request_hook1('quit', ResultDescription, StateIn, StateOut) :-
    ResultDescription = quit('Bye'),
    StateOut = StateIn.

Example transcript, that reads the initial counter, and increments it a few times and finally tells the server to quit:

$ sicstus -l counter_server --goal 'counter_server_entrypoint.'
% ...
% compiled ... counter_server.pl ...
|: {"jsonrpc":"2.0","id":1,"method":"current"}
⇒ {"jsonrpc":"2.0","id":1,"result":0}
|: {"jsonrpc":"2.0","id":2,"method":"increment"}
⇒ {"jsonrpc":"2.0","id":2,"result":1}
|: {"jsonrpc":"2.0","id":3,"method":"increment"}
⇒ {"jsonrpc":"2.0","id":3,"result":2}
|: {"jsonrpc":"2.0","id":3,"method":"quit"}
⇒ {"jsonrpc":"2.0","id":3,"result":"Bye"}
$

Since this is run from the terminal, SICStus will prompt with ‘|:’ while waiting for input. The JSON text on those lines is the input to the program, the ‘’ precedes responses, i.e the output from the program.

10.23.2 Minimal Call Hook

This example implements a call hook that can backtrack over a state consisting of a list of atoms. In this simple case we do not need to convert between Prolog and JSON representations, since a Prolog list of atoms will correspond to a JSON array of strings. As in the previous example, ‘|:’ comes before input, whereas ‘’ comes before responses.

% member_server.pl
:- module(member_server, [member_server_entrypoint/0]).
:- use_module(library(lists), [reverse/2]).
:- use_module(simple_jsonrpc_server, []).

member_server_entrypoint :-
    State = [a,b,c],
    simple_jsonrpc_server_entrypoint(request_hook, call_hook, [state(State)]).

% The only "plain" request we accept is 'quit', to exit the server.
request_hook(Message, ResultDescription, StateIn, StateOut) :-
    Message = request(Method,_Id,_Params,_RPC),
    request_hook1(Method, ResultDescription, StateIn, StateOut).

% Requests:
% 'quit' (exit the server).
request_hook1('quit', ResultDescription, StateIn, StateOut) :-
    ResultDescription = quit('Bye'),
    StateOut = StateIn.


% Requests:
% 'elements' (return all the elements in the list)
% 'member' (return each element in turn on backtracking)
% 'reverse' (reverse the list in the state)
% The state is a list of atoms, which can thus be used as JSON data
% as-is.
call_hook(elements, _Variables, ResultDescription, StateIn, StateOut) :-
    ResultDescription = result(StateIn),
    StateOut = StateIn.
call_hook(member, _Variables, ResultDescription, StateIn, StateOut) :-
    member(Element, StateIn), % Backtrack over the list.
    ResultDescription = result(Element),
    StateOut = StateIn.
call_hook(reverse, _Variables, ResultDescription, StateIn, StateOut) :-
    reverse(StateIn, StateOut),
    ResultDescription = result(@(null)).

The first part of the example starts the server, retrieves the contents using a once-request with elements as the term. Since this is using once, it will not add to the stack of active calls.

$ sicstus -l member_server --goal "member_server_entrypoint."
% ...
% compiled ... member_server.pl ...
|: {"jsonrpc":"2.0","id":1,"method":"once","params":["elements"]}
⇒ {"jsonrpc":"2.0","id":1,"result":["a","b","c"]}

The client (i.e., in this case, the user at the keyboard) then uses a call request, with the term member, which will invoke the call_hook/5 which will call member/2 which will succeed with the first element (the JSON string "a") of the list, which will also be the (first) result of this call. This call, with ‘id’ 2, will then be referred to by retry and cut requests, below.

|: {"jsonrpc":"2.0","id":2,"method":"call","params":["member"]}
⇒ {"jsonrpc":"2.0","id":2,"result":"a"}

Just to demonstrate that you can send other requests between a call request and subsequent retry or cut, we now once again look at the state (it is unchanged, as expected).

|: {"jsonrpc":"2.0","id":3,"method":"once","params":["elements"]}
⇒ {"jsonrpc":"2.0","id":3,"result":["a","b","c"]}

Now we ask for a new solution from the active call, i.e. the next solution from the call to member/2. This succeeds, a second time, and returns "b". Asking for the next solution returns the last element of the list, "c".

|: {"jsonrpc":"2.0","id":4,"method":"retry","params":{"call_id":2}}
⇒ {"jsonrpc":"2.0","id":4,"result":"b"}
|: {"jsonrpc":"2.0","id":5,"method":"retry","params":{"call_id":2}}
⇒ {"jsonrpc":"2.0","id":5,"result":"c"}

If we now ask for yet another solution, we get back an error response, telling us that there were no more solution and that the call is no longer active:

|: {"jsonrpc":"2.0","id":6,"method":"retry","params":{"call_id":2}}
⇒ {"jsonrpc":"2.0","id":6,"error":{"code":-4711,"message":"Failure"}}

Since the call with identifier 2 is no longer active, an attempt to cut this goal will give an error: (output reformatted)

|: {"jsonrpc":"2.0","id":7,"method":"cut","params":{"call_id":2}}
⇒ {"jsonrpc":"2.0","id":7,
            "error":{"code":-4713,"message":"No active call",
                      "data":{"jsonrpc":"2.0","id":7,
                               "method":"cut","params":{"call_id":2}}}}

The error did not break anything, we can now request the elements, reverse the state, and when once again looking at the elements we find that the list in state has indeed been reversed:

|: {"jsonrpc":"2.0","id":8,"method":"once","params":["elements"]}
⇒ {"jsonrpc":"2.0","id":8,"result":["a","b","c"]}
|: {"jsonrpc":"2.0","id":9,"method":"once","params":["reverse"]}
⇒ {"jsonrpc":"2.0","id":9,"result":null}
|: {"jsonrpc":"2.0","id":10,"method":"once","params":["elements"]}
⇒ {"jsonrpc":"2.0","id":10,"result":["c","b","a"]}

As before, we now use a call request to call member/2, resulting in an active call (with id 11). Since the state list was reversed above, the first element is "c". We ask for one more solution, finding "b".

|: {"jsonrpc":"2.0","id":11,"method":"call","params":["member"]}
⇒ {"jsonrpc":"2.0","id":11,"result":"c"}
|: {"jsonrpc":"2.0","id":12,"method":"retry","params":{"call_id":11}}
⇒ {"jsonrpc":"2.0","id":12,"result":"b"}

Finally, we do not want any more solutions from this goal, so instead of asking for more elements we cut away the remaining alternatives. The call is now no longer active.

|: {"jsonrpc":"2.0","id":13,"method":"cut","params":{"call_id":11}}
⇒ {"jsonrpc":"2.0","id":13,"result":null}

Finally we look at the state once again, and tell the server to quit:

|: {"jsonrpc":"2.0","id":14,"method":"once","params":["elements"]}
⇒ {"jsonrpc":"2.0","id":14,"result":["c","b","a"]}
|: {"jsonrpc":"2.0","id":15,"method":"quit"}
⇒ {"jsonrpc":"2.0","id":15,"result":"Bye"}


Send feedback on this subject.