使用子进程时处理键盘中断

时间:2014-05-23 10:20:57

标签: python subprocess

我有一个名为monitiq_install.py的python脚本,它使用subprocess python模块调用其他脚本(或模块)。但是,如果用户发送键盘中断(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 个答案:

答案 0 :(得分:9)

如果在终端中按 Ctrl + C ,则会将SIGINT发送到进程组内的所有进程。请参阅child process receives parent's SIGINT

这就是为什么你看到来自子进程的回溯,尽管在父进程中尝试/除了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

答案 1 :(得分:2)

您可以使用try执行此操作,但以下情况除外:

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作为最佳安全措施。

答案 2 :(得分:1)

这段代码会产生一个子进程,并像 shell (bash, zsh, ...) 一样向他们传递信号,比如 SIGINT,...。

这意味着 KeyboardInterrupt 不再被 Python 进程看到,但是子进程收到它并被正确杀死。

它的工作原理是在 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'])