sudo commands through ephemeral terminal
This commit is contained in:
@@ -3,9 +3,16 @@ import { existsSync } from 'node:fs';
|
||||
import { cp, mkdir } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import * as pty from 'node-pty';
|
||||
|
||||
const run = (cmd, args, opts = {}) =>
|
||||
new Promise((resolve) => {
|
||||
const proc = execFile(cmd, args, { stdio: 'ignore', ...opts }, () => resolve());
|
||||
proc.on('error', () => resolve());
|
||||
});
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const isDocker = existsSync('/opt/terminal-templates/.zshrc');
|
||||
|
||||
@@ -60,25 +67,14 @@ const ensureUserFiles = async (homeDir) => {
|
||||
if (ohMyZshSource && existsSync(ohMyZshSource)) {
|
||||
await cp(ohMyZshSource, ohMyZshPath, { recursive: true });
|
||||
} else {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ['git', 'clone', '--depth=1', 'https://github.com/ohmyzsh/ohmyzsh.git', ohMyZshPath],
|
||||
stdout: 'ignore',
|
||||
stderr: 'ignore',
|
||||
});
|
||||
await proc.exited;
|
||||
await run('git', ['clone', '--depth=1', 'https://github.com/ohmyzsh/ohmyzsh.git', ohMyZshPath]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDocker) {
|
||||
const starshipBin = join(homeDir, '.local', 'bin', 'starship');
|
||||
if (!existsSync(starshipBin)) {
|
||||
const installProc = Bun.spawn({
|
||||
cmd: ['sh', '-c', 'curl -sS https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"'],
|
||||
env: { ...process.env, HOME: homeDir },
|
||||
stdout: 'ignore',
|
||||
stderr: 'ignore',
|
||||
});
|
||||
await installProc.exited;
|
||||
await run('sh', ['-c', 'curl -sS https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"'], { env: { ...process.env, HOME: homeDir } });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -149,15 +145,36 @@ wss.on('connection', (ws) => {
|
||||
const cwd = msg.cwd ?? process.cwd();
|
||||
const homeDir = msg.homeDir ?? process.cwd();
|
||||
const userLabel = msg.userLabel ?? 'officer';
|
||||
const prompt = `${userLabel} in %~ %# `;
|
||||
const bashPrompt = `${userLabel} \\w \\$ `;
|
||||
const cols = msg.cols ?? 80;
|
||||
const rows = msg.rows ?? 24;
|
||||
const isHost = !!msg.host;
|
||||
|
||||
try {
|
||||
await ensureUserFiles(homeDir);
|
||||
} catch {
|
||||
// ignore
|
||||
let ptyEnv;
|
||||
if (isHost) {
|
||||
ptyEnv = { ...process.env, TERM: 'xterm-256color' };
|
||||
} else {
|
||||
const prompt = `${userLabel} in %~ %# `;
|
||||
const bashPrompt = `${userLabel} \\w \\$ `;
|
||||
|
||||
try {
|
||||
await ensureUserFiles(homeDir);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
ptyEnv = {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
ZDOTDIR: homeDir,
|
||||
ZSH: `${homeDir}/.oh-my-zsh`,
|
||||
SHELL: shell.command,
|
||||
USER: userLabel,
|
||||
LOGNAME: userLabel,
|
||||
OFFICER_TERMINAL_USER: userLabel,
|
||||
PROMPT: prompt,
|
||||
PS1: bashPrompt,
|
||||
TERM: 'xterm-256color',
|
||||
};
|
||||
}
|
||||
|
||||
let term;
|
||||
@@ -167,19 +184,7 @@ wss.on('connection', (ws) => {
|
||||
cols,
|
||||
rows,
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
ZDOTDIR: homeDir,
|
||||
ZSH: `${homeDir}/.oh-my-zsh`,
|
||||
SHELL: shell.command,
|
||||
USER: userLabel,
|
||||
LOGNAME: userLabel,
|
||||
OFFICER_TERMINAL_USER: userLabel,
|
||||
PROMPT: prompt,
|
||||
PS1: bashPrompt,
|
||||
TERM: 'xterm-256color',
|
||||
},
|
||||
env: ptyEnv,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to start terminal';
|
||||
@@ -205,7 +210,8 @@ wss.on('connection', (ws) => {
|
||||
}
|
||||
});
|
||||
|
||||
term.onExit(() => {
|
||||
term.onExit(({ exitCode, signal }) => {
|
||||
console.log(`[sidecar] session ${sessionId} exited code=${exitCode} signal=${signal}`);
|
||||
if (session.ws) {
|
||||
sendJson(session.ws, { type: 'exit' });
|
||||
}
|
||||
@@ -228,7 +234,11 @@ wss.on('connection', (ws) => {
|
||||
if (msg.cols > 0 && msg.rows > 0) {
|
||||
session.cols = msg.cols;
|
||||
session.rows = msg.rows;
|
||||
session.term.resize(msg.cols, msg.rows);
|
||||
try {
|
||||
session.term.resize(msg.cols, msg.rows);
|
||||
} catch {
|
||||
// PTY may have already exited
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'cwd':
|
||||
|
||||
@@ -236,20 +236,28 @@ const sidecarAlive = async (port: number): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
const startHostSidecar = async () => {
|
||||
if (await sidecarAlive(HOST_SIDECAR_PORT)) {
|
||||
console.log(`[terminal] host sidecar already running on port ${HOST_SIDECAR_PORT}`);
|
||||
return;
|
||||
const killSidecarOnPort = (port: number) => {
|
||||
try {
|
||||
const result = Bun.spawnSync({ cmd: ['fuser', '-k', `${port}/tcp`], stdout: 'ignore', stderr: 'ignore' });
|
||||
if (result.exitCode === 0) console.log(`[terminal] killed stale sidecar on port ${port}`);
|
||||
} catch {
|
||||
// fuser not available or failed
|
||||
}
|
||||
};
|
||||
|
||||
const startHostSidecar = async () => {
|
||||
if (hostSidecarProcess) {
|
||||
hostSidecarProcess.kill();
|
||||
await hostSidecarProcess.exited.catch(() => {});
|
||||
hostSidecarProcess = null;
|
||||
}
|
||||
|
||||
killSidecarOnPort(HOST_SIDECAR_PORT);
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
const sidecarPath = fileURLToPath(new URL('./pty-sidecar.mjs', import.meta.url));
|
||||
hostSidecarProcess = Bun.spawn({
|
||||
cmd: ['bun', sidecarPath],
|
||||
cmd: ['node', sidecarPath],
|
||||
env: { ...process.env, TERMINAL_PTY_PORT: String(HOST_SIDECAR_PORT) },
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
@@ -323,6 +331,7 @@ export const terminalWebsocket = {
|
||||
sidecar.send(
|
||||
JSON.stringify({
|
||||
type: 'init',
|
||||
host: true,
|
||||
sessionId: ws.data.sessionId ?? `host-${ws.data.userId}`,
|
||||
shell: { command: process.env.SHELL ?? '/bin/zsh', args: ['-i'] },
|
||||
cwd: resolveCwd(process.env.HOME!, ws.data.cwd),
|
||||
|
||||
Reference in New Issue
Block a user