use crate::exec;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
const MAX_FINISHED: usize = 200;
const MAX_OUTPUT: usize = 4 * 1024 * 1024;
struct Job {
id: u64,
label: String,
done: bool,
code: Option<i64>,
stdout: Vec<u8>,
stderr: Vec<u8>,
error: Option<String>,
}
impl Job {
fn result_value(&self) -> Value {
json!({
"ok": true,
"id": self.id,
"label": self.label,
"done": true,
"code": self.code,
"error": self.error,
"stdout": crate::proto::b64_encode(&self.stdout),
"stderr": crate::proto::b64_encode(&self.stderr),
})
}
}
#[derive(Default)]
struct Registry {
next_id: u64,
jobs: HashMap<u64, Job>,
}
fn registry() -> &'static Mutex<Registry> {
static REG: OnceLock<Mutex<Registry>> = OnceLock::new();
REG.get_or_init(|| Mutex::new(Registry::default()))
}
fn cap(mut v: Vec<u8>) -> Vec<u8> {
v.truncate(MAX_OUTPUT);
v
}
pub fn handle(cmd: &str, req: &Value) -> Value {
match cmd {
"job_start" => start(req),
"job_poll" => poll(),
"job_list" => list(),
"job_result" => result(req),
_ => json!({"ok": false, "err": "unknown_cmd"}),
}
}
fn start(req: &Value) -> Value {
if req["program"].as_str().is_none() {
return json!({"ok": false, "err": "no_program"});
}
let label = req["label"]
.as_str()
.or_else(|| req["program"].as_str())
.unwrap_or("job")
.to_string();
let notify = req["notify"].as_bool().unwrap_or(true);
let id = {
let mut reg = registry().lock().unwrap();
reg.next_id += 1;
let id = reg.next_id;
reg.jobs.insert(
id,
Job {
id,
label: label.clone(),
done: false,
code: None,
stdout: Vec::new(),
stderr: Vec::new(),
error: None,
},
);
id
};
let req = req.clone();
std::thread::spawn(move || {
let outcome = exec::run_raw(&req);
let mut summary = String::new();
{
let mut reg = registry().lock().unwrap();
if let Some(job) = reg.jobs.get_mut(&id) {
job.done = true;
match outcome {
Ok(res) => {
job.code = res.code.map(i64::from);
job.stdout = cap(res.stdout);
job.stderr = cap(res.stderr);
summary = match res.code {
Some(0) => "completed".to_string(),
Some(c) => format!("exited {c}"),
None => "terminated".to_string(),
};
}
Err(e) => {
summary = format!("failed to start: {e}");
job.error = Some(e);
}
}
}
evict_overflow(&mut reg);
}
if notify {
crate::osops::handle(
"notify",
&json!({
"title": format!("zwire-host: {label}"),
"body": format!("job #{id} {summary}"),
}),
);
}
});
json!({"ok": true, "job": id})
}
fn evict_overflow(reg: &mut Registry) {
let mut finished: Vec<u64> = reg.jobs.values().filter(|j| j.done).map(|j| j.id).collect();
if finished.len() <= MAX_FINISHED {
return;
}
finished.sort_unstable();
for id in &finished[..finished.len() - MAX_FINISHED] {
reg.jobs.remove(id);
}
}
fn poll() -> Value {
let mut reg = registry().lock().unwrap();
let done: Vec<u64> = reg.jobs.values().filter(|j| j.done).map(|j| j.id).collect();
let mut results: Vec<Value> = done
.iter()
.filter_map(|id| reg.jobs.remove(id).map(|j| j.result_value()))
.collect();
results.sort_by_key(|v| v["id"].as_u64().unwrap_or(0));
json!({"ok": true, "jobs": results})
}
fn list() -> Value {
let reg = registry().lock().unwrap();
let mut jobs: Vec<Value> = reg
.jobs
.values()
.map(|j| json!({"id": j.id, "label": j.label, "running": !j.done, "code": j.code}))
.collect();
jobs.sort_by_key(|v| v["id"].as_u64().unwrap_or(0));
json!({"ok": true, "jobs": jobs})
}
fn result(req: &Value) -> Value {
let Some(id) = req["id"].as_u64() else {
return json!({"ok": false, "err": "no_id"});
};
let mut reg = registry().lock().unwrap();
match reg.jobs.get(&id) {
None => json!({"ok": false, "err": "no_such_job"}),
Some(j) if !j.done => json!({"ok": true, "id": id, "running": true}),
Some(_) => reg.jobs.remove(&id).unwrap().result_value(),
}
}