Подтвердить что ты не робот

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

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

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

Вот короткий script, который я написал, чтобы проверить трюк двойного 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')

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

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

4b9b3361

Ответ 1

Не использовать 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, это будет отправлено пользователю (что хорошо для отладки, но в противном случае script, вероятно, должно быть тихо).

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

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

Ответ 2

Я думаю, что есть две проблемы: 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) закомментирован. Когда вызов остается на месте, последняя строка никогда не появляется в браузере. (Но в остальном все содержимое печатается в браузере.)

Ответ 3

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

Ответ 4

Как отмечали другие ответы, сложно запустить постоянный процесс из вашего CGI script, потому что этот процесс должен четко отделить себя от программы 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 script. Этот подход имеет смысл в среде, где у вас ограниченный контроль над сервером и не может полагаться на такие вещи, как cron или механизм init.d.

Ответ 5

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

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

Ответ 6

Мне нужно было разбить 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

Ответ 7

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

Ответ 8

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

Ответ 9

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

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

os.system("echo sudo /srv/scripts/myapp.py | /usr/bin/at now")