Path: blob/main/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts
13399 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 l10n from '@vscode/l10n';6import { promises as fs } from 'fs';7import { connect } from 'net';8import * as vscode from 'vscode';9import { IAuthenticationService } from '../../../platform/authentication/common/authentication';10import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';11import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';12import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';13import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';14import { IGitService } from '../../../platform/git/common/gitService';15import { IOctoKitService } from '../../../platform/github/common/githubService';16import { ILogService } from '../../../platform/log/common/logService';17import { ITasksService } from '../../../platform/tasks/common/tasksService';18import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';19import { ITerminalService } from '../../../platform/terminal/common/terminalService';20import { assertNever } from '../../../util/vs/base/common/assert';21import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation';22import { Disposable } from '../../../util/vs/base/common/lifecycle';23import * as path from '../../../util/vs/base/common/path';24import { URI } from '../../../util/vs/base/common/uri';25import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';26import { ChatSessionsUriHandler, CustomUriHandler } from '../../chatSessions/vscode/chatSessionsUriHandler';27import { EXTENSION_ID } from '../../common/constants';28import { ILaunchConfigService, needsWorkspaceFolderForTaskError } from '../common/launchConfigService';29import { CopilotDebugCommandSessionFactory } from '../node/copilotDebugCommandSessionFactory';30import { SimpleRPC } from '../node/copilotDebugWorker/rpc';31import { IStartOptions, StartResultKind } from '../node/copilotDebugWorker/shared';32import { CopilotDebugCommandHandle } from './copilotDebugCommandHandle';33import { handleDebugSession } from './copilotDebugCommandSession';3435//@ts-ignore36import powershellScript from '../node/copilotDebugWorker/copilotDebugWorker.ps1';3738// When enabled, holds the storage location of binaries for the PATH:39const WAS_REGISTERED_STORAGE_KEY = 'copilot-chat.terminalToDebugging.registered';40export const COPILOT_DEBUG_COMMAND = `copilot-debug`;41const DEBUG_COMMAND_JS = 'copilotDebugCommand.js';4243export class CopilotDebugCommandContribution extends Disposable implements vscode.UriHandler {44private chatSessionsUriHandler: CustomUriHandler;45private registerSerializer: Promise<void>;4647constructor(48@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,49@ILogService private readonly logService: ILogService,50@IInstantiationService private readonly instantiationService: IInstantiationService,51@IConfigurationService private readonly configurationService: IConfigurationService,52@ILaunchConfigService private readonly launchConfigService: ILaunchConfigService,53@IAuthenticationService private readonly authService: IAuthenticationService,54@ITelemetryService private readonly telemetryService: ITelemetryService,55@ITasksService private readonly tasksService: ITasksService,56@ITerminalService private readonly terminalService: ITerminalService,57@IOctoKitService private readonly _octoKitService: IOctoKitService,58@IGitService private readonly _gitService: IGitService,59@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,60@IFileSystemService private readonly fileSystemService: IFileSystemService,61) {62super();6364this._register(vscode.window.registerUriHandler(this));65this._register(this.configurationService.onDidChangeConfiguration(e => {66if (e.affectsConfiguration(ConfigKey.TerminalToDebuggerEnabled.fullyQualifiedId)) {67this.registerSerializer = this.registerSerializer.then(() => this.registerEnvironment());68}69}));70this._register(vscode.commands.registerCommand('github.copilot.chat.startCopilotDebugCommand', async () => {71const term = vscode.window.createTerminal();72term.show(false);73term.sendText('copilot-debug <your command here>', false);74}));7576this.registerSerializer = this.registerEnvironment();77// Initialize ChatSessionsUriHandler with extension context for storage78this.chatSessionsUriHandler = new ChatSessionsUriHandler(this._octoKitService, this._gitService, this._gitExtensionService, this.context, this.logService, this.fileSystemService, this.telemetryService);79// Check for pending chat sessions when this contribution is initialized80(this.chatSessionsUriHandler as ChatSessionsUriHandler).openPendingSession().catch((err) => {81this.logService.error('Failed to check for pending chat sessions from debug command contribution:', err);82});83const globPattern = new vscode.RelativePattern(this.context.globalStorageUri, '.pendingSession');84const fileWatcher = vscode.workspace.createFileSystemWatcher(globPattern);85this._register(fileWatcher);86const pendingFileHandling = async () => {87this.logService.info('Detected creation of pending session file from debug command contribution.');88// A new pending session file was created, try to open it89(this.chatSessionsUriHandler as ChatSessionsUriHandler).openPendingSession().catch((err) => {90this.logService.error('Failed to open pending chat session after pending session file creation:', err);91});92};93this._register(fileWatcher.onDidCreate(async () => {94await pendingFileHandling();95}));96this._register(fileWatcher.onDidChange(async () => {97await pendingFileHandling();98}));99}100101private async ensureTask(workspaceFolder: URI | undefined, def: vscode.TaskDefinition, handle: CopilotDebugCommandHandle): Promise<boolean> {102if (!workspaceFolder) {103handle.printLabel('red', needsWorkspaceFolderForTaskError());104return false;105}106107if (this.tasksService.hasTask(workspaceFolder, def)) {108return true;109}110111handle.printJson(def);112const run = await handle.confirm(l10n.t`The model indicates the above task should be run before debugging. Do you want to save+run it?`, true);113if (!run) {114return false;115}116117// Configure the task to only show on errors to avoid taking focus away118// from the terminal in this use case.119def.presentation ??= {};120def.presentation.reveal = 'silent';121await this.tasksService.ensureTask(workspaceFolder, def);122123return true;124}125126handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {127if (this.chatSessionsUriHandler.canHandleUri(uri)) {128return this.chatSessionsUriHandler.handleUri(uri);129}130const pipePath = process.platform === 'win32' ? '\\\\.\\pipe\\' + uri.path.slice(1) : uri.path;131const cts = new CancellationTokenSource();132133const queryParams = new URLSearchParams(uri.query);134const referrer = queryParams.get('referrer');135/* __GDPR__136"uriHandler" : {137"owner": "lramos15",138"comment": "Reports when the uri handler is called in the copilot extension",139"referrer": { "classification": "SystemMetaData", "purpose": "BusinessInsight", "comment": "The referrer query param for the uri" }140}141*/142this.telemetryService.sendMSFTTelemetryEvent('uriHandler', {143referrer: referrer || 'unknown',144});145146const socket = connect(pipePath, () => {147this.logService.info(`Got a debug connection on ${pipePath}`);148149const rpc = new SimpleRPC(socket);150const handle = new CopilotDebugCommandHandle(rpc);151const { launchConfigService, authService } = this;152const exit = (code: number, error?: string) => handle.exit(code, error);153const factory = this.instantiationService.createInstance(CopilotDebugCommandSessionFactory, {154ensureTask: (wf, def) => this.ensureTask(wf || vscode.workspace.workspaceFolders?.[0].uri, def, handle),155isGenerating: () => handle.printLabel('blue', l10n.t('Generating debug configuration...')),156prompt: async (text, defaultValue) =>157handle.question(text, defaultValue).then(r => r || defaultValue),158});159160rpc.registerMethod('start', async function start(opts: IStartOptions): Promise<void> {161if (!authService.copilotToken) {162await authService.getGitHubSession('any', { createIfNone: { detail: l10n.t('Sign in to GitHub to use Copilot debug.') } });163}164const result = await factory.start(opts, cts.token);165166switch (result.kind) {167case StartResultKind.NoConfig:168await handle.printLabel('red', l10n.t`Could not create a launch configuration: ${result.text}`);169await exit(1);170break;171case StartResultKind.Ok:172if (opts.printOnly) {173await handle.output('stdout', JSON.stringify(result.config, undefined, 2).replaceAll('\n', '\r\n'));174await exit(0);175} else if (opts.save) {176handle.confirm(l10n.t('Configuration saved, debug now?'), true).then(debug => {177if (debug) {178vscode.debug.startDebugging(result.folder && vscode.workspace.getWorkspaceFolder(result.folder), result.config);179}180exit(0);181});182} else {183handleDebugSession(184launchConfigService,185result.folder && vscode.workspace.getWorkspaceFolder(result.folder),186{187...result.config,188internalConsoleOptions: 'neverOpen',189},190handle,191opts.once,192newOpts => start({ ...opts, ...newOpts }),193);194}195break;196case StartResultKind.Cancelled:197exit(1);198break;199case StartResultKind.NeedExtension:200handle.confirm(l10n.t`We generated a "${result.debugType}" debug configuration, but you don't have an extension installed for that. Do you want to look for one?`, true).then(search => {201if (search) {202vscode.commands.executeCommand('workbench.extensions.search', `@category:debuggers ${result.debugType}`);203}204exit(0);205});206break;207default:208assertNever(result);209}210});211});212213socket.on('error', e => {214this.logService.error(`Error connecting to debug client on ${pipePath}: ${e}`);215cts.dispose(true);216});217218socket.on('end', () => {219cts.dispose(true);220});221}222223private getVersionNonce() {224if (this.context.extensionMode !== vscode.ExtensionMode.Production) {225return String(Date.now());226}227228const extensionInfo = vscode.extensions.getExtension(EXTENSION_ID);229return (extensionInfo?.packageJSON.version ?? String(Date.now())) + '/' + vscode.env.remoteName;230}231232private async registerEnvironment() {233const enabled = this.configurationService.getConfig(ConfigKey.TerminalToDebuggerEnabled);234const globalStorageUri = this.context.globalStorageUri;235if (!globalStorageUri) {236// globalStorageUri is not available in extension tests: see MockExtensionContext237return;238}239240const storageLocation = path.join(this.context.globalStorageUri.fsPath, 'debugCommand');241const previouslyStoredAt = this.context.globalState.get<{242location: string;243version: string;244}>(WAS_REGISTERED_STORAGE_KEY);245246const versionNonce = this.getVersionNonce();247if (!enabled) {248if (previouslyStoredAt) {249// 1. disabling an enabled state250this.terminalService.removePathContribution('copilot-debug');251await fs.rm(previouslyStoredAt.location, { recursive: true, force: true });252}253} else if (!previouslyStoredAt) {254// 2. enabling a disabled state255this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });256await this.fillStoragePath(storageLocation);257} else if (previouslyStoredAt.version !== versionNonce) {258// 3. upgrading the worker259this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });260await this.fillStoragePath(storageLocation);261} else if (enabled) {262// 4. already enabled and up to date, just ensure PATH contribution263this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });264}265266this.context.globalState.update(WAS_REGISTERED_STORAGE_KEY, enabled ? {267location: storageLocation,268version: versionNonce,269} : undefined);270}271272private async fillStoragePath(storagePath: string) {273const callbackUri = vscode.Uri.from({274scheme: vscode.env.uriScheme,275authority: EXTENSION_ID,276});277278let remoteCommand = '';279if (vscode.env.remoteName) {280remoteCommand = (vscode.env.appName.includes('Insider') ? 'code-insiders' : 'code') + ' --openExternal ';281}282283await fs.mkdir(storagePath, { recursive: true });284285if (process.platform === 'win32') {286const ps1Path = path.join(storagePath, `${COPILOT_DEBUG_COMMAND}.ps1`);287await fs.writeFile(ps1Path, powershellScript288.replaceAll('__CALLBACK_URL_PLACEHOLDER__', callbackUri)289.replaceAll('__REMOTE_COMMAND_PLACEHOLDER__', remoteCommand));290await fs.writeFile(path.join(storagePath, `${COPILOT_DEBUG_COMMAND}.bat`), makeBatScript(ps1Path));291} else {292const shPath = path.join(storagePath, COPILOT_DEBUG_COMMAND);293await fs.writeFile(shPath, makeShellScript(remoteCommand, storagePath, callbackUri));294await fs.chmod(shPath, 0o750);295}296297await fs.copyFile(path.join(__dirname, DEBUG_COMMAND_JS), path.join(storagePath, DEBUG_COMMAND_JS));298}299}300301const makeShellScript = (remoteCommand: string, dir: string, callbackUri: vscode.Uri) => `#!/bin/sh302unset NODE_OPTIONS303ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(dir, DEBUG_COMMAND_JS)}" "${callbackUri}" "${remoteCommand}" "$@"`;304305const makeBatScript = (ps1Path: string) => `@echo off306powershell -ExecutionPolicy Bypass -File "${ps1Path}" %*307`;308309310