mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token";
|
||||
|
||||
export const shellSyncFailedChannel: MessageChannel<string> = {
|
||||
id: "shell-sync-failed-channel",
|
||||
};
|
||||
+11
@@ -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 computeShellEnvironmentInjectable from "./compute-shell-environment.injectable";
|
||||
|
||||
export default getGlobalOverride(computeShellEnvironmentInjectable, () => async () => ({
|
||||
callWasSuccessful: true,
|
||||
}));
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 timeoutHandle = setTimeout(() => controller.abort(), 30_000);
|
||||
const result = await computeUnixShellEnvironment(shell, { signal: controller.signal });
|
||||
|
||||
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;
|
||||
|
||||
+11
@@ -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 computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable";
|
||||
|
||||
export default getGlobalOverride(computeUnixShellEnvironmentInjectable, () => async () => {
|
||||
throw new Error("Tried to get unix shell env without override");
|
||||
});
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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 "../../../main/child-process/spawn.injectable";
|
||||
import randomUUIDInjectable from "../../../main/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");
|
||||
command = ` ${command}`; // This prevents the command from being added to the history
|
||||
} 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.debug(`[UNIX-SHELL-ENV]: got the following output`, { rawOutput });
|
||||
|
||||
const matchedOutput = regex.exec(rawOutput)?.[1];
|
||||
|
||||
if (!matchedOutput) {
|
||||
return resolve({
|
||||
callWasSuccessful: false,
|
||||
error: "Something has blocked the shell from producing the environement variables",
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedEnv = JSON.parse(matchedOutput) 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;
|
||||
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* 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 "../../../main/child-process/spawn.injectable";
|
||||
import spawnInjectable from "../../../main/child-process/spawn.injectable";
|
||||
import randomUUIDInjectable from "../../../main/crypto/random-uuid.injectable";
|
||||
import { getDiForUnitTesting } from "../../../main/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: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 type { MessageChannelSender } from "../../../common/utils/channel/message-to-channel-injection-token";
|
||||
import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
|
||||
import { shellSyncFailedChannel } from "../common/failure-channel";
|
||||
|
||||
const emitShellSyncFailedInjectable = getInjectable({
|
||||
id: "emit-shell-sync-failed",
|
||||
instantiate: (di): MessageChannelSender<typeof shellSyncFailedChannel> => {
|
||||
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
|
||||
|
||||
return (error) => sendMessageToChannel(shellSyncFailedChannel, error);
|
||||
},
|
||||
});
|
||||
|
||||
export default emitShellSyncFailedInjectable;
|
||||
@@ -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 process from "process";
|
||||
|
||||
const processEnvInjectable = getInjectable({
|
||||
id: "process-env",
|
||||
instantiate: () => process.env,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default processEnvInjectable;
|
||||
@@ -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 process from "process";
|
||||
|
||||
const processExecPathInjectable = getInjectable({
|
||||
id: "process-exec-path",
|
||||
instantiate: () => process.execPath,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default processExecPathInjectable;
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 "../../../main/start-main-application/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 "../../../main/electron-app/electron-app.injectable";
|
||||
import computeShellEnvironmentInjectable from "./compute-shell-environment.injectable";
|
||||
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
|
||||
import emitShellSyncFailedInjectable from "./emit-failure.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);
|
||||
const emitShellSyncFailed = di.inject(emitShellSyncFailedInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-shell",
|
||||
run: async (): Promise<void> => {
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
|
||||
const result = await computeShellEnvironment(resolvedUserShellSetting.get());
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
logger.error(`[SHELL-SYNC]: ${result.error}`);
|
||||
emitShellSyncFailed(result.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
export default setupShellInjectable;
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getMessageChannelListenerInjectable } from "../../../common/utils/channel/message-channel-listener-injection-token";
|
||||
import showErrorNotificationInjectable from "../../../renderer/components/notifications/show-error-notification.injectable";
|
||||
import { shellSyncFailedChannel } from "../common/failure-channel";
|
||||
|
||||
const shellSyncFailureListenerInjectable = getMessageChannelListenerInjectable({
|
||||
id: "notification",
|
||||
channel: shellSyncFailedChannel,
|
||||
handler: (di) => {
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
return (errorMessage) => showErrorNotification(`Failed to sync shell environment: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
|
||||
export default shellSyncFailureListenerInjectable;
|
||||
Reference in new issue
Block a user