Improve UX for shell env sync failure (#6644)

* Move files to be under a feature folder

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add error notification on shell sync failure

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve error handling of case where match is not found

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-11-25 04:53:09 -08:00
1 parent 27fb128c05
commit 65b14b9e7b
15 files changed
+85 -21

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.inject
import environmentVariablesInjectable from "../common/utils/environment-variables.injectable";
import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable";
import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable";
import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable";
import setupShellInjectable from "../features/shell-sync/main/setup-shell.injectable";
import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable";
import stopServicesAndExitAppInjectable from "./stop-services-and-exit-app.injectable";
import isDevelopmentInjectable from "../common/vars/is-development.injectable";
@@ -17,7 +17,7 @@ import type WebSocket from "ws";
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
import computeShellEnvironmentInjectable from "../../../features/shell-sync/main/compute-shell-environment.injectable";
import spawnPtyInjectable from "../spawn-pty.injectable";
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
import appNameInjectable from "../../../common/vars/app-name.injectable";
@@ -12,7 +12,7 @@ import isMacInjectable from "../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import createKubeJsonApiForClusterInjectable from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
import computeShellEnvironmentInjectable from "../../../features/shell-sync/main/compute-shell-environment.injectable";
import spawnPtyInjectable from "../spawn-pty.injectable";
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
import appNameInjectable from "../../../common/vars/app-name.injectable";
+1 -1
View File
@@ -13,7 +13,7 @@ import type * as pty from "node-pty";
import { getOrInsertWith } from "../../common/utils";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
import type { Logger } from "../../common/logger";
import type { ComputeShellEnvironment } from "../utils/shell-env/compute-shell-environment.injectable";
import type { ComputeShellEnvironment } from "../../features/shell-sync/main/compute-shell-environment.injectable";
import type { SpawnPty } from "./spawn-pty.injectable";
import type { InitializableState } from "../../common/initializable-state/create";
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
@@ -1,69 +0,0 @@
/**
* 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 loggerInjectable from "../../../common/logger.injectable";
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
import { unionPATHs } from "../../../common/utils/union-env-path";
import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable";
import electronAppInjectable from "../../electron-app/electron-app.injectable";
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
const setupShellInjectable = getInjectable({
id: "setup-shell",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const isSnapPackage = di.inject(isSnapPackageInjectable);
const electronApp = di.inject(electronAppInjectable);
const resolvedUserShellSetting = di.inject(userShellSettingInjectable);
const computeShellEnvironment = di.inject(computeShellEnvironmentInjectable);
return {
id: "setup-shell",
run: async (): Promise<void> => {
logger.info("🐚 Syncing shell environment");
const result = await computeShellEnvironment(resolvedUserShellSetting.get());
if (!result.callWasSuccessful) {
return void logger.error(`[SHELL-SYNC]: ${result.error}`);
}
const env = result.response;
if (!env) {
return void logger.debug("[SHELL-SYNC]: nothing to do, env not special in shells");
}
if (!env.LANG) {
// the LANG env var expects an underscore instead of electron's dash
env.LANG = `${electronApp.getLocale().replace("-", "_")}.UTF-8`;
} else if (!env.LANG.endsWith(".UTF-8")) {
env.LANG += ".UTF-8";
}
if (!isSnapPackage) {
// Prefer the synced PATH over the initial one
process.env.PATH = unionPATHs(env.PATH ?? "", process.env.PATH ?? "");
}
// The spread operator allows joining of objects. The precedence is last to first.
process.env = {
...env,
...process.env,
};
logger.info(`[SHELL-SYNC]: Synced shell env`);
logger.debug(`[SHELL-SYNC]: updated env`, process.env);
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
causesSideEffects: true,
});
export default setupShellInjectable;
@@ -1,53 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncResult } from "../../../common/utils/async-result";
import { getInjectable } from "@ogre-tools/injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable";
export type EnvironmentVariables = Partial<Record<string, string>>;
export type ComputeShellEnvironment = (shell: string) => Promise<AsyncResult<EnvironmentVariables | undefined, string>>;
const computeShellEnvironmentInjectable = getInjectable({
id: "compute-shell-environment",
instantiate: (di): ComputeShellEnvironment => {
const isWindows = di.inject(isWindowsInjectable);
const computeUnixShellEnvironment = di.inject(computeUnixShellEnvironmentInjectable);
if (isWindows) {
return async () => ({
callWasSuccessful: true,
response: undefined,
});
}
return async (shell) => {
const controller = new AbortController();
const shellEnv = computeUnixShellEnvironment(shell, { signal: controller.signal });
const timeoutHandle = setTimeout(() => controller.abort(), 30_000);
const result = await shellEnv;
clearTimeout(timeoutHandle);
if (result.callWasSuccessful) {
return result;
}
if (controller.signal.aborted) {
return {
callWasSuccessful: false,
error: `Resolving shell environment is taking very long. Please review your shell configuration: ${result.error}`,
};
}
return result;
};
},
});
export default computeShellEnvironmentInjectable;
@@ -1,11 +0,0 @@
/**
* 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 computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable";
export default getGlobalOverride(computeUnixShellEnvironmentInjectable, () => async () => {
throw new Error("Tried to get unix shell env without override");
});
@@ -1,178 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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";
import loggerInjectable from "../../../common/logger.injectable";
import processExecPathInjectable from "./execPath.injectable";
import processEnvInjectable from "./env.injectable";
import { object } from "../../../common/utils";
import type { AsyncResult } from "../../../common/utils/async-result";
export interface UnixShellEnvOptions {
signal: AbortSignal;
}
export type ComputeUnixShellEnvironment = (shell: string, opts: UnixShellEnvOptions) => Promise<AsyncResult<EnvironmentVariables, string>>;
/**
* @param src The object containing the current environment variables
* @param overrides The environment variables that want to be overridden before passing the env to a child process
* @returns The combination of environment variables and a function which resets an object of environment variables to the values the keys corresponded to in `src` (rather than `overrides`)
*/
const getResetProcessEnv = (src: Partial<Record<string, string>>, overrides: Partial<Record<string, string>>): {
resetEnvPairs: (target: Partial<Record<string, string>>) => void;
env: Partial<Record<string, string>>;
} => {
const originals = object.entries(overrides).map(([name]) => [name, src[name]] as const);
return {
env: {
...src,
...overrides,
},
resetEnvPairs: (target) => {
for (const [name, orginalValue] of originals) {
if (typeof orginalValue === "string") {
target[name] = orginalValue;
} else {
delete target[name];
}
}
},
};
};
const computeUnixShellEnvironmentInjectable = getInjectable({
id: "compute-unix-shell-environment",
instantiate: (di): ComputeUnixShellEnvironment => {
const powerShellName = /^pwsh(-preview)?$/;
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 processExecPath = di.inject(processExecPathInjectable);
const processEnv = di.inject(processEnvInjectable);
const getShellSpecifices = (shellName: string) => {
const mark = randomUUID().replace(/-/g, "");
const regex = new RegExp(`${mark}(\\{.*\\})${mark}`);
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 '${processExecPath}' -p '\\"${mark}\\" + JSON.stringify(process.env) + \\"${mark}\\"'`,
shellArgs: ["-Login"],
regex,
};
}
let command = `'${processExecPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
const shellArgs = ["-l"];
if (fishLikeShellName.test(shellName)) {
shellArgs.push("-c", command);
command = "";
} else if (!cshLikeShellName.test(shellName)) {
// zsh (at least, maybe others) don't load RC files when in non-interactive mode, even when using -l (login) option
shellArgs.push("-i");
} else {
// Some shells don't support any other options when providing the -l (login) shell option
}
return { command, shellArgs, regex };
};
return async (shellPath, opts) => {
const { resetEnvPairs, env } = getResetProcessEnv(processEnv, {
ELECTRON_RUN_AS_NODE: "1",
ELECTRON_NO_ATTACH_CONSOLE: "1",
TERM: "screen-256color-bce", // required for fish
});
const shellName = getBasenameOfPath(shellPath);
const { command, shellArgs, regex } = getShellSpecifices(shellName);
logger.info(`[UNIX-SHELL-ENV]: running against ${shellPath}`, { command, shellArgs });
return new Promise((resolve) => {
const shellProcess = spawn(shellPath, shellArgs, {
signal: opts.signal,
detached: true,
env,
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const getErrorContext = (other: object = {}) => {
const context = {
...other,
stdout: Buffer.concat(stdout).toString("utf-8"),
stderr: Buffer.concat(stderr).toString("utf-8"),
};
return JSON.stringify(context, null, 4);
};
shellProcess.stdout.on("data", b => stdout.push(b));
shellProcess.stderr.on("data", b => stderr.push(b));
shellProcess.on("error", (error) => {
if (opts.signal.aborted) {
resolve({
callWasSuccessful: false,
error: `timeout: ${getErrorContext()}`,
});
} else {
resolve({
callWasSuccessful: false,
error: `Failed to spawn ${shellPath}: ${getErrorContext({ error: String(error) })}`,
});
}
});
shellProcess.on("close", (code, signal) => {
if (code || signal) {
return resolve({
callWasSuccessful: false,
error: `Shell did not exit sucessfully: ${getErrorContext({ code, signal })}`,
});
}
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) as Partial<Record<string, string>>;
resetEnvPairs(resolvedEnv);
resolve({
callWasSuccessful: true,
response: resolvedEnv,
});
} catch (err) {
resolve({
callWasSuccessful: false,
error: String(err),
});
}
});
shellProcess.stdin.end(command);
});
};
},
causesSideEffects: true,
});
export default computeUnixShellEnvironmentInjectable;
@@ -1,509 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { ChildProcessWithoutNullStreams } from "child_process";
import EventEmitter from "events";
import { flushPromises } from "../../../common/test-utils/flush-promises";
import type { Spawn } from "../../child-process/spawn.injectable";
import spawnInjectable from "../../child-process/spawn.injectable";
import randomUUIDInjectable from "../../crypto/random-uuid.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { ComputeUnixShellEnvironment } from "./compute-unix-shell-environment.injectable";
import computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable";
import processEnvInjectable from "./env.injectable";
import processExecPathInjectable from "./execPath.injectable";
import MemoryStream from "memorystream";
const expectedEnv = {
SOME_ENV_VAR: "some-env-value",
ELECTRON_RUN_AS_NODE: "1",
ELECTRON_NO_ATTACH_CONSOLE: "1",
TERM: "screen-256color-bce",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
};
describe("computeUnixShellEnvironment technical tests", () => {
let di: DiContainer;
let computeUnixShellEnvironment: ComputeUnixShellEnvironment;
let spawnMock: jest.MockedFunction<Spawn>;
let shellProcessFake: ChildProcessWithoutNullStreams;
let stdinValue: string;
let shellStdin: MemoryStream;
let shellStdout: MemoryStream;
let shellStderr: MemoryStream;
let unixShellEnv: ReturnType<ComputeUnixShellEnvironment>;
beforeEach(() => {
di = getDiForUnitTesting({
doGeneralOverrides: true,
});
spawnMock = jest.fn().mockImplementation((spawnfile, spawnargs) => {
shellStdin = new MemoryStream();
shellStdout = new MemoryStream();
shellStderr = new MemoryStream();
stdinValue = "";
shellStdin.on("data", (chunk) => {
stdinValue += chunk.toString();
});
return shellProcessFake = Object.assign(new EventEmitter(), {
stdin: shellStdin,
stdout: shellStdout,
stderr: shellStderr,
stdio: [
shellStdin,
shellStdout,
shellStderr,
] as any,
killed: false,
kill: jest.fn(),
send: jest.fn(),
disconnect: jest.fn(),
unref: jest.fn(),
ref: jest.fn(),
connected: false,
exitCode: null,
signalCode: null,
spawnargs,
spawnfile,
});
});
di.override(spawnInjectable, () => spawnMock);
di.override(randomUUIDInjectable, () => () => "deadbeef");
di.override(processEnvInjectable, () => ({
SOME_ENV_VAR: "some-env-value",
TERM: "some-other-value",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
}));
di.override(processExecPathInjectable, () => "/some/process/exec/path");
di.unoverride(computeUnixShellEnvironmentInjectable);
di.permitSideEffects(computeUnixShellEnvironmentInjectable);
computeUnixShellEnvironment = di.inject(computeUnixShellEnvironmentInjectable);
});
describe.each([
"/bin/csh",
"/bin/tcsh",
])("when shell is %s", (shellPath) => {
beforeEach(async () => {
const controller = new AbortController();
unixShellEnv = computeUnixShellEnvironment(shellPath, { signal: controller.signal });
await flushPromises();
});
it("should spawn a process with the correct arguments", () => {
expect(spawnMock).toBeCalledWith(
shellPath,
[
"-l",
],
expect.objectContaining({
env: expectedEnv,
}),
);
});
it("should send the command via stdin", () => {
expect(stdinValue).toBe(`'/some/process/exec/path' -p '"deadbeef" + JSON.stringify(process.env) + "deadbeef"'`);
});
it("should close stdin", () => {
expect(shellStdin.readableEnded).toBe(true);
});
describe("when process errors", () => {
beforeEach(() => {
shellProcessFake.emit("error", new Error("some-error"));
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: `Failed to spawn ${shellPath}: ${JSON.stringify({
error: "Error: some-error",
stdout: "",
stderr: "",
}, null, 4)}`,
});
});
});
describe("when process exits with non-zero exit code", () => {
beforeEach(() => {
shellProcessFake.emit("close", 1, null);
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 1,\n "signal": null,\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process exits with a signal", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0, "SIGKILL");
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 0,\n "signal": "SIGKILL",\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process stdout emits some data", () => {
beforeEach(() => {
const fakeInnerEnv = {
PATH: "/bin",
...expectedEnv,
};
shellStdout.emit("data", Buffer.from(`some-other-datadeadbeef${JSON.stringify(fakeInnerEnv)}deadbeefsome-third-other-data`));
});
describe("when process successfully exits", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0);
});
it("should resolve the env", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: true,
response: {
PATH: "/bin",
SOME_ENV_VAR: "some-env-value",
TERM: "some-other-value",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
},
});
});
});
});
});
describe.each([
"/bin/bash",
"/bin/sh",
"/bin/zsh",
])("when shell is %s", (shellPath) => {
beforeEach(async () => {
const controller = new AbortController();
unixShellEnv = computeUnixShellEnvironment(shellPath, { signal: controller.signal });
await flushPromises();
});
it("should spawn a process with the correct arguments", () => {
expect(spawnMock).toBeCalledWith(
shellPath,
[
"-l",
"-i",
],
expect.objectContaining({
env: expectedEnv,
}),
);
});
it("should send the command via stdin", () => {
expect(stdinValue).toBe(`'/some/process/exec/path' -p '"deadbeef" + JSON.stringify(process.env) + "deadbeef"'`);
});
it("should close stdin", () => {
expect(shellStdin.readableEnded).toBe(true);
});
describe("when process errors", () => {
beforeEach(() => {
shellProcessFake.emit("error", new Error("some-error"));
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: `Failed to spawn ${shellPath}: ${JSON.stringify({
error: "Error: some-error",
stdout: "",
stderr: "",
}, null, 4)}`,
});
});
});
describe("when process exits with non-zero exit code", () => {
beforeEach(() => {
shellProcessFake.emit("close", 1, null);
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 1,\n "signal": null,\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process exits with a signal", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0, "SIGKILL");
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 0,\n "signal": "SIGKILL",\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process stdout emits some data", () => {
beforeEach(() => {
const fakeInnerEnv = {
PATH: "/bin",
...expectedEnv,
};
shellStdout.emit("data", Buffer.from(`some-other-datadeadbeef${JSON.stringify(fakeInnerEnv)}deadbeefsome-third-other-data`));
});
describe("when process successfully exits", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0);
});
it("should resolve the env", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: true,
response: {
PATH: "/bin",
SOME_ENV_VAR: "some-env-value",
TERM: "some-other-value",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
},
});
});
});
});
});
describe.each([
"/usr/local/bin/fish",
])("when shell is %s", (shellPath) => {
beforeEach(async () => {
const controller = new AbortController();
unixShellEnv = computeUnixShellEnvironment(shellPath, { signal: controller.signal });
await flushPromises();
});
it("should spawn a process with the correct arguments", () => {
expect(spawnMock).toBeCalledWith(
shellPath,
[
"-l",
"-c",
`'/some/process/exec/path' -p '"deadbeef" + JSON.stringify(process.env) + "deadbeef"'`,
],
expect.objectContaining({
env: expectedEnv,
}),
);
});
it("should not send anything via stdin", () => {
expect(stdinValue).toBe("");
});
it("should close stdin", () => {
expect(shellStdin.readableEnded).toBe(true);
});
describe("when process errors", () => {
beforeEach(() => {
shellProcessFake.emit("error", new Error("some-error"));
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: `Failed to spawn ${shellPath}: ${JSON.stringify({
error: "Error: some-error",
stdout: "",
stderr: "",
}, null, 4)}`,
});
});
});
describe("when process exits with non-zero exit code", () => {
beforeEach(() => {
shellProcessFake.emit("close", 1, null);
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 1,\n "signal": null,\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process exits with a signal", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0, "SIGKILL");
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 0,\n "signal": "SIGKILL",\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process stdout emits some data", () => {
beforeEach(() => {
const fakeInnerEnv = {
PATH: "/bin",
...expectedEnv,
};
shellStdout.emit("data", Buffer.from(`some-other-datadeadbeef${JSON.stringify(fakeInnerEnv)}deadbeefsome-third-other-data`));
});
describe("when process successfully exits", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0);
});
it("should resolve the env", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: true,
response: {
PATH: "/bin",
SOME_ENV_VAR: "some-env-value",
TERM: "some-other-value",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
},
});
});
});
});
});
describe.each([
"/usr/local/bin/pwsh",
"/usr/local/bin/pwsh-preview",
])("when shell is %s", (shellPath) => {
beforeEach(async () => {
const controller = new AbortController();
unixShellEnv = computeUnixShellEnvironment(shellPath, { signal: controller.signal });
await flushPromises();
});
it("should spawn a process with the correct arguments", () => {
expect(spawnMock).toBeCalledWith(
shellPath,
[
"-Login",
],
expect.objectContaining({
env: expectedEnv,
}),
);
});
it("should send the command via stdin", () => {
expect(stdinValue).toBe(`Command '/some/process/exec/path' -p '\\"deadbeef\\" + JSON.stringify(process.env) + \\"deadbeef\\"'`);
});
it("should close stdin", () => {
expect(shellStdin.readableEnded).toBe(true);
});
describe("when process errors", () => {
beforeEach(() => {
shellProcessFake.emit("error", new Error("some-error"));
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: `Failed to spawn ${shellPath}: ${JSON.stringify({
error: "Error: some-error",
stdout: "",
stderr: "",
}, null, 4)}`,
});
});
});
describe("when process exits with non-zero exit code", () => {
beforeEach(() => {
shellProcessFake.emit("close", 1, null);
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 1,\n "signal": null,\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process exits with a signal", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0, "SIGKILL");
});
it("should resolve with a failed call", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: false,
error: 'Shell did not exit sucessfully: {\n "code": 0,\n "signal": "SIGKILL",\n "stdout": "",\n "stderr": ""\n}',
});
});
});
describe("when process stdout emits some data", () => {
beforeEach(() => {
const fakeInnerEnv = {
PATH: "/bin",
...expectedEnv,
};
shellStdout.emit("data", Buffer.from(`some-other-datadeadbeef${JSON.stringify(fakeInnerEnv)}deadbeefsome-third-other-data`));
});
describe("when process successfully exits", () => {
beforeEach(() => {
shellProcessFake.emit("close", 0);
});
it("should resolve the env", async () => {
await expect(unixShellEnv).resolves.toEqual({
callWasSuccessful: true,
response: {
PATH: "/bin",
SOME_ENV_VAR: "some-env-value",
TERM: "some-other-value",
SOME_THIRD_NON_UNDEFINED_VALUE: "",
},
});
});
});
});
});
});
@@ -1,14 +0,0 @@
/**
* 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 process from "process";
const processEnvInjectable = getInjectable({
id: "process-env",
instantiate: () => process.env,
causesSideEffects: true,
});
export default processEnvInjectable;
@@ -1,14 +0,0 @@
/**
* 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 process from "process";
const processExecPathInjectable = getInjectable({
id: "process-exec-path",
instantiate: () => process.execPath,
causesSideEffects: true,
});
export default processExecPathInjectable;