use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::Notify;
use tokio::task::AbortHandle;
use crate::buffer::LineTerminator;
use crate::group::ProcessGroup;
use crate::result::Outcome;
use crate::sys::pid_gate::PidGate;
use super::{Backend, OutputReader, RunningProcess, TS_PENDING};
#[derive(Debug, Clone, Copy)]
pub(crate) struct ScriptedResultInfo {
pub(crate) truncated: bool,
pub(crate) total_lines: usize,
pub(crate) total_bytes: usize,
pub(crate) duration: Duration,
}
pub(crate) fn split_pump_lines(text: &str, terminator: LineTerminator) -> Vec<String> {
match terminator {
LineTerminator::Newline => text.lines().map(str::to_owned).collect(),
LineTerminator::CarriageReturn => {
let mut lines = Vec::new();
let bytes = text.as_bytes();
let (mut start, mut i) = (0usize, 0usize);
while i < bytes.len() {
match bytes[i] {
b'\n' => {
lines.push(text[start..i].to_owned());
i += 1;
}
b'\r' => {
lines.push(text[start..i].to_owned());
i += if bytes.get(i + 1) == Some(&b'\n') {
2
} else {
1
};
}
_ => {
i += 1;
continue;
}
}
start = i;
}
if start < bytes.len() {
lines.push(text[start..].to_owned());
}
lines
}
}
}
#[derive(Clone)]
pub(super) struct ScriptedKill {
killed: Arc<AtomicBool>,
signal: Arc<Notify>,
feeders: Arc<Vec<AbortHandle>>,
}
impl ScriptedKill {
fn fire(&self) {
self.killed.store(true, Ordering::Release);
for feeder in self.feeders.iter() {
feeder.abort();
}
self.signal.notify_one();
}
}
pub(crate) struct ScriptedProc {
stdout: Option<tokio::io::DuplexStream>,
stderr: Option<tokio::io::DuplexStream>,
kill: ScriptedKill,
code: Option<i32>,
timed_out: bool,
signal: Option<i32>,
exit_at: Option<tokio::time::Instant>,
}
impl ScriptedProc {
pub(crate) fn new(
stdout_text: String,
stderr_text: String,
code: Option<i32>,
timed_out: bool,
signal: Option<i32>,
lifetime: Option<Duration>,
line_delay: Option<Duration>,
) -> Self {
let mut feeders = Vec::new();
let mut feed = |text: String| {
let (mut tx, rx) = tokio::io::duplex(64 * 1024);
if text.is_empty() {
return rx; }
let task = tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
match line_delay {
None => {
let _ = tx.write_all(text.as_bytes()).await;
}
Some(delay) => {
for line in text.split_inclusive('\n') {
tokio::time::sleep(delay).await;
if tx.write_all(line.as_bytes()).await.is_err() {
break;
}
}
}
}
});
feeders.push(task.abort_handle());
rx
};
let stdout = feed(stdout_text);
let stderr = feed(stderr_text);
Self {
stdout: Some(stdout),
stderr: Some(stderr),
kill: ScriptedKill {
killed: Arc::new(AtomicBool::new(false)),
signal: Arc::new(Notify::new()),
feeders: Arc::new(feeders),
},
code,
timed_out,
signal,
exit_at: lifetime.map(|d| tokio::time::Instant::now() + d),
}
}
pub(super) fn kill(&self) {
self.kill.fire();
}
pub(super) fn own_group(&self) -> Option<&Arc<ProcessGroup>> {
None
}
pub(super) fn kill_handle(&self) -> ScriptedKill {
self.kill.clone()
}
pub(super) fn take_stdout_reader(&mut self) -> Option<OutputReader> {
self.stdout.take().map(|p| Box::new(p) as OutputReader)
}
pub(super) fn take_stderr_reader(&mut self) -> Option<OutputReader> {
self.stderr.take().map(|p| Box::new(p) as OutputReader)
}
pub(super) async fn wait_outcome(&mut self) -> Outcome {
fn classify(code: Option<i32>, timed_out: bool, signal: Option<i32>) -> Outcome {
match (code, timed_out) {
(_, true) => Outcome::TimedOut,
(Some(code), false) => Outcome::Exited(code),
(None, false) => Outcome::Signalled(signal),
}
}
let already_exited = matches!(self.exit_at, Some(at) if at <= tokio::time::Instant::now());
if self.kill.killed.load(Ordering::Acquire) && !already_exited {
Outcome::Signalled(None)
} else if already_exited {
classify(self.code, self.timed_out, self.signal)
} else {
match self.exit_at {
Some(at) => {
tokio::select! {
biased;
() = self.kill.signal.notified() => Outcome::Signalled(None),
() = tokio::time::sleep_until(at) => classify(self.code, self.timed_out, self.signal),
}
}
None => {
self.kill.signal.notified().await;
Outcome::Signalled(None)
}
}
}
}
pub(super) fn has_exited_now(&self) -> bool {
self.kill.killed.load(Ordering::Acquire)
|| self
.exit_at
.is_some_and(|at| tokio::time::Instant::now() >= at)
}
}
impl RunningProcess {
pub(crate) fn from_scripted(
command: &crate::command::Command,
scripted: ScriptedProc,
recorded: Option<ScriptedResultInfo>,
) -> Self {
Self {
program: command.program_name(),
backend: Backend::Scripted(Box::new(scripted)),
timeout: command.configured_timeout(),
timeout_grace: command.configured_timeout_grace(),
timeout_signal: command.timeout_signal_raw(),
pid: None,
#[cfg(feature = "stats")]
proc_identity: None,
stdout_config: command.stdout_config().with_encoding(encoding_rs::UTF_8),
stderr_config: command.stderr_config().with_encoding(encoding_rs::UTF_8),
buffer: command.output_buffer_policy(),
ok_codes: command.ok_codes_vec(),
stdout_sink: None,
stderr_sink: None,
stdout_pump: None,
stderr_pump: None,
stdin_error: None,
stdout_piped: command.stdout_is_piped(),
deadline_task: None,
timeout_state: Arc::new(AtomicU8::new(TS_PENDING)),
pid_gate: Arc::new(PidGate::new(None)),
cancel_token: command.cancel_token(),
cancel_task: None,
cancel_at_exit: None,
started: Instant::now(),
deadline_anchor: tokio::time::Instant::now(),
start_time: SystemTime::now(),
scripted_result: recorded,
}
}
pub(super) fn arm_scripted_deadline(&mut self) {
if self.deadline_task.is_some() {
return;
}
let (Some(limit), Some(kill)) = (self.timeout, self.backend.scripted_kill()) else {
return;
};
let started = self.deadline_anchor;
let timeout_state = self.timeout_state.clone();
self.deadline_task = Some(tokio::spawn(async move {
if !super::deadline::wait_deadline_and_claim(started, limit, &timeout_state).await {
return; }
kill.fire();
}));
}
}