use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::protocol::ProcessInfo;
use crate::cli::Format;
use crate::commands::dogs;
use crate::commands::shep_toml::ShepToml;
use crate::dog_index::{self, Need};
use crate::output::Streams;
use crate::terminal_safe::sanitise;
mod state;
#[cfg(test)]
mod tests;
pub(crate) use state::run;
pub(crate) const LOG_FILE_HINT_BYTES: u64 = 100 * 1024 * 1024;
pub(crate) const RESTART_HINT_COUNT: u32 = 3;
const BARK: &str = "bark";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Hint {
Logs,
Alerts,
Upgrade,
}
impl Hint {
pub(crate) const ALL: [Self; 3] = [Self::Logs, Self::Alerts, Self::Upgrade];
pub(crate) const fn name(self) -> &'static str {
match self {
Self::Logs => "logs",
Self::Alerts => "alerts",
Self::Upgrade => "upgrade",
}
}
pub(crate) fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|hint| hint.name() == name)
}
const fn need(self) -> Option<Need> {
match self {
Self::Logs => Some(Need::Logs),
Self::Alerts => Some(Need::Alerts),
Self::Upgrade => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum OfferState {
NotInstalled,
Installed,
BuiltIn,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Offer {
pub(crate) name: String,
pub(crate) state: OfferState,
}
impl Offer {
pub(crate) fn command(&self) -> String {
match self.state {
OfferState::NotInstalled => format!("shep install {}", self.name),
OfferState::Installed | OfferState::BuiltIn => format!("shep enable {}", self.name),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Shown {
pub(crate) hint: Hint,
pub(crate) line: String,
pub(crate) offers: Vec<Offer>,
}
pub(crate) fn next(paths: &ShepPaths, index_url: &str, flock: &[ProcessInfo]) -> Option<Shown> {
let off = state::Off::read(&paths.home);
let enabled = ShepToml::read_only(&paths.daemon_config)
.map(|config| config.enabled_dog_names())
.unwrap_or_default();
let index = dog_index::cached_only(paths, index_url);
Hint::ALL
.into_iter()
.filter(|hint| !off.contains(*hint))
.find_map(|hint| show(hint, paths, index.as_ref(), &enabled, flock))
}
fn show(
hint: Hint,
paths: &ShepPaths,
index: Option<&dog_index::Index>,
enabled: &[String],
flock: &[ProcessInfo],
) -> Option<Shown> {
if hint == Hint::Upgrade {
return show_upgrade(index);
}
let sheep = flock
.iter()
.filter(|info| info.dog.is_none() && applies(hint, info))
.min_by_key(|info| info.id)?;
let names = offered_names(hint, index);
if names.iter().any(|name| enabled.contains(name)) {
return None;
}
let offers = offers_of(paths, names);
if offers.is_empty() {
return None;
}
let (name, _) = sanitise(&sheep.name);
let commands: Vec<String> = offers.iter().map(Offer::command).collect();
let line = match hint {
Hint::Logs => format!(
"hint: {name}'s log is past {} MiB. Try: {}",
LOG_FILE_HINT_BYTES / (1024 * 1024),
commands.join(" or ")
),
Hint::Alerts => format!(
"hint: {name} has restarted {} times. Try: {}",
sheep.restarts,
commands.join(" or ")
),
Hint::Upgrade => return None,
};
Some(Shown { hint, line, offers })
}
fn show_upgrade(index: Option<&dog_index::Index>) -> Option<Shown> {
let out = index?.shep.as_ref()?;
let this: semver::Version = env!("CARGO_PKG_VERSION").parse().ok()?;
(*out > this).then(|| Shown {
hint: Hint::Upgrade,
line: format!("hint: shep {out} is out and this is {this}. Try: shep upgrade"),
offers: Vec::new(),
})
}
fn offers_of(paths: &ShepPaths, names: Vec<String>) -> Vec<Offer> {
names
.into_iter()
.map(|name| {
let state = if name == BARK {
OfferState::BuiltIn
} else if dogs::installed(paths, &name).is_some() {
OfferState::Installed
} else {
OfferState::NotInstalled
};
Offer { name, state }
})
.collect()
}
fn applies(hint: Hint, sheep: &ProcessInfo) -> bool {
match hint {
Hint::Logs => [&sheep.out_file, &sheep.err_file]
.into_iter()
.flatten()
.filter_map(|path| std::fs::metadata(path).ok())
.any(|meta| meta.len() > LOG_FILE_HINT_BYTES),
Hint::Alerts => sheep.restarts >= RESTART_HINT_COUNT,
Hint::Upgrade => false,
}
}
fn offered_names(hint: Hint, index: Option<&dog_index::Index>) -> Vec<String> {
let mut names = Vec::new();
if hint == Hint::Alerts {
names.push(BARK.to_owned());
}
let Some(need) = hint.need() else {
return names;
};
for row in index.map_or(&[][..], |index| &index.dogs[..]) {
if row.needs.contains(&need) && !names.contains(&row.adopt_as) {
names.push(row.adopt_as.clone());
}
}
names
}
pub(crate) async fn print_under_flock(
streams: &mut Streams<'_>,
paths: &ShepPaths,
quiet: bool,
follow: bool,
stdout_is_terminal: bool,
) {
if streams.fmt != Format::Table || quiet || follow || !stdout_is_terminal {
return;
}
let Ok(client) = Client::connect(&paths.socket).await else {
return;
};
let Ok(flock) = client.list_flock().await else {
return;
};
if let Some(shown) = next(paths, &dog_index::index_url(), &flock) {
let _ = writeln!(streams.out, "{}", shown.line);
}
}