use std::time::Duration;
use qframe::runtime::{Keep, Process, ProcessOutcome};
use super::Outcome;
use super::workspace::Workspace;
pub(super) const LIMIT: Duration = Duration::from_secs(120);
pub(super) const MAX_OUTPUT_BYTES: usize = 30 * 1024;
pub(super) const MAX_OUTPUT_LINES: usize = 400;
const GRACE: Duration = Duration::from_secs(2);
pub(super) fn run(workspace: &Workspace, command: &str, cancelled: &dyn Fn() -> bool, limit: Duration) -> Outcome {
run_watching(workspace, command, cancelled, limit, &mut |_| {})
}
pub(super) fn run_watching(
workspace: &Workspace,
command: &str,
cancelled: &dyn Fn() -> bool,
limit: Duration,
on_tail: &mut dyn FnMut(&str),
) -> Outcome {
if command.trim().is_empty() {
return Outcome::failed("`run_command` needs a non-empty `command`.".to_owned());
}
let keep = Keep::bytes(MAX_OUTPUT_BYTES).lines(MAX_OUTPUT_LINES).limit(limit).after_exit(GRACE);
let collected = Process::new("sh").arg("-c").arg(command).dir(workspace.root()).no_stdin().collect_watching(
keep,
cancelled,
|tail| on_tail(tail),
);
let collected = match collected {
Ok(collected) => collected,
Err(error) => return Outcome::failed(format!("The command could not be started: {error}.")),
};
let output = shown(&collected.text, collected.trimmed);
if collected.cancelled {
return Outcome::done(format!("{output}\nStopped: the person cancelled the command."));
}
if collected.timed_out {
return Outcome::done(format!("{output}\nStopped: the command ran longer than {} seconds.", limit.as_secs()));
}
let ending = match collected.outcome {
ProcessOutcome::Finished { code: Some(code) } => format!("Exit code: {code}."),
ProcessOutcome::Finished { code: None } => "Ended by a signal.".to_owned(),
ProcessOutcome::Cancelled => "Stopped: the person cancelled the command.".to_owned(),
};
Outcome::done(format!("{output}\n{ending}"))
}
pub(super) fn shown(text: &str, trimmed: bool) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return "(no output)".to_owned();
}
let shown = lines.join("\n");
if trimmed { format!("(Output cut: only the last {} lines are shown.)\n{shown}", lines.len()) } else { shown }
}