Path: blob/main/src/vs/workbench/contrib/debug/browser/debugAdapterManager.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 { RunOnceScheduler } from '../../../../base/common/async.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { IJSONSchema, IJSONSchemaMap } from '../../../../base/common/jsonSchema.js';8import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';9import Severity from '../../../../base/common/severity.js';10import * as strings from '../../../../base/common/strings.js';11import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';12import { IEditorModel } from '../../../../editor/common/editorCommon.js';13import { ILanguageService } from '../../../../editor/common/languages/language.js';14import { ITextModel } from '../../../../editor/common/model.js';15import * as nls from '../../../../nls.js';16import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';17import { ICommandService } from '../../../../platform/commands/common/commands.js';18import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';19import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';20import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';23import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';24import { Registry } from '../../../../platform/registry/common/platform.js';25import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';26import { Breakpoints } from '../common/breakpoints.js';27import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IConfigurationManager, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, IGuessedDebugger, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from '../common/debug.js';28import { Debugger } from '../common/debugger.js';29import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from '../common/debugSchemas.js';30import { TaskDefinitionRegistry } from '../../tasks/common/taskDefinitionRegistry.js';31import { ITaskService } from '../../tasks/common/taskService.js';32import { launchSchemaId } from '../../../services/configuration/common/configuration.js';33import { IEditorService } from '../../../services/editor/common/editorService.js';34import { IExtensionService } from '../../../services/extensions/common/extensions.js';35import { ILifecycleService, LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';3637const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);3839export interface IAdapterManagerDelegate {40onDidNewSession: Event<IDebugSession>;41configurationManager(): IConfigurationManager;42}4344export class AdapterManager extends Disposable implements IAdapterManager {4546private debuggers: Debugger[];47private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[];48private debugAdapterFactories = new Map<string, IDebugAdapterFactory>();49private debuggersAvailable!: IContextKey<boolean>;50private debugExtensionsAvailable!: IContextKey<boolean>;51private readonly _onDidRegisterDebugger = new Emitter<void>();52private readonly _onDidDebuggersExtPointRead = new Emitter<void>();53private breakpointContributions: Breakpoints[] = [];54private debuggerWhenKeys = new Set<string>();55private taskLabels: string[] = [];5657/** Extensions that were already active before any debugger activation events */58private earlyActivatedExtensions: Set<string> | undefined;5960private usedDebugTypes = new Set<string>();6162constructor(63private readonly delegate: IAdapterManagerDelegate,64@IEditorService private readonly editorService: IEditorService,65@IConfigurationService private readonly configurationService: IConfigurationService,66@IQuickInputService private readonly quickInputService: IQuickInputService,67@IInstantiationService private readonly instantiationService: IInstantiationService,68@ICommandService private readonly commandService: ICommandService,69@IExtensionService private readonly extensionService: IExtensionService,70@IContextKeyService private readonly contextKeyService: IContextKeyService,71@ILanguageService private readonly languageService: ILanguageService,72@IDialogService private readonly dialogService: IDialogService,73@ILifecycleService private readonly lifecycleService: ILifecycleService,74@ITaskService private readonly tasksService: ITaskService,75@IMenuService private readonly menuService: IMenuService,76) {77super();78this.adapterDescriptorFactories = [];79this.debuggers = [];80this.registerListeners();81this.contextKeyService.bufferChangeEvents(() => {82this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService);83this.debugExtensionsAvailable = CONTEXT_DEBUG_EXTENSION_AVAILABLE.bindTo(contextKeyService);84});85this._register(this.contextKeyService.onDidChangeContext(e => {86if (e.affectsSome(this.debuggerWhenKeys)) {87this.debuggersAvailable.set(this.hasEnabledDebuggers());88this.updateDebugAdapterSchema();89}90}));91this._register(this.onDidDebuggersExtPointRead(() => {92this.debugExtensionsAvailable.set(this.debuggers.length > 0);93}));9495// generous debounce since this will end up calling `resolveTask` internally96const updateTaskScheduler = this._register(new RunOnceScheduler(() => this.updateTaskLabels(), 5000));9798this._register(Event.any(tasksService.onDidChangeTaskConfig, tasksService.onDidChangeTaskProviders)(() => {99updateTaskScheduler.cancel();100updateTaskScheduler.schedule();101}));102this.lifecycleService.when(LifecyclePhase.Eventually)103.then(() => this.debugExtensionsAvailable.set(this.debuggers.length > 0)); // If no extensions with a debugger contribution are loaded104105this._register(delegate.onDidNewSession(s => {106this.usedDebugTypes.add(s.configuration.type);107}));108109updateTaskScheduler.schedule();110}111112private registerListeners(): void {113debuggersExtPoint.setHandler((extensions, delta) => {114delta.added.forEach(added => {115added.value.forEach(rawAdapter => {116if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) {117added.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'."));118}119120if (rawAdapter.type !== '*') {121const existing = this.getDebugger(rawAdapter.type);122if (existing) {123existing.merge(rawAdapter, added.description);124} else {125const dbg = this.instantiationService.createInstance(Debugger, this, rawAdapter, added.description);126dbg.when?.keys().forEach(key => this.debuggerWhenKeys.add(key));127this.debuggers.push(dbg);128}129}130});131});132133// take care of all wildcard contributions134extensions.forEach(extension => {135extension.value.forEach(rawAdapter => {136if (rawAdapter.type === '*') {137this.debuggers.forEach(dbg => dbg.merge(rawAdapter, extension.description));138}139});140});141142delta.removed.forEach(removed => {143const removedTypes = removed.value.map(rawAdapter => rawAdapter.type);144this.debuggers = this.debuggers.filter(d => removedTypes.indexOf(d.type) === -1);145});146147this.updateDebugAdapterSchema();148this._onDidDebuggersExtPointRead.fire();149});150151breakpointsExtPoint.setHandler(extensions => {152this.breakpointContributions = extensions.flatMap(ext => ext.value.map(breakpoint => this.instantiationService.createInstance(Breakpoints, breakpoint)));153});154}155156private updateTaskLabels() {157this.tasksService.getKnownTasks().then(tasks => {158this.taskLabels = tasks.map(task => task._label);159this.updateDebugAdapterSchema();160});161}162163private updateDebugAdapterSchema() {164// update the schema to include all attributes, snippets and types from extensions.165const items = (<IJSONSchema>launchSchema.properties!['configurations'].items);166const taskSchema = TaskDefinitionRegistry.getJsonSchema();167const definitions: IJSONSchemaMap = {168'common': {169properties: {170'name': {171type: 'string',172description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."),173default: 'Launch'174},175'debugServer': {176type: 'number',177description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"),178default: 4711179},180'preLaunchTask': {181anyOf: [taskSchema, {182type: ['string']183}],184default: '',185defaultSnippets: [{ body: { task: '', type: '' } }],186description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts."),187examples: this.taskLabels,188},189'postDebugTask': {190anyOf: [taskSchema, {191type: ['string'],192}],193default: '',194defaultSnippets: [{ body: { task: '', type: '' } }],195description: nls.localize('debugPostDebugTask', "Task to run after debug session ends."),196examples: this.taskLabels,197},198'presentation': presentationSchema,199'internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA,200'suppressMultipleSessionWarning': {201type: 'boolean',202description: nls.localize('suppressMultipleSessionWarning', "Disable the warning when trying to start the same debug configuration more than once."),203default: true204}205}206}207};208launchSchema.definitions = definitions;209items.oneOf = [];210items.defaultSnippets = [];211this.debuggers.forEach(adapter => {212const schemaAttributes = adapter.getSchemaAttributes(definitions);213if (schemaAttributes && items.oneOf) {214items.oneOf.push(...schemaAttributes);215}216const configurationSnippets = adapter.configurationSnippets;217if (configurationSnippets && items.defaultSnippets) {218items.defaultSnippets.push(...configurationSnippets);219}220});221jsonRegistry.registerSchema(launchSchemaId, launchSchema);222}223224registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable {225debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher));226this.debuggersAvailable.set(this.hasEnabledDebuggers());227this._onDidRegisterDebugger.fire();228229return {230dispose: () => {231debugTypes.forEach(debugType => this.debugAdapterFactories.delete(debugType));232}233};234}235236hasEnabledDebuggers(): boolean {237for (const [type] of this.debugAdapterFactories) {238const dbg = this.getDebugger(type);239if (dbg && dbg.enabled) {240return true;241}242}243244return false;245}246247createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined {248const factory = this.debugAdapterFactories.get(session.configuration.type);249if (factory) {250return factory.createDebugAdapter(session);251}252return undefined;253}254255substituteVariables(debugType: string, folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {256const factory = this.debugAdapterFactories.get(debugType);257if (factory) {258return factory.substituteVariables(folder, config);259}260return Promise.resolve(config);261}262263runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise<number | undefined> {264const factory = this.debugAdapterFactories.get(debugType);265if (factory) {266return factory.runInTerminal(args, sessionId);267}268return Promise.resolve(void 0);269}270271registerDebugAdapterDescriptorFactory(debugAdapterProvider: IDebugAdapterDescriptorFactory): IDisposable {272this.adapterDescriptorFactories.push(debugAdapterProvider);273return {274dispose: () => {275this.unregisterDebugAdapterDescriptorFactory(debugAdapterProvider);276}277};278}279280unregisterDebugAdapterDescriptorFactory(debugAdapterProvider: IDebugAdapterDescriptorFactory): void {281const ix = this.adapterDescriptorFactories.indexOf(debugAdapterProvider);282if (ix >= 0) {283this.adapterDescriptorFactories.splice(ix, 1);284}285}286287getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined> {288const config = session.configuration;289const providers = this.adapterDescriptorFactories.filter(p => p.type === config.type && p.createDebugAdapterDescriptor);290if (providers.length === 1) {291return providers[0].createDebugAdapterDescriptor(session);292} else {293// TODO@AW handle n > 1 case294}295return Promise.resolve(undefined);296}297298getDebuggerLabel(type: string): string | undefined {299const dbgr = this.getDebugger(type);300if (dbgr) {301return dbgr.label;302}303304return undefined;305}306307get onDidRegisterDebugger(): Event<void> {308return this._onDidRegisterDebugger.event;309}310311get onDidDebuggersExtPointRead(): Event<void> {312return this._onDidDebuggersExtPointRead.event;313}314315canSetBreakpointsIn(model: ITextModel): boolean {316const languageId = model.getLanguageId();317if (!languageId || languageId === 'jsonc' || languageId === 'log') {318// do not allow breakpoints in our settings files and output319return false;320}321if (this.configurationService.getValue<IDebugConfiguration>('debug').allowBreakpointsEverywhere) {322return true;323}324325return this.breakpointContributions.some(breakpoints => breakpoints.language === languageId && breakpoints.enabled);326}327328getDebugger(type: string): Debugger | undefined {329return this.debuggers.find(dbg => strings.equalsIgnoreCase(dbg.type, type));330}331332getEnabledDebugger(type: string): Debugger | undefined {333const adapter = this.getDebugger(type);334return adapter && adapter.enabled ? adapter : undefined;335}336337someDebuggerInterestedInLanguage(languageId: string): boolean {338return !!this.debuggers339.filter(d => d.enabled)340.find(a => a.interestedInLanguage(languageId));341}342343async guessDebugger(gettingConfigurations: boolean): Promise<IGuessedDebugger | undefined> {344const activeTextEditorControl = this.editorService.activeTextEditorControl;345let candidates: Debugger[] = [];346let languageLabel: string | null = null;347let model: IEditorModel | null = null;348if (isCodeEditor(activeTextEditorControl)) {349model = activeTextEditorControl.getModel();350const language = model ? model.getLanguageId() : undefined;351if (language) {352languageLabel = this.languageService.getLanguageName(language);353}354const adapters = this.debuggers355.filter(a => a.enabled)356.filter(a => language && a.interestedInLanguage(language));357if (adapters.length === 1) {358return { debugger: adapters[0] };359}360if (adapters.length > 1) {361candidates = adapters;362}363}364365// We want to get the debuggers that have configuration providers in the case we are fetching configurations366// Or if a breakpoint can be set in the current file (good hint that an extension can handle it)367if ((!languageLabel || gettingConfigurations || (model && this.canSetBreakpointsIn(model))) && candidates.length === 0) {368await this.activateDebuggers('onDebugInitialConfigurations');369370candidates = this.debuggers371.filter(a => a.enabled)372.filter(dbg => dbg.hasInitialConfiguration() || dbg.hasDynamicConfigurationProviders() || dbg.hasConfigurationProvider());373}374375if (candidates.length === 0 && languageLabel) {376if (languageLabel.indexOf(' ') >= 0) {377languageLabel = `'${languageLabel}'`;378}379const { confirmed } = await this.dialogService.confirm({380type: Severity.Warning,381message: nls.localize('CouldNotFindLanguage', "You don't have an extension for debugging {0}. Should we find a {0} extension in the Marketplace?", languageLabel),382primaryButton: nls.localize({ key: 'findExtension', comment: ['&& denotes a mnemonic'] }, "&&Find {0} extension", languageLabel)383});384if (confirmed) {385await this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);386}387return undefined;388}389390this.initExtensionActivationsIfNeeded();391392candidates.sort((first, second) => first.label.localeCompare(second.label));393candidates = candidates.filter(a => !a.isHiddenFromDropdown);394395const suggestedCandidates: Debugger[] = [];396const otherCandidates: Debugger[] = [];397candidates.forEach(d => {398const descriptor = d.getMainExtensionDescriptor();399if (descriptor.id && !!this.earlyActivatedExtensions?.has(descriptor.id)) {400// Was activated early401suggestedCandidates.push(d);402} else if (this.usedDebugTypes.has(d.type)) {403// Was used already404suggestedCandidates.push(d);405} else {406otherCandidates.push(d);407}408});409410const picks: ({ label: string; pick?: () => IGuessedDebugger | Promise<IGuessedDebugger | undefined>; type?: string } | MenuItemAction)[] = [];411const dynamic = await this.delegate.configurationManager().getDynamicProviders();412if (suggestedCandidates.length > 0) {413picks.push(414{ type: 'separator', label: nls.localize('suggestedDebuggers', "Suggested") },415...suggestedCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));416}417418if (otherCandidates.length > 0) {419if (picks.length > 0) {420picks.push({ type: 'separator', label: '' });421}422423picks.push(...otherCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));424}425426if (dynamic.length) {427if (picks.length) {428picks.push({ type: 'separator', label: '' });429}430431for (const d of dynamic) {432picks.push({433label: nls.localize('moreOptionsForDebugType', "More {0} options...", d.label),434pick: async (): Promise<IGuessedDebugger | undefined> => {435const cfg = await d.pick();436if (!cfg) { return undefined; }437return cfg && { debugger: this.getDebugger(d.type)!, withConfig: cfg };438},439});440}441}442443picks.push(444{ type: 'separator', label: '' },445{ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }446);447448const contributed = this.menuService.getMenuActions(MenuId.DebugCreateConfiguration, this.contextKeyService);449for (const [, action] of contributed) {450for (const item of action) {451picks.push(item);452}453}454455const placeHolder = nls.localize('selectDebug', "Select debugger");456return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder }).then(async picked => {457if (picked && 'pick' in picked && typeof picked.pick === 'function') {458return await picked.pick();459}460461if (picked instanceof MenuItemAction) {462picked.run();463return;464}465466if (picked) {467this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);468}469470return undefined;471});472}473474private initExtensionActivationsIfNeeded(): void {475if (!this.earlyActivatedExtensions) {476this.earlyActivatedExtensions = new Set<string>();477478const status = this.extensionService.getExtensionsStatus();479for (const id in status) {480if (!!status[id].activationTimes) {481this.earlyActivatedExtensions.add(id);482}483}484}485}486487async activateDebuggers(activationEvent: string, debugType?: string): Promise<void> {488this.initExtensionActivationsIfNeeded();489490const promises: Promise<any>[] = [491this.extensionService.activateByEvent(activationEvent),492this.extensionService.activateByEvent('onDebug')493];494if (debugType) {495promises.push(this.extensionService.activateByEvent(`${activationEvent}:${debugType}`));496}497await Promise.all(promises);498}499}500501502