playwright/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m
Dmitry Gozman ded2bc2396
browser(webkit): postpone creation of the first page (#4769)
When we create the first page in the default context in headless mode on mac,
it gets NSWindow that is "not visible". Although we call [window setIsVisible:YES],
later on window.isVisible still returns NO.

We create our offscreen "headless" NSWindow directly from applicationDidFinishLaunching:.
Experiments show that delaying this by 100ms makes everything work. As a symptom,
we get applicationDidUnhide: notification that does not happen when we create the window
immediately.

Perhaps, we create the window too early, and there is some essential initialization
that happens after applicationDidFinishLaunching:. However, if we call
[NSApp activateIgnoringOtherApps:YES] like we do in headful mode, everything works.

The only solution that worked so far is creating the first page after a timeout.
2020-12-29 13:49:39 -08:00

495 lines
20 KiB
Objective-C

/*
* Copyright (C) 2010-2016 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 "AppDelegate.h"
#import "BrowserWindowController.h"
#import <WebKit/WKNavigationActionPrivate.h>
#import <WebKit/WKNavigationDelegatePrivate.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKUserContentControllerPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/WebNSURLExtras.h>
#import <WebKit/WebKit.h>
#import <WebKit/_WKDownload.h>
#import <WebKit/_WKExperimentalFeature.h>
#import <WebKit/_WKInternalDebugFeature.h>
#import <WebKit/_WKProcessPoolConfiguration.h>
#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
@implementation NSApplication (PlaywrightApplicationExtensions)
- (BrowserAppDelegate *)browserAppDelegate
{
return (BrowserAppDelegate *)[self delegate];
}
@end
@interface NSApplication (TouchBar)
@property (getter=isAutomaticCustomizeTouchBarMenuItemEnabled) BOOL automaticCustomizeTouchBarMenuItemEnabled;
@property (readonly, nonatomic) WKWebViewConfiguration *defaultConfiguration;
@end
@implementation WebViewDialog
- (void)dealloc
{
[_webView release];
_webView = nil;
[super dealloc];
}
@end
enum {
_NSBackingStoreUnbuffered = 3
};
NSString* const ActivityReason = @"Batch headless process";
const NSActivityOptions ActivityOptions =
(NSActivityUserInitiatedAllowingIdleSystemSleep |
NSActivityLatencyCritical) &
~(NSActivitySuddenTerminationDisabled |
NSActivityAutomaticTerminationDisabled);
@implementation BrowserAppDelegate
- (id)init
{
self = [super init];
if (!self)
return nil;
_initialURL = nil;
_userDataDir = nil;
_proxyServer = nil;
_proxyBypassList = nil;
NSArray *arguments = [[NSProcessInfo processInfo] arguments];
NSRange subargs = NSMakeRange(1, [arguments count] - 1);
NSArray *subArray = [arguments subarrayWithRange:subargs];
for (NSString *argument in subArray) {
if (![argument hasPrefix:@"--"])
_initialURL = argument;
if ([argument hasPrefix:@"--user-data-dir="]) {
NSRange range = NSMakeRange(16, [argument length] - 16);
_userDataDir = [[argument substringWithRange:range] copy];
}
if ([argument hasPrefix:@"--proxy="]) {
NSRange range = NSMakeRange(8, [argument length] - 8);
_proxyServer = [[argument substringWithRange:range] copy];
}
if ([argument hasPrefix:@"--proxy-bypass-list="]) {
NSRange range = NSMakeRange(20, [argument length] - 20);
_proxyBypassList = [[argument substringWithRange:range] copy];
}
}
_headless = [arguments containsObject: @"--headless"];
_noStartupWindow = [arguments containsObject: @"--no-startup-window"];
_browserContexts = [[NSMutableSet alloc] init];
if (_headless) {
_headlessWindows = [[NSMutableSet alloc] init];
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
[[NSProcessInfo processInfo] beginActivityWithOptions:ActivityOptions
reason:ActivityReason];
_dialogs = [[NSMutableSet alloc] init];
} else {
[NSApp activateIgnoringOtherApps:YES];
}
if ([arguments containsObject: @"--inspector-pipe"])
[_WKBrowserInspector initializeRemoteInspectorPipe:self headless:_headless];
return self;
}
- (void)awakeFromNib
{
if ([NSApp respondsToSelector:@selector(setAutomaticCustomizeTouchBarMenuItemEnabled:)])
[NSApp setAutomaticCustomizeTouchBarMenuItemEnabled:YES];
}
- (NSDictionary *)proxyConfiguration:(NSString *)proxyServer WithBypassList:(NSString *)proxyBypassList
{
if (!proxyServer || ![proxyServer length])
return nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSMutableDictionary *dictionary = [[[NSMutableDictionary alloc] init] autorelease];
NSURL *proxyURL = [NSURL URLWithString:proxyServer];
NSString *host = [proxyURL host];
NSNumber *port = [proxyURL port];
if ([proxyServer hasPrefix:@"socks5://"]) {
[dictionary setObject:host forKey:(NSString *)kCFStreamPropertySOCKSProxyHost];
if (port)
[dictionary setObject:port forKey:(NSString *)kCFStreamPropertySOCKSProxyPort];
} else {
[dictionary setObject:host forKey:(NSString *)kCFStreamPropertyHTTPSProxyHost];
[dictionary setObject:host forKey:(NSString *)kCFStreamPropertyHTTPProxyHost];
if (port) {
[dictionary setObject:port forKey:(NSString *)kCFStreamPropertyHTTPSProxyPort];
[dictionary setObject:port forKey:(NSString *)kCFStreamPropertyHTTPProxyPort];
}
}
if (proxyBypassList && [proxyBypassList length]) {
NSArray* bypassList = [proxyBypassList componentsSeparatedByString:@","];
[dictionary setObject:bypassList forKey:@"ExceptionsList"];
}
#pragma clang diagnostic pop
return dictionary;
}
- (WKWebsiteDataStore *)persistentDataStore
{
static WKWebsiteDataStore *dataStore;
if (!dataStore) {
_WKWebsiteDataStoreConfiguration *configuration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
if (_userDataDir) {
NSURL *cookieFile = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/cookie.db", _userDataDir]];
[configuration _setCookieStorageFile:cookieFile];
NSURL *applicationCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ApplicationCache", _userDataDir]];
[configuration setApplicationCacheDirectory:applicationCacheDirectory];
NSURL *cacheStorageDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/CacheStorage", _userDataDir]];
[configuration _setCacheStorageDirectory:cacheStorageDirectory];
NSURL *indexedDBDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/IndexedDB", _userDataDir]];
[configuration _setIndexedDBDatabaseDirectory:indexedDBDirectory];
NSURL *localStorageDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/LocalStorage", _userDataDir]];
[configuration _setWebStorageDirectory:localStorageDirectory];
NSURL *mediaCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/MediaCache", _userDataDir]];
[configuration setMediaCacheDirectory:mediaCacheDirectory];
NSURL *mediaKeysDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/MediaKeys", _userDataDir]];
[configuration setMediaKeysStorageDirectory:mediaKeysDirectory];
NSURL *networkCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/NetworkCache", _userDataDir]];
[configuration setNetworkCacheDirectory:networkCacheDirectory];
NSURL *loadStatsDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ResourceLoadStatistics", _userDataDir]];
[configuration _setResourceLoadStatisticsDirectory:loadStatsDirectory];
NSURL *serviceWorkersDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ServiceWorkers", _userDataDir]];
[configuration _setServiceWorkerRegistrationDirectory:serviceWorkersDirectory];
NSURL *webSqlDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/WebSQL", _userDataDir]];
[configuration _setWebSQLDatabaseDirectory:webSqlDirectory];
}
[configuration setProxyConfiguration:[self proxyConfiguration:_proxyServer WithBypassList:_proxyBypassList]];
dataStore = [[WKWebsiteDataStore alloc] _initWithConfiguration:configuration];
}
return dataStore;
}
- (WKWebViewConfiguration *)defaultConfiguration
{
static WKWebViewConfiguration *configuration;
if (!configuration) {
configuration = [[WKWebViewConfiguration alloc] init];
configuration.websiteDataStore = [self persistentDataStore];
configuration.preferences._fullScreenEnabled = YES;
configuration.preferences._developerExtrasEnabled = YES;
configuration.preferences._mediaDevicesEnabled = YES;
configuration.preferences._mockCaptureDevicesEnabled = YES;
configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO;
configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO;
configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO;
configuration.preferences._domTimersThrottlingEnabled = NO;
configuration.preferences._requestAnimationFrameEnabled = YES;
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
processConfiguration.forceOverlayScrollbars = YES;
configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];
}
return configuration;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
if (!_headless)
[self _updateNewWindowKeyEquivalents];
if (_noStartupWindow)
return;
// Force creation of the default browser context.
[self defaultConfiguration];
// Creating the first NSWindow immediately makes it invisible in headless mode,
// so we postpone it for 50ms. Experiments show that 10ms is not enough, and 20ms is enough.
// We give it 50ms just in case.
[NSTimer scheduledTimerWithTimeInterval: 0.05
repeats: NO
block:(void *)^(NSTimer* timer)
{
[self createNewPage:0 withURL:_initialURL ? _initialURL : @"about:blank"];
_initialURL = nil;
}];
}
- (void)_updateNewWindowKeyEquivalents
{
NSString *normalWindowEquivalent = @"n";
_newWebKit2WindowItem.keyEquivalentModifierMask = NSEventModifierFlagCommand;
_newWebKit2WindowItem.keyEquivalent = normalWindowEquivalent;
}
#pragma mark WKBrowserInspectorDelegate
- (WKWebViewConfiguration *) sessionConfiguration:(uint64_t)sessionID
{
for (_WKBrowserContext *browserContext in _browserContexts) {
if ([[browserContext dataStore] sessionID] != sessionID)
continue;
WKWebViewConfiguration *configuration = [[[self defaultConfiguration] copy] autorelease];
configuration.websiteDataStore = [browserContext dataStore];
configuration.processPool = [browserContext processPool];
return configuration;
}
return [self defaultConfiguration];
}
- (WKWebView *)createNewPage:(uint64_t)sessionID
{
return [self createNewPage:sessionID withURL:@"about:blank"];
}
- (WKWebView *)createNewPage:(uint64_t)sessionID withURL:(NSString*)urlString
{
WKWebViewConfiguration *configuration = [self sessionConfiguration:sessionID];
if (_headless)
return [self createHeadlessPage:configuration withURL:urlString];
return [self createHeadfulPage:configuration withURL:urlString];
}
- (WKWebView *)createHeadfulPage:(WKWebViewConfiguration *)configuration withURL:(NSString*)urlString
{
// WebView lifecycle will control the BrowserWindowController life times.
BrowserWindowController *controller = [[BrowserWindowController alloc] initWithConfiguration:configuration];
if (!controller)
return nil;
[controller loadURLString:urlString];
NSWindow *window = controller.window;
[window setIsVisible:YES];
return [controller webView];
}
- (WKWebView *)createHeadlessPage:(WKWebViewConfiguration *)configuration withURL:(NSString*)urlString
{
NSRect rect = NSMakeRect(0, 0, 1280, 720);
NSScreen *firstScreen = [[NSScreen screens] objectAtIndex:0];
NSRect windowRect = NSOffsetRect(rect, -10000, [firstScreen frame].size.height - rect.size.height + 10000);
NSWindow* window = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSWindowStyleMaskBorderless backing:(NSBackingStoreType)_NSBackingStoreUnbuffered defer:YES];
WKWebView* webView = [[WKWebView alloc] initWithFrame:[window.contentView bounds] configuration:configuration];
webView._windowOcclusionDetectionEnabled = NO;
if (!webView)
return nil;
webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[window.contentView addSubview:webView];
[window setIsVisible:YES];
if (urlString) {
NSURL *url = [NSURL _webkit_URLWithUserTypedString:urlString];
[webView loadRequest:[NSURLRequest requestWithURL:url]];
}
[_headlessWindows addObject:window];
webView.navigationDelegate = self;
webView.UIDelegate = self;
return [webView autorelease];
}
- (_WKBrowserContext *)createBrowserContext:(NSString *)proxyServer WithBypassList:(NSString *) proxyBypassList
{
_WKBrowserContext *browserContext = [[_WKBrowserContext alloc] init];
_WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease];
processConfiguration.forceOverlayScrollbars = YES;
_WKWebsiteDataStoreConfiguration *dataStoreConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration] autorelease];
if (!proxyServer || ![proxyServer length])
proxyServer = _proxyServer;
if (!proxyBypassList || ![proxyBypassList length])
proxyBypassList = _proxyBypassList;
[dataStoreConfiguration setProxyConfiguration:[self proxyConfiguration:proxyServer WithBypassList:proxyBypassList]];
browserContext.dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration] autorelease];
browserContext.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease];
[_browserContexts addObject:browserContext];
return browserContext;
}
- (void)deleteBrowserContext:(uint64_t)sessionID
{
for (_WKBrowserContext *browserContext in _browserContexts) {
if ([[browserContext dataStore] sessionID] != sessionID)
continue;
[_browserContexts removeObject:browserContext];
return;
}
}
- (void)quit
{
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
}
#pragma mark WKUIDelegate
- (void)webViewDidClose:(WKWebView *)webView {
[self webView:webView handleJavaScriptDialog:false value:nil];
for (NSWindow *window in _headlessWindows) {
if (webView.window != window)
continue;
[webView removeFromSuperview];
[window close];
[_headlessWindows removeObject:window];
break;
}
}
- (void)_webView:(WKWebView *)webView getWindowFrameWithCompletionHandler:(void (^)(CGRect))completionHandler
{
completionHandler([webView.window frame]);
}
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
WebViewDialog* dialog = [[WebViewDialog alloc] autorelease];
dialog.webView = webView;
dialog.completionHandler = ^void (BOOL accept, NSString* value) {
completionHandler();
};
[_dialogs addObject:dialog];
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
{
WebViewDialog* dialog = [[WebViewDialog alloc] autorelease];
dialog.webView = webView;
dialog.completionHandler = ^void (BOOL accept, NSString* value) {
completionHandler(accept);
};
[_dialogs addObject:dialog];
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
{
WebViewDialog* dialog = [[WebViewDialog alloc] autorelease];
dialog.webView = webView;
dialog.completionHandler = ^void (BOOL accept, NSString* value) {
completionHandler(accept && value ? value : nil);
};
[_dialogs addObject:dialog];
}
- (void)_webView:(WKWebView *)webView runBeforeUnloadConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
{
WebViewDialog* dialog = [[WebViewDialog alloc] autorelease];
dialog.webView = webView;
dialog.completionHandler = ^void (BOOL accept, NSString* value) {
completionHandler(accept);
};
[_dialogs addObject:dialog];
}
- (void)webView:(WKWebView *)webView handleJavaScriptDialog:(BOOL)accept value:(NSString *)value
{
for (WebViewDialog *dialog in _dialogs) {
if (dialog.webView != webView)
continue;
dialog.completionHandler(accept, value);
[_dialogs removeObject:dialog];
break;
}
}
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
return [self createHeadlessPage:configuration withURL:nil];
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
LOG(@"decidePolicyForNavigationAction");
if (navigationAction.shouldPerformDownload) {
decisionHandler(WKNavigationActionPolicyDownload);
return;
}
if (navigationAction._canHandleRequest) {
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
decisionHandler(WKNavigationActionPolicyCancel);
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
if (![navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
decisionHandler(WKNavigationResponsePolicyAllow);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
if (disposition && [disposition hasPrefix:@"attachment"]) {
decisionHandler(WKNavigationResponsePolicyDownload);
return;
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
- (void)webView:(WKWebView *)webView navigationAction:(WKNavigationAction *)navigationAction didBecomeDownload:(WKDownload *)download
{
download.delegate = self;
}
- (void)webView:(WKWebView *)webView navigationResponse:(WKNavigationResponse *)navigationResponse didBecomeDownload:(WKDownload *)download
{
download.delegate = self;
}
#pragma mark WKDownloadDelegate
- (void)download:(WKDownload *)download decideDestinationUsingResponse:(NSURLResponse *)response suggestedFilename:(NSString *)suggestedFilename completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler
{
completionHandler(nil);
}
@end