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