Skip to content

Investigate XPC for CLI-to-daemon IPC #1

@pszypowicz

Description

@pszypowicz

Context

MicGuard currently uses two IPC mechanisms between the CLI (mic-guard) and the running daemon:

  1. Config file writes + DispatchSource file watcher — for enable, disable, set commands. The CLI writes to ~/.config/mic-guard/, the daemon watches the directory with a DispatchSource and reacts.
  2. DistributedNotificationCenter — for ping (and soon mute/setVolume). The CLI posts a notification, the daemon observes it and acts.

Both work reliably today because the app is not sandboxed (distributed via Homebrew, not the Mac App Store).

Why consider XPC

Apple's developer documentation recommends XPC (NSXPCConnection) as the standard macOS pattern for inter-process communication between a CLI tool and a daemon/agent. Key advantages:

  • Bidirectional communication — strongly typed interfaces via NSXPCInterface and Swift protocols. Currently we can't get a response back from the daemon (e.g., confirming a mute toggle succeeded).
  • Reliable delivery — DistributedNotificationCenter docs explicitly warn that notifications may be dropped if the server's queue fills up and have "unbounded latency". XPC guarantees delivery.
  • Structured messages — typed protocol methods vs. parsing string dictionaries from userInfo.
  • Security — built-in process isolation. DistributedNotificationCenter docs note it is "NOT a secure communications protocol".
  • Sandboxing ready — if MicGuard ever moves to the Mac App Store, sandboxed apps cannot send distributed notifications with userInfo (must be nil). XPC works seamlessly with sandboxing.

Current assessment (March 2025)

DistributedNotificationCenter is adequate for now:

  • The app is not sandboxed and has no plans to be
  • All commands are fire-and-forget (the daemon broadcasts statusChanged back, closing the feedback loop naturally via CoreAudio listeners)
  • The existing pattern is simple and proven
  • Adding XPC would require: a Mach service, a launchd plist, a separate XPC service target in the Xcode project, and an NSXPCInterface protocol — significant complexity

What an XPC migration would look like

  1. Define a Swift protocol for the daemon's interface:
    @objc protocol MicGuardDaemonProtocol {
        func toggleMute(reply: @escaping (Bool) -> Void)
        func setVolume(_ volume: Int, reply: @escaping (Bool) -> Void)
        func setPreferredDevice(_ name: String, reply: @escaping (Bool) -> Void)
        func setEnabled(_ enabled: Bool)
        func requestStatus(reply: @escaping ([String: String]) -> Void)
    }
  2. Register a Mach service in the daemon (via NSXPCListener)
  3. Have the CLI connect via NSXPCConnection(machServiceName:)
  4. Replace all file-write and notification-based IPC with XPC calls
  5. Keep DistributedNotificationCenter only for outbound broadcasts to external consumers (sketchybar, etc.) — those are genuinely one-to-many and appropriate for notifications

When to pursue this

  • If the app needs request/response IPC (e.g., CLI needs to confirm an action succeeded)
  • If sandboxing becomes a requirement (Mac App Store distribution)
  • If notification drops become a problem in practice
  • As a general architecture improvement when the IPC surface grows

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestresearchResearch and investigation tasks

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions