筛选出Python子流程模块中需要终端的命令
我正在开发一个机器人,它接受来自网络(XMPP)的命令,并使用Python中的子流程模块来执行这些命令,并发送回命令的输出。本质上,它是一个类似于SSH的基于XMPP的非交互式shell 机器人只执行来自经过验证的可信源的命令,因此允许使用任意shell命令(筛选出Python子流程模块中需要终端的命令,python,terminal,subprocess,tty,Python,Terminal,Subprocess,Tty,我正在开发一个机器人,它接受来自网络(XMPP)的命令,并使用Python中的子流程模块来执行这些命令,并发送回命令的输出。本质上,它是一个类似于SSH的基于XMPP的非交互式shell 机器人只执行来自经过验证的可信源的命令,因此允许使用任意shell命令(shell=True) 然而,当我不小心发送了一些需要tty的命令时,机器人被卡住了 例如: subprocess.check_output(['vim'], shell=False) subprocess.check_output('vi
shell=True
)
然而,当我不小心发送了一些需要tty的命令时,机器人被卡住了
例如:
subprocess.check_output(['vim'], shell=False)
subprocess.check_output('vim', shell=True)
如果收到上述每一个命令,则机器人会被卡住,运行机器人的终端也会断开
虽然机器人只接收来自经过验证的可信来源的命令,但人类会犯错误。我怎样才能让机器人过滤掉那些会自动中断的命令呢?我知道有os.isatty
,但我如何利用它呢?有没有办法检测那些“坏”命令并拒绝执行它们
TL;博士:
例如,有两种命令:
- 像
:这样的命令不需要tty来运行ls
- 像
这样的命令:需要tty;如果未给出tty,则中断子进程vim
我怎么知道一个命令是
ls
-like还是vim
-like,如果它是vim
-like,我会拒绝运行该命令?好吧,SSH已经是一个允许用户运行的工具。身份验证非常棘手,请注意,从安全角度来看,构建您描述的软件有点风险
没有办法确定进程是否需要tty。而且没有os.isatty
方法,因为如果您运行的子进程需要一个,并不意味着就有一个。:)
一般来说,如果你考虑一个白色的命令列表,那么从安全的角度来看,解决这个问题可能更安全。你可以选择那个白名单来避免需要tty的事情,因为我认为你不会轻易摆脱这个问题 非常感谢@J.F.Sebastia的帮助(参见问题下方的评论),我已经为我的案例找到了解决方案(变通?)
vim
断开端子而ls
断开端子的原因是vim
需要tty。正如Sebastia所说,我们可以使用pty.openpty()
为vim提供一个pty。输入pty保证命令不会破坏终端,我们可以添加一个timout
来自动终止这些进程。以下是(脏的)工作示例:
#!/usr/bin/env python3
import pty
from subprocess import STDOUT, check_output, TimeoutExpired
master_fd, slave_fd = pty.openpty()
try:
output1 = check_output(['ls', '/'], stdin=slave_fd, stderr=STDOUT, universal_newlines=True, timeout=3)
print(output1)
except TimeoutExpired:
print('Timed out')
try:
output2 = check_output(['vim'], stdin=slave_fd, stderr=STDOUT, universal_newlines=True, timeout=3)
print(output2)
except TimeoutExpired:
print('Timed out')
#!/usr/bin/env蟒蛇3
进口公司
从子流程导入标准输出中,检查\u输出,TimeoutExpired
主线程,从线程=pty.openpty()
尝试:
output1=检查输出(['ls','/',stdin=slave\u fd,stderr=STDOUT,universal\u newlines=True,timeout=3)
打印(输出1)
除TimeoutExpired外:
打印('超时')
尝试:
output2=检查输出(['vim'],stdin=slave\u fd,stderr=STDOUT,universal\u newlines=True,timeout=3)
打印(输出2)
除TimeoutExpired外:
打印('超时')
请注意,我们需要处理的是stdin,而不是stdout或stderr。您期望的是一个函数,它接收命令作为输入,并通过运行命令返回有意义的输出 由于该命令是任意的,对tty的要求只是可能发生的许多不好的情况之一(其他包括运行无限循环),您的函数应该只关注它的运行周期,换句话说,一个命令是否“坏”应该由它是否在有限的时间内结束来决定,由于
子流程
本质上是异步的,因此您可以运行该命令并在更高的视野中处理它
要播放演示代码,您可以更改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\u output
来运行命令,不像check\u output
会等待进程结束,Popen
会立即返回,因此我们可以做进一步的事情来控制进程vim
永远不会结束,它肯定会作为一个“毫无意义”的命令被杀死Popen
对象的communicate
方法来获取命令的stdout和stderr,然后您可以选择返回给用户的内容不需要tty模拟,我们应该异步运行子进程,然后用定时器控制它,以确定是否应该终止它,对于那些正常结束的子进程,它安全且易于获得输出。您可以参考我在中的答案:,它使用伪终端使stdout无阻塞,并在句柄stdin/stdout中使用select 我可以将
命令
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)
为什么要使用
检查输出()
?您需要捕获任意命令的标准输出吗?我会尝试你也可以@J.F.Sebastian提供一个伪tty命令似乎是个好主意,但我没有成功尝试这个。我不熟悉fds,像ls
和vim
这样的命令在fds方面有什么不同吗?我可以从他们的fds中判断命令是否“坏”(需要tty)吗?关键是,对于好的、坏的和丑陋的命令,您使用相同的pty
启用的代码。我提供了几个工作代码示例的链接,这些示例演示了如何从子流程读取输出,如果子流程的标准流连接起来,则可能会改变其行为