Skip to content

Commit

Permalink
Add page.settings.scriptTimeout and plumb it through to JSC.
Browse files Browse the repository at this point in the history
The underlying JavaScriptCore mechanism for interrupting scripts
has changed, and requires all-new glue in the QtWebKit layer.

PhantomJS issue ariya#12504.
  • Loading branch information
Chris Bamford authored and zackw committed Nov 3, 2015
1 parent d9569fa commit 4be9c0c
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/consts.h
Expand Up @@ -63,6 +63,7 @@
#define PAGE_SETTINGS_WEB_SECURITY_ENABLED "webSecurityEnabled"
#define PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS "javascriptCanOpenWindows"
#define PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS "javascriptCanCloseWindows"
#define PAGE_SETTINGS_SCRIPT_TIMEOUT "scriptTimeout"

#define DEFAULT_WEBDRIVER_CONFIG "127.0.0.1:8910"

Expand Down
1 change: 1 addition & 0 deletions src/phantom.cpp
Expand Up @@ -139,6 +139,7 @@ void Phantom::init()
m_defaultPageSettings[PAGE_SETTINGS_WEB_SECURITY_ENABLED] = QVariant::fromValue(m_config.webSecurityEnabled());
m_defaultPageSettings[PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS] = QVariant::fromValue(m_config.javascriptCanOpenWindows());
m_defaultPageSettings[PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS] = QVariant::fromValue(m_config.javascriptCanCloseWindows());
m_defaultPageSettings[PAGE_SETTINGS_SCRIPT_TIMEOUT] = QVariant::fromValue(0.0);
m_page->applySettings(m_defaultPageSettings);

setLibraryPath(QFileInfo(m_config.scriptFile()).dir().absolutePath());
Expand Down
47 changes: 47 additions & 0 deletions src/qt/qtwebkit/Source/JavaScriptCore/API/JSContextRef.h
Expand Up @@ -125,6 +125,53 @@ JS_EXPORT JSObjectRef JSContextGetGlobalObject(JSContextRef ctx);
*/
JS_EXPORT JSContextGroupRef JSContextGetGroup(JSContextRef ctx) AVAILABLE_IN_WEBKIT_VERSION_4_0;

/*!
@typedef JSShouldTerminateCallback
@abstract The callback invoked when script execution has exceeded the allowed
time limit previously specified via JSContextGroupSetExecutionTimeLimit.
@param ctx The execution context to use.
@param context User specified context data previously passed to
JSContextGroupSetExecutionTimeLimit.
@discussion If you named your function Callback, you would declare it like this:
bool Callback(JSContextRef ctx, void* context);
If you return true, the timed out script will terminate.
If you return false, the script will run for another period of the allowed
time limit specified via JSContextGroupSetExecutionTimeLimit.
Within this callback function, you may call JSContextGroupSetExecutionTimeLimit
to set a new time limit, or JSContextGroupClearExecutionTimeLimit to cancel the
timeout.
*/
typedef bool
(*JSShouldTerminateCallback) (JSContextRef ctx, void* context);

/*!
@function
@abstract Sets the script execution time limit.
@param group The JavaScript context group that this time limit applies to.
@param limit The time limit of allowed script execution time in seconds.
@param callback The callback function that will be invoked when the time limit
has been reached. This will give you a chance to decide if you want to
terminate the script or not. If you pass a NULL callback, the script will be
terminated unconditionally when the time limit has been reached.
@param context User data that you can provide to be passed back to you
in your callback.
In order to guarantee that the execution time limit will take effect, you will
need to call JSContextGroupSetExecutionTimeLimit before you start executing
any scripts.
*/
JS_EXPORT void JSContextGroupSetExecutionTimeLimit(JSContextGroupRef, double limit, JSShouldTerminateCallback, void* context) AVAILABLE_IN_WEBKIT_VERSION_4_0;

/*!
@function
@abstract Clears the script execution time limit.
@param group The JavaScript context group that the time limit is cleared on.
*/
JS_EXPORT void JSContextGroupClearExecutionTimeLimit(JSContextGroupRef) AVAILABLE_IN_WEBKIT_VERSION_4_0;

#ifdef __cplusplus
}
#endif
Expand Down
Expand Up @@ -195,6 +195,32 @@ void QWebFrameAdapter::handleGestureEvent(QGestureEventFacade* gestureEvent)
}
#endif

// Stub for QWebFramePrivate
bool QWebFrameAdapter::shouldInterruptJavaScript()
{
return false;
}

bool terminateCallback(JSContextRef ctx, void* context)
{
QWebFrameAdapter* frameAdapter = static_cast<QWebFrameAdapter*>(context);
return frameAdapter->shouldInterruptJavaScript();
}

void QWebFrameAdapter::setJSTimeout(double timeout) {
ScriptController* scriptController = frame->script();
JSC::ExecState* exec = scriptController->globalObject(mainThreadNormalWorld())->globalExec();

JSContextGroupRef contextGroup = toRef(&exec->vm());

if (timeout == 0.0) {
JSContextGroupClearExecutionTimeLimit(contextGroup);
} else {
JSContextGroupSetExecutionTimeLimit(contextGroup, timeout,
terminateCallback, this);
}
}

QVariant QWebFrameAdapter::evaluateJavaScript(const QString &scriptSource, const QString &location)
{
ScriptController* scriptController = frame->script();
Expand Down
Expand Up @@ -23,6 +23,7 @@
#include "FrameLoaderClientQt.h"
#include "PlatformEvent.h"
#include "PlatformExportMacros.h"
#include "JSContextRef.h"

#if ENABLE(ORIENTATION_EVENTS) && HAVE(QTSENSORS)
#include "qorientationsensor.h"
Expand Down Expand Up @@ -142,6 +143,7 @@ class WEBKIT_EXPORTDATA QWebFrameAdapter {
virtual void didStartProvisionalLoad() = 0;
virtual void didClearWindowObject() = 0;
virtual bool handleProgressFinished(QPoint*) = 0;
virtual bool shouldInterruptJavaScript();
virtual void emitInitialLayoutCompleted() = 0;
virtual void emitIconChanged() = 0;
virtual void emitLoadStarted(bool originatingLoad) = 0;
Expand All @@ -155,6 +157,8 @@ class WEBKIT_EXPORTDATA QWebFrameAdapter {
#endif
QWebFrameAdapter* createFrame(QWebFrameData*);

void setJSTimeout(double timeout);

QVariant evaluateJavaScript(const QString& scriptSource, const QString& location);
void addToJavaScriptWindowObject(const QString& name, QObject*, ValueOwnership);

Expand Down
Expand Up @@ -174,6 +174,7 @@ class WEBKIT_EXPORTDATA QWebPageAdapter {
virtual bool javaScriptConfirm(QWebFrameAdapter*, const QString& msg) = 0;
virtual bool javaScriptPrompt(QWebFrameAdapter*, const QString& msg, const QString& defaultValue, QString* result) = 0;
virtual bool shouldInterruptJavaScript() = 0;
virtual void setJSTimeout(double timeout) = 0;
virtual void printRequested(QWebFrameAdapter*) = 0;
virtual void databaseQuotaExceeded(QWebFrameAdapter*, const QString& databaseName) = 0;
virtual void applicationCacheQuotaExceeded(QWebSecurityOrigin*, quint64 defaultOriginQuota, quint64 totalSpaceNeeded) = 0;
Expand Down
14 changes: 14 additions & 0 deletions src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebframe.cpp
Expand Up @@ -108,6 +108,11 @@ void QWebFramePrivate::emitLoadStarted(bool originatingLoad)
emit q->loadStarted();
}

bool QWebFramePrivate::shouldInterruptJavaScript()
{
return page->shouldInterruptJavaScript();
}

void QWebFramePrivate::emitLoadFinished(bool originatingLoad, bool ok)
{
if (page && originatingLoad)
Expand Down Expand Up @@ -1308,4 +1313,13 @@ QWebFrameAdapter *QWebFrame::handle() const
return d;
}

/*!
Set the maximum amount of time a script can run in this frame.
Value is in seconds. Zero means forever.
*/
void QWebFrame::setJSTimeout(double timeout)
{
d->setJSTimeout(timeout);
}

#include "moc_qwebframe.cpp"
2 changes: 2 additions & 0 deletions src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebframe.h
Expand Up @@ -217,6 +217,8 @@ class QWEBKITWIDGETS_EXPORT QWebFrame : public QObject {
};
#endif

void setJSTimeout(double timeout);

public Q_SLOTS:
QVariant evaluateJavaScript(const QString& scriptSource, const QString& location = QString());
#ifndef QT_NO_PRINTER
Expand Down
1 change: 1 addition & 0 deletions src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebframe_p.h
Expand Up @@ -57,6 +57,7 @@ class QWebFramePrivate : public QWebFrameAdapter {
virtual void didStartProvisionalLoad() OVERRIDE;
virtual void didClearWindowObject() OVERRIDE;
virtual bool handleProgressFinished(QPoint*) OVERRIDE;
virtual bool shouldInterruptJavaScript() OVERRIDE;
virtual void emitInitialLayoutCompleted() OVERRIDE;
virtual void emitIconChanged() OVERRIDE;
virtual void emitLoadStarted(bool originatingLoad) OVERRIDE;
Expand Down
24 changes: 23 additions & 1 deletion src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebpage.cpp
Expand Up @@ -202,6 +202,7 @@ QWebPagePrivate::QWebPagePrivate(QWebPage *qq)
, inspector(0)
, inspectorIsInternalOnly(false)
, m_lastDropAction(Qt::IgnoreAction)
, m_jsTimeout(0)
{
WebKit::initializeWebKitWidgets();
initializeWebCorePage();
Expand Down Expand Up @@ -311,6 +312,19 @@ bool QWebPagePrivate::shouldInterruptJavaScript()
return q->shouldInterruptJavaScript();
}

static void setJSTimeout_r(QWebFrame *frame, double timeout)
{
frame->setJSTimeout(timeout);
foreach (QWebFrame *child, frame->childFrames())
setJSTimeout_r(child, timeout);
}

void QWebPagePrivate::setJSTimeout(double timeout)
{
m_jsTimeout = timeout;
setJSTimeout_r(mainFrame.data(), timeout);
}

void QWebPagePrivate::printRequested(QWebFrameAdapter *frame)
{
emit q->printRequested(QWebFramePrivate::kit(frame));
Expand Down Expand Up @@ -405,7 +419,9 @@ void QWebPagePrivate::emitDownloadRequested(const QNetworkRequest &request)

void QWebPagePrivate::emitFrameCreated(QWebFrameAdapter *frame)
{
emit q->frameCreated(QWebFramePrivate::kit(frame));
QWebFrame *pub = QWebFramePrivate::kit(frame);
setJSTimeout_r(pub, m_jsTimeout);
emit q->frameCreated(pub);
}

bool QWebPagePrivate::errorPageExtension(QWebPageAdapter::ErrorPageOption *opt, QWebPageAdapter::ErrorPageReturn *out)
Expand Down Expand Up @@ -454,6 +470,7 @@ void QWebPagePrivate::createMainFrame()
{
if (!mainFrame) {
mainFrame = new QWebFrame(q);
setJSTimeout_r(mainFrame.data(), m_jsTimeout);
emit q->frameCreated(mainFrame.data());
}
}
Expand Down Expand Up @@ -1623,6 +1640,11 @@ bool QWebPage::shouldInterruptJavaScript()
#endif
}

void QWebPage::setJSTimeout(double timeout)
{
d->setJSTimeout(timeout);
}

void QWebPage::setFeaturePermission(QWebFrame* frame, Feature feature, PermissionPolicy policy)
{
#if !ENABLE(NOTIFICATIONS) && !ENABLE(LEGACY_NOTIFICATIONS) && !ENABLE(GEOLOCATION)
Expand Down
1 change: 1 addition & 0 deletions src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebpage.h
Expand Up @@ -388,6 +388,7 @@ class QWEBKITWIDGETS_EXPORT QWebPage : public QObject {

QWebPageAdapter* handle() const;

void setJSTimeout(double timeout);
virtual bool shouldInterruptJavaScript();

Q_SIGNALS:
Expand Down
2 changes: 2 additions & 0 deletions src/qt/qtwebkit/Source/WebKit/qt/WidgetApi/qwebpage_p.h
Expand Up @@ -91,6 +91,7 @@ class QWebPagePrivate : public QWebPageAdapter {
virtual void javaScriptError(const QString& message, int lineNumber, const QString& sourceID, const QString& stack) OVERRIDE;

virtual bool shouldInterruptJavaScript() OVERRIDE;
virtual void setJSTimeout(double timeout) OVERRIDE;
virtual void printRequested(QWebFrameAdapter*) OVERRIDE;
virtual void databaseQuotaExceeded(QWebFrameAdapter*, const QString& databaseName) OVERRIDE;
virtual void applicationCacheQuotaExceeded(QWebSecurityOrigin*, quint64 defaultOriginQuota, quint64 totalSpaceNeeded) OVERRIDE;
Expand Down Expand Up @@ -206,6 +207,7 @@ class QWebPagePrivate : public QWebPageAdapter {
QWebInspector* inspector;
bool inspectorIsInternalOnly; // True if created through the Inspect context menu action
Qt::DropAction m_lastDropAction;
double m_jsTimeout;
};

#endif
4 changes: 4 additions & 0 deletions src/webpage.cpp
Expand Up @@ -637,6 +637,10 @@ void WebPage::applySettings(const QVariantMap& def)
m_networkAccessManager->setResourceTimeout(def[PAGE_SETTINGS_RESOURCE_TIMEOUT].toInt());
}

if (def.contains(PAGE_SETTINGS_SCRIPT_TIMEOUT)) {
m_customWebPage->setJSTimeout(def[PAGE_SETTINGS_SCRIPT_TIMEOUT].toFloat());
}

}

QString WebPage::userAgent() const
Expand Down
10 changes: 3 additions & 7 deletions test/module/webpage/long-running-javascript.js
@@ -1,18 +1,14 @@
async_test(function () {
var page = require('webpage').create();

page.settings.scriptTimeout = 0.25;
page.onLongRunningScript = this.step_func_done(function () {
page.stopJavaScript();
return true;
});

page.open(TEST_HTTP_BASE + "js-infinite-loop.html",
this.step_func(function (s) {
assert_equals(s, "success");
}));

}, "page.onLongRunningScript can interrupt scripts", {
skip: true // https://github.com/ariya/phantomjs/issues/13490
// The underlying WebKit feature is so broken that an
// infinite loop in a _page_ script prevents timeouts
// from firing in the _controller_!
});
}, "page.onLongRunningScript can interrupt scripts");

0 comments on commit 4be9c0c

Please sign in to comment.