mirror of
https://github.com/MCSManager/MCSManager.git
synced 2024-12-15 07:40:01 +08:00
Feat: pty resize via named pipe
This commit is contained in:
parent
161909f0e7
commit
50a7a5cb9e
@ -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") {
|
||||
|
@ -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<any> {
|
||||
const dockerProcess = instance.process as Partial<NodeProcessAdapter>;
|
||||
if (typeof dockerProcess?.resize === "function") {
|
||||
dockerProcess?.resize(size.w, size.h);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<IPtySubProcessCfg> {
|
||||
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"));
|
||||
}
|
||||
}
|
21
daemon/src/entity/commands/pty/pty_resize.ts
Normal file
21
daemon/src/entity/commands/pty/pty_resize.ts
Normal file
@ -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<any> {
|
||||
const pty = instance.process as Partial<GoPtyProcessAdapter>;
|
||||
if (typeof pty?.resize === "function") {
|
||||
pty?.resize(size.w, size.h);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user