Leo’s WebSocket Server¶
leo/core/leoserver.py is websocket server providing Leo’s resources to clients. The server offers single or multiple concurrent websockets.
WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.
Clients can be written in any language: - leo.core.leoclient is an example client written in python. - leoInteg (https://github.com/boltex/leointeg) is written in typescript.
At startup, the server creates a single instance of Leo’s bridge. (core.leoBridge) Clients can open (or create) Leo outlines in the bridge. Thereafter, clients can query or change the contents of the outlines.
Clients encode requests as JSON strings and receive results from the server as JSON strings.
Run the leoserver.py script with the “–help” command line parameter to learn more about the server’s capabilities.
The communication protocol¶
The following sections describe the communication protocol, that is, the format of requests sent from the client and responses sent from the server.
Requests¶
JSON requests (from the client to the server) must have three keys:
“id”: A string containing a positive integer.
“action”: A string denoting the kind of request. There are three forms:
Actions prefixed with ‘!’ must be the name of public method of the LeoServer class in leoserver.py.
Actions prefixed with ‘-’ must be the name of a Leo command.
Otherwise, the action must be name of a method in one of Leo’s subcommander classes.
“param”: A dict containing additional information. The param key often contains “ap”, “text” and “keep” keys.
The server’s _ap_to_p and _p_to_ap methods convert Leo positions to archived positions (strings) that can be converted to JSON.
The “action” and “param” members mimic the “function name” and “parameters” or a procedure call. And is in fact implemented as a remote procedure call.
The LeoServer._do_message method checks and handles incoming requests in a one by one, blocking synchronous manner.
Responses¶
JSON responses (from the server to the client) always have the following keys unless the request was a ‘getter’ command:
“id”: The incoming request id this JSON message is responding to.
“commander”: A dict describing c, the open commander.
“node”: None, or an archived position describing c.p.
Optional keys:
“package”: A dict containing extra data.
“p”: “An archived position”.
If the request was a ‘getter’ command, the JSON response will only contain the specific structure that was requested.
Async messages¶
The server also sends asynchronous messages, i.e. messages that are not responses to a specific request, that are meant to communicate a changed state or request a user input through a modal dialog, or anything else that was not initiated by a connected clients’ direct request.
Examples of ‘async’ messages:
An entry added to the log pane.
Signal the detection of an external file change.
Signal the automatic refresh of an outline.
A signal to ask the user about reloading a Leo file that has changed after changing a git branch.
The main server loop¶
The ws_handler function contains the main server loop.
ws_handler dispatches incoming requests to the _do_request method, which then calls the appropriate handler, either in the LeoServer class, or (via Leo’s bridge) to Leo itself.
Request handlers raise the following exceptions:
ServerError indicates an invalid user request. The server returns a response describing the error.
InternalServerError and error within the server itself. The server prints an error message and stops.
TerminateServer causes the server to terminate normally.
Compatibility and extensibility¶
Requests from the client to the server may reference:
Public methods of the LeoServer class.
The names of Leo commands.
Methods of Leo’s subcommander classes.
The public methods of the LeoServer class can only be changed in an upward compatible manner. If necessary, new public methods can be added at any time.
Leo is a programming environment in which user scripts can access all of Leo’s source code. For this reason, the names of methods in Leo’s core change rarely. Similarly, clients can usually assume that the names of Leo commands won’t change.
Multiple concurrent connections¶
Since a WebSocket server can receive events from clients, process them to update the application state, and synchronize the resulting state across clients, leoserver.py has the ability to listen and interact with more than one concurrent client, and have the state of the underlying Leo core application be represented in real time to all connected clients.
If a server is started with the command line argument parameter ‘limit’ set to more than one (1), then it will allow for more than one client to be connected at the same time, sharing in real time the state, and control, of the underlying Leo instance.
This can be used to have multiple client programs share the same views and functionality of a single Leo instance. Or be used to live-share the editing process with collaborators or spectators in real time.
By default, this limit is set to one (1) to prevent multiple connections and force each client to have their own individual Leo server instances running on different ports.
Command line arguments¶
leoserver.py has optional command line arguments that you can provide to get specific behaviors from the server.
–file <filename> or -f <filename>
Open the given file after starting the server.
–persist
Prevent the server from closing when last client disconnects
–limit <number> or -l <number>
Allow more than one connection to be open simultaneously. By default, only one connection may be open at a time.
–address <address> or -a <address>
Set the listening address.
–port <number> or -p <number>
Set the listening port.
–help or -h
List all available command-line arguments.
Acknowledgments¶
Félix Malboeuf wrote the original version of leoserver.py, including the first draft of the all-important ws_handler function.
Edward K. Ream made the code more pythonic and provided Leo-specific advice.