diff --git a/daemon/src/entity/commands/dispatcher.ts b/daemon/src/entity/commands/dispatcher.ts index 0d3293fd..3417f6ef 100755 --- a/daemon/src/entity/commands/dispatcher.ts +++ b/daemon/src/entity/commands/dispatcher.ts @@ -13,9 +13,8 @@ import PtyStartCommand from "./pty/pty_start"; import PtyStopCommand from "./pty/pty_stop"; import OpenFrpTask from "./task/openfrp"; import RconCommand from "./steam/rcon_command"; -import NodePtyStartCommand from "./pty/node_pty_start"; import DockerResizeCommand from "./docker/docker_pty_resize"; -import NodePtyResizeCommand from "./pty/node_pty_resize"; +import PtyResizeCommand from "./pty/pty_resize"; // Instance function dispatcher // Dispatch and assign different functions according to different types @@ -48,10 +47,9 @@ export default class FunctionDispatcher extends InstanceCommand { // Enable emulated terminal mode if (instance.config.terminalOption.pty && instance.config.processType === "general") { - // instance.setPreset("start", new PtyStartCommand()); - instance.setPreset("start", new NodePtyStartCommand()); + instance.setPreset("start", new PtyStartCommand()); instance.setPreset("stop", new PtyStopCommand()); - instance.setPreset("resize", new NodePtyResizeCommand()); + instance.setPreset("resize", new PtyResizeCommand()); } // Whether to enable Docker PTY mode if (instance.config.processType === "docker") { diff --git a/daemon/src/entity/commands/pty/node_pty_resize.ts b/daemon/src/entity/commands/pty/node_pty_resize.ts deleted file mode 100644 index ee3b5fb6..00000000 --- a/daemon/src/entity/commands/pty/node_pty_resize.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Instance from "../../instance/instance"; -import InstanceCommand from "../base/command"; -import { NodeProcessAdapter } from "./node_pty_start"; - -interface IResizeOptions { - h: number; - w: number; -} - -export default class NodePtyResizeCommand extends InstanceCommand { - constructor() { - super("ResizeTTY"); - } - - async exec(instance: Instance, size?: IResizeOptions): Promise { - const dockerProcess = instance.process as Partial; - if (typeof dockerProcess?.resize === "function") { - dockerProcess?.resize(size.w, size.h); - } - } -} diff --git a/daemon/src/entity/commands/pty/node_pty_start.ts b/daemon/src/entity/commands/pty/node_pty_start.ts deleted file mode 100644 index bb98d32b..00000000 --- a/daemon/src/entity/commands/pty/node_pty_start.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { $t } from "../../../i18n"; -import os from "os"; -import Instance from "../../instance/instance"; -import logger from "../../../service/log"; -import fs from "fs-extra"; -import path from "path"; -import readline from "readline"; -import InstanceCommand from "../base/command"; -import { ChildProcess, ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { commandStringToArray } from "../base/command_parser"; -import FunctionDispatcher from "../dispatcher"; -import type { IPty, spawn as spawnType } from "node-pty"; -import { killProcess } from "common"; -import { EventEmitter } from "koa"; -import { IInstanceProcess } from "../../instance/interface"; -interface IPtySubProcessCfg { - pid: number; -} - -// Error exception at startup -class StartupError extends Error { - constructor(msg: string) { - super(msg); - } -} - -// process adapter -export class NodeProcessAdapter extends EventEmitter implements IInstanceProcess { - public pid?: number; - public exitCode: number; - - constructor(private process: IPty) { - super(); - this.pid = this.process.pid; - this.process.onData((data: string | Buffer) => this.emit("data", data)); - this.process.onExit((info) => { - this.exitCode = info.exitCode; - this.emit("exit", info.exitCode); - }); - } - - public resize(w: number, h: number) { - this.process.resize(w, h); - } - - public write(data?: string) { - return this.process.write(data); - } - - public kill(s?: any) { - return killProcess(this.pid, this.process); - } - - public async destroy() { - try { - // remove all dynamically added event listeners - for (const n of this.eventNames()) this.removeAllListeners(n); - if (this.exitCode) { - this.kill(); - } - } catch (error) {} - } -} - -export default class NodePtyStartCommand extends InstanceCommand { - constructor() { - super("PtyStartCommand"); - } - - readPtySubProcessConfig(subProcess: ChildProcessWithoutNullStreams): Promise { - return new Promise((r, j) => { - const errConfig = { - pid: 0 - }; - const rl = readline.createInterface({ - input: subProcess.stdout, - crlfDelay: Infinity - }); - rl.on("line", (line = "") => { - try { - rl.removeAllListeners(); - const cfg = JSON.parse(line) as IPtySubProcessCfg; - if (cfg.pid == null) throw new Error("Error"); - r(cfg); - } catch (error) { - r(errConfig); - } - }); - setTimeout(() => { - r(errConfig); - }, 1000 * 3); - }); - } - - async exec(instance: Instance, source = "Unknown") { - if ( - !instance.config.startCommand || - !instance.config.cwd || - !instance.config.ie || - !instance.config.oe - ) - throw new StartupError($t("TXT_CODE_pty_start.cmdErr")); - if (!fs.existsSync(instance.absoluteCwdPath())) - throw new StartupError($t("TXT_CODE_pty_start.cwdNotExist")); - if (!path.isAbsolute(path.normalize(instance.config.cwd))) - throw new StartupError($t("TXT_CODE_pty_start.mustAbsolutePath")); - - // PTY mode correctness check - logger.info($t("TXT_CODE_pty_start.startPty", { source: source })); - - try { - require.resolve("node-pty"); - } catch (e) { - // node-pty not available - instance.println("ERROR", $t("TXT_CODE_pty_start.startErr")); - instance.config.terminalOption.pty = false; - await instance.forceExec(new FunctionDispatcher()); - await instance.execPreset("start", source); - return; - } - - // command parsing - const commandList = commandStringToArray(instance.config.startCommand); - const commandExeFile = commandList[0]; - const commandParameters = commandList.slice(1); - if (commandList.length === 0) { - throw new StartupError($t("TXT_CODE_general_start.cmdEmpty")); - } - - logger.info("----------------"); - logger.info($t("TXT_CODE_general_start.startInstance", { source: source })); - logger.info($t("TXT_CODE_general_start.instanceUuid", { uuid: instance.instanceUuid })); - logger.info($t("TXT_CODE_general_start.startCmd", { cmdList: JSON.stringify(commandList) })); - logger.info($t("TXT_CODE_general_start.cwd", { cwd: instance.config.cwd })); - logger.info("----------------"); - - const { spawn } = require("node-pty") as { - spawn: typeof spawnType; - }; - - logger.info($t("模式:仿真终端(node-pty)")); - - const ptyProcess = spawn(commandExeFile, commandParameters, { - name: "xterm-256color", - cols: 170, - rows: 40, - cwd: instance.config.cwd, - env: { ...process.env, TERM: "xterm-256color" }, - encoding: null - }); - - if (!ptyProcess.pid) { - instance.println( - "ERROR", - $t("TXT_CODE_general_start.pidErr", { - startCommand: instance.config.startCommand, - commandExeFile: commandExeFile, - commandParameters: JSON.stringify(commandParameters) - }) - ); - throw new StartupError($t("TXT_CODE_general_start.startErr")); - } - - const processAdapter = new NodeProcessAdapter(ptyProcess); - instance.started(processAdapter); - logger.info( - $t("TXT_CODE_pty_start.startSuccess", { - instanceUuid: instance.instanceUuid, - pid: processAdapter.pid - }) - ); - instance.println("INFO", $t("TXT_CODE_pty_start.startEmulatedTerminal")); - } -} diff --git a/daemon/src/entity/commands/pty/pty_resize.ts b/daemon/src/entity/commands/pty/pty_resize.ts new file mode 100644 index 00000000..7c7a4e9c --- /dev/null +++ b/daemon/src/entity/commands/pty/pty_resize.ts @@ -0,0 +1,21 @@ +import Instance from "../../instance/instance"; +import InstanceCommand from "../base/command"; +import { GoPtyProcessAdapter } from "./pty_start"; + +interface IResizeOptions { + h: number; + w: number; +} + +export default class PtyResizeCommand extends InstanceCommand { + constructor() { + super("ResizeTTY"); + } + + async exec(instance: Instance, size?: IResizeOptions): Promise { + const pty = instance.process as Partial; + if (typeof pty?.resize === "function") { + pty?.resize(size.w, size.h); + } + } +} diff --git a/daemon/src/entity/commands/pty/pty_start.ts b/daemon/src/entity/commands/pty/pty_start.ts index 0532337c..719397ea 100755 --- a/daemon/src/entity/commands/pty/pty_start.ts +++ b/daemon/src/entity/commands/pty/pty_start.ts @@ -11,10 +11,9 @@ import { IInstanceProcess } from "../../instance/interface"; import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process"; import { commandStringToArray } from "../base/command_parser"; import { killProcess } from "common"; -import GeneralStartCommand from "../general/general_start"; import FunctionDispatcher from "../dispatcher"; -import StartCommand from "../start"; import { PTY_PATH } from "../../../const"; +import { Writable } from "stream"; interface IPtySubProcessCfg { pid: number; @@ -27,16 +26,47 @@ class StartupError extends Error { } } +const GO_PTY_MSG_TYPE = { + RESIZE: 0x04 +}; + // process adapter export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProcess { - pid?: number | string; + private pipeClient: Writable; - constructor(private process: ChildProcess, ptySubProcessPid: number) { + constructor(private process: ChildProcess, public pid: number, public pipeName: string) { super(); - this.pid = ptySubProcessPid; process.stdout.on("data", (text) => this.emit("data", text)); process.stderr.on("data", (text) => this.emit("data", text)); process.on("exit", (code) => this.emit("exit", code)); + this.initNamedPipe(); + } + + private initNamedPipe() { + const fd = fs.openSync(this.pipeName, "w"); + const writePipe = fs.createWriteStream(null, { fd }); + writePipe.on("close", () => {}); + writePipe.on("end", () => {}); + writePipe.on("error", (err) => { + logger.error("Pipe error:", this.pipeName, err); + }); + this.pipeClient = writePipe; + } + + public resize(w: number, h: number) { + const MAX_W = 900; + if (w > MAX_W) w = MAX_W; + if (h > MAX_W) h = MAX_W; + const resizeStruct = JSON.stringify({ width: Number(w), height: Number(h) }); + const len = resizeStruct.length; + const lenBuff = Buffer.alloc(2); + lenBuff.writeInt16BE(len, 0); + const buf = Buffer.from([GO_PTY_MSG_TYPE.RESIZE, ...lenBuff, ...Buffer.from(resizeStruct)]); + this.writeToNamedPipe(buf); + } + + public writeToNamedPipe(data: Buffer) { + this.pipeClient.write(data); } public write(data?: string) { @@ -60,6 +90,10 @@ export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProces if (this.process) for (const eventName of this.process.eventNames()) this.process.stdout.removeAllListeners(eventName); + if (this.pipeClient) + for (const eventName of this.pipeClient.eventNames()) + this.pipeClient.removeAllListeners(eventName); + this.pipeClient?.destroy(); this.process?.stdout?.destroy(); this.process?.stderr?.destroy(); if (this.process?.exitCode === null) { @@ -146,16 +180,25 @@ export default class PtyStartCommand extends InstanceCommand { if (commandList.length === 0) return instance.failure(new StartupError($t("TXT_CODE_pty_start.cmdEmpty"))); + + const pipeLinuxDir = "/tmp/mcsmanager-instance-pipe"; + if (!fs.existsSync(pipeLinuxDir)) fs.mkdirsSync(pipeLinuxDir); + let pipeName = `${pipeLinuxDir}/pipe-${instance.instanceUuid}`; + if (os.platform() === "win32") { + pipeName = `\\\\.\\pipe\\${pipeName}`; + } + const ptyParameter = [ - "-dir", - instance.config.cwd, - "-cmd", - JSON.stringify(commandList), "-size", `${instance.config.terminalOption.ptyWindowCol},${instance.config.terminalOption.ptyWindowRow}`, - "-color", "-coder", - instance.config.oe + instance.config.oe, + "-dir", + instance.config.cwd, + "-fifo", + pipeName, + "-cmd", + JSON.stringify(commandList) ]; logger.info("----------------"); @@ -190,7 +233,7 @@ export default class PtyStartCommand extends InstanceCommand { // create process adapter const ptySubProcessCfg = await this.readPtySubProcessConfig(subProcess); - const processAdapter = new GoPtyProcessAdapter(subProcess, ptySubProcessCfg.pid); + const processAdapter = new GoPtyProcessAdapter(subProcess, ptySubProcessCfg.pid, pipeName); logger.info(`pty.exe response: ${JSON.stringify(ptySubProcessCfg)}`); // After reading the configuration, Need to check the process status