Cross-platform development
plesty-server runs on Windows benches and on macOS/Linux developer
machines. This page lists what actually differs, and the one rule that keeps
those differences from spreading through the code.
The rule
Every branch on the operating system lives in
plesty/server/host.pyand nowhere else.
host.py is short and organised by question — which host is this,
filesystem layout, child processes — and every function documents its
Windows and POSIX answer side by side. The rest of the package asks
host and stays platform-blind: supervisor.py calls host.interrupt(pid)
and never mentions SIGINT or CTRL_BREAK_EVENT.
The rule is enforced, not just stated: tests/test_host.py greps every
module in the package for platform markers (sys.platform, os.name,
win32, creationflags, S_IWRITE, /Scripts/, taskkill, …) and fails
when one appears outside host.py. A contributor who adds a Windows
workaround in the wrong place gets a red test, not a code-review comment a
month later.
What differs, and what host does about it
Question |
POSIX (macOS, Linux) |
Windows |
|
|---|---|---|---|
Where is the interpreter of a venv? |
|
|
|
Which interpreter re-executes us? |
any; |
|
|
How do I start a long-running child with no window the operator sees? |
any child; no console exists |
a console program with no console to inherit gets a new, visible one — the waiter takes a hidden console of its own ( |
|
How do I start a process nobody has to reap? |
|
there is no |
|
Should I wait for the launcher after it reported a pid? |
yes — it has exited, waiting collects it (no zombie) |
no — it is the waiter; waiting would block until the server ends (this bug existed: |
|
How do I start a child without a console window popping up? |
nothing to do |
|
|
How do I ask a process to stop cleanly? |
|
|
|
…to stop a bit more firmly? |
|
no equivalent — skipped |
|
…to end it and everything it started? |
|
|
|
Which signals should the waiter forward? |
|
same, plus |
|
Is a pid alive? |
|
|
|
Can I delete a git checkout? |
yes |
|
|
Everything else — paths via pathlib, os.environ, subprocess.Popen
with lists, os.replace for atomic writes, UTF-8 with errors="replace"
when reading logs — behaves the same on both and needs no branch.
Habits that keep it that way
Parameterise instead of branching where you can:
venv_pythontakesplatform; tests call it with"win32"on a Mac.Make every
hostfunction total — it returns a sensible answer on every platform and never raises “not supported here”.terminateon Windows does nothing rather than failing.Mark the lines a host cannot reach with
# pragma: no cover — exercised on the Windows CI job, and make sure the CI actually has one — it istests:windowsin.gitlab-ci.yml, which runs the whole suite on asaas-windows-medium-amd64runner for merge requests, the default branch,expand tags. Coverage on macOS or Linux says nothing about the Windows branch; that job is wheredetach,deliver_stopandkill_treeare truly executed. The marker is a claim about it, so a marker without the job is a false one.Never test on the platform name when you can test for the feature (
hasattr(signal, "SIGBREAK")) — but keep even that insidehost.py, so the one-glance inventory stays complete.Prefer files over process relationships for shared state; a JSON record in the home behaves identically everywhere, a parent/child link does not (see Process management).
Adding a new difference
Write the function in
host.pyunder the matching heading, with the POSIX: / Windows: lines in its docstring.Call it from the service; do not import
sys.platformthere.Run
uv run pytest tests/test_host.py— the guard test tells you if a marker slipped out.If the Windows branch cannot run here, mark it
pragma: no coverand add a casetests:windowswill exercise — the marker excludes the line from the Linux coverage gate, it does not excuse it from being run.