/* * Copyright (C) 2018 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 "DeprecatedGlobalValues.h" #import "PlatformUtilities.h" #import "Test.h" #import "TestNavigationDelegate.h" #import "TestWKWebView.h" #import #import #import #import #import #import #import #import static bool didCrash; static _WKProcessTerminationReason expectedCrashReason; static bool startedLoad; static bool finishedLoad; static bool shouldLoadAgainOnCrash; static bool calledAllCallbacks; static unsigned callbackCount; static unsigned crashHandlerCount; static unsigned loadCount; static unsigned expectedLoadCount; static NSString *testHTML = @""; @interface CrashOnStartNavigationDelegate : NSObject @end @implementation CrashOnStartNavigationDelegate - (void)_webView:(WKWebView *)webView webContentProcessDidTerminateWithReason:(_WKProcessTerminationReason)reason { EXPECT_FALSE(didCrash); didCrash = true; EXPECT_EQ(expectedCrashReason, reason); EXPECT_EQ(0, webView._webProcessIdentifier); // Attempt the load again. if (shouldLoadAgainOnCrash) [webView loadHTMLString:testHTML baseURL:nil]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { finishedLoad = true; } @end @interface BasicNavigationDelegateWithoutCrashHandler : NSObject @end @implementation BasicNavigationDelegateWithoutCrashHandler - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { startedLoad = true; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { finishedLoad = true; } @end @interface CrashRecoveryScriptMessageHandler : NSObject @end @implementation CrashRecoveryScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { EXPECT_STREQ([(NSString *)[message body] UTF8String], "LOADED"); receivedScriptMessage = true; } @end TEST(WKNavigation, FailureToStartWebProcessRecovery) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto handler = adoptNS([[CrashRecoveryScriptMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:handler.get() name:@"testHandler"]; [configuration.get().processPool _makeNextWebProcessLaunchFailForTesting]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); auto delegate = adoptNS([[CrashOnStartNavigationDelegate alloc] init]); [webView setNavigationDelegate:delegate.get()]; finishedLoad = false; didCrash = false; receivedScriptMessage = false; expectedCrashReason = _WKProcessTerminationReasonCrash; shouldLoadAgainOnCrash = true; [webView loadHTMLString:testHTML baseURL:nil]; TestWebKitAPI::Util::run(&finishedLoad); EXPECT_TRUE(didCrash); EXPECT_TRUE(!!webView.get()._webProcessIdentifier); EXPECT_TRUE(receivedScriptMessage); } TEST(WKNavigation, FailureToStartWebProcessAfterCrashRecovery) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto handler = adoptNS([[CrashRecoveryScriptMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:handler.get() name:@"testHandler"]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); auto delegate = adoptNS([[CrashOnStartNavigationDelegate alloc] init]); [webView setNavigationDelegate:delegate.get()]; receivedScriptMessage = false; finishedLoad = false; didCrash = false; [webView loadHTMLString:testHTML baseURL:nil]; TestWebKitAPI::Util::run(&finishedLoad); EXPECT_FALSE(didCrash); EXPECT_TRUE(!!webView.get()._webProcessIdentifier); EXPECT_TRUE(receivedScriptMessage); receivedScriptMessage = false; shouldLoadAgainOnCrash = false; expectedCrashReason = _WKProcessTerminationReasonRequestedByClient; [webView _killWebContentProcessAndResetState]; TestWebKitAPI::Util::run(&didCrash); EXPECT_TRUE(!webView.get()._webProcessIdentifier); EXPECT_FALSE(receivedScriptMessage); expectedCrashReason = _WKProcessTerminationReasonCrash; didCrash = false; finishedLoad = false; receivedScriptMessage = false; shouldLoadAgainOnCrash = true; [configuration.get().processPool _makeNextWebProcessLaunchFailForTesting]; [webView loadHTMLString:testHTML baseURL:nil]; TestWebKitAPI::Util::run(&finishedLoad); EXPECT_TRUE(didCrash); EXPECT_TRUE(!!webView.get()._webProcessIdentifier); EXPECT_TRUE(receivedScriptMessage); } TEST(WKNavigation, AutomaticVisibleViewReloadAfterWebProcessCrash) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get() addToWindow:YES]); auto delegate = adoptNS([[BasicNavigationDelegateWithoutCrashHandler alloc] init]); [webView setNavigationDelegate:delegate.get()]; startedLoad = false; finishedLoad = false; [webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"rich-and-plain-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; TestWebKitAPI::Util::run(&finishedLoad); startedLoad = false; finishedLoad = false; // Simulate crash. [webView _killWebContentProcess]; // Since we do not deal with the crash, WebKit should attempt a reload. TestWebKitAPI::Util::run(&finishedLoad); startedLoad = false; finishedLoad = false; // Simulate another crash. [webView _killWebContentProcess]; // WebKit should not attempt to reload again. EXPECT_FALSE(startedLoad); TestWebKitAPI::Util::sleep(0.5); EXPECT_FALSE(startedLoad); } TEST(WKNavigation, AutomaticHiddenViewDelayedReloadAfterWebProcessCrash) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get() addToWindow:YES]); auto delegate = adoptNS([[BasicNavigationDelegateWithoutCrashHandler alloc] init]); [webView setNavigationDelegate:delegate.get()]; // Make sure the view is not visible. [webView removeFromSuperview]; startedLoad = false; finishedLoad = false; [webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"rich-and-plain-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; TestWebKitAPI::Util::run(&finishedLoad); startedLoad = false; finishedLoad = false; // Simulate crash. [webView _killWebContentProcess]; TestWebKitAPI::Util::sleep(0.5); // WebKit should not have attempted a reload since the view is not visible. EXPECT_FALSE(startedLoad); EXPECT_FALSE(finishedLoad); // Make the view visible. [webView addToTestWindow]; [webView focus]; // WebKit should have triggered a reload when the view became visible. TestWebKitAPI::Util::run(&finishedLoad); } TEST(WKNavigation, ProcessCrashDuringCallback) { auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]); auto delegate = adoptNS([[BasicNavigationDelegateWithoutCrashHandler alloc] init]); [webView setNavigationDelegate:delegate.get()]; startedLoad = false; finishedLoad = false; [webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"rich-and-plain-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; TestWebKitAPI::Util::run(&finishedLoad); startedLoad = false; finishedLoad = false; callbackCount = 0; calledAllCallbacks = false; __block WKWebView *view = webView.get(); [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); [view _close]; // Calling _close will also invalidate all callbacks. ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { if (!!error) EXPECT_TRUE(error.code == WKErrorWebContentProcessTerminated || error.code == WKErrorWebViewInvalidated); ++callbackCount; if (callbackCount == 6) calledAllCallbacks = true; }]; // Simulate a crash, which should invalidate all pending callbacks. [webView _killWebContentProcess]; TestWebKitAPI::Util::run(&calledAllCallbacks); TestWebKitAPI::Util::sleep(0.5); EXPECT_EQ(6U, callbackCount); } @interface NavigationDelegateWithCrashHandlerThatLoadsAgain : NSObject @end @implementation NavigationDelegateWithCrashHandlerThatLoadsAgain - (void)_webView:(WKWebView *)webView webContentProcessDidTerminateWithReason:(_WKProcessTerminationReason)reason { ++crashHandlerCount; // Attempt the load again synchronously. [webView loadHTMLString:@"foo" baseURL:nil]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { if (++loadCount == expectedLoadCount) finishedLoad = true; } @end TEST(WKNavigation, ReloadRelatedViewsInProcessDidTerminate) { const unsigned numberOfViews = 20; auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get()]); Vector> webViews; webViews.append(webView1); configuration.get()._relatedWebView = webView1.get(); for (unsigned i = 0; i < numberOfViews - 1; ++i) { auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get()]); webViews.append(webView); } auto delegate = adoptNS([[NavigationDelegateWithCrashHandlerThatLoadsAgain alloc] init]); for (auto& webView : webViews) [webView setNavigationDelegate:delegate.get()]; crashHandlerCount = 0; loadCount = 0; expectedLoadCount = numberOfViews; finishedLoad = false; for (auto& webView : webViews) [webView loadHTMLString:@"foo" baseURL:nil]; TestWebKitAPI::Util::run(&finishedLoad); EXPECT_EQ(0U, crashHandlerCount); auto pidBefore = [webView1 _webProcessIdentifier]; EXPECT_TRUE(!!pidBefore); for (auto& webView : webViews) EXPECT_EQ(pidBefore, [webView _webProcessIdentifier]); loadCount = 0; finishedLoad = false; // Kill the WebContent process. The crash handler should reload all views. kill(pidBefore, 9); TestWebKitAPI::Util::run(&finishedLoad); EXPECT_EQ(numberOfViews, crashHandlerCount); auto pidAfter = [webView1 _webProcessIdentifier]; EXPECT_TRUE(!!pidAfter); for (auto& webView : webViews) EXPECT_EQ(pidAfter, [webView _webProcessIdentifier]); } TEST(WKNavigation, WebViewURLInProcessDidTerminate) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get()]); [webView synchronouslyLoadTestPageNamed:@"simple"]; NSString *viewURL = [webView URL].absoluteString; EXPECT_TRUE(!!viewURL); auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); [webView setNavigationDelegate:navigationDelegate.get()]; __block bool done = false; navigationDelegate.get().webContentProcessDidTerminate = ^(WKWebView *view) { EXPECT_EQ(view, webView.get()); EXPECT_WK_STREQ(view.URL.absoluteString, viewURL); done = true; }; kill([webView _webProcessIdentifier], 9); TestWebKitAPI::Util::run(&done); } TEST(WKNavigation, WebProcessLimit) { constexpr unsigned maxProcessCount = 10; [WKProcessPool _setWebProcessCountLimit:maxProcessCount]; auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) { finishedLoad = true; }]; auto createWebView = [&] { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100) configuration:configuration.get()]); [webView setNavigationDelegate:navigationDelegate.get()]; finishedLoad = false; [webView loadTestPageNamed:@"simple"]; TestWebKitAPI::Util::run(&finishedLoad); return webView; }; [navigationDelegate setWebContentProcessDidTerminate:^(WKWebView *) { didCrash = true; }]; Vector> views; for (unsigned i = 0; i < maxProcessCount; ++i) views.append(createWebView()); EXPECT_FALSE(didCrash); for (auto& view : views) EXPECT_NE([view _webProcessIdentifier], 0); // We have now reached the WebProcess cap, let's try and launch a new one. __block unsigned crashCount = 0; [navigationDelegate setWebContentProcessDidTerminate:^(WKWebView * view) { EXPECT_EQ(views[0], view); ++crashCount; }]; views.append(createWebView()); EXPECT_EQ(crashCount, 1U); for (unsigned i = 0; i < views.size(); ++i) { if (!i) EXPECT_EQ([views[i] _webProcessIdentifier], 0); else EXPECT_NE([views[i] _webProcessIdentifier], 0); } crashCount = 0; [navigationDelegate setWebContentProcessDidTerminate:^(WKWebView * view) { EXPECT_EQ(views[1], view); ++crashCount; }]; views.append(createWebView()); EXPECT_EQ(crashCount, 1U); for (unsigned i = 0; i < views.size(); ++i) { if (i < 2) EXPECT_EQ([views[i] _webProcessIdentifier], 0); else EXPECT_NE([views[i] _webProcessIdentifier], 0); } [WKProcessPool _setWebProcessCountLimit:400]; }