Tener subproceso. Popen sólo espera en su proceso infantil para regresar, pero no ningún nieto

Tengo un guión pitón que hace esto:

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)

Funciona como se espera, espera que el subproceso termine a través de p.communicate(). Sin embargo, dentro del pythonscript.py quiero "fuego y olvido" un proceso "abuelo". Estoy haciendo esto por sobreescribir la función de la unión:

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

Y comenzando así:

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

Esto también funciona bien Acabo de ejecutar pythonscript.py en un terminal bash o bajo script. El control y una respuesta regresa mientras el proceso infantil iniciado por EverLastingProcess sigue adelante. Sin embargo, cuando corro pythonscript.py con Popen ejecutando el proceso como se muestra anteriormente, se ve desde los tiempos que el Papa está esperando al nieto para terminar. ¿Cómo puedo hacerlo para que el Papa sólo espere el proceso del niño, y no ningún proceso de nieto?

Pregunta hecha hace 3 años, 4 meses, 28 días - Por codecrusader


4 Respuestas:

  • La solución anterior (utilizando la join método con el shell=True Además) dejó de funcionar cuando actualizamos nuestro Python recientemente.

    Hay muchas referencias en Internet sobre las piezas y partes de esto, pero me costó hacer algo para encontrar una solución útil a todo el problema.

    La siguiente solución se ha probado en Python 3.9.5 y 3.9.7.

    Sinopsis

    Los nombres de los scripts coinciden con los del ejemplo de código a continuación.

    Un programa de alto nivel (grandparent.py):

    • Utiliza subprocess.run o subprocess. Popen para llamar a un programa (parent.py)
    • Chequea el valor de retorno de los padres. Py por la cordura.
    • Colecciona stdout y stderr del proceso principal 'parent.py'.
    • No quiere esperar a que el nieto termine.

    El programa llamado (parent.py)

    • Podría hacer algunas cosas primero.
    • Hace un proceso muy largo (el nieto - "longProcess" en el código de abajo).
    • Podría hacer más trabajo.
    • Devuelve sus resultados y salidas mientras el nieto (longProcess) continúa haciendo lo que hace.

    Sinopsis de solución

    La parte importante no es tanto lo que pasa con subprocess. En cambio, el método para crear el nieto/longProcess es la parte crítica. Es necesario asegurar que el nieto sea verdaderamente emancipado de los padres. Py.

    • El subproceso sólo necesita ser utilizado de una manera que captura la salida.
    • The longProcess (grandchild) needs the following to happen:
      • Debería empezar a usar multiprocesamiento.
      • Necesita el "daemon" de multiprocesamiento para False.
      • También debe invocarse mediante el procedimiento de doble tenedor.
      • En el doble tenedor, es necesario realizar trabajos adicionales para asegurar que el proceso sea verdaderamente separado de los padres. Py. Específicamente:
        • Aparta la ejecución del entorno de los padres. Py.
        • Usar el manejo de archivos para asegurar que el nieto ya no use los mangos de archivos (estdin, stdout, stderr) heredados de los padres. Py.

    Código de ejemplo

    abuelo.py - llama padre. py subprocess.run()

    #!/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 - comienza el largoProceso/abuelo y sale, dejando al nieto corriendo. Después de 10 segundos, el nieto escribirá información de tiempo para /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
    

    Referencias

    El código anterior se inspiró principalmente en los siguientes dos recursos.

    1. Esta referencia es sucinta sobre el uso del doble tenedor pero no incluye el manejo de archivos que necesitamos en esta situación.
    2. Esta referencia contiene el manejo de archivos necesario, pero hace muchas otras cosas que no necesitamos.

    Respondida el Dec 18, 2020 a las 11:31 - por codecrusaderx

    Votos positivos: 0 | Votos negativos: 0

  • Editar: el siguiente dejó de trabajar después de una actualización de Python, ver la respuesta aceptada de Lachele.

    Respuesta de trabajo de un colega, cambio a shell= Verdadero como este:

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

    He probado y los subprocesos del nieto permanecen vivos después de que los procesos del niño regresen sin esperar a que terminen.

    Respondida el Dec 18, 2020 a las 11:40 - por binarybard

    Votos positivos: 0 | Votos negativos: 0

  • El problema que estás experimentando parece estar relacionado con la estructura de los procesos que estás creando. Cuando utilizas subprocess.Popen para lanzar el script pythonscript.py, estás creando un proceso hijo que a su vez puede estar creando procesos nietos.

    Para asegurarte de que el proceso padre solo espera al proceso hijo creado con Popen y no a ningún proceso nieto, puedes modificar tu enfoque en la creación de los procesos. Una forma de lograrlo es utilizando multiprocessing.Process en lugar de subprocess.Popen para lanzar tus procesos.

    Aquí te muestro cómo podrías estructurar tu código para lograr esto:

    • En tu script principal, puedes utilizar `multiprocessing.Process` para crear un proceso padre que a su vez lance un proceso hijo que ejecutará `pythonscript.py`:
    python
    import multiprocessing
    
    def launch_child_process():
        p = subprocess.Popen(["python", "pythonscript.py"], stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=False) 
        theStdin = request.input.encode('utf-8')
        (outputhere, errorshere) = p.communicate(input=theStdin)
    
    if __name__ == '__main__':
        main_process = multiprocessing.Process(target=launch_child_process)
        main_process.start()
        main_process.join()
    
    • En `pythonscript.py`, puedes continuar utilizando tu clase `EverLastingProcess` de la forma que lo estás haciendo actualmente para crear procesos nietos sin que el proceso padre espere a ellos.

    Al estructurar tu código de esta manera, el proceso padre creado en main_process con multiprocessing.Process solo esperará al proceso hijo creado en launch_child_process, y no a cualquier proceso nieto que se cree dentro de pythonscript.py.

    Espero que esta sugerencia te ayude a resolver tu problema y logres el comportamiento deseado en tu script. Si necesitas más ayuda, no dudes en preguntar.

    Respondida el Dec 19, 2020 a las 11:32 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • Para que el proceso padre (Papa) solo espere a que finalice el proceso hijo, y no a ningún proceso nieto, puedes utilizar el argumento preexec_fn al crear el subproceso. Este argumento especifica una función que se ejecutará antes de que se cree el proceso hijo.

    En tu caso, puedes utilizar la función preexec_fn para establecer el nuevo grupo de procesos del proceso hijo. Esto hará que el proceso nieto sea un proceso hijo del proceso hijo, no del proceso padre.

    Aquí tienes un ejemplo de cómo puedes hacerlo:

    python
    import subprocess
    
    def preexec_fn():
        os.setpgrp()
    
    p = subprocess.Popen(
        ["python", "pythonscript.py"],
        stdin=PIPE,
        stdout=PIPE,
        stderr=PIPE,
        shell=False,
        preexec_fn=preexec_fn
    )
    

    Al utilizar la función preexec_fn para establecer el nuevo grupo de procesos del proceso hijo, el proceso padre solo esperará a que finalice el proceso hijo. El proceso nieto no será un proceso hijo del proceso padre, por lo que no esperará a que finalice.

    Respondida el Dec 19, 2020 a las 11:44 - por Gemini

    Votos positivos: 0 | Votos negativos: 0