use super::spec::Spec;
use super::supervisor;
use super::{
CommandOutput, RemoteCommand, RemoteCommandError, RemoteExitStatus, StdinMode,
SupervisionOutcome,
};
use crate::address::RemoteEndpoint;
use std::time::Duration;
use strop_core::process::{capture_with, CaptureError, CapturePolicy, StdinPolicy};
use strop_core::worker::CancelToken;
pub(super) const STDOUT_LIMIT: u64 = 16 * 1024 * 1024;
pub(super) const STDERR_LIMIT: u64 = 64 * 1024;
pub(super) const STDERR_TAIL: u64 = 2 * 1024;
pub(super) const DEFAULT_DEADLINE: Duration = Duration::from_secs(120);
pub(super) fn supervised(
endpoint: &RemoteEndpoint,
command: &RemoteCommand,
mode: StdinMode,
) -> Result<(std::process::Command, super::SupervisionKey), RemoteCommandError> {
let key = super::SupervisionKey::generate();
let spec = Spec::encode(
mode,
key.nonce(),
command.program(),
command.args(),
command.cwd(),
)?;
let line = supervisor::command_line(&spec.encoded()?);
Ok((crate::ssh::exec_command(endpoint, &line), key))
}
pub(super) fn run(
endpoint: &RemoteEndpoint,
command: &RemoteCommand,
token: &CancelToken,
) -> Result<CommandOutput, RemoteCommandError> {
let (mut process, key) = supervised(endpoint, command, StdinMode::Finite)?;
let policy = CapturePolicy {
stdout_limit: STDOUT_LIMIT,
stderr_limit: STDERR_LIMIT,
stderr_tail: STDERR_TAIL,
deadline: command.deadline(),
stdin: StdinPolicy::Held,
};
let output = match capture_with(&mut process, token, &policy) {
Ok(output) => output,
Err(CaptureError::Cancelled) => {
return Err(RemoteCommandError::Cancelled {
diagnostics: "cancellation observed while the command ran".into(),
})
}
Err(CaptureError::TimedOut(deadline)) => {
return Err(RemoteCommandError::Timeout {
seconds: deadline.as_secs(),
})
}
Err(CaptureError::Spawn(message)) => return Err(RemoteCommandError::Spawn { message }),
Err(CaptureError::Failure(failure)) => {
return Err(RemoteCommandError::Local {
message: format!("{:?}: {}", failure.kind, failure.message),
})
}
};
classify(output, &key)
}
pub(super) fn classify(
mut output: strop_core::process::CommandOutput,
key: &super::SupervisionKey,
) -> Result<CommandOutput, RemoteCommandError> {
let ssh_code = output.status.code();
let records = key.records(&output.stderr);
key.remove_records(&mut output.stderr);
if let Some(SupervisionOutcome::LaunchFailure(detail)) = records
.iter()
.find(|record| matches!(record, SupervisionOutcome::LaunchFailure(_)))
{
return Err(RemoteCommandError::Launch {
diagnostics: detail.clone(),
});
}
match records.last() {
Some(SupervisionOutcome::Exited(code)) => Ok(CommandOutput {
status: RemoteExitStatus::Exited(*code),
stdout: output.stdout,
stderr: output.stderr,
stdout_dropped: output.stdout_dropped,
stderr_dropped: output.stderr_dropped,
}),
Some(SupervisionOutcome::Signaled(signal)) => Ok(CommandOutput {
status: RemoteExitStatus::Signaled(*signal),
stdout: output.stdout,
stderr: output.stderr,
stdout_dropped: output.stdout_dropped,
stderr_dropped: output.stderr_dropped,
}),
Some(SupervisionOutcome::Cancelled) => Err(RemoteCommandError::Cancelled {
diagnostics: tail(&output.stderr),
}),
Some(SupervisionOutcome::SupervisorError(stage)) => Err(RemoteCommandError::Supervisor {
stage: stage.clone(),
diagnostics: tail(&output.stderr),
}),
Some(SupervisionOutcome::LaunchFailure(detail)) => Err(RemoteCommandError::Launch {
diagnostics: detail.clone(),
}),
None => classify_without_record(ssh_code, &output.stderr),
}
}
fn classify_without_record(
ssh_code: Option<i32>,
stderr: &[u8],
) -> Result<CommandOutput, RemoteCommandError> {
let diagnostics = String::from_utf8_lossy(stderr).trim().to_string();
match ssh_code {
Some(127) | Some(126) => Err(RemoteCommandError::MissingPython { diagnostics }),
Some(255) | None => Err(RemoteCommandError::Transport {
exit: ssh_code,
diagnostics,
}),
Some(code) => Err(RemoteCommandError::Supervisor {
stage: "exited-without-report".into(),
diagnostics: format!("supervised session ended with ssh exit {code}: {diagnostics}"),
}),
}
}
fn tail(stderr: &[u8]) -> String {
let text = String::from_utf8_lossy(stderr);
let text = text.trim_end();
let mut start = text.len().saturating_sub(2048);
while !text.is_char_boundary(start) {
start += 1;
}
text[start..].trim().to_string()
}