123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- import { DockgeServer } from "./dockge-server";
- import * as os from "node:os";
- import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
- import { LimitQueue } from "./utils/limit-queue";
- import { DockgeSocket } from "./util-server";
- import {
- allowedCommandList, allowedRawKeys,
- getComposeTerminalName,
- getCryptoRandomInt,
- PROGRESS_TERMINAL_ROWS,
- TERMINAL_COLS,
- TERMINAL_ROWS
- } from "./util-common";
- import { sync as commandExistsSync } from "command-exists";
- import { log } from "./log";
- /**
- * Terminal for running commands, no user interaction
- */
- export class Terminal {
- protected static terminalMap : Map<string, Terminal> = new Map();
- protected _ptyProcess? : pty.IPty;
- protected server : DockgeServer;
- protected buffer : LimitQueue<string> = new LimitQueue(100);
- protected _name : string;
- protected file : string;
- protected args : string | string[];
- protected cwd : string;
- protected callback? : (exitCode : number) => void;
- protected _rows : number = TERMINAL_ROWS;
- protected _cols : number = TERMINAL_COLS;
- constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
- this.server = server;
- this._name = name;
- //this._name = "terminal-" + Date.now() + "-" + getCryptoRandomInt(0, 1000000);
- this.file = file;
- this.args = args;
- this.cwd = cwd;
- Terminal.terminalMap.set(this.name, this);
- }
- get rows() {
- return this._rows;
- }
- set rows(rows : number) {
- this._rows = rows;
- try {
- this.ptyProcess?.resize(this.cols, this.rows);
- } catch (e) {
- if (e instanceof Error) {
- log.debug("Terminal", "Failed to resize terminal: " + e.message);
- }
- }
- }
- get cols() {
- return this._cols;
- }
- set cols(cols : number) {
- this._cols = cols;
- try {
- this.ptyProcess?.resize(this.cols, this.rows);
- } catch (e) {
- if (e instanceof Error) {
- log.debug("Terminal", "Failed to resize terminal: " + e.message);
- }
- }
- }
- public start() {
- if (this._ptyProcess) {
- return;
- }
- this._ptyProcess = pty.spawn(this.file, this.args, {
- name: this.name,
- cwd: this.cwd,
- cols: TERMINAL_COLS,
- rows: this.rows,
- });
- // On Data
- this._ptyProcess.onData((data) => {
- this.buffer.pushItem(data);
- if (this.server.io) {
- this.server.io.to(this.name).emit("terminalWrite", this.name, data);
- }
- });
- // On Exit
- this._ptyProcess.onExit((res) => {
- this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
- // Remove room
- this.server.io.in(this.name).socketsLeave(this.name);
- Terminal.terminalMap.delete(this.name);
- log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
- if (this.callback) {
- this.callback(res.exitCode);
- }
- });
- }
- public onExit(callback : (exitCode : number) => void) {
- this.callback = callback;
- }
- public join(socket : DockgeSocket) {
- socket.join(this.name);
- }
- public leave(socket : DockgeSocket) {
- socket.leave(this.name);
- }
- public get ptyProcess() {
- return this._ptyProcess;
- }
- public get name() {
- return this._name;
- }
- /**
- * Get the terminal output string
- */
- getBuffer() : string {
- if (this.buffer.length === 0) {
- return "";
- }
- return this.buffer.join("");
- }
- close() {
- this._ptyProcess?.kill();
- }
- /**
- * Get a running and non-exited terminal
- * @param name
- */
- public static getTerminal(name : string) : Terminal | undefined {
- return Terminal.terminalMap.get(name);
- }
- public static getOrCreateTerminal(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) : Terminal {
- // Since exited terminal will be removed from the map, it is safe to get the terminal from the map
- let terminal = Terminal.getTerminal(name);
- if (!terminal) {
- terminal = new Terminal(server, name, file, args, cwd);
- }
- return terminal;
- }
- public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise<number> {
- const terminal = new Terminal(server, terminalName, file, args, cwd);
- terminal.rows = PROGRESS_TERMINAL_ROWS;
- if (socket) {
- terminal.join(socket);
- }
- return new Promise((resolve) => {
- terminal.onExit((exitCode : number) => {
- resolve(exitCode);
- });
- terminal.start();
- });
- }
- }
- /**
- * Interactive terminal
- * Mainly used for container exec
- */
- export class InteractiveTerminal extends Terminal {
- public write(input : string) {
- this.ptyProcess?.write(input);
- }
- resetCWD() {
- const cwd = process.cwd();
- this.ptyProcess?.write(`cd "${cwd}"\r`);
- }
- }
- /**
- * User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir
- */
- export class MainTerminal extends InteractiveTerminal {
- constructor(server : DockgeServer, name : string) {
- let shell;
- if (os.platform() === "win32") {
- if (commandExistsSync("pwsh.exe")) {
- shell = "pwsh.exe";
- } else {
- shell = "powershell.exe";
- }
- } else {
- shell = "bash";
- }
- super(server, name, shell, [], server.stacksDir);
- }
- public write(input : string) {
- // For like Ctrl + C
- if (allowedRawKeys.includes(input)) {
- super.write(input);
- return;
- }
- // Check if the command is allowed
- const cmdParts = input.split(" ");
- const executable = cmdParts[0].trim();
- log.debug("console", "Executable: " + executable);
- log.debug("console", "Executable length: " + executable.length);
- if (!allowedCommandList.includes(executable)) {
- throw new Error("Command not allowed.");
- }
- super.write(input);
- }
- }
|