subprocess.Popen 子进程无法使用input()解决方案

???为什么变成了VIP文章?要点脸吧CSDN

在做Python 打包工具https://github.com/QPT-Family/QPT(将Python程序打包为EXE)项目的时候,由于在启动EXE文件后,需要有一个配置环境的环节,因此想启动主程序就要考虑用subprocess.Popen或者动态导入的方式来执行。

但考虑到未来多程序入口等等要实现的功能,最终放弃了动态导入以及其他启动方式,选择了subprocess.Popen。

实际上subprocess.Popen也不是什么特别舒服的选择,在subprocess.Popen中启动的py文件在Windows下是默认不能使用input()的,因为终端不会允许你进行输入,但问题不大,分享一下自己的不是很靠谱的解决方案。

方案1 手动做个管道

由于是多进程,那么我们先做个最基础的subprocess.Popen运行py文件的demo,作为主进程。

import subprocess

# 创建一个subprocess.Popen用于启动另一个py文件
terminal = subprocess.Popen(
    args=['python "tmp.py"'],
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    stdin=subprocess.PIPE)

# 获取该py文件运行时的输出信息并打印
while True:
    line = terminal.stdout.readline().decode('utf-8', errors="ignore")
    if line:
        print(line)
    else:
        break

其中tmp.py则是我们含有input()的demo,内容如下:

print("开始")
x = input("请输入")
print(x)

不难发现,光标显示在“开始”文字的后面,而且input()中的“请输入”三个字并没有被打印在屏幕上,此时我们即使在终端中进行输入(不管是不是在IDE中)都无法输入我们要input()到程序的内容。
在这里插入图片描述虽然子进程没办法用input(),那么主进程是否可以呢?(这不废话嘛,主进程当然可以),所以我们可以思考几个问题:

  1. 主进程的input()结果能否传递到子进程?
  2. 主进程如何得知子进程正在等待input()?

1.1 向子进程传递input()信息

1.1.1 stdin.write方案不可行

关于问题1,虽然我们可以用terminal.stdin.write(shell)来向子进程传递信息,但由于input()的特殊性质(连“请输入”仨字都没打印出来),这个方式在尝试后发现子进程依旧阻塞,故放弃terminal.stdin.write(shell)

1.1.2 文件方案(凑合用)

虽然不是很想做很多硬盘的I/O操作,但文件方案是最省事的一种,我们只需要在得知子进程需要input()之后,向临时目录里写入一个文件,并且写入主进程中用户input()的信息,这样子进程按时检测文件是否有写入信息,即可得知用户想input()什么内容。

主进程py文件,修改为:

while True:
    line = t.stdout.readline().decode('utf-8', errors="ignore")
    if line:
        if 得到子进程的信号:
	        raw = input(pro)
	        f = open(path, "wb")
	        f.write(raw.encode("utf-8"))
	        f.flush()
	        f.close()
        print(line)
    else:
        break

子进程py文件修改为:

__COMPATIBLE_INPUT_START_FLAG = "---QPT COMPATIBLE_INPUT_START_FLAG---"  # 37
__COMPATIBLE_INPUT_END_FLAG = "---QPT COMPATIBLE_INPUT_END_FLAG---"  # 35
__COMPATIBLE_INPUT_SPLIT_FLAG = "---QPT COMPATIBLE_INPUT_SPLIT_FLAG---"  # 37

def __compatible_input():
    """
    对QPT中使用多进程启动Python程序做input的适配
    因input在subprocess.Popen中并不会显式向用户提供输入
    :return:
    """
    import builtins
    import os

    def _wrapper(__prompt):
        # 间隔扫描主进程是否有生成文件
        while True:
            if os.path.exists(主进程写入的文件路径):
                break
            else:
                time.sleep(0.2)
        with open(主进程写入的文件路径, "rb") as f:
            raw = f.read().decode("utf-8")
        return raw

    builtins.__dict__["input"] = _wrapper

# 将默认的input()替换为我们特制的方法
__compatible_input()

print("开始")
x = input("请输入")
print(x)

做到这里之后,我们就可以考虑子进程如何向主进程发送需要input()的信号了,以及我们还要给予他们一个约定:我们要往哪个文件去写入信息

1.2 向主进程发送需要input()信号

由1.1.2可得,我们不仅需要发送input()信号,而且还需要约定一个文件作为管道。
为了避免频繁的读写文件,每次input()时都创建一个独一无二的文件,这样只需要判断文件是否存在即可得知主进程中用户是否已经写入了文件,避免频发打开文件查看内容来做频繁的I/O操作。

同时,由于我们主进程可以得到子进程的输出信息,那么我们只需要让子进程打印一个特殊的标识符(例如:ABCDEFG[信息文本]GFEDCBA),这样主进程在捕获到该格式的信息后,去掉标识符ABCDEFG和GFEDCBA,即可抽取到信息文本,而且还可以得知子进程发出了它的信号。

这里,我们不仅要传递子进程需要input()这一信号,还要传递约定主进程要往哪个文件中写入用户的input()信息。

那么__compatible_input()就需要修改为:


__COMPATIBLE_INPUT_START_FLAG = "---QPT COMPATIBLE_INPUT_START_FLAG---"  # 37
__COMPATIBLE_INPUT_END_FLAG = "---QPT COMPATIBLE_INPUT_END_FLAG---"  # 35
__COMPATIBLE_INPUT_SPLIT_FLAG = "---QPT COMPATIBLE_INPUT_SPLIT_FLAG---"  # 37

def __compatible_input():
    """
    对QPT中使用多进程启动Python程序做input的适配
    因input在subprocess.Popen中并不会显式向用户提供输入
    :return:
    """
    import builtins
    import uuid
    import os
    import time
    from tempfile import gettempdir

    _path = os.path.join(gettempdir(), "Python_PIPE")
    os.makedirs(_path, exist_ok=True)
    _path = os.path.join(_path, str(uuid.uuid4()) + ".txt")

    def _wrapper(__prompt):
        # 向主进程抛出input信号
        print(__COMPATIBLE_INPUT_START_FLAG + # 开始标识符
              _path + # 约定主进程写入哪个文件
              __COMPATIBLE_INPUT_SPLIT_FLAG + # 分割标识符
              str(__prompt) + # 向主进程发送input(提示词)中的提示词信息
              __COMPATIBLE_INPUT_END_FLAG)	# 结束标识符

        # 检测用户是否写入文件
        while True:
            if os.path.exists(_path):
                break
            else:
                time.sleep(0.2)

        # 读取用户的输入情况
        with open(_path, "rb") as f:
            raw = f.read().decode("utf-8")
        # 读完就扔
        os.remove(_path)
        
        # 返回主进程给予子进程的input()信息
        return raw
    
    # 替换子进程默认的input()为我们特制的input()
    builtins.__dict__["input"] = _wrapper

同时,主进程py文件也需要做出改动

import subprocess

t = subprocess.Popen(
    args=['python tmp.py'],
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    stdin=subprocess.PIPE)

# 持续获取子进程输出
while True:
    line = t.stdout.readline().decode('utf-8', errors="ignore")
    if line:
        # 判断子进程是否发出input()信号
        if "---QPT COMPATIBLE_INPUT_START_FLAG---" in line:
            # 解析约定的写入路径与提示词
            path, pro = line.replace(
                "---QPT COMPATIBLE_INPUT_START_FLAG---", "").replace(
                "---QPT COMPATIBLE_INPUT_END_FLAG---", "").split("---QPT COMPATIBLE_INPUT_SPLIT_FLAG---")
            print("path", path, pro)
            # 在主进程中获取用户输入
            raw = input(pro)
            # 向约定的文件中写入
            with open(path, "wb") as f:
                f.write(raw.encode("utf-8"))
        print(line)
    else:
        break

1.3 完整实现代码

1.3.1 主进程
# Author: Acer Zhang
# Datetime: 2022/5/11 
# Copyright belongs to the author.
# Please indicate the source for reprinting.

import subprocess

t = subprocess.Popen(
    args=['python tmp.py'],
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    stdin=subprocess.PIPE)

# 持续获取子进程输出
while True:
    line = t.stdout.readline().decode('utf-8', errors="ignore")
    if line:
        # 判断子进程是否发出input()信号
        if "---QPT COMPATIBLE_INPUT_START_FLAG---" in line:
            # 解析约定的写入路径与提示词
            path, pro = line.replace(
                "---QPT COMPATIBLE_INPUT_START_FLAG---", "").replace(
                "---QPT COMPATIBLE_INPUT_END_FLAG---", "").split("---QPT COMPATIBLE_INPUT_SPLIT_FLAG---")
            print("path", path, pro)
            # 在主进程中获取用户输入
            raw = input(pro)
            # 向约定的文件中写入
            with open(path, "wb") as f:
                f.write(raw.encode("utf-8"))
        print(line)
    else:
        break
1.3.2 子进程
# Author: Acer Zhang
# Datetime: 2022/5/11 
# Copyright belongs to the author.
# Please indicate the source for reprinting.

__COMPATIBLE_INPUT_START_FLAG = "---QPT COMPATIBLE_INPUT_START_FLAG---"  # 37
__COMPATIBLE_INPUT_END_FLAG = "---QPT COMPATIBLE_INPUT_END_FLAG---"  # 35
__COMPATIBLE_INPUT_SPLIT_FLAG = "---QPT COMPATIBLE_INPUT_SPLIT_FLAG---"  # 37


def __compatible_input():
    """
    对QPT中使用多进程启动Python程序做input的适配
    因input在subprocess.Popen中并不会显式向用户提供输入
    :return:
    """
    import builtins
    import uuid
    import os
    import time
    from tempfile import gettempdir

    _path = os.path.join(gettempdir(), "Python_PIPE")
    os.makedirs(_path, exist_ok=True)
    _path = os.path.join(_path, str(uuid.uuid4()) + ".txt")

    def _wrapper(__prompt):
        print(__COMPATIBLE_INPUT_START_FLAG +
              _path +
              __COMPATIBLE_INPUT_SPLIT_FLAG +
              str(__prompt) +
              __COMPATIBLE_INPUT_END_FLAG)

        while True:
            if os.path.exists(_path):
                break
            else:
                time.sleep(0.2)
        with open(_path, "rb") as f:
            raw = f.read().decode("utf-8")
        return raw

    builtins.__dict__["input"] = _wrapper


def wrapper():
    __compatible_input()


wrapper()
print("开始")
x = input("请输入")
print(x)

—分割线—
该方案仍需添加行缓存环境变量PYTHONUNBUFFERED=1

方案2 新起一个终端

不多赘述了,很多方式都能打开一个新的窗口,在新的窗口中运行py文件,想干啥干啥,简单粗暴,方案也多的很。

参考链接

Python打包工具-QPT

  1. 方案1完整测试代码https://github.com/QPT-Family/QPT/tree/开发分支/unit_test/input_test
  2. 方案1在QPT中应用代码
    2.1 https://github.com/QPT-Family/QPT/tree/开发分支/qpt/run_wrapper.py
    2.2 https://github.com/QPT-Family/QPT/tree/开发分支/qpt/kernel/qterminal.py
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值