use std::collections::{BTreeMap, BTreeSet};
use std::io::Read as _;
use std::path::Path;
use shep_client::{Client, START_DEADLINE};
use shep_core::config::{AppConfig, DeclaredApp, ResetDepth};
use shep_core::protocol::{EnvValue, ProcessInfo, Request, Response, SelectorSpec};
use crate::cli::StartArgs;
use crate::commands::lifecycle::configure::{
apply_declared, apply_interpreters, first_failure, reset_depth, reset_flag,
};
use crate::commands::lifecycle::resolve::{
TargetError, resolve_target_declared, split_assignments,
};
use crate::commands::lifecycle::respawn::{already_registered, resume_all};
use crate::commands::lifecycle::selectors::{
fail_target, flock_matches, flock_now, is_reachable_as_a_name, render_outcome, request_each,
selector_miss,
};
use crate::commands::lifecycle::staged_start_deadline;
use crate::commands::selector::parse_selector;
use crate::exit::ExitCode;
use crate::output::{FlockRows, Streams};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Load {
Start,
Add,
}
impl Load {
pub(crate) fn verb(self) -> &'static str {
match self {
Self::Start => "start",
Self::Add => "add",
}
}
}
pub async fn start(
client: &Client,
streams: &mut Streams<'_>,
args: &StartArgs,
discovered: Option<&Path>,
interpreters: &BTreeMap<String, String>,
) -> ExitCode {
load(client, streams, args, discovered, interpreters, Load::Start).await
}
pub async fn add(
client: &Client,
streams: &mut Streams<'_>,
args: &StartArgs,
discovered: Option<&Path>,
interpreters: &BTreeMap<String, String>,
) -> ExitCode {
load(client, streams, args, discovered, interpreters, Load::Add).await
}
async fn load(
client: &Client,
streams: &mut Streams<'_>,
args: &StartArgs,
discovered: Option<&Path>,
interpreters: &BTreeMap<String, String>,
mode: Load,
) -> ExitCode {
let (assignments, targets) = split_assignments(&args.targets);
if !assignments.is_empty() && targets.len() > 1 {
let message = "an assignment takes one target: an environment belongs to one sheep";
return streams.fail(ExitCode::Usage, message);
}
if !assignments.is_empty() && targets.is_empty() {
let quoted = args
.targets
.iter()
.find(|word| word.split_whitespace().count() > 1);
let message = match quoted {
Some(word) => format!(
"an assignment needs a target; `{word}` is one word, so drop the quotes and \
let the shell split it"
),
None => {
"an assignment needs a target: a script path, or a sheep the flock has".to_string()
}
};
return streams.fail(ExitCode::Usage, &message);
}
if args.name.is_some() && targets.len() > 1 {
let message = "--name takes one target: a name belongs to one sheep";
return streams.fail(ExitCode::Usage, message);
}
if targets.is_empty() {
let mut started = Vec::new();
let code = load_one(
client,
streams,
args,
None,
discovered,
interpreters,
&mut started,
mode,
&assignments,
)
.await;
if code == ExitCode::Success {
let wrote = render_outcome(client, streams, mode.verb(), FlockRows(started)).await;
if wrote != ExitCode::Success {
return wrote;
}
}
return code;
}
let mut failure: Option<ExitCode> = None;
let mut started = Vec::new();
for target in targets {
let code = load_one(
client,
streams,
args,
Some(target),
discovered,
interpreters,
&mut started,
mode,
&assignments,
)
.await;
if code != ExitCode::Success {
failure = failure.or(Some(code));
}
}
if failure.is_none() {
let wrote = render_outcome(client, streams, mode.verb(), FlockRows(started)).await;
if wrote != ExitCode::Success {
return wrote;
}
}
failure.unwrap_or(ExitCode::Success)
}
async fn set_assignments(
client: &Client,
name: &str,
assignments: &BTreeMap<String, String>,
) -> Result<(), String> {
for (key, value) in assignments {
let body = Request::SetSheepEnv {
name: name.to_string(),
key: key.clone(),
value: Some(EnvValue::from(value.clone())),
};
match client.request(body).await {
Ok(Response::SheepEnvSet { .. }) => {}
Ok(_unrecognised) => {
return Err(format!(
"{name}: the shepherd answered {key} with something this client does not \
understand"
));
}
Err(err) => return Err(format!("{name}: {key} was not recorded ({err})")),
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn load_one(
client: &Client,
streams: &mut Streams<'_>,
args: &StartArgs,
target: Option<&str>,
discovered: Option<&Path>,
interpreters: &BTreeMap<String, String>,
started: &mut Vec<shep_core::protocol::ProcessInfo>,
mode: Load,
assignments: &BTreeMap<String, String>,
) -> ExitCode {
let mut listing: Option<Vec<ProcessInfo>> = None;
let mut missed: Option<String> = None;
if let Some(token) = target
&& token != "-"
&& !args.flockfile
{
let selector = match parse_selector(streams, token) {
Ok(selector) => selector,
Err(code) => return code,
};
if is_reachable_as_a_name(&selector) {
let flock = flock_now(client).await;
let matched = flock_matches(&selector, &flock);
if !matched.is_empty() {
if let Some(flag) = reset_flag(args) {
let message = format!(
"{flag} needs a Flockfile to reset to; {token} names a sheep, not a file"
);
return streams.fail(ExitCode::Usage, &message);
}
if !assignments.is_empty() {
let names: BTreeSet<&str> =
matched.iter().map(|info| info.name.as_str()).collect();
if names.len() > 1 {
let message = format!(
"an assignment takes a script path or one sheep; {token} names several"
);
return streams.fail(ExitCode::Usage, &message);
}
for name in &names {
if let Err(reason) = set_assignments(client, name, assignments).await {
return streams.fail(ExitCode::Failure, &reason);
}
}
}
return match mode {
Load::Start => {
resume_all(client, streams, Some(token), &matched, started).await
}
Load::Add => already_registered(streams, &matched),
};
}
missed = selector_miss(token, &selector, &flock);
listing = Some(flock);
}
}
let discovered = discovered.map(|p| p.to_string_lossy().into_owned());
let target: &str = match (target, discovered.as_deref()) {
(Some(target), _) => target,
(None, Some(found)) => found,
(None, None) => {
let message = "no target and no Flockfile in this directory";
return streams.fail(ExitCode::Usage, message);
}
};
let stdin = if target == "-" {
let mut buf = Vec::new();
if let Err(source) = std::io::stdin().lock().read_to_end(&mut buf) {
return fail_target(streams, &TargetError::Stdin(source));
}
buf
} else {
Vec::new()
};
let mut apps =
match resolve_target_declared(target, args.name.as_deref(), &stdin, args.flockfile) {
Ok(apps) => apps,
Err(TargetError::Unresolvable { .. }) if missed.is_some() => {
let message = missed.unwrap_or_default();
return streams.fail(ExitCode::NotFound, &message);
}
Err(err) => return fail_target(streams, &err),
};
if let Some(flag) = reset_flag(args)
&& apps.iter().all(|app| app.declared.is_empty())
{
let message =
format!("{flag} needs a Flockfile to reset to; {target} is a script, not a Flockfile");
return streams.fail(ExitCode::Usage, &message);
}
if !assignments.is_empty() && apps.iter().any(|app| !app.declared.is_empty()) {
let message = format!(
"an assignment takes a script path or one sheep; {target} is a Flockfile, which \
may declare several"
);
return streams.fail(ExitCode::Usage, &message);
}
if let Some(fold) = &args.fold {
for app in &mut apps {
app.config.fold = Some(fold.clone());
}
}
if let Some(cwd) = &args.cwd {
for app in &mut apps {
app.config.cwd = Some(cwd.clone());
}
}
if args.no_log_timestamps {
for app in &mut apps {
app.config.log_timestamps = false;
}
}
for app in &mut apps {
for (key, value) in assignments {
app.config.env.insert(key.clone(), value.clone());
}
}
apply_interpreters(&mut apps, interpreters, args.interpreter.as_deref());
let flock = match listing {
Some(flock) => flock,
None => flock_now(client).await,
};
let mut resumed: Vec<(DeclaredApp, Vec<ProcessInfo>)> = Vec::new();
let mut fresh = Vec::new();
for app in apps {
let rows: Vec<ProcessInfo> = flock
.iter()
.filter(|info| info.name == app.config.name)
.cloned()
.collect();
if rows.is_empty() {
fresh.push(app);
} else {
resumed.push((app, rows));
}
}
let declared: Vec<DeclaredApp> = resumed.iter().map(|(app, _)| app.clone()).collect();
let applied = apply_declared(client, streams, &declared, reset_depth(args), mode).await;
if !resumed.is_empty() && mode == Load::Start {
let existing: Vec<ProcessInfo> = resumed
.iter()
.flat_map(|(_, rows)| rows.iter().cloned())
.collect();
let code = resume_all(client, streams, None, &existing, started).await;
if code != ExitCode::Success {
return first_failure(applied, code);
}
}
if fresh.is_empty() {
return applied;
}
let apps: Vec<AppConfig> = fresh.iter().map(|app| app.config.clone()).collect();
let (procs, failure) = request_each(
client,
streams, &[SelectorSpec::All],
Some(match mode {
Load::Add => START_DEADLINE,
Load::Start => staged_start_deadline(&apps),
}),
|_| match mode {
Load::Start => Request::Start { apps: apps.clone() },
Load::Add => Request::Add { apps: apps.clone() },
},
|response| match response {
Response::Started(procs) | Response::Added(procs) => Some(procs),
_ => None,
},
)
.await;
let registered: BTreeSet<&str> = procs.iter().map(|info| info.name.as_str()).collect();
let established: Vec<DeclaredApp> = fresh
.into_iter()
.filter(|app| registered.contains(app.config.name.as_str()))
.collect();
let recorded = apply_declared(client, streams, &established, ResetDepth::None, mode).await;
for name in ®istered {
if let Err(reason) = set_assignments(client, name, assignments).await {
let message = format!(
"{reason}; it is set for this spawn, but a Flockfile load could replace it"
);
streams.aside(mode.verb(), &message);
}
}
started.extend(procs);
first_failure(
applied,
first_failure(failure.unwrap_or(ExitCode::Success), recorded),
)
}