use std::path::Path;
use std::time::{Duration, Instant};
use shep_client::Client;
use shep_core::protocol::{Request, Response};
use crate::exit::ExitCode;
use crate::output::{KillRow, Streams, emit, write_outcome};
const KILL_TEARDOWN_WAIT: Duration = Duration::from_secs(10);
const KILL_POLL_INTERVAL: Duration = Duration::from_millis(20);
pub async fn kill(client: Client, streams: &mut Streams<'_>) -> ExitCode {
kill_with_wait(client, streams, KILL_TEARDOWN_WAIT).await
}
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())
}
}
}
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;
#[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(client, &mut streams).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"
);
}
}