plesty.server.services.launch

Detached process launch: no zombies, and an exit code every process can read.

Several plesty-server processes look at the same servers and jobs — the CLI, the agent, fleet run, the GUI. A child that its parent never reaps is a zombie: kill(pid, 0) still succeeds, so a different process takes it for alive and waits a full stop escalation for nothing. And a plain detached child loses its exit code for everyone but its parent.

So nothing here is a direct child. launch() spawns a small waiter (python -m plesty.server.services.launch) that detaches itself (plesty.server.host.detach()), starts the real command with its output appended to the log, forwards the interrupt it receives, waits, and writes the exit code to exit_file. Callers track the waiter’s pid: alive means the command runs; the exit file appears when it is over.

Attributes

SWITCH_POLL_S

READ_EXIT_TRIES

READ_EXIT_PAUSE_S

Classes

_Relay

Copy a child's output into the current log, and move to another on request.

Functions

stop_file(→ pathlib.Path)

The file that asks the waiter of exit_file to stop its command.

switch_file(→ pathlib.Path)

The file that asks the waiter of exit_file to write to another log.

request_switch(→ bool)

Ask a relaying waiter to write to log from now on; True once it has.

_resolve(→ str)

executable as an absolute path, found on the PATH env carries.

launch(→ int)

Start command detached; return the pid of the waiter that owns it.

read_exit(→ int | None)

The exit code recorded in exit_file, or None while it cannot be read yet.

_wait(→ int)

The waiter: run the command, forward the interrupt, record the exit code.

_write_exit(→ None)

_report_pid(→ None)

main(→ int)

Entry point of the waiter (spec as JSON on stdin).

Module Contents

plesty.server.services.launch.stop_file(exit_file: str | os.PathLike[str]) → pathlib.Path

The file that asks the waiter of exit_file to stop its command.

Alongside the exit file, so anything that knows where a command reports its end also knows how to ask it to end (see plesty.server.host.request_stop()).

Parameters:

exit_file (str | os.PathLike[str])

Return type:

pathlib.Path

plesty.server.services.launch.switch_file(exit_file: str | os.PathLike[str]) → pathlib.Path

The file that asks the waiter of exit_file to write to another log.

Its content is the path of the new log. The waiter opens it, writes there from then on, and removes this file to say it has (see request_switch()).

Parameters:

exit_file (str | os.PathLike[str])

Return type:

pathlib.Path

plesty.server.services.launch.SWITCH_POLL_S = 0.2
plesty.server.services.launch.request_switch(exit_file: str | os.PathLike[str], log: str | os.PathLike[str], timeout: float = 5.0) → bool

Ask a relaying waiter to write to log from now on; True once it has.

A file rather than a signal, for the reason the stop is one: on Windows nothing outside the waiter’s console can signal it.

Parameters:
  • exit_file (str | os.PathLike[str])

  • log (str | os.PathLike[str])

  • timeout (float)

Return type:

bool

plesty.server.services.launch._resolve(executable: str, env: collections.abc.Mapping[str, str]) → str

executable as an absolute path, found on the PATH env carries.

A bare name has to be resolved here because Windows will not do it against the environment the child is given: CreateProcess searches the calling process’s PATH. Handing it an absolute path settles the question on both platforms and removes the difference rather than branching on it.

A name that resolves to nothing is returned unchanged, so the failure is the same OSError from the launcher that it always was, naming the command the operator typed rather than a path they never wrote.

Parameters:
  • executable (str) – command[0] — a bare name, or already a path.

  • env (collections.abc.Mapping[str, str]) – The environment the command will run with.

Return type:

str

plesty.server.services.launch.launch(command: collections.abc.Sequence[str], cwd: str | os.PathLike[str], env: collections.abc.Mapping[str, str], log: str | os.PathLike[str], exit_file: str | os.PathLike[str], banner: str = '', relay: bool = False) → int

Start command detached; return the pid of the waiter that owns it.

Parameters:
  • command (collections.abc.Sequence[str]) – The command line.

  • cwd (str | os.PathLike[str]) – Working directory.

  • env (collections.abc.Mapping[str, str]) – Full environment for the command.

  • log (str | os.PathLike[str]) – File its stdout+stderr are appended to.

  • exit_file (str | os.PathLike[str]) – File the exit code is written to when it ends (removed first, so its presence means “finished”).

  • banner (str) – A line written to the log before this function returns, so a caller may read the log on the pid it is handed.

  • relay (bool) – The waiter copies the command’s output into the log rather than handing the command the file, so the log can be switched to another file while it runs (request_switch()). For a device server, whose log is cleared while it runs; a job’s log is never switched and keeps the direct handle.

Raises:
  • OSError – When the waiter itself cannot be started.

  • RuntimeError – When the waiter did not report a pid.

Return type:

int

plesty.server.services.launch.READ_EXIT_TRIES = 5
plesty.server.services.launch.READ_EXIT_PAUSE_S = 0.02
plesty.server.services.launch.read_exit(exit_file: str | os.PathLike[str]) → int | None

The exit code recorded in exit_file, or None while it cannot be read yet.

The waiter writes the code to a temporary file and moves it into place (_write_exit()). On Windows, opening the file while that move is under way is refused with PermissionError — a supervisor checking a stopping device at that moment raised it instead of answering (found on the project’s own Windows runner). The move takes milliseconds, so the read is tried again briefly, and a file still refused is reported as not there yet: every caller polls again.

Parameters:

exit_file (str | os.PathLike[str])

Return type:

int | None

plesty.server.services.launch._wait(spec: dict[str, Any]) → int

The waiter: run the command, forward the interrupt, record the exit code.

Parameters:

spec (dict[str, Any])

Return type:

int

class plesty.server.services.launch._Relay(log: str, child: subprocess.Popen[bytes], request: pathlib.Path)

Copy a child’s output into the current log, and move to another on request.

The log used to be the child’s own stdout. Clearing it then had to truncate a file the server kept writing at its old offset: the gap filled with NULs, and an 11.7 MB log with no line breaks hung every reader (#23). Here the waiter holds the file, so it can close one and open the next between two reads, and nothing is ever truncated.

Parameters:
  • log (str)

  • child (subprocess.Popen[bytes])

  • request (pathlib.Path)

path
_file
_lock
_done
_request
_reader
_copy(child: subprocess.Popen[bytes]) → None
Parameters:

child (subprocess.Popen[bytes])

Return type:

None

_watch() → None
Return type:

None

finish() → str

Drain what the child left, close the file, and return the log in use.

Return type:

str

plesty.server.services.launch._write_exit(exit_file: str, code: int) → None
Parameters:
  • exit_file (str)

  • code (int)

Return type:

None

plesty.server.services.launch._report_pid(pid: int) → None
Parameters:

pid (int)

Return type:

None

plesty.server.services.launch.main() → int

Entry point of the waiter (spec as JSON on stdin).

Reached as python -m plesty.server.services.launch from a source checkout and as plesty-server _launch from a bundle.

Return type:

int