| /// Prevents the process from suspending by holding a `ProcessInfo` expiry |
| /// activity assertion. |
| /// |
| /// The assertion is released if: |
| /// |
| /// * You explicitly release the assertion by calling ``release()``. |
| /// * There are no more strong references to the object and so it gets |
| /// deinitialised. |
| /// * The system ‘calls in’ the assertion, in which case it calls the |
| /// ``systemDidReleaseAssertion`` closure, if set. |
| /// |
| /// You should aim to explicitly release the assertion yourself, as soon as |
| /// you’ve completed the work that the assertion covers. |
| /// |
| /// This uses `performExpiringActivity(withReason:using:)`, which… well… how to |
| /// say this kindly… has some very odd design characteristics. The API kinda |
| /// makes sense if you’re doing CPU bound work but its design does not work well |
| /// if you’re doing something I/O bound, like networking (r. 109839489). And |
| /// that’s the primary use case for this class. The end result is that you have |
| /// to waste a thread that’s just sitting inside the expiry closure doing |
| /// nothing. Moreover, this is a Dispatch worker thread, so there’s a limit to |
| /// how many times you can do this. So, you have to be _really_ careful not to |
| /// allocate too many instances of this class. |
| /// |
| /// I could fix this by having all the instances share a single assertion but… |
| /// well… let’s just say this code is already complicated. |
| |
| final class QRunInBackgroundAssertionEx { |
| |
| /// The name used when creating the assertion. |
| |
| let name: String |
| |
| /// Called when the system releases the assertion itself. |
| /// |
| /// This is called on the main thread. |
| /// |
| /// To help avoid retain cycles, the object sets this to `nil` whenever the |
| /// assertion is released. |
| |
| var systemDidReleaseAssertion: (() -> Void)? { |
| willSet { dispatchPrecondition(condition: .onQueue(.main)) } |
| } |
| |
| // I would’ve liked to use `OSAllocatedUnfairLock` but it requires iOS 16. |
| // In its absence, be aware that `stateLock` protects… you guess it!… |
| // `state. |
| |
| private let stateLock: NSLock = NSLock() |
| private enum State { |
| case starting |
| case started(DispatchSemaphore) |
| case released |
| } |
| private var state: State |
| |
| /// Creates an assertion with the given name. |
| /// |
| /// The name isn’t used by the system but it does show up in various logs so |
| /// it’s important to choose one that’s meaningful to you. |
| /// |
| /// Must be called on the main thread. |
| |
| init(name: String) { |
| dispatchPrecondition(condition: .onQueue(.main)) |
| self.name = name |
| self.systemDidReleaseAssertion = nil |
| self.state = .starting |
| |
| // See “Concurrency Notes” below. |
| |
| ProcessInfo.processInfo.performExpiringActivity(withReason: name) { didExpire in |
| let semaphore = self.stateLock.withLock { () -> DispatchSemaphore? in |
| switch (self.state, didExpire) { |
| case (.starting, true): |
| // Failed to start; we can’t represent this in our API so we |
| // just flipped to the `.released` state and we’re done. |
| self.state = .released |
| return nil |
| case (.starting, false): |
| // Started successfully. Let’s block (outside the lock, of |
| // course) waiting on the semaphore. |
| let semaphore = DispatchSemaphore(value: 0) |
| self.state = .started(semaphore) |
| return semaphore |
| case (.started(let semaphore), true): |
| // We have started and now we’re expiring. Signal our |
| // semaphore to unblock the thread that’s waiting on it. |
| semaphore.signal() |
| self.state = .released |
| // Run the ‘did release’ callback. This is async, so we can |
| // kick it off with the lock held. |
| DispatchQueue.main.async { self.runSystemDidReleaseAssertion() } |
| return nil |
| case (.started(_), false): |
| // This shouldn’t be possible. |
| fatalError() |
| case (.released, _): |
| // Our client called `release()` before we managed to start. |
| // That’s weird, but easy to handle. |
| return nil |
| } |
| } |
| if let semaphore { |
| semaphore.wait() |
| } |
| } |
| } |
| |
| /// Release the assertion. |
| /// |
| /// It’s safe to call this redundantly, that is, call it twice in a row or |
| /// call it on an assertion that’s expired. |
| /// |
| /// Must be called on the main thread. |
| |
| func release() { |
| dispatchPrecondition(condition: .onQueue(.main)) |
| self.releaseOnAnyThread() |
| // Set to `nil` to reduce the chances of a retain loop. |
| self.systemDidReleaseAssertion = nil |
| } |
| |
| private func releaseOnAnyThread() { |
| self.stateLock.withLock { |
| switch self.state { |
| case .starting: |
| // The transition from `.starting` to `.started` happens |
| // asynchonously, so it’s possible that you could release the |
| // assertion before that’s completed. This sets the state to |
| // `.released` so that the concurrent code doing the transition |
| // just gives up. |
| self.state = .released |
| case .started(let semaphore): |
| // Unblock the thread waiting in our closure. |
| semaphore.signal() |
| self.state = .released |
| case .released: |
| // Releasing redundantly is a no-op. |
| break |
| } |
| } |
| } |
| |
| private func runSystemDidReleaseAssertion() { |
| dispatchPrecondition(condition: .onQueue(.main)) |
| self.systemDidReleaseAssertion?() |
| // Set to `nil` to reduce the chances of a retain loop. |
| self.systemDidReleaseAssertion = nil |
| } |
| |
| deinit { |
| // We don’t apply this assert because it’s hard to force the last object |
| // reference to be released on the main thread. Fortunately, |
| // `releaseOnAnyThread()` is thread safe. |
| // |
| // dispatchPrecondition(condition: .onQueue(.main)) |
| |
| self.releaseOnAnyThread() |
| |
| // We don’t nil out `systemDidReleaseAssertion` here because that |
| // property is confined to the main thread and we can’t be sure we’re |
| // running on the main thread. However, that’s not a problem because |
| // the rationale for nil’ing this out is to minimise retain loop and, if |
| // we got to this deinitialiser, that’s not a problem. |
| |
| self.systemDidReleaseAssertion = nil |
| } |
| } |