Support running requests and responses through a set of transforms between Code and the Webkit adapter, for testability

This commit is contained in:
Rob 2015-10-18 22:06:30 -07:00
Родитель 36c721df26
Коммит 09ceed618c
8 изменённых файлов: 188 добавлений и 50 удалений

61
adapter/adapterProxy.ts Normal file
Просмотреть файл

@ -0,0 +1,61 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import Utilities = require('../webkit/utilities');
export type EventHandler = (event: DebugProtocol.Event) => void;
export class AdapterProxy {
public constructor(private _requestTranslators: IDebugTranslator[], private _debugAdapter: IDebugAdapter, private _eventHandler: EventHandler) {
this._debugAdapter.registerEventHandler(this._eventHandler);
}
public dispatchRequest(request: DebugProtocol.Request): Promise<any> {
if (!(request.command in this._debugAdapter)) {
Promise.reject('unknowncommand');
}
return this.translateRequest(request)
// Pass the modified args to the adapter
.then(() => this._debugAdapter[request.command](request.arguments))
// Pass the body back through the translators and ensure the body is returned
.then((body?) => {
return this.translateResponse(request, body)
.then(() => body);
});
}
/**
* Pass the request arguments through the translators. They modify the object in place.
*/
private translateRequest(request: DebugProtocol.Request): Promise<void> {
return this._requestTranslators.reduce(
(p, translator) => {
// If the translator implements this command, give it a chance to modify the args. Otherwise skip it
return request.command in translator ?
p.then(() => translator[request.command](request.arguments)) :
p;
}, Promise.resolve<void>())
}
/**
* Pass the response body back through the translators in reverse order. They modify the body in place.
*/
private translateResponse(request: DebugProtocol.Request, body: any): Promise<void> {
if (!body) {
return Promise.resolve<void>();
}
const reversedTranslators = Utilities.reversedArr(this._requestTranslators);
return reversedTranslators.reduce(
(p, translator) => {
// If the translator implements this command, give it a chance to modify the args. Otherwise skip it
const bodyTranslateMethodName = request.command + "Response";
return bodyTranslateMethodName in translator ?
p.then(() => translator[bodyTranslateMethodName](body)) :
p;
}, Promise.resolve<void>());
}
}

Просмотреть файл

@ -0,0 +1,47 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
/**
* Converts from 1 based lines on the client side to 0 based lines on the target side
*/
export class LineNumberTranslator {
private _targetLinesStartAt1: boolean;
private _clientLinesStartAt1: boolean;
constructor(targetLinesStartAt1: boolean) {
this._targetLinesStartAt1 = targetLinesStartAt1;
}
public initialize(args: IInitializeRequestArgs): void {
this._clientLinesStartAt1 = args.linesStartAt1;
}
public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): void {
args.lines = args.lines.map(line => this.convertClientLineToTarget(line));
}
public setBreakpointsResponse(response: SetBreakpointsResponseBody): void {
response.breakpoints.forEach(bp => bp.line = this.convertTargetLineToClient(bp.line));
}
public stackTraceResponse(response: StackTraceResponseBody): void {
response.stackFrames.forEach(frame => frame.line = this.convertTargetLineToClient(frame.line));
}
private convertClientLineToTarget(line: number): number {
if (this._targetLinesStartAt1) {
return this._clientLinesStartAt1 ? line : line + 1;
}
return this._clientLinesStartAt1 ? line - 1 : line;
}
private convertTargetLineToClient(line: number): number {
if (this._targetLinesStartAt1) {
return this._clientLinesStartAt1 ? line : line - 1;
}
return this._clientLinesStartAt1 ? line + 1 : line;
}
}

Просмотреть файл

@ -10,11 +10,11 @@ var typescript = require('typescript');
var sourcemaps = require('gulp-sourcemaps'); var sourcemaps = require('gulp-sourcemaps');
var sources = [ var sources = [
'adapter',
'common', 'common',
'node', 'node',
'webkit', 'webkit',
'typings', 'typings',
'mux'
].map(function(tsFolder) { return tsFolder + '/**/*.ts'; }); ].map(function(tsFolder) { return tsFolder + '/**/*.ts'; });
var projectConfig = { var projectConfig = {

Просмотреть файл

@ -8,6 +8,8 @@
"outDir": "out" "outDir": "out"
}, },
"files": [ "files": [
"adapter/adapterProxy.ts",
"adapter/lineNumberTranslator.ts",
"webkit/openDebugWebKit.ts", "webkit/openDebugWebKit.ts",
"webkit/pathUtilities.ts", "webkit/pathUtilities.ts",
"webkit/sourceMaps.ts", "webkit/sourceMaps.ts",

Просмотреть файл

@ -67,4 +67,11 @@ export class DebounceHelper {
fn(); fn();
} }
} }
export function reversedArr(arr: any[]): any[] {
return arr.reduce((reversed: any[], x: any) => {
reversed.unshift(x);
return reversed;
}, []);
}

23
webkit/webKitAdapterInterfaces.d.ts поставляемый
Просмотреть файл

@ -74,3 +74,26 @@ interface IDebugAdapter {
threads(): Promise<ThreadsResponseBody>; threads(): Promise<ThreadsResponseBody>;
evaluate(args: DebugProtocol.EvaluateArguments): Promise<EvaluateResponseBody>; evaluate(args: DebugProtocol.EvaluateArguments): Promise<EvaluateResponseBody>;
} }
declare type PromiseOrNot<T> = T | Promise<T>;
interface IDebugTranslator {
initialize?(args: IInitializeRequestArgs): PromiseOrNot<void>;
launch?(args: ILaunchRequestArgs): PromiseOrNot<void>;
attach?(args: IAttachRequestArgs): PromiseOrNot<void>;
setBreakpoints?(args: DebugProtocol.SetBreakpointsArguments): PromiseOrNot<void>;
setExceptionBreakpoints?(args: DebugProtocol.SetExceptionBreakpointsArguments): PromiseOrNot<void>;
stackTrace?(args: DebugProtocol.StackTraceArguments): PromiseOrNot<void>;
scopes?(args: DebugProtocol.ScopesArguments): PromiseOrNot<void>;
variables?(args: DebugProtocol.VariablesArguments): PromiseOrNot<void>;
source?(args: DebugProtocol.SourceArguments): PromiseOrNot<void>;
evaluate?(args: DebugProtocol.EvaluateArguments): PromiseOrNot<void>;
setBreakpointsResponse?(response: SetBreakpointsResponseBody): PromiseOrNot<void>;
stackTraceResponse?(response: StackTraceResponseBody): PromiseOrNot<void>;
scopesResponse?(response: ScopesResponseBody): PromiseOrNot<void>;
variablesResponse?(response: VariablesResponseBody): PromiseOrNot<void>;
sourceResponse?(response: SourceResponseBody): PromiseOrNot<void>;
threadsResponse?(response: ThreadsResponseBody): PromiseOrNot<void>;
evaluateResponse?(response: EvaluateResponseBody): PromiseOrNot<void>;
}

Просмотреть файл

@ -23,7 +23,6 @@ export class WebKitDebugAdapter implements IDebugAdapter {
private static THREAD_ID = 1; private static THREAD_ID = 1;
private static PAGE_PAUSE_MESSAGE = 'Paused in Visual Studio Code'; private static PAGE_PAUSE_MESSAGE = 'Paused in Visual Studio Code';
private _debuggerLinesStartAt1: boolean;
private _clientLinesStartAt1: boolean; private _clientLinesStartAt1: boolean;
private _clientCWD: string; private _clientCWD: string;
@ -46,8 +45,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
private _setBreakpointsRequestQ: Promise<any>; private _setBreakpointsRequestQ: Promise<any>;
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { public constructor() {
this._debuggerLinesStartAt1 = debuggerLinesStartAt1;
this._variableHandles = new Handles<string>(); this._variableHandles = new Handles<string>();
this._overlayHelper = new Utilities.DebounceHelper(/*timeoutMs=*/200); this._overlayHelper = new Utilities.DebounceHelper(/*timeoutMs=*/200);
@ -74,10 +72,6 @@ export class WebKitDebugAdapter implements IDebugAdapter {
this._eventHandler = eventHandler; this._eventHandler = eventHandler;
} }
private sendEvent(event: DebugProtocol.Event): void {
this._eventHandler(event);
}
public initialize(args: IInitializeRequestArgs): Promise<void> { public initialize(args: IInitializeRequestArgs): Promise<void> {
this._clientLinesStartAt1 = args.linesStartAt1; this._clientLinesStartAt1 = args.linesStartAt1;
if (args.sourceMaps) { if (args.sourceMaps) {
@ -145,7 +139,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
return this._webKitConnection.attach(port) return this._webKitConnection.attach(port)
.then( .then(
() => this.sendEvent(new InitializedEvent()), () => this._eventHandler(new InitializedEvent()),
e => { e => {
this.clearEverything(); this.clearEverything();
return Promise.reject(e); return Promise.reject(e);
@ -160,7 +154,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
*/ */
private terminateSession(): void { private terminateSession(): void {
if (this._clientAttached) { if (this._clientAttached) {
this.sendEvent(new TerminatedEvent()); this._eventHandler(new TerminatedEvent());
} }
this.clearEverything(); this.clearEverything();
@ -188,7 +182,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
this._overlayHelper.doAndCancel(() => this._webKitConnection.page_setOverlayMessage(WebKitDebugAdapter.PAGE_PAUSE_MESSAGE)); this._overlayHelper.doAndCancel(() => this._webKitConnection.page_setOverlayMessage(WebKitDebugAdapter.PAGE_PAUSE_MESSAGE));
this._currentStack = notification.callFrames; this._currentStack = notification.callFrames;
const exceptionText = notification.reason === 'exception' ? notification.data.description : undefined; const exceptionText = notification.reason === 'exception' ? notification.data.description : undefined;
this.sendEvent(new StoppedEvent('pause', /*threadId=*/WebKitDebugAdapter.THREAD_ID, exceptionText)); this._eventHandler(new StoppedEvent('pause', /*threadId=*/WebKitDebugAdapter.THREAD_ID, exceptionText));
} }
private onDebuggerResumed(): void { private onDebuggerResumed(): void {
@ -267,7 +261,6 @@ export class WebKitDebugAdapter implements IDebugAdapter {
private _addBreakpoints(sourcePath: string, scriptId: WebKitProtocol.Debugger.ScriptId, lines: number[]): Promise<WebKitProtocol.Debugger.SetBreakpointResponse[]> { private _addBreakpoints(sourcePath: string, scriptId: WebKitProtocol.Debugger.ScriptId, lines: number[]): Promise<WebKitProtocol.Debugger.SetBreakpointResponse[]> {
// Adjust lines for sourcemaps, call setBreakpoint for all breakpoints in the script simultaneously // Adjust lines for sourcemaps, call setBreakpoint for all breakpoints in the script simultaneously
const responsePs = lines const responsePs = lines
.map(clientLine => this.convertClientLineToDebugger(clientLine))
.map(debuggerLine => { .map(debuggerLine => {
// Sourcemap lines // Sourcemap lines
if (this._sourceMaps) { if (this._sourceMaps) {
@ -305,7 +298,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
return <DebugProtocol.Breakpoint>{ return <DebugProtocol.Breakpoint>{
verified: true, verified: true,
line: this.convertDebuggerLineToClient(line) line: line
} }
}); });
} }
@ -373,7 +366,7 @@ export class WebKitDebugAdapter implements IDebugAdapter {
id: i, id: i,
name: callFrame.functionName || '(eval code)', // anything else? name: callFrame.functionName || '(eval code)', // anything else?
source, source,
line: this.convertDebuggerLineToClient(line), line: line,
column column
}; };
}); });
@ -522,20 +515,6 @@ export class WebKitDebugAdapter implements IDebugAdapter {
return ''; return '';
} }
private convertClientLineToDebugger(line): number {
if (this._debuggerLinesStartAt1) {
return this._clientLinesStartAt1 ? line : line + 1;
}
return this._clientLinesStartAt1 ? line - 1 : line;
}
private convertDebuggerLineToClient(line): number {
if (this._debuggerLinesStartAt1) {
return this._clientLinesStartAt1 ? line : line - 1;
}
return this._clientLinesStartAt1 ? line + 1 : line;
}
} }
/** /**

Просмотреть файл

@ -6,28 +6,54 @@ import {Response} from '../common/v8Protocol';
import {DebugSession, ErrorDestination} from '../common/debugSession'; import {DebugSession, ErrorDestination} from '../common/debugSession';
import {WebKitDebugAdapter} from './webKitDebugAdapter'; import {WebKitDebugAdapter} from './webKitDebugAdapter';
import {AdapterProxy} from '../adapter/adapterProxy';
import {LineNumberTranslator} from '../adapter/lineNumberTranslator';
export class WebKitDebugSession extends DebugSession { export class WebKitDebugSession extends DebugSession {
private _debugAdapter: IDebugAdapter; private _adapterProxy: AdapterProxy;
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { public constructor(targetLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer); super(targetLinesStartAt1, isServer);
this._debugAdapter = new WebKitDebugAdapter(debuggerLinesStartAt1, isServer); this._adapterProxy = new AdapterProxy(
this._debugAdapter.registerEventHandler(event => this.sendEvent(event)); [new LineNumberTranslator(targetLinesStartAt1)],
new WebKitDebugAdapter(),
event => this.sendEvent(event));
} }
/**
* Overload sendEvent to log
*/
public sendEvent(event: DebugProtocol.Event): void { public sendEvent(event: DebugProtocol.Event): void {
console.log(`To client: ${JSON.stringify(event) }`); console.log(`To client: ${JSON.stringify(event) }`);
super.sendEvent(event); super.sendEvent(event);
} }
/**
* Overload sendResponse to log
*/
public sendResponse(response: DebugProtocol.Response): void {
console.log(`To client: ${JSON.stringify(response) }`);
super.sendResponse(response);
}
/**
* Takes a response and a promise to the response body. If the promise is successful, assigns the response body and sends the response.
* If the promise fails, sets the appropriate response parameters and sends the response.
*/
public sendResponseAsync(response: DebugProtocol.Response, responseP: Promise<any>): void { public sendResponseAsync(response: DebugProtocol.Response, responseP: Promise<any>): void {
responseP.then( responseP.then(
(body) => { (body?) => {
response.body = body; response.body = body;
this.sendResponse(response); this.sendResponse(response);
}, },
e => { e => {
const eStr = e.toString();
if (eStr === 'unknowncommand') {
this.sendErrorResponse(response, 1014, 'Unrecognized request', null, ErrorDestination.Telemetry);
return;
}
console.log(e.toString()); console.log(e.toString());
response.message = e.toString(); response.message = e.toString();
response.success = false; response.success = false;
@ -35,25 +61,18 @@ export class WebKitDebugSession extends DebugSession {
}); });
} }
public sendResponse(response: DebugProtocol.Response): void { /**
console.log(`To client: ${JSON.stringify(response) }`); * Overload dispatchRequest to dispatch to the adapter proxy instead of debugSession's methods for each request.
super.sendResponse(response); */
}
protected dispatchRequest(request: DebugProtocol.Request): void { protected dispatchRequest(request: DebugProtocol.Request): void {
console.log(`From client: ${request.command}(${JSON.stringify(request.arguments) })`);
const response = new Response(request); const response = new Response(request);
try { try {
if (request.command in this._debugAdapter) { console.log(`From client: ${request.command}(${JSON.stringify(request.arguments) })`);
this.sendResponseAsync( this.sendResponseAsync(
response, response,
this._debugAdapter[request.command](request.arguments)); this._adapterProxy.dispatchRequest(request));
} else {
this.sendErrorResponse(response, 1014, "unrecognized request", null, ErrorDestination.Telemetry);
}
} catch (e) { } catch (e) {
this.sendErrorResponse(response, 1104, "exception while processing request (exception: {_exception})", { _exception: e.message }, ErrorDestination.Telemetry); this.sendErrorResponse(response, 1104, 'Exception while processing request (exception: {_exception})', { _exception: e.message }, ErrorDestination.Telemetry);
} }
} }
} }