mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-26 18:27:38 +01:00
08ed2b692a
* debug: allow debugging the renderer process This adds a new parameter to the `launchVSCode` request -- `debugRenderer`. If set to true and we're in Electron, the main process will attempt to stand up a CDP-speaking server and respond with the port the debugger can connect to in order to handle debug events. Note: this only works when webviews are iframe-based. It's _possible_ to do the same treatment to webviews with greater complexity. I didn't implement that today, and maybe we say since iframes are coming eventually (and they can be toggled on with a user setting that we could have in the starter...) we don't need do add handling for that. This does not work when debugging in web because there's no way to force the browser to enter debug mode. Code in this PR is still rough, I will keep this in PR until I have js-debug working end to end and iron out any kinks. * fixup! adopt new api * webviews: add 'purpose' flag and extension ID to iframe webview uri The `purpose` can be used for notebooks instead of the extension ID, since there's no extension associated with the renderer view. The renderer URL now looks like: ``` https://<guid>.vscode-webview-test.com/<random>/index.html?id=<guid>&extensionId=&purpose=notebookRenderer ``` And a renderer is: ``` https://<guid>.vscode-webview-test.com/<random>/index.html?id=<guid>&extensionId=connor4312.vsix-viewer&purpose=undefined ``` I wanted to put this in the page title, but unfortunately this is hard due to https://bugs.chromium.org/p/chromium/issues/detail?id=1058108. Instead, add it to the url which _is_ available and given in the first targetInfoChanged event. Co-authored-by: Andre Weinand <aweinand@microsoft.com>
726 lines
25 KiB
TypeScript
726 lines
25 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as nls from 'vs/nls';
|
|
import { Event, Emitter } from 'vs/base/common/event';
|
|
import * as objects from 'vs/base/common/objects';
|
|
import { Action } from 'vs/base/common/actions';
|
|
import * as errors from 'vs/base/common/errors';
|
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils';
|
|
import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/contrib/debug/common/debug';
|
|
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
|
|
import { IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { IProcessEnvironment } from 'vs/base/common/platform';
|
|
import { env as processEnv } from 'vs/base/common/process';
|
|
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
|
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
|
|
|
/**
|
|
* This interface represents a single command line argument split into a "prefix" and a "path" half.
|
|
* The optional "prefix" contains arbitrary text and the optional "path" contains a file system path.
|
|
* Concatenating both results in the original command line argument.
|
|
*/
|
|
interface ILaunchVSCodeArgument {
|
|
prefix?: string;
|
|
path?: string;
|
|
}
|
|
|
|
interface ILaunchVSCodeArguments {
|
|
args: ILaunchVSCodeArgument[];
|
|
debugRenderer?: boolean;
|
|
env?: { [key: string]: string | null; };
|
|
}
|
|
|
|
/**
|
|
* Encapsulates the DebugAdapter lifecycle and some idiosyncrasies of the Debug Adapter Protocol.
|
|
*/
|
|
export class RawDebugSession implements IDisposable {
|
|
|
|
private allThreadsContinued = true;
|
|
private _readyForBreakpoints = false;
|
|
private _capabilities: DebugProtocol.Capabilities;
|
|
|
|
// shutdown
|
|
private debugAdapterStopped = false;
|
|
private inShutdown = false;
|
|
private terminated = false;
|
|
private firedAdapterExitEvent = false;
|
|
|
|
// telemetry
|
|
private startTime = 0;
|
|
private didReceiveStoppedEvent = false;
|
|
|
|
// DAP events
|
|
private readonly _onDidInitialize = new Emitter<DebugProtocol.InitializedEvent>();
|
|
private readonly _onDidStop = new Emitter<DebugProtocol.StoppedEvent>();
|
|
private readonly _onDidContinued = new Emitter<DebugProtocol.ContinuedEvent>();
|
|
private readonly _onDidTerminateDebugee = new Emitter<DebugProtocol.TerminatedEvent>();
|
|
private readonly _onDidExitDebugee = new Emitter<DebugProtocol.ExitedEvent>();
|
|
private readonly _onDidThread = new Emitter<DebugProtocol.ThreadEvent>();
|
|
private readonly _onDidOutput = new Emitter<DebugProtocol.OutputEvent>();
|
|
private readonly _onDidBreakpoint = new Emitter<DebugProtocol.BreakpointEvent>();
|
|
private readonly _onDidLoadedSource = new Emitter<DebugProtocol.LoadedSourceEvent>();
|
|
private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>();
|
|
private readonly _onDidProgressUpdate = new Emitter<DebugProtocol.ProgressUpdateEvent>();
|
|
private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>();
|
|
private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>();
|
|
private readonly _onDidEvent = new Emitter<DebugProtocol.Event>();
|
|
|
|
// DA events
|
|
private readonly _onDidExitAdapter = new Emitter<AdapterEndEvent>();
|
|
private debugAdapter: IDebugAdapter | null;
|
|
|
|
private toDispose: IDisposable[] = [];
|
|
|
|
constructor(
|
|
debugAdapter: IDebugAdapter,
|
|
dbgr: IDebugger,
|
|
private readonly telemetryService: ITelemetryService,
|
|
public readonly customTelemetryService: ITelemetryService | undefined,
|
|
private readonly extensionHostDebugService: IExtensionHostDebugService,
|
|
private readonly openerService: IOpenerService,
|
|
private readonly notificationService: INotificationService
|
|
) {
|
|
this.debugAdapter = debugAdapter;
|
|
this._capabilities = Object.create(null);
|
|
|
|
this.toDispose.push(this.debugAdapter.onError(err => {
|
|
this.shutdown(err);
|
|
}));
|
|
|
|
this.toDispose.push(this.debugAdapter.onExit(code => {
|
|
if (code !== 0) {
|
|
this.shutdown(new Error(`exit code: ${code}`));
|
|
} else {
|
|
// normal exit
|
|
this.shutdown();
|
|
}
|
|
}));
|
|
|
|
this.debugAdapter.onEvent(event => {
|
|
switch (event.event) {
|
|
case 'initialized':
|
|
this._readyForBreakpoints = true;
|
|
this._onDidInitialize.fire(event);
|
|
break;
|
|
case 'loadedSource':
|
|
this._onDidLoadedSource.fire(<DebugProtocol.LoadedSourceEvent>event);
|
|
break;
|
|
case 'capabilities':
|
|
if (event.body) {
|
|
const capabilities = (<DebugProtocol.CapabilitiesEvent>event).body.capabilities;
|
|
this.mergeCapabilities(capabilities);
|
|
}
|
|
break;
|
|
case 'stopped':
|
|
this.didReceiveStoppedEvent = true; // telemetry: remember that debugger stopped successfully
|
|
this._onDidStop.fire(<DebugProtocol.StoppedEvent>event);
|
|
break;
|
|
case 'continued':
|
|
this.allThreadsContinued = (<DebugProtocol.ContinuedEvent>event).body.allThreadsContinued === false ? false : true;
|
|
this._onDidContinued.fire(<DebugProtocol.ContinuedEvent>event);
|
|
break;
|
|
case 'thread':
|
|
this._onDidThread.fire(<DebugProtocol.ThreadEvent>event);
|
|
break;
|
|
case 'output':
|
|
this._onDidOutput.fire(<DebugProtocol.OutputEvent>event);
|
|
break;
|
|
case 'breakpoint':
|
|
this._onDidBreakpoint.fire(<DebugProtocol.BreakpointEvent>event);
|
|
break;
|
|
case 'terminated':
|
|
this._onDidTerminateDebugee.fire(<DebugProtocol.TerminatedEvent>event);
|
|
break;
|
|
case 'exit':
|
|
this._onDidExitDebugee.fire(<DebugProtocol.ExitedEvent>event);
|
|
break;
|
|
case 'progressStart':
|
|
this._onDidProgressStart.fire(event as DebugProtocol.ProgressStartEvent);
|
|
break;
|
|
case 'progressUpdate':
|
|
this._onDidProgressUpdate.fire(event as DebugProtocol.ProgressUpdateEvent);
|
|
break;
|
|
case 'progressEnd':
|
|
this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent);
|
|
break;
|
|
default:
|
|
this._onDidCustomEvent.fire(event);
|
|
break;
|
|
}
|
|
this._onDidEvent.fire(event);
|
|
});
|
|
|
|
this.debugAdapter.onRequest(request => this.dispatchRequest(request, dbgr));
|
|
}
|
|
|
|
get onDidExitAdapter(): Event<AdapterEndEvent> {
|
|
return this._onDidExitAdapter.event;
|
|
}
|
|
|
|
get capabilities(): DebugProtocol.Capabilities {
|
|
return this._capabilities;
|
|
}
|
|
|
|
/**
|
|
* DA is ready to accepts setBreakpoint requests.
|
|
* Becomes true after "initialized" events has been received.
|
|
*/
|
|
get readyForBreakpoints(): boolean {
|
|
return this._readyForBreakpoints;
|
|
}
|
|
|
|
//---- DAP events
|
|
|
|
get onDidInitialize(): Event<DebugProtocol.InitializedEvent> {
|
|
return this._onDidInitialize.event;
|
|
}
|
|
|
|
get onDidStop(): Event<DebugProtocol.StoppedEvent> {
|
|
return this._onDidStop.event;
|
|
}
|
|
|
|
get onDidContinued(): Event<DebugProtocol.ContinuedEvent> {
|
|
return this._onDidContinued.event;
|
|
}
|
|
|
|
get onDidTerminateDebugee(): Event<DebugProtocol.TerminatedEvent> {
|
|
return this._onDidTerminateDebugee.event;
|
|
}
|
|
|
|
get onDidExitDebugee(): Event<DebugProtocol.ExitedEvent> {
|
|
return this._onDidExitDebugee.event;
|
|
}
|
|
|
|
get onDidThread(): Event<DebugProtocol.ThreadEvent> {
|
|
return this._onDidThread.event;
|
|
}
|
|
|
|
get onDidOutput(): Event<DebugProtocol.OutputEvent> {
|
|
return this._onDidOutput.event;
|
|
}
|
|
|
|
get onDidBreakpoint(): Event<DebugProtocol.BreakpointEvent> {
|
|
return this._onDidBreakpoint.event;
|
|
}
|
|
|
|
get onDidLoadedSource(): Event<DebugProtocol.LoadedSourceEvent> {
|
|
return this._onDidLoadedSource.event;
|
|
}
|
|
|
|
get onDidCustomEvent(): Event<DebugProtocol.Event> {
|
|
return this._onDidCustomEvent.event;
|
|
}
|
|
|
|
get onDidProgressStart(): Event<DebugProtocol.ProgressStartEvent> {
|
|
return this._onDidProgressStart.event;
|
|
}
|
|
|
|
get onDidProgressUpdate(): Event<DebugProtocol.ProgressUpdateEvent> {
|
|
return this._onDidProgressUpdate.event;
|
|
}
|
|
|
|
get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> {
|
|
return this._onDidProgressEnd.event;
|
|
}
|
|
|
|
get onDidEvent(): Event<DebugProtocol.Event> {
|
|
return this._onDidEvent.event;
|
|
}
|
|
|
|
//---- DebugAdapter lifecycle
|
|
|
|
/**
|
|
* Starts the underlying debug adapter and tracks the session time for telemetry.
|
|
*/
|
|
async start(): Promise<void> {
|
|
if (!this.debugAdapter) {
|
|
return Promise.reject(new Error(nls.localize('noDebugAdapterStart', "No debug adapter, can not start debug session.")));
|
|
}
|
|
|
|
await this.debugAdapter.startSession();
|
|
this.startTime = new Date().getTime();
|
|
}
|
|
|
|
/**
|
|
* Send client capabilities to the debug adapter and receive DA capabilities in return.
|
|
*/
|
|
async initialize(args: DebugProtocol.InitializeRequestArguments): Promise<DebugProtocol.InitializeResponse> {
|
|
const response = await this.send('initialize', args);
|
|
this.mergeCapabilities(response.body);
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Terminate the debuggee and shutdown the adapter
|
|
*/
|
|
disconnect(restart = false): Promise<any> {
|
|
return this.shutdown(undefined, restart);
|
|
}
|
|
|
|
//---- DAP requests
|
|
|
|
async launchOrAttach(config: IConfig): Promise<DebugProtocol.Response> {
|
|
const response = await this.send(config.request, config);
|
|
this.mergeCapabilities(response.body);
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Try killing the debuggee softly...
|
|
*/
|
|
terminate(restart = false): Promise<DebugProtocol.TerminateResponse> {
|
|
if (this.capabilities.supportsTerminateRequest) {
|
|
if (!this.terminated) {
|
|
this.terminated = true;
|
|
return this.send('terminate', { restart }, undefined, 1000);
|
|
}
|
|
return this.disconnect(restart);
|
|
}
|
|
return Promise.reject(new Error('terminated not supported'));
|
|
}
|
|
|
|
restart(): Promise<DebugProtocol.RestartResponse> {
|
|
if (this.capabilities.supportsRestartRequest) {
|
|
return this.send('restart', null);
|
|
}
|
|
return Promise.reject(new Error('restart not supported'));
|
|
}
|
|
|
|
async next(args: DebugProtocol.NextArguments): Promise<DebugProtocol.NextResponse> {
|
|
const response = await this.send('next', args);
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
return response;
|
|
}
|
|
|
|
async stepIn(args: DebugProtocol.StepInArguments): Promise<DebugProtocol.StepInResponse> {
|
|
const response = await this.send('stepIn', args);
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
return response;
|
|
}
|
|
|
|
async stepOut(args: DebugProtocol.StepOutArguments): Promise<DebugProtocol.StepOutResponse> {
|
|
const response = await this.send('stepOut', args);
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
return response;
|
|
}
|
|
|
|
async continue(args: DebugProtocol.ContinueArguments): Promise<DebugProtocol.ContinueResponse> {
|
|
const response = await this.send<DebugProtocol.ContinueResponse>('continue', args);
|
|
if (response && response.body && response.body.allThreadsContinued !== undefined) {
|
|
this.allThreadsContinued = response.body.allThreadsContinued;
|
|
}
|
|
this.fireSimulatedContinuedEvent(args.threadId, this.allThreadsContinued);
|
|
|
|
return response;
|
|
}
|
|
|
|
pause(args: DebugProtocol.PauseArguments): Promise<DebugProtocol.PauseResponse> {
|
|
return this.send('pause', args);
|
|
}
|
|
|
|
terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise<DebugProtocol.TerminateThreadsResponse> {
|
|
if (this.capabilities.supportsTerminateThreadsRequest) {
|
|
return this.send('terminateThreads', args);
|
|
}
|
|
return Promise.reject(new Error('terminateThreads not supported'));
|
|
}
|
|
|
|
setVariable(args: DebugProtocol.SetVariableArguments): Promise<DebugProtocol.SetVariableResponse> {
|
|
if (this.capabilities.supportsSetVariable) {
|
|
return this.send<DebugProtocol.SetVariableResponse>('setVariable', args);
|
|
}
|
|
return Promise.reject(new Error('setVariable not supported'));
|
|
}
|
|
|
|
async restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): Promise<DebugProtocol.RestartFrameResponse> {
|
|
if (this.capabilities.supportsRestartFrame) {
|
|
const response = await this.send('restartFrame', args);
|
|
this.fireSimulatedContinuedEvent(threadId);
|
|
return response;
|
|
}
|
|
return Promise.reject(new Error('restartFrame not supported'));
|
|
}
|
|
|
|
stepInTargets(args: DebugProtocol.StepInTargetsArguments): Promise<DebugProtocol.StepInTargetsResponse> {
|
|
if (this.capabilities.supportsStepInTargetsRequest) {
|
|
return this.send('stepInTargets', args);
|
|
}
|
|
return Promise.reject(new Error('stepInTargets not supported'));
|
|
}
|
|
|
|
completions(args: DebugProtocol.CompletionsArguments, token: CancellationToken): Promise<DebugProtocol.CompletionsResponse> {
|
|
if (this.capabilities.supportsCompletionsRequest) {
|
|
return this.send<DebugProtocol.CompletionsResponse>('completions', args, token);
|
|
}
|
|
return Promise.reject(new Error('completions not supported'));
|
|
}
|
|
|
|
setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise<DebugProtocol.SetBreakpointsResponse> {
|
|
return this.send<DebugProtocol.SetBreakpointsResponse>('setBreakpoints', args);
|
|
}
|
|
|
|
setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise<DebugProtocol.SetFunctionBreakpointsResponse> {
|
|
if (this.capabilities.supportsFunctionBreakpoints) {
|
|
return this.send<DebugProtocol.SetFunctionBreakpointsResponse>('setFunctionBreakpoints', args);
|
|
}
|
|
return Promise.reject(new Error('setFunctionBreakpoints not supported'));
|
|
}
|
|
|
|
dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<DebugProtocol.DataBreakpointInfoResponse> {
|
|
if (this.capabilities.supportsDataBreakpoints) {
|
|
return this.send<DebugProtocol.DataBreakpointInfoResponse>('dataBreakpointInfo', args);
|
|
}
|
|
return Promise.reject(new Error('dataBreakpointInfo not supported'));
|
|
}
|
|
|
|
setDataBreakpoints(args: DebugProtocol.SetDataBreakpointsArguments): Promise<DebugProtocol.SetDataBreakpointsResponse> {
|
|
if (this.capabilities.supportsDataBreakpoints) {
|
|
return this.send<DebugProtocol.SetDataBreakpointsResponse>('setDataBreakpoints', args);
|
|
}
|
|
return Promise.reject(new Error('setDataBreakpoints not supported'));
|
|
}
|
|
|
|
setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise<DebugProtocol.SetExceptionBreakpointsResponse> {
|
|
return this.send<DebugProtocol.SetExceptionBreakpointsResponse>('setExceptionBreakpoints', args);
|
|
}
|
|
|
|
breakpointLocations(args: DebugProtocol.BreakpointLocationsArguments): Promise<DebugProtocol.BreakpointLocationsResponse> {
|
|
if (this.capabilities.supportsBreakpointLocationsRequest) {
|
|
return this.send('breakpointLocations', args);
|
|
}
|
|
return Promise.reject(new Error('breakpointLocations is not supported'));
|
|
}
|
|
|
|
configurationDone(): Promise<DebugProtocol.ConfigurationDoneResponse> {
|
|
if (this.capabilities.supportsConfigurationDoneRequest) {
|
|
return this.send('configurationDone', null);
|
|
}
|
|
return Promise.reject(new Error('configurationDone not supported'));
|
|
}
|
|
|
|
stackTrace(args: DebugProtocol.StackTraceArguments, token: CancellationToken): Promise<DebugProtocol.StackTraceResponse> {
|
|
return this.send<DebugProtocol.StackTraceResponse>('stackTrace', args, token);
|
|
}
|
|
|
|
exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise<DebugProtocol.ExceptionInfoResponse> {
|
|
if (this.capabilities.supportsExceptionInfoRequest) {
|
|
return this.send<DebugProtocol.ExceptionInfoResponse>('exceptionInfo', args);
|
|
}
|
|
return Promise.reject(new Error('exceptionInfo not supported'));
|
|
}
|
|
|
|
scopes(args: DebugProtocol.ScopesArguments, token: CancellationToken): Promise<DebugProtocol.ScopesResponse> {
|
|
return this.send<DebugProtocol.ScopesResponse>('scopes', args, token);
|
|
}
|
|
|
|
variables(args: DebugProtocol.VariablesArguments, token?: CancellationToken): Promise<DebugProtocol.VariablesResponse> {
|
|
return this.send<DebugProtocol.VariablesResponse>('variables', args, token);
|
|
}
|
|
|
|
source(args: DebugProtocol.SourceArguments): Promise<DebugProtocol.SourceResponse> {
|
|
return this.send<DebugProtocol.SourceResponse>('source', args);
|
|
}
|
|
|
|
loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise<DebugProtocol.LoadedSourcesResponse> {
|
|
if (this.capabilities.supportsLoadedSourcesRequest) {
|
|
return this.send<DebugProtocol.LoadedSourcesResponse>('loadedSources', args);
|
|
}
|
|
return Promise.reject(new Error('loadedSources not supported'));
|
|
}
|
|
|
|
threads(): Promise<DebugProtocol.ThreadsResponse> {
|
|
return this.send<DebugProtocol.ThreadsResponse>('threads', null);
|
|
}
|
|
|
|
evaluate(args: DebugProtocol.EvaluateArguments): Promise<DebugProtocol.EvaluateResponse> {
|
|
return this.send<DebugProtocol.EvaluateResponse>('evaluate', args);
|
|
}
|
|
|
|
async stepBack(args: DebugProtocol.StepBackArguments): Promise<DebugProtocol.StepBackResponse> {
|
|
if (this.capabilities.supportsStepBack) {
|
|
const response = await this.send('stepBack', args);
|
|
if (response.body === undefined) { // TODO@AW why this check?
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
}
|
|
return response;
|
|
}
|
|
return Promise.reject(new Error('stepBack not supported'));
|
|
}
|
|
|
|
async reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise<DebugProtocol.ReverseContinueResponse> {
|
|
if (this.capabilities.supportsStepBack) {
|
|
const response = await this.send('reverseContinue', args);
|
|
if (response.body === undefined) { // TODO@AW why this check?
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
}
|
|
return response;
|
|
}
|
|
return Promise.reject(new Error('reverseContinue not supported'));
|
|
}
|
|
|
|
gotoTargets(args: DebugProtocol.GotoTargetsArguments): Promise<DebugProtocol.GotoTargetsResponse> {
|
|
if (this.capabilities.supportsGotoTargetsRequest) {
|
|
return this.send('gotoTargets', args);
|
|
}
|
|
return Promise.reject(new Error('gotoTargets is not supported'));
|
|
}
|
|
|
|
async goto(args: DebugProtocol.GotoArguments): Promise<DebugProtocol.GotoResponse> {
|
|
if (this.capabilities.supportsGotoTargetsRequest) {
|
|
const response = await this.send('goto', args);
|
|
this.fireSimulatedContinuedEvent(args.threadId);
|
|
return response;
|
|
}
|
|
|
|
return Promise.reject(new Error('goto is not supported'));
|
|
}
|
|
|
|
cancel(args: DebugProtocol.CancelArguments): Promise<DebugProtocol.CancelResponse> {
|
|
return this.send('cancel', args);
|
|
}
|
|
|
|
custom(request: string, args: any): Promise<DebugProtocol.Response> {
|
|
return this.send(request, args);
|
|
}
|
|
|
|
//---- private
|
|
|
|
private async shutdown(error?: Error, restart = false): Promise<any> {
|
|
if (!this.inShutdown) {
|
|
this.inShutdown = true;
|
|
if (this.debugAdapter) {
|
|
try {
|
|
await this.send('disconnect', { restart }, undefined, 1000);
|
|
} finally {
|
|
this.stopAdapter(error);
|
|
}
|
|
} else {
|
|
return this.stopAdapter(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async stopAdapter(error?: Error): Promise<any> {
|
|
try {
|
|
if (this.debugAdapter) {
|
|
const da = this.debugAdapter;
|
|
this.debugAdapter = null;
|
|
await da.stopSession();
|
|
this.debugAdapterStopped = true;
|
|
}
|
|
} finally {
|
|
this.fireAdapterExitEvent(error);
|
|
}
|
|
}
|
|
|
|
private fireAdapterExitEvent(error?: Error): void {
|
|
if (!this.firedAdapterExitEvent) {
|
|
this.firedAdapterExitEvent = true;
|
|
|
|
const e: AdapterEndEvent = {
|
|
emittedStopped: this.didReceiveStoppedEvent,
|
|
sessionLengthInSeconds: (new Date().getTime() - this.startTime) / 1000
|
|
};
|
|
if (error && !this.debugAdapterStopped) {
|
|
e.error = error;
|
|
}
|
|
this._onDidExitAdapter.fire(e);
|
|
}
|
|
}
|
|
|
|
private async dispatchRequest(request: DebugProtocol.Request, dbgr: IDebugger): Promise<void> {
|
|
|
|
const response: DebugProtocol.Response = {
|
|
type: 'response',
|
|
seq: 0,
|
|
command: request.command,
|
|
request_seq: request.seq,
|
|
success: true
|
|
};
|
|
|
|
const safeSendResponse = (response: DebugProtocol.Response) => this.debugAdapter && this.debugAdapter.sendResponse(response);
|
|
|
|
switch (request.command) {
|
|
case 'launchVSCode':
|
|
this.launchVsCode(<ILaunchVSCodeArguments>request.arguments).then(result => {
|
|
response.body = {
|
|
rendererDebugPort: result.rendererDebugPort,
|
|
//processId: pid
|
|
};
|
|
safeSendResponse(response);
|
|
}, err => {
|
|
response.success = false;
|
|
response.message = err.message;
|
|
safeSendResponse(response);
|
|
});
|
|
break;
|
|
case 'runInTerminal':
|
|
try {
|
|
const shellProcessId = await dbgr.runInTerminal(request.arguments as DebugProtocol.RunInTerminalRequestArguments);
|
|
const resp = response as DebugProtocol.RunInTerminalResponse;
|
|
resp.body = {};
|
|
if (typeof shellProcessId === 'number') {
|
|
resp.body.shellProcessId = shellProcessId;
|
|
}
|
|
safeSendResponse(resp);
|
|
} catch (err) {
|
|
response.success = false;
|
|
response.message = err.message;
|
|
safeSendResponse(response);
|
|
}
|
|
break;
|
|
default:
|
|
response.success = false;
|
|
response.message = `unknown request '${request.command}'`;
|
|
safeSendResponse(response);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise<IOpenExtensionWindowResult> {
|
|
|
|
const args: string[] = [];
|
|
|
|
for (let arg of vscodeArgs.args) {
|
|
const a2 = (arg.prefix || '') + (arg.path || '');
|
|
const match = /^--(.+)=(.+)$/.exec(a2);
|
|
if (match && match.length === 3) {
|
|
const key = match[1];
|
|
let value = match[2];
|
|
|
|
if ((key === 'file-uri' || key === 'folder-uri') && !isUri(arg.path)) {
|
|
value = URI.file(value).toString();
|
|
}
|
|
args.push(`--${key}=${value}`);
|
|
} else {
|
|
args.push(a2);
|
|
}
|
|
}
|
|
|
|
let env: IProcessEnvironment = {};
|
|
if (vscodeArgs.env) {
|
|
// merge environment variables into a copy of the process.env
|
|
env = objects.mixin(processEnv, vscodeArgs.env);
|
|
// and delete some if necessary
|
|
Object.keys(env).filter(k => env[k] === null).forEach(key => delete env[key]);
|
|
}
|
|
|
|
return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env, !!vscodeArgs.debugRenderer);
|
|
}
|
|
|
|
private send<R extends DebugProtocol.Response>(command: string, args: any, token?: CancellationToken, timeout?: number): Promise<R> {
|
|
return new Promise<DebugProtocol.Response>((completeDispatch, errorDispatch) => {
|
|
if (!this.debugAdapter) {
|
|
if (this.inShutdown) {
|
|
// We are in shutdown silently complete
|
|
completeDispatch();
|
|
} else {
|
|
errorDispatch(new Error(nls.localize('noDebugAdapter', "No debug adapter found. Can not send '{0}'.", command)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
let cancelationListener: IDisposable;
|
|
const requestId = this.debugAdapter.sendRequest(command, args, (response: DebugProtocol.Response) => {
|
|
if (cancelationListener) {
|
|
cancelationListener.dispose();
|
|
}
|
|
|
|
if (response.success) {
|
|
completeDispatch(response);
|
|
} else {
|
|
errorDispatch(response);
|
|
}
|
|
}, timeout);
|
|
|
|
if (token) {
|
|
cancelationListener = token.onCancellationRequested(() => {
|
|
cancelationListener.dispose();
|
|
if (this.capabilities.supportsCancelRequest) {
|
|
this.cancel({ requestId });
|
|
}
|
|
});
|
|
}
|
|
}).then(undefined, err => Promise.reject(this.handleErrorResponse(err)));
|
|
}
|
|
|
|
private handleErrorResponse(errorResponse: DebugProtocol.Response): Error {
|
|
|
|
if (errorResponse.command === 'canceled' && errorResponse.message === 'canceled') {
|
|
return errors.canceled();
|
|
}
|
|
|
|
const error: DebugProtocol.Message | undefined = errorResponse?.body?.error;
|
|
const errorMessage = errorResponse?.message || '';
|
|
|
|
if (error && error.sendTelemetry) {
|
|
const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage;
|
|
this.telemetryDebugProtocolErrorResponse(telemetryMessage);
|
|
}
|
|
|
|
const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage;
|
|
const url = error?.url;
|
|
if (error && url) {
|
|
const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info");
|
|
return createErrorWithActions(userMessage, {
|
|
actions: [new Action('debug.moreInfo', label, undefined, true, () => {
|
|
this.openerService.open(URI.parse(url));
|
|
return Promise.resolve(null);
|
|
})]
|
|
});
|
|
}
|
|
if (error && error.format && error.showUser) {
|
|
this.notificationService.error(userMessage);
|
|
}
|
|
|
|
return new Error(userMessage);
|
|
}
|
|
|
|
private mergeCapabilities(capabilities: DebugProtocol.Capabilities | undefined): void {
|
|
if (capabilities) {
|
|
this._capabilities = objects.mixin(this._capabilities, capabilities);
|
|
}
|
|
}
|
|
|
|
private fireSimulatedContinuedEvent(threadId: number, allThreadsContinued = false): void {
|
|
this._onDidContinued.fire({
|
|
type: 'event',
|
|
event: 'continued',
|
|
body: {
|
|
threadId,
|
|
allThreadsContinued
|
|
},
|
|
seq: undefined!
|
|
});
|
|
}
|
|
|
|
private telemetryDebugProtocolErrorResponse(telemetryMessage: string | undefined) {
|
|
/* __GDPR__
|
|
"debugProtocolErrorResponse" : {
|
|
"error" : { "classification": "CallstackOrException", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryService.publicLogError('debugProtocolErrorResponse', { error: telemetryMessage });
|
|
if (this.customTelemetryService) {
|
|
/* __GDPR__TODO__
|
|
The message is sent in the name of the adapter but the adapter doesn't know about it.
|
|
However, since adapters are an open-ended set, we can not declared the events statically either.
|
|
*/
|
|
this.customTelemetryService.publicLogError('debugProtocolErrorResponse', { error: telemetryMessage });
|
|
}
|
|
}
|
|
|
|
dispose(): void {
|
|
dispose(this.toDispose);
|
|
}
|
|
}
|