提交 4cd09ff1 编写于 作者: A André Weinand

reify the DebugAdapter; fixes #45129

上级 ced0b88e
......@@ -6,7 +6,7 @@
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import uri from 'vs/base/common/uri';
import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData } from 'vs/workbench/parts/debug/common/debug';
import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug';
import { TPromise } from 'vs/base/common/winjs.base';
import {
ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext,
......@@ -14,6 +14,10 @@ import {
} from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import severity from 'vs/base/common/severity';
import { AbstractDebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol';
import { convertToDAPaths, convertToVSCPaths } from 'vs/workbench/parts/debug/node/DapPathConverter';
import * as paths from 'vs/base/common/paths';
@extHostNamedCustomer(MainContext.MainThreadDebugService)
export class MainThreadDebugService implements MainThreadDebugServiceShape {
......@@ -21,6 +25,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape {
private _proxy: ExtHostDebugServiceShape;
private _toDispose: IDisposable[];
private _breakpointEventsActive: boolean;
private _debugAdapters: Map<number, ExtensionHostDebugAdapter>;
private _debugAdaptersHandleCounter = 1;
constructor(
extHostContext: IExtHostContext,
......@@ -46,6 +52,18 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape {
}
}
}));
this._debugAdapters = new Map<number, ExtensionHostDebugAdapter>();
// register a default DA provider
debugService.getConfigurationManager().registerDebugAdapterProvider('*', {
createDebugAdapter: (debugType, adapterInfo) => {
const handle = this._debugAdaptersHandleCounter++;
const da = new ExtensionHostDebugAdapter(handle, this._proxy, debugType, adapterInfo);
this._debugAdapters.set(handle, da);
return da;
}
});
}
public dispose(): void {
......@@ -208,4 +226,59 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape {
this.debugService.logToRepl(value, severity.Warning);
return TPromise.wrap<void>(undefined);
}
public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) {
convertToVSCPaths(message, source => {
if (typeof source.path === 'object') {
source.path = uri.revive(source.path).toString();
}
});
this._debugAdapters.get(handle).acceptMessage(message);
}
public $acceptDAError(handle: number, name: string, message: string, stack: string) {
this._debugAdapters.get(handle).fireError(handle, new Error(`${name}: ${message}\n${stack}`));
}
public $acceptDAExit(handle: number, code: number, signal: string) {
this._debugAdapters.get(handle).fireExit(handle, code, signal);
}
}
class ExtensionHostDebugAdapter extends AbstractDebugAdapter {
constructor(private _handle: number, private _proxy: ExtHostDebugServiceShape, private _debugType: string, private _adapterExecutable: IAdapterExecutable | null) {
super();
}
public fireError(handle: number, err: Error) {
this._onError.fire(err);
}
public fireExit(handle: number, code: number, signal: string) {
this._onExit.fire(code);
}
public startSession(): TPromise<void> {
return this._proxy.$startDASession(this._handle, this._debugType, this._adapterExecutable);
}
public sendMessage(message: DebugProtocol.ProtocolMessage): void {
convertToDAPaths(message, source => {
if (paths.isAbsolute(source.path)) {
(<any>source).path = uri.file(source.path);
} else {
(<any>source).path = uri.parse(source.path);
}
});
this._proxy.$sendDAMessage(this._handle, message);
}
public stopSession(): TPromise<void> {
return this._proxy.$stopDASession(this._handle);
}
}
......@@ -106,7 +106,7 @@ export function createApiFactory(
const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(rpcProtocol, extHostHeapService, extHostLogService));
const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands));
rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace);
const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace));
const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService));
rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration);
const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol));
const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics));
......
......@@ -465,6 +465,9 @@ export interface MainThreadSCMShape extends IDisposable {
export type DebugSessionUUID = string;
export interface MainThreadDebugServiceShape extends IDisposable {
$acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage);
$acceptDAError(handle: number, name: string, message: string, stack: string);
$acceptDAExit(handle: number, code: number, signal: string);
$registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): TPromise<any>;
$unregisterDebugConfigurationProvider(handle: number): TPromise<any>;
$startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise<boolean>;
......@@ -783,6 +786,9 @@ export interface ISourceMultiBreakpointDto {
}
export interface ExtHostDebugServiceShape {
$startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null): TPromise<void>;
$stopDASession(handle: number): TPromise<void>;
$sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise<void>;
$resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): TPromise<IConfig>;
$provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise<IConfig[]>;
$debugAdapterExecutable(handle: number, folder: UriComponents | undefined): TPromise<IAdapterExecutable>;
......
......@@ -12,17 +12,19 @@ import {
IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto
} from 'vs/workbench/api/node/extHost.protocol';
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import * as vscode from 'vscode';
import URI, { UriComponents } from 'vs/base/common/uri';
import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint } from 'vs/workbench/api/node/extHostTypes';
import { generateUuid } from 'vs/base/common/uuid';
import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol';
import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/DapPathConverter';
import * as paths from 'vs/base/common/paths';
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
import { IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug';
export class ExtHostDebugService implements ExtHostDebugServiceShape {
private _workspace: ExtHostWorkspace;
private _handleCounter: number;
private _handlers: Map<number, vscode.DebugConfigurationProvider>;
......@@ -52,10 +54,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape {
private readonly _onDidChangeBreakpoints: Emitter<vscode.BreakpointsChangeEvent>;
private _debugAdapters: Map<number, DebugAdapter>;
constructor(mainContext: IMainContext, workspace: ExtHostWorkspace) {
this._workspace = workspace;
constructor(mainContext: IMainContext, private _workspace: ExtHostWorkspace, private _extensionService: ExtHostExtensionService) {
this._handleCounter = 0;
this._handlers = new Map<number, vscode.DebugConfigurationProvider>();
......@@ -77,6 +79,51 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape {
this._breakpoints = new Map<string, vscode.Breakpoint>();
this._breakpointEventsActive = false;
this._debugAdapters = new Map<number, DebugAdapter>();
}
public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null): TPromise<void> {
const mythis = this;
const da = new class extends DebugAdapter {
// DA -> VS Code
public acceptMessage(message: DebugProtocol.ProtocolMessage) {
convertToVSCPaths(message, source => {
if (paths.isAbsolute(source.path)) {
(<any>source).path = URI.file(source.path);
}
});
mythis._debugServiceProxy.$acceptDAMessage(handle, message);
}
}(debugType, adpaterExecutable, this._extensionService.getAllExtensionDescriptions());
this._debugAdapters.set(handle, da);
da.onError(err => this._debugServiceProxy.$acceptDAError(handle, err.name, err.message, err.stack));
da.onExit(code => this._debugServiceProxy.$acceptDAExit(handle, code, null));
return da.startSession();
}
public $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise<void> {
// VS Code -> DA
convertToDAPaths(message, source => {
if (typeof source.path === 'object') {
source.path = URI.revive(source.path).fsPath;
}
});
const da = this._debugAdapters.get(handle);
if (da) {
da.sendMessage(message);
}
return void 0;
}
public $stopDASession(handle: number): TPromise<void> {
const da = this._debugAdapters.get(handle);
this._debugAdapters.delete(handle);
return da ? da.stopSession() : void 0;
}
private startBreakpoints() {
......
......@@ -20,6 +20,7 @@ import { Range, IRange } from 'vs/editor/common/core/range';
import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IDisposable } from 'vs/base/common/lifecycle';
export const VIEWLET_ID = 'workbench.view.debug';
export const VARIABLES_VIEW_ID = 'workbench.debug.variablesView';
......@@ -347,6 +348,7 @@ export interface IDebugConfiguration {
hideActionBar: boolean;
showInStatusBar: 'never' | 'always' | 'onFirstSessionStart';
internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart';
extensionHostDebugAdapter: boolean;
}
export interface IGlobalConfig {
......@@ -380,6 +382,22 @@ export interface ICompound {
configurations: (string | { name: string, folder: string })[];
}
export interface IDebugAdapter extends IDisposable {
readonly onError: Event<Error>;
readonly onExit: Event<number>;
onRequest(callback: (request: DebugProtocol.Request) => void);
onEvent(callback: (event: DebugProtocol.Event) => void);
startSession(): TPromise<void>;
sendMessage(message: DebugProtocol.ProtocolMessage): void;
sendResponse(response: DebugProtocol.Response): void;
sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void;
stopSession(): TPromise<void>;
}
export interface IDebugAdapterProvider {
createDebugAdapter(debugType: string, adapterInfo: IAdapterExecutable | null): IDebugAdapter;
}
export interface IAdapterExecutable {
command?: string;
args?: string[];
......@@ -448,6 +466,9 @@ export interface IConfigurationManager {
resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any): TPromise<any>;
debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise<IAdapterExecutable | undefined>;
registerDebugAdapterProvider(debugType: string, debugAdapterLauncher: IDebugAdapterProvider);
createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable | null): IDebugAdapter;
}
export interface ILaunch {
......
......@@ -209,6 +209,11 @@ configurationRegistry.registerConfiguration({
description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces"),
default: { configurations: [], compounds: [] },
$ref: launchSchemaId
},
'debug.extensionHostDebugAdapter': {
type: 'boolean',
description: nls.localize({ comment: ['This is the description for a setting'], key: 'extensionHostDebugAdapter' }, "Run debug adapter in extension host"),
default: false
}
}
});
......
......@@ -26,8 +26,8 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug';
import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter';
import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug';
import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
......@@ -221,7 +221,7 @@ const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname';
const DEBUG_SELECTED_ROOT = 'debug.selectedroot';
export class ConfigurationManager implements IConfigurationManager {
private adapters: Adapter[];
private debuggers: Debugger[];
private breakpointModeIdsSet = new Set<string>();
private launches: ILaunch[];
private selectedName: string;
......@@ -229,6 +229,8 @@ export class ConfigurationManager implements IConfigurationManager {
private toDispose: IDisposable[];
private _onDidSelectConfigurationName = new Emitter<void>();
private providers: IDebugConfigurationProvider[];
private debugAdapterProviders: Map<string, IDebugAdapterProvider>;
constructor(
@IWorkspaceContextService private contextService: IWorkspaceContextService,
......@@ -241,7 +243,7 @@ export class ConfigurationManager implements IConfigurationManager {
@ILifecycleService lifecycleService: ILifecycleService
) {
this.providers = [];
this.adapters = [];
this.debuggers = [];
this.toDispose = [];
this.registerListeners(lifecycleService);
this.initLaunches();
......@@ -250,6 +252,7 @@ export class ConfigurationManager implements IConfigurationManager {
if (previousSelectedLaunch) {
this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE));
}
this.debugAdapterProviders = new Map<string, IDebugAdapterProvider>();
}
public registerDebugConfigurationProvider(handle: number, debugConfigurationProvider: IDebugConfigurationProvider): void {
......@@ -300,12 +303,24 @@ export class ConfigurationManager implements IConfigurationManager {
return TPromise.as(undefined);
}
public registerDebugAdapterProvider(debugType: string, debugAdapterLauncher: IDebugAdapterProvider) {
this.debugAdapterProviders.set(debugType, debugAdapterLauncher);
}
public createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable): IDebugAdapter {
let dap = this.debugAdapterProviders.get(debugType);
if (!dap) {
dap = this.debugAdapterProviders.get('*');
}
return dap.createDebugAdapter(debugType, adapterExecutable);
}
private registerListeners(lifecycleService: ILifecycleService): void {
debuggersExtPoint.setHandler((extensions) => {
extensions.forEach(extension => {
extension.value.forEach(rawAdapter => {
if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) {
extension.collector.error(nls.localize('debugNoType', "Debug adapter 'type' can not be omitted and must be of type 'string'."));
extension.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'."));
}
if (rawAdapter.enableBreakpointsFor) {
rawAdapter.enableBreakpointsFor.languageIds.forEach(modeId => {
......@@ -313,17 +328,17 @@ export class ConfigurationManager implements IConfigurationManager {
});
}
const duplicate = this.adapters.filter(a => a.type === rawAdapter.type).pop();
const duplicate = this.debuggers.filter(a => a.type === rawAdapter.type).pop();
if (duplicate) {
duplicate.merge(rawAdapter, extension.description);
} else {
this.adapters.push(new Adapter(this, rawAdapter, extension.description, this.configurationService, this.commandService));
this.debuggers.push(new Debugger(this, rawAdapter, extension.description, this.configurationService, this.commandService));
}
});
});
// update the schema to include all attributes, snippets and types from extensions.
this.adapters.forEach(adapter => {
this.debuggers.forEach(adapter => {
const items = (<IJSONSchema>schema.properties['configurations'].items);
const schemaAttributes = adapter.getSchemaAttributes();
if (schemaAttributes) {
......@@ -448,24 +463,24 @@ export class ConfigurationManager implements IConfigurationManager {
return this.breakpointModeIdsSet.has(modeId);
}
public getAdapter(type: string): Adapter {
return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop();
public getAdapter(type: string): Debugger {
return this.debuggers.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop();
}
public guessAdapter(type?: string): TPromise<Adapter> {
public guessAdapter(type?: string): TPromise<Debugger> {
if (type) {
const adapter = this.getAdapter(type);
return TPromise.as(adapter);
}
const editor = this.editorService.getActiveEditor();
let candidates: Adapter[];
let candidates: Debugger[];
if (editor) {
const codeEditor = editor.getControl();
if (isCodeEditor(codeEditor)) {
const model = codeEditor.getModel();
const language = model ? model.getLanguageIdentifier().language : undefined;
const adapters = this.adapters.filter(a => a.languages && a.languages.indexOf(language) >= 0);
const adapters = this.debuggers.filter(a => a.languages && a.languages.indexOf(language) >= 0);
if (adapters.length === 1) {
return TPromise.as(adapters[0]);
}
......@@ -476,11 +491,11 @@ export class ConfigurationManager implements IConfigurationManager {
}
if (!candidates) {
candidates = this.adapters.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider);
candidates = this.debuggers.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider);
}
return this.quickOpenService.pick([...candidates, { label: 'More...', separator: { border: true } }], { placeHolder: nls.localize('selectDebug', "Select Environment") })
.then(picked => {
if (picked instanceof Adapter) {
if (picked instanceof Debugger) {
return picked;
}
if (picked) {
......
......@@ -4,27 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as cp from 'child_process';
import * as net from 'net';
import { Event, Emitter } from 'vs/base/common/event';
import * as platform from 'vs/base/common/platform';
import * as objects from 'vs/base/common/objects';
import { Action } from 'vs/base/common/actions';
import * as errors from 'vs/base/common/errors';
import { TPromise } from 'vs/base/common/winjs.base';
import * as stdfork from 'vs/base/node/stdFork';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal';
import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution';
import * as debug from 'vs/workbench/parts/debug/common/debug';
import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter';
import { V8Protocol } from 'vs/workbench/parts/debug/node/v8Protocol';
import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter';
import { IOutputService } from 'vs/workbench/parts/output/common/output';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement';
import { TerminalSupport } from 'vs/workbench/parts/debug/electron-browser/terminalSupport';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { StreamDebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol';
export interface SessionExitedEvent extends debug.DebugEvent {
body: {
......@@ -40,14 +37,46 @@ export interface SessionTerminatedEvent extends debug.DebugEvent {
};
}
export class RawDebugSession extends V8Protocol implements debug.ISession {
export class SocketDebugAdapter extends StreamDebugAdapter {
private socket: net.Socket;
constructor(private host: string, private port: number) {
super();
}
startSession(): TPromise<void> {
return new TPromise<void>((c, e) => {
this.socket = net.createConnection(this.port, this.host, () => {
this.connect(this.socket, <any>this.socket);
c(null);
});
this.socket.on('error', (err: any) => {
e(err);
});
this.socket.on('close', () => this._onExit.fire(0));
});
}
stopSession(): TPromise<void> {
if (this.socket !== null) {
this.socket.end();
this.socket = undefined;
}
return void 0;
}
}
export class RawDebugSession implements debug.ISession {
private debugAdapter: debug.IDebugAdapter;
public emittedStopped: boolean;
public readyForBreakpoints: boolean;
private serverProcess: cp.ChildProcess;
private socket: net.Socket = null;
private cachedInitServer: TPromise<void>;
//private serverProcess: cp.ChildProcess;
//private socket: net.Socket = null;
private cachedInitServerP: TPromise<void>;
private startTime: number;
public disconnected: boolean;
private sentPromises: TPromise<DebugProtocol.Response>[];
......@@ -66,9 +95,9 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
private readonly _onDidEvent: Emitter<DebugProtocol.Event>;
constructor(
id: string,
private id: string,
private debugServerPort: number,
private adapter: Adapter,
private _debugger: Debugger,
public customTelemetryService: ITelemetryService,
public root: IWorkspaceFolder,
@INotificationService private notificationService: INotificationService,
......@@ -78,7 +107,6 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
@IExternalTerminalService private nativeTerminalService: IExternalTerminalService,
@IConfigurationService private configurationService: IConfigurationService
) {
super(id);
this.emittedStopped = false;
this.readyForBreakpoints = false;
this.allThreadsContinued = true;
......@@ -96,6 +124,10 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
this._onDidEvent = new Emitter<DebugProtocol.Event>();
}
public getId(): string {
return this.id;
}
public get onDidInitialize(): Event<DebugProtocol.InitializedEvent> {
return this._onDidInitialize.event;
}
......@@ -137,28 +169,49 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
}
private initServer(): TPromise<void> {
if (this.cachedInitServer) {
return this.cachedInitServer;
if (this.cachedInitServerP) {
return this.cachedInitServerP;
}
const serverPromise = this.debugServerPort ? this.connectServer(this.debugServerPort) : this.startServer();
this.cachedInitServer = serverPromise.then(() => {
const startSessionP = this.startSession();
this.cachedInitServerP = startSessionP.then(() => {
this.startTime = new Date().getTime();
}, err => {
this.cachedInitServer = null;
this.cachedInitServerP = null;
return TPromise.wrapError(err);
});
return this.cachedInitServer;
return this.cachedInitServerP;
}
private startSession(): TPromise<void> {
const debugAdapterP = this.debugServerPort
? TPromise.as(new SocketDebugAdapter('127.0.0.1', this.debugServerPort))
: this._debugger.createDebugAdapter(this.root, this.outputService);
return debugAdapterP.then(debugAdapter => {
this.debugAdapter = debugAdapter;
this.debugAdapter.onError(err => this.onDapServerError(err));
this.debugAdapter.onEvent(event => this.onDapEvent(event));
this.debugAdapter.onRequest(request => this.dispatchRequest(request));
this.debugAdapter.onExit(code => this.onServerExit());
return this.debugAdapter.startSession();
});
}
public custom(request: string, args: any): TPromise<DebugProtocol.Response> {
return this.send(request, args);
}
protected send<R extends DebugProtocol.Response>(command: string, args: any, cancelOnDisconnect = true): TPromise<R> {
private send<R extends DebugProtocol.Response>(command: string, args: any, cancelOnDisconnect = true): TPromise<R> {
return this.initServer().then(() => {
const promise = super.send<R>(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => {
const promise = this.internalSend<R>(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => {
const error = errorResponse && errorResponse.body ? errorResponse.body.error : null;
const errorMessage = errorResponse ? errorResponse.message : '';
const telemetryMessage = error ? debug.formatPII(error.format, true, error.variables) : errorMessage;
......@@ -199,8 +252,22 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
});
}
protected onEvent(event: debug.DebugEvent): void {
event.sessionId = this.getId();
private internalSend<R extends DebugProtocol.Response>(command: string, args: any): TPromise<R> {
let errorCallback: (error: Error) => void;
return new TPromise<R>((completeDispatch, errorDispatch) => {
errorCallback = errorDispatch;
this.debugAdapter.sendRequest(command, args, (result: R) => {
if (result.success) {
completeDispatch(result);
} else {
errorDispatch(result);
}
});
}, () => errorCallback(errors.canceled()));
}
private onDapEvent(event: debug.DebugEvent): void {
event.sessionId = this.id;
if (event.event === 'initialized') {
this.readyForBreakpoints = true;
......@@ -317,7 +384,7 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
this.sentPromises = [];
}, 1000);
if ((this.serverProcess || this.socket) && !this.disconnected) {
if (this.debugAdapter && !this.disconnected) {
// point of no return: from now on don't report any errors
this.disconnected = true;
return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer());
......@@ -392,16 +459,24 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
return (new Date().getTime() - this.startTime) / 1000;
}
protected dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void {
private dispatchRequest(request: DebugProtocol.Request): void {
const response: DebugProtocol.Response = {
type: 'response',
seq: 0,
command: request.command,
request_seq: request.seq,
success: true
};
if (request.command === 'runInTerminal') {
TerminalSupport.runInTerminal(this.terminalService, this.nativeTerminalService, this.configurationService, <DebugProtocol.RunInTerminalRequestArguments>request.arguments, <DebugProtocol.RunInTerminalResponse>response).then(() => {
this.sendResponse(response);
this.debugAdapter.sendResponse(response);
}, e => {
response.success = false;
response.message = e.message;
this.sendResponse(response);
this.debugAdapter.sendResponse(response);
});
} else if (request.command === 'handshake') {
try {
......@@ -411,16 +486,16 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
response.body = {
signature: sig
};
this.sendResponse(response);
this.debugAdapter.sendResponse(response);
} catch (e) {
response.success = false;
response.message = e.message;
this.sendResponse(response);
this.debugAdapter.sendResponse(response);
}
} else {
response.success = false;
response.message = `unknown request '${request.command}'`;
this.sendResponse(response);
this.debugAdapter.sendResponse(response);
}
}
......@@ -436,111 +511,36 @@ export class RawDebugSession extends V8Protocol implements debug.ISession {
});
}
private connectServer(port: number): TPromise<void> {
return new TPromise<void>((c, e) => {
this.socket = net.createConnection(port, '127.0.0.1', () => {
this.connect(this.socket, <any>this.socket);
c(null);
});
this.socket.on('error', (err: any) => {
e(err);
});
this.socket.on('close', () => this.onServerExit());
});
}
private startServer(): TPromise<any> {
return this.adapter.getAdapterExecutable(this.root).then(ae => this.launchServer(ae).then(() => {
this.serverProcess.on('error', (err: Error) => this.onServerError(err));
this.serverProcess.on('exit', (code: number, signal: string) => this.onServerExit());
const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, '');
// this.serverProcess.stdout.on('data', (data: string) => {
// console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;');
// });
this.serverProcess.stderr.on('data', (data: string) => {
this.outputService.getChannel(ExtensionsChannelId).append(sanitize(data));
});
this.connect(this.serverProcess.stdout, this.serverProcess.stdin);
}));
}
private launchServer(launch: debug.IAdapterExecutable): TPromise<void> {
return new TPromise<void>((c, e) => {
if (launch.command === 'node') {
if (Array.isArray(launch.args) && launch.args.length > 0) {
stdfork.fork(launch.args[0], launch.args.slice(1), {}, (err, child) => {
if (err) {
e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", launch.args[0])));
}
this.serverProcess = child;
c(null);
});
} else {
e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter.")));
}
} else {
this.serverProcess = cp.spawn(launch.command, launch.args, {
stdio: [
'pipe', // stdin
'pipe', // stdout
'pipe' // stderr
],
});
c(null);
}
});
}
private stopServer(): TPromise<any> {
if (this.socket !== null) {
this.socket.end();
this.cachedInitServer = null;
if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) {
this.debugAdapter.stopSession();
this.cachedInitServerP = null;
}
this.onEvent({ event: 'exit', type: 'event', seq: 0 });
if (!this.serverProcess) {
this.onDapEvent({ event: 'exit', type: 'event', seq: 0 });
if (/* !this.serverProcess */ this.debugAdapter instanceof SocketDebugAdapter) {
return TPromise.as(null);
}
this.disconnected = true;
let ret: TPromise<void>;
// when killing a process in windows its child
// processes are *not* killed but become root
// processes. Therefore we use TASKKILL.EXE
if (platform.isWindows) {
ret = new TPromise<void>((c, e) => {
const killer = cp.exec(`taskkill /F /T /PID ${this.serverProcess.pid}`, function (err, stdout, stderr) {
if (err) {
return e(err);
}
});
killer.on('exit', c);
killer.on('error', e);
});
} else {
this.serverProcess.kill('SIGTERM');
ret = TPromise.as(null);
}
return ret;
return this.debugAdapter.stopSession();
}
protected onServerError(err: Error): void {
this.notificationService.error(nls.localize('stoppingDebugAdapter', "{0}. Stopping the debug adapter.", err.message));
private onDapServerError(err: Error): void {
this.notificationService.error(err.message || err.toString());
this.stopServer().done(null, errors.onUnexpectedError);
}
private onServerExit(): void {
this.serverProcess = null;
this.cachedInitServer = null;
//this.serverProcess = null;
this.debugAdapter = null;
this.cachedInitServerP = null;
if (!this.disconnected) {
this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly"));
}
this.onEvent({ event: 'exit', type: 'event', seq: 0 });
this.onDapEvent({ event: 'exit', type: 'event', seq: 0 });
}
public dispose(): void {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: boolean, source: DebugProtocol.Source | undefined) => void) {
switch (msg.type) {
case 'event':
const event = <DebugProtocol.Event>msg;
switch (event.event) {
case 'output':
fixSourcePaths(false, (<DebugProtocol.OutputEvent>event).body.source);
break;
case 'loadedSource':
fixSourcePaths(false, (<DebugProtocol.LoadedSourceEvent>event).body.source);
break;
case 'breakpoint':
fixSourcePaths(false, (<DebugProtocol.BreakpointEvent>event).body.breakpoint.source);
break;
default:
break;
}
break;
case 'request':
const request = <DebugProtocol.Request>msg;
switch (request.command) {
case 'setBreakpoints':
fixSourcePaths(true, (<DebugProtocol.SetBreakpointsArguments>request.arguments).source);
break;
case 'source':
fixSourcePaths(true, (<DebugProtocol.SourceArguments>request.arguments).source);
break;
case 'gotoTargets':
fixSourcePaths(true, (<DebugProtocol.GotoTargetsArguments>request.arguments).source);
break;
default:
break;
}
break;
case 'response':
const response = <DebugProtocol.Response>msg;
switch (response.command) {
case 'stackTrace':
const r1 = <DebugProtocol.StackTraceResponse>response;
r1.body.stackFrames.forEach(frame => fixSourcePaths(false, frame.source));
break;
case 'loadedSources':
const r2 = <DebugProtocol.LoadedSourcesResponse>response;
r2.body.sources.forEach(source => fixSourcePaths(false, source));
break;
case 'scopes':
const r3 = <DebugProtocol.ScopesResponse>response;
r3.body.scopes.forEach(scope => fixSourcePaths(false, scope.source));
break;
case 'setFunctionBreakpoints':
const r4 = <DebugProtocol.SetFunctionBreakpointsResponse>response;
r4.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source));
break;
case 'setBreakpoints':
const r5 = <DebugProtocol.SetBreakpointsResponse>response;
r5.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source));
break;
default:
break;
}
break;
}
}
export function convertToDAPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void) {
convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => {
if (toDA && source) {
fixSourcePaths(source);
}
});
}
export function convertToVSCPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void) {
convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => {
if (!toDA && source) {
fixSourcePaths(source);
}
});
}
\ No newline at end of file
......@@ -3,110 +3,61 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import * as strings from 'vs/base/common/strings';
import * as objects from 'vs/base/common/objects';
import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform';
import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IConfig, IRawAdapter, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug';
import { IConfig, IRawAdapter, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration } from 'vs/workbench/parts/debug/common/debug';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IOutputService } from 'vs/workbench/parts/output/common/output';
import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol';
export class Adapter {
export class Debugger {
private _mergedExtensionDescriptions: IExtensionDescription[];
constructor(private configurationManager: IConfigurationManager, private rawAdapter: IRawAdapter, public extensionDescription: IExtensionDescription,
@IConfigurationService private configurationService: IConfigurationService,
@ICommandService private commandService: ICommandService
) {
if (rawAdapter.windows) {
rawAdapter.win = rawAdapter.windows;
}
this._mergedExtensionDescriptions = [extensionDescription];
}
public hasConfigurationProvider = false;
public getAdapterExecutable(root: IWorkspaceFolder, verifyAgainstFS = true): TPromise<IAdapterExecutable> {
public createDebugAdapter(root: IWorkspaceFolder, outputService: IOutputService): TPromise<IDebugAdapter> {
return this.getAdapterExecutable(root).then(adapterExecutable => {
const debugConfigs = this.configurationService.getValue<IDebugConfiguration>('debug');
if (debugConfigs.extensionHostDebugAdapter) {
return this.configurationManager.createDebugAdapter(this.rawAdapter.type, adapterExecutable);
} else {
return new DebugAdapter(this.rawAdapter.type, adapterExecutable, this._mergedExtensionDescriptions, outputService);
}
});
}
public getAdapterExecutable(root: IWorkspaceFolder): TPromise<IAdapterExecutable | null> {
return this.configurationManager.debugAdapterExecutable(root ? root.uri : undefined, this.rawAdapter.type).then(adapterExecutable => {
if (adapterExecutable) {
return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS);
return adapterExecutable;
}
// try deprecated command based extension API
if (this.rawAdapter.adapterExecutableCommand) {
return this.commandService.executeCommand<IAdapterExecutable>(this.rawAdapter.adapterExecutableCommand, root ? root.uri.toString() : undefined).then(ad => {
return this.verifyAdapterDetails(ad, verifyAgainstFS);
});
return this.commandService.executeCommand<IAdapterExecutable>(this.rawAdapter.adapterExecutableCommand, root ? root.uri.toString() : undefined);
}
// fallback: executable contribution specified in package.json
adapterExecutable = <IAdapterExecutable>{
command: this.getProgram(),
args: this.getAttributeBasedOnPlatform('args')
};
const runtime = this.getRuntime();
if (runtime) {
const runtimeArgs = this.getAttributeBasedOnPlatform('runtimeArgs');
adapterExecutable.args = (runtimeArgs || []).concat([adapterExecutable.command]).concat(adapterExecutable.args || []);
adapterExecutable.command = runtime;
}
return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS);
return TPromise.as(null);
});
}
private verifyAdapterDetails(details: IAdapterExecutable, verifyAgainstFS: boolean): TPromise<IAdapterExecutable> {
if (details.command) {
if (verifyAgainstFS) {
if (path.isAbsolute(details.command)) {
return new TPromise<IAdapterExecutable>((c, e) => {
fs.exists(details.command, exists => {
if (exists) {
c(details);
} else {
e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", details.command)));
}
});
});
} else {
// relative path
if (details.command.indexOf('/') < 0 && details.command.indexOf('\\') < 0) {
// no separators: command looks like a runtime name like 'node' or 'mono'
return TPromise.as(details); // TODO: check that the runtime is available on PATH
}
}
} else {
return TPromise.as(details);
}
}
return TPromise.wrapError(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },
"Cannot determine executable for debug adapter '{0}'.", this.type)));
}
private getRuntime(): string {
let runtime = this.getAttributeBasedOnPlatform('runtime');
if (runtime && runtime.indexOf('./') === 0) {
runtime = paths.join(this.extensionDescription.extensionFolderPath, runtime);
}
return runtime;
}
private getProgram(): string {
let program = this.getAttributeBasedOnPlatform('program');
if (program) {
program = paths.join(this.extensionDescription.extensionFolderPath, program);
}
return program;
}
public get aiKey(): string {
return this.rawAdapter.aiKey;
}
......@@ -132,6 +83,10 @@ export class Adapter {
}
public merge(secondRawAdapter: IRawAdapter, extensionDescription: IExtensionDescription): void {
// remember all ext descriptions that are the source of this debugger
this._mergedExtensionDescriptions.push(extensionDescription);
// Give priority to built in debug adapters
if (extensionDescription.isBuiltin) {
this.extensionDescription = extensionDescription;
......@@ -248,19 +203,4 @@ export class Adapter {
return attributes;
});
}
private getAttributeBasedOnPlatform(key: string): any {
let result: any;
if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432') && this.rawAdapter.winx86) {
result = this.rawAdapter.winx86[key];
} else if (platform.isWindows && this.rawAdapter.win) {
result = this.rawAdapter.win[key];
} else if (platform.isMacintosh && this.rawAdapter.osx) {
result = this.rawAdapter.osx[key];
} else if (platform.isLinux && this.rawAdapter.linux) {
result = this.rawAdapter.linux[key];
}
return result || this.rawAdapter[key];
}
}
......@@ -3,68 +3,82 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as cp from 'child_process';
import * as stream from 'stream';
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
import * as objects from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import * as stdfork from 'vs/base/node/stdFork';
import { Emitter, Event } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import { canceled } from 'vs/base/common/errors';
import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import * as debug from 'vs/workbench/parts/debug/common/debug';
import { IOutputService } from 'vs/workbench/parts/output/common/output';
export abstract class V8Protocol {
/**
* Abstract implementation of the low level API for a debug adapter.
* Missing is how this API communicates with the debug adapter.
*/
export abstract class AbstractDebugAdapter implements debug.IDebugAdapter {
private static readonly TWO_CRLF = '\r\n\r\n';
private outputStream: stream.Writable;
private sequence: number;
private pendingRequests: Map<number, (e: DebugProtocol.Response) => void>;
private rawData: Buffer;
private contentLength: number;
private requestCallback: (request: DebugProtocol.Request) => void;
private eventCallback: (request: DebugProtocol.Event) => void;
protected readonly _onError: Emitter<Error>;
protected readonly _onExit: Emitter<number>;
constructor(private id: string) {
constructor() {
this.sequence = 1;
this.contentLength = -1;
this.pendingRequests = new Map<number, (e: DebugProtocol.Response) => void>();
this.rawData = Buffer.allocUnsafe(0);
this._onError = new Emitter<Error>();
this._onExit = new Emitter<number>();
}
public getId(): string {
return this.id;
abstract startSession(): TPromise<void>;
abstract stopSession(): TPromise<void>;
public dispose(): void {
}
protected abstract onServerError(err: Error): void;
protected abstract onEvent(event: DebugProtocol.Event): void;
protected abstract dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void;
abstract sendMessage(message: DebugProtocol.ProtocolMessage): void;
protected connect(readable: stream.Readable, writable: stream.Writable): void {
public get onError(): Event<Error> {
return this._onError.event;
}
this.outputStream = writable;
public get onExit(): Event<number> {
return this._onExit.event;
}
readable.on('data', (data: Buffer) => {
this.rawData = Buffer.concat([this.rawData, data]);
this.handleData();
});
public onEvent(callback: (event: DebugProtocol.Event) => void) {
if (this.eventCallback) {
this._onError.fire(new Error(`attempt to set more than one 'Event' callback`));
}
this.eventCallback = callback;
}
protected send<R extends DebugProtocol.Response>(command: string, args: any): TPromise<R> {
let errorCallback: (error: Error) => void;
return new TPromise<R>((completeDispatch, errorDispatch) => {
errorCallback = errorDispatch;
this.doSend(command, args, (result: R) => {
if (result.success) {
completeDispatch(result);
} else {
errorDispatch(result);
}
});
}, () => errorCallback(canceled()));
public onRequest(callback: (request: DebugProtocol.Request) => void) {
if (this.requestCallback) {
this._onError.fire(new Error(`attempt to set more than one 'Request' callback`));
}
this.requestCallback = callback;
}
public sendResponse(response: DebugProtocol.Response): void {
if (response.seq > 0) {
console.error(`attempt to send more than one response for command ${response.command}`);
this._onError.fire(new Error(`attempt to send more than one response for command ${response.command}`));
} else {
this.sendMessage('response', response);
this.internalSend('response', response);
}
}
private doSend(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void {
public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void {
const request: any = {
command: command
......@@ -73,7 +87,7 @@ export abstract class V8Protocol {
request.arguments = args;
}
this.sendMessage('request', request);
this.internalSend('request', request);
if (clb) {
// store callback for this request
......@@ -81,19 +95,85 @@ export abstract class V8Protocol {
}
}
private sendMessage(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void {
public acceptMessage(message: DebugProtocol.ProtocolMessage) {
switch (message.type) {
case 'event':
if (this.eventCallback) {
this.eventCallback(<DebugProtocol.Event>message);
}
break;
case 'request':
if (this.requestCallback) {
this.requestCallback(<DebugProtocol.Request>message);
}
break;
case 'response':
const response = <DebugProtocol.Response>message;
const clb = this.pendingRequests.get(response.request_seq);
if (clb) {
this.pendingRequests.delete(response.request_seq);
clb(response);
}
break;
}
}
private internalSend(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void {
message.type = typ;
message.seq = this.sequence++;
const json = JSON.stringify(message);
const length = Buffer.byteLength(json, 'utf8');
this.sendMessage(message);
}
}
/**
* An implementation that communicates via two streams with the debug adapter.
*/
export abstract class StreamDebugAdapter extends AbstractDebugAdapter {
this.outputStream.write('Content-Length: ' + length.toString() + V8Protocol.TWO_CRLF, 'utf8');
this.outputStream.write(json, 'utf8');
private static readonly TWO_CRLF = '\r\n\r\n';
private outputStream: stream.Writable;
private rawData: Buffer;
private contentLength: number;
constructor() {
super();
}
public connect(readable: stream.Readable, writable: stream.Writable): void {
this.outputStream = writable;
this.rawData = Buffer.allocUnsafe(0);
this.contentLength = -1;
readable.on('data', (data: Buffer) => this.handleData(data));
// readable.on('close', () => {
// this._emitEvent(new Event('close'));
// });
// readable.on('error', (error) => {
// this._emitEvent(new Event('error', 'readable error: ' + (error && error.message)));
// });
// writable.on('error', (error) => {
// this._emitEvent(new Event('error', 'writable error: ' + (error && error.message)));
// });
}
private handleData(): void {
public sendMessage(message: DebugProtocol.ProtocolMessage): void {
if (this.outputStream) {
const json = JSON.stringify(message);
this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8');
}
}
private handleData(data: Buffer): void {
this.rawData = Buffer.concat([this.rawData, data]);
while (true) {
if (this.contentLength >= 0) {
if (this.rawData.length >= this.contentLength) {
......@@ -101,55 +181,226 @@ export abstract class V8Protocol {
this.rawData = this.rawData.slice(this.contentLength);
this.contentLength = -1;
if (message.length > 0) {
this.dispatch(message);
try {
this.acceptMessage(<DebugProtocol.ProtocolMessage>JSON.parse(message));
} catch (e) {
this._onError.fire(new Error((e.message || e) + '\n' + message));
}
}
continue; // there may be more complete messages to process
}
} else {
const s = this.rawData.toString('utf8', 0, this.rawData.length);
const idx = s.indexOf(V8Protocol.TWO_CRLF);
const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF);
if (idx !== -1) {
const match = /Content-Length: (\d+)/.exec(s);
if (match && match[1]) {
this.contentLength = Number(match[1]);
this.rawData = this.rawData.slice(idx + V8Protocol.TWO_CRLF.length);
continue; // try to handle a complete message
const header = this.rawData.toString('utf8', 0, idx);
const lines = header.split('\r\n');
for (const h of lines) {
const kvPair = h.split(/: +/);
if (kvPair[0] === 'Content-Length') {
this.contentLength = Number(kvPair[1]);
}
}
this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length);
continue;
}
}
break;
}
}
}
/**
* An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout.
*/
export class DebugAdapter extends StreamDebugAdapter {
private _serverProcess: cp.ChildProcess;
constructor(private _debugType: string, private _adapterExecutable: debug.IAdapterExecutable | null, extensionDescriptions: IExtensionDescription[], private _outputService?: IOutputService) {
super();
if (!this._adapterExecutable) {
this._adapterExecutable = DebugAdapter.platformAdapterExecutable(extensionDescriptions, this._debugType);
}
}
private dispatch(body: string): void {
try {
const rawData = JSON.parse(body);
switch (rawData.type) {
case 'event':
this.onEvent(<DebugProtocol.Event>rawData);
break;
case 'response':
const response = <DebugProtocol.Response>rawData;
const clb = this.pendingRequests.get(response.request_seq);
if (clb) {
this.pendingRequests.delete(response.request_seq);
clb(response);
startSession(): TPromise<void> {
return new TPromise<void>((c, e) => {
// verify executables
if (this._adapterExecutable.command) {
if (paths.isAbsolute(this._adapterExecutable.command)) {
if (!fs.existsSync(this._adapterExecutable.command)) {
e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", this._adapterExecutable.command)));
}
} else {
// relative path
if (this._adapterExecutable.command.indexOf('/') < 0 && this._adapterExecutable.command.indexOf('\\') < 0) {
// no separators: command looks like a runtime name like 'node' or 'mono'
// TODO: check that the runtime is available on PATH
}
}
} else {
e(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },
"Cannot determine executable for debug adapter '{0}'.", this._debugType)));
}
if (this._adapterExecutable.command === 'node' /*&& this.outputService*/) {
if (Array.isArray(this._adapterExecutable.args) && this._adapterExecutable.args.length > 0) {
stdfork.fork(this._adapterExecutable.args[0], this._adapterExecutable.args.slice(1), {}, (err, child) => {
if (err) {
e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", this._adapterExecutable.args[0])));
}
this._serverProcess = child;
c(null);
});
} else {
e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter.")));
}
} else {
this._serverProcess = cp.spawn(this._adapterExecutable.command, this._adapterExecutable.args);
c(null);
}
}).then(_ => {
this._serverProcess.on('error', (err: Error) => this._onError.fire(err));
this._serverProcess.on('exit', (code: number, signal: string) => this._onExit.fire(code));
if (this._outputService) {
const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, '');
// this.serverProcess.stdout.on('data', (data: string) => {
// console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;');
// });
this._serverProcess.stderr.on('data', (data: string) => {
this._outputService.getChannel(ExtensionsChannelId).append(sanitize(data));
});
}
this.connect(this._serverProcess.stdout, this._serverProcess.stdin);
}, err => {
this._onError.fire(err);
});
}
stopSession(): TPromise<void> {
// when killing a process in windows its child
// processes are *not* killed but become root
// processes. Therefore we use TASKKILL.EXE
if (platform.isWindows) {
return new TPromise<void>((c, e) => {
const killer = cp.exec(`taskkill /F /T /PID ${this._serverProcess.pid}`, function (err, stdout, stderr) {
if (err) {
return e(err);
}
});
killer.on('exit', c);
killer.on('error', e);
});
} else {
this._serverProcess.kill('SIGTERM');
return TPromise.as(null);
}
}
private static extract(dbg: debug.IRawAdapter, extensionFolderPath: string) {
if (!dbg) {
return undefined;
}
let x: debug.IRawAdapter = {};
if (dbg.runtime) {
if (dbg.runtime.indexOf('./') === 0) { // TODO
x.runtime = paths.join(extensionFolderPath, dbg.runtime);
} else {
x.runtime = dbg.runtime;
}
}
if (dbg.runtimeArgs) {
x.runtimeArgs = dbg.runtimeArgs;
}
if (dbg.program) {
if (!paths.isAbsolute(dbg.program)) {
x.program = paths.join(extensionFolderPath, dbg.program);
} else {
x.program = dbg.program;
}
}
if (dbg.args) {
x.args = dbg.args;
}
if (dbg.win) {
x.win = DebugAdapter.extract(dbg.win, extensionFolderPath);
}
if (dbg.winx86) {
x.winx86 = DebugAdapter.extract(dbg.winx86, extensionFolderPath);
}
if (dbg.windows) {
x.windows = DebugAdapter.extract(dbg.windows, extensionFolderPath);
}
if (dbg.osx) {
x.osx = DebugAdapter.extract(dbg.osx, extensionFolderPath);
}
if (dbg.linux) {
x.linux = DebugAdapter.extract(dbg.linux, extensionFolderPath);
}
return x;
}
static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): debug.IAdapterExecutable {
let result: debug.IRawAdapter = {};
debugType = debugType.toLowerCase();
// merge all contributions into one
for (const ed of extensionDescriptions) {
if (ed.contributes) {
const debuggers = <debug.IRawAdapter[]>ed.contributes['debuggers'];
if (debuggers && debuggers.length > 0) {
const dbgs = debuggers.filter(d => d.type.toLowerCase() === debugType);
for (const dbg of dbgs) {
// extract relevant attributes and make then absolute where needed
const dbg1 = DebugAdapter.extract(dbg, ed.extensionFolderPath);
// merge
objects.mixin(result, dbg1, ed.isBuiltin);
}
break;
case 'request':
const request = <DebugProtocol.Request>rawData;
const resp: DebugProtocol.Response = {
type: 'response',
seq: 0,
command: request.command,
request_seq: request.seq,
success: true
};
this.dispatchRequest(request, resp);
break;
}
}
} catch (e) {
this.onServerError(new Error((e.message || e) + '\n' + body));
}
// select the right platform
let platformInfo: debug.IRawEnvAdapter;
if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
platformInfo = result.winx86;
} else if (platform.isWindows) {
platformInfo = result.win || result.windows;
} else if (platform.isMacintosh) {
platformInfo = result.osx;
} else if (platform.isLinux) {
platformInfo = result.linux;
}
platformInfo = platformInfo || result;
// these are the relevant attributes
let program = platformInfo.program || result.program;
const args = platformInfo.args || result.args;
let runtime = platformInfo.runtime || result.runtime;
const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs;
if (runtime) {
return {
command: runtime,
args: (runtimeArgs || []).concat([program]).concat(args || [])
};
} else {
return {
command: program,
args: args || []
};
}
}
}
......@@ -6,15 +6,17 @@
import * as assert from 'assert';
import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform';
import { IRawAdapter, IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug';
import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter';
import { IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug';
import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import uri from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol';
suite('Debug - Adapter', () => {
let adapter: Adapter;
suite('Debug - Debugger', () => {
let _debugger: Debugger;
const extensionFolderPath = 'a/b/c/';
const rawAdapter = {
type: 'mock',
......@@ -44,6 +46,73 @@ suite('Debug - Adapter', () => {
}
]
};
const extensionDescriptor0 = {
id: 'adapter',
name: 'myAdapter',
version: '1.0.0',
publisher: 'vscode',
extensionFolderPath: extensionFolderPath,
isBuiltin: false,
engines: null,
contributes: {
'debuggers': [
rawAdapter
]
}
};
const extensionDescriptor1 = {
id: 'extension1',
name: 'extension1',
version: '1.0.0',
publisher: 'vscode',
extensionFolderPath: '/e1/b/c/',
isBuiltin: false,
engines: null,
contributes: {
'debuggers': [
{
type: 'mock',
runtime: 'runtime',
runtimeArgs: ['rarg'],
program: 'mockprogram',
args: ['parg']
}
]
}
};
const extensionDescriptor2 = {
id: 'extension2',
name: 'extension2',
version: '1.0.0',
publisher: 'vscode',
extensionFolderPath: '/e2/b/c/',
isBuiltin: false,
engines: null,
contributes: {
'debuggers': [
{
type: 'mock',
win: {
runtime: 'winRuntime',
program: 'winProgram'
},
linux: {
runtime: 'linuxRuntime',
program: 'linuxProgram'
},
osx: {
runtime: 'osxRuntime',
program: 'osxProgram'
}
}
]
}
};
const configurationManager = {
debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise<IAdapterExecutable | undefined> {
return TPromise.as(undefined);
......@@ -51,26 +120,25 @@ suite('Debug - Adapter', () => {
};
setup(() => {
adapter = new Adapter(<IConfigurationManager>configurationManager, rawAdapter, { extensionFolderPath, id: 'adapter', name: 'myAdapter', version: '1.0.0', publisher: 'vscode', isBuiltin: false, engines: null },
new TestConfigurationService(), null);
_debugger = new Debugger(<IConfigurationManager>configurationManager, rawAdapter, extensionDescriptor0, new TestConfigurationService(), null);
});
teardown(() => {
adapter = null;
_debugger = null;
});
test('attributes', () => {
assert.equal(adapter.type, rawAdapter.type);
assert.equal(adapter.label, rawAdapter.label);
assert.equal(_debugger.type, rawAdapter.type);
assert.equal(_debugger.label, rawAdapter.label);
return adapter.getAdapterExecutable(undefined, false).then(details => {
assert.equal(details.command, paths.join(extensionFolderPath, rawAdapter.program));
assert.deepEqual(details.args, rawAdapter.args);
});
const ae = DebugAdapter.platformAdapterExecutable([extensionDescriptor0], 'mock');
assert.equal(ae.command, paths.join(extensionFolderPath, rawAdapter.program));
assert.deepEqual(ae.args, rawAdapter.args);
});
test('schema attributes', () => {
const schemaAttribute = adapter.getSchemaAttributes()[0];
const schemaAttribute = _debugger.getSchemaAttributes()[0];
assert.notDeepEqual(schemaAttribute, rawAdapter.configurationAttributes);
Object.keys(rawAdapter.configurationAttributes.launch).forEach(key => {
assert.deepEqual(schemaAttribute[key], rawAdapter.configurationAttributes.launch[key]);
......@@ -83,38 +151,13 @@ suite('Debug - Adapter', () => {
assert.equal(!!schemaAttribute['properties']['preLaunchTask'], true);
});
test('merge', () => {
const da: IRawAdapter = {
type: 'mock',
win: {
runtime: 'winRuntime'
},
linux: {
runtime: 'linuxRuntime'
},
osx: {
runtime: 'osxRuntime'
},
runtimeArgs: ['first arg'],
program: 'mockprogram',
args: ['arg']
};
adapter.merge(da, {
name: 'my name',
id: 'my_id',
version: '1.0',
publisher: 'mockPublisher',
isBuiltin: true,
extensionFolderPath: 'a/b/c/d',
engines: null
});
test('merge platform specific attributes', () => {
return adapter.getAdapterExecutable(undefined, false).then(details => {
assert.equal(details.command, platform.isLinux ? da.linux.runtime : platform.isMacintosh ? da.osx.runtime : da.win.runtime);
assert.deepEqual(details.args, da.runtimeArgs.concat(['a/b/c/d/mockprogram'].concat(da.args)));
});
const ae2 = DebugAdapter.platformAdapterExecutable([extensionDescriptor1, extensionDescriptor2], 'mock');
assert.equal(ae2.command, platform.isLinux ? 'linuxRuntime' : (platform.isMacintosh ? 'osxRuntime' : 'winRuntime'));
const xprogram = platform.isLinux ? 'linuxProgram' : (platform.isMacintosh ? 'osxProgram' : 'winProgram');
assert.deepEqual(ae2.args, ['rarg', '/e2/b/c/' + xprogram, 'parg']);
});
test('initial config file content', () => {
......@@ -134,7 +177,7 @@ suite('Debug - Adapter', () => {
' ]',
'}'].join('\n');
return adapter.getInitialConfigurationContent().then(content => {
return _debugger.getInitialConfigurationContent().then(content => {
assert.equal(content, expected);
}, err => assert.fail(err));
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册