将Ctrl-C发送到通过subprocess.Popen和ssh启动的远程进程

时间:2011-01-12 13:16:16

标签: python unix ssh popen

如何向Ctrl-C个对象中的多个ssh -t进程发送Popen()个?

我有一些Python代码可以启动远程主机上的脚本:

# kickoff.py

# i call 'ssh' w/ the '-t' flag so that when i press 'ctrl-c', it get's
# sent to the script on the remote host.  otherwise 'ctrol-c' would just
# kill things on this end, and the script would still be running on the
# remote server
a = subprocess.Popen(['ssh', '-t', 'remote-host', './script.sh', 'a'])
a.communicate()

这很好用,但我需要启动远程主机上的多个脚本:

# kickoff.py

a = subprocess.Popen(['ssh', '-t', 'remote-host', './script.sh', 'a'])
b = subprocess.Popen(['ssh', '-t', 'remote-host', './script.sh', 'b'])
a.communicate()
b.communicate()

结果是Ctrl-C无法可靠地杀死所有内容,之后我的终端总是出现乱码(我必须运行'重置')。那么当主要文件被杀死时,如何杀死两个远程脚本呢?

注意:我正在尝试避免登录到远程主机,在进程列表中搜索“script.sh”,并向两个进程发送SIGINT。我只想在启动脚本上按Ctrl-C,然后杀死两个远程进程。一个不太理想的解决方案可能涉及确定性地找到远程脚本的PID,但我不知道如何在我当前的设置中这样做。

更新:在远程服务器上启动的脚本实际启动了几个子进程,虽然查找ssh确实会终止原始远程脚本(可能是SIGHUP的b / c),但子任务是没有被杀死。

4 个答案:

答案 0 :(得分:8)

我能够成功杀死所有子进程的唯一方法是使用pexpect

a = pexpect.spawn(['ssh', 'remote-host', './script.sh', 'a'])
a.expect('something')

b = pexpect.spawn(['ssh', 'remote-host', './script.sh', 'b'])
b.expect('something else')

# ...

# to kill ALL of the children
a.sendcontrol('c')
a.close()

b.sendcontrol('c')
b.close()

这足够可靠。我相信其他人早些时候发布了这个答案,但后来删除了答案,所以我会发帖,以防其他人好奇。

答案 1 :(得分:4)

当被杀死时,ssh会向远程进程发送一个SIGHUP。您可以将远程进程包装到shell或python脚本中,当脚本收到SIGHUP时会将其终止(请参阅bash的trap命令和python中的信号模块)

甚至可以使用膨胀的命令行而不是远程包装脚本来执行此操作。

问题是杀死远程进程并不是你想要的,你想要的是在你做Ctrl + C后有一个工作终端。要做到这一点,你必须杀死远程进程并查看剩余的输出,其中包含一些终端控制序列以将终端重置为正确的状态。为此,您需要一个机制来表示包装器脚本以终止进程。这不是一回事。

答案 2 :(得分:2)

我没试过这个,但也许你可以抓住一个KeyboardInterrupt然后杀死进程:

try
    a = subprocess.Popen(['ssh', '-t', 'remote-host', './script.sh', 'a'])
    b = subprocess.Popen(['ssh', '-t', 'remote-host', './script.sh', 'b'])
    a.communicate()
    b.communicate()
except KeyboardInterrupt:
    os.kill(a.pid, signal.SIGTERM)
    os.kill(b.pid, signal.SIGTERM)

答案 3 :(得分:1)

通过取消映射我关心的所有信号,我解决了类似的问题。当按下Ctrl + C时,它仍将被传递到子进程,但是Python将等到子进程退出之后再处理主脚本中的信号。只要子进程响应Ctrl + C,这对于信号子进程就可以正常工作。

class DelayedSignalHandler(object):
    def __init__(self, managed_signals):
        self.managed_signals = managed_signals
        self.managed_signals_queue = list()
        self.old_handlers = dict()

    def _handle_signal(self, caught_signal, frame):
        self.managed_signals_queue.append((caught_signal, frame))

    def __enter__(self):
        for managed_signal in self.managed_signals:
            old_handler = signal.signal(managed_signal, self._handle_signal)
            self.old_handlers[managed_signal] = old_handler

    def __exit__(self, *_):
        for managed_signal, old_handler in self.old_handlers.iteritems():
            signal.signal(managed_signal, old_handler)

        for managed_signal, frame in self.managed_signals_queue:
            self.old_handlers[managed_signal](managed_signal, frame)

现在,我的子流程代码如下所示:

    with DelayedSignalHandler((signal.SIGINT, signal.SIGTERM, signal.SIGHUP)):
        exit_value = subprocess.call(command_and_arguments)

每当按下Ctrl + C时,允许应用程序在处理信号之前退出,因此您不必担心终端会出现乱码,因为子进程线程没有与主进程线程同时终止