mattermost-mobile-upstream/libraries/@mattermost/rnutils/ios/Managers/DatabaseActivityAssertion.swift
Elias Nahum bc84b6ee3a
Guard long-running DB chains against RUNNINGBOARD 0xdead10cc on iOS (#10110)
* Guard long-running DB chains against RUNNINGBOARD 0xdead10cc on iOS

iOS can terminate the app if it's suspended while a SQLite statement
still holds the shared App Group database lock. AppDelegate already
held a single background-task assertion across the background
transition, but it was a blind, fixed-duration timer: it never
tracked whether a DB operation was actually still running, and a
chain that started after the timer had already begun (or ran long
enough to outlast it) got no protection.

Add DatabaseActivityAssertion (a Swift port of Quinn "The Eskimo!"'s
QRunInBackgroundAssertion) and DatabaseLockProtectionManager, which
lets any caller - the app lifecycle transition, or a specific
long-running chain - acquire its own independent background-task
job and release it exactly when its own work finishes. Wire this
into the websocket reconnect sync (doReconnect) and the deferred
per-team entry sync (restDeferredAppEntryActions), the two longest
sequential local-DB chains in the app, via a new pair of RNUtils
methods (iOS only; Android is a no-op, since 0xdead10cc has no
Android equivalent).

* fix tests
2026-09-02 15:07:34 +08:00

61 lines
1.9 KiB
Swift

import Foundation
import UIKit
import Gekidou
/// Prevents suspension via a `UIApplication` background task assertion. Ported from
/// Quinn "The Eskimo!"'s `QRunInBackgroundAssertion` (https://developer.apple.com/forums/thread/729335).
class DatabaseActivityAssertion {
let name: String
/// Called on the main thread if the system releases the assertion itself.
var systemDidReleaseAssertion: (() -> Void)? {
willSet { dispatchPrecondition(condition: .onQueue(.main)) }
}
private(set) var isGranted: Bool
private var taskID: UIBackgroundTaskIdentifier
init(name: String) {
dispatchPrecondition(condition: .onQueue(.main))
self.name = name
self.systemDidReleaseAssertion = nil
self.taskID = .invalid
self.isGranted = false
let t = UIApplication.shared.beginBackgroundTask(withName: name) {
self.taskDidExpire()
}
self.taskID = t
self.isGranted = t != .invalid
if !isGranted {
GekidouLogger.shared.log(.error, "DatabaseActivityAssertion: OS declined background task %{public}@", name)
}
}
/// Safe to call redundantly. Must be called on the main thread.
func release() {
dispatchPrecondition(condition: .onQueue(.main))
consumeValidTaskID {}
}
deinit {
consumeValidTaskID {}
}
private func consumeValidTaskID(_ body: () -> Void) {
guard taskID != .invalid else { return }
UIApplication.shared.endBackgroundTask(taskID)
taskID = .invalid
body()
systemDidReleaseAssertion = nil
}
private func taskDidExpire() {
dispatchPrecondition(condition: .onQueue(.main))
GekidouLogger.shared.log(.error, "DatabaseActivityAssertion: OS expired background task %{public}@", name)
consumeValidTaskID {
self.systemDidReleaseAssertion?()
}
}
}