use shep_client::{Client, RequestError};
use shep_core::paths::ShepPaths;
use shep_core::protocol::{Request, Response, RpcErrorCode, SelectorSpec};
use shep_core::selector::ProcessSelector;
use crate::commands::dogs;
use crate::commands::lifecycle::selectors::{flock_now, render_outcome};
use crate::commands::rpc::{client_error, unexpected_response};
use crate::commands::selector::parse_selector;
use crate::commands::shep_toml::ShepToml;
use crate::exit::ExitCode;
use crate::output::{FlockRows, Streams};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Toggle {
Enable,
Disable,
}
impl Toggle {
fn verb(self) -> &'static str {
match self {
Self::Enable => "enable",
Self::Disable => "disable",
}
}
fn request(self, name: &str) -> Request {
let selector = SelectorSpec::Name(name.to_string());
match self {
Self::Enable => Request::Enable { selector },
Self::Disable => Request::Disable { selector },
}
}
}
pub(crate) async fn toggle(
streams: &mut Streams<'_>,
paths: &ShepPaths,
verb: Toggle,
name: &str,
) -> ExitCode {
if names_a_dog(paths, name) {
return match verb {
Toggle::Enable => dogs::enable(streams, paths, name).await,
Toggle::Disable => dogs::disable(streams, paths, name).await,
};
}
let client = match dogs::connect_or_absent(paths, streams).await {
Ok(Some(client)) => client,
Ok(None) => {
let message = format!(
"no shepherd is running; `shep {} {name}` needs one to mark a sheep. \
start one with `shep start <target>`",
verb.verb()
);
return streams.fail(ExitCode::DaemonUnreachable, &message);
}
Err(code) => return code,
};
toggle_sheep(&client, streams, verb, name).await
}
fn names_a_dog(paths: &ShepPaths, name: &str) -> bool {
if crate::dog::BUILT_IN_DOGS.contains(&name) {
return true;
}
ShepToml::read_only(&paths.daemon_config).is_ok_and(|cfg| {
cfg.adopted_dog_names().iter().any(|dog| dog == name)
|| cfg.enabled_dog_names().iter().any(|dog| dog == name)
})
}
pub(crate) async fn toggle_sheep(
client: &Client,
streams: &mut Streams<'_>,
verb: Toggle,
name: &str,
) -> ExitCode {
let selector = match parse_selector(streams, name) {
Ok(selector) => selector,
Err(code) => return code,
};
let app = match selector {
ProcessSelector::Name(app) => app,
ProcessSelector::Instance { name: app, .. } => {
return refuse_instance(streams, verb, name, &app);
}
ProcessSelector::Id(id) => {
let flock = flock_now(client).await;
return match flock.iter().find(|info| info.id == id) {
Some(info) => refuse_instance(streams, verb, name, &info.name),
None => streams.fail(ExitCode::NotFound, &format!("no sheep has id {id}")),
};
}
_ => {
let message = format!(
"`shep {} {name}` is not supported yet; name one sheep, such as `shep {} api`",
verb.verb(),
verb.verb()
);
return streams.fail(ExitCode::Usage, &message);
}
};
let response = match client.request(verb.request(&app)).await {
Ok(response) => response,
Err(RequestError::Rpc(err)) if err.code == RpcErrorCode::Unsupported => {
let message = format!(
"this shepherd is too old to {} a sheep; upgrade it with `shep {}`",
verb.verb(),
crate::version_guard::VERSION_SKEW_REMEDY
);
return streams.fail(ExitCode::Unsupported, &message);
}
Err(err) => return client_error(streams, &err),
};
match (verb, response) {
(Toggle::Disable, Response::Disabled(procs)) => {
render_outcome(client, streams, "disable", FlockRows(procs)).await
}
(Toggle::Enable, Response::Enabled(procs)) if procs.is_empty() => {
streams.note(
"not_disabled",
&format!("`{app}` is not disabled; nothing changed"),
);
ExitCode::Success
}
(Toggle::Enable, Response::Enabled(procs)) => {
render_outcome(client, streams, "enable", FlockRows(procs)).await
}
_ => unexpected_response(streams),
}
}
fn refuse_instance(streams: &mut Streams<'_>, verb: Toggle, given: &str, app: &str) -> ExitCode {
let message = format!(
"`{given}` is one instance of `{app}`, and `{}` acts on a whole app; \
run `shep {} {app}`",
verb.verb(),
verb.verb()
);
streams.fail(ExitCode::Usage, &message)
}