blob: 4276799ac0857519acec60b77889d5df6011f73d [file] [log] [blame]
/*
* Copyright (C) 2013-2016 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.
*/
WI.DebuggerManager = class DebuggerManager extends WI.Object
{
constructor()
{
super();
WI.notifications.addEventListener(WI.Notification.DebugUIEnabledDidChange, this._debugUIEnabledDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.DisplayLocationDidChange, this._breakpointDisplayLocationDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._breakpointDisabledStateDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._breakpointEditablePropertyDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._breakpointEditablePropertyDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._breakpointEditablePropertyDidChange, this);
WI.Breakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleBreakpointActionsDidChange, this);
WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._targetRemoved, this);
WI.settings.pauseForInternalScripts.addEventListener(WI.Setting.Event.Changed, this._pauseForInternalScriptsDidChange, this);
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
this._breakpointsEnabledSetting = new WI.Setting("breakpoints-enabled", true);
this._allExceptionsBreakpointEnabledSetting = new WI.Setting("break-on-all-exceptions", false);
this._uncaughtExceptionsBreakpointEnabledSetting = new WI.Setting("break-on-uncaught-exceptions", false);
this._assertionFailuresBreakpointEnabledSetting = new WI.Setting("break-on-assertion-failures", false);
this._asyncStackTraceDepthSetting = new WI.Setting("async-stack-trace-depth", 200);
let specialBreakpointLocation = new WI.SourceCodeLocation(null, Infinity, Infinity);
this._allExceptionsBreakpoint = new WI.Breakpoint(specialBreakpointLocation, {
disabled: !this._allExceptionsBreakpointEnabledSetting.value,
});
this._allExceptionsBreakpoint.resolved = true;
this._uncaughtExceptionsBreakpoint = new WI.Breakpoint(specialBreakpointLocation, {
disabled: !this._uncaughtExceptionsBreakpointEnabledSetting.value,
});
this._uncaughtExceptionsBreakpoint.resolved = true;
this._assertionFailuresBreakpoint = new WI.Breakpoint(specialBreakpointLocation, {
disabled: !this._assertionFailuresBreakpointEnabledSetting.value,
});
this._assertionFailuresBreakpoint.resolved = true;
this._breakpoints = [];
this._breakpointContentIdentifierMap = new Multimap;
this._breakpointScriptIdentifierMap = new Multimap;
this._breakpointIdMap = new Map;
this._breakOnExceptionsState = "none";
this._updateBreakOnExceptionsState();
this._nextBreakpointActionIdentifier = 1;
this._activeCallFrame = null;
this._internalWebKitScripts = [];
this._targetDebuggerDataMap = new Map;
// Used to detect deleted probe actions.
this._knownProbeIdentifiersForBreakpoint = new Map;
// Main lookup tables for probes and probe sets.
this._probesByIdentifier = new Map;
this._probeSetsByBreakpoint = new Map;
// Restore the correct breakpoints enabled setting if Web Inspector had
// previously been left in a state where breakpoints were temporarily disabled.
this._temporarilyDisabledBreakpointsRestoreSetting = new WI.Setting("temporarily-disabled-breakpoints-restore", null);
if (this._temporarilyDisabledBreakpointsRestoreSetting.value !== null) {
this._breakpointsEnabledSetting.value = this._temporarilyDisabledBreakpointsRestoreSetting.value;
this._temporarilyDisabledBreakpointsRestoreSetting.value = null;
}
this._temporarilyDisableBreakpointsRequestCount = 0;
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
(async () => {
let existingSerializedBreakpoints = WI.Setting.migrateValue("breakpoints");
if (existingSerializedBreakpoints) {
for (let existingSerializedBreakpoint of existingSerializedBreakpoints)
await WI.objectStores.breakpoints.putObject(WI.Breakpoint.fromJSON(existingSerializedBreakpoint));
}
let serializedBreakpoints = await WI.objectStores.breakpoints.getAll();
this._restoringBreakpoints = true;
for (let serializedBreakpoint of serializedBreakpoints) {
let breakpoint = WI.Breakpoint.fromJSON(serializedBreakpoint);
const key = null;
WI.objectStores.breakpoints.associateObject(breakpoint, key, serializedBreakpoint);
this.addBreakpoint(breakpoint);
}
this._restoringBreakpoints = false;
})();
}
// Target
initializeTarget(target)
{
let targetData = this.dataForTarget(target);
// Initialize global state.
target.DebuggerAgent.enable();
target.DebuggerAgent.setBreakpointsActive(this._breakpointsEnabledSetting.value);
target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState);
// COMPATIBILITY (iOS 10): DebuggerAgent.setPauseOnAssertions did not exist yet.
if (target.DebuggerAgent.setPauseOnAssertions)
target.DebuggerAgent.setPauseOnAssertions(this._assertionFailuresBreakpointEnabledSetting.value);
// COMPATIBILITY (iOS 10): Debugger.setAsyncStackTraceDepth did not exist yet.
if (target.DebuggerAgent.setAsyncStackTraceDepth)
target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
// COMPATIBILITY (iOS 12): DebuggerAgent.setPauseForInternalScripts did not exist yet.
if (target.DebuggerAgent.setPauseForInternalScripts)
target.DebuggerAgent.setPauseForInternalScripts(WI.settings.pauseForInternalScripts.value);
if (this.paused)
targetData.pauseIfNeeded();
// Initialize breakpoints.
this._restoringBreakpoints = true;
for (let breakpoint of this._breakpoints) {
if (breakpoint.disabled)
continue;
if (!breakpoint.contentIdentifier)
continue;
this._setBreakpoint(breakpoint, target);
}
this._restoringBreakpoints = false;
}
// Public
get paused()
{
for (let [target, targetData] of this._targetDebuggerDataMap) {
if (targetData.paused)
return true;
}
return false;
}
get activeCallFrame()
{
return this._activeCallFrame;
}
set activeCallFrame(callFrame)
{
if (callFrame === this._activeCallFrame)
return;
this._activeCallFrame = callFrame || null;
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
}
dataForTarget(target)
{
let targetData = this._targetDebuggerDataMap.get(target);
if (targetData)
return targetData;
targetData = new WI.DebuggerData(target);
this._targetDebuggerDataMap.set(target, targetData);
return targetData;
}
get allExceptionsBreakpoint() { return this._allExceptionsBreakpoint; }
get uncaughtExceptionsBreakpoint() { return this._uncaughtExceptionsBreakpoint; }
get assertionFailuresBreakpoint() { return this._assertionFailuresBreakpoint; }
get breakpoints() { return this._breakpoints; }
breakpointForIdentifier(id)
{
return this._breakpointIdMap.get(id) || null;
}
breakpointsForSourceCode(sourceCode)
{
console.assert(sourceCode instanceof WI.Resource || sourceCode instanceof WI.Script);
if (sourceCode instanceof WI.SourceMapResource)
return Array.from(this.breakpointsForSourceCode(sourceCode.sourceMap.originalSourceCode)).filter((breakpoint) => breakpoint.sourceCodeLocation.displaySourceCode === sourceCode);
let contentIdentifierBreakpoints = this._breakpointContentIdentifierMap.get(sourceCode.contentIdentifier);
if (contentIdentifierBreakpoints) {
this._associateBreakpointsWithSourceCode(contentIdentifierBreakpoints, sourceCode);
return contentIdentifierBreakpoints;
}
if (sourceCode instanceof WI.Script) {
let scriptIdentifierBreakpoints = this._breakpointScriptIdentifierMap.get(sourceCode.id);
if (scriptIdentifierBreakpoints) {
this._associateBreakpointsWithSourceCode(scriptIdentifierBreakpoints, sourceCode);
return scriptIdentifierBreakpoints;
}
}
return [];
}
breakpointForSourceCodeLocation(sourceCodeLocation)
{
console.assert(sourceCodeLocation instanceof WI.SourceCodeLocation);
for (let breakpoint of this.breakpointsForSourceCode(sourceCodeLocation.sourceCode)) {
if (breakpoint.sourceCodeLocation.isEqual(sourceCodeLocation))
return breakpoint;
}
return null;
}
isBreakpointRemovable(breakpoint)
{
return breakpoint !== this._allExceptionsBreakpoint
&& breakpoint !== this._uncaughtExceptionsBreakpoint;
}
isBreakpointSpecial(breakpoint)
{
return !this.isBreakpointRemovable(breakpoint)
|| breakpoint === this._assertionFailuresBreakpoint;
}
isBreakpointEditable(breakpoint)
{
return !this.isBreakpointSpecial(breakpoint);
}
get breakpointsEnabled()
{
return this._breakpointsEnabledSetting.value;
}
set breakpointsEnabled(enabled)
{
if (this._breakpointsEnabledSetting.value === enabled)
return;
console.assert(!(enabled && this.breakpointsDisabledTemporarily), "Should not enable breakpoints when we are temporarily disabling breakpoints.");
if (enabled && this.breakpointsDisabledTemporarily)
return;
this._breakpointsEnabledSetting.value = enabled;
this._updateBreakOnExceptionsState();
for (let target of WI.targets) {
target.DebuggerAgent.setBreakpointsActive(enabled);
target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState);
}
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointsEnabledDidChange);
}
get breakpointsDisabledTemporarily()
{
return this._temporarilyDisabledBreakpointsRestoreSetting.value !== null;
}
scriptForIdentifier(id, target)
{
console.assert(target instanceof WI.Target);
return this.dataForTarget(target).scriptForIdentifier(id);
}
scriptsForURL(url, target)
{
// FIXME: This may not be safe. A Resource's URL may differ from a Script's URL.
console.assert(target instanceof WI.Target);
return this.dataForTarget(target).scriptsForURL(url);
}
get searchableScripts()
{
return this.knownNonResourceScripts.filter((script) => !!script.contentIdentifier);
}
get knownNonResourceScripts()
{
let knownScripts = [];
for (let [target, targetData] of this._targetDebuggerDataMap) {
for (let script of targetData.scripts) {
if (script.resource)
continue;
if (isWebInspectorConsoleEvaluationScript(script.sourceURL))
continue;
if (!WI.isDebugUIEnabled() && isWebKitInternalScript(script.sourceURL))
continue;
knownScripts.push(script);
}
}
return knownScripts;
}
get asyncStackTraceDepth()
{
return this._asyncStackTraceDepthSetting.value;
}
set asyncStackTraceDepth(x)
{
if (this._asyncStackTraceDepthSetting.value === x)
return;
this._asyncStackTraceDepthSetting.value = x;
for (let target of WI.targets)
target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
}
get probeSets()
{
return [...this._probeSetsByBreakpoint.values()];
}
probeForIdentifier(identifier)
{
return this._probesByIdentifier.get(identifier);
}
pause()
{
if (this.paused)
return Promise.resolve();
this.dispatchEventToListeners(WI.DebuggerManager.Event.WaitingToPause);
let listener = new WI.EventListener(this, true);
let managerResult = new Promise(function(resolve, reject) {
listener.connect(WI.debuggerManager, WI.DebuggerManager.Event.Paused, resolve);
});
let promises = [];
for (let [target, targetData] of this._targetDebuggerDataMap)
promises.push(targetData.pauseIfNeeded());
return Promise.all([managerResult, ...promises]);
}
resume()
{
if (!this.paused)
return Promise.resolve();
let listener = new WI.EventListener(this, true);
let managerResult = new Promise(function(resolve, reject) {
listener.connect(WI.debuggerManager, WI.DebuggerManager.Event.Resumed, resolve);
});
let promises = [];
for (let [target, targetData] of this._targetDebuggerDataMap)
promises.push(targetData.resumeIfNeeded());
return Promise.all([managerResult, ...promises]);
}
stepOver()
{
if (!this.paused)
return Promise.reject(new Error("Cannot step over because debugger is not paused."));
let listener = new WI.EventListener(this, true);
let managerResult = new Promise(function(resolve, reject) {
listener.connect(WI.debuggerManager, WI.DebuggerManager.Event.ActiveCallFrameDidChange, resolve);
});
let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepOver()
.catch(function(error) {
listener.disconnect();
console.error("DebuggerManager.stepOver failed: ", error);
throw error;
});
return Promise.all([managerResult, protocolResult]);
}
stepInto()
{
if (!this.paused)
return Promise.reject(new Error("Cannot step into because debugger is not paused."));
let listener = new WI.EventListener(this, true);
let managerResult = new Promise(function(resolve, reject) {
listener.connect(WI.debuggerManager, WI.DebuggerManager.Event.ActiveCallFrameDidChange, resolve);
});
let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepInto()
.catch(function(error) {
listener.disconnect();
console.error("DebuggerManager.stepInto failed: ", error);
throw error;
});
return Promise.all([managerResult, protocolResult]);
}
stepOut()
{
if (!this.paused)
return Promise.reject(new Error("Cannot step out because debugger is not paused."));
let listener = new WI.EventListener(this, true);
let managerResult = new Promise(function(resolve, reject) {
listener.connect(WI.debuggerManager, WI.DebuggerManager.Event.ActiveCallFrameDidChange, resolve);
});
let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepOut()
.catch(function(error) {
listener.disconnect();
console.error("DebuggerManager.stepOut failed: ", error);
throw error;
});
return Promise.all([managerResult, protocolResult]);
}
continueUntilNextRunLoop(target)
{
return this.dataForTarget(target).continueUntilNextRunLoop();
}
continueToLocation(script, lineNumber, columnNumber)
{
return script.target.DebuggerAgent.continueToLocation({scriptId: script.id, lineNumber, columnNumber});
}
addBreakpoint(breakpoint)
{
console.assert(breakpoint instanceof WI.Breakpoint);
if (!breakpoint)
return;
if (this.isBreakpointSpecial(breakpoint)) {
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointAdded, {breakpoint});
return;
}
if (breakpoint.contentIdentifier)
this._breakpointContentIdentifierMap.add(breakpoint.contentIdentifier, breakpoint);
if (breakpoint.scriptIdentifier)
this._breakpointScriptIdentifierMap.add(breakpoint.scriptIdentifier, breakpoint);
this._breakpoints.push(breakpoint);
if (!breakpoint.disabled) {
const specificTarget = undefined;
this._setBreakpoint(breakpoint, specificTarget);
}
if (!this._restoringBreakpoints)
WI.objectStores.breakpoints.putObject(breakpoint);
this._addProbesForBreakpoint(breakpoint);
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointAdded, {breakpoint});
}
removeBreakpoint(breakpoint)
{
console.assert(breakpoint instanceof WI.Breakpoint);
if (!breakpoint)
return;
console.assert(this.isBreakpointRemovable(breakpoint));
if (!this.isBreakpointRemovable(breakpoint))
return;
if (this.isBreakpointSpecial(breakpoint)) {
breakpoint.disabled = true;
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointRemoved, {breakpoint});
return;
}
this._breakpoints.remove(breakpoint);
if (breakpoint.identifier)
this._removeBreakpoint(breakpoint);
if (breakpoint.contentIdentifier)
this._breakpointContentIdentifierMap.delete(breakpoint.contentIdentifier, breakpoint);
if (breakpoint.scriptIdentifier)
this._breakpointScriptIdentifierMap.delete(breakpoint.scriptIdentifier, breakpoint);
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
breakpoint.disabled = true;
breakpoint.clearActions();
if (!this._restoringBreakpoints)
WI.objectStores.breakpoints.deleteObject(breakpoint);
this._removeProbesForBreakpoint(breakpoint);
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointRemoved, {breakpoint});
}
nextBreakpointActionIdentifier()
{
return this._nextBreakpointActionIdentifier++;
}
// Protected (Called from WI.DebuggerObserver)
breakpointResolved(target, breakpointIdentifier, location)
{
// Called from WI.DebuggerObserver.
let breakpoint = this._breakpointIdMap.get(breakpointIdentifier);
console.assert(breakpoint);
if (!breakpoint)
return;
console.assert(breakpoint.identifier === breakpointIdentifier);
if (!breakpoint.sourceCodeLocation.sourceCode) {
let sourceCodeLocation = this._sourceCodeLocationFromPayload(target, location);
breakpoint.sourceCodeLocation.sourceCode = sourceCodeLocation.sourceCode;
}
breakpoint.resolved = true;
}
reset()
{
// Called from WI.DebuggerObserver.
let wasPaused = this.paused;
WI.Script.resetUniqueDisplayNameNumbers();
this._internalWebKitScripts = [];
this._targetDebuggerDataMap.clear();
this._ignoreBreakpointDisplayLocationDidChangeEvent = true;
// Mark all the breakpoints as unresolved. They will be reported as resolved when
// breakpointResolved is called as the page loads.
for (let breakpoint of this._breakpoints) {
breakpoint.resolved = false;
if (breakpoint.sourceCodeLocation.sourceCode)
breakpoint.sourceCodeLocation.sourceCode = null;
}
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptsCleared);
if (wasPaused)
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
}
debuggerDidPause(target, callFramesPayload, reason, data, asyncStackTracePayload)
{
// Called from WI.DebuggerObserver.
if (this._delayedResumeTimeout) {
clearTimeout(this._delayedResumeTimeout);
this._delayedResumeTimeout = undefined;
}
let wasPaused = this.paused;
let targetData = this._targetDebuggerDataMap.get(target);
let callFrames = [];
let pauseReason = this._pauseReasonFromPayload(reason);
let pauseData = data || null;
for (var i = 0; i < callFramesPayload.length; ++i) {
var callFramePayload = callFramesPayload[i];
var sourceCodeLocation = this._sourceCodeLocationFromPayload(target, callFramePayload.location);
// FIXME: There may be useful call frames without a source code location (native callframes), should we include them?
if (!sourceCodeLocation)
continue;
if (!sourceCodeLocation.sourceCode)
continue;
// Exclude the case where the call frame is in the inspector code.
if (!WI.isDebugUIEnabled() && isWebKitInternalScript(sourceCodeLocation.sourceCode.sourceURL))
continue;
let scopeChain = this._scopeChainFromPayload(target, callFramePayload.scopeChain);
let callFrame = WI.CallFrame.fromDebuggerPayload(target, callFramePayload, scopeChain, sourceCodeLocation);
callFrames.push(callFrame);
}
let activeCallFrame = callFrames[0];
if (!activeCallFrame) {
// FIXME: This may not be safe for multiple threads/targets.
// This indicates we were pausing in internal scripts only (Injected Scripts).
// Just resume and skip past this pause. We should be fixing the backend to
// not send such pauses.
if (wasPaused)
target.DebuggerAgent.continueUntilNextRunLoop();
else
target.DebuggerAgent.resume();
this._didResumeInternal(target);
return;
}
let asyncStackTrace = WI.StackTrace.fromPayload(target, asyncStackTracePayload);
targetData.updateForPause(callFrames, pauseReason, pauseData, asyncStackTrace);
// Pause other targets because at least one target has paused.
// FIXME: Should this be done on the backend?
for (let [otherTarget, otherTargetData] of this._targetDebuggerDataMap)
otherTargetData.pauseIfNeeded();
let activeCallFrameDidChange = this._activeCallFrame && this._activeCallFrame.target === target;
if (activeCallFrameDidChange)
this._activeCallFrame = activeCallFrame;
else if (!wasPaused) {
this._activeCallFrame = activeCallFrame;
activeCallFrameDidChange = true;
}
if (!wasPaused)
this.dispatchEventToListeners(WI.DebuggerManager.Event.Paused);
this.dispatchEventToListeners(WI.DebuggerManager.Event.CallFramesDidChange, {target});
if (activeCallFrameDidChange)
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
}
debuggerDidResume(target)
{
// Called from WI.DebuggerObserver.
// COMPATIBILITY (iOS 10): Debugger.resumed event was ambiguous. When stepping
// we would receive a Debugger.resumed and we would not know if it really meant
// the backend resumed or would pause again due to a step. Legacy backends wait
// 50ms, and treat it as a real resume if we haven't paused in that time frame.
// This delay ensures the user interface does not flash between brief steps
// or successive breakpoints.
if (!target.DebuggerAgent.setPauseOnAssertions) {
this._delayedResumeTimeout = setTimeout(this._didResumeInternal.bind(this, target), 50);
return;
}
this._didResumeInternal(target);
}
playBreakpointActionSound(breakpointActionIdentifier)
{
// Called from WI.DebuggerObserver.
InspectorFrontendHost.beep();
}
scriptDidParse(target, scriptIdentifier, url, startLine, startColumn, endLine, endColumn, isModule, isContentScript, sourceURL, sourceMapURL)
{
// Called from WI.DebuggerObserver.
// Don't add the script again if it is already known.
let targetData = this.dataForTarget(target);
let existingScript = targetData.scriptForIdentifier(scriptIdentifier);
if (existingScript) {
console.assert(existingScript.url === (url || null));
console.assert(existingScript.range.startLine === startLine);
console.assert(existingScript.range.startColumn === startColumn);
console.assert(existingScript.range.endLine === endLine);
console.assert(existingScript.range.endColumn === endColumn);
return;
}
if (!WI.isDebugUIEnabled() && isWebKitInternalScript(sourceURL))
return;
let range = new WI.TextRange(startLine, startColumn, endLine, endColumn);
let sourceType = isModule ? WI.Script.SourceType.Module : WI.Script.SourceType.Program;
let script = new WI.Script(target, scriptIdentifier, range, url, sourceType, isContentScript, sourceURL, sourceMapURL);
targetData.addScript(script);
// FIXME: <https://webkit.org/b/164427> Web Inspector: WorkerTarget's mainResource should be a Resource not a Script
// We make the main resource of a WorkerTarget the Script instead of the Resource
// because the frontend may not be informed of the Resource. We should guarantee
// the frontend is informed of the Resource.
if (WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) {
// A ServiceWorker starts with a LocalScript for the main resource but we can replace it during initialization.
if (target.mainResource instanceof WI.LocalScript) {
if (script.url === target.name)
target.mainResource = script;
}
} else if (!target.mainResource && target !== WI.mainTarget) {
// A Worker starts without a main resource and we insert one.
if (script.url === target.name) {
target.mainResource = script;
if (script.resource)
target.resourceCollection.remove(script.resource);
}
}
if (isWebKitInternalScript(script.sourceURL)) {
this._internalWebKitScripts.push(script);
if (!WI.isDebugUIEnabled())
return;
}
// Console expressions are not added to the UI by default.
if (isWebInspectorConsoleEvaluationScript(script.sourceURL))
return;
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
if ((target !== WI.mainTarget || WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) && !script.isMainResource() && !script.resource)
target.addScript(script);
}
scriptDidFail(target, url, scriptSource)
{
const sourceType = WI.Script.SourceType.Program;
let script = new WI.LocalScript(target, url, sourceType, scriptSource);
// If there is already a resource we don't need to have the script anymore,
// we only need a script to use for parser error location links.
if (script.resource)
return;
let targetData = this.dataForTarget(target);
targetData.addScript(script);
target.addScript(script);
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
}
didSampleProbe(target, sample)
{
console.assert(this._probesByIdentifier.has(sample.probeId), "Unknown probe identifier specified for sample: ", sample);
let probe = this._probesByIdentifier.get(sample.probeId);
let elapsedTime = WI.timelineManager.computeElapsedTime(sample.timestamp);
let object = WI.RemoteObject.fromPayload(sample.payload, target);
probe.addSample(new WI.ProbeSample(sample.sampleId, sample.batchId, elapsedTime, object));
}
// Private
_sourceCodeLocationFromPayload(target, payload)
{
let targetData = this.dataForTarget(target);
let script = targetData.scriptForIdentifier(payload.scriptId);
if (!script)
return null;
return script.createSourceCodeLocation(payload.lineNumber, payload.columnNumber);
}
_scopeChainFromPayload(target, payload)
{
let scopeChain = [];
for (let i = 0; i < payload.length; ++i)
scopeChain.push(this._scopeChainNodeFromPayload(target, payload[i]));
return scopeChain;
}
_scopeChainNodeFromPayload(target, payload)
{
var type = null;
switch (payload.type) {
case DebuggerAgent.ScopeType.Global:
type = WI.ScopeChainNode.Type.Global;
break;
case DebuggerAgent.ScopeType.With:
type = WI.ScopeChainNode.Type.With;
break;
case DebuggerAgent.ScopeType.Closure:
type = WI.ScopeChainNode.Type.Closure;
break;
case DebuggerAgent.ScopeType.Catch:
type = WI.ScopeChainNode.Type.Catch;
break;
case DebuggerAgent.ScopeType.FunctionName:
type = WI.ScopeChainNode.Type.FunctionName;
break;
case DebuggerAgent.ScopeType.NestedLexical:
type = WI.ScopeChainNode.Type.Block;
break;
case DebuggerAgent.ScopeType.GlobalLexicalEnvironment:
type = WI.ScopeChainNode.Type.GlobalLexicalEnvironment;
break;
// COMPATIBILITY (iOS 9): Debugger.ScopeType.Local used to be provided by the backend.
// Newer backends no longer send this enum value, it should be computed by the frontend.
// Map this to "Closure" type. The frontend can recalculate this when needed.
case DebuggerAgent.ScopeType.Local:
type = WI.ScopeChainNode.Type.Closure;
break;
default:
console.error("Unknown type: " + payload.type);
}
let object = WI.RemoteObject.fromPayload(payload.object, target);
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.PauseOnNextStatement:
return WI.DebuggerManager.PauseReason.PauseOnNextStatement;
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) {
case WI.BreakpointAction.Type.Log:
return DebuggerAgent.BreakpointActionType.Log;
case WI.BreakpointAction.Type.Evaluate:
return DebuggerAgent.BreakpointActionType.Evaluate;
case WI.BreakpointAction.Type.Sound:
return DebuggerAgent.BreakpointActionType.Sound;
case WI.BreakpointAction.Type.Probe:
return DebuggerAgent.BreakpointActionType.Probe;
default:
console.assert(false);
return DebuggerAgent.BreakpointActionType.Log;
}
}
_debuggerBreakpointOptions(breakpoint)
{
let actions = breakpoint.actions;
actions = actions.map((action) => action.toProtocol());
actions = actions.filter((action) => {
if (action.type !== WI.BreakpointAction.Type.Log)
return true;
if (!/\$\{.*?\}/.test(action.data))
return true;
let lexer = new WI.BreakpointLogMessageLexer;
let tokens = lexer.tokenize(action.data);
if (!tokens)
return false;
let templateLiteral = tokens.reduce((text, token) => {
if (token.type === WI.BreakpointLogMessageLexer.TokenType.PlainText)
return text + token.data.escapeCharacters("`\\");
if (token.type === WI.BreakpointLogMessageLexer.TokenType.Expression)
return text + "${" + token.data + "}";
return text;
}, "");
action.data = "console.log(`" + templateLiteral + "`)";
action.type = WI.BreakpointAction.Type.Evaluate;
return true;
});
return {
condition: breakpoint.condition,
ignoreCount: breakpoint.ignoreCount,
autoContinue: breakpoint.autoContinue,
actions,
};
}
_setBreakpoint(breakpoint, specificTarget)
{
console.assert(!breakpoint.disabled);
if (breakpoint.disabled)
return;
if (!this._restoringBreakpoints && !this.breakpointsDisabledTemporarily) {
// Enable breakpoints since a breakpoint is being set. This eliminates
// a multi-step process for the user that can be confusing.
this.breakpointsEnabled = true;
}
function didSetBreakpoint(target, error, breakpointIdentifier, locations) {
if (error) {
WI.reportInternalError(error);
return;
}
this._breakpointIdMap.set(breakpointIdentifier, breakpoint);
breakpoint.identifier = breakpointIdentifier;
// Debugger.setBreakpoint returns a single location.
if (!(locations instanceof Array))
locations = [locations];
for (let location of locations)
this.breakpointResolved(target, breakpointIdentifier, location);
}
// The breakpoint will be resolved again by calling DebuggerAgent, so mark it as unresolved.
// If something goes wrong it will stay unresolved and show up as such in the user interface.
// When setting for a new target, don't change the resolved target.
if (!specificTarget)
breakpoint.resolved = false;
// Convert BreakpointAction types to DebuggerAgent protocol types.
// NOTE: Breakpoint.options returns new objects each time, so it is safe to modify.
let options = this._debuggerBreakpointOptions(breakpoint);
for (let action of options.actions)
action.type = this._debuggerBreakpointActionType(action.type);
if (breakpoint.contentIdentifier) {
let targets = specificTarget ? [specificTarget] : WI.targets;
for (let target of targets) {
target.DebuggerAgent.setBreakpointByUrl.invoke({
lineNumber: breakpoint.sourceCodeLocation.lineNumber,
url: breakpoint.contentIdentifier,
urlRegex: undefined,
columnNumber: breakpoint.sourceCodeLocation.columnNumber,
options
}, didSetBreakpoint.bind(this, target), target.DebuggerAgent);
}
} else if (breakpoint.scriptIdentifier) {
let target = breakpoint.target;
target.DebuggerAgent.setBreakpoint.invoke({
location: {scriptId: breakpoint.scriptIdentifier, lineNumber: breakpoint.sourceCodeLocation.lineNumber, columnNumber: breakpoint.sourceCodeLocation.columnNumber},
options
}, didSetBreakpoint.bind(this, target), target.DebuggerAgent);
} else
WI.reportInternalError("Unknown source for breakpoint.");
}
_removeBreakpoint(breakpoint, callback)
{
if (!breakpoint.identifier)
return;
function didRemoveBreakpoint(error)
{
if (error)
console.error(error);
this._breakpointIdMap.delete(breakpoint.identifier);
breakpoint.identifier = null;
// Don't reset resolved here since we want to keep disabled breakpoints looking like they
// are resolved in the user interface. They will get marked as unresolved in reset.
if (typeof callback === "function")
callback();
}
if (breakpoint.contentIdentifier) {
for (let target of WI.targets)
target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this));
} else if (breakpoint.scriptIdentifier) {
let target = breakpoint.target;
target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this));
}
}
_breakpointDisplayLocationDidChange(event)
{
if (this._ignoreBreakpointDisplayLocationDidChangeEvent)
return;
let breakpoint = event.target;
if (!breakpoint.identifier || breakpoint.disabled)
return;
// Remove the breakpoint with its old id.
this._removeBreakpoint(breakpoint, () => {
// Add the breakpoint at its new lineNumber and get a new id.
this._restoringBreakpoints = true;
this._setBreakpoint(breakpoint);
this._restoringBreakpoints = false;
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointMoved, {breakpoint});
});
}
_breakpointDisabledStateDidChange(event)
{
let breakpoint = event.target;
if (breakpoint === this._allExceptionsBreakpoint) {
if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily)
this.breakpointsEnabled = true;
this._allExceptionsBreakpointEnabledSetting.value = !breakpoint.disabled;
this._updateBreakOnExceptionsState();
for (let target of WI.targets)
target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState);
return;
}
if (breakpoint === this._uncaughtExceptionsBreakpoint) {
if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily)
this.breakpointsEnabled = true;
this._uncaughtExceptionsBreakpointEnabledSetting.value = !breakpoint.disabled;
this._updateBreakOnExceptionsState();
for (let target of WI.targets)
target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState);
return;
}
if (breakpoint === this._assertionFailuresBreakpoint) {
if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily)
this.breakpointsEnabled = true;
this._assertionFailuresBreakpointEnabledSetting.value = !breakpoint.disabled;
for (let target of WI.targets)
target.DebuggerAgent.setPauseOnAssertions(this._assertionFailuresBreakpointEnabledSetting.value);
return;
}
if (!this._restoringBreakpoints)
WI.objectStores.breakpoints.putObject(breakpoint);
if (breakpoint.disabled)
this._removeBreakpoint(breakpoint);
else
this._setBreakpoint(breakpoint);
}
_breakpointEditablePropertyDidChange(event)
{
let breakpoint = event.target;
if (!this._restoringBreakpoints)
WI.objectStores.breakpoints.putObject(breakpoint);
if (breakpoint.disabled)
return;
console.assert(this.isBreakpointEditable(breakpoint));
if (!this.isBreakpointEditable(breakpoint))
return;
// Remove the breakpoint with its old id.
this._removeBreakpoint(breakpoint, () => {
// Add the breakpoint with its new properties and get a new id.
this._restoringBreakpoints = true;
this._setBreakpoint(breakpoint);
this._restoringBreakpoints = false;
});
}
_handleBreakpointActionsDidChange(event)
{
this._breakpointEditablePropertyDidChange(event);
this._updateProbesForBreakpoint(event.target);
}
_startDisablingBreakpointsTemporarily()
{
if (++this._temporarilyDisableBreakpointsRequestCount > 1)
return;
console.assert(!this.breakpointsDisabledTemporarily, "Already temporarily disabling breakpoints.");
if (this.breakpointsDisabledTemporarily)
return;
this._temporarilyDisabledBreakpointsRestoreSetting.value = this._breakpointsEnabledSetting.value;
this.breakpointsEnabled = false;
}
_stopDisablingBreakpointsTemporarily()
{
this._temporarilyDisableBreakpointsRequestCount = Math.max(0, this._temporarilyDisableBreakpointsRequestCount - 1);
if (this._temporarilyDisableBreakpointsRequestCount > 0)
return;
console.assert(this.breakpointsDisabledTemporarily, "Was not temporarily disabling breakpoints.");
if (!this.breakpointsDisabledTemporarily)
return;
let restoreState = this._temporarilyDisabledBreakpointsRestoreSetting.value;
this._temporarilyDisabledBreakpointsRestoreSetting.value = null;
this.breakpointsEnabled = restoreState;
}
_handleTimelineCapturingStateChanged(event)
{
switch (WI.timelineManager.capturingState) {
case WI.TimelineManager.CapturingState.Starting:
this._startDisablingBreakpointsTemporarily();
if (this.paused)
this.resume();
break;
case WI.TimelineManager.CapturingState.Inactive:
this._stopDisablingBreakpointsTemporarily();
break;
}
}
_handleAuditManagerTestScheduled(event)
{
this._startDisablingBreakpointsTemporarily();
if (this.paused)
this.resume();
}
_handleAuditManagerTestCompleted(event)
{
this._stopDisablingBreakpointsTemporarily();
}
_targetRemoved(event)
{
let wasPaused = this.paused;
this._targetDebuggerDataMap.delete(event.data.target);
if (!this.paused && wasPaused)
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
}
_pauseForInternalScriptsDidChange(event)
{
for (let target of WI.targets) {
if (target.DebuggerAgent.setPauseForInternalScripts)
target.DebuggerAgent.setPauseForInternalScripts(WI.settings.pauseForInternalScripts.value);
}
}
_mainResourceDidChange(event)
{
if (!event.target.isMainFrame())
return;
this._didResumeInternal(WI.mainTarget);
}
_didResumeInternal(target)
{
if (!this.paused)
return;
if (this._delayedResumeTimeout) {
clearTimeout(this._delayedResumeTimeout);
this._delayedResumeTimeout = undefined;
}
let activeCallFrameDidChange = false;
if (this._activeCallFrame && this._activeCallFrame.target === target) {
this._activeCallFrame = null;
activeCallFrameDidChange = true;
}
this.dataForTarget(target).updateForResume();
if (!this.paused)
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
this.dispatchEventToListeners(WI.DebuggerManager.Event.CallFramesDidChange, {target});
if (activeCallFrameDidChange)
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
}
_updateBreakOnExceptionsState()
{
let state = "none";
if (this._breakpointsEnabledSetting.value) {
if (!this._allExceptionsBreakpoint.disabled)
state = "all";
else if (!this._uncaughtExceptionsBreakpoint.disabled)
state = "uncaught";
}
this._breakOnExceptionsState = state;
switch (state) {
case "all":
// Mark the uncaught breakpoint as unresolved since "all" includes "uncaught".
// That way it is clear in the user interface that the breakpoint is ignored.
this._uncaughtExceptionsBreakpoint.resolved = false;
break;
case "uncaught":
case "none":
// Mark the uncaught breakpoint as resolved again.
this._uncaughtExceptionsBreakpoint.resolved = true;
break;
}
}
_associateBreakpointsWithSourceCode(breakpoints, sourceCode)
{
this._ignoreBreakpointDisplayLocationDidChangeEvent = true;
for (let breakpoint of breakpoints) {
if (!breakpoint.sourceCodeLocation.sourceCode)
breakpoint.sourceCodeLocation.sourceCode = sourceCode;
// SourceCodes can be unequal if the SourceCodeLocation is associated with a Script and we are looking at the Resource.
console.assert(breakpoint.sourceCodeLocation.sourceCode === sourceCode || breakpoint.sourceCodeLocation.sourceCode.contentIdentifier === sourceCode.contentIdentifier);
}
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
}
_addProbesForBreakpoint(breakpoint)
{
if (this._knownProbeIdentifiersForBreakpoint.has(breakpoint))
return;
this._knownProbeIdentifiersForBreakpoint.set(breakpoint, new Set);
this._updateProbesForBreakpoint(breakpoint);
}
_removeProbesForBreakpoint(breakpoint)
{
console.assert(this._knownProbeIdentifiersForBreakpoint.has(breakpoint));
this._updateProbesForBreakpoint(breakpoint);
this._knownProbeIdentifiersForBreakpoint.delete(breakpoint);
}
_updateProbesForBreakpoint(breakpoint)
{
let knownProbeIdentifiers = this._knownProbeIdentifiersForBreakpoint.get(breakpoint);
if (!knownProbeIdentifiers) {
// Sometimes actions change before the added breakpoint is fully dispatched.
this._addProbesForBreakpoint(breakpoint);
return;
}
let seenProbeIdentifiers = new Set;
for (let probeAction of breakpoint.probeActions) {
let probeIdentifier = probeAction.id;
console.assert(probeIdentifier, "Probe added without breakpoint action identifier: ", breakpoint);
seenProbeIdentifiers.add(probeIdentifier);
if (!knownProbeIdentifiers.has(probeIdentifier)) {
// New probe; find or create relevant probe set.
knownProbeIdentifiers.add(probeIdentifier);
let probeSet = this._probeSetForBreakpoint(breakpoint);
let newProbe = new WI.Probe(probeIdentifier, breakpoint, probeAction.data);
this._probesByIdentifier.set(probeIdentifier, newProbe);
probeSet.addProbe(newProbe);
break;
}
let probe = this._probesByIdentifier.get(probeIdentifier);
console.assert(probe, "Probe known but couldn't be found by identifier: ", probeIdentifier);
// Update probe expression; if it differed, change events will fire.
probe.expression = probeAction.data;
}
// Look for missing probes based on what we saw last.
for (let probeIdentifier of knownProbeIdentifiers) {
if (seenProbeIdentifiers.has(probeIdentifier))
break;
// The probe has gone missing, remove it.
let probeSet = this._probeSetForBreakpoint(breakpoint);
let probe = this._probesByIdentifier.get(probeIdentifier);
this._probesByIdentifier.delete(probeIdentifier);
knownProbeIdentifiers.delete(probeIdentifier);
probeSet.removeProbe(probe);
// Remove the probe set if it has become empty.
if (!probeSet.probes.length) {
this._probeSetsByBreakpoint.delete(probeSet.breakpoint);
probeSet.willRemove();
this.dispatchEventToListeners(WI.DebuggerManager.Event.ProbeSetRemoved, {probeSet});
}
}
}
_probeSetForBreakpoint(breakpoint)
{
let probeSet = this._probeSetsByBreakpoint.get(breakpoint);
if (!probeSet) {
probeSet = new WI.ProbeSet(breakpoint);
this._probeSetsByBreakpoint.set(breakpoint, probeSet);
this.dispatchEventToListeners(WI.DebuggerManager.Event.ProbeSetAdded, {probeSet});
}
return probeSet;
}
_debugUIEnabledDidChange()
{
let eventType = WI.isDebugUIEnabled() ? WI.DebuggerManager.Event.ScriptAdded : WI.DebuggerManager.Event.ScriptRemoved;
for (let script of this._internalWebKitScripts)
this.dispatchEventToListeners(eventType, {script});
}
};
WI.DebuggerManager.Event = {
BreakpointAdded: "debugger-manager-breakpoint-added",
BreakpointRemoved: "debugger-manager-breakpoint-removed",
BreakpointMoved: "debugger-manager-breakpoint-moved",
WaitingToPause: "debugger-manager-waiting-to-pause",
Paused: "debugger-manager-paused",
Resumed: "debugger-manager-resumed",
CallFramesDidChange: "debugger-manager-call-frames-did-change",
ActiveCallFrameDidChange: "debugger-manager-active-call-frame-did-change",
ScriptAdded: "debugger-manager-script-added",
ScriptRemoved: "debugger-manager-script-removed",
ScriptsCleared: "debugger-manager-scripts-cleared",
BreakpointsEnabledDidChange: "debugger-manager-breakpoints-enabled-did-change",
ProbeSetAdded: "debugger-manager-probe-set-added",
ProbeSetRemoved: "debugger-manager-probe-set-removed",
};
WI.DebuggerManager.PauseReason = {
AnimationFrame: "animation-frame",
Assertion: "assertion",
Breakpoint: "breakpoint",
CSPViolation: "CSP-violation",
DebuggerStatement: "debugger-statement",
DOM: "DOM",
EventListener: "event-listener",
Exception: "exception",
Fetch: "fetch",
PauseOnNextStatement: "pause-on-next-statement",
Timer: "timer",
XHR: "xhr",
Other: "other",
};