匿名管道和CreateProcess在Python中的应用


写在前面

有时候需要在程序内部调用cmd执行命令,主程序需要拿到命令的输出信息,python中有以下几种方案可以实现该操作:

  1. os.popen
  2. os.system(可能无法拿到返回)
  3. 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)

写在后面