Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as l10n from '@vscode/l10n';
7
import { promises as fs } from 'fs';
8
import { connect } from 'net';
9
import * as vscode from 'vscode';
10
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
11
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
12
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
13
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
14
import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';
15
import { IGitService } from '../../../platform/git/common/gitService';
16
import { IOctoKitService } from '../../../platform/github/common/githubService';
17
import { ILogService } from '../../../platform/log/common/logService';
18
import { ITasksService } from '../../../platform/tasks/common/tasksService';
19
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
20
import { ITerminalService } from '../../../platform/terminal/common/terminalService';
21
import { assertNever } from '../../../util/vs/base/common/assert';
22
import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
23
import { Disposable } from '../../../util/vs/base/common/lifecycle';
24
import * as path from '../../../util/vs/base/common/path';
25
import { URI } from '../../../util/vs/base/common/uri';
26
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
27
import { ChatSessionsUriHandler, CustomUriHandler } from '../../chatSessions/vscode/chatSessionsUriHandler';
28
import { EXTENSION_ID } from '../../common/constants';
29
import { ILaunchConfigService, needsWorkspaceFolderForTaskError } from '../common/launchConfigService';
30
import { CopilotDebugCommandSessionFactory } from '../node/copilotDebugCommandSessionFactory';
31
import { SimpleRPC } from '../node/copilotDebugWorker/rpc';
32
import { IStartOptions, StartResultKind } from '../node/copilotDebugWorker/shared';
33
import { CopilotDebugCommandHandle } from './copilotDebugCommandHandle';
34
import { handleDebugSession } from './copilotDebugCommandSession';
35
36
//@ts-ignore
37
import powershellScript from '../node/copilotDebugWorker/copilotDebugWorker.ps1';
38
39
// When enabled, holds the storage location of binaries for the PATH:
40
const WAS_REGISTERED_STORAGE_KEY = 'copilot-chat.terminalToDebugging.registered';
41
export const COPILOT_DEBUG_COMMAND = `copilot-debug`;
42
const DEBUG_COMMAND_JS = 'copilotDebugCommand.js';
43
44
export class CopilotDebugCommandContribution extends Disposable implements vscode.UriHandler {
45
private chatSessionsUriHandler: CustomUriHandler;
46
private registerSerializer: Promise<void>;
47
48
constructor(
49
@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,
50
@ILogService private readonly logService: ILogService,
51
@IInstantiationService private readonly instantiationService: IInstantiationService,
52
@IConfigurationService private readonly configurationService: IConfigurationService,
53
@ILaunchConfigService private readonly launchConfigService: ILaunchConfigService,
54
@IAuthenticationService private readonly authService: IAuthenticationService,
55
@ITelemetryService private readonly telemetryService: ITelemetryService,
56
@ITasksService private readonly tasksService: ITasksService,
57
@ITerminalService private readonly terminalService: ITerminalService,
58
@IOctoKitService private readonly _octoKitService: IOctoKitService,
59
@IGitService private readonly _gitService: IGitService,
60
@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,
61
@IFileSystemService private readonly fileSystemService: IFileSystemService,
62
) {
63
super();
64
65
this._register(vscode.window.registerUriHandler(this));
66
this._register(this.configurationService.onDidChangeConfiguration(e => {
67
if (e.affectsConfiguration(ConfigKey.TerminalToDebuggerEnabled.fullyQualifiedId)) {
68
this.registerSerializer = this.registerSerializer.then(() => this.registerEnvironment());
69
}
70
}));
71
this._register(vscode.commands.registerCommand('github.copilot.chat.startCopilotDebugCommand', async () => {
72
const term = vscode.window.createTerminal();
73
term.show(false);
74
term.sendText('copilot-debug <your command here>', false);
75
}));
76
77
this.registerSerializer = this.registerEnvironment();
78
// Initialize ChatSessionsUriHandler with extension context for storage
79
this.chatSessionsUriHandler = new ChatSessionsUriHandler(this._octoKitService, this._gitService, this._gitExtensionService, this.context, this.logService, this.fileSystemService, this.telemetryService);
80
// Check for pending chat sessions when this contribution is initialized
81
(this.chatSessionsUriHandler as ChatSessionsUriHandler).openPendingSession().catch((err) => {
82
this.logService.error('Failed to check for pending chat sessions from debug command contribution:', err);
83
});
84
const globPattern = new vscode.RelativePattern(this.context.globalStorageUri, '.pendingSession');
85
const fileWatcher = vscode.workspace.createFileSystemWatcher(globPattern);
86
this._register(fileWatcher);
87
const pendingFileHandling = async () => {
88
this.logService.info('Detected creation of pending session file from debug command contribution.');
89
// A new pending session file was created, try to open it
90
(this.chatSessionsUriHandler as ChatSessionsUriHandler).openPendingSession().catch((err) => {
91
this.logService.error('Failed to open pending chat session after pending session file creation:', err);
92
});
93
};
94
this._register(fileWatcher.onDidCreate(async () => {
95
await pendingFileHandling();
96
}));
97
this._register(fileWatcher.onDidChange(async () => {
98
await pendingFileHandling();
99
}));
100
}
101
102
private async ensureTask(workspaceFolder: URI | undefined, def: vscode.TaskDefinition, handle: CopilotDebugCommandHandle): Promise<boolean> {
103
if (!workspaceFolder) {
104
handle.printLabel('red', needsWorkspaceFolderForTaskError());
105
return false;
106
}
107
108
if (this.tasksService.hasTask(workspaceFolder, def)) {
109
return true;
110
}
111
112
handle.printJson(def);
113
const 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);
114
if (!run) {
115
return false;
116
}
117
118
// Configure the task to only show on errors to avoid taking focus away
119
// from the terminal in this use case.
120
def.presentation ??= {};
121
def.presentation.reveal = 'silent';
122
await this.tasksService.ensureTask(workspaceFolder, def);
123
124
return true;
125
}
126
127
handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {
128
if (this.chatSessionsUriHandler.canHandleUri(uri)) {
129
return this.chatSessionsUriHandler.handleUri(uri);
130
}
131
const pipePath = process.platform === 'win32' ? '\\\\.\\pipe\\' + uri.path.slice(1) : uri.path;
132
const cts = new CancellationTokenSource();
133
134
const queryParams = new URLSearchParams(uri.query);
135
const referrer = queryParams.get('referrer');
136
/* __GDPR__
137
"uriHandler" : {
138
"owner": "lramos15",
139
"comment": "Reports when the uri handler is called in the copilot extension",
140
"referrer": { "classification": "SystemMetaData", "purpose": "BusinessInsight", "comment": "The referrer query param for the uri" }
141
}
142
*/
143
this.telemetryService.sendMSFTTelemetryEvent('uriHandler', {
144
referrer: referrer || 'unknown',
145
});
146
147
const socket = connect(pipePath, () => {
148
this.logService.info(`Got a debug connection on ${pipePath}`);
149
150
const rpc = new SimpleRPC(socket);
151
const handle = new CopilotDebugCommandHandle(rpc);
152
const { launchConfigService, authService } = this;
153
const exit = (code: number, error?: string) => handle.exit(code, error);
154
const factory = this.instantiationService.createInstance(CopilotDebugCommandSessionFactory, {
155
ensureTask: (wf, def) => this.ensureTask(wf || vscode.workspace.workspaceFolders?.[0].uri, def, handle),
156
isGenerating: () => handle.printLabel('blue', l10n.t('Generating debug configuration...')),
157
prompt: async (text, defaultValue) =>
158
handle.question(text, defaultValue).then(r => r || defaultValue),
159
});
160
161
rpc.registerMethod('start', async function start(opts: IStartOptions): Promise<void> {
162
if (!authService.copilotToken) {
163
await authService.getGitHubSession('any', { createIfNone: { detail: l10n.t('Sign in to GitHub to use Copilot debug.') } });
164
}
165
const result = await factory.start(opts, cts.token);
166
167
switch (result.kind) {
168
case StartResultKind.NoConfig:
169
await handle.printLabel('red', l10n.t`Could not create a launch configuration: ${result.text}`);
170
await exit(1);
171
break;
172
case StartResultKind.Ok:
173
if (opts.printOnly) {
174
await handle.output('stdout', JSON.stringify(result.config, undefined, 2).replaceAll('\n', '\r\n'));
175
await exit(0);
176
} else if (opts.save) {
177
handle.confirm(l10n.t('Configuration saved, debug now?'), true).then(debug => {
178
if (debug) {
179
vscode.debug.startDebugging(result.folder && vscode.workspace.getWorkspaceFolder(result.folder), result.config);
180
}
181
exit(0);
182
});
183
} else {
184
handleDebugSession(
185
launchConfigService,
186
result.folder && vscode.workspace.getWorkspaceFolder(result.folder),
187
{
188
...result.config,
189
internalConsoleOptions: 'neverOpen',
190
},
191
handle,
192
opts.once,
193
newOpts => start({ ...opts, ...newOpts }),
194
);
195
}
196
break;
197
case StartResultKind.Cancelled:
198
exit(1);
199
break;
200
case StartResultKind.NeedExtension:
201
handle.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 => {
202
if (search) {
203
vscode.commands.executeCommand('workbench.extensions.search', `@category:debuggers ${result.debugType}`);
204
}
205
exit(0);
206
});
207
break;
208
default:
209
assertNever(result);
210
}
211
});
212
});
213
214
socket.on('error', e => {
215
this.logService.error(`Error connecting to debug client on ${pipePath}: ${e}`);
216
cts.dispose(true);
217
});
218
219
socket.on('end', () => {
220
cts.dispose(true);
221
});
222
}
223
224
private getVersionNonce() {
225
if (this.context.extensionMode !== vscode.ExtensionMode.Production) {
226
return String(Date.now());
227
}
228
229
const extensionInfo = vscode.extensions.getExtension(EXTENSION_ID);
230
return (extensionInfo?.packageJSON.version ?? String(Date.now())) + '/' + vscode.env.remoteName;
231
}
232
233
private async registerEnvironment() {
234
const enabled = this.configurationService.getConfig(ConfigKey.TerminalToDebuggerEnabled);
235
const globalStorageUri = this.context.globalStorageUri;
236
if (!globalStorageUri) {
237
// globalStorageUri is not available in extension tests: see MockExtensionContext
238
return;
239
}
240
241
const storageLocation = path.join(this.context.globalStorageUri.fsPath, 'debugCommand');
242
const previouslyStoredAt = this.context.globalState.get<{
243
location: string;
244
version: string;
245
}>(WAS_REGISTERED_STORAGE_KEY);
246
247
const versionNonce = this.getVersionNonce();
248
if (!enabled) {
249
if (previouslyStoredAt) {
250
// 1. disabling an enabled state
251
this.terminalService.removePathContribution('copilot-debug');
252
await fs.rm(previouslyStoredAt.location, { recursive: true, force: true });
253
}
254
} else if (!previouslyStoredAt) {
255
// 2. enabling a disabled state
256
this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });
257
await this.fillStoragePath(storageLocation);
258
} else if (previouslyStoredAt.version !== versionNonce) {
259
// 3. upgrading the worker
260
this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });
261
await this.fillStoragePath(storageLocation);
262
} else if (enabled) {
263
// 4. already enabled and up to date, just ensure PATH contribution
264
this.terminalService.contributePath('copilot-debug', storageLocation, { command: COPILOT_DEBUG_COMMAND });
265
}
266
267
this.context.globalState.update(WAS_REGISTERED_STORAGE_KEY, enabled ? {
268
location: storageLocation,
269
version: versionNonce,
270
} : undefined);
271
}
272
273
private async fillStoragePath(storagePath: string) {
274
const callbackUri = vscode.Uri.from({
275
scheme: vscode.env.uriScheme,
276
authority: EXTENSION_ID,
277
});
278
279
let remoteCommand = '';
280
if (vscode.env.remoteName) {
281
remoteCommand = (vscode.env.appName.includes('Insider') ? 'code-insiders' : 'code') + ' --openExternal ';
282
}
283
284
await fs.mkdir(storagePath, { recursive: true });
285
286
if (process.platform === 'win32') {
287
const ps1Path = path.join(storagePath, `${COPILOT_DEBUG_COMMAND}.ps1`);
288
await fs.writeFile(ps1Path, powershellScript
289
.replaceAll('__CALLBACK_URL_PLACEHOLDER__', callbackUri)
290
.replaceAll('__REMOTE_COMMAND_PLACEHOLDER__', remoteCommand));
291
await fs.writeFile(path.join(storagePath, `${COPILOT_DEBUG_COMMAND}.bat`), makeBatScript(ps1Path));
292
} else {
293
const shPath = path.join(storagePath, COPILOT_DEBUG_COMMAND);
294
await fs.writeFile(shPath, makeShellScript(remoteCommand, storagePath, callbackUri));
295
await fs.chmod(shPath, 0o750);
296
}
297
298
await fs.copyFile(path.join(__dirname, DEBUG_COMMAND_JS), path.join(storagePath, DEBUG_COMMAND_JS));
299
}
300
}
301
302
const makeShellScript = (remoteCommand: string, dir: string, callbackUri: vscode.Uri) => `#!/bin/sh
303
unset NODE_OPTIONS
304
ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${path.join(dir, DEBUG_COMMAND_JS)}" "${callbackUri}" "${remoteCommand}" "$@"`;
305
306
const makeBatScript = (ps1Path: string) => `@echo off
307
powershell -ExecutionPolicy Bypass -File "${ps1Path}" %*
308
`;
309
310