mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { Cluster } from "../../common/cluster/cluster";
|
||||
import type WebSocket from "ws";
|
||||
import openLocalShellSessionInjectable from "./local-shell-session/open.injectable";
|
||||
import openNodeShellSessionInjectable from "./node-shell-session/open.injectable";
|
||||
|
||||
export interface OpenShellSessionArgs {
|
||||
websocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
|
||||
|
||||
const openShellSessionInjectable = getInjectable({
|
||||
id: "open-shell-session",
|
||||
|
||||
instantiate: (di): OpenShellSession => {
|
||||
const openLocalShellSession = di.inject(openLocalShellSessionInjectable);
|
||||
const openNodeShellSession = di.inject(openNodeShellSessionInjectable);
|
||||
|
||||
return ({ nodeName, ...args }) => (
|
||||
nodeName
|
||||
? openNodeShellSession({ nodeName, ...args })
|
||||
: openLocalShellSession(args)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default openShellSessionInjectable;
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./node-shell-session/node-shell-session";
|
||||
export * from "./local-shell-session/local-shell-session";
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
|
||||
import { ShellSession } from "../shell-session";
|
||||
import type { ModifyTerminalShellEnv } from "../shell-env-modifier/modify-terminal-shell-env.injectable";
|
||||
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
|
||||
import type { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable";
|
||||
import type { GetBasenameOfPath } from "../../../common/path/get-basename.injectable";
|
||||
|
||||
export interface LocalShellSessionDependencies extends ShellSessionDependencies {
|
||||
readonly directoryForBinaries: string;
|
||||
readonly userStore: UserStore;
|
||||
modifyTerminalShellEnv: ModifyTerminalShellEnv;
|
||||
joinPaths: JoinPaths;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
getBasenameOfPath: GetBasenameOfPath;
|
||||
}
|
||||
|
||||
export class LocalShellSession extends ShellSession {
|
||||
ShellType = "shell";
|
||||
|
||||
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
|
||||
super(dependencies, args);
|
||||
}
|
||||
|
||||
protected getPathEntries(): string[] {
|
||||
return [this.dependencies.directoryForBinaries];
|
||||
}
|
||||
|
||||
protected get cwd(): string | undefined {
|
||||
return this.cluster.preferences?.terminalCWD;
|
||||
}
|
||||
|
||||
public async open() {
|
||||
// extensions can modify the env
|
||||
const env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, await this.getCachedShellEnv());
|
||||
const shell = env.PTYSHELL;
|
||||
|
||||
if (!shell) {
|
||||
throw new Error("PTYSHELL is not defined with the environment");
|
||||
}
|
||||
|
||||
const args = await this.getShellArgs(shell);
|
||||
|
||||
await this.openShellProcess(shell, args, env);
|
||||
}
|
||||
|
||||
protected async getShellArgs(shell: string): Promise<string[]> {
|
||||
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
|
||||
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries
|
||||
? await this.kubectlBinDirP
|
||||
: this.dependencies.getDirnameOfPath(pathFromPreferences);
|
||||
|
||||
switch(this.dependencies.getBasenameOfPath(shell)) {
|
||||
case "powershell.exe":
|
||||
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`];
|
||||
case "bash":
|
||||
return ["--init-file", this.dependencies.joinPaths(await this.kubectlBinDirP, ".bash_set_path")];
|
||||
case "fish":
|
||||
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
||||
case "zsh":
|
||||
return ["--login"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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 { LocalShellSessionDependencies } from "./local-shell-session";
|
||||
import { LocalShellSession } from "./local-shell-session";
|
||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||
import modifyTerminalShellEnvInjectable from "../shell-env-modifier/modify-terminal-shell-env.injectable";
|
||||
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
|
||||
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import type WebSocket from "ws";
|
||||
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import computeShellEnvironmentInjectable from "../../../features/shell-sync/main/compute-shell-environment.injectable";
|
||||
import spawnPtyInjectable from "../spawn-pty.injectable";
|
||||
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
|
||||
import appNameInjectable from "../../../common/vars/app-name.injectable";
|
||||
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import statInjectable from "../../../common/fs/stat.injectable";
|
||||
|
||||
export interface OpenLocalShellSessionArgs {
|
||||
websocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export type OpenLocalShellSession = (args: OpenLocalShellSessionArgs) => Promise<void>;
|
||||
|
||||
const openLocalShellSessionInjectable = getInjectable({
|
||||
id: "open-local-shell-session",
|
||||
|
||||
instantiate: (di): OpenLocalShellSession => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
const dependencies: LocalShellSessionDependencies = {
|
||||
directoryForBinaries: di.inject(directoryForBinariesInjectable),
|
||||
isMac: di.inject(isMacInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
userShellSetting: di.inject(userShellSettingInjectable),
|
||||
appName: di.inject(appNameInjectable),
|
||||
buildVersion: di.inject(buildVersionInjectable),
|
||||
modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
|
||||
spawnPty: di.inject(spawnPtyInjectable),
|
||||
stat: di.inject(statInjectable),
|
||||
};
|
||||
|
||||
return (args) => {
|
||||
const kubectl = createKubectl(args.cluster.version);
|
||||
const session = new LocalShellSession(dependencies, { kubectl, ...args });
|
||||
|
||||
return session.open();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default openLocalShellSessionInjectable;
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type 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 pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
|
||||
import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
|
||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
||||
import statInjectable from "../../../common/fs/stat.injectable";
|
||||
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.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 { 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",
|
||||
}));
|
||||
di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); });
|
||||
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
|
||||
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
di.override(statInjectable, () => () => { throw new Error("tried call stat without override"); });
|
||||
});
|
||||
|
||||
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(),
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
const websocket = {
|
||||
on: jest.fn(() => websocket),
|
||||
once: jest.fn(() => websocket),
|
||||
} as Partial<WebSocket> as WebSocket;
|
||||
|
||||
await openLocalShellSession({
|
||||
cluster: {
|
||||
getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig",
|
||||
preferences: {},
|
||||
} as Partial<Cluster> as Cluster,
|
||||
tabId: "my-tab-id",
|
||||
websocket,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
|
||||
import { ShellOpenError, ShellSession } from "../shell-session";
|
||||
import { get, once } from "lodash";
|
||||
import { NodeApi } from "../../../common/k8s-api/endpoints";
|
||||
import { TerminalChannels } from "../../../common/terminal/channels";
|
||||
import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
|
||||
import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable";
|
||||
|
||||
export interface NodeShellSessionArgs extends ShellSessionArgs {
|
||||
nodeName: string;
|
||||
}
|
||||
|
||||
export interface NodeShellSessionDependencies extends ShellSessionDependencies {
|
||||
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
|
||||
createKubeApi: CreateKubeApi;
|
||||
}
|
||||
|
||||
export class NodeShellSession extends ShellSession {
|
||||
ShellType = "node-shell";
|
||||
|
||||
protected readonly podName = `node-shell-${uuid()}`;
|
||||
protected readonly nodeName: string;
|
||||
protected readonly cwd: string | undefined = undefined;
|
||||
|
||||
constructor(protected readonly dependencies: NodeShellSessionDependencies, { nodeName, ...args }: NodeShellSessionArgs) {
|
||||
super(dependencies, args);
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
public async open() {
|
||||
const kc = await this.cluster.getProxyKubeconfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const shell = await this.kubectl.getPath();
|
||||
|
||||
const cleanup = once(() => {
|
||||
coreApi
|
||||
.deleteNamespacedPod(this.podName, "kube-system")
|
||||
.catch(error => this.dependencies.logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
|
||||
});
|
||||
|
||||
this.websocket.once("close", cleanup);
|
||||
|
||||
try {
|
||||
await this.createNodeShellPod(coreApi);
|
||||
await this.waitForRunningPod(kc);
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
|
||||
this.send({
|
||||
type: TerminalChannels.STDOUT,
|
||||
data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`,
|
||||
});
|
||||
|
||||
throw new ShellOpenError(
|
||||
"failed to create node pod",
|
||||
error instanceof Error
|
||||
? { cause: error }
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const env = await this.getCachedShellEnv();
|
||||
const args = ["exec", "-i", "-t", "-n", "kube-system", this.podName, "--"];
|
||||
const nodeApi = this.dependencies.createKubeApi(NodeApi, {
|
||||
request: this.dependencies.createKubeJsonApiForCluster(this.cluster.id),
|
||||
});
|
||||
const node = await nodeApi.get({ name: this.nodeName });
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No node with name=${this.nodeName} found`);
|
||||
}
|
||||
|
||||
const nodeOs = node.getOperatingSystem();
|
||||
|
||||
switch (nodeOs) {
|
||||
default:
|
||||
this.dependencies.logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
|
||||
// fallthrough
|
||||
case "linux":
|
||||
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
|
||||
break;
|
||||
case "windows":
|
||||
args.push("powershell");
|
||||
break;
|
||||
}
|
||||
|
||||
await this.openShellProcess(shell, args, env);
|
||||
}
|
||||
|
||||
protected createNodeShellPod(coreApi: CoreV1Api) {
|
||||
const imagePullSecrets = this.cluster.imagePullSecret
|
||||
? [{
|
||||
name: this.cluster.imagePullSecret,
|
||||
}]
|
||||
: undefined;
|
||||
|
||||
return coreApi
|
||||
.createNamespacedPod("kube-system", {
|
||||
metadata: {
|
||||
name: this.podName,
|
||||
namespace: "kube-system",
|
||||
},
|
||||
spec: {
|
||||
nodeName: this.nodeName,
|
||||
restartPolicy: "Never",
|
||||
terminationGracePeriodSeconds: 0,
|
||||
hostPID: true,
|
||||
hostIPC: true,
|
||||
hostNetwork: true,
|
||||
tolerations: [{
|
||||
operator: "Exists",
|
||||
}],
|
||||
priorityClassName: "system-node-critical",
|
||||
containers: [{
|
||||
name: "shell",
|
||||
image: this.cluster.nodeShellImage,
|
||||
securityContext: {
|
||||
privileged: true,
|
||||
},
|
||||
command: ["nsenter"],
|
||||
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"],
|
||||
}],
|
||||
imagePullSecrets,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected waitForRunningPod(kc: KubeConfig): Promise<void> {
|
||||
this.dependencies.logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
new Watch(kc)
|
||||
.watch(`/api/v1/namespaces/kube-system/pods`,
|
||||
{},
|
||||
// callback is called for each received object.
|
||||
(type, { metadata: { name }, status }) => {
|
||||
if (name === this.podName) {
|
||||
switch (status.phase) {
|
||||
case "Running":
|
||||
return resolve();
|
||||
case "Failed":
|
||||
return reject(`Failed to be created: ${status.message || "unknown error"}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
// done callback is called if the watch terminates normally
|
||||
(err) => {
|
||||
this.dependencies.logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
|
||||
reject(err);
|
||||
},
|
||||
)
|
||||
.then(req => {
|
||||
setTimeout(() => {
|
||||
this.dependencies.logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
|
||||
req.abort();
|
||||
reject("Pod creation timed out");
|
||||
}, 2 * 60 * 1000); // 2 * 60 * 1000
|
||||
})
|
||||
.catch(error => {
|
||||
this.dependencies.logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { Cluster } from "../../../common/cluster/cluster";
|
||||
import type WebSocket from "ws";
|
||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
||||
import type { NodeShellSessionDependencies } from "./node-shell-session";
|
||||
import { NodeShellSession } from "./node-shell-session";
|
||||
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import createKubeJsonApiForClusterInjectable from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
|
||||
import computeShellEnvironmentInjectable from "../../../features/shell-sync/main/compute-shell-environment.injectable";
|
||||
import spawnPtyInjectable from "../spawn-pty.injectable";
|
||||
import userShellSettingInjectable from "../../../common/user-store/shell-setting.injectable";
|
||||
import appNameInjectable from "../../../common/vars/app-name.injectable";
|
||||
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import statInjectable from "../../../common/fs/stat.injectable";
|
||||
import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable";
|
||||
|
||||
export interface NodeShellSessionArgs {
|
||||
websocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
nodeName: string;
|
||||
}
|
||||
|
||||
export type OpenNodeShellSession = (args: NodeShellSessionArgs) => Promise<void>;
|
||||
|
||||
const openNodeShellSessionInjectable = getInjectable({
|
||||
id: "open-node-shell-session",
|
||||
instantiate: (di): OpenNodeShellSession => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
const dependencies: NodeShellSessionDependencies = {
|
||||
isMac: di.inject(isMacInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
userShellSetting: di.inject(userShellSettingInjectable),
|
||||
appName: di.inject(appNameInjectable),
|
||||
buildVersion: di.inject(buildVersionInjectable),
|
||||
createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable),
|
||||
computeShellEnvironment: di.inject(computeShellEnvironmentInjectable),
|
||||
spawnPty: di.inject(spawnPtyInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
stat: di.inject(statInjectable),
|
||||
createKubeApi: di.inject(createKubeApiInjectable),
|
||||
};
|
||||
|
||||
return async (args) => {
|
||||
const kubectl = createKubectl(args.cluster.version);
|
||||
const session = new NodeShellSession(dependencies, { kubectl, ...args });
|
||||
|
||||
return session.open();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default openNodeShellSessionInjectable;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 { computed } from "mobx";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { isDefined } from "../../../common/utils";
|
||||
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
|
||||
|
||||
export type ModifyTerminalShellEnv = (clusterId: ClusterId, env: Partial<Record<string, string>>) => Partial<Record<string, string>>;
|
||||
|
||||
const modifyTerminalShellEnvInjectable = getInjectable({
|
||||
id: "terminal-shell-env-modify",
|
||||
|
||||
instantiate: (di): ModifyTerminalShellEnv => {
|
||||
const extensions = di.inject(mainExtensionsInjectable);
|
||||
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
const terminalShellEnvModifiers = computed(() => (
|
||||
extensions.get()
|
||||
.map((extension) => extension.terminalShellEnvModifier)
|
||||
.filter(isDefined)
|
||||
));
|
||||
|
||||
return (clusterId, env) => {
|
||||
const modifiers = terminalShellEnvModifiers.get();
|
||||
|
||||
if (modifiers.length === 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
const entity = catalogEntityRegistry.findById(clusterId);
|
||||
|
||||
if (entity) {
|
||||
const ctx = { catalogEntity: entity };
|
||||
|
||||
// clone it so the passed value is not also modified
|
||||
env = JSON.parse(JSON.stringify(env));
|
||||
env = modifiers.reduce((env, modifier) => modifier(ctx, env), env);
|
||||
}
|
||||
|
||||
return env;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default modifyTerminalShellEnvInjectable;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
|
||||
export interface ShellEnvContext {
|
||||
catalogEntity: CatalogEntity;
|
||||
}
|
||||
|
||||
export type ShellEnvModifier = (ctx: ShellEnvContext, env: Record<string, string | undefined>) => Record<string, string | undefined>;
|
||||
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { Kubectl } from "../kubectl/kubectl";
|
||||
import type WebSocket from "ws";
|
||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import type * as pty from "node-pty";
|
||||
import { getOrInsertWith } from "../../common/utils";
|
||||
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { ComputeShellEnvironment } from "../../features/shell-sync/main/compute-shell-environment.injectable";
|
||||
import type { SpawnPty } from "./spawn-pty.injectable";
|
||||
import type { InitializableState } from "../../common/initializable-state/create";
|
||||
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
|
||||
import type { Stat } from "../../common/fs/stat.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export class ShellOpenError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(`${message}`, options);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this);
|
||||
}
|
||||
}
|
||||
|
||||
export enum WebSocketCloseEvent {
|
||||
/**
|
||||
* The connection successfully completed the purpose for which it was created.
|
||||
*/
|
||||
NormalClosure = 1000,
|
||||
/**
|
||||
* The endpoint is going away, either because of a server failure or because
|
||||
* the browser is navigating away from the page that opened the connection.
|
||||
*/
|
||||
GoingAway = 1001,
|
||||
/**
|
||||
* The endpoint is terminating the connection due to a protocol error.
|
||||
*/
|
||||
ProtocolError = 1002,
|
||||
/**
|
||||
* The connection is being terminated because the endpoint received data of a
|
||||
* type it cannot accept. (For example, a text-only endpoint received binary
|
||||
* data.)
|
||||
*/
|
||||
UnsupportedData = 1003,
|
||||
/**
|
||||
* Indicates that no status code was provided even though one was expected.
|
||||
*/
|
||||
NoStatusReceived = 1005,
|
||||
/**
|
||||
* Indicates that a connection was closed abnormally (that is, with no close
|
||||
* frame being sent) when a status code is expected.
|
||||
*/
|
||||
AbnormalClosure = 1006,
|
||||
/**
|
||||
* The endpoint is terminating the connection because a message was received
|
||||
* that contained inconsistent data (e.g., non-UTF-8 data within a text message).
|
||||
*/
|
||||
InvalidFramePayloadData = 1007,
|
||||
/**
|
||||
* The endpoint is terminating the connection because it received a message
|
||||
* that violates its policy. This is a generic status code, used when codes
|
||||
* 1003 and 1009 are not suitable.
|
||||
*/
|
||||
PolicyViolation = 1008,
|
||||
/**
|
||||
* The endpoint is terminating the connection because a data frame was
|
||||
* received that is too large.
|
||||
*/
|
||||
MessageTooBig = 1009,
|
||||
/**
|
||||
* The client is terminating the connection because it expected the server to
|
||||
* negotiate one or more extension, but the server didn't.
|
||||
*/
|
||||
MissingExtension = 1010,
|
||||
/**
|
||||
* The server is terminating the connection because it encountered an
|
||||
* unexpected condition that prevented it from fulfilling the request.
|
||||
*/
|
||||
InternalError = 1011,
|
||||
/**
|
||||
* The server is terminating the connection because it is restarting.
|
||||
*/
|
||||
ServiceRestart = 1012,
|
||||
/**
|
||||
* The server is terminating the connection due to a temporary condition,
|
||||
* e.g. it is overloaded and is casting off some of its clients.
|
||||
*/
|
||||
TryAgainLater = 1013,
|
||||
/**
|
||||
* The server was acting as a gateway or proxy and received an invalid
|
||||
* response from the upstream server. This is similar to 502 HTTP Status Code.
|
||||
*/
|
||||
BadGateway = 1014,
|
||||
/**
|
||||
* Indicates that the connection was closed due to a failure to perform a TLS
|
||||
* handshake (e.g., the server certificate can't be verified).
|
||||
*/
|
||||
TlsHandshake = 1015,
|
||||
}
|
||||
|
||||
export interface ShellSessionDependencies {
|
||||
readonly isWindows: boolean;
|
||||
readonly isMac: boolean;
|
||||
readonly logger: Logger;
|
||||
readonly userShellSetting: IComputedValue<string>;
|
||||
readonly appName: string;
|
||||
readonly buildVersion: InitializableState<string>;
|
||||
computeShellEnvironment: ComputeShellEnvironment;
|
||||
spawnPty: SpawnPty;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
stat: Stat;
|
||||
}
|
||||
|
||||
export interface ShellSessionArgs {
|
||||
kubectl: Kubectl;
|
||||
websocket: WebSocket;
|
||||
cluster: Cluster;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export abstract class ShellSession {
|
||||
abstract readonly ShellType: string;
|
||||
|
||||
private static readonly shellEnvs = new Map<string, Record<string, string | undefined>>();
|
||||
private static readonly processes = new Map<string, pty.IPty>();
|
||||
|
||||
/**
|
||||
* Kill all remaining shell backing processes. Should be called when about to
|
||||
* quit
|
||||
*/
|
||||
public static cleanup(): void {
|
||||
for (const shellProcess of this.processes.values()) {
|
||||
try {
|
||||
process.kill(shellProcess.pid);
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
this.processes.clear();
|
||||
}
|
||||
|
||||
protected running = false;
|
||||
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: 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, () => (
|
||||
this.dependencies.spawnPty(shell, args, {
|
||||
rows: 30,
|
||||
cols: 80,
|
||||
cwd,
|
||||
env,
|
||||
name: "xterm-256color",
|
||||
// TODO: Something else is broken here so we need to force the use of winPty on windows
|
||||
useConpty: false,
|
||||
})
|
||||
));
|
||||
|
||||
this.dependencies.logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
|
||||
|
||||
return { shellProcess, resume };
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: ShellSessionDependencies, { kubectl, websocket, cluster, tabId: terminalId }: ShellSessionArgs) {
|
||||
this.kubectl = kubectl;
|
||||
this.websocket = websocket;
|
||||
this.cluster = cluster;
|
||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||
this.kubectlBinDirP = this.kubectl.binDir();
|
||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
||||
}
|
||||
|
||||
protected send(message: TerminalMessage): void {
|
||||
this.websocket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
|
||||
const cwdOptions = [this.cwd];
|
||||
|
||||
if (this.dependencies.isWindows) {
|
||||
cwdOptions.push(
|
||||
env.USERPROFILE,
|
||||
os.homedir(),
|
||||
"C:\\",
|
||||
);
|
||||
} else {
|
||||
cwdOptions.push(
|
||||
env.HOME,
|
||||
os.homedir(),
|
||||
);
|
||||
|
||||
if (this.dependencies.isMac) {
|
||||
cwdOptions.push("/Users");
|
||||
} else {
|
||||
cwdOptions.push("/home");
|
||||
}
|
||||
}
|
||||
|
||||
for (const potentialCwd of cwdOptions) {
|
||||
if (!potentialCwd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await this.dependencies.stat(potentialCwd);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return potentialCwd;
|
||||
}
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
return "."; // Always valid
|
||||
}
|
||||
|
||||
protected async openShellProcess(shell: string, args: string[], env: Record<string, string | undefined>) {
|
||||
const cwd = await this.getCwd(env);
|
||||
const { shellProcess, resume } = this.ensureShellProcess(shell, args, env, cwd);
|
||||
|
||||
if (resume) {
|
||||
this.send({ type: TerminalChannels.CONNECTED });
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
shellProcess.onData(data => this.send({ type: TerminalChannels.STDOUT, data }));
|
||||
shellProcess.onExit(({ exitCode }) => {
|
||||
this.dependencies.logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
|
||||
|
||||
// This might already be false because of the kill() within the websocket.on("close") handler
|
||||
if (this.running) {
|
||||
this.running = false;
|
||||
|
||||
if (exitCode > 0) {
|
||||
this.send({ type: TerminalChannels.STDOUT, data: "Terminal will auto-close in 15 seconds ..." });
|
||||
setTimeout(() => this.exit(), 15 * 1000);
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.websocket
|
||||
.on("message", (rawData: unknown): void => {
|
||||
if (!this.running) {
|
||||
return void this.dependencies.logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
|
||||
}
|
||||
|
||||
if (!(rawData instanceof Buffer)) {
|
||||
return void this.dependencies.logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
|
||||
}
|
||||
|
||||
const data = rawData.toString();
|
||||
|
||||
try {
|
||||
const message: TerminalMessage = JSON.parse(data);
|
||||
|
||||
switch (message.type) {
|
||||
case TerminalChannels.STDIN:
|
||||
shellProcess.write(message.data);
|
||||
break;
|
||||
case TerminalChannels.RESIZE:
|
||||
shellProcess.resize(message.data.width, message.data.height);
|
||||
break;
|
||||
case TerminalChannels.PING:
|
||||
this.dependencies.logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
|
||||
break;
|
||||
default:
|
||||
this.dependencies.logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
|
||||
}
|
||||
})
|
||||
.once("close", code => {
|
||||
this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
|
||||
|
||||
const stopShellSession = this.running
|
||||
&& (
|
||||
(
|
||||
code !== WebSocketCloseEvent.AbnormalClosure
|
||||
&& code !== WebSocketCloseEvent.GoingAway
|
||||
)
|
||||
|| this.cluster.disconnected
|
||||
);
|
||||
|
||||
if (stopShellSession) {
|
||||
this.running = false;
|
||||
|
||||
try {
|
||||
this.dependencies.logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
|
||||
shellProcess.kill();
|
||||
ShellSession.processes.delete(this.terminalId);
|
||||
} catch (error) {
|
||||
this.dependencies.logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.dependencies.emitAppEvent({ name: this.ShellType, action: "open" });
|
||||
}
|
||||
|
||||
protected getPathEntries(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected async getCachedShellEnv() {
|
||||
const { id: clusterId } = this.cluster;
|
||||
|
||||
let env = ShellSession.shellEnvs.get(clusterId);
|
||||
|
||||
if (!env) {
|
||||
env = await this.getShellEnv();
|
||||
ShellSession.shellEnvs.set(clusterId, env);
|
||||
} else {
|
||||
// refresh env in the background
|
||||
this.getShellEnv().then((shellEnv: any) => {
|
||||
ShellSession.shellEnvs.set(clusterId, shellEnv);
|
||||
});
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
protected async getShellEnv() {
|
||||
const shell = this.dependencies.userShellSetting.get();
|
||||
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 (this.dependencies.isWindows) {
|
||||
env.PTYSHELL = shell || "powershell.exe";
|
||||
env.PATH = pathStr;
|
||||
env.LENS_SESSION = "true";
|
||||
env.WSLENV = [
|
||||
env.WSLENV,
|
||||
"KUBECONFIG/up:LENS_SESSION/u",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(":");
|
||||
} else if (shell !== undefined) {
|
||||
env.PTYSHELL = shell;
|
||||
env.PATH = pathStr;
|
||||
} else {
|
||||
env.PTYSHELL = ""; // blank runs the system default shell
|
||||
}
|
||||
|
||||
if (path.basename(env.PTYSHELL) === "zsh") {
|
||||
env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME;
|
||||
env.ZDOTDIR = await this.kubectlBinDirP;
|
||||
env.DISABLE_AUTO_UPDATE = "true";
|
||||
}
|
||||
|
||||
env.PTYPID = process.pid.toString();
|
||||
env.KUBECONFIG = await this.kubeconfigPathP;
|
||||
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;
|
||||
}
|
||||
|
||||
env.NO_PROXY = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
env.NO_PROXY,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join();
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
protected exit(code = WebSocketCloseEvent.NormalClosure) {
|
||||
if (this.websocket.readyState == this.websocket.OPEN) {
|
||||
this.websocket.close(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user