Запустите фоновый процесс / демон из CGI-скрипта - PullRequest
18 голосов
/ 17 мая 2011

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

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

Вот короткий скрипт, который я написал для проверки уловки с двумя форками в 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), но разницы нет.

Что я делаю не так?

Ответы [ 10 ]

12 голосов
/ 23 мая 2011

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

Такой подход двойного разветвления является своего рода хаком, который, на мой взгляд, указывает на то, что этого делать не следует :).В любом случае для 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 на окнах (против трюка с форком, который не будет работать на окнах).

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

6 голосов
/ 19 мая 2011

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

6 голосов
/ 17 мая 2011

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

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

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

У вас есть исходный процесс (дедушка, печатает «успех»), средний родитель и внук («lol.txt»).

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

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

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

Обратите внимание, что я также переместил success к прародителю; нет никакой гарантии, что , какой дочерний элемент будет запускаться первым после вызова fork(2), и вы рискуете, что дочерний элемент будет порожден, и потенциально пытаетесь записать вывод в стандартную ошибку out или standard, до того, как средний родитель мог бы иметь возможность написать 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) закомментирован. Когда вызов остается на месте, последняя строка никогда не появляется в браузере. (Но в остальном весь контент печатается в браузере.)

4 голосов
/ 04 марта 2014

Мне нужно было сломать стандартный вывод, а также стандартный вывод:

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
1 голос
/ 28 июля 2018

Бывают ситуации, когда передача работы демону или хрону не подходит.Иногда вам действительно нужно выполнить разветвление, позволить родительскому выходу (чтобы Apache был доволен) и позволить чему-то медленному случиться с ребенком.

Что сработало для меня: Когда закончится генерация веб-вывода, и до разветвления:

fflush (стандартный вывод), закрыть (0), закрыть (1), закрыть (2);// в процессе ПЕРЕД ВАМИ FORK

Затем fork () и немедленный выход родительского элемента (0);

Дочерний элемент СНОВА делает close (0), close (1), close(2);а также setsid ();... и затем продолжает заниматься всем, что ему нужно.

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

1 голос
/ 24 апреля 2013

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

#!/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()) и есть огромный один Первый ничего не делал, а второй закрыл канал от веб-сервера и полностью отключился от клиента. Разветвление необходимо только потому, что веб-сервер через некоторое время завершит свои процессы, и вашему длинному процессу, вероятно, потребуется больше времени для завершения.

1 голос
/ 02 сентября 2012

Как уже отмечалось в других ответах, сложно запустить постоянный процесс из сценария CGI, поскольку этот процесс должен четко отделить себя от программы CGI. Я обнаружил, что отличная универсальная программа для этого - daemon . Он позаботится о беспорядочных деталях, связанных с дескрипторами открытых файлов, группами процессов, корневым каталогом и т. Д. И т. Д. Таким образом, шаблон такой программы 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.

0 голосов
/ 07 октября 2017

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

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

.
os.system("echo sudo /srv/scripts/myapp.py | /usr/bin/at now")
0 голосов
/ 17 апреля 2013

Моя голова все еще болит от этого.Я перепробовал все возможные способы использования вашего кода с закрытием форка и стандартного вывода, установкой нуля или чем угодно, но ничего не получалось.Отображение вывода незавершенного процесса зависит от конфигурации веб-сервера (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", чтобы сделать различие между родителем и потомком, потому что я вызываю один и тот жесценарий для обоих, но вы можете сделать это проще, вызвав другой сценарий.Если вам пригодится полный пример, пожалуйста, спросите.

0 голосов
/ 17 мая 2011

Я не пробовал использовать 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
...