plesty.server.model.job

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 Job is the record of such a run: its command, pid, timestamps and exit code live in <home>/jobs/<id>.json and its stdout+stderr in <home>/logs/jobs/<id>.log. 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 JobStore.create() before and JobStore.finish() after, and 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

LOG_CHUNK

LOG_END

LOST_EXIT

CANCELLED_EXIT

TAIL_LIMIT

Exceptions

JobError

A job record does not exist or cannot be read.

Classes

Job

One unit of long-running work.

JobStore

Job records and logs under a Home.

Functions

read_log(→ tuple[str, int])

Read the log at path from byte offset.

log_end(→ tuple[str, int, int])

The end of the log at path: at most its last limit bytes, from a line start.

log_before(→ tuple[str, int])

The part of the log at path before byte end: at most limit bytes, from a line start.

tail_file(→ str)

The last lines lines of path, read from at most its last limit bytes.

_from_line_start(→ tuple[int, bytes])

Drop the partial line a read from the middle of a file begins with.

_now(→ str)

Module Contents

plesty.server.model.job.LOG_CHUNK = 65536
plesty.server.model.job.LOG_END = 262144
plesty.server.model.job.LOST_EXIT = -1
plesty.server.model.job.CANCELLED_EXIT = -9
exception plesty.server.model.job.JobError

Bases: RuntimeError

A job record does not exist or cannot be read.

Initialize self. See help(type(self)) for accurate signature.

class plesty.server.model.job.Job

One unit of long-running work.

Variables:
  • id – <name>-<kind>-<timestamp>, unique per bench.

  • name – Device instance the job belongs to.

  • kind – install, update, field-test, command …

  • command – The command line.

  • cwd – Working directory.

  • pid – Process id while it runs.

  • started_utc – ISO timestamp of the launch.

  • finished_utc – ISO timestamp of the end, or None while running.

  • returncode – Exit code once finished (None while running; LOST_EXIT when the process vanished without one).

  • log – Path of the log file.

  • detail – Kind-specific data (e.g. the reports a field test wrote).

id: str
name: str
kind: str
command: list[str]
cwd: str
pid: int | None
started_utc: str
finished_utc: str | None = None
returncode: int | None = None
log: str = ''
detail: dict[str, Any]
property running: bool

Whether the job has not finished yet.

Return type:

bool

property state: str

running, done (exit 0), failed (any other exit).

Return type:

str

to_dict() → dict[str, Any]

JSON form, with the derived state included.

Return type:

dict[str, Any]

classmethod from_dict(data: collections.abc.Mapping[str, Any]) → Job

Rebuild a record from to_dict() output (extra keys ignored).

Parameters:

data (collections.abc.Mapping[str, Any])

Return type:

Job

class plesty.server.model.job.JobStore(home: plesty.server.model.home.Home)

Job records and logs under a Home.

Bind to a home.

Parameters:

home (plesty.server.model.home.Home) – The bench home.

home
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).

Parameters:
  • name (str) – Device instance the job belongs to.

  • kind (str) – Job kind, part of the id.

  • command (list[str]) – The command line.

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

  • pid (int | None) – Process id when already known.

  • detail (collections.abc.Mapping[str, Any] | None) – Kind-specific data stored on the record.

Return type:

Job

save(job: Job) → pathlib.Path

Write job to its record file; returns the path.

Parameters:

job (Job)

Return type:

pathlib.Path

get(job_id: str) → Job | None

The record of job_id, or None when unknown or unreadable.

Parameters:

job_id (str)

Return type:

Job | None

require(job_id: str) → Job

Like get() but raises JobError when unknown.

Parameters:

job_id (str)

Return type:

Job

list(name: str | None = None, kind: str | None = None) → list[Job]

Jobs on record, newest first, optionally filtered by device and kind.

Parameters:
  • name (str | None)

  • kind (str | None)

Return type:

list[Job]

finish(job_id: str, returncode: int) → Job

Close the record of job_id with returncode; returns it.

Parameters:
  • job_id (str)

  • returncode (int)

Return type:

Job

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.

Parameters:
  • job (Job) – The record to bring up to date (mutated and saved when it ends).

  • alive (collections.abc.Callable[[int], bool]) – Whether a pid is still running.

  • exit_code (collections.abc.Callable[[str], int | None]) – 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 (LOST_EXIT then), unchanged while it still runs.

Return type:

Job

log(job_id: str, offset: int = 0, limit: int = LOG_CHUNK) → tuple[str, int]

Read the job log from byte offset.

Parameters:
  • job_id (str) – The job.

  • offset (int) – Where the previous read stopped (0 for the start).

  • limit (int) – 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.

Return type:

tuple[str, int]

log_end(job_id: str, limit: int = LOG_END) → tuple[str, int, int]

The end of the job log; see log_end().

Parameters:
  • job_id (str)

  • limit (int)

Return type:

tuple[str, int, int]

log_before(job_id: str, end: int, limit: int = LOG_END) → tuple[str, int]

The part of the job log before byte end; see log_before().

Parameters:
  • job_id (str)

  • end (int)

  • limit (int)

Return type:

tuple[str, int]

tail(job_id: str, lines: int = 50) → str

The last lines lines of the job log (empty when there is none yet).

Parameters:
  • job_id (str)

  • lines (int)

Return type:

str

plesty.server.model.job.read_log(path: pathlib.Path, offset: int = 0, limit: int = LOG_CHUNK) → tuple[str, int]

Read the log at path from byte offset.

A job’s log and the copy a run keeps of it (#33) are read the same way.

Parameters:
  • path (pathlib.Path) – The log file.

  • offset (int) – Where the previous read stopped (0 for the start).

  • limit (int) – 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.

Return type:

tuple[str, int]

plesty.server.model.job.log_end(path: pathlib.Path, limit: int = LOG_END) → tuple[str, int, int]

The end of the log at path: at most its last limit bytes, from a line start.

What a log view opens on, so a long log is not read and painted from its first byte to reach the last (#28).

Parameters:
  • path (pathlib.Path) – The log file.

  • limit (int) – Most bytes to return.

Returns:

The text, the byte it starts at (0 when it is the whole log), and the byte it ends at — where reading on continues.

Return type:

tuple[str, int, int]

plesty.server.model.job.log_before(path: pathlib.Path, end: int, limit: int = LOG_END) → tuple[str, int]

The part of the log at path before byte end: at most limit bytes, from a line start.

Parameters:
  • path (pathlib.Path) – The log file.

  • end (int) – Where the part already shown begins.

  • limit (int) – Most bytes to return.

Returns:

The text and the byte it starts at; ("", 0) at the top.

Return type:

tuple[str, int]

plesty.server.model.job.TAIL_LIMIT = 262144
plesty.server.model.job.tail_file(path: pathlib.Path, lines: int, limit: int = TAIL_LIMIT) → str

The last lines lines of path, read from at most its last limit bytes.

One read of a bounded span. The loop this replaced prepended each 8 KB block to everything read so far until enough newlines turned up — on a file with none (a truncated log a server kept writing at its old offset fills with NULs) that is quadratic in the file’s size, and an 11.7 MB log hung the console, the agent and every reader with it (#23). A line longer than limit is shown from its last limit bytes.

Parameters:
  • path (pathlib.Path)

  • lines (int)

  • limit (int)

Return type:

str

plesty.server.model.job._from_line_start(start: int, data: bytes) → tuple[int, bytes]

Drop the partial line a read from the middle of a file begins with.

A read from byte 0 starts on a line already. One whose span holds no line break at all is kept whole: a line longer than the span still shows.

Parameters:
  • start (int)

  • data (bytes)

Return type:

tuple[int, bytes]

plesty.server.model.job._now() → str
Return type:

str