watchOS background task crashes on setTaskCompleted

1k views Asked by At

In my watch extension delegate init function, I set up KVO observers on the WCSession:

if WCSession.isSupported() {
  let defaultSession = WCSession.default
  defaultSession.addObserver(self, forKeyPath: "activationState", 
                             options: [.old, .new], 
                             context: &ExtensionDelegate.wcSessionKVOcontext)
  defaultSession.addObserver(self, forKeyPath: "hasContentPending", 
                             options: [.old, .new], 
                             context: &ExtensionDelegate.wcSessionKVOcontext)
}

In order to complete all watch background tasks, this calls the function

  override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &ExtensionDelegate.wcSessionKVOcontext {
      printStatusChanges(keyPath: keyPath, change: change)
      DispatchQueue.main.async {
        self.completeAllTasksIfReady()
      }
    }
  }

with

  private func completeAllTasksIfReady() {
    let session = WCSession.default
    // the session's properties only have valid values if the session is activated, so check that first
    if session.activationState == .activated && !session.hasContentPending {
            if wcBackgroundTasks.isEmpty {
                print("No background tasks")
            } else {
                wcBackgroundTasks.forEach { $0.setTaskCompleted() }
                print("\(wcBackgroundTasks.count) connectivity background tasks completed")
            }
      wcBackgroundTasks.removeAll()
    }
  }

Normally, this works fine.
However I had a crash with the following log:

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x1db2eda8 __abort_with_payload + 24
1   libsystem_kernel.dylib          0x1db2ae60 abort_with_payload_wrapper_internal + 60
2   libsystem_kernel.dylib          0x1db2ae24 abort_with_payload_wrapper_internal + 0
3   WatchKit                        0x2ddfce86 -[WKRefreshBackgroundTask setTaskCompleted] + 414
4   Watch Extension                 0x00037258 closure #1 in ExtensionDelegate.completeAllTasksIfReady() + 209496 (ExtensionDelegate.swift:196)
5   Watch Extension                 0x000372b0 thunk for @callee_guaranteed (@owned WKRefreshBackgroundTask) -> (@error @owned Error) + 209584 (ExtensionDelegate.swift:0)
6   Watch Extension                 0x0003af08 partial apply for thunk for @callee_guaranteed (@owned WKRefreshBackgroundTask) -> (@error @owned Error) + 225032 (ExtensionDelegate.swift:0)
7   libswiftCore.dylib              0x00525350 0x2fc000 + 2265936
8   libswiftCore.dylib              0x00433b7c 0x2fc000 + 1276796
9   libswiftCore.dylib              0x00304060 0x2fc000 + 32864
10  Watch Extension                 0x00036eec ExtensionDelegate.completeAllTasksIfReady() + 208620 (ExtensionDelegate.swift:196)
11  Watch Extension                 0x000350d4 closure #1 in ExtensionDelegate.observeValue(forKeyPath:of:change:context:) + 200916 (ExtensionDelegate.swift:108)
12  Watch Extension                 0x0000993c _T0Ieg_IeyB_TR + 22844 (AlertManager.swift:0)
13  libdispatch.dylib               0x1d9d3456 _dispatch_call_block_and_release + 10
14  libdispatch.dylib               0x1d9d3432 _dispatch_client_callout + 6
15  libdispatch.dylib               0x1d9e3604 _dispatch_main_queue_callback_4CF$VARIANT$mp + 858
16  CoreFoundation                  0x1df7db1e __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 10
17  CoreFoundation                  0x1df7b73c __CFRunLoopRun + 932
18  CoreFoundation                  0x1dec7660 CFRunLoopRunSpecific + 534
19  GraphicsServices                0x1fb7ab3e GSEventRunModal + 94
20  UIKit                           0x24746604 UIApplicationMain + 156
21  libxpc.dylib                    0x1dcf7b14 _xpc_objc_main + 586
22  libxpc.dylib                    0x1dcf94a8 xpc_main + 154
23  Foundation                      0x1e9b2cf2 service_connection_handler + 0
24  PlugInKit                       0x2525c06e -[PKService run] + 676
25  WatchKit                        0x2de1b036 main + 162
26  libdyld.dylib                   0x1da2e782 start + 2  

Obviously, the crash happens when setTaskCompleted is called in one of the WKRefreshBackgroundTask tasks.

But what could be the reason, or how to debug this?

1

There are 1 answers

1
Reinhard Männer On BEST ANSWER

My fault. Apple replied to my bug report with:

This issue behaves as intended based on the following:
- this is an assert put in place to make sure that the same task is not completed more than once
- this is indicated by "called on WKWatchConnectivityRefreshBackgroundTask: 0x16e7b230 that's already been completed”
- this likely points to a bug (or bugs) in the project’s code and not with the SDK/API

I thus double checked if it is possible in my code that for a WKWatchConnectivityRefreshBackgroundTask function setTaskCompleted is called more than once. This is indeed the case:

When I started to develop my watch extension, I copied Apple demo code. It contains the following function:

func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
    for backgroundTask in backgroundTasks {
        if let wcBackgroundTask = backgroundTask as? WKWatchConnectivityRefreshBackgroundTask {
            // store a reference to the task objects as we might have to wait to complete them
            self.wcBackgroundTasks.append(wcBackgroundTask)
        } else {
            // immediately complete all other task types as we have not added support for them
            backgroundTask.setTaskCompleted()
        }
    }
    completeAllTasksIfReady()
}

Here, a WKWatchConnectivityRefreshBackgroundTask that should be handled is stored in an array wcBackgroundTasks. Later in completeAllTasksIfReady() all tasks in this array are completed by wcBackgroundTasks.forEach { $0.setTaskCompleted() }.

My fault was that I stored my WKWatchConnectivityRefreshBackgroundTask in this array (and called later setTaskCompleted for it) and handled it immediately and set setTaskCompleted. So this was called twice.
Thanks to the Apple engineer who found this.