Skip to content

fix: install the equalizer driver through a privileged helper - #367

Merged
isaaclins merged 4 commits into
mainfrom
fix/357-driver-install
Sep 3, 2026
Merged

fix: install the equalizer driver through a privileged helper#367
isaaclins merged 4 commits into
mainfrom
fix/357-driver-install

Conversation

@isaaclins

@isaaclins isaaclins commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • add the SpotiglassEQPrivilegedHelper LaunchDaemon target and bundle its plist and executable in the app
  • register the daemon with SMAppService.daemon(plistName:) and call it over a code-signing-constrained XPC connection
  • compare bundled and installed driver versions, installing missing or older bundles and repairing unreadable ones before restarting coreaudiod
  • remove the Terminal-command error path and its English, Spanish, and German catalog entries
  • cover version ordering, install decisions, and silent error mapping with unit tests

The helper validates that requests point to the containing signed app's driver and the fixed system HAL destination. It uses copyfile metadata preservation so the driver's signed executable mtime survives the replacement.

make build, make release, make embed-driver, the static audits, and a build-for-testing compile passed locally. I did not execute the XCTest suite because this task explicitly forbids local tests. The authorization prompt, LaunchDaemon approval, coreaudiod reload, 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 generic plus certificate leaf[subject.OU] = "BHAF4L4726" for Apple-signed builds, or the explicitly allowed local self-signed alternative certificate 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.

Copilot AI lite review requested due to automatic review settings September 3, 2026 15:32
@isaaclins
isaaclins force-pushed the fix/357-driver-install branch from 561a28d to e7e5a46 Compare September 3, 2026 15:32
@isaaclins
isaaclins force-pushed the fix/357-driver-install branch from e7e5a46 to 0da05f5 Compare September 3, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 SpotiglassEQPrivilegedHelper LaunchDaemon target (embedded in the app) and an XPC client that registers it via SMAppService.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 @MainActor engine, 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 thread SpotiglassEQPrivilegedHelper/main.swift Outdated
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
isaaclins force-pushed the fix/357-driver-install branch from 0da05f5 to 69c6963 Compare September 3, 2026 16:03
@isaaclins
isaaclins merged commit d949029 into main Sep 3, 2026
2 checks passed
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.

Equalizer must install its audio driver itself, not tell the user to run Terminal commands

2 participants