Next: lib-linda, Previous: lib-jsonrpc, Up: The Prolog Library [Contents][Index]
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.
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)
.
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.
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"}