#[cfg(all(test, unix))]
use std::path::PathBuf;
use std::sync::{Arc, mpsc};
use shep_core::paths::ShepPaths;
use super::*;
#[cfg(all(test, unix))]
use crate::commands::dogs::stub_reach;
use crate::commands::dogs::{Job, Progress, Prompt, Reach, Step, run};
use crate::exit::ExitCode;
mod actions;
#[cfg(test)]
mod tests;
const DONE_SHOWS: Duration = Duration::from_secs(6);
const END_BUDGET: Duration = Duration::from_secs(3);
#[derive(Debug)]
pub(crate) struct GrantReply(mpsc::Sender<bool>);
impl GrantReply {
fn answer(&self, yes: bool) {
let _ = self.0.send(yes);
}
}
#[derive(Debug)]
pub(crate) struct DogRun {
pub(crate) msgs: tokio::sync::mpsc::UnboundedReceiver<Msg>,
cancel: Option<Cancel>,
thread: Option<std::thread::JoinHandle<()>>,
ended: mpsc::Receiver<()>,
}
impl DogRun {
pub(crate) fn end(&mut self) -> bool {
self.cancel = None;
let ended = match self.ended.recv_timeout(END_BUDGET) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => true,
Err(mpsc::RecvTimeoutError::Timeout) => false,
};
if ended && let Some(thread) = self.thread.take() {
let _ = thread.join();
}
ended
}
}
#[derive(Debug)]
struct Cancel(
#[expect(
dead_code,
reason = "the run races its receiver against this sender; dropping the sender ends the run"
)]
tokio::sync::oneshot::Sender<()>,
);
#[derive(Debug)]
pub(crate) enum DogRunner {
Real,
#[cfg(all(test, unix))]
Stub(PathBuf),
}
impl DogRunner {
pub(crate) fn start(&self, paths: &ShepPaths, job: Job) -> DogRun {
let (sink, msgs) = tokio::sync::mpsc::unbounded_channel();
let (cancel, cancelled) = tokio::sync::oneshot::channel();
let (ended_tx, ended) = mpsc::channel();
let paths = paths.clone();
let reach = match self {
Self::Real => Reach::real(),
#[cfg(all(test, unix))]
Self::Stub(cargo) => stub_reach(cargo),
};
let thread = std::thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
runtime.block_on(async move {
let name = job.name().to_owned();
let progress = {
let (sink, name) = (sink.clone(), name.clone());
Progress::new(move |step| {
let _ = sink.send(Msg::DogStep {
name: name.clone(),
step,
});
})
};
let answer = {
let (sink, name) = (sink.clone(), name.clone());
move |prompt: &Prompt| {
let (reply, waiting) = mpsc::channel();
if sink
.send(Msg::DogGrant {
name: name.clone(),
prompt: prompt.clone(),
reply: Arc::new(GrantReply(reply)),
})
.is_err()
{
return false;
}
waiting.recv().unwrap_or(false)
}
};
let outcome = tokio::select! {
biased;
_ = cancelled => None,
got = run(&paths, job, reach, progress, answer) => Some(got),
};
if let Some((code, output)) = outcome {
let _ = sink.send(Msg::DogJobDone {
name,
ok: code == ExitCode::Success,
output,
});
}
});
let _ = ended_tx.send(());
});
DogRun {
msgs,
cancel: Some(Cancel(cancel)),
thread: Some(thread),
ended,
}
}
}
#[derive(Debug)]
pub(crate) enum DogJob {
Idle,
Running {
job: Job,
step: Option<Step>,
started: Instant,
elapsed: Duration,
grants: HashMap<u64, Arc<GrantReply>>,
},
Done {
verb: &'static str,
name: String,
at: Instant,
},
}
impl DogJob {
pub(crate) fn line(&self) -> Option<String> {
match self {
Self::Idle => None,
Self::Running {
job, step, elapsed, ..
} => Some(format!(
"{} {} {} {}s",
job_word(job),
job.name(),
step_word(*step),
elapsed.as_secs()
)),
Self::Done { verb, name, .. } => Some(format!("{verb} {name}")),
}
}
pub(super) fn tick(&mut self, now: Instant) {
match self {
Self::Idle => {}
Self::Running {
started, elapsed, ..
} => {
*elapsed = now.saturating_duration_since(*started);
}
Self::Done { at, .. } if now.saturating_duration_since(*at) >= DONE_SHOWS => {
*self = Self::Idle;
}
Self::Done { .. } => {}
}
}
fn grant(&mut self, tag: u64) -> Option<Arc<GrantReply>> {
let Self::Running { grants, .. } = self else {
return None;
};
grants.remove(&tag)
}
fn hold(&mut self, tag: u64, reply: Arc<GrantReply>) {
if let Self::Running { grants, .. } = self {
grants.insert(tag, reply);
}
}
fn holds(&self, tag: u64) -> bool {
matches!(self, Self::Running { grants, .. } if grants.contains_key(&tag))
}
fn release_grants(&mut self) {
if let Self::Running { grants, .. } = self {
grants.clear();
}
}
}
fn job_word(job: &Job) -> &'static str {
match job {
Job::Install { .. } => "installing",
Job::Upgrade { .. } => "upgrading",
Job::Delete { .. } => "deleting",
}
}
fn done_word(job: &Job) -> &'static str {
match job {
Job::Install { .. } => "installed",
Job::Upgrade { .. } => "upgraded",
Job::Delete { .. } => "deleted",
}
}
fn step_word(step: Option<Step>) -> &'static str {
match step {
None => "starting",
Some(Step::Downloading) => "downloading",
Some(Step::Building) => "building with cargo",
Some(Step::Adopting) => "enabling",
}
}
impl App {
fn start_job(&mut self, job: Job) -> Effect {
let Some(authority) = WriteAuthority::granted(self) else {
self.notice = Some(Notice::grave(READ_ONLY_REFUSAL.to_string()));
return Effect::None;
};
if let DogJob::Running { job: running, .. } = &self.dog_job {
let (name, doing, tag) = (
running.name().to_owned(),
job_word(running),
self.mint_tag(),
);
self.open_modal(Modal::output(
tag,
job_word(&job),
&format!("{name} is still {doing}. This one waits for it."),
));
return Effect::None;
}
self.dog_job = DogJob::Running {
job: job.clone(),
step: None,
started: self.now,
elapsed: Duration::ZERO,
grants: HashMap::new(),
};
Effect::StartDogJob { job, authority }
}
pub(super) fn on_dog_step(&mut self, name: &str, step: Step) -> Effect {
if let DogJob::Running {
job,
step: at,
grants,
..
} = &mut self.dog_job
&& job.name() == name
{
*at = Some(step);
grants.clear();
}
Effect::None
}
pub(super) fn on_dog_grant(
&mut self,
name: &str,
prompt: Prompt,
reply: Arc<GrantReply>,
) -> Effect {
let DogJob::Running { job, .. } = &self.dog_job else {
return Effect::None;
};
if job.name() != name {
return Effect::None;
}
let tag = self.mint_tag();
self.dog_job.hold(tag, reply);
self.open_modal(Modal::grant(tag, prompt));
Effect::None
}
pub(super) fn on_dog_job_done(&mut self, name: &str, ok: bool, output: String) -> Effect {
let DogJob::Running { job, .. } = &self.dog_job else {
return Effect::None;
};
if job.name() != name {
return Effect::None;
}
let (verb, done) = (job_word(job).to_owned(), done_word(job));
self.dog_job = if ok {
DogJob::Done {
verb: done,
name: name.to_owned(),
at: self.now,
}
} else {
DogJob::Idle
};
if ok {
return Effect::LoadCatalogue { fetch: false };
}
let tag = self.mint_tag();
self.open_modal(Modal::output(tag, &format!("{verb} {name}"), &output));
Effect::None
}
pub(super) fn on_dog_quit(&mut self) -> Option<Effect> {
let DogJob::Running { job, .. } = &self.dog_job else {
return None;
};
let (name, doing, tag) = (job.name().to_owned(), job_word(job), self.mint_tag());
self.dog_quit = Some(tag);
self.open_modal(Modal::confirm(
tag,
format!("quit lookout and stop {doing} {name}?"),
));
Some(Effect::None)
}
pub(super) fn on_dog_modal_answer(&mut self, tag: u64, yes: bool) -> Option<Effect> {
if self.dog_quit == Some(tag) {
self.dog_quit = None;
return Some(if yes { Effect::Quit } else { Effect::None });
}
if self.dog_job.holds(tag) {
return Some(Effect::ModalAnswered { tag, yes });
}
self.on_pane_action_answer(tag, yes)
.or_else(|| self.on_delete_answer(tag, yes))
}
pub(crate) fn release_dog_grants(&mut self) {
self.dog_job.release_grants();
}
pub(crate) fn answer_grant(&mut self, tag: u64, yes: bool) {
if let Some(reply) = self.dog_job.grant(tag) {
reply.answer(yes);
}
}
pub(crate) fn dog_job_line(&self) -> Option<String> {
self.dog_job.line()
}
pub(super) fn mint_tag(&mut self) -> u64 {
self.next_modal_tag += 1;
self.next_modal_tag
}
}