Bug 901364 - Hoist slow script prompt machinery into nsGlobalWindow. r=mrbkap

This commit is contained in:
Bobby Holley 2013-08-12 12:54:49 -07:00
parent 8bc140769d
commit 3b168e2acb
3 changed files with 183 additions and 177 deletions

View File

@ -239,6 +239,10 @@
#include "mozilla/dom/SpeechSynthesis.h"
#endif
#ifdef MOZ_JSDEBUGGER
#include "jsdIDebuggerService.h"
#endif
// Apple system headers seem to have a check() macro. <sigh>
#ifdef check
#undef check
@ -9374,6 +9378,164 @@ nsGlobalWindow::HandleIdleActiveEvent()
return NS_OK;
}
nsGlobalWindow::SlowScriptResponse
nsGlobalWindow::ShowSlowScriptDialog()
{
nsresult rv;
AutoJSContext cx;
// If it isn't safe to run script, then it isn't safe to bring up the prompt
// (since that spins the event loop). In that (rare) case, we just kill the
// script and report a warning.
if (!nsContentUtils::IsSafeToRunScript()) {
JS_ReportWarning(cx, "A long running script was terminated");
return KillSlowScript;
}
// Get the nsIPrompt interface from the docshell
nsCOMPtr<nsIDocShell> ds = GetDocShell();
NS_ENSURE_TRUE(ds, KillSlowScript);
nsCOMPtr<nsIPrompt> prompt = do_GetInterface(ds);
NS_ENSURE_TRUE(prompt, KillSlowScript);
// Check if we should offer the option to debug
JS::RootedScript script(cx);
unsigned lineno;
bool hasFrame = JS_DescribeScriptedCaller(cx, script.address(), &lineno);
bool debugPossible = hasFrame && js::CanCallContextDebugHandler(cx);
#ifdef MOZ_JSDEBUGGER
// Get the debugger service if necessary.
if (debugPossible) {
bool jsds_IsOn = false;
const char jsdServiceCtrID[] = "@mozilla.org/js/jsd/debugger-service;1";
nsCOMPtr<jsdIExecutionHook> jsdHook;
nsCOMPtr<jsdIDebuggerService> jsds = do_GetService(jsdServiceCtrID, &rv);
// Check if there's a user for the debugger service that's 'on' for us
if (NS_SUCCEEDED(rv)) {
jsds->GetDebuggerHook(getter_AddRefs(jsdHook));
jsds->GetIsOn(&jsds_IsOn);
}
// If there is a debug handler registered for this runtime AND
// ((jsd is on AND has a hook) OR (jsd isn't on (something else debugs)))
// then something useful will be done with our request to debug.
debugPossible = ((jsds_IsOn && (jsdHook != nullptr)) || !jsds_IsOn);
}
#endif
// Get localizable strings
nsXPIDLString title, msg, stopButton, waitButton, debugButton, neverShowDlg;
rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptTitle",
title);
nsresult tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"StopScriptButton",
stopButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"WaitForScriptButton",
waitButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"DontAskAgain",
neverShowDlg);
if (NS_FAILED(tmp)) {
rv = tmp;
}
if (debugPossible) {
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"DebugScriptButton",
debugButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptWithDebugMessage",
msg);
if (NS_FAILED(tmp)) {
rv = tmp;
}
}
else {
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptMessage",
msg);
if (NS_FAILED(tmp)) {
rv = tmp;
}
}
// GetStringFromName can return NS_OK and still give NULL string
if (NS_FAILED(rv) || !title || !msg || !stopButton || !waitButton ||
(!debugButton && debugPossible) || !neverShowDlg) {
NS_ERROR("Failed to get localized strings.");
return ContinueSlowScript;
}
// Append file and line number information, if available
if (script) {
const char *filename = JS_GetScriptFilename(cx, script);
if (filename) {
nsXPIDLString scriptLocation;
NS_ConvertUTF8toUTF16 filenameUTF16(filename);
const PRUnichar *formatParams[] = { filenameUTF16.get() };
rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptLocation",
formatParams,
scriptLocation);
if (NS_SUCCEEDED(rv) && scriptLocation) {
msg.AppendLiteral("\n\n");
msg.Append(scriptLocation);
msg.Append(':');
msg.AppendInt(lineno);
}
}
}
int32_t buttonPressed = 0; // In case the user exits dialog by clicking X.
bool neverShowDlgChk = false;
uint32_t buttonFlags = nsIPrompt::BUTTON_POS_1_DEFAULT +
(nsIPrompt::BUTTON_TITLE_IS_STRING *
(nsIPrompt::BUTTON_POS_0 + nsIPrompt::BUTTON_POS_1));
// Add a third button if necessary.
if (debugPossible)
buttonFlags += nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2;
// Null out the operation callback while we're re-entering JS here.
JSOperationCallback old = JS_SetOperationCallback(cx, nullptr);
// Open the dialog.
rv = prompt->ConfirmEx(title, msg, buttonFlags, waitButton, stopButton,
debugButton, neverShowDlg, &neverShowDlgChk,
&buttonPressed);
JS_SetOperationCallback(cx, old);
if (NS_SUCCEEDED(rv) && (buttonPressed == 0)) {
return neverShowDlgChk ? AlwaysContinueSlowScript : ContinueSlowScript;
}
if ((buttonPressed == 2) && debugPossible) {
return js_CallContextDebugHandler(cx) ? ContinueSlowScript : KillSlowScript;
}
JS_ClearPendingException(cx);
return KillSlowScript;
}
uint32_t
nsGlobalWindow::FindInsertionIndex(IdleObserverHolder* aIdleObserver)
{

View File

@ -692,6 +692,13 @@ public:
mAllowScriptsToClose = true;
}
enum SlowScriptResponse {
ContinueSlowScript = 0,
AlwaysContinueSlowScript,
KillSlowScript
};
SlowScriptResponse ShowSlowScriptDialog();
#ifdef MOZ_GAMEPAD
void AddGamepad(uint32_t aIndex, mozilla::dom::Gamepad* aGamepad);
void RemoveGamepad(uint32_t aIndex);

View File

@ -679,25 +679,9 @@ DumpString(const nsAString &str)
}
#endif
static already_AddRefed<nsIPrompt>
GetPromptFromContext(nsJSContext* ctx)
{
nsCOMPtr<nsPIDOMWindow> win(do_QueryInterface(ctx->GetGlobalObject()));
NS_ENSURE_TRUE(win, nullptr);
nsIDocShell *docShell = win->GetDocShell();
NS_ENSURE_TRUE(docShell, nullptr);
// Get the nsIPrompt interface from the docshell
nsCOMPtr<nsIPrompt> prompt = do_GetInterface(docShell);
return prompt.forget();
}
bool
nsJSContext::DOMOperationCallback(JSContext *cx)
{
nsresult rv;
// Get the native context
nsJSContext *ctx = static_cast<nsJSContext *>(::JS_GetContextPrivate(cx));
@ -743,172 +727,25 @@ nsJSContext::DOMOperationCallback(JSContext *cx)
return true;
}
if (!nsContentUtils::IsSafeToRunScript()) {
// If it isn't safe to run script, then it isn't safe to bring up the
// prompt (since that will cause the event loop to spin). In this case
// (which is rare), we just stop the script... But report a warning so
// that developers have some idea of what went wrong.
nsCOMPtr<nsPIDOMWindow> domWin = do_QueryInterface(ctx->GetGlobalObject());
NS_ENSURE_TRUE(domWin, false);
nsGlobalWindow::SlowScriptResponse response =
static_cast<nsGlobalWindow*>(domWin.get())->ShowSlowScriptDialog();
JS_ReportWarning(cx, "A long running script was terminated");
if (response == nsGlobalWindow::KillSlowScript) {
return false;
}
// If we get here we're most likely executing an infinite loop in JS,
// we'll tell the user about this and we'll give the user the option
// of stopping the execution of the script.
nsCOMPtr<nsIPrompt> prompt = GetPromptFromContext(ctx);
NS_ENSURE_TRUE(prompt, false);
// Check if we should offer the option to debug
JS::RootedScript script(cx);
unsigned lineno;
bool hasFrame = ::JS_DescribeScriptedCaller(cx, script.address(), &lineno);
bool debugPossible = hasFrame && js::CanCallContextDebugHandler(cx);
#ifdef MOZ_JSDEBUGGER
// Get the debugger service if necessary.
if (debugPossible) {
bool jsds_IsOn = false;
const char jsdServiceCtrID[] = "@mozilla.org/js/jsd/debugger-service;1";
nsCOMPtr<jsdIExecutionHook> jsdHook;
nsCOMPtr<jsdIDebuggerService> jsds = do_GetService(jsdServiceCtrID, &rv);
// Check if there's a user for the debugger service that's 'on' for us
if (NS_SUCCEEDED(rv)) {
jsds->GetDebuggerHook(getter_AddRefs(jsdHook));
jsds->GetIsOn(&jsds_IsOn);
}
// If there is a debug handler registered for this runtime AND
// ((jsd is on AND has a hook) OR (jsd isn't on (something else debugs)))
// then something useful will be done with our request to debug.
debugPossible = ((jsds_IsOn && (jsdHook != nullptr)) || !jsds_IsOn);
}
#endif
// Get localizable strings
nsXPIDLString title, msg, stopButton, waitButton, debugButton, neverShowDlg;
rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptTitle",
title);
nsresult tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"StopScriptButton",
stopButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"WaitForScriptButton",
waitButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"DontAskAgain",
neverShowDlg);
if (NS_FAILED(tmp)) {
rv = tmp;
}
if (debugPossible) {
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"DebugScriptButton",
debugButton);
if (NS_FAILED(tmp)) {
rv = tmp;
}
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptWithDebugMessage",
msg);
if (NS_FAILED(tmp)) {
rv = tmp;
ctx->mOperationCallbackTime = PR_Now();
if (response == nsGlobalWindow::AlwaysContinueSlowScript) {
if (isTrackingChromeCodeTime) {
Preferences::SetInt("dom.max_chrome_script_run_time", 0);
sMaxChromeScriptRunTime = NS_UNLIMITED_SCRIPT_RUNTIME;
} else {
Preferences::SetInt("dom.max_script_run_time", 0);
sMaxScriptRunTime = NS_UNLIMITED_SCRIPT_RUNTIME;
}
}
else {
tmp = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptMessage",
msg);
if (NS_FAILED(tmp)) {
rv = tmp;
}
}
//GetStringFromName can return NS_OK and still give NULL string
if (NS_FAILED(rv) || !title || !msg || !stopButton || !waitButton ||
(!debugButton && debugPossible) || !neverShowDlg) {
NS_ERROR("Failed to get localized strings.");
return true;
}
// Append file and line number information, if available
if (script) {
const char *filename = ::JS_GetScriptFilename(cx, script);
if (filename) {
nsXPIDLString scriptLocation;
NS_ConvertUTF8toUTF16 filenameUTF16(filename);
const PRUnichar *formatParams[] = { filenameUTF16.get() };
rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"KillScriptLocation",
formatParams,
scriptLocation);
if (NS_SUCCEEDED(rv) && scriptLocation) {
msg.AppendLiteral("\n\n");
msg.Append(scriptLocation);
msg.Append(':');
msg.AppendInt(lineno);
}
}
}
int32_t buttonPressed = 0; //In case user exits dialog by clicking X
bool neverShowDlgChk = false;
uint32_t buttonFlags = nsIPrompt::BUTTON_POS_1_DEFAULT +
(nsIPrompt::BUTTON_TITLE_IS_STRING *
(nsIPrompt::BUTTON_POS_0 + nsIPrompt::BUTTON_POS_1));
// Add a third button if necessary:
if (debugPossible)
buttonFlags += nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2;
// Null out the operation callback while we're re-entering JS here.
::JS_SetOperationCallback(cx, nullptr);
// Open the dialog.
rv = prompt->ConfirmEx(title, msg, buttonFlags, waitButton, stopButton,
debugButton, neverShowDlg, &neverShowDlgChk,
&buttonPressed);
::JS_SetOperationCallback(cx, DOMOperationCallback);
if (NS_SUCCEEDED(rv) && (buttonPressed == 0)) {
// Allow the script to continue running
if (neverShowDlgChk) {
if (isTrackingChromeCodeTime) {
Preferences::SetInt("dom.max_chrome_script_run_time", 0);
sMaxChromeScriptRunTime = NS_UNLIMITED_SCRIPT_RUNTIME;
} else {
Preferences::SetInt("dom.max_script_run_time", 0);
sMaxScriptRunTime = NS_UNLIMITED_SCRIPT_RUNTIME;
}
}
ctx->mOperationCallbackTime = PR_Now();
return true;
}
else if ((buttonPressed == 2) && debugPossible) {
return js_CallContextDebugHandler(cx);
}
JS_ClearPendingException(cx);
return false;
return true;
}
void