Path: blob/main/src/vs/workbench/api/browser/mainThreadDebugService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';6import { URI as uri, UriComponents } from '../../../base/common/uri.js';7import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from '../../contrib/debug/common/debug.js';8import {9ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext,10IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto11} from '../common/extHost.protocol.js';12import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';13import severity from '../../../base/common/severity.js';14import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js';15import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js';16import { convertToVSCPaths, convertToDAPaths, isSessionAttach } from '../../contrib/debug/common/debugUtils.js';17import { ErrorNoTelemetry } from '../../../base/common/errors.js';18import { IDebugVisualizerService } from '../../contrib/debug/common/debugVisualizers.js';19import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';20import { Event } from '../../../base/common/event.js';21import { isDefined } from '../../../base/common/types.js';2223@extHostNamedCustomer(MainContext.MainThreadDebugService)24export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory {2526private readonly _proxy: ExtHostDebugServiceShape;27private readonly _toDispose = new DisposableStore();28private readonly _debugAdapters: Map<number, ExtensionHostDebugAdapter>;29private _debugAdaptersHandleCounter = 1;30private readonly _debugConfigurationProviders: Map<number, IDebugConfigurationProvider>;31private readonly _debugAdapterDescriptorFactories: Map<number, IDebugAdapterDescriptorFactory>;32private readonly _extHostKnownSessions: Set<DebugSessionUUID>;33private readonly _visualizerHandles = new Map<string, IDisposable>();34private readonly _visualizerTreeHandles = new Map<string, IDisposable>();3536constructor(37extHostContext: IExtHostContext,38@IDebugService private readonly debugService: IDebugService,39@IDebugVisualizerService private readonly visualizerService: IDebugVisualizerService,40) {41this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService);4243const sessionListeners = new DisposableMap<IDebugSession, DisposableStore>();44this._toDispose.add(sessionListeners);45this._toDispose.add(debugService.onDidNewSession(session => {46this._proxy.$acceptDebugSessionStarted(this.getSessionDto(session));47const store = sessionListeners.get(session);48store?.add(session.onDidChangeName(name => {49this._proxy.$acceptDebugSessionNameChanged(this.getSessionDto(session), name);50}));51}));52// Need to start listening early to new session events because a custom event can come while a session is initialising53this._toDispose.add(debugService.onWillNewSession(session => {54let store = sessionListeners.get(session);55if (!store) {56store = new DisposableStore();57sessionListeners.set(session, store);58}59store.add(session.onDidCustomEvent(event => this._proxy.$acceptDebugSessionCustomEvent(this.getSessionDto(session), event)));60}));61this._toDispose.add(debugService.onDidEndSession(({ session, restart }) => {62this._proxy.$acceptDebugSessionTerminated(this.getSessionDto(session));63this._extHostKnownSessions.delete(session.getId());6465// keep the session listeners around since we still will get events after they restart66if (!restart) {67sessionListeners.deleteAndDispose(session);68}6970// any restarted session will create a new DA, so always throw the old one away.71for (const [handle, value] of this._debugAdapters) {72if (value.session === session) {73this._debugAdapters.delete(handle);74// break;75}76}77}));78this._toDispose.add(debugService.getViewModel().onDidFocusSession(session => {79this._proxy.$acceptDebugSessionActiveChanged(this.getSessionDto(session));80}));81this._toDispose.add(toDisposable(() => {82for (const [handle, da] of this._debugAdapters) {83da.fireError(handle, new Error('Extension host shut down'));84}85}));8687this._debugAdapters = new Map();88this._debugConfigurationProviders = new Map();89this._debugAdapterDescriptorFactories = new Map();90this._extHostKnownSessions = new Set();9192const viewModel = this.debugService.getViewModel();93this._toDispose.add(Event.any(viewModel.onDidFocusStackFrame, viewModel.onDidFocusThread)(() => {94const stackFrame = viewModel.focusedStackFrame;95const thread = viewModel.focusedThread;96if (stackFrame) {97this._proxy.$acceptStackFrameFocus({98kind: 'stackFrame',99threadId: stackFrame.thread.threadId,100frameId: stackFrame.frameId,101sessionId: stackFrame.thread.session.getId(),102} satisfies IStackFrameFocusDto);103} else if (thread) {104this._proxy.$acceptStackFrameFocus({105kind: 'thread',106threadId: thread.threadId,107sessionId: thread.session.getId(),108} satisfies IThreadFocusDto);109} else {110this._proxy.$acceptStackFrameFocus(undefined);111}112}));113114this.sendBreakpointsAndListen();115}116117$registerDebugVisualizerTree(treeId: string, canEdit: boolean): void {118this._visualizerTreeHandles.set(treeId, this.visualizerService.registerTree(treeId, {119disposeItem: id => this._proxy.$disposeVisualizedTree(id),120getChildren: e => this._proxy.$getVisualizerTreeItemChildren(treeId, e),121getTreeItem: e => this._proxy.$getVisualizerTreeItem(treeId, e),122editItem: canEdit ? ((e, v) => this._proxy.$editVisualizerTreeItem(e, v)) : undefined123}));124}125126$unregisterDebugVisualizerTree(treeId: string): void {127this._visualizerTreeHandles.get(treeId)?.dispose();128this._visualizerTreeHandles.delete(treeId);129}130131$registerDebugVisualizer(extensionId: string, id: string): void {132const handle = this.visualizerService.register({133extensionId: new ExtensionIdentifier(extensionId),134id,135disposeDebugVisualizers: ids => this._proxy.$disposeDebugVisualizers(ids),136executeDebugVisualizerCommand: id => this._proxy.$executeDebugVisualizerCommand(id),137provideDebugVisualizers: (context, token) => this._proxy.$provideDebugVisualizers(extensionId, id, context, token).then(r => r.map(IDebugVisualization.deserialize)),138resolveDebugVisualizer: (viz, token) => this._proxy.$resolveDebugVisualizer(viz.id, token),139});140this._visualizerHandles.set(`${extensionId}/${id}`, handle);141}142143$unregisterDebugVisualizer(extensionId: string, id: string): void {144const key = `${extensionId}/${id}`;145this._visualizerHandles.get(key)?.dispose();146this._visualizerHandles.delete(key);147}148149private sendBreakpointsAndListen(): void {150// set up a handler to send more151this._toDispose.add(this.debugService.getModel().onDidChangeBreakpoints(e => {152// Ignore session only breakpoint events since they should only reflect in the UI153if (e && !e.sessionOnly) {154const delta: IBreakpointsDeltaDto = {};155if (e.added) {156delta.added = this.convertToDto(e.added);157}158if (e.removed) {159delta.removed = e.removed.map(x => x.getId());160}161if (e.changed) {162delta.changed = this.convertToDto(e.changed);163}164165if (delta.added || delta.removed || delta.changed) {166this._proxy.$acceptBreakpointsDelta(delta);167}168}169}));170171// send all breakpoints172const bps = this.debugService.getModel().getBreakpoints();173const fbps = this.debugService.getModel().getFunctionBreakpoints();174const dbps = this.debugService.getModel().getDataBreakpoints();175if (bps.length > 0 || fbps.length > 0) {176this._proxy.$acceptBreakpointsDelta({177added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps))178});179}180}181182public dispose(): void {183this._toDispose.dispose();184}185186// interface IDebugAdapterProvider187188createDebugAdapter(session: IDebugSession): IDebugAdapter {189const handle = this._debugAdaptersHandleCounter++;190const da = new ExtensionHostDebugAdapter(this, handle, this._proxy, session);191this._debugAdapters.set(handle, da);192return da;193}194195substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {196return Promise.resolve(this._proxy.$substituteVariables(folder ? folder.uri : undefined, config));197}198199runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise<number | undefined> {200return this._proxy.$runInTerminal(args, sessionId);201}202203// RPC methods (MainThreadDebugServiceShape)204205public $registerDebugTypes(debugTypes: string[]) {206this._toDispose.add(this.debugService.getAdapterManager().registerDebugAdapterFactory(debugTypes, this));207}208209public $registerBreakpoints(DTOs: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void> {210211for (const dto of DTOs) {212if (dto.type === 'sourceMulti') {213const rawbps = dto.lines.map((l): IBreakpointData => ({214id: l.id,215enabled: l.enabled,216lineNumber: l.line + 1,217column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784218condition: l.condition,219hitCondition: l.hitCondition,220logMessage: l.logMessage,221mode: l.mode,222}));223this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps);224} else if (dto.type === 'function') {225this.debugService.addFunctionBreakpoint({226name: dto.functionName,227mode: dto.mode,228condition: dto.condition,229hitCondition: dto.hitCondition,230enabled: dto.enabled,231logMessage: dto.logMessage232}, dto.id);233} else if (dto.type === 'data') {234this.debugService.addDataBreakpoint({235description: dto.label,236src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId },237canPersist: dto.canPersist,238accessTypes: dto.accessTypes,239accessType: dto.accessType,240mode: dto.mode241});242}243}244return Promise.resolve();245}246247public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise<void> {248breakpointIds.forEach(id => this.debugService.removeBreakpoints(id));249functionBreakpointIds.forEach(id => this.debugService.removeFunctionBreakpoints(id));250dataBreakpointIds.forEach(id => this.debugService.removeDataBreakpoints(id));251return Promise.resolve();252}253254public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, handle: number): Promise<void> {255256const provider: IDebugConfigurationProvider = {257type: debugType,258triggerKind: providerTriggerKind259};260if (hasProvide) {261provider.provideDebugConfigurations = (folder, token) => {262return this._proxy.$provideDebugConfigurations(handle, folder, token);263};264}265if (hasResolve) {266provider.resolveDebugConfiguration = (folder, config, token) => {267return this._proxy.$resolveDebugConfiguration(handle, folder, config, token);268};269}270if (hasResolve2) {271provider.resolveDebugConfigurationWithSubstitutedVariables = (folder, config, token) => {272return this._proxy.$resolveDebugConfigurationWithSubstitutedVariables(handle, folder, config, token);273};274}275this._debugConfigurationProviders.set(handle, provider);276this._toDispose.add(this.debugService.getConfigurationManager().registerDebugConfigurationProvider(provider));277278return Promise.resolve(undefined);279}280281public $unregisterDebugConfigurationProvider(handle: number): void {282const provider = this._debugConfigurationProviders.get(handle);283if (provider) {284this._debugConfigurationProviders.delete(handle);285this.debugService.getConfigurationManager().unregisterDebugConfigurationProvider(provider);286}287}288289public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise<void> {290291const provider: IDebugAdapterDescriptorFactory = {292type: debugType,293createDebugAdapterDescriptor: session => {294return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session)));295}296};297this._debugAdapterDescriptorFactories.set(handle, provider);298this._toDispose.add(this.debugService.getAdapterManager().registerDebugAdapterDescriptorFactory(provider));299300return Promise.resolve(undefined);301}302303public $unregisterDebugAdapterDescriptorFactory(handle: number): void {304const provider = this._debugAdapterDescriptorFactories.get(handle);305if (provider) {306this._debugAdapterDescriptorFactories.delete(handle);307this.debugService.getAdapterManager().unregisterDebugAdapterDescriptorFactory(provider);308}309}310311private getSession(sessionId: DebugSessionUUID | undefined): IDebugSession | undefined {312if (sessionId) {313return this.debugService.getModel().getSession(sessionId, true);314}315return undefined;316}317318public async $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, options: IStartDebuggingOptions): Promise<boolean> {319const folderUri = folder ? uri.revive(folder) : undefined;320const launch = this.debugService.getConfigurationManager().getLaunch(folderUri);321const parentSession = this.getSession(options.parentSessionID);322const saveBeforeStart = typeof options.suppressSaveBeforeStart === 'boolean' ? !options.suppressSaveBeforeStart : undefined;323const debugOptions: IDebugSessionOptions = {324noDebug: options.noDebug,325parentSession,326lifecycleManagedByParent: options.lifecycleManagedByParent,327repl: options.repl,328compact: options.compact,329compoundRoot: parentSession?.compoundRoot,330saveBeforeRestart: saveBeforeStart,331testRun: options.testRun,332333suppressDebugStatusbar: options.suppressDebugStatusbar,334suppressDebugToolbar: options.suppressDebugToolbar,335suppressDebugView: options.suppressDebugView,336};337try {338return this.debugService.startDebugging(launch, nameOrConfig, debugOptions, saveBeforeStart);339} catch (err) {340throw new ErrorNoTelemetry(err && err.message ? err.message : 'cannot start debugging');341}342}343344public $setDebugSessionName(sessionId: DebugSessionUUID, name: string): void {345const session = this.debugService.getModel().getSession(sessionId);346session?.setName(name);347}348349public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Promise<any> {350const session = this.debugService.getModel().getSession(sessionId, true);351if (session) {352return session.customRequest(request, args).then(response => {353if (response && response.success) {354return response.body;355} else {356return Promise.reject(new ErrorNoTelemetry(response ? response.message : 'custom request failed'));357}358});359}360return Promise.reject(new ErrorNoTelemetry('debug session not found'));361}362363public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise<DebugProtocol.Breakpoint | undefined> {364const session = this.debugService.getModel().getSession(sessionId, true);365if (session) {366return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId));367}368return Promise.reject(new ErrorNoTelemetry('debug session not found'));369}370371public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise<void> {372if (sessionId) {373const session = this.debugService.getModel().getSession(sessionId, true);374if (session) {375return this.debugService.stopSession(session, isSessionAttach(session));376}377} else { // stop all378return this.debugService.stopSession(undefined);379}380return Promise.reject(new ErrorNoTelemetry('debug session not found'));381}382383public $appendDebugConsole(value: string): void {384// Use warning as severity to get the orange color for messages coming from the debug extension385const session = this.debugService.getViewModel().focusedSession;386session?.appendToRepl({ output: value, sev: severity.Warning });387}388389public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) {390this.getDebugAdapter(handle).acceptMessage(convertToVSCPaths(message, false));391}392393public $acceptDAError(handle: number, name: string, message: string, stack: string) {394// don't use getDebugAdapter since an error can be expected on a post-close395this._debugAdapters.get(handle)?.fireError(handle, new Error(`${name}: ${message}\n${stack}`));396}397398public $acceptDAExit(handle: number, code: number, signal: string) {399// don't use getDebugAdapter since an error can be expected on a post-close400this._debugAdapters.get(handle)?.fireExit(handle, code, signal);401}402403private getDebugAdapter(handle: number): ExtensionHostDebugAdapter {404const adapter = this._debugAdapters.get(handle);405if (!adapter) {406throw new Error('Invalid debug adapter');407}408return adapter;409}410411// dto helpers412413public $sessionCached(sessionID: string) {414// remember that the EH has cached the session and we do not have to send it again415this._extHostKnownSessions.add(sessionID);416}417418419getSessionDto(session: undefined): undefined;420getSessionDto(session: IDebugSession): IDebugSessionDto;421getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined;422getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined {423if (session) {424const sessionID = <DebugSessionUUID>session.getId();425if (this._extHostKnownSessions.has(sessionID)) {426return sessionID;427} else {428// this._sessions.add(sessionID); // #69534: see $sessionCached above429return {430id: sessionID,431type: session.configuration.type,432name: session.name,433folderUri: session.root ? session.root.uri : undefined,434configuration: session.configuration,435parent: session.parentSession?.getId(),436};437}438}439return undefined;440}441442private convertToDto(bps: (ReadonlyArray<IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IInstructionBreakpoint>)): Array<ISourceBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto> {443return bps.map(bp => {444if ('name' in bp) {445const fbp: IFunctionBreakpoint = bp;446return {447type: 'function',448id: fbp.getId(),449enabled: fbp.enabled,450condition: fbp.condition,451hitCondition: fbp.hitCondition,452logMessage: fbp.logMessage,453functionName: fbp.name454} satisfies IFunctionBreakpointDto;455} else if ('src' in bp) {456const dbp: IDataBreakpoint = bp;457return {458type: 'data',459id: dbp.getId(),460dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address,461enabled: dbp.enabled,462condition: dbp.condition,463hitCondition: dbp.hitCondition,464logMessage: dbp.logMessage,465accessType: dbp.accessType,466label: dbp.description,467canPersist: dbp.canPersist468} satisfies IDataBreakpointDto;469} else if ('uri' in bp) {470const sbp: IBreakpoint = bp;471return {472type: 'source',473id: sbp.getId(),474enabled: sbp.enabled,475condition: sbp.condition,476hitCondition: sbp.hitCondition,477logMessage: sbp.logMessage,478uri: sbp.uri,479line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0,480character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0,481} satisfies ISourceBreakpointDto;482} else {483return undefined;484}485}).filter(isDefined);486}487}488489/**490* DebugAdapter that communicates via extension protocol with another debug adapter.491*/492class ExtensionHostDebugAdapter extends AbstractDebugAdapter {493494constructor(private readonly _ds: MainThreadDebugService, private _handle: number, private _proxy: ExtHostDebugServiceShape, readonly session: IDebugSession) {495super();496}497498fireError(handle: number, err: Error) {499this._onError.fire(err);500}501502fireExit(handle: number, code: number, signal: string) {503this._onExit.fire(code);504}505506startSession(): Promise<void> {507return Promise.resolve(this._proxy.$startDASession(this._handle, this._ds.getSessionDto(this.session)));508}509510sendMessage(message: DebugProtocol.ProtocolMessage): void {511this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, true));512}513514async stopSession(): Promise<void> {515await this.cancelPendingRequests();516return Promise.resolve(this._proxy.$stopDASession(this._handle));517}518}519520521