mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-01-12 14:54:34 +08:00
commit
ed4795224a
@ -119,7 +119,11 @@ export class ProcessWrapper extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
export function killProcess(pid: string | number, process: ChildProcess, signal?: any) {
|
||||
export function killProcess(
|
||||
pid: string | number,
|
||||
process: { kill(signal?: any): any },
|
||||
signal?: any
|
||||
) {
|
||||
try {
|
||||
if (os.platform() === "win32") {
|
||||
execSync(`taskkill /PID ${pid} /T /F`);
|
||||
|
34
daemon/package-lock.json
generated
34
daemon/package-lock.json
generated
@ -60,6 +60,9 @@
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"../common": {
|
||||
@ -3207,6 +3210,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
|
||||
"integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@ -3238,6 +3247,16 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
@ -7353,6 +7372,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
|
||||
"integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
|
||||
"optional": true
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@ -7378,6 +7403,15 @@
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node-pty": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
|
||||
|
@ -44,6 +44,9 @@
|
||||
"uuid": "^8.3.2",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@types/axios": "^0.14.0",
|
||||
|
@ -13,6 +13,9 @@ 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";
|
||||
|
||||
// Instance function dispatcher
|
||||
// Dispatch and assign different functions according to different types
|
||||
@ -44,20 +47,16 @@ export default class FunctionDispatcher extends InstanceCommand {
|
||||
}
|
||||
|
||||
// Enable emulated terminal mode
|
||||
if (
|
||||
instance.config.terminalOption.pty &&
|
||||
instance.config.terminalOption.ptyWindowCol &&
|
||||
instance.config.terminalOption.ptyWindowRow &&
|
||||
instance.config.processType === "general"
|
||||
) {
|
||||
instance.setPreset("start", new PtyStartCommand());
|
||||
if (instance.config.terminalOption.pty && instance.config.processType === "general") {
|
||||
// instance.setPreset("start", new PtyStartCommand());
|
||||
instance.setPreset("start", new NodePtyStartCommand());
|
||||
instance.setPreset("stop", new PtyStopCommand());
|
||||
instance.setPreset("resize", new NullCommand());
|
||||
instance.setPreset("resize", new NodePtyResizeCommand());
|
||||
}
|
||||
// Whether to enable Docker PTY mode
|
||||
if (instance.config.processType === "docker") {
|
||||
instance.setPreset("start", new DockerStartCommand());
|
||||
instance.setPreset("resize", new NullCommand());
|
||||
instance.setPreset("resize", new DockerResizeCommand());
|
||||
}
|
||||
if (instance.config.enableRcon) {
|
||||
instance.setPreset("command", new RconCommand());
|
||||
|
24
daemon/src/entity/commands/docker/docker_pty_resize.ts
Normal file
24
daemon/src/entity/commands/docker/docker_pty_resize.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { DockerProcessAdapter } from "./docker_start";
|
||||
|
||||
export interface IResizeOptions {
|
||||
h: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
export default class DockerResizeCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("ResizeTTY");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, size?: IResizeOptions): Promise<any> {
|
||||
const dockerProcess = instance?.process as Partial<DockerProcessAdapter>;
|
||||
if (typeof dockerProcess?.container?.resize === "function") {
|
||||
await dockerProcess?.container?.resize({
|
||||
h: size.h,
|
||||
w: size.w
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
21
daemon/src/entity/commands/pty/node_pty_resize.ts
Normal file
21
daemon/src/entity/commands/pty/node_pty_resize.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
174
daemon/src/entity/commands/pty/node_pty_start.ts
Normal file
174
daemon/src/entity/commands/pty/node_pty_start.ts
Normal file
@ -0,0 +1,174 @@
|
||||
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"));
|
||||
}
|
||||
}
|
@ -111,8 +111,8 @@ export default class Instance extends EventEmitter {
|
||||
) {
|
||||
if (this.status() != Instance.STATUS_STOP)
|
||||
throw new Error($t("TXT_CODE_instanceConf.cantModifyPtyModel"));
|
||||
if (!fs.existsSync(PTY_PATH) && cfg?.terminalOption?.pty === true)
|
||||
throw new Error($t("TXT_CODE_instanceConf.ptyNotExist", { path: PTY_PATH }));
|
||||
// if (!fs.existsSync(PTY_PATH) && cfg?.terminalOption?.pty === true)
|
||||
// throw new Error($t("TXT_CODE_instanceConf.ptyNotExist", { path: PTY_PATH }));
|
||||
configureEntityParams(this.config.terminalOption, cfg.terminalOption, "pty", Boolean);
|
||||
this.forceExec(new FunctionDispatcher());
|
||||
}
|
||||
@ -224,8 +224,10 @@ export default class Instance extends EventEmitter {
|
||||
started(process: IInstanceProcess) {
|
||||
this.config.lastDatetime = Date.now();
|
||||
const outputCode = this.config.terminalOption.pty ? "utf-8" : this.config.oe;
|
||||
process.on("data", (text) => this.emit("data", iconv.decode(text, outputCode)));
|
||||
process.on("exit", (code) => this.stopped(code));
|
||||
process.on("data", (text: any) => {
|
||||
this.emit("data", iconv.decode(text, outputCode));
|
||||
});
|
||||
process.on("exit", (code: number) => this.stopped(code));
|
||||
this.process = process;
|
||||
this.instanceStatus = Instance.STATUS_RUNNING;
|
||||
this.emit("open", this);
|
||||
|
@ -129,7 +129,7 @@ routerApp.on("stream/resize", async (ctx, data) => {
|
||||
try {
|
||||
const instanceUuid = ctx.session?.stream?.instanceUuid;
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (instance.config.processType === "docker") await instance.execPreset("resize", data);
|
||||
if (instance) await instance.execPreset("resize", data);
|
||||
} catch (error) {
|
||||
// protocol.responseError(ctx, error);
|
||||
}
|
||||
|
@ -60,7 +60,12 @@ export function useTerminal() {
|
||||
const terminal = ref<Terminal>();
|
||||
const isConnect = ref<boolean>(false);
|
||||
const socketAddress = ref("");
|
||||
|
||||
let fitAddonTask: NodeJS.Timer;
|
||||
let cachedSize = {
|
||||
w: 160,
|
||||
h: 40
|
||||
};
|
||||
|
||||
const execute = async (config: UseTerminalParams) => {
|
||||
isReady.value = false;
|
||||
@ -144,6 +149,18 @@ export function useTerminal() {
|
||||
return socket;
|
||||
};
|
||||
|
||||
const refreshWindowSize = (w: number, h: number) => {
|
||||
if (cachedSize.h !== h || cachedSize.w !== w) {
|
||||
cachedSize = {
|
||||
w,
|
||||
h
|
||||
};
|
||||
socket?.emit("stream/resize", {
|
||||
data: cachedSize
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initTerminalWindow = (element: HTMLElement) => {
|
||||
const background = hasBgImage.value ? "#00000000" : "#1e1e1e";
|
||||
const term = new Terminal({
|
||||
@ -167,8 +184,11 @@ export function useTerminal() {
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(element);
|
||||
fitAddon.fit();
|
||||
refreshWindowSize(term.cols - 1, term.rows - 1);
|
||||
fitAddonTask = setInterval(() => {
|
||||
fitAddon.fit();
|
||||
refreshWindowSize(term.cols - 1, term.rows - 1);
|
||||
// Auto resize pty win size
|
||||
}, 1000);
|
||||
|
||||
term.onData((data) => {
|
||||
|
Loading…
Reference in New Issue
Block a user