use crate::Result;
use crate::daemon::Daemon;
use crate::daemon_id::DaemonId;
use crate::env;
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::StopSignal;
use crate::procs::PROCS;
use crate::state_file::StateFile;
use crate::supervisor::supervisor_record_is_live;
mod run;
mod start;
mod status;
mod stop;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KillOrStopOutcome {
Killed,
AlreadyDead,
StillRunning,
Unidentified,
}
pub fn unidentified_supervisor_error() -> miette::Report {
miette::miette!(
"a pitchfork supervisor is listening on {}, but the state file does not record its pid, \
so it cannot be stopped or replaced automatically. Stop its `pitchfork supervisor run` \
process manually.",
crate::ipc::socket_display()
)
}
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment)]
pub struct Supervisor {
#[usage(subcommand)]
command: Commands,
}
#[derive(Debug, usage_rs::Subcommands)]
enum Commands {
Run(run::Run),
Start(start::Start),
Status(status::Status),
Stop(stop::Stop),
}
impl Supervisor {
pub async fn run(self) -> Result<()> {
match self.command {
Commands::Run(run) => run.run().await,
Commands::Start(start) => start.run().await,
Commands::Status(status) => status.run().await,
Commands::Stop(stop) => stop.run().await,
}
}
}
pub async fn kill_or_stop(record: &Daemon, force: bool) -> Result<KillOrStopOutcome> {
let Some(existing_pid) = record.pid else {
return Ok(KillOrStopOutcome::AlreadyDead);
};
if !supervisor_record_is_live(record) {
return Ok(KillOrStopOutcome::AlreadyDead);
}
if !force {
return Ok(KillOrStopOutcome::StillRunning);
}
debug!("stopping pid {existing_pid}");
let Some(expected_start_time) = record.start_time.or_else(|| PROCS.start_time(existing_pid))
else {
return Err(miette::miette!(
"cannot verify the identity of supervisor pid {existing_pid}: its start time is unreadable; not signalling it. Try rerun with sudo."
));
};
#[cfg(unix)]
match request_shutdown().await {
ShutdownRequest::Accepted(budget) => {
return wait_for_exit_or_kill(record, existing_pid, expected_start_time, budget).await;
}
ShutdownRequest::Unanswered => {
let budget = match request_shutdown().await {
ShutdownRequest::Accepted(budget) => budget,
_ => estimated_stop_budget().await,
};
return wait_for_exit_or_kill(record, existing_pid, expected_start_time, budget).await;
}
ShutdownRequest::NotHandled => {}
}
let stop_signal: i32 = StopSignal::default().into();
let stop_budget = estimated_stop_budget().await;
let killed = PROCS
.kill_if_start_time_matches_async(
existing_pid,
Some(expected_start_time),
stop_signal,
Some(stop_budget),
)
.await;
match killed {
Ok(true) => Ok(KillOrStopOutcome::Killed),
Ok(false) => Ok(KillOrStopOutcome::AlreadyDead),
Err(e) => Err(miette::miette!("{e}. Try rerun with sudo.")),
}
}
#[cfg(unix)]
enum ShutdownRequest {
Accepted(std::time::Duration),
Unanswered,
NotHandled,
}
#[cfg(unix)]
async fn request_shutdown() -> ShutdownRequest {
let connect = IpcClient::connect(false);
let Ok(Ok(client)) = tokio::time::timeout(std::time::Duration::from_secs(5), connect).await
else {
return ShutdownRequest::NotHandled;
};
match client.shutdown().await {
Ok(Some(budget)) => ShutdownRequest::Accepted(budget),
Ok(None) => ShutdownRequest::NotHandled,
Err(_) => ShutdownRequest::Unanswered,
}
}
async fn estimated_stop_budget() -> std::time::Duration {
let base = crate::settings::settings().supervisor_stop_timeout();
let daemons = match live_daemons().await {
Some(daemons) => daemons,
None => StateFile::read(&*env::PITCHFORK_STATE_FILE)
.map(|sf| sf.daemons.into_values().collect())
.unwrap_or_default(),
};
crate::supervisor::shutdown_budget(&daemons, base)
}
#[cfg(unix)]
async fn wait_for_exit_or_kill(
record: &Daemon,
pid: u32,
expected_start_time: u64,
budget: std::time::Duration,
) -> Result<KillOrStopOutcome> {
let deadline = tokio::time::Instant::now() + budget;
while tokio::time::Instant::now() < deadline {
if !supervisor_record_is_live(record) {
return Ok(KillOrStopOutcome::Killed);
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
warn!(
"supervisor pid {pid} did not exit within {}ms of the stop request, sending SIGKILL",
budget.as_millis()
);
PROCS
.kill_if_start_time_matches_async(
pid,
Some(expected_start_time),
libc::SIGKILL,
Some(std::time::Duration::from_secs(1)),
)
.await
.map(|_| KillOrStopOutcome::Killed)
.map_err(|e| miette::miette!("{e}. Try rerun with sudo."))
}
async fn live_daemons() -> Option<Vec<Daemon>> {
let ask = async {
let client = IpcClient::connect(false).await.ok()?;
client.active_daemons().await.ok()
};
tokio::time::timeout(std::time::Duration::from_secs(2), ask)
.await
.ok()
.flatten()
}
pub fn existing_supervisor() -> Result<Option<Daemon>> {
let sf = StateFile::read(&*env::PITCHFORK_STATE_FILE)?;
Ok(sf.daemons.get(&DaemonId::pitchfork()).cloned())
}
pub async fn resolve_existing_supervisor(force: bool) -> Result<(Option<u32>, KillOrStopOutcome)> {
let mut record = existing_supervisor()?;
if !record.as_ref().is_some_and(supervisor_record_is_live) {
if !crate::ipc::supervisor_listening().await {
let existing_pid = record.and_then(|d| d.pid);
return Ok((existing_pid, KillOrStopOutcome::AlreadyDead));
}
debug!("supervisor is listening on the IPC socket but not recorded; asking it to restore");
if let Err(err) = IpcClient::connect(false).await {
debug!("failed to connect to the unrecorded supervisor: {err:?}");
}
record = existing_supervisor()?;
if !record.as_ref().is_some_and(supervisor_record_is_live) {
return Ok((None, KillOrStopOutcome::Unidentified));
}
}
let record = record.expect("a live record was found above");
let outcome = kill_or_stop(&record, force).await?;
Ok((record.pid, outcome))
}