基于Django的在线Python编译器:Subprocess捕获STDIN输入问题
解决Django在线Python编译器中input提示流式传输与用户输入交互问题
原来的流式HTTP响应只能单向传输数据,没法接收用户输入,所以要实现input语句的完整交互,必须改用WebSocket双向通信。以下是具体实现步骤:
后端实现(基于Django Channels)
- 先安装Django Channels并配置ASGI服务(确保项目支持异步WebSocket)
- 编写WebSocket Consumer处理子进程的IO与客户端通信:
import subprocess import json from channels.generic.websocket import AsyncWebsocketConsumer import asyncio class CompilerConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() # 从路由参数获取待执行的Python代码 code = self.scope['url_route']['kwargs']['code'] # 启动异步子进程,开启管道用于stdin/stdout/stderr交互 self.process = await asyncio.create_subprocess_exec( 'python', '-c', code, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, text=True # 以文本模式处理流,避免字节流转换麻烦 ) # 启动两个异步任务,分别读取标准输出和错误输出 asyncio.create_task(self._read_stream(self.process.stdout, 'stdout')) asyncio.create_task(self._read_stream(self.process.stderr, 'stderr')) async def _read_stream(self, stream, stream_type): """实时读取子进程输出并发送给客户端""" while True: line = await stream.readline() if not line: # 流结束,退出循环 break # 将输出内容发送给客户端,带上类型标记 await self.send(text_data=json.dumps({ 'type': stream_type, 'content': line.rstrip('\n') # 去掉换行符,前端自行处理显示 })) # 子进程结束后发送退出信号 await self.send(text_data=json.dumps({'type': 'exit'})) async def receive(self, text_data): """接收客户端发送的用户输入,写入子进程stdin""" data = json.loads(text_data) if data.get('type') == 'input': # 输入内容末尾加换行符,模拟用户回车操作 self.process.stdin.write(f"{data['content']}\n") await self.process.stdin.drain() # 确保数据写入管道 async def disconnect(self, close_code): """客户端断开连接时终止子进程""" if hasattr(self, 'process'): self.process.terminate() await self.process.wait()
前端实现
前端需要建立WebSocket连接,根据后端发送的消息类型处理输出,在收到input提示时显示输入框,收集用户输入后发送回后端:
<div id="terminal" style="padding: 10px; background: #f5f5f5; font-family: monospace;"></div> <input type="text" id="input-box" style="display: none; margin-top: 10px; padding: 5px; width: 100%;"> <script> const terminal = document.getElementById('terminal'); const inputBox = document.getElementById('input-box'); // 替换成实际的WebSocket路由,这里假设code_id是代码的唯一标识 const ws = new WebSocket(`ws://${window.location.host}/ws/compiler/your-code-id/`); ws.onmessage = function(event) { const data = JSON.parse(event.data); const outputLine = document.createElement('div'); if (data.type === 'stdout') { outputLine.textContent = data.content; terminal.appendChild(outputLine); // 检测是否是input提示(这里简单通过包含": "判断,可根据实际场景调整逻辑) if (data.content.includes(': ')) { inputBox.style.display = 'block'; inputBox.focus(); // 监听回车事件,发送输入内容 inputBox.onkeydown = function(e) { if (e.key === 'Enter') { ws.send(JSON.stringify({ 'type': 'input', 'content': inputBox.value })); inputBox.value = ''; inputBox.style.display = 'none'; inputBox.onkeydown = null; } }; } } else if (data.type === 'stderr') { // 错误输出用红色显示 outputLine.style.color = '#dc3545'; outputLine.textContent = data.content; terminal.appendChild(outputLine); } else if (data.type === 'exit') { const exitMsg = document.createElement('div'); exitMsg.textContent = '代码执行结束'; exitMsg.style.marginTop = '10px'; exitMsg.style.color = '#6c757d'; terminal.appendChild(exitMsg); ws.close(); } }; ws.onclose = function() { const closeMsg = document.createElement('div'); closeMsg.textContent = '连接已断开'; closeMsg.style.marginTop = '10px'; closeMsg.style.color = '#6c757d'; terminal.appendChild(closeMsg); }; </script>
关键说明
- 为什么不用原来的流式HTTP?因为HTTP是单向协议,服务器无法主动接收客户端的输入,必须用WebSocket这种双向通信协议才能完成"服务器发提示→客户端传输入→服务器写stdin"的闭环。
- 子进程必须用异步方式启动并读取流,避免阻塞Django服务的其他请求。
- 前端判断input提示的逻辑可以根据实际情况优化,比如后端在发送提示时额外标记类型,前端直接识别标记更可靠。
内容的提问来源于stack exchange,提问作者Winmari Manzano
相关产品推荐
相关产品推荐

