[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-93301":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":16,"stars30d":16,"stars90d":15,"forks30d":15,"starsTrendScore":17,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":10,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":10,"pushedAt":10,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":15,"starSnapshotCount":15,"syncStatus":33,"lastSyncTime":34,"discoverSource":35},93301,"sigwire","yeet-src\u002Fsigwire","yeet-src","tail -f for signals. Every signal any process on the box raises — who sent it, who it hit, which signal, how it was raised (kill(2), the kernel, a POSIX timer), whether the target caught it and how long its handler ran, whether it tore a blocked syscall out with EINTR.","https:\u002F\u002Fyeet.cx",null,"JavaScript",141,4,103,0,38,1,61.9,false,"master",true,[23,24,25,26,27,28,29],"ipc","linux","logs","showcase","signals","systems","tail","2026-07-22 04:02:08","# `sigwire`\n\n> **`tail -f` for signals.** Every signal any process on the box raises — who sent it, who it hit, which signal, how it was raised (`kill(2)`, the kernel, a POSIX timer), whether the target caught it and how long its handler ran, whether it tore a blocked syscall out with `EINTR` — decoded off the kernel's signal tracepoints and streamed live to your terminal. No `strace -f` on one pid, no `ptrace`, no cooperation from the processes involved.\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fplatform-Linux-1793D1\" alt=\"Linux\">\n  \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fbuilt%20with-yeet%20%2B%20eBPF-8A2BE2\" alt=\"yeet + eBPF\">\n  \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-Dual%20BSD%2FGPL-3DA639\" alt=\"Dual BSD\u002FGPL\">\n  \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002FJxVseaAVAU\">\u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fchat-Discord-5865F2\" alt=\"Discord\">\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"assets\u002Fsigwire.gif\" alt=\"sigwire streaming live signals as a switchboard in the terminal\">\n\u003C\u002Fp>\n\n**`sigwire` turns the kernel's signal machinery into a live patchbay: each line is `sender ──SIGNAL──▶ target`, coloured by severity, tagged with how it was raised, whether the target **caught** it (and how long its handler ran), whether it **interrupted a blocked syscall** (`↯ EINTR read`), collapsed to `×N` when something spams, and marked `☠` when it's a genuine killing blow.** A side rail tallies what's flying across the wire; pause and pick a row to inspect the full picture — disposition, handler address, `sigaction` flags, and the signals the target was blocking at that instant.\n\nBecause it hooks the kernel's *tracepoints*, not any one process, a single run watches every signal on the host at once — your app, a supervisor, the kernel's own fault machinery — with none of them aware they're being traced.\n\n> [!TIP]\n> **Two sides of every signal.** sigwire watches both `signal:signal_generate` (the *sender's* view — who raised what, the switchboard line) and `signal:signal_deliver` (the *target's* view — did it catch it, with which handler and flags, what was it blocking, and did it interrupt a syscall). Two more hooks — `rt_sigreturn(2)` and the syscall-exit tracepoint — time the handler and catch EINTR. It's all correlated back into one row. This split is also why the `☠ fatal` count is deliberately conservative (see [What counts as fatal](#what-counts-as-fatal)): generation happens before delivery, so the sender side can't know a signal's fate — only the delivery side can, and only for the cases it observes.\n\n## Quick start\n\n```sh\ncurl -fsSL https:\u002F\u002Fyeet.cx | sh              # install the yeet daemon (one time)\nyeet run github:yeet-src\u002Fsigwire             # run the dashboard (the daemon does the privileged BPF load)\n```\n[Manual install guide](https:\u002F\u002Fyeet.cx\u002Fdocs\u002Fmanual-installation) | Linux only\n\nNothing to configure — signals are constant background traffic on any box, so rows start landing at the top immediately. Want to make some yourself? `kill -USR1 \u003Cpid>`, `Ctrl-C` a foreground job, or start any managed runtime and watch its GC\u002Fscheduler ping its own threads (`↯ EINTR futex` scrolling by).\n\n## Controls\n\nThe feed follows the newest signal by default; select a row or pause and it holds still while data keeps flowing underneath.\n\n| key | action |\n| --- | ------ |\n| `p` · `Space` | pause \u002F resume the feed (freeze it to read) |\n| `↑`\u002F`↓`, `k`\u002F`j` | pause and inspect a row — opens the detail panel |\n| `\u002F` | fuzzy filter — matches process, pid, signal, source, and disposition; matched characters highlight live |\n| `e` | filter to **interrupted syscalls only** (`↯ EINTR` \u002F `↺ restarted`) |\n| `s` | open the **signal picker** — mute or show any signal, live |\n| `Esc` | back out one layer — clear the filter \u002F close the picker \u002F drop the selection, then quit |\n| `q` | quit |\n\n## What you're looking at\n\nEach row is one generated signal, newest at the top:\n\n```\n WHEN            SENDER  SIGNAL        TARGET               NOTE\n  now       bash·4402──SIGINT───▶  node·8813        kill(2)  ↯ EINTR read  caught 41µs\n 1.2s    systemd·1──────SIGTERM──▶  nginx·1291       kill(2)  caught 1.2ms\n 3.4s     kernel·8813──SIGSEGV──▶  chrome·8813       fault    default  ☠\n 4.1s   postgres·507──SIGUSR1───▶  postgres·509 ×6  kill(2)  caught 9µs\n```\n\nEach row is one block: the **sender → target** are `comm·pid` (the sender is whoever raised the signal, `current`; the target is who it's aimed at), the **wire** in the middle carries the signal name coloured by severity, `×N` folds a burst of the identical signal into one line, and the **note** on the right gives the source, then any syscall interruption, then the disposition.\n\nEach row is frozen the moment its delivery resolves and never mutates again — so a burst scrolls past as a stable log, not a flickering aggregate.\n\n**The wire is coloured by severity** on the same 256-color palette as the rest of the UI:\n\n| severity | signals | colour |\n|---|---|---|\n| kill | `SIGKILL` | hot red |\n| fatal (core-dumping) | `SEGV` `BUS` `ABRT` `ILL` `FPE` `TRAP` `SYS` `QUIT` | red |\n| terminating | `TERM` `INT` `HUP` `PIPE` `ALRM` … | amber |\n| job control | `STOP` `TSTP` `TTIN` `TTOU` | yellow |\n| continue | `CONT` | green |\n| user | `USR1` `USR2` | cyan |\n| real-time | `SIGRTMIN+n` | violet |\n| housekeeping | `CHLD` `URG` `WINCH` … | grey |\n\nThe **note** is the source (`kill(2)`, `tgkill`, `sigqueue`, `timer`, `kernel`, `fault`); then, if it interrupted a blocked syscall, `↯ EINTR read` (or `↺ restarted read` when `SA_RESTART` auto-resumed it); then the **disposition** — `caught 41µs` (a handler ran, and how long it took), `default` (no handler, the default action applied), or `⊘ ignored`. A `☠` marks a genuine killing blow (see [What counts as fatal](#what-counts-as-fatal)).\n\n> [!NOTE]\n> **`↯ EINTR` is the one to watch.** A signal that lands while a thread is parked in a slow syscall (`read`, `poll`, `accept`, `futex`, `nanosleep`, …) yanks it out: the syscall returns `-1` \u002F `EINTR` and, unless the handler set `SA_RESTART`, it does **not** resume — the app has to retry. Forgetting that is a classic, maddening, timing-dependent bug (\"why did my `read()` fail *once*?\"). sigwire shows it happening, live, and which syscall took the hit. Press **`e`** to hide everything else and watch only the interrupts.\n\nThe rail on the right is the aggregate view: **top signals** by volume, a breakdown **by source**, and a **delivery** tally — how many signals were caught vs. hit their default vs. ignored.\n\n## Inspect a signal\n\nPress `↑`\u002F`↓` (or `p`) to freeze the feed and select a row; the rail turns into a detail panel with everything the delivery side knows about that exact signal:\n\n```\n SIGNAL\n  SIGUSR1  (10)  user\n from    ctarget·3980913\n to      ctarget·3980913\n RAISED\n  via     tgkill\n  code    SI_TKILL\n  scope   thread\n  result  delivered\n DELIVERY\n  handled caught\n  syscall EINTR ← read\n  handler 0x55f0a1c3\n  ran     3.0ms\n  flags   SA_SIGINFO\n TARGET BLOCKS\n  SIGINT SIGQUIT SIGTERM\n```\n\n- **handled** — `caught` (ran a userspace handler), `default` (→ the default action: terminate \u002F core dump \u002F stop \u002F ignore), or `ignored`.\n- **syscall** — if this signal interrupted a blocked syscall: `EINTR ← read` (userspace saw `EINTR`) or `restarted read` (`SA_RESTART` resumed it transparently).\n- **ran** — how long the handler executed, measured from delivery to the `rt_sigreturn(2)` that ends it. (Runtimes that only latch a flag in the C handler and do the real work later — CPython, Go — show a tiny time here; that's them, not sigwire.)\n- **flags** — the `sigaction` flags on the handler (`SA_RESTART`, `SA_SIGINFO`, `SA_NODEFER`, …).\n- **TARGET BLOCKS** — the signals the target had blocked (its `sigprocmask`) at the moment of delivery, straight off its `task_struct`.\n\n`Esc` closes the inspector; `p` resumes the live feed.\n\n## What counts as fatal\n\nThe `☠ fatal` counter and the `☠` row badge are intentionally strict. Because `signal_generate` fires at *generation*, sigwire can't see whether the target installed a handler — a `SIGTERM` might be caught and turned into a clean shutdown, or ignored entirely. So it only counts a death when it's unambiguous:\n\n- **`SIGKILL`** delivered — uncatchable, unignorable, always fatal; **or**\n- a **core-dumping signal** (`SEGV`\u002F`BUS`\u002F`ABRT`\u002F`ILL`\u002F`FPE`\u002F`TRAP`\u002F`SYS`\u002F`QUIT`) that the **kernel itself raised** (a synchronous fault, not a userspace `kill`).\n\nEverything else — a `SIGTERM` from `systemd`, a `SIGINT` from your `Ctrl-C`, a runtime's `SIGPWR` to its own threads — is shown and coloured, but not counted as a death, because it probably wasn't one.\n\n## The signal picker (a live kernel knob)\n\nThree signals are pure background hum on any busy box: `SIGCHLD` (every child reap), `SIGURG` (Go's async-preemption heartbeat), and `SIGWINCH` (terminal resizes, broadcast to every foreground process). sigwire mutes those three **in the kernel** by default so the feed is the interesting traffic — but which signals are noise is your call.\n\nPress `s` to open the **signal picker**: a modal list of every signal with its live severity colour and how many you've seen, each toggleable between `shown` and `muted`. Arrow to one (or **type its number** — `1`, `5` → jump to 15) and hit `space`, and that signal flips instantly. `a` toggles them **all** at once. The titlebar's `muted` count tracks how many are hidden.\n\nThis is the two-way half of the demo: the mute mask is a `__u64` global in the running BPF program's `.data` section, and toggling a row patches the matching bit through `DataSec.patch()` while the program keeps running. The kernel drops muted signals before they ever reach the ring buffer, so muting costs you nothing — and unmuting brings a signal back mid-stream with no reload.\n\n## How it works\n\nThe core is [`src\u002Fbpf\u002Fsigwire.bpf.c`](src\u002Fbpf\u002Fsigwire.bpf.c) + [`src\u002Fbpf\u002Fdeliver.bpf.c`](src\u002Fbpf\u002Fdeliver.bpf.c) (kernel, linked into one object) and [`src\u002Fprobes\u002Fsigwire.js`](src\u002Fprobes\u002Fsigwire.js) (userspace). Everything is correlated by `(target tid, signal)`.\n\n### The BPF side\n\nTwo source files link into a single loadable object, `bin\u002Fprobe.bpf.o`, with four tracepoint programs:\n\n| Program | Attached to | What it captures |\n|---|---|---|\n| `on_signal_generate` | `signal:signal_generate` | the sender (`current`) + target (`comm`\u002F`pid`), the signal, `si_code`, `group` flag, `result` — dropped in-kernel if the signal's bit is set in the live `mute_mask` |\n| `on_signal_deliver` | `signal:signal_deliver` | the target's disposition (`sa_handler`), `sa_flags`, and — off `task_struct` — its `blocked` sigset; stamps delivery for handler timing |\n| (rt_sigreturn) | `syscalls:sys_enter_rt_sigreturn` | diffs against the stamped delivery for the handler's run time |\n| (sys_exit) | `raw_syscalls:sys_exit` | records the rare `-ERESTART*` return so the next `signal_deliver` resolves it into `EINTR`\u002F`restarted` + the interrupted syscall number |\n\nMaps connect kernel to userspace:\n\n- `events` — `RINGBUF`, one `signal_event` per generation.\n- `dispatch` — `RINGBUF`, one `dispatch_event` per delivery \u002F handler-return.\n- `mute_mask` — a `__u64` global in the `.data` section; the picker patches individual bits to drop signals in-kernel.\n- `handler_start` \u002F `restart_pending` — `HASH` keyed by tid, per-thread scratch that pairs a delivery with its `rt_sigreturn`, and a syscall's `-ERESTART*` exit with the delivery that follows.\n\n### The JS side\n\n| file | responsibility |\n|---|---|\n| [`src\u002Fprobes\u002Fprobe.js`](src\u002Fprobes\u002Fprobe.js) | loads `bin\u002Fprobe.bpf.o` once, binds the maps, starts the programs (they auto-attach) |\n| [`src\u002Fprobes\u002Fsigwire.js`](src\u002Fprobes\u002Fsigwire.js) | the only BPF-aware data module: folds both ring buffers into a rolling feed with tallies, correlates delivery onto generation, owns the mute-mask knob — exposes the `feed`, `visible`, `muteMask` signals |\n| [`src\u002Fmain.jsx`](src\u002Fmain.jsx) | composition root: input, selection, responsive layout (rail hides on narrow terminals), `mount` |\n| [`src\u002Fcomponents\u002Ffeed.jsx`](src\u002Fcomponents\u002Ffeed.jsx) | the switchboard: `sender ──SIG──▶ target`, disposition\u002Flatency, badges, tint, coalescing |\n| [`src\u002Fcomponents\u002Ftally.jsx`](src\u002Fcomponents\u002Ftally.jsx) | the side rail — top signals, breakdown by source, delivery tally |\n| [`src\u002Fcomponents\u002Fdetail.jsx`](src\u002Fcomponents\u002Fdetail.jsx) | the inspector — per-signal disposition, handler, flags, blocked mask |\n| [`src\u002Fcomponents\u002Fpicker.jsx`](src\u002Fcomponents\u002Fpicker.jsx) | the signal picker modal — mutes\u002Fshows each signal via the kernel mute mask |\n| [`src\u002Fcomponents\u002Ftitlebar.jsx`](src\u002Fcomponents\u002Ftitlebar.jsx) | brand, live rate, totals, the `☠ fatal` counter, muted count, live\u002Fpaused |\n| [`src\u002Fcomponents\u002Ffooter.jsx`](src\u002Fcomponents\u002Ffooter.jsx) | key hints and the live filter prompt |\n| [`src\u002Flib\u002Fsignals.js`](src\u002Flib\u002Fsignals.js) | the one source of truth: name, severity, colour, `si_code` → source, disposition, flags, mask decode, fatality |\n| [`src\u002Flib\u002Fformat.js`](src\u002Flib\u002Fformat.js) | pure formatters — padding, truncation, `ago()`, durations, compact counts |\n| [`src\u002Flib\u002Ffuzzy.js`](src\u002Flib\u002Ffuzzy.js) | subsequence fuzzy match over process + pid + signal + source + disposition |\n\nThe model is a rolling **feed of generated signals**, coalescing identical repeats into `×N` rows. A signal's generation row is frozen the instant its delivery resolves — so a row already on screen never changes or jumps. A 120 ms window timer publishes one snapshot per frame, so a busy ring buffer costs one re-render, not thousands.\n\n### Why tracepoints, not `strace`\u002F`ptrace`\n\n`strace -f` follows one process tree and stops the tracee on every event; `ptrace` is per-target and intrusive. The signal tracepoints are the seam where the *kernel* raises and delivers a signal, for *every* process, with no per-app setup and no stopping anyone. Pairing generation ↔ delivery ↔ `rt_sigreturn` is what yields the sender\u002Ftarget pair, the disposition, per-handler latency, and the EINTR verdict that tie a signal's whole life together.\n\n## Testing across kernels\n\n`make veristat` loads `bin\u002Fprobe.bpf.o` with veristat on **your** kernel — a quick check that every program passes the verifier, plus per-program complexity (insns\u002Fstates). Loading BPF needs privileges, so use `sudo`.\n\nA program that loads on your laptop can be rejected by an older kernel's verifier. [`.github\u002Fworkflows\u002Fkernel-matrix.yml`](.github\u002Fworkflows\u002Fkernel-matrix.yml) guards against that: for each kernel in its matrix it builds the object, boots that kernel in a VM ([cilium's little-vm-helper](https:\u002F\u002Fgithub.com\u002Fcilium\u002Flittle-vm-helper), images from `quay.io\u002Flvh-images`), and runs the vendored static **veristat** against it — failing the job if the verifier rejects any program, and pivoting the per-kernel results into one ✅\u002F❌ grid. The in-VM gate is [`build\u002Fverify-kernel.sh`](build\u002Fverify-kernel.sh).\n\nRun the same matrix locally (Linux + KVM) with `make veristat-matrix` — it boots the kernel images with `lvh` + QEMU and prints an `ok`\u002F`FAIL` grid. Pick kernels with `make veristat-matrix KERNELS=\"6.6 bpf-next\"`.\n\n## Requirements\n\n> [!IMPORTANT]\n> - **A Linux kernel with BTF** (`CONFIG_DEBUG_INFO_BTF`) for CO-RE — `bpftool` generates `src\u002Fbpf\u002Finclude\u002Fvmlinux.h` from it. Default on current Arch, Fedora, Ubuntu, and Debian (every mainstream distro kernel since ~5.4).\n> - **The yeet daemon**, which performs the privileged BPF load. The BPF capabilities are delegated to a daemonized process, so `sigwire` itself runs unprivileged. `curl -fsSL https:\u002F\u002Fyeet.cx | sh` installs it.\n>\n> To build from source you also need `clang` and `bpftool` — but the vendored static toolchain supplies them, so you don't need a system C\u002FBPF toolchain. No node\u002Fnpm: esbuild is vendored too and the project has no third-party deps.\n\n## Honest caveats\n\n> [!NOTE]\n> `sigwire` is observability, not enforcement. It shows you what was raised; it does not block, delay, or alter any signal.\n\n- **A row is a *raised* signal.** The switchboard line comes from generation; the target may catch it, block it, or have already exited. The disposition\u002Fhandler\u002Fmask columns come from the *delivery* side and fill in only once the kernel actually delivers it — a blocked or still-pending signal shows no disposition. See [What counts as fatal](#what-counts-as-fatal).\n- **Correlation is best-effort.** Generation and delivery are separate tracepoints with no shared id, matched on `(target tid, signal)` within a time window. Under a storm of the same signal to the same thread the pairing can smear; it's right in the overwhelming common case.\n- **Handler timing measures the kernel's frame, not your intent.** `ran` is delivery → `rt_sigreturn`. A handler that only sets a flag (CPython, Go's runtime) returns in microseconds even if the \"real\" work happens later in the event loop — accurate, just not what you might expect.\n- **EINTR detection watches every syscall exit.** Catching interrupted syscalls means attaching to `raw_syscalls:sys_exit`, which fires on *every* syscall return system-wide (the handler bails immediately on all but the rare `-ERESTART*` codes, so the added cost is a couple of instructions per syscall — but it is not zero). Syscall *names* are an x86-64 table; other arches show the raw syscall number.\n- **The sender of a kernel signal is `current`.** For a synchronous fault (`SIGSEGV` from a bad access) that's the faulting task itself — correct and useful. For an asynchronous kernel signal, `current` is whatever task was running when the kernel raised it, which is a hint, not gospel.\n- **Real-time signal numbering is nominal.** `SIGRTMIN+n` is shown by raw offset; libraries reserve the low few for their own use.\n- **`comm` is 16 bytes.** Long process names are truncated by the kernel, not by sigwire.\n\n## Community questions\n\n**Does it slow the traced processes down?**\nNo meaningful overhead. The tracepoint programs are passive; the cost is a bounded ring-buffer write per signal (and the couple of instructions per syscall exit for EINTR detection), and the ring buffer drops rather than blocks if userspace falls behind.\n\n**Will it show signals aimed at a process that was already running when I start it?**\nYes. The tracepoints fire for every signal from the moment sigwire attaches, regardless of when the sender or target started — there's no per-process state to have missed.\n\n**Does it work for any process, or just one?**\nAny process on the host, all at once — the sender\u002Ftarget gutter tells them apart. It's the whole machine's signal traffic, not one pid.\n\n**Can I export the feed?**\nNot built in. The `RingBuf.subscribe` callbacks in `probes\u002Fsigwire.js` hold every decoded record, so a JSON\u002FHTTP\u002FKafka sink is a branch there. To set up a managed pipeline, [contact us](https:\u002F\u002Fyeet.cx\u002F).\n\n## Building from source\n\n```sh\nmake          # clang + bpftool → bin\u002Fprobe.bpf.o ; esbuild → src\u002Findex.jsx\nmake bpf      # just the BPF object\nmake bundle   # just the JS bundle\nmake clean    # remove build artifacts\n```\n\nThen `yeet run .` runs the local build. `make` runs two independent compilers: **clang + bpftool** link `src\u002Fbpf\u002F*.bpf.c` into the loadable object `bin\u002Fprobe.bpf.o`; **esbuild** bundles `src\u002Fmain.jsx` into `src\u002Findex.jsx`, resolving the `@\u002F` (source root) and `#\u002F` (project root) **bundle-time aliases** via tsconfig `paths` and leaving `yeet:*` builtins external. Both compilers come from a vendored static toolchain, so the build needs no system C\u002FBPF toolchain and no node\u002Fnpm. The generated `vmlinux.h`, `src\u002Findex.jsx`, and `bin\u002F*.bpf.o` are build artifacts.\n\nBecause the aliases are bundle-time only, the runtime locates the BPF object with `import.meta.dirname` rather than an alias. See [`AGENTS.md`](AGENTS.md) (aka `CLAUDE.md`) for the yeet dashboard-authoring guide.\n\n## License\n\nDual BSD\u002FGPL. The BPF program declares `char LICENSE[] SEC(\"license\") = \"Dual BSD\u002FGPL\"` in [`src\u002Fbpf\u002Fsigwire.bpf.c`](src\u002Fbpf\u002Fsigwire.bpf.c), which the kernel requires for the helpers it uses.\n\n---\n\nBuilt with [yeet](https:\u002F\u002Fyeet.cx\u002Fdocs\u002F), a JS runtime for writing eBPF programs on Linux. Join us on [Discord](https:\u002F\u002Fdiscord.gg\u002FJxVseaAVAU).\n","sigwire 是一个基于 eBPF 的 Linux 系统信号实时监控工具，提供类似 `tail -f` 的流式信号观测能力。它通过内核 tracepoints 零侵入地捕获全系统所有进程的信号收发事件，精确记录发送方、接收方、信号类型、触发方式（kill、内核、POSIX timer）、是否被处理、信号处理器执行时长，以及是否导致 EINTR 中断。无需 ptrace 或目标进程配合，支持实时着色、聚合与深度详情查看。适用于系统调试、信号行为分析、服务稳定性排查及内核级 IPC 故障诊断等场景。",2,"2026-07-15 02:30:12","CREATED_QUERY"]