diff --git a/src/main/utils/shell-env/compute-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-shell-environment.injectable.ts index e3a60fcd03..8307f1f319 100644 --- a/src/main/utils/shell-env/compute-shell-environment.injectable.ts +++ b/src/main/utils/shell-env/compute-shell-environment.injectable.ts @@ -6,7 +6,7 @@ import type { AsyncResult } from "../../../common/utils/async-result"; import { getInjectable } from "@ogre-tools/injectable"; import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; -import { disposer } from "../../../common/utils"; +import { disposer, hasTypedProperty, isString } from "../../../common/utils"; import computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable"; export type EnvironmentVariables = Partial>; @@ -47,6 +47,13 @@ const computeShellEnvironmentInjectable = getInjectable({ }; } + if (error && hasTypedProperty(error, "stderr", isString)) { + return { + callWasSuccessful: false, + error: `${error}:\n${error.stderr}`, + }; + } + return { callWasSuccessful: false, error: String(error), diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts index 90108f7029..e1be5d7948 100644 --- a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts +++ b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts @@ -7,6 +7,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import spawnInjectable from "../../child-process/spawn.injectable"; import randomUUIDInjectable from "../../crypto/random-uuid.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; export interface UnixShellEnvOptions { signal: AbortSignal; @@ -14,19 +15,33 @@ export interface UnixShellEnvOptions { export type ComputeUnixShellEnvironment = (shell: string, opts: UnixShellEnvOptions) => Promise; +const getResetProcessEnv = (src: Partial>, names: string[]): ((target: Partial>) => void) => { + const pairs = names.map(name => ([name, src[name]] as const)); + + return (target) => { + for (const [name, orginalValue] of pairs) { + if (orginalValue) { + target[name] = orginalValue; + } else { + delete target[name]; + } + } + }; +}; + const computeUnixShellEnvironmentInjectable = getInjectable({ id: "compute-unix-shell-environment", instantiate: (di): ComputeUnixShellEnvironment => { const powerShellName = /^pwsh(-preview)?$/; - const nonBashLikeShellName = /^t?csh$/; + const cshLikeShellName = /^(t?csh)$/; + const fishLikeShellName = /^fish$/; const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); const spawn = di.inject(spawnInjectable); + const logger = di.inject(loggerInjectable); const randomUUID = di.inject(randomUUIDInjectable); - const getShellSpecifices = (shellPath: string, mark: string) => { - const shellName = getBasenameOfPath(shellPath); - + const getShellSpecifices = (shellName: string, mark: string) => { if (powerShellName.test(shellName)) { // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how // you escape single quotes inside of a single quoted string. @@ -38,66 +53,80 @@ const computeUnixShellEnvironmentInjectable = getInjectable({ return { command: `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`, - shellArgs: nonBashLikeShellName.test(shellName) - // tcsh and csh don't support any other options when providing the -l (login) shell option + shellArgs: cshLikeShellName.test(shellName) || fishLikeShellName.test(shellName) + // Some shells don't support any other options when providing the -l (login) shell option ? ["-l"] // zsh (at least, maybe others) don't load RC files when in non-interactive mode, even when using -l (login) option : ["-li"], }; }; + return async (shellPath, opts) => { - const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; - const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; + const resetEnvPairs = getResetProcessEnv(process.env, [ + "ELECTRON_RUN_AS_NODE", + "ELECTRON_NO_ATTACH_CONSOLE", + "TERM", + ]); const env = { ...process.env, ELECTRON_RUN_AS_NODE: "1", ELECTRON_NO_ATTACH_CONSOLE: "1", + TERM: "screen-256color-bce", // required for fish }; const mark = randomUUID().replace(/-/g, ""); const regex = new RegExp(`${mark}(\\{.*\\})${mark}`); const { command, shellArgs } = getShellSpecifices(shellPath, mark); + logger.info(`[UNIX-SHELL-ENV]: running against ${shellPath}`, { command, shellArgs }); + return new Promise((resolve, reject) => { + const shellName = getBasenameOfPath(shellPath); + const isFishShellLike = fishLikeShellName.test(shellName); + + if (isFishShellLike) { + shellArgs.push("-c", command); + } + const shellProcess = spawn(shellPath, shellArgs, { - detached: true, signal: opts.signal, env, }); const stdout: Buffer[] = []; + const stderr: Buffer[] = []; shellProcess.stdout.on("data", b => stdout.push(b)); + shellProcess.stderr.on("data", b => stderr.push(b)); shellProcess.on("error", (err) => reject(err)); shellProcess.on("close", (code, signal) => { if (code || signal) { - return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); + return reject(Object.assign(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`), { + stderr: Buffer.concat(stderr).toString("utf-8"), + })); } try { const rawOutput = Buffer.concat(stdout).toString("utf-8"); + + logger.info(`[UNIX-SHELL-ENV]: got the following output`, { rawOutput }); + const match = regex.exec(rawOutput); const strippedRawOutput = match ? match[1] : "{}"; const resolvedEnv = JSON.parse(strippedRawOutput); - if (runAsNode) { - resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; - } else { - delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; - } - - if (noAttach) { - resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; - } else { - delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; - } - + resetEnvPairs(resolvedEnv); resolve(resolvedEnv); } catch (err) { reject(err); } }); - shellProcess.stdin.end(command); + + if (isFishShellLike) { + shellProcess.stdin.end(); + } else { + shellProcess.stdin.end(command); + } }); }; },