use std::io;
use std::io::BufReader;
use std::io::Read;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;
use std::process::Stdio;
use std::thread;
#[cfg(not(unix))]
use sysinfo::Pid;
#[cfg(not(unix))]
use sysinfo::ProcessRefreshKind;
#[cfg(not(unix))]
use sysinfo::ProcessesToUpdate;
#[cfg(not(unix))]
use sysinfo::Signal;
#[cfg(not(unix))]
use sysinfo::System;
use crate::channel::Sender;
use crate::ci;
use crate::constants::CARGO_COMMAND_NAME;
use crate::project::AbsolutePath;
use crate::scan;
use crate::scan::BackgroundMsg;
use crate::tui::app::App;
use crate::tui::app::PendingClean;
use crate::tui::constants::CARGO_BENCH_FLAG;
use crate::tui::constants::CARGO_BENCH_SUBCOMMAND;
use crate::tui::constants::CARGO_CLEAN_SUBCOMMAND;
use crate::tui::constants::CARGO_COLOR_ALWAYS_FLAG;
use crate::tui::constants::CARGO_EXAMPLE_FLAG;
use crate::tui::constants::CARGO_FEATURES_FLAG;
use crate::tui::constants::CARGO_PACKAGE_FLAG;
use crate::tui::constants::CARGO_RELEASE_FLAG;
use crate::tui::constants::CARGO_RUN_SUBCOMMAND;
use crate::tui::messages::CiFetchMsg;
use crate::tui::messages::CleanMsg;
use crate::tui::messages::ExampleMsg;
use crate::tui::panes::CiFetchKind;
use crate::tui::panes::PendingCiFetch;
use crate::tui::panes::PendingExampleRun;
use crate::tui::panes::RunTargetKind;
pub(super) fn spawn_example_process(app: &mut App, run: &PendingExampleRun) {
let mut cmd = Command::new(CARGO_COMMAND_NAME);
match run.run_target_kind {
RunTargetKind::Binary => {
cmd.arg(CARGO_RUN_SUBCOMMAND);
},
RunTargetKind::Example => {
cmd.arg(CARGO_RUN_SUBCOMMAND)
.arg(CARGO_EXAMPLE_FLAG)
.arg(&run.target_name);
},
RunTargetKind::Bench => {
cmd.arg(CARGO_BENCH_SUBCOMMAND)
.arg(CARGO_BENCH_FLAG)
.arg(&run.target_name);
},
}
if run.build_mode.is_release() {
cmd.arg(CARGO_RELEASE_FLAG);
}
if let Some(pkg) = &run.package_name {
cmd.arg(CARGO_PACKAGE_FLAG).arg(pkg);
}
if !run.required_features.is_empty() {
cmd.arg(CARGO_FEATURES_FLAG)
.arg(run.required_features.join(","));
}
cmd.current_dir(&run.abs_path)
.arg(CARGO_COLOR_ALWAYS_FLAG)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
isolate_example_process(&mut cmd);
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
app.inflight
.set_example_title(Some(run.display_path.clone()));
app.set_example_output(vec![format!("Failed to start: {e}")]);
app.inflight
.set_example_running(Some(run.display_path.clone()));
return;
},
};
let pid = child.id();
*app.inflight
.example_child()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pid);
let display_path = run.display_path.clone();
let target_name = run.target_name.clone();
let mode = run.build_mode.label();
app.inflight.set_example_title(Some(display_path.clone()));
app.set_example_output(vec![format!("Building {target_name}{mode}...")]);
app.inflight
.set_example_running(Some(format!("{display_path}{mode}")));
let stderr = child.stderr.take();
let stdout = child.stdout.take();
let pid_holder = app.inflight.example_child();
let example_sender = app.background.example_sender();
thread::spawn(move || {
let stderr_reader = stderr.map(|stream| {
let example_sender = example_sender.clone();
thread::spawn(move || read_with_progress(&example_sender, stream))
});
let stdout_reader = stdout.map(|stream| {
let example_sender = example_sender.clone();
thread::spawn(move || read_with_progress(&example_sender, stream))
});
let _ = child.wait();
if let Some(reader) = stderr_reader {
let _ = reader.join();
}
if let Some(reader) = stdout_reader {
let _ = reader.join();
}
*pid_holder
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
let _ = example_sender.send(ExampleMsg::Finished);
});
}
#[cfg(unix)]
fn isolate_example_process(cmd: &mut Command) { cmd.process_group(0); }
#[cfg(not(unix))]
fn isolate_example_process(_: &mut Command) {}
#[cfg(unix)]
pub(super) fn stop_example_process(pid: u32) -> bool {
signal_with_kill("-TERM", format!("-{pid}")) || signal_with_kill("-TERM", pid.to_string())
}
#[cfg(unix)]
fn signal_with_kill(signal: &str, target: String) -> bool {
Command::new("kill")
.arg(signal)
.arg(target)
.status()
.is_ok_and(|status| status.success())
}
#[cfg(not(unix))]
pub(super) fn stop_example_process(pid: u32) -> bool {
let mut system = System::new();
let pid = Pid::from_u32(pid);
system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[pid]),
true,
ProcessRefreshKind::nothing(),
);
system
.process(pid)
.is_some_and(|process| process.kill_with(Signal::Term).unwrap_or(false))
}
fn read_with_progress(example_sender: &Sender<ExampleMsg>, stream: impl io::Read) {
let mut reader = BufReader::new(stream);
let mut buf = Vec::new();
let mut byte = [0u8; 1];
while reader.read_exact(&mut byte).is_ok() {
match byte[0] {
b'\n' => {
let line = String::from_utf8_lossy(&buf).to_string();
let _ = example_sender.send(ExampleMsg::Output(line));
buf.clear();
},
b'\r' => {
if !buf.is_empty() {
let line = String::from_utf8_lossy(&buf).to_string();
let _ = example_sender.send(ExampleMsg::Progress(line));
buf.clear();
}
},
b => buf.push(b),
}
}
if !buf.is_empty() {
let line = String::from_utf8_lossy(&buf).to_string();
let _ = example_sender.send(ExampleMsg::Output(line));
}
}
pub(super) fn spawn_clean_process(app: &mut App, pending: &PendingClean) {
let mut cmd = std::process::Command::new(CARGO_COMMAND_NAME);
cmd.arg(CARGO_CLEAN_SUBCOMMAND)
.current_dir(&pending.abs_path)
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
app.clean_spawn_failed(&pending.abs_path);
app.show_timed_toast("cargo clean failed", e.to_string());
return;
},
};
let clean_sender = app.background.clean_sender();
let abs_path = pending.abs_path.clone();
thread::spawn(move || {
let _ = child.wait();
let _ = clean_sender.send(CleanMsg::Finished(abs_path));
});
}
pub(super) fn spawn_ci_fetch(app: &App, fetch: &PendingCiFetch) -> bool {
let path = Path::new(&fetch.project_path);
let Some(repo_url) = app.project_list.fetch_url_for(path) else {
return false;
};
let Some(owner_repo) = ci::parse_owner_repo(&repo_url) else {
return false;
};
let ci_fetch_sender = app.background.ci_fetch_sender();
let background_tx = app.background.background_sender();
let client = app.net.http_client();
let project_path = fetch.project_path.clone();
let ci_run_count = fetch.ci_run_count;
let oldest_created_at = fetch.oldest_created_at.clone();
let ci_fetch_kind = fetch.ci_fetch_kind;
let url = repo_url;
thread::spawn(move || {
let (result, network) = match ci_fetch_kind {
CiFetchKind::Older => {
let oldest = oldest_created_at
.as_deref()
.unwrap_or("1970-01-01T00:00:00Z");
scan::fetch_older_runs(
&client,
&url,
owner_repo.owner(),
owner_repo.repo(),
oldest,
ci_run_count,
)
},
CiFetchKind::Sync => {
let (result, _meta, signal) = scan::fetch_ci_runs_cached(
&client,
&url,
owner_repo.owner(),
owner_repo.repo(),
ci_run_count,
);
(result, signal)
},
};
scan::emit_service_signal(&background_tx, network);
let _ = ci_fetch_sender.send(CiFetchMsg::Complete {
path: project_path,
result,
kind: ci_fetch_kind,
});
});
true
}
pub(super) fn spawn_priority_fetch(app: &App, _: &str, abs_path: &str, name: Option<&String>) {
let sender = app.background.background_sender();
let client = app.net.http_client();
let abs = AbsolutePath::from(abs_path);
let project_name = name.cloned();
thread::spawn(move || {
let path: AbsolutePath = abs.clone();
scan::emit_git_info(&sender, &abs);
let bytes = scan::dir_size(&abs);
let _ = sender.send(BackgroundMsg::DiskUsage {
path: path.clone(),
bytes,
});
if let Some(name) = project_name.as_ref() {
let _ = sender.send(BackgroundMsg::CratesIoFetchQueued { name: name.clone() });
let (info, signal) = client.fetch_crates_io_info(name);
scan::emit_service_signal(&sender, signal);
if let Some(info) = info {
let _ = sender.send(BackgroundMsg::CratesIoVersion {
path,
version: info.version,
prerelease: info.prerelease,
downloads: info.downloads,
});
}
let _ = sender.send(BackgroundMsg::CratesIoFetchComplete { name: name.clone() });
}
});
}