mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 10:28:54 +00:00
* 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
61 lines
1.9 KiB
Swift
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?()
|
|
}
|
|
}
|
|
}
|