///|
/// The tracked execution's state. Interrupted means its owner disappeared;
/// it does not assert that every descendant process was terminated.
pub(all) enum JobState {
Running
Stopping
Exited(Int32)
Stopped(String)
Interrupted
} derive(Eq, Debug)
///|
pub(all) enum JobEventKind {
Started
Updated
Finished
Failed
} derive(Eq, Debug)
///|
pub(all) enum JobStopOutcome {
Stopped
AlreadyTerminal
NotFound
WrongRuntime
} derive(Eq, Debug)
///|
/// A durable background execution record. output_file is a basename relative
/// to jobs/; clients resolve its current host path via the store.
pub(all) struct JobRecord {
generation : String
id : String
revision : Int32
command : String
description : String?
cwd : String?
state : JobState
started_at_ms : Int64
backgrounded_at_ms : Int64
finished_at_ms : Int64?
last_output_at_ms : Int64?
output_file : String?
output_persistent : Bool
output_bytes : Int64
output_chars : Int32
output_truncated : Bool
invalid_utf8 : Bool
output_error : String?
persistence_error : String?
cleanup_error : String?
/// Optional structured results retained with this job.
data : Json?
/// Set when the job is a subagent; `None` for every other job.
subagent : SubagentInfo?
} derive(Eq, Debug)
///|
pub fn JobState::is_terminal(self : JobState) -> Bool {
match self {
Running | Stopping => false
_ => true
}
}
///|
pub impl ToJson for JobState with fn to_json(self) {
match self {
Running => { "kind": "running" }
Stopping => { "kind": "stopping" }
Exited(code) => { "kind": "exited", "code": code }
Stopped(reason) => { "kind": "stopped", "reason": reason }
Interrupted => { "kind": "interrupted" }
}
}
///|
pub impl FromJson for JobState with fn from_json(json, path) {
match json {
{ "kind": "running", .. } => Running
{ "kind": "stopping", .. } => Stopping
{ "kind": "interrupted", .. } => Interrupted
{ "kind": "stopped", "reason": String(reason), .. } if !reason.is_empty() =>
Stopped(reason)
{ "kind": "exited", "code": Number(code, ..), .. } if code >= -2147483648.0 &&
code <= 2147483647.0 &&
code.to_int().to_double() == code => Exited(code.to_int())
_ => raise JsonDecodeError((path, "invalid background job state"))
}
}
///|
fn job_field(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> Json raise @json.JsonDecodeError {
match fields.get(name) {
Some(value) => value
None => raise JsonDecodeError((path, "missing job field: " + name))
}
}
///|
fn[T : FromJson] job_optional(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> T? raise @json.JsonDecodeError {
match fields.get(name) {
None | Some(Null) => None
Some(value) => Some(T::from_json(value, path))
}
}
///|
fn[T : FromJson] job_required(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
T::from_json(job_field(fields, name, path), path)
}
///|
fn[T : ToJson] job_optional_json(value : T?) -> Json {
match value {
Some(value) => value.to_json()
None => null
}
}
///|
fn job_uint(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> Int64 raise @json.JsonDecodeError {
match job_field(fields, name, path) {
Number(value, ..) if value >= 0 &&
value <= 9007199254740991.0 &&
value.to_int64().to_double() == value => value.to_int64()
_ =>
raise JsonDecodeError((path, "invalid nonnegative job integer: " + name))
}
}
///|
fn job_optional_uint(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> Int64? raise @json.JsonDecodeError {
match fields.get(name) {
None | Some(Null) => None
Some(_) => Some(job_uint(fields, name, path))
}
}
///|
/// An artifact component, never a path supplied by output text.
pub fn valid_job_component(value : String) -> Bool {
!value.is_empty() &&
value != "." &&
value != ".." &&
value.length() <= 200 &&
value.all(c => {
c is ('a'..<='z' | 'A'..<='Z' | '0'..<='9') ||
c == '-' ||
c == '_' ||
c == '.'
})
}
///|
pub impl ToJson for JobRecord with fn to_json(self) {
let fields : Map[String, Json] = {
"schema_version": 1,
"cleanup_error": job_optional_json(self.cleanup_error),
"generation": Json(self.generation),
"id": Json(self.id),
"revision": Json(self.revision),
"command": Json(self.command),
"description": job_optional_json(self.description),
"cwd": job_optional_json(self.cwd),
"state": self.state.to_json(),
"started_at_ms": Json(self.started_at_ms.to_double()),
"backgrounded_at_ms": Json(self.backgrounded_at_ms.to_double()),
"finished_at_ms": job_optional_json(
self.finished_at_ms.map(value => value.to_double()),
),
"last_output_at_ms": job_optional_json(
self.last_output_at_ms.map(value => value.to_double()),
),
"output_file": job_optional_json(self.output_file),
"output_persistent": Json(self.output_persistent),
"output_bytes": Json(self.output_bytes.to_double()),
"output_chars": Json(self.output_chars),
"output_truncated": Json(self.output_truncated),
"invalid_utf8": Json(self.invalid_utf8),
"output_error": job_optional_json(self.output_error),
"persistence_error": job_optional_json(self.persistence_error),
}
if self.data is Some(data) {
fields["data"] = data
}
if self.subagent is Some(subagent) {
fields["subagent"] = subagent.to_json()
}
Json::object(fields)
}
///|
pub impl FromJson for JobRecord with fn from_json(json, path) {
guard json is Object(fields) &&
fields.get("schema_version") is Some(Number(1, ..)) else {
raise JsonDecodeError((path, "expected background job schema version 1"))
}
let cleanup_error : String? = job_optional(fields, "cleanup_error", path)
let data : Json? = job_optional(fields, "data", path)
let subagent : SubagentInfo? = job_optional(fields, "subagent", path)
let generation : String = job_required(fields, "generation", path)
let id : String = job_required(fields, "id", path)
let revision = job_uint(fields, "revision", path)
let command : String = job_required(fields, "command", path)
let description : String? = job_optional(fields, "description", path)
let cwd : String? = job_optional(fields, "cwd", path)
let state : JobState = job_required(fields, "state", path)
let started_at_ms = job_uint(fields, "started_at_ms", path)
let backgrounded_at_ms = job_uint(fields, "backgrounded_at_ms", path)
let finished_at_ms : Int64? = job_optional_uint(
fields, "finished_at_ms", path,
)
let last_output_at_ms : Int64? = job_optional_uint(
fields, "last_output_at_ms", path,
)
let output_file : String? = job_optional(fields, "output_file", path)
let output_persistent : Bool = job_required(fields, "output_persistent", path)
let output_bytes = job_uint(fields, "output_bytes", path)
let output_chars = job_uint(fields, "output_chars", path)
let output_truncated : Bool = job_required(fields, "output_truncated", path)
let invalid_utf8 : Bool = job_required(fields, "invalid_utf8", path)
let output_error : String? = job_optional(fields, "output_error", path)
let persistence_error : String? = job_optional(
fields, "persistence_error", path,
)
guard valid_job_component(generation) &&
valid_job_component(id) &&
revision > 0 &&
revision <= 2147483647L &&
output_chars <= 2147483647L &&
output_file.map_or(true, valid_job_component) else {
raise JsonDecodeError((path, "invalid background job identity or count"))
}
{
generation,
cleanup_error,
data,
id,
revision: revision.to_int(),
command,
description,
cwd,
state,
started_at_ms,
backgrounded_at_ms,
finished_at_ms,
last_output_at_ms,
output_file,
output_persistent,
output_bytes,
output_chars: output_chars.to_int(),
output_truncated,
invalid_utf8,
output_error,
persistence_error,
subagent,
}
}
///|
pub extend JobState with Eq::{equal, not_equal}
///|
pub extend JobState with Debug::{to_repr}
///|
pub extend JobRecord with Eq::{equal, not_equal}
///|
pub extend JobRecord with Debug::{to_repr}
///|
pub extend JobEventKind with Eq::{equal, not_equal}
///|
pub extend JobEventKind with Debug::{to_repr}
///|
pub extend JobStopOutcome with Eq::{equal, not_equal}
///|
pub extend JobStopOutcome with Debug::{to_repr}
///|
pub impl ToJson for JobEventKind with fn to_json(self) {
match self {
Started => Json("started")
Updated => Json("updated")
Finished => Json("finished")
Failed => Json("failed")
}
}
///|
pub impl FromJson for JobEventKind with fn from_json(json, path) {
match json {
String("started") => Started
String("updated") => Updated
String("finished") => Finished
String("failed") => Failed
_ => raise JsonDecodeError((path, "invalid JobEventKind"))
}
}
///|
pub impl ToJson for JobStopOutcome with fn to_json(self) {
match self {
Stopped => Json("stopped")
AlreadyTerminal => Json("already_terminal")
NotFound => Json("not_found")
WrongRuntime => Json("wrong_runtime")
}
}
///|
pub impl FromJson for JobStopOutcome with fn from_json(json, path) {
match json {
String("stopped") => Stopped
String("already_terminal") => AlreadyTerminal
String("not_found") => NotFound
String("wrong_runtime") => WrongRuntime
_ => raise JsonDecodeError((path, "invalid JobStopOutcome"))
}
}
///|
pub extend JobRecord with ToJson::{to_json}
///|
pub extend JobState with ToJson::{to_json}
///|
pub extend JobEventKind with ToJson::{to_json}
///|
pub extend JobStopOutcome with ToJson::{to_json}
///|
pub extend JobState with FromJson::{from_json}
///|
pub extend JobRecord with FromJson::{from_json}
///|
pub extend JobEventKind with FromJson::{from_json}
///|
pub extend JobStopOutcome with FromJson::{from_json}