fix: install the equalizer driver through a privileged helper - #367
Merged
Conversation
isaaclins
force-pushed
the
fix/357-driver-install
branch
from
September 3, 2026 15:32
561a28d to
e7e5a46
Compare
isaaclins
force-pushed
the
fix/357-driver-install
branch
from
September 3, 2026 15:34
e7e5a46 to
0da05f5
Compare
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a root-privileged LaunchDaemon + XPC installation path that needs careful human validation (security constraints and main-thread blocking risks) on a real signed/notarized build.
Pull request overview
This PR implements a privileged-helper-based installation path for the Spotiglass equalizer HAL driver, replacing the previous “run these commands in Terminal” UX with an SMAppService LaunchDaemon + code-signing-constrained XPC flow, as required by issue #357.
Changes:
- Adds the
SpotiglassEQPrivilegedHelperLaunchDaemon target (embedded in the app) and an XPC client that registers it viaSMAppService.daemon(plistName:)and requests driver installation. - Introduces a driver version/install policy (compare bundled vs installed; install missing/stale, repair unreadable) and maps helper failures to non-user-facing diagnostics.
- Updates tests, scripts, and documentation; removes the localized Terminal-instructions error strings.
File summaries
| File | Description |
|---|---|
| SpotiglassTests/EqualizerHALPluginTests.swift | Updates install tests to exercise version-based replacement and verify user-facing error suppression. |
| SpotiglassTests/EqualizerDriverInstallPolicyTests.swift | Adds unit tests for version ordering, install decisions, and diagnostic-only error mapping. |
| SpotiglassEQPrivilegedHelper/main.swift | Implements the privileged helper XPC service that validates request paths, copies the driver bundle, and restarts coreaudiod. |
| SpotiglassEQPrivilegedHelper/com.isaaclins.spotiglass.eqprivilegedhelper.plist | Adds LaunchDaemon plist defining the Mach service and embedded program path. |
| SpotiglassEQDriver/README.md | Documents the new privileged-helper installation approach. |
| SpotiglassEQDriver/build-driver.sh | Ensures the standalone driver build stamps version fields for upgrade detection and removes Terminal install instructions. |
| Spotiglass/Playback/EqualizerPrivilegedHelperClient.swift | Adds the SMAppService registration + XPC client used to install the driver via the helper. |
| Spotiglass/Playback/EqualizerHALPluginController.swift | Switches install/repair to policy-driven decisions and delegates privileged installation to the helper client. |
| Spotiglass/Playback/EqualizerDriverInstallPolicy.swift | Adds driver version parsing/comparison, install decision logic, and helper error mapping. |
| Spotiglass/Playback/AudioEqualizerEngine.swift | Stops surfacing helper/driver-install diagnostics to the UI by using user-facing descriptions only. |
| Spotiglass/Localizable.xcstrings | Removes the user-visible Terminal-instruction error strings for EQ driver installation. |
| Spotiglass/App/SpotiglassApp.swift | Tweaks startup-restore description to reflect the new “logged + reset” behavior for driver unavailability. |
| Spotiglass.xcodeproj/project.pbxproj | Adds the helper target, embeds helper + plist into the app bundle, and links ServiceManagement. |
| scripts/sparkle-release.sh | Passes driver version env vars into the driver build and signs the embedded privileged helper before sealing the app. |
| scripts/setup-eq-driver-signing.sh | Updates developer instructions to rely on the helper install flow instead of manual Terminal commands. |
| scripts/eq-qa.sh | Updates the manual QA checklist for authorization prompt + helper-driven install/restart + silent repair/upgrade. |
| scripts/coverage-allowlist.json | Adds coverage allowlisting for the new helper client code path. |
| Makefile | Propagates marketing/build versions into the driver build and updates embed-driver messaging to match the helper flow. |
| docs/equalizer.md | Updates end-user and architecture docs for helper-driven install and silent upgrades/repairs. |
| docs/equalizer-xcode-target.md | Documents the helper target embedding requirements and verification steps. |
| docs/equalizer-qa.md | Updates QA steps to cover authorization, helper-driven restart, and silent upgrade/repair. |
| docs/equalizer-proof.md | Updates “proof bundle” to reflect the helper install path and new test coverage areas. |
| docs/ci-and-releases.md | Notes the helper as an explicitly signed nested artifact during local releases. |
| docs/building-and-testing.md | Updates build/testing docs to reflect helper embedding and signing order in release packaging. |
Review details
Suppressed comments (1)
Spotiglass/Playback/EqualizerPrivilegedHelperClient.swift:215
- The XPC request blocks the calling thread for up to 30 seconds via DispatchSemaphore.wait(). Since EQ enable flows through a
@MainActorengine, this can hang the UI and risks watchdog termination if the helper is slow to respond. Consider using an async continuation-based wrapper (or moving the call to a background task) rather than blocking the caller.
let waitResult = completion.wait(timeout: .now() + 30)
connection.invalidate()
guard waitResult == .success else {
throw EqualizerDriverInstallError.helperUnavailable(
message: "the helper did not reply within 30 seconds"
)
}
- Files reviewed: 24/24 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+16
to
+21
| enum EqualizerPrivilegedHelperIdentity { | ||
| static let machServiceName = "com.isaaclins.spotiglass.eqprivilegedhelper" | ||
| static let plistName = "com.isaaclins.spotiglass.eqprivilegedhelper.plist" | ||
| static let helperRequirement = "identifier \"com.isaaclins.spotiglass.eqprivilegedhelper\"" | ||
| static let driverBundleName = "SpotiglassEQDriver.driver" | ||
| static let driverRelativePath = "Contents/Library/Audio/Plug-Ins/HAL" |
Comment on lines
+12
to
+15
| private enum EqualizerPrivilegedHelperIdentity { | ||
| static let machServiceName = "com.isaaclins.spotiglass.eqprivilegedhelper" | ||
| static let clientRequirement = "identifier \"com.isaaclins.spotiglass\"" | ||
| static let driverBundleName = "SpotiglassEQDriver.driver" |
Comment on lines
+474
to
+482
| let deadline = Date().addingTimeInterval(driverLoadTimeout) | ||
| repeat { | ||
| if let deviceID = lookupSpotiglassEQDeviceID() { | ||
| return deviceID | ||
| } | ||
| if Date() >= deadline { break } | ||
| Thread.sleep(forTimeInterval: min(0.05, max(0, deadline.timeIntervalSinceNow))) | ||
| } while true | ||
| return nil |
Comment on lines
+99
to
+103
| if fileManager.fileExists(atPath: destinationURL.path) { | ||
| try fileManager.removeItem(at: destinationURL) | ||
| } | ||
| try fileManager.moveItem(at: temporaryURL, to: destinationURL) | ||
| try restartCoreAudio() |
| atomically: true, | ||
| encoding: .utf8 | ||
| ) | ||
| try? writeDriverVersion("0.1.0", build: "1", to: fixtureBundle) |
isaaclins
force-pushed
the
fix/357-driver-install
branch
from
September 3, 2026 16:03
0da05f5 to
69c6963
Compare
This was referenced Sep 3, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SpotiglassEQPrivilegedHelperLaunchDaemon target and bundle its plist and executable in the appSMAppService.daemon(plistName:)and call it over a code-signing-constrained XPC connectioncoreaudiodThe helper validates that requests point to the containing signed app's driver and the fixed system HAL destination. It uses
copyfilemetadata preservation so the driver's signed executable mtime survives the replacement.make build,make release,make embed-driver, the static audits, and abuild-for-testingcompile passed locally. I did not execute the XCTest suite because this task explicitly forbids local tests. The authorization prompt, LaunchDaemon approval,coreaudiodreload, and audible EQ path still need a human to verify on a real signed and notarized build.Closes #357
Security hardening: the XPC code-signing requirements now pin the expected bundle identifier and signer, using
anchor apple genericpluscertificate leaf[subject.OU] = "BHAF4L4726"for Apple-signed builds, or the explicitly allowed local self-signed alternativecertificate leaf[subject.CN] = "Spotiglass Local Dev". The helper validates the bundled driver signature against the same signer policy before copying it into the system HAL directory.