当使用子进程时,如何处理键盘中断

13

我有一个名为monitiq_install.py的Python脚本,它使用subprocess模块调用其他脚本(或模块)。但是,如果用户发送了键盘中断信号(CTRL + C),它会退出,但是会抛出一个异常。我希望它能够优雅地退出。

我的代码:

import os
import sys
from os import listdir
from os.path import isfile, join
from subprocess import Popen, PIPE
import json

# Run a module and capture output and exit code
def runModule(module):
    try:
        # Run Module
        process = Popen(os.path.dirname(os.path.realpath(__file__)) + "/modules/" + module, shell=True, stdout=PIPE, bufsize=1)
        for line in iter(process.stdout.readline, b''):
            print line,

        process.communicate()
        exit_code = process.wait();

        return exit_code;
    except KeyboardInterrupt:
        print "Got keyboard interupt!";
        sys.exit(0);

我遇到的错误信息如下:

python monitiq_install.py -a
Invalid module filename: create_db_user_v0_0_0.pyc
Not Running Module: '3parssh_install' as it is already installed
######################################
Running Module: 'create_db_user' Version: '0.0.3'
Choose username for Monitiq DB User [MONITIQ]
^CTraceback (most recent call last):
  File "/opt/monitiq-universal/install/modules/create_db_user-v0_0_3.py", line 132, in <module>
    inputVal = raw_input("");
Traceback (most recent call last):
  File "monitiq_install.py", line 40, in <module>
KeyboardInterrupt
    module_install.runModules();
  File "/opt/monitiq-universal/install/module_install.py", line 86, in runModules
    exit_code = runModule(module);
  File "/opt/monitiq-universal/install/module_install.py", line 19, in runModule
    for line in iter(process.stdout.readline, b''):
KeyboardInterrupt
一个解决方案或一些指示将会很有帮助 :)

--编辑 使用try catch

Running Module: 'create_db_user' Version: '0.0.0'
Choose username for Monitiq DB User [MONITIQ]
^CGot keyboard interupt!
Traceback (most recent call last):
  File "monitiq_install.py", line 36, in <module>
    module_install.runModules();
  File "/opt/monitiq-universal/install/module_install.py", line 90, in runModules
    exit_code = runModule(module);
  File "/opt/monitiq-universal/install/module_install.py", line 29, in runModule
    sys.exit(0);
NameError: global name 'sys' is not defined
Traceback (most recent call last):
  File "/opt/monitiq-universal/install/modules/create_db_user-v0_0_0.py", line 132, in <module>
    inputVal = raw_input("");
KeyboardInterrupt
3个回答

23

如果在终端中按下Ctrl + C,则将向进程组中的所有进程发送SIGINT信号。请参见子进程接收父进程的SIGINT

这就是为什么您会看到子进程的回溯信息,尽管父进程中有try/except KeyboardInterrupt的原因。

您可以抑制子进程的stderr输出:stderr=DEVNULL。或者在新的进程组中启动它:start_new_session=True

import sys
from subprocess import call

try:
    call([sys.executable, 'child.py'], start_new_session=True)
except KeyboardInterrupt:
    print('Ctrl C')
else:
    print('no exception')
如果在上面的例子中删除start_new_session=True,则子进程中也可能会引发KeyboardInterrupt并且您可能会获得回溯信息。
如果subprocess.DEVNULL不可用,则可以使用DEVNULL = open(os.devnull, 'r+b', 0)。如果start_new_session参数不可用,则可以在POSIX上使用preexec_fn=os.setsid

2
您可以使用以下的try和except来实现此操作:
import subprocess
try:
    proc = subprocess.Popen("dir /S", shell=True,  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    while proc.poll() is None:
        print proc.stdout.readline()
except KeyboardInterrupt:
    print "Got Keyboard interrupt"

您可以避免在执行中使用shell=True,这是最佳的安全实践。

我之前没有考虑过使用shell=True可能存在的安全问题。谢谢! - woverton
看起来你正在try, except块中执行一些代码,同时还有一些代码在外面。能否展示一下你的代码? - venpa
当然是的,已添加到我的问题中。 - woverton
我认为@woverton在那个文件里使用了sys.exit(0)语句,但是没有导入sys模块,所以才会出现NameError:全局名称'sys'未定义的错误提示。 - Radek Simko
顺便提一下,这是Python语言,所以除非你将两个语句放在同一行,否则分号是无用的。 - Radek Simko
Sys 已被导入。修改了问题。感谢 Radek,我想我只是喜欢我的分号.. - woverton

1
这段代码会生成一个子进程,并像shell(bash、zsh等)一样向它们传递诸如SIGINT等的信号。这意味着Python进程不再看到KeyboardInterrupt,但子进程会收到并正确终止。
它通过在Python中设置新的前台进程组来运行进程来实现。
import os
import signal
import subprocess
import sys
import termios

def run_as_fg_process(*args, **kwargs):
    """
    the "correct" way of spawning a new subprocess:
    signals like C-c must only go
    to the child process, and not to this python.

    the args are the same as subprocess.Popen

    returns Popen().wait() value

    Some side-info about "how ctrl-c works":
    https://unix.stackexchange.com/a/149756/1321

    fun fact: this function took a whole night
              to be figured out.
    """

    old_pgrp = os.tcgetpgrp(sys.stdin.fileno())
    old_attr = termios.tcgetattr(sys.stdin.fileno())

    user_preexec_fn = kwargs.pop("preexec_fn", None)

    def new_pgid():
        if user_preexec_fn:
            user_preexec_fn()

        # set a new process group id
        os.setpgid(os.getpid(), os.getpid())

        # generally, the child process should stop itself
        # before exec so the parent can set its new pgid.
        # (setting pgid has to be done before the child execs).
        # however, Python 'guarantee' that `preexec_fn`
        # is run before `Popen` returns.
        # this is because `Popen` waits for the closure of
        # the error relay pipe '`errpipe_write`',
        # which happens at child's exec.
        # this is also the reason the child can't stop itself
        # in Python's `Popen`, since the `Popen` call would never
        # terminate then.
        # `os.kill(os.getpid(), signal.SIGSTOP)`

    try:
        # fork the child
        child = subprocess.Popen(*args, preexec_fn=new_pgid,
                                 **kwargs)

        # we can't set the process group id from the parent since the child
        # will already have exec'd. and we can't SIGSTOP it before exec,
        # see above.
        # `os.setpgid(child.pid, child.pid)`

        # set the child's process group as new foreground
        os.tcsetpgrp(sys.stdin.fileno(), child.pid)
        # revive the child,
        # because it may have been stopped due to SIGTTOU or
        # SIGTTIN when it tried using stdout/stdin
        # after setpgid was called, and before we made it
        # forward process by tcsetpgrp.
        os.kill(child.pid, signal.SIGCONT)

        # wait for the child to terminate
        ret = child.wait()

    finally:
        # we have to mask SIGTTOU because tcsetpgrp
        # raises SIGTTOU to all current background
        # process group members (i.e. us) when switching tty's pgrp
        # it we didn't do that, we'd get SIGSTOP'd
        hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
        # make us tty's foreground again
        os.tcsetpgrp(sys.stdin.fileno(), old_pgrp)
        # now restore the handler
        signal.signal(signal.SIGTTOU, hdlr)
        # restore terminal attributes
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_attr)

    return ret


# example:
run_as_fg_process(['openage', 'edit', '-f', 'random_map.rms'])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接