web-dev-qa-db-fra.com

Python: rappel lorsque cmd se termine

Je lance actuellement un programme en utilisant subprocess.Popen(cmd, Shell=TRUE)

Je suis assez nouveau sur Python, mais il me semble qu'il devrait y avoir une API qui me permet de faire quelque chose de similaire à:

subprocess.Popen(cmd, Shell=TRUE,  postexec_fn=function_to_call_on_exit)

Je le fais pour que function_to_call_on_exit Puisse faire quelque chose en sachant que le cmd est sorti (par exemple en comptant le nombre de processus externes en cours d'exécution)

Je suppose que je pourrais envelopper assez trivialement un sous-processus dans une classe qui combinait le threading avec la méthode Popen.wait(), mais comme je n'ai pas encore fait de threading dans Python encore et il semble que cela pourrait être assez courant pour qu'une API existe, j'ai pensé que j'essaierais d'en trouver une en premier.

Merci d'avance :)

56
Who

Vous avez raison - il n'y a pas d'API Nice pour cela. Vous avez également raison sur votre deuxième point - il est trivialement facile de concevoir une fonction qui le fait pour vous en utilisant le filetage.

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

Même le threading est assez facile en Python, mais notez que si onExit () est coûteux en calcul, vous voudrez mettre cela dans un processus séparé à la place en utilisant le multiprocessing (afin que le GIL ne ralentisse pas votre programme). C'est en fait très simple - vous pouvez simplement remplacer tous les appels à threading.Thread avec multiprocessing.Process car ils suivent (presque) la même API.

59
Daniel G

Il y a concurrent.futures module dans Python 3.2 (disponible via pip install futures Pour les anciens Python < 3.2):

pool = Pool(max_workers=1)
f = pool.submit(subprocess.call, "sleep 2; echo done", Shell=True)
f.add_done_callback(callback)

Le rappel sera appelé dans le même processus que celui appelé f.add_done_callback().

Programme complet

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()

Production

$ 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
16
jfs

J'ai modifié la réponse de Daniel G pour simplement passer les arguments et les kwargs du sous-processus.

Dans mon cas, j'avais une méthode postExec() que je voulais exécuter après subprocess.Popen('exe', cwd=WORKING_DIR)

Avec le code ci-dessous, il devient simplement 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
12
Phil

J'ai eu le même problème et l'ai résolu en utilisant multiprocessing.Pool. Il y a deux astuces hacky impliquées:

  1. faire la taille de la piscine 1
  2. passer des arguments itérables dans un itérable de longueur 1

le résultat est une fonction exécutée avec rappel à la fin

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()

Dans mon cas, je voulais que l'invocation soit également non bloquante. Fonctionne magnifiquement

6
idiotype

J'ai été inspiré par la réponse de Daniel G. et j'ai implémenté un cas d'utilisation très simple - dans mon travail, j'ai souvent besoin d'appeler à plusieurs reprises le même processus (externe) avec des arguments différents. J'avais piraté un moyen de déterminer quand chaque appel spécifique a été effectué, mais maintenant j'ai un moyen beaucoup plus propre d'émettre des rappels.

J'aime cette implémentation car elle est très simple, mais elle me permet d'émettre des appels asynchrones vers plusieurs processeurs (notez que j'utilise multiprocessing au lieu de threading) et de recevoir une notification à la fin.

J'ai testé l'exemple de programme et fonctionne très bien. Veuillez modifier à volonté et fournir des commentaires.

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 __== "__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"

Exemple de sortie:

program continued
finished Sleeper calling ./sleeper with arguments (1,)
finished Sleeper calling ./sleeper with arguments (2,)
finished Sleeper calling ./sleeper with arguments (5,)

Voici le code source de sleeper.c - mon exemple de processus externe "chronophage"

#include<stdlib.h>
#include<unistd.h>

int main(int argc, char *argv[]){
  unsigned int t = atoi(argv[1]);
  sleep(t);
  return EXIT_SUCCESS;
}

compiler comme:

gcc -o sleeper sleeper.c
2
Escualo

AFAIK il n'y a pas une telle API, du moins pas dans le module subprocess. Vous devez rouler quelque chose par vous-même, en utilisant éventuellement des threads.

0
pajton