Skip to content
Merged
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
57 changes: 57 additions & 0 deletions apps/finicky/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ - (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)sho

// Use bool for openWindow and related logic
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
[self terminateOtherInstances];

bool openWindow = self.forceOpenWindow;
if (!openWindow) {
// Even if we aren't forcing the window to open, we still want to open it if didn't receive a URL
Expand All @@ -44,6 +46,61 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification {
QueueWindowDisplay(openWindow);
}

// Ensure only one Finicky process is running. macOS's Launch Services normally
// routes GetURL events to an already-running instance, but that routing can fail
// (stale LS registration after app moves/updates, different bundle paths, SSO
// agents launching from another context) and silently spawn a second process
// that proceeds to create its own status bar icon. Without a guard, duplicates
// accumulate in the menu bar over time. Terminate any other instances we find
// with the same bundle identifier so we're the single surviving process.
- (void)terminateOtherInstances {
NSString *selfBundleID = [[NSBundle mainBundle] bundleIdentifier];
if (!selfBundleID) {
return;
}

NSArray<NSRunningApplication *> *instances = [NSRunningApplication runningApplicationsWithBundleIdentifier:selfBundleID];
pid_t myPID = [[NSRunningApplication currentApplication] processIdentifier];

NSMutableArray<NSRunningApplication *> *duplicates = [NSMutableArray array];
for (NSRunningApplication *app in instances) {
if ([app processIdentifier] == myPID) continue;
if ([app isTerminated]) continue;
[duplicates addObject:app];
}

if (duplicates.count == 0) {
return;
}

// -[NSRunningApplication terminate] returning YES only means the request was
// sent, not that the target has exited. Fire all requests in parallel, then
// share a single deadline across the poll so 16 hung duplicates don't cost
// 16x the wait budget.
for (NSRunningApplication *app in duplicates) {
NSLog(@"Terminating duplicate Finicky instance (pid %d)", [app processIdentifier]);
[app terminate];
}

NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:1.0];
while ([deadline timeIntervalSinceNow] > 0) {
BOOL anyAlive = NO;
for (NSRunningApplication *app in duplicates) {
if (![app isTerminated]) { anyAlive = YES; break; }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!anyAlive) return;

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
}

for (NSRunningApplication *app in duplicates) {
if ([app isTerminated]) continue;
NSLog(@"Duplicate Finicky instance (pid %d) did not exit within 1s; force-terminating", [app processIdentifier]);
[app forceTerminate];
}
}

- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag {
if (!flag) {
// If there are no visible windows, we should open a new one
Expand Down
Loading