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:
parent
11e7a38823
commit
40c81d74f3
@ -22,5 +22,6 @@ exports.default = async function notarizing(context) {
|
|||||||
appPath: `${appOutDir}/${appName}.app`,
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
appleId: process.env.APPLEID,
|
appleId: process.env.APPLEID,
|
||||||
appleIdPassword: process.env.APPLEIDPASS,
|
appleIdPassword: process.env.APPLEIDPASS,
|
||||||
|
ascProvider:process.env.ASCPROVIDER,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "6.1.11",
|
"version": "6.1.12",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2022 OpenLens Authors",
|
"copyright": "© 2022 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
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" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@ interface Dependencies {
|
|||||||
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
|
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
|
||||||
|
|
||||||
createShellSession: (args: {
|
createShellSession: (args: {
|
||||||
webSocket: WebSocket;
|
websocket: WebSocket;
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
nodeName?: string;
|
nodeName?: string;
|
||||||
@ -37,8 +37,8 @@ export const shellApiRequest = ({ createShellSession, authenticateRequest, clust
|
|||||||
|
|
||||||
const ws = new WebSocketServer({ noServer: true });
|
const ws = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
ws.handleUpgrade(req, socket, head, (webSocket) => {
|
ws.handleUpgrade(req, socket, head, (websocket) => {
|
||||||
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
|
const shell = createShellSession({ websocket, cluster, tabId, nodeName });
|
||||||
|
|
||||||
shell.open()
|
shell.open()
|
||||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import localShellSessionInjectable from "./local-shell-session/local-shell-sessi
|
|||||||
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable";
|
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable";
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
webSocket: WebSocket;
|
websocket: WebSocket;
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
nodeName?: string;
|
nodeName?: string;
|
||||||
|
|||||||
@ -8,9 +8,18 @@ import type { Cluster } from "../../../common/cluster/cluster";
|
|||||||
import type WebSocket from "ws";
|
import type WebSocket from "ws";
|
||||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||||
import terminalShellEnvModifiersInjectable from "../shell-env-modifier/terminal-shell-env-modify.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 {
|
interface InstantiationParameter {
|
||||||
webSocket: WebSocket;
|
websocket: WebSocket;
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
}
|
}
|
||||||
@ -18,13 +27,26 @@ interface InstantiationParameter {
|
|||||||
const localShellSessionInjectable = getInjectable({
|
const localShellSessionInjectable = getInjectable({
|
||||||
id: "local-shell-session",
|
id: "local-shell-session",
|
||||||
|
|
||||||
instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => {
|
instantiate: (di, { cluster, tabId, websocket }: InstantiationParameter) => {
|
||||||
const createKubectl = di.inject(createKubectlInjectable);
|
const createKubectl = di.inject(createKubectlInjectable);
|
||||||
const localShellEnvModify = di.inject(terminalShellEnvModifiersInjectable);
|
|
||||||
|
|
||||||
const kubectl = createKubectl(cluster.version);
|
return new LocalShellSession({
|
||||||
|
modifyTerminalShellEnv: di.inject(terminalShellEnvModifiersInjectable),
|
||||||
return new LocalShellSession(localShellEnvModify, kubectl, webSocket, cluster, tabId);
|
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,
|
lifecycle: lifecycleEnum.transient,
|
||||||
|
|||||||
@ -3,20 +3,23 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import type { UserStore } from "../../../common/user-store";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
|
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
|
||||||
import { ShellSession } from "../shell-session";
|
import { ShellSession } from "../shell-session";
|
||||||
import type { Kubectl } from "../../kubectl/kubectl";
|
|
||||||
import { baseBinariesDir } from "../../../common/vars";
|
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 {
|
export class LocalShellSession extends ShellSession {
|
||||||
ShellType = "shell";
|
ShellType = "shell";
|
||||||
|
|
||||||
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
|
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
|
||||||
super(kubectl, websocket, cluster, terminalId);
|
super(dependencies, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPathEntries(): string[] {
|
protected getPathEntries(): string[] {
|
||||||
@ -28,11 +31,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.shellEnvModify(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) {
|
||||||
@ -45,8 +45,8 @@ export class LocalShellSession extends ShellSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getShellArgs(shell: string): Promise<string[]> {
|
protected async getShellArgs(shell: string): Promise<string[]> {
|
||||||
const pathFromPreferences = UserStore.getInstance().kubectlBinariesPath || this.kubectl.getBundledPath();
|
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
|
||||||
const kubectlPathDir = UserStore.getInstance().downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
|
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
|
||||||
|
|
||||||
switch(path.basename(shell)) {
|
switch(path.basename(shell)) {
|
||||||
case "powershell.exe":
|
case "powershell.exe":
|
||||||
|
|||||||
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 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,9 +7,17 @@ import type { Cluster } from "../../../common/cluster/cluster";
|
|||||||
import type WebSocket from "ws";
|
import type WebSocket from "ws";
|
||||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||||
import { NodeShellSession } from "./node-shell-session";
|
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 {
|
interface InstantiationParameter {
|
||||||
webSocket: WebSocket;
|
websocket: WebSocket;
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
@ -18,12 +26,25 @@ interface InstantiationParameter {
|
|||||||
const nodeShellSessionInjectable = getInjectable({
|
const nodeShellSessionInjectable = getInjectable({
|
||||||
id: "node-shell-session",
|
id: "node-shell-session",
|
||||||
|
|
||||||
instantiate: (di, { cluster, tabId, webSocket, nodeName }: InstantiationParameter) => {
|
instantiate: (di, { cluster, tabId, websocket, nodeName }: InstantiationParameter) => {
|
||||||
const createKubectl = di.inject(createKubectlInjectable);
|
const createKubectl = di.inject(createKubectlInjectable);
|
||||||
|
|
||||||
const kubectl = createKubectl(cluster.version);
|
return new NodeShellSession({
|
||||||
|
appName: di.inject(appNameInjectable),
|
||||||
return new NodeShellSession(nodeName, kubectl, webSocket, cluster, tabId);
|
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,
|
lifecycle: lifecycleEnum.transient,
|
||||||
|
|||||||
@ -3,28 +3,32 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
||||||
import type { KubeConfig } 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 { ShellOpenError, ShellSession } from "../shell-session";
|
||||||
import { get, once } from "lodash";
|
import { get, once } from "lodash";
|
||||||
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
|
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
|
||||||
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
|
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
import type { Kubectl } from "../../kubectl/kubectl";
|
|
||||||
import { TerminalChannels } from "../../../common/terminal/channels";
|
import { TerminalChannels } from "../../../common/terminal/channels";
|
||||||
|
|
||||||
|
interface NodeShellArgs extends ShellSessionArgs {
|
||||||
|
nodeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class NodeShellSession extends ShellSession {
|
export class NodeShellSession extends ShellSession {
|
||||||
ShellType = "node-shell";
|
ShellType = "node-shell";
|
||||||
|
|
||||||
protected readonly podName = `node-shell-${uuid()}`;
|
protected readonly podName = `node-shell-${uuid()}`;
|
||||||
|
|
||||||
protected readonly cwd: string | undefined = undefined;
|
protected readonly cwd: string | undefined = undefined;
|
||||||
|
protected readonly nodeName: string;
|
||||||
|
|
||||||
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
|
constructor(deps: ShellSessionDependencies, args: NodeShellArgs) {
|
||||||
super(kubectl, socket, cluster, terminalId);
|
super(deps, args);
|
||||||
|
this.nodeName = args.nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
|
|||||||
@ -6,19 +6,19 @@
|
|||||||
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 { isMac, isWindows } from "../../common/vars";
|
import type * as pty from "node-pty";
|
||||||
import { UserStore } from "../../common/user-store";
|
|
||||||
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 logger from "../logger";
|
import logger from "../logger";
|
||||||
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 { 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) {
|
||||||
@ -104,6 +104,24 @@ export enum WebSocketCloseEvent {
|
|||||||
TlsHandshake = 1015,
|
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 {
|
export abstract class ShellSession {
|
||||||
abstract readonly ShellType: string;
|
abstract readonly ShellType: string;
|
||||||
|
|
||||||
@ -130,17 +148,20 @@ export abstract class ShellSession {
|
|||||||
protected readonly kubectlBinDirP: Promise<string>;
|
protected readonly kubectlBinDirP: Promise<string>;
|
||||||
protected readonly kubeconfigPathP: Promise<string>;
|
protected readonly kubeconfigPathP: Promise<string>;
|
||||||
protected readonly terminalId: string;
|
protected readonly terminalId: string;
|
||||||
|
protected readonly kubectl: Kubectl;
|
||||||
|
protected readonly websocket: WebSocket;
|
||||||
|
protected readonly cluster: Cluster;
|
||||||
|
|
||||||
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,
|
||||||
@ -152,10 +173,13 @@ export abstract class ShellSession {
|
|||||||
return { shellProcess, resume };
|
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.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||||
this.kubectlBinDirP = this.kubectl.binDir();
|
this.kubectlBinDirP = this.kubectl.binDir();
|
||||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
this.terminalId = `${this.cluster.id}:${args.tabId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected send(message: TerminalMessage): void {
|
protected send(message: TerminalMessage): void {
|
||||||
@ -165,7 +189,7 @@ export abstract class ShellSession {
|
|||||||
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
|
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
|
||||||
const cwdOptions = [this.cwd];
|
const cwdOptions = [this.cwd];
|
||||||
|
|
||||||
if (isWindows) {
|
if (this.dependencies.isWindows) {
|
||||||
cwdOptions.push(
|
cwdOptions.push(
|
||||||
env.USERPROFILE,
|
env.USERPROFILE,
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
@ -177,7 +201,7 @@ export abstract class ShellSession {
|
|||||||
os.homedir(),
|
os.homedir(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMac) {
|
if (this.dependencies.isMac) {
|
||||||
cwdOptions.push("/Users");
|
cwdOptions.push("/Users");
|
||||||
} else {
|
} else {
|
||||||
cwdOptions.push("/home");
|
cwdOptions.push("/home");
|
||||||
@ -313,19 +337,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 (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)
|
||||||
@ -345,8 +377,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,12 +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 {
|
||||||
run: async () => {
|
id: "setup-shell",
|
||||||
|
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