xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/terminal.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2023 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15import * as fs from 'fs';
16import * as path from 'path';
17import * as process from 'process';
18
19// Convert `exec` from callback style to promise style.
20import { exec as cbExec } from 'child_process';
21import util from 'node:util';
22const exec = util.promisify(cbExec);
23
24import * as vscode from 'vscode';
25
26import { vendoredBazeliskPath } from './bazel';
27import logger from './logging';
28import { bazel_executable, settings } from './settings';
29
30type InitScript = 'activate' | 'bootstrap';
31
32/**
33 * Generate the configuration to launch an activated or bootstrapped terminal.
34 *
35 * @param initScript The name of the script to be sourced on launch
36 * @returns Options to be provided to terminal launch functions
37 */
38function getShellConfig(
39  initScript: InitScript = 'activate',
40): vscode.TerminalOptions {
41  const shell = settings.terminalShell();
42
43  return {
44    name: 'Pigweed Terminal',
45    shellPath: shell,
46    shellArgs: ['-c', `. ./${initScript}.sh; exec ${shell} -i`],
47  };
48}
49
50/**
51 * Launch an activated terminal.
52 */
53export function launchTerminal() {
54  const shellConfig = getShellConfig();
55  logger.info(`Launching activated terminal with: ${shellConfig.shellPath}`);
56  vscode.window.createTerminal(shellConfig).show();
57}
58
59/**
60 * Launch a activated terminal by bootstrapping it.
61 */
62export function launchBootstrapTerminal() {
63  const shellConfig = getShellConfig();
64  logger.info(`Launching bootstrapepd terminal with: ${shellConfig.shellPath}`);
65  vscode.window.createTerminal(getShellConfig('bootstrap')).show();
66}
67
68/**
69 * Get the type of shell running in an integrated terminal, e.g. bash, zsh, etc.
70 *
71 * This is a bit roundabout; it grabs the shell pid because that seems to be
72 * the only useful piece of information we can get from the VSC API. Then we
73 * use `ps` to find the name of the binary associated with that pid.
74 */
75async function getShellTypeFromTerminal(
76  terminal: vscode.Terminal,
77): Promise<string | undefined> {
78  const pid = await terminal.processId;
79
80  if (!pid) {
81    logger.error('Terminal has no pid');
82    return;
83  }
84
85  logger.info(`Searching for shell with pid=${pid}`);
86
87  let cmd: string;
88  let pidPos: number;
89  let namePos: number;
90
91  switch (process.platform) {
92    case 'linux': {
93      cmd = `ps -A`;
94      pidPos = 1;
95      namePos = 4;
96      break;
97    }
98    case 'darwin': {
99      cmd = `ps -ax`;
100      pidPos = 0;
101      namePos = 3;
102      break;
103    }
104    default: {
105      logger.error(`Platform not currently supported: ${process.platform}`);
106      return;
107    }
108  }
109
110  const { stdout } = await exec(cmd);
111
112  // Split the output into a list of processes, each of which is a tuple of
113  // data from each column of the process table.
114  const processes = stdout.split('\n').map((line) => line.split(/[ ]+/));
115
116  // Find the shell process by pid and extract the process name
117  const shellProcessName = processes
118    .filter((it) => pid === parseInt(it[pidPos]))
119    .at(0)
120    ?.at(namePos);
121
122  if (!shellProcessName) {
123    logger.error(`Could not find process with pid=${pid}`);
124    return;
125  }
126
127  return path.basename(shellProcessName);
128}
129
130/** Prepend the path to Bazelisk into the active terminal's path. */
131export async function patchBazeliskIntoTerminalPath(
132  terminal?: vscode.Terminal,
133): Promise<void> {
134  const bazeliskPath = bazel_executable.get() ?? vendoredBazeliskPath();
135
136  if (!bazeliskPath) {
137    logger.error(
138      "Couldn't activate Bazelisk in terminal because none could be found",
139    );
140    return;
141  }
142
143  // When using the vendored Bazelisk, the binary name won't be `bazelisk` --
144  // it will be something like `bazelisk-darwin_arm64`. But the user expects
145  // to just run `bazelisk` in the terminal. So while this is not entirely
146  // ideal, we just create a symlink in the same directory if the binary name
147  // isn't plain `bazelisk`.
148  if (path.basename(bazeliskPath) !== 'bazelisk') {
149    try {
150      fs.symlink(
151        bazeliskPath,
152        path.join(path.dirname(bazeliskPath), 'bazelisk'),
153        (error) => {
154          const message = error
155            ? `${error.errno} ${error.message}`
156            : 'unknown error';
157          throw new Error(message);
158        },
159      );
160    } catch (error: unknown) {
161      logger.error(`Failed to create Bazelisk symlink for ${bazeliskPath}`);
162      return;
163    }
164  }
165
166  // Should grab the currently active terminal or most recently active, if a
167  // specific terminal reference wasn't provided.
168  terminal = terminal ?? vscode.window.activeTerminal;
169
170  // If there's no terminal, create one
171  if (!terminal) {
172    terminal = vscode.window.createTerminal();
173  }
174
175  if (!terminal) {
176    logger.error(
177      "Couldn't activate Bazelisk in terminal because no terminals could be found",
178    );
179    return;
180  }
181
182  const shellType = await getShellTypeFromTerminal(terminal);
183
184  let cmd: string;
185
186  switch (shellType) {
187    case 'bash':
188    case 'zsh': {
189      cmd = `export PATH="${path.dirname(bazeliskPath)}:$\{PATH}"`;
190      break;
191    }
192    case 'fish': {
193      cmd = `set -x --prepend PATH "${path.dirname(bazeliskPath)}"`;
194      break;
195    }
196    default: {
197      const message = shellType
198        ? `This shell is not currently supported: ${shellType}`
199        : "Couldn't determine how to activate Bazelisk in your terminal. " +
200          'Check the Pigweed output panel for more information.';
201
202      vscode.window.showErrorMessage(message);
203      return;
204    }
205  }
206
207  logger.info(`Patching Bazelisk path into ${shellType} terminal`);
208  terminal.sendText(cmd, true);
209  terminal.show();
210}
211