summaryrefslogtreecommitdiff
path: root/packages/core/src/ide/process-utils.ts
blob: a201f45ee4274c76a78bf8e3441903926f7ea752 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { exec } from 'child_process';
import { promisify } from 'util';
import os from 'os';
import path from 'path';

const execAsync = promisify(exec);

const MAX_TRAVERSAL_DEPTH = 32;

/**
 * Fetches the parent process ID and name for a given process ID.
 *
 * @param pid The process ID to inspect.
 * @returns A promise that resolves to the parent's PID and name.
 */
async function getParentProcessInfo(pid: number): Promise<{
  parentPid: number;
  name: string;
}> {
  const platform = os.platform();
  if (platform === 'win32') {
    const command = `wmic process where "ProcessId=${pid}" get Name,ParentProcessId /value`;
    const { stdout } = await execAsync(command);
    const nameMatch = stdout.match(/Name=([^\n]*)/);
    const processName = nameMatch ? nameMatch[1].trim() : '';
    const ppidMatch = stdout.match(/ParentProcessId=(\d+)/);
    const parentPid = ppidMatch ? parseInt(ppidMatch[1], 10) : 0;
    return { parentPid, name: processName };
  } else {
    const command = `ps -o ppid=,command= -p ${pid}`;
    const { stdout } = await execAsync(command);
    const trimmedStdout = stdout.trim();
    const ppidString = trimmedStdout.split(/\s+/)[0];
    const parentPid = parseInt(ppidString, 10);
    const fullCommand = trimmedStdout.substring(ppidString.length).trim();
    const processName = path.basename(fullCommand.split(' ')[0]);
    return { parentPid: isNaN(parentPid) ? 1 : parentPid, name: processName };
  }
}

/**
 * Traverses the process tree on Unix-like systems to find the IDE process ID.
 *
 * The strategy is to find the shell process that spawned the CLI, and then
 * find that shell's parent process (the IDE). To get the true IDE process,
 * we traverse one level higher to get the grandparent.
 *
 * @returns A promise that resolves to the numeric PID.
 */
async function getIdeProcessIdForUnix(): Promise<number> {
  const shells = ['zsh', 'bash', 'sh', 'tcsh', 'csh', 'ksh', 'fish', 'dash'];
  let currentPid = process.pid;

  for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
    try {
      const { parentPid, name } = await getParentProcessInfo(currentPid);

      const isShell = shells.some((shell) => name === shell);
      if (isShell) {
        // The direct parent of the shell is often a utility process (e.g. VS
        // Code's `ptyhost` process). To get the true IDE process, we need to
        // traverse one level higher to get the grandparent.
        try {
          const { parentPid: grandParentPid } =
            await getParentProcessInfo(parentPid);
          if (grandParentPid > 1) {
            return grandParentPid;
          }
        } catch {
          // Ignore if getting grandparent fails, we'll just use the parent pid.
        }
        return parentPid;
      }

      if (parentPid <= 1) {
        break; // Reached the root
      }
      currentPid = parentPid;
    } catch {
      // Process in chain died
      break;
    }
  }

  console.error(
    'Failed to find shell process in the process tree. Falling back to top-level process, which may be inaccurate. If you see this, please file a bug via /bug.',
  );
  return currentPid;
}

/**
 * Traverses the process tree on Windows to find the IDE process ID.
 *
 * The strategy is to find the grandchild of the root process.
 *
 * @returns A promise that resolves to the numeric PID.
 */
async function getIdeProcessIdForWindows(): Promise<number> {
  let currentPid = process.pid;

  for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
    try {
      const { parentPid } = await getParentProcessInfo(currentPid);

      if (parentPid > 0) {
        try {
          const { parentPid: grandParentPid } =
            await getParentProcessInfo(parentPid);
          if (grandParentPid === 0) {
            // Found grandchild of root
            return currentPid;
          }
        } catch {
          // getting grandparent failed, proceed
        }
      }

      if (parentPid <= 0) {
        break; // Reached the root
      }
      currentPid = parentPid;
    } catch {
      // Process in chain died
      break;
    }
  }
  return currentPid;
}

/**
 * Traverses up the process tree to find the process ID of the IDE.
 *
 * This function uses different strategies depending on the operating system
 * to identify the main application process (e.g., the main VS Code window
 * process).
 *
 * If the IDE process cannot be reliably identified, it will return the
 * top-level ancestor process ID as a fallback.
 *
 * @returns A promise that resolves to the numeric PID of the IDE process.
 * @throws Will throw an error if the underlying shell commands fail.
 */
export async function getIdeProcessId(): Promise<number> {
  const platform = os.platform();

  if (platform === 'win32') {
    return getIdeProcessIdForWindows();
  }

  return getIdeProcessIdForUnix();
}