Subproceso Python: devolución de llamada cuando se sale de cmd
callback subprocess (6)
AFAIK no existe tal API, al menos no en el módulo de subprocess
. Necesitas rodar algo por tu cuenta, posiblemente usando hilos.
Actualmente estoy iniciando un programa usando subprocess.Popen(cmd, shell=TRUE)
Soy bastante nuevo en Python, pero parece que debería haber alguna API que me permita hacer algo similar a:
subprocess.Popen(cmd, shell=TRUE, postexec_fn=function_to_call_on_exit)
Estoy haciendo esto para que function_to_call_on_exit
pueda hacer algo basándose en saber que el cmd ha salido (por ejemplo, manteniendo la cuenta del número de procesos externos que se están ejecutando actualmente)
Supongo que podría trivialmente envolver el subproceso en una clase que combina subprocesos con el método Popen.wait()
, pero como aún no he hecho subprocesos en Python y parece que esto podría ser lo suficientemente común como para que exista una API, Pensé que iba a tratar de encontrar uno primero.
Gracias por adelantado :)
Hay un módulo concurrent.futures
en Python 3.2 (disponible a través de los pip install futures
para Python <3.2 más antiguo):
pool = Pool(max_workers=1)
f = pool.submit(subprocess.call, "sleep 2; echo done", shell=True)
f.add_done_callback(callback)
La devolución de llamada se llamará en el mismo proceso que se llamó f.add_done_callback()
.
Programa completo
import logging
import subprocess
# to install run `pip install futures` on Python <3.2
from concurrent.futures import ThreadPoolExecutor as Pool
info = logging.getLogger(__name__).info
def callback(future):
if future.exception() is not None:
info("got exception: %s" % future.exception())
else:
info("process returned %d" % future.result())
def main():
logging.basicConfig(
level=logging.INFO,
format=("%(relativeCreated)04d %(process)05d %(threadName)-10s "
"%(levelname)-5s %(msg)s"))
# wait for the process completion asynchronously
info("begin waiting")
pool = Pool(max_workers=1)
f = pool.submit(subprocess.call, "sleep 2; echo done", shell=True)
f.add_done_callback(callback)
pool.shutdown(wait=False) # no .submit() calls after that point
info("continue waiting asynchronously")
if __name__=="__main__":
main()
Salida
$ python . && python3 .
0013 05382 MainThread INFO begin waiting
0021 05382 MainThread INFO continue waiting asynchronously
done
2025 05382 Thread-1 INFO process returned 0
0007 05402 MainThread INFO begin waiting
0014 05402 MainThread INFO continue waiting asynchronously
done
2018 05402 Thread-1 INFO process returned 0
Me inspiré en la respuesta de Daniel G. e implementé un caso de uso muy simple. En mi trabajo a menudo necesito hacer llamadas repetidas al mismo proceso (externo) con diferentes argumentos. Había pirateado una forma de determinar cuándo se hacía cada llamada específica, pero ahora tengo una forma mucho más clara de emitir devoluciones de llamada.
Me gusta esta implementación porque es muy simple, pero me permite emitir llamadas asíncronas a múltiples procesadores (aviso que uso multiprocessing
lugar de threading
) y recibir una notificación cuando se complete.
He probado el programa de muestra y funciona muy bien. Por favor, edita a voluntad y proporciona comentarios.
import multiprocessing
import subprocess
class Process(object):
"""This class spawns a subprocess asynchronously and calls a
`callback` upon completion; it is not meant to be instantiated
directly (derived classes are called instead)"""
def __call__(self, *args):
# store the arguments for later retrieval
self.args = args
# define the target function to be called by
# `multiprocessing.Process`
def target():
cmd = [self.command] + [str(arg) for arg in self.args]
process = subprocess.Popen(cmd)
# the `multiprocessing.Process` process will wait until
# the call to the `subprocess.Popen` object is completed
process.wait()
# upon completion, call `callback`
return self.callback()
mp_process = multiprocessing.Process(target=target)
# this call issues the call to `target`, but returns immediately
mp_process.start()
return mp_process
if __name__ == "__main__":
def squeal(who):
"""this serves as the callback function; its argument is the
instance of a subclass of Process making the call"""
print "finished %s calling %s with arguments %s" % (
who.__class__.__name__, who.command, who.args)
class Sleeper(Process):
"""Sample implementation of an asynchronous process - define
the command name (available in the system path) and a callback
function (previously defined)"""
command = "./sleeper"
callback = squeal
# create an instance to Sleeper - this is the Process object that
# can be called repeatedly in an asynchronous manner
sleeper_run = Sleeper()
# spawn three sleeper runs with different arguments
sleeper_run(5)
sleeper_run(2)
sleeper_run(1)
# the user should see the following message immediately (even
# though the Sleeper calls are not done yet)
print "program continued"
Salida de muestra:
program continued
finished Sleeper calling ./sleeper with arguments (1,)
finished Sleeper calling ./sleeper with arguments (2,)
finished Sleeper calling ./sleeper with arguments (5,)
A continuación se muestra el código fuente de sleeper.c
: mi proceso externo "que consume mucho tiempo" de muestra
#include<stdlib.h>
#include<unistd.h>
int main(int argc, char *argv[]){
unsigned int t = atoi(argv[1]);
sleep(t);
return EXIT_SUCCESS;
}
compilar como
gcc -o sleeper sleeper.c
Modifiqué la respuesta de Daniel G para simplemente pasar el subproceso. Abrir args y kwargs como ellos mismos en lugar de como un tupple / list separado, ya que quería usar argumentos de palabras clave con subprocess.Popen.
En mi caso tuve un método postExec()
que quería ejecutar después de subprocess.Popen(''exe'', cwd=WORKING_DIR)
Con el código a continuación, simplemente se convierte en popenAndCall(postExec, ''exe'', cwd=WORKING_DIR)
import threading
import subprocess
def popenAndCall(onExit, *popenArgs, **popenKWArgs):
"""
Runs a subprocess.Popen, and then calls the function onExit when the
subprocess completes.
Use it exactly the way you''d normally use subprocess.Popen, except include a
callable to execute as the first argument. onExit is a callable object, and
*popenArgs and **popenKWArgs are simply passed up to subprocess.Popen.
"""
def runInThread(onExit, popenArgs, popenKWArgs):
proc = subprocess.Popen(*popenArgs, **popenKWArgs)
proc.wait()
onExit()
return
thread = threading.Thread(target=runInThread,
args=(onExit, popenArgs, popenKWArgs))
thread.start()
return thread # returns immediately after the thread starts
Tienes razón, no hay una API agradable para esto. También tiene razón en su segundo punto: es trivialmente fácil diseñar una función que haga esto por usted mediante el uso de hilos.
import threading
import subprocess
def popenAndCall(onExit, popenArgs):
"""
Runs the given args in a subprocess.Popen, and then calls the function
onExit when the subprocess completes.
onExit is a callable object, and popenArgs is a list/tuple of args that
would give to subprocess.Popen.
"""
def runInThread(onExit, popenArgs):
proc = subprocess.Popen(*popenArgs)
proc.wait()
onExit()
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
# returns immediately after the thread starts
return thread
Incluso el subprocesamiento es bastante fácil en Python, pero tenga en cuenta que si onExit () es computacionalmente costoso, querrá poner esto en un proceso separado en lugar de usar multiprocesamiento (para que GIL no ralentice su programa). En realidad es muy simple: básicamente puede reemplazar todas las llamadas a threading.Thread
con multiprocessing.Process
ya que siguen (casi) la misma API.
Tuve el mismo problema, y lo resolví utilizando multiprocessing.Pool
. Hay dos trucos hacky involucrados:
- hacer el tamaño de la piscina 1
- pasar argumentos iterables dentro de un iterable de longitud 1
el resultado es una función ejecutada con devolución de llamada al finalizar
def sub(arg):
print arg #prints [1,2,3,4,5]
return "hello"
def cb(arg):
print arg # prints "hello"
pool = multiprocessing.Pool(1)
rval = pool.map_async(sub,([[1,2,3,4,5]]),callback =cb)
(do stuff)
pool.close()
En mi caso, también quería que la invocación no fuera bloqueada. Funciona muy bien