plesty.server.host
Everything that differs between operating systems, in one place.
plesty-server runs on Windows benches and on macOS/Linux developer machines.
Every branch on the platform lives in this module and nowhere else: the rest
of the package asks host for the answer and stays platform-blind. When
a new difference turns up, add it here — one function per question, each
documenting the Windows and the POSIX answer side by side.
Rules of the module:
No other module tests
sys.platformoros.name.Every function is total — it returns a sensible answer on every platform, never raises for “not supported here”.
Functions take the platform as a parameter where that makes them testable from any host (
venv_python()); the module-levelwindows()is the default.
Attributes
Functions
|
Whether platform is Windows — the only one with console windows and |
|
The interpreter inside a virtual environment. |
|
The file name of an executable called name: |
|
Split a written command line into arguments the way platform reads it. |
|
Strip one surrounding pair of quotes, as POSIX splitting would. |
|
Make link stand in for the file target. |
|
Delete a directory tree even when git left read-only files in it. |
|
Rename a directory, waiting out whoever is holding it open. |
|
|
|
|
|
|
|
|
|
|
|
Ask the waiter pid to shut its command down cleanly. |
|
Call on_stop when stop_file appears — the Windows half of a clean stop. |
|
The signal a clean stop arrives as, for a process that must catch one. |
|
Ask child to end cleanly, from the process that started it. |
|
Run run in a process nobody has to reap, and tell report its pid. |
|
Wait for a launcher process once it has reported the detached pid. |
|
Route the signals that mean "stop" to handler in the current process. |
|
Whether a process with pid exists (no psutil). |
|
|
|
Tell pid to end ( |
|
End pid and everything it started, without asking. |
|
Send sig to pid (or its process group), ignoring a vanished target. |
Module Contents
- plesty.server.host.Report
- plesty.server.host.windows(platform: str = sys.platform) bool
Whether platform is Windows — the only one with console windows and
Scripts/.- Parameters:
platform (str)
- Return type:
bool
- plesty.server.host.venv_python(venv: pathlib.Path, platform: str = sys.platform) pathlib.Path
The interpreter inside a virtual environment.
Windows:
<venv>/Scripts/python.exe. POSIX:<venv>/bin/python.- Parameters:
venv (pathlib.Path)
platform (str)
- Return type:
pathlib.Path
- plesty.server.host.executable(name: str, platform: str = sys.platform) str
The file name of an executable called name:
name.exeon Windows,nameon POSIX.- Parameters:
name (str)
platform (str)
- Return type:
str
- plesty.server.host.split_args(text: str, platform: str = sys.platform) list[str]
Split a written command line into arguments the way platform reads it.
POSIX:
shlex.split()— backslash escapes the next character and quotes group; both are removed. Windows: backslash is the path separator and escapes nothing. POSIX splitting eats it silently, so--config C:\lab\dev.iniarrives asC:labdev.iniand the device is handed a path that cannot exist. Only quotes group here, and they are stripped as POSIX splitting strips them.- Parameters:
text (str) – The command line as the fleet file or the console spells it.
platform (str) – The host to split for; defaults to this one.
- Return type:
list[str]
- plesty.server.host._unquote(token: str) str
Strip one surrounding pair of quotes, as POSIX splitting would.
- Parameters:
token (str)
- Return type:
str
- plesty.server.host.link_or_copy(target: pathlib.Path, link: pathlib.Path) None
Make link stand in for the file target.
POSIX: a symlink. Windows: creating one needs
SeCreateSymbolicLinkPrivilege, held by an elevated shell or a machine in Developer Mode and by neither a bench nor a CI runner — the attempt raisesWinError 1314. The file is copied instead, which resolves to the same interpreter or program.- Parameters:
target (pathlib.Path) – The existing file.
link (pathlib.Path) – The path to create.
- Return type:
None
- plesty.server.host.remove_tree(path: pathlib.Path) None
Delete a directory tree even when git left read-only files in it.
Windows:
shutil.rmtree()refuses read-only files, and git marks everything under.git/objectsread-only — so a checkout could not be uninstalled where a PyPI venv could. The retry clears the bit first. A file that is open elsewhere cannot be deleted at all here, where POSIX unlinks it happily: an antivirus or the search indexer holds one for a moment (the retry waits that out), a running device server holds its own interpreter for as long as it runs (PermissionErrorreaches the caller, which is the honest answer — the device has to be stopped first). POSIX: the owner may delete read-only and open files; no retry is reached.- Parameters:
path (pathlib.Path)
- Return type:
None
- plesty.server.host._RETRY_DELAYS = (0.1, 0.3, 0.6)
- plesty.server.host.move_tree(source: pathlib.Path, target: pathlib.Path) None
Rename a directory, waiting out whoever is holding it open.
Windows: renaming a directory fails with
PermissionErrorwhile any process holds a handle to it or to a file inside it, and something always does for a moment right after a tree is written — an antivirus scanning a fresh venv, the search indexer. POSIX renames it out from under an open handle and never reaches the retry. When the waiting does not help, the error reaches the caller: whatever holds it is not letting go.- Parameters:
source (pathlib.Path)
target (pathlib.Path)
- Return type:
None
- plesty.server.host._clear_readonly_and_retry(func: collections.abc.Callable[[str], None], path: str, _exc: BaseException) None
- Parameters:
func (collections.abc.Callable[[str], None])
path (str)
_exc (BaseException)
- Return type:
None
- plesty.server.host.spawn_kwargs(new_group: bool = False) dict[str, Any]
subprocesskeyword arguments for a child that shows no window.Windows: a console program started from a process without a console of its own gets a brand-new console window — one black box per
uvcall over the GUI.CREATE_NO_WINDOWstops it; the child keeps its handles, so job logs still fill. new_group addsCREATE_NEW_PROCESS_GROUPso it can be signalled on its own. POSIX: there is no window; new_group starts a new session so the child outlives (and is not interrupted with) its parent.- Parameters:
new_group (bool) – Detach the child from this process’s signals.
- Return type:
dict[str, Any]
- plesty.server.host.echoing_kwargs() dict[str, Any]
subprocesskeyword arguments for a child whose output must reach our stdout.Windows:
CREATE_NO_WINDOWleaves the child with no console, and a child with neither a console nor handles of its own writes its output nowhere — a field test’s whole transcript vanished from the job log that way, leaving only the lines the bench itself had written around it. Naming the streams makessubprocesspass real handles, so the flag can suppress the window without taking the output with it. POSIX: inheriting is enough and there is no window to suppress; naming them changes nothing.Falls back to plain inheritance when this process has no usable stdout — a windowed process has nothing to hand down in any case.
- Return type:
dict[str, Any]
- plesty.server.host.waiter_kwargs() dict[str, Any]
subprocesskeyword arguments for the launcher that waits on a command.Windows: the waiter needs a console of its own — hidden — for two reasons that pull in opposite directions. Its child must inherit a console, or, being a console program with none to inherit, Windows gives it a new and visible one: a black box on the operator’s screen for every device server and every job. And the waiter must share that console with the child, because
GenerateConsoleCtrlEventcannot cross from one console to another, and a clean stop is exactly that event.CREATE_NO_WINDOWgives neither: it runs the waiter with no console at all, so the child allocates the visible one.CREATE_NEW_CONSOLEwithSW_HIDEgives a real console created hidden — no window, nothing to flash, and an event the waiter can deliver.POSIX: a new session, as for any detached child.
- Return type:
dict[str, Any]
- plesty.server.host.interruptible_kwargs() dict[str, Any]
subprocesskeyword arguments for a child this process must be able to stop.Windows:
CREATE_NEW_PROCESS_GROUPmakes the child the leader of a group that can be sentCTRL_BREAK_EVENT. It deliberately does not addCREATE_NO_WINDOW: that would give the child a console of its own, and a console control event cannot cross from one console to another. The child inherits the waiter’s console instead, whichwaiter_kwargs()creates hidden for exactly this — so no window appears either way. POSIX: nothing; the child stays in our group andsend_signalreaches it.- Return type:
dict[str, Any]
- plesty.server.host._STOP_POLL = 0.2
- plesty.server.host.request_stop(pid: int, stop_file: pathlib.Path) None
Ask the waiter pid to shut its command down cleanly.
POSIX:
SIGINT, which the waiter forwards to the command. Windows: no console control event can reach it. The waiter runs on an invisible console of its own and the process asking may have no console at all —plesty-server-gui.exeis a GUI binary — andGenerateConsoleCtrlEventonly reaches the caller’s own console. It would fail silently and the bench would fall through to killing every device outright, so the waiter watches for stop_file instead (watch_for_stop()) and creating it is the request.- Parameters:
pid (int) – The waiter that owns the command.
stop_file (pathlib.Path) – The file that waiter watches.
- Return type:
None
- plesty.server.host.watch_for_stop(stop_file: pathlib.Path, on_stop: collections.abc.Callable[[], None]) None
Call on_stop when stop_file appears — the Windows half of a clean stop.
POSIX: returns at once. A stop arrives as a signal there, and the handler
forward_signals()installed does the same work. Windows: starts a daemon thread polling for the file, so the waiter can be asked to stop by a process that shares no console with it.- Parameters:
stop_file (pathlib.Path) – The file whose appearance means “stop”.
on_stop (collections.abc.Callable[[], None]) – Run once, on the watching thread, when it appears.
- Return type:
None
- plesty.server.host.stop_signal() int
The signal a clean stop arrives as, for a process that must catch one.
POSIX:
SIGINT. Windows:SIGBREAK.CTRL_BREAK_EVENTis the only console control event that can be aimed at a single process group —CTRL_C_EVENTis disabled outright for a group made withCREATE_NEW_PROCESS_GROUP— so it is whatdeliver_stop()sends, and Python delivers it asSIGBREAK. A server that only handlesSIGINTis killed outright here (exit0xC000013A) instead of closing its device.- Return type:
int
- plesty.server.host.deliver_stop(child: subprocess.Popen[bytes]) None
Ask child to end cleanly, from the process that started it.
POSIX:
SIGINT. Windows:CTRL_BREAK_EVENTto the group the child leads — it shares this process’s console, which is whatinterruptible_kwargs()buys. A child that has already gone is not an error.- Parameters:
child (subprocess.Popen[bytes])
- Return type:
None
- plesty.server.host.detach(run: collections.abc.Callable[[], int], report: Report) int
Run run in a process nobody has to reap, and tell report its pid.
POSIX: fork; the child starts a new session, points its standard streams at
/dev/null, runs and_exits. The parent reports the child’s pid and returns 0 — it exits at once, so the child is reparented toinitand no zombie is left for whoever started us. Windows: there is no fork; this process is the detached one. It reports its own pid, then runs — so a caller must never wait for it to finish (seereap_launcher()).- Parameters:
run (collections.abc.Callable[[], int]) – The work of the detached process; its return value is its exit code.
report (Report) – Receives the pid the caller should track.
- Returns:
The exit code of the calling process.
- Return type:
int
- plesty.server.host.reap_launcher(proc: subprocess.Popen[bytes], timeout: float = 10) None
Wait for a launcher process once it has reported the detached pid.
POSIX: the launcher is the intermediate parent of
detach(); it has exited already, and waiting collects it so it is not left a zombie. Windows: the launcher is the long-lived process — waiting would block until the server or job it runs has ended, so nothing is done.- Parameters:
proc (subprocess.Popen[bytes])
timeout (float)
- Return type:
None
- plesty.server.host.forward_signals(handler: collections.abc.Callable[[int, Any], None]) None
Route the signals that mean “stop” to handler in the current process.
POSIX:
SIGINT,SIGTERMandSIGHUP(the terminal closed). Windows:SIGINT/SIGTERM(as Python maps them) plusSIGBREAK, which is ignored rather than forwarded — a console child in the same group receivesCTRL_BREAK_EVENTon its own.Installed before any child starts: a caught signal resets to its default across exec, whereas an ignored one is inherited.
- Parameters:
handler (collections.abc.Callable[[int, Any], None])
- Return type:
None
- plesty.server.host.pid_alive(pid: int) bool
Whether a process with pid exists (no psutil).
POSIX:
kill(pid, 0)— a permission error still means it exists. Windows:OpenProcess+GetExitCodeProcess=STILL_ACTIVE. Servers and jobs are never children of the asking process (seedetach()), so there are no zombies to mistake for live processes.- Parameters:
pid (int)
- Return type:
bool
- plesty.server.host._pid_alive_windows(pid: int) bool
- Parameters:
pid (int)
- Return type:
bool
- plesty.server.host.terminate(pid: int) None
Tell pid to end (
SIGTERM). Windows has no equivalent;kill_tree()is next.- Parameters:
pid (int)
- Return type:
None
- plesty.server.host.kill_tree(pid: int) None
End pid and everything it started, without asking.
POSIX:
SIGKILLto the process group (the detached process leads one). Windows:taskkill /T /F.- Parameters:
pid (int)
- Return type:
None
- plesty.server.host._signal(pid: int, sig: int, group: bool = False) None
Send sig to pid (or its process group), ignoring a vanished target.
- Parameters:
pid (int)
sig (int)
group (bool)
- Return type:
None