写在前面
有时候需要在程序内部调用cmd执行命令,主程序需要拿到命令的输出信息,python中有以下几种方案可以实现该操作:
os.popen
os.system
(可能无法拿到返回)subprocess
其中使用最为广泛的,应该是subprocess.Popen
,本文所要介绍的内容,就是参考该库源码实现的。
为了能够动态的读取目标进程的输出,而不是等到子进程执行完毕一次性读取,所以没有使用subprocess
。
CreateProcess
CreateProcess是一个Windows API,该API用于创建一个新的进程,并返回新进程相关信息,可以通过自定义参数重定向目标进程的输入输出流。
匿名管道
管道常用于进程间通信,在本篇文章中,需要创建两组管道,第一组的写管道用作子进程的标准输出和标准错误,第二组的读管道用作子进程的标准输入 。 而父进程可以向第二组的写管道写入数据(比如需要执行的cmd命令),或是从第一组的读管道读出子进程的输出信息。
代码
import subprocess
import _winapi
SW_SHOW = 5
SW_HIDE = 0
def run(argv:list):
# 创建标准输出
hOutputRead,hOutputWrite = _winapi.CreatePipe(None, 0)
# 复制句柄,否则子进程无法正常继承句柄
hWrite = _winapi.DuplicateHandle(
_winapi.GetCurrentProcess(), hOutputWrite,
_winapi.GetCurrentProcess(), 0, 1,
_winapi.DUPLICATE_SAME_ACCESS)
# 创建标准输入
hInputRead,hInputWrite = _winapi.CreatePipe(None, 0)
# 复制句柄,否则子进程无法正常继承句柄
hRead = _winapi.DuplicateHandle(
_winapi.GetCurrentProcess(), hInputRead,
_winapi.GetCurrentProcess(), 0, 1,
_winapi.DUPLICATE_SAME_ACCESS)
# 将列表转换成命令
cmd = ' '.join(argv)
# CreateProcess的重要参数,可以控制窗口风格和标准输入输出,也可以用ctypes写结构体
si = subprocess.STARTUPINFO()
# 指示控制台窗口使用si.wShowWindow的值作为窗口风格
si.dwFlags |= _winapi.STARTF_USESHOWWINDOW
# 指示标准输入输出使用si中定义的句柄
si.dwFlags |= _winapi.STARTF_USESTDHANDLES
# 隐藏窗口
si.wShowWindow = SW_HIDE
# 标准错误
si.hStdError = hWrite
# 标准输出
si.hStdOutput = hWrite
# 标准输入
si.hStdInput = hRead
# 创建子进程
hp, ht, pid, tid = _winapi.CreateProcess(None,
cmd,
None,
None,
True, # 子进程是否继承父进程句柄
_winapi.CREATE_NEW_CONSOLE, # 创建新的控制台窗口
None,
None,
si)
# 在开始读管道前,需要关闭以下句柄,否则最后一次读操作会阻塞
_winapi.CloseHandle(hOutputWrite)
_winapi.CloseHandle(hInputRead)
_winapi.CloseHandle(hWrite)
_winapi.CloseHandle(hRead)
# TODO:如果需要写入命令,请在关闭`hInputWrite`之前
# _winapi.WriteFile(hInputWrite,'dir\n'.encode('gb2312'),0)
_winapi.CloseHandle(hInputWrite)
while True:
try:
ret,status = _winapi.ReadFile(hOutputRead,1024,0)
# 必须忽略BrokenPipeError
except BrokenPipeError:
_winapi.CloseHandle(hOutputRead)
break
# 打印读到的信息,注意编码,有时候需要切换成gb2312
print(ret.decode('utf8').replace('\r\n','\n'),end = '')
# 等待子进程执行完毕
_winapi.WaitForSingleObject(hp,_winapi.INFINITE)
_winapi.CloseHandle(ht)
_winapi.CloseHandle(hp)