0

I need to preface this by saying that I'm coming from years of developing on Windows. This time I'm developing the UI for my macOS app using SwiftUI. The goal is to allow only one instance of the app, depending on where it's started from. For instance, if you copy the app into:

/Users/me/Documents/MyAppCopy/MyApp.app

and to:

/Users/me/Documents/MyApp.app

There should be only two instances of the app allowed, each from those respective locations.

On Windows I would use a named kernel object, say a named event, create it when the app starts and see if it already existed. If so, I will quit the app. Then when the first instance of the app closes, the named event is destroyed by the system automatically.

So I thought to try the same on macOS, but evidently Linux/BSD treats named objects differently.

If I do get the name of the object by calling:

var objName : Bundle.main.bundlePath
IsAnotherInstanceRunning(objName, objName.lengthOfBytes(using: String.Encoding.utf8))

and then using C, remove slashes from it, and use it in a named semaphore:

bool IsAnotherInstanceRunning(const char* pBundlePath, size_t szcbLnPath)
{
    bool bResult = false;

    char* pName = (char*)malloc(szcbLnPath + 1);
    if(pName)
    {
        memcpy(pName, pBundlePath, szcbLnPath);
        pName[szcbLnPath] = 0;
        
        //Remove slashes
        int cFnd = '/';
        char *cp = strchr(pName, cFnd);
        while (cp)
        {
            *cp = '_';
            cp = strchr(cp, cFnd);
        }
        
        //Create if doesn't exist, and return an error if it exists
        sem_t *sem = sem_open(pName, O_CREAT | O_EXCL, 0644, 0);
        if(sem == SEM_FAILED)
        {
            //Failed, see why
            int nErr = errno;
            if(nErr == EEXIST)
            {
                //Already have it
                bResult = true;
            }
        }
        
        free(pName);
    }

    return bResult;
}

Assuming that the path name isn't too long (this is an issue, but it is irrelevant to this question) - the approach above works, but it has one downside.

If I don't close and remove the name semaphore with:

sem_close(sem);
sem_unlink(pName);

It stays in the system if the instance of my app crashes. Which creates an obvious problem for the code above.

So how would you do this on Linux/macOS?

c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • If you want to check if your app is already running then take a look at `NSRunningApplication`. – Willeke Dec 07 '22 at 10:27
  • If the app crashes, there won't be any direct way to determine that it has gone away. We could operate some periodic integrity check using `semctl(cmd=GETPID)` to find the last process that touched each semaphore in the state the app used. If that process is lost in the check, we could perform the given cleanup `sem_close(sem)`. This [ans](https://stackoverflow.com/a/2058642/8458465) seems to do this using a lock file attached to it. – Jishan Shaikh Dec 07 '22 at 10:31

1 Answers1

1

To prevent/abort the start of an app while another instance is running, you can also use high-level AppKit stuff (we do this in one of our apps and it works reliably).

Use NSWorkspace.runningApplications to get a list of, well, the currently running applications. Check this list, and filter it for the bundleIdentifier of your app. You can then also check the bundleURL to decide whether it's OK to start the current app, which seems to be what you want to do. See also NSRunningApplication.current to get the informations about your current process.

(You can do [otherRunningApplication isEqual:NSRunningApplication.current] to check/filter the current process.)

Do this check in your applicationWillFinishLaunching or applicationDidFinishLaunching method.

DarkDust
  • 90,870
  • 19
  • 190
  • 224
  • Thanks. I'm using SwiftUI. Does it also have `applicationWillFinishLaunching`? – c00000fd Dec 07 '22 at 14:15
  • @c00000fd: For pure SwiftUI, you can use [`NSApplicationDelegateAdaptor `](https://developer.apple.com/documentation/swiftui/nsapplicationdelegateadaptor) to integrate an app delegate implementation. See also [this example](https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-an-appdelegate-to-a-swiftui-app). – DarkDust Dec 08 '22 at 07:27
  • Sorry for this delay. I tried to use NSApplicationDelegateAdaptor in my macOS app (that example you gave me is for iOS) and I can't seem to have either applicationWillFinishLaunching nor applicationDidFinishLaunching to be called when my app starts. Would you mind providing a code sample how it needs to be done? – c00000fd Dec 10 '22 at 15:46