NEFilterManager completion handler not called from Command Line Tool

Hello, I'm experiencing an issue with enabling a Content Filter Network Extension from a command line tool. When I call the LoadFromPreferences method on NEFilterManager.shared() the completion handler is not called.

I've tried this with a simple semaphore and tried running it on a RunLoop, but none of this works.

Any help would be appreciated.

I've tried adding a small demo project illustrating the issue, but the add file option does not seem to work.

I'll paste the code here:

Semaphore Demo

class SemaphoreDemo {

    let filterManager = NEFilterManager.shared()
    var semaphore = DispatchSemaphore(value: 0)

    func demo() {
        print("Semaphore demo")
        self.filterManager.loadFromPreferences { (error) in
            print("Load from preferences callback")

            if let error = error {
                print("ERROR \(error.localizedDescription)")
                return
            }

            let config = NEFilterProviderConfiguration()
            config.filterDataProviderBundleIdentifier  = "BUNDLE_IDENTIFIER"
            config.filterSockets = true

            self.filterManager.isEnabled = true
            self.filterManager.localizedDescription = "LOCALIZED_DESCRIPTION"
            self.filterManager.providerConfiguration = config

            self.filterManager.saveToPreferences { (error) in
                if let error = error {
                    print("ERROR \(error.localizedDescription)")
                } else {
                    print("SUCCESS")
                }
                self.semaphore.signal()
            }
        }

        self.semaphore.wait()
    }
}
class RunloopDemo {

    let filterManager = NEFilterManager.shared()

    func demo() {
        print("Runloop demo")

        let currentRunLoop = CFRunLoopGetCurrent()
//        let currentRunLoop = CFRunLoopGetMain()

        self.filterManager.loadFromPreferences { [weak currentRunLoop] (error) in
            print("Load from preferences callback")

            if let error = error {
                print("ERROR \(error.localizedDescription)")
                return
            }

            let config = NEFilterProviderConfiguration()
            config.filterDataProviderBundleIdentifier  = "Bundle IDENTIFIER"
            config.filterSockets = true

            self.filterManager.isEnabled = true
            self.filterManager.localizedDescription = "LOCALIZED DESCRIPTION"
            self.filterManager.providerConfiguration = config

            self.filterManager.saveToPreferences { (error) in
                if let error = error {
                    print("ERROR \(error.localizedDescription)")
                } else {
                    print("SUCCESS")
                }
                CFRunLoopStop(currentRunLoop)
            }
        }

        CFRunLoopRun()
    }
}

The callback is never called.

Thanks.

Answered by DTS Engineer in 792574022

SO, the first question to ask here is what your code actually "does"?

Based on my read of the code, I believe what you're actually getting is that it prints:

"Hello, World!"
"Runloop demo"

...and then exits. To confirm things fully, I'd actually recommend adding one more print statement after "runloopDemo.demo()" saying "Exiting" at which point I believe you'll see this:

"Hello, World!"
"Runloop demo"
"Exiting"

Everything below this assumes this basic description is correct, but if it's not correct then I'd like to see what it did print and think about things further. What I've described below is definitely at least part of the problem, but it's possible there are other complications as well.

Anyway, building on that assumption:

The Problem:

The underlying cause of all this is that CFRunLoopRun never blocked and simply returned immediately. What's going on here is actually the standard behavior of the run loop system, though the NSRunLoop documentation states it better than CFRunLoop:

"If no input sources or timers are attached to the run loop, this method exits immediately;"

In other words, if the run loop doesn't have something to "do", it returns immediately. That's a straightforward description, but in practice it can be far more complicated/tricky. That's because:

  1. It's ENTIRELY possible to use/rely on a run loop without actually meeting the requirement above. As a concrete example, the code below uses the main run loop, but it does NOT meet the requirements above and will NOT prevent the run loop from returning. FYI, this broad approach is a common pattern in most of our frameworks and is very likely what "loadFromPreferences" actually does.
DispatchQueue.global().async {
    print("Global")
    //Do work...
    DispatchQueue.main.async {
        //Call delegate so the work is done
        print("Main")
    }
}

  1. In practice, VERY little of our code EXPLICITLY says what how it actually interacts with the run loop, which means you can't actually "know" whether any of our code will "hold" the run loop. That means even if something happens to work now, there's no guarantee it will work in the future.

In other words, the only way you can guarantee that the run loop won't return on your is have specifically attached "something" that prevents it's return.

Solutions:

1) Use DispatchMain()

Architecturally, the system actually has two mechanisms for "event dispatch"- the run loop based architecture and the Dispatch based system. Within that context, "DispatchMain" acts as the GCD equivalent of "NSApplicationMain". It's role is to park the main and process the main queue in case where the run loop can't/shouldn't be used.

I mention DispatchMain for completeness, but I would NOT recommend using it. The problem here is issue #2 above. DispatchMain works fine when the code involved relies on GCD, but it isn't (by design) a full replacement for the run loop. You could use it if you KNEW none of the code you were using used run loops but, by defintion, you don't know that because you're using our code. This is what can make it particularly painful- it's not that it won't work, it's that it WILL work fine... until you use the wrong API, at which point you end up creating exactly the kind of weird failure that started all of this. Note that these issues do NOT apply in reverse. In concrete terms, "DispatchQueue.main.async" works find with a run loop, but "performSelectorOnMainThread" does not work with DispatchMain.

2) Take Control of the Run Loop

The whole problem here occurs because the run loop doesn't have anything to wait on, so give it something to wait on. The simplest approach is to simple schedule a timer to "hold" the run loop. For development purposes, I generally start with something like this:

Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
    print("Still Running")
}

...so the repeating timer reassures me that the main run loop is still there waiting for work. Shipping code often ends up needing a timer for SOMETHING, but if you don't have any use then you can also set it to an arbitrarily large value so that it "never" fires. What matters here is that the timer exists, not that it actually fires.

A word on how you finish all your work. You're code tries to exit by calling:

CFRunLoopStop(currentRunLoop)

...but that actually has problems with issue #2 as well. Just like you can't guarantee our code is "holding" the run loop, you ALSO can't guarantee ISN'T holding the run loop. It's entirely possible to get all of this code working, only to find that now your code won't exit... because "someone else" is holding the run loop. In theory you could fix this by cleaning up everything "properly", but in practice that's tricky to manage, can break when something change, and makes everything slower.

My recommendation here is that you do whatever cleanup/saving/etc you code requires, then use Dispatch.after to directly call "exit()" after a brief delay (my default is ~0.1s). The delay gives our frameworks a short amount of time for allow any IPC message to clear (most of our frameworks rely IPC to supporting daemon) and also ensure that you're not calling "exit" in the middle of our own implementation. Note that while calling exit(0) like this sounds bad/dangerous, this is in fact exactly how AppKit actually quits, as well as many other parts of the system.

Closing Tidbits

A few extra points I want to highlight here:

-Quinn has an extended write up about run loops that's worth reviewing. It doesn't directly address what's happening here, but I'd recommend reviewing it.

-NSRunLoop and CFRunLoop are DIRECT equivalents and my recommendation would be to use NSRunLoop as your "base" API. It's documentation is a bit better and getCFRunLoop will get you to CFRunLoop if you actually need it for a specific API.

-As a broad warning, my (admittedly skewed) experience is that DispatchSemaphore is more often a sign of misunderstandings and problems than it is a useful API. If you understand how the system works it's unnecessary and if you don't understand how the system works... it doesn't work. It's not a tool I'd recommend reaching for.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

main.swift contains this code:

import Foundation

print("Hello, World!")
//let semaphoreDemo = SemaphoreDemo()
//semaphoreDemo.demo()

let runloopDemo = RunloopDemo()
runloopDemo.demo()```

SO, the first question to ask here is what your code actually "does"?

Based on my read of the code, I believe what you're actually getting is that it prints:

"Hello, World!"
"Runloop demo"

...and then exits. To confirm things fully, I'd actually recommend adding one more print statement after "runloopDemo.demo()" saying "Exiting" at which point I believe you'll see this:

"Hello, World!"
"Runloop demo"
"Exiting"

Everything below this assumes this basic description is correct, but if it's not correct then I'd like to see what it did print and think about things further. What I've described below is definitely at least part of the problem, but it's possible there are other complications as well.

Anyway, building on that assumption:

The Problem:

The underlying cause of all this is that CFRunLoopRun never blocked and simply returned immediately. What's going on here is actually the standard behavior of the run loop system, though the NSRunLoop documentation states it better than CFRunLoop:

"If no input sources or timers are attached to the run loop, this method exits immediately;"

In other words, if the run loop doesn't have something to "do", it returns immediately. That's a straightforward description, but in practice it can be far more complicated/tricky. That's because:

  1. It's ENTIRELY possible to use/rely on a run loop without actually meeting the requirement above. As a concrete example, the code below uses the main run loop, but it does NOT meet the requirements above and will NOT prevent the run loop from returning. FYI, this broad approach is a common pattern in most of our frameworks and is very likely what "loadFromPreferences" actually does.
DispatchQueue.global().async {
    print("Global")
    //Do work...
    DispatchQueue.main.async {
        //Call delegate so the work is done
        print("Main")
    }
}

  1. In practice, VERY little of our code EXPLICITLY says what how it actually interacts with the run loop, which means you can't actually "know" whether any of our code will "hold" the run loop. That means even if something happens to work now, there's no guarantee it will work in the future.

In other words, the only way you can guarantee that the run loop won't return on your is have specifically attached "something" that prevents it's return.

Solutions:

1) Use DispatchMain()

Architecturally, the system actually has two mechanisms for "event dispatch"- the run loop based architecture and the Dispatch based system. Within that context, "DispatchMain" acts as the GCD equivalent of "NSApplicationMain". It's role is to park the main and process the main queue in case where the run loop can't/shouldn't be used.

I mention DispatchMain for completeness, but I would NOT recommend using it. The problem here is issue #2 above. DispatchMain works fine when the code involved relies on GCD, but it isn't (by design) a full replacement for the run loop. You could use it if you KNEW none of the code you were using used run loops but, by defintion, you don't know that because you're using our code. This is what can make it particularly painful- it's not that it won't work, it's that it WILL work fine... until you use the wrong API, at which point you end up creating exactly the kind of weird failure that started all of this. Note that these issues do NOT apply in reverse. In concrete terms, "DispatchQueue.main.async" works find with a run loop, but "performSelectorOnMainThread" does not work with DispatchMain.

2) Take Control of the Run Loop

The whole problem here occurs because the run loop doesn't have anything to wait on, so give it something to wait on. The simplest approach is to simple schedule a timer to "hold" the run loop. For development purposes, I generally start with something like this:

Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
    print("Still Running")
}

...so the repeating timer reassures me that the main run loop is still there waiting for work. Shipping code often ends up needing a timer for SOMETHING, but if you don't have any use then you can also set it to an arbitrarily large value so that it "never" fires. What matters here is that the timer exists, not that it actually fires.

A word on how you finish all your work. You're code tries to exit by calling:

CFRunLoopStop(currentRunLoop)

...but that actually has problems with issue #2 as well. Just like you can't guarantee our code is "holding" the run loop, you ALSO can't guarantee ISN'T holding the run loop. It's entirely possible to get all of this code working, only to find that now your code won't exit... because "someone else" is holding the run loop. In theory you could fix this by cleaning up everything "properly", but in practice that's tricky to manage, can break when something change, and makes everything slower.

My recommendation here is that you do whatever cleanup/saving/etc you code requires, then use Dispatch.after to directly call "exit()" after a brief delay (my default is ~0.1s). The delay gives our frameworks a short amount of time for allow any IPC message to clear (most of our frameworks rely IPC to supporting daemon) and also ensure that you're not calling "exit" in the middle of our own implementation. Note that while calling exit(0) like this sounds bad/dangerous, this is in fact exactly how AppKit actually quits, as well as many other parts of the system.

Closing Tidbits

A few extra points I want to highlight here:

-Quinn has an extended write up about run loops that's worth reviewing. It doesn't directly address what's happening here, but I'd recommend reviewing it.

-NSRunLoop and CFRunLoop are DIRECT equivalents and my recommendation would be to use NSRunLoop as your "base" API. It's documentation is a bit better and getCFRunLoop will get you to CFRunLoop if you actually need it for a specific API.

-As a broad warning, my (admittedly skewed) experience is that DispatchSemaphore is more often a sign of misunderstandings and problems than it is a useful API. If you understand how the system works it's unnecessary and if you don't understand how the system works... it doesn't work. It's not a tool I'd recommend reaching for.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thanks for your thorough answer. I've added the print("Exiting") line and is not showing up. This is exactly what I expected.

I think the program/debugger hangs on loadFromPreferences. When I pause program execution it shows a thread containing CFRunLoopRun.

Same goes for the semaphore example I've provided. The program does not return/completionhandle on loadFromPreferences. Pausing program execution shows semaphore_wait_trap.

What am I trying to achieve: Install/Enable a content filter from code. I would expect a popup showing up once I call the savePreferences function, but my code does not get to that line.

NOTE1: I've also tried wrapping the loadFromPreferences within a DispatchQueue.main.async without success. NOTE: I've also tried the async variant of loadFromPreferences without success.

NEFilterManager completion handler not called from Command Line Tool
 
 
Q