1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Release 6.1.12 (#6408)

* Release 6.1.12

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

* Adding asc provider (#6302)

* 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>

* Fix lint

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

* Fix lints

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

* Fix build issue

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

* Fix test invocation

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

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Co-authored-by: Billy Tobon <billy.tobon@gmail.com>
This commit is contained in:
Sebastian Malton 2022-10-13 09:56:49 -04:00 committed by GitHub
parent 11e7a38823
commit 40c81d74f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 452 additions and 178 deletions

View File

@ -22,5 +22,6 @@ exports.default = async function notarizing(context) {
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,
ascProvider:process.env.ASCPROVIDER,
});
};

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "6.1.11",
"version": "6.1.12",
"main": "static/build/main.js",
"copyright": "© 2022 OpenLens Authors",
"license": "MIT",

View 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;

View File

@ -7,14 +7,14 @@ import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
describe("clearKubeconfigEnvVars tests", () => {
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", () => {
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", () => {
expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: 1 });
expect(clearKubeconfigEnvVars({ a: "22", kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: "22" });
});
});

View File

@ -16,7 +16,7 @@ interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
createShellSession: (args: {
webSocket: WebSocket;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;
@ -37,8 +37,8 @@ export const shellApiRequest = ({ createShellSession, authenticateRequest, clust
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (webSocket) => {
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
ws.handleUpgrade(req, socket, head, (websocket) => {
const shell = createShellSession({ websocket, cluster, tabId, nodeName });
shell.open()
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));

View File

@ -9,7 +9,7 @@ import localShellSessionInjectable from "./local-shell-session/local-shell-sessi
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable";
interface Args {
webSocket: WebSocket;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;

View File

@ -8,9 +8,18 @@ import type { Cluster } from "../../../common/cluster/cluster";
import type WebSocket from "ws";
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import terminalShellEnvModifiersInjectable from "../shell-env-modifier/terminal-shell-env-modify.injectable";
import appNameInjectable from "../../../common/vars/app-name.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable";
import spawnPtyInjectable from "../spawn-pty.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
interface InstantiationParameter {
webSocket: WebSocket;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
@ -18,13 +27,26 @@ interface InstantiationParameter {
const localShellSessionInjectable = getInjectable({
id: "local-shell-session",
instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => {
instantiate: (di, { cluster, tabId, websocket }: InstantiationParameter) => {
const createKubectl = di.inject(createKubectlInjectable);
const localShellEnvModify = di.inject(terminalShellEnvModifiersInjectable);
const kubectl = createKubectl(cluster.version);
return new LocalShellSession(localShellEnvModify, kubectl, webSocket, cluster, tabId);
return new LocalShellSession({
modifyTerminalShellEnv: di.inject(terminalShellEnvModifiersInjectable),
appName: di.inject(appNameInjectable),
buildVersion: di.inject(buildVersionInjectable),
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
resolvedShell: di.inject(resolvedShellInjectable),
spawnPty: di.inject(spawnPtyInjectable),
userStore: di.inject(userStoreInjectable),
}, {
kubectl: createKubectl(cluster.version),
websocket,
cluster,
tabId,
});
},
lifecycle: lifecycleEnum.transient,

View File

@ -3,20 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import path from "path";
import { UserStore } from "../../../common/user-store";
import type { Cluster } from "../../../common/cluster/cluster";
import type { UserStore } from "../../../common/user-store";
import type { ClusterId } from "../../../common/cluster-types";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellSession } from "../shell-session";
import type { Kubectl } from "../../kubectl/kubectl";
import { baseBinariesDir } from "../../../common/vars";
interface LocalShellSessionDependencies extends ShellSessionDependencies {
readonly userStore: UserStore;
modifyTerminalShellEnv: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>;
}
export class LocalShellSession extends ShellSession {
ShellType = "shell";
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, websocket, cluster, terminalId);
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
super(dependencies, args);
}
protected getPathEntries(): string[] {
@ -28,11 +31,8 @@ export class LocalShellSession extends ShellSession {
}
public async open() {
let env = await this.getCachedShellEnv();
// extensions can modify the env
env = this.shellEnvModify(this.cluster.id, env);
const env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, await this.getCachedShellEnv());
const shell = env.PTYSHELL;
if (!shell) {
@ -45,8 +45,8 @@ export class LocalShellSession extends ShellSession {
}
protected async getShellArgs(shell: string): Promise<string[]> {
const pathFromPreferences = UserStore.getInstance().kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = UserStore.getInstance().downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
switch(path.basename(shell)) {
case "powershell.exe":

View 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 resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable";
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 { SpawnPty } from "../spawn-pty.injectable";
import spawnPtyInjectable from "../spawn-pty.injectable";
import localShellSessionInjectable from "./local-shell-session.injectable";
describe("technical unit tests for local shell sessions", () => {
let di: DiContainer;
beforeEach(() => {
di = getDiForUnitTesting({
doGeneralOverrides: true,
});
di.override(resolvedShellInjectable, () => "powershell.exe");
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(buildVersionInjectable, () => ({
get: () => "1.1.1",
}));
});
describe("when on windows", () => {
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);
});
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(),
};
});
const session = di.inject(localShellSessionInjectable, {
cluster: {
getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig",
preferences: {},
} as Partial<Cluster> as Cluster,
tabId: "my-tab-id",
websocket: new WebSocket(null),
});
await session.open();
});
});
});
});

View File

@ -7,9 +7,17 @@ import type { Cluster } from "../../../common/cluster/cluster";
import type WebSocket from "ws";
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import { NodeShellSession } from "./node-shell-session";
import loggerInjectable from "../../../common/logger.injectable";
import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable";
import appNameInjectable from "../../../common/vars/app-name.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import spawnPtyInjectable from "../spawn-pty.injectable";
interface InstantiationParameter {
webSocket: WebSocket;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName: string;
@ -18,12 +26,25 @@ interface InstantiationParameter {
const nodeShellSessionInjectable = getInjectable({
id: "node-shell-session",
instantiate: (di, { cluster, tabId, webSocket, nodeName }: InstantiationParameter) => {
instantiate: (di, { cluster, tabId, websocket, nodeName }: InstantiationParameter) => {
const createKubectl = di.inject(createKubectlInjectable);
const kubectl = createKubectl(cluster.version);
return new NodeShellSession(nodeName, kubectl, webSocket, cluster, tabId);
return new NodeShellSession({
appName: di.inject(appNameInjectable),
buildVersion: di.inject(buildVersionInjectable),
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
resolvedShell: di.inject(resolvedShellInjectable),
spawnPty: di.inject(spawnPtyInjectable),
}, {
nodeName,
kubectl: createKubectl(cluster.version),
websocket,
cluster,
tabId,
});
},
lifecycle: lifecycleEnum.transient,

View File

@ -3,28 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import { v4 as uuid } from "uuid";
import { Watch, CoreV1Api } from "@kubernetes/client-node";
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellOpenError, ShellSession } from "../shell-session";
import { get, once } from "lodash";
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
import logger from "../../logger";
import type { Kubectl } from "../../kubectl/kubectl";
import { TerminalChannels } from "../../../common/terminal/channels";
interface NodeShellArgs extends ShellSessionArgs {
nodeName: string;
}
export class NodeShellSession extends ShellSession {
ShellType = "node-shell";
protected readonly podName = `node-shell-${uuid()}`;
protected readonly cwd: string | undefined = undefined;
protected readonly nodeName: string;
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, socket, cluster, terminalId);
constructor(deps: ShellSessionDependencies, args: NodeShellArgs) {
super(deps, args);
this.nodeName = args.nodeName;
}
public async open() {

View File

@ -6,19 +6,19 @@
import type { Cluster } from "../../common/cluster/cluster";
import type { Kubectl } from "../kubectl/kubectl";
import type WebSocket from "ws";
import { shellEnv } from "../utils/shell-env";
import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path";
import os, { userInfo } from "os";
import { isMac, isWindows } from "../../common/vars";
import { UserStore } from "../../common/user-store";
import * as pty from "node-pty";
import type * as pty from "node-pty";
import { appEventBus } from "../../common/app-event-bus/event-bus";
import logger from "../logger";
import { stat } from "fs/promises";
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 { SpawnPty } from "./spawn-pty.injectable";
import type { InitializableState } from "../../common/initializable-state/create";
export class ShellOpenError extends Error {
constructor(message: string, options?: ErrorOptions) {
@ -104,6 +104,24 @@ export enum WebSocketCloseEvent {
TlsHandshake = 1015,
}
export interface ShellSessionDependencies {
readonly isWindows: boolean;
readonly isMac: boolean;
readonly logger: Logger;
readonly resolvedShell: string | undefined;
readonly appName: string;
readonly buildVersion: InitializableState<string>;
computeShellEnvironment: ComputeShellEnvironment;
spawnPty: SpawnPty;
}
export interface ShellSessionArgs {
kubectl: Kubectl;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
export abstract class ShellSession {
abstract readonly ShellType: string;
@ -130,17 +148,20 @@ export abstract class ShellSession {
protected readonly kubectlBinDirP: Promise<string>;
protected readonly kubeconfigPathP: Promise<string>;
protected readonly terminalId: string;
protected readonly kubectl: Kubectl;
protected readonly websocket: WebSocket;
protected readonly cluster: Cluster;
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 shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => (
pty.spawn(shell, args, {
this.dependencies.spawnPty(shell, args, {
rows: 30,
cols: 80,
cwd,
env: env as Record<string, string>,
env,
name: "xterm-256color",
// TODO: Something else is broken here so we need to force the use of winPty on windows
useConpty: false,
@ -152,10 +173,13 @@ export abstract class ShellSession {
return { shellProcess, resume };
}
constructor(protected readonly kubectl: Kubectl, protected readonly websocket: WebSocket, protected readonly cluster: Cluster, terminalId: string) {
constructor(protected readonly dependencies: ShellSessionDependencies, args: ShellSessionArgs) {
this.cluster = args.cluster;
this.kubectl = args.kubectl;
this.websocket = args.websocket;
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
this.kubectlBinDirP = this.kubectl.binDir();
this.terminalId = `${cluster.id}:${terminalId}`;
this.terminalId = `${this.cluster.id}:${args.tabId}`;
}
protected send(message: TerminalMessage): void {
@ -165,7 +189,7 @@ export abstract class ShellSession {
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
const cwdOptions = [this.cwd];
if (isWindows) {
if (this.dependencies.isWindows) {
cwdOptions.push(
env.USERPROFILE,
os.homedir(),
@ -177,7 +201,7 @@ export abstract class ShellSession {
os.homedir(),
);
if (isMac) {
if (this.dependencies.isMac) {
cwdOptions.push("/Users");
} else {
cwdOptions.push("/home");
@ -313,19 +337,27 @@ export abstract class ShellSession {
}
protected async getShellEnv() {
const shell = UserStore.getInstance().resolvedShell;
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv(shell || userInfo().shell))));
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter);
const shell = this.dependencies.resolvedShell || userInfo().shell;
const result = await this.dependencies.computeShellEnvironment(shell);
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
if (isWindows) {
env.SystemRoot = process.env.SystemRoot;
if (this.dependencies.isWindows) {
env.PTYSHELL = shell || "powershell.exe";
env.PATH = pathStr;
env.LENS_SESSION = "true";
env.WSLENV = [
process.env.WSLENV,
env.WSLENV,
"KUBECONFIG/up:LENS_SESSION/u",
]
.filter(Boolean)
@ -345,8 +377,8 @@ export abstract class ShellSession {
env.PTYPID = process.pid.toString();
env.KUBECONFIG = await this.kubeconfigPathP;
env.TERM_PROGRAM = app.getName();
env.TERM_PROGRAM_VERSION = app.getVersion();
env.TERM_PROGRAM = this.dependencies.appName;
env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get();
if (this.cluster.preferences.httpsProxy) {
env.HTTPS_PROXY = this.cluster.preferences.httpsProxy;

View File

@ -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");
});

View 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;

View File

@ -5,11 +5,11 @@
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
import { shellEnv } from "../../utils/shell-env";
import os from "os";
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";
const setupShellInjectable = getInjectable({
id: "setup-shell",
@ -18,12 +18,24 @@ const setupShellInjectable = getInjectable({
const logger = di.inject(loggerInjectable);
const isSnapPackage = di.inject(isSnapPackageInjectable);
const electronApp = di.inject(electronAppInjectable);
const computeShellEnvironment = di.inject(computeShellEnvironmentInjectable);
return {
run: async () => {
id: "setup-shell",
run: async (): Promise<void> => {
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) {
// the LANG env var expects an underscore instead of electron's dash

View File

@ -13,7 +13,7 @@ const anyKubeconfig = /^kubeconfig$/i;
* before KUBECONFIG and we only set KUBECONFIG.
* @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(
Object.entries(env)
.filter(([key]) => anyKubeconfig.exec(key) === null),

View File

@ -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 {};
}

View File

@ -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;

View File

@ -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");
});

View File

@ -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;