Signal Handling API for Systems Programming #288

Closed
opened 2026-01-20 22:08:25 +00:00 by navicore · 1 comment
navicore commented 2026-01-20 22:08:25 +00:00 (Migrated from github.com)

Motivation

Seq aims to support systems programming, where signal handling is essential for:

  • Graceful shutdown (SIGTERM, SIGINT)
  • Configuration reload (SIGHUP)
  • Child process management (SIGCHLD)
  • Custom application signals (SIGUSR1, SIGUSR2)
  • Ignoring signals (SIGPIPE for network servers)

Currently, Seq has a default SIGINT handler that exits on Ctrl-C (#287). This issue explores a user-facing API for customizing signal behavior.

Design Constraints

1. Async-Signal-Safety

Signal handlers execute in an interrupt context with severe restrictions:

  • Cannot allocate memory
  • Cannot use locks/mutexes
  • Limited to ~30 POSIX-defined safe functions
  • Cannot safely run arbitrary Seq code (quotations)

2. Stack-Based Paradigm

The API should feel natural in Seq's concatenative style.

3. May Scheduler Integration

Signals and coroutines need careful coordination.

4. Cross-Platform

  • Unix: Full signal support (SIGINT, SIGTERM, SIGHUP, SIGUSR1, etc.)
  • Windows: Limited (SIGINT and SIGTERM-ish via SetConsoleCtrlHandler)

Proposed API Options

Simple, safe, fits the "check in your event loop" pattern:

# Trap signals (set internal flag on receipt, don't exit)
signal.SIGINT signal.trap
signal.SIGTERM signal.trap
signal.SIGHUP signal.trap

# Check and clear flag (returns Bool)
signal.SIGINT signal.received?   # ( -- Bool ) true if received since last check

# Check without clearing
signal.SIGINT signal.pending?    # ( -- Bool )

# Reset to default behavior
signal.SIGINT signal.default

# Ignore signal entirely
signal.SIGPIPE signal.ignore

# Example: Graceful shutdown loop
: server-loop ( -- )
  signal.SIGINT signal.received? if
    "Shutting down..." io.write-line
    return
  then
  handle-next-request
  server-loop
;

Pros:

  • Simple to implement (just atomic flags)
  • Safe (no code runs in signal context)
  • Predictable (user controls when to check)
  • Fits Seq's explicit style

Cons:

  • Requires polling
  • Can miss signals if not checked frequently
  • User must remember to check

Option B: Channel-Based (Advanced)

Signals delivered as messages on a channel:

# Create a channel that receives signal notifications
signal.SIGINT signal.channel   # ( Signal -- Channel )

# Use in a strand
sig-chan [
  "Received SIGINT" io.write-line
  cleanup-resources
] strand.spawn

# Or with select for multiple signals
[ int-chan term-chan hup-chan ] channel.select
match
  | signal.SIGINT => [ handle-interrupt ]
  | signal.SIGTERM => [ handle-terminate ]
  | signal.SIGHUP => [ reload-config ]
end

Pros:

  • Fits CSP concurrency model
  • Can handle signals asynchronously
  • Composable with other channel operations

Cons:

  • More complex implementation (dedicated signal thread)
  • Higher overhead
  • Requires strand for async handling

Combine both for flexibility:

# Simple flag-based (synchronous)
signal.SIGINT signal.trap
signal.SIGINT signal.received?

# Channel-based (asynchronous)  
signal.SIGHUP signal.channel

# Convenience: wait for any of these signals
[ signal.SIGINT signal.SIGTERM ] signal.wait  # ( Signals -- Signal )

Implementation Sketch

Flag-Based Core

// In runtime
static SIGNAL_FLAGS: [AtomicBool; 32] = [...];  // One per signal

extern "C" fn signal_handler(sig: c_int) {
    SIGNAL_FLAGS[sig as usize].store(true, Ordering::SeqCst);
}

#[no_mangle]
pub extern "C" fn patch_seq_signal_trap(stack: Stack) -> Stack {
    let (stack, sig) = pop(stack);
    let sig_num = sig.as_int();
    unsafe { libc::signal(sig_num, signal_handler as _); }
    stack
}

#[no_mangle] 
pub extern "C" fn patch_seq_signal_received(stack: Stack) -> Stack {
    let (stack, sig) = pop(stack);
    let sig_num = sig.as_int() as usize;
    let was_set = SIGNAL_FLAGS[sig_num].swap(false, Ordering::SeqCst);
    push(stack, Value::Bool(was_set))
}

Signal Constants

# In stdlib or builtins
: signal.SIGINT 2 ;
: signal.SIGTERM 15 ;
: signal.SIGHUP 1 ;
: signal.SIGPIPE 13 ;
: signal.SIGUSR1 10 ;
: signal.SIGUSR2 12 ;
# etc.

Use Cases

1. Graceful HTTP Server Shutdown

: main ( -- Int )
  signal.SIGINT signal.trap
  signal.SIGTERM signal.trap
  signal.SIGPIPE signal.ignore  # Don't crash on client disconnect
  
  8080 server-listen
  server-loop
  
  "Goodbye!" io.write-line
  0
;

: server-loop ( Server -- Server )
  signal.SIGINT signal.received?
  signal.SIGTERM signal.received? or if
    return  # Exit loop, proceed to cleanup
  then
  
  accept-connection
  handle-request
  server-loop
;

2. Config Reload on SIGHUP

: main ( -- Int )
  signal.SIGHUP signal.trap
  load-config
  
  [
    signal.SIGHUP signal.received? if
      "Reloading config..." io.write-line
      load-config
    then
    process-work
  ] loop
;

3. Worker Process Management

: main ( -- Int )
  signal.SIGCHLD signal.trap
  
  spawn-workers
  
  [
    signal.SIGCHLD signal.received? if
      reap-children
      respawn-if-needed
    then
    coordinate-workers
  ] loop
;

Open Questions

  1. Signal masking during critical sections?

    signal.SIGINT signal.block
    # critical code
    signal.SIGINT signal.unblock
    
  2. sigaction vs signal?

    • sigaction is more portable and allows SA_RESTART
    • Worth the complexity?
  3. Real-time signals (SIGRTMIN+n)?

    • Queued, carry data
    • Overkill for v1?
  4. Windows support?

    • Only SIGINT and SIGTERM-ish via SetConsoleCtrlHandler
    • Abstract behind same API or separate?
  5. Integration with weaves/strands?

    • Should signal.wait be a yielding operation?
    • Interrupt blocked channel operations?

Milestones

v1: Basic Flag API

  • Signal constants in stdlib
  • signal.trap, signal.received?, signal.pending?
  • signal.default, signal.ignore
  • Unix only initially

v2: Channel API

  • signal.channel for async handling
  • Integration with channel.select
  • Signal thread for delivery

v3: Advanced

  • Signal masking (signal.block, signal.unblock)
  • Windows support
  • sigaction flags (SA_RESTART, etc.)

References

## Motivation Seq aims to support systems programming, where signal handling is essential for: - Graceful shutdown (SIGTERM, SIGINT) - Configuration reload (SIGHUP) - Child process management (SIGCHLD) - Custom application signals (SIGUSR1, SIGUSR2) - Ignoring signals (SIGPIPE for network servers) Currently, Seq has a default SIGINT handler that exits on Ctrl-C (#287). This issue explores a user-facing API for customizing signal behavior. ## Design Constraints ### 1. Async-Signal-Safety Signal handlers execute in an interrupt context with severe restrictions: - Cannot allocate memory - Cannot use locks/mutexes - Limited to ~30 POSIX-defined safe functions - Cannot safely run arbitrary Seq code (quotations) ### 2. Stack-Based Paradigm The API should feel natural in Seq's concatenative style. ### 3. May Scheduler Integration Signals and coroutines need careful coordination. ### 4. Cross-Platform - Unix: Full signal support (SIGINT, SIGTERM, SIGHUP, SIGUSR1, etc.) - Windows: Limited (SIGINT and SIGTERM-ish via SetConsoleCtrlHandler) --- ## Proposed API Options ### Option A: Flag-Based (Recommended for v1) Simple, safe, fits the "check in your event loop" pattern: ```seq # Trap signals (set internal flag on receipt, don't exit) signal.SIGINT signal.trap signal.SIGTERM signal.trap signal.SIGHUP signal.trap # Check and clear flag (returns Bool) signal.SIGINT signal.received? # ( -- Bool ) true if received since last check # Check without clearing signal.SIGINT signal.pending? # ( -- Bool ) # Reset to default behavior signal.SIGINT signal.default # Ignore signal entirely signal.SIGPIPE signal.ignore # Example: Graceful shutdown loop : server-loop ( -- ) signal.SIGINT signal.received? if "Shutting down..." io.write-line return then handle-next-request server-loop ; ``` **Pros:** - Simple to implement (just atomic flags) - Safe (no code runs in signal context) - Predictable (user controls when to check) - Fits Seq's explicit style **Cons:** - Requires polling - Can miss signals if not checked frequently - User must remember to check ### Option B: Channel-Based (Advanced) Signals delivered as messages on a channel: ```seq # Create a channel that receives signal notifications signal.SIGINT signal.channel # ( Signal -- Channel ) # Use in a strand sig-chan [ "Received SIGINT" io.write-line cleanup-resources ] strand.spawn # Or with select for multiple signals [ int-chan term-chan hup-chan ] channel.select match | signal.SIGINT => [ handle-interrupt ] | signal.SIGTERM => [ handle-terminate ] | signal.SIGHUP => [ reload-config ] end ``` **Pros:** - Fits CSP concurrency model - Can handle signals asynchronously - Composable with other channel operations **Cons:** - More complex implementation (dedicated signal thread) - Higher overhead - Requires strand for async handling ### Option C: Hybrid Approach (Recommended for v2) Combine both for flexibility: ```seq # Simple flag-based (synchronous) signal.SIGINT signal.trap signal.SIGINT signal.received? # Channel-based (asynchronous) signal.SIGHUP signal.channel # Convenience: wait for any of these signals [ signal.SIGINT signal.SIGTERM ] signal.wait # ( Signals -- Signal ) ``` --- ## Implementation Sketch ### Flag-Based Core ```rust // In runtime static SIGNAL_FLAGS: [AtomicBool; 32] = [...]; // One per signal extern "C" fn signal_handler(sig: c_int) { SIGNAL_FLAGS[sig as usize].store(true, Ordering::SeqCst); } #[no_mangle] pub extern "C" fn patch_seq_signal_trap(stack: Stack) -> Stack { let (stack, sig) = pop(stack); let sig_num = sig.as_int(); unsafe { libc::signal(sig_num, signal_handler as _); } stack } #[no_mangle] pub extern "C" fn patch_seq_signal_received(stack: Stack) -> Stack { let (stack, sig) = pop(stack); let sig_num = sig.as_int() as usize; let was_set = SIGNAL_FLAGS[sig_num].swap(false, Ordering::SeqCst); push(stack, Value::Bool(was_set)) } ``` ### Signal Constants ```seq # In stdlib or builtins : signal.SIGINT 2 ; : signal.SIGTERM 15 ; : signal.SIGHUP 1 ; : signal.SIGPIPE 13 ; : signal.SIGUSR1 10 ; : signal.SIGUSR2 12 ; # etc. ``` --- ## Use Cases ### 1. Graceful HTTP Server Shutdown ```seq : main ( -- Int ) signal.SIGINT signal.trap signal.SIGTERM signal.trap signal.SIGPIPE signal.ignore # Don't crash on client disconnect 8080 server-listen server-loop "Goodbye!" io.write-line 0 ; : server-loop ( Server -- Server ) signal.SIGINT signal.received? signal.SIGTERM signal.received? or if return # Exit loop, proceed to cleanup then accept-connection handle-request server-loop ; ``` ### 2. Config Reload on SIGHUP ```seq : main ( -- Int ) signal.SIGHUP signal.trap load-config [ signal.SIGHUP signal.received? if "Reloading config..." io.write-line load-config then process-work ] loop ; ``` ### 3. Worker Process Management ```seq : main ( -- Int ) signal.SIGCHLD signal.trap spawn-workers [ signal.SIGCHLD signal.received? if reap-children respawn-if-needed then coordinate-workers ] loop ; ``` --- ## Open Questions 1. **Signal masking during critical sections?** ```seq signal.SIGINT signal.block # critical code signal.SIGINT signal.unblock ``` 2. **sigaction vs signal?** - `sigaction` is more portable and allows SA_RESTART - Worth the complexity? 3. **Real-time signals (SIGRTMIN+n)?** - Queued, carry data - Overkill for v1? 4. **Windows support?** - Only SIGINT and SIGTERM-ish via SetConsoleCtrlHandler - Abstract behind same API or separate? 5. **Integration with weaves/strands?** - Should `signal.wait` be a yielding operation? - Interrupt blocked channel operations? --- ## Milestones ### v1: Basic Flag API - [ ] Signal constants in stdlib - [ ] `signal.trap`, `signal.received?`, `signal.pending?` - [ ] `signal.default`, `signal.ignore` - [ ] Unix only initially ### v2: Channel API - [ ] `signal.channel` for async handling - [ ] Integration with `channel.select` - [ ] Signal thread for delivery ### v3: Advanced - [ ] Signal masking (`signal.block`, `signal.unblock`) - [ ] Windows support - [ ] `sigaction` flags (SA_RESTART, etc.) --- ## References - POSIX signal handling: https://pubs.opengroup.org/onlinepubs/9699919799/functions/signal.html - Async-signal-safe functions: https://man7.org/linux/man-pages/man7/signal-safety.7.html - Rust signal-hook crate: https://docs.rs/signal-hook/
navicore commented 2026-01-21 16:58:33 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/pull/291
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#288
No description provided.