Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLISessionTracker.ts
13405 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 { execFile } from 'child_process';
7
import { l10n, Terminal, window } from 'vscode';
8
import { Disposable, IDisposable } from '../../../../util/vs/base/common/lifecycle';
9
import { isWindows } from '../../../../util/vs/base/common/platform';
10
import { createDecorator } from '../../../../util/vs/platform/instantiation/common/instantiation';
11
12
export const ICopilotCLISessionTracker = createDecorator<ICopilotCLISessionTracker>('ICopilotCLISessionTracker');
13
14
export interface SessionProcessInfo {
15
readonly pid: number;
16
readonly ppid: number;
17
}
18
19
export interface ICopilotCLISessionTracker extends Disposable {
20
readonly _serviceBrand: undefined;
21
/**
22
* Record the PID and PPID for a newly connected session.
23
* Returns a disposable that removes the session when disposed.
24
*/
25
registerSession(sessionId: string, info: SessionProcessInfo): IDisposable;
26
27
/**
28
* Set the display name for a session (called by the CLI).
29
*/
30
setSessionName(sessionId: string, name: string): void;
31
32
/**
33
* Get a display name for a session, falling back to the sessionId.
34
*/
35
getSessionDisplayName(sessionId: string): string;
36
37
/**
38
* Get the IDs of all connected sessions.
39
*/
40
getSessionIds(): readonly string[];
41
42
/**
43
* Directly associate a terminal with a session.
44
* The mapping is automatically removed when the terminal is closed.
45
*/
46
setSessionTerminal(sessionId: string, terminal: Terminal): void;
47
48
/**
49
* Get the terminal associated with a session.
50
* Returns `undefined` if no matching terminal is found.
51
*/
52
getTerminal(sessionId: string): Promise<Terminal | undefined>;
53
}
54
55
export class CopilotCLISessionTracker extends Disposable implements ICopilotCLISessionTracker {
56
declare _serviceBrand: undefined;
57
private readonly _sessions = new Map<string, SessionProcessInfo>();
58
private readonly _sessionNames = new Map<string, string>();
59
private readonly _sessionTerminals = new Map<string, Terminal>();
60
private readonly _grandparentPids = new Map<string, number[]>();
61
62
constructor() {
63
super();
64
this._register(window.onDidCloseTerminal(closedTerminal => {
65
for (const [id, t] of this._sessionTerminals) {
66
if (t === closedTerminal) {
67
this._sessionTerminals.delete(id);
68
}
69
}
70
}));
71
}
72
registerSession(sessionId: string, info: SessionProcessInfo): IDisposable {
73
this._sessions.set(sessionId, info);
74
return {
75
dispose: () => {
76
this._sessions.delete(sessionId);
77
this._sessionNames.delete(sessionId);
78
this._sessionTerminals.delete(sessionId);
79
this._grandparentPids.delete(sessionId);
80
}
81
};
82
}
83
84
setSessionName(sessionId: string, name: string): void {
85
this._sessionNames.set(sessionId, name);
86
}
87
88
getSessionDisplayName(sessionId: string): string {
89
return this._sessionNames.get(sessionId) || l10n.t('Copilot CLI Session');
90
}
91
92
getSessionIds(): readonly string[] {
93
return Array.from(this._sessions.keys());
94
}
95
96
setSessionTerminal(sessionId: string, terminal: Terminal): void {
97
this._sessionTerminals.set(sessionId, terminal);
98
}
99
100
async getTerminal(sessionId: string): Promise<Terminal | undefined> {
101
// Check direct terminal mapping first
102
const directTerminal = this._sessionTerminals.get(sessionId);
103
if (directTerminal) {
104
return directTerminal;
105
}
106
107
const info = this._sessions.get(sessionId);
108
if (!info) {
109
return undefined;
110
}
111
112
// Try matching by PPID first
113
const matchByPpid = await this._findTerminalByPid(info.ppid);
114
if (matchByPpid) {
115
return matchByPpid;
116
}
117
118
// Fallback: try the grandparent PID (PPID of the PPID), using cache
119
// Try fetching up to 4 generations of parent PIDs to account for different shell configurations (e.g. login shells, shell wrappers)
120
const ppids = this._grandparentPids.get(sessionId) ?? [];
121
this._grandparentPids.set(sessionId, ppids);
122
let previousPpid = info.ppid;
123
for (let index = 0; index < 4; index++) {
124
if (ppids.length <= index) {
125
const pid = await getParentPid(previousPpid);
126
if (pid) {
127
ppids.push(pid);
128
} else {
129
break;
130
}
131
}
132
const pid = ppids[index];
133
previousPpid = pid;
134
const terminal = pid ? await this._findTerminalByPid(pid) : undefined;
135
if (terminal) {
136
this._sessionTerminals.set(sessionId, terminal);
137
return terminal;
138
}
139
}
140
141
return undefined;
142
}
143
144
private async _findTerminalByPid(targetPid: number): Promise<Terminal | undefined> {
145
const terminalPids = window.terminals.map(t => t.processId.then(pid => ({ terminal: t, pid })));
146
147
for (const promise of terminalPids) {
148
try {
149
const { terminal, pid } = await promise;
150
if (pid && targetPid === pid) {
151
return terminal;
152
}
153
} catch {
154
//
155
}
156
}
157
158
return undefined;
159
}
160
}
161
162
/**
163
* Look up the parent PID of a given process.
164
* Uses PowerShell on Windows and `ps` on Linux/macOS.
165
* Returns `undefined` if the lookup fails for any reason.
166
*/
167
export async function getParentPid(pid: number): Promise<number | undefined> {
168
try {
169
const stdout = await new Promise<string>((resolve, reject) => {
170
const args = isWindows
171
? ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ParentProcessId`]
172
: ['-o', 'ppid=', '-p', String(pid)];
173
const cmd = isWindows ? 'powershell.exe' : 'ps';
174
execFile(cmd, args, { windowsHide: true }, (err, out) => {
175
if (err) {
176
reject(err);
177
} else {
178
resolve(out);
179
}
180
});
181
});
182
183
const parsed = parseInt(stdout.trim(), 10);
184
return isNaN(parsed) ? undefined : parsed;
185
} catch {
186
return undefined;
187
}
188
}
189
190