f5d2f6446d09f360c4128483ebbe4339e42e302e
[WebKit-https.git] / Source / WebKit / UIProcess / ios / ProcessAssertionIOS.mm
1 /*
2  * Copyright (C) 2014-2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "ProcessAssertion.h"
28
29 #if PLATFORM(IOS_FAMILY)
30
31 #import "AssertionServicesSPI.h"
32 #import "Logging.h"
33 #import <UIKit/UIApplication.h>
34 #import <wtf/HashMap.h>
35 #import <wtf/RunLoop.h>
36 #import <wtf/Vector.h>
37
38 using WebKit::ProcessAndUIAssertion;
39
40 // This gives some time to our child processes to process the ProcessWillSuspendImminently IPC but makes sure we release
41 // the background task before the UIKit timeout (We get killed if we do not release the background task within 5 seconds
42 // on the expiration handler getting called).
43 static const Seconds releaseBackgroundTaskAfterExpirationDelay { 2_s };
44 static const Seconds maximumBackgroundTaskDuration { 20_s };
45
46 @interface WKProcessAssertionBackgroundTaskManager : NSObject
47
48 + (WKProcessAssertionBackgroundTaskManager *)shared;
49
50 - (void)addAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion;
51 - (void)removeAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion;
52
53 @end
54
55 @implementation WKProcessAssertionBackgroundTaskManager
56 {
57     UIBackgroundTaskIdentifier _backgroundTask;
58     HashSet<ProcessAndUIAssertion*> _assertionsNeedingBackgroundTask;
59     BOOL _applicationIsBackgrounded;
60     dispatch_block_t _releaseTask;
61     dispatch_block_t _timeoutTask;
62 }
63
64 + (WKProcessAssertionBackgroundTaskManager *)shared
65 {
66     static WKProcessAssertionBackgroundTaskManager *shared = [WKProcessAssertionBackgroundTaskManager new];
67     return shared;
68 }
69
70 static bool isBackgroundState(BKSApplicationState state)
71 {
72     return state == BKSApplicationStateBackgroundRunning || state == BKSApplicationStateBackgroundTaskSuspended;
73 }
74
75 - (instancetype)init
76 {
77     self = [super init];
78     if (!self)
79         return nil;
80
81     _backgroundTask = UIBackgroundTaskInvalid;
82
83     {
84         auto applicationStateMonitor = adoptNS([[BKSApplicationStateMonitor alloc] init]);
85         _applicationIsBackgrounded = isBackgroundState([applicationStateMonitor mostElevatedApplicationStateForPID:getpid()]);
86         [applicationStateMonitor invalidate];
87     }
88
89     NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
90     UIApplication *application = [UIApplication sharedApplication];
91
92     [notificationCenter addObserverForName:UIApplicationWillEnterForegroundNotification object:application queue:nil usingBlock:^(NSNotification *) {
93         RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: Application will enter foreground", self);
94         _applicationIsBackgrounded = NO;
95         [self _cancelReleaseTask];
96         [self _cancelTimeoutTask];
97         [self _updateBackgroundTask];
98     }];
99
100     [notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification object:application queue:nil usingBlock:^(NSNotification *) {
101         RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: Application did enter background", self);
102         _applicationIsBackgrounded = YES;
103
104         // We do not want to keep the app awake for more than 20 seconds after it gets backgrounded, so start the timeout timer now.
105         if (_backgroundTask != UIBackgroundTaskInvalid)
106             [self _scheduleTimeoutTask];
107     }];
108
109     return self;
110 }
111
112 - (void)dealloc
113 {
114     [self _releaseBackgroundTask];
115     [super dealloc];
116 }
117
118 - (void)addAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion
119 {
120     _assertionsNeedingBackgroundTask.add(&assertion);
121     [self _updateBackgroundTask];
122 }
123
124 - (void)removeAssertionNeedingBackgroundTask:(ProcessAndUIAssertion&)assertion
125 {
126     _assertionsNeedingBackgroundTask.remove(&assertion);
127     [self _updateBackgroundTask];
128 }
129
130 - (void)_notifyAssertionsOfImminentSuspension
131 {
132     ASSERT(RunLoop::isMain());
133
134     for (auto* assertion : copyToVector(_assertionsNeedingBackgroundTask))
135         assertion->uiAssertionWillExpireImminently();
136 }
137
138 - (void)_scheduleTimeoutTask
139 {
140     ASSERT(_backgroundTask != UIBackgroundTaskInvalid);
141
142     if (_timeoutTask)
143         return;
144
145     RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _scheduleTimeoutTask", self);
146     _timeoutTask = dispatch_block_create((dispatch_block_flags_t)0, ^{
147         _timeoutTask = nil;
148         RELEASE_LOG_ERROR(ProcessSuspension, "Background task was running for too long so WebKit will end it shortly.");
149         [self _backgroundTaskExpired];
150     });
151
152     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, maximumBackgroundTaskDuration.value() * NSEC_PER_SEC), dispatch_get_main_queue(), _timeoutTask);
153 #if !__has_feature(objc_arc)
154     // dispatch_async() does a Block_copy() / Block_release() on behalf of the caller.
155     Block_release(_timeoutTask);
156 #endif
157 }
158
159 - (void)_cancelTimeoutTask
160 {
161     if (!_timeoutTask)
162         return;
163
164     RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _cancelTimeoutTask", self);
165     dispatch_block_cancel(_timeoutTask);
166     _timeoutTask = nil;
167 }
168
169 - (void)_scheduleReleaseTask
170 {
171     ASSERT(!_releaseTask);
172     if (_releaseTask)
173         return;
174
175     RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _scheduleReleaseTask because the expiration handler has been called", self);
176     _releaseTask = dispatch_block_create((dispatch_block_flags_t)0, ^{
177         _releaseTask = nil;
178         [self _releaseBackgroundTask];
179     });
180     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, releaseBackgroundTaskAfterExpirationDelay.value() * NSEC_PER_SEC), dispatch_get_main_queue(), _releaseTask);
181 #if !__has_feature(objc_arc)
182     // dispatch_async() does a Block_copy() / Block_release() on behalf of the caller.
183     Block_release(_releaseTask);
184 #endif
185 }
186
187 - (void)_cancelReleaseTask
188 {
189     if (!_releaseTask)
190         return;
191
192     RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _cancelReleaseTask because the application is foreground again", self);
193     dispatch_block_cancel(_releaseTask);
194     _releaseTask = nil;
195 }
196
197 - (void)_backgroundTaskExpired
198 {
199     RELEASE_LOG_ERROR(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _backgroundTaskExpired", self);
200     ASSERT(_applicationIsBackgrounded);
201     [self _cancelTimeoutTask];
202
203     // Tell our child processes they will suspend imminently.
204     if (RunLoop::isMain())
205         [self _notifyAssertionsOfImminentSuspension];
206     else {
207         // The expiration handler gets called on a non-main thread when the underlying assertion could not be taken (rdar://problem/27278419).
208         dispatch_sync(dispatch_get_main_queue(), ^{
209             [self _notifyAssertionsOfImminentSuspension];
210         });
211     }
212
213     [self _scheduleReleaseTask];
214 }
215
216 - (void)_updateBackgroundTask
217 {
218     if (!_assertionsNeedingBackgroundTask.isEmpty() && _backgroundTask == UIBackgroundTaskInvalid) {
219         if (_applicationIsBackgrounded) {
220             RELEASE_LOG_ERROR(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because the application is already in the background", self);
221             return;
222         }
223         RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - beginBackgroundTaskWithName", self);
224         _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"com.apple.WebKit.ProcessAssertion" expirationHandler:^{
225             RELEASE_LOG_ERROR(ProcessSuspension, "Background task expired while holding WebKit ProcessAssertion (isMainThread? %d, _applicationIsBackgrounded? %d).", RunLoop::isMain(), _applicationIsBackgrounded);
226             if (!_applicationIsBackgrounded) {
227                 // We've received the invalidation warning after the app has become foreground again. In this case, we should not
228                 // warn clients of imminent suspension. To be safe (avoid potential killing), we end the task right away and call
229                 // _updateBackgroundTask asynchronously to start a new task if necessary.
230                 [self _releaseBackgroundTask];
231                 dispatch_async(dispatch_get_main_queue(), ^{
232                     [self _updateBackgroundTask];
233                 });
234                 return;
235             }
236             
237             [self _backgroundTaskExpired];
238         }];
239     } else if (_assertionsNeedingBackgroundTask.isEmpty())
240         [self _releaseBackgroundTask];
241 }
242
243 - (void)_releaseBackgroundTask
244 {
245     if (_backgroundTask == UIBackgroundTaskInvalid)
246         return;
247
248     RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - endBackgroundTask", self);
249     [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask];
250     [self _cancelReleaseTask];
251     [self _cancelTimeoutTask];
252     _backgroundTask = UIBackgroundTaskInvalid;
253 }
254
255 @end
256
257 namespace WebKit {
258
259 const BKSProcessAssertionFlags suspendedTabFlags = (BKSProcessAssertionAllowIdleSleep);
260 const BKSProcessAssertionFlags backgroundTabFlags = (BKSProcessAssertionPreventTaskSuspend);
261 const BKSProcessAssertionFlags foregroundTabFlags = (BKSProcessAssertionPreventTaskSuspend | BKSProcessAssertionWantsForegroundResourcePriority | BKSProcessAssertionPreventTaskThrottleDown);
262
263 static BKSProcessAssertionFlags flagsForState(AssertionState assertionState)
264 {
265     switch (assertionState) {
266     case AssertionState::Suspended:
267         return suspendedTabFlags;
268     case AssertionState::Background:
269     case AssertionState::UnboundedNetworking:
270         return backgroundTabFlags;
271     case AssertionState::Foreground:
272         return foregroundTabFlags;
273     }
274 }
275
276 static AssertionReason reasonForState(AssertionState assertionState)
277 {
278     switch (assertionState) {
279     case AssertionState::UnboundedNetworking:
280         return AssertionReason::FinishTaskUnbounded;
281     case AssertionState::Suspended:
282     case AssertionState::Background:
283     case AssertionState::Foreground:
284         return AssertionReason::Extension;
285     }
286 }
287
288 static BKSProcessAssertionReason toBKSProcessAssertionReason(AssertionReason reason)
289 {
290     switch (reason) {
291     case AssertionReason::Extension:
292         return BKSProcessAssertionReasonExtension;
293     case AssertionReason::FinishTask:
294         return BKSProcessAssertionReasonFinishTask;
295     case AssertionReason::FinishTaskUnbounded:
296         return BKSProcessAssertionReasonFinishTaskUnbounded;
297     case AssertionReason::MediaPlayback:
298         return BKSProcessAssertionReasonMediaPlayback;
299     }
300 }
301
302 ProcessAssertion::ProcessAssertion(pid_t pid, const String& name, AssertionState assertionState)
303     : ProcessAssertion(pid, name, assertionState, reasonForState(assertionState))
304 {
305 }
306
307 ProcessAssertion::ProcessAssertion(pid_t pid, const String& name, AssertionState assertionState, AssertionReason assertionReason)
308     : m_assertionState(assertionState)
309 {
310     auto weakThis = makeWeakPtr(*this);
311     BKSProcessAssertionAcquisitionHandler handler = ^(BOOL acquired) {
312         if (!acquired) {
313             RELEASE_LOG_ERROR(ProcessSuspension, " %p - ProcessAssertion() PID %d Unable to acquire assertion for process with PID %d", this, getpid(), pid);
314             dispatch_async(dispatch_get_main_queue(), ^{
315                 if (weakThis)
316                     processAssertionWasInvalidated();
317             });
318         }
319     };
320     RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion() PID %d acquiring assertion for process with PID %d, name '%s'", this, getpid(), pid, name.utf8().data());
321     
322     m_assertion = adoptNS([[BKSProcessAssertion alloc] initWithPID:pid flags:flagsForState(assertionState) reason:toBKSProcessAssertionReason(assertionReason) name:(NSString *)name withHandler:handler]);
323     m_assertion.get().invalidationHandler = ^() {
324         dispatch_async(dispatch_get_main_queue(), ^{
325             RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion() Process assertion for process with PID %d was invalidated", this, pid);
326             if (weakThis)
327                 processAssertionWasInvalidated();
328         });
329     };
330 }
331
332 ProcessAssertion::~ProcessAssertion()
333 {
334     m_assertion.get().invalidationHandler = nil;
335
336     RELEASE_LOG(ProcessSuspension, "%p - ~ProcessAssertion() Releasing process assertion", this);
337     [m_assertion invalidate];
338 }
339
340 void ProcessAssertion::processAssertionWasInvalidated()
341 {
342     ASSERT(RunLoop::isMain());
343     RELEASE_LOG_ERROR(ProcessSuspension, "%p - ProcessAssertion::processAssertionWasInvalidated()", this);
344
345     m_validity = Validity::No;
346 }
347
348 void ProcessAssertion::setState(AssertionState assertionState)
349 {
350     if (m_assertionState == assertionState)
351         return;
352
353     RELEASE_LOG(ProcessSuspension, "%p - ProcessAssertion::setState(%u) previousState: %u", this, static_cast<unsigned>(assertionState), static_cast<unsigned>(m_assertionState));
354     m_assertionState = assertionState;
355     [m_assertion setFlags:flagsForState(assertionState)];
356 }
357
358 void ProcessAndUIAssertion::updateRunInBackgroundCount()
359 {
360     bool shouldHoldBackgroundTask = validity() != Validity::No && state() != AssertionState::Suspended;
361     if (m_isHoldingBackgroundTask == shouldHoldBackgroundTask)
362         return;
363
364     if (shouldHoldBackgroundTask)
365         [[WKProcessAssertionBackgroundTaskManager shared] addAssertionNeedingBackgroundTask:*this];
366     else
367         [[WKProcessAssertionBackgroundTaskManager shared] removeAssertionNeedingBackgroundTask:*this];
368
369     m_isHoldingBackgroundTask = shouldHoldBackgroundTask;
370 }
371
372 ProcessAndUIAssertion::ProcessAndUIAssertion(pid_t pid, const String& reason, AssertionState assertionState)
373     : ProcessAssertion(pid, reason, assertionState)
374 {
375     updateRunInBackgroundCount();
376 }
377
378 ProcessAndUIAssertion::~ProcessAndUIAssertion()
379 {
380     if (m_isHoldingBackgroundTask)
381         [[WKProcessAssertionBackgroundTaskManager shared] removeAssertionNeedingBackgroundTask:*this];
382 }
383
384 void ProcessAndUIAssertion::setState(AssertionState assertionState)
385 {
386     ProcessAssertion::setState(assertionState);
387     updateRunInBackgroundCount();
388 }
389
390 void ProcessAndUIAssertion::uiAssertionWillExpireImminently()
391 {
392     if (auto* client = this->client())
393         client->uiAssertionWillExpireImminently();
394 }
395
396 void ProcessAndUIAssertion::processAssertionWasInvalidated()
397 {
398     ProcessAssertion::processAssertionWasInvalidated();
399     updateRunInBackgroundCount();
400 }
401
402 } // namespace WebKit
403
404 #endif // PLATFORM(IOS_FAMILY)