use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use super::{Lifecycle, LifecycleError, ServiceRecord};
use crate::config::types::ServiceTemplateConfig;
const KILL: &str = "/bin/kill";
const KILL_WAIT: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub(super) struct Running {
pid: u32,
pub(super) digest: [u8; 32],
pub(super) starter: String,
exited: Arc<(Mutex<bool>, Condvar)>,
}
pub(super) fn start(
lifecycle: &Arc<Lifecycle>,
record: &ServiceRecord,
template: &ServiceTemplateConfig,
starter: &str,
) -> Result<(Running, [u8; 32]), LifecycleError> {
let server = lifecycle
.server_address()
.ok_or_else(|| LifecycleError::Process("the server is not listening yet".to_owned()))?;
let token = credential()?;
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(lifecycle.log_path(&record.id))
.map_err(|error| LifecycleError::Process(format!("log: {}", error.kind())))?;
let log_err = log
.try_clone()
.map_err(|error| LifecycleError::Process(format!("log: {}", error.kind())))?;
let mut command = Command::new(&template.command);
command
.args(&template.args)
.env_clear()
.stdin(Stdio::null())
.stdout(log)
.stderr(log_err);
for name in &template.env {
if let Some(value) = std::env::var_os(name) {
command.env(name, value);
}
}
command
.env("LIMINAL_SERVER", server)
.env("LIMINAL_SERVICE", &record.id)
.env("LIMINAL_SUBJECTS", record.subjects.join(","))
.env("LIMINAL_TOKEN", &token);
let mut child = command.spawn().map_err(|error| {
LifecycleError::Process(format!(
"{} could not start: {}",
template.command.display(),
error.kind()
))
})?;
let pid = child.id();
let exited = Arc::new((Mutex::new(false), Condvar::new()));
let watcher = Arc::clone(lifecycle);
let seen = Arc::clone(&exited);
let id = record.id.clone();
std::thread::Builder::new()
.name(format!("service-{id}"))
.spawn(move || {
let status = child.wait().map_or_else(
|error| format!("wait failed: {}", error.kind()),
|s| s.to_string(),
);
let (flag, signal) = &*seen;
if let Ok(mut done) = flag.lock() {
*done = true;
}
signal.notify_all();
watcher.exited(&id, &status, now_ms());
})
.map_err(|error| LifecycleError::Process(format!("watcher: {}", error.kind())))?;
let digest = *blake3::hash(token.as_bytes()).as_bytes();
tracing::info!(service = %record.id, pid, "service process started");
Ok((
Running {
pid,
digest,
starter: starter.to_owned(),
exited,
},
digest,
))
}
pub(super) fn stop(process: &Running, timeout_ms: u64) -> Result<(), String> {
if signal(process, "TERM")? {
return Ok(());
}
if wait(process, Duration::from_millis(timeout_ms)) {
return Ok(());
}
if signal(process, "KILL")? {
return Ok(());
}
if wait(process, KILL_WAIT) {
Ok(())
} else {
Err(format!(
"process {} did not exit after SIGKILL",
process.pid
))
}
}
fn signal(process: &Running, name: &str) -> Result<bool, String> {
let (flag, _) = &*process.exited;
let done = flag
.lock()
.map_err(|_| "the exit flag is poisoned".to_owned())?;
if *done {
return Ok(true);
}
let sent = Command::new(KILL)
.arg(format!("-{name}"))
.arg(process.pid.to_string())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
drop(done);
match sent {
Ok(status) if status.success() => Ok(false),
Ok(status) => Err(format!("kill -{name} {} answered {status}", process.pid)),
Err(error) => Err(format!(
"kill -{name} {} could not run: {}",
process.pid,
error.kind()
)),
}
}
fn wait(process: &Running, limit: Duration) -> bool {
let (flag, signal) = &*process.exited;
let deadline = Instant::now() + limit;
let Ok(mut done) = flag.lock() else {
return false;
};
while !*done {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return false;
}
match signal.wait_timeout(done, left) {
Ok((guard, _)) => done = guard,
Err(_) => return false,
}
}
true
}
fn credential() -> Result<String, LifecycleError> {
let mut bytes = [0_u8; 32];
std::fs::File::open("/dev/urandom")
.and_then(|mut source| source.read_exact(&mut bytes))
.map_err(|error| {
LifecycleError::Process(format!("no random credential: {}", error.kind()))
})?;
Ok(hex::encode(bytes))
}
pub(super) fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |since| {
u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
})
}