Pythonator

Background service API

The window is only a view. The work is done by Pythonator’s background service (the daemon), which keeps running when the window closes. The window talks to it over a local socket, and so can your own scripts, a status-bar widget, a deploy script or a test harness. Anything the window can do, a script can do.

The socket isn’t on the network: no other computer and no other user of this computer can open it.

Connecting

System Address
macOS, Linux The Unix socket daemon.sock in the data folder, mode 0600. If that path is longer than 100 bytes (a deep PYTHONATOR_HOME), a socket can’t live there, and it is <16 hex>.sock in $XDG_RUNTIME_DIR/pythonator/ or else in <temp folder>/pythonator-<your uid>/, a folder only you can open; the hex is the start of the SHA-256 of the data folder’s full path. socket_path() in pyctl.py does this
Windows The named pipe \\.\pipe\pythonator-<16 hex>, where the hex is the start of the SHA-256 of the data folder’s full path in lower case; only your account may open it

The background service writes a fresh random token to daemon.token in the data folder (readable only by you) each time it starts. Send it first:

{"op": "hello", "req": 1, "token": "<the contents of daemon.token>"}

A wrong token, or any other message first, and the connection is closed. After a correct hello you receive a reply, then a snapshot of every service and its state, the list of plugins, and their panels.

If the background service isn’t running, start it with pythonator --daemon (the window does this for you).

Messages

Both directions send one JSON object per line, UTF-8, at most 1 MB per line.

Requests have an op and a req number of your choice. Every request gets exactly one reply with the same req:

{"ev": "reply", "req": 7, "ok": true, "data": ...}
{"ev": "reply", "req": 8, "ok": false, "error": "Invalid id: String should match pattern ..."}

A request with a field it doesn’t know, a missing field or a bad value is refused with an error, never guessed at.

Events arrive whenever something happens, to every connected client. They have an ev and no req.

Requests

op Fields Does, and data in the reply
hello token Authenticates; data: {"version", "protocol"}
ping   "pong"
snapshot   Every service, its state and the settings, as in the snapshot event
capabilities   {"sandbox": [[protection, status]...], "app": [[level, text]...]}, what System check shows
add service Adds a service (see the service object)
update service Replaces the service with the same id
remove id Stops and removes it, with its logs and sandbox folders (not its project)
start id, confirmed Starts it. A Full access service replies "error": "confirm_full" until sent with "confirmed": true once
stop, restart id  
start_all, stop_all    
input id, text Sends a line to the program’s input
install id Install packages
pull id git pull, fast-forward only
tail id The recent output, as one string
clear id Clears the recent output
watch id From now on, send this connection the output events of that service only ("": none). The window uses it for the service it shows
check id The environment check: [[level, text]...]
settings settings Saves the settings (plugin permissions change only through the plugin requests)
plugin_enable plugin, enabled  
plugin_settings plugin, values  
plugin_grant plugin, permissions, live Grants permissions to a third-party plugin; live: true leaves test mode
plugin_action plugin, action, value As if a panel button was pressed
plugin_install source A folder or a git address; data: the plugin’s id
plugin_remove plugin  
shutdown stop_services Ends the background service, stopping every service unless false

Events

ev Fields When
snapshot services, states, settings After hello
services services A service was added, changed or removed
state id, state, kind, pid, exit, note, restarts, started, task, next_run A service changed state. kind is the event: started, stopped, exited, crashed, gave_up, blocked, installing, installed, install_failed
output id, text New output, secrets masked, ANSI colour codes included
stats items: {id: {cpu, memory, processes, ports, remote}} Every 2 seconds, for running services
notice level, text Something to tell the user
plugins plugins A plugin was enabled, changed, crashed…
plugin_activity plugin, text A line in a plugin’s Activity
panel plugin, name, panel A plugin’s panel changed; panel: null removes it

A client that reads too slowly first gets only the newest output (with a notice saying so), and is disconnected if it falls 64 MB behind. The services don’t wait for anyone.

The service object

The same object is stored in services.json. Only name and folder are required.

{
  "id": "3f9a1c2b7d10",
  "name": "web api",
  "folder": "/home/me/code/api",
  "runtime": "python",
  "entry": "main.py",
  "command": [],
  "args": ["--workers", "2"],
  "env": {"LOG_LEVEL": "info"},
  "env_file": ".env",
  "mode": "sandbox",
  "network": {"enabled": true, "ports": [8000], "expose": "local"},
  "sandbox": {"write_project": true, "folders": [{"path": "/home/me/datasets", "write": false}]},
  "container": {"engine": "auto", "image": "", "network": "", "read_only_root": true},
  "restart": "on-failure",
  "max_restarts": 5,
  "stop_timeout": 10.0,
  "autostart": false,
  "memory_mb": 0,
  "git": {"url": "https://github.com/me/api.git", "branch": "", "pull_on_start": false},
  "schedule": ""
}
Field Values
runtime python, node, bun, deno, go, java, dotnet, ruby, php, rust, shell, powershell, or custom with command
mode sandbox, container, full
network.expose local (this computer) or lan (other computers too)
restart never, on-failure, always
container.engine auto, docker, podman
schedule empty, every 15m, @daily, or a cron line

Every field is checked the same way as in the window: see Services.

Example: a command-line client

pyctl.py is a complete client in under 90 lines of standard-library Python, for Windows, macOS and Linux:

python docs/examples/pyctl.py list
python docs/examples/pyctl.py start "web api"
python docs/examples/pyctl.py tail "web api"

The core of it:

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(socket_path())  # daemon.sock in the data folder, see Connecting
file = sock.makefile("rwb")
token = (home / "daemon.token").read_text().strip()
file.write(json.dumps({"op": "hello", "req": 1, "token": token}).encode() + b"\n")
file.flush()
for line in file:
    message = json.loads(line)
    ...

Versioning

hello returns the protocol number, 1 in this version. It changes only when a change would break existing clients; new requests, events and fields don’t change it.