Skip to content

Latest commit

 

History

History
202 lines (159 loc) · 9 KB

plugin-api.md

File metadata and controls

202 lines (159 loc) · 9 KB

Plugin API

Everything a plugin can declare, receive and do, in plugin API version 1. For a walk-through, read Writing a plugin first.

How a plugin runs

Pythonator's background service                 the plugin's own process (sandboxed)
┌──────────────────────────────┐   JSON lines   ┌───────────────────────────────────┐
│ plugin host                  │ ─────────────▶ │ worker: loads plugin.py,          │
│  · checks every message      │   stdin        │ calls your on_* methods           │
│  · applies permissions       │ ◀───────────── │                                   │
│  · rate limit, size limit    │   stdout       │ self.notify(), self.show(), ...   │
└──────────────────────────────┘                └───────────────────────────────────┘

Pythonator starts Pythonator --plugin-worker <plugin folder> <data folder> inside the same sandbox services use, with network only if the plugin was granted network. The worker imports your file (compiling it fresh, never using .pyc files shipped with it), sends hello, and then calls your methods as messages arrive, one at a time.

Your code never sees Pythonator's objects, only copies of the data your permissions allow.

plugin.toml

[plugin]
id = "my-plugin"            # required: a-z, 0-9 and -, 2 to 41 characters, starting with a letter
name = "My plugin"          # required: shown everywhere, up to 64 characters
version = "1.2.0"           # required: 1 to 4 numbers, optional suffix (1.2.0-beta1)
api = 1                     # the plugin API it was written for; a newer one than Pythonator's is refused
entry = "plugin.py"         # the Python file, inside the plugin folder
description = "..."         # up to 300 characters
permissions = ["network"]   # see below
author = "you"
homepage = "https://..."

[[plugin.settings]]         # any number, up to 40
key = "interval"            # a-z, 0-9 and _, starting with a letter
label = "Seconds between checks"
kind = "int"                # text, secret, bool, int, choice, lines
default = 30
choices = []                # for kind = "choice"
help = "Shown under the field."

The file must be under 64 KB. Unknown keys are refused, so a typo is an error, not a silent no-op. Nothing in the plugin is imported to read it.

Permissions

Name Gives
network Network in the plugin's sandbox
read_logs on_output calls with the services' output
inspect cpu, memory, listening and remote in ServiceInfo, and on_stats calls
control start, stop, restart work (in test mode they are recorded instead)
policy check_start is asked before each start (in test mode a refusal is only recorded)
ui show displays a panel

Official plugins get what they declare. Third-party plugins get only what the user grants, which can be less. self.permissions holds what this run was granted, and self.test_mode whether it is in test mode.

The Plugin class

from pythonator.sdk import Plugin, ServiceEvent, ServiceInfo, ui

Define exactly one subclass of Plugin in your entry file.

Attributes

Attribute Type Holds
settings dict[str, Any] The settings, defaults filled in
services dict[str, ServiceInfo] Every service by id, kept up to date
permissions frozenset[str] Permissions granted for this run
test_mode bool Whether actions are only recorded
data_dir Path A private folder for the plugin's own files; the only place it can write

Methods you override

Method Called
on_start() Once, after settings and services are filled in. Start your threads here
on_stop() Before the plugin is stopped. Keep it short
on_event(event) A service started, stopped, crashed... (see the kinds below)
on_output(service, lines) New output of a service, up to 200 lines at a time (read_logs)
on_stats() Every 2 seconds, after services got new CPU, memory and ports (inspect)
check_start(service) -> str | None Before a start: return a reason to refuse it (policy); answer within 2 s
on_settings() The user saved new settings
on_action(action, value) A button of your panel was pressed
on_tick() About every 5 seconds

An exception in any of them is reported in the plugin's Activity with its traceback, and the plugin goes on.

Methods you call

Method Does Needs
log(text) A line in the plugin's Activity
notify(text, level="info") A notice in the window; level is info, ok, warn or error
show(panel) Shows or replaces your panel ui
start(id), stop(id), restart(id) Acts on a service control

They are safe to call from any thread.

ServiceInfo

Read-only. The fields under inspect are zero or empty without that permission.

Field Type
id, name str
runtime str python, node, go... or custom
mode str sandbox, container or full
state str stopped, starting, running, stopping, waiting, failed, task
network bool May use the network
ports tuple[int, ...] The ports it declares
expose str local or lan
memory_limit_mb int 0 for none
restarts int Restarts in the last 10 minutes
cpu float inspect: percent of one core
memory int inspect: bytes, whole process tree
listening tuple[int, ...] inspect: ports it really listens on
remote tuple[str, ...] inspect: address:port it is connected to, up to 50

ServiceEvent

Field
kind started, stopped, exited, crashed, gave_up, blocked, installing, installed, install_failed
service The ServiceInfo after the event
exit_code For exited and crashed
note A short explanation, for example the reason of a blocked start

Panels

Build panels with ui. They are data, checked by Pythonator and drawn with native widgets: text is always shown as text, never as HTML.

Builder Shows
ui.panel(title, *items) The panel: a title (up to 80 characters) and up to 200 items
ui.text(text, tone="") A paragraph, up to 4000 characters
ui.stat(label, value, tone="") A small card with a label and a big value
ui.table(columns, rows, tones=None) Up to 12 columns and 500 rows; tones colours each row
ui.button(label, action, value="") A button that calls on_action(action, value); action is letters, digits and _.:-
ui.row(*items) Items side by side, up to 20

A tone is "", ok, warn, error or muted. A panel holds at most 500 items in all. A panel that breaks these rules is refused, with the reason in the Activity, and the previous panel stays.

Limits

Limit Value What happens
Messages per second 200, bursts of 400 Extra messages are dropped; after 2000 dropped the plugin is disabled
Message size 64 KB The plugin is disabled
Not a protocol message The plugin is disabled
Crashes 3 in 10 minutes The plugin is disabled; before that it restarts after 2 s, then 4 s
check_start 2 s The start goes ahead without the plugin's answer

The protocol

You don't need this to write a plugin in Python; the SDK speaks it for you. One JSON object per line, UTF-8, with the message type in t.

From Pythonator to the plugin (on its stdin):

t Fields
init settings, permissions, test_mode, services (a list of service views)
event kind, exit, note, service
services services: the list changed
output id, lines
stats services with CPU, memory and ports
check req, service: answer with verdict
settings settings
action action, value
tick
stop

From the plugin to Pythonator (on its stdout):

t Fields
hello Sent once when loaded; Pythonator answers with init
log, error text
notify text, level
panel panel
control op (start, stop, restart), id
verdict req, deny (empty to allow)

The worker sends the protocol on the real stdout and redirects print() to stderr, which becomes the Activity.

Compatibility

The API version changes only when a change would break existing plugins. New optional fields and new methods don't change it. Pythonator refuses a plugin with a newer api than its own, with a message saying it needs a newer Pythonator.