use std::collections::BTreeMap;
use std::path::Path;
use clap::ValueEnum as _;
use shep_client::{Client, START_DEADLINE};
use shep_core::config::{DeclaredApp, ResetDepth};
use shep_core::protocol::{Request, Response, SheepApplied};
use crate::cli::{ResetMode, StartArgs};
use crate::commands::lifecycle::Load;
use crate::exit::ExitCode;
use crate::output::Streams;
pub(crate) fn any_restart_failed(procs: &[shep_core::protocol::ProcessInfo]) -> bool {
procs
.iter()
.any(|info| info.status == shep_core::status::ProcStatus::Errored)
}
pub(crate) fn default_cwd_to_flockfile_dir(
apps: Vec<DeclaredApp>,
flockfile: &Path,
) -> Vec<DeclaredApp> {
let Some(dir) = std::fs::canonicalize(flockfile)
.ok()
.and_then(|abs| abs.parent().map(Path::to_path_buf))
.map(|dir| shep_core::paths::strip_verbatim_prefix(&dir).into_owned())
else {
return apps;
};
let dir = dir.to_string_lossy().into_owned();
apps.into_iter()
.map(|mut app| {
if app.config.cwd.is_none() {
app.config.cwd = Some(dir.clone());
}
app
})
.collect()
}
pub(crate) async fn apply_declared(
client: &Client,
streams: &mut Streams<'_>,
declared: &[DeclaredApp],
reset: ResetDepth,
mode: Load,
) -> ExitCode {
let apps: Vec<DeclaredApp> = declared
.iter()
.filter(|app| !app.declared.is_empty())
.cloned()
.collect();
if apps.is_empty() {
return ExitCode::Success;
}
let report = match client
.request_with_deadline(Request::ApplyConfig { apps, reset }, Some(START_DEADLINE))
.await
{
Ok(Response::Applied(report)) => report,
Ok(_other) => {
let message = "the daemon answered the config load with a response this client does \
not understand; the Flockfile's edits are not in effect";
return streams.fail(ExitCode::Internal, message);
}
Err(err) => {
let message = format!(
"the Flockfile's edits could not be applied, so they are not in effect: {err}"
);
return streams.fail(ExitCode::from(&err), &message);
}
};
let mut failure: Option<ExitCode> = None;
for sheep in report {
let Some(message) = applied_line(&sheep) else {
continue;
};
if sheep.refused.is_some() {
failure = failure.or(Some(streams.fail(REFUSED_EXIT, &message)));
} else {
streams.aside(mode.verb(), &message);
}
}
failure.unwrap_or(ExitCode::Success)
}
const REFUSED_EXIT: ExitCode = ExitCode::InvalidConfig;
pub(crate) fn first_failure(earlier: ExitCode, later: ExitCode) -> ExitCode {
if earlier == ExitCode::Success {
later
} else {
earlier
}
}
pub(crate) fn applied_line(sheep: &SheepApplied) -> Option<String> {
let name = &sheep.name;
let mut parts = Vec::new();
if !sheep.applied.is_empty() {
parts.push(format!("applied {}", sheep.applied.join(", ")));
}
if !sheep.pending.is_empty() {
parts.push(format!(
"{} waiting on the next spawn (`shep reload {name}` promotes them)",
sheep.pending.join(", ")
));
}
if let Some(refused) = &sheep.refused {
parts.push(refused.clone());
}
if parts.is_empty() {
return None;
}
Some(format!("{name}: {}", parts.join("; ")))
}
pub(crate) fn mapped_interpreter(
script: &str,
interpreters: &BTreeMap<String, String>,
) -> Option<String> {
let extension = Path::new(script).extension()?.to_str()?;
interpreters.get(extension).cloned()
}
pub(crate) fn apply_interpreters(
apps: &mut [DeclaredApp],
interpreters: &BTreeMap<String, String>,
flag: Option<&str>,
) {
if !interpreters.is_empty() {
for app in apps.iter_mut() {
if app.config.interpreter.is_none()
&& let Some(mapped) = mapped_interpreter(&app.config.script, interpreters)
{
app.config.interpreter = Some(mapped);
}
}
}
if let Some(interpreter) = flag {
for app in apps.iter_mut() {
app.config.interpreter = Some(interpreter.to_string());
}
}
}
pub(crate) fn reset_flag(args: &StartArgs) -> Option<String> {
args.reset.map(|mode| {
let name = mode
.to_possible_value()
.expect("every ResetMode variant has a possible value")
.get_name()
.to_string();
format!("--reset={name}")
})
}
pub(crate) fn reset_depth(args: &StartArgs) -> ResetDepth {
args.reset.map_or(ResetDepth::None, ResetMode::to_depth)
}