diff --git a/src/main/crypto/random-uuid.global-override-for-injectable.ts b/src/main/crypto/random-uuid.global-override-for-injectable.ts new file mode 100644 index 0000000000..63e5364a8a --- /dev/null +++ b/src/main/crypto/random-uuid.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import randomUUIDInjectable from "./random-uuid.injectable"; + +export default getGlobalOverride(randomUUIDInjectable, () => () => { + throw new Error("Tried to get a randomUUID without override"); +}); diff --git a/src/main/crypto/random-uuid.injectable.ts b/src/main/crypto/random-uuid.injectable.ts new file mode 100644 index 0000000000..cecee487e2 --- /dev/null +++ b/src/main/crypto/random-uuid.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { randomUUID } from "crypto"; + +const randomUUIDInjectable = getInjectable({ + id: "random-uuid", + instantiate: () => randomUUID, + causesSideEffects: true, +}); + +export default randomUUIDInjectable; 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 7186e96a88..668fa95aea 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 @@ -2,13 +2,13 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { spawn } from "child_process"; -import { randomUUID } from "crypto"; -import { basename } from "path"; import type { EnvironmentVariables } from "./compute-shell-environment.injectable"; 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"; -interface UnixShellEnvOptions { +export interface UnixShellEnvOptions { signal?: AbortSignal; } @@ -16,76 +16,91 @@ export type ComputeUnixShellEnvironment = (shell: string, opts?: UnixShellEnvOpt const computeUnixShellEnvironmentInjectable = getInjectable({ id: "compute-unix-shell-environment", - instantiate: (): ComputeUnixShellEnvironment => async (shell, opts) => { - const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; - const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; - const env = { - ...process.env, - ELECTRON_RUN_AS_NODE: "1", - ELECTRON_NO_ATTACH_CONSOLE: "1", - }; - const mark = randomUUID().replace(/-/g, ""); - const regex = new RegExp(`${mark}(.*)${mark}`); - const shellName = basename(shell); - let command: string; - let shellArgs: string[]; + instantiate: (di): ComputeUnixShellEnvironment => { + const powerShellName = /^pwsh(-preview)?$/; + const nonBashLikeShellName = /^t?csh$/; - if (/^pwsh(-preview)?$/.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. - command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`; - shellArgs = ["-Login", "-Command"]; - } else { - command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; + const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); + const spawn = di.inject(spawnInjectable); + const randomUUID = di.inject(randomUUIDInjectable); - if (shellName === "tcsh") { - shellArgs = ["-ic"]; - } else { - shellArgs = ["-ilc"]; + const getShellSpecifices = (shellPath: string, mark: string) => { + const shellName = getBasenameOfPath(shellPath); + + 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. + return { + command: `Command '${process.execPath}' -p '\\"${mark}\\" + JSON.stringify(process.env) + \\"${mark}\\"'`, + shellArgs: ["-Login"], + }; } - } - return new Promise((resolve, reject) => { - const shellProcess = spawn(shell, [...shellArgs, command], { - detached: true, - stdio: ["ignore", "pipe", "pipe"], - env, - }); - const stdout: Buffer[] = []; + 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 + ? ["-l"] + // zsh (at least, maybe others) don't load RC files when in non-interactive mode, even when using -l (login) option + : ["-li"], + }; + }; - opts?.signal?.addEventListener("abort", () => shellProcess.kill()); + return async (shellPath, opts = {}) => { + const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; + const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; + const env = { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + ELECTRON_NO_ATTACH_CONSOLE: "1", + }; + const mark = randomUUID().replace(/-/g, ""); + const regex = new RegExp(`${mark}(\\{.*\\})${mark}`); + const { command, shellArgs } = getShellSpecifices(shellPath, mark); - shellProcess.on("error", (err) => reject(err)); - shellProcess.stdout.on("data", b => stdout.push(b)); - shellProcess.on("close", (code, signal) => { - if (code || signal) { - return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); - } + return new Promise((resolve, reject) => { + const shellProcess = spawn(shellPath, shellArgs, { + detached: true, + env, + }); + const stdout: Buffer[] = []; - try { - const rawOutput = Buffer.concat(stdout).toString("utf-8"); - const match = regex.exec(rawOutput); - const strippedRawOutput = match ? match[1] : "{}"; - const resolvedEnv = JSON.parse(strippedRawOutput); + opts.signal?.addEventListener("abort", () => shellProcess.kill()); - if (runAsNode) { - resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; - } else { - delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; + shellProcess.stdout.on("data", b => stdout.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})`)); } - if (noAttach) { - resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; - } else { - delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; - } + try { + const rawOutput = Buffer.concat(stdout).toString("utf-8"); + const match = regex.exec(rawOutput); + const strippedRawOutput = match ? match[1] : "{}"; + const resolvedEnv = JSON.parse(strippedRawOutput); - resolve(resolvedEnv); - } catch(err) { - reject(err); - } + 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"]; + } + + resolve(resolvedEnv); + } catch (err) { + reject(err); + } + }); + shellProcess.stdin.end(command); }); - }); + }; }, causesSideEffects: true, });