Commit 69538c3b authored by mnaganov@chromium.org's avatar mnaganov@chromium.org

[Android WebView] Terminate execution of stuck JS code on navigation requests

Allow WebView.loadUrl-family functions to terminate execution of JS code that
seems to be stuck in a loop.

BUG=390906

Review URL: https://codereview.chromium.org/366913006

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@281715 0039d316-1c4b-4281-b951-d872f2087c98
parent 1f0a2e48
......@@ -106,6 +106,7 @@
'../printing/printing.gyp:printing',
'../skia/skia.gyp:skia',
'../third_party/WebKit/public/blink.gyp:blink',
'../v8/tools/gyp/v8.gyp:v8',
'../ui/gl/gl.gyp:gl',
'../ui/shell_dialogs/shell_dialogs.gyp:shell_dialogs',
'../webkit/common/gpu/webkit_gpu.gyp:webkit_gpu',
......@@ -225,6 +226,8 @@
'public/browser/draw_gl.h',
'renderer/aw_content_renderer_client.cc',
'renderer/aw_content_renderer_client.h',
'renderer/aw_execution_termination_filter.cc',
'renderer/aw_execution_termination_filter.h',
'renderer/aw_key_systems.cc',
'renderer/aw_key_systems.h',
'renderer/aw_permission_client.cc',
......
......@@ -104,6 +104,10 @@ void AwRenderViewHostExt::SetJsOnlineProperty(bool network_up) {
Send(new AwViewMsg_SetJsOnlineProperty(network_up));
}
void AwRenderViewHostExt::SendCheckRenderThreadResponsiveness() {
Send(new AwViewMsg_CheckRenderThreadResponsiveness());
}
void AwRenderViewHostExt::RenderViewCreated(
content::RenderViewHost* render_view_host) {
Send(new AwViewMsg_SetBackgroundColor(web_contents()->GetRoutingID(),
......
......@@ -79,6 +79,8 @@ class AwRenderViewHostExt : public content::WebContentsObserver,
void SetBackgroundColor(SkColor c);
void SetJsOnlineProperty(bool network_up);
void SendCheckRenderThreadResponsiveness();
private:
// content::WebContentsObserver implementation.
virtual void RenderViewCreated(content::RenderViewHost* view_host) OVERRIDE;
......
......@@ -79,6 +79,10 @@ IPC_MESSAGE_ROUTED1(AwViewMsg_SetBackgroundColor,
IPC_MESSAGE_CONTROL1(AwViewMsg_SetJsOnlineProperty,
bool /* network_up */)
// Sent prior to making a navigation via loadUrl to make sure that
// render thread isn't stuck in a loop induced by JavaScript code.
IPC_MESSAGE_CONTROL0(AwViewMsg_CheckRenderThreadResponsiveness)
//-----------------------------------------------------------------------------
// RenderView messages
// These are messages sent from the renderer to the browser process.
......
......@@ -1027,6 +1027,8 @@ public class AwContents {
* @param params Parameters for this load.
*/
public void loadUrl(LoadUrlParams params) {
if (mNativeAwContents == 0) return;
if (params.getLoadUrlType() == LoadUrlParams.LOAD_TYPE_DATA &&
!params.isBaseUrlDataScheme()) {
// This allows data URLs with a non-data base URL access to file:///android_asset/ and
......@@ -1065,12 +1067,11 @@ public class AwContents {
}
}
if (mNativeAwContents != 0) {
nativeSetExtraHeadersForUrl(
mNativeAwContents, params.getUrl(), params.getExtraHttpRequestHeadersString());
}
nativeSetExtraHeadersForUrl(
mNativeAwContents, params.getUrl(), params.getExtraHttpRequestHeadersString());
params.setExtraHeaders(new HashMap<String, String>());
nativeSendCheckRenderThreadResponsiveness(mNativeAwContents);
mContentViewCore.loadUrl(params);
// The behavior of WebViewClassic uses the populateVisitedLinks callback in WebKit.
......@@ -2458,6 +2459,7 @@ public class AwContents {
private native void nativeClearView(long nativeAwContents);
private native void nativeSetExtraHeadersForUrl(long nativeAwContents,
String url, String extraHeaders);
private native void nativeSendCheckRenderThreadResponsiveness(long nativeAwContents);
private native void nativeInvokeGeolocationCallback(
long nativeAwContents, boolean value, String requestingFrame);
......
......@@ -4,9 +4,12 @@
package org.chromium.android_webview.test;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Pair;
import junit.framework.Assert;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.chromium.android_webview.AwContents;
......@@ -331,4 +334,71 @@ public class LoadUrlTest extends AwTestBase {
if (webServer != null) webServer.shutdown();
}
}
private static class TestController {
private final Object mLock = new Object();
private boolean mIsReady = false;
public void notifyPageIsReady() {
synchronized (mLock) {
mIsReady = true;
mLock.notify();
}
}
public void waitUntilIsReady() {
synchronized (mLock) {
while (!mIsReady) {
try {
mLock.wait(WAIT_TIMEOUT_MS);
} catch (Exception e) {
continue;
}
if (!mIsReady) {
Assert.fail("Wait timed out");
}
}
mIsReady = false;
}
}
}
// Verify that it is possible to interrupt JS scripts stuck in an infinite loop
// by calling loadUrl on a WebView.
@LargeTest
@Feature({"AndroidWebView"})
public void testLoadUrlInterruptsLoopedScripts() throws Throwable {
final String infiniteLoopPage =
"<html><head>" +
" <script>" +
" function infiniteLoop() {" +
" test.notifyPageIsReady();" +
" while(1);" +
" }" +
" </script>" +
"</head><body onload='setTimeout(infiniteLoop, 0)'>" +
"</body></html>";
final String simplePage = "<html><body onload='test.notifyPageIsReady()'></body></html>";
final String expectedTitle = "PASS";
final String pageWithTitle = "<html><body onload='document.title=\"" + expectedTitle +
"\"; test.notifyPageIsReady()'></body></html>";
final AwTestContainerView testContainerView =
createAwTestContainerViewOnMainSync(new TestAwContentsClient());
final AwContents awContents = testContainerView.getAwContents();
getAwSettingsOnUiThread(awContents).setJavaScriptEnabled(true);
final TestController testController = new TestController();
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
awContents.addPossiblyUnsafeJavascriptInterface(testController, "test", null);
}
});
loadDataAsync(awContents, infiniteLoopPage, "text/html", false);
testController.waitUntilIsReady();
loadDataAsync(awContents, simplePage, "text/html", false);
testController.waitUntilIsReady();
// Load another page that runs JS to make sure that the WebView is still functional.
loadDataAsync(awContents, pageWithTitle, "text/html", false);
testController.waitUntilIsReady();
assertEquals(expectedTitle, getTitleOnUiThread(awContents));
}
}
......@@ -1121,6 +1121,11 @@ void AwContents::SetExtraHeadersForUrl(JNIEnv* env, jobject obj,
extra_headers);
}
void AwContents::SendCheckRenderThreadResponsiveness(JNIEnv* env, jobject obj) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
render_view_host_ext_->SendCheckRenderThreadResponsiveness();
}
void AwContents::SetJsOnlineProperty(JNIEnv* env,
jobject obj,
jboolean network_up) {
......
......@@ -131,6 +131,7 @@ class AwContents : public FindHelper::Listener,
void ClearView(JNIEnv* env, jobject obj);
void SetExtraHeadersForUrl(JNIEnv* env, jobject obj,
jstring url, jstring extra_headers);
void SendCheckRenderThreadResponsiveness(JNIEnv* env, jobject obj);
void DrawGL(AwDrawGLInfo* draw_info);
......
......@@ -7,6 +7,7 @@
#include "android_webview/common/aw_resource.h"
#include "android_webview/common/render_view_messages.h"
#include "android_webview/common/url_constants.h"
#include "android_webview/renderer/aw_execution_termination_filter.h"
#include "android_webview/renderer/aw_key_systems.h"
#include "android_webview/renderer/aw_permission_client.h"
#include "android_webview/renderer/aw_render_frame_ext.h"
......@@ -31,6 +32,7 @@
#include "third_party/WebKit/public/platform/WebURLError.h"
#include "third_party/WebKit/public/platform/WebURLRequest.h"
#include "third_party/WebKit/public/web/WebFrame.h"
#include "third_party/WebKit/public/web/WebKit.h"
#include "third_party/WebKit/public/web/WebNavigationType.h"
#include "third_party/WebKit/public/web/WebSecurityPolicy.h"
#include "url/gurl.h"
......@@ -61,6 +63,17 @@ void AwContentRendererClient::RenderThreadStarted() {
visited_link_slave_.reset(new visitedlink::VisitedLinkSlave);
thread->AddObserver(visited_link_slave_.get());
execution_termination_filter_ = new AwExecutionTerminationFilter(
thread->GetIOMessageLoopProxy(),
thread->GetMessageLoop()->message_loop_proxy());
thread->AddFilter(execution_termination_filter_.get());
thread->AddObserver(this);
}
void AwContentRendererClient::WebKitInitialized() {
execution_termination_filter_->SetRenderThreadIsolate(
blink::mainThreadIsolate());
}
bool AwContentRendererClient::HandleNavigation(
......
......@@ -16,7 +16,10 @@ class VisitedLinkSlave;
namespace android_webview {
class AwContentRendererClient : public content::ContentRendererClient {
class AwExecutionTerminationFilter;
class AwContentRendererClient : public content::ContentRendererClient,
public content::RenderProcessObserver {
public:
AwContentRendererClient();
virtual ~AwContentRendererClient();
......@@ -50,9 +53,13 @@ class AwContentRendererClient : public content::ContentRendererClient {
blink::WebNavigationPolicy default_policy,
bool is_redirect) OVERRIDE;
// content::RenderProcessObserver implementation.
virtual void WebKitInitialized() OVERRIDE;
private:
scoped_ptr<AwRenderProcessObserver> aw_render_process_observer_;
scoped_ptr<visitedlink::VisitedLinkSlave> visited_link_slave_;
scoped_refptr<AwExecutionTerminationFilter> execution_termination_filter_;
};
} // namespace android_webview
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "android_webview/renderer/aw_execution_termination_filter.h"
#include <v8.h>
#include "android_webview/common/render_view_messages.h"
#include "base/message_loop/message_loop_proxy.h"
namespace {
const int kTerminationTimeoutInSeconds = 3;
} // namespace
namespace android_webview {
AwExecutionTerminationFilter::AwExecutionTerminationFilter(
const scoped_refptr<base::MessageLoopProxy>& io_message_loop,
const scoped_refptr<base::MessageLoopProxy>& main_message_loop)
: io_message_loop_(io_message_loop),
main_message_loop_(main_message_loop),
render_thread_isolate_(NULL) {
}
AwExecutionTerminationFilter::~AwExecutionTerminationFilter() {
}
void AwExecutionTerminationFilter::SetRenderThreadIsolate(
v8::Isolate* isolate) {
render_thread_isolate_ = isolate;
}
bool AwExecutionTerminationFilter::OnMessageReceived(
const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(AwExecutionTerminationFilter, message)
IPC_MESSAGE_HANDLER(AwViewMsg_CheckRenderThreadResponsiveness,
OnCheckRenderThreadResponsiveness)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void AwExecutionTerminationFilter::OnCheckRenderThreadResponsiveness() {
termination_timer_.Start(
FROM_HERE,
base::TimeDelta::FromSeconds(kTerminationTimeoutInSeconds),
base::Bind(&AwExecutionTerminationFilter::TerminateExecution, this));
// Post a request to stop the timer via render thread's message loop
// to ensure that render thread is responsive.
main_message_loop_->PostTask(
FROM_HERE,
base::Bind(&AwExecutionTerminationFilter::StopTimerOnMainThread,
this));
}
void AwExecutionTerminationFilter::StopTimerOnMainThread() {
io_message_loop_->PostTask(
FROM_HERE,
base::Bind(&AwExecutionTerminationFilter::StopTimer, this));
}
void AwExecutionTerminationFilter::StopTimer() {
termination_timer_.Stop();
}
void AwExecutionTerminationFilter::TerminateExecution() {
if (render_thread_isolate_) {
LOG(WARNING) << "Trying to terminate JavaScript execution because "
"renderer is unresponsive";
v8::V8::TerminateExecution(render_thread_isolate_);
}
}
} // namespace android_webview
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ANDROID_WEBVIEW_RENDERER_AW_EXECUTION_TERMINATION_FILTER_H_
#define ANDROID_WEBVIEW_RENDERER_AW_EXECUTION_TERMINATION_FILTER_H_
#include "base/memory/scoped_ptr.h"
#include "base/timer/timer.h"
#include "ipc/message_filter.h"
namespace base {
class MessageLoopProxy;
}
namespace v8 {
class Isolate;
}
namespace android_webview {
// The purpose of AwExecutionTerminationFilter is to attempt to terminate
// any JavaScript code that is stuck in a loop before doing a navigation
// originating from a Andoird WebView URL loading functions.
//
// This is how it works. AwExecutionTerminationFilter is created on render
// thread. It listens on IO thread for navigation requests coming from
// AwContents.loadUrl calls. On each such a request, it posts a delayed
// cancellable task on the IO thread's message loop and, at the same time, posts
// a cancellation task on the render thread's message loop. If render thread
// is not stuck, the cancellation task runs and cancels the delayed task.
// Otherwise, the delayed task runs and terminates execution of JS code
// from the IO thread.
class AwExecutionTerminationFilter : public IPC::MessageFilter {
public:
AwExecutionTerminationFilter(
const scoped_refptr<base::MessageLoopProxy>& io_message_loop,
const scoped_refptr<base::MessageLoopProxy>& main_message_loop);
void SetRenderThreadIsolate(v8::Isolate* isolate);
private:
virtual ~AwExecutionTerminationFilter();
// IPC::MessageFilter methods.
virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
void OnCheckRenderThreadResponsiveness();
void StopTimerOnMainThread();
void StopTimer();
void TerminateExecution();
const scoped_refptr<base::MessageLoopProxy> io_message_loop_;
const scoped_refptr<base::MessageLoopProxy> main_message_loop_;
v8::Isolate* render_thread_isolate_;
base::OneShotTimer<AwExecutionTerminationFilter> termination_timer_;
DISALLOW_COPY_AND_ASSIGN(AwExecutionTerminationFilter);
};
} // namespace android_webview
#endif // ANDROID_WEBVIEW_RENDERER_AW_EXECUTION_TERMINATION_FILTER_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment