cjpais/Handy · critical

failed to register transcription signal handlers

Error message

failed to register transcription signal handlers

What it means

Panic on macOS when signal-hook's Signals::new cannot install handlers for SIGUSR1/SIGUSR2. Registration allocates the signal set and installs dispositions; it fails when the process cannot manage signals — another component already owning these user signals incompatibly, sigaction/kqueue registration errors (e.g. EMFILE), or restricted runtimes (sandboxes, some test harnesses) that block signal management.

Solutions

  1. Identify the other owner of SIGUSR1/SIGUSR2 in the process and let exactly one component register them
  2. Degrade gracefully instead of expecting: log a warning and continue — remote control still works via handy --toggle-transcription through the single-instance channel
  3. If a sandbox policy blocks signal management, allow user signals or run without the signal feature

Example fix

// before
#[cfg(target_os = "macos")]
let mut signals =
    Signals::new([SIGUSR1, SIGUSR2]).expect("failed to register transcription signal handlers");

// after — signals are an optional remote control; skip them on failure
#[cfg(target_os = "macos")]
let mut signals = match Signals::new([SIGUSR1, SIGUSR2]) {
    Ok(s) => s,
    Err(e) => {
        log::warn!("signal handlers disabled: {e}");
        return;
    }
};
Defensive patterns

Strategy: fallback

Prevention

When it happens

Trigger: setup_signal_handler at startup on macOS: Signals::new([SIGUSR1, SIGUSR2]) returns Err because a prior in-process handler claims one of the signals incompatibly, or registration fails under sandbox/resource constraints.

Common situations: Embedding Handy's core in another app or test runner that also installs SIGUSR handlers; running under debuggers/profilers that intercept user signals; forks that re-register handlers or call setup twice.

Related errors


AI-assisted analysis of cjpais/Handy@c6fa60da2f (2026-08-17). Data as JSON: /api/errors/1f66a202d7117123. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/signal_handle.rs:38

        c.send_external_input(binding_id, source);
    } else {
        warn!("TranscriptionCoordinator not initialized");
    }
}

/// Listen for Unix signals that remotely toggle transcription.
///
/// SIGUSR2 toggles plain transcription on all Unix platforms. SIGUSR1
/// (transcription with post-processing) is only handled on macOS: on Linux,
/// WebKitGTK's JavaScriptCore garbage collector sends SIGUSR1 to its own
/// threads to suspend them, so handling it caused phantom recordings on every
/// GC cycle (#1660). Linux users should use `handy --toggle-post-process`
/// instead.
#[cfg(unix)]
pub fn setup_signal_handler(app_handle: AppHandle) {
    #[cfg(target_os = "macos")]
    let mut signals =
        Signals::new([SIGUSR1, SIGUSR2]).expect("failed to register transcription signal handlers");
    #[cfg(not(target_os = "macos"))]
    let mut signals =
        Signals::new([SIGUSR2]).expect("failed to register transcription signal handlers");
    #[cfg(target_os = "macos")]
    debug!("Signal handlers registered (SIGUSR1, SIGUSR2)");
    #[cfg(not(target_os = "macos"))]
    debug!("Signal handler registered (SIGUSR2; SIGUSR1 is left to WebKitGTK)");
    thread::spawn(move || {
        for sig in signals.forever() {
            let (binding_id, signal_name) = match sig {
                #[cfg(target_os = "macos")]
                SIGUSR1 => ("transcribe_with_post_process", "SIGUSR1"),
                SIGUSR2 => ("transcribe", "SIGUSR2"),
                _ => continue,
            };
            debug!("Received {signal_name}");
            send_transcription_input(&app_handle, binding_id, signal_name);
        }

View on GitHub (pinned to c6fa60da2f)