objective-cmacoscocoaobjective-c++nsworkspace

How reliable is NSWorkspace's runningApplications?


I want to terminate some applications execution after an amount of time passed. I am polling NSWorkspace's runningApplications for a lack of something to observe on (if the application is just running, does it notify of anything?)

My issue is that applications are only sometimes terminated, sometimes they take a few seconds after the time they should be closed (according to an internal timer) and sometimes they do not terminate at all!

I tried using both terminate and forceTerminate methods.

In the code snippet, apps_ is a vector of strings containing application names. It is updated regularly by another thread and its data is received before running the below code. They all run inside an es_handler_block_t

NSArray<NSRunningApplication *> *running_apps = [NSWorkspace sharedWorkspace].runningApplications;
for (const auto &app_ : apps_) {
    //std::cout << app_ << "\n";
    for (NSRunningApplication *app in running_apps) {
        if ([[NSString stringWithUTF8String:app_.c_str()] isEqualToString:[app.executableURL lastPathComponent]] ) {
            std::string app_name = [[app.executableURL absoluteString] UTF8String];
            std::cout << "Terminating app " << app_name << "\n";
            bool res_f = [app forceTerminate];
            bool res_t = [app terminate];
            LOG_DBG("Force terminate: %d", res_f);
            LOG_DBG("Terminate: %d", res_t);
            break;
        }
    }
}

I read in runningApplications documentation that "this property will only change when the main run loop runs in a common mode". What does it mean?

I suppose it is something related to runningApplication's polling, as inserting a breakpoint in the above code (before the if check) and then resuming execution instantly kills the application that would otherwise still run.

I am not blocking the main function. I only have an Endpoint Security class for the framework, networking is done on some other thread, and I return with NSApplicationMain(argc, argv);

What could be the issue? Thanks.

EDIT: I am leveraging the Cocoa Framework to create an agent that displays only in the system tray and it has root privileges. Preventing apps from launching is successfully achieved using the Endpoint Security Framework, but I did not find a reliable way to kill already running applications that works every time.

LATER EDIT: I managed to add an observer to [[NSWorkspace sharedWorkspace] notificationCenter], but what notification should I subscribe to for an application that is running? I tried with hide but it does not work if the user just clicks on the red window button, only if he hides the app from the dock. But I still want to close it even if there is no interaction between the user and the running app.


Solution

  • Terminating apps reliably on macOS depends on a lot of factors, such as:

    1. Entitlements
    2. How the app was launched
    3. If there are any background apps keeping the main app alive as soon as it crashes or is quit by another process

    For example, your app may be allowed to terminate one app, but, for example, if you kill a process that was launched by launchd with a KeepAlive command using a plist like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>KeepAlive</key>
        <true/>
        <key>Label</key>
        <string>com.bundleidentifierOf.AnUnquitabbleApp</string>
        <key>ProgramArguments</key>
        <array>
            <string>/path/to/the/AnUnquitableApp</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
    </dict>
    </plist>
    

    You will be in a for surprise, because the moment you try to do so, launchd will relaunch it immediately, and it will seem as though the app was not killed, but it was. You can see that an app is relaunched, when its PID changes every time you try to kill it.

    You can check that using the command:

    ps -ax
    

    in Terminal to list all processes running on your Mac. If you want to know whether an application is running, then use:

    ps -ax | grep "AnUnquitableAppNameHere"
    

    and then you can kill it by using its PID with

    kill <PID>
    

    This will help you to determine which apps you can kill with a simple QUIT signal, which ones you will need to use stronger signals to terminate them and the ones are unquitable, because to kill them requires privileges or entitlements your app may not have. Read the man page of ps and kill to learn more about your target apps.

    There are many other ways on macOS to gain persistence, and if you want to understand how to reliably quit apps, you need to understand how they gained persistence in the first place.

    For example, if a user has admin / superuser privileges, it can gain persistence by installing a daemon in /Library/LaunchDaemons. If your app has no root privileges, it is unlikely you will be able to kill that process.

    Another way for apps to maintain persistence besides a launchd plist is , for example, have a background app that will relaunch the main app as soon as the latter is forced to quit. An example is that if you use kill to kill the latest version of Microsoft Word you will see that it will be relaunched by such a helper that will complain that Wordwas forced to quit.

    The nicest way to kill an app is actually to send it a Quit Apple Event and that is what I usually do.

    I do not want to discourage you, but what you are trying to achieve is more difficult that it might seem in the first place. And even if you succeed, remember that the user may kill your app and that will defeat what you are trying to achieve, unless it is kept alive by a launchd plist or another form of persistence.

    Now if you want to observe which apps were killed or launched without polling, I definitely recommend that you read TN2050.

    Now replying to your specific questions:

    "this property will only change when the main run loop runs in a common mode"

    This means that you need to have a running loop for NSRunningApplication to be updated. If you have a GUI app, such a loop is already installed for you. If your app is a CLI, then this is not installed and you need to install such a running loop yourself . If you are using Objective-C then you can do as recommended in this question.

    Therefore, what you are trying to achieve has a lot of caveats, and you need to be aware of them before proceeding.