From ee51ef4936b38991c9ae6908e6f273339522bc22 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:07:19 +0900 Subject: [PATCH 1/6] Allow passing a message to init_ctrlc (untested) --- c_src/sigint.c | 16 +++++++++++++++- c_src/sigint.h | 2 +- examples/ctrlc_probe.rs | 2 +- examples/multi_ctrlc.rs | 2 +- src/lib.rs | 18 +++++++++++++----- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/c_src/sigint.c b/c_src/sigint.c index aff9f94..240837b 100644 --- a/c_src/sigint.c +++ b/c_src/sigint.c @@ -1,17 +1,31 @@ #include +#include #include #include +#include volatile sig_atomic_t is_sigint_received = 0; +const char *sigint_message = NULL; +size_t sigint_message_size = 0; void handle_sigint(int signo) { (void)signo; is_sigint_received = 1; + if (sigint_message) + { + write(STDERR_FILENO, sigint_message, sigint_message_size); + write(STDERR_FILENO, "\n", 1); + } } -int init_sigint_handler() +int init_sigint_handler(const char *message) { + if (message) + { + sigint_message = message; + sigint_message_size = strlen(sigint_message); + } struct sigaction sa = {0}; sa.sa_handler = handle_sigint; sigemptyset(&sa.sa_mask); diff --git a/c_src/sigint.h b/c_src/sigint.h index d076914..8f6291c 100644 --- a/c_src/sigint.h +++ b/c_src/sigint.h @@ -3,7 +3,7 @@ #include -int init_sigint_handler(void); +int init_sigint_handler(const char *message); sig_atomic_t get_is_sigint_received(void); void reset_is_sigint_received(void); diff --git a/examples/ctrlc_probe.rs b/examples/ctrlc_probe.rs index 447a1f1..a144b52 100644 --- a/examples/ctrlc_probe.rs +++ b/examples/ctrlc_probe.rs @@ -2,7 +2,7 @@ use std::io::{stdout, Write}; use std::{thread, time::Duration}; fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc()?; + ctrlc_tiny::init_ctrlc(None)?; println!("probe started"); stdout().flush()?; diff --git a/examples/multi_ctrlc.rs b/examples/multi_ctrlc.rs index 324d660..e82e20b 100644 --- a/examples/multi_ctrlc.rs +++ b/examples/multi_ctrlc.rs @@ -4,7 +4,7 @@ //! by resetting the internal flag after each detection. fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc()?; + ctrlc_tiny::init_ctrlc(None)?; let mut count = 0; loop { diff --git a/src/lib.rs b/src/lib.rs index a2e6cd6..6cb2dac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ mod bindings; -use std::{io, sync::Once}; +use std::{ffi, io, mem, sync::Once}; static INIT: Once = Once::new(); @@ -32,10 +32,18 @@ static INIT: Once = Once::new(); /// } /// # Ok::<_, std::io::Error>(()) /// ``` -pub fn init_ctrlc() -> io::Result<()> { +pub fn init_ctrlc(message: Option<&str>) -> io::Result<()> { let mut result = Ok(()); INIT.call_once(|| unsafe { - if bindings::init_sigint_handler() != 0 { + let message = if let Some(message) = message { + let c_string = ffi::CString::new(message).unwrap(); + let ptr = c_string.as_ptr(); + mem::forget(c_string); + ptr + } else { + std::ptr::null() + }; + if bindings::init_sigint_handler(message) != 0 { result = Err(io::Error::last_os_error()); } }); @@ -107,8 +115,8 @@ mod tests { #[test] fn init_ctrlc_should_succeed_and_be_idempotent() { - assert!(init_ctrlc().is_ok()); - assert!(init_ctrlc().is_ok()); + assert!(init_ctrlc(None).is_ok()); + assert!(init_ctrlc(None).is_ok()); } #[test] From f69d49a158347b9aa654d35ad7e86016b3ae9714 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:19:44 +0900 Subject: [PATCH 2/6] Stop adding unwanted line breaks to messages --- c_src/sigint.c | 1 - examples/multi_ctrlc.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/c_src/sigint.c b/c_src/sigint.c index 240837b..20aef41 100644 --- a/c_src/sigint.c +++ b/c_src/sigint.c @@ -15,7 +15,6 @@ void handle_sigint(int signo) if (sigint_message) { write(STDERR_FILENO, sigint_message, sigint_message_size); - write(STDERR_FILENO, "\n", 1); } } diff --git a/examples/multi_ctrlc.rs b/examples/multi_ctrlc.rs index e82e20b..3fb2026 100644 --- a/examples/multi_ctrlc.rs +++ b/examples/multi_ctrlc.rs @@ -4,7 +4,7 @@ //! by resetting the internal flag after each detection. fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc(None)?; + ctrlc_tiny::init_ctrlc(Some("Ctrl-C detected"))?; let mut count = 0; loop { @@ -12,7 +12,7 @@ fn main() -> std::io::Result<()> { ctrlc_tiny::reset_ctrlc_received(); count += 1; - println!("SIGINT received {} time(s)", count); + println!(" {} time(s)", count); if count == 10 { break; From d5298fc670594b758c5c33c23afe51c894411888 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:41:45 +0900 Subject: [PATCH 3/6] Refactor code and update documentation --- examples/ctrlc_probe.rs | 2 +- examples/multi_ctrlc.rs | 2 +- src/lib.rs | 105 ++++++++++++++++++++++++++++------------ 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/examples/ctrlc_probe.rs b/examples/ctrlc_probe.rs index a144b52..447a1f1 100644 --- a/examples/ctrlc_probe.rs +++ b/examples/ctrlc_probe.rs @@ -2,7 +2,7 @@ use std::io::{stdout, Write}; use std::{thread, time::Duration}; fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc(None)?; + ctrlc_tiny::init_ctrlc()?; println!("probe started"); stdout().flush()?; diff --git a/examples/multi_ctrlc.rs b/examples/multi_ctrlc.rs index 3fb2026..7775db2 100644 --- a/examples/multi_ctrlc.rs +++ b/examples/multi_ctrlc.rs @@ -4,7 +4,7 @@ //! by resetting the internal flag after each detection. fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc(Some("Ctrl-C detected"))?; + ctrlc_tiny::init_ctrlc_with_print("Ctrl-C detected")?; let mut count = 0; loop { diff --git a/src/lib.rs b/src/lib.rs index 6cb2dac..5b08053 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,24 @@ use std::{ffi, io, mem, sync::Once}; static INIT: Once = Once::new(); +fn init_ctrlc_internal(message: Option<&str>) -> io::Result<()> { + let mut result = Ok(()); + INIT.call_once(|| unsafe { + let message = if let Some(message) = message { + let c_string = ffi::CString::new(message).unwrap(); + let ptr = c_string.as_ptr(); + mem::forget(c_string); + ptr + } else { + std::ptr::null() + }; + if bindings::init_sigint_handler(message) != 0 { + result = Err(io::Error::last_os_error()); + } + }); + result +} + /// Initializes the SIGINT (Ctrl-C) signal handler. /// /// This function installs a minimal, signal-safe handler for `SIGINT`. @@ -14,6 +32,10 @@ static INIT: Once = Once::new(); /// the signal handler will only be installed once. /// Repeated calls are safe and have no additional effect. /// +/// # Note +/// +/// Use either this function OR [`init_ctrlc_with_print()`], not both. +/// /// # Errors /// /// Returns an `Err` if the underlying system call (`sigaction`) @@ -24,30 +46,51 @@ static INIT: Once = Once::new(); /// /// ```rust,no_run /// ctrlc_tiny::init_ctrlc()?; -/// loop { -/// if ctrlc_tiny::is_ctrlc_received() { -/// println!("Ctrl-C detected!"); -/// break; -/// } +/// while !ctrlc_tiny::is_ctrlc_received() { +/// // Do work here /// } /// # Ok::<_, std::io::Error>(()) /// ``` -pub fn init_ctrlc(message: Option<&str>) -> io::Result<()> { - let mut result = Ok(()); - INIT.call_once(|| unsafe { - let message = if let Some(message) = message { - let c_string = ffi::CString::new(message).unwrap(); - let ptr = c_string.as_ptr(); - mem::forget(c_string); - ptr - } else { - std::ptr::null() - }; - if bindings::init_sigint_handler(message) != 0 { - result = Err(io::Error::last_os_error()); - } - }); - result +pub fn init_ctrlc() -> io::Result<()> { + init_ctrlc_internal(None) +} + +/// Initializes the SIGINT (Ctrl-C) signal handler with a custom message. +/// +/// This function installs a minimal, signal-safe handler for `SIGINT`. +/// Once installed, any incoming Ctrl-C will set an internal flag and +/// print the specified message to stderr. +/// The flag can later be queried via [`is_ctrlc_received()`]. +/// +/// This function may be called multiple times; +/// the signal handler will only be installed once. +/// Repeated calls are safe and have no additional effect. +/// +/// # Note +/// +/// Use either this function OR [`init_ctrlc()`], not both. +/// +/// # Arguments +/// +/// * `message` - The message to print to stderr when Ctrl-C is pressed +/// +/// # Errors +/// +/// Returns an `Err` if the underlying system call (`sigaction`) +/// fails during handler installation. This typically indicates a +/// low-level OS error or permission issue. +/// +/// # Examples +/// +/// ```rust,no_run +/// ctrlc_tiny::init_ctrlc_with_print("Ctrl+C pressed\n")?; +/// while !ctrlc_tiny::is_ctrlc_received() { +/// // Do work here +/// } +/// # Ok::<_, std::io::Error>(()) +/// ``` +pub fn init_ctrlc_with_print(message: &str) -> io::Result<()> { + init_ctrlc_internal(Some(message)) } /// Checks whether Ctrl-C (SIGINT) has been received. @@ -63,12 +106,9 @@ pub fn init_ctrlc(message: Option<&str>) -> io::Result<()> { /// # Examples /// /// ```rust,no_run -/// ctrlc_tiny::init_ctrlc()?; -/// loop { -/// if ctrlc_tiny::is_ctrlc_received() { -/// println!("Received Ctrl-C"); -/// break; -/// } +/// ctrlc_tiny::init_ctrlc_with_print("Ctrl+C pressed\n")?; +/// while !ctrlc_tiny::is_ctrlc_received() { +/// // Do work here /// } /// # Ok::<_, std::io::Error>(()) /// ``` @@ -90,7 +130,7 @@ pub fn is_ctrlc_received() -> bool { /// # Examples /// /// ```rust,no_run -/// ctrlc_tiny::init_ctrlc()?; +/// ctrlc_tiny::init_ctrlc_with_print("Ctrl+C pressed\n")?; /// let mut count = 0; /// loop { /// if ctrlc_tiny::is_ctrlc_received() { @@ -115,8 +155,13 @@ mod tests { #[test] fn init_ctrlc_should_succeed_and_be_idempotent() { - assert!(init_ctrlc(None).is_ok()); - assert!(init_ctrlc(None).is_ok()); + assert!(init_ctrlc().is_ok()); + assert!(init_ctrlc().is_ok()); + } + + #[test] + fn init_ctrlc_with_print_should_succeed() { + assert!(init_ctrlc_with_print("Test message\n").is_ok()); } #[test] From 64eee691c3f29a8108ea3300c6922234674ee595 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:49:19 +0900 Subject: [PATCH 4/6] Add tests --- .../{ctrlc_probe.rs => e2e_init_ctrlc.rs} | 4 +- examples/e2e_init_ctrlc_with_print.rs | 17 ++++++ tests/e2e.rs | 57 +++++++++++++++++-- 3 files changed, 72 insertions(+), 6 deletions(-) rename examples/{ctrlc_probe.rs => e2e_init_ctrlc.rs} (92%) create mode 100644 examples/e2e_init_ctrlc_with_print.rs diff --git a/examples/ctrlc_probe.rs b/examples/e2e_init_ctrlc.rs similarity index 92% rename from examples/ctrlc_probe.rs rename to examples/e2e_init_ctrlc.rs index 447a1f1..6b2334d 100644 --- a/examples/ctrlc_probe.rs +++ b/examples/e2e_init_ctrlc.rs @@ -4,7 +4,7 @@ use std::{thread, time::Duration}; fn main() -> std::io::Result<()> { ctrlc_tiny::init_ctrlc()?; - println!("probe started"); + println!("e2e_init_ctrlc started"); stdout().flush()?; let mut count = 0; @@ -22,4 +22,4 @@ fn main() -> std::io::Result<()> { } Ok(()) -} +} \ No newline at end of file diff --git a/examples/e2e_init_ctrlc_with_print.rs b/examples/e2e_init_ctrlc_with_print.rs new file mode 100644 index 0000000..697cf82 --- /dev/null +++ b/examples/e2e_init_ctrlc_with_print.rs @@ -0,0 +1,17 @@ +use std::io::{stdout, Write}; + +fn main() -> std::io::Result<()> { + ctrlc_tiny::init_ctrlc_with_print("Ctrl+C pressed\n")?; + + println!("e2e_init_ctrlc_with_print started"); + stdout().flush()?; + + while !ctrlc_tiny::is_ctrlc_received() { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + + println!("Finished"); + stdout().flush()?; + + Ok(()) +} \ No newline at end of file diff --git a/tests/e2e.rs b/tests/e2e.rs index df7118c..6cfd19f 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,9 +1,9 @@ -use std::{io::BufRead, process::Command, thread, time::Duration}; +use std::{io::{BufRead, Read}, process::Command, thread, time::Duration}; #[test] -fn e2e_test() { +fn e2e_init_ctrlc_test() { let child = Command::new("cargo") - .args(&["run", "--example", "ctrlc_probe"]) + .args(&["run", "--example", "e2e_init_ctrlc"]) .stdout(std::process::Stdio::piped()) .spawn() .expect("failed to start example"); @@ -22,7 +22,7 @@ fn e2e_test() { let line = line.trim(); if started { lines.push(line.to_string()); - } else if line == "probe started" { + } else if line == "e2e_init_ctrlc started" { started = true; } if started { @@ -39,3 +39,52 @@ fn e2e_test() { "Expected Ctrl-C detection in child output" ); } + +#[test] +fn e2e_init_ctrlc_with_print_test() { + let child = Command::new("cargo") + .args(&["run", "--example", "e2e_init_ctrlc_with_print"]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("failed to start example"); + + let child_id = child.id(); + let stdout = child.stdout.expect("failed to capture stdout"); + let stderr = child.stderr.expect("failed to capture stderr"); + let mut stdout_reader = std::io::BufReader::new(stdout); + let mut stderr_reader = std::io::BufReader::new(stderr); + let mut stdout_lines = Vec::new(); + let mut started = false; + + loop { + let mut line = String::new(); + if stdout_reader.read_line(&mut line).expect("failed to read line") == 0 { + break; + } + let line = line.trim(); + if started { + stdout_lines.push(line.to_string()); + } else if line == "e2e_init_ctrlc_with_print started" { + started = true; + unsafe { + libc::kill(child_id as i32, libc::SIGINT); + } + } + thread::sleep(Duration::from_millis(50)); + } + + let mut stderr_content = String::new(); + stderr_reader.read_to_string(&mut stderr_content).expect("failed to read stderr"); + + assert_eq!( + stdout_lines, + vec!["Finished"], + "Expected finish message in stdout" + ); + assert!( + stderr_content.contains("Ctrl+C pressed"), + "Expected Ctrl+C message in stderr, got: {}", + stderr_content + ); +} From 2bc33dbe35b643cb8f65660368b77c85ff8de0b0 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:54:14 +0900 Subject: [PATCH 5/6] Update README --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ce9a43..ecb36ec 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,9 @@ Example: ```rust fn main() -> std::io::Result<()> { - ctrlc_tiny::init_ctrlc()?; + ctrlc_tiny::init_ctrlc_with_print("Ctrl+C pressed\n")?; - loop { - if ctrlc_tiny::is_ctrlc_received() { - println!("Ctrl-C detected"); - break; - } + while !ctrlc_tiny::is_ctrlc_received() { // work... } From 66febd52182e9d584009748a72bb6f0000fc3279 Mon Sep 17 00:00:00 2001 From: Koji Murata Date: Wed, 17 Sep 2025 21:58:12 +0900 Subject: [PATCH 6/6] Apply code formatter --- examples/e2e_init_ctrlc.rs | 2 +- examples/e2e_init_ctrlc_with_print.rs | 4 ++-- tests/e2e.rs | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/e2e_init_ctrlc.rs b/examples/e2e_init_ctrlc.rs index 6b2334d..9506147 100644 --- a/examples/e2e_init_ctrlc.rs +++ b/examples/e2e_init_ctrlc.rs @@ -22,4 +22,4 @@ fn main() -> std::io::Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/examples/e2e_init_ctrlc_with_print.rs b/examples/e2e_init_ctrlc_with_print.rs index 697cf82..77f3f73 100644 --- a/examples/e2e_init_ctrlc_with_print.rs +++ b/examples/e2e_init_ctrlc_with_print.rs @@ -9,9 +9,9 @@ fn main() -> std::io::Result<()> { while !ctrlc_tiny::is_ctrlc_received() { std::thread::sleep(std::time::Duration::from_millis(50)); } - + println!("Finished"); stdout().flush()?; Ok(()) -} \ No newline at end of file +} diff --git a/tests/e2e.rs b/tests/e2e.rs index 6cfd19f..4c6a670 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,4 +1,9 @@ -use std::{io::{BufRead, Read}, process::Command, thread, time::Duration}; +use std::{ + io::{BufRead, Read}, + process::Command, + thread, + time::Duration, +}; #[test] fn e2e_init_ctrlc_test() { @@ -59,7 +64,11 @@ fn e2e_init_ctrlc_with_print_test() { loop { let mut line = String::new(); - if stdout_reader.read_line(&mut line).expect("failed to read line") == 0 { + if stdout_reader + .read_line(&mut line) + .expect("failed to read line") + == 0 + { break; } let line = line.trim(); @@ -75,7 +84,9 @@ fn e2e_init_ctrlc_with_print_test() { } let mut stderr_content = String::new(); - stderr_reader.read_to_string(&mut stderr_content).expect("failed to read stderr"); + stderr_reader + .read_to_string(&mut stderr_content) + .expect("failed to read stderr"); assert_eq!( stdout_lines,