# 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](gui.md). | | CLI | `plesty.server.cli` | presenter | `plesty-server …` commands — see [Command line](cli.md). | | Host | `plesty.server.host` | stdlib | The **only** module with platform branches — see [Cross-platform development](cross_platform.md). | | Re-exec | `plesty.server.reexec` | stdlib | How `plesty-server` starts another copy of itself (`python -m plesty.server` with its own interpreter); see [Packaging](packaging.md). | | Client | `plesty.bench` | `pyzmq`, `click` | The remote client — a separate, published package; see [Remote benches](remote.md). | 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: ```text ~/.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/.log one log per job └── jobs/ ├── .json Job record └── .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` (`/run//.env`): parse/render. | ## The services | Service | Does | |---|---| | `Installer` | Creates environments with `uv` (and `git` for checkouts) and records them in the `DeviceManager`. See [Installing](installing.md). | | `launch` | The detached launcher every server and job goes through. See [Process management](process_management.md). | | `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 `ProcessRecord`s. 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 `git` — `PATH`, the usual Windows installs, `/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: ```text 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 -f _job install `` — 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/.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.