Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...
}

Expand Down
15 changes: 14 additions & 1 deletion c_src/sigint.c
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
#include <signal.h>
#include <string.h>
#include <stdatomic.h>
#include <stddef.h>
#include <unistd.h>

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);
Expand Down
2 changes: 1 addition & 1 deletion c_src/sigint.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#include <signal.h>

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);

Expand Down
2 changes: 1 addition & 1 deletion examples/ctrlc_probe.rs → examples/e2e_init_ctrlc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions examples/e2e_init_ctrlc_with_print.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
4 changes: 2 additions & 2 deletions examples/multi_ctrlc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
//! 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 {
if ctrlc_tiny::is_ctrlc_received() {
ctrlc_tiny::reset_ctrlc_received();

count += 1;
println!("SIGINT received {} time(s)", count);
println!(" {} time(s)", count);

if count == 10 {
break;
Expand Down
93 changes: 73 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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`.
Expand All @@ -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`)
Expand All @@ -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.
Expand All @@ -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>(())
/// ```
Expand All @@ -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() {
Expand Down Expand Up @@ -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());
Expand Down
68 changes: 64 additions & 4 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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 {
Expand All @@ -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
);
}