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.

Signal
Handler location
Behavior

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 is the agent's runtime-reload signal. Two distinct workflows depend on it.

Coexistence with external logrotate

The 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:

Caught signal
Resulting exit code
Shell convention

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:

Goal
Command

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?