use std::path::Path;
use std::time::{Duration, Instant};
use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::protocol::{Request, Response};
use shep_daemon::boot::{self, Shepherd};
use crate::exit::ExitCode;
use crate::output::{KillRow, Streams, emit, write_outcome};
pub(crate) const KILL_TEARDOWN_WAIT: Duration = Duration::from_secs(10);
const KILL_POLL_INTERVAL: Duration = Duration::from_millis(20);
pub async fn kill(paths: &ShepPaths, streams: &mut Streams<'_>) -> ExitCode {
match Client::connect(&paths.socket).await {
Ok(client) => kill_with_wait(client, streams, KILL_TEARDOWN_WAIT).await,
Err(_) => kill_socket_free(paths, streams).await,
}
}
pub async fn kill_socket_free(paths: &ShepPaths, streams: &mut Streams<'_>) -> ExitCode {
kill_socket_free_with_wait(paths, streams, KILL_TEARDOWN_WAIT).await
}
async fn kill_socket_free_with_wait(
paths: &ShepPaths,
streams: &mut Streams<'_>,
wait: Duration,
) -> ExitCode {
let pid = match boot::daemon_liveness(paths) {
Ok(Shepherd::Running(pid)) => pid,
Ok(Shepherd::Booting) => {
let message = "a shepherd is starting up and has not recorded its pid yet; try again";
return streams.fail(ExitCode::DaemonUnreachable, message);
}
Ok(Shepherd::Absent) => {
let message = format!(
"no shepherd is running (nothing holds the lock on `{}`)",
boot::pidfile(paths).display()
);
return streams.fail(ExitCode::DaemonUnreachable, &message);
}
Err(err) => return streams.fail(ExitCode::Failure, &err.to_string()),
};
if let Err((code, message)) = signal_graceful_stop(pid) {
return streams.fail(code, &message);
}
#[cfg(unix)]
{
if wait_for_socket_to_disappear(&paths.socket, wait).await {
write_outcome(emit(
&mut *streams.out,
streams.fmt,
"kill",
KillRow {
pid,
socket_removed: true,
},
streams.style,
))
} else {
let message = "the shepherd was signalled, but teardown is still in progress";
streams.fail(ExitCode::DeadlineExceeded, message)
}
}
#[cfg(windows)]
{
let _ = wait;
ExitCode::Failure
}
}
pub(crate) fn signal_graceful_stop(pid: u32) -> Result<(), (ExitCode, String)> {
#[cfg(unix)]
{
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
let Ok(target) = i32::try_from(pid) else {
let message = format!("the recorded pid {pid} is not one this platform can signal");
return Err((ExitCode::Internal, message));
};
signal::kill(Pid::from_raw(target), Signal::SIGTERM).map_err(|errno| {
let message = format!("could not signal the shepherd at pid {pid}: {errno}");
(ExitCode::Failure, message)
})
}
#[cfg(windows)]
{
let message = format!(
"stopping the shepherd without the control pipe is not available on Windows: \
there is no signal to send it. The shepherd (pid {pid}) does handle the console \
control events, so press Ctrl-C in the window it is running in, or close that \
window, and it will stop its flock on the way out"
);
Err((ExitCode::Failure, message))
}
}
pub async fn kill_with_wait(client: Client, streams: &mut Streams<'_>, wait: Duration) -> ExitCode {
let socket = client.socket().to_path_buf();
let pid = client.daemon().pid;
let response = client.request(Request::KillDaemon).await;
drop(client);
match response {
Ok(Response::ShuttingDown) => {
if wait_for_socket_to_disappear(&socket, wait).await {
write_outcome(emit(
&mut *streams.out,
streams.fmt,
"kill",
KillRow {
pid,
socket_removed: true,
},
streams.style,
))
} else {
let message = "the daemon acknowledged shutdown, but teardown is still in progress";
streams.fail(ExitCode::DeadlineExceeded, message)
}
}
Ok(_) => {
let message = "the daemon answered with a response this client does not understand";
streams.fail(ExitCode::Internal, message)
}
Err(err) => {
let code = ExitCode::from(&err);
streams.fail(code, &err.to_string())
}
}
}
pub(crate) async fn wait_for_socket_to_disappear(socket: &Path, wait: Duration) -> bool {
let start = Instant::now();
loop {
if !control_address_answers(socket) {
return true;
}
if start.elapsed() >= wait {
return false;
}
tokio::time::sleep(KILL_POLL_INTERVAL).await;
}
}
fn control_address_answers(socket: &Path) -> bool {
#[cfg(unix)]
{
socket.exists()
}
#[cfg(windows)]
{
const ERROR_FILE_NOT_FOUND: i32 = 2;
match std::fs::OpenOptions::new().read(true).open(socket) {
Ok(_) => true,
Err(err) if err.raw_os_error() == Some(ERROR_FILE_NOT_FOUND) => false,
Err(_) => true,
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use shep_client::testing::fake_client_on;
use super::*;
use crate::cli::Format;
use crate::exit::ExitCode;
use crate::output::Streams;
fn test_paths(dir: &tempfile::TempDir) -> ShepPaths {
let paths = ShepPaths::resolve(
&|key| (key == "SHEP_HOME").then(|| dir.path().to_string_lossy().into_owned()),
Path::new("/nonexistent"),
);
std::fs::create_dir_all(&paths.pids).unwrap();
std::fs::create_dir_all(&paths.run).unwrap();
paths
}
#[cfg(unix)]
fn hold_pidfile_lock(paths: &ShepPaths, pid: Option<u32>) -> nix::fcntl::Flock<std::fs::File> {
use std::io::Write as _;
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(boot::pidfile(paths))
.unwrap();
let mut lock =
nix::fcntl::Flock::lock(file, nix::fcntl::FlockArg::LockExclusiveNonblock).unwrap();
if let Some(pid) = pid {
write!(&mut *lock, "{pid}").unwrap();
lock.flush().unwrap();
}
lock
}
#[cfg(unix)]
#[tokio::test]
async fn kill_falls_back_to_the_pidfile_when_the_handshake_refuses() {
use std::os::unix::process::ExitStatusExt as _;
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
let refusal = Err(shep_core::protocol::RpcError {
code: shep_core::protocol::RpcErrorCode::ProtocolMismatch,
message: "daemon speaks protocol 1, client sent 2".to_string(),
daemon_version: None,
});
let _daemon = shep_client::testing::fake_daemon(&paths.socket, refusal).await;
let child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.unwrap();
let _lock = hold_pidfile_lock(&paths, Some(child.id()));
let socket = paths.socket.clone();
let reaper = std::thread::spawn(move || {
let mut child = child;
let status = child.wait().unwrap();
let _ = std::fs::remove_file(&socket);
status
});
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
kill(&paths, &mut streams).await
};
assert_eq!(code, ExitCode::Success, "{}", String::from_utf8_lossy(&err));
let status = reaper.join().unwrap();
assert_eq!(
status.signal(),
Some(nix::sys::signal::Signal::SIGTERM as i32),
"the flock stops cleanly only if the daemon got its own handler's signal"
);
}
#[tokio::test]
async fn kill_refuses_a_pid_the_lock_does_not_prove_is_sheps() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
std::fs::write(boot::pidfile(&paths), "999999").unwrap();
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
kill(&paths, &mut streams).await
};
assert_ne!(code, ExitCode::Success);
let err = String::from_utf8(err).unwrap();
assert!(err.contains("no shepherd"), "{err}");
}
#[cfg(unix)]
#[tokio::test]
async fn kill_reports_a_booting_shepherd_rather_than_an_absence() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
let _lock = hold_pidfile_lock(&paths, None);
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
kill(&paths, &mut streams).await
};
assert_ne!(code, ExitCode::Success);
let err = String::from_utf8(err).unwrap();
assert!(err.contains("starting up"), "{err}");
assert!(
!err.contains("no shepherd"),
"a shepherd that is starting is not an absent one: {err}"
);
}
#[tokio::test]
async fn kill_waits_for_the_socket_to_disappear_before_reporting_success() {
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&path).await;
daemon.reply_shutting_down_then_unlink_after(Duration::from_millis(120));
assert!(path.exists());
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
kill_with_wait(client, &mut streams, KILL_TEARDOWN_WAIT).await
};
assert_eq!(code, ExitCode::Success);
assert!(
!path.exists(),
"success must mean the socket is actually gone"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_teardown_that_never_finishes_reports_in_progress_not_success() {
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&path).await;
daemon.reply_shutting_down_and_never_unlink();
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
kill_with_wait(client, &mut streams, Duration::from_millis(80)).await
};
assert_eq!(code, ExitCode::DeadlineExceeded);
assert!(
path.exists(),
"precondition: the fake really did leave the socket behind"
);
}
}