
Strumento di osservabilità dei segnali del kernel in tempo reale che utilizza eBPF tracepoint per trasmettere ogni segnale generato su un host Linux, mostrando in tempo reale mittente, destinatario, disposizione, latenza del gestore e interruzioni delle chiamate di sistema.
sigwire
tail -fper i segnali. Ogni segnale lanciato da qualsiasi processo sulla macchina — chi lo ha inviato, chi lo ha ricevuto, quale segnale, come è stato lanciato (kill(2), il kernel, un timer POSIX), se il destinatario lo ha catturato e per quanto tempo il suo handler è stato eseguito, se ha strappato una chiamata di sistema bloccata conEINTR— decodificato dai tracepoint dei segnali del kernel e trasmesso in diretta sul tuo terminale. Nientestrace -fsu un singolo pid, nienteptrace, nessuna cooperazione dai processi coinvolti.
sigwire trasforma il meccanismo dei segnali del kernel in un patchbay live: ogni riga è sender ──SIGNAL──▶ target, colorata per gravità, etichettata con come è stato lanciato, se il target lo ha catturato (e per quanto tempo il suo handler è stato eseguito), se ha interrotto una chiamata di sistema bloccata (↯ EINTR read), compresso in ×N quando qualcosa invia ripetutamente, e contrassegnato con ☠ quando è un vero colpo letale. Un pannello laterale conta cosa sta volando sul cavo; metti in pausa e scegli una riga per ispezionare il quadro completo — disposizione, indirizzo dell'handler, flag di sigaction e i segnali che il target stava bloccando in quell'istante.
Poiché si aggancia ai tracepoint del kernel, non a un singolo processo, una singola esecuzione osserva ogni segnale sull'host in una volta — la tua applicazione, un supervisore, il meccanismo di fault del kernel stesso — senza che nessuno di loro sappia di essere tracciato.
[!TIP] Due lati di ogni segnale. sigwire osserva sia
signal:signal_generate(la vista del mittente — chi ha lanciato cosa, la linea del centralino) siasignal:signal_deliver(la vista del destinatario — lo ha catturato, con quale handler e flag, cosa stava bloccando, e ha interrotto una chiamata di sistema). Altri due hook —rt_sigreturn(2)e il tracepoint di uscita dalla chiamata di sistema — misurano il tempo dell'handler e catturano EINTR. Tutto è correlato in una singola riga. Questa divisione è anche il motivo per cui il conteggio☠ fatalè deliberatamente conservativo (vedi Cosa viene considerato fatale): la generazione avviene prima della consegna, quindi il lato mittente non può conoscere il destino del segnale — solo il lato di consegna può, e solo per i casi che osserva.
curl -fsSL https://yeet.cx | sh # install the yeet daemon (one time) yeet run github:yeet-src/sigwire # run the dashboard (the daemon does the privileged BPF load)
[Guida all'installazione manuale](https://yeet.cx/docs/manual-installation) | Solo Linux
Niente da configurare — i segnali sono traffico di fondo costante su qualsiasi macchina, quindi le righe iniziano ad arrivare immediatamente in cima. Vuoi crearne qualcuno da solo? `kill -USR1 <pid>`, `Ctrl-C` su un job in foreground, o avvia un runtime gestito e guarda il suo GC/scheduler fare ping ai propri thread (`↯ EINTR futex` che scorrono).
## Controlli
Il feed segue il segnale più recente per impostazione predefinita; seleziona una riga o metti in pausa e rimane fermo mentre i dati continuano a fluire sotto.
| key | action |
| --- | ------ |
| `p` · `Space` | metti in pausa / riprendi il feed (congelalo per leggere) |
| `↑`/`↓`, `k`/`j` | metti in pausa e ispeziona una riga — apre il pannello dei dettagli |
| `/` | filtro fuzzy — corrisponde a processo, pid, segnale, sorgente e disposizione; i caratteri corrispondenti si evidenziano in tempo reale |
| `e` | filtra solo le **syscall interrotte** (`↯ EINTR` / `↺ riavviate`) |
| `s` | apri il **selettore di segnali** — silenzia o mostra qualsiasi segnale, in tempo reale |
| `Esc` | torna indietro di un livello — cancella il filtro / chiudi il selettore / abbandona la selezione, poi esci |
| `q` | esci |
## Cosa stai guardando
Ogni riga è un segnale generato, il più recente in cima:```
WHEN SENDER SIGNAL TARGET NOTE
now bash·4402──SIGINT───▶ node·8813 kill(2) ↯ EINTR read caught 41µs
1.2s systemd·1──────SIGTERM──▶ nginx·1291 kill(2) caught 1.2ms
3.4s kernel·8813──SIGSEGV──▶ chrome·8813 fault default ☠
4.1s postgres·507──SIGUSR1───▶ postgres·509 ×6 kill(2) caught 9µs
Ogni riga è un blocco: il mittente → destinatario sono comm·pid (il mittente è chi ha generato il segnale, current; il destinatario è a chi è rivolto), il cavo al centro porta il nome del segnale colorato per gravità, ×N comprime un burst dello stesso segnale in una riga, e la nota a destra fornisce la sorgente, poi eventuali interruzioni di syscall, poi la disposizione.
Ogni riga viene congelata nel momento in cui la sua consegna viene risolta e non muta mai più — quindi un burst scorre come un log stabile, non un aggregato tremolante.
Il cavo è colorato per gravità sulla stessa tavolozza a 256 colori del resto dell'interfaccia utente:
La nota è la sorgente (kill(2), tgkill, sigqueue, timer, kernel, fault); poi, se ha interrotto una syscall bloccata, ↯ EINTR read (o ↺ restarted read quando SA_RESTART l'ha ripresa automaticamente); poi la disposizione — caught 41µs (un handler è stato eseguito, e per quanto tempo), default (nessun handler, azione predefinita applicata), o ⊘ ignored. Un ☠ segna un colpo letale reale (vedi Cosa conta come fatale).
[!NOTE]
↯ EINTRè ciò che si deve osservare. Un segnale che arriva mentre un thread è bloccato in una syscall lenta (read,poll,accept,futex,nanosleep, …) lo tira fuori: la syscall restituisce-1/EINTRe, a meno che l'handler non abbia impostatoSA_RESTART, non riprende — l'app deve riprovare. Dimenticarlo è un bug classico, frustrante e dipendente dal tempismo ("perché la miaread()ha fallito una volta?"). sigwire lo mostra mentre accade, in tempo reale, e quale syscall ha subito l'interruzione. Premieper nascondere tutto il resto e guardare solo le interruzioni.
La barra a destra è la vista aggregata: segnali principali per volume, una ripartizione per sorgente, e un conteggio di consegna — quanti segnali sono stati catturati vs. hanno raggiunto il default vs. ignorati.
Premere ↑/↓ (o p) per bloccare il flusso e selezionare una riga; la barra si trasforma in un pannello di dettaglio con tutto ciò che il lato di consegna sa su quel segnale esatto:```
SIGNAL
SIGUSR1 (10) user
from ctarget·3980913
to ctarget·3980913
RAISED
via tgkill
code SI_TKILL
scope thread
result delivered
DELIVERY
handled caught
syscall EINTR ← read
handler 0x55f0a1c3
ran 3.0ms
flags SA_SIGINFO
TARGET BLOCKS
SIGINT SIGQUIT SIGTERM
- **handled** — `caught` (ha eseguito un gestore nello spazio utente), `default` (→ l'azione predefinita: termina / core dump / arresta / ignora), o `ignored`.
- **syscall** — se questo segnale ha interrotto una syscall bloccata: `EINTR ← read` (lo spazio utente ha visto `EINTR`) o `restarted read` (`SA_RESTART` l'ha ripresa trasparentemente).
- **ran** — per quanto tempo il gestore è stato eseguito, misurato dalla consegna fino alla `rt_sigreturn(2)` che lo conclude. (I runtime che si limitano a impostare un flag nel gestore C e svolgono il lavoro reale dopo — CPython, Go — mostrano un tempo minimo qui; è colpa loro, non di sigwire.)
- **flags** — i flag `sigaction` sul gestore (`SA_RESTART`, `SA_SIGINFO`, `SA_NODEFER`, …).
- **TARGET BLOCKS** — i segnali che il target aveva bloccato (il suo `sigprocmask`) al momento della consegna, direttamente dal suo `task_struct`.
`Esc` chiude l'ispettore; `p` riprende il feed live.
## Cosa conta come fatale
Il contatore `☠ fatal` e il badge di riga `☠` sono intenzionalmente rigorosi. Poiché `signal_generate` scatta al momento della *generazione*, sigwire non può vedere se il target ha installato un gestore — un `SIGTERM` potrebbe essere intercettato e trasformato in uno spegnimento pulito, o ignorato del tutto. Quindi conta una morte solo quando è inequivocabile:
- **`SIGKILL`** consegnato — non intercettabile, non ignorabile, sempre fatale; **o**
- un **segnale che genera core dump** (`SEGV`/`BUS`/`ABRT`/`ILL`/`FPE`/`TRAP`/`SYS`/`QUIT`) che è stato **sollevato dal kernel stesso** (un errore sincrono, non una `kill` dallo spazio utente).
Tutto il resto — un `SIGTERM` da `systemd`, un `SIGINT` dal tuo `Ctrl-C`, un `SIGPWR` di un runtime ai propri thread — viene mostrato e colorato, ma non conteggiato come morte, perché probabilmente non lo è.
## Il selettore di segnali (una manopola del kernel in tempo reale)
Tre segnali sono puro rumore di fondo su qualsiasi sistema impegnato: `SIGCHLD` (ogni raccolta di figli), `SIGURG` (il battito di prelazione asincrona di Go) e `SIGWINCH` (ridimensionamenti del terminale, trasmessi a ogni processo in primo piano). sigwire silenzia questi tre **nel kernel** per impostazione predefinita, così il feed è il traffico interessante — ma stabilire quali segnali siano rumore spetta a te.
Premi `s` per aprire il **selettore di segnali**: un elenco modale di ogni segnale con il suo colore di severità in tempo reale e quanti ne hai visti, ognuno commutable tra `shown` e `muted`. Spostati con le frecce (o **digita il suo numero** — `1`, `5` → vai a 15) e premi `space`, e quel segnale si commuta istantaneamente. `a` li commuta **tutti** in una volta. Il conteggio `muted` nella barra del titolo tiene traccia di quanti sono nascosti.
Questa è la metà interattiva della demo: la maschera di silenziamento è un globale `__u64` nella sezione `.data` del programma BPF in esecuzione, e commutare una riga modifica il bit corrispondente tramite `DataSec.patch()` mentre il programma continua a funzionare. Il kernel scarta i segnali silenziati prima che raggiungano il ring buffer, quindi silenziare non costa nulla — e riattivare riporta un segnale in mezzo al flusso senza ricaricare.
## Come funziona
Il nucleo è [`src/bpf/sigwire.bpf.c`](https://github.com/yeet-src/sigwire/blob/HEAD/src/bpf/sigwire.bpf.c) + [`src/bpf/deliver.bpf.c`](https://github.com/yeet-src/sigwire/blob/HEAD/src/bpf/deliver.bpf.c) (kernel, collegati in un unico oggetto) e [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/sigwire.js) (spazio utente). Tutto è correlato da `(target tid, signal)`.
### Il lato BPF
Due file sorgente si collegano in un unico oggetto caricabile, `bin/probe.bpf.o`, con quattro programmi tracepoint:
| Programma | Collegato a | Cosa cattura |
|---|---|---|
| `on_signal_generate` | `signal:signal_generate` | il mittente (`current`) + target (`comm`/`pid`), il segnale, `si_code`, flag `group`, `result` — scartato nel kernel se il bit del segnale è impostato nella `mute_mask` live |
| `on_signal_deliver` | `signal:signal_deliver` | la disposizione del target (`sa_handler`), `sa_flags`, e — dal `task_struct` — il suo sigset `blocked`; timbra la consegna per il timing del gestore |
| (rt_sigreturn) | `syscalls:sys_enter_rt_sigreturn` | differenza rispetto alla consegna timbrata per il tempo di esecuzione del gestore |
| (sys_exit) | `raw_syscalls:sys_exit` | registra il raro ritorno `-ERESTART*` in modo che la successiva `signal_deliver` lo risolva in `EINTR`/`restarted` + il numero di syscall interrotto |
Le mappe collegano kernel e spazio utente:
- `events` — `RINGBUF`, un `signal_event` per generazione.
- `dispatch` — `RINGBUF`, un `dispatch_event` per consegna / ritorno dal gestore.
- `mute_mask` — un globale `__u64` nella sezione `.data`; il selettore modifica singoli bit per scartare segnali nel kernel.
- `handler_start` / `restart_pending` — `HASH` con chiave tid, area di lavoro per-thread che abbina una consegna con la sua `rt_sigreturn`, e l'uscita `-ERESTART*` di una syscall con la consegna successiva.
### Il lato JS
| file | responsabilità |
|---|---|
| [`src/probes/probe.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/probe.js) | carica `bin/probe.bpf.o` una volta, collega le mappe, avvia i programmi (si auto-collegano) |
| [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/sigwire.js) | l'unico modulo dati consapevole di BPF: fonde entrambi i ring buffer in un feed scorrevole con conteggi, correla la consegna alla generazione, possiede la manopola mute-mask — espone i segnali `feed`, `visible`, `muteMask` |
| [`src/main.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/main.jsx) | radice di composizione: input, selezione, layout responsive (la barra laterale si nasconde su terminali stretti), `mount` |
| [`src/components/feed.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/feed.jsx) | il quadro di comando: `mittente ──SIG──▶ target`, disposizione/latenza, badge, tonalità, coalescenza |
| [`src/components/tally.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/tally.jsx) | la barra laterale — segnali principali, ripartizione per sorgente, conteggio consegne |
| [`src/components/detail.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/detail.jsx) | l'ispettore — disposizione per segnale, gestore, flag, maschera di blocco |
| [`src/components/picker.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/picker.jsx) | il modale del selettore di segnali — silenzia/mostra ogni segnale tramite la maschera di silenziamento del kernel |
| [`src/components/titlebar.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/titlebar.jsx) | marchio, frequenza live, totali, il contatore `☠ fatal`, conteggio silenziati, live/pausa |
| [`src/components/footer.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/footer.jsx) | suggerimenti sui tasti e il prompt del filtro live |
| [`src/lib/signals.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/signals.js) | l'unica fonte di verità: nome, severità, colore, `si_code` → sorgente, disposizione, flag, decodifica maschera, fatalità |
| [`src/lib/format.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/format.js) | formattatori puri — padding, troncamento, `ago()`, durate, conteggi compatti |
| [`src/lib/fuzzy.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/fuzzy.js) | fuzzy match per sottosequenza su processo + pid + segnale + sorgente + disposizione |
Il modello è un **feed scorrevole di segnali generati**, che fonde ripetizioni identiche in righe `×N`. La riga di generazione di un segnale viene congelata nell'istante in cui la sua consegna si risolve — quindi una riga già a schermo non cambia mai né salta. Un timer a finestra di 120 ms pubblica un'istantanea per frame, quindi un ring buffer affollato costa un solo re-render, non migliaia.
### Perché tracepoint, non `strace`/`ptrace`
`strace -f` segue un albero di processi e ferma il tracee a ogni evento; `ptrace` è per-target e intrusivo. I tracepoint dei segnali sono il punto in cui il *kernel* solleva e consegna un segnale, per *ogni* processo, senza configurazione per-app e senza fermare nessuno. L'abbinamento generazione ↔ consegna ↔ `rt_sigreturn` è ciò che produce la coppia mittente/target, la disposizione, la latenza per-gestore e il verdetto EINTR che legano insieme l'intera vita di un segnale.
## Test su diversi kernel
`make veristat` carica `bin/probe.bpf.o` con veristat sul **tuo** kernel — un controllo rapido che ogni programma superi il verifier, più la complessità per programma (insns/states). Caricare BPF richiede privilegi, quindi usa `sudo`.
Un programma che si carica sul tuo laptop può essere rifiutato dal verifier di un kernel più vecchio. [`.github/workflows/kernel-matrix.yml`](https://github.com/yeet-src/sigwire/blob/HEAD/.github/workflows/kernel-matrix.yml) protegge da questo: per ogni kernel nella sua matrice costruisce l'oggetto, avvia quel kernel in una VM ([little-vm-helper](https://github.com/cilium/little-vm-helper) di cilium, immagini da `quay.io/lvh-images`), ed esegue il **veristat** statico venduto su di esso — fallendo il job se il verifier rifiuta qualsiasi programma, e trasformando i risultati per kernel in una griglia ✅/❌. Il gate in-VM è [`build/verify-kernel.sh`](https://github.com/yeet-src/sigwire/blob/HEAD/build/verify-kernel.sh).
Esegui la stessa matrice localmente (Linux + KVM) con `make veristat-matrix` — avvia le immagini del kernel con `lvh` + QEMU e stampa una griglia `ok`/`FAIL`. Seleziona kernel con `make veristat-matrix KERNELS="6.6 bpf-next"`.
## Requisiti
> [!IMPORTANT]
> - **Un kernel Linux con BTF** (`CONFIG_DEBUG_INFO_BTF`) per CO-RE — `bpftool` genera `src/bpf/include/vmlinux.h` da esso. Predefinito su Arch, Fedora, Ubuntu e Debian attuali (ogni kernel di distribuzione mainstream da ~5.4).
> - **Il demone yeet**, che esegue il caricamento BPF privilegiato. Le capacità BPF sono delegate a un processo demonizzato, quindi `sigwire` stesso viene eseguito senza privilegi. `curl -fsSL https://yeet.cx | sh` lo installa.
>
> Per compilare dal sorgente hai anche bisogno di `clang` e `bpftool` — ma la toolchain statica venduta li fornisce, quindi non hai bisogno di una toolchain C/BPF di sistema. Nessun node/npm: anche esbuild è venduto e il progetto non ha dipendenze di terze parti.
## Avvertenze oneste
> [!NOTE]
> `sigwire` è osservabilità, non applicazione. Ti mostra ciò che è stato sollevato; non blocca, ritarda o altera alcun segnale.
- **Una riga è un segnale *sollevato*.** La linea del quadro di comando proviene dalla generazione; il target potrebbe intercettarlo, bloccarlo o essere già uscito. Le colonne disposizione/gestore/maschera provengono dal lato *consegna* e si riempiono solo quando il kernel lo consegna effettivamente — un segnale bloccato o ancora in sospeso non mostra disposizione. Vedi [What counts as fatal](#what-counts-as-fatal).
- **La correlazione è best-effort.** Generazione e consegna sono tracepoint separati senza un ID condiviso, abbinati su `(target tid, signal)` entro una finestra temporale. Sotto una tempesta dello stesso segnale allo stesso thread l'abbinamento può sfocare; è corretto nel caso comune prevalente.
- **Il timing del gestore misura il frame del kernel, non la tua intenzione.** `ran` è consegna → `rt_sigreturn`. Un gestore che imposta solo un flag (CPython, runtime di Go) ritorna in microsecondi anche se il lavoro "reale" avviene dopo nel ciclo degli eventi — accurato, ma non quello che ci si aspetterebbe.
- **La rilevazione EINTR osserva ogni uscita di syscall.** Catturare syscall interrotte significa collegarsi a `raw_syscalls:sys_exit`, che scatta su *ogni* ritorno di syscall a livello di sistema (il gestore esce immediatamente per tutti tranne i rari codici `-ERESTART*`, quindi il costo aggiuntivo è un paio di istruzioni per syscall — ma non è zero). I *nomi* delle syscall sono una tabella x86-64; altre architetture mostrano il numero di syscall grezzo.
- **Il mittente di un segnale del kernel è `current`.** Per un errore sincrono (`SIGSEGV` da un accesso errato) è il task stesso che ha causato l'errore — corretto e utile. Per un segnale asincrono del kernel, `current` è qualsiasi task era in esecuzione quando il kernel lo ha sollevato, che è un suggerimento, non una verità assoluta.
- **La numerazione dei segnali in tempo reale è nominale.** `SIGRTMIN+n` viene mostrato per offset grezzo; le librerie riservano i primi pochi per uso proprio.
- **`comm` è 16 byte.** I nomi di processo lunghi vengono troncati dal kernel, non da sigwire.
## Domande della comunità
**Rallenta i processi tracciati?**
Nessun overhead significativo. I programmi tracepoint sono passivi; il costo è una scrittura limitata nel ring buffer per segnale (e le due istruzioni per uscita syscall per la rilevazione EINTR), e il ring buffer lascia cadere piuttosto che bloccare se lo spazio utente è in ritardo.
**Mostrerà segnali diretti a un processo già in esecuzione quando lo avvio?**
Sì. I tracepoint scattano per ogni segnale dal momento in cui sigwire si collega, indipendentemente da quando il mittente o il target sono partiti — non c'è stato per-processo da aver perso.
**Funziona per qualsiasi processo, o solo uno?**
Qualsiasi processo sull'host, tutto in una volta — la colonna mittente/target li distingue. È il traffico di segnali dell'intera macchina, non di un singolo pid.
**Posso esportare il feed?**
Non integrato. I callback `RingBuf.subscribe` in `probes/sigwire.js` contengono ogni record decodificato, quindi un sink JSON/HTTP/Kafka è un ramo lì. Per configurare una pipeline gestita, [contattaci](https://yeet.cx/).
## Compilazione dal sorgente```sh
make # clang + bpftool → bin/probe.bpf.o ; esbuild → src/index.jsx
make bpf # just the BPF object
make bundle # just the JS bundle
make clean # remove build artifacts
Poi yeet run . esegue la build locale. make esegue due compilatori indipendenti: clang + bpftool collegano src/bpf/*.bpf.c nell'oggetto caricabile bin/probe.bpf.o; esbuild raggruppa src/main.jsx in src/index.jsx, risolvendo gli alias di bundle-time @/ (radice sorgente) e #/ (radice progetto) tramite i paths di tsconfig e lasciando i built-in yeet:* esterni. Entrambi i compilatori provengono da una toolchain statica venduta, quindi la build non necessita di toolchain C/BPF di sistema né di node/npm. I file generati vmlinux.h, src/index.jsx e bin/*.bpf.o sono artefatti di build.
Poiché gli alias sono solo al momento del bundle, il runtime localizza l'oggetto BPF con import.meta.dirname anziché con un alias. Vedi AGENTS.md (alias CLAUDE.md) per la guida alla creazione della dashboard yeet.
Doppia licenza BSD/GPL. Il programma BPF dichiara char LICENSE[] SEC("license") = "Dual BSD/GPL" in src/bpf/sigwire.bpf.c, che il kernel richiede per gli helper che utilizza.
Realizzato con yeet, un runtime JS per scrivere programmi eBPF su Linux. Unisciti a noi su Discord.
| gravità | segnali | colore |
|---|
| kill | SIGKILL | hot red |
| fatale (con core dump) | SEGV BUS ABRT ILL FPE TRAP SYS QUIT | red |
| terminazione | TERM INT HUP PIPE ALRM … | amber |
| controllo job | STOP TSTP TTIN TTOU | yellow |
| continua | CONT | green |
| utente | USR1 USR2 | cyan |
| tempo reale | SIGRTMIN+n | violet |
| manutenzione | CHLD URG WINCH … | grey |