Path: blob/main/src/vs/workbench/api/node/extHostDebugService.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 * as vscode from 'vscode';6import { createCancelablePromise, disposableTimeout, firstParallel, RunOnceScheduler, timeout } from '../../../base/common/async.js';7import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';8import * as platform from '../../../base/common/platform.js';9import * as nls from '../../../nls.js';10import { IExternalTerminalService } from '../../../platform/externalTerminal/common/externalTerminal.js';11import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from '../../../platform/externalTerminal/node/externalTerminalService.js';12import { ISignService } from '../../../platform/sign/common/sign.js';13import { SignService } from '../../../platform/sign/node/signService.js';14import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js';15import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from '../../contrib/debug/node/debugAdapter.js';16import { hasChildProcesses, prepareCommand } from '../../contrib/debug/node/terminals.js';17import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js';18import { IExtHostCommands } from '../common/extHostCommands.js';19import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration.js';20import { ExtHostDebugServiceBase, ExtHostDebugSession } from '../common/extHostDebugService.js';21import { IExtHostEditorTabs } from '../common/extHostEditorTabs.js';22import { IExtHostExtensionService } from '../common/extHostExtensionService.js';23import { IExtHostRpcService } from '../common/extHostRpcService.js';24import { IExtHostTerminalService } from '../common/extHostTerminalService.js';25import { IExtHostTesting } from '../common/extHostTesting.js';26import { DebugAdapterExecutable, DebugAdapterNamedPipeServer, DebugAdapterServer, ThemeIcon } from '../common/extHostTypes.js';27import { IExtHostVariableResolverProvider } from '../common/extHostVariableResolverService.js';28import { IExtHostWorkspace } from '../common/extHostWorkspace.js';29import { IExtHostTerminalShellIntegration } from '../common/extHostTerminalShellIntegration.js';3031export class ExtHostDebugService extends ExtHostDebugServiceBase {3233private _integratedTerminalInstances = new DebugTerminalCollection();34private _terminalDisposedListener: IDisposable | undefined;3536constructor(37@IExtHostRpcService extHostRpcService: IExtHostRpcService,38@IExtHostWorkspace workspaceService: IExtHostWorkspace,39@IExtHostExtensionService extensionService: IExtHostExtensionService,40@IExtHostConfiguration configurationService: IExtHostConfiguration,41@IExtHostTerminalService private _terminalService: IExtHostTerminalService,42@IExtHostTerminalShellIntegration private _terminalShellIntegrationService: IExtHostTerminalShellIntegration,43@IExtHostEditorTabs editorTabs: IExtHostEditorTabs,44@IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider,45@IExtHostCommands commands: IExtHostCommands,46@IExtHostTesting testing: IExtHostTesting,47) {48super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing);49}5051protected override createDebugAdapter(adapter: vscode.DebugAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined {52if (adapter instanceof DebugAdapterExecutable) {53return new ExecutableDebugAdapter(this.convertExecutableToDto(adapter), session.type);54} else if (adapter instanceof DebugAdapterServer) {55return new SocketDebugAdapter(this.convertServerToDto(adapter));56} else if (adapter instanceof DebugAdapterNamedPipeServer) {57return new NamedPipeDebugAdapter(this.convertPipeServerToDto(adapter));58} else {59return super.createDebugAdapter(adapter, session);60}61}6263protected override daExecutableFromPackage(session: ExtHostDebugSession, extensionRegistry: ExtensionDescriptionRegistry): DebugAdapterExecutable | undefined {64const dae = ExecutableDebugAdapter.platformAdapterExecutable(extensionRegistry.getAllExtensionDescriptions(), session.type);65if (dae) {66return new DebugAdapterExecutable(dae.command, dae.args, dae.options);67}68return undefined;69}7071protected override createSignService(): ISignService | undefined {72return new SignService();73}7475public override async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise<number | undefined> {7677if (args.kind === 'integrated') {7879if (!this._terminalDisposedListener) {80// React on terminal disposed and check if that is the debug terminal #1295681this._terminalDisposedListener = this._register(this._terminalService.onDidCloseTerminal(terminal => {82this._integratedTerminalInstances.onTerminalClosed(terminal);83}));84}8586const configProvider = await this._configurationService.getConfigProvider();87const shell = this._terminalService.getDefaultShell(true);88const shellArgs = this._terminalService.getDefaultShellArgs(true);8990const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process");9192const shellConfig = JSON.stringify({ shell, shellArgs });93let terminal = await this._integratedTerminalInstances.checkout(shellConfig, terminalName);9495let cwdForPrepareCommand: string | undefined;96let giveShellTimeToInitialize = false;9798if (!terminal) {99const options: vscode.TerminalOptions = {100shellPath: shell,101shellArgs: shellArgs,102cwd: args.cwd,103name: terminalName,104iconPath: new ThemeIcon('debug'),105};106giveShellTimeToInitialize = true;107terminal = this._terminalService.createTerminalFromOptions(options, {108isFeatureTerminal: true,109// Since debug termnials are REPLs, we want shell integration to be enabled.110// Ignore isFeatureTerminal when evaluating shell integration enablement.111forceShellIntegration: true,112useShellEnvironment: true113});114this._integratedTerminalInstances.insert(terminal, shellConfig);115116} else {117cwdForPrepareCommand = args.cwd;118}119120terminal.show(true);121122const shellProcessId = await terminal.processId;123124if (giveShellTimeToInitialize) {125// give a new terminal some time to initialize the shell (most recently, #228191)126// - If shell integration is available, use that as a deterministic signal127// - Debounce content being written to known when the prompt is available128// - Give a longer timeout otherwise129const enum Timing {130DataDebounce = 500,131MaxDelay = 5000,132}133134const ds = new DisposableStore();135await new Promise<void>(resolve => {136const scheduler = ds.add(new RunOnceScheduler(resolve, Timing.DataDebounce));137ds.add(this._terminalService.onDidWriteTerminalData(e => {138if (e.terminal === terminal) {139scheduler.schedule();140}141}));142ds.add(this._terminalShellIntegrationService.onDidChangeTerminalShellIntegration(e => {143if (e.terminal === terminal) {144resolve();145}146}));147ds.add(disposableTimeout(resolve, Timing.MaxDelay));148});149150ds.dispose();151} else {152if (terminal.state.isInteractedWith && !terminal.shellIntegration) {153terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969154await timeout(200); // mirroring https://github.com/microsoft/vscode/blob/c67ccc70ece5f472ec25464d3eeb874cfccee9f1/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts#L852-L857155}156157if (configProvider.getConfiguration('debug.terminal').get<boolean>('clearBeforeReusing')) {158// clear terminal before reusing it159let clearCommand: string;160if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0 || shell.indexOf('cmd.exe') >= 0) {161clearCommand = 'cls';162} else if (shell.indexOf('bash') >= 0) {163clearCommand = 'clear';164} else if (platform.isWindows) {165clearCommand = 'cls';166} else {167clearCommand = 'clear';168}169170if (terminal.shellIntegration) {171const ds = new DisposableStore();172const execution = terminal.shellIntegration.executeCommand(clearCommand);173await new Promise<void>(resolve => {174ds.add(this._terminalShellIntegrationService.onDidEndTerminalShellExecution(e => {175if (e.execution === execution) {176resolve();177}178}));179ds.add(disposableTimeout(resolve, 500)); // 500ms timeout to ensure we resolve180});181182ds.dispose();183} else {184terminal.sendText(clearCommand);185await timeout(200); // add a small delay to ensure the command is processed, see #240953186}187}188}189190const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand, args.env);191192if (terminal.shellIntegration) {193terminal.shellIntegration.executeCommand(command);194} else {195terminal.sendText(command);196}197198// Mark terminal as unused when its session ends, see #112055199const sessionListener = this.onDidTerminateDebugSession(s => {200if (s.id === sessionId) {201this._integratedTerminalInstances.free(terminal);202sessionListener.dispose();203}204});205206return shellProcessId;207208} else if (args.kind === 'external') {209return runInExternalTerminal(args, await this._configurationService.getConfigProvider());210}211return super.$runInTerminal(args, sessionId);212}213}214215let externalTerminalService: IExternalTerminalService | undefined = undefined;216217function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, configProvider: ExtHostConfigProvider): Promise<number | undefined> {218if (!externalTerminalService) {219if (platform.isWindows) {220externalTerminalService = new WindowsExternalTerminalService();221} else if (platform.isMacintosh) {222externalTerminalService = new MacExternalTerminalService();223} else if (platform.isLinux) {224externalTerminalService = new LinuxExternalTerminalService();225} else {226throw new Error('external terminals not supported on this platform');227}228}229const config = configProvider.getConfiguration('terminal');230return externalTerminalService.runInTerminal(args.title!, args.cwd, args.args, args.env || {}, config.external || {});231}232233class DebugTerminalCollection {234/**235* Delay before a new terminal is a candidate for reuse. See #71850236*/237private static minUseDelay = 1000;238239private _terminalInstances = new Map<vscode.Terminal, { lastUsedAt: number; config: string }>();240241public async checkout(config: string, name: string, cleanupOthersByName = false) {242const entries = [...this._terminalInstances.entries()];243const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => {244245// Only allow terminals that match the title. See #123189246if (terminal.name !== name) {247return null;248}249250if (termInfo.lastUsedAt !== -1 && await hasChildProcesses(await terminal.processId)) {251return null;252}253254// important: date check and map operations must be synchronous255const now = Date.now();256if (termInfo.lastUsedAt + DebugTerminalCollection.minUseDelay > now || ct.isCancellationRequested) {257return null;258}259260if (termInfo.config !== config) {261if (cleanupOthersByName) {262terminal.dispose();263}264return null;265}266267termInfo.lastUsedAt = now;268return terminal;269}));270271return await firstParallel(promises, (t): t is vscode.Terminal => !!t);272}273274public insert(terminal: vscode.Terminal, termConfig: string) {275this._terminalInstances.set(terminal, { lastUsedAt: Date.now(), config: termConfig });276}277278public free(terminal: vscode.Terminal) {279const info = this._terminalInstances.get(terminal);280if (info) {281info.lastUsedAt = -1;282}283}284285public onTerminalClosed(terminal: vscode.Terminal) {286this._terminalInstances.delete(terminal);287}288}289290291