3

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.

Razvan Axinie
  • 118
  • 1
  • 7
  • Your question must be better formulated, as I really do not understand what your motivations are. I regularly terminate apps with my app without a problem and without polling. However, you need the right entitlements to terminate a given app. Thus, you need to better explain what kind of applications you want to terminate, so that we can help you. Are you talking about only user land apps, or are you talking about any process running on MacOS? (Actually, NSRunningApplication is quite useless if you want to enumerate all processes running on your machine). – jvarela Jun 01 '20 at 00:10
  • Are you talking about specific apps, or any app regardless if it has a bundle or not? – jvarela Jun 01 '20 at 00:11
  • I want to terminate user apps, like Twitter, Whatsapp, etc. My motivation is software that restricts access to time-wasting apps, however one is allowed to use them for a few minutes if he wants to. After that he can't no more. If he tries to open a blocked app he is not allowed and the program runs fine. It's terminating a running app that does not seem to work so well. – Razvan Axinie Jun 01 '20 at 07:41

1 Answers1

2

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.

jvarela
  • 3,744
  • 1
  • 22
  • 43
  • Thank you for your elaborate answer, I realised I missed some details in my initial post. It seems that most (maybe all) applications that I want to kill do not have the kill 'prevention', as calling terminate on their corresponding NSRunningApplication does kill them successfully, but sometimes it does not (a bug that I cannot reproduce constantly, it happens just sometimes). – Razvan Axinie Jun 02 '20 at 06:08
  • "If you have a GUI app, such a loop is already installed for you. " - I think this is the key. I only have a UIElement (app is agent), no window for GUI. If I click my app icon in the system tray (top right of screen, next to wifi etc), suddenly everything works as expected. The applications that should be terminated are indeed. Apparently there is some sort of interaction with the app needed to update NSWorkspace's applications. – Razvan Axinie Jun 03 '20 at 07:19
  • Yes, because you need a running run loop to receive update events. Otherwise the info you have does not change. – jvarela Jun 03 '20 at 07:27
  • Then can I somehow do this from code if there is no user interaction with my app by design? It is a Cocoa app but with no windows. I have an AppDelegate and I return from main with NSApplicationMain. – Razvan Axinie Jun 03 '20 at 07:34
  • Of course, the solution is in my answer. Read the part where I say how you can install such a loop. :) – jvarela Jun 03 '20 at 07:36
  • Yes I saw that but I want to keep my icon in the system tray – Razvan Axinie Jun 03 '20 at 07:39
  • Hello, i recently needed to write a CLI app that works with NSWorkspace. I come out that I need a runLoop as you guys said and I made it. If you want to keep you system try you can for sure, if you have it you should have the mainRunLoop installed also. So to trigger NSWorkspace correcly use something like `[[NSRunLoop mainRunLoop] performBlock: ... ]`, add your notification observer from the mainRunLoop using `performBlock:` or similar. I wrote an answer in the discussion that @jvarela linked. https://stackoverflow.com/a/73827541/1197170 – cescobaz Sep 23 '22 at 12:22