use std::io::Read;
use std::os::unix::process::CommandExt;
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,
approved: Option<&super::templates::TemplateRecord>,
starter: &str,
) -> Result<(Running, [u8; 32]), LifecycleError> {
let staged = approved
.map(|approved| lifecycle.stage_template(approved))
.transpose()
.map_err(|error| LifecycleError::Process(error.to_string()))?;
let server = lifecycle
.server_address()
.ok_or_else(|| LifecycleError::Process("the server is not listening yet".to_owned()))?;
let token = credential()?;
let (output, errors) = if approved.is_some() {
(Stdio::null(), Stdio::null())
} else {
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 error_log = log
.try_clone()
.map_err(|error| LifecycleError::Process(format!("log: {}", error.kind())))?;
(Stdio::from(log), Stdio::from(error_log))
};
let mut command = Command::new(staged.as_ref().map_or(
template.command.as_path(),
super::templates::StagedCommand::command,
));
if staged.is_some() {
command.arg0(&template.command);
}
command
.args(&template.args)
.env_clear()
.stdin(Stdio::null())
.stdout(output)
.stderr(errors);
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.as_str());
let (pid, exited) = spawn_watched(&mut command, lifecycle, &record.id, staged)?;
drop(command);
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,
))
}
type ExitSignal = Arc<(Mutex<bool>, Condvar)>;
fn spawn_watched(
command: &mut Command,
lifecycle: &Arc<Lifecycle>,
id: &str,
staged: Option<super::templates::StagedCommand>,
) -> Result<(u32, ExitSignal), LifecycleError> {
let (send, receive) = std::sync::mpsc::sync_channel::<(
std::process::Child,
Option<super::templates::StagedCommand>,
)>(1);
let mut child_signals = signal_hook::iterator::Signals::new([signal_hook::consts::SIGCHLD])
.map_err(|error| LifecycleError::Process(format!("child_exit_signal: {}", error.kind())))?;
let exited = Arc::new((Mutex::new(false), Condvar::new()));
let seen = Arc::clone(&exited);
let watcher = Arc::clone(lifecycle);
let id = id.to_owned();
let thread = std::thread::Builder::new()
.name(format!("service-{id}"))
.spawn(move || {
let Ok((mut child, staged)) = receive.recv() else {
return;
};
let status = observe_exit(&mut child, &mut child_signals, &seen)
.unwrap_or_else(std::convert::identity);
drop(staged);
seen.1.notify_all();
watcher.exited(&id, &status, now_ms());
})
.map_err(|error| LifecycleError::Process(format!("watcher: {}", error.kind())))?;
let child = match command.spawn() {
Ok(child) => child,
Err(error) => {
drop(send);
thread
.join()
.map_err(|_| LifecycleError::Process("watcher_join_failed".into()))?;
return Err(LifecycleError::Process(format!(
"command could not start: {}",
error.kind()
)));
}
};
let pid = child.id();
if let Err(error) = send.send((child, staged)) {
let (mut child, staged) = error.0;
let killed = child.kill();
let joined = child.wait();
drop(staged);
thread
.join()
.map_err(|_| LifecycleError::Process("watcher_join_failed".into()))?;
killed.map_err(|error| {
LifecycleError::Process(format!("unwatched_child_signal_failed: {}", error.kind()))
})?;
joined.map_err(|error| {
LifecycleError::Process(format!("unwatched_child_join_failed: {}", error.kind()))
})?;
return Err(LifecycleError::Process("watcher_handoff_failed".into()));
}
Ok((pid, exited))
}
fn observe_exit(
child: &mut std::process::Child,
signals: &mut signal_hook::iterator::Signals,
seen: &ExitSignal,
) -> Result<String, String> {
for _ in signals.forever() {
let Ok(mut done) = seen.0.lock() else {
cleanup_child(child)?;
return Err("child_exit_signal_poisoned".into());
};
match child.try_wait() {
Ok(Some(status)) => {
*done = true;
return Ok(status.to_string());
}
Ok(None) => {}
Err(error) => {
let killed = child.kill();
let joined = child.wait();
*done = true;
drop(done);
killed.map_err(|error| format!("child_cleanup_signal_failed: {}", error.kind()))?;
joined.map_err(|error| format!("child_cleanup_join_failed: {}", error.kind()))?;
return Err(format!("child_wait_failed: {}", error.kind()));
}
}
}
cleanup_child(child)?;
Err("child_exit_signal_closed".into())
}
fn cleanup_child(child: &mut std::process::Child) -> Result<(), String> {
if child
.try_wait()
.map_err(|error| format!("child_cleanup_state_failed: {}", error.kind()))?
.is_none()
{
child
.kill()
.map_err(|error| format!("child_cleanup_signal_failed: {}", error.kind()))?;
child
.wait()
.map_err(|error| format!("child_cleanup_join_failed: {}", error.kind()))?;
}
Ok(())
}
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<zeroize::Zeroizing<String>, LifecycleError> {
let mut bytes = zeroize::Zeroizing::new([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(zeroize::Zeroizing::new(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)
})
}