让 subprocess.Popen 只等待它的子进程到 return,而不是任何孙子进程

Have subprocess.Popen only wait on its child process to return, but not any grandchildren

我有一个 python 脚本可以执行此操作:

p = subprocess.Popen(pythonscript.py, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=False) 
theStdin=request.input.encode('utf-8')
(outputhere,errorshere) = p.communicate(input=theStdin)

它按预期工作,它等待子进程通过 p.communicate() 完成。但是在 pythonscript.py 中,我想“触发并忘记”一个“孙子”进程。我目前正在通过覆盖连接函数来做到这一点:

class EverLastingProcess(Process):
    def join(self, *args, **kwargs):
        pass # Overwrites join so that it doesn't block. Otherwise parent waits.
    def __del__(self):
        pass

然后这样开始:

p = EverLastingProcess(target=nameOfMyFunction, args=(arg1, etc,), daemon=False)
p.start()

这也很好,我只是在 bash 终端或 bash 脚本中 运行 pythonscript.py。控制和响应 returns,而 EverLastingProcess 启动的子进程继续运行。但是,当我 运行 pythonscript.py 和 Popen 运行 进行如上所示的过程时,从时间上看,Popen 正在等待孙子完成。 我怎样才能让 Popen 只等待子进程,而不是任何孙进程?

编辑: 以下在 Python 升级后停止工作,请参阅 Lachele 接受的答案。

同事的工作答案,改为shell=True,像这样:

p = subprocess.Popen(pythonscript.py, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True)

我已经测试过,孙子进程在子进程 returns 之后保持活动状态,而无需等待它们完成。

当我们最近升级 Python 时,上面的解决方案(使用 join 方法和 shell=True 添加)停止工作。

互联网上有很多关于这部分内容的参考资料,但我花了一些时间才想出一个对整个问题有用的解决方案。

以下解决方案已在 Python 3.9.5 和 3.9.7 中测试。

问题概要

脚本的名称与下面代码示例中的名称相匹配。

一个top-level程序(grandparent.py):

  • 使用subprocess.run或subprocess.Popen调用程序(parent.py)
  • 检查 parent.py 中的 return 值是否正常。
  • 从主进程收集 stdout 和 stderr 'parent.py'。
  • 不想等待孙子完成。

调用的程序(parent.py)

  • 可能会先做一些事情。
  • 生成一个非常长的进程(孙进程 - 下面代码中的“longProcess”)。
  • 可能会做更多的工作。
  • Returns 它的结果并退出,而孙子 (longProcess) 继续做它所做的事情。

解决方案概要

重要的部分不是 subprocess 发生的事情。相反,创建 grandchild/longProcess 的方法是关键部分。要保证孙子真正解脱parent.py.

  • 子进程只需要以捕获输出的方式使用。
  • longProcess(孙子)需要发生以下情况:
    • 应该使用多处理启动。
    • 需要将多处理的 'daemon' 设置为 False。
    • 它也应该使用 double-fork 过程调用。
    • 在double-fork中,需要做额外的工作来确保进程真正独立于parent.py。具体来说:
      • 将执行从 parent.py 的环境中移开。
      • 使用文件处理确保孙子不再使用从 parent.py.
      • 继承的文件句柄(stdin、stdout、stderr)

示例代码

grandparent.py - 使用 subprocess.run()

调用 parent.py
#!/usr/bin/env python3
import subprocess 
p = subprocess.run(["/usr/bin/python3", "/path/to/parent.py"], capture_output=True) 

## Comment the following if you don't need reassurance

print("The return code is:  " + str(p.returncode))
print("The standard out is: ")
print(p.stdout)
print("The standard error is: ")
print(p.stderr)

parent.py - 启动 longProcess/grandchild 并退出,留下孙 运行。 10 秒后,孙子会将计时信息写入 /tmp/timelog

!/usr/bin/env python3

import time
def longProcess() :
    time.sleep(10)
    fo = open("/tmp/timelog", "w")
    fo.write("I slept!  The time now is: " + time.asctime(time.localtime()) + "\n")
    fo.close()


import os,sys
def spawnDaemon(func):
    # do the UNIX double-fork magic, see Stevens' "Advanced
    # Programming in the UNIX Environment" for details (ISBN 0201563177)
    try:
        pid = os.fork()
        if pid > 0: # parent process
            return
    except OSError as e:
        print("fork #1 failed. See next. " )
        print(e)
        sys.exit(1)

    # Decouple from the parent environment.
    os.chdir("/")
    os.setsid()
    os.umask(0)

    # do second fork
    try:
        pid = os.fork()
        if pid > 0:
            # exit from second parent
            sys.exit(0)
    except OSError as  e:
        print("fork #2 failed. See next. " )
        print(e)
        print(1)

    # Redirect standard file descriptors.
    # Here, they are reassigned to /dev/null, but they could go elsewhere.
    sys.stdout.flush()
    sys.stderr.flush()
    si = open('/dev/null', 'r')
    so = open('/dev/null', 'a+')
    se = open('/dev/null', 'a+')
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

    # Run your daemon
    func()

    # Ensure that the daemon exits when complete
    os._exit(os.EX_OK)


import multiprocessing
daemonicGrandchild=multiprocessing.Process(target=spawnDaemon, args=(longProcess,))
daemonicGrandchild.daemon=False
daemonicGrandchild.start()
print("have started the daemon")  # This will get captured as stdout by grandparent.py

参考资料

以上代码主要受到以下两个资源的启发。

  1. .
  2. This reference contains the needed file handling, but does many other things that we do not need.