mod python;
mod run;
mod spec;
mod supervisor;
use crate::address::RemoteEndpoint;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::time::Duration;
use strop_core::worker::CancelToken;
#[derive(Debug, Clone)]
pub struct RemoteCommand {
program: OsString,
args: Vec<OsString>,
cwd: PathBuf,
deadline: Duration,
}
impl RemoteCommand {
pub fn new(
program: impl Into<OsString>,
args: Vec<OsString>,
cwd: &Path,
) -> Result<Self, RemoteCommandError> {
let command = Self {
program: program.into(),
args,
cwd: cwd.to_path_buf(),
deadline: run::DEFAULT_DEADLINE,
};
spec::Spec::encode(
StdinMode::Finite,
[0u8; 16],
&command.program,
&command.args,
&command.cwd,
)
.map_err(|error| match error {
RemoteCommandError::ArgvTooLarge { .. } => RemoteCommandError::Invalid {
detail: "program and arguments are too large for a remote command line".into(),
},
other => other,
})?;
Ok(command)
}
pub fn program(&self) -> &OsStr {
&self.program
}
pub fn args(&self) -> &[OsString] {
&self.args
}
pub fn cwd(&self) -> &Path {
&self.cwd
}
pub fn deadline(&self) -> Duration {
self.deadline
}
pub fn with_deadline(mut self, deadline: Duration) -> Self {
self.deadline = deadline;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StdinMode {
Finite,
Relayed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteExitStatus {
Exited(u32),
Signaled(u32),
}
impl RemoteExitStatus {
pub fn code(&self) -> Option<u32> {
match *self {
RemoteExitStatus::Exited(code) => Some(code),
RemoteExitStatus::Signaled(_) => None,
}
}
pub fn signal(&self) -> Option<u32> {
match *self {
RemoteExitStatus::Exited(_) => None,
RemoteExitStatus::Signaled(signal) => Some(signal),
}
}
pub fn success(&self) -> bool {
*self == RemoteExitStatus::Exited(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
pub status: RemoteExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub stdout_dropped: u64,
pub stderr_dropped: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupervisionKey {
nonce: [u8; 16],
}
impl SupervisionKey {
pub(crate) fn generate() -> Self {
Self {
nonce: spec::nonce(),
}
}
pub(crate) fn nonce(&self) -> [u8; 16] {
self.nonce
}
pub fn records(&self, stderr: &[u8]) -> Vec<SupervisionOutcome> {
supervisor::records(stderr, &self.hex())
}
pub(crate) fn remove_records(&self, stderr: &mut Vec<u8>) {
let marker = format!("STROP-SUP-v1 {} ", self.hex());
let mut cursor = 0;
while cursor < stderr.len() {
let Some(relative) = stderr[cursor..]
.windows(marker.len())
.position(|bytes| bytes == marker.as_bytes())
else {
break;
};
let start = cursor + relative;
if start != 0 && stderr[start - 1] != b'\n' {
cursor = start + marker.len();
continue;
}
let Some(length) = stderr[start..].iter().position(|&byte| byte == b'\n') else {
break;
};
let end = start + length + 1;
if !self.records(&stderr[start..end]).is_empty() {
let first = start.saturating_sub(1);
stderr.drain(first..end);
cursor = first;
} else {
cursor = end;
}
}
}
fn hex(&self) -> String {
self.nonce
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupervisionOutcome {
Exited(u32),
Signaled(u32),
Cancelled,
LaunchFailure(String),
SupervisorError(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RemoteCommandError {
#[error("remote command refused: {detail}")]
Invalid { detail: String },
#[error("cannot spawn ssh: {message}")]
Spawn { message: String },
#[error(
"remote command line cannot carry {bytes} encoded bytes (argv/cwd too large for one ssh command)"
)]
ArgvTooLarge { bytes: usize },
#[error(
"remote execution needs compatible Python 3.8+ on remote PATH or STROP_REMOTE_PYTHON: {diagnostics}"
)]
MissingPython { diagnostics: String },
#[error("remote program could not start: {diagnostics}")]
Launch { diagnostics: String },
#[error("remote supervisor failed at {stage}: {diagnostics}")]
Supervisor { stage: String, diagnostics: String },
#[error("ssh transport failed (exit {exit:?}): {diagnostics}")]
Transport {
exit: Option<i32>,
diagnostics: String,
},
#[error("remote command cancelled before completion: {diagnostics}")]
Cancelled { diagnostics: String },
#[error("remote command did not finish within {seconds} seconds")]
Timeout { seconds: u64 },
#[error("local process supervision failed: {message}")]
Local { message: String },
}
pub fn command(
endpoint: &RemoteEndpoint,
command: &RemoteCommand,
) -> Result<std::process::Command, RemoteCommandError> {
command_supervised(endpoint, command, StdinMode::Relayed).map(|(process, _)| process)
}
pub fn command_supervised(
endpoint: &RemoteEndpoint,
command: &RemoteCommand,
mode: StdinMode,
) -> Result<(std::process::Command, SupervisionKey), RemoteCommandError> {
run::supervised(endpoint, command, mode)
}
pub fn run(
endpoint: &RemoteEndpoint,
command: &RemoteCommand,
token: &CancelToken,
) -> Result<CommandOutput, RemoteCommandError> {
run::run(endpoint, command, token)
}
#[cfg(all(test, unix))]
mod tests;