Architecture

plesty-server is built as model – services – presenter – view, with one extra module, host, for everything that depends on the operating system. The split answers one question for each piece of code: does it need the OS, the screen, or neither?

Layer

Package

Depends on

Knows about

Model

plesty.server.model

stdlib, yaml

Data and rules only: the fleet file, device specs, installed versions, job and process records, field-test requests and reports. Pure Python, no subprocesses, no Qt. Fully testable in memory.

Services

plesty.server.services

model, host, uv/git/zmq

The model’s side effects on the bench: creating environments (Installer), running commands as jobs (JobRunner), launching and stopping device servers (Supervisor). Take model objects in, hand model objects back.

Presenter

plesty.server.presenter

model, services

Bench — choreography: install → start, stop → field test → relaunch, fleet up. Driven by both the CLI and the GUI.

View

plesty.server.view

presenter, plesty.lib.ui.theme, PySide6

The Qt console: plesty-lib’s palette and fonts, our own layout (menu bar, device table, detail pane, dialogs). Renders state, emits intents, never touches a service — see The console.

CLI

plesty.server.cli

presenter

plesty-server commands — see Command line.

Host

plesty.server.host

stdlib

The only module with platform branches — see Cross-platform development.

Re-exec

plesty.server.reexec

stdlib

How plesty-server starts another copy of itself (python -m plesty.server with its own interpreter); see Packaging.

Client

plesty.bench

pyzmq, click

The remote client — a separate, published package; see Remote benches.

Dependencies point downwards only. Services never import each other’s internals; a presenter composes them.

The home directory

Everything plesty-server writes lives under one directory, ~/.plesty/server by default (PLESTY_SERVER_HOME, home: in the fleet file or --home override it). Home in the model is the one place that knows the layout:

~/.plesty/server/
├── fleet.yaml                  the bench's fleet (optional; cwd first)
├── fleets/                     where the console's New/Open/Save fleet dialogs start
├── packages/                   installed device modules, one env per version
│   └── plesty-pm100d/
│       ├── 0.2.1/              uv venv (relocatable)
│       ├── 0.2.1.json          InstalledVersion record
│       ├── 0.3.0/
│       └── 0.3.0.json
├── repos/                      git checkouts, one per (package, ref)
│   └── plesty-pm100d/exp/      checkout; its .venv is the environment
├── state/                      one ProcessRecord per started server
│   ├── pm100d.json
│   └── pm100d.exit             exit code, written when it ends
├── run/pm100d/                 the server's working directory
├── logs/
│   ├── pm100d.log              server stdout+stderr, appended per start
│   └── jobs/<job-id>.log       one log per job
└── jobs/
    ├── <job-id>.json           Job record
    └── <job-id>.exit

Every record is a small JSON file. That is deliberate: several plesty-server processes look at the same bench at once — a CLI call, the agent, the GUI — and a file is the simplest thing all of them can read and that survives the process that wrote it.

The model

Module

Holds

fleet, device, ports

Fleet and DeviceSpec — what fleet.yaml declares: package, version or git+ref, args, env, port, autostart. Port rules (explicit → --tcp-port in args → *_TCP_PORT in env → 5555) and allocation.

home

The directory layout above.

device_manager

What is installed: InstalledVersion (package, version, source pypi/git, ref, commit, env path) and DeviceManager — the inventory. resolve(spec) answers “which installed version serves this spec, or must we install?”.

job

Job records and JobStore — create/finish/list records, read logs by byte offset. Knows nothing about processes; refresh() takes is the pid alive and what exit code was left as callables.

process

ProcessRecord (what a server was started with) and ProcessStatus (stopped / starting / running / exited).

field_test

FieldTestRun (tier, address, gates → argv) and FieldTestReport (the published field-test.json, summarised).

catalogue

CatalogueEntry — one module of the hub catalogue (/api/modules/), its standard and check/field-test status.

envfile

The device’s .env (<home>/run/<name>/.env): parse/render.

The services

Service

Does

Installer

Creates environments with uv (and git for checkouts) and records them in the DeviceManager. See Installing.

launch

The detached launcher every server and job goes through. See Process management.

JobRunner

Runs a command as a job: launches, then supplies JobStore with the two facts it lacks (pid alive? exit code?). Install, update, field test and plesty check are all jobs.

Supervisor

Starts/stops/probes device servers and keeps their ProcessRecords. Liveness is a protocol probe (describe over ZMQ), not a pid check.

Catalogue

Fetches and searches the hub catalogue (stdlib HTTP, injectable fetcher); pypi_release asks PyPI what the registry does not know.

gitbin

Finds gitPATH, the usual Windows installs, <home>/tools/git — and installs a portable one for a Windows bench that has none.

Agent

Serves plesty.bench clients: runs CLI argument lists in-process and returns the output; fetch for reports.

How a request flows

“Start pm100d” from the CLI or the GUI:

presenter
  │ fleet = load_fleet(...)                     model
  │ spec  = fleet["pm100d"]                     model
  │ inst  = manager.resolve(spec)               model   ─ None? → installer.install(spec) (a job)
  │ py    = installer.python(inst)              service
  │ status = supervisor.start(spec, py)         service ─ launch() → ProcessRecord → probe until ready
  └ view.show(status)                           view

Nothing in that chain imports Qt until the last line, and nothing before the service line touches the operating system.

Long work runs as a job of plesty-server itself

Bench.install(), update() and field_test() return a Job at once. The job’s command is python -m plesty.server --home <home> -f <fleet> _job install <name> — the same program, a hidden command group, run detached through the launcher like every other job. So the install’s uv output lands in logs/jobs/<id>.log, the CLI streams it (plesty-server install pm100d follows the log until the exit code), the GUI tails the same file, and a closed terminal does not kill a half-done install. The foreground twins (install_now, update_now, field_test_now) are what the _job commands call.