Daemon architecture
The daemon (pithyxd) is the only privileged process on a Pithyx box: one C# binary (.NET 10, ASP.NET Core) with one SQLite database. It serves the desktop and the API, runs every system command through one command runner, and manages storage, accounts, file services, apps and Docker. The desktop never runs with elevated rights.
Overview
Section titled “Overview”The daemon is a modular monolith. Each module is a folder under daemon/src/Pithyx.Daemon: Apps, Auth, Backup, Commands, Database, Errors, FileServices, Files, Health, Hosting, Installer, Jobs, Layouts, Localization, Logs, Monitoring, Network, Notifications, Permissions, Realtime, Shares, Storage, SystemSettings, Updates and Users. The solution is daemon/Pithyx.slnx, the tests are in daemon/tests/Pithyx.Daemon.Tests.
- Process.
pithyx.serviceruns/usr/lib/pithyx/daemon/pithyxd. The unit restarts it on failure after 2 seconds, allows 5 starts in 120 seconds and does not restart on exit codes 65 and 78. - Ports. Kestrel listens on 7484. In real mode 7484 speaks TLS (
Hosting/WebTransport): a connection whose first byte is no TLS handshake goes to plain HTTP and is redirected tohttps://. On a real box (production build) it also listens on 443 (TLS) and 80 (a development build on the ports ofPITHYX_DEV_PROXY_PORTS). ThereReverseProxy/HostProxyruns first: it forwards the domains of proxy hosts with YARP’sIHttpForwarder, redirects those of redirect hosts and leaves every other name toTransportRedirect, which redirects to 7484, except/api/install-idon 80. Every TLS handshake gets its certificate by server name fromReverseProxy/ProxyService. Simulation and the installer’s live system serve plain HTTP. The certificates come fromTls/BoxCertificates. - Requests. Every
/apiendpoint requires a session unless it is marked anonymous. The policies areadmin,powerandappToken. - Static files. The daemon serves the desktop from
/usr/lib/pithyx/desktop(orPITHYX_DESKTOP_DIR), with a fallback to the single-page app. - Modes. Real mode talks to the system. Simulation mode (
PITHYX_SIMULATE=1) answers from fixtures; see Developing the daemon. - Installer mode. With
PITHYX_INSTALLER=1or the kernel command line optionpithyx.installer, only the installer and setup endpoints exist. - Exit codes. 1 means the database failed, 65 means the database was written by a newer daemon, 75 asks systemd to restart (after a configuration restore) and 78 means a second installation (see below).
Per-app user ids come from 40000 to 49999, user accounts from 20000 to 29999 and groups from 30000 to 39999. Ids are never reused.
Apps run as containers: the daemon talks to Docker over its unix socket and starts a backend with docker compose up --wait as a job. See Backends.
The daemon has no watchdog (sd_notify heartbeats) and no virtual machine module.
Command runner
Section titled “Command runner”Every external command goes through ICommandRunner. Code never calls zfs, zpool or smartctl directly.
RunAsync(CommandRequest, ct)returns aCommandResult.StreamLinesAsyncis for long-lived commands: the request timeout does not apply and cancelling kills the process.- A
CommandRequesthas the file name, an argument list (no shell), an optional timeout, indexes of sensitive arguments and optional standard input. Standard input is written once, closed and never logged. - The default timeout is 1 minute. A timeout kills the whole process tree.
- The runner sets
LC_ALL=C.UTF-8and passes the current trace id asTRACEPARENT, so a command can be matched to its request. - Each call is logged with command, arguments, exit code, duration and the output truncated to 4096 characters. Sensitive arguments appear as
***. - In simulation mode
FixtureCommandRunneranswers from a scenario folder with<id>.json,<id>.stdout, optional<id>.stderrand<id>.streamfiles. Record new fixtures withtools/fixtures/record.sh <scenario> <id> -- <command> [args...]; it anonymizes serial numbers and WWNs.
Parsers of command output are pure static functions with fixture tests, for example LsblkParser, ZfsDatasetParser and ZfsSnapshotParser.
Storage backend
Section titled “Storage backend”IStorageBackend has two members: ReadAsync returns the pool list and WatchAsync yields once per change. ZfsBackend is the only implementation; there is no Btrfs or mdadm backend.
Around it sit the disk and pool inventories, the planners (StoragePlanner, DatasetPlanner, PoolImportScanner) and the command plans (ZfsPlans, SnapshotPlans, ZfsScrubPlans, ImportPlans) that are shown to the user before they run. DiskGuard refuses destructive actions on disks that hold data unless the user confirms with the disk serial.
Storage state reaches clients through the topics storage/disks, storage/pools and storage/alerts.
Database and migrations
Section titled “Database and migrations”The daemon uses SQLite through Dapper with explicit SQL, no ORM and no repository layer. The database is /var/lib/pithyx/pithyx.db, or the path in PITHYX_DB. It runs in WAL mode. Logs live in a separate logs.db.
- Migrations are embedded files
daemon/src/Pithyx.Daemon/Database/Migrations/NNNN_name.sql(28 today). Each one runs in its own transaction; a failure exits with code 1. The tableschema_versionrecords what ran. - Before migrating a non-empty database, the daemon writes a copy with
VACUUM INTOnamedpithyx.db.pre-<NNNN>-<time>.bak, whereNNNNis the first pending version. - If the database contains versions the daemon does not know, the daemon logs a critical message and exits with 65. Start a newer daemon or point
PITHYX_DBat another database. - Dates are stored as UTC ISO 8601 text.
- The configuration backup covers the database, the Samba password database, TLS certificates and SSH host keys. A new table must belong to a backup part or be marked as runtime state.
Paths on the box
Section titled “Paths on the box”| Path | Content |
|---|---|
/var/lib/pithyx/pithyx.db |
The database (override with PITHYX_DB) |
/var/lib/pithyx/catalogs/ |
One clone per enabled catalog |
/var/lib/pithyx/tls/ |
The box’s certificate authority, its own certificate and an uploaded one (PEM, readable by root only) |
/var/log/pithyx/ |
daemon.json and logs.db (override with PITHYX_LOG_DIR) |
/var/cache/pithyx/ |
Caches (override with PITHYX_CACHE_DIR) |
/usr/lib/pithyx/desktop/ |
The built desktop |
/etc/pithyx/environment |
dev marks a development machine; anything else means prod |
/<pool>/.pithyx/ |
App data and Docker data on the app pool, never on the system disk |
A machine has exactly one Pithyx installation. A daemon started from a checkout without PITHYX_DB on a machine that already has /etc/pithyx/environment refuses to start and exits with 78.