use cli::{DaemonArgs, Format, GlobalArgs, StartArgs};
use commands::daemon::{daemon_exit_code, run_daemon};
use commands::lifecycle::{self, Load};
use exit::ExitCode;
use launch::launch_daemon;
use output::Streams;
use shep_client::Client;
use shep_client::spawn::{SpawnOutcome, connect_or_spawn};
use shep_core::paths::ShepPaths;
use crate::config::interpreters_from_config;
use crate::home::{report_home_refusal, resolve_paths};
use crate::version_guard::{VersionGuard, refuse_version_skew};
use crate::{cli, commands, exit, launch, output, status};
pub(crate) fn emit_error_locked(fmt: Format, code: ExitCode, message: &str) {
let mut err = std::io::stderr().lock();
let _ = output::emit_error(&mut err, fmt, code.code_str(), message);
}
pub(crate) async fn connect_or_spawn_client(
streams: &mut Streams<'_>,
paths: &ShepPaths,
guard: VersionGuard,
) -> Result<Client, ExitCode> {
let launch_paths = paths.clone();
match connect_or_spawn(&paths.socket, move || launch_daemon(&launch_paths)).await {
Ok(SpawnOutcome::Connected(client) | SpawnOutcome::Spawned(client)) => {
refuse_version_skew(streams, &client, guard)?;
Ok(client)
}
Err(err) => {
let code = ExitCode::from(&err);
Err(streams.fail(code, &err.to_string()))
}
}
}
pub(crate) async fn load_command(
streams: &mut Streams<'_>,
paths: &ShepPaths,
guard: VersionGuard,
args: &StartArgs,
mode: Load,
) -> ExitCode {
let discovered = if args.targets.is_empty() {
std::env::current_dir()
.ok()
.and_then(|cwd| shep_core::config::flockfile::discover(&cwd))
} else {
None
};
if args.targets.is_empty() && discovered.is_none() {
return match mode {
Load::Start => start_bare_shepherd(streams, paths, guard).await,
Load::Add => streams.fail(
ExitCode::Usage,
"no target and no Flockfile in this directory",
),
};
}
let shep_toml_text = std::fs::read_to_string(&paths.daemon_config).ok();
let interpreters = interpreters_from_config(shep_toml_text.as_deref());
let client = match connect_or_spawn_client(streams, paths, guard).await {
Ok(client) => client,
Err(code) => return code,
};
match mode {
Load::Start => {
lifecycle::start(&client, streams, args, discovered.as_deref(), &interpreters).await
}
Load::Add => {
lifecycle::add(&client, streams, args, discovered.as_deref(), &interpreters).await
}
}
}
pub(crate) async fn start_bare_shepherd(
streams: &mut Streams<'_>,
paths: &ShepPaths,
guard: VersionGuard,
) -> ExitCode {
let before = status::ShepherdStatus::probe(paths).await;
if let Some(online) = &before.online {
let message = format!(
"shepherd already up (pid {}). `shep start <target>` adds a sheep.",
online.pid
);
streams.aside("start", &message);
return ExitCode::Success;
}
match connect_or_spawn_client(streams, paths, guard).await {
Ok(client) => {
let restored = client
.request(shep_core::protocol::Request::ListFlock)
.await;
let known = match &restored {
Ok(shep_core::protocol::Response::Flock(procs)) => procs.len(),
_ => 0,
};
let message = if known == 0 {
format!(
"shepherd up, flock at {}. Nothing running yet; \
`shep start <target>` adds a sheep.",
paths.home.display()
)
} else {
format!(
"shepherd up, flock at {}. {known} sheep restored from the roll; \
`shep flock` lists them.",
paths.home.display()
)
};
streams.note("start", &message);
ExitCode::Success
}
Err(code) => code,
}
}
pub(crate) fn unreachable_message(err: &shep_client::ConnectError) -> String {
match err {
shep_client::ConnectError::Connect { path, source }
if source.kind() == std::io::ErrorKind::NotFound =>
{
format!(
"no shepherd is running (no socket at `{}`); \
start one with `shep start <target>`",
path.display()
)
}
other => other.to_string(),
}
}
pub(crate) async fn run_daemon_command(
fmt: Format,
global: &GlobalArgs,
args: &DaemonArgs,
) -> ExitCode {
let paths = match resolve_paths(global) {
Ok(paths) => paths,
Err(refusal) => return report_home_refusal(fmt, &refusal),
};
match run_daemon(paths, args).await {
Ok(()) => ExitCode::Success,
Err(err) => {
let code = daemon_exit_code(&err);
emit_error_locked(fmt, code, &err.to_string());
code
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn an_absent_socket_names_the_next_command_and_other_failures_do_not() {
use std::io::{Error, ErrorKind};
let absent = shep_client::ConnectError::Connect {
path: std::path::PathBuf::from("/root/.shep/run/shep.sock"),
source: Error::from(ErrorKind::NotFound),
};
assert_eq!(
unreachable_message(&absent),
"no shepherd is running (no socket at `/root/.shep/run/shep.sock`); \
start one with `shep start <target>`"
);
let denied = shep_client::ConnectError::Connect {
path: std::path::PathBuf::from("/root/.shep/run/shep.sock"),
source: Error::from(ErrorKind::PermissionDenied),
};
let text = unreachable_message(&denied);
assert!(
text.starts_with("could not connect to `/root/.shep/run/shep.sock`:"),
"a permission failure must keep the library's wording, got {text:?}"
);
assert!(
!text.contains("shep start"),
"a permission failure must not send the operator to `shep start`, got {text:?}"
);
}
}