Запустить фоновый процесс / демон из сценария CGI

17

Я пытаюсь запустить фоновый процесс из сценариев CGI. В принципе, когда форма отправляется, сценарий CGI указывает пользователю, что его или ее запрос обрабатывается, в то время как фоновый скрипт выполняет фактическую обработку (потому что обработка имеет тенденцию занимать много времени.) Проблема, с которой я сталкиваюсь заключается в том, что Apache не будет отправлять выходные данные родительского скрипта CGI в браузер до завершения дочернего скрипта.

Мне сказал коллега, что то, что я хочу сделать, невозможно, потому что не существует способа предотвратить Apache от ожидания завершения всего дерева процессов CGI-скрипта. Тем не менее, я также видел многочисленные ссылки в Интернете на трюк «двойной вилки», который должен выполнять эту работу. Трюк описывается в ответе на переполнение стека , но я видел аналогичный код в другом месте.

Вот короткий скрипт, который я написал, чтобы проверить трюк двойного fork в Python:

import os
import sys

if os.fork():
    print 'Content-type: text/html\n\n Done'
    sys.exit(0)

if os.fork():
    os.setsid()
    sys.exit(0)

# Second child
os.chdir("/")
sys.stdout.close()
sys.stderr.close()
sys.stdin.close()

f = open('/tmp/lol.txt', 'w')

while 1:
     f.write('test\n')

Если я запустил это из оболочки, он делает именно то, что я ожидаю: исходный скрипт и первый потомок умирают, а второй потомок продолжает работать, пока он не будет убит вручную. Но если я получаю доступ к нему через CGI, страница не будет загружаться, пока я не убью второго потомка, или Apache не убьет его из-за таймаута CGI. Я также попытался заменить второй sys.exit(0) os._exit(0) , но нет никакой разницы.

Что я делаю неправильно?

    
задан Mitch Lindgren 17.05.2011 в 01:15
источник
  • programers.stackexchange.com/questions/47436/... –  dietbuddha 17.05.2011 в 01:43

9 ответов

11

Не использовать fork - запустить пакет отдельно

Этот подход с двойным форпованием - это своего рода хак, который для меня - это указание, что это не должно быть сделано :). Для CGI в любом случае. По общему принципу, что, если что-то слишком сложно выполнить, вы, вероятно, приближаетесь к нему неправильно.

К счастью, вы даете справочную информацию о том, что вам нужно - вызов CGI для инициирования некоторой обработки, которая происходит независимо и для возврата обратно вызывающему абоненту. Ну, конечно - есть команды unix, которые делают именно это: команда расписания запускается в определенное время ( at ) или всякий раз, когда процессор свободен ( batch ). Так сделайте это вместо:

import os

os.system("batch <<< '/home/some_user/do_the_due.py'")
# or if you don't want to wait for system idle, 
#   os.system("at now <<< '/home/some_user/do_the_due.py'")

print 'Content-type: text/html\n'
print 'Done!'

И у вас это есть. Имейте в виду, что если есть какой-то вывод в stdout / stderr, это будет отправлено пользователю (что хорошо для отладки, но в противном случае скрипт, вероятно, должен молчать).

PS. я просто вспомнил, что Windows также имеет версию at , поэтому при незначительной модификации вызова вы можете также работать с apache в окнах (vs fork trick, который не будет работать на окнах).

ПФС. убедитесь, что процесс, выполняющий CGI, не исключается в /etc/at.deny из планирования пакетных заданий

    
ответ дан Nas Banov 23.05.2011 в 00:13
  • @ Наслаждаемся большим признанием за хороший ответ, но рекомендуемый инструмент beanstalk рекомендуется использовать более гибкий и потенциально лучше подходит для большего количества приложений. Хотел бы я разделить щедрость между вами двумя. –  sarnold 25.05.2011 в 23:02
  • благодарит @sarnold, ваш комментарий достаточно полезен. но я думаю, что это будет серьезный перебор. он обеспечивает развязку между производителем и потребителем (например, с помощью / пакет с помощью cron за кулисами), но он не запустит процесс потребления, чтобы выполнить эту работу, вам нужно сделать это самостоятельно, но как? Если у вас есть один потребитель, всегда работающий и зависающий для работы в очереди, у вас есть узкое место только одной задачи, выполняемой за раз. И что, если вы хотите сделать несколько параллельно, тогда вы будете запускать N процессов все время и ждать очереди, неудобно. –  Nas Banov 26.05.2011 в 07:09
  • также, что, если демон beanstalk не работает и не прослушивает сокет? вызов библиотеки python завершится неудачно, необходимо обработать это. в простом случае, описанном в вопросе, использование очереди сообщений (какой beanstalkd - как IBM MQS, MSMQ, JMS и tibco randevous) создает больше проблем, чем решает. –  Nas Banov 26.05.2011 в 07:15
  • @Nas, вы правы, если целью является только запуск процесса, ваш ответ довольно чистый (и пакет (1) - хороший способ добраться туда :), но beanstalk или подобные инструменты позволяют оставаясь «внутри» языка программирования для отложенных задач, не требуется каждый раз выполнять fork () + exec () и т. д. Различные инструменты для разных заданий. –  sarnold 26.05.2011 в 22:18
  • Когда я запускаю теперь <<< /path/to/script.py из bash, он работает нормально. Но когда я запускаю скрипт python, который вызывает os.system (теперь <<< /path/to/script.py), я получаю эту ошибку sh: 1: Синтаксическая ошибка: перенаправление непредвиденное –  ArmenB 04.08.2014 в 20:20
Показать остальные комментарии
6

Я думаю, что есть два вопроса: setsid находится в неправильном месте и выполняет буферизованные операции ввода-вывода у одного из переходных детей:

if os.fork():
  print "success"
  sys.exit(0)

if os.fork():
  os.setsid()
  sys.exit()

У вас есть оригинальный процесс (grandparent, prints "success"), средний родитель и внук ("lol.txt").

Вызов os.setsid() выполняется в среднем родительском после того, как внук был создан . Средний родитель не может влиять на сеанс внука после создания внука. Попробуйте следующее:

print "success"
sys.stdout.flush()
if os.fork():
    sys.exit(0)
os.setsid()
if os.fork():
    sys.exit(0)

Это создает новый сеанс до появления внука. Затем средний родитель умирает, оставляя сессию без лидера группы процессов, гарантируя, что любые вызовы для открытия терминала не сработают, следя за тем, чтобы никогда не было блокировки ввода или вывода терминала или отправки неожиданных сигналов ребенку.

Обратите внимание, что я также переместил success в grandparent; нет гарантии того, что ребенок запускается первым после вызова fork(2) , и вы рискуете, что ребенок будет порожден, и потенциально попытайтесь записать вывод на стандартную или стандартную ошибку, до середины родитель мог бы написать success удаленному клиенту.

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

Изменить Я нашел странное поведение, которое я не могу объяснить:

#!/usr/bin/python

import os
import sys
import time

print "Content-type: text/plain\r\n\r\npid: " + str(os.getpid()) + "\nppid: " + str(os.getppid())
sys.stdout.flush()

if os.fork():
    print "\nfirst fork pid: " + str(os.getpid()) + "\nppid: " + str(os.getppid())
    sys.exit(0)

os.setsid()

print "\nafter setsid pid: " + str(os.getpid()) + "\nppid: " + str(os.getppid())

sys.stdout.flush()

if os.fork():
    print "\nsecond fork pid: " + str(os.getpid()) + "\nppid: " + str(os.getppid())
    sys.exit(0)

#os.sleep(1) # comment me out, uncomment me, notice following line appear and dissapear
print "\nafter second fork pid: " + str(os.getpid()) + "\nppid: " + str(os.getppid())

Последняя строка, after second fork pid , появляется только при комментировании os.sleep(1) . Когда вызов остается на месте, последняя строка никогда не появляется в браузере. (Но в остальном все содержимое печатается в браузере.)

    
ответ дан sarnold 17.05.2011 в 01:47
  • Спасибо за совет, но я боюсь, что это не решило мою проблему; Apache все равно ничего не отправит клиенту до тех пор, пока внук не умрет. Вот точный код, который я сейчас использую, измененный из исходного кода на основе вашего ответа: pastebin.com/LzeSzpgt (В строке 4 я пытался бросить EOF, чтобы посмотреть, не изменилось ли это, но это не так.) –  Mitch Lindgren 17.05.2011 в 16:51
  • @ Митч, я очень смущен, я потратил полчаса, пытаясь поиграть с этим, но не могу получить поведение, которое я ожидаю, или поведение, которое вы хотите. Смущенный и разочарованный. –  sarnold 18.05.2011 в 04:27
  • Я знаю, как вы себя чувствуете! Многие люди говорили мне об этом, но мне пока не повезло. Тем не менее, спасибо за попытку попробовать. Я теперь склоняюсь к использованию только демона, который проверяет ожидающие задачи каждые 10 секунд или около того, вместо того, чтобы пытаться запустить его из сценария CGI. –  Mitch Lindgren 18.05.2011 в 17:34
  • @Mitch, если вы можете использовать inotify (7) (возможно, через пакет incron, возможно, через привязки python-pyinotify или python-inotifyx), вы, вероятно, можете получить решение лучше, чем этот маршрут, но я «Мне любопытно узнать, как Апач знает, когда умирают внуки, это не похоже на то, что (а) это должно быть осуществимо (б) это разумно. Итак, теперь это личное. :) –  sarnold 19.05.2011 в 01:25
  • Я действительно ищу отложенные задачи в удаленной базе данных MySQL, а не в файловой системе, но inotify выглядит довольно аккуратно - я уверен, что в какой-то момент я найду для нее какое-то использование. Спасибо за добавление щедрости. Мне тоже интересно, как и почему Apache делает то, что делает. –  Mitch Lindgren 19.05.2011 в 01:58
Показать остальные комментарии
6

Я бы не стал обсуждать проблему таким образом. Если вам нужно выполнить какую-либо задачу асинхронно, почему бы не использовать рабочую очередь, например beanstalkd , вместо того чтобы пытаться отменить задачи из запрос? Для python существуют клиентские библиотеки для beanstalkd.

    
ответ дан drsnyder 19.05.2011 в 02:19
  • Как это решит случай, описанный в вопросе? Приходит вызов веб-сервера, необходимо запустить новую задачу. Кто это сделает? Beanstalkd этого не делает. –  Nas Banov 26.05.2011 в 07:17
  • Beanstalkd позволяет отправлять задания в очередь. Используя предложенную модель, вы отправляете задания в очередь (исключая необходимость вилки и позволяя вам выполнять запрос), и ваши потребители будут выполнять задания с очереди и «выполнять работу». Это довольно широко распространенный шаблон для «делать вещи» в фоновом режиме. –  drsnyder 06.11.2011 в 00:25
2

Мне нужно было разбить stdout, а также stderr следующим образом:

sys.stdout.flush()
os.close(sys.stdout.fileno()) # Break web pipe
sys.sterr.flush()
os.close(sys.stderr.fileno()) # Break web pipe
if os.fork(): # Get out parent process
   sys.exit()
#background processing follows here
    
ответ дан cbienmueller 04.03.2014 в 20:22
  • простейшее решение здесь –  Jimmy Johnson 09.05.2018 в 18:35
1

Как отмечали другие ответы, сложно запустить постоянный процесс из вашего сценария CGI, потому что этот процесс должен четко отделить себя от программы CGI. Я обнаружил, что для этого является отличной программой общего назначения демон . Он заботится о беспорядочных деталях, связанных с открытыми файловыми дескрипторами, группами процессов, корневым каталогом и т. Д. И т. Д. Для вас. Таким образом, модель такой программы CGI:

#!/bin/sh
foo-service-ping || daemon --restart foo-service

# ... followed below by some CGI handler that uses the "foo" service

В исходном сообщении описывается случай, когда вы хотите, чтобы ваша программа CGI быстро возвращалась, в то время как нерестится от фонового процесса, чтобы завершить обработку этого одного запроса. Но есть также случай, когда ваше веб-приложение зависит от текущей службы, которая должна поддерживаться. (Другие люди говорили об использовании beanstalkd для обработки заданий. Но как вы гарантируете, что beanstalkd сам жив?) Один из способов сделать это - перезапустить службу (если она отключена) из сценария CGI. Этот подход имеет смысл в среде, где у вас ограниченный контроль над сервером и не может полагаться на такие вещи, как cron или механизм init.d.

    
ответ дан gcbenison 02.09.2012 в 07:50
1

Хорошо, я добавляю более простое решение, если вам не нужно запускать другой скрипт, но продолжайте в том же, чтобы выполнить длинный процесс в фоновом режиме. Это позволит вам мгновенно увидеть ожидающее сообщение клиента и продолжить обработку вашего сервера, даже если клиент убьет сеанс браузера:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import time
import datetime

print "Content-Type: text/html;charset=ISO-8859-1\n\n"
print "<html>Please wait...<html>\n"
sys.stdout.flush()
os.close(sys.stdout.fileno()) # Break web pipe
if os.fork(): # Get out parent process
   sys.exit()

# Continue with new child process
time.sleep(1)  # Be sure the parent process reach exit command.
os.setsid() # Become process group leader

# From here I cannot print to Webserver.
# But I can write in other files or do any long process.
f=open('long_process.log', 'a+')
f.write( "Starting {0} ...\n".format(datetime.datetime.now()) )
f.flush()
time.sleep(15)
f.write( "Still working {0} ...\n".format(datetime.datetime.now()) )
f.flush()
time.sleep(300)
f.write( "Still alive - Apache didn't scalped me!\n" )
f.flush()
time.sleep(150)
f.write( "Finishing {0} ...\n".format(datetime.datetime.now()) )
f.flush()
f.close()

Я прочитал половину Интернета в течение одной недели без успеха на этом, наконец, я попытался проверить, есть ли разница между sys.stdout.close() и os.close(sys.stdout.fileno()) и существует огромный : Первый ничего не сделал, а второй закрыл трубку с веб-сервера и полностью отключился от клиента. Вилка нужна только потому, что через некоторое время веб-сервер будет убивать свои процессы, а вашему длительному процессу, вероятно, потребуется больше времени.

    
ответ дан Le Droid 23.04.2013 в 23:19
0

Я не пробовал использовать fork , но я выполнил то, что вы просили, выполнив sys.stdout.flush() после исходного сообщения, прежде чем вызывать фоновый процесс.

то есть.

print "Please wait..."
sys.stdout.flush()

output = some_processing() # put what you want to accomplish here
print output               # in my case output was a redirect to a results page
    
ответ дан Wern 17.05.2011 в 01:40
  • Если some_processing () занимает час, ваш веб-сервер быстро закончит открытые процессы (переменная MaxClients Apache prefork) для обработки входящих запросов. Быть осторожен. :) –  sarnold 17.05.2011 в 01:49
0

У меня голова все еще болит. Я пробовал все возможные способы использовать ваш код с закрытием fork и stdout, обнулением или чем угодно, но ничего не работало. Экран вывода незавершенного процесса зависит от конфигурации веб-сервера (Apache или другого), и в моем случае он не был в состоянии изменить его, поэтому пытается использовать «Transfer-Encoding: chunked; chunk = CRLF» и «sys.stdout.flush () "тоже не работал. Вот решение, которое наконец-то сработало.

Короче говоря, используйте что-то вроде:

if len(sys.argv) == 1:  # I'm in the parent process
   childProcess = subprocess.Popen('./myScript.py X', bufsize=0, stdin=open("/dev/null", "r"), stdout=open("/dev/null", "w"), stderr=open("/dev/null", "w"), shell=True)
   print "My HTML message that says to wait a long time"
else: # Here comes the child and his long process
   # From here I cannot print to Webserver, but I can write in files that will be refreshed in my web page.
   time.sleep(15)  # To verify the parent completes rapidly.

Я использую параметр «X», чтобы сделать различие между родителем и дочерним, потому что я вызываю один и тот же сценарий для обоих, но вы можете сделать это проще, вызвав другой скрипт. Если вам будет полезен полный пример, пожалуйста, спросите.

    
ответ дан Le Droid 17.04.2013 в 01:06
0

Для тысяч, у которых "sh: 1: Syntax error: redirection unexpected" с решением at / batch, попробуйте использовать что-то вроде этого:

Убедитесь, что команда at установлена, и пользователь, запускающий приложение, не входит в /etc/at.deny

os.system("echo sudo /srv/scripts/myapp.py | /usr/bin/at now")
    
ответ дан Alejandro Teijeiro 07.10.2017 в 05:19