Skip to content

fix(linux): replace SIGUSR1/SIGUSR2 with SIGRTMIN+1/+2 to avoid WebKit GC conflict#1267

Open
muriloime wants to merge 2 commits into
cjpais:mainfrom
muriloime:fix/linux-sigusr-webkit-gc-conflict
Open

fix(linux): replace SIGUSR1/SIGUSR2 with SIGRTMIN+1/+2 to avoid WebKit GC conflict#1267
muriloime wants to merge 2 commits into
cjpais:mainfrom
muriloime:fix/linux-sigusr-webkit-gc-conflict

Conversation

@muriloime
Copy link
Copy Markdown

Problem

On Linux, Handy triggers ghost transcriptions at predictable intervals after every startup:

  • ~2 minutes 2 seconds after launch
  • ~6 minutes 49 seconds after the first ghost trigger

This happens even with no keyboard input.

Root Cause

WebKitGTK (embedded by Tauri as the webview engine on Linux) uses SIGUSR1 internally for JavaScriptCore's garbage collector "stop the world" mechanism — it suspends GC threads via pthread_kill(thread, SIGUSR1).

Because signal-hook's self-pipe file descriptor is inherited by WebKit child processes on fork, those intra-process GC signals leak back into the parent Handy process and are misinterpreted as user-triggered transcription commands.

The exact timing is determined by JSC's GC timer formula in GCActivityCallback.cpp:

delay = lastGCLength(10ms) / ((heapMB) × 0.0003125)
  • First trigger: heap ~268 KB → delay = 10ms / (0.262 × 0.0003125) = 122 seconds
  • Second trigger: heap ~80 KB after compaction → 409 seconds later

The double signal (always two SIGUSR1 in the same second) comes from the two WebKitWebProcess children both running JSC GC simultaneously.

Fix

Switch from SIGUSR1/SIGUSR2 to POSIX real-time signals SIGRTMIN+1 and SIGRTMIN+2, which WebKit/JSC does not use. Real-time signals are specifically designed for application-level IPC and avoid conflicts with standard signals repurposed by system libraries.

Changes:

  • src-tauri/Cargo.toml: add libc = "0.2" under [target.'cfg(unix)'.dependencies]
  • src-tauri/src/lib.rs: register SIGRTMIN+1/SIGRTMIN+2 instead of SIGUSR1/SIGUSR2
  • src-tauri/src/signal_handle.rs: update handler to match on the runtime RT signal values

Migration

Users with raw signal shortcuts (e.g. pkill -USR1 handy / pkill -USR2 handy) should switch to the CLI flags instead:

handy --toggle-transcription      # was: pkill -USR2 -n handy
handy --toggle-post-process       # was: pkill -USR1 -n handy

These already use D-Bus via tauri-plugin-single-instance and require no signal at all.

Evidence

Handy log showing the ghost triggers every session, exactly 122s and 409s after startup:

[07:52:51] Signal handlers registered (SIGUSR1, SIGUSR2)
[07:54:53] Received SIGUSR1  ← 122s after start, no key pressed
[07:54:53] Received SIGUSR1  ← double signal from 2 WebKit children
[08:01:42] Received SIGUSR1  ← 409s later

Consistent across 10+ sessions in the log history.

🤖 Generated with Claude Code

…t GC conflict

WebKitGTK (embedded by Tauri as the webview engine) uses SIGUSR1 internally
for JavaScriptCore's garbage collector "stop the world" thread suspension via
pthread_kill. Because signal-hook's self-pipe file descriptor is inherited by
WebKit child processes on fork, those intra-process GC signals leak back into
the parent Handy process and are misinterpreted as user-triggered transcription.

This causes ghost transcription activations at predictable intervals after every
startup: ~2 min (first JSC full GC, heap ~268 KB) and ~7 min later (second GC
after heap compaction, ~80 KB), with the exact timing determined by JSC's GC
timer formula in GCActivityCallback.cpp:
  delay = lastGCLength(10ms) / ((heapMB) × 0.0003125)

Fix: switch to POSIX real-time signals SIGRTMIN+1 (transcribe_with_post_process)
and SIGRTMIN+2 (transcribe), which WebKit/JSC does not use.

Migration note: users with `pkill -USR1 handy` / `pkill -USR2 handy` shortcuts
should switch to `handy --toggle-post-process` / `handy --toggle-transcription`
(D-Bus path, no raw signals needed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 10, 2026

I don't think this really can be merged without disrupting quite a lot of people

Maybe SIGUSR1 can be changed but otherwise this is a breaking change

@muriloime
Copy link
Copy Markdown
Author

I will try to come up with another approach then

…mpat

Keep SIGUSR2 → transcribe unchanged (backward compatible for existing shortcuts).
Only change SIGUSR1 → SIGRTMIN+1 for transcribe_with_post_process, since SIGUSR1
is the signal WebKit/JSC repurposes for GC thread suspension.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@muriloime
Copy link
Copy Markdown
Author

muriloime commented Apr 10, 2026

Updated. SIGUSR2 is kept untouched — only SIGUSR1 is replaced with SIGRTMIN+1, since SIGUSR1 is specifically the signal WebKit/JSC repurposes for GC. SIGUSR2 users are unaffected.

@cjpais do you think this less intrusive approach is ok?

@muriloime
Copy link
Copy Markdown
Author

@cjpais Forgot to mention, but of course I stopped seing Handy automatically starting at 122s , and 409s on each Handy start. It was quite annoying ( weirdly this happened only on 1 of my two linux boxes)

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 11, 2026

Thanks for the info, I will take a look and think more on this soon. Just give me some time, I am working on some parallel stuff which is higher priority for me at the moment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants