Docker容器中跨脚本访问子进程无响应问题求助
我用Script 1作为Docker容器的入口点,通过asyncio.create_subprocess_exec启动游戏服务器子进程,同时监听控制台输入维持服务器运行。另外有Script 2作为API,接收外部命令后通过docker attach尝试向该子进程的stdin写入命令。API返回200状态码,但子进程完全无响应;手动执行docker attach并输入命令时,子进程响应正常。怀疑和PTY(伪终端)有关,寻求帮助。
Script 1 核心代码
import asyncio import aioconsole import sys def console_print(msg): # 自定义打印函数 print(msg) async def running_server_inpipe(game_server: asyncio.subprocess.Process): while True: line = await aioconsole.ainput("Enter Command: ") game_server.stdin.write(line.encode('utf-8') + b"\n") await game_server.stdin.drain() console_print("INPIPE - Command Sent: " + line) async def running_server_outpipe(game_server: asyncio.subprocess.Process): async for out_line in game_server.stdout: console_print("OUTPIPE - Server Says: " + out_line.decode('utf-8', 'replace')) async def running_server_errpipe(game_server: asyncio.subprocess.Process): async for err_line in game_server.stderr: console_print("ERRORPIPE - Server Error: " + err_line.decode('utf-8', 'replace')) async def main(): run_command = ["your-game-server-command"] # 替换为实际启动命令 server_start_string = "Server started" # 替换为服务器启动完成的标识字符串 game_server = await asyncio.create_subprocess_exec(*run_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) async for startup_out_line in game_server.stdout: console_print(startup_out_line.decode()) if server_start_string in startup_out_line.decode('utf-8', 'replace'): print("Server Started") break async with asyncio.TaskGroup() as tg: in_task = tg.create_task(running_server_inpipe(game_server)) out_task = tg.create_task(running_server_outpipe(game_server)) err_task = tg.create_task(running_server_errpipe(game_server)) retcode = await game_server.wait() if retcode == 0: print("Server exited normally") else: print(f"Error: server exited with code {retcode}") in_task.cancel() out_task.cancel() err_task.cancel() sys.exit(0 if retcode == 0 else 1) if __name__ == "__main__": asyncio.run(main())
Script 2 核心代码
from flask import Flask, request, jsonify import subprocess API1 = Flask(__name__) @API1.route('/quit', methods=['POST']) def receive_quit_data(): data = request.get_json() try: server = data['server'] except KeyError: return jsonify({'status': 'No Server Provided'}) engage_container = ["docker", "attach", f"{server}"] container_process = subprocess.Popen(engage_container, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True) container_process.communicate(input="quit\n") container_process.wait() if container_process.return_code == 0: return jsonify({'status': 'Command Sent'}) return jsonify({'status': 'Command Failed'}) if __name__ == "__main__": API1.run()
问题原因
手动执行docker attach时,Docker会自动为容器分配PTY(伪终端),游戏服务器的终端会话能识别来自PTY的输入。但通过subprocess.Popen调用docker attach时,默认不会分配PTY,导致输入的命令无法被服务器识别。同时,Script 1中启动游戏服务器时未分配PTY,也会让服务器忽略非终端来源的输入。
修复方案
方案1:修改Script 2,强制分配PTY
在调用docker attach时添加-it参数,并让subprocess使用PTY作为标准输入:
engage_container = ["docker", "attach", "-it", f"{server}"] container_process = subprocess.Popen( engage_container, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.PTY, text=True )
-it参数强制Docker分配PTY,stdin=subprocess.PTY让subprocess模拟终端输入环境。
方案2:修改Script 1,为游戏服务器分配PTY
启动游戏服务器时通过PTY创建终端会话,确保服务器能接收终端输入:
import pty import os import asyncio async def main(): run_command = ["your-game-server-command"] server_start_string = "Server started" # 创建PTY主从设备 master_fd, slave_fd = pty.openpty() # 启动子进程,绑定slave端作为标准输入输出 game_server = await asyncio.create_subprocess_exec( *run_command, stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, start_new_session=True ) os.close(slave_fd) # 关闭slave端,子进程已继承 # 将master_fd包装为asyncio流 reader, writer = await asyncio.open_unix_connection(fd=master_fd) # 读取启动输出 while True: line = await reader.readline() if not line: break output = line.decode('utf-8', 'replace') console_print(output) if server_start_string in output: print("Server Started") break async def running_server_inpipe(): while True: line = await aioconsole.ainput("Enter Command: ") writer.write((line + "\n").encode('utf-8')) await writer.drain() console_print("INPIPE - Command Sent: " + line) async def running_server_outpipe(): while True: line = await reader.readline() if not line: break console_print("OUTPIPE - Server Says: " + line.decode('utf-8', 'replace')) async with asyncio.TaskGroup() as tg: in_task = tg.create_task(running_server_inpipe()) out_task = tg.create_task(running_server_outpipe()) retcode = await game_server.wait() if retcode == 0: print("Server exited normally") else: print(f"Error: server exited with code {retcode}") in_task.cancel() out_task.cancel() sys.exit(0 if retcode == 0 else 1)
这种方式让游戏服务器运行在PTY环境中,无论输入来自手动docker attach还是API调用的docker attach,都能被识别。
方案3:替代方案——使用docker exec而非docker attach
如果游戏服务器支持通过向进程标准输入写入命令,可直接用docker exec在容器内执行写入操作,无需依赖PTY交互:
# Script 2中替换engage_container和执行逻辑 engage_container = ["docker", "exec", f"{server}", "sh", "-c", "echo 'quit' > /proc/$(pidof your-game-server-process)/fd/0"] subprocess.run(engage_container, check=True) return jsonify({'status': 'Command Sent'})
需要替换your-game-server-process为游戏服务器进程的名称,确保能通过pidof找到进程ID。
内容的提问来源于stack exchange,提问作者R4pt0r

