From b499591f209bd492d253e75d276584f7a9afbb33 Mon Sep 17 00:00:00 2001 From: WFInfo Date: Sun, 12 Jul 2026 13:40:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DWindows=E5=B9=BB?= =?UTF-8?q?=E5=85=BD=E5=B8=95=E9=B2=81=E8=BD=AC=E5=8F=91=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E6=B5=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 使用PTY包装转发进程解决stdout块缓冲问题 - Windows下通过spawn()以管道模式启动的进程,stdout会切换为块缓冲(~4KB) - 游戏服务器日志输出量低,缓冲区填不满导致输出不实时 - 改用PTY二进制文件包装转发进程,让游戏服务器获得伪终端,保持行缓冲 2. 修复实例重启后旧PTY退出事件覆盖新实例状态的竞态条件 - virtualSocket的terminal-exit处理器增加会话ID检查 - 防止旧会话退出事件将新实例的status覆写为stopped 3. 修复handleInput中session.socket被无操作虚拟socket替换的问题 - stopInstance/sendInput创建的虚拟socket会在会话断开时替换session.socket - 导致后续流转发输出丢失(emit变为空操作) - 增加socket.connected检查,仅允许真实Socket.IO连接替换 4. 实例会话启用autoCloseOnForwardExit - 转发进程退出后自动关闭无用的PTY shell - 加速实例停止流程,减少竞态条件窗口 Co-Authored-By: Claude --- .../src/modules/instance/InstanceManager.ts | 10 ++++ .../src/modules/terminal/TerminalManager.ts | 58 +++++++++++++------ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/server/src/modules/instance/InstanceManager.ts b/server/src/modules/instance/InstanceManager.ts index 3a09475..c1e6528 100644 --- a/server/src/modules/instance/InstanceManager.ts +++ b/server/src/modules/instance/InstanceManager.ts @@ -570,6 +570,11 @@ export class InstanceManager extends EventEmitter { if (event === 'terminal-output') { this.emit('instance-output', { id, data: data.data }) } else if (event === 'terminal-exit') { + // 防止旧终端会话的退出事件覆盖新会话的状态 + if (instance.terminalSessionId !== terminalSessionId) { + this.logger.info(`忽略旧终端会话退出事件: ${terminalSessionId} (当前: ${instance.terminalSessionId})`) + return + } this.logger.info(`实例 ${instance.name} 终端会话退出`) instance.status = 'stopped' instance.pid = undefined @@ -578,6 +583,10 @@ export class InstanceManager extends EventEmitter { this.emit('instance-status-changed', { id, status: 'stopped' }) this.saveInstances() } else if (event === 'terminal-error') { + if (instance.terminalSessionId !== terminalSessionId) { + this.logger.info(`忽略旧终端会话错误事件: ${terminalSessionId} (当前: ${instance.terminalSessionId})`) + return + } this.logger.error(`实例 ${instance.name} 终端错误:`, data.error) instance.status = 'error' instance.pid = undefined @@ -619,6 +628,7 @@ export class InstanceManager extends EventEmitter { workingDirectory: instance.workingDirectory, enableStreamForward: instance.enableStreamForward || false, programPath: instance.programPath || '', + autoCloseOnForwardExit: instance.enableStreamForward || false, terminalUser: instance.terminalUser }) diff --git a/server/src/modules/terminal/TerminalManager.ts b/server/src/modules/terminal/TerminalManager.ts index 35a1054..8a15460 100755 --- a/server/src/modules/terminal/TerminalManager.ts +++ b/server/src/modules/terminal/TerminalManager.ts @@ -641,16 +641,37 @@ export class TerminalManager { this.logger.info(`参数列表: ${JSON.stringify(args)}`) // 启动目标程序进程 - const forwardProcess = spawn(executablePath, args, { - stdio: ['pipe', 'pipe', 'pipe'], - cwd: session.workingDirectory, - env: { - ...process.env, - TERM: 'xterm-256color', - COLORTERM: 'truecolor' - }, - detached: os.platform() !== 'win32' // 在非Windows平台创建独立进程组 - }) + // 在Windows上使用PTY包装,避免stdout块缓冲导致输出不实时 + let forwardProcess: ChildProcess + if (os.platform() === 'win32' && this.ptyPath) { + const ptyArgs = [ + '-dir', session.workingDirectory, + '-size', '100,30', + '-coder', 'UTF-8', + '-cmd', JSON.stringify([executablePath, ...args]) + ] + this.logger.info(`使用PTY包装转发进程以避免stdout缓冲: ${this.ptyPath}`) + forwardProcess = spawn(this.ptyPath, ptyArgs, { + stdio: ['pipe', 'pipe', 'pipe'], + cwd: session.workingDirectory, + env: { + ...process.env, + TERM: 'xterm-256color', + COLORTERM: 'truecolor' + } + }) + } else { + forwardProcess = spawn(executablePath, args, { + stdio: ['pipe', 'pipe', 'pipe'], + cwd: session.workingDirectory, + env: { + ...process.env, + TERM: 'xterm-256color', + COLORTERM: 'truecolor' + }, + detached: os.platform() !== 'win32' // 在非Windows平台创建独立进程组 + }) + } session.streamForwardProcess = forwardProcess @@ -728,14 +749,16 @@ export class TerminalManager { sessionId: session.id, data: exitMessage }) - - // 如果配置了自动关闭,且转发进程异常退出,则关闭整个终端会话 - if (session.autoCloseOnForwardExit && (code !== 0 || signal)) { + + session.streamForwardProcess = undefined + + // 如果配置了自动关闭,则在转发进程退出后关闭终端会话 + if (session.autoCloseOnForwardExit) { session.socket.emit('terminal-output', { sessionId: session.id, - data: `\r\n[转发进程异常退出,正在关闭终端会话...]\r\n` + data: `\r\n[转发进程已退出,正在关闭终端会话...]\r\n` }) - + // 延迟关闭,让用户看到消息 setTimeout(() => { this.closePty(session.socket, { sessionId: session.id }) @@ -749,8 +772,6 @@ export class TerminalManager { }) } } - - session.streamForwardProcess = undefined }) // 处理转发进程错误 @@ -805,7 +826,8 @@ export class TerminalManager { } // 如果会话之前断开连接,现在重新连接 - if (session.disconnected) { + // 仅当传入的是真实的Socket.IO socket时才替换(避免虚拟socket覆盖) + if (session.disconnected && (socket as any).connected !== undefined) { session.disconnected = false session.disconnectedAt = undefined session.socket = socket