Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLISessionTracker.ts
13405 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 { execFile } from 'child_process';6import { l10n, Terminal, window } from 'vscode';7import { Disposable, IDisposable } from '../../../../util/vs/base/common/lifecycle';8import { isWindows } from '../../../../util/vs/base/common/platform';9import { createDecorator } from '../../../../util/vs/platform/instantiation/common/instantiation';1011export const ICopilotCLISessionTracker = createDecorator<ICopilotCLISessionTracker>('ICopilotCLISessionTracker');1213export interface SessionProcessInfo {14readonly pid: number;15readonly ppid: number;16}1718export interface ICopilotCLISessionTracker extends Disposable {19readonly _serviceBrand: undefined;20/**21* Record the PID and PPID for a newly connected session.22* Returns a disposable that removes the session when disposed.23*/24registerSession(sessionId: string, info: SessionProcessInfo): IDisposable;2526/**27* Set the display name for a session (called by the CLI).28*/29setSessionName(sessionId: string, name: string): void;3031/**32* Get a display name for a session, falling back to the sessionId.33*/34getSessionDisplayName(sessionId: string): string;3536/**37* Get the IDs of all connected sessions.38*/39getSessionIds(): readonly string[];4041/**42* Directly associate a terminal with a session.43* The mapping is automatically removed when the terminal is closed.44*/45setSessionTerminal(sessionId: string, terminal: Terminal): void;4647/**48* Get the terminal associated with a session.49* Returns `undefined` if no matching terminal is found.50*/51getTerminal(sessionId: string): Promise<Terminal | undefined>;52}5354export class CopilotCLISessionTracker extends Disposable implements ICopilotCLISessionTracker {55declare _serviceBrand: undefined;56private readonly _sessions = new Map<string, SessionProcessInfo>();57private readonly _sessionNames = new Map<string, string>();58private readonly _sessionTerminals = new Map<string, Terminal>();59private readonly _grandparentPids = new Map<string, number[]>();6061constructor() {62super();63this._register(window.onDidCloseTerminal(closedTerminal => {64for (const [id, t] of this._sessionTerminals) {65if (t === closedTerminal) {66this._sessionTerminals.delete(id);67}68}69}));70}71registerSession(sessionId: string, info: SessionProcessInfo): IDisposable {72this._sessions.set(sessionId, info);73return {74dispose: () => {75this._sessions.delete(sessionId);76this._sessionNames.delete(sessionId);77this._sessionTerminals.delete(sessionId);78this._grandparentPids.delete(sessionId);79}80};81}8283setSessionName(sessionId: string, name: string): void {84this._sessionNames.set(sessionId, name);85}8687getSessionDisplayName(sessionId: string): string {88return this._sessionNames.get(sessionId) || l10n.t('Copilot CLI Session');89}9091getSessionIds(): readonly string[] {92return Array.from(this._sessions.keys());93}9495setSessionTerminal(sessionId: string, terminal: Terminal): void {96this._sessionTerminals.set(sessionId, terminal);97}9899async getTerminal(sessionId: string): Promise<Terminal | undefined> {100// Check direct terminal mapping first101const directTerminal = this._sessionTerminals.get(sessionId);102if (directTerminal) {103return directTerminal;104}105106const info = this._sessions.get(sessionId);107if (!info) {108return undefined;109}110111// Try matching by PPID first112const matchByPpid = await this._findTerminalByPid(info.ppid);113if (matchByPpid) {114return matchByPpid;115}116117// Fallback: try the grandparent PID (PPID of the PPID), using cache118// Try fetching up to 4 generations of parent PIDs to account for different shell configurations (e.g. login shells, shell wrappers)119const ppids = this._grandparentPids.get(sessionId) ?? [];120this._grandparentPids.set(sessionId, ppids);121let previousPpid = info.ppid;122for (let index = 0; index < 4; index++) {123if (ppids.length <= index) {124const pid = await getParentPid(previousPpid);125if (pid) {126ppids.push(pid);127} else {128break;129}130}131const pid = ppids[index];132previousPpid = pid;133const terminal = pid ? await this._findTerminalByPid(pid) : undefined;134if (terminal) {135this._sessionTerminals.set(sessionId, terminal);136return terminal;137}138}139140return undefined;141}142143private async _findTerminalByPid(targetPid: number): Promise<Terminal | undefined> {144const terminalPids = window.terminals.map(t => t.processId.then(pid => ({ terminal: t, pid })));145146for (const promise of terminalPids) {147try {148const { terminal, pid } = await promise;149if (pid && targetPid === pid) {150return terminal;151}152} catch {153//154}155}156157return undefined;158}159}160161/**162* Look up the parent PID of a given process.163* Uses PowerShell on Windows and `ps` on Linux/macOS.164* Returns `undefined` if the lookup fails for any reason.165*/166export async function getParentPid(pid: number): Promise<number | undefined> {167try {168const stdout = await new Promise<string>((resolve, reject) => {169const args = isWindows170? ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ParentProcessId`]171: ['-o', 'ppid=', '-p', String(pid)];172const cmd = isWindows ? 'powershell.exe' : 'ps';173execFile(cmd, args, { windowsHide: true }, (err, out) => {174if (err) {175reject(err);176} else {177resolve(out);178}179});180});181182const parsed = parseInt(stdout.trim(), 10);183return isNaN(parsed) ? undefined : parsed;184} catch {185return undefined;186}187}188189190