diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index 261191cf..ad00c07a 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -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 @@ -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 *instances = [NSRunningApplication runningApplicationsWithBundleIdentifier:selfBundleID]; + pid_t myPID = [[NSRunningApplication currentApplication] processIdentifier]; + + NSMutableArray *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; } + } + 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