Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

BGAppRefreshTask Canceled Immediately by dasd in Network Extension

Dear Apple Support Team,

My app, io.cylonix.sase, has a BGAppRefreshTask (io.cylonix.sase.ios.refresh) that is canceled by dasd ~9ms after submission from a Network Extension. Please help identify the cause and suggest a solution.

App Details:

App ID: io.cylonix.sase iOS Version: 17.1.1 (iPhone Xs Max) Network Extension: saseWgNetworkExtension with packet-tunnel-provider entitlement Use Case: VPN app; Network Extension records file receipts in shared group UserDefaults and schedules BGAppRefreshTask to wake the main app. App Usage: High (frequently used) System State: Sufficient resources (not low on battery or memory) Issue: The task is submitted but canceled immediately with priority 10. It has never run, so rate-limiting is not an issue. `

debug	22:09:37.952749-0700	dasd	Best binding found for evaluator 0x16d541720: <private>
debug	22:09:37.954483-0700	dasd	Invoking selector backgroundTaskSchedulerPermittedIdentifiersWithContext:tableID:unitID:unitBytes: on <LSApplicationRecord 0x724844650>
default	22:09:37.955563-0700	dasd	CANCELED: bgRefresh-io.cylonix.sase.ios.refresh:ABDAFA at priority 10 <private>!
Answered by DTS Engineer in 838299022

Thank you for the reply and the link for detailed background task information.

Here is what I am trying to do:

So, focusing on the background approach you've been trying:

  1. The system was really only designed to support task being submitted by the component that will run them, which can only be your main app.

  2. To the extent that it does "work", that support is basically accidental and could break at any time.

  3. Even if it did work PERFECTLY, I don't think BGAppRefreshTask would actually be a good fit for what you're trying to do.

Focussing in #3, the problem is the possible time gap between these two steps:

  1. The network extension then needs to notify the suspended main APP about receiving the file by submitting a background task request for an ID to get the main APP to run a background task.

  2. The main APP registers to be waken up by the ID and moves the file from the shared container to its own document area so that the Files app can view it.

Basically, the EXPECTED time gap between the submission of the task and the app being woken runs from:

  • Best case: "A several times an hour" or a delay of say 5-20 min., for an app the user uses heavily during the time window the task want to run.

  • Worse case: "Never", for an app the user rarely/never interacts with during the task time window.

Also, as I described here, the way that development device usage patterns interact with the scheduling engine mean that it's very easy to end up with a test device that makes this system look FAR more reliable/consistent that it will be in real world use. The bottom line here is that this simply is not an issue BGAppRefreshTask was ever designed to solve.

SO, in terms of what will work, I don't think there is anyway to share you group directory into Files.app (which is what you really want), but I'm double checking on that.

It's more work but the most "complete" solution would be to use a FileProvider extension, probably the replicated variant. Note that if you want to support bidirectional transfers (moving data "back" to the remote device), then EXACTLY the approach you should use.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

My app, io.cylonix.sase, has a BGAppRefreshTask (io.cylonix.sase.ios.refresh) that is canceled by dasd ~9ms after submission from a Network Extension. Please help identify the cause and suggest a solution.

What are you actually trying to do here? You said you're trying to do this:

Network Extension records file receipts in shared group UserDefaults and schedules BGAppRefreshTask to wake the main app.

However, that's not something I'd expect to actually work. Your network extension cannot run background tasks (because it isn't an app) and submitting tasks from secondary components isn't something we've ever supported. Last but not least, BGAppRefreshTask isn't a reliable tool for waking apps into the background, as it's refresh frequency is heavily defined by app usage patterns. Note that, as described here, this can also mean that testing on development devices can be EXTREMELY misleading compared to it's real world behavior.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin

Thank you for the reply and the link for detailed background task information.

Here is what I am trying to do:

  1. The network extension receives a file from peers via the established packet tunnel similar to airdrop.

  2. The network extension then needs to notify the suspended main APP about receiving the file by submitting a background task request for an ID to get the main APP to run a background task.

  3. The main APP registers to be waken up by the ID and moves the file from the shared container to its own document area so that the Files app can view it.

Here is the code snippet that does that.

` import NetworkExtension import BackgroundTasks import os

// In NEPacketTunnelProvider class PacketTunnelProvider: NEPacketTunnelProvider { private static let log = OSLog(subsystem: "io.cylonix.sase", category: "NetworkExtension")

func handleFileReceipt(fileName: String) {
    let defaults = UserDefaults(suiteName: "group.io.cylonix.sase")
    defaults?.set(true, forKey: "FileReceived")
    defaults?.set(fileName, forKey: "FileName")
    defaults?.synchronize()
    os_log(.info, log: Self.log, "File receipt recorded: %{public}@ at %{public}@", fileName, Date().description)
    
    scheduleRefreshTask()
}

private func scheduleRefreshTask() {
    let request = BGAppRefreshTaskRequest(identifier: "io.cylonix.sase.ios.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 1)
    os_log(.info, log: Self.log, "Submitting refresh task")
    do {
        try BGTaskScheduler.shared.submit(request)
        os_log(.info, log: Self.log, "Refresh task submitted")
    } catch {
        os_log(.error, log: Self.log, "Refresh task submission failed: %{public}@", error.localizedDescription)
    }
}

}

// In BackgroundTaskManager class BackgroundTaskManager { static let shared = BackgroundTaskManager() private static let log = OSLog(subsystem: "io.cylonix.sase", category: "MainApp")

func registerBackgroundTasks() {
    BGTaskScheduler.shared.register(forTaskWithIdentifier: "io.cylonix.sase.ios.refresh", using: .main) { task in
        self.handleRefreshTask(task as! BGAppRefreshTask)
    }
}

func handleRefreshTask(_ task: BGAppRefreshTask) {
    os_log(.info, log: Self.log, "Handling refresh task")
    task.expirationHandler = {
        os_log(.error, log: Self.log, "Refresh task expired")
        task.setTaskCompleted(success: false)
    }
    checkMissedFiles()
    task.setTaskCompleted(success: true)
}

func checkMissedFiles() {
    let defaults = UserDefaults(suiteName: "group.io.cylonix.sase")
    if defaults?.bool(forKey: "FileReceived") == true, let fileName = defaults?.string(forKey: "FileName") {
        os_log(.info, log: Self.log, "Found missed file: %{public}@", fileName)
        defaults?.set(false, forKey: "FileReceived")
        defaults?.set(nil, forKey: "FileName")
        defaults?.synchronize()
    }
}

}`

When I am running the test, I didn't run it through Xcode in debug mode as in that case the main app always stays active. I did it through the release mode either from the test flight distribution or via "flutter run --release" that's in release mode too. I also tried background processing task too and the behavior is the same.

Thanks, Randy

Accepted Answer

Thank you for the reply and the link for detailed background task information.

Here is what I am trying to do:

So, focusing on the background approach you've been trying:

  1. The system was really only designed to support task being submitted by the component that will run them, which can only be your main app.

  2. To the extent that it does "work", that support is basically accidental and could break at any time.

  3. Even if it did work PERFECTLY, I don't think BGAppRefreshTask would actually be a good fit for what you're trying to do.

Focussing in #3, the problem is the possible time gap between these two steps:

  1. The network extension then needs to notify the suspended main APP about receiving the file by submitting a background task request for an ID to get the main APP to run a background task.

  2. The main APP registers to be waken up by the ID and moves the file from the shared container to its own document area so that the Files app can view it.

Basically, the EXPECTED time gap between the submission of the task and the app being woken runs from:

  • Best case: "A several times an hour" or a delay of say 5-20 min., for an app the user uses heavily during the time window the task want to run.

  • Worse case: "Never", for an app the user rarely/never interacts with during the task time window.

Also, as I described here, the way that development device usage patterns interact with the scheduling engine mean that it's very easy to end up with a test device that makes this system look FAR more reliable/consistent that it will be in real world use. The bottom line here is that this simply is not an issue BGAppRefreshTask was ever designed to solve.

SO, in terms of what will work, I don't think there is anyway to share you group directory into Files.app (which is what you really want), but I'm double checking on that.

It's more work but the most "complete" solution would be to use a FileProvider extension, probably the replicated variant. Note that if you want to support bidirectional transfers (moving data "back" to the remote device), then EXACTLY the approach you should use.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin

Really appreciate the great response. I was indeed misled by the fact the background processing or background refresh task request submission succeeded without error by the network extension. Thank you for clarifying that the request is designed to be only submitted by the app itself and not any of its extensions.

The key issue I am trying to solve is the notification about a new file being dropped to the device, just like airdrop notifications. I would probably opt for a push notification like an email server will do instead. The FileProvider extension is something I will also check out if it helps on notifications of a remote change that can be emulated with a local folder.

The syncing part is less an issue as it is more about transfers instead of syncing. Files App can view these files since my app adopts the share locally solution as you pointed out for the FieProvider extension with the right UIFileSharingEnabled setting.

Really appreciate the help Randy

The key issue I am trying to solve is the notification about a new file being dropped to the device, just like airdrop notifications.

Have you tried just having your network extension post the notification directly? I don't know what network extension you're using, but I know that allowed from a local push connectivity extension (and possibly others). Note that you set up your notification so that the users actions run your app in the background, which would then allow you to shift the file into "Documents".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Great suggestions. This works! I have tested it and it worked as you described. I was not opting for this route all along because the idea of network extension shall not have UI access.

Really appreciate it. Randy

BGAppRefreshTask Canceled Immediately by dasd in Network Extension
 
 
Q