IEDiagnosticsAdapter/IEWebKitImpl/debugger.ts

761 строка
28 KiB
TypeScript

//
// Copyright (C) Microsoft. All rights reserved.
//
/// <reference path="Interfaces.d.ts"/>
/// <reference path="Common.ts"/>
module IEDiagnosticsAdapter {
"use strict";
enum BreakResumeAction {
Abort,
Continue,
StepInto,
StepOver,
StepOut,
Ignore, // Continue, but with state (fast refresh)
StepDocument // Step to document boundary for JMC
}
enum ConnectionResult {
Succeeded = 0,
Failed = 1,
FailedAlreadyAttached = 2
}
enum BreakReason {
Step, // User stepped
Breakpoint, // Hit explicit breakpoint
DebuggerBlock, // Debugger is breaking another thread
HostInitiated, // Host (e.g. mshtml) initiated a breakpoint - not currently used
LanguageInitiated, // "debugger;"
DebuggerHalt, // Pause button
Error, // Exception
Jit, // JIT dialog
MutationBreakpoint // Hit mutation breakpoint
}
enum MutationType {
None = 0,
Update = 1,
Delete = 1 << 1,
All = Update | Delete
}
enum CauseBreakAction {
BreakOnAny = 0,
BreakOnAnyNewWorkerStarting = 1,
BreakIntoSpecificWorker = 2,
UnsetBreakOnAnyNewWorkerStarting = 3
}
interface ISourceLocation {
docId: number;
start: number;
length: number;
}
interface IStackFrame {
callFrameId: number;
functionName: string;
isInTryBlock: boolean;
isInternal: boolean;
location: ISourceLocation;
}
interface IGetSourceTextResult {
loadFailed: boolean;
text: string;
}
interface IBreakpointInfo {
location: ISourceLocation;
eventTypes: string[];
breakpointId: string;
isBound: boolean;
isEnabled?: boolean;
condition?: string;
isTracepoint?: boolean;
failed?: boolean;
isPseudoBreakpoint?: boolean;
}
interface IResolvedBreakpointInfo {
breakpointId: number;
newDocId: number;
start: number;
length: number;
isBound: boolean;
}
interface IBreakEventInfo {
firstFrameId: number;
errorId: number;
breakReason: BreakReason;
description: string;
isFirstChanceException: boolean;
isUserUnhandled: boolean;
breakpoints?: IBreakpointInfo[];
systemThreadId?: number;
breakEventType: string;
mutationBreakpointId: number;
mutationType: MutationType;
}
interface IDocument {
docId: number;
parentDocId: number;
url: string;
mimeType: string;
length: number;
isDynamicCode: boolean;
headers: string[];
sourceMapUrlFromHeader: string;
longDocumentId: number;
}
interface IPropertyInfo {
propertyId: string;
name: string;
type: string;
fullName: string;
value: string;
expandable: boolean;
readOnly: boolean;
fake: boolean;
invalid: boolean;
returnValue: boolean;
}
interface IPropertyInfoContainer {
propInfos: IPropertyInfo[];
hasAdditionalChildren: boolean;
}
interface ISetMutationBreakpointResult {
success: boolean;
breakpointId: any;
objectName: string;
}
interface IDebuggerDispatch {
addEventListener(type: string, listener: Function): void;
addEventListener(type: "onAddDocuments", listener: (documents: IDocument[]) => void): void;
addEventListener(type: "onRemoveDocuments", listener: (docIds: number[]) => void): void;
addEventListener(type: "onUpdateDocuments", listener: (documents: IDocument[]) => void): void;
addEventListener(type: "onResolveBreakpoints", listener: (breakpoints: IResolvedBreakpointInfo[]) => void): void;
addEventListener(type: "onBreak", listener: (breakEventInfo: IBreakEventInfo) => void): void;
removeEventListener(type: string, listener: Function): void;
removeEventListener(type: "onAddDocuments", listener: (documents: IDocument[]) => void): void;
removeEventListener(type: "onRemoveDocuments", listener: (docIds: number[]) => void): void;
removeEventListener(type: "onUpdateDocuments", listener: (documents: IDocument[]) => void): void;
removeEventListener(type: "onResolveBreakpoints", listener: (breakpoints: IResolvedBreakpointInfo[]) => void): void;
removeEventListener(type: "onBreak", listener: (breakEventInfo: IBreakEventInfo) => void): void;
enable(): boolean;
disable(): boolean;
isEnabled(): boolean;
connect(enable: boolean): ConnectionResult;
disconnect(): boolean;
shutdown(): boolean;
isConnected(): boolean;
causeBreak(causeBreakAction: CauseBreakAction, workerId: number): boolean;
resume(breakResumeAction: BreakResumeAction): boolean;
addCodeBreakpoint(docId: number, start: number, condition: string, isTracepoint: boolean): IBreakpointInfo;
addEventBreakpoint(eventTypes: string[], isEnabled: boolean, condition: string, isTracepoint: boolean): IBreakpointInfo;
addPendingBreakpoint(url: string, start: number, condition: string, isEnabled: boolean, isTracepoint: boolean): number;
removeBreakpoint(breakpointId: number): boolean;
updateBreakpoint(breakpointId: number, condition: string, isTracepoint: boolean): boolean;
setBreakpointEnabledState(breakpointId: number, enable: boolean): boolean;
getBreakpointIdFromSourceLocation(docId: number, start: number): number;
getThreadDescription(): string;
getThreads(): string[];
getFrames(framesNeeded: number): IStackFrame[];
getSourceText(docId: number): IGetSourceTextResult;
getLocals(frameId: number): number; /* propertyNum */
eval(frameId: number, evalString: string): IPropertyInfo;
getChildProperties(propertyId: number, start: number, length: number): IPropertyInfoContainer;
setPropertyValueAsString(propertyId: number, value: string): boolean;
canSetNextStatement(docId: number, position: number): boolean;
setNextStatement(docId: number, position: number): boolean;
setBreakOnFirstChanceExceptions(value: boolean): boolean;
canSetMutationBreakpoint(propertyId: number, setOnObject: boolean, mutationType: MutationType): boolean;
setMutationBreakpoint(propertyId: number, setOnObject: boolean, mutationType: MutationType): ISetMutationBreakpointResult;
deleteMutationBreakpoint(breakpointId: number): boolean;
setMutationBreakpointEnabledState(breakpointId: number, enabled: boolean): boolean;
}
interface IWebKitPropResult {
wasThrown: boolean;
result: IWebKitRemoteObject;
}
declare var host: IProxyDebuggerDispatch;
declare var debug: IDebuggerDispatch;
class DebuggerProxy {
private _debugger: IDebuggerDispatch;
private _isAtBreakpoint: boolean;
private _isAwaitingDebuggerEnableCall: boolean;
private _isEnabled: boolean;
private _documentMap: Map<string, number>;
private _lineEndingsMap: Map<number, number[]>;
private _intellisenseExpression: string;
private _intellisenseFrame: any;
constructor() {
this._debugger = debug;
this._isAtBreakpoint = false;
this._documentMap = new Map<string, number>();
this._lineEndingsMap = new Map<number, number[]>();
this._intellisenseFrame = null;
this._intellisenseExpression = "";
// Hook up notifications
this._debugger.addEventListener("onAddDocuments", (documents: IDocument[]) => this.onAddDocuments(documents));
this._debugger.addEventListener("onRemoveDocuments", (docIds: number[]) => this.onRemoveDocuments(docIds));
this._debugger.addEventListener("onUpdateDocuments", (documents: IDocument[]) => this.onUpdateDocuments(documents));
this._debugger.addEventListener("onResolveBreakpoints", (breakpoints: IResolvedBreakpointInfo[]) => this.onResolveBreakpoints(breakpoints));
this._debugger.addEventListener("onBreak", (breakEventInfo: IBreakEventInfo) => this.onBreak(breakEventInfo));
host.addEventListener("onmessage", (data: string) => this.onMessage(data));
}
private onMessage(data: string): void {
// Try to parse the requested command
var request: IWebKitRequest = null;
try {
request = <IWebKitRequest>JSON.parse(data);
} catch (ex) {
this.postResponse(0, {
error: { description: "Invalid request" }
});
return;
}
// Process a successful request
if (request) {
var methodParts = request.method.split(".");
if (!this._isAtBreakpoint && methodParts[0] !== "Debugger" && methodParts[0] !== "Custom") {
return host.postMessageToEngine("browser", this._isAtBreakpoint, JSON.stringify(request));
}
switch (methodParts[0]) {
case "Runtime":
this.processRuntime(methodParts[1], request);
break;
case "Debugger":
this.processDebugger(methodParts[1], request);
break;
case "Custom":
this.processCustom(methodParts[1], request);
break;
default:
return host.postMessageToEngine("browser", this._isAtBreakpoint, JSON.stringify(request));
}
}
}
private getLineEndings(docId: number, text?: string): number[] {
if (!this._lineEndingsMap.has(docId)) {
var textResult = text || this._debugger.getSourceText(docId).text;
if (textResult) {
var total = [];
var lines = textResult.split(/\r\n|\n|\r/);
for (var i = 0; i < lines.length; i++) {
total.push(lines[i].length + 2);
}
this._lineEndingsMap.set(docId, total);
} else {
this._lineEndingsMap.set(docId, [0]);
}
}
return this._lineEndingsMap.get(docId);
}
private getLineColumnFromOffset(docId: number, offset: number): any {
var lineEndings = this.getLineEndings(docId);
var columnNumber = 0;
var lineNumber = 0;
var charCount = 0;
for (var i = 0; i < lineEndings.length; i++) {
charCount += lineEndings[i];
if (offset < charCount) {
lineNumber = i;
columnNumber = charCount - offset;
break;
}
}
return { lineNumber: lineNumber, columnNumber: columnNumber, scriptId: "" + docId };
}
private getRemoteObjectFromProp(prop: IPropertyInfo): IWebKitPropResult {
var type = prop.type.toLowerCase();
var subType = null;
var value: any;
var index = type.indexOf(",");
if (index !== -1) {
type = "object";
subType = type.substring(index + 2).toLowerCase();
}
if (subType === "function") {
type = "function";
subType = null;
}
if (type === "null") {
type = "object";
subType = "null";
}
if (type === "object" && prop.value === "undefined") {
type = "undefined";
subType = null;
}
var wasThrown = false;
if (type === "error") {
type = "object";
wasThrown = true;
}
if (type === "number") {
value = parseFloat(prop.value);
}
if (typeof prop.value === "string" && prop.value.length > 2 && prop.value.indexOf("\"") === 0 && prop.value.lastIndexOf("\"") === prop.value.length - 1) {
prop.value = prop.value.substring(1, prop.value.length - 1);
}
var resultDesc = {
objectId: (prop.expandable ? "" + prop.propertyId : null),
type: type,
value: (typeof value !== "undefined" ? value : prop.value),
description: prop.value.toString()
};
if (type === "object") {
(<any>resultDesc).className = "Object";
(<any>resultDesc).subType = subType;
}
return {
wasThrown: wasThrown,
result: resultDesc
};
}
private postResponse(id: number, value: IWebKitResult): void {
// Send the response back over the websocket
var response: IWebKitResponse = Common.createResponse(id, value);
host.postMessage(JSON.stringify(response));
}
private postNotification(method: string, params: any): void {
var notification: IWebKitNotification = {
method: method,
params: params
};
host.postMessage(JSON.stringify(notification));
}
private callFunctionOn(request: IWebKitRequest): IWebKitResult {
if (this._intellisenseFrame && this._intellisenseExpression) {
var prop = this._debugger.eval(this._intellisenseFrame, this._intellisenseExpression);
this._intellisenseExpression = "";
this._intellisenseFrame = null;
if (!prop) {
return {
error: "Could not find object"
};
}
var childProps = this._debugger.getChildProperties(<any>prop.propertyId, 0, 0);
var value = {};
for (var i = 0; i < childProps.propInfos.length; i++) {
var childProp: IPropertyInfo = childProps.propInfos[i];
if (!childProp.fake) {
value[childProp.name] = true;
}
}
return {
result: {
result: {
type: "object",
value: value
},
wasThrown: false
}
};
}
}
private processRuntime(method: string, request: IWebKitRequest): void {
var processedResult: IWebKitResult;
switch (method) {
// copy-pasta from runtime.ts, why is this is two places???
case "enable":
processedResult = { result: {} };
break;
case "callFunctionOn":
processedResult = this.callFunctionOn(request);
break;
case "evaluate":
var prop = debug.eval(request.params.contextId, request.params.expression);
if (prop) {
processedResult = {
result: this.getRemoteObjectFromProp(prop)
};
}
break;
case "getProperties":
var id = parseInt(request.params.objectId);
var props = this._debugger.getChildProperties(id, 0, 0);
var viewAccessorOnly = request.params.accessorPropertiesOnly;
var propDescriptions = [];
for (var i = 0; i < props.propInfos.length; i++) {
var prop = props.propInfos[i];
if (!prop.fake) {
if (typeof viewAccessorOnly !== "undefined") {
if (viewAccessorOnly && !prop.readOnly) {
continue;
} else if (!viewAccessorOnly && prop.readOnly) {
continue;
}
}
var remote = this.getRemoteObjectFromProp(prop);
propDescriptions.push({
name: prop.name,
value: remote.result,
wasThrown: remote.wasThrown
});
}
}
processedResult = {
result: {
result: propDescriptions
}
};
break;
default:
processedResult = {};
break;
}
this.postResponse(request.id, processedResult);
}
private processCustom(method: string, request: IWebKitRequest): void {
switch (method) {
case "toolsDisconnected":
this.debuggerResume(BreakResumeAction.Continue);
return host.postMessageToEngine("browser", this._isAtBreakpoint, "{\"method\":\"Custom.toolsDisconnected\"}");
this._debugger.disconnect();
this._isEnabled = false;
break;
case "testResetState":
this.debuggerResume(BreakResumeAction.Continue);
return host.postMessageToEngine("browser", this._isAtBreakpoint, "{\"method\":\"Custom.testResetState\"}");
break;
}
}
private processDebugger(method: string, request: IWebKitRequest): void {
var processedResult: IWebKitResult;
switch (method) {
case "canSetScriptSource":
processedResult = {
result: {
result: false
}
};
break;
case "continueToLocation":
break;
case "disable":
this.debuggerResume(BreakResumeAction.Continue);
break;
case "enable":
this.debuggerEnable(request.id);
return;
case "evaluateOnCallFrame":
// Intelisense from the chrome dev tools calls this than runtime.callFunctionOn.
// We need to return information on the object when runtime.callFunctionOn is called, so save state we will need now
if (request.params.objectGroup === "completion") {
this._intellisenseExpression = request.params.expression;
this._intellisenseFrame = request.params.callFrameId;
}
var frameId = parseInt(request.params.callFrameId);
var prop = this._debugger.eval(frameId, request.params.expression);
if (prop) {
processedResult = {
result: this.getRemoteObjectFromProp(prop)
};
}
break;
case "getScriptSource":
var docId: number = parseInt(request.params.scriptId);
var textResult = this._debugger.getSourceText(docId);
if (!textResult.loadFailed) {
this.getLineEndings(docId, textResult.text);
processedResult = {
result: {
scriptSource: " " + textResult.text
}
};
}
break;
case "pause":
this._debugger.causeBreak(CauseBreakAction.BreakOnAny, 0);
break;
case "removeBreakpoint":
var bpId = parseInt(request.params.breakpointId);
this._debugger.removeBreakpoint(bpId);
break;
case "resume":
this.debuggerResume(BreakResumeAction.Continue);
break;
case "searchInContent":
break;
case "setBreakpoint":
break;
case "setBreakpointByUrl":
if (this._documentMap.has(request.params.url)) {
try {
var docId: number = this._documentMap.get(request.params.url);
var lineEndings = this.getLineEndings(docId);
var charCount = 0;
for (var i = 0; i < request.params.lineNumber; i++) {
charCount += lineEndings[i];
}
charCount += request.params.columnNumber;
var info = this._debugger.addCodeBreakpoint(docId, charCount, request.params.condition, false);
var location = this.getLineColumnFromOffset(docId, info.location.start);
processedResult = {
result: {
breakpointId: "" + info.breakpointId,
locations: [location]
}
};
} catch (ex) {
this.postResponse(request.id, {
error: { description: "Invalid request" }
});
return;
}
} else {
processedResult = {
error: { description: "Not implemented" }
};
}
break;
case "setBreakpointsActive":
break;
case "setPauseOnExceptions":
break;
case "setScriptSource":
break;
case "stepInto":
this.debuggerResume(BreakResumeAction.StepInto);
break;
case "stepOut":
this.debuggerResume(BreakResumeAction.StepOut);
break;
case "stepOver":
this.debuggerResume(BreakResumeAction.StepOver);
break;
default:
processedResult = {};
break;
}
if (!processedResult) {
processedResult = {};
}
this.postResponse(request.id, processedResult);
}
private debuggerEnable(id: number): void {
if (!this._isEnabled && !this._isAwaitingDebuggerEnableCall) {
var listener = (succeeded: boolean) => {
this._debugger.removeEventListener("debuggingenabled", listener);
this._isAwaitingDebuggerEnableCall = false;
if (succeeded) {
// Now that we have enabled debugging, try to connect to the target
var connectionResult = this._debugger.connect(/*enabled=*/ true);
if (connectionResult === ConnectionResult.Succeeded) {
this._isEnabled = true;
}
}
this.postResponse(id, { result: {} });
};
this._debugger.addEventListener("debuggingenabled", listener);
// This call is asynchronous as it needs to go across threads
this._isAwaitingDebuggerEnableCall = true;
this._debugger.enable();
} else {
// Already connected, so return success
this.postResponse(id, { result: {} });
}
}
private debuggerResume(action: BreakResumeAction): void {
this._debugger.resume(action);
this._isAtBreakpoint = false;
}
private onAddDocuments(documents: IDocument[]): void {
for (var i = 0; i < documents.length; i++) {
var document: IDocument = documents[i];
this._documentMap.set(document.url, document.docId);
this.postNotification("Debugger.scriptParsed", {
scriptId: "" + document.docId,
url: document.url,
startLine: 0,
startColumn: 0,
endLine: document.length,
endColumn: document.length,
isContentScript: false,
sourceMapURL: document.sourceMapUrlFromHeader
});
}
}
private onRemoveDocuments(docIds: number[]): void {
}
private onUpdateDocuments(documents: IDocument[]): void {
}
private onResolveBreakpoints(breakpoints: IResolvedBreakpointInfo[]): void {
}
private onBreak(breakEventInfo: IBreakEventInfo): boolean {
this._isAtBreakpoint = true;
var callFrames = [];
var frames = this._debugger.getFrames(0);
for (var i = 0; i < frames.length; i++) {
var scopes = [];
var localId = this._debugger.getLocals(frames[i].callFrameId);
if (localId) {
scopes.push({
object: {
className: "Object",
description: "Object",
objectId: "" + localId,
type: "object"
},
type: "local"
});
var locals = this._debugger.getChildProperties(localId, 0, 0);
for (var j = 0; j < locals.propInfos.length; j++) {
var prop = locals.propInfos[j];
if (prop.fake) {
scopes.push({
object: {
className: "Object",
description: "Object",
objectId: "" + prop.propertyId,
type: "object"
},
type: "closure"
});
}
}
}
callFrames.push({
callFrameId: "" + frames[i].callFrameId,
functionName: frames[i].functionName,
location: this.getLineColumnFromOffset(frames[i].location.docId, frames[i].location.start),
scopeChain: scopes,
this: null
});
}
this.postNotification("Debugger.paused", {
callFrames: callFrames,
reason: "other",
data: null
});
return true;
}
}
export class App {
private _proxy: DebuggerProxy;
public main(): void {
this._proxy = new DebuggerProxy();
}
}
var app = new App();
app.main();
}