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... } diff --git a/c_src/sigint.c b/c_src/sigint.c index aff9f94..20aef41 100644 --- a/c_src/sigint.c +++ b/c_src/sigint.c @@ -1,17 +1,30 @@ #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); + } } -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/e2e_init_ctrlc.rs similarity index 93% rename from examples/ctrlc_probe.rs rename to examples/e2e_init_ctrlc.rs index 447a1f1..9506147 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; diff --git a/examples/e2e_init_ctrlc_with_print.rs b/examples/e2e_init_ctrlc_with_print.rs new file mode 100644 index 0000000..77f3f73 --- /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(()) +} diff --git a/examples/multi_ctrlc.rs b/examples/multi_ctrlc.rs index 324d660..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()?; + ctrlc_tiny::init_ctrlc_with_print("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; diff --git a/src/lib.rs b/src/lib.rs index a2e6cd6..5b08053 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,27 @@ mod bindings; -use std::{io, sync::Once}; +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,22 +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() -> io::Result<()> { - let mut result = Ok(()); - INIT.call_once(|| unsafe { - if bindings::init_sigint_handler() != 0 { - result = Err(io::Error::last_os_error()); - } - }); - 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. @@ -55,12 +106,9 @@ pub fn init_ctrlc() -> 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>(()) /// ``` @@ -82,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() { @@ -111,6 +159,11 @@ mod tests { assert!(init_ctrlc().is_ok()); } + #[test] + fn init_ctrlc_with_print_should_succeed() { + assert!(init_ctrlc_with_print("Test message\n").is_ok()); + } + #[test] fn is_ctrlc_received_initially_false() { assert!(!is_ctrlc_received()); diff --git a/tests/e2e.rs b/tests/e2e.rs index df7118c..4c6a670 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,9 +1,14 @@ -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 +27,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 +44,58 @@ 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 + ); +}