signals
Signal-handling reference for the linuxguard-agent start process — SIGHUP (log-level reload + log rotation), SIGTERM (143), SIGINT (130), and the re-raise convention.
This page documents which POSIX signals the linuxguard-agent process handles, what each handler does, and which exit code results when the agent terminates because of a signal. Only the start command installs signal handlers — every other subcommand (config, probe, support-bundle, enroll, unenroll, show-config, status, version, stop) is a one-shot invocation with no signal.Notify calls.
Handled signals
The start process installs two cooperating handlers — a primary goroutine for SIGINT / SIGTERM / SIGHUP and a separate SIGHUP listener for log-level reload. Go's signal.Notify broadcasts a delivered SIGHUP to both handlers independently, so the two effects are guaranteed to fire on the same signal.
SIGHUP
Primary handler + dedicated log-level reload handler
Two independent effects: (1) the active lumberjack rotator calls Rotate() to close and reopen the current log file descriptor; (2) config.Setup re-runs and the resolved log level is applied via lglog.SetLevel.
SIGTERM
Primary handler
The signal is stored in the caughtSignal atomic value, the agent context is cancelled, and main returns. After the agent run unwinds, the agent calls os.Exit(128 + int(syscall.SIGTERM)) = os.Exit(143).
SIGINT
Primary handler
Same path as SIGTERM. The agent calls os.Exit(128 + int(syscall.SIGINT)) = os.Exit(130).
Signals not in the table above (SIGUSR1, SIGUSR2, SIGPIPE, etc.) are handled by the Go runtime defaults. The agent installs signal.Notify for exactly the three signals above; nothing else is intercepted.
Why send SIGHUP?
SIGHUP?SIGHUP is the agent's runtime-reload signal. Two distinct workflows depend on it.
Coexistence with external logrotate
logrotateThe agent writes to /var/log/linuxguard/agent.log via a lumberjack.Logger rotator that handles its own size-based and age-based rotation. Operators who additionally configure system logrotate need the agent to release the inherited file descriptor on the rotated file so disk usage stops growing on the old inode. The recommended /etc/logrotate.d/linuxguard fragment is:
/var/log/linuxguard/agent.log {
daily
rotate 14
compress
missingok
notifempty
postrotate
/bin/kill -HUP $(cat /var/run/linuxguard-agent.pid 2>/dev/null) 2>/dev/null || true
endscript
}The postrotate kill -HUP triggers the lumberjack Rotate() call which closes and reopens the agent's log writer. Without the postrotate hook, the rotated file remains open via the inherited fd and disk usage continues growing on the old inode.
Log-level reload without restart
linuxguard-agent config set log_level <level> persists the new level to the local config database AND sends SIGHUP to the running agent via deliverSighup. The agent's SIGHUP reload goroutine then re-reads the persisted config (via config.Setup) and applies the new level via lglog.SetLevel. The agent does not need a restart for the new level to take effect.
External tooling that bypasses config set log_level (e.g., editing the config database directly) must kill -HUP $(cat /var/run/linuxguard-agent.pid) itself for the new level to apply.
Re-raise convention
When the agent catches SIGINT or SIGTERM, it does NOT re-raise via signal.Reset + syscall.Kill(getpid, sig). Instead, main calls os.Exit(128 + int(sig)) directly:
SIGINT (2)
130
128 + 2
SIGTERM (15)
143
128 + 15
The os.Exit(128+signum) path is intentional. An earlier signal.Reset + syscall.Kill approach routed through Go's runtime dieFromSignal (runtime/signal_unix.go). Empirically, on Go 1.25 inside a distroless containerized PID-1 deployment, the synchronous self-raise(sig) did NOT terminate the process before the runtime's 5 osyields elapsed, so dieFromSignal fell through to its exit(2) fallback and docker wait reported exit code 2 for every signal-induced shutdown. The os.Exit(128+signum) approach matches the shell convention that docker wait reports, regardless of WIFEXITED vs WIFSIGNALED. Operators and orchestrators see 143 for SIGTERM deterministically.
Sending signals to the agent
The agent writes its PID to /var/run/linuxguard-agent.pid on start (typical service mode only — ephemeral and PID-1 modes skip the PID file). Standard tooling for signal delivery:
Reload log level + close-and-reopen log file
sudo kill -HUP $(cat /var/run/linuxguard-agent.pid)
Graceful shutdown (orchestrated)
sudo kill -TERM $(cat /var/run/linuxguard-agent.pid) or sudo systemctl stop linuxguard-agent
Interactive cancellation (foreground)
Ctrl-C in the terminal hosting the agent
Force kill (LAST RESORT)
sudo kill -KILL $(cat /var/run/linuxguard-agent.pid) — bypasses the signal handler; the agent does not unwind cleanly and the next start performs a stale-pidfile check.
The systemd unit (/lib/systemd/system/linuxguard-agent.service in packaged installs) uses KillSignal=SIGTERM so systemctl stop produces the expected 143 exit. The systemd-journal log shows Stopped LinuxGuard Agent and Main process exited, code=exited, status=143/n/a on a graceful stop.
Signals NOT handled by other commands
config, probe, support-bundle, enroll, unenroll, show-config, status, and version are one-shot invocations: they do not call signal.Notify. The Go runtime's default SIGINT / SIGTERM behavior applies — Ctrl-C during linuxguard-agent probe (for example) aborts the process via the runtime default with no graceful JSON output and no 128+signum re-raise.
The single exception is the SIGHUP that config set log_level and config unset log_level send via syscall.Kill to the running start process. That is a signal sent BY config, not handled by config; config itself does not install a handler.
Related: start | config | exit-codes | env-variables | CLI Reference
Last updated
Was this helpful?