| /* |
| * Copyright (C) 2014-2019 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "ProcessAssertion.h" |
| |
| #if PLATFORM(IOS_FAMILY) |
| |
| #import "Logging.h" |
| #import "RunningBoardServicesSPI.h" |
| #import "WebProcessPool.h" |
| #import <UIKit/UIApplication.h> |
| #import <wtf/HashMap.h> |
| #import <wtf/RunLoop.h> |
| #import <wtf/Vector.h> |
| #import <wtf/WeakHashSet.h> |
| #import <wtf/WeakObjCPtr.h> |
| #import <wtf/WorkQueue.h> |
| |
| using WebKit::ProcessAndUIAssertion; |
| |
| static WorkQueue& assertionsWorkQueue() |
| { |
| static NeverDestroyed<Ref<WorkQueue>> workQueue(WorkQueue::create("ProcessAssertion Queue")); |
| return workQueue.get(); |
| } |
| |
| // This gives some time to our child processes to process the ProcessWillSuspendImminently IPC but makes sure we release |
| // the background task before the UIKit timeout (We get killed if we do not release the background task within 5 seconds |
| // on the expiration handler getting called). |
| static const Seconds releaseBackgroundTaskAfterExpirationDelay { 2_s }; |
| |
| static bool processHasActiveRunTimeLimitation() |
| { |
| return [RBSProcessHandle currentProcess].activeLimitations.runTime != RBSProcessTimeLimitationNone; |
| } |
| |
| @interface WKProcessAssertionBackgroundTaskManager |
| : NSObject <RBSAssertionObserving> |
| |
| + (WKProcessAssertionBackgroundTaskManager *)shared; |
| |
| - (void)addAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion; |
| - (void)removeAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion; |
| |
| @end |
| |
| @implementation WKProcessAssertionBackgroundTaskManager |
| { |
| RetainPtr<RBSAssertion> _backgroundTask; |
| std::atomic<bool> _backgroundTaskWasInvalidated; |
| WeakHashSet<ProcessAndUIAssertion> _assertionsNeedingBackgroundTask; |
| dispatch_block_t _pendingTaskReleaseTask; |
| } |
| |
| + (WKProcessAssertionBackgroundTaskManager *)shared |
| { |
| static WKProcessAssertionBackgroundTaskManager *shared = [WKProcessAssertionBackgroundTaskManager new]; |
| return shared; |
| } |
| |
| - (instancetype)init |
| { |
| self = [super init]; |
| if (!self) |
| return nil; |
| |
| // FIXME: Stop relying on UIApplication notifications as this does not work as expected for daemons or ViewServices. |
| // We should likely use ProcessTaskStateObserver to monitor suspension state. |
| [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:[UIApplication sharedApplication] queue:nil usingBlock:^(NSNotification *) { |
| [self _cancelPendingReleaseTask]; |
| [self _updateBackgroundTask]; |
| }]; |
| |
| [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:[UIApplication sharedApplication] queue:nil usingBlock:^(NSNotification *) { |
| if (![self _hasBackgroundTask]) |
| WebKit::WebProcessPool::notifyProcessPoolsApplicationIsAboutToSuspend(); |
| }]; |
| |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| [self _releaseBackgroundTask]; |
| [super dealloc]; |
| } |
| |
| - (void)addAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion |
| { |
| _assertionsNeedingBackgroundTask.add(assertion); |
| [self _updateBackgroundTask]; |
| } |
| |
| - (void)removeAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion |
| { |
| _assertionsNeedingBackgroundTask.remove(assertion); |
| [self _updateBackgroundTask]; |
| } |
| |
| - (void)_notifyAssertionsOfImminentSuspension |
| { |
| ASSERT(RunLoop::isMain()); |
| |
| Vector<WeakPtr<ProcessAndUIAssertion>> assertionsNeedingBackgroundTask; |
| for (auto& assertion : _assertionsNeedingBackgroundTask) |
| assertionsNeedingBackgroundTask.append(assertion); |
| |
| // Note that we don't expect clients to register new assertions when getting notified that the UI assertion will expire imminently. |
| // If clients were to do so, then those new assertions would not get notified of the imminent suspension. |
| for (auto assertion : assertionsNeedingBackgroundTask) { |
| if (assertion) |
| assertion->uiAssertionWillExpireImminently(); |
| } |
| } |
| |
| |
| - (void)_scheduleReleaseTask |
| { |
| ASSERT(!_pendingTaskReleaseTask); |
| if (_pendingTaskReleaseTask) |
| return; |
| |
| RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: _scheduleReleaseTask because the expiration handler has been called", self); |
| WorkQueue::main().dispatchAfter(releaseBackgroundTaskAfterExpirationDelay, [self, retainedSelf = retainPtr(self)] { |
| _pendingTaskReleaseTask = nil; |
| [self _releaseBackgroundTask]; |
| }); |
| } |
| |
| - (void)_cancelPendingReleaseTask |
| { |
| if (!_pendingTaskReleaseTask) |
| return; |
| |
| RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: _cancelPendingReleaseTask because the application is foreground again", self); |
| dispatch_block_cancel(_pendingTaskReleaseTask); |
| _pendingTaskReleaseTask = nil; |
| } |
| |
| - (BOOL)_hasBackgroundTask |
| { |
| return !!_backgroundTask; |
| } |
| |
| - (void)_updateBackgroundTask |
| { |
| if (!_assertionsNeedingBackgroundTask.computesEmpty() && (![self _hasBackgroundTask] || _backgroundTaskWasInvalidated)) { |
| if (processHasActiveRunTimeLimitation()) { |
| RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because RunningBoard has already started the expiration timer", self); |
| return; |
| } |
| RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: beginBackgroundTaskWithName", self); |
| RBSTarget *target = [RBSTarget currentProcess]; |
| RBSDomainAttribute *domainAttribute = [RBSDomainAttribute attributeWithDomain:@"com.apple.common" name:@"FinishTaskInterruptable"]; |
| _backgroundTask = adoptNS([[RBSAssertion alloc] initWithExplanation:@"WebKit UIProcess background task" target:target attributes:@[domainAttribute]]); |
| [_backgroundTask addObserver:self]; |
| |
| _backgroundTaskWasInvalidated = false; |
| [_backgroundTask acquireWithInvalidationHandler:nil]; |
| RELEASE_LOG(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: Took a FinishTaskInterruptable assertion for own process"); |
| } else if (_assertionsNeedingBackgroundTask.computesEmpty()) { |
| // Release the background task asynchronously because releasing the background task may destroy the ProcessThrottler and we don't |
| // want it to get destroyed while in the middle of updating its assertion. |
| RunLoop::main().dispatch([self, strongSelf = retainPtr(self)] { |
| if (_assertionsNeedingBackgroundTask.computesEmpty()) |
| [self _releaseBackgroundTask]; |
| }); |
| } |
| } |
| |
| - (void)assertionWillInvalidate:(RBSAssertion *)assertion |
| { |
| ASSERT(assertion == _backgroundTask.get()); |
| [self _handleBackgroundTaskExpiration]; |
| } |
| |
| - (void)assertion:(RBSAssertion *)assertion didInvalidateWithError:(NSError *)error |
| { |
| ASSERT(assertion == _backgroundTask.get()); |
| RELEASE_LOG_ERROR(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: FinishTaskInterruptable assertion was invalidated, error: %{public}@", error); |
| _backgroundTaskWasInvalidated = true; |
| } |
| |
| - (void)_handleBackgroundTaskExpiration |
| { |
| auto remainingTime = [RBSProcessHandle currentProcess].activeLimitations.runTime; |
| RELEASE_LOG(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: Background task expired while holding WebKit ProcessAssertion (isMainThread=%d, remainingTime=%g).", RunLoop::isMain(), remainingTime); |
| |
| callOnMainRunLoopAndWait([self] { |
| [self _handleBackgroundTaskExpirationOnMainThread]; |
| }); |
| } |
| |
| - (void)_handleBackgroundTaskExpirationOnMainThread |
| { |
| ASSERT(RunLoop::isMain()); |
| |
| auto remainingTime = [RBSProcessHandle currentProcess].activeLimitations.runTime; |
| RELEASE_LOG(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: _handleBackgroundTaskExpirationOnMainThread (remainingTime=%g).", remainingTime); |
| |
| // If there is no time limitation, then it means that the process is now allowed to run again and the expiration notification |
| // is outdated (e.g. we did not have time to process the expiration notification before suspending and thus only process it |
| // upon resuming, or the user reactivated the app shortly after expiration). |
| if (remainingTime == RBSProcessTimeLimitationNone) { |
| [self _releaseBackgroundTask]; |
| RunLoop::main().dispatch([self, strongSelf = retainPtr(self)] { |
| [self _updateBackgroundTask]; |
| }); |
| return; |
| } |
| |
| [self _notifyAssertionsOfImminentSuspension]; |
| [self _scheduleReleaseTask]; |
| } |
| |
| - (void)_releaseBackgroundTask |
| { |
| if (![self _hasBackgroundTask]) |
| return; |
| |
| RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: endBackgroundTask", self); |
| if (processHasActiveRunTimeLimitation()) |
| WebKit::WebProcessPool::notifyProcessPoolsApplicationIsAboutToSuspend(); |
| |
| [_backgroundTask removeObserver:self]; |
| [_backgroundTask invalidate]; |
| _backgroundTask = nullptr; |
| } |
| |
| @end |
| |
| typedef void(^RBSAssertionInvalidationCallbackType)(); |
| |
| @interface WKRBSAssertionDelegate : NSObject<RBSAssertionObserving> |
| @property (copy) RBSAssertionInvalidationCallbackType prepareForInvalidationCallback; |
| @property (copy) RBSAssertionInvalidationCallbackType invalidationCallback; |
| @end |
| |
| @implementation WKRBSAssertionDelegate |
| - (void)dealloc |
| { |
| [_prepareForInvalidationCallback release]; |
| [_invalidationCallback release]; |
| [super dealloc]; |
| } |
| |
| - (void)assertionWillInvalidate:(RBSAssertion *)assertion |
| { |
| RELEASE_LOG(ProcessSuspension, "%p - WKRBSAssertionDelegate: assertionWillInvalidate", self); |
| |
| RunLoop::main().dispatch([weakSelf = WeakObjCPtr<WKRBSAssertionDelegate>(self)] { |
| auto strongSelf = weakSelf.get(); |
| if (strongSelf && strongSelf.get().prepareForInvalidationCallback) |
| strongSelf.get().prepareForInvalidationCallback(); |
| }); |
| } |
| |
| - (void)assertion:(RBSAssertion *)assertion didInvalidateWithError:(NSError *)error |
| { |
| RELEASE_LOG(ProcessSuspension, "%p - WKRBSAssertionDelegate: assertion was invalidated, error: %{public}@", error, self); |
| |
| RunLoop::main().dispatch([weakSelf = WeakObjCPtr<WKRBSAssertionDelegate>(self)] { |
| auto strongSelf = weakSelf.get(); |
| if (strongSelf && strongSelf.get().invalidationCallback) |
| strongSelf.get().invalidationCallback(); |
| }); |
| } |
| @end |
| |
| namespace WebKit { |
| |
| static NSString *runningBoardNameForAssertionType(ProcessAssertionType assertionType) |
| { |
| switch (assertionType) { |
| case ProcessAssertionType::Suspended: |
| return @"Suspended"; |
| case ProcessAssertionType::Background: |
| return @"Background"; |
| case ProcessAssertionType::UnboundedNetworking: |
| return @"UnboundedNetworking"; |
| case ProcessAssertionType::Foreground: |
| return @"Foreground"; |
| case ProcessAssertionType::MediaPlayback: |
| return @"MediaPlayback"; |
| case ProcessAssertionType::FinishTaskInterruptable: |
| return @"FinishTaskInterruptable"; |
| } |
| } |
| |
| static NSString *runningBoardDomainForAssertionType(ProcessAssertionType assertionType) |
| { |
| switch (assertionType) { |
| case ProcessAssertionType::Suspended: |
| case ProcessAssertionType::Background: |
| case ProcessAssertionType::UnboundedNetworking: |
| case ProcessAssertionType::Foreground: |
| case ProcessAssertionType::MediaPlayback: |
| return @"com.apple.webkit"; |
| case ProcessAssertionType::FinishTaskInterruptable: |
| return @"com.apple.common"; |
| } |
| } |
| |
| ProcessAssertion::ProcessAssertion(pid_t pid, const String& reason, ProcessAssertionType assertionType) |
| : m_assertionType(assertionType) |
| , m_pid(pid) |
| , m_reason(reason) |
| { |
| NSString *runningBoardAssertionName = runningBoardNameForAssertionType(assertionType); |
| ASSERT(runningBoardAssertionName); |
| if (pid <= 0) { |
| RELEASE_LOG_ERROR(ProcessSuspension, "%p - ProcessAssertion: Failed to acquire RBS %{public}@ assertion '%{public}s' for process because PID %d is invalid", this, runningBoardAssertionName, reason.utf8().data(), pid); |
| m_wasInvalidated = true; |
| return; |
| } |
| |
| RBSTarget *target = [RBSTarget targetWithPid:pid]; |
| RBSDomainAttribute *domainAttribute = [RBSDomainAttribute attributeWithDomain:runningBoardDomainForAssertionType(assertionType) name:runningBoardAssertionName]; |
| m_rbsAssertion = adoptNS([[RBSAssertion alloc] initWithExplanation:reason target:target attributes:@[domainAttribute]]); |
| |
| m_delegate = adoptNS([[WKRBSAssertionDelegate alloc] init]); |
| [m_rbsAssertion addObserver:m_delegate.get()]; |
| m_delegate.get().invalidationCallback = ^{ |
| RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion: RBS %{public}@ assertion for process with PID=%d was invalidated", this, runningBoardAssertionName, pid); |
| processAssertionWasInvalidated(); |
| }; |
| m_delegate.get().prepareForInvalidationCallback = ^{ |
| RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion() RBS %{public}@ assertion for process with PID=%d will be invalidated", this, runningBoardAssertionName, pid); |
| processAssertionWillBeInvalidated(); |
| }; |
| } |
| |
| void ProcessAssertion::acquireAsync(CompletionHandler<void()>&& completionHandler) |
| { |
| ASSERT(isMainRunLoop()); |
| assertionsWorkQueue().dispatch([protectedThis = Ref { *this }, completionHandler = WTFMove(completionHandler)]() mutable { |
| protectedThis->acquireSync(); |
| RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)]() mutable { |
| if (completionHandler) |
| completionHandler(); |
| }); |
| }); |
| } |
| |
| void ProcessAssertion::acquireSync() |
| { |
| NSError *acquisitionError = nil; |
| if (![m_rbsAssertion acquireWithError:&acquisitionError]) { |
| RELEASE_LOG_ERROR(ProcessSuspension, "%p - ProcessAssertion: Failed to acquire RBS assertion '%{public}s' for process with PID=%d, error: %{public}@", this, m_reason.utf8().data(), m_pid, acquisitionError); |
| RunLoop::main().dispatch([weakThis = WeakPtr { *this }] { |
| if (weakThis) |
| weakThis->processAssertionWasInvalidated(); |
| }); |
| } else |
| RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion: Successfully took RBS assertion '%{public}s' for process with PID=%d", this, m_reason.utf8().data(), m_pid); |
| } |
| |
| ProcessAssertion::~ProcessAssertion() |
| { |
| RELEASE_LOG(ProcessSuspension, "%p - ~ProcessAssertion: Releasing process assertion '%{public}s' for process with PID=%d", this, m_reason.utf8().data(), m_pid); |
| |
| if (m_rbsAssertion) { |
| m_delegate.get().invalidationCallback = nil; |
| m_delegate.get().prepareForInvalidationCallback = nil; |
| [m_rbsAssertion removeObserver:m_delegate.get()]; |
| m_delegate = nil; |
| [m_rbsAssertion invalidate]; |
| } |
| } |
| |
| void ProcessAssertion::processAssertionWillBeInvalidated() |
| { |
| ASSERT(RunLoop::isMain()); |
| RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion::processAssertionWillBeInvalidated() PID=%d", this, m_pid); |
| |
| if (m_prepareForInvalidationHandler) |
| m_prepareForInvalidationHandler(); |
| } |
| |
| void ProcessAssertion::processAssertionWasInvalidated() |
| { |
| ASSERT(RunLoop::isMain()); |
| RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion::processAssertionWasInvalidated() PID=%d", this, m_pid); |
| |
| m_wasInvalidated = true; |
| if (m_invalidationHandler) |
| m_invalidationHandler(); |
| } |
| |
| bool ProcessAssertion::isValid() const |
| { |
| return !m_wasInvalidated; |
| } |
| |
| void ProcessAndUIAssertion::updateRunInBackgroundCount() |
| { |
| bool shouldHoldBackgroundTask = isValid() && type() != ProcessAssertionType::Suspended; |
| if (m_isHoldingBackgroundTask == shouldHoldBackgroundTask) |
| return; |
| |
| if (shouldHoldBackgroundTask) |
| [[WKProcessAssertionBackgroundTaskManager shared] addAssertionNeedingBackgroundTask:*this]; |
| else |
| [[WKProcessAssertionBackgroundTaskManager shared] removeAssertionNeedingBackgroundTask:*this]; |
| |
| m_isHoldingBackgroundTask = shouldHoldBackgroundTask; |
| } |
| |
| ProcessAndUIAssertion::ProcessAndUIAssertion(pid_t pid, const String& reason, ProcessAssertionType assertionType) |
| : ProcessAssertion(pid, reason, assertionType) |
| { |
| updateRunInBackgroundCount(); |
| } |
| |
| ProcessAndUIAssertion::~ProcessAndUIAssertion() |
| { |
| if (m_isHoldingBackgroundTask) |
| [[WKProcessAssertionBackgroundTaskManager shared] removeAssertionNeedingBackgroundTask:*this]; |
| } |
| |
| void ProcessAndUIAssertion::uiAssertionWillExpireImminently() |
| { |
| if (m_uiAssertionExpirationHandler) |
| m_uiAssertionExpirationHandler(); |
| } |
| |
| void ProcessAndUIAssertion::processAssertionWasInvalidated() |
| { |
| ASSERT(RunLoop::isMain()); |
| |
| WeakPtr weakThis { *this }; |
| ProcessAssertion::processAssertionWasInvalidated(); |
| |
| // Calling ProcessAssertion::processAssertionWasInvalidated() may have destroyed |this|. |
| if (weakThis) |
| updateRunInBackgroundCount(); |
| } |
| |
| } // namespace WebKit |
| |
| #endif // PLATFORM(IOS_FAMILY) |