Skip to content

Commit 568ee34

Browse files
branchseerclaude
andcommitted
fix(pty_terminal): use signal_hook instead of ctrlc for musl compat
ctrlc::set_handler spawns a background thread to monitor signals. The subprocess closure runs during .init_array (via ctor), and on musl, newly-created threads cannot execute during init because musl holds a lock. This causes ctrlc's monitoring thread to never run, silently swallowing SIGINT and causing send_ctrl_c_interrupts_process to hang. Replace ctrlc with signal_hook::low_level::register on Unix, which installs a raw signal handler without spawning threads. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent decbd3b commit 568ee34

1 file changed

Lines changed: 55 additions & 16 deletions

File tree

crates/pty_terminal/tests/terminal.rs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,49 @@ fn send_ctrl_c_interrupts_process() {
275275
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
276276
use std::io::{Write, stdout};
277277

278-
// On Windows, clear the "ignore CTRL_C" flag set by Rust runtime
279-
// so that CTRL_C_EVENT reaches the ctrlc handler.
278+
// On Unix, use signal_hook directly instead of ctrlc.
279+
// ctrlc spawns a background thread to monitor signals, but the subprocess
280+
// closure runs during .init_array (via ctor). On musl, newly-created threads
281+
// cannot execute during init (musl holds a lock), so ctrlc's thread never
282+
// runs and SIGINT is silently swallowed.
283+
// signal_hook::low_level::register installs a raw signal handler with no
284+
// background thread, avoiding the issue entirely.
285+
#[cfg(unix)]
286+
{
287+
use std::sync::{
288+
Arc,
289+
atomic::{AtomicBool, Ordering},
290+
};
291+
292+
let interrupted = Arc::new(AtomicBool::new(false));
293+
let flag = Arc::clone(&interrupted);
294+
295+
// SAFETY: The closure only performs an atomic store, which is signal-safe.
296+
unsafe {
297+
signal_hook::low_level::register(signal_hook::consts::SIGINT, move || {
298+
flag.store(true, Ordering::SeqCst);
299+
})
300+
.unwrap();
301+
}
302+
303+
println!("ready");
304+
stdout().flush().unwrap();
305+
306+
loop {
307+
if interrupted.load(Ordering::SeqCst) {
308+
print!("INTERRUPTED");
309+
stdout().flush().unwrap();
310+
std::process::exit(0);
311+
}
312+
std::thread::yield_now();
313+
}
314+
}
315+
316+
// On Windows, ctrlc works fine (no .init_array/musl issue).
280317
#[cfg(windows)]
281318
{
319+
// Clear the "ignore CTRL_C" flag set by Rust runtime
320+
// so that CTRL_C_EVENT reaches the ctrlc handler.
282321
// SAFETY: Declaring correct signature for SetConsoleCtrlHandler from kernel32.
283322
unsafe extern "system" {
284323
fn SetConsoleCtrlHandler(
@@ -291,23 +330,23 @@ fn send_ctrl_c_interrupts_process() {
291330
unsafe {
292331
SetConsoleCtrlHandler(None, 0); // FALSE = remove ignore
293332
}
294-
}
295333

296-
ctrlc::set_handler(move || {
297-
// Write directly and exit from the handler to avoid races.
298-
use std::io::Write;
299-
let _ = write!(std::io::stdout(), "INTERRUPTED");
300-
let _ = std::io::stdout().flush();
301-
std::process::exit(0);
302-
})
303-
.unwrap();
334+
ctrlc::set_handler(move || {
335+
// Write directly and exit from the handler to avoid races.
336+
use std::io::Write;
337+
let _ = write!(std::io::stdout(), "INTERRUPTED");
338+
let _ = std::io::stdout().flush();
339+
std::process::exit(0);
340+
})
341+
.unwrap();
304342

305-
println!("ready");
306-
stdout().flush().unwrap();
343+
println!("ready");
344+
stdout().flush().unwrap();
307345

308-
// Block until Ctrl+C handler exits the process.
309-
loop {
310-
std::thread::park();
346+
// Block until Ctrl+C handler exits the process.
347+
loop {
348+
std::thread::park();
349+
}
311350
}
312351
}));
313352

0 commit comments

Comments
 (0)