Web Inspector: implement blackboxing of script resources
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Sep 2019 02:26:37 +0000 (02:26 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Sep 2019 02:26:37 +0000 (02:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=17240
<rdar://problem/5732847>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

When a script is blackboxed and the debugger attempts to pause in that script, the pause
reason/data will be saved and execution will continue until it has left the blackboxed
script. Once outside, execution is paused with the saved reason/data.

This is especially useful when debugging issues using libraries/frameworks, as it allows the
developer to "skip" the internal logic of the library/framework and instead focus only on
how they're using it.

* inspector/protocol/Debugger.json:
Add `setShouldBlackboxURL` command.

* inspector/agents/InspectorDebuggerAgent.h:
* inspector/agents/InspectorDebuggerAgent.cpp:
(Inspector::InspectorDebuggerAgent):
(Inspector::InspectorDebuggerAgent::enable):
(Inspector::InspectorDebuggerAgent::updatePauseReasonAndData): Added.
(Inspector::InspectorDebuggerAgent::schedulePauseOnNextStatement):
(Inspector::InspectorDebuggerAgent::cancelPauseOnNextStatement):
(Inspector::InspectorDebuggerAgent::setShouldBlackboxURL): Added.
(Inspector::InspectorDebuggerAgent::setPauseForInternalScripts):
(Inspector::InspectorDebuggerAgent::didParseSource):
(Inspector::InspectorDebuggerAgent::didPause):
(Inspector::InspectorDebuggerAgent::didContinue):
(Inspector::InspectorDebuggerAgent::breakProgram):
(Inspector::InspectorDebuggerAgent::clearDebuggerBreakpointState):
(Inspector::InspectorDebuggerAgent::clearPauseDetails): Added.
(Inspector::InspectorDebuggerAgent::clearBreakDetails): Deleted.
Renamed "break" to "pause" to match `Debugger` naming.

* debugger/Debugger.h:
* debugger/Debugger.cpp:
(JSC::Debugger::pauseIfNeeded):
(JSC::Debugger::setBlackboxType): Added.
(JSC::Debugger::clearBlackbox): Added.
(JSC::Debugger::isBlacklisted const): Deleted.
(JSC::Debugger::addToBlacklist): Deleted.
(JSC::Debugger::clearBlacklist): Deleted.

Source/WebInspectorUI:

When a script is blackboxed and the debugger attempts to pause in that script, the pause
reason/data will be saved and execution will continue until it has left the blackboxed
script. Once outside, execution is paused with the saved reason/data.

This is especially useful when debugging issues using libraries/frameworks, as it allows the
developer to "skip" the internal logic of the library/framework and instead focus only on
how they're using it.

* UserInterface/Controllers/DebuggerManager.js:
(WI.DebuggerManager):
(WI.DebuggerManager.prototype.initializeTarget):
(WI.DebuggerManager.supportsBlackboxingScripts): Added.
(WI.DebuggerManager.pauseReasonFromPayload): Added.
(WI.DebuggerManager.prototype.isScriptBlackboxed): Added.
(WI.DebuggerManager.prototype.setShouldBlackboxScript): Added.
(WI.DebuggerManager.prototype._pauseReasonFromPayload):
(WI.DebuggerManager.prototype._pauseReasonFromPayload): Deleted.

* UserInterface/Models/SourceCode.js:
(WI.SourceCode.prototype.get isScript): Added.
(WI.SourceCode.prototype.get supportsScriptBlackboxing): Added.
* UserInterface/Models/Script.js:
(WI.Script.prototype.get isScript): Added.
* UserInterface/Models/Resource.js:
(WI.Resource.prototype.get isScript): Added.
Provide a more straightforward way of determining if a `WI.SourceCode` is a script.

* UserInterface/Views/DebuggerSidebarPanel.js:
(WI.DebuggerSidebarPanel.prototype._updatePauseReason):
(WI.DebuggerSidebarPanel.prototype._updatePauseReasonSection):
* UserInterface/Views/SourcesNavigationSidebarPanel.js:
(WI.SourcesNavigationSidebarPanel.prototype._updatePauseReason):
(WI.SourcesNavigationSidebarPanel.prototype._updatePauseReasonSection):
Display the original pause reason and breakpoint (if applicable) when pausing after leaving
a blackboxed script.

* UserInterface/Views/SourceCodeTreeElement.js:
(WI.SourceCodeTreeElement.prototype.canSelectOnMouseDown): Added.
(WI.SourceCodeTreeElement.prototype.updateStatus): Added.
(WI.SourceCodeTreeElement.prototype._updateSourceCode):
(WI.SourceCodeTreeElement.prototype._updateToggleBlackboxImageElementState): Added.
(WI.SourceCodeTreeElement.prototype._handleToggleBlackboxedImageElementClick): Added.
* UserInterface/Views/SourceCodeTreeElement.css: Added.
(.tree-outline .item .status > .toggle-script-blackboxed):
(.tree-outline:not(.navigation-sidebar-panel-content-tree-outline) .item .status > .toggle-script-blackboxed,):
(.tree-outline:focus .item.selected .status > .toggle-script-blackboxed):
(.tree-outline .item .status > .toggle-script-blackboxed.blackboxed):
(@media (prefers-color-scheme: dark) .tree-outline .item .status > .toggle-script-blackboxed):

* UserInterface/Views/ResourceTreeElement.js:
(WI.ResourceTreeElement.prototype._updateResource):
(WI.ResourceTreeElement.prototype.updateStatus): Added.
(WI.ResourceTreeElement.prototype._updateStatus): Deleted.
Make sure that the loading indicator doesn't override the blackbox toggle.

* UserInterface/Base/Setting.js:
(WI.Setting.prototype.set value):
(WI.Setting.prototype.save): Added.
When modifying an array value, that doesn't go through `WI.Setting.prototype.set value`, so
we need a more "manual" way of saving the new value.

* UserInterface/Main.html:
* Localizations/en.lproj/localizedStrings.js:

* UserInterface/Test/TestHarness.js:
(TestHarness.prototype.newline): Added.
(TestHarness.prototype.expectException):
Add a special case for logging error message objects when running protocol tests.

LayoutTests:

* inspector/debugger/setShouldBlackboxURL.html: Added.
* inspector/debugger/setShouldBlackboxURL-expected.txt: Added.

* inspector/model/remote-object-api-expected.txt:
Update output since `WI.TestHarness.prototype.expectException` now logs the `.constructor.name`.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@249450 268f45cc-cd09-0410-ab3c-d52691b4dbfc

24 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/debugger/setShouldBlackboxURL-expected.txt [new file with mode: 0644]
LayoutTests/inspector/debugger/setShouldBlackboxURL.html [new file with mode: 0644]
LayoutTests/inspector/model/remote-object-api-expected.txt
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/debugger/Debugger.cpp
Source/JavaScriptCore/debugger/Debugger.h
Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.h
Source/JavaScriptCore/inspector/protocol/Debugger.json
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Setting.js
Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Resource.js
Source/WebInspectorUI/UserInterface/Models/Script.js
Source/WebInspectorUI/UserInterface/Models/SourceCode.js
Source/WebInspectorUI/UserInterface/Test/TestHarness.js
Source/WebInspectorUI/UserInterface/Views/DebuggerSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/ResourceTreeElement.js
Source/WebInspectorUI/UserInterface/Views/SourceCodeTreeElement.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/SourceCodeTreeElement.js
Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js

index 6a9e1aa..7f0c2c2 100644 (file)
@@ -1,5 +1,19 @@
 2019-09-03  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: implement blackboxing of script resources
+        https://bugs.webkit.org/show_bug.cgi?id=17240
+        <rdar://problem/5732847>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/debugger/setShouldBlackboxURL.html: Added.
+        * inspector/debugger/setShouldBlackboxURL-expected.txt: Added.
+
+        * inspector/model/remote-object-api-expected.txt:
+        Update output since `WI.TestHarness.prototype.expectException` now logs the `.constructor.name`.
+
+2019-09-03  Devin Rousso  <drousso@apple.com>
+
         REGRESSION (r249132): [macOS WK2] Layout Test inspector/layers/layerTreeDidChange.html is a flaky failure
         https://bugs.webkit.org/show_bug.cgi?id=201435
 
diff --git a/LayoutTests/inspector/debugger/setShouldBlackboxURL-expected.txt b/LayoutTests/inspector/debugger/setShouldBlackboxURL-expected.txt
new file mode 100644 (file)
index 0000000..c68ac18
--- /dev/null
@@ -0,0 +1,105 @@
+Tests Debugger.setShouldBlackboxURL.
+
+
+== Running test suite: Debugger.setShouldBlackboxURL
+-- Running test case: Debugger.setShouldBlackboxURL.stepOver
+Evaluating 'createScripts("stepOver")'...
+Blackboxing 'stepOverMiddle.js'...
+Setting breakpoint in 'stepOverInner.js'...
+Evaluating 'stepOverOuter(10)'...
+
+Paused in 'stepOverInner:3:1'.
+Reason: 'Breakpoint'
+{
+  "breakpointId": "stepOverInner.js:3:0"
+}
+Stepping over...
+
+Paused in 'stepOverOuter:3:1'.
+Reason: 'BlackboxedScript'
+{
+  "originalReason": "other",
+  "originalData": {
+    "breakpointId": "stepOverInner.js:3:0"
+  }
+}
+Stepping over...
+
+Resuming...
+PASS: Resumed.
+PASS: Should not pause in 'stepOverMiddle'.
+
+-- Running test case: Debugger.setShouldBlackboxURL.PauseInCaller
+Evaluating 'createScripts("pauseInCaller")'...
+Blackboxing 'pauseInCallerInner.js'...
+Setting breakpoint in 'pauseInCallerInner.js'...
+Evaluating 'pauseInCallerOuter(10)'...
+
+Paused in 'pauseInCallerMiddle:3:1'.
+Reason: 'BlackboxedScript'
+{
+  "originalReason": "Breakpoint",
+  "originalData": {
+    "breakpointId": "pauseInCallerInner.js:2:0"
+  }
+}
+Stepping over...
+
+Paused in 'pauseInCallerOuter:3:1'.
+Reason: 'other'
+Stepping over...
+
+Resuming...
+PASS: Resumed.
+PASS: Should not pause in 'pauseInCallerInner'.
+
+-- Running test case: Debugger.setShouldBlackboxURL.PauseInCallee
+Evaluating 'createScripts("pauseInCallee")'...
+Blackboxing 'pauseInCalleeOuter.js'...
+Setting breakpoint in 'pauseInCalleeOuter.js'...
+Evaluating 'pauseInCalleeOuter(10)'...
+
+Paused in 'pauseInCalleeMiddle:2:4'.
+Reason: 'BlackboxedScript'
+{
+  "originalReason": "Breakpoint",
+  "originalData": {
+    "breakpointId": "pauseInCalleeOuter.js:2:0"
+  }
+}
+Stepping over...
+
+Paused in 'pauseInCalleeMiddle:3:1'.
+Reason: 'other'
+Stepping over...
+
+Resuming...
+PASS: Resumed.
+PASS: Should not pause in 'pauseInCalleeOuter'.
+
+-- Running test case: Debugger.setShouldBlackboxURL.Invalid.emptyURL
+PASS: Should produce an exception.
+{
+  "code": -32000,
+  "message": "URL must not be empty",
+  "data": [
+    {
+      "code": -32000,
+      "message": "URL must not be empty"
+    }
+  ]
+}
+
+-- Running test case: Debugger.setShouldBlackboxURL.Invalid.injectedScript
+PASS: Should produce an exception.
+{
+  "code": -32000,
+  "message": "Blackboxing of internal scripts is controlled by 'Debugger.setPauseForInternalScripts'",
+  "data": [
+    {
+      "code": -32000,
+      "message": "Blackboxing of internal scripts is controlled by 'Debugger.setPauseForInternalScripts'"
+    }
+  ]
+}
+
diff --git a/LayoutTests/inspector/debugger/setShouldBlackboxURL.html b/LayoutTests/inspector/debugger/setShouldBlackboxURL.html
new file mode 100644 (file)
index 0000000..9558f75
--- /dev/null
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/protocol-test.js"></script>
+<script>
+function createScripts(id) {
+    eval(
+`
+window.${id}Inner = function ${id}Inner(x) {
+    return x + 42;
+};
+//# sourceURL=${id}Inner.js
+`
+    );
+
+    eval(
+`
+window.${id}Middle = function ${id}Middle(x) {
+    return ${id}Inner(x);
+};
+//# sourceURL=${id}Middle.js
+`
+    );
+
+    eval(
+`
+window.${id}Outer = function ${id}Outer(x) {
+    return ${id}Middle(x);
+};
+//# sourceURL=${id}Outer.js
+`
+    );
+}
+
+function test()
+{
+    let suite = ProtocolTest.createAsyncSuite("Debugger.setShouldBlackboxURL");
+
+    let sourceURLRegExpQueries = new Map;
+    let pausedFunctionNames = [];
+    let resumeCallback = null;
+
+    InspectorProtocol.sendCommand("Debugger.enable", {});
+    InspectorProtocol.sendCommand("Debugger.setBreakpointsActive", {active: true});
+
+    InspectorProtocol.eventHandler["Debugger.scriptParsed"] = function(message) {
+        let sourceURL = message.params.sourceURL;
+        for (let [regExp, callback] of sourceURLRegExpQueries) {
+            if (regExp.test(sourceURL)) {
+                sourceURLRegExpQueries.delete(regExp);
+                callback(sourceURL);
+            }
+        };
+    };
+
+    InspectorProtocol.eventHandler["Debugger.paused"] = function(message) {
+        let topCallFrame = message.params.callFrames[0];
+        let functionName = topCallFrame.functionName;
+        if (functionName === "global code") {
+            ProtocolTest.log("Resuming...");
+            InspectorProtocol.sendCommand(`Debugger.resume`, {}, InspectorProtocol.checkForError);
+            return;
+        }
+
+        ProtocolTest.log(`Paused in '${functionName}:${topCallFrame.location.lineNumber}:${topCallFrame.location.columnNumber}'.`);
+        ProtocolTest.log(`Reason: '${message.params.reason}'`);
+        if (message.params.data)
+            ProtocolTest.json(message.params.data);
+        pausedFunctionNames.push(functionName);
+
+        ProtocolTest.log("Stepping over...");
+        ProtocolTest.newline();
+        InspectorProtocol.sendCommand(`Debugger.stepOver`, {}, InspectorProtocol.checkForError);
+    };
+
+    InspectorProtocol.eventHandler["Debugger.resumed"] = function(message) {
+        ProtocolTest.pass("Resumed.");
+        resumeCallback();
+    };
+
+    async function setBlackbox(url) {
+        ProtocolTest.log(`Blackboxing '${url}'...`);
+        await InspectorProtocol.awaitCommand({
+            method: "Debugger.setShouldBlackboxURL",
+            params: {url, shouldBlackbox: true},
+        });
+    }
+
+    async function setBreakpoint(url, lineNumber) {
+        ProtocolTest.log(`Setting breakpoint in '${url}'...`);
+        await InspectorProtocol.awaitCommand({
+            method: "Debugger.setBreakpointByUrl",
+            params: {url, lineNumber},
+        });
+    }
+
+    async function listenForSourceParsed(sourceURLRegExp) {
+        return new Promise((resolve, reject) => {
+            sourceURLRegExpQueries.set(sourceURLRegExp, resolve);
+        });
+    }
+
+    async function evaluate(expression) {
+        ProtocolTest.log(`Evaluating '${expression}'...`);
+        return InspectorProtocol.awaitCommand({
+            method: "Runtime.evaluate",
+            params: {expression},
+        });
+    }
+
+    suite.addTestCase({
+        name: "Debugger.setShouldBlackboxURL.stepOver",
+        description: "Check that stepping through a blackboxed script doesn't pause.",
+        async test() {
+            let resumePromise = new Promise((resolve, reject) => {
+                resumeCallback = function() {
+                    ProtocolTest.expectThat(!pausedFunctionNames.includes("stepOverMiddle"), "Should not pause in 'stepOverMiddle'.");
+                    resolve();
+                };
+            });
+
+            let [stepOverInnerSourceURL, stepOverMiddleSourceURL] = await Promise.all([
+                listenForSourceParsed(/stepOverInner\.js$/),
+                listenForSourceParsed(/stepOverMiddle\.js$/),
+                listenForSourceParsed(/stepOverOuter\.js$/),
+                evaluate(`createScripts("stepOver")`),
+            ]);
+
+            await setBlackbox(stepOverMiddleSourceURL);
+            await setBreakpoint(stepOverInnerSourceURL, 3); // last line of function, so it only pauses once
+            evaluate(`stepOverOuter(10)`);
+
+            ProtocolTest.newline();
+
+            await resumePromise;
+        },
+    });
+
+    suite.addTestCase({
+        name: "Debugger.setShouldBlackboxURL.PauseInCaller",
+        description: "Check that the debugger will pause in the caller if a breakpoint is set in a blackboxed script.",
+        async test() {
+            let resumePromise = new Promise((resolve, reject) => {
+                resumeCallback = function() {
+                    ProtocolTest.expectThat(!pausedFunctionNames.includes("pauseInCallerInner"), "Should not pause in 'pauseInCallerInner'.");
+                    resolve();
+                };
+            });
+
+            let [pauseInCallerInnerSourceURL] = await Promise.all([
+                listenForSourceParsed(/pauseInCallerInner\.js$/),
+                listenForSourceParsed(/pauseInCallerMiddle\.js$/),
+                listenForSourceParsed(/pauseInCallerOuter\.js$/),
+                evaluate(`createScripts("pauseInCaller")`),
+            ]);
+
+            await setBlackbox(pauseInCallerInnerSourceURL);
+            await setBreakpoint(pauseInCallerInnerSourceURL, 2);
+            evaluate(`pauseInCallerOuter(10)`);
+
+            ProtocolTest.newline();
+
+            await resumePromise;
+        },
+    });
+
+    suite.addTestCase({
+        name: "Debugger.setShouldBlackboxURL.PauseInCallee",
+        description: "Check that the debugger will pause in the callee if a breakpoint is set in a blackboxed script.",
+        async test() {
+            let resumePromise = new Promise((resolve, reject) => {
+                resumeCallback = function() {
+                    ProtocolTest.expectThat(!pausedFunctionNames.includes("pauseInCalleeOuter"), "Should not pause in 'pauseInCalleeOuter'.");
+                    resolve();
+                };
+            });
+
+            let [pauseInCalleeInnerSourceURL, pauseInCalleeMiddleSourceURL, pauseInCalleeOuterSourceURL] = await Promise.all([
+                listenForSourceParsed(/pauseInCalleeInner\.js$/),
+                listenForSourceParsed(/pauseInCalleeMiddle\.js$/),
+                listenForSourceParsed(/pauseInCalleeOuter\.js$/),
+                evaluate(`createScripts("pauseInCallee")`),
+            ]);
+
+            await setBlackbox(pauseInCalleeOuterSourceURL);
+            await setBreakpoint(pauseInCalleeOuterSourceURL, 2);
+            evaluate(`pauseInCalleeOuter(10)`);
+
+            ProtocolTest.newline();
+
+            await resumePromise;
+        },
+    });
+
+    suite.addTestCase({
+        name: "Debugger.setShouldBlackboxURL.Invalid.emptyURL",
+        description: "Check that an error is thrown if the given url is empty.",
+        async test() {
+            await ProtocolTest.expectException(async () => {
+                await InspectorProtocol.awaitCommand({
+                    method: "Debugger.setShouldBlackboxURL",
+                    params: {url: "", shouldBlackbox: true},
+                });
+            });
+        },
+    });
+
+    suite.addTestCase({
+        name: "Debugger.setShouldBlackboxURL.Invalid.injectedScript",
+        description: "Check that an error is thrown if the given url matches an injected script url.",
+        async test() {
+            await ProtocolTest.expectException(async () => {
+                await InspectorProtocol.awaitCommand({
+                    method: "Debugger.setShouldBlackboxURL",
+                    params: {url: "__InjectedScript__test.js", shouldBlackbox: true},
+                });
+            });
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Tests Debugger.setShouldBlackboxURL.</p>
+</body>
+</html>
index 3cc7933..9581e64 100644 (file)
@@ -33,7 +33,7 @@ PASS: Fetched property value should be as expected.
 
 -- Running test case: RemoteObject.getProperty.FailureWithPromise
 PASS: Should produce an exception.
-[object Object]
+RemoteObject
 
 -- Running test case: RemoteObject.fetchProperties.Success
 PASS: Result object should contain three keys.
index 8bf8d92..35c75f3 100644 (file)
@@ -1,3 +1,49 @@
+2019-09-03  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: implement blackboxing of script resources
+        https://bugs.webkit.org/show_bug.cgi?id=17240
+        <rdar://problem/5732847>
+
+        Reviewed by Joseph Pecoraro.
+
+        When a script is blackboxed and the debugger attempts to pause in that script, the pause
+        reason/data will be saved and execution will continue until it has left the blackboxed
+        script. Once outside, execution is paused with the saved reason/data.
+
+        This is especially useful when debugging issues using libraries/frameworks, as it allows the
+        developer to "skip" the internal logic of the library/framework and instead focus only on
+        how they're using it.
+
+        * inspector/protocol/Debugger.json:
+        Add `setShouldBlackboxURL` command.
+
+        * inspector/agents/InspectorDebuggerAgent.h:
+        * inspector/agents/InspectorDebuggerAgent.cpp:
+        (Inspector::InspectorDebuggerAgent):
+        (Inspector::InspectorDebuggerAgent::enable):
+        (Inspector::InspectorDebuggerAgent::updatePauseReasonAndData): Added.
+        (Inspector::InspectorDebuggerAgent::schedulePauseOnNextStatement):
+        (Inspector::InspectorDebuggerAgent::cancelPauseOnNextStatement):
+        (Inspector::InspectorDebuggerAgent::setShouldBlackboxURL): Added.
+        (Inspector::InspectorDebuggerAgent::setPauseForInternalScripts):
+        (Inspector::InspectorDebuggerAgent::didParseSource):
+        (Inspector::InspectorDebuggerAgent::didPause):
+        (Inspector::InspectorDebuggerAgent::didContinue):
+        (Inspector::InspectorDebuggerAgent::breakProgram):
+        (Inspector::InspectorDebuggerAgent::clearDebuggerBreakpointState):
+        (Inspector::InspectorDebuggerAgent::clearPauseDetails): Added.
+        (Inspector::InspectorDebuggerAgent::clearBreakDetails): Deleted.
+        Renamed "break" to "pause" to match `Debugger` naming.
+
+        * debugger/Debugger.h:
+        * debugger/Debugger.cpp:
+        (JSC::Debugger::pauseIfNeeded):
+        (JSC::Debugger::setBlackboxType): Added.
+        (JSC::Debugger::clearBlackbox): Added.
+        (JSC::Debugger::isBlacklisted const): Deleted.
+        (JSC::Debugger::addToBlacklist): Deleted.
+        (JSC::Debugger::clearBlacklist): Deleted.
+
 2019-09-03  Mark Lam  <mark.lam@apple.com>
 
         Remove the need to pass performJITMemcpy as a pointer.
index 8133513..0446623 100644 (file)
@@ -695,7 +695,9 @@ void Debugger::pauseIfNeeded(CallFrame* callFrame)
         return;
 
     intptr_t sourceID = DebuggerCallFrame::sourceIDForCallFrame(m_currentCallFrame);
-    if (isBlacklisted(sourceID))
+
+    auto blackboxTypeIterator = m_blackboxedScripts.find(sourceID);
+    if (blackboxTypeIterator != m_blackboxedScripts.end() && blackboxTypeIterator->value == BlackboxType::Ignored)
         return;
 
     DebuggerPausedScope debuggerPausedScope(*this);
@@ -713,6 +715,7 @@ void Debugger::pauseIfNeeded(CallFrame* callFrame)
     if (!pauseNow)
         return;
 
+    bool afterBlackboxedScript = m_afterBlackboxedScript;
     clearNextPauseState();
 
     // Make sure we are not going to pause again on breakpoint actions by
@@ -736,8 +739,20 @@ void Debugger::pauseIfNeeded(CallFrame* callFrame)
             m_pausingBreakpointID = breakpoint.id;
     }
 
+    if (blackboxTypeIterator != m_blackboxedScripts.end() && blackboxTypeIterator->value == BlackboxType::Deferred) {
+        m_afterBlackboxedScript = true;
+        setPauseOnNextStatement(true);
+        return;
+    }
+
     {
-        PauseReasonDeclaration reason(*this, didHitBreakpoint ? PausedForBreakpoint : m_reasonForPause);
+        auto reason = m_reasonForPause;
+        if (afterBlackboxedScript)
+            reason = PausedAfterBlackboxedScript;
+        else if (didHitBreakpoint)
+            reason = PausedForBreakpoint;
+        PauseReasonDeclaration rauseReasonDeclaration(*this, reason);
+
         handlePause(vmEntryGlobalObject, m_reasonForPause);
         scope.releaseAssertNoException();
     }
@@ -908,6 +923,7 @@ void Debugger::clearNextPauseState()
     m_pauseOnCallFrame = nullptr;
     m_pauseAtNextOpportunity = false;
     m_pauseOnStepOut = false;
+    m_afterBlackboxedScript = false;
 }
 
 void Debugger::didReachBreakpoint(CallFrame* callFrame)
@@ -928,19 +944,17 @@ DebuggerCallFrame& Debugger::currentDebuggerCallFrame()
     return *m_currentDebuggerCallFrame;
 }
 
-bool Debugger::isBlacklisted(SourceID sourceID) const
+void Debugger::setBlackboxType(SourceID sourceID, Optional<BlackboxType> type)
 {
-    return m_blacklistedScripts.contains(sourceID);
-}
-
-void Debugger::addToBlacklist(SourceID sourceID)
-{
-    m_blacklistedScripts.add(sourceID);
+    if (type)
+        m_blackboxedScripts.set(sourceID, type.value());
+    else
+        m_blackboxedScripts.remove(sourceID);
 }
 
-void Debugger::clearBlacklist()
+void Debugger::clearBlackbox()
 {
-    m_blacklistedScripts.clear();
+    m_blackboxedScripts.clear();
 }
 
 } // namespace JSC
index ab8bea8..31a6e0d 100644 (file)
@@ -100,6 +100,7 @@ public:
         PausedAtEndOfProgram,
         PausedForBreakpoint,
         PausedForDebuggerStatement,
+        PausedAfterBlackboxedScript,
     };
     ReasonForPause reasonForPause() const { return m_reasonForPause; }
     BreakpointID pausingBreakpointID() const { return m_pausingBreakpointID; }
@@ -111,9 +112,9 @@ public:
     void stepOverStatement();
     void stepOutOfFunction();
 
-    bool isBlacklisted(SourceID) const;
-    void addToBlacklist(SourceID);
-    void clearBlacklist();
+    enum class BlackboxType { Deferred, Ignored };
+    void setBlackboxType(SourceID, Optional<BlackboxType>);
+    void clearBlackbox();
 
     bool isPaused() const { return m_isPaused; }
     bool isStepping() const { return m_steppingMode == SteppingModeEnabled; }
@@ -224,7 +225,7 @@ private:
     VM& m_vm;
     HashSet<JSGlobalObject*> m_globalObjects;
     HashMap<SourceID, DebuggerParseData, WTF::IntHash<SourceID>, WTF::UnsignedWithZeroKeyHashTraits<SourceID>> m_parseDataMap;
-    HashSet<SourceID, WTF::IntHash<SourceID>, WTF::UnsignedWithZeroKeyHashTraits<SourceID>> m_blacklistedScripts;
+    HashMap<SourceID, BlackboxType, WTF::IntHash<SourceID>, WTF::UnsignedWithZeroKeyHashTraits<SourceID>> m_blackboxedScripts;
 
     PauseOnExceptionsState m_pauseOnExceptionsState;
     bool m_pauseAtNextOpportunity : 1;
@@ -242,6 +243,7 @@ private:
     CallFrame* m_currentCallFrame { nullptr };
     unsigned m_lastExecutedLine;
     SourceID m_lastExecutedSourceID;
+    bool m_afterBlackboxedScript { false };
 
     BreakpointID m_topBreakpointID;
     BreakpointID m_pausingBreakpointID;
index 8f5e96b..82a005a 100644 (file)
@@ -41,6 +41,7 @@
 #include "ScriptCallStackFactory.h"
 #include "ScriptDebugServer.h"
 #include "ScriptObject.h"
+#include <wtf/Function.h>
 #include <wtf/JSONValues.h>
 #include <wtf/NeverDestroyed.h>
 #include <wtf/Stopwatch.h>
@@ -60,6 +61,11 @@ static String objectGroupForBreakpointAction(const ScriptBreakpointAction& actio
     return makeString("breakpoint-action-", action.identifier);
 }
 
+static bool isWebKitInjectedScript(const String& sourceURL)
+{
+    return sourceURL.startsWith("__InjectedScript_") && sourceURL.endsWith(".js");
+}
+
 InspectorDebuggerAgent::InspectorDebuggerAgent(AgentContext& context)
     : InspectorAgentBase("Debugger"_s)
     , m_frontendDispatcher(makeUnique<DebuggerFrontendDispatcher>(context.frontendRouter))
@@ -67,8 +73,8 @@ InspectorDebuggerAgent::InspectorDebuggerAgent(AgentContext& context)
     , m_scriptDebugServer(context.environment.scriptDebugServer())
     , m_injectedScriptManager(context.injectedScriptManager)
 {
-    // FIXME: make breakReason optional so that there was no need to init it with "other".
-    clearBreakDetails();
+    // FIXME: make pauseReason optional so that there was no need to init it with "other".
+    clearPauseDetails();
 }
 
 InspectorDebuggerAgent::~InspectorDebuggerAgent() = default;
@@ -91,6 +97,16 @@ void InspectorDebuggerAgent::enable()
 
     for (auto* listener : copyToVector(m_listeners))
         listener->debuggerWasEnabled();
+
+    for (auto& [sourceID, script] : m_scripts) {
+        Optional<JSC::Debugger::BlackboxType> blackboxType;
+        if (isWebKitInjectedScript(script.sourceURL)) {
+            if (!m_pauseForInternalScripts)
+                blackboxType = JSC::Debugger::BlackboxType::Ignored;
+        } else if ((!script.sourceURL.isEmpty() && m_blackboxedURLs.contains(script.sourceURL)) || (!script.url.isEmpty() && m_blackboxedURLs.contains(script.url)))
+            blackboxType = JSC::Debugger::BlackboxType::Deferred;
+        m_scriptDebugServer.setBlackboxType(sourceID, blackboxType);
+    }
 }
 
 void InspectorDebuggerAgent::disable(bool isBeingDestroyed)
@@ -169,6 +185,17 @@ void InspectorDebuggerAgent::setSuppressAllPauses(bool suppress)
     m_scriptDebugServer.setSuppressAllPauses(suppress);
 }
 
+void InspectorDebuggerAgent::updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason reason, RefPtr<JSON::Object>&& data)
+{
+    if (m_pauseReason != DebuggerFrontendDispatcher::Reason::BlackboxedScript) {
+        m_preBlackboxPauseReason = m_pauseReason;
+        m_preBlackboxPauseData = WTFMove(m_pauseData);
+    }
+
+    m_pauseReason = reason;
+    m_pauseData = WTFMove(data);
+}
+
 static RefPtr<JSON::Object> buildAssertPauseReason(const String& message)
 {
     auto reason = Protocol::Debugger::AssertPauseReason::create().release();
@@ -698,15 +725,14 @@ void InspectorDebuggerAgent::getFunctionDetails(ErrorString& errorString, const
     injectedScript.getFunctionDetails(errorString, functionId, details);
 }
 
-void InspectorDebuggerAgent::schedulePauseOnNextStatement(DebuggerFrontendDispatcher::Reason breakReason, RefPtr<JSON::Object>&& data)
+void InspectorDebuggerAgent::schedulePauseOnNextStatement(DebuggerFrontendDispatcher::Reason reason, RefPtr<JSON::Object>&& data)
 {
     if (m_javaScriptPauseScheduled)
         return;
 
     m_javaScriptPauseScheduled = true;
 
-    m_breakReason = breakReason;
-    m_breakData = WTFMove(data);
+    updatePauseReasonAndData(reason, WTFMove(data));
 
     JSC::JSLockHolder locker(m_scriptDebugServer.vm());
     m_scriptDebugServer.setPauseOnNextStatement(true);
@@ -719,7 +745,7 @@ void InspectorDebuggerAgent::cancelPauseOnNextStatement()
 
     m_javaScriptPauseScheduled = false;
 
-    clearBreakDetails();
+    clearPauseDetails();
     m_scriptDebugServer.setPauseOnNextStatement(false);
     m_enablePauseWhenIdle = false;
 }
@@ -864,6 +890,33 @@ void InspectorDebuggerAgent::evaluateOnCallFrame(ErrorString& errorString, const
     }
 }
 
+void InspectorDebuggerAgent::setShouldBlackboxURL(ErrorString& errorString, const String& url, bool shouldBlackbox)
+{
+    if (url.isEmpty()) {
+        errorString = "URL must not be empty"_s;
+        return;
+    }
+
+    if (isWebKitInjectedScript(url)) {
+        errorString = "Blackboxing of internal scripts is controlled by 'Debugger.setPauseForInternalScripts'"_s;
+        return;
+    }
+
+    if (shouldBlackbox)
+        m_blackboxedURLs.add(url);
+    else
+        m_blackboxedURLs.remove(url);
+
+    auto blackboxType = shouldBlackbox ? Optional<JSC::Debugger::BlackboxType>(JSC::Debugger::BlackboxType::Deferred) : WTF::nullopt;
+    for (auto& [sourceID, script] : m_scripts) {
+        if (isWebKitInjectedScript(script.sourceURL))
+            continue;
+        if (script.sourceURL != url && script.url != url)
+            continue;
+        m_scriptDebugServer.setBlackboxType(sourceID, blackboxType);
+    }
+}
+
 void InspectorDebuggerAgent::scriptExecutionBlockedByCSP(const String& directiveText)
 {
     if (m_scriptDebugServer.pauseOnExceptionsState() != JSC::Debugger::DontPauseOnExceptions)
@@ -891,13 +944,12 @@ void InspectorDebuggerAgent::setPauseForInternalScripts(ErrorString&, bool shoul
 
     m_pauseForInternalScripts = shouldPause;
 
-    if (m_pauseForInternalScripts)
-        m_scriptDebugServer.clearBlacklist();
-}
-
-static bool isWebKitInjectedScript(const String& sourceURL)
-{
-    return sourceURL.startsWith("__InjectedScript_") && sourceURL.endsWith(".js");
+    auto blackboxType = !m_pauseForInternalScripts ? Optional<JSC::Debugger::BlackboxType>(JSC::Debugger::BlackboxType::Ignored) : WTF::nullopt;
+    for (auto& [sourceID, script] : m_scripts) {
+        if (!isWebKitInjectedScript(script.sourceURL))
+            continue;
+        m_scriptDebugServer.setBlackboxType(sourceID, blackboxType);
+    }
 }
 
 void InspectorDebuggerAgent::didParseSource(JSC::SourceID sourceID, const Script& script)
@@ -916,8 +968,11 @@ void InspectorDebuggerAgent::didParseSource(JSC::SourceID sourceID, const Script
 
     m_scripts.set(sourceID, script);
 
-    if (hasSourceURL && isWebKitInjectedScript(sourceURL) && !m_pauseForInternalScripts)
-        m_scriptDebugServer.addToBlacklist(sourceID);
+    if (isWebKitInjectedScript(sourceURL)) {
+        if (!m_pauseForInternalScripts)
+            m_scriptDebugServer.setBlackboxType(sourceID, JSC::Debugger::BlackboxType::Ignored);
+    } else if ((hasSourceURL && m_blackboxedURLs.contains(sourceURL)) || (!script.url.isEmpty() && m_blackboxedURLs.contains(script.url)))
+        m_scriptDebugServer.setBlackboxType(sourceID, JSC::Debugger::BlackboxType::Deferred);
 
     String scriptURLForBreakpoints = hasSourceURL ? script.sourceURL : script.url;
     if (scriptURLForBreakpoints.isEmpty())
@@ -996,24 +1051,30 @@ void InspectorDebuggerAgent::didPause(JSC::ExecState& scriptState, JSC::JSValue
     InjectedScript injectedScript = m_injectedScriptManager.injectedScriptFor(&scriptState);
 
     // If a high level pause pause reason is not already set, try to infer a reason from the debugger.
-    if (m_breakReason == DebuggerFrontendDispatcher::Reason::Other) {
+    if (m_pauseReason == DebuggerFrontendDispatcher::Reason::Other) {
         switch (m_scriptDebugServer.reasonForPause()) {
         case JSC::Debugger::PausedForBreakpoint: {
-            JSC::BreakpointID debuggerBreakpointId = m_scriptDebugServer.pausingBreakpointID();
-            if (debuggerBreakpointId != m_continueToLocationBreakpointID) {
-                m_breakReason = DebuggerFrontendDispatcher::Reason::Breakpoint;
-                m_breakData = buildBreakpointPauseReason(debuggerBreakpointId);
-            }
+            auto debuggerBreakpointId = m_scriptDebugServer.pausingBreakpointID();
+            if (debuggerBreakpointId != m_continueToLocationBreakpointID)
+                updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason::Breakpoint, buildBreakpointPauseReason(debuggerBreakpointId));
             break;
         }
         case JSC::Debugger::PausedForDebuggerStatement:
-            m_breakReason = DebuggerFrontendDispatcher::Reason::DebuggerStatement;
-            m_breakData = nullptr;
+            updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason::DebuggerStatement, nullptr);
             break;
         case JSC::Debugger::PausedForException:
-            m_breakReason = DebuggerFrontendDispatcher::Reason::Exception;
-            m_breakData = buildExceptionPauseReason(exceptionOrCaughtValue, injectedScript);
+            updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason::Exception, buildExceptionPauseReason(exceptionOrCaughtValue, injectedScript));
             break;
+        case JSC::Debugger::PausedAfterBlackboxedScript: {
+            // There should be no break data, as we would've already continued past the breakpoint.
+            ASSERT(!m_pauseData);
+
+            // Don't call `updatePauseReasonAndData` so as to not override `m_preBlackboxPauseData`.
+            if (m_pauseReason != DebuggerFrontendDispatcher::Reason::BlackboxedScript)
+                m_preBlackboxPauseReason = m_pauseReason;
+            m_pauseReason = DebuggerFrontendDispatcher::Reason::BlackboxedScript;
+            break;
+        }
         case JSC::Debugger::PausedAtStatement:
         case JSC::Debugger::PausedAtExpression:
         case JSC::Debugger::PausedBeforeReturn:
@@ -1026,6 +1087,24 @@ void InspectorDebuggerAgent::didPause(JSC::ExecState& scriptState, JSC::JSValue
         }
     }
 
+    if (m_scriptDebugServer.reasonForPause() == JSC::Debugger::PausedAfterBlackboxedScript) {
+        // Ensure that `m_preBlackboxPauseReason` is populated with the most recent data.
+        updatePauseReasonAndData(m_pauseReason, nullptr);
+
+        RefPtr<JSON::Object> data;
+        if (auto debuggerBreakpointId = m_scriptDebugServer.pausingBreakpointID()) {
+            ASSERT(debuggerBreakpointId != m_continueToLocationBreakpointID);
+            data = JSON::Object::create();
+            data->setString("originalReason"_s, Protocol::InspectorHelpers::getEnumConstantValue(DebuggerFrontendDispatcher::Reason::Breakpoint));
+            data->setValue("originalData"_s, buildBreakpointPauseReason(debuggerBreakpointId));
+        } else if (m_preBlackboxPauseData) {
+            data = JSON::Object::create();
+            data->setString("originalReason"_s, Protocol::InspectorHelpers::getEnumConstantValue(m_preBlackboxPauseReason));
+            data->setValue("originalData"_s, m_preBlackboxPauseData);
+        }
+        updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason::BlackboxedScript, WTFMove(data));
+    }
+
     // Set $exception to the exception or caught value.
     if (exceptionOrCaughtValue && !injectedScript.hasNoValue()) {
         injectedScript.setExceptionValue(exceptionOrCaughtValue);
@@ -1042,7 +1121,7 @@ void InspectorDebuggerAgent::didPause(JSC::ExecState& scriptState, JSC::JSValue
             asyncStackTrace = it->value->buildInspectorObject();
     }
 
-    m_frontendDispatcher->paused(currentCallFrames(injectedScript), m_breakReason, m_breakData, asyncStackTrace);
+    m_frontendDispatcher->paused(currentCallFrames(injectedScript), m_pauseReason, m_pauseData, asyncStackTrace);
 
     m_javaScriptPauseScheduled = false;
 
@@ -1087,17 +1166,17 @@ void InspectorDebuggerAgent::didContinue()
     m_pausedScriptState = nullptr;
     m_currentCallStack = { };
     m_injectedScriptManager.releaseObjectGroup(InspectorDebuggerAgent::backtraceObjectGroup);
-    clearBreakDetails();
+    clearPauseDetails();
     clearExceptionValue();
 
     if (m_conditionToDispatchResumed == ShouldDispatchResumed::WhenContinued)
         m_frontendDispatcher->resumed();
 }
 
-void InspectorDebuggerAgent::breakProgram(DebuggerFrontendDispatcher::Reason breakReason, RefPtr<JSON::Object>&& data)
+void InspectorDebuggerAgent::breakProgram(DebuggerFrontendDispatcher::Reason reason, RefPtr<JSON::Object>&& data)
 {
-    m_breakReason = breakReason;
-    m_breakData = WTFMove(data);
+    updatePauseReasonAndData(reason, WTFMove(data));
+
     m_scriptDebugServer.breakProgram();
 }
 
@@ -1118,7 +1197,7 @@ void InspectorDebuggerAgent::clearDebuggerBreakpointState()
         JSC::JSLockHolder holder(m_scriptDebugServer.vm());
         m_scriptDebugServer.clearBreakpointActions();
         m_scriptDebugServer.clearBreakpoints();
-        m_scriptDebugServer.clearBlacklist();
+        m_scriptDebugServer.clearBlackbox();
     }
 
     m_pausedScriptState = nullptr;
@@ -1127,7 +1206,7 @@ void InspectorDebuggerAgent::clearDebuggerBreakpointState()
     m_breakpointIdentifierToDebugServerBreakpointIDs.clear();
     m_debuggerBreakpointIdentifierToInspectorBreakpointIdentifier.clear();
     m_continueToLocationBreakpointID = JSC::noBreakpointID;
-    clearBreakDetails();
+    clearPauseDetails();
     m_javaScriptPauseScheduled = false;
     m_hasExceptionValue = false;
 
@@ -1158,10 +1237,9 @@ bool InspectorDebuggerAgent::assertPaused(ErrorString& errorString)
     return true;
 }
 
-void InspectorDebuggerAgent::clearBreakDetails()
+void InspectorDebuggerAgent::clearPauseDetails()
 {
-    m_breakReason = DebuggerFrontendDispatcher::Reason::Other;
-    m_breakData = nullptr;
+    updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason::Other, nullptr);
 }
 
 void InspectorDebuggerAgent::clearExceptionValue()
index 18a7d24..16f4bf8 100644 (file)
@@ -85,6 +85,7 @@ public:
     void setPauseOnMicrotasks(ErrorString&, bool enabled) final;
     void setPauseForInternalScripts(ErrorString&, bool shouldPause) final;
     void evaluateOnCallFrame(ErrorString&, const String& callFrameId, const String& expression, const String* objectGroup, const bool* includeCommandLineAPI, const bool* doNotPauseOnExceptionsAndMuteConsole, const bool* returnByValue, const bool* generatePreview, const bool* saveResult, const bool* emulateUserGesture, RefPtr<Protocol::Runtime::RemoteObject>& result, Optional<bool>& wasThrown, Optional<int>& savedResultIndex) override;
+    void setShouldBlackboxURL(ErrorString&, const String& url, bool shouldBlackbox) final;
 
     // ScriptDebugListener
     void didParseSource(JSC::SourceID, const Script&) final;
@@ -115,11 +116,11 @@ public:
     void willDispatchAsyncCall(AsyncCallType, int callbackId);
     void didDispatchAsyncCall();
 
-    void schedulePauseOnNextStatement(DebuggerFrontendDispatcher::Reason breakReason, RefPtr<JSON::Object>&& data);
+    void schedulePauseOnNextStatement(DebuggerFrontendDispatcher::Reason, RefPtr<JSON::Object>&& data);
     void cancelPauseOnNextStatement();
     bool pauseOnNextStatementEnabled() const { return m_javaScriptPauseScheduled; }
 
-    void breakProgram(DebuggerFrontendDispatcher::Reason breakReason, RefPtr<JSON::Object>&& data);
+    void breakProgram(DebuggerFrontendDispatcher::Reason, RefPtr<JSON::Object>&& data);
     void scriptExecutionBlockedByCSP(const String& directiveText);
 
     class Listener {
@@ -159,7 +160,7 @@ private:
     bool assertPaused(ErrorString&);
     void clearDebuggerBreakpointState();
     void clearInspectorBreakpointState();
-    void clearBreakDetails();
+    void clearPauseDetails();
     void clearExceptionValue();
     void clearAsyncStackTraceData();
 
@@ -168,6 +169,8 @@ private:
     void willStepAndMayBecomeIdle();
     void didBecomeIdle();
 
+    void updatePauseReasonAndData(DebuggerFrontendDispatcher::Reason, RefPtr<JSON::Object>&& data);
+
     RefPtr<JSON::Object> buildBreakpointPauseReason(JSC::BreakpointID);
     RefPtr<JSON::Object> buildExceptionPauseReason(JSC::JSValue exception, const InjectedScript&);
 
@@ -182,6 +185,7 @@ private:
     ScriptDebugServer& m_scriptDebugServer;
     InjectedScriptManager& m_injectedScriptManager;
     HashMap<JSC::SourceID, Script> m_scripts;
+    HashSet<String> m_blackboxedURLs;
 
     HashSet<Listener*> m_listeners;
 
@@ -192,10 +196,14 @@ private:
     HashMap<String, RefPtr<JSON::Object>> m_javaScriptBreakpoints;
     HashMap<JSC::BreakpointID, String> m_debuggerBreakpointIdentifierToInspectorBreakpointIdentifier;
     JSC::BreakpointID m_continueToLocationBreakpointID { JSC::noBreakpointID };
-    DebuggerFrontendDispatcher::Reason m_breakReason;
-    RefPtr<JSON::Object> m_breakData;
     ShouldDispatchResumed m_conditionToDispatchResumed { ShouldDispatchResumed::No };
 
+    DebuggerFrontendDispatcher::Reason m_pauseReason;
+    RefPtr<JSON::Object> m_pauseData;
+
+    DebuggerFrontendDispatcher::Reason m_preBlackboxPauseReason;
+    RefPtr<JSON::Object> m_preBlackboxPauseData;
+
     HashMap<AsyncCallIdentifier, RefPtr<AsyncStackTrace>> m_pendingAsyncCalls;
     Optional<AsyncCallIdentifier> m_currentAsyncCallIdentifier;
     int m_asyncStackTraceDepth { 0 };
index 637076a..15abd36 100644 (file)
                 { "name": "wasThrown", "type": "boolean", "optional": true, "description": "True if the result was thrown during the evaluation." },
                 { "name": "savedResultIndex", "type": "integer", "optional": true, "description": "If the result was saved, this is the $n index that can be used to access the value." }
             ]
+        },
+        {
+            "name": "setShouldBlackboxURL",
+            "description": "Sets whether the given URL should be in the list of blackboxed scripts, which are ignored when pausing/stepping/debugging.",
+            "parameters": [
+                { "name": "url", "type": "string" },
+                { "name": "shouldBlackbox", "type": "boolean" }
+            ]
         }
     ],
     "events": [
             "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.",
             "parameters": [
                 { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call stack the virtual machine stopped on." },
-                { "name": "reason", "type": "string", "enum": ["XHR", "Fetch", "DOM", "AnimationFrame", "Interval", "Listener", "Timeout", "exception", "assert", "CSPViolation", "DebuggerStatement", "Breakpoint", "PauseOnNextStatement", "Microtask", "other"], "description": "Pause reason." },
+                { "name": "reason", "type": "string", "enum": ["XHR", "Fetch", "DOM", "AnimationFrame", "Interval", "Listener", "Timeout", "exception", "assert", "CSPViolation", "DebuggerStatement", "Breakpoint", "PauseOnNextStatement", "Microtask", "BlackboxedScript", "other"], "description": "Pause reason." },
                 { "name": "data", "type": "object", "optional": true, "description": "Object containing break-specific auxiliary properties." },
                 { "name": "asyncStackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "Linked list of asynchronous StackTraces." }
             ]
index ef2c5fd..661c34e 100644 (file)
@@ -1,3 +1,80 @@
+2019-09-03  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: implement blackboxing of script resources
+        https://bugs.webkit.org/show_bug.cgi?id=17240
+        <rdar://problem/5732847>
+
+        Reviewed by Joseph Pecoraro.
+
+        When a script is blackboxed and the debugger attempts to pause in that script, the pause
+        reason/data will be saved and execution will continue until it has left the blackboxed
+        script. Once outside, execution is paused with the saved reason/data.
+
+        This is especially useful when debugging issues using libraries/frameworks, as it allows the
+        developer to "skip" the internal logic of the library/framework and instead focus only on
+        how they're using it.
+
+        * UserInterface/Controllers/DebuggerManager.js:
+        (WI.DebuggerManager):
+        (WI.DebuggerManager.prototype.initializeTarget):
+        (WI.DebuggerManager.supportsBlackboxingScripts): Added.
+        (WI.DebuggerManager.pauseReasonFromPayload): Added.
+        (WI.DebuggerManager.prototype.isScriptBlackboxed): Added.
+        (WI.DebuggerManager.prototype.setShouldBlackboxScript): Added.
+        (WI.DebuggerManager.prototype._pauseReasonFromPayload):
+        (WI.DebuggerManager.prototype._pauseReasonFromPayload): Deleted.
+
+        * UserInterface/Models/SourceCode.js:
+        (WI.SourceCode.prototype.get isScript): Added.
+        (WI.SourceCode.prototype.get supportsScriptBlackboxing): Added.
+        * UserInterface/Models/Script.js:
+        (WI.Script.prototype.get isScript): Added.
+        * UserInterface/Models/Resource.js:
+        (WI.Resource.prototype.get isScript): Added.
+        Provide a more straightforward way of determining if a `WI.SourceCode` is a script.
+
+        * UserInterface/Views/DebuggerSidebarPanel.js:
+        (WI.DebuggerSidebarPanel.prototype._updatePauseReason):
+        (WI.DebuggerSidebarPanel.prototype._updatePauseReasonSection):
+        * UserInterface/Views/SourcesNavigationSidebarPanel.js:
+        (WI.SourcesNavigationSidebarPanel.prototype._updatePauseReason):
+        (WI.SourcesNavigationSidebarPanel.prototype._updatePauseReasonSection):
+        Display the original pause reason and breakpoint (if applicable) when pausing after leaving
+        a blackboxed script.
+
+        * UserInterface/Views/SourceCodeTreeElement.js:
+        (WI.SourceCodeTreeElement.prototype.canSelectOnMouseDown): Added.
+        (WI.SourceCodeTreeElement.prototype.updateStatus): Added.
+        (WI.SourceCodeTreeElement.prototype._updateSourceCode):
+        (WI.SourceCodeTreeElement.prototype._updateToggleBlackboxImageElementState): Added.
+        (WI.SourceCodeTreeElement.prototype._handleToggleBlackboxedImageElementClick): Added.
+        * UserInterface/Views/SourceCodeTreeElement.css: Added.
+        (.tree-outline .item .status > .toggle-script-blackboxed):
+        (.tree-outline:not(.navigation-sidebar-panel-content-tree-outline) .item .status > .toggle-script-blackboxed,):
+        (.tree-outline:focus .item.selected .status > .toggle-script-blackboxed):
+        (.tree-outline .item .status > .toggle-script-blackboxed.blackboxed):
+        (@media (prefers-color-scheme: dark) .tree-outline .item .status > .toggle-script-blackboxed):
+
+        * UserInterface/Views/ResourceTreeElement.js:
+        (WI.ResourceTreeElement.prototype._updateResource):
+        (WI.ResourceTreeElement.prototype.updateStatus): Added.
+        (WI.ResourceTreeElement.prototype._updateStatus): Deleted.
+        Make sure that the loading indicator doesn't override the blackbox toggle.
+
+        * UserInterface/Base/Setting.js:
+        (WI.Setting.prototype.set value):
+        (WI.Setting.prototype.save): Added.
+        When modifying an array value, that doesn't go through `WI.Setting.prototype.set value`, so
+        we need a more "manual" way of saving the new value.
+
+        * UserInterface/Main.html:
+        * Localizations/en.lproj/localizedStrings.js:
+
+        * UserInterface/Test/TestHarness.js:
+        (TestHarness.prototype.newline): Added.
+        (TestHarness.prototype.expectException):
+        Add a special case for logging error message objects when running protocol tests.
+
 2019-08-29  Keith Rollin  <krollin@apple.com>
 
         Update .xcconfig symbols to reflect the current set of past and future product versions.
index a24ac52..e43e2d6 100644 (file)
@@ -335,6 +335,7 @@ localizedStrings["Debugger:"] = "Debugger:";
 localizedStrings["Debugs"] = "Debugs";
 localizedStrings["Decoded"] = "Decoded";
 localizedStrings["Default"] = "Default";
+localizedStrings["Deferred pause from blackboxed script"] = "Deferred pause from blackboxed script";
 localizedStrings["Delete"] = "Delete";
 localizedStrings["Delete Breakpoint"] = "Delete Breakpoint";
 localizedStrings["Delete Breakpoints"] = "Delete Breakpoints";
@@ -587,6 +588,7 @@ localizedStrings["IP Address"] = "IP Address";
 localizedStrings["Identity"] = "Identity";
 localizedStrings["Idle"] = "Idle";
 localizedStrings["Ignore"] = "Ignore";
+localizedStrings["Ignore script when debugging"] = "Ignore script when debugging";
 localizedStrings["Ignore the resource cache when loading resources"] = "Ignore the resource cache when loading resources";
 localizedStrings["Ignored"] = "Ignored";
 localizedStrings["Image"] = "Image";
@@ -597,6 +599,7 @@ localizedStrings["Import"] = "Import";
 localizedStrings["Imported"] = "Imported";
 localizedStrings["Imported - %s"] = "Imported - %s";
 localizedStrings["Imported \u2014 %s"] = "Imported \u2014 %s";
+localizedStrings["Include script when debugging"] = "Include script when debugging";
 localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent width:"] = "Indent width:";
 localizedStrings["Index"] = "Index";
index 9394ae6..664f6ac 100644 (file)
@@ -113,9 +113,13 @@ WI.Setting = class Setting extends WI.Object
 
         this._value = value;
 
+        this.save();
+    }
+
+    save()
+    {
         if (!window.InspectorTest && window.localStorage) {
             try {
-                // Use Object.shallowEqual to properly compare objects.
                 if (Object.shallowEqual(this._value, this._defaultValue))
                     delete window.localStorage[this._localStorageKey];
                 else
index 81127c7..835a31f 100644 (file)
@@ -89,6 +89,8 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
 
         this._nextBreakpointActionIdentifier = 1;
 
+        this._blackboxURLsSetting = new WI.Setting("debugger-blackbox-urls", []);
+
         this._activeCallFrame = null;
 
         this._internalWebKitScripts = [];
@@ -157,6 +159,12 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         if (target.DebuggerAgent.setAsyncStackTraceDepth)
             target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
 
+        // COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
+        if (target.DebuggerAgent.setShouldBlackboxURL) {
+            for (let url of this._blackboxURLsSetting.value)
+                target.DebuggerAgent.setShouldBlackboxURL(url, true);
+        }
+
         if (WI.isEngineeringBuild) {
             // COMPATIBILITY (iOS 12): DebuggerAgent.setPauseForInternalScripts did not exist yet.
             if (target.DebuggerAgent.setPauseForInternalScripts)
@@ -178,6 +186,55 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         this._restoringBreakpoints = false;
     }
 
+    // Static
+
+    static supportsBlackboxingScripts()
+    {
+        return !!InspectorBackend.domains.Debugger.setShouldBlackboxURL;
+    }
+
+    static pauseReasonFromPayload(payload)
+    {
+        switch (payload) {
+        case InspectorBackend.domains.Debugger.PausedReason.AnimationFrame:
+            return WI.DebuggerManager.PauseReason.AnimationFrame;
+        case InspectorBackend.domains.Debugger.PausedReason.Assert:
+            return WI.DebuggerManager.PauseReason.Assertion;
+        case InspectorBackend.domains.Debugger.PausedReason.BlackboxedScript:
+            return WI.DebuggerManager.PauseReason.BlackboxedScript;
+        case InspectorBackend.domains.Debugger.PausedReason.Breakpoint:
+            return WI.DebuggerManager.PauseReason.Breakpoint;
+        case InspectorBackend.domains.Debugger.PausedReason.CSPViolation:
+            return WI.DebuggerManager.PauseReason.CSPViolation;
+        case InspectorBackend.domains.Debugger.PausedReason.DOM:
+            return WI.DebuggerManager.PauseReason.DOM;
+        case InspectorBackend.domains.Debugger.PausedReason.DebuggerStatement:
+            return WI.DebuggerManager.PauseReason.DebuggerStatement;
+        case InspectorBackend.domains.Debugger.PausedReason.EventListener:
+            return WI.DebuggerManager.PauseReason.EventListener;
+        case InspectorBackend.domains.Debugger.PausedReason.Exception:
+            return WI.DebuggerManager.PauseReason.Exception;
+        case InspectorBackend.domains.Debugger.PausedReason.Fetch:
+            return WI.DebuggerManager.PauseReason.Fetch;
+        case InspectorBackend.domains.Debugger.PausedReason.Interval:
+            return WI.DebuggerManager.PauseReason.Interval;
+        case InspectorBackend.domains.Debugger.PausedReason.Listener:
+            return WI.DebuggerManager.PauseReason.Listener;
+        case InspectorBackend.domains.Debugger.PausedReason.Microtask:
+            return WI.DebuggerManager.PauseReason.Microtask;
+        case InspectorBackend.domains.Debugger.PausedReason.PauseOnNextStatement:
+            return WI.DebuggerManager.PauseReason.PauseOnNextStatement;
+        case InspectorBackend.domains.Debugger.PausedReason.Timeout:
+            return WI.DebuggerManager.PauseReason.Timeout;
+        case InspectorBackend.domains.Debugger.PausedReason.Timer:
+            return WI.DebuggerManager.PauseReason.Timer;
+        case InspectorBackend.domains.Debugger.PausedReason.XHR:
+            return WI.DebuggerManager.PauseReason.XHR;
+        default:
+            return WI.DebuggerManager.PauseReason.Other;
+        }
+    }
+
     // Public
 
     get paused()
@@ -349,6 +406,30 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         return knownScripts;
     }
 
+    isScriptBlackboxed(sourceCode)
+    {
+        return this._blackboxURLsSetting.value.includes(sourceCode.contentIdentifier);
+    }
+
+    setShouldBlackboxScript(sourceCode, shouldBlackbox)
+    {
+        console.assert(DebuggerManager.supportsBlackboxingScripts());
+        console.assert(sourceCode instanceof WI.SourceCode);
+        console.assert(sourceCode.contentIdentifier);
+        console.assert(!isWebKitInjectedScript(sourceCode.contentIdentifier));
+
+        this._blackboxURLsSetting.value.toggleIncludes(sourceCode.contentIdentifier, shouldBlackbox);
+        this._blackboxURLsSetting.save();
+
+        for (let target of WI.targets) {
+            // COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
+            if (target.DebuggerAgent.setShouldBlackboxURL)
+                target.DebuggerAgent.setShouldBlackboxURL(sourceCode.contentIdentifier, !!shouldBlackbox);
+        }
+
+        this.dispatchEventToListeners(DebuggerManager.Event.BlackboxedURLsChanged);
+    }
+
     get asyncStackTraceDepth()
     {
         return this._asyncStackTraceDepthSetting.value;
@@ -617,7 +698,7 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         let targetData = this._targetDebuggerDataMap.get(target);
 
         let callFrames = [];
-        let pauseReason = this._pauseReasonFromPayload(reason);
+        let pauseReason = DebuggerManager.pauseReasonFromPayload(reason);
         let pauseData = data || null;
 
         for (var i = 0; i < callFramesPayload.length; ++i) {
@@ -843,47 +924,6 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         return new WI.ScopeChainNode(type, [object], payload.name, payload.location, payload.empty);
     }
 
-    _pauseReasonFromPayload(payload)
-    {
-        // FIXME: Handle other backend pause reasons.
-        switch (payload) {
-        case DebuggerAgent.PausedReason.AnimationFrame:
-            return WI.DebuggerManager.PauseReason.AnimationFrame;
-        case DebuggerAgent.PausedReason.Assert:
-            return WI.DebuggerManager.PauseReason.Assertion;
-        case DebuggerAgent.PausedReason.Breakpoint:
-            return WI.DebuggerManager.PauseReason.Breakpoint;
-        case DebuggerAgent.PausedReason.CSPViolation:
-            return WI.DebuggerManager.PauseReason.CSPViolation;
-        case DebuggerAgent.PausedReason.DOM:
-            return WI.DebuggerManager.PauseReason.DOM;
-        case DebuggerAgent.PausedReason.DebuggerStatement:
-            return WI.DebuggerManager.PauseReason.DebuggerStatement;
-        case DebuggerAgent.PausedReason.EventListener:
-            return WI.DebuggerManager.PauseReason.EventListener;
-        case DebuggerAgent.PausedReason.Exception:
-            return WI.DebuggerManager.PauseReason.Exception;
-        case DebuggerAgent.PausedReason.Fetch:
-            return WI.DebuggerManager.PauseReason.Fetch;
-        case DebuggerAgent.PausedReason.Interval:
-            return WI.DebuggerManager.PauseReason.Interval;
-        case DebuggerAgent.PausedReason.Listener:
-            return WI.DebuggerManager.PauseReason.Listener;
-        case DebuggerAgent.PausedReason.Microtask:
-            return WI.DebuggerManager.PauseReason.Microtask;
-        case DebuggerAgent.PausedReason.PauseOnNextStatement:
-            return WI.DebuggerManager.PauseReason.PauseOnNextStatement;
-        case DebuggerAgent.PausedReason.Timeout:
-            return WI.DebuggerManager.PauseReason.Timeout;
-        case DebuggerAgent.PausedReason.Timer:
-            return WI.DebuggerManager.PauseReason.Timer;
-        case DebuggerAgent.PausedReason.XHR:
-            return WI.DebuggerManager.PauseReason.XHR;
-        default:
-            return WI.DebuggerManager.PauseReason.Other;
-        }
-    }
-
     _debuggerBreakpointActionType(type)
     {
         switch (type) {
@@ -1390,11 +1430,13 @@ WI.DebuggerManager.Event = {
     BreakpointsEnabledDidChange: "debugger-manager-breakpoints-enabled-did-change",
     ProbeSetAdded: "debugger-manager-probe-set-added",
     ProbeSetRemoved: "debugger-manager-probe-set-removed",
+    BlackboxedURLsChanged: "blackboxed-urls-changed",
 };
 
 WI.DebuggerManager.PauseReason = {
     AnimationFrame: "animation-frame",
     Assertion: "assertion",
+    BlackboxedScript: "blackboxed-script",
     Breakpoint: "breakpoint",
     CSPViolation: "CSP-violation",
     DebuggerStatement: "debugger-statement",
index f1c691d..d83754c 100644 (file)
     <link rel="stylesheet" href="Views/Slider.css">
     <link rel="stylesheet" href="Views/SoftContextMenu.css">
     <link rel="stylesheet" href="Views/SourceCodeTextEditor.css">
+    <link rel="stylesheet" href="Views/SourceCodeTreeElement.css">
     <link rel="stylesheet" href="Views/SourcesNavigationSidebarPanel.css">
     <link rel="stylesheet" href="Views/SpreadsheetCSSStyleDeclarationEditor.css">
     <link rel="stylesheet" href="Views/SpreadsheetCSSStyleDeclarationSection.css">
index ac73a1e..31aed8b 100644 (file)
@@ -352,6 +352,11 @@ WI.Resource = class Resource extends WI.SourceCode
         return true;
     }
 
+    get isScript()
+    {
+        return this._type === Resource.Type.Script;
+    }
+
     get displayName()
     {
         return WI.displayNameForURL(this._url, this.urlComponents);
index b07b7b1..fa298eb 100644 (file)
@@ -120,6 +120,11 @@ WI.Script = class Script extends WI.SourceCode
         return this._resource.mimeType;
     }
 
+    get isScript()
+    {
+        return true;
+    }
+
     get displayName()
     {
         if (this._url && !this._dynamicallyAddedScriptElement)
index 78622f8..92e2b99 100644 (file)
@@ -103,6 +103,22 @@ WI.SourceCode = class SourceCode extends WI.Object
         return this.url;
     }
 
+    get isScript()
+    {
+        // Implemented by subclasses if needed.
+        return false;
+    }
+
+    get supportsScriptBlackboxing()
+    {
+        if (!this.isScript)
+            return false;
+        if (!WI.DebuggerManager.supportsBlackboxingScripts())
+            return false;
+        let contentIdentifier = this.contentIdentifier;
+        return contentIdentifier && !isWebKitInjectedScript(contentIdentifier);
+    }
+
     get sourceMaps()
     {
         return this._sourceMaps || [];
index ec7359a..d447b89 100644 (file)
@@ -219,8 +219,18 @@ TestHarness = class TestHarness extends WI.Object
 
         let expectAndDumpError = (e) => {
             this.expectNotNull(e, "Should produce an exception.");
-            if (e)
+            if (!e)
+                return;
+
+            if (e instanceof Error || !(e instanceof Object))
                 this.log(e.toString());
+            else {
+                try {
+                    this.json(e);
+                } catch {
+                    this.log(e.constructor.name);
+                }
+            }
         }
 
         let error = null;
index a4c4859..efc1486 100644 (file)
@@ -1166,15 +1166,13 @@ WI.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WI.NavigationSideba
         this._pauseReasonTreeOutline = null;
 
         this._updatePauseReasonGotoArrow();
-        return this._updatePauseReasonSection();
-    }
-
-    _updatePauseReasonSection()
-    {
         let target = WI.debuggerManager.activeCallFrame.target;
         let targetData = WI.debuggerManager.dataForTarget(target);
-        let {pauseReason, pauseData} = targetData;
+        return this._updatePauseReasonSection(target, targetData.pauseReason, targetData.pauseData);
+    }
 
+    _updatePauseReasonSection(target, pauseReason, pauseData)
+    {
         switch (pauseReason) {
         case WI.DebuggerManager.PauseReason.AnimationFrame:
             this._pauseReasonTreeOutline = this.createContentTreeOutline({suppressFiltering: true});
@@ -1203,6 +1201,20 @@ WI.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WI.NavigationSideba
             this._pauseReasonGroup.rows = [this._pauseReasonTextRow];
             return true;
 
+        case WI.DebuggerManager.PauseReason.BlackboxedScript: {
+            console.assert(pauseData);
+            if (pauseData)
+                this._updatePauseReasonSection(target, WI.DebuggerManager.pauseReasonFromPayload(pauseData.originalReason), pauseData.originalData);
+
+            // Don't use `_pauseReasonTextRow` as it may have already been set.
+            let blackboxReasonTextRow = new WI.DetailsSectionTextRow(WI.UIString("Deferred pause from blackboxed script"));
+            blackboxReasonTextRow.__blackboxReason = true;
+
+            let existingRows = this._pauseReasonGroup.rows.filter((row) => !row.__blackboxReason);
+            this._pauseReasonGroup.rows = [blackboxReasonTextRow, ...existingRows];
+            return true;
+        }
+
         case WI.DebuggerManager.PauseReason.Breakpoint:
             console.assert(pauseData, "Expected breakpoint identifier, but found none.");
             if (pauseData && pauseData.breakpointId) {
index 278c005..85a90de 100644 (file)
@@ -118,8 +118,8 @@ WI.ResourceTreeElement = class ResourceTreeElement extends WI.SourceCodeTreeElem
         if (this._resource) {
             this._resource.removeEventListener(WI.Resource.Event.URLDidChange, this._urlDidChange, this);
             this._resource.removeEventListener(WI.Resource.Event.TypeDidChange, this._typeDidChange, this);
-            this._resource.removeEventListener(WI.Resource.Event.LoadingDidFinish, this._updateStatus, this);
-            this._resource.removeEventListener(WI.Resource.Event.LoadingDidFail, this._updateStatus, this);
+            this._resource.removeEventListener(WI.Resource.Event.LoadingDidFinish, this.updateStatus, this);
+            this._resource.removeEventListener(WI.Resource.Event.LoadingDidFail, this.updateStatus, this);
         }
 
         this._updateSourceCode(resource);
@@ -128,11 +128,11 @@ WI.ResourceTreeElement = class ResourceTreeElement extends WI.SourceCodeTreeElem
 
         resource.addEventListener(WI.Resource.Event.URLDidChange, this._urlDidChange, this);
         resource.addEventListener(WI.Resource.Event.TypeDidChange, this._typeDidChange, this);
-        resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._updateStatus, this);
-        resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._updateStatus, this);
+        resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this.updateStatus, this);
+        resource.addEventListener(WI.Resource.Event.LoadingDidFail, this.updateStatus, this);
 
         this._updateTitles();
-        this._updateStatus();
+        this.updateStatus();
         this._updateToolTip();
     }
 
@@ -175,17 +175,13 @@ WI.ResourceTreeElement = class ResourceTreeElement extends WI.SourceCodeTreeElem
             this.callFirstAncestorFunction("descendantResourceTreeElementMainTitleDidChange", [this, oldMainTitle]);
     }
 
-    populateContextMenu(contextMenu, event)
+    updateStatus()
     {
-        WI.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
-
-        super.populateContextMenu(contextMenu, event);
-    }
+        super.updateStatus();
 
-    // Private
+        if (!this._resource)
+            return;
 
-    _updateStatus()
-    {
         if (this._resource.hadLoadingError())
             this.addClassName(WI.ResourceTreeElement.FailedStyleClassName);
         else
@@ -194,15 +190,33 @@ WI.ResourceTreeElement = class ResourceTreeElement extends WI.SourceCodeTreeElem
         if (this._resource.isLoading()) {
             if (!this.status || !this.status[WI.ResourceTreeElement.SpinnerSymbol]) {
                 let spinner = new WI.IndeterminateProgressSpinner;
-                this.status = spinner.element;
-                this.status[WI.ResourceTreeElement.SpinnerSymbol] = true;
+                if (this.status)
+                    this.statusElement.insertAdjacentElement("afterbegin", spinner.element);
+                else
+                    this.status = spinner.element;
+                this.status[WI.ResourceTreeElement.SpinnerSymbol] = spinner.element;
             }
         } else {
-            if (this.status && this.status[WI.ResourceTreeElement.SpinnerSymbol])
-                this.status = "";
+            if (this.status && this.status[WI.ResourceTreeElement.SpinnerSymbol]) {
+                if (this.status === this.status[WI.ResourceTreeElement.SpinnerSymbol])
+                    this.status = null;
+                else {
+                    this.status[WI.ResourceTreeElement.SpinnerSymbol].remove();
+                    this.status[WI.ResourceTreeElement.SpinnerSymbol] = null;
+                }
+            }
         }
     }
 
+    populateContextMenu(contextMenu, event)
+    {
+        WI.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
+
+        super.populateContextMenu(contextMenu, event);
+    }
+
+    // Private
+
     _updateToolTip()
     {
         this.tooltip = this._resource.displayURL;
diff --git a/Source/WebInspectorUI/UserInterface/Views/SourceCodeTreeElement.css b/Source/WebInspectorUI/UserInterface/Views/SourceCodeTreeElement.css
new file mode 100644 (file)
index 0000000..76d88a3
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+.tree-outline .item .status > .toggle-script-blackboxed {
+    width: 18px;
+    margin-top: 2px;
+    content: url(../Images/Hide.svg);
+}
+
+.tree-outline:not(.navigation-sidebar-panel-content-tree-outline) .item .status > .toggle-script-blackboxed,
+.tree-outline .item:not(:hover, .selected) .status > .toggle-script-blackboxed:not(.blackboxed) {
+    display: none;
+}
+
+.tree-outline:focus .item.selected .status > .toggle-script-blackboxed {
+    filter: invert();
+}
+
+.tree-outline .item .status > .toggle-script-blackboxed.blackboxed {
+    opacity: 0.5;
+}
+
+@media (prefers-color-scheme: dark) {
+    .tree-outline .item .status > .toggle-script-blackboxed {
+        filter: invert();
+    }
+}
index 87de40d..4c288aa 100644 (file)
@@ -128,6 +128,13 @@ WI.SourceCodeTreeElement = class SourceCodeTreeElement extends WI.FolderizedTree
             findAndCombineFolderChains(this.children[i], null);
     }
 
+    canSelectOnMouseDown(event)
+    {
+        if (this._toggleBlackboxedImageElement && this._toggleBlackboxedImageElement.contains(event.target))
+            return false;
+        return super.canSelectOnMouseDown(event);
+    }
+
     // Protected
 
     descendantResourceTreeElementTypeDidChange(childTreeElement, oldType)
@@ -150,6 +157,20 @@ WI.SourceCodeTreeElement = class SourceCodeTreeElement extends WI.FolderizedTree
             childTreeElement.revealAndSelect(true, false, true);
     }
 
+    updateStatus()
+    {
+        if (this._sourceCode.supportsScriptBlackboxing) {
+            if (!this._toggleBlackboxedImageElement) {
+                this._toggleBlackboxedImageElement = document.createElement("img");
+                this._toggleBlackboxedImageElement.classList.add("toggle-script-blackboxed");
+                this._toggleBlackboxedImageElement.addEventListener("click", this._handleToggleBlackboxedImageElementClicked.bind(this));
+            }
+
+            this.status = this._toggleBlackboxedImageElement;
+            this._updateToggleBlackboxImageElementState();
+        }
+    }
+
     // Protected (ResourceTreeElement calls this when its Resource changes dynamically for Frames)
 
     _updateSourceCode(sourceCode)
@@ -159,12 +180,44 @@ WI.SourceCodeTreeElement = class SourceCodeTreeElement extends WI.FolderizedTree
         if (this._sourceCode === sourceCode)
             return;
 
-        if (this._sourceCode)
+        let oldSupportsScriptBlackboxing = false;
+
+        if (this._sourceCode) {
+            oldSupportsScriptBlackboxing = this._sourceCode.supportsScriptBlackboxing;
+
             this._sourceCode.removeEventListener(WI.SourceCode.Event.SourceMapAdded, this.updateSourceMapResources, this);
+        }
 
         this._sourceCode = sourceCode;
         this._sourceCode.addEventListener(WI.SourceCode.Event.SourceMapAdded, this.updateSourceMapResources, this);
 
+        let newSupportsScriptBlackboxing = this._sourceCode.supportsScriptBlackboxing;
+        if (oldSupportsScriptBlackboxing !== newSupportsScriptBlackboxing) {
+            if (newSupportsScriptBlackboxing)
+                WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BlackboxedURLsChanged, this._updateToggleBlackboxImageElementState, this);
+            else
+                WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.BlackboxedURLsChanged, this._updateToggleBlackboxImageElementState, this);
+        }
+
         this.updateSourceMapResources();
+
+        this.updateStatus();
+    }
+
+    // Private
+
+    _updateToggleBlackboxImageElementState()
+    {
+        let isBlackboxed = WI.debuggerManager.isScriptBlackboxed(this._sourceCode);
+        this._toggleBlackboxedImageElement.classList.toggle("blackboxed", isBlackboxed);
+        this._toggleBlackboxedImageElement.title = isBlackboxed ? WI.UIString("Include script when debugging") : WI.UIString("Ignore script when debugging");
+    }
+
+    _handleToggleBlackboxedImageElementClicked(event)
+    {
+        let isBlackboxed = WI.debuggerManager.isScriptBlackboxed(this._sourceCode);
+        WI.debuggerManager.setShouldBlackboxScript(this._sourceCode, !isBlackboxed);
+
+        this._updateToggleBlackboxImageElementState();
     }
 };
index 4ba5f52..e245ad6 100644 (file)
@@ -1172,7 +1172,10 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         this._pauseReasonTreeOutline = null;
 
         this._updatePauseReasonGotoArrow();
-        return this._updatePauseReasonSection();
+
+        let target = WI.debuggerManager.activeCallFrame.target;
+        let targetData = WI.debuggerManager.dataForTarget(target);
+        return this._updatePauseReasonSection(target, targetData.pauseReason, targetData.pauseData);
     }
 
     _updatePauseReasonGotoArrow()
@@ -1194,12 +1197,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         this._pauseReasonLinkContainerElement.appendChild(linkElement);
     }
 
-    _updatePauseReasonSection()
+    _updatePauseReasonSection(target, pauseReason, pauseData)
     {
-        let target = WI.debuggerManager.activeCallFrame.target;
-        let targetData = WI.debuggerManager.dataForTarget(target);
-        let {pauseReason, pauseData} = targetData;
-
         switch (pauseReason) {
         case WI.DebuggerManager.PauseReason.AnimationFrame: {
             this._pauseReasonTreeOutline = this.createContentTreeOutline({suppressFiltering: true});
@@ -1227,6 +1226,20 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             this._pauseReasonGroup.rows = [this._pauseReasonTextRow];
             return true;
 
+        case WI.DebuggerManager.PauseReason.BlackboxedScript: {
+            console.assert(pauseData);
+            if (pauseData)
+                this._updatePauseReasonSection(target, WI.DebuggerManager.pauseReasonFromPayload(pauseData.originalReason), pauseData.originalData);
+
+            // Don't use `_pauseReasonTextRow` as it may have already been set.
+            let blackboxReasonTextRow = new WI.DetailsSectionTextRow(WI.UIString("Deferred pause from blackboxed script"));
+            blackboxReasonTextRow.__blackboxReason = true;
+
+            let existingRows = this._pauseReasonGroup.rows.filter((row) => !row.__blackboxReason);
+            this._pauseReasonGroup.rows = [blackboxReasonTextRow, ...existingRows];
+            return true;
+        }
+
         case WI.DebuggerManager.PauseReason.Breakpoint: {
             console.assert(pauseData, "Expected breakpoint identifier, but found none.");
             if (!pauseData || !pauseData.breakpointId)