use serde::Deserialize;
use std::sync::OnceLock;
fn rt() -> &'static tokio::runtime::Runtime {
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("looop: failed to build tokio runtime")
})
}
#[derive(Debug, Deserialize, Default)]
pub struct Session {
pub id: String,
#[serde(default)]
#[allow(dead_code)]
pub cmd: Option<serde_json::Value>,
#[serde(default)]
pub state: String,
#[serde(default)]
pub alive: bool,
#[serde(default)]
pub exit_code: Option<i64>,
#[serde(default)]
pub note: Option<String>,
}
impl Session {
pub fn is_looop(&self) -> bool {
self.id.starts_with("looop-")
}
pub fn flagged(&self) -> bool {
self.note.as_deref().map(|n| !n.is_empty()).unwrap_or(false)
}
}
pub fn list() -> Vec<Session> {
match rt().block_on(::babysit::api::list_sessions()) {
Ok(sessions) => sessions
.into_iter()
.map(|s| Session {
id: s.id,
cmd: None,
state: s.state,
alive: s.alive,
exit_code: s.exit_code.map(|c| c as i64),
note: s.note,
})
.collect(),
Err(_) => Vec::new(),
}
}
pub fn list_looop() -> Vec<Session> {
list().into_iter().filter(Session::is_looop).collect()
}
pub fn prune() {
use ::babysit::session::{self, State};
rt().block_on(async {
let ids = match session::list_ids().await {
Ok(ids) => ids,
Err(_) => return,
};
for id in ids {
let Ok(meta) = session::read_meta(&id).await else {
continue; };
let status = session::read_status(&id).await.ok();
let alive = session::is_pid_alive(meta.babysit_pid);
let dead = match status.as_ref().map(|s| s.state) {
Some(State::Exited | State::Killed) => true,
Some(State::Starting | State::Running) if !alive => true,
None if !alive => true,
_ => false,
};
if dead && let Ok(dir) = ::babysit::paths::session_dir(&id) {
let _ = tokio::fs::remove_dir_all(&dir).await;
}
}
});
}
pub fn status_exists(session: &str) -> bool {
list().iter().any(|s| s.id == session)
}
pub fn kill(session: &str) -> anyhow::Result<()> {
rt().block_on(::babysit::sub::kill(Some(session.to_string()), false))
}
pub fn flag(session: &str, message: Option<String>) -> anyhow::Result<()> {
rt().block_on(::babysit::sub::flag(
Some(session.to_string()),
message,
false,
))
}
pub fn unflag(session: &str) -> anyhow::Result<()> {
rt().block_on(::babysit::sub::unflag(Some(session.to_string()), false))
}
pub fn attach(session: &str) -> anyhow::Result<i32> {
rt().block_on(::babysit::attach::attach(Some(session.to_string())))
}
pub fn ls(json: bool, watch: bool, interval: String) -> anyhow::Result<()> {
rt().block_on(::babysit::sub::list(json, watch, interval))
}
pub fn spawn_detached(cmd: Vec<String>, session: &str) -> anyhow::Result<()> {
suppress_stdout(|| {
rt().block_on(::babysit::run::run(
cmd,
Some(session.to_string()),
true, None, false, None, None, None, true, ))
})
.map(|_code| ())
}
pub fn run_detached_worker(args: &[String]) -> anyhow::Result<i32> {
use anyhow::Context;
let mut id = None;
let mut no_tty = false;
let mut timeout = None;
let mut idle_timeout = None;
let mut size = None;
let mut cmd: Vec<String> = Vec::new();
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--detached-id" => id = it.next().cloned(),
"--no-tty" => no_tty = true,
"--timeout" => timeout = it.next().cloned(),
"--idle-timeout" => idle_timeout = it.next().cloned(),
"--size" => size = it.next().cloned(),
"--" => {
cmd = it.by_ref().cloned().collect();
break;
}
_ => {} }
}
let id = id.context("looop run --detached-id: missing worker id")?;
rt().block_on(::babysit::run::run(
cmd,
None,
false,
Some(id),
no_tty,
timeout,
idle_timeout,
size,
false,
))
}
#[cfg(unix)]
fn suppress_stdout<T>(f: impl FnOnce() -> T) -> T {
use std::io::Write;
use std::os::unix::io::AsRawFd;
unsafe extern "C" {
fn dup(fd: i32) -> i32;
fn dup2(a: i32, b: i32) -> i32;
fn close(fd: i32) -> i32;
}
let Ok(devnull) = std::fs::OpenOptions::new().write(true).open("/dev/null") else {
return f();
};
let _ = std::io::stdout().flush();
unsafe {
let saved = dup(1);
if saved < 0 {
return f();
}
dup2(devnull.as_raw_fd(), 1);
let out = f();
let _ = std::io::stdout().flush();
dup2(saved, 1);
close(saved);
out
}
}
#[cfg(not(unix))]
fn suppress_stdout<T>(f: impl FnOnce() -> T) -> T {
f()
}
pub fn is_alive(session: &str) -> bool {
list().iter().any(|s| s.id == session && s.alive)
}
pub fn any_looop_alive() -> bool {
list_looop().iter().any(|s| s.alive)
}
#[cfg(test)]
mod tests {
use super::*;
fn sess(id: &str, note: Option<&str>) -> Session {
Session {
id: id.to_string(),
note: note.map(str::to_string),
..Default::default()
}
}
#[test]
fn is_looop_matches_only_prefixed_ids() {
assert!(sess("looop-triage", None).is_looop());
assert!(!sess("other-job", None).is_looop());
assert!(!sess("looop", None).is_looop()); }
#[test]
fn flagged_iff_nonempty_note() {
assert!(sess("looop-x", Some("help")).flagged());
assert!(!sess("looop-x", Some("")).flagged());
assert!(!sess("looop-x", None).flagged());
}
}