有个服务器端命令行工具,想直接放到web上执行,比如说mysql命令行,python里有多种实现方式。
subprocess
通过subprocess的popen方法创建一个子进程,然后向子进程发送输入,另起线程获取子进程的输出。
import subprocess
import threading
import sys
import inspect
import ctypes
class CliService(object):
def __init__(self):
self.output_lock = threading.Lock() # 用于输出操作的锁
self.cmd_lock = threading.Lock() # 用于判断命令执行状态的锁
self.output = []
self.shell_client = None
self.read_thread = None # 输出的读取线程
self.is_active = False # 当前是否活动
self.prompt = """prompt=function(){print("mysql>");}\n""" # 提示符
self.cmd_running = False # 命令是否在执行
# kill掉某个线程
def _async_raise(self, tid, exctype):
"""Raises an exception in the threads with id tid"""
if not inspect.isclass(exctype):
raise TypeError("Only types can be raised (not instances)")
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
# 后台读取输出
def read_output_background(self):
while True:
if not self.shell_client.stdout.readable():
break
line = self.shell_client.stdout.readline() # 这里是阻塞的
if not line:
break
line = line.decode("utf8")
# 判断是否为结束表示符
if line == "mysql>\n":
self.cmd_lock.acquire(True)
self.cmd_running = False
self.cmd_lock.release()
else:
# 加入到output里
self.output_lock.acquire(True)
self.output.append(line)
self.output_lock.release()
print(line)
print("read exit")
self.read_thread = None
if self.shell_client.poll() is not None: # 子进程已经退出
self.is_active = False
self.cmd_running = False
print("read: %s" % self.is_active)
# 获取日志
def get_output(self):
try:
self.output_lock.acquire(True)
text = "".join(self.output)
self.output.clear()
return text
finally:
self.output_lock.release()
# 以子进程形式运行命令
def init_cmd(self, cmd):
self.shell_client = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True)
if self.shell_client.poll() is None:
self.is_active = True
else:
return False, u"建立连接失败"
self.read_thread = threading.Thread(target=self.read_output_background)
self.read_thread.start()
# set prompt
return self.run_cmd(cmd=self.prompt)
# 执行命令
def run_cmd(self, cmd):
try:
self.cmd_lock.acquire(True)
#if self.cmd_running:
# return False, u"有未执行完成的命令"
if not self.is_active:
return False, u"当前连接不可用"
self.cmd_running = True
self.shell_client.stdin.write(cmd.encode(encoding="utf8"))
self.shell_client.stdin.flush()
return True, "push cmd done"
except Exception as e:
print(e)
return False, "执行命令出现异常:%s" % e
finally:
self.cmd_lock.release()
# 判断客户端是否已经结束
def is_terminal(self):
return not self.is_active
def close(self):
self.run_cmd(cmd="exit\n")
def __del__(self):
if self.read_thread is not None:
self._async_raise(self.read_thread.ident, SystemExit)
("stop it")
if __name__ == "__main__":
cmd = 'mysql'
ms = CliService()
ms.init_cmd(cmd)
for cmd in sys.stdin:
print(ms.is_terminal())
if ms.is_terminal():
print("exit")
break
ms.run_cmd(cmd)
if ms.is_terminal():
print("exit")
print("main exit")
pexpect
上面使用subprocess的方式看上去有点复杂,需要另起线程去获取输出,有没有更简单的办法呢?
有的,它就是pexpect,使用超级简单!
使用pexpect.spawn生成子进程,通过sendline向子进程发送命令,同步获取命令输出结果。
import pexpect
class CliService(object):
def __init__(self, cmd='', prompt=''):
self.client = None
self.prompt = prompt
self.init_cmd(cmd)
# 以子进程形式运行命令
def init_cmd(self, cmd):
self.client = pexpect.spawn(cmd, encoding='utf-8', echo=False)
self.client.expect(self.prompt)
# 执行命令
def run_cmd(self, cmd):
self.client.sendline(cmd)
self.client.expect(self.prompt)
return self.client.before
def is_alive(self):
return self.client.isalive()
if __name__ == '__main__':
cli = CliService('mysql', 'mysql> ')
for cmd in ['show databases;', 'show tables;']:
print(cmd)
print(cli.run_cmd(cmd))
不过上面两种方法都有一个致命的缺点,那就是需要维护会话与进程的关系,并且在服务端多进程的情况下,每次请求可能会到不同的进程下执行,这样上一次的子进程新的请求访问不了。
这里有一个实现方式是web端每次连接的时候,不直接创建子进程,而是创建一个任务,任务包含会话信息和命令信息,然后服务端运行一个后台常驻进程,从任务列表里获取任务并创建或者获取对应的子进程,执行相应的命令,并将命令结果写回任务里,通过websocket或者web端主动轮询命令结果。
当然这里的实现方式还是很多的,你会怎么实现呢?