plesty.server.model.job ======================= .. py:module:: plesty.server.model.job .. autoapi-nested-parse:: Long-running work on the bench as jobs: a record and a log. An install, an update, a field test or an arbitrary command in a device's environment takes seconds to minutes — too long for one request/reply and too interesting to run blind. A :class:`Job` is the record of such a run: its command, pid, timestamps and exit code live in ``/jobs/.json`` and its stdout+stderr in ``/logs/jobs/.log``. :class:`JobStore` owns both files — creating, updating, listing records and reading logs by byte offset — so a job started by the agent is visible to the CLI and the GUI. Starting and killing the process is not the model's business: whoever launches it calls :meth:`JobStore.create` before and :meth:`JobStore.finish` after, and :meth:`JobStore.refresh` settles a still-``running`` record from the two facts only the process layer knows — is the pid alive, and what exit code was left. Attributes ---------- .. autoapisummary:: plesty.server.model.job.LOG_CHUNK plesty.server.model.job.LOST_EXIT plesty.server.model.job.CANCELLED_EXIT Exceptions ---------- .. autoapisummary:: plesty.server.model.job.JobError Classes ------- .. autoapisummary:: plesty.server.model.job.Job plesty.server.model.job.JobStore Functions --------- .. autoapisummary:: plesty.server.model.job.tail_file plesty.server.model.job._now Module Contents --------------- .. py:data:: LOG_CHUNK :value: 65536 .. py:data:: LOST_EXIT :value: -1 .. py:data:: CANCELLED_EXIT :value: -9 .. py:exception:: JobError Bases: :py:obj:`RuntimeError` A job record does not exist or cannot be read. Initialize self. See help(type(self)) for accurate signature. .. py:class:: Job One unit of long-running work. :ivar id: ``--``, unique per bench. :ivar name: Device instance the job belongs to. :ivar kind: ``install``, ``update``, ``field-test``, ``command`` … :ivar command: The command line. :ivar cwd: Working directory. :ivar pid: Process id while it runs. :ivar started_utc: ISO timestamp of the launch. :ivar finished_utc: ISO timestamp of the end, or ``None`` while running. :ivar returncode: Exit code once finished (``None`` while running; :data:`LOST_EXIT` when the process vanished without one). :ivar log: Path of the log file. :ivar detail: Kind-specific data (e.g. the reports a field test wrote). .. py:attribute:: id :type: str .. py:attribute:: name :type: str .. py:attribute:: kind :type: str .. py:attribute:: command :type: list[str] .. py:attribute:: cwd :type: str .. py:attribute:: pid :type: int | None .. py:attribute:: started_utc :type: str .. py:attribute:: finished_utc :type: str | None :value: None .. py:attribute:: returncode :type: int | None :value: None .. py:attribute:: log :type: str :value: '' .. py:attribute:: detail :type: dict[str, Any] .. py:property:: running :type: bool Whether the job has not finished yet. .. py:property:: state :type: str ``running``, ``done`` (exit 0), ``failed`` (any other exit). .. py:method:: to_dict() -> dict[str, Any] JSON form, with the derived ``state`` included. .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> Job :classmethod: Rebuild a record from :meth:`to_dict` output (extra keys ignored). .. py:class:: JobStore(home: plesty.server.model.home.Home) Job records and logs under a :class:`Home`. Bind to a home. :param home: The bench home. .. py:attribute:: home .. py:method:: create(name: str, kind: str, command: list[str], cwd: str | os.PathLike[str], pid: int | None = None, detail: collections.abc.Mapping[str, Any] | None = None) -> Job Open a new record and return it (state ``running``). :param name: Device instance the job belongs to. :param kind: Job kind, part of the id. :param command: The command line. :param cwd: Working directory. :param pid: Process id when already known. :param detail: Kind-specific data stored on the record. .. py:method:: save(job: Job) -> pathlib.Path Write *job* to its record file; returns the path. .. py:method:: get(job_id: str) -> Job | None The record of *job_id*, or ``None`` when unknown or unreadable. .. py:method:: require(job_id: str) -> Job Like :meth:`get` but raises :class:`JobError` when unknown. .. py:method:: list(name: str | None = None, kind: str | None = None) -> list[Job] Jobs on record, newest first, optionally filtered by device and kind. .. py:method:: finish(job_id: str, returncode: int) -> Job Close the record of *job_id* with *returncode*; returns it. .. py:method:: refresh(job: Job, alive: collections.abc.Callable[[int], bool], exit_code: collections.abc.Callable[[str], int | None]) -> Job Settle a ``running`` record against what the process layer knows. :param job: The record to bring up to date (mutated and saved when it ends). :param alive: Whether a pid is still running. :param exit_code: The exit code left for a job id, or ``None`` when none. :returns: *job*, finished when an exit code exists or the pid is gone (:data:`LOST_EXIT` then), unchanged while it still runs. .. py:method:: log(job_id: str, offset: int = 0, limit: int = LOG_CHUNK) -> tuple[str, int] Read the job log from byte *offset*. :param job_id: The job. :param offset: Where the previous read stopped (0 for the start). :param limit: Maximum bytes to return. :returns: The text read and the offset to continue from. Decoding is lenient — a chunk may cut a multi-byte character. .. py:method:: tail(job_id: str, lines: int = 50) -> str The last *lines* lines of the job log (empty when there is none yet). .. py:function:: tail_file(path: pathlib.Path, lines: int, block: int = 8192) -> str The last *lines* lines of *path*, read from the end in *block* steps. .. py:function:: _now() -> str