diff --git a/src/common/terminal/channels.ts b/src/common/terminal/channels.ts index f958c9c696..d8c513deb6 100644 --- a/src/common/terminal/channels.ts +++ b/src/common/terminal/channels.ts @@ -10,6 +10,7 @@ export enum TerminalChannels { CONNECTED = "connected", RESIZE = "resize", PING = "ping", + ERROR = "error", } export type TerminalMessage = { @@ -28,4 +29,7 @@ export type TerminalMessage = { }; } | { type: TerminalChannels.PING; +} | { + type: TerminalChannels.ERROR; + data: string; }; diff --git a/src/main/shell-session/local-shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session/local-shell-session.ts index 49e6dd059f..cf2ec2d1db 100644 --- a/src/main/shell-session/local-shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -10,6 +10,7 @@ import type { ModifyTerminalShellEnv } from "../shell-env-modifier/modify-termin 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"; +import { TerminalChannels } from "../../../common/terminal/channels"; export interface LocalShellSessionDependencies extends ShellSessionDependencies { readonly directoryForBinaries: string; @@ -41,7 +42,13 @@ export class LocalShellSession extends ShellSession { const shell = env.PTYSHELL; if (!shell) { - throw new Error("PTYSHELL is not defined with the environment"); + this.send({ + type: TerminalChannels.ERROR, + data: "PTYSHELL is not defined with the environment", + }); + this.dependencies.logger.warn(`[LOCAL-SHELL-SESSION]: PTYSHELL env var is not defined for ${this.terminalId}`); + + return; } const args = await this.getShellArgs(shell); diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 196625f42a..8a48ae6ef1 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -156,23 +156,34 @@ export abstract class ShellSession { protected abstract get cwd(): string | undefined; - protected ensureShellProcess(shell: string, args: string[], env: Partial>, 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, - }) - )); + protected ensureShellProcess(shell: string, args: string[], env: Partial>, cwd: string): { shellProcess: pty.IPty; resume: boolean } | null { + try { + const resume = ShellSession.processes.has(this.terminalId); - this.dependencies.logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`); + 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, + }) + )); - return { shellProcess, resume }; + this.dependencies.logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`); + + return { shellProcess, resume }; + } catch (error) { + this.send({ + type: TerminalChannels.ERROR, + data: `Failed to start shell (${shell}): ${error}`, + }); + this.dependencies.logger.warn(`[SHELL-SESSION]: Failed to start PTY for ${this.terminalId}: ${error}`, { shell }); + + return null; + } } constructor(protected readonly dependencies: ShellSessionDependencies, { kubectl, websocket, cluster, tabId: terminalId }: ShellSessionArgs) { @@ -231,7 +242,13 @@ export abstract class ShellSession { protected async openShellProcess(shell: string, args: string[], env: Record) { const cwd = await this.getCwd(env); - const { shellProcess, resume } = this.ensureShellProcess(shell, args, env, cwd); + const ensured = this.ensureShellProcess(shell, args, env, cwd); + + if (!ensured) { + return; + } + + const { shellProcess, resume } = ensured; if (resume) { this.send({ type: TerminalChannels.CONNECTED }); diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts index fadd544bb6..864c1e243a 100644 --- a/src/renderer/api/terminal-api.ts +++ b/src/renderer/api/terminal-api.ts @@ -34,6 +34,7 @@ export interface TerminalApiQuery extends Record { export interface TerminalEvents extends WebSocketEvents { ready: () => void; connected: () => void; + error: (error: string) => void; } export interface TerminalApiDependencies extends WebSocketApiDependencies { @@ -145,6 +146,9 @@ export class TerminalApi extends WebSocketApi { case TerminalChannels.CONNECTED: this.emit("connected"); break; + case TerminalChannels.ERROR: + this.emit("error", message.data); + break; default: this.dependencies.logger.warn(`[TERMINAL-API]: unknown or unhandleable message type`, message); break; diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index 5f49882f1c..6a0ddb89f8 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -104,6 +104,7 @@ export class Terminal { this.api.once("ready", clearOnce); this.api.once("connected", clearOnce); this.api.on("data", this.onApiData); + this.api.on("error", this.onApiError); window.addEventListener("resize", this.onResize); const linkProvider = new LinkProvider( @@ -152,6 +153,10 @@ export class Terminal { this.xterm.write(data); }; + onApiError = (data: string) => { + this.xterm.writeln(data); + }; + onData = (data: string) => { if (!this.api.isReady) return; this.api.sendMessage({