
Herramienta de observabilidad de señales del kernel en vivo que utiliza tracepoints eBPF para transmitir cada señal generada en un host Linux, mostrando emisor, destino, disposición, latencia del manejador e interrupciones de llamadas al sistema en tiempo real.
sigwire
tail -fpara señales. Cada señal que cualquier proceso en el sistema genera — quién la envió, a quién impactó, qué señal, cómo se generó (kill(2), el kernel, un temporizador POSIX), si el objetivo la atrapó y cuánto tiempo ejecutó su manejador, si interrumpió una syscall bloqueada conEINTR— decodificada desde los tracepoints de señales del kernel y transmitida en vivo a tu terminal. Sinstrace -fen un solo pid, sinptrace, sin cooperación de los procesos involucrados.
sigwire convierte la maquinaria de señales del kernel en un patchbay en vivo: cada línea es remitente ──SEÑAL──▶ destinatario, coloreada por severidad, etiquetada con cómo se generó, si el destinatario atrapó la señal (y cuánto tiempo ejecutó su manejador), si interrumpió una syscall bloqueada (↯ EINTR read), colapsada a ×N cuando algo hace spam, y marcada ☠ cuando es un golpe mortal real. Un carril lateral cuenta lo que está volando por el cable; pausa y selecciona una fila para inspeccionar la imagen completa — disposición, dirección del manejador, flags de sigaction, y las señales que el destinatario estaba bloqueando en ese instante.
Porque engancha los tracepoints del kernel, no a un solo proceso, una sola ejecución observa cada señal en el host a la vez — tu aplicación, un supervisor, la propia maquinaria de fallos del kernel — sin que ninguno de ellos sepa que está siendo rastreado.
[!TIP] Dos caras de cada señal. sigwire monitorea tanto
signal:signal_generate(la vista del remitente — quién generó qué, la línea del panel de conexiones) comosignal:signal_deliver(la vista del destinatario — si la atrapó, con qué manejador y flags, qué estaba bloqueando, y si interrumpió una syscall). Dos enganches más —rt_sigreturn(2)y el tracepoint de salida de syscall — miden el manejador y capturan EINTR. Todo se correlaciona de nuevo en una fila. Esta división es también por qué el conteo de☠ fatales deliberadamente conservador (ver Qué cuenta como fatal): la generación ocurre antes de la entrega, por lo que el lado del remitente no puede conocer el destino de una señal — solo el lado de la entrega puede, y solo para los casos que observa.
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)
[Manual install guide](https://yeet.cx/docs/manual-installation) | Solo Linux
Nada que configurar — las señales son tráfico de fondo constante en cualquier máquina, así que las filas empiezan a aparecer inmediatamente en la parte superior. ¿Quieres generar algunas tú mismo? `kill -USR1 <pid>`, `Ctrl-C` en un trabajo en primer plano, o inicia cualquier runtime gestionado y observa cómo su GC/scheduler hace ping a sus propios hilos (`↯ EINTR futex` pasando).
## Controles
El flujo sigue la señal más nueva por defecto; selecciona una fila o pausa y se mantiene quieto mientras los datos siguen fluyendo debajo.
| tecla | acción |
| ----- | ------ |
| `p` · `Space` | pausar / reanudar el flujo (congelarlo para leer) |
| `↑`/`↓`, `k`/`j` | pausar e inspeccionar una fila — abre el panel de detalles |
| `/` | filtro difuso — coincide con proceso, pid, señal, fuente y disposición; los caracteres coincidentes se resaltan en vivo |
| `e` | filtrar solo **syscalls interrumpidas** (`↯ EINTR` / `↺ reiniciadas`) |
| `s` | abre el **selector de señales** — silenciar o mostrar cualquier señal, en vivo |
| `Esc` | retroceder un nivel — limpiar el filtro / cerrar el selector / soltar la selección, luego salir |
| `q` | salir |
## Lo que estás viendo
Cada fila es una señal generada, la más nueva arriba:```
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
Cada fila es un bloque: el remitente → destino son comm·pid (el remitente es quien elevó la señal, current; el destino es a quién va dirigida), el cable en el medio lleva el nombre de la señal coloreado por severidad, ×N pliega una ráfaga de la señal idéntica en una línea, y la nota a la derecha da el origen, luego cualquier interrupción de syscall, luego la disposición.
Cada fila se congela en el momento en que su entrega se resuelve y nunca muta de nuevo — por lo que una ráfaga se desplaza como un registro estable, no un agregado parpadeante.
El cable se colorea por severidad en la misma paleta de 256 colores que el resto de la interfaz:
| severidad | señales | color |
|---|---|---|
| kill | SIGKILL | rojo intenso |
| fatal (con volcado de núcleo) | SEGV BUS ABRT ILL FPE TRAP SYS QUIT | rojo |
| terminación | TERM INT HUP PIPE ALRM … | ámbar |
| control de trabajos | STOP TSTP TTIN TTOU | amarillo |
| continuar | CONT | verde |
| usuario | USR1 USR2 | cian |
| tiempo real | SIGRTMIN+n | violeta |
| mantenimiento | CHLD URG WINCH … | gris |
La nota es el origen (kill(2), tgkill, sigqueue, timer, kernel, fault); luego, si interrumpió una syscall bloqueada, ↯ EINTR read (o ↺ restarted read cuando SA_RESTART la reanudó automáticamente); luego la disposición — caught 41µs (un manejador se ejecutó, y cuánto tardó), default (sin manejador, se aplicó la acción por defecto), o ⊘ ignored. Un ☠ marca un golpe mortal real (ver Qué cuenta como fatal).
[!NOTE]
↯ EINTRes el que hay que vigilar. Una señal que llega mientras un hilo está estacionado en una syscall lenta (read,poll,accept,futex,nanosleep, …) lo saca de allí: la syscall devuelve-1/EINTRy, a menos que el manejador haya establecidoSA_RESTART, no se reanuda — la aplicación debe reintentar. Olvidar eso es un error clásico, exasperante, dependiente del tiempo ("¿por qué miread()falló una vez?"). sigwire lo muestra ocurriendo en vivo, y qué syscall recibió el golpe. Presionaepara ocultar todo lo demás y ver solo las interrupciones.
El carril de la derecha es la vista agregada: señales principales por volumen, un desglose por origen y un recuento de entrega — cuántas señales fueron capturadas vs. alcanzaron su valor por defecto vs. ignoradas.
Presiona ↑/↓ (o p) para congelar el flujo y seleccionar una fila; el carril se convierte en un panel de detalles con todo lo que el lado de entrega sabe sobre esa señal exacta:```
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` (se ejecutó un manejador en espacio de usuario), `default` (→ acción por defecto: terminar / volcado de núcleo / detener / ignorar) o `ignored`.
- **syscall** — si esta señal interrumpió una llamada al sistema bloqueada: `EINTR ← read` (el espacio de usuario vio `EINTR`) o `restarted read` (`SA_RESTART` la reanudó de forma transparente).
- **ran** — cuánto tiempo se ejecutó el manejador, medido desde la entrega hasta `rt_sigreturn(2)` que la finaliza. (Los tiempos de ejecución que solo marcan una bandera en el manejador C y hacen el trabajo real más tarde — CPython, Go — muestran un tiempo pequeño aquí; eso son ellos, no sigwire).
- **flags** — las banderas de `sigaction` en el manejador (`SA_RESTART`, `SA_SIGINFO`, `SA_NODEFER`, …).
- **TARGET BLOCKS** — las señales que el objetivo tenía bloqueadas (su `sigprocmask`) en el momento de la entrega, directamente desde su `task_struct`.
`Esc` cierra el inspector; `p` reanuda la transmisión en vivo.
## Qué cuenta como fatal
El contador `☠ fatal` y la insignia de fila `☠` son intencionalmente estrictos. Debido a que `signal_generate` se dispara en la *generación*, sigwire no puede ver si el objetivo instaló un manejador — un `SIGTERM` podría ser capturado y convertido en un apagado limpio, o ignorado por completo. Por lo tanto, solo cuenta una muerte cuando es inequívoco:
- **`SIGKILL`** entregada — incapturable, inignorable, siempre fatal; **o**
- una **señal de volcado de núcleo** (`SEGV`/`BUS`/`ABRT`/`ILL`/`FPE`/`TRAP`/`SYS`/`QUIT`) que el **propio núcleo generó** (una falla síncrona, no un `kill` de espacio de usuario).
Todo lo demás — un `SIGTERM` de `systemd`, un `SIGINT` de tu `Ctrl-C`, un `SIGPWR` de un runtime a sus propios hilos — se muestra y se colorea, pero no se cuenta como una muerte, porque probablemente no lo fue.
## El selector de señales (un control en vivo del kernel)
Tres señales son ruido de fondo puro en cualquier máquina ocupada: `SIGCHLD` (cada reap de hijo), `SIGURG` (latido de prevención asíncrona de Go) y `SIGWINCH` (cambios de tamaño de terminal, transmitidos a todos los procesos en primer plano). sigwire silencia esas tres **en el kernel** por defecto para que el flujo sea el tráfico interesante — pero qué señales son ruido depende de ti.
Presiona `s` para abrir el **selector de señales**: una lista modal de cada señal con su color de gravedad en vivo y cuántas has visto, cada una conmutable entre `shown` y `muted`. Navega con flechas a una (o **escribe su número** — `1`, `5` → salta a 15) y presiona `space`, y esa señal cambia instantáneamente. `a` las cambia **todas** a la vez. El contador `muted` en la barra de título rastrea cuántas están ocultas.
Esta es la mitad bidireccional de la demostración: la máscara de silencio es un `__u64` global en la sección `.data` del programa BPF en ejecución, y al alternar una fila se modifica el bit correspondiente a través de `DataSec.patch()` mientras el programa sigue ejecutándose. El kernel descarta las señales silenciadas antes de que lleguen al búfer anular, por lo que silenciar no cuesta nada — y quitar el silencio trae una señal de vuelta a mitad del flujo sin recarga.
## Cómo funciona
El núcleo está en [`src/bpf/sigwire.bpf.c`](https://github.com/yeet-src/sigwire/blob/master/src/bpf/sigwire.bpf.c) + [`src/bpf/deliver.bpf.c`](https://github.com/yeet-src/sigwire/blob/master/src/bpf/deliver.bpf.c) (kernel, enlazados en un solo objeto) y [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/master/src/probes/sigwire.js) (espacio de usuario). Todo se correlaciona mediante `(tid objetivo, señal)`.
### El lado BPF
Dos archivos fuente se enlazan en un único objeto cargable, `bin/probe.bpf.o`, con cuatro programas de tracepoint:
| Programa | Adjunto a | Lo que captura |
|---|---|---|
| `on_signal_generate` | `signal:signal_generate` | el remitente (`current`) + objetivo (`comm`/`pid`), la señal, `si_code`, bandera `group`, `resultado` — descartado en el kernel si el bit de la señal está establecido en `mute_mask` en vivo |
| `on_signal_deliver` | `signal:signal_deliver` | la disposición del objetivo (`sa_handler`), `sa_flags` y — desde `task_struct` — su conjunto de señales `blocked`; marca la entrega para medir el tiempo del manejador |
| (rt_sigreturn) | `syscalls:sys_enter_rt_sigreturn` | diferencia contra la entrega marcada para el tiempo de ejecución del manejador |
| (sys_exit) | `raw_syscalls:sys_exit` | registra el raro retorno `-ERESTART*` para que la siguiente `signal_deliver` lo resuelva en `EINTR`/`restarted` + el número de llamada al sistema interrumpida |
Los mapas conectan el kernel con el espacio de usuario:
- `events` — `RINGBUF`, un `signal_event` por generación.
- `dispatch` — `RINGBUF`, un `dispatch_event` por entrega / retorno de manejador.
- `mute_mask` — un `__u64` global en la sección `.data`; el selector modifica bits individuales para descartar señales en el kernel.
- `handler_start` / `restart_pending` — `HASH` keyed by tid, scratch por hilo que empareja una entrega con su `rt_sigreturn`, y la salida `-ERESTART*` de una llamada al sistema con la entrega que la sigue.
### El lado JS
| archivo | responsabilidad |
|---|---|
| [`src/probes/probe.js`](https://github.com/yeet-src/sigwire/blob/master/src/probes/probe.js) | carga `bin/probe.bpf.o` una vez, vincula los mapas, inicia los programas (se auto-adjuntan) |
| [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/master/src/probes/sigwire.js) | el único módulo de datos consciente de BPF: combina ambos búferes anulares en un flujo continuo con recuentos, correlaciona la entrega con la generación, posee el control de la máscara de silencio — expone las señales `feed`, `visible`, `muteMask` |
| [`src/main.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/main.jsx) | raíz de composición: entrada, selección, diseño responsivo (el carril se oculta en terminales estrechos), `mount` |
| [`src/components/feed.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/feed.jsx) | la centralita: `sender ──SIG──▶ target`, disposición/latencia, insignias, tinte, consolidación |
| [`src/components/tally.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/tally.jsx) | el carril lateral — señales principales, desglose por origen, recuento de entregas |
| [`src/components/detail.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/detail.jsx) | el inspector — disposición por señal, manejador, banderas, máscara bloqueada |
| [`src/components/picker.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/picker.jsx) | el modal selector de señales — silencia/muestra cada señal a través de la máscara de silencio del kernel |
| [`src/components/titlebar.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/titlebar.jsx) | marca, tasa en vivo, totales, contador `☠ fatal`, conteo de silenciados, en vivo/pausado |
| [`src/components/footer.jsx`](https://github.com/yeet-src/sigwire/blob/master/src/components/footer.jsx) | sugerencias de teclas y el aviso de filtro en vivo |
| [`src/lib/signals.js`](https://github.com/yeet-src/sigwire/blob/master/src/lib/signals.js) | la única fuente de verdad: nombre, gravedad, color, `si_code` → origen, disposición, banderas, decodificación de máscara, fatalidad |
| [`src/lib/format.js`](https://github.com/yeet-src/sigwire/blob/master/src/lib/format.js) | formateadores puros — relleno, truncamiento, `ago()`, duraciones, recuentos compactos |
| [`src/lib/fuzzy.js`](https://github.com/yeet-src/sigwire/blob/master/src/lib/fuzzy.js) | coincidencia difusa de subsecuencias sobre proceso + pid + señal + origen + disposición |
El modelo es un **flujo continuo de señales generadas**, consolidando repeticiones idénticas en filas `×N`. La fila de generación de una señal se congela en el instante en que se resuelve su entrega — por lo que una fila ya en pantalla nunca cambia ni salta. Un temporizador de ventana de 120 ms publica una instantánea por fotograma, por lo que un búfer anular ocupado cuesta un solo renderizado, no miles.
### Por qué tracepoints, no `strace`/`ptrace`
`strace -f` sigue un árbol de procesos y detiene el trazado en cada evento; `ptrace` es por objetivo e intrusivo. Los tracepoints de señal son la costura donde el *kernel* genera y entrega una señal, para *todos* los procesos, sin configuración por aplicación y sin detener a nadie. Emparejar generación ↔ entrega ↔ `rt_sigreturn` es lo que produce el par remitente/objetivo, la disposición, la latencia por manejador y el veredicto EINTR que unen toda la vida de una señal.
## Pruebas en distintos kernels
`make veristat` carga `bin/probe.bpf.o` con veristat en **tu** kernel — una verificación rápida de que cada programa pasa el verificador, más la complejidad por programa (insns/states). Cargar BPF necesita privilegios, así que usa `sudo`.
Un programa que carga en tu portátil puede ser rechazado por el verificador de un kernel más antiguo. [`.github/workflows/kernel-matrix.yml`](https://github.com/yeet-src/sigwire/blob/master/.github/workflows/kernel-matrix.yml) protege contra eso: para cada kernel en su matriz construye el objeto, arranca ese kernel en una VM ([little-vm-helper de cilium](https://github.com/cilium/little-vm-helper), imágenes de `quay.io/lvh-images`), y ejecuta el **veristat** estático incluido contra él — fallando el trabajo si el verificador rechaza algún programa, y pivotando los resultados por kernel en una cuadrícula ✅/❌. La puerta de enlace en la VM es [`build/verify-kernel.sh`](https://github.com/yeet-src/sigwire/blob/master/build/verify-kernel.sh).
Ejecuta la misma matriz localmente (Linux + KVM) con `make veristat-matrix` — arranca las imágenes del kernel con `lvh` + QEMU e imprime una cuadrícula `ok`/`FAIL`. Selecciona kernels con `make veristat-matrix KERNELS="6.6 bpf-next"`.
## Requisitos
> [!IMPORTANT]
> - **Un kernel Linux con BTF** (`CONFIG_DEBUG_INFO_BTF`) para CO-RE — `bpftool` genera `src/bpf/include/vmlinux.h` a partir de él. Predeterminado en Arch, Fedora, Ubuntu y Debian actuales (todos los kernels de distribuciones convencionales desde ~5.4).
> - **El demonio yeet**, que realiza la carga BPF privilegiada. Las capacidades BPF se delegan a un proceso demonizado, por lo que `sigwire` se ejecuta sin privilegios. `curl -fsSL https://yeet.cx | sh` lo instala.
>
> Para construir desde el código fuente también necesitas `clang` y `bpftool` — pero la cadena de herramientas estática incluida los proporciona, por lo que no necesitas una cadena de herramientas C/BPF del sistema. No node/npm: esbuild también está incluido y el proyecto no tiene dependencias de terceros.
## Advertencias honestas
> [!NOTE]
> `sigwire` es observabilidad, no aplicación. Muestra lo que se generó; no bloquea, retrasa ni altera ninguna señal.
- **Una fila es una señal *generada*.** La línea de la centralita proviene de la generación; el objetivo puede capturarla, bloquearla o ya haber salido. Las columnas de disposición/manejador/máscara provienen del lado de *entrega* y se completan solo una vez que el kernel realmente la entrega — una señal bloqueada o aún pendiente no muestra disposición. Consulta [Qué cuenta como fatal](#qué-cuenta-como-fatal).
- **La correlación es del mejor esfuerzo.** La generación y la entrega son tracepoints separados sin identificación compartida, emparejados por `(tid objetivo, señal)` dentro de una ventana de tiempo. Bajo una tormenta de la misma señal al mismo hilo, el emparejamiento puede difuminarse; es correcto en el caso común abrumador.
- **La medición del manejador mide el marco del kernel, no tu intención.** `ran` es entrega → `rt_sigreturn`. Un manejador que solo establece una bandera (CPython, el runtime de Go) regresa en microsegundos incluso si el trabajo "real" ocurre más tarde en el bucle de eventos — preciso, solo que tal vez no lo que esperas.
- **La detección de EINTR observa cada salida de llamada al sistema.** Capturar llamadas al sistema interrumpidas significa adjuntarse a `raw_syscalls:sys_exit`, que se dispara en *cada* retorno de llamada al sistema en todo el sistema (el manejador sale inmediatamente en todos excepto los raros códigos `-ERESTART*`, por lo que el costo adicional es un par de instrucciones por llamada al sistema — pero no es cero). Los *nombres* de las llamadas al sistema son una tabla x86-64; otras arquitecturas muestran el número de llamada al sistema sin procesar.
- **El remitente de una señal del kernel es `current`.** Para una falla síncrona (`SIGSEGV` de un acceso incorrecto) esa es la tarea que falla — correcto y útil. Para una señal asíncrona del kernel, `current` es la tarea que se estaba ejecutando cuando el kernel la generó, lo cual es una pista, no un evangelio.
- **La numeración de señales en tiempo real es nominal.** `SIGRTMIN+n` se muestra por desplazamiento sin procesar; las bibliotecas reservan los pocos bajos para su propio uso.
- **`comm` tiene 16 bytes.** Los nombres largos de procesos son truncados por el kernel, no por sigwire.
## Preguntas de la comunidad
**¿Ralentiza los procesos trazados?**
Sin sobrecarga significativa. Los programas de tracepoint son pasivos; el costo es una escritura limitada en el búfer anular por señal (y el par de instrucciones por salida de llamada al sistema para la detección de EINTR), y el búfer anular descarta en lugar de bloquear si el espacio de usuario se queda atrás.
**¿Mostrará señales dirigidas a un proceso que ya se estaba ejecutando cuando lo inicié?**
Sí. Los tracepoints se disparan para cada señal desde el momento en que sigwire se adjunta, independientemente de cuándo comenzaron el remitente o el objetivo — no hay estado por proceso que se haya perdido.
**¿Funciona para cualquier proceso, o solo uno?**
Cualquier proceso en el host, todos a la vez — la canaleta remitente/objetivo los distingue. Es el tráfico de señales de toda la máquina, no un pid.
**¿Puedo exportar el flujo?**
No está integrado. Las devoluciones de llamada `RingBuf.subscribe` en `probes/sigwire.js` contienen cada registro decodificado, por lo que un sumidero JSON/HTTP/Kafka es una rama allí. Para configurar una canalización gestionada, [contáctanos](https://yeet.cx/).
## Construir desde el código fuente```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
Entonces yeet run . ejecuta la compilación local. make ejecuta dos compiladores independientes: clang + bpftool enlazan src/bpf/*.bpf.c en el objeto cargable bin/probe.bpf.o; esbuild empaqueta src/main.jsx en src/index.jsx, resolviendo los alias de tiempo de empaquetado @/ (raíz del código fuente) y #/ (raíz del proyecto) a través de paths en tsconfig y dejando las funciones integradas yeet:* como externas. Ambos compiladores provienen de una cadena de herramientas estática empaquetada (vendored), por lo que la compilación no necesita la cadena de herramientas C/BPF del sistema ni node/npm. Los archivos generados vmlinux.h, src/index.jsx y bin/*.bpf.o son artefactos de compilación.
Debido a que los alias son solo de tiempo de empaquetado, el tiempo de ejecución localiza el objeto BPF con import.meta.dirname en lugar de un alias. Consulta AGENTS.md (también conocido como CLAUDE.md) para la guía de creación de paneles de yeet.
Dual BSD/GPL. El programa BPF declara char LICENSE[] SEC("license") = "Dual BSD/GPL" en src/bpf/sigwire.bpf.c, que el kernel requiere para las ayudas (helpers) que utiliza.