Подпроцесс Python: обратный вызов при выходе из cmd - PullRequest
50 голосов
/ 06 апреля 2010

Я сейчас запускаю программу, используя subprocess.Popen(cmd, shell=TRUE)

Я довольно новичок в Python, но мне кажется, что должен быть какой-то API, позволяющий мне сделать что-то похожее на:

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

Я делаю это так, чтобы function_to_call_on_exit мог что-то сделать, зная, что cmd завершился (например, сохраняя счетчик количества запущенных внешних процессов)

Я предполагаю, чтоЯ мог бы довольно просто обернуть подпроцесс в класс, который сочетал многопоточность с методом Popen.wait(), но, поскольку я еще не выполнил многопоточность в Python, и кажется, что это может быть достаточно распространенным для существования API, я подумал, чтопопробуйте сначала найти один.

Заранее спасибо:)

Ответы [ 6 ]

58 голосов
/ 06 апреля 2010

Вы правы - для этого нет подходящего API. Вы также правы в своем втором замечании - тривиально легко спроектировать функцию, которая сделает это за вас, используя многопоточность.

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

Даже многопоточность в Python довольно проста, но учтите, что если onExit () вычислительно дорог, вы захотите поместить это в отдельный процесс вместо использования многопроцессорности (чтобы GIL не замедлял вашу программу). На самом деле это очень просто - вы можете просто заменить все вызовы на threading.Thread на multiprocessing.Process, так как они следуют (почти) одному и тому же API.

15 голосов
/ 06 марта 2011

В Python 3.2 есть модуль concurrent.futures (доступен через pip install futures для старых версий Python <3.2): </p>

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

Обратный вызов будет вызван в том же процессе, который вызвал f.add_done_callback().

Полная программа

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

выход

$ 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
12 голосов
/ 31 мая 2012

Я изменил ответ Дэниела Г., чтобы просто передать подпроцесс. Открывать аргументы и kwargs как самих себя, а не как отдельный набор / список, так как я хотел использовать аргументы ключевого слова с подпроцессом. Открыто.

В моем случае у меня был метод postExec(), который я хотел запустить после subprocess.Popen('exe', cwd=WORKING_DIR)

С кодом ниже он просто становится 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
6 голосов
/ 09 января 2011

У меня была такая же проблема, и я решил ее, используя multiprocessing.Pool. Есть два хитрых трюка:

  1. сделать размер пула 1
  2. передать итерируемые аргументы в пределах итерируемой длины 1

результат - одна функция, выполняемая с обратным вызовом при завершении

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

В моем случае я хотел, чтобы вызов также был неблокирующим. Работает красиво

2 голосов
/ 06 марта 2011

Меня вдохновил ответ Даниэля Г. и я реализовал очень простой вариант использования - в моей работе мне часто приходится повторять вызовы одного и того же (внешнего) процесса с разными аргументами. Я взломал способ определения того, когда был сделан каждый конкретный вызов, но теперь у меня есть более чистый способ создания обратных вызовов.

Мне нравится эта реализация, потому что она очень проста, но она позволяет мне выполнять асинхронные вызовы для нескольких процессоров (обратите внимание, что я использую multiprocessing вместо threading) и получать уведомление по завершении.

Я протестировал образец программы и прекрасно работает. Пожалуйста, отредактируйте по желанию и предоставьте отзыв.

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"

Пример вывода:

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

Ниже приведен исходный код sleeper.c - моего примера "трудоемкого" внешнего процесса

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

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

скомпилировать как:

gcc -o sleeper sleeper.c
0 голосов
/ 06 апреля 2010

AFAIK такого API нет, по крайней мере, в модуле subprocess. Вы должны свернуть что-то самостоятельно, возможно, используя потоки.

...