mirror of
https://github.com/openai/codex.git
synced 2026-10-20 08:19:50 +00:00
Bound tmux option probes to one second (#50219)
## Why A hung tmux command or a descendant holding stdout open can block terminal input setup indefinitely. ## What changed Run tmux option probes on a worker thread with a shared one-second deadline, returning default input options when the wait times out. Prevent overlapping probes and kill and reap commands that exceed the deadline. Capture stdout in a temporary file so completed commands do not wait for descendants to close it, and read at most 4 KiB of output. ## Testing Add Unix regression tests for successful output, killing and reaping a timed-out command, and returning promptly when a descendant keeps stdout open. GitOrigin-RevId: a5a4c9323a3e18d1baada5ab1cf3734ce6a9403a
This commit is contained in:
@@ -3,8 +3,20 @@
|
||||
//! Mouse capture is suppressed only when the containing session explicitly reports mouse off.
|
||||
//! Missing tmux or an inconclusive probe preserves the normal input policy.
|
||||
|
||||
use std::io::Read;
|
||||
use std::io::Seek;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 1);
|
||||
const PROBE_POLL_INTERVAL: Duration = Duration::from_millis(/*millis*/ 10);
|
||||
const MAX_PROBE_OUTPUT_BYTES: u64 = 4096;
|
||||
static PROBE_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Capture policy for the current terminal setup, shared by fullscreen and overlays.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -31,16 +43,69 @@ pub(super) fn options() -> Options {
|
||||
let Ok(path) = codex_utils_path::system_path() else {
|
||||
return Options::default();
|
||||
};
|
||||
read_options(pane.as_deref(), |args| {
|
||||
let output = Command::new(&executable)
|
||||
.env("PATH", &path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.ok()?;
|
||||
output.status.success().then_some(output.stdout)
|
||||
})
|
||||
if PROBE_IN_FLIGHT.swap(true, Ordering::AcqRel) {
|
||||
return Options::default();
|
||||
}
|
||||
let deadline = Instant::now() + PROBE_TIMEOUT;
|
||||
let (sender, receiver) = mpsc::sync_channel(1);
|
||||
if std::thread::Builder::new()
|
||||
.name("tmux-options".into())
|
||||
.spawn(move || {
|
||||
let options = read_options(pane.as_deref(), |args| {
|
||||
let mut command = Command::new(&executable);
|
||||
command
|
||||
.env("PATH", &path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
command_stdout_before_deadline(&mut command, deadline)
|
||||
});
|
||||
PROBE_IN_FLIGHT.store(false, Ordering::Release);
|
||||
let _ = sender.send(options);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
PROBE_IN_FLIGHT.store(false, Ordering::Release);
|
||||
return Options::default();
|
||||
}
|
||||
|
||||
receiver
|
||||
.recv_timeout(deadline.saturating_duration_since(Instant::now()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn command_stdout_before_deadline(command: &mut Command, deadline: Instant) -> Option<Vec<u8>> {
|
||||
if Instant::now() >= deadline {
|
||||
return None;
|
||||
}
|
||||
let mut stdout = tempfile::tempfile().ok()?;
|
||||
command.stdout(stdout.try_clone().ok()?);
|
||||
let mut child = command.spawn().ok()?;
|
||||
loop {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
if !status.success() {
|
||||
return None;
|
||||
}
|
||||
stdout.rewind().ok()?;
|
||||
let mut output = Vec::new();
|
||||
stdout
|
||||
.take(MAX_PROBE_OUTPUT_BYTES)
|
||||
.read_to_end(&mut output)
|
||||
.ok()?;
|
||||
return Some(output);
|
||||
}
|
||||
Ok(None) if !remaining.is_zero() => {
|
||||
std::thread::sleep(remaining.min(PROBE_POLL_INTERVAL));
|
||||
}
|
||||
Ok(None) | Err(_) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_options(pane: Option<&str>, mut run: impl FnMut(&[&str]) -> Option<Vec<u8>>) -> Options {
|
||||
|
||||
@@ -3,6 +3,81 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn completed_probe_returns_stdout() {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "printf 'csi-u\\t0\\n'"]);
|
||||
|
||||
assert_eq!(
|
||||
command_stdout_before_deadline(
|
||||
&mut command,
|
||||
Instant::now() + Duration::from_secs(/*secs*/ 5)
|
||||
),
|
||||
Some(b"csi-u\t0\n".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn hanging_probe_times_out_and_reaps_child() {
|
||||
let pid_file = tempfile::NamedTempFile::new().unwrap();
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command
|
||||
.args(["-c", "echo $$ > \"$1\"; exec /bin/sleep 30", "sh"])
|
||||
.arg(pid_file.path());
|
||||
let started = Instant::now();
|
||||
|
||||
assert_eq!(
|
||||
command_stdout_before_deadline(
|
||||
&mut command,
|
||||
Instant::now() + Duration::from_secs(/*secs*/ 1)
|
||||
),
|
||||
None
|
||||
);
|
||||
assert!(started.elapsed() < Duration::from_secs(/*secs*/ 3));
|
||||
let pid = std::fs::read_to_string(pid_file.path())
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse::<libc::pid_t>()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
// SAFETY: signal 0 only checks whether this process still exists.
|
||||
unsafe {
|
||||
libc::kill(pid, /*sig*/ 0)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
std::io::Error::last_os_error().raw_os_error(),
|
||||
Some(libc::ESRCH)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn completed_probe_does_not_wait_for_descendant_holding_stdout() {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "sleep 30 & printf '%s\\tcsi-u\\t0\\n' \"$!\""]);
|
||||
let started = Instant::now();
|
||||
|
||||
let output = command_stdout_before_deadline(
|
||||
&mut command,
|
||||
Instant::now() + Duration::from_secs(/*secs*/ 5),
|
||||
)
|
||||
.unwrap();
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
let (pid, settings) = output.split_once('\t').unwrap();
|
||||
let pid = pid.parse::<libc::pid_t>().unwrap();
|
||||
// SAFETY: the child PID came directly from the test shell.
|
||||
unsafe {
|
||||
libc::kill(pid, libc::SIGKILL);
|
||||
}
|
||||
|
||||
assert_eq!(settings, "csi-u\t0\n");
|
||||
assert!(started.elapsed() < Duration::from_secs(/*secs*/ 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_probe_targets_the_containing_pane_and_only_disables_confirmed_mouse_off() {
|
||||
for (mouse, mouse_capture) in [
|
||||
|
||||
Reference in New Issue
Block a user