为什么我的asyncio事件循环死亡当我杀死一个分叉子进程(PTY)?



我尝试创建一个生成bash shell的软件,并通过websockets使它们可控。

它基于fastapi和fastapi_socketio在服务器端和套接字。

必须承认,当涉及到asyncio时,我是一个绝对的新手。当我自己控制它时,我可以使用它,但我不熟悉管理事件循环等来自其他模块。

要启动PTY,我使用PTY模块中的fork()方法,如图"1"- fork PTY"(提交的命令为"/bin/bash"):

它实际上工作得很好。client_sid是套接字。我可以在我的web UI中通过xtermjs无缝地控制多个终端。

但我有一个问题。当我发出退出命令时;在xtermjs中,我希望子进程退出并释放文件描述符。这应该通过图2中显示的方法中的fstat方法来检测-该方法将pty的STDOUT/ERR发送到远程套接字;该方法应该退出并关闭websocket连接。

实际情况是,web终端以非常快的方式接收到多个异常(图"3 -显示给客户端的错误),当我试图用CTRL+C关闭uvicorn时,我得到图"4 -当我试图用CTRL+C关闭uvicorn时显示的错误。

我非常感谢任何关于这个主题的帮助,因为我对异步python(可能还有OS/pty)还没有足够深入的了解。

对我来说,这感觉就像从我的主进程分叉的子进程以某种方式与asyncio循环交互,但我真的不知道如何。子进程可能继承了asyncio循环并在它死亡时杀死它,这有意义吗?

我想到的唯一解决办法是检测"杀死"。从web UI发出的命令,但这会错过例如发送到PTY子进程的kill信号,并且它不是真正干净。

Thanks in regard.

1 - fork一个PTY

async def pty_handle_pty_config(self, sio: AsyncServer, client_sid: str, message: dict):

if not client_sid in self.clients or self.clients[client_sid] is None:
await self.disconnect_client(sio=sio, client_sid=client_sid)
return
if not isinstance(message, dict) or not 'command' in message or not isinstance(message['command'], str):
await self.disconnect_client(sio=sio, client_sid=client_sid)
return
child_pid, fd = fork() # pty.fork()
if child_pid == 0:
subproc_run(message['command']) # subprocess.run()
else:
self.ptys[client_sid] = {
'fd': fd
}
self.set_winsize(client_sid, 50, 50)
await sio.emit('pty_begin', data=dict(state='success'), namespace='/pty', room=client_sid)
sio.start_background_task(
target=self.pty_read_and_forward,
sio=sio,
client_sid=client_sid,
client_data=self.clients[client_sid]
)

2 -发送pty的STDOUT/ERR到远端套接字的方法

async def pty_read_and_forward(self, sio: AsyncServer, client_sid: str, client_data: dict):
log = get_logger()
max_read_bytes = 1024 * 20
loop = get_event_loop()
while True:
try:
await async_sleep(.05) # asyncio.sleep
timeout_sec = 0
(data_ready, _, _) = await loop.run_in_executor(None, select, [self.ptys[client_sid]['fd']], [], [], timeout_sec) 
if data_ready:
output = await loop.run_in_executor(None, os_read, self.ptys[client_sid]['fd'], max_read_bytes) # os.read
try:
fstat(self.ptys[client_sid]['fd']) # os.fstat
except OSError as exc:
log.error(exc)
break
await sio.emit(
event='pty_out',
data=dict(
output=output.decode('utf-8', errors='ignore')
),
namespace='/pty',
room=client_sid
)
except Exception as exc:
if not client_sid in self.clients:
log.info(f'PTY session closed [sid={client_sid};user={client_data["username"]}]')
else:
log.warn(f'PTY session closed unexpectedly [sid={client_sid};user={client_data["username"]}] - {excstr(exc)}')
break

3 -向客户端显示的错误

asyncio.exceptions.CancelledError
Process SpawnProcess-2:
Traceback (most recent call last):
File "/usr/lib/python3.10/multiprocessing/process.py", line 314, in _bootstrap
self.run()
File "/usr/lib/python3.10/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/usr/local/lib/python3.10/dist-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
target(sockets=sockets)
File "/usr/local/lib/python3.10/dist-packages/uvicorn/server.py", line 60, in run
return asyncio.run(self.serve(sockets=sockets))
File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/usr/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
return future.result()
File "/usr/local/lib/python3.10/dist-packages/uvicorn/server.py", line 80, in serve
await self.main_loop()
File "/usr/local/lib/python3.10/dist-packages/uvicorn/server.py", line 221, in main_loop
await asyncio.sleep(0.1)
File "/usr/lib/python3.10/asyncio/tasks.py", line 599, in sleep
loop = events.get_running_loop()
RuntimeError: no running event loop

4 -当我尝试用CTRL+C关闭uvicorn时显示的错误

Traceback (most recent call last):
File "/usr/lib/python3.10/asyncio/unix_events.py", line 42, in _sighandler_noop
def _sighandler_noop(signum, frame):
BlockingIOError: [Errno 11] Resource temporarily unavailable

我很幸运地通过调用os.execvpe()而不是使用subprocess.run()解决了这个问题。这种方法将子进程替换为一个全新的进程,同时保持与父进程能够读取的文件描述符的连接。

我真的不知道在父进程中使用asyncio时在子进程中使用fork()的后果。这是一个非常糟糕的方法,因为asyncio将无法将子事件循环与父事件循环分开。如果你关闭了孩子的电脑,父母的电脑也会因为"分享"而停止工作。他们的环境多亏了fork()。

在python中混合asyncio和fork:坏主意?它是这样工作的:

child_pid, fd = fork()
if child_pid == 0:
# This is the forked process. Replace it with the command
# the user wants to run.
env = os.environ.copy()
execvpe(message['command'], [message['command'],], env)

最新更新