use std::io;
pub const SSH_LEAK_ENV_VARS: [&str; 3] = ["SSH_CLIENT", "SSH_CONNECTION", "SSH_TTY"];
pub fn strip_ssh_leak_env_async(cmd: &mut tokio::process::Command) {
for var in SSH_LEAK_ENV_VARS {
cmd.env_remove(var);
}
}
pub fn strip_ssh_leak_env_builder(cmd: &mut portable_pty::CommandBuilder) {
for var in SSH_LEAK_ENV_VARS {
cmd.env_remove(var);
}
}
#[cfg(unix)]
pub fn write_pty(fd: i32, data: &[u8]) -> io::Result<usize> {
let n = unsafe { libc::write(fd, data.as_ptr() as *const libc::c_void, data.len()) };
if n < 0 { Err(io::Error::last_os_error()) } else { Ok(n as usize) }
}
#[cfg(windows)]
pub fn write_pty(writer: &mut dyn io::Write, data: &[u8]) -> io::Result<usize> {
writer.write(data)
}
#[cfg(unix)]
pub fn kill_session_process(pid: u32) {
unsafe {
libc::kill(pid as i32, libc::SIGHUP);
}
}
#[cfg(unix)]
pub fn pid_alive(pid: u32) -> bool {
let r = unsafe { libc::kill(pid as i32, 0) };
r == 0 || io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(unix)]
pub fn kill_process_escalating(pid: u32) {
const GRACE: std::time::Duration = std::time::Duration::from_millis(250);
const POLL: std::time::Duration = std::time::Duration::from_millis(20);
let wait_exit = |signal: i32| {
unsafe { libc::kill(pid as i32, signal) };
let deadline = std::time::Instant::now() + GRACE;
while std::time::Instant::now() < deadline {
if !pid_alive(pid) {
return true;
}
std::thread::sleep(POLL);
}
!pid_alive(pid)
};
if wait_exit(libc::SIGHUP) {
return;
}
if wait_exit(libc::SIGTERM) {
return;
}
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
pub fn kill_session_process(pid: u32) {
use std::thread;
use std::time::Duration;
use windows_sys::Win32::System::Console::{CTRL_CLOSE_EVENT, GenerateConsoleCtrlEvent};
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess};
unsafe {
let _ = GenerateConsoleCtrlEvent(CTRL_CLOSE_EVENT, 0);
}
thread::sleep(Duration::from_millis(500));
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if !handle.is_null() {
let _ = TerminateProcess(handle, 1);
windows_sys::Win32::Foundation::CloseHandle(handle);
}
}
}
#[cfg(windows)]
pub fn kill_process_escalating(pid: u32) {
kill_session_process(pid);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn tokio_command_strips_ssh_leak_vars() {
let mut cmd = tokio::process::Command::new("/usr/bin/env");
cmd.env_clear();
for var in SSH_LEAK_ENV_VARS {
cmd.env(var, "leak");
}
cmd.env("OMNITERM_KEEP", "1");
strip_ssh_leak_env_async(&mut cmd);
let out = cmd.output().await.expect("env 应可执行");
assert!(out.status.success());
let lines: Vec<String> =
String::from_utf8_lossy(&out.stdout).lines().map(String::from).collect();
for var in SSH_LEAK_ENV_VARS {
assert!(
!lines.iter().any(|l| l.starts_with(&format!("{var}="))),
"{var} 不应出现在 spawn 进程环境里"
);
}
assert!(lines.iter().any(|l| l == "OMNITERM_KEEP=1"));
}
#[test]
fn builder_strips_ssh_leak_vars() {
let mut cmd = portable_pty::CommandBuilder::new("true");
for var in SSH_LEAK_ENV_VARS {
cmd.env(var, "leak");
}
cmd.env("OMNITERM_SESSION_ID", "s1");
strip_ssh_leak_env_builder(&mut cmd);
for var in SSH_LEAK_ENV_VARS {
assert!(cmd.get_env(var).is_none(), "{var} 不应留在 CommandBuilder 环境里");
}
assert_eq!(cmd.get_env("OMNITERM_SESSION_ID"), Some(std::ffi::OsStr::new("s1")));
}
}