Podproces Pythona: wywołanie zwrotne po wyjściu cmd

Uruchamiam obecnie program używając subprocess.Popen(cmd, shell=TRUE)

Jestem całkiem nowy w Pythonie, ale wydaje mi się, że powinno być jakieś api, które pozwala mi zrobić coś podobnego do:

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

Robię to po to, aby function_to_call_on_exit móc coś zrobić, wiedząc, że cmd się zakończył (na przykład zliczając liczbę uruchomionych zewnętrznych procesów)

Zakładam, że mógłbym dość trywialnie zawinąć podproces w klasę, która łączyła wątki z metodą Popen.wait(), ale jako Nie robiłem jeszcze wątków w Pythonie i wydaje się, że może to być wystarczająco powszechne, aby API istniało, pomyślałem, że najpierw spróbuję je znaleźć.

Z góry dzięki:)

Author: Who, 2010-04-06

6 answers

Masz rację - nie ma do tego ładnego API. Masz również rację co do drugiego punktu - trywialnie łatwo jest zaprojektować funkcję, która robi to za Ciebie za pomocą wątku.

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

Nawet threading jest dość łatwy w Pythonie, ale zauważ, że jeśli onexit () jest kosztowne obliczeniowo, będziesz chciał umieścić to w osobnym procesie zamiast za pomocą przetwarzania wieloprocesorowego(aby GIL nie spowalniał Twojego programu). W rzeczywistości jest to bardzo proste - można po prostu zastąpić wszystkie wywołania threading.Thread multiprocessing.Process ponieważ podążają za (prawie) tym samym API.

 55
Author: Daniel G,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-06 04:10:59

Jest concurrent.futures moduł w Pythonie 3.2 (dostępny przez pip install futures dla starszego Pythona

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

Wywołanie zwrotne zostanie wywołane w tym samym procesie, który wywołał f.add_done_callback().

Pełny program

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

Wyjście

$ 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
 15
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-06 09:43:22

Zmodyfikowałem odpowiedź Daniela G, aby po prostu przejść podproces.Popen args i kwargs jako siebie, a nie jako osobny tupple / list, ponieważ chciałem użyć argumentów słów kluczowych z podprocesem.Popen.

W moim przypadku miałem metodę postExec() którą chciałem uruchomić po subprocess.Popen('exe', cwd=WORKING_DIR)

Z poniższym kodem, po prostu staje się 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
Author: Phil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-30 20:39:33

Miałem ten sam problem i rozwiązałem go za pomocą multiprocessing.Pool. W grę wchodzą dwie hakerskie sztuczki:

  1. make size of pool 1
  2. przekazuje argumenty iterowalne w ramach iterowalnej długości 1

Wynikiem jest jedna funkcja wykonywana z wywołaniem zwrotnym po zakończeniu

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

W moim przypadku chciałem, aby inwokacja była również nieblokująca. Działa pięknie

 6
Author: idiotype,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-08 23:08:46

Zainspirował mnie Daniel G. answer i zaimplementowałem bardzo prosty przypadek użycia - w mojej pracy często muszę wykonywać powtarzające się wywołania do tego samego (zewnętrznego) procesu z różnymi argumentami. Hacked sposób, aby określić, kiedy każdy konkretny połączenie zostało zrobione, ale teraz mam znacznie czystszy sposób, aby wystawić wywołań zwrotnych.

Podoba mi się Ta implementacja, ponieważ jest bardzo prosta, ale pozwala mi na wykonywanie asynchronicznych wywołań do wielu procesorów (zauważ, że używam multiprocessing zamiast threading) i otrzymywanie powiadomienie po zakończeniu.

Przetestowałem przykładowy program i działa świetnie. Proszę edytować do woli i przesłać opinię.
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"

Przykładowe wyjście:

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

Poniżej znajduje się kod źródłowy sleeper.c - mój przykładowy" czasochłonny " proces zewnętrzny

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

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

Skompiluj jako:

gcc -o sleeper sleeper.c
 2
Author: Escualo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-06 07:14:22

AFAIK nie ma takiego API, przynajmniej nie w module subprocess. Musisz toczyć coś na własną rękę, ewentualnie za pomocą nici.

 0
Author: pajton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-05 23:52:26