# How MTPLX Handles Fan Control via ThermalForge and Crash Recovery

> Discover how MTPLX fan control uses ThermalForge and a watchdog for reliable operation. Learn about crash recovery ensuring automatic fan curves persist even after process failure.

- Repository: [Youssof Altoukhi/MTPLX](https://github.com/youssofal/MTPLX)
- Tags: internals
- Published: 2026-09-13

---

**TLDR: MTPLX delegates fan control to the open-source ThermalForge utility, using a JSON marker-file ownership system and a detached side-car watchdog to guarantee that fans return to automatic curves even if the main process crashes or receives SIGKILL.**

MTPLX is an open-source macOS power management toolkit that leverages ThermalForge for hardware-level fan control. Unlike simple shell scripts, the implementation in [[`mtplx/thermal.py`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py)](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py) provides verified command execution, state persistence, and robust crash recovery mechanisms. This article examines the architectural layers that enable safe **fan control via ThermalForge** while preventing the system from being left with permanently maxed-out fans after unexpected terminations.

## Detecting and Selecting the ThermalForge Binary

The detection logic begins with `detect_thermal_control()`, which orchestrates tool discovery. The helper [`_find_thermalforge_`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py#L21-L35) prioritizes a private copy installed in `~/.mtplx/bin` over system-wide installations, falling back to any `thermalforge` binary found on `PATH`. If no binary is located, the function returns [`INSTALL_INSTRUCTIONS`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py#L39-L46) to guide the user through setup rather than failing silently.

## Executing and Verifying Fan Commands

### Setting Thermal Profiles

When a user invokes `mtplx max`, the system calls `set_thermal_profile_verified()`. This function constructs command candidates via `_profile_command_candidates` and delegates execution to `set_thermal_profile()`. It issues password-less sudo commands to ThermalForge, requesting specific profiles such as "max" or "silent".

### Status Verification

After issuing a command, MTPLX polls the daemon status using `thermal_status()` and `_status_command_candidates`. The verification layer parses JSON output through `fan_summary()` to confirm the hardware actually responded. Two boolean checks enforce correctness: `_summary_indicates_max` confirms the daemon registered the target RPM, while `_summary_indicates_actual_ramp` verifies the physical fans have spun up to the requested speed.

## Crash Recovery Through Marker Files

To prevent "stuck" fan states, MTPLX implements an ownership marker system. When enabling max fan mode, [`_write_max_marker`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py#L18-L24) serializes the process PID, ownership token, and binary path to `~/.mtplx/max-active.json`, protected by `_max_marker_lock` to serialize concurrent access. An `atexit` handler installed by `install_max_lifecycle_hooks()` triggers `restore_thermal_profile_verified()` on graceful exit, issuing `thermalforge auto` and confirming the return to automatic curves.

If the process crashes or is killed with SIGKILL, the marker persists. On the next launch, [`check_and_recover_stale_max()`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py#L76-L84) detects the stale PID, invokes `restore_thermal_profile_verified()`, and clears the marker. This guarantees the system never remains in an unintended thermal state due to a previous crash.

## The Side-Car Watchdog for Hard Crashes

Because `atexit` handlers do not survive `SIGHUP` or terminal closure, MTPLX spawns a detached side-car via [`_spawn_thermal_sidecar()`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal.py#L20-L28) defined in [[`mtplx/thermal_sidecar.py`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py)](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py). Running in its own session, this watchdog monitors the parent PID and executes `thermalforge auto` through password-less sudo (configured via `install_passwordless_sudoers_rule()`) upon detecting parent death. This mechanism provides a safety net when the main process cannot execute cleanup code.

## Practical Implementation Examples

```python

# Example: Enable max-fan mode and verify it worked

result = set_thermal_profile_verified(
    profile="max",               # "max" or "silent"

    settle_seconds=1.0,         # give daemon time to update the target RPM

    require_actual_ramp=True,   # wait for the hardware to spin up

)
print(result["message"])        # Human-readable outcome

# → "fans ramped to max (actual 7800 RPM; target 7900 RPM)"

```

```python

# Example: Restore default fan behaviour (used on graceful shutdown)

restore = restore_thermal_profile_verified()
print(restore["message"])      # "fan profile restored" if successful

```

```python

# Example: Automatic crash recovery on next launch

recovery = check_and_recover_stale_max()
if recovery["recovered"]:
    print("Recovered from previous crash – fans are now auto.")

```

```python

# Example: Install password-less sudo rule for ThermalForge (once per user)

install = install_passwordless_sudoers_rule()
print(install["message"])

# → "Passwordless sudo for thermalforge is configured."

```

## Summary

- MTPLX detects ThermalForge binaries using `detect_thermal_control()`, preferring local copies in `~/.mtplx/bin` over system paths.
- **Verified execution** ensures fan commands actually affect hardware through `set_thermal_profile_verified()` and dual-layer status checks.
- A **JSON marker file** tracks active "max" mode sessions with file-locking to prevent race conditions.
- **Automatic recovery** via `check_and_recover_stale_max()` restores default fan curves if a previous session crashed.
- The **side-car watchdog** survives terminal closure and SIGKILL scenarios to guarantee `thermalforge auto` execution via [[`mtplx/thermal_sidecar.py`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py)](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py).

## Frequently Asked Questions

### What happens if MTPLX crashes while fans are set to maximum speed?

The ownership marker at `~/.mtplx/max-active.json` remains on disk. On the next start, `check_and_recover_stale_max()` detects the stale PID and automatically runs `restore_thermal_profile_verified()` to return fans to auto mode before clearing the marker.

### Why does MTPLX use a side-car process for fan control?

The side-car, implemented in [[`mtplx/thermal_sidecar.py`](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py)](https://github.com/youssofal/MTPLX/blob/main/mtplx/thermal_sidecar.py), survives terminal closure and SIGKILL signals that would otherwise prevent cleanup code from running. It ensures fans never stay stuck at maximum speed when the parent process dies unexpectedly.

### How does MTPLX verify that ThermalForge actually changed the fan speeds?

After issuing commands, MTPLX polls daemon status via `thermal_status()` and checks both `_summary_indicates_max` for target RPM confirmation and `_summary_indicates_actual_ramp` for physical fan rotation verification.

### Where does MTPLX store the crash recovery marker?

The marker is stored at `~/.mtplx/max-active.json` and contains the process PID, ownership token, and ThermalForge binary path, protected by a file lock at `_max_marker_lock` to serialize concurrent access.