use std::time::{Duration, Instant};
#[cfg(any(test, feature = "test-support"))]
fn harness_helper() -> bool {
#[cfg(test)]
{
true
}
#[cfg(not(test))]
{
static HARNESS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*HARNESS.get_or_init(|| {
std::env::current_exe()
.ok()
.and_then(|exe| exe.file_name().map(|n| n.to_string_lossy().into_owned()))
.is_some_and(|name| name.starts_with("lean_ctx_server-"))
})
}
}
mod gate;
mod guard;
mod spawn_helper;
mod spawner;
mod starter;
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(dead_code, unused_imports, clippy::unwrap_used))]
pub mod tests;
mod wire;
pub use gate::{Refusal, start_behind_guard};
#[cfg(any(test, feature = "test-support"))]
pub use guard::force_outcome_for_test;
pub use guard::{CallGuard, GuardOutcome};
pub(crate) use spawn_helper::lead_new_session;
pub use spawner::{HelpedChild, HelperSpawn, shutdown_spawn_helper, spawn_shell};
static GUARD_HOST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(cfg!(test));
fn guard_host() -> bool {
#[cfg(any(test, feature = "test-support"))]
if harness_helper() {
return true;
}
GUARD_HOST.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn start_guarded_child(
command: &mut std::process::Command,
) -> Result<(std::process::Child, Option<CallGuard>), Refusal> {
if !guard_host() {
note_no_guard_host();
return command
.spawn()
.map(|child| (child, None))
.map_err(Refusal::NotStarted);
}
start_behind_guard(command).map(|(child, guard)| (child, Some(guard)))
}
fn note_no_guard_host() {
static NOTED: std::sync::Once = std::sync::Once::new();
NOTED.call_once(|| {
tracing::debug!(
"process guard unavailable: this executable is not lean-ctx; child processes run without a guard"
);
});
}
pub(crate) const GUARD_MODE: &str = "__group-guard";
pub(crate) const STARTER_MODE: &str = "__session-starter";
pub(crate) const HELPER_MODE: &str = "__spawn-helper";
pub(crate) const SESSION_ENV: &str = "LEAN_CTX_SESSION_CONTAINMENT";
pub(crate) const SESSION_ACK: &[u8] = b"{\"leanctx_session_containment\":1}\n";
pub(crate) const EXIT_USAGE: i32 = 64;
pub(crate) const EXIT_CONTAINMENT: i32 = 70;
pub const END_NOTICE: &str =
"[lean-ctx: processes started by this command could not be confirmed as ended]";
pub const HELPER_NOTICE: &str = "[lean-ctx: this command ran without the process helper; had lean-ctx stopped, nothing would have ended it]";
pub const UNCLEAR_NOTICE: &str =
"[lean-ctx: this command may or may not have run; it was not repeated]";
pub const RELEASE_NOTICE: &str = "[lean-ctx: this command finished, but lean-ctx could not confirm that its background processes were left running]";
pub fn end_notice(outcome: GuardOutcome) -> Option<&'static str> {
(outcome != GuardOutcome::Ended).then_some(END_NOTICE)
}
const END_BUDGET: Duration = Duration::from_secs(5);
pub fn dispatch_hidden_mode() -> Option<i32> {
GUARD_HOST.store(true, std::sync::atomic::Ordering::Relaxed);
let mut args = std::env::args_os();
let _program = args.next();
let first = args.next()?;
if first == GUARD_MODE {
return Some(guard::main(args));
}
if first == STARTER_MODE {
return Some(starter::main_explicit(args));
}
if first == HELPER_MODE {
return Some(spawn_helper::main(args));
}
if first == "engine" && std::env::var_os(SESSION_ENV).is_some_and(|value| value == "1") {
return starter::main_requested();
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GroupEnd {
Empty,
Unconfirmed,
Survivors,
Refused,
}
pub(crate) fn end_pinned_group(group: libc::pid_t, budget: Duration) -> GroupEnd {
const ROUNDS_WITHOUT_SIGHT: u32 = 5;
let Some(group) = u32::try_from(group)
.ok()
.and_then(|group| crate::ipc::process::group_target(group).ok())
else {
return GroupEnd::Refused;
};
let deadline = Instant::now() + budget;
let mut rounds = 0_u32;
loop {
unsafe { libc::killpg(group, libc::SIGKILL) };
rounds += 1;
let pause = Duration::from_millis(u64::from(rounds.min(10)) * 2);
std::thread::sleep(pause);
let seen = live_members(group);
if seen == Some(0) {
std::thread::sleep(pause);
if live_members(group) == Some(0) {
return GroupEnd::Empty;
}
} else if seen.is_none() && rounds >= ROUNDS_WITHOUT_SIGHT {
return GroupEnd::Unconfirmed;
}
if Instant::now() >= deadline {
return GroupEnd::Survivors;
}
}
}
pub fn live_members(group: libc::pid_t) -> Option<usize> {
let ps = ["/bin/ps", "/usr/bin/ps"]
.into_iter()
.find(|path| std::path::Path::new(path).is_file())
.unwrap_or("ps");
let output = std::process::Command::new(ps)
.args(["-A", "-o", "pgid=", "-o", "stat="])
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(count_live(&String::from_utf8_lossy(&output.stdout), group))
}
fn count_live(table: &str, group: libc::pid_t) -> usize {
table
.lines()
.filter(|line| {
let mut fields = line.split_whitespace();
let in_group = fields
.next()
.and_then(|pgid| pgid.parse::<libc::pid_t>().ok())
== Some(group);
in_group && !fields.next().is_some_and(|state| state.starts_with('Z'))
})
.count()
}
fn write_fd(fd: libc::c_int, mut bytes: &[u8]) -> bool {
while !bytes.is_empty() {
let written = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) };
if written > 0 {
bytes = &bytes[written as usize..];
} else if std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
return false;
}
}
true
}
fn set_termination_signals(handler: libc::sighandler_t) {
for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP] {
unsafe { libc::signal(signal, handler) };
}
unsafe { libc::signal(libc::SIGPIPE, libc::SIG_IGN) };
}