refactor(helper): migrate to SMAppService for macOS 13+ helper install#3194
refactor(helper): migrate to SMAppService for macOS 13+ helper install#3194marxo126 wants to merge 1 commit into
Conversation
SMJobBless was deprecated in macOS 13. As of macOS 26.4.1 the OS-level
smd daemon now rejects SMJobBless installs that previously worked,
silently breaking fan control for end users on the latest macOS:
smd: App "eu.exelban.Stats" did not pass helper check
"eu.exelban.Stats.SMC.Helper".
Migrate to the modern SMAppService API for macOS 13+, keeping the
existing SMJobBless flow as a fallback for macOS 11/12.
Changes:
- New `SMC/Helper/eu.exelban.Stats.SMC.Helper.plist` declaring
`Label`, `MachServices`, and `BundleProgram` pointing to the helper
binary at `Contents/Library/LaunchServices/...` (its existing path,
unchanged).
- New Copy Files build phase in `Stats.xcodeproj/project.pbxproj`
copies the plist to `Contents/Library/LaunchDaemons/` in the app
bundle, where SMAppService daemons are loaded from.
- `Kit/helpers.swift`:
- `install(completion:)` branches on macOS version. macOS 13+ calls
`SMAppService.daemon(plistName:).register()`. Older macOS keeps
the existing SMJobBless + AuthorizationCreate flow intact.
- `uninstall()` similarly branches to `service.unregister()` on 13+.
- `isInstalled` now queries `service.status` on 13+.
- New `requiresApproval` property surfaces the
`.requiresApproval` SMAppService status so callers can show
"approve in System Settings → Login Items & Extensions"
guidance. (Not yet wired into popup UI — separate UI patch.)
- Helper code (`SMC/Helper/main.swift`) and existing
`SMC/Helper/Launchd.plist` left untouched. The legacy plist is
still consumed by the Helper target itself for the SMJobBless path.
Note on testing: the changes are compile-verified and the bundled
plist lands at the expected `Contents/Library/LaunchDaemons/...`
path. SMAppService specifically requires a Developer ID-signed app to
register a daemon (unlike SMJobBless which permitted unsigned testing
within reason), so behavioral verification needs to happen against a
properly-signed build of Stats.
|
Status — API contract verified via direct probe; runtime behaviour blocked on Developer ID Application cert. What was tested
What CAN'T be tested without a Developer ID
Maintainability notes
Why this matters
References #3193. |
|
Closing — won't pursue upstream. Thanks for Stats, used it long-term and learned a lot from the codebase. Work continues on personal fork at marxo126/stats for own M4 Max use only. |
|
hi, sorry but as I say I'm more and more convinced to drop the fan support, so most probably all theses PRs have no much sense. That said, this one is something I'm definitely more interested in, and I'll most likely take a closer look at the implementation. Thanks for your time! |
Thanks and no worry, I will contitue working in my fork until everything tested and worked well with my developer account then I will let to you know for test if good or not. |
Why
SMJobBless was deprecated in macOS 13. As of macOS 26.4.1 the OS-level smd daemon now rejects SMJobBless installs that previously worked, silently breaking fan control for end users on the latest macOS:
```
smd: App "eu.exelban.Stats" did not pass helper check "eu.exelban.Stats.SMC.Helper".
```
Reproduces consistently on a fresh macOS 26.4.1 install — clicking Install fan helper silently fails, no helper files land at `/Library/PrivilegedHelperTools/`, and the popup keeps showing the placeholder forever. Affects every user who upgrades to macOS 26.
Change
Migrate the install/uninstall flow to the modern `SMAppService` API (macOS 13+) while keeping the existing SMJobBless code path intact as a fallback for macOS 11/12.
User flow on macOS 13+
Net diff
```
3 files changed, +89 lines net
SMC/Helper/eu.exelban.Stats.SMC.Helper.plist | +14 (new)
Kit/helpers.swift | +55
Stats.xcodeproj/project.pbxproj | +14 (PBXFileReference, PBXBuildFile, PBXCopyFilesBuildPhase, group children, build phase wiring)
```
Testing — important caveat
Compile-verified (`BUILD SUCCEEDED`, plist lands at `Contents/Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist` in the built bundle). Direct API probe of `SMAppService.daemon(plistName:).register()` against the dev build returns `SMAppServiceErrorDomain code 3 — Codesigning failure loading plist (-67028)`, which is the expected rejection of unsigned dev builds — `SMAppService` strictly requires a Developer ID signature.
Behavioural verification therefore requires a Developer ID-signed build, which I don't have locally. The PR is filed as Draft so you can build with your signing cert, register the daemon once, and confirm the user-side flow (System Settings prompt → approval → helper bootstraps).
If the registration call succeeds and the System Settings approval prompt appears, the migration is good. If `register()` throws on a properly-signed build, the most likely culprits are:
Happy to iterate on any of the above.
Pre-13 fallback
`installSMJobBless()` and the older uninstall path are unchanged — verbatim from main. Any users on macOS 11/12 see no behavioural difference.