/** * @license * Copyright 2019-2020 Balena Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { diffTrimmedLines } from 'diff'; export const ROOT = path.join(__dirname, '..'); /** Tap and buffer this process' stdout and stderr */ export class StdOutTap { public stdoutBuf: string[] = []; public stderrBuf: string[] = []; public allBuf: string[] = []; // both stdout and stderr protected origStdoutWrite: typeof process.stdout.write; protected origStderrWrite: typeof process.stdout.write; constructor(protected printDots = false) {} tap() { this.origStdoutWrite = process.stdout.write; this.origStderrWrite = process.stderr.write; process.stdout.write = (chunk: string, ...args: any[]): boolean => { this.stdoutBuf.push(chunk); this.allBuf.push(chunk); const str = this.printDots ? '.' : chunk; return this.origStdoutWrite.call(process.stdout, str, ...args); }; process.stderr.write = (chunk: string, ...args: any[]): boolean => { this.stderrBuf.push(chunk); this.allBuf.push(chunk); const str = this.printDots ? '.' : chunk; return this.origStderrWrite.call(process.stderr, str, ...args); }; } untap() { process.stdout.write = this.origStdoutWrite; process.stderr.write = this.origStderrWrite; if (this.printDots) { console.error(''); } } } /** * Diff strings by line, using the 'diff' npm package: * https://www.npmjs.com/package/diff */ export function diffLines(str1: string, str2: string): string { const diffObjs = diffTrimmedLines(str1, str2); const prefix = (chunk: string, char: string) => chunk .split('\n') .map((line: string) => `${char} ${line}`) .join('\n'); const diffStr = diffObjs .map((part: any) => { return part.added ? prefix(part.value, '+') : part.removed ? prefix(part.value, '-') : prefix(part.value, ' '); }) .join('\n'); return diffStr; } export function loadPackageJson() { const packageJsonPath = path.join(ROOT, 'package.json'); const packageJson = fs.readFileSync(packageJsonPath, 'utf8'); return JSON.parse(packageJson); } /** * Error handling wrapper around the npm `which` package: * "Like the unix which utility. Finds the first instance of a specified * executable in the PATH environment variable. Does not cache the results, * so hash -r is not needed when the PATH changes." * * @param program Basename of a program, for example 'ssh' * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE' */ export async function which(program: string): Promise<string> { const whichMod = await import('which'); let programPath: string; try { programPath = await whichMod(program); } catch (err) { if (err.code === 'ENOENT') { throw new Error(`'${program}' program not found. Is it installed?`); } throw err; } return programPath; } /** * Call which(programName) and spawn() with the given arguments. Throw an error * if the process exit code is not zero. */ export async function whichSpawn( programName: string, args: string[] = [], ): Promise<void> { const program = await which(programName); let error: Error | undefined; let exitCode: number | undefined; try { exitCode = await new Promise<number>((resolve, reject) => { try { spawn(program, args, { stdio: 'inherit' }) .on('error', reject) .on('close', resolve); } catch (err) { reject(err); } }); } catch (err) { error = err; } if (error || exitCode) { const msg = [ `${programName} failed with exit code ${exitCode}:`, `"${program}" [${args}]`, ]; if (error) { msg.push(`${error}`); } throw new Error(msg.join('\n')); } }