|
| 1 | +use std::sync::Arc; |
| 2 | + |
| 3 | +/// Write a milestone directly to the controlling terminal, bypassing stdout. |
| 4 | +/// |
| 5 | +/// In labeled/piped modes stdout is a pipe and milestones would be stuck in |
| 6 | +/// the line-buffered writer until a newline arrives. This function writes to |
| 7 | +/// the terminal directly so the milestone reaches the PTY regardless of how |
| 8 | +/// stdout is configured. |
| 9 | +#[cfg(unix)] |
| 10 | +fn write_milestone_to_tty(name: &str) { |
| 11 | + use std::io::Write; |
| 12 | + let milestone = pty_terminal_test_client::encoded_milestone(name); |
| 13 | + let mut tty = std::fs::OpenOptions::new().write(true).open("/dev/tty").unwrap(); |
| 14 | + tty.write_all(&milestone).unwrap(); |
| 15 | + tty.flush().unwrap(); |
| 16 | +} |
| 17 | + |
| 18 | +/// Write a milestone directly to the console output with VT processing enabled. |
| 19 | +/// |
| 20 | +/// Opens `CONOUT$` via `CreateFileW`, enables `ENABLE_VIRTUAL_TERMINAL_PROCESSING` |
| 21 | +/// so OSC 8 sequences are interpreted by the console, then writes the milestone |
| 22 | +/// via [`WriteConsoleW`]. `ConPTY` monitors the console buffer and forwards the |
| 23 | +/// resulting VT output to the PTY master. |
| 24 | +#[cfg(windows)] |
| 25 | +fn write_milestone_to_tty(name: &str) { |
| 26 | + use std::ffi::c_void; |
| 27 | + |
| 28 | + // SAFETY: These are stable Windows API functions with well-defined behavior. |
| 29 | + unsafe extern "system" { |
| 30 | + fn CreateFileW( |
| 31 | + name: *const u16, |
| 32 | + access: u32, |
| 33 | + share: u32, |
| 34 | + security: *const c_void, |
| 35 | + disposition: u32, |
| 36 | + flags: u32, |
| 37 | + template: *const c_void, |
| 38 | + ) -> *mut c_void; |
| 39 | + fn GetConsoleMode(handle: *mut c_void, mode: *mut u32) -> i32; |
| 40 | + fn SetConsoleMode(handle: *mut c_void, mode: u32) -> i32; |
| 41 | + fn WriteConsoleW( |
| 42 | + handle: *mut c_void, |
| 43 | + buffer: *const u16, |
| 44 | + chars_to_write: u32, |
| 45 | + chars_written: *mut u32, |
| 46 | + reserved: *const c_void, |
| 47 | + ) -> i32; |
| 48 | + fn CloseHandle(handle: *mut c_void) -> i32; |
| 49 | + } |
| 50 | + |
| 51 | + const GENERIC_READ: u32 = 0x8000_0000; |
| 52 | + const GENERIC_WRITE: u32 = 0x4000_0000; |
| 53 | + const FILE_SHARE_WRITE: u32 = 0x0000_0002; |
| 54 | + const OPEN_EXISTING: u32 = 3; |
| 55 | + const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004; |
| 56 | + const INVALID_HANDLE: *mut c_void = -1_isize as *mut c_void; |
| 57 | + |
| 58 | + let conout: Vec<u16> = "CONOUT$\0".encode_utf16().collect(); |
| 59 | + |
| 60 | + // SAFETY: Opening CONOUT$ with read/write access to query and set console mode. |
| 61 | + let handle = unsafe { |
| 62 | + CreateFileW( |
| 63 | + conout.as_ptr(), |
| 64 | + GENERIC_READ | GENERIC_WRITE, |
| 65 | + FILE_SHARE_WRITE, |
| 66 | + std::ptr::null(), |
| 67 | + OPEN_EXISTING, |
| 68 | + 0, |
| 69 | + std::ptr::null(), |
| 70 | + ) |
| 71 | + }; |
| 72 | + assert!(handle != INVALID_HANDLE, "failed to open CONOUT$"); |
| 73 | + |
| 74 | + // Enable VT processing so OSC 8 sequences are interpreted. |
| 75 | + let mut mode: u32 = 0; |
| 76 | + // SAFETY: `handle` is a valid console output handle from `CreateFileW`. |
| 77 | + // `GetConsoleMode` writes the current mode into `mode`. |
| 78 | + unsafe { GetConsoleMode(handle, &raw mut mode) }; |
| 79 | + // SAFETY: Setting the console mode with VT processing enabled. |
| 80 | + unsafe { SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) }; |
| 81 | + |
| 82 | + // Encode the milestone as a UTF-8 string, then convert to UTF-16 for |
| 83 | + // `WriteConsoleW`. |
| 84 | + let milestone = pty_terminal_test_client::encoded_milestone(name); |
| 85 | + let milestone_str = String::from_utf8(milestone).expect("milestone is valid UTF-8"); |
| 86 | + let wide: Vec<u16> = milestone_str.encode_utf16().collect(); |
| 87 | + |
| 88 | + let mut written: u32 = 0; |
| 89 | + // SAFETY: `handle` is valid, `wide` is a valid UTF-16 buffer. `WriteConsoleW` |
| 90 | + // writes up to `wide.len()` characters and stores the count in `written`. |
| 91 | + unsafe { |
| 92 | + WriteConsoleW( |
| 93 | + handle, |
| 94 | + wide.as_ptr(), |
| 95 | + wide.len().try_into().unwrap(), |
| 96 | + &raw mut written, |
| 97 | + std::ptr::null(), |
| 98 | + ); |
| 99 | + } |
| 100 | + |
| 101 | + // SAFETY: Restoring original console mode and closing the handle. |
| 102 | + unsafe { |
| 103 | + SetConsoleMode(handle, mode); |
| 104 | + CloseHandle(handle); |
| 105 | + } |
| 106 | +} |
| 107 | + |
1 | 108 | /// exit-on-ctrlc |
2 | 109 | /// |
3 | 110 | /// Sets up a Ctrl+C handler, emits a "ready" milestone, then waits. |
@@ -25,19 +132,26 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> { |
25 | 132 | } |
26 | 133 | } |
27 | 134 |
|
28 | | - ctrlc::set_handler(move || { |
29 | | - use std::io::Write; |
30 | | - let _ = write!(std::io::stdout(), "ctrl-c received"); |
31 | | - let _ = std::io::stdout().flush(); |
32 | | - std::process::exit(0); |
| 135 | + let ctrlc_once_lock = Arc::new(std::sync::OnceLock::<()>::new()); |
| 136 | + |
| 137 | + ctrlc::set_handler({ |
| 138 | + let ctrlc_once_lock = Arc::clone(&ctrlc_once_lock); |
| 139 | + move || { |
| 140 | + let _ = ctrlc_once_lock.set(()); |
| 141 | + } |
33 | 142 | })?; |
34 | 143 |
|
35 | | - pty_terminal_test_client::mark_milestone("ready"); |
36 | | - // Print a newline so the milestone bytes get flushed through line-buffered |
37 | | - // writers (labeled/grouped log modes). |
38 | | - println!(); |
| 144 | + // Write the milestone directly to the controlling terminal, bypassing |
| 145 | + // stdout. In labeled/piped modes stdout is a pipe and milestones would |
| 146 | + // be stuck in the line-buffered writer until a newline arrives. |
| 147 | + // |
| 148 | + // On Unix: /dev/tty opens the controlling terminal (the PTY). |
| 149 | + // On Windows: open CONOUT$ with ENABLE_VIRTUAL_TERMINAL_PROCESSING so |
| 150 | + // OSC 8 sequences are processed by the console, then forwarded by ConPTY |
| 151 | + // to the PTY output. |
| 152 | + write_milestone_to_tty("ready"); |
39 | 153 |
|
40 | | - loop { |
41 | | - std::thread::park(); |
42 | | - } |
| 154 | + ctrlc_once_lock.wait(); |
| 155 | + println!("ctrl-c received"); |
| 156 | + Ok(()) |
43 | 157 | } |
0 commit comments