23

Normally an application bundle on OS X can only be started once, however by simply copying the bundle the same application can be launched twice. What's the best strategy to detect and stop this possibility?

On Windows this effect can simply be achieved by the application creating a named resource at launch and then exit if the named resource can't be created, indicating that another process is running that has already created the same resource. These resources are released in a reliable way on Windows when the application quits.

The problem I have seen when researching this is that the APIs on OS X keep state in the file system and thus makes the strategy used on windows unreliable, i.e lingering files after an improper exit can falsely indicate that the application is already running.

The APIs I can use to achieve the same effect on OS X are: posix, carbon and boost.

Ideas?

John Topley
  • 113,588
  • 46
  • 195
  • 237
Per Ersson
  • 771
  • 2
  • 6
  • 10
  • Why do you even want to do this? Unlike Windows, the operating system takes care of preventing multiple instances of an application from running in the common case. In the uncommon case, why prevent it? – Chris Hanson Apr 05 '09 at 08:13
  • 1
    The application in question is a game. By running multiple copies of the game on a single machine a player would have an unfair advantage over other players in some situations. – Per Ersson Apr 08 '09 at 09:13

9 Answers9

30

This is extremely easy in Snow Leopard:

- (void)deduplicateRunningInstances {
    if ([[NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]] count] > 1) {
        [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Another copy of %@ is already running.", [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]] 
                         defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"This copy will now quit."] runModal];
        
        [NSApp terminate:nil];
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Jeff Seibert
  • 301
  • 3
  • 4
  • 2
    In OS 10.8, doing this check right at start in main.m doesn't work because at that stage the running app itself is not in the array yet (probably only gets registered in the next runloop or so), so instead of "> 1" you'd have to do check for "> 0". To play this safe w.r.t. future versions, it's best to explicitly check the array for the current app: `for (NSRunningApplication* runningApp in runningApplications) { if (![runningApp isEqual:[NSRunningApplication currentApplication]]) { // Alert and exit }}` – Andreas Zollmann Sep 17 '13 at 20:01
  • 2
    One more important issue: runningApplicationsWithBundleIdentifier returns the running applications matching a bundleID, but crucially only those owned by the current user (so these solutions won't prevent different users on this machine from running your app at the same time. – Andreas Zollmann Sep 17 '13 at 20:46
9

A low-level solution is to use flock().

Each instance would attempt to lock a file on startup, and if the lock fails then another instance is already running. Flocks are automagically released when your program exits, so no worries about stale locks.

Note that whatever solution you choose, you need to make a conscious decision about what it means to have "multiple instances". Specifically, if multiple users are running your app at the same time, is that ok?

vasi
  • 1,026
  • 7
  • 6
  • Thanks, that solution will be fine. The lock files will be per user to not block multiple users on the same machine to start the app at the same time. – Per Ersson Mar 26 '09 at 19:09
8

There's a mysterious Info.plist key called "Application prohibits multiple instances," but it doesn't seem to work for me. I am writing a CLI application and executing it from within a bundle. Perhaps it would work in a GUI application, but I haven't tried.

Raffi Khatchadourian
  • 3,042
  • 3
  • 31
  • 37
  • 4
    This key (LSMultipleInstancesProhibited) works well when the app is launched from Launchpad or Finder. As a bonus the already running app is brought to the front. To me this is better then displaying an error dialog. The key does not work when the app is started from command line. – Maf Dec 01 '15 at 08:23
4

As has already been mentioned Cocoa applications do not usually allow you to run more than one instance at a time.

In general, a cocoa way to solve this look at launchedApplications in NSWorkspace. This returns an NSArray containing a dictionary for each launched application. You can loop through the array to see if the app you are looking for is already running. I would advise that you use the value with the key NSApplicationBundleIdentifier which will have a value like "com.mycompany.myapp" rather than looking for the name. If you need to find the bundle identifier for an app you can look at its info.plist file in the app package.

Jon Steinmetz
  • 4,104
  • 1
  • 23
  • 21
3

First off, it's “Mac OS X” or “OS X”. There is no such thing as “OS/X”.

Second, Mac OS X doesn't come with Boost; you would need to bundle it with your application.

Third, most of Carbon is not available in 64-bit. This is a clear signal that those portions of Carbon will go away someday (when Apple abandons 32-bit in its hardware). Sooner or later, you will have to either rewrite your app with Cocoa or abandon the Mac.

Normally an application bundle on OS/X can only be started once, however by simply renaming the bundle the same application can be launched twice.

No it can't. Launching the renamed or moved application will simply activate (bring to the front) the process that was already running; it won't start a new, second process alongside the first one.


There are several ways to tell whether an application is already running. In each case, you do this on launch:

  1. Use Cocoa's NSConnection to register a connection with a single constant name. This will fail if the name is already registered. (You can use Foundation from a Carbon app; it's the Application Kit you have to be careful with.)
  2. Use the Process Manager to scan the process list for processes whose bundle identifier match the one you're looking for. The bundle identifier isn't unchangeable, but it's harder to change than the filename or location.
  3. If you're looking to see when someone runs a second copy of yourself, you can use CFNotificationCenter:

    1. Add yourself as an observer for “com.yourdomain.yourappname.LaunchResponse”.
    2. Post a notification under the name “com.yourdomain.yourappname.LaunchCall”.
    3. Add yourself as an observer for “com.yourdomain.yourappname.LaunchCall”.

    In your observation callback for the Call notification, post the Response notification.
    In your observation callback for the Response notification, exit.

    Thus, when the first process starts, it will Call and get no Response; when the second process starts, it will Call, get a Response from the first process, and exit in deference to the first.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
2

This is a combination of Romans' and Jeff's answers for Swift 2.0: If another instance of the app with the same bundle ID is already running, show an alert, activate the other instance and quit the duplicate instance.

func applicationDidFinishLaunching(aNotification: NSNotification) {
    /* Check if another instance of this app is running. */
    let bundleID = NSBundle.mainBundle().bundleIdentifier!
    if NSRunningApplication.runningApplicationsWithBundleIdentifier(bundleID).count > 1 {
        /* Show alert. */
        let alert = NSAlert()
        alert.addButtonWithTitle("OK")
        let appName = NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleNameKey as String) as! String
        alert.messageText = "Another copy of \(appName) is already running."
        alert.informativeText = "This copy will now quit."
        alert.alertStyle = NSAlertStyle.CriticalAlertStyle
        alert.runModal()

        /* Activate the other instance and terminate this instance. */
        let apps = NSRunningApplication.runningApplicationsWithBundleIdentifier(bundleID)
        for app in apps {
            if app != NSRunningApplication.currentApplication() {
                app.activateWithOptions([.ActivateAllWindows, .ActivateIgnoringOtherApps])
                break
            }
        }
        NSApp.terminate(nil)
    }

    /* ... */
}
seb
  • 2,350
  • 24
  • 30
1

This is a version of seb's for Swift 3.0: If another instance of the app with the same bundle ID is already running, show an alert, activate the other instance and quit the duplicate instance.

func applicationDidFinishLaunching(aNotification: NSNotification) {
    /* Check if another instance of this app is running. */
    let bundleID = Bundle.main.bundleIdentifier!
    if NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).count > 1 {
         /* Show alert. */
         let alert = NSAlert()
         alert.addButton(withTitle: "OK")
         let appName = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as! String
         alert.messageText = "Another copy of \(appName) is already running."
         alert.informativeText = "This copy will now quit."
         alert.alertStyle = NSAlert.Style.critical
         alert.runModal()

         /* Activate the other instance and terminate this instance. */
         let apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
             for app in apps {
                  if app != NSRunningApplication.current {
                      app.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
                      break
                  }
             }
                NSApp.terminate(nil)
         }   
       /* ... */
}
Mavro
  • 571
  • 8
  • 19
  • This does not seem to work when the app is run by different users. In that case, `NSRunningApplication.runningApplications` does not return the other user's instances. – akim May 29 '18 at 16:19
1

What about IPC? You could open a socket and negotiate with the other launched instance. You'd have to be careful though, that it works if both apps start at the same time.

I can't provide you with sample code, as I haven't (yet, but I will soon) used it.

Georg Schölly
  • 124,188
  • 49
  • 220
  • 267
  • 2
    Be careful that you don't break your app's ability to run under multiple users at the same time. An application that quits when another user is already using it is broken. – Peter Hosey Mar 26 '09 at 08:41
0

detect if application with same bundleID is running, activate it and close what starts.

- (id)init method of < NSApplicationDelegate >

    NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]];
    if ([apps count] > 1)
    {
        NSRunningApplication *curApp = [NSRunningApplication currentApplication];
        for (NSRunningApplication *app in apps)
        {
            if(app != curApp)
            {
                [app activateWithOptions:NSApplicationActivateAllWindows|NSApplicationActivateIgnoringOtherApps];
                break;
            }
        }
        [NSApp terminate:nil];
        return nil;
    }
Roman Solodyashkin
  • 799
  • 12
  • 17