使用特定信号终止通过asyncio运行的外部程序

时间:2017-01-08 18:19:11

标签: python python-3.x python-asyncio

我需要终止从具有特定信号的asyncio Python脚本运行的外部程序,比如SIGTERM。我的问题是程序总是接收SIGINT,即使我发送SIGTERM信号。

以下是测试用例,可以找到以下测试中使用的fakeprg的源代码here

import asyncio
import traceback
import os
import os.path
import sys
import time
import signal
import shlex

from functools import partial


class ExtProgramRunner:
    run = True
    processes = []

    def __init__(self):
        pass

    def start(self, loop):
        self.current_loop = loop
        self.current_loop.add_signal_handler(signal.SIGINT, lambda: asyncio.async(self.stop('SIGINT')))
        self.current_loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.async(self.stop('SIGTERM')))
        asyncio.async(self.cancel_monitor())
        asyncio.Task(self.run_external_programs())

    @asyncio.coroutine
    def stop(self, sig):
        print("Got {} signal".format(sig))
        self.run = False
        for process in self.processes:
            print("sending SIGTERM signal to the process with pid {}".format(process.pid))
            process.send_signal(signal.SIGTERM)
        print("Canceling all tasks")
        for task in asyncio.Task.all_tasks():
            task.cancel()

    @asyncio.coroutine
    def cancel_monitor(self):
        while True:
            try:
                yield from asyncio.sleep(0.05)
            except asyncio.CancelledError:
                break
        print("Stopping loop")
        self.current_loop.stop()

    @asyncio.coroutine
    def run_external_programs(self):
        os.makedirs("/tmp/files0", exist_ok=True)
        os.makedirs("/tmp/files1", exist_ok=True)
        # schedule tasks for execution
        asyncio.Task(self.run_cmd_forever("/tmp/fakeprg /tmp/files0 1000"))
        asyncio.Task(self.run_cmd_forever("/tmp/fakeprg /tmp/files1 5000"))

    @asyncio.coroutine
    def run_cmd_forever(self, cmd):
        args = shlex.split(cmd)
        while self.run:
            process = yield from asyncio.create_subprocess_exec(*args)
            self.processes.append(process)
            exit_code = yield from process.wait()
            for idx, p in enumerate(self.processes):
                if process.pid == p.pid:
                    self.processes.pop(idx)
            print("External program '{}' exited with exit code {}, relauching".format(cmd, exit_code))


def main():
    loop = asyncio.get_event_loop()

    try:
        daemon = ExtProgramRunner()
        loop.call_soon(daemon.start, loop)

        # start main event loop
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    except asyncio.CancelledError as exc:
        print("asyncio.CancelledError")
    except Exception as exc:
        print(exc, file=sys.stderr)
        print("====", file=sys.stderr)
        print(traceback.format_exc(), file=sys.stderr)
    finally:
        print("Stopping daemon...")
        loop.close()


if __name__ == '__main__':
    main()

1 个答案:

答案 0 :(得分:1)

原因是:当你启动你的python程序(父)并启动它的进程/tmp/fakeprg(子)时,他们会得到所有不同的进程及其pid,但它们都是{{3} }。您的shell已绑定到此论坛,因此当您点击Ctrl-CSIGINT),Ctrl-YSIGTSTP)或Ctrl-\SIGQUIT)时它们被发送到前台进程组中的所有进程

在你的代码中,这发生在父母甚至可以通过send_signal向孩子发送信号之前,所以这条线路向已经死亡的过程发送信号(并且应该失败,所以IMO是一个问题与asyncio)。

要解决此问题,您可以将子进程明确地放入一个单独的进程组,如下所示:

asyncio.create_subprocess_exec(*args, preexec_fn=os.setpgrp)