在 Python 子进程模块中过滤掉需要终端的命令
Posted
技术标签:
【中文标题】在 Python 子进程模块中过滤掉需要终端的命令【英文标题】:Filter out command that needs a terminal in Python subprocess module 【发布时间】:2015-07-20 06:55:53 【问题描述】:我正在开发一个机器人,它接受来自网络 (XMPP) 的命令并使用 Python 中的子进程模块来执行它们并发送回命令的输出。本质上,它是一个类似 SSH 的基于 XMPP 的非交互式 shell。
机器人只执行来自经过验证的可信来源的命令,因此允许任意 shell 命令 (shell=True
)。
但是,当我不小心发送了一些需要 tty 的命令时,机器人就卡住了。
例如:
subprocess.check_output(['vim'], shell=False)
subprocess.check_output('vim', shell=True)
如果收到以上每条命令,机器人就会卡住,机器人运行的终端就坏了。
虽然机器人只接收来自经过验证的可信来源的命令,但人类会犯错。我怎样才能让机器人过滤掉那些会破坏自己的命令?我知道有os.isatty
,但我怎么能利用它呢?有没有办法检测那些“坏”的命令并拒绝执行?
TL;DR:
比如说,有两种命令:
ls
之类的命令:不需要 tty 即可运行。
像vim
这样的命令:需要一个tty;如果没有给出 tty,则中断子进程。
我怎么知道一个命令是ls
-like 还是vim
-like 并且如果它是vim
-like 则拒绝运行该命令?
【问题讨论】:
你为什么使用check_output()
?您是否需要捕获任意命令的标准输出?我会尝试provide pseudo-tty whether the subprocess requires it or not using pexpect
or pty.spawn()
你也可以use pty
with subprocess
(for greater flexibility)
@J.F.Sebastian 为命令提供伪 tty 似乎是个好主意,但我没有成功尝试。我对 fds 不熟悉,ls
和 vim
这样的命令在 fds 方面有什么区别吗?我可以从他们的 fds 中判断命令是否“坏”(需要 tty)吗?
重点是您使用相同 pty
-enabled 代码来处理好、坏和丑陋的命令。我已经提供了几个工作代码示例的链接,这些示例展示了如何从子进程读取输出,如果其标准流连接到终端,该子进程可能会改变其行为。如果您运行任何示例但它们失败了,请在相应答案下留下评论。
.. 或用the minimal complete code example 更新您的问题,显示问题(提及您期望发生什么以及会发生什么)。
【参考方案1】:
您期望的是一个函数,它接收命令作为输入,并通过运行命令返回有意义的输出。
由于命令是任意的,对 tty 的要求只是可能发生的许多不良情况之一(其他包括运行无限循环),您的函数应该只关心它的运行时间,换句话说,一个命令是“坏的”与否应该由是否在有限的时间内结束来确定,并且由于subprocess
本质上是异步的,您只需运行命令并在更高的视野中处理它。
要播放的演示代码,您可以更改cmd
的值,看看它的表现有何不同:
#!/usr/bin/env python
# coding: utf-8
import time
import subprocess
from subprocess import PIPE
#cmd = ['ls']
#cmd = ['sleep', '3']
cmd = ['vim', '-u', '/dev/null']
print 'call cmd'
p = subprocess.Popen(cmd, shell=True,
stdin=PIPE, stderr=PIPE, stdout=PIPE)
print 'called', p
time_limit = 2
timer = 0
time_gap = 0.2
ended = False
while True:
time.sleep(time_gap)
returncode = p.poll()
print 'process status', returncode
timer += time_gap
if timer >= time_limit:
print 'timeout, kill process'
p.kill()
break
if returncode is not None:
ended = True
break
if ended:
print 'process ended by', returncode
print 'read'
out, err = p.communicate()
print 'out', repr(out)
print 'error', repr(err)
else:
print 'process failed'
以上代码有三点值得注意:
我们使用Popen
而不是check_output
来运行命令,不像check_output
会等待进程结束,Popen
立即返回,因此我们可以做进一步的事情来控制进程.
我们实现了一个计时器来检查进程的状态,如果它运行的时间过长,我们手动杀死它,因为我们认为如果一个进程不能在有限的时间内结束,它就没有意义。这样你的原始问题就会得到解决,因为vim
永远不会结束,它肯定会被当作“无意义”的命令杀死。
在定时器帮助我们过滤掉坏命令后,我们可以通过调用Popen
对象的communicate
方法来获取命令的stdout和stderr,然后由你选择返回什么用户。
结论
不需要tty模拟,我们应该异步运行子进程,然后通过定时器控制它来决定是否应该杀死它,对于那些正常结束的,它安全且易于获得输出。
【讨论】:
很好的示范!我没有意识到check_output
是同步的,而底层Popen
是异步的。我会接受这个作为答案。谢谢!【参考方案2】:
嗯,SSH 已经是一个允许用户remotely execute commands and be authenticated at the same time 的工具。身份验证非常棘手,请注意构建您描述的软件从安全角度来看有点风险。
没有办法确定进程是否需要 tty。并且没有os.isatty
方法,因为如果您运行需要一个的子流程并不意味着有一个。 :)
一般而言,从安全角度来看,如果您考虑使用命令白名单,它可能会更安全,并且也是解决此问题的方法。您可以选择该白名单来避免需要 tty 的事情,因为我认为您不会轻易解决这个问题。
【讨论】:
OpenSSH 或其他交互式远程 shell 在我的情况下由于各种原因不是一个选项。该机器人基于 XMPP。它旨在接收聊天消息中的命令,执行它们,然后发回它们的输出。 另外,Shell=False
的白名单不是一个选项,因为管道、重定向和其他 shell 功能不可用。为接收到的命令重建管道让我很头疼。【参考方案3】:
非常感谢@J.F.在 Sebastia 的帮助下(请参阅问题下的 cmets),我找到了适合我的情况的解决方案(解决方法?)。
vim
中断终端而ls
不中断的原因是vim
需要一个 tty。正如 Sebastia 所说,我们可以使用 pty.openpty()
为 vim 提供 pty。提供 pty 保证该命令不会破坏终端,我们可以添加 timout
来自动终止此类进程。这是(肮脏的)工作示例:
#!/usr/bin/env python3 进口pty from subprocess import STDOUT, check_output, TimeoutExpired master_fd, slave_fd = pty.openpty() 尝试: output1 = check_output(['ls', '/'], stdin=slave_fd, stderr=STDOUT, universal_newlines=True, timeout=3) 打印(输出1) 除了 TimeoutExpired: print('超时') 尝试: output2 = check_output(['vim'], stdin=slave_fd, stderr=STDOUT, universal_newlines=True, timeout=3) 打印(输出2) 除了 TimeoutExpired: print('超时')
请注意,我们需要处理的是标准输入,而不是标准输出或标准错误。
【讨论】:
timeout
for subprocess 是 Python 3 中的一个新特性,为了更好的兼容性,您可以自己实现一个简单的计时器。
对 Python2.x 的子进程超时使用 pypi.python.org/pypi/subprocess32。它是 Python 3.x 的向后移植【参考方案4】:
你可以参考我的答案:https://***.com/a/43012138/3555925,它使用伪终端使stdout无阻塞,并在句柄stdin/stdout中使用select。
我可以将command
var 修改为'vim'
。脚本运行良好。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import select
import termios
import tty
import pty
from subprocess import Popen
command = 'vim'
# save original tty setting then set it to raw mode
old_tty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())
# open pseudo-terminal to interact with subprocess
master_fd, slave_fd = pty.openpty()
# use os.setsid() process the leader of a new session, or bash job control will not be enabled
p = Popen(command,
preexec_fn=os.setsid,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
universal_newlines=True)
while p.poll() is None:
r, w, e = select.select([sys.stdin, master_fd], [], [])
if sys.stdin in r:
d = os.read(sys.stdin.fileno(), 10240)
os.write(master_fd, d)
elif master_fd in r:
o = os.read(master_fd, 10240)
if o:
os.write(sys.stdout.fileno(), o)
# restore tty settings back
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
【讨论】:
以上是关于在 Python 子进程模块中过滤掉需要终端的命令的主要内容,如果未能解决你的问题,请参考以下文章
Bash命令在终端中有效,但在python脚本中的“子进程”中无效[重复]