从CGI脚本启动后台进程/守护进程

时间:2011-05-16 23:15:25

标签: python apache cgi fork

我正在尝试从CGI脚本启动后台进程。基本上,当提交表单时,CGI脚本将向用户指示正在处理他或她的请求,而后台脚本执行实际处理(因为处理往往需要很长时间。)我面临的问题是,在子脚本终止之前,Apache不会将父CGI脚本的输出发送到浏览器。

一位同事告诉我,我想做的事情是不可能的,因为没有办法阻止Apache等待CGI脚本的整个进程树死掉。但是,我也看到网上有很多引用“双叉”技巧,这个技巧应该可以完成。诀窍在this Stack Overflow answer中简洁地描述,但我在其他地方看到了类似的代码。

这是我编写的用于测试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')

如果我从shell运行它,它完全符合我的期望:原始脚本和第一个后代死亡,第二个后代继续运行,直到它被手动杀死。但是如果我通过CGI访问它,那么在我杀死第二个后代之后页面将不会加载,或者由于CGI超时而Apache杀死它。我也尝试用sys.exit(0)替换第二个os._exit(0),但没有区别。

我做错了什么?

10 个答案:

答案 0 :(得分:12)

不要分叉 - 单独运行批处理

这种双重分叉方法是某种黑客行为,对我来说这表明不应该这样做:)。无论如何,对于CGI。根据一般原则,如果事情太难实现,你可能会以错误的方式接近它。

幸运的是,你提供了你需要的背景信息 - 一个CGI调用来启动一些独立发生的处理并返回给调用者。确定 - 有unix命令就是这样 - 调度命令在特定时间(at)或CPU空闲(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,所以通过对调用进行微小的修改,你也可以在windows下的apache下工作(相对于在Windows上不起作用的fork技巧) )。

PPS。确保/etc/at.deny中没有从调度批处理作业

中排除运行CGI的进程

答案 1 :(得分:6)

我认为有两个问题:setsid位于错误的位置,并在其中一个瞬态子节点中执行缓冲IO操作:

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)之后,无法保证哪个子项将首先运行,并且您将面临生成子项的风险,并可能尝试将输出写入标准输出或标准错误,之后中间父母可能有机会将success写入远程客户端。

在这种情况下,流很快就会关闭,但是,在多个进程之间混合标准IO流一定会带来困难:如果可以的话,将它们全部保存在一个进程中。

编辑我发现了一个我无法解释的奇怪行为:

#!/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)来电时显示。当呼叫保持不变时,最后一行永远不会出现在浏览器中。 (但是否则所有内容都会打印到浏览器中。)

答案 2 :(得分:6)

我不会以这种方式解决这个问题。如果您需要异步执行某些任务,为什么不使用beanstalkd之类的工作队列而不是尝试从请求中分离任务?有可供python使用的beanstalkd客户端库。

答案 3 :(得分:4)

我需要像这样打破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

答案 4 :(得分:1)

正如其他答案所指出的那样,从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程序快速返回的情况,同时产生后台进程以完成处理该请求。但是,也有一种情况是您的Web应用程序依赖于必须保持活动的正在运行的服务。 (其他人已经讨论过使用beanstalkd来处理作业。但是你如何确保beanstalkd本身还活着?)一种方法是从CGI脚本中重启服务(如果它已经关闭)。在您对服务器的控制有限且不能依赖cron或init.d机制等环境的环境中,这种方法很有意义。

答案 5 :(得分: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())之间是否存在差异,并且巨大 one:当第二个从Web服务器关闭管道并完全断开与客户端的连接时,第一个没有做任何事情。 fork只是必要的,因为web服务器会在一段时间后终止它的进程,而你的漫长过程可能需要更多的时间才能完成。

答案 6 :(得分:1)

在某些情况下,不适合将工作传递给守护程序或cron。有时,您确实确实需要分叉,让父级退出(以使Apache开心)并让子级发生缓慢的事情。

对我有用的东西:完成生成Web输出后,并且在fork之前:

fflush(stdout),close(0),close(1),close(2); //在分叉之前的过程中

然后fork()并让父级立即退出(0);

然后孩子再次做 close(0),close(1),close(2); 还有一个 setsid(); ...然后继续做任何需要做的事情。

为什么即使事先在原始过程中将它们关闭,也需要在孩子中将其关闭,这使我感到困惑,但这是有效的。并非没有第二套比赛。这是在Linux上(在树莓派上)。

答案 7 :(得分: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

答案 8 :(得分:0)

我的头仍然在那个伤害。我尝试了所有可能的方法来使用你的代码与fork和stdout关闭,nulling或任何东西,但没有任何工作。未完成的进程输出显示取决于webserver(Apache或其他)配置,在我的情况下,它不是一个更改它的选项,因此尝试使用&#34; Transfer-Encoding:chunked; chunk = CRLF&#34;和&#34; sys.stdout.flush()&#34;也没有工作。这是最终有效的解决方案。

简而言之,请使用以下内容:

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.

我使用&#34; X&#34;参数,以区分父和子,因为我为两者调用相同的脚本,但你可以通过调用另一个脚本更简单。如果一个完整的例子有用,请询问。

答案 9 :(得分: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")