fix: pos systemd units stop fast — TimeoutStopSec=5s everywhere, listener TERM traps; drop dead pos-health/pos-entertainment unit docs

This commit is contained in:
Your Name
2026-08-13 13:16:58 +00:00
parent 8b0ce3ba89
commit d9316ec7f7
15 changed files with 82 additions and 67 deletions
+21 -24
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44190 |
| ## 3. Installation Flow | 191243 |
| ## 4. The `pos` CLI System | 244313 |
| ## 5. Shared Library — `lib/common.sh` | 314345 |
| ## 6. Docker Compose / ScaleTail | 346388 |
| ## 7. Optional Apps (`apps/`) | 389418 |
| ## 8. Entertainment Module | 419432 |
| ## 9. Systemd Services | 433445 |
| ## 10. Configuration Files | 446472 |
| ## 11. Coding Conventions | 473505 |
| ## 12. Development Workflow | 506558 |
| ## 13. Key File Quick Reference | 559616 |
| ## 14. Common Tasks for Agents | 617646 |
| ## 2. Directory Structure | 44189 |
| ## 3. Installation Flow | 190241 |
| ## 4. The `pos` CLI System | 242311 |
| ## 5. Shared Library — `lib/common.sh` | 312343 |
| ## 6. Docker Compose / ScaleTail | 344386 |
| ## 7. Optional Apps (`apps/`) | 387416 |
| ## 8. Entertainment Module | 417430 |
| ## 9. Systemd Services | 431442 |
| ## 10. Configuration Files | 443469 |
| ## 11. Coding Conventions | 470502 |
| ## 12. Development Workflow | 503555 |
| ## 13. Key File Quick Reference | 556613 |
| ## 14. Common Tasks for Agents | 614643 |
<!-- GEN:END docmap -->
## 1. Project Overview
@@ -160,8 +160,7 @@ Linux_post_install/
├── systemd/
│ ├── autostart.service # Runs autostart.sh on boot
│ ├── ssh-agent.service # System-wide SSH agent socket
── pos-health.service # Runs the health digest as the installing user (triggered by timer)
│ └── pos-health.timer # Daily 08:00 trigger for the health digest (enabled when Telegram is configured)
── usb-automount.service # Auto-mounts USB sticks (usb-automount feature)
├── scripts/ # Dev tooling
│ ├── gen-docs.sh # Regenerates code-derived doc sections + completion flags
@@ -208,8 +207,7 @@ User runs: ./install.sh [--apps|--full|--feature|--dry-run|--skip <phase>|--step
│ └─ PATH export in ~/.bashrc
│ └─ Bash completion for pos CLI
│ └─ Copies systemd/*.service + systemd/*.timer → /etc/systemd/system/, enables them
│ (autostart.service only when the `autostart` flag is set;
│ pos-health.timer only when the user's Telegram config exists)
│ (autostart.service and usb-automount.service only when their feature flags are set)
├─ Phase 4: ScaleTail clone
│ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail
@@ -436,8 +434,7 @@ Public-API "entertainment" plugins (weather, joke, gold) that can auto-send thei
|---------|------|---------|
| `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |
| `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot |
| `pos-health.service` | `systemd/pos-health.service` | Runs `pos system health --send --markdown` once (oneshot) as the installing user |
| `pos-health.timer` | `systemd/pos-health.timer` | Daily 08:00 trigger for the digest (enabled only when `~/.config/linux_post_install/telegram.env` exists) |
| `usb-automount.service` | `systemd/usb-automount.service` | Auto-mounts USB sticks at `/media/<label>` (usb-automount feature) |
All `.service` files in `systemd/` are automatically copied to `/etc/systemd/system/` and enabled by `postinstall.sh` (timers too, when present).
@@ -566,8 +563,8 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `lib/common.sh` | 144 | Shared library (log/warn/err/run/spawn, dry-run aware, `load_system_env`) |
| `lib/flags.sh` | 60 | Feature flag store (set/clear/is_set/value/list/status) |
| `lib/notify.sh` | 76 | Multi-platform alerting (`notify_send`) — opt-in source, silent-fails |
| `lib/entertainment-lib.sh` | 350 | Entertainment module lib (ENABLED parsing, scheduler sync) |
| `lib/scheduler-lib.sh` | 822 | Scheduler lib (job parsing, notify policies, per-job user timers, legacy migrate) |
| `lib/entertainment-lib.sh` | 354 | Entertainment module lib (ENABLED parsing, scheduler sync) |
| `lib/scheduler-lib.sh` | 830 | Scheduler lib (job parsing, notify policies, per-job user timers, legacy migrate) |
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
| `bin/flag-clear` | 21 | Unset a flag |
@@ -576,9 +573,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
<!-- GEN:START filetable -->
| `bin/pos` | 292 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) |
| `bin/pos-communication-matrix-listener` | 565 | Matrix listener: map /command → bash, run them on room messages |
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) |
| `bin/pos-communication-telegram-listener` | 563 | Telegram bot listener: map /command → bash, run them on chat messages |
| `bin/pos-communication-telegram-listener` | 566 | Telegram bot listener: map /command → bash, run them on chat messages |
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
| `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
@@ -593,7 +590,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) |
| `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) |
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
| `bin/pos-network-download` | 949 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
| `bin/pos-network-download` | 952 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
| `bin/pos-network-scan` | 271 | Parallel ping sweep of CIDR |
+11
View File
@@ -323,6 +323,17 @@ then listing it in `NOTIFY_PLATFORM`. Platform keys map to tool names via `notif
Check before creating, use `>>` with grep guards, don't overwrite user configs.
### Systemd units
Every unit a tool writes (or `systemd/` ships) sets `TimeoutStopSec=5s` so a
stuck process can't stall a reboot for the 90s systemd default. Long-polling
daemons (listeners) also `trap TERM INT` in their loop so a stop returns in
well under a second — the unit timeout is the backstop. Keep `KillMode=`
explicit (`control-group`) on the daemons. A oneshot job running at shutdown is
SIGKILLed 5s after stop begins — fine, `Persistent` timers re-run it next boot.
Existing installs keep the old unit files until the tool rewrites them (re-run
the `enable` path), so template changes need a regeneration step on live boxes.
### Managed Config Blocks
To let a tool own a slice of a user/system config file (e.g. Samba shares in `/etc/samba/smb.conf`) without clobbering hand edits, delimit the tool's section with start/end marker lines and rewrite only that slice:
+2 -2
View File
@@ -70,8 +70,8 @@ Adding another platform = create `bin/pos-communication-<p>` implementing
### Scheduling
- **Daily health digest** (`pos system health --send` at 08:00) — `systemd/pos-health.{service,timer}`,
enabled by postinstall once `telegram.env` exists. See [system](howto/system.md).
- **Daily health digest** — add a `daily` schedule job `pos system health --send --markdown`
via `pos system schedule config` (the old `pos-health.{service,timer}` units are gone). See [system](howto/system.md).
- **Entertainment auto-triggers** — per-plugin `pos entertainment enable <plugin> <interval>`,
uses systemd user timers (or cron fallback). See [entertainment](howto/entertainment.md).
- **`pos system schedule` jobs** — run any command on a per-job timer and notify
+1 -1
View File
@@ -217,7 +217,7 @@ The standalone `vbox` command still works and forwards to `pos docker vbox` (see
| `pos system health [--send] [--markdown]` | `bin/pos-system-health` | Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | `--send`/`--markdown` are notification-only: they send the summary via `lib/notify.sh` to every platform in `NOTIFY_PLATFORM` and do NOT print the dashboard (so wrappers like the Telegram listener don't echo it back — pair with the listener's `@quiet` marker). `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2) and `BACKUP_SERVICE_ROOTS` come from `~/.config/linux_post_install/system.env`; `--help` shows the effective values. Platform list from `~/.config/linux_post_install/notify.env` |
| `pos system schedule <cmd>` | `bin/pos-system-schedule` | Scheduled jobs — run a command on a timer, notify (or stay silent): `run [name\|all]`, `list`, `config`, `enable [name\|all]`, `disable [name\|all]`, `status`, `migrate`. Each job is a file in `~/.config/linux_post_install/schedule.d/<name>.env` with `INTERVAL` (`5m…59m`, `1h…23h`, `hourly`, `daily`, `weekly`, `OnCalendar=…`), `NOTIFY` policy, optional `MSG`, `RULE` (threshold only), and `COMMAND` = the literal rest of the line (pipes/quotes/`sudo` fine). Policies: `always` (full output every run), `onchange` (send when output differs from the last run; first run always sends), `onerror` (non-zero exit or empty output), `threshold` (first numeric output vs `RULE`, alert on false→true + one recovery — the old event-trigger behavior), `never` (side-effect jobs, no notify) | One systemd **user** timer pair per job (`pos-schedule-<name>.timer` + oneshot `.service`, `Persistent=true`), reconciled on `enable`/`disable`; the legacy single `pos-event-trigger` timer is auto-removed. `migrate` converts a pre-existing `event.env` rule set into `schedule.d/rule-N.env` threshold jobs. `config` is an interactive editor (add/edit/remove/enable/disable, validates interval + threshold); alerts via `lib/notify.sh`; `--dry-run` previews runs/writes/sends; jobs are arbitrary shell commands (chmod 600, same trust model as the Telegram map); starter jobs in `config/schedule.d/` auto-installed no-clobber by postinstall |
`systemd/pos-health.service` + `systemd/pos-health.timer` run `pos system health --send --markdown` daily at 08:00 as the installing user. `postinstall.sh` enables the timer automatically once `~/.config/linux_post_install/telegram.env` exists — re-run postinstall after configuring a notify platform to pick it up. The service also loads `system.env` + `notify.env` via `EnvironmentFile=`.
A scheduled job is the recommended way to run the health dashboard on a timer, e.g. a `daily` job with `COMMAND=pos system health --send` — no separate systemd unit needed (the old `pos-health.{service,timer}` units are gone; a legacy install may still have them failed/leftover — disable and remove them).
### ssh
+16 -32
View File
@@ -6,8 +6,8 @@ The units installed and enabled by `postinstall.sh`, plus the `pos` bash complet
- [`autostart.service`](#autostartservice)
- [`usb-automount.service`](#usb-automountservice)
- [`ssh-agent.service`](#ssh-agentservice)
- [`pos-health.service`](#pos-healthservice)
- [Per-user units (`pos network download`)](#per-user-units-pos-network-download)
- [Stop behavior](#stop-behavior)
- [Feature-flag gating](#feature-flag-gating)
- [Bash completion](#bash-completion)
@@ -81,32 +81,6 @@ WantedBy=multi-user.target
**Configuration:** socket at `/run/ssh-agent/socket` (world-readable/writable). `~/.bashrc` (set by `postinstall.sh`) exports `SSH_AUTH_SOCK` to it. Not gated on any feature flag.
### pos-health.service
**Purpose:** daily "health digest" — runs `pos system health --send --markdown` at 08:00 and sends the report to the configured notify platform(s).
```ini
[Unit]
Description=POS Health digest (daily report via configured notify platforms)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=__POS_USER__
EnvironmentFile=-%h/.config/linux_post_install/system.env
EnvironmentFile=-%h/.config/linux_post_install/notify.env
ExecStart=/usr/local/bin/pos system health --send --markdown
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
```
The service is `Type=oneshot` and is driven **only** by its companion `pos-health.timer` (`WantedBy=timers.target`); the service itself is never enabled directly.
**Configuration:** `postinstall.sh` substitutes `__POS_USER__` with the installing user (`${SUDO_USER:-$USER}`) so the digest uses that user's real notify config. The `EnvironmentFile=` lines load `system.env` (health/backup settings) and `notify.env` (`NOTIFY_PLATFORM`). The timer is enabled only when a Telegram config (`~/.config/linux_post_install/telegram.env`) already exists — otherwise postinstall warns and skips; re-run postinstall after configuring a notify platform to install it.
---
## Per-user units (`pos network download`)
@@ -134,9 +108,23 @@ enable-linger` warning so the user units survive logout.
---
## Stop behavior
Every pos unit — the tool-written user units (`pos-aria2`, `pos-aria2-retry`,
`scheduler-lib.sh` job units, `entertainment-lib.sh` plugin units, the Telegram
and Matrix listener units) and the shipped `systemd/*.service` files — sets
`TimeoutStopSec=5s` so nothing can stall a reboot for the systemd default of
90s. The two long-polling listener daemons also `trap TERM INT` in their poll
loop (kills in-flight curl, exits 0), so `systemctl --user stop` returns in
well under a second; `TimeoutStopSec` is the backstop if a process ignores
SIGTERM. A oneshot job that happens to be running at shutdown gets SIGKILLed
5s after stop begins — timers are `Persistent`, so the work re-runs next boot.
---
## Feature-flag gating
The systemd loop in `postinstall.sh` special-cases three units:
The systemd loop in `postinstall.sh` special-cases two units:
```bash
if [ "$svc_name" = "autostart.service" ] && ! flag_is_set autostart; then
@@ -147,14 +135,10 @@ if [ "$svc_name" = "usb-automount.service" ] && ! flag_is_set usb-automount; the
warn "usb-automount feature not installed — skipping usb-automount.service (run ./install.sh --feature)"
continue
fi
if [ "$svc_name" = "pos-health.service" ]; then
# substitute User= and enable pos-health.timer only if Telegram is configured
fi
```
- `autostart.service` is **enabled** only when the `autostart` feature flag is set (`./install.sh --feature` or `flag-set autostart`). See [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libflagssh--feature-flags).
- `usb-automount.service` is **enabled** only when the `usb-automount` feature flag is set — same mechanism.
- `pos-health.service` is **not** enabled at all — `postinstall.sh` enables `pos-health.timer` instead, and only when a Telegram config already exists.
---
+15 -8
View File
@@ -47,19 +47,26 @@ NOTIFY_PLATFORM=telegram
### Daily digest (automated)
`systemd/pos-health.{service,timer}` run `pos system health --send --markdown`
at 08:00 as the installing user. Enable it (re-run postinstall after Telegram
is configured):
Run the health report on a timer with a scheduled job (no systemd unit needed):
```bash
./postinstall.sh # enables timer once telegram.env exists
systemctl list-timers | grep pos-health
systemctl start pos-health.service # run once now, check status
pos system schedule config # add a job: INTERVAL=daily,
# COMMAND=pos system health --send --markdown
systemctl --user list-timers | grep pos-schedule
pos system schedule run <name> # run once now
```
The old `pos-health.{service,timer}` systemd units are gone — a legacy install
may still have them failed/leftover; disable and remove them:
```bash
sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null
sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload
```
**Recipes:**
- Watch the backup age without email: enable the digest; if the backup check
turns WARN you'll see it in the morning report.
- Watch the backup age without email: add the daily digest job; if the backup
check turns WARN you'll see it in the morning report.
- Exit code in a cron/scheduled check:
`pos system health >/dev/null 2>&1 || notify_send "health FAIL"`.