Running Timer inside NetworkExtension

Currently I'm using this code for running scheduled tasks inside NetworkExtension:

let timer = Timer(fireAt: nextRunTime, interval: 0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: false)
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)

It runs more or less ok, until it doesn't. If I'm connected to console app on mac and to debugger on xcode it seems to be running well. When I disconnect everything looks like after some time this stops working.

Am I doing something wrong? Is there any better way to run code inside NetworkExtension at predefined time in the future?

Am I doing something wrong? Is there any better way to run code inside NetworkExtension at predefined time in the future?

NetworkExtension providers do not have guaranteed access to a main run loop (RunLoop.main) and if you needed guaranteed access to a run loop you would need to handle this on your own thread which is a pain. Instead you should think about using a queue-based API if available. I also wanted to ask what you are trying to do with this timer here?

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

I have to call API from time to time.

What exactly do you mean by "Queue-base API"? DispatchQueue.main.asyncAfter()?

I generally recommend against using anonymous timers because you have no way to cancel them. This includes things like:

  • -[NSObject performSelector:withObject:afterDelay:]

  • DispatchQueue.asyncAfter(…)

It’s much better to use timers that return a ‘handle’ to the timer. In the Dispatch world this means DispatchSourceTimer, created using DispatchSource.makeTimerSource(…).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Then when I'm back on wifi, it resumes.

That sort of behaviour is typically associated with your process being suspended.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

So I did some more testing. Used two types of timers:

Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(doSomeStuff), userInfo: nil, repeats: true)

and

queue = DispatchQueue(label: "Timer", qos: .background)
timerSource = DispatchSource.makeTimerSource(queue: queue)
timerSource.schedule(deadline: .now() + .seconds(Int(nextRunTime.timeIntervalSinceNow)), repeating: 2 * 60, leeway: .seconds(10)) 
timerSource.setEventHandler { [weak self] in
    self?.doSomeOtherStuff()
}
timerSource.resume()

This is all happening inside NetworkExtension with VPN connected at the time of testing, so the process runs all the time, except when it doesn't :)

Following are timestamps from syslog. I selected two of them (first one and the last) that are from the first Timer. Others are just logs from VPN library. As you can see in between there were some things going on, NE was woken up (unfreezed? don't know how to call that state) several times and traffic was flowing, extension was running then got to void again, then running, etc. Still the timer was not called.

The second timer was set to be fired each two minutes, so it was called even less frequently than the first one.

All in all it looks like timer does not respect the wall time and just uses some internal counter that ticks according to its runloop. And if between two runs OS decides to freeze the process, this time just disappears from the counter.

Questions:

  1. Is there any timer or a mechanism like that that can call my code every now and then and in case the process was frozen in between would count time according to real time?
  2. Where can I find any documentation how this freezing/unfreezing works on iOS/ipadOS?
  3. Is there a way to detect that NE was frozen for some time and just got some time to run again?

There are are a variety of ways that timers can miss deadlines, but the most likely ones are:

  • Your process got suspended

  • The entire device went to sleep

The first mechanism is most relevant to apps. If a user moves the app to the background, the system will typically suspends it shortly thereafter. In contrast, an NE provider isn’t suspended as long as it’s active [1].

However, the second option is always a possibility. The NEProvider subclass has -sleepWithCompletionHandler: and -wake methods that you can override to learn about this.

Both NSTimer (Timer in Swift) and Dispatch timer sources rely on Mach absolute time. This stops counting when the CPU stops, that is, when the device sleeps. If you run a timer across a sleep, you’ll see some oddities on the other side. My general advice is that you remove your timers when sleep is a possibility and reschedule them when that’s no longer the case.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] A tunnel provider is consider active if the tunnel is up.

@jaroslavqwerty Were you able to find a solution for this problem? My understanding is that Timer & DispatchSource, Both of these approaches were not working for you. I am also having the same problem, Please advise if you found a solution for this problem

@eskimo @meaton

@jaroslavqwerty Were you able to find a solution for this problem? My understanding is that Timer & DispatchSource, Both of these approaches were not working for you. I am also having the same problem, Please advise if you found a solution for this problem

@eskimo @meaton

Running Timer inside NetworkExtension
 
 
Q