Paramiko 执行大 wget 命令时挂起
你好,我在一个Ubuntu 10的服务器上执行一个命令时遇到了问题,这个命令是用来下载一个100MB的文件。其他短一点的命令都能正常工作,只有这个不行。下面的代码块展示了我如何使用paramiko库,以及我尝试解决这个问题的不同方法(可以查看不同的run或exec方法)。在exec_cmd这个方法中,执行到这一行时就卡住了:
out = self.in_buffer.read(nbytes, self.timeout)
这是来自paramiko库的channel.py模块中的recv方法。
而同样的wget命令在Mac上用普通的ssh工具在终端中执行时却完全没问题。
"""
Management of SSH connections
"""
import logging
import os
import paramiko
import socket
import time
import StringIO
class SSHClient():
def __init__(self):
self._ssh_client = paramiko.SSHClient()
self._ssh_client.load_system_host_keys()
self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.time_out = 300
self.wait = 5
def connect(self, hostname, user, pkey):
retry = self.time_out
self.hostname = hostname
logging.info("connecting to:%s user:%s key:%s" % (hostname, user, pkey))
while retry > 0:
try:
self._ssh_client.connect(hostname,
username=user,
key_filename=os.path.expanduser(pkey),
timeout=self.time_out)
return
except socket.error, (value,message):
if value == 61 or value == 111:
logging.warning('SSH Connection refused, will retry in 5 seconds')
time.sleep(self.wait)
retry -= self.wait
else:
raise
except paramiko.BadHostKeyException:
logging.warning("%s has an entry in ~/.ssh/known_hosts and it doesn't match" % self.server.hostname)
logging.warning('Edit that file to remove the entry and then try again')
retry = 0
except EOFError:
logging.warning('Unexpected Error from SSH Connection, retry in 5 seconds')
time.sleep(self.wait)
retry -= self.wait
logging.error('Could not establish SSH connection')
def exists(self, path):
status = self.run('[ -a %s ] || echo "FALSE"' % path)
if status[1].startswith('FALSE'):
return 0
return 1
def shell(self):
"""
Start an interactive shell session on the remote host.
"""
channel = self._ssh_client.invoke_shell()
interactive_shell(channel)
def run(self, command):
"""
Execute a command on the remote host. Return a tuple containing
an integer status and a string containing all output from the command.
"""
logging.info('running:%s on %s' % (command, self.hostname))
log_fp = StringIO.StringIO()
status = 0
try:
t = self._ssh_client.exec_command(command)
except paramiko.SSHException:
logging.error("Error executing command: " + command)
status = 1
log_fp.write(t[1].read())
log_fp.write(t[2].read())
t[0].close()
t[1].close()
t[2].close()
logging.info('output: %s' % log_fp.getvalue())
return (status, log_fp.getvalue())
def run_pty(self, command):
"""
Execute a command on the remote host with a pseudo-terminal.
Returns a string containing the output of the command.
"""
logging.info('running:%s on %s' % (command, self.hostname))
channel = self._ssh_client.get_transport().open_session()
channel.get_pty()
status = 0
try:
channel.exec_command(command)
except:
logging.error("Error executing command: " + command)
status = 1
return status, channel.recv(1024)
def close(self):
transport = self._ssh_client.get_transport()
transport.close()
def run_remote(self, cmd, check_exit_status=True, verbose=True, use_sudo=False):
logging.info('running:%s on %s' % (cmd, self.hostname))
ssh = self._ssh_client
chan = ssh.get_transport().open_session()
stdin = chan.makefile('wb')
stdout = chan.makefile('rb')
stderr = chan.makefile_stderr('rb')
processed_cmd = cmd
if use_sudo:
processed_cmd = 'sudo -S bash -c "%s"' % cmd.replace('"', '\\"')
chan.exec_command(processed_cmd)
result = {
'stdout': [],
'stderr': [],
}
exit_status = chan.recv_exit_status()
result['exit_status'] = exit_status
def print_output():
for line in stdout:
result['stdout'].append(line)
logging.info(line)
for line in stderr:
result['stderr'].append(line)
logging.info(line)
if verbose:
print processed_cmd
print_output()
return exit_status,result
def exec_cmd(self, cmd):
import select
ssh = self._ssh_client
channel = ssh.get_transport().open_session()
END = "CMD_EPILOGqwkjidksjk58754dskhjdksjKDSL"
cmd += ";echo " + END
logging.info('running:%s on %s' % (cmd, self.hostname))
channel.exec_command(cmd)
out = ""
buf = ""
while END not in buf:
rl, wl, xl = select.select([channel],[],[],0.0)
if len(rl) > 0:
# Must be stdout
buf = channel.recv(1024)
logging.info(buf)
out += buf
return 0, out
2 个回答
2
- 在这种情况下,我建议使用列表追加然后再合并。为什么呢?因为在Python中,字符串是不可变的。这意味着每次你使用
+=
时,实际上是在创建两个新字符串,并且还要读取一个第三个字符串。而如果你先创建一个列表并往里面添加内容,就能减少创建字符串的数量。 - 你真的需要多次调用select吗?我的理解是,你并不太在乎这个过程是否会阻塞线程。因为
select
基本上是对同名C方法的一个封装:select() 和 pselect() 允许程序监控多个文件描述符,等待其中一个或多个文件描述符变得“准备好”进行某种输入输出操作(比如可以输入)。如果可以在不阻塞的情况下执行相应的输入输出操作(例如,read(2)),那么这个文件描述符就被认为是准备好的。
- 你的代码中并没有监听
socket.timeout
异常。 - 写入标准输出或文件系统可能会很耗资源,但你却在记录每一行
recv
返回的内容。你能把日志记录的那行代码移动一下吗? - 你有没有考虑手动处理通道的读取?实际上,你只需要的代码是:
try:
out = self.in_buffer.read(nbytes, self.timeout)
except PipeTimeout, e:
# do something with error
虽然不能保证,但这样可以减少额外的处理。
4
我也遇到过同样的问题,我的Python脚本在远程SSH客户端运行一个shell脚本时卡住了,因为那个脚本在下载一个400MB的文件。
我发现给wget命令加上一个超时时间解决了这个问题。
最开始我用的是:
wget http://blah:8888/file.zip
现在我改成了:
wget -q -T90 http://blah:8888/file.zip
这样就顺利多了!
希望这对你有帮助。