Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/bluetooth-prompt-mcp-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"ewiz": patch
---

**No more Bluetooth permission prompt from the Automation tab.** The tab's live state read
your connected Bluetooth devices every few seconds, which made macOS ask for Bluetooth
access on every update and relaunch eWiz when you allowed it. Bluetooth is now read only
when a rule actually depends on it.

**The AI-agent server no longer crashes when a session ends.** Ending an agent session
sent a signal that crashed `ewiz-mcp` instead of releasing its keep-awake.
4 changes: 3 additions & 1 deletion Sources/eWiz/TriggerSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ struct TriggerLiveStateView: View {
Divider().padding(.leading, 12)
row("USB devices", list(s.usbDevices, empty: "None"))
Divider().padding(.leading, 12)
row("Bluetooth", list(s.bluetoothDevices, empty: "None connected"))
row("Bluetooth", store.observesBluetooth
? list(s.bluetoothDevices, empty: "None connected")
: "Read when a rule uses it (needs Bluetooth access)")
Divider().padding(.leading, 12)
row("Volumes", list(s.externalVolumes, empty: "None mounted"))
Divider().padding(.leading, 12)
Expand Down
16 changes: 14 additions & 2 deletions Sources/eWiz/TriggerStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,23 @@ final class TriggerStore: ObservableObject {

/// Sensors we need this tick: everything while a live view is open, otherwise
/// only what the enabled rules actually reference.
///
/// Bluetooth is the exception: it's read only when a rule asks for it. Listing paired
/// devices goes through IOBluetooth, which macOS gates behind a permission prompt, and
/// on grant it restarts the app. eWiz is ad-hoc signed, so TCC ties that grant to the
/// exact build and asks again after every update. Opening the Automation tab was
/// triggering all of that for a row nobody had set a rule on.
private var kindsInUse: Set<TriggerKind> {
if viewers > 0 { return Set(TriggerKind.allCases) }
return Set(rules.filter(\.enabled).flatMap { $0.conditions.map(\.kind) })
let ruleKinds = Set(rules.filter(\.enabled).flatMap { $0.conditions.map(\.kind) })
guard viewers > 0 else { return ruleKinds }
var all = Set(TriggerKind.allCases)
if !ruleKinds.contains(.bluetoothDevice) { all.remove(.bluetoothDevice) }
return all
}

/// Whether the live view is reading Bluetooth, so it can say why the row is blank.
var observesBluetooth: Bool { kindsInUse.contains(.bluetoothDevice) }

/// Start, retime, or stop the poll depending on what's needed right now.
private func reconfigure() {
timer?.invalidate()
Expand Down
9 changes: 8 additions & 1 deletion Sources/ewiz-mcp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ let server = MCPServer(awake: awake, version: version)

// A lid hold outlives this process, so it's handed back however the session ends: the
// client closing stdin (below) or a signal (here).
//
// `awake` is bound to a local first. Top-level variables in main.swift are main-actor
// isolated under Swift 6, and reading one from the signal source's global queue tripped
// the runtime's isolation assertion: every SIGTERM from a client ending its session
// crashed the server (EXC_BREAKPOINT in dispatch_assert_queue) instead of releasing the
// hold. AgentAwake is Sendable and guards itself with a lock, so the local is safe anywhere.
let awakeForSignals = awake
var signalSources: [DispatchSourceSignal] = []
for sig in [SIGTERM, SIGINT, SIGHUP] {
signal(sig, SIG_IGN)
let source = DispatchSource.makeSignalSource(signal: sig, queue: .global())
source.setEventHandler {
awake.shutdown()
awakeForSignals.shutdown()
exit(0)
}
source.resume()
Expand Down
Loading