Skip to content

cli: route fan commands through the privileged daemon (stacked on #29) - #32

Open
titan550 wants to merge 2 commits into
ProducerGuy:mainfrom
titan550:feat/daemon-routed-fan-cli
Open

cli: route fan commands through the privileged daemon (stacked on #29)#32
titan550 wants to merge 2 commits into
ProducerGuy:mainfrom
titan550:feat/daemon-routed-fan-cli

Conversation

@titan550

Copy link
Copy Markdown

Depends on #29. This branch is stacked on fix/auto-app-kill-optin; the first commit here is #29. Until #29 merges, please review only the top commit (feat(cli): route fan commands through the privileged daemon). Once #29 lands, this rebases to a single commit and the diff collapses to the routing change alone.

Problem

The CLI's max, set, and auto always write to the SMC directly, which requires root. On a machine where the ThermalForge daemon is already running as root — the normal installed state — an unprivileged thermalforge max fails with "Fan unlock failed: Timed out setting fan 0 to manual mode. Run with sudo," because AppleSMC accepts reads but the firmware rejects every fan-mode write, and the unlock loop times out. Callers are forced into sudo (or fail) while a privileged path that already speaks max/set/auto sits idle on /tmp/thermalforge.sock. Direct writes also race the app→daemon control loop instead of coordinating with it.

Change

  • FanCommandRouter.apply: one entry point that uses the daemon when it's running (no sudo, coordinates with the app) and falls back to direct SMC writes otherwise (daemon-less installs behave exactly as before, including the sudo requirement).
  • CLI rewiring: max, set (all fans), and auto go through the router. Single-fan set --fan N intentionally stays direct-to-SMC — per-fan control is not part of the socket protocol — and still needs sudo.
  • nohb (unsupervised hold): one-shot CLI commands append a nohb token so the daemon leaves its heartbeat watchdog disarmed; without it, the 15s watchdog silently reverts a fire-and-forget thermalforge max to auto. The menu bar app omits the token and stays supervised/crash-protected exactly as today.

Compatibility

Older daemons tokenize on whitespace and ignore the trailing nohb for max/set, so a new CLI against an old daemon degrades to today's behavior (hold reverts after the watchdog window) rather than erroring. There is no CLI↔daemon version handshake in the protocol — that's noted here as future work rather than smuggled into this PR.

Build-verified with swift build -c release; the daemon-routed max/status/auto sequence was exercised end-to-end against a live root daemon on an M5 Max (fans commanded to 5349/5777 RPM without sudo, then restored).

John Shojaei added 2 commits July 17, 2026 23:26
`auto` ran `killall ThermalForgeApp` unconditionally so the fan reset would
stick against a running profile. But that meant any programmatic caller —
e.g. a server restoring fans to auto on shutdown — silently killed the user's
menu bar app. The daemon's own `auto` path never did this.

Gate the app-quit behind a new `--stop-app` flag (default off), so `auto` is
a pure fan-reset command. Interactive users who want the old behavior can pass
`--stop-app`.
`max`, `set` (all fans), and `auto` now go through the daemon socket when
the daemon is running, so they work without sudo and coordinate with the
menu bar app instead of racing it for SMC control. Direct SMC writes
remain the fallback when no daemon is installed.

One-shot CLI holds send a trailing `nohb` token so the daemon leaves its
heartbeat watchdog disarmed; without it the 15s watchdog would silently
revert a fire-and-forget `thermalforge max` to auto. The menu bar app
stays supervised (crash-protected) as before.

Single-fan `set --fan N` still needs direct SMC access: per-fan control
is not exposed over the daemon socket protocol.

Stacked on fix/auto-app-kill-optin.
@titan550
titan550 marked this pull request as ready for review July 19, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant