use std::collections::BTreeMap;
use std::process::{Command, Stdio};
use kernel::{Address, AxCode, AxError, RunId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct BacklogId(u64);
impl std::fmt::Display for BacklogId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "bg-{}", self.0)
}
}
#[derive(Clone)]
pub struct Backlog {
table: std::sync::Arc<std::sync::Mutex<Table>>,
scratch: Scratch,
window: PollBudget,
sink: Option<Sink>,
}
impl Default for Backlog {
fn default() -> Backlog {
Backlog::with_window(PollBudget::DEFAULT)
}
}
#[derive(Default)]
struct Table {
next: u64,
members: BTreeMap<BacklogId, Member>,
jobs: jobs::Jobs,
}
impl Backlog {
#[must_use]
pub fn new() -> Backlog {
Backlog::default()
}
#[must_use]
pub fn with_window(window: PollBudget) -> Backlog {
Backlog {
table: std::sync::Arc::default(),
scratch: Scratch::open(),
window,
sink: None,
}
}
#[must_use]
pub fn with_sink(self, sink: Sink) -> Backlog {
Backlog {
sink: Some(sink),
..self
}
}
pub fn run(
&self,
owner: RunId,
scope: &Address,
what: String,
mut command: Command,
) -> Result<Started, AxError> {
let id = self.mint()?;
let dir = self.scratch.dir(id);
std::fs::create_dir_all(&dir).map_err(|err| storage(&dir, &err))?;
let out = std::fs::File::create(dir.join("out")).map_err(|err| storage(&dir, &err))?;
let errs = std::fs::File::create(dir.join("err")).map_err(|err| storage(&dir, &err))?;
command
.stdin(Stdio::null())
.stdout(Stdio::from(out))
.stderr(Stdio::from(errs));
let child = command.spawn().map_err(|err| {
AxError::failure(
AxCode::ToolUnavailable,
"start a command",
format!("{what}: {err}"),
)
.with_recovery("check the program name, or use the shell arm")
})?;
self.enrol(
id,
Member {
scope: scope.clone(),
what: what.clone(),
body: Body::Command {
child,
dir: dir.clone(),
claim: Claim::Window(owner),
tail: Tail::default(),
},
},
)?;
let mut tail = Tail::default();
for _ in 0..self.window.polls() {
if let Some(exit) = self.settle(id)? {
let (stdout, stderr) = collect(&dir);
return Ok(Started::Settled {
exit,
stdout,
stderr,
});
}
if let Some(sink) = &self.sink {
sink.deliver(tail.take(&dir, (owner, id), self.window.read_per_poll()));
}
std::thread::sleep(self.window.interval());
}
self.hand_over(id, tail)?;
Ok(Started::Backgrounded { id, what })
}
pub fn enrol_run(&self, scope: &Address, what: String) -> Result<BacklogId, AxError> {
let id = self.mint()?;
self.enrol(
id,
Member {
scope: scope.clone(),
what,
body: Body::Run(RunState::Going),
},
)?;
Ok(id)
}
pub fn stopping(&self, id: BacklogId) -> Result<bool, AxError> {
let table = self.hold()?;
Ok(table
.members
.get(&id)
.is_some_and(|member| matches!(member.body, Body::Run(RunState::Stopping))))
}
pub fn leave(&self, id: BacklogId) -> Result<(), AxError> {
let mut table = self.hold()?;
table.members.remove(&id);
Ok(())
}
pub fn halt(&self, scope: Option<&Address>) -> Result<usize, AxError> {
let mut table = self.hold()?;
let mut reached = 0usize;
for member in table.members.values_mut() {
let covered = scope.is_none_or(|within| member.scope.is_within(within));
if !covered {
continue;
}
let stopped = match &mut member.body {
Body::Command { child, .. } => child.kill().is_ok(),
Body::Run(state) => {
*state = RunState::Stopping;
true
}
};
if stopped {
reached = reached.saturating_add(1);
}
}
Ok(reached)
}
pub fn harvest(&self, owner: RunId) -> Result<Vec<Finished>, AxError> {
let mut table = self.hold()?;
let mut done = Vec::new();
let mut spent = Vec::new();
let mut live = Vec::new();
for (id, member) in &mut table.members {
let Body::Command {
child,
dir,
claim,
tail,
} = &mut member.body
else {
continue;
};
match claim {
Claim::Run(run) if *run == owner => {}
Claim::Nobody => {}
Claim::Window(_) | Claim::Run(_) => continue,
}
let Some(exit) = Exit::polled(child) else {
if let (Claim::Run(_), Some(_)) = (claim, &self.sink) {
live.extend(tail.take(dir, (owner, *id), self.window.read_per_poll()));
}
continue;
};
spent.push(*id);
if let Claim::Run(_) = claim {
let (stdout, stderr) = collect(dir);
done.push(Finished {
id: *id,
what: member.what.clone(),
exit,
stdout,
stderr,
});
} else {
drop(std::fs::remove_dir_all(dir));
}
}
for id in spent {
table.members.remove(&id);
}
drop(table);
if let Some(sink) = &self.sink {
sink.deliver(live);
}
Ok(done)
}
pub fn release(&self, owner: RunId) -> usize {
let mut table = self
.table
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
table.jobs.forget(owner);
let mut released = 0usize;
for member in table.members.values_mut() {
if let Body::Command { claim, child, .. } = &mut member.body
&& *claim == Claim::Run(owner)
{
*claim = Claim::Nobody;
drop(child.kill());
released = released.saturating_add(1);
}
}
released
}
pub fn standing(&self, scope: &Address) -> Result<Vec<Standing>, AxError> {
let table = self.hold()?;
Ok(table
.members
.iter()
.filter(|(_, member)| member.scope.is_within(scope))
.map(|(id, member)| Standing {
id: *id,
scope: member.scope.clone(),
what: member.what.clone(),
kind: member.body.kind(),
})
.collect())
}
fn hold(&self) -> Result<std::sync::MutexGuard<'_, Table>, AxError> {
self.table.lock().map_err(|_| {
AxError::failure(
AxCode::StorageFatal,
"reach the backlog",
"the table was left locked by a thread that died",
)
.with_recovery("restart this city; what was running is reported by `sprawling resume`")
})
}
fn mint(&self) -> Result<BacklogId, AxError> {
let mut table = self.hold()?;
table.next = table.next.saturating_add(1);
Ok(BacklogId(table.next))
}
}
#[cfg(test)]
mod tests;
mod jobs;
mod member;
mod report;
mod scratch;
mod tail;
pub mod waiting;
pub use jobs::RunProcesses;
use member::{Body, Claim, Member, RunState, collect, storage};
pub use report::{BacklogKind, Finished, Standing, Started};
use scratch::Scratch;
use tail::Tail;
pub use tail::{Chunk, Sink, Stream};
pub use waiting::{Exit, PollBudget, Unseen};