From 0d3868a30c37d3fd90bdc64730fa93de01907739 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 5 Oct 2026 21:05:54 +0000 Subject: [PATCH] Wait for SIGCONT when resuming the TUI (#51192) ## What changed Install a temporary `SIGCONT` handler and unblock the signal during `Ctrl+Z` suspension. After sending `SIGTSTP`, wait for the handler to confirm resumption, returning a timeout error if no confirmation arrives within the bounded wait. Restore the previous signal handler and thread signal mask, resume terminal stderr handling, and reapply terminal modes even when suspension fails. Propagate signal setup, delivery, and restoration errors to the caller. GitOrigin-RevId: 1a7e10e982b1f6b2c7c7d75362a01989409b3b4d --- codex-rs/tui/src/tui/job_control.rs | 77 ++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/tui/job_control.rs b/codex-rs/tui/src/tui/job_control.rs index 58a05b4147..7cb12c874b 100644 --- a/codex-rs/tui/src/tui/job_control.rs +++ b/codex-rs/tui/src/tui/job_control.rs @@ -202,13 +202,78 @@ impl PreparedResumeAction { /// Deliver SIGTSTP after restoring terminal state, then re-applies terminal modes once resumed. fn suspend_process() -> Result<()> { + static JOB_RESUMED: AtomicBool = AtomicBool::new(false); + extern "C" fn record_job_resumed(_signal: libc::c_int) { + JOB_RESUMED.store(true, Ordering::Release); + } super::restore()?; super::terminal_stderr::pause()?; - unsafe { - libc::kill(/*pid*/ 0, libc::SIGTSTP) + // SAFETY: both actions are initialized before they are passed to libc. + let (mut action, mut previous_action): (libc::sigaction, libc::sigaction) = + unsafe { std::mem::zeroed() }; + let (mut continue_set, mut previous_mask): (libc::sigset_t, libc::sigset_t) = + unsafe { std::mem::zeroed() }; + action.sa_sigaction = record_job_resumed as *const () as libc::sighandler_t; + action.sa_flags = libc::SA_RESTART; + let action_installed = unsafe { + libc::sigemptyset(&mut action.sa_mask); + libc::sigaction(libc::SIGCONT, &action, &mut previous_action) == 0 }; - // After the process resumes, reapply terminal modes so drawing can continue. - super::terminal_stderr::resume()?; - super::set_modes()?; - Ok(()) + let unblock_result = if action_installed { + unsafe { + libc::sigemptyset(&mut continue_set); + libc::sigaddset(&mut continue_set, libc::SIGCONT); + libc::pthread_sigmask(libc::SIG_UNBLOCK, &continue_set, &mut previous_mask) + } + } else { + 0 + }; + let suspend_result = if !action_installed { + Err(std::io::Error::last_os_error()) + } else if unblock_result != 0 { + Err(std::io::Error::from_raw_os_error(unblock_result)) + } else if unsafe { + JOB_RESUMED.store(false, Ordering::Relaxed); + libc::kill(/*pid*/ 0, libc::SIGTSTP) + } == 0 + { + for _ in 0..1000 { + if JOB_RESUMED.load(Ordering::Acquire) { + break; + } + std::thread::park_timeout(std::time::Duration::from_millis(1)); + } + if JOB_RESUMED.load(Ordering::Acquire) { + Ok(()) + } else { + Err(std::io::ErrorKind::TimedOut.into()) + } + } else { + Err(std::io::Error::last_os_error()) + }; + let restore_mask_result = if action_installed && unblock_result == 0 { + let result = unsafe { + libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut()) + }; + if result == 0 { + Ok(()) + } else { + Err(std::io::Error::from_raw_os_error(result)) + } + } else { + Ok(()) + }; + // SAFETY: previous_action was initialized by the successful sigaction call above. + let restore_result = if !action_installed + || unsafe { libc::sigaction(libc::SIGCONT, &previous_action, std::ptr::null_mut()) } == 0 + { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + }; + suspend_result + .and(restore_mask_result) + .and(restore_result) + .and(super::terminal_stderr::resume()) + .and(super::set_modes()) }