mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix windows shell not having all environment variables (#6402)
* Fix windows shell not having all environment variables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix startup due to buildVersion dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * Call cleanup in computeShellEnvironment Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
4e02ccb1c7
commit
d96918c966
13
src/common/user-store/resolved-shell.injectable.ts
Normal file
13
src/common/user-store/resolved-shell.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 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 userStoreInjectable from "./user-store.injectable";
|
||||||
|
|
||||||
|
const resolvedShellInjectable = getInjectable({
|
||||||
|
id: "resolved-shell",
|
||||||
|
instantiate: (di) => di.inject(userStoreInjectable).resolvedShell,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default resolvedShellInjectable;
|
||||||
@ -7,14 +7,14 @@ import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
|||||||
|
|
||||||
describe("clearKubeconfigEnvVars tests", () => {
|
describe("clearKubeconfigEnvVars tests", () => {
|
||||||
it("should not touch non kubeconfig keys", () => {
|
it("should not touch non kubeconfig keys", () => {
|
||||||
expect(clearKubeconfigEnvVars({ a: 1 })).toStrictEqual({ a: 1 });
|
expect(clearKubeconfigEnvVars({ a: "22" })).toStrictEqual({ a: "22" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a single kubeconfig key", () => {
|
it("should remove a single kubeconfig key", () => {
|
||||||
expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1" })).toStrictEqual({ a: 1 });
|
expect(clearKubeconfigEnvVars({ a: "22", kubeconfig: "1" })).toStrictEqual({ a: "22" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a two kubeconfig key", () => {
|
it("should remove a two kubeconfig key", () => {
|
||||||
expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: 1 });
|
expect(clearKubeconfigEnvVars({ a: "22", kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: "22" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,11 +36,8 @@ export class LocalShellSession extends ShellSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
let env = await this.getCachedShellEnv();
|
|
||||||
|
|
||||||
// extensions can modify the env
|
// extensions can modify the env
|
||||||
env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, env);
|
const env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, await this.getCachedShellEnv());
|
||||||
|
|
||||||
const shell = env.PTYSHELL;
|
const shell = env.PTYSHELL;
|
||||||
|
|
||||||
if (!shell) {
|
if (!shell) {
|
||||||
|
|||||||
@ -17,6 +17,11 @@ import type WebSocket from "ws";
|
|||||||
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
|
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
|
||||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||||
|
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
|
||||||
|
import spawnPtyInjectable from "../spawn-pty.injectable";
|
||||||
|
import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable";
|
||||||
|
import appNameInjectable from "../../../common/vars/app-name.injectable";
|
||||||
|
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||||
|
|
||||||
export interface OpenLocalShellSessionArgs {
|
export interface OpenLocalShellSessionArgs {
|
||||||
websocket: WebSocket;
|
websocket: WebSocket;
|
||||||
@ -34,13 +39,18 @@ const openLocalShellSessionInjectable = getInjectable({
|
|||||||
const dependencies: LocalShellSessionDependencies = {
|
const dependencies: LocalShellSessionDependencies = {
|
||||||
directoryForBinaries: di.inject(directoryForBinariesInjectable),
|
directoryForBinaries: di.inject(directoryForBinariesInjectable),
|
||||||
isMac: di.inject(isMacInjectable),
|
isMac: di.inject(isMacInjectable),
|
||||||
modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable),
|
|
||||||
isWindows: di.inject(isWindowsInjectable),
|
isWindows: di.inject(isWindowsInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
userStore: di.inject(userStoreInjectable),
|
userStore: di.inject(userStoreInjectable),
|
||||||
|
resolvedShell: di.inject(resolvedShellInjectable),
|
||||||
|
appName: di.inject(appNameInjectable),
|
||||||
|
buildVersion: di.inject(buildVersionInjectable),
|
||||||
|
modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable),
|
||||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||||
joinPaths: di.inject(joinPathsInjectable),
|
joinPaths: di.inject(joinPathsInjectable),
|
||||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||||
|
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
|
||||||
|
spawnPty: di.inject(spawnPtyInjectable),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (args) => {
|
return (args) => {
|
||||||
|
|||||||
90
src/main/shell-session/local-shell-session/techincal.test.ts
Normal file
90
src/main/shell-session/local-shell-session/techincal.test.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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 { WebSocket } from "ws";
|
||||||
|
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import platformInjectable from "../../../common/vars/platform.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||||
|
import type { Kubectl } from "../../kubectl/kubectl";
|
||||||
|
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||||
|
import type { OpenShellSession } from "../create-shell-session.injectable";
|
||||||
|
import type { SpawnPty } from "../spawn-pty.injectable";
|
||||||
|
import spawnPtyInjectable from "../spawn-pty.injectable";
|
||||||
|
import openLocalShellSessionInjectable from "./open.injectable";
|
||||||
|
|
||||||
|
describe("technical unit tests for local shell sessions", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting({
|
||||||
|
doGeneralOverrides: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||||
|
di.override(buildVersionInjectable, () => ({
|
||||||
|
get: () => "1.1.1",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when on windows", () => {
|
||||||
|
let openLocalShellSession: OpenShellSession;
|
||||||
|
let spawnPtyMock: jest.MockedFunction<SpawnPty>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di.override(platformInjectable, () => "win32");
|
||||||
|
|
||||||
|
spawnPtyMock = jest.fn();
|
||||||
|
di.override(spawnPtyInjectable, () => spawnPtyMock);
|
||||||
|
|
||||||
|
di.override(createKubectlInjectable, () => () => ({
|
||||||
|
binDir: async () => "/some-kubectl-binary-dir",
|
||||||
|
getBundledPath: () => "/some-bundled-kubectl-path",
|
||||||
|
}) as Partial<Kubectl> as Kubectl);
|
||||||
|
|
||||||
|
openLocalShellSession = di.inject(openLocalShellSessionInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when opening a local shell session", () => {
|
||||||
|
it("should pass through all environment variables to shell", async () => {
|
||||||
|
process.env.MY_TEST_ENV_VAR = "true";
|
||||||
|
|
||||||
|
spawnPtyMock.mockImplementationOnce((file, args, options) => {
|
||||||
|
expect(options.env).toMatchObject({
|
||||||
|
MY_TEST_ENV_VAR: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cols: 80,
|
||||||
|
rows: 40,
|
||||||
|
pid: 12343,
|
||||||
|
handleFlowControl: false,
|
||||||
|
kill: jest.fn(),
|
||||||
|
onData: jest.fn(),
|
||||||
|
onExit: jest.fn(),
|
||||||
|
pause: jest.fn(),
|
||||||
|
process: "my-pty",
|
||||||
|
resize: jest.fn(),
|
||||||
|
resume: jest.fn(),
|
||||||
|
write: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await openLocalShellSession({
|
||||||
|
cluster: {
|
||||||
|
getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig",
|
||||||
|
preferences: {},
|
||||||
|
} as Partial<Cluster> as Cluster,
|
||||||
|
tabId: "my-tab-id",
|
||||||
|
websocket: new WebSocket(null),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -12,6 +12,11 @@ import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
|||||||
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
|
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
import createKubeJsonApiForClusterInjectable from "../../../common/k8s-api/create-kube-json-api-for-cluster.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 spawnPtyInjectable from "../spawn-pty.injectable";
|
||||||
|
import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable";
|
||||||
|
import appNameInjectable from "../../../common/vars/app-name.injectable";
|
||||||
|
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||||
|
|
||||||
export interface NodeShellSessionArgs {
|
export interface NodeShellSessionArgs {
|
||||||
websocket: WebSocket;
|
websocket: WebSocket;
|
||||||
@ -28,7 +33,12 @@ const openNodeShellSessionInjectable = getInjectable({
|
|||||||
isMac: di.inject(isMacInjectable),
|
isMac: di.inject(isMacInjectable),
|
||||||
isWindows: di.inject(isWindowsInjectable),
|
isWindows: di.inject(isWindowsInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
|
resolvedShell: di.inject(resolvedShellInjectable),
|
||||||
|
appName: di.inject(appNameInjectable),
|
||||||
|
buildVersion: di.inject(buildVersionInjectable),
|
||||||
createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable),
|
createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable),
|
||||||
|
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
|
||||||
|
spawnPty: di.inject(spawnPtyInjectable),
|
||||||
};
|
};
|
||||||
const kubectl = createKubectl(params.cluster.version);
|
const kubectl = createKubectl(params.cluster.version);
|
||||||
const session = new NodeShellSession(dependencies, { kubectl, ...params });
|
const session = new NodeShellSession(dependencies, { kubectl, ...params });
|
||||||
|
|||||||
@ -6,18 +6,18 @@
|
|||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import type { Kubectl } from "../kubectl/kubectl";
|
import type { Kubectl } from "../kubectl/kubectl";
|
||||||
import type WebSocket from "ws";
|
import type WebSocket from "ws";
|
||||||
import { shellEnv } from "../utils/shell-env";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os, { userInfo } from "os";
|
import os, { userInfo } from "os";
|
||||||
import { UserStore } from "../../common/user-store";
|
import type * as pty from "node-pty";
|
||||||
import * as pty from "node-pty";
|
|
||||||
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
||||||
import { stat } from "fs/promises";
|
import { stat } from "fs/promises";
|
||||||
import { getOrInsertWith } from "../../common/utils";
|
import { getOrInsertWith } from "../../common/utils";
|
||||||
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
||||||
import type { Logger } from "../../common/logger";
|
import type { Logger } from "../../common/logger";
|
||||||
|
import type { ComputeShellEnvironment } from "../utils/shell-env/compute-shell-environment.injectable";
|
||||||
|
import type { SpawnPty } from "./spawn-pty.injectable";
|
||||||
|
import type { InitializableState } from "../../common/initializable-state/create";
|
||||||
|
|
||||||
export class ShellOpenError extends Error {
|
export class ShellOpenError extends Error {
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
@ -107,6 +107,11 @@ export interface ShellSessionDependencies {
|
|||||||
readonly isWindows: boolean;
|
readonly isWindows: boolean;
|
||||||
readonly isMac: boolean;
|
readonly isMac: boolean;
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
|
readonly resolvedShell: string | undefined;
|
||||||
|
readonly appName: string;
|
||||||
|
readonly buildVersion: InitializableState<string>;
|
||||||
|
computeShellEnvironment: ComputeShellEnvironment;
|
||||||
|
spawnPty: SpawnPty;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShellSessionArgs {
|
export interface ShellSessionArgs {
|
||||||
@ -148,14 +153,14 @@ export abstract class ShellSession {
|
|||||||
|
|
||||||
protected abstract get cwd(): string | undefined;
|
protected abstract get cwd(): string | undefined;
|
||||||
|
|
||||||
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string | undefined>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
protected ensureShellProcess(shell: string, args: string[], env: Partial<Record<string, string>>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
||||||
const resume = ShellSession.processes.has(this.terminalId);
|
const resume = ShellSession.processes.has(this.terminalId);
|
||||||
const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => (
|
const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => (
|
||||||
pty.spawn(shell, args, {
|
this.dependencies.spawnPty(shell, args, {
|
||||||
rows: 30,
|
rows: 30,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
cwd,
|
cwd,
|
||||||
env: env as Record<string, string>,
|
env,
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
// TODO: Something else is broken here so we need to force the use of winPty on windows
|
// TODO: Something else is broken here so we need to force the use of winPty on windows
|
||||||
useConpty: false,
|
useConpty: false,
|
||||||
@ -331,19 +336,27 @@ export abstract class ShellSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getShellEnv() {
|
protected async getShellEnv() {
|
||||||
const shell = UserStore.getInstance().resolvedShell;
|
const shell = this.dependencies.resolvedShell || userInfo().shell;
|
||||||
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv(shell || userInfo().shell))));
|
const result = await this.dependencies.computeShellEnvironment(shell);
|
||||||
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter);
|
const rawEnv = (() => {
|
||||||
|
if (result.callWasSuccessful) {
|
||||||
|
return result.response ?? process.env;
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.env;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(rawEnv)));
|
||||||
|
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), env.PATH].join(path.delimiter);
|
||||||
|
|
||||||
delete env.DEBUG; // don't pass DEBUG into shells
|
delete env.DEBUG; // don't pass DEBUG into shells
|
||||||
|
|
||||||
if (this.dependencies.isWindows) {
|
if (this.dependencies.isWindows) {
|
||||||
env.SystemRoot = process.env.SystemRoot;
|
|
||||||
env.PTYSHELL = shell || "powershell.exe";
|
env.PTYSHELL = shell || "powershell.exe";
|
||||||
env.PATH = pathStr;
|
env.PATH = pathStr;
|
||||||
env.LENS_SESSION = "true";
|
env.LENS_SESSION = "true";
|
||||||
env.WSLENV = [
|
env.WSLENV = [
|
||||||
process.env.WSLENV,
|
env.WSLENV,
|
||||||
"KUBECONFIG/up:LENS_SESSION/u",
|
"KUBECONFIG/up:LENS_SESSION/u",
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@ -363,8 +376,8 @@ export abstract class ShellSession {
|
|||||||
|
|
||||||
env.PTYPID = process.pid.toString();
|
env.PTYPID = process.pid.toString();
|
||||||
env.KUBECONFIG = await this.kubeconfigPathP;
|
env.KUBECONFIG = await this.kubeconfigPathP;
|
||||||
env.TERM_PROGRAM = app.getName();
|
env.TERM_PROGRAM = this.dependencies.appName;
|
||||||
env.TERM_PROGRAM_VERSION = app.getVersion();
|
env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get();
|
||||||
|
|
||||||
if (this.cluster.preferences.httpsProxy) {
|
if (this.cluster.preferences.httpsProxy) {
|
||||||
env.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
env.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 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 spawnPtyInjectable from "./spawn-pty.injectable";
|
||||||
|
|
||||||
|
export default getGlobalOverride(spawnPtyInjectable, () => () => {
|
||||||
|
throw new Error("Tried to spawn a PTY without an override");
|
||||||
|
});
|
||||||
21
src/main/shell-session/spawn-pty.injectable.ts
Normal file
21
src/main/shell-session/spawn-pty.injectable.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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 { IPty, IPtyForkOptions, IWindowsPtyForkOptions } from "node-pty";
|
||||||
|
import { spawn } from "node-pty";
|
||||||
|
|
||||||
|
export type WindowsSpawnPtyOptions = Omit<IWindowsPtyForkOptions, "env"> & { env?: Partial<Record<string, string>> };
|
||||||
|
export type UnixSpawnPtyOptions = Omit<IPtyForkOptions, "env"> & { env?: Partial<Record<string, string>> };
|
||||||
|
export type SpawnPtyOptions = UnixSpawnPtyOptions | WindowsSpawnPtyOptions;
|
||||||
|
|
||||||
|
export type SpawnPty = (file: string, args: string[], options: SpawnPtyOptions) => IPty;
|
||||||
|
|
||||||
|
const spawnPtyInjectable = getInjectable({
|
||||||
|
id: "spawn-pty",
|
||||||
|
instantiate: () => spawn as SpawnPty,
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default spawnPtyInjectable;
|
||||||
@ -5,11 +5,11 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
|
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
|
||||||
import { shellEnv } from "../../utils/shell-env";
|
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { unionPATHs } from "../../../common/utils/union-env-path";
|
import { unionPATHs } from "../../../common/utils/union-env-path";
|
||||||
import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable";
|
import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable";
|
||||||
import electronAppInjectable from "../../electron-app/electron-app.injectable";
|
import electronAppInjectable from "../../electron-app/electron-app.injectable";
|
||||||
|
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
|
||||||
|
|
||||||
const setupShellInjectable = getInjectable({
|
const setupShellInjectable = getInjectable({
|
||||||
id: "setup-shell",
|
id: "setup-shell",
|
||||||
@ -18,13 +18,24 @@ const setupShellInjectable = getInjectable({
|
|||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
const isSnapPackage = di.inject(isSnapPackageInjectable);
|
const isSnapPackage = di.inject(isSnapPackageInjectable);
|
||||||
const electronApp = di.inject(electronAppInjectable);
|
const electronApp = di.inject(electronAppInjectable);
|
||||||
|
const computeShellEnvironment = di.inject(computeShellEnvironmentInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: "setup-shell",
|
id: "setup-shell",
|
||||||
run: async () => {
|
run: async (): Promise<void> => {
|
||||||
logger.info("🐚 Syncing shell environment");
|
logger.info("🐚 Syncing shell environment");
|
||||||
|
|
||||||
const env = await shellEnv(os.userInfo().shell);
|
const result = await computeShellEnvironment(os.userInfo().shell);
|
||||||
|
|
||||||
|
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) {
|
if (!env.LANG) {
|
||||||
// the LANG env var expects an underscore instead of electron's dash
|
// the LANG env var expects an underscore instead of electron's dash
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const anyKubeconfig = /^kubeconfig$/i;
|
|||||||
* before KUBECONFIG and we only set KUBECONFIG.
|
* before KUBECONFIG and we only set KUBECONFIG.
|
||||||
* @param env The current copy of env
|
* @param env The current copy of env
|
||||||
*/
|
*/
|
||||||
export function clearKubeconfigEnvVars(env: Record<string, any>): Record<string, any> {
|
export function clearKubeconfigEnvVars(env: Partial<Record<string, string>>): Partial<Record<string, string>> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(env)
|
Object.entries(env)
|
||||||
.filter(([key]) => anyKubeconfig.exec(key) === null),
|
.filter(([key]) => anyKubeconfig.exec(key) === null),
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { isWindows } from "../../common/vars";
|
|
||||||
import logger from "../logger";
|
|
||||||
|
|
||||||
export type EnvironmentVariables = Partial<Record<string, string>>;
|
|
||||||
|
|
||||||
|
|
||||||
async function unixShellEnvironment(shell: string): Promise<EnvironmentVariables> {
|
|
||||||
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[];
|
|
||||||
|
|
||||||
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}"'`;
|
|
||||||
|
|
||||||
if (shellName === "tcsh") {
|
|
||||||
shellArgs = ["-ic"];
|
|
||||||
} else {
|
|
||||||
shellArgs = ["-ilc"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const shellProcess = spawn(shell, [...shellArgs, command], {
|
|
||||||
detached: true,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
const stdout: Buffer[] = [];
|
|
||||||
|
|
||||||
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})`));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rawOutput = Buffer.concat(stdout).toString("utf-8");
|
|
||||||
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"];
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(resolvedEnv);
|
|
||||||
} catch(err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let shellSyncFailed = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to get the shell environment per the user's existing startup scripts.
|
|
||||||
* If the environment can't be retrieved after 5 seconds an error message is logged.
|
|
||||||
* Subsequent calls after such a timeout simply log an error message without trying
|
|
||||||
* to get the environment, unless forceRetry is true.
|
|
||||||
* @param shell the shell to get the environment from
|
|
||||||
* @returns object containing the shell's environment variables. An empty object is
|
|
||||||
* returned if the call fails.
|
|
||||||
*/
|
|
||||||
export async function shellEnv(shell: string) : Promise<EnvironmentVariables> {
|
|
||||||
if (isWindows) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shellSyncFailed) {
|
|
||||||
try {
|
|
||||||
return await Promise.race([
|
|
||||||
unixShellEnvironment(shell),
|
|
||||||
new Promise<EnvironmentVariables>((_resolve, reject) => setTimeout(() => {
|
|
||||||
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
|
||||||
}, 30_000)),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`shellEnv: ${error}`);
|
|
||||||
shellSyncFailed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error("shellSync(): Resolving shell environment took too long. Please review your shell configuration.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 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 { disposer } from "../../../common/utils";
|
||||||
|
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 cleanup = disposer();
|
||||||
|
|
||||||
|
const timeoutHandle = setTimeout(() => controller.abort(), 30_000);
|
||||||
|
|
||||||
|
cleanup.push(() => clearTimeout(timeoutHandle));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: await shellEnv,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return {
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "Resolving shell environment is taking very long. Please review your shell configuration.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: String(error),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default computeShellEnvironmentInjectable;
|
||||||
|
|
||||||
@ -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");
|
||||||
|
});
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
interface UnixShellEnvOptions {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComputeUnixShellEnvironment = (shell: string, opts?: UnixShellEnvOptions) => Promise<EnvironmentVariables>;
|
||||||
|
|
||||||
|
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[];
|
||||||
|
|
||||||
|
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}"'`;
|
||||||
|
|
||||||
|
if (shellName === "tcsh") {
|
||||||
|
shellArgs = ["-ic"];
|
||||||
|
} else {
|
||||||
|
shellArgs = ["-ilc"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const shellProcess = spawn(shell, [...shellArgs, command], {
|
||||||
|
detached: true,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
const stdout: Buffer[] = [];
|
||||||
|
|
||||||
|
opts?.signal?.addEventListener("abort", () => shellProcess.kill());
|
||||||
|
|
||||||
|
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})`));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawOutput = Buffer.concat(stdout).toString("utf-8");
|
||||||
|
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"];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(resolvedEnv);
|
||||||
|
} catch(err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default computeUnixShellEnvironmentInjectable;
|
||||||
Loading…
Reference in New Issue
Block a user