// ***************************************************************************** // Copyright (C) 2018 Red Hat, Inc. and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { Path } from '@theia/core/lib/common/path'; import * as theia from '@theia/plugin'; import { URI } from '@theia/core/shared/vscode-uri'; import { Breakpoint, DebugStackFrameDTO, DebugThreadDTO } from '../../common/plugin-api-rpc-model'; import { DebugConfigurationProviderTriggerKind, DebugExt, DebugMain, PLUGIN_RPC_CONTEXT as Ext, TerminalOptionsExt } from '../../common/plugin-api-rpc'; import { PluginPackageDebuggersContribution } from '../../common/plugin-protocol'; import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl } from '../command-registry'; import { ConnectionImpl } from '../../common/connection'; import { DEBUG_SCHEME, SCHEME_PATTERN } from '@theia/debug/lib/common/debug-uri-utils'; import { Disposable, Breakpoint as BreakpointExt, SourceBreakpoint, FunctionBreakpoint, Location, Range, URI as URIImpl, DebugStackFrame, DebugThread } from '../types-impl'; import { PluginDebugAdapterSession } from './plugin-debug-adapter-session'; import { PluginDebugAdapterTracker } from './plugin-debug-adapter-tracker'; import { generateUuid } from '@theia/core/lib/common/uuid'; import { DebugAdapter } from '@theia/debug/lib/common/debug-model'; import { PluginDebugAdapterCreator } from './plugin-debug-adapter-creator'; import { NodeDebugAdapterCreator } from '../node/debug/plugin-node-debug-adapter-creator'; import { DebugProtocol } from '@vscode/debugprotocol'; import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration'; import { checkTestRunInstance } from '../tests'; import { PluginLogger } from '../logger'; interface ConfigurationProviderRecord { handle: number; type: string; trigger: DebugConfigurationProviderTriggerKind, provider: theia.DebugConfigurationProvider; } /* eslint-disable @typescript-eslint/no-explicit-any */ @injectable() export class DebugExtImpl implements DebugExt { @inject(RPCProtocol) protected readonly rpc: RPCProtocol; // debug sessions by sessionId private sessions = new Map(); private configurationProviderHandleGenerator: number; private configurationProviders: ConfigurationProviderRecord[]; /** * Only use internally, don't send it to the frontend. It's expensive! * It's already there as a part of the plugin metadata. */ private debuggersContributions = new Map(); private descriptorFactories = new Map(); private trackerFactories: [string, theia.DebugAdapterTrackerFactory][] = []; private contributionPaths = new Map(); private contributionTypes = new Map(); private connectionExt: ConnectionImpl; private commandRegistryExt: CommandRegistryImpl; private proxy: DebugMain; private logger: PluginLogger; private readonly onDidChangeBreakpointsEmitter = new Emitter(); private readonly onDidChangeActiveDebugSessionEmitter = new Emitter(); private readonly onDidTerminateDebugSessionEmitter = new Emitter(); private readonly onDidCreateDebugSessionEmitter = new Emitter(); private readonly onDidStartDebugSessionEmitter = new Emitter(); private readonly onDidReceiveDebugSessionCustomEmitter = new Emitter(); activeDebugSession: theia.DebugSession | undefined; activeDebugConsole: theia.DebugConsole; _activeStackItem: theia.DebugStackFrame | theia.DebugThread | undefined; private readonly onDidChangeActiveStackItemEmitter = new Emitter(); private readonly _breakpoints = new Map(); private frontendAdapterCreator = new PluginDebugAdapterCreator(); private backendAdapterCreator = new NodeDebugAdapterCreator(); get breakpoints(): theia.Breakpoint[] { return [...this._breakpoints.values()]; } constructor() { this.activeDebugConsole = { append: (value: string) => this.proxy.$appendToDebugConsole(value), appendLine: (value: string) => this.proxy.$appendLineToDebugConsole(value) }; this.configurationProviderHandleGenerator = 0; this.configurationProviders = []; } @postConstruct() initialize(): void { this.proxy = this.rpc.getProxy(Ext.DEBUG_MAIN); this.logger = new PluginLogger(this.rpc, 'debug'); } /** * Sets dependencies. */ assistedInject(connectionExt: ConnectionImpl, commandRegistryExt: CommandRegistryImpl): void { this.connectionExt = connectionExt; this.commandRegistryExt = commandRegistryExt; } /** * Registers contributions. * @param pluginFolder plugin folder path * @param pluginType plugin type * @param contributions available debuggers contributions */ registerDebuggersContributions(pluginFolder: string, pluginType: theia.PluginType, contributions: PluginPackageDebuggersContribution[]): void { contributions.forEach(contribution => { this.contributionPaths.set(contribution.type, pluginFolder); this.contributionTypes.set(contribution.type, pluginType); this.debuggersContributions.set(contribution.type, contribution); this.proxy.$registerDebuggerContribution({ type: contribution.type, label: contribution.label || contribution.type }); this.logger.debug(`Debugger contribution has been registered: ${contribution.type}`); }); } get onDidReceiveDebugSessionCustomEvent(): theia.Event { return this.onDidReceiveDebugSessionCustomEmitter.event; } get onDidChangeActiveDebugSession(): theia.Event { return this.onDidChangeActiveDebugSessionEmitter.event; } get onDidTerminateDebugSession(): theia.Event { return this.onDidTerminateDebugSessionEmitter.event; } get onDidCreateDebugSession(): theia.Event { return this.onDidCreateDebugSessionEmitter.event; } get onDidStartDebugSession(): theia.Event { return this.onDidStartDebugSessionEmitter.event; } get onDidChangeActiveStackItem(): theia.Event { return this.onDidChangeActiveStackItemEmitter.event; } get onDidChangeBreakpoints(): theia.Event { return this.onDidChangeBreakpointsEmitter.event; } addBreakpoints(breakpoints: readonly theia.Breakpoint[]): void { const added: theia.Breakpoint[] = []; for (const b of breakpoints) { if (this._breakpoints.has(b.id)) { continue; } this._breakpoints.set(b.id, b); added.push(b); } if (added.length) { this.onDidChangeBreakpointsEmitter.fire({ added, removed: [], changed: [] }); this.proxy.$addBreakpoints(added); } } removeBreakpoints(breakpoints: readonly theia.Breakpoint[]): void { const removed: theia.Breakpoint[] = []; const removedIds: string[] = []; for (const b of breakpoints) { if (!this._breakpoints.has(b.id)) { continue; } this._breakpoints.delete(b.id); removed.push(b); removedIds.push(b.id); } if (removed.length) { this.onDidChangeBreakpointsEmitter.fire({ added: [], removed, changed: [] }); this.proxy.$removeBreakpoints(removedIds); } } startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration, options: theia.DebugSessionOptions): PromiseLike { const optionsDto: DebugSessionOptions = { parentSessionId: options.parentSession?.id, compact: options.compact, consoleMode: options.consoleMode, suppressSaveBeforeStart: options.suppressSaveBeforeStart, suppressDebugStatusbar: options.suppressDebugStatusbar, suppressDebugView: options.suppressDebugView, lifecycleManagedByParent: options.lifecycleManagedByParent, noDebug: options.noDebug, }; if (options.testRun) { const run = checkTestRunInstance(options.testRun); optionsDto.testRun = { controllerId: run.controller.id, runId: run.id }; } return this.proxy.$startDebugging(folder, nameOrConfiguration, optionsDto); } stopDebugging(session?: theia.DebugSession): PromiseLike { return this.proxy.$stopDebugging(session?.id); } asDebugSourceUri(source: theia.DebugProtocolSource, session?: theia.DebugSession): theia.Uri { return this.getDebugSourceUri(source, session?.id); } private getDebugSourceUri(raw: DebugProtocol.Source, sessionId?: string): theia.Uri { if (raw.sourceReference && raw.sourceReference > 0) { let query = 'ref=' + String(raw.sourceReference); if (sessionId) { query += `&session=${sessionId}`; } return URIImpl.from({ scheme: DEBUG_SCHEME, path: raw.path ?? '', query }); } if (!raw.path) { throw new Error('Unrecognized source type: ' + JSON.stringify(raw)); } if (raw.path.match(SCHEME_PATTERN)) { return URIImpl.parse(raw.path); } return URIImpl.file(raw.path); } registerDebugAdapterDescriptorFactory(debugType: string, factory: theia.DebugAdapterDescriptorFactory): Disposable { if (this.descriptorFactories.has(debugType)) { throw new Error(`Descriptor factory for ${debugType} has been already registered`); } this.descriptorFactories.set(debugType, factory); return Disposable.create(() => this.descriptorFactories.delete(debugType)); } registerDebugAdapterTrackerFactory(debugType: string, factory: theia.DebugAdapterTrackerFactory): Disposable { if (!factory) { return Disposable.create(() => { }); } this.trackerFactories.push([debugType, factory]); return Disposable.create(() => { this.trackerFactories = this.trackerFactories.filter(tuple => tuple[1] !== factory); }); } registerDebugConfigurationProvider(debugType: string, provider: theia.DebugConfigurationProvider, trigger: DebugConfigurationProviderTriggerKind): Disposable { this.logger.info(`Debug configuration provider has been registered: ${debugType}, trigger: ${trigger}`); const handle = this.configurationProviderHandleGenerator++; this.configurationProviders.push({ handle, type: debugType, trigger, provider }); const descriptor = { handle, type: debugType, trigger, provideDebugConfiguration: !!provider.provideDebugConfigurations, resolveDebugConfigurations: !!provider.resolveDebugConfiguration, resolveDebugConfigurationWithSubstitutedVariables: !!provider.resolveDebugConfigurationWithSubstitutedVariables }; this.proxy.$registerDebugConfigurationProvider(descriptor); return Disposable.create(() => { this.configurationProviders = this.configurationProviders.filter(p => (p.handle !== handle)); this.proxy.$unregisterDebugConfigurationProvider(handle); }); } set activeStackItem(stackItem: theia.DebugStackFrame | theia.DebugThread | undefined) { if (this._activeStackItem === stackItem) { return; } this._activeStackItem = stackItem; this.onDidChangeActiveStackItemEmitter.fire(this.activeStackItem); } get activeStackItem(): theia.DebugStackFrame | theia.DebugThread | undefined { return this._activeStackItem; } async $onDidChangeActiveThread(debugThread: DebugThreadDTO | undefined): Promise { if (!debugThread) { this.activeStackItem = undefined; return; } const session = this.sessions.get(debugThread.sessionId); if (session) { this.activeStackItem = new DebugThread(session, debugThread.threadId); } } async $onDidChangeActiveFrame(debugFrame: DebugStackFrameDTO | undefined): Promise { if (!debugFrame) { this.activeStackItem = undefined; return; } const session = this.sessions.get(debugFrame!.sessionId); if (session) { this.activeStackItem = new DebugStackFrame(session, debugFrame.threadId, debugFrame.frameId); } } async $onSessionCustomEvent(sessionId: string, event: string, body?: any): Promise { const session = this.sessions.get(sessionId); if (session) { this.onDidReceiveDebugSessionCustomEmitter.fire({ event, body, session }); } } async $sessionDidCreate(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (session) { this.onDidCreateDebugSessionEmitter.fire(session); } } async $sessionDidStart(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (session) { this.onDidStartDebugSessionEmitter.fire(session); } } async $sessionDidDestroy(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (session) { this.onDidTerminateDebugSessionEmitter.fire(session); this.sessions.delete(sessionId); } } async $sessionDidChange(sessionId: string | undefined): Promise { this.activeDebugSession = sessionId ? this.sessions.get(sessionId) : undefined; this.onDidChangeActiveDebugSessionEmitter.fire(this.activeDebugSession); } async $breakpointsDidChange(added: Breakpoint[], removed: string[], changed: Breakpoint[]): Promise { const a: theia.Breakpoint[] = []; const r: theia.Breakpoint[] = []; const c: theia.Breakpoint[] = []; for (const b of added) { if (this._breakpoints.has(b.id)) { continue; } const bExt = this.toBreakpointExt(b); if (bExt) { this._breakpoints.set(bExt.id, bExt); a.push(bExt); } } for (const id of removed) { const bExt = this._breakpoints.get(id); if (bExt) { this._breakpoints.delete(id); r.push(bExt); } } for (const b of changed) { const bExt = this._breakpoints.get(b.id); if (bExt) { const { functionName, location, enabled, condition, hitCondition, logMessage } = b; if (bExt instanceof FunctionBreakpoint && functionName) { Object.assign(bExt, { enabled, condition, hitCondition, logMessage, functionName }); } else if (bExt instanceof SourceBreakpoint && location) { const range = new Range(location.range.startLineNumber, location.range.startColumn, location.range.endLineNumber, location.range.endColumn); Object.assign(bExt, { enabled, condition, hitCondition, logMessage, location: new Location(URI.revive(location.uri), range) }); } c.push(bExt); } } this.onDidChangeBreakpointsEmitter.fire({ added: a, removed: r, changed: c }); } protected toBreakpointExt({ functionName, location, enabled, condition, hitCondition, logMessage, id }: Breakpoint): BreakpointExt | undefined { if (location) { const range = new Range(location.range.startLineNumber, location.range.startColumn, location.range.endLineNumber, location.range.endColumn); return new SourceBreakpoint(new Location(URI.revive(location.uri), range), enabled, condition, hitCondition, logMessage, id); } if (functionName) { return new FunctionBreakpoint(functionName!, enabled, condition, hitCondition, logMessage, id); } return undefined; } async $createDebugSession(debugConfiguration: DebugConfiguration, workspaceFolderUri: string | undefined): Promise { const sessionId = generateUuid(); const parentSession = debugConfiguration.parentSessionId ? this.sessions.get(debugConfiguration.parentSessionId) : undefined; const theiaSession: theia.DebugSession = { id: sessionId, type: debugConfiguration.type, name: debugConfiguration.name, parentSession: parentSession, workspaceFolder: this.toWorkspaceFolder(workspaceFolderUri), configuration: debugConfiguration, customRequest: async (command: string, args?: any) => { const response = await this.proxy.$customRequest(sessionId, command, args); if (response && response.success) { return response.body; } return Promise.reject(new Error(response.message ?? 'custom request failed')); }, getDebugProtocolBreakpoint: async (breakpoint: Breakpoint) => this.proxy.$getDebugProtocolBreakpoint(sessionId, breakpoint.id) }; const tracker = await this.createDebugAdapterTracker(theiaSession); const communicationProvider = await this.createDebugAdapter(theiaSession, debugConfiguration); const debugAdapterSession = new PluginDebugAdapterSession(communicationProvider, tracker, theiaSession); this.sessions.set(sessionId, debugAdapterSession); const connection = await this.connectionExt!.ensureConnection(sessionId); debugAdapterSession.start(connection); return sessionId; } async $terminateDebugSession(sessionId: string): Promise { const debugAdapterSession = this.sessions.get(sessionId); if (debugAdapterSession) { await debugAdapterSession.stop(); } } async $getTerminalCreationOptions(debugType: string): Promise { return this.doGetTerminalCreationOptions(debugType); } async doGetTerminalCreationOptions(debugType: string): Promise { return undefined; } private getConfigurationProviderRecord(handle: number): { provider: theia.DebugConfigurationProvider type: string } { const record = this.configurationProviders.find(p => p.handle === handle); if (!record) { throw new Error('No Debug configuration provider found with given handle number: ' + handle); } const { provider, type } = record; return { provider, type }; } async $provideDebugConfigurationsByHandle( handle: number, workspaceFolderUri: string | undefined ): Promise { const { provider, type } = this.getConfigurationProviderRecord(handle); const configurations = await provider.provideDebugConfigurations?.( this.toWorkspaceFolder(workspaceFolderUri) ); if (!configurations) { throw new Error( 'nothing returned from DebugConfigurationProvider.provideDebugConfigurations, type: ' + type ); } return configurations; } async $resolveDebugConfigurationByHandle( handle: number, workspaceFolderUri: string | undefined, debugConfiguration: theia.DebugConfiguration ): Promise { const { provider } = this.getConfigurationProviderRecord(handle); return provider.resolveDebugConfiguration?.( this.toWorkspaceFolder(workspaceFolderUri), debugConfiguration ); } async $resolveDebugConfigurationWithSubstitutedVariablesByHandle( handle: number, workspaceFolderUri: string | undefined, debugConfiguration: theia.DebugConfiguration ): Promise { const { provider } = this.getConfigurationProviderRecord(handle); return provider.resolveDebugConfigurationWithSubstitutedVariables?.( this.toWorkspaceFolder(workspaceFolderUri), debugConfiguration ); } protected async createDebugAdapterTracker(session: theia.DebugSession): Promise { return PluginDebugAdapterTracker.create(session, this.trackerFactories); } protected async createDebugAdapter(session: theia.DebugSession, debugConfiguration: theia.DebugConfiguration): Promise { const executable = await this.resolveDebugAdapterExecutable(debugConfiguration); const descriptorFactory = this.descriptorFactories.get(session.type); return this.getAdapterCreator(debugConfiguration).createDebugAdapter(session, debugConfiguration, executable, descriptorFactory); } protected async resolveDebugAdapterExecutable(debugConfiguration: theia.DebugConfiguration): Promise { const { type } = debugConfiguration; const contribution = this.debuggersContributions.get(type); if (contribution) { if (contribution.adapterExecutableCommand) { const executable = await this.commandRegistryExt.executeCommand(contribution.adapterExecutableCommand); if (executable) { return executable; } } else { const contributionPath = this.contributionPaths.get(type); if (contributionPath) { return this.getAdapterCreator(debugConfiguration).resolveDebugAdapterExecutable(contributionPath, contribution); } } } throw new Error(`It is not possible to provide debug adapter executable for '${debugConfiguration.type}'.`); } private toWorkspaceFolder(folder: string | undefined): theia.WorkspaceFolder | undefined { if (!folder) { return undefined; } const uri = URI.parse(folder); const path = new Path(uri.path); return { uri: uri, name: path.base, index: 0 }; } private getAdapterCreator(debugConfiguration: theia.DebugConfiguration): PluginDebugAdapterCreator { const pluginType = this.contributionTypes.get(debugConfiguration.type); return pluginType === 'frontend' ? this.frontendAdapterCreator : this.backendAdapterCreator; } }