Skip to content

Commit cb2159a

Browse files
danielbodartclaude
andcommitted
Block indefinitely waiting for mic permission instead of crash-looping
When mic is denied, poll every 5s forever instead of failing after 30s. This prevents launchd KeepAlive from restarting the process and spawning duplicate permission dialogs. The process stays alive and quiet until the user grants mic access (via the dialog or System Settings). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 87b1e4f commit cb2159a

2 files changed

Lines changed: 19 additions & 25 deletions

File tree

src/platform/macos/audio.zig

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,13 @@ pub const AudioCapture = struct {
6464
// explicitly chose the device.
6565
const mic_status = if (target != null) @as(c_int, 3) else capsper_mic_permission_status();
6666
if (mic_status != 3) {
67-
// Not yet authorized: request permission (or poll if denied).
68-
// For notDetermined (0): shows the system permission dialog and blocks.
69-
// For denied (2): polls for 30s in case a dialog from a previous
70-
// launch is still visible (launchd restarts race with user clicking Allow).
71-
// For restricted (1): polls briefly then fails.
72-
if (mic_status == 1) {
73-
log.err("Microphone access is restricted by system policy.", .{});
74-
return error.AudioInitFailed;
75-
}
76-
log.info("Requesting microphone permission...", .{});
67+
// Not yet authorized — blocks until the user grants permission.
68+
// For notDetermined: shows the system dialog and waits.
69+
// For denied: polls until granted via System Settings.
70+
// Never returns failure except for restricted (system policy).
71+
log.info("Waiting for microphone permission...", .{});
7772
if (capsper_mic_request_permission() == 0) {
78-
log.err("Microphone permission denied.", .{});
79-
log.err("Grant access in: System Settings → Privacy & Security → Microphone", .{});
73+
log.err("Microphone access is restricted by system policy.", .{});
8074
return error.AudioInitFailed;
8175
}
8276
}

src/platform/macos/mic_permission.m

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ int capsper_mic_permission_status(void) {
1010
return (int)[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
1111
}
1212

13-
// Blocks until the user responds to the permission dialog.
14-
// Returns 1 if granted, 0 if denied.
13+
// Blocks until microphone permission is granted.
1514
// If status is notDetermined, shows the permission dialog and blocks.
16-
// If status is denied, polls for up to 30 seconds in case a permission
17-
// dialog from a previous launch is still visible (launchd restarts can
18-
// race with the user clicking "Allow").
15+
// If status is denied, blocks and polls indefinitely — the user may be
16+
// responding to a dialog from a previous launch, or may grant access
17+
// via System Settings. Either way, we wait rather than crash and restart
18+
// (which would spawn duplicate permission dialogs via launchd KeepAlive).
19+
// Returns 1 if granted, 0 only for restricted (system policy, no recovery).
1920
int capsper_mic_request_permission(void) {
2021
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
2122
if (status == AVAuthorizationStatusAuthorized) return 1;
23+
if (status == AVAuthorizationStatusRestricted) return 0;
2224

2325
if (status == AVAuthorizationStatusNotDetermined) {
2426
__block int granted = 0;
@@ -28,19 +30,17 @@ int capsper_mic_request_permission(void) {
2830
dispatch_semaphore_signal(sem);
2931
}];
3032
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
31-
return granted;
33+
if (granted) return 1;
34+
// User denied — fall through to polling below
3235
}
3336

34-
// Status is denied or restricted. Poll briefly in case the user is
35-
// responding to a mic dialog from a previous launch attempt — launchd's
36-
// KeepAlive restarts can overlap with a pending permission prompt.
37-
for (int i = 0; i < 15; i++) {
38-
[NSThread sleepForTimeInterval:2.0];
37+
// Denied: poll every 5s until granted. The user can grant access via
38+
// System Settings → Privacy & Security → Microphone at any time.
39+
while (1) {
40+
[NSThread sleepForTimeInterval:5.0];
3941
if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]
4042
== AVAuthorizationStatusAuthorized) {
4143
return 1;
4244
}
4345
}
44-
return 0;
4546
}
46-

0 commit comments

Comments
 (0)