import { ChildProcessWithoutNullStreams } from "child_process"; import { spawn } from "cross-spawn"; import * as which from "which"; import { dirname } from "path"; import { ensureCloudMtaInstalledInPath } from "./execution"; let mtaCommandPromise: Promise | undefined = undefined; export async function findMtaCommand(): Promise { // "mta-local" package is always available. However, we don't want to download the mta executable // if it's already in the path and has the correct version. // So, check first if there is an "mta" executable available. let mtaPath: string | null = null; try { mtaPath = await which("mta"); } catch (e) { // Ignore errors from "which" } // Check the version of the global mta if (mtaPath) { try { await ensureCloudMtaInstalledInPath(mtaPath); } catch (e) { // Don't use the found path - it's not in the correct version (or can't be executed) mtaPath = null; } } if (!mtaPath) { // eslint-disable-next-line @typescript-eslint/no-var-requires mtaPath = require("mta-local").paths["mta"] as string; } return mtaPath; } let mtaTestCommand: string | undefined = undefined; export function getMtaCommand(): Promise { // For tests if (mtaTestCommand !== undefined) { return Promise.resolve(mtaTestCommand); } if (mtaCommandPromise === undefined) { mtaCommandPromise = findMtaCommand(); } return mtaCommandPromise; } export function setMtaCommandForTests(command: string | undefined): void { mtaTestCommand = command; } export async function runMta(args: string[]): Promise { return runProcess(await getMtaCommand(), args); } export async function runProcess( command: string, args: string[] ): Promise { return new Promise((resolve, reject) => { // Run the process in its directory instead of relying on the process cwd. // This doesn't matter for the logic, but the process might fail if the cwd doesn't exist. // If the command is not a path the cwd will still be used. const proc = spawn(command, args, { cwd: dirname(command) }); procEvents(command, proc, resolve, reject); }); } function procEvents( command: string, proc: ChildProcessWithoutNullStreams, resolve: (value: string) => void, reject: (value: Error) => void ) { const output: string[] = []; let handled = false; proc.stdout.on("data", (data) => { output.push(String(data)); }); proc.stderr.on("data", (data) => { output.push(String(data)); }); proc.on("error", (err) => { // istanbul ignore if - defensive programming, it should not happen if (handled) { return; } handled = true; reject(err); }); proc.on("exit", (code, signal) => { // istanbul ignore if - defensive programming, it should not happen if (handled) { return; } handled = true; const allOutput = output.join("").trim(); let returnValue = allOutput.substring(allOutput.lastIndexOf("\n") + 1); // If the last line doesn't start with an opening curly brace \"{\", it isn't a JSON object as expected, // so instead, all the output is taken. // istanbul ignore if - defensive programming, we don't have such a use case currently if (!returnValue.startsWith("{")) { returnValue = allOutput; } if (code === 0) { resolve(returnValue); } else { // Not using else-if here due to istanbul issue: https://github.com/gotwarlost/istanbul/issues/781 // istanbul ignore if - signals cannot be triggered easily in tests if (signal !== null) { const error: ErrorWithCode = new Error(`Could not execute ${command}`); error.code = signal; reject(error); } else { try { // Tries to get the error message. If no message is found, uses the return value itself. // istanbul ignore next - no easy way to test the condition returnValue = JSON.parse(returnValue).message || returnValue; } catch (e) { // Ignores this and uses the return value itself. } reject(new Error(returnValue)); } } }); } export interface ErrorWithCode extends Error { code?: string; }