use std::io::IsTerminal;
use std::time::Duration;
use cli::{Commands, Format};
use commands::query;
use exit::ExitCode;
use output::Streams;
use shep_client::Client;
use crate::client::unreachable_message;
use crate::remote::Link;
use crate::{cli, commands, exit, output};
const RECOVERY_VERBS: [&str; 3] = ["kill", "daemon reload", "ping"];
fn recovery_verb(command: &Commands) -> Option<&'static str> {
match command {
Commands::Kill => Some("kill"),
Commands::Ping => Some("ping"),
Commands::Daemon(args) => match args.cmd {
Some(cli::DaemonCmd::Reload) => Some("daemon reload"),
None => None,
},
_ => None,
}
}
#[allow(dead_code)]
const READ_VERBS: [&str; 3] = ["flock", "describe", "bleats"];
fn read_verb(command: &Commands) -> Option<&'static str> {
match command {
Commands::Flock(_) => Some("flock"),
Commands::Describe(_) => Some("describe"),
Commands::Bleats(_) => Some("bleats"),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionGuard {
Enforce,
Exempt,
Warn,
}
impl VersionGuard {
pub(crate) fn for_command(command: &Commands) -> Self {
match (recovery_verb(command), read_verb(command)) {
(Some(_), _) => Self::Exempt,
(None, Some(_)) => Self::Warn,
(None, None) => Self::Enforce,
}
}
}
const VERSION_SKEW_CAUSE: [&str; 2] = [
"`cargo install shep` replaced the binary. It did not restart the",
"shepherd, which is still running the old code.",
];
const VERSION_SKEW_HOST_CAUSE: [&str; 2] = [
"The shepherd on that host runs a different shep than this one.",
"A reload runs the shep installed there; if they still differ, install the same version on both.",
];
pub(crate) const VERSION_SKEW_REMEDY: &str = RECOVERY_VERBS[1];
fn version_skew_command(host: Option<&str>) -> String {
match host {
Some(dest) => format!("ssh {dest} shep {VERSION_SKEW_REMEDY}"),
None => format!("shep {VERSION_SKEW_REMEDY}"),
}
}
fn version_skew_instruction(fmt: Format, host: Option<&str>) -> String {
let command = version_skew_command(host);
match fmt {
Format::Json => format!("Run `{command}`."),
Format::Table => format!("Run:\n {command}"),
}
}
fn version_skew_summary(running: &str, host: Option<&str>) -> String {
let shepherd = match host {
Some(dest) => format!("the shepherd on {dest}"),
None => "the running shepherd".to_owned(),
};
format!(
"this shep is {}, {shepherd} is {running}",
env!("CARGO_PKG_VERSION")
)
}
pub(crate) fn version_skew_warning(client: &Client, host: Option<&str>) -> Option<String> {
let running = client.daemon().daemon_version.as_str();
if running == env!("CARGO_PKG_VERSION") {
return None;
}
let summary = version_skew_summary(running, host);
let command = version_skew_command(host);
let sentence = format!(
"{summary}. Listing works, but verbs that change the flock refuse until it is reloaded. Run `{command}`."
);
Some(crate::terminal_safe::sanitise(&sentence).0)
}
pub(crate) fn host_skew(
client: &Client,
host: &str,
guard: VersionGuard,
) -> Result<Option<String>, String> {
match guard {
VersionGuard::Exempt => Ok(None),
VersionGuard::Warn => Ok(version_skew_warning(client, Some(host))),
VersionGuard::Enforce => {
let running = client.daemon().daemon_version.as_str();
if running == env!("CARGO_PKG_VERSION") {
return Ok(None);
}
let summary = version_skew_summary(running, Some(host));
let command = version_skew_command(Some(host));
let sentence = format!("{summary}. Run `{command}`.");
Err(crate::terminal_safe::sanitise(&sentence).0)
}
}
}
pub(crate) fn refuse_version_skew(
streams: &mut Streams<'_>,
client: &Client,
guard: VersionGuard,
) -> Result<(), ExitCode> {
let running = client.daemon().daemon_version.as_str();
match guard {
VersionGuard::Exempt => return Ok(()),
VersionGuard::Warn => {
if let Some(warning) = version_skew_warning(client, client.host()) {
streams.aside("version_skew", &warning);
}
return Ok(());
}
VersionGuard::Enforce if running == env!("CARGO_PKG_VERSION") => return Ok(()),
VersionGuard::Enforce => {}
}
let code = ExitCode::VersionSkew;
let host = client.host();
let cause = match host {
Some(_) => VERSION_SKEW_HOST_CAUSE,
None => VERSION_SKEW_CAUSE,
};
let summary = version_skew_summary(running, host);
match streams.fmt {
Format::Json => {
let cause = cause.join(" ");
let instruction = version_skew_instruction(Format::Json, host);
streams.fail(code, &format!("{summary}. {cause} {instruction}"));
}
Format::Table => {
let cause = cause.join("\n");
let summary = crate::terminal_safe::sanitise(&summary).0;
let instruction = version_skew_instruction(Format::Table, host);
streams.fail(code, &format!("{summary}\n\n{cause}\n\n{instruction}"));
}
}
Err(code)
}
pub(crate) async fn connect_client(
streams: &mut Streams<'_>,
link: &Link<'_>,
guard: VersionGuard,
) -> Result<Client, ExitCode> {
let dialled = match link {
Link::Local(paths) => Client::connect(&paths.socket).await,
Link::Host { dest, options } => Client::connect_ssh_with(dest, options).await,
};
match dialled {
Ok(client) => {
refuse_version_skew(streams, &client, guard)?;
Ok(client)
}
Err(err) => {
let code = ExitCode::from(&err);
Err(streams.fail(code, &unreachable_message(&err)))
}
}
}
pub(crate) async fn flock_command(
streams: &mut Streams<'_>,
link: &Link<'_>,
guard: VersionGuard,
args: &cli::FlockArgs,
) -> ExitCode {
flock_command_on(streams, link, guard, args, std::io::stdout().is_terminal()).await
}
pub(crate) async fn flock_command_on(
streams: &mut Streams<'_>,
link: &Link<'_>,
guard: VersionGuard,
args: &cli::FlockArgs,
stdout_is_terminal: bool,
) -> ExitCode {
let paths = match link {
Link::Local(paths) => *paths,
Link::Host { .. } if args.follow => {
return follow_flock_command(streams, link, guard, args, stdout_is_terminal).await;
}
Link::Host { dest, .. } => {
return match connect_client(streams, link, guard).await {
Ok(client) => query::flock_from(&client, streams, Some(dest)).await,
Err(code) => code,
};
}
};
if args.follow {
return follow_flock_command(streams, link, guard, args, stdout_is_terminal).await;
}
let hosts = query::fleet::listed_hosts(streams, paths);
if !hosts.is_empty() {
return query::fleet::flock_all(streams, paths, guard, &hosts).await;
}
match Client::connect(&paths.socket).await {
Ok(client) => match refuse_version_skew(streams, &client, guard) {
Ok(()) => query::flock(&client, streams).await,
Err(code) => code,
},
Err(err) if streams.fmt == Format::Json => {
let code = ExitCode::from(&err);
streams.fail(code, &unreachable_message(&err))
}
Err(shep_client::ConnectError::Connect { .. }) => query::flock_from_roll(streams, paths),
Err(err) => {
let code = ExitCode::from(&err);
streams.fail(code, &flock_connect_refusal_message(&err))
}
}
}
async fn follow_flock_command(
streams: &mut Streams<'_>,
link: &Link<'_>,
guard: VersionGuard,
args: &cli::FlockArgs,
stdout_is_terminal: bool,
) -> ExitCode {
if streams.fmt == Format::Json {
return streams.fail(
ExitCode::Usage,
"`--follow` redraws a table; `--format json` has no follow form",
);
}
if !stdout_is_terminal {
return streams.fail(
ExitCode::Usage,
"`--follow` needs a terminal; stdout is not one",
);
}
match connect_client(streams, link, guard).await {
Ok(client) => {
query::flock_follow(&client, streams, Duration::from_secs(args.interval)).await
}
Err(code) => code,
}
}
pub(crate) fn flock_connect_refusal_message(err: &shep_client::ConnectError) -> String {
format!("{err}; run `shep {VERSION_SKEW_REMEDY}`")
}
#[cfg(test)]
mod tests;