Files
shell-rs/src/testing.rs
T
XuzandClaude Opus 5.5 e09adeefb7 外部 CLI 新增 exec --terminal:在已打开的终端里执行命令
只允许登录一次的堡垒机(外部连接)上,exec 重新登录会被拒绝,另开通道又会结束会话。
--terminal 把命令打进这台主机在 ShellRS 里已打开的远程终端:先发 Ctrl-C,探测出
交互 shell(bash、dash、busybox ash)后输入命令行,经 OSC 标记取回输出、退出码和
执行上下文(--json 的 context)。不做忙碌检测,只拒绝全屏程序和同一终端已有一次运行。
外部连接上普通 exec 登录失败时,说明里提示改用 --terminal。

shells.yml 在 Docker 里对 bash 4.2–5.2、dash、busybox ash 跑真实终端测试。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:16:30 +08:00

108 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! What the unit tests share: starting processes without upsetting the
//! tests that need a closed socket to stay closed.
use std::process::Command;
use std::sync::{Mutex, MutexGuard};
static FORKS: Mutex<()> = Mutex::new(());
/// Hold while starting a process, or while making a socket that must refuse
/// connections once it is closed.
///
/// macOS makes a socket and marks it not to be handed to started programs
/// in two steps. A process another test starts in between, on its own
/// thread, gets a copy of the socket and keeps it open as long as it runs:
/// a listener the test has closed still takes connections. A process gets
/// its copies only as it starts, so holding this around starting one and
/// around making the socket keeps the two apart.
pub fn no_forks() -> MutexGuard<'static, ()> {
FORKS
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
/// Run `script` with this machine's `sh`, with `PATH` set to `path` when
/// given. Unix only, like the tests that run scripts for real.
#[cfg(unix)]
pub fn sh(script: &str, path: Option<&str>) -> std::process::Output {
let mut command = Command::new("sh");
command.args(["-c", script]);
if let Some(path) = path {
command.env("PATH", path);
}
let _forks = no_forks();
command.output().expect("sh runs")
}
/// A program on a terminal of its own: what it prints comes in on `output`.
/// Unix only, like `sh`.
#[cfg(unix)]
pub struct Pty {
pub output: std::sync::mpsc::Receiver<Vec<u8>>,
writer: Box<dyn std::io::Write + Send>,
child: Box<dyn portable_pty::Child + Send + Sync>,
_master: Box<dyn portable_pty::MasterPty + Send>,
}
#[cfg(unix)]
impl Pty {
pub fn write(&mut self, bytes: &[u8]) {
self.writer.write_all(bytes).expect("pty takes input");
self.writer.flush().expect("pty takes input");
}
}
#[cfg(unix)]
impl Drop for Pty {
fn drop(&mut self) {
let _ = self.child.kill();
}
}
/// Start `argv` on a new terminal of 120 × 40.
#[cfg(unix)]
pub fn pty(argv: &[&str]) -> Pty {
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
let pair = native_pty_system()
.openpty(PtySize {
rows: 40,
cols: 120,
pixel_width: 0,
pixel_height: 0,
})
.expect("pty opens");
let mut command = CommandBuilder::from_argv(argv.iter().map(Into::into).collect());
command.env("TERM", "xterm-256color");
let child = {
let _forks = no_forks();
pair.slave.spawn_command(command).expect("program starts")
};
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().expect("pty reads");
let writer = pair.master.take_writer().expect("pty writes");
let (sender, output) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut buffer = [0_u8; 8192];
while let Ok(count) = std::io::Read::read(&mut reader, &mut buffer) {
if count == 0 || sender.send(buffer[..count].to_vec()).is_err() {
break;
}
}
});
Pty {
output,
writer,
child,
_master: pair.master,
}
}
/// Whether this machine's `sh` reads `script` without a syntax error.
pub fn sh_accepts(script: &str) -> bool {
let mut command = Command::new("sh");
command.args(["-n", "-c", script]);
let _forks = no_forks();
command.status().expect("sh runs").success()
}