plesty.server.services.supervisor ================================= .. py:module:: plesty.server.services.supervisor .. autoapi-nested-parse:: Start, stop, probe and log the device servers of one bench. A device server is `` -m plesty. `` — the entry point every hub device ships. The supervisor launches it detached through :mod:`plesty.server.services.launch` (so no ``plesty-server`` process ever owns it as a child) with its stdout and stderr appended to ``/logs/.log``, remembers pid and port in ``/state/.json`` so a later ``plesty-server`` invocation, the agent or the GUI finds it, and judges liveness the way experiments do: a ``describe`` request over ZMQ answered with ``status: ok``. Stopping is graceful first — the interrupt lets plesty-lib close the device session (``[SERVER] Shutting down...``) — and escalates to a kill only after ``stop_timeout`` seconds. Every platform difference on the way (how to interrupt, how to kill, how to tell a pid is alive) is a call into :mod:`plesty.server.host`. Attributes ---------- .. autoapisummary:: plesty.server.services.supervisor.PROBE_HOST plesty.server.services.supervisor._START_LOCKS plesty.server.services.supervisor._LOCKS_GUARD Exceptions ---------- .. autoapisummary:: plesty.server.services.supervisor.SupervisorError plesty.server.services.supervisor.AlreadyRunning plesty.server.services.supervisor.NotRunning plesty.server.services.supervisor.PortBusy Classes ------- .. autoapisummary:: plesty.server.services.supervisor.Supervisor Functions --------- .. autoapisummary:: plesty.server.services.supervisor._start_lock plesty.server.services.supervisor.probe plesty.server.services.supervisor.request plesty.server.services.supervisor._now Module Contents --------------- .. py:data:: PROBE_HOST :value: '127.0.0.1' .. py:exception:: SupervisorError Bases: :py:obj:`RuntimeError` A start or stop request could not be carried out. Initialize self. See help(type(self)) for accurate signature. .. py:exception:: AlreadyRunning Bases: :py:obj:`SupervisorError` ``start`` was asked for a device whose server is already up. Initialize self. See help(type(self)) for accurate signature. .. py:exception:: NotRunning Bases: :py:obj:`SupervisorError` ``stop`` was asked for a device that has no live server. Initialize self. See help(type(self)) for accurate signature. .. py:exception:: PortBusy Bases: :py:obj:`SupervisorError` ``start`` found the device's port already answering — another server holds it. Initialize self. See help(type(self)) for accurate signature. .. py:data:: _START_LOCKS :type: dict[str, threading.Lock] .. py:data:: _LOCKS_GUARD .. py:function:: _start_lock(name: str) -> threading.Lock .. py:function:: probe(port: int, timeout_ms: int = 2000, host_addr: str = PROBE_HOST) -> bool Ask the server on *port* to describe itself. :returns: ``True`` when a plesty-lib server answered ``status: ok`` in time. .. py:function:: request(port: int, message: dict[str, Any], timeout_ms: int = 5000, host_addr: str = PROBE_HOST) -> dict[str, Any] Send one request to the server on *port* and return its reply. The same DEALER socket :func:`probe` uses, with the message left to the caller: this is how a developer asks a running device server anything its plesty-lib command solver understands, without a client library. :raises SupervisorError: When nobody answers in time, or the answer is not JSON the wire format allows. .. py:class:: Supervisor(home: plesty.server.model.home.Home, ready_timeout: float | None = None, stop_timeout: float | None = None, probe_timeout_ms: int | None = None) Launch and control device servers under a :class:`Home`. Bind the supervisor to a home. :param home: The bench home. :param ready_timeout: Seconds :meth:`start` waits for the first probe answer. :param stop_timeout: Seconds :meth:`stop` waits before escalating. :param probe_timeout_ms: Milliseconds one probe waits for its reply. :param (Defaults come from ``config.yaml`` ``running``.): .. py:attribute:: home .. py:attribute:: ready_timeout .. py:attribute:: stop_timeout .. py:attribute:: probe_timeout_ms .. py:method:: record(name: str) -> plesty.server.model.process.ProcessRecord | None The persisted launch record of *name*, or ``None``. .. py:method:: exit_file(name: str) -> pathlib.Path Where the launcher records the exit code of the server of *name*. .. py:method:: known() -> list[str] Names that have a launch record (alive or not). .. py:method:: _drop_record(name: str) -> None .. py:method:: alive(name: str) -> bool Whether the recorded server of *name* still runs. .. py:method:: poll(name: str) -> int | None Exit code of the server of *name*, ``None`` while it runs. :data:`~plesty.server.model.job.LOST_EXIT` when the process is gone without a recorded exit (the bench rebooted, or someone killed the launcher) — and when nothing is known at all. .. py:method:: status(name: str, probe_port: bool = True) -> plesty.server.model.process.ProcessStatus Observe *name*: record, process existence and (optionally) a probe. :param name: Device instance name. :param probe_port: Whether to send a ``describe`` to tell ``running`` from ``starting``; skipping it is cheaper for bulk listings. .. py:method:: wait_ready(name: str, timeout: float | None = None) -> bool Poll until the server of *name* answers a probe. :returns: ``True`` once it answers; ``False`` on timeout or when the process exits first. .. py:method:: start(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool = True, timeout: float | None = None, package: str = '', version: str = '') -> plesty.server.model.process.ProcessStatus Launch the server of *spec* with the interpreter *python*. :param spec: Module, arguments, environment and port to run with. :param python: Interpreter of the environment the module is installed in. :param wait: Block until the server answers a probe (or fails). :param timeout: Readiness wait in seconds; defaults to :attr:`ready_timeout`. :param package: Distribution name, stored on the record for listings. :param version: Installed version, likewise. :returns: The status after launch — ``running`` when it came up, ``starting`` when *wait* is off or it has not answered yet, ``exited`` when it died before answering (``detail`` carries the log tail). :raises AlreadyRunning: When a live server of that name exists. :raises SupervisorError: When the interpreter cannot be executed. .. py:method:: _start_locked(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool, timeout: float | None, package: str, version: str) -> plesty.server.model.process.ProcessStatus .. py:method:: stop(name: str, timeout: float | None = None) -> bool Interrupt the server of *name*, escalating to a kill if it lingers. Interrupt → wait *timeout* → terminate → wait 3 s → kill tree. :returns: ``True`` when a process was stopped, ``False`` when only a stale record was cleared. :raises NotRunning: When nothing (not even a stale record) is known. .. py:method:: restart(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool = True, timeout: float | None = None) -> plesty.server.model.process.ProcessStatus Stop the server of *spec* if it runs, then :meth:`start` it again. .. py:method:: relaunch(record: plesty.server.model.process.ProcessRecord, wait: bool = True) -> plesty.server.model.process.ProcessStatus Start the server again exactly as *record* says it was started. Used after a host-tier field test: the interpreter, arguments and environment of the interrupted launch are what the bench expects back. .. py:method:: stop_all(names: list[str] | None = None) -> list[str] Stop every known (or the given) server; returns the names stopped. .. py:method:: tail(name: str, lines: int = 50) -> str The last *lines* lines of the log of *name* (``""`` when no log). .. py:method:: _wait_exit(name: str, timeout: float) -> bool .. py:function:: _now() -> str