use crate::types::{WorkerConfig, WorkerId, declared_os};
use anyhow::{Context, Result};
use openssh::{ControlPersist, KnownHosts, Session, SessionBuilder, Stdio};
use std::collections::HashMap;
use std::ffi::OsString;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::sync::{RwLock, mpsc};
use tracing::{debug, error, warn};
pub use crate::ssh_utils::{
CommandResult, EnvPrefix, build_env_prefix, is_retryable_transport_error,
is_retryable_transport_error_text, is_valid_env_key, shell_escape_value,
};
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
const MAX_OUTPUT_SIZE: u64 = 10 * 1024 * 1024;
const HEALTH_CHECK_COMMAND: &str = "echo ok";
const POOL_LIVENESS_PROBE_TIMEOUT: Duration = Duration::from_secs(10);
fn is_expected_health_check_output(stdout: &str) -> bool {
stdout
.trim()
.lines()
.last()
.is_some_and(is_health_check_sentinel)
}
fn is_health_check_sentinel(line: &str) -> bool {
matches!(line.trim(), "ok")
}
#[derive(Debug, Clone)]
pub struct SshOptions {
pub connect_timeout: Duration,
pub command_timeout: Duration,
pub server_alive_interval: Option<Duration>,
pub control_persist_idle: Option<Duration>,
pub control_master: bool,
pub known_hosts: KnownHostsPolicy,
}
impl Default for SshOptions {
fn default() -> Self {
Self {
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
command_timeout: DEFAULT_COMMAND_TIMEOUT,
server_alive_interval: None,
control_persist_idle: None,
control_master: false,
known_hosts: KnownHostsPolicy::Add,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KnownHostsPolicy {
Strict,
Add,
AcceptAll,
}
#[cfg(test)]
mod retry_tests {
use super::*;
use crate::test_guard;
#[test]
fn test_retryable_transport_error_text() {
let _guard = test_guard!();
assert!(is_retryable_transport_error_text(
"ssh: connect to host 1.2.3.4 port 22: Connection timed out"
));
assert!(is_retryable_transport_error_text(
"kex_exchange_identification: Connection reset by peer"
));
assert!(is_retryable_transport_error_text("Broken pipe"));
assert!(is_retryable_transport_error_text("Network is unreachable"));
}
#[test]
fn test_non_retryable_transport_error_text() {
let _guard = test_guard!();
assert!(!is_retryable_transport_error_text(
"Permission denied (publickey)."
));
assert!(!is_retryable_transport_error_text(
"Host key verification failed."
));
assert!(!is_retryable_transport_error_text(
"Could not resolve hostname worker.example.com: Name or service not known"
));
assert!(!is_retryable_transport_error_text(
"Identity file /nope/id_rsa not accessible: No such file or directory"
));
}
}
pub struct SshClient {
config: WorkerConfig,
options: SshOptions,
session: Option<Session>,
}
impl SshClient {
pub fn new(config: WorkerConfig, options: SshOptions) -> Self {
Self {
config,
options,
session: None,
}
}
pub fn worker_id(&self) -> &WorkerId {
&self.config.id
}
pub fn is_connected(&self) -> bool {
self.session.is_some()
}
fn is_configured_for(&self, config: &WorkerConfig) -> bool {
self.config.id == config.id
&& self.config.host == config.host
&& self.config.user == config.user
&& self.config.identity_file == config.identity_file
&& declared_os(&self.config.tags) == declared_os(&config.tags)
}
pub async fn connect(&mut self) -> Result<()> {
if self.session.is_some() {
debug!("Already connected to {}", self.config.id);
return Ok(());
}
let destination = format!("{}@{}", self.config.user, self.config.host);
debug!("Connecting to {} via SSH...", destination);
let session = match self
.connect_with_mode(&destination, self.options.control_master)
.await
{
Ok(session) => session,
Err(primary_error) if self.options.control_master => {
warn!(
"SSH ControlMaster connection to {} failed ({}). Retrying without ControlMaster.",
destination, primary_error
);
self.connect_with_mode(&destination, false)
.await
.with_context(|| {
format!(
"Failed to connect to {} after retrying without ControlMaster",
destination
)
})?
}
Err(primary_error) => {
return Err(primary_error)
.with_context(|| format!("Failed to connect to {}", destination));
}
};
debug!("Connected to {} ({})", self.config.id, self.config.host);
self.session = Some(session);
Ok(())
}
async fn connect_with_mode(&self, destination: &str, control_master: bool) -> Result<Session> {
let mut builder = SessionBuilder::default();
self.configure_builder(&mut builder, control_master);
builder.connect(destination).await.with_context(|| {
if control_master {
format!(
"Failed to connect to {} with ControlMaster enabled",
destination
)
} else {
format!(
"Failed to connect to {} with ControlMaster disabled",
destination
)
}
})
}
fn configure_builder(&self, builder: &mut SessionBuilder, control_master: bool) {
let known_hosts = match self.options.known_hosts {
KnownHostsPolicy::Strict => KnownHosts::Strict,
KnownHostsPolicy::Add => KnownHosts::Add,
KnownHostsPolicy::AcceptAll => KnownHosts::Accept,
};
builder
.known_hosts_check(known_hosts)
.connect_timeout(self.options.connect_timeout);
if let Some(interval) = self.options.server_alive_interval {
builder.server_alive_interval(interval);
}
let identity_path = shellexpand::tilde(&self.config.identity_file);
if Path::new(identity_path.as_ref()).exists() {
builder.keyfile(identity_path.as_ref());
}
match control_persist_mode(control_master, self.options.control_persist_idle) {
ControlPersistMode::IdleFor(nonzero) => {
builder.control_persist(ControlPersist::IdleFor(nonzero));
}
ControlPersistMode::TooLarge(secs) => {
warn!("control_persist_idle too large ({secs}s); closing after initial connection");
builder.control_persist(ControlPersist::ClosedAfterInitialConnection);
}
ControlPersistMode::Closed => {
builder.control_persist(ControlPersist::ClosedAfterInitialConnection);
}
}
if control_master {
let control_dir = {
let home_ssh = dirs::home_dir().map(|h| h.join(".ssh").join("rch"));
if let Some(ref dir) = home_ssh {
dir.clone()
} else if let Some(runtime_dir) = dirs::runtime_dir() {
runtime_dir.join("rch-ssh")
} else {
let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
std::env::temp_dir().join(format!("rch-ssh-{}", username))
}
};
if let Err(e) = std::fs::create_dir_all(&control_dir) {
warn!(
"Failed to create SSH control directory {:?}: {}",
control_dir, e
);
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(
&control_dir,
std::fs::Permissions::from_mode(0o700),
) {
warn!(
"Failed to set permissions on SSH control directory {:?}: {}",
control_dir, e
);
}
}
}
builder.control_directory(&control_dir);
}
}
pub async fn disconnect(&mut self) -> Result<()> {
if let Some(session) = self.session.take() {
debug!("Disconnecting from {}", self.config.id);
session.close().await?;
debug!("Disconnected from {}", self.config.id);
}
Ok(())
}
pub async fn reconnect(&mut self) -> Result<()> {
if let Err(e) = self.disconnect().await {
debug!(
"Ignoring error closing stale session to {} before reconnect: {}",
self.config.id, e
);
}
self.connect().await
}
pub async fn execute(&self, command: &str) -> Result<CommandResult> {
self.execute_with_timeout(command, self.options.command_timeout)
.await
}
pub async fn execute_with_timeout(
&self,
command: &str,
command_timeout: Duration,
) -> Result<CommandResult> {
if prefers_system_ssh(&self.config) {
return system_ssh_execute(
&self.config,
command,
command_timeout,
self.options
.server_alive_interval
.unwrap_or(Duration::from_secs(2)),
)
.await;
}
let session = self.session.as_ref().context("Not connected to worker")?;
let start = std::time::Instant::now();
debug!(
"Executing on {}: {}",
self.config.id,
crate::util::mask_sensitive_command(command)
);
let mut child = session
.command("sh")
.arg("-c")
.arg(command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.await
.with_context(|| format!("Failed to spawn command on {}", self.config.id))?;
let execution_future = async {
let stdout_handle = child.stdout().take();
let stderr_handle = child.stderr().take();
let stdout_fut = async {
if let Some(out) = stdout_handle {
let reader = BufReader::new(out);
let mut take = reader.take(MAX_OUTPUT_SIZE);
let mut buf = String::new();
take.read_to_string(&mut buf).await?;
let mut reader = take.into_inner();
let mut sink = tokio::io::sink();
tokio::io::copy(&mut reader, &mut sink).await?;
if buf.len() >= MAX_OUTPUT_SIZE as usize {
buf.push_str("\n...[output truncated]...\n");
}
Ok::<String, anyhow::Error>(buf)
} else {
Ok(String::new())
}
};
let stderr_fut = async {
if let Some(err) = stderr_handle {
let reader = BufReader::new(err);
let mut take = reader.take(MAX_OUTPUT_SIZE);
let mut buf = String::new();
take.read_to_string(&mut buf).await?;
let mut reader = take.into_inner();
let mut sink = tokio::io::sink();
tokio::io::copy(&mut reader, &mut sink).await?;
if buf.len() >= MAX_OUTPUT_SIZE as usize {
buf.push_str("\n...[output truncated]...\n");
}
Ok::<String, anyhow::Error>(buf)
} else {
Ok(String::new())
}
};
let (stdout, stderr) = tokio::try_join!(stdout_fut, stderr_fut)?;
let status = child
.wait()
.await
.with_context(|| "Failed to wait for command completion")?;
Ok::<_, anyhow::Error>((status, stdout, stderr))
};
match tokio::time::timeout(command_timeout, execution_future).await {
Ok(result) => {
let (status, stdout, stderr) = result?;
let duration = start.elapsed();
let exit_code = status.code().unwrap_or(-1);
debug!(
"Command completed on {} (exit={}, duration={}ms)",
self.config.id,
exit_code,
duration.as_millis()
);
Ok(CommandResult {
exit_code,
stdout,
stderr,
duration_ms: duration.as_millis() as u64,
})
}
Err(_) => {
warn!(
"Command timed out on {} after {:?}",
self.config.id, command_timeout
);
anyhow::bail!("Command timed out after {:?}", command_timeout);
}
}
}
pub async fn execute_streaming<F, G>(
&self,
command: &str,
mut on_stdout: F,
mut on_stderr: G,
) -> Result<CommandResult>
where
F: FnMut(&str),
G: FnMut(&str),
{
let session = self.session.as_ref().context("Not connected to worker")?;
let start = std::time::Instant::now();
debug!(
"Executing (streaming) on {}: {}",
self.config.id,
crate::util::mask_sensitive_command(command)
);
let mut child = session
.command("sh")
.arg("-c")
.arg(command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.await
.with_context(|| format!("Failed to spawn command on {}", self.config.id))?;
let stdout = child.stdout().take();
let stderr = child.stderr().take();
let (tx, mut rx) = mpsc::channel(100);
if let Some(out) = stdout {
let tx = tx.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(out);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break, Ok(_) => {
if tx.send(StreamEvent::Stdout(line.clone())).await.is_err() {
break; }
}
Err(_) => break, }
}
});
}
if let Some(err) = stderr {
let tx = tx.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(err);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break, Ok(_) => {
if tx.send(StreamEvent::Stderr(line.clone())).await.is_err() {
break; }
}
Err(_) => break, }
}
});
}
drop(tx);
let mut stdout_acc = String::new();
let mut stderr_acc = String::new();
enum StreamEvent {
Stdout(String),
Stderr(String),
}
let streaming_future = async {
while let Some(event) = rx.recv().await {
match event {
StreamEvent::Stdout(line) => {
on_stdout(&line);
if stdout_acc.len() < MAX_OUTPUT_SIZE as usize {
stdout_acc.push_str(&line);
if stdout_acc.len() >= MAX_OUTPUT_SIZE as usize {
stdout_acc.push_str("\n...[output truncated]...\n");
}
}
}
StreamEvent::Stderr(line) => {
on_stderr(&line);
if stderr_acc.len() < MAX_OUTPUT_SIZE as usize {
stderr_acc.push_str(&line);
if stderr_acc.len() >= MAX_OUTPUT_SIZE as usize {
stderr_acc.push_str("\n...[output truncated]...\n");
}
}
}
}
}
let status = child.wait().await?;
Ok::<_, anyhow::Error>(status)
};
match tokio::time::timeout(self.options.command_timeout, streaming_future).await {
Ok(result) => {
let status = result?;
let duration = start.elapsed();
let exit_code = status.code().unwrap_or(-1);
Ok(CommandResult {
exit_code,
stdout: stdout_acc,
stderr: stderr_acc,
duration_ms: duration.as_millis() as u64,
})
}
Err(_) => {
warn!(
"Command (streaming) timed out on {} after {:?}, cleaning up",
self.config.id, self.options.command_timeout
);
anyhow::bail!("Command timed out after {:?}", self.options.command_timeout);
}
}
}
pub async fn health_check(&self) -> Result<bool> {
self.health_check_with_timeout(self.options.command_timeout)
.await
}
pub async fn health_check_with_timeout(&self, timeout: Duration) -> Result<bool> {
match self
.execute_with_timeout(HEALTH_CHECK_COMMAND, timeout)
.await
{
Ok(result) => Ok(result.success() && is_expected_health_check_output(&result.stdout)),
Err(e) => {
warn!("Health check failed for {}: {}", self.config.id, e);
Ok(false)
}
}
}
}
fn pooled_client_options(pool_options: &SshOptions, config: &WorkerConfig) -> SshOptions {
let mut options = pool_options.clone();
if declared_os(&config.tags).as_deref() == Some("windows") {
options.control_master = false;
}
options
}
pub(crate) fn prefers_system_ssh(config: &WorkerConfig) -> bool {
declared_os(&config.tags).as_deref() == Some("windows")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteShellCommand {
pub command: String,
pub stdin_script: Option<String>,
}
#[must_use]
pub fn remote_shell_command(config: &WorkerConfig, command: &str) -> RemoteShellCommand {
if prefers_system_ssh(config) {
RemoteShellCommand {
command: "sh -s".into(),
stdin_script: (command != "sh -s").then(|| {
format!(
"exec sh -c {} </dev/null\n",
shell_escape::escape(command.into())
)
}),
}
} else {
RemoteShellCommand {
command: command.into(),
stdin_script: None,
}
}
}
pub(crate) fn system_ssh_argv(
config: &WorkerConfig,
command: &RemoteShellCommand,
server_alive_interval: Duration,
) -> Vec<OsString> {
let identity_path = shellexpand::tilde(&config.identity_file);
let destination = format!("{}@{}", config.user, config.host);
let mut argv: Vec<OsString> = Vec::with_capacity(10);
argv.push(OsString::from("ssh"));
argv.push(OsString::from("-i"));
argv.push(OsString::from(identity_path.as_ref()));
argv.push(OsString::from("-o"));
argv.push(OsString::from("BatchMode=yes"));
argv.push(OsString::from("-o"));
argv.push(OsString::from("ConnectTimeout=8"));
argv.push(OsString::from("-o"));
argv.push(OsString::from("StrictHostKeyChecking=accept-new"));
if !server_alive_interval.is_zero() {
let secs = server_alive_interval.as_secs().max(1);
argv.push(OsString::from("-o"));
argv.push(OsString::from(format!("ServerAliveInterval={secs}")));
}
argv.push(OsString::from(destination));
argv.push(OsString::from(&command.command));
argv
}
pub async fn write_ssh_command_stdin(
mut stdin: tokio::process::ChildStdin,
input: &str,
) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
let result = async {
stdin.write_all(input.as_bytes()).await?;
stdin.shutdown().await
}
.await;
match result {
Err(error) if error.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
result => result,
}
}
pub(crate) async fn system_ssh_execute(
config: &WorkerConfig,
command: &str,
command_timeout: Duration,
server_alive_interval: Duration,
) -> Result<CommandResult> {
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
let prepared = remote_shell_command(config, command);
let argv = system_ssh_argv(config, &prepared, server_alive_interval);
let input = prepared.stdin_script;
let args = argv.into_iter().skip(1);
let start = std::time::Instant::now();
debug!(
"system-ssh executing on {}: {}",
config.id,
crate::util::mask_sensitive_command(command)
);
let mut child = Command::new("ssh")
.args(args)
.env_clear()
.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.with_context(|| {
format!(
"failed to spawn system ssh for {} (Windows fallback)",
config.id
)
})?;
let execution = async {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let stdin = child.stdin.take();
let stdin_fut = async {
if let (Some(stdin), Some(input)) = (stdin, input) {
write_ssh_command_stdin(stdin, &input).await?;
}
Ok::<(), anyhow::Error>(())
};
let stdout_fut = async {
let mut buf = String::new();
if let Some(out) = stdout {
let mut handle = out.take(MAX_OUTPUT_SIZE);
handle.read_to_string(&mut buf).await?;
}
Ok::<String, anyhow::Error>(buf)
};
let stderr_fut = async {
let mut buf = String::new();
if let Some(err) = stderr {
let mut handle = err.take(MAX_OUTPUT_SIZE);
handle.read_to_string(&mut buf).await?;
}
Ok::<String, anyhow::Error>(buf)
};
let ((), stdout, stderr) = tokio::try_join!(stdin_fut, stdout_fut, stderr_fut)?;
let status = child
.wait()
.await
.with_context(|| "system ssh command failed to wait for completion")?;
Ok::<_, anyhow::Error>((status, stdout, stderr))
};
match tokio::time::timeout(command_timeout, execution).await {
Ok(result) => {
let (status, stdout, stderr) = result?;
let duration = start.elapsed();
let exit_code = status.code().unwrap_or(-1);
debug!(
"system-ssh command completed on {} (exit={}, duration={}ms)",
config.id,
exit_code,
duration.as_millis()
);
Ok(CommandResult {
exit_code,
stdout,
stderr,
duration_ms: duration.as_millis() as u64,
})
}
Err(_) => {
warn!(
"system-ssh command timed out on {} after {:?}",
config.id, command_timeout
);
anyhow::bail!("Command timed out after {:?}", command_timeout);
}
}
}
pub struct SshPool {
connections: Arc<RwLock<HashMap<WorkerId, Arc<RwLock<SshClient>>>>>,
options: SshOptions,
}
impl SshPool {
pub fn new(options: SshOptions) -> Self {
Self {
connections: Arc::new(RwLock::new(HashMap::new())),
options,
}
}
pub async fn get_or_connect(&self, config: &WorkerConfig) -> Result<Arc<RwLock<SshClient>>> {
self.get_or_connect_probing_within(config, POOL_LIVENESS_PROBE_TIMEOUT)
.await
}
async fn get_or_connect_probing_within(
&self,
config: &WorkerConfig,
probe_timeout: Duration,
) -> Result<Arc<RwLock<SshClient>>> {
let shared_client = self.get_or_create_client_entry(config).await;
let reusable = {
let guard = shared_client.read().await;
guard.is_connected()
&& guard
.health_check_with_timeout(probe_timeout)
.await
.unwrap_or(false)
};
if reusable {
debug!("Reusing live connection to {}", config.id);
return Ok(shared_client);
}
let mut client_guard = shared_client.write().await;
if !client_guard.is_connected() {
client_guard.connect().await?;
} else if !client_guard
.health_check_with_timeout(probe_timeout)
.await
.unwrap_or(false)
{
warn!(
"Pooled SSH connection to {} failed liveness probe; reconnecting",
config.id
);
client_guard.reconnect().await?;
}
drop(client_guard);
Ok(shared_client)
}
async fn get_or_create_client_entry(&self, config: &WorkerConfig) -> Arc<RwLock<SshClient>> {
let worker_id = config.id.clone();
loop {
let existing_client = {
let connections = self.connections.read().await;
connections.get(&worker_id).cloned()
};
if let Some(client) = existing_client {
let is_configured_for_worker = {
let guard = client.read().await;
guard.is_configured_for(config)
};
if is_configured_for_worker {
return client;
}
let replacement = Arc::new(RwLock::new(SshClient::new(
config.clone(),
pooled_client_options(&self.options, config),
)));
let replaced = {
let mut connections = self.connections.write().await;
if connections
.get(&worker_id)
.is_some_and(|current| Arc::ptr_eq(current, &client))
{
connections.insert(worker_id.clone(), replacement.clone());
true
} else {
false
}
};
if replaced {
debug!(
"Replaced SSH connection entry for {} after endpoint config changed",
worker_id
);
return replacement;
}
continue;
}
let new_client = Arc::new(RwLock::new(SshClient::new(
config.clone(),
pooled_client_options(&self.options, config),
)));
let inserted = {
let mut connections = self.connections.write().await;
if connections.contains_key(&worker_id) {
false
} else {
connections.insert(worker_id.clone(), new_client.clone());
true
}
};
if inserted {
return new_client;
}
}
}
pub async fn close(&self, worker_id: &WorkerId) -> Result<()> {
let client = {
let mut connections = self.connections.write().await;
connections.remove(worker_id)
};
if let Some(client) = client {
let mut client = client.write().await;
client.disconnect().await?;
}
Ok(())
}
pub async fn close_all(&self) -> Result<()> {
let clients: Vec<_> = {
let mut connections = self.connections.write().await;
connections.drain().map(|(_, v)| v).collect()
};
for client in clients {
let mut client = client.write().await;
if let Err(e) = client.disconnect().await {
error!("Error closing connection: {}", e);
}
}
Ok(())
}
pub async fn active_connections(&self) -> usize {
self.connections.read().await.len()
}
pub async fn run_with_timeout(
&self,
config: &WorkerConfig,
command: &str,
command_timeout: Duration,
) -> Result<CommandResult> {
let client = self
.get_or_connect_probing_within(config, command_timeout.min(POOL_LIVENESS_PROBE_TIMEOUT))
.await?;
let guard = client.read().await;
guard.execute_with_timeout(command, command_timeout).await
}
}
impl Default for SshPool {
fn default() -> Self {
Self::new(SshOptions::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_guard;
#[test]
fn test_command_result_success() {
let _guard = test_guard!();
let result = CommandResult {
exit_code: 0,
stdout: "output".to_string(),
stderr: String::new(),
duration_ms: 100,
};
assert!(result.success());
let failed = CommandResult {
exit_code: 1,
stdout: String::new(),
stderr: "error".to_string(),
duration_ms: 50,
};
assert!(!failed.success());
}
#[test]
fn test_ssh_options_default() {
let _guard = test_guard!();
let options = SshOptions::default();
assert_eq!(options.connect_timeout, Duration::from_secs(10));
assert_eq!(options.command_timeout, Duration::from_secs(300));
assert!(options.server_alive_interval.is_none());
assert!(options.control_persist_idle.is_none());
assert!(!options.control_master);
}
#[test]
fn test_ssh_client_creation() {
let _guard = test_guard!();
let config = WorkerConfig {
id: WorkerId::new("test-worker"),
host: "192.168.1.100".to_string(),
user: "ubuntu".to_string(),
identity_file: "~/.ssh/id_rsa".to_string(),
total_slots: 8,
priority: 100,
tags: vec!["rust".to_string()],
tools: Vec::new(),
};
let client = SshClient::new(config.clone(), SshOptions::default());
assert_eq!(client.worker_id().as_str(), "test-worker");
assert!(!client.is_connected());
}
#[test]
fn test_expected_health_check_output_accepts_sentinel_as_last_line() {
let _guard = test_guard!();
assert!(is_expected_health_check_output("ok\n"));
assert!(is_expected_health_check_output("login banner\nok\n"));
assert!(!is_expected_health_check_output(""));
assert!(!is_expected_health_check_output("not ok\n"));
assert!(!is_expected_health_check_output("ok\npost-command noise\n"));
}
fn worker_config(id: &str, host: &str, user: &str, identity_file: &str) -> WorkerConfig {
WorkerConfig {
id: WorkerId::new(id),
host: host.to_string(),
user: user.to_string(),
identity_file: identity_file.to_string(),
total_slots: 8,
priority: 100,
tags: vec!["rust".to_string()],
tools: Vec::new(),
}
}
#[test]
fn test_ssh_client_configured_for_ignores_scheduling_fields() {
let _guard = test_guard!();
let config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
let client = SshClient::new(config.clone(), SshOptions::default());
let mut scheduling_only_change = config;
scheduling_only_change.total_slots = 16;
scheduling_only_change.priority = 250;
scheduling_only_change.tags = vec!["rust".to_string(), "gpu".to_string()];
assert!(client.is_configured_for(&scheduling_only_change));
}
#[test]
fn test_ssh_client_configured_for_detects_endpoint_changes() {
let _guard = test_guard!();
let config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
let client = SshClient::new(config, SshOptions::default());
assert!(!client.is_configured_for(&worker_config(
"worker-a",
"192.168.1.101",
"ubuntu",
"~/.ssh/id_rsa",
)));
assert!(!client.is_configured_for(&worker_config(
"worker-a",
"192.168.1.100",
"admin",
"~/.ssh/id_rsa",
)));
assert!(!client.is_configured_for(&worker_config(
"worker-a",
"192.168.1.100",
"ubuntu",
"~/.ssh/other_key",
)));
}
#[tokio::test]
async fn test_ssh_pool_reuses_matching_disconnected_entry() {
let _guard = test_guard!();
let pool = SshPool::default();
let config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
let first = pool.get_or_create_client_entry(&config).await;
let second = pool.get_or_create_client_entry(&config).await;
assert!(Arc::ptr_eq(&first, &second));
assert_eq!(pool.active_connections().await, 1);
}
#[tokio::test]
async fn test_ssh_pool_reuses_single_entry_and_close_all_drops_to_zero() {
let _guard = test_guard!();
let options = SshOptions {
control_master: true,
control_persist_idle: Some(Duration::from_secs(60)),
..Default::default()
};
let pool = SshPool::new(options);
let config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
for _ in 0..5 {
let _entry = pool.get_or_create_client_entry(&config).await;
}
assert_eq!(
pool.active_connections().await,
1,
"repeated borrows of one worker must reuse a single pool entry"
);
pool.close_all().await.expect("close_all should succeed");
assert_eq!(
pool.active_connections().await,
0,
"close_all must drop all pooled connections"
);
}
#[test]
fn test_pool_options_map_to_bounded_idle_persist() {
let _guard = test_guard!();
assert_eq!(
control_persist_mode(true, Some(Duration::from_secs(60))),
ControlPersistMode::IdleFor(NonZeroUsize::new(60).unwrap()),
"pool mux options must keep a warm master for a bounded idle window"
);
}
fn windows_worker_config(id: &str) -> WorkerConfig {
let mut config = worker_config(id, "100.68.2.11", "jeffr", "~/.ssh/surfacebookje_key");
config.tags = vec!["rust".to_string(), crate::types::os_tag("windows")];
config
}
#[test]
fn test_pooled_options_disable_control_master_for_windows_workers() {
let _guard = test_guard!();
let pool_mux = SshOptions {
control_master: true,
control_persist_idle: Some(Duration::from_secs(60)),
..Default::default()
};
let config = windows_worker_config("wsurf");
let effective = pooled_client_options(&pool_mux, &config);
assert!(
!effective.control_master,
"a Windows worker must never get a pooled ControlMaster"
);
assert_eq!(effective.connect_timeout, pool_mux.connect_timeout);
assert_eq!(effective.command_timeout, pool_mux.command_timeout);
assert_eq!(
effective.server_alive_interval,
pool_mux.server_alive_interval
);
assert_eq!(
effective.control_persist_idle,
pool_mux.control_persist_idle
);
assert_eq!(effective.known_hosts, pool_mux.known_hosts);
let pool_plain = SshOptions {
control_master: false,
..pool_mux
};
assert!(!pooled_client_options(&pool_plain, &config).control_master);
let mut mixed_case = windows_worker_config("wsurf-2");
mixed_case.tags = vec![crate::types::os_tag("Windows")];
assert!(!pooled_client_options(&pool_mux, &mixed_case).control_master);
}
#[test]
fn test_pooled_options_keep_pool_options_verbatim_for_non_windows_workers() {
let _guard = test_guard!();
let pool_mux = SshOptions {
control_master: true,
control_persist_idle: Some(Duration::from_secs(60)),
..Default::default()
};
let linux = worker_config("contabo-a", "1.2.3.4", "root", "~/.ssh/id_rsa");
let unlabelled = worker_config("contabo-b", "1.2.3.5", "root", "~/.ssh/id_rsa");
for config in [&linux, &unlabelled] {
let effective = pooled_client_options(&pool_mux, config);
assert!(
effective.control_master,
"non-Windows workers keep the pool's mux setting"
);
assert_eq!(
effective.control_persist_idle,
pool_mux.control_persist_idle
);
}
let pool_plain = SshOptions {
control_master: false,
..pool_mux
};
for config in [&linux, &unlabelled] {
assert!(!pooled_client_options(&pool_plain, config).control_master);
}
}
#[tokio::test]
async fn test_pool_entry_for_windows_worker_is_never_mux() {
let _guard = test_guard!();
let pool = SshPool::new(SshOptions {
control_master: true,
control_persist_idle: Some(Duration::from_secs(60)),
..Default::default()
});
let config = windows_worker_config("wsurf");
let entry = pool.get_or_create_client_entry(&config).await;
let guard = entry.read().await;
assert!(
!guard.options.control_master,
"pooled entry for a Windows worker must be non-mux"
);
assert_eq!(
guard.options.control_persist_idle,
Some(Duration::from_secs(60))
);
}
#[tokio::test]
async fn test_pool_entry_for_linux_worker_keeps_mux() {
let _guard = test_guard!();
let pool = SshPool::new(SshOptions {
control_master: true,
control_persist_idle: Some(Duration::from_secs(60)),
..Default::default()
});
let config = worker_config("contabo-a", "1.2.3.4", "root", "~/.ssh/id_rsa");
let entry = pool.get_or_create_client_entry(&config).await;
let guard = entry.read().await;
assert!(
guard.options.control_master,
"pooled entry for a Linux worker must keep the pool's mux setting"
);
}
#[tokio::test]
async fn test_ssh_pool_replaces_stale_entry_when_endpoint_changes() {
let _guard = test_guard!();
let pool = SshPool::default();
let old_config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
let new_config = worker_config("worker-a", "192.168.1.101", "admin", "~/.ssh/new_key");
let stale = pool.get_or_create_client_entry(&old_config).await;
let replacement = pool.get_or_create_client_entry(&new_config).await;
assert!(!Arc::ptr_eq(&stale, &replacement));
assert_eq!(pool.active_connections().await, 1);
let replacement_guard = replacement.read().await;
assert!(replacement_guard.is_configured_for(&new_config));
}
#[tokio::test]
async fn test_ssh_pool_os_transition_replaces_transport_but_case_and_scheduling_reuse() {
let _guard = test_guard!();
let pool = SshPool::new(SshOptions {
control_master: true,
..Default::default()
});
let mut config = worker_config("worker-a", "1.2.3.4", "root", "~/.ssh/id_rsa");
let unlabelled = pool.get_or_create_client_entry(&config).await;
config.tags.push(crate::types::os_tag("windows"));
let windows = pool.get_or_create_client_entry(&config).await;
assert!(!Arc::ptr_eq(&unlabelled, &windows));
{
let client = windows.read().await;
assert!(prefers_system_ssh(&client.config));
assert!(!client.options.control_master);
}
config.tags = vec!["gpu".into(), "os: WiNdOwS ".into()];
config.total_slots += 1;
config.priority += 1;
let reused = pool.get_or_create_client_entry(&config).await;
assert!(Arc::ptr_eq(&windows, &reused));
config.tags = vec![crate::types::os_tag("linux")];
let linux = pool.get_or_create_client_entry(&config).await;
assert!(!Arc::ptr_eq(&windows, &linux));
{
let client = linux.read().await;
assert!(!prefers_system_ssh(&client.config));
assert!(client.options.control_master);
}
assert!(prefers_system_ssh(&windows.read().await.config));
assert!(unlabelled.read().await.options.control_master);
assert_eq!(pool.active_connections().await, 1);
config.tags.clear();
let removed = pool.get_or_create_client_entry(&config).await;
assert!(!Arc::ptr_eq(&linux, &removed));
assert!(removed.read().await.options.control_master);
assert_eq!(pool.active_connections().await, 1);
}
#[tokio::test]
async fn test_health_check_reports_not_alive_without_session() {
let _guard = test_guard!();
let config = worker_config("worker-a", "192.168.1.100", "ubuntu", "~/.ssh/id_rsa");
let client = SshClient::new(config, SshOptions::default());
assert!(!client.is_connected());
let alive = client
.health_check()
.await
.expect("health_check maps execution errors to Ok(false), never Err");
assert!(!alive, "a client with no session must not report as alive");
}
#[test]
fn test_build_env_prefix_quotes_and_rejects() {
let _guard = test_guard!();
let mut env = HashMap::new();
env.insert("RUSTFLAGS".to_string(), "-C target-cpu=native".to_string());
env.insert("QUOTED".to_string(), "a'b".to_string());
env.insert("BADVAL".to_string(), "line1\nline2".to_string());
let allowlist = vec![
"RUSTFLAGS".to_string(),
"QUOTED".to_string(),
"MISSING".to_string(),
"BADVAL".to_string(),
"BAD=KEY".to_string(),
];
let prefix = build_env_prefix(&allowlist, |key| env.get(key).cloned());
assert!(prefix.prefix.contains("RUSTFLAGS='-C target-cpu=native'"));
assert!(prefix.prefix.contains("QUOTED='a'\\''b'"));
assert!(!prefix.prefix.contains("MISSING="));
assert!(!prefix.prefix.contains("BADVAL="));
assert!(prefix.rejected.contains(&"BADVAL".to_string()));
assert!(prefix.rejected.contains(&"BAD=KEY".to_string()));
assert_eq!(
prefix.applied,
vec!["RUSTFLAGS".to_string(), "QUOTED".to_string()]
);
}
mod proptest_ssh_escaping {
use super::*;
use proptest::prelude::*;
use std::collections::HashMap;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn test_is_valid_env_key_no_panic(s in ".*") {
let _guard = test_guard!();
let _ = is_valid_env_key(&s);
}
#[test]
fn test_is_valid_env_key_accepts_valid(
first in "[a-zA-Z_]",
rest in "[a-zA-Z0-9_]{0,50}"
) {
let _guard = test_guard!();
let key = format!("{}{}", first, rest);
prop_assert!(is_valid_env_key(&key), "Should accept valid key: {}", key);
}
#[test]
fn test_is_valid_env_key_rejects_digit_start(
digit in "[0-9]",
rest in "[a-zA-Z0-9_]{0,20}"
) {
let _guard = test_guard!();
let key = format!("{}{}", digit, rest);
prop_assert!(!is_valid_env_key(&key), "Should reject digit-start key: {}", key);
}
#[test]
fn test_shell_escape_value_no_panic(s in ".*") {
let _guard = test_guard!();
let _ = shell_escape_value(&s);
}
#[test]
fn test_shell_escape_value_rejects_unsafe(
prefix in "[a-zA-Z0-9 ]{0,10}",
bad_char in "[\n\r\0]",
suffix in "[a-zA-Z0-9 ]{0,10}"
) {
let _guard = test_guard!();
let value = format!("{}{}{}", prefix, bad_char, suffix);
prop_assert!(shell_escape_value(&value).is_none(),
"Should reject value with unsafe char: {:?}", value);
}
#[test]
fn test_shell_escape_value_accepts_safe(s in "[a-zA-Z0-9 !@#$%^&*()_+=\\-\\[\\]{}|;:,./<>?]{0,100}") {
let result = shell_escape_value(&s);
prop_assert!(result.is_some(), "Should accept safe value: {:?}", s);
let escaped = match result {
Some(escaped) => escaped,
None => {
prop_assert!(false, "Should accept safe value: {:?}", s);
String::new()
}
};
if s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
prop_assert!(escaped.starts_with('\'') || escaped.contains('\''),
"Value with special chars should be quoted: {:?} -> {:?}", s, escaped);
}
}
#[test]
fn test_shell_escape_value_escapes_quotes(
prefix in "[a-zA-Z0-9]{0,10}",
suffix in "[a-zA-Z0-9]{0,10}"
) {
let _guard = test_guard!();
let value = format!("{}'{}", prefix, suffix);
let result = shell_escape_value(&value);
prop_assert!(result.is_some());
let escaped = match result {
Some(escaped) => escaped,
None => {
prop_assert!(false, "Should escape single quote: {}", value);
String::new()
}
};
prop_assert!(escaped.contains("'\\''"),
"Should escape single quote: {} -> {}", value, escaped);
}
#[test]
fn test_build_env_prefix_no_panic(
keys in prop::collection::vec("[a-zA-Z_][a-zA-Z0-9_]{0,10}", 0..10),
values in prop::collection::vec(".*", 0..10)
) {
let mut env = HashMap::new();
for (i, key) in keys.iter().enumerate() {
if let Some(val) = values.get(i) {
env.insert(key.clone(), val.clone());
}
}
let allowlist: Vec<String> = keys;
let _ = build_env_prefix(&allowlist, |k| env.get(k).cloned());
}
#[test]
fn test_build_env_prefix_rejects_invalid_keys(
invalid_key in "[0-9][a-zA-Z0-9_]{0,10}" ) {
let _guard = test_guard!();
let mut env = HashMap::new();
env.insert(invalid_key.clone(), "value".to_string());
let allowlist = vec![invalid_key.clone()];
let prefix = build_env_prefix(&allowlist, |k| env.get(k).cloned());
prop_assert!(!is_valid_env_key(&invalid_key),
"Key should be invalid: {}", invalid_key);
prop_assert!(prefix.rejected.contains(&invalid_key),
"Should reject invalid key: {}", invalid_key);
prop_assert!(prefix.prefix.is_empty());
}
#[test]
fn test_build_env_prefix_missing_values(
keys in prop::collection::vec("[A-Z_][A-Z0-9_]{0,10}", 1..5)
) {
let env: HashMap<String, String> = HashMap::new();
let prefix = build_env_prefix(&keys, |k| env.get(k).cloned());
prop_assert!(prefix.prefix.is_empty(), "Should be empty when no values");
prop_assert!(prefix.applied.is_empty());
prop_assert!(prefix.rejected.is_empty());
}
}
#[test]
fn test_shell_escape_edge_cases() {
let _guard = test_guard!();
let result = shell_escape_value("");
assert_eq!(result, Some("''".to_string()));
let result = shell_escape_value("'");
assert_eq!(result, Some("''\\'''".to_string()));
let result = shell_escape_value("'''");
assert_eq!(
result
.as_deref()
.map(|escaped| escaped.matches("'\\''").count()),
Some(3)
);
let result = shell_escape_value("日本語");
assert!(result.is_some());
let result = shell_escape_value("🔥🚀");
assert!(result.is_some());
let result = shell_escape_value("it's a \"test\" with $vars");
assert!(result.is_some());
}
#[test]
fn test_is_valid_env_key_edge_cases() {
let _guard = test_guard!();
assert!(!is_valid_env_key(""));
assert!(is_valid_env_key("_"));
assert!(is_valid_env_key("A"));
assert!(is_valid_env_key("PATH"));
assert!(is_valid_env_key("HOME"));
assert!(is_valid_env_key("RUSTFLAGS"));
assert!(is_valid_env_key("CC"));
assert!(is_valid_env_key("_PRIVATE"));
assert!(is_valid_env_key("MY_VAR_123"));
assert!(!is_valid_env_key("1VAR"));
assert!(!is_valid_env_key("123"));
assert!(!is_valid_env_key("MY-VAR"));
assert!(!is_valid_env_key("MY.VAR"));
assert!(!is_valid_env_key("MY VAR"));
assert!(!is_valid_env_key("MY=VAR"));
assert!(!is_valid_env_key("日本語"));
assert!(!is_valid_env_key("VAR🔥"));
}
#[test]
fn test_build_env_prefix_integration() {
let _guard = test_guard!();
let mut env = HashMap::new();
env.insert("VALID".to_string(), "simple".to_string());
env.insert("WITH_QUOTE".to_string(), "it's here".to_string());
env.insert("NEWLINE".to_string(), "line1\nline2".to_string());
env.insert("UNICODE".to_string(), "日本語".to_string());
env.insert("EMPTY".to_string(), String::new());
env.insert("123INVALID".to_string(), "value".to_string());
let allowlist = vec![
"VALID".to_string(),
"WITH_QUOTE".to_string(),
"NEWLINE".to_string(),
"UNICODE".to_string(),
"EMPTY".to_string(),
"123INVALID".to_string(),
"MISSING".to_string(),
];
let prefix = build_env_prefix(&allowlist, |k| env.get(k).cloned());
assert!(prefix.applied.contains(&"VALID".to_string()));
assert!(prefix.prefix.contains("VALID=simple"));
assert!(prefix.applied.contains(&"WITH_QUOTE".to_string()));
assert!(prefix.rejected.contains(&"NEWLINE".to_string()));
assert!(prefix.applied.contains(&"UNICODE".to_string()));
assert!(prefix.applied.contains(&"EMPTY".to_string()));
assert!(prefix.rejected.contains(&"123INVALID".to_string()));
assert!(!prefix.applied.contains(&"MISSING".to_string()));
assert!(!prefix.rejected.contains(&"MISSING".to_string()));
}
#[test]
fn test_shell_escape_roundtrip_safety() {
let _guard = test_guard!();
let test_values = [
"simple",
"with spaces",
"with\ttab",
"special!@#$%^&*()",
"quoted\"value",
"path/to/file",
"-flag",
"--long-flag=value",
"",
];
for value in &test_values {
let escaped = shell_escape_value(value);
assert!(escaped.is_some(), "Should escape: {:?}", value);
}
}
}
fn windows_worker(id: &str) -> WorkerConfig {
let mut config = worker_config(id, "100.68.2.11", "jeffr", "~/.ssh/surfacebookje_key");
config.tags = vec!["rust".to_string(), crate::types::os_tag("windows")];
config
}
#[test]
fn test_prefers_system_ssh_for_windows_tag() {
let _guard = test_guard!();
let cfg = windows_worker("wsurf");
assert!(prefers_system_ssh(&cfg));
}
#[test]
fn test_prefers_system_ssh_case_normalized() {
let _guard = test_guard!();
let mut cfg = windows_worker("wsurf-mixed");
cfg.tags = vec![crate::types::os_tag("Windows")];
assert!(prefers_system_ssh(&cfg));
let mut upper = windows_worker("wsurf-upper");
upper.tags = vec![crate::types::os_tag("WINDOWS")];
assert!(prefers_system_ssh(&upper));
}
#[test]
fn test_prefers_system_ssh_false_for_linux() {
let _guard = test_guard!();
let mut linux = worker_config("contabo-a", "1.2.3.4", "root", "~/.ssh/id_rsa");
linux.tags = vec!["rust".to_string(), crate::types::os_tag("linux")];
assert!(!prefers_system_ssh(&linux));
let unlabelled = worker_config("contabo-b", "1.2.3.5", "root", "~/.ssh/id_rsa");
assert!(!prefers_system_ssh(&unlabelled));
}
#[test]
fn test_system_ssh_argv_basic_windows() {
let _guard = test_guard!();
let cfg = windows_worker("wsurf");
let command = remote_shell_command(&cfg, "uname -a");
let argv = system_ssh_argv(&cfg, &command, Duration::from_secs(2));
let s: Vec<String> = argv
.iter()
.map(|o| o.to_string_lossy().into_owned())
.collect();
assert_eq!(s[0], "ssh", "argv[0] is the program name");
assert_eq!(s[1], "-i");
assert!(
s[2].ends_with(".ssh/surfacebookje_key"),
"identity file is the tilde-expanded path: {}",
s[2]
);
assert!(s.contains(&"-o".to_string()));
assert!(s.contains(&"BatchMode=yes".to_string()));
assert!(s.contains(&"ConnectTimeout=8".to_string()));
assert!(s.contains(&"StrictHostKeyChecking=accept-new".to_string()));
assert!(s.contains(&"ServerAliveInterval=2".to_string()));
assert!(!s.iter().any(|arg| arg == "-p"));
assert!(s.contains(&"jeffr@100.68.2.11".to_string()));
assert_eq!(s[s.len() - 1], "sh -s", "Windows gets only a fixed reader");
}
#[test]
fn test_system_ssh_script_transport_keeps_posix_argv_and_exact_stdin_reader() {
let _guard = test_guard!();
let linux = worker_config("linux", "1.2.3.4", "root", "~/.ssh/id_rsa");
let windows = windows_worker("windows");
let script = "printf '%s\\n' '%PATH% !VAR! & | < > ^ $()'\nprintf done";
let prepared = remote_shell_command(&linux, script);
let argv = system_ssh_argv(&linux, &prepared, Duration::ZERO);
assert_eq!(argv.last().unwrap(), script);
assert!(prepared.stdin_script.is_none());
for command in [script, "sh -s", "", "sh -s\n"] {
let prepared = remote_shell_command(&windows, command);
let argv = system_ssh_argv(&windows, &prepared, Duration::ZERO);
assert_eq!(argv.last().unwrap(), "sh -s");
assert_eq!(prepared.stdin_script.is_none(), command == "sh -s");
}
}
#[tokio::test]
async fn test_system_ssh_stdin_roundtrip_matches_null_stdin_shell() {
use std::process::Stdio;
let _guard = test_guard!();
let config = windows_worker("windows");
for script in [
"",
"printf '%s\\n' \"apostrophe's\" 'double\"quote' '%PATH% !VAR! ^ & | < >' '$() `x` \\'",
"cat\nprintf 'AFTER\\n'\nread value || printf 'EMPTY\\n'",
"cat <<'END'\nheredoc ' \" % ! $() \\\nEND\nprintf 'done\\n'",
"printf '%s\\n' 'first\nsecond' # trailing comment",
"printf 'before\\n'; printf 'diagnostic\\n' >&2; exit 37",
"# comment ending with a backslash \\",
] {
let expected = tokio::process::Command::new("sh")
.args(["-c", script])
.stdin(Stdio::null())
.output()
.await
.unwrap();
let mut child = tokio::process::Command::new("sh")
.arg("-s")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let stdin = child.stdin.take().unwrap();
let payload = remote_shell_command(&config, script).stdin_script.unwrap();
let (written, observed) = tokio::time::timeout(Duration::from_secs(5), async {
tokio::join!(
write_ssh_command_stdin(stdin, &payload),
child.wait_with_output()
)
})
.await
.unwrap();
written.unwrap();
let observed = observed.unwrap();
assert_eq!(observed.status.code(), expected.status.code(), "{script:?}");
assert_eq!(observed.stdout, expected.stdout, "{script:?}");
assert_eq!(observed.stderr, expected.stderr, "{script:?}");
}
}
#[tokio::test]
async fn test_system_ssh_split_stdin_never_becomes_command_input() {
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
let _guard = test_guard!();
let script = "cat\nprintf 'AFTER\\n'\nread value || printf 'EMPTY\\n'";
let payload = remote_shell_command(&windows_worker("windows"), script)
.stdin_script
.unwrap();
let split = payload.find("cat\n").unwrap() + "cat\n".len();
let mut child = tokio::process::Command::new("sh")
.arg("-s")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let writer = async {
stdin.write_all(&payload.as_bytes()[..split]).await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
stdin.write_all(&payload.as_bytes()[split..]).await.unwrap();
stdin.shutdown().await.unwrap();
};
let ((), output) = tokio::time::timeout(Duration::from_secs(5), async {
tokio::join!(writer, child.wait_with_output())
})
.await
.unwrap();
let output = output.unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"AFTER\nEMPTY\n");
assert!(output.stderr.is_empty());
}
#[tokio::test]
async fn test_system_ssh_closed_stdin_preserves_early_exit_diagnostic() {
use std::process::Stdio;
let _guard = test_guard!();
let mut child = tokio::process::Command::new("sh")
.args(["-c", "printf 'remote failure\\n' >&2; exit 37"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let stdin = child.stdin.take().unwrap();
child.wait().await.unwrap();
write_ssh_command_stdin(stdin, &"x".repeat(128 * 1024))
.await
.unwrap();
let output = child.wait_with_output().await.unwrap();
assert_eq!(output.status.code(), Some(37));
assert_eq!(output.stderr, b"remote failure\n");
assert!(output.stdout.is_empty());
}
#[test]
fn test_system_ssh_argv_zero_server_alive_omits_keepalive() {
let _guard = test_guard!();
let cfg = windows_worker("wsurf");
let command = remote_shell_command(&cfg, "echo hi");
let argv = system_ssh_argv(&cfg, &command, Duration::from_secs(0));
let s: Vec<String> = argv
.iter()
.map(|o| o.to_string_lossy().into_owned())
.collect();
assert!(
!s.iter().any(|a| a.starts_with("ServerAliveInterval=")),
"no ServerAliveInterval flag when interval is 0; got {:?}",
s
);
}
#[test]
fn test_system_ssh_argv_long_keepalive_preserved() {
let _guard = test_guard!();
let cfg = windows_worker("wsurf");
let command = remote_shell_command(&cfg, "true");
let argv = system_ssh_argv(&cfg, &command, Duration::from_secs(60));
let s: Vec<String> = argv
.iter()
.map(|o| o.to_string_lossy().into_owned())
.collect();
assert!(s.contains(&"ServerAliveInterval=60".to_string()));
}
#[test]
fn test_system_ssh_argv_uses_tilde_expanded_identity() {
let _guard = test_guard!();
let mut cfg = windows_worker("wsurf");
cfg.identity_file = "~/.ssh/operator_key".to_string();
let command = remote_shell_command(&cfg, "true");
let argv = system_ssh_argv(&cfg, &command, Duration::from_secs(2));
let s: Vec<String> = argv
.iter()
.map(|o| o.to_string_lossy().into_owned())
.collect();
assert!(
!s.contains(&"~/.ssh/operator_key".to_string()),
"identity file must be tilde-expanded, got {:?}",
s
);
assert!(
s.contains(&"~/.ssh/operator_key".to_string())
|| s.iter().any(|a| a.ends_with(".ssh/operator_key")),
"tilde-expanded path appears in argv: {:?}",
s
);
}
#[test]
fn test_dispatch_helper_matches_declared_os_key() {
let _guard = test_guard!();
let cfg = windows_worker("wsurf");
assert_eq!(declared_os(&cfg.tags).as_deref(), Some("windows"));
assert!(prefers_system_ssh(&cfg));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ControlPersistMode {
Closed,
IdleFor(NonZeroUsize),
TooLarge(u64),
}
fn control_persist_mode(control_master: bool, idle: Option<Duration>) -> ControlPersistMode {
match idle {
Some(idle) if control_master && !idle.is_zero() => match usize::try_from(idle.as_secs()) {
Ok(secs) => match NonZeroUsize::new(secs) {
Some(nonzero) => ControlPersistMode::IdleFor(nonzero),
None => ControlPersistMode::Closed,
},
Err(_) => ControlPersistMode::TooLarge(idle.as_secs()),
},
_ => ControlPersistMode::Closed,
}
}
#[cfg(test)]
mod control_persist_tests {
use super::*;
use std::time::Duration;
#[test]
fn non_mux_sessions_never_persist_forever() {
assert_eq!(
control_persist_mode(false, None),
ControlPersistMode::Closed
);
assert_eq!(
control_persist_mode(false, Some(Duration::from_secs(60))),
ControlPersistMode::Closed
);
}
#[test]
fn mux_without_idle_closes() {
assert_eq!(control_persist_mode(true, None), ControlPersistMode::Closed);
assert_eq!(
control_persist_mode(true, Some(Duration::from_secs(0))),
ControlPersistMode::Closed
);
}
#[test]
fn mux_with_idle_keeps_warm() {
assert_eq!(
control_persist_mode(true, Some(Duration::from_secs(60))),
ControlPersistMode::IdleFor(NonZeroUsize::new(60).unwrap())
);
}
}