use std::ffi::OsString;
use std::io::{BufRead, BufReader, Read};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use crate::args;
use crate::base_fix::BaseFixAsk;
use crate::command_log;
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::progress;
use crate::run_ending;
const POLL: Duration = Duration::from_millis(100);
#[derive(Debug, PartialEq, Eq)]
pub enum Kind {
Ticket { spec_branch: String },
BaseFix { base: String },
}
impl Kind {
pub fn base(&self) -> &str {
match self {
Kind::Ticket { spec_branch } => spec_branch,
Kind::BaseFix { base } => base,
}
}
fn hidden_argument(&self) -> &'static str {
match self {
Kind::Ticket { .. } => args::SPEC_BRANCH,
Kind::BaseFix { .. } => args::BASE_FIX_INTO,
}
}
}
pub enum Ended {
Reached {
pr_url: Option<String>,
base_fix: Option<String>,
},
Failed { cause: String, log: Option<String> },
Interrupted,
}
pub fn start(issue: &IssueUrl, kind: &Kind, base_fix: &BaseFixAsk) -> Result<Child> {
start_from(&own_executable()?, issue, kind, base_fix)
}
fn own_executable() -> Result<PathBuf> {
if cfg!(target_os = "linux") {
Ok(PathBuf::from("/proc/self/exe"))
} else {
std::env::current_exe().context("no thirdshift executable")
}
}
fn start_from(
executable: &Path,
issue: &IssueUrl,
kind: &Kind,
base_fix: &BaseFixAsk,
) -> Result<Child> {
let base_fix = match base_fix {
BaseFixAsk::Allow => vec![args::BASE_FIX],
BaseFixAsk::Forbid => Vec::new(),
BaseFixAsk::Undecided { retry } => vec![args::OFFER_BASE_FIX, retry],
};
let mut command = Command::new(executable);
if cfg!(target_os = "linux")
&& let Some(own) = own_command()
{
command.arg0(own);
}
command
.args([kind.hidden_argument(), kind.base()])
.args([args::STAMP, command_log::stamp()])
.args(base_fix)
.arg(&issue.url)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| {
let leads_to = match std::fs::read_link(executable) {
Ok(target) => format!(" ({})", target.display()),
Err(_) => String::new(),
};
format!(
"could not start the Run for #{} from {}{leads_to}",
issue.number,
executable.display()
)
})
}
fn own_command() -> Option<OsString> {
std::env::args_os().next()
}
pub fn name_this_process() {
#[cfg(target_os = "linux")]
if let Some(own) = own_command()
&& let Some(name) = Path::new(&own).file_name()
&& let Ok(name) = std::ffi::CString::new(name.as_encoded_bytes())
{
unsafe { libc::prctl(libc::PR_SET_NAME, name.as_ptr()) };
}
}
pub fn wait(number: u64, mut child: Child) -> Result<Ended> {
let stderr = child.stderr.take().context("no stderr from the Run")?;
let relay = thread::spawn(move || -> std::io::Result<_> {
let mut reader = run_ending::Reader::default();
for line in BufReader::new(stderr).lines() {
reader.read(progress::relay(number, &line?));
}
Ok(reader)
});
let mut passed_on = false;
let status = loop {
if !passed_on && interrupt::requested() {
unsafe { libc::kill(child.id() as libc::pid_t, libc::SIGTERM) };
passed_on = true;
}
if let Some(status) = child
.try_wait()
.with_context(|| format!("could not wait for the Run for #{number}"))?
{
break status;
}
thread::sleep(POLL);
};
let reader = relay
.join()
.map_err(|_| anyhow!("the relay of the Run for #{number} panicked"))?
.with_context(|| format!("could not read the Run for #{number}"))?;
let mut stdout = String::new();
child
.stdout
.take()
.context("no stdout from the Run")?
.read_to_string(&mut stdout)
.with_context(|| format!("could not read the Run for #{number}"))?;
let ending = reader.finish(&stdout, status.success());
Ok(match ending.outcome {
Ok(pr_url) => Ended::Reached {
pr_url,
base_fix: ending.base_fix,
},
Err(_) if interrupt::requested() => Ended::Interrupted,
Err(failure) => Ended::Failed {
cause: failure.cause.unwrap_or_else(|| format!("the Run {status}")),
log: failure.log,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
fn cause_of_not_starting_from(executable: &Path) -> String {
let issue = IssueUrl::parse("https://github.com/acme/widgets/issues/248").unwrap();
let kind = Kind::Ticket {
spec_branch: "issue-237".to_string(),
};
let error = start_from(executable, &issue, &kind, &BaseFixAsk::Forbid).unwrap_err();
format!("{error:#}")
}
#[test]
fn a_child_run_that_cannot_be_started_names_the_executable_that_was_tried() {
let cause = cause_of_not_starting_from(Path::new("/no/such/thirdshift"));
assert!(
cause.starts_with("could not start the Run for #248 from /no/such/thirdshift: "),
"{cause}"
);
}
#[test]
fn a_child_run_that_cannot_be_started_from_a_link_names_the_file_it_leads_to() {
let dir = tempfile::tempdir().unwrap();
let link = dir.path().join("exe");
std::os::unix::fs::symlink("/no/such/thirdshift (deleted)", &link).unwrap();
let cause = cause_of_not_starting_from(&link);
assert!(
cause.starts_with(&format!(
"could not start the Run for #248 from {} (/no/such/thirdshift (deleted)): ",
link.display()
)),
"{cause}"
);
}
}