"fork" im CGI-Skript erzeugt hängende HTML-Seite

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
chanklaus
User
Beiträge: 1
Registriert: Donnerstag 3. Juni 2010, 08:19

Liebe "pythons"!

Bis vor einiger Zeit habe ich meine Programme in Perl geschrieben, jetzt aber, in einem neuen Job, habe ich begonnen Python zu programmieren. Ich komme damit recht gut klar, nur die objektorientierte Programmierung habe ich noch nicht wirklich aufgenommen.

Ich habe versucht, eines meiner CGI-Skripte aus Perl in Python umzuschreiben. Dieses Skript lief unter Perl, so wie ich es haben wollte, aber jetzt unter Python bekomme ich es nicht richtig zum laufen. Ich bin mit meiner Weisheit am Ende und würde Euch gerne um Hilfe bitten. Das Problem, das ich habe, hängt mit einem "fork"-Befehl und den Eigenschaften von STDERR, STDIN und STDOUT auf der Betriebssystem-Ebene zusammen - und da fehlt mir doch das intimere Wissen.

Die Grundidee des CGI-Skripts ist diese: Gelegentlich setze ich Web-Services auf, die große Mengen an Daten verarbeiten müssen und daher lange Prozessierungs-Zeiten haben. Damit nun der Benutzer des Services nicht vor dem (untätigen) Bildschirm sitzt und sich fragt, ob wohl das Programm abgestürzt ist (weil schon seit Ewigkeiten nichts mehr passiert ist) baue ich eine "Bitte Warten"-Seite auf, auf der ich die abgelaufene Zeit immer wieder aktualisiere oder sogar Status-Meldungen über das, was gerade bei der Prozessierung passiert, abliefere. Diese Seite wird dann erst umgeschaltet, wenn ich die Ergebnisse präsentieren kann.

Das kann man natürlich über eine Serie von Skriptes machen. Ich wollte eine elegantere Lösung, in der alles in nur einem Skript geregelt wird. Das kann man dadurch realisieren, indem man zunächst ein Web-Formular aufbaut, in dem alle nötigen Eingaben gesammelt werden. Wenn der Benutzer die Eingaben abschickt ruft sich das Skript selbst auf - mit verändertem Parameter-Satz - "forked" den Prozess, berechnet die angefragten Daten in einem Ast des "fork" und produziert (und "refreshed") die "Bitte Warten"-Seite im anderen "fork"-Ast, bis die Daten-Produktion beendet ist. Dann erst wird die angezeigte HTML-Seite auf eine "Ergebnis"-Seite umgestellt und die "forks" beendet.

Ich hatte das Problem schon in der Perl-Version meines Demo-Skriptes (das ich auch nur mit der Hilfe eines Forums lösen konnte): die Äste des "fork" sind anscheinend nicht unabhängig, die Standard I/O muß geschlossen werden, sonst hängt die "Bitte Warten"-Seite, bis der Daten-Prozess fertig ist, fängt dann für eine Sekunde an zu zählen, um dann sofort auf die "Resultate" umzuschalten. Wenn man aber alle Standard I/O schließt und wieder öffnet (sonst kann die HTML-Seite ja nicht erzeugt werden) hängt der Daten-Prozess.

Hier der Quell-Code des Demo-Programmes (mit englischen Kommentaren):

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os, re, sys, cgi, time
import cgitb; cgitb.enable()

Change  = "Apr-09-2010"

# ### START SUBROUTINE SECTION ############################################################################ #

def ErrorMsg(Stat,Mess): #** Creates an "Error" message; ***************************************************#
  Html = "Content-Type: text/html\n\n"
  Html += "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n\n<html>\n"
  Html += "<head>\n  <title>DEMO Error Message</title>\n"
  Html += "  <!-- Last changes: %s -->\n</head>\n\n" % (Change)
  Html += "<body>\n<center>\n\n"
  Html += "<span style=\"font-size:22pt; color:Green\">DEMO ERROR MESSAGE</span><br>\n<br>\n"
  if (Stat == "0"):
    Html += "!!! System error - could NOT fork process !!!<br>\n"
  elif (Stat == "1"):
    Html += "!!! System error - could NOT write \"HTML\" file '%s'!!!<br>\n" % (Mess)
  Html += "</center>\n</body>\n</html>\n"
  print Html
  sys.exit(0);


def StartPage(): #** Creates the "Start Page"; *************************************************************#
  Html = "Content-Type: text/html\n\n"
  Html += "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n\n<html>\n"
  Html += "<head>\n  <title>DEMO START PAGE</title>\n"
  Html += "  <!-- Last changes: %s -->\n</head>\n\n" % (Change)
  Html += "<body>\n<center>\n<br>\n"
  Html += "<span style=\"font-size:22pt; color:Green\">DEMO START PAGE</span><br>\n<br>\n"
  Html += "<form action=\"%s\" method=\"post\">\n" % (CGIPath)
  Html += "<input type=\"hidden\" name=\"STAT\" value=\"1\">\n"
  Html += "<b>TYPE A "TICK NUMBER":</b><br>\n"
  Html += "<input type=\"text\" name=\"Tick\" size=\"10\"><br><br>\n"
  Html += "<input type=\"submit\" value=\"PROCESS\"> <input type=\"reset\" value=\"RESET FORM\">\n"
  Html += "</form>\n</center>\n</body>\n</html>\n"
  return Html


def ProcessPage(): #** Creates the "Process Page"; *********************************************************#
#                                                                                                           #
# If both STDERR calls are run, after sending the form page the program switches to the waiting page and    #
# counts seconds - without switching to the "Results Page"                                                  #
#                                                                                                           #
# The same happens if only the first STDERR call is run                                                     #
#                                                                                                           #
# If just the second STDERR call is run after sending the form page the program switches to the initial     #
# "Waiting Page", then stays there until the long process is finished, and AFTER that switches to the       #
# "Waiting Page", counting seconds - without switching to the "Results Page"                                #
#                                                                                                           #
# If none of them is called after sending the form page the program switches to the initial "Waiting Page", #
# then stays there until the long process is finished, and then switches to the "Results Page". This is     #
# what is intended, except for that between the switch to the initial "Waiting Page" and the final switch   #
# to the "Results Page" seconds should have been counted                                                    #
#                                                                                                           #
# None of the STDERR calls makes it into the "error.log" - which doesn't surprise, because STDERR is        #
# obviously closed                                                                                          #
#                                                                                                           #
##  sys.stderr.write("%s: CHILD: PROCESS PAGE started\n" % (CGIPath))                 ### First STDERR call ###
  Html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n<html>\n"
  Html += "<head>\n  <title>DEMO PROCESS PAGE</title>\n"
  Html += "  <meta HTTP-EQUIV=\"Expires\" CONTENT=\"NOW\">\n</head>\n\n"
  Html += "<body>\n<center>\n<br>\n"
  Html += "<span style=\"font-size:22pt; color:Green\">DEMO PROCESS PAGE</span><br>\n<br>\n"
  Html += "<span style=\"font-size:22pt; color:Violet\">...THAT'S THE TEST...</span><br>\n<br>\n"
  Html += "...<a href=\"%s\">go back to the DEMO start page</a>...<br>\n" % (CGIPath)
  Html += "</center>\n</body>\n</html>\n"
#---- >>>>> ------------------------------------------------------------------------------------------------#
  time.sleep(float(Ticks))                                                   # Simulation of a long process #
#---- <<<<< ------------------------------------------------------------------------------------------------#
##  sys.stderr.write("%s: CHILD: PROCESS PAGE created\n" % (CGIPath))                ### Second STDERR call ###
  return Html


def WaitingPage(Url,Tact): #** Creates the "Waiting Page"; *************************************************#
  Html = "Content-Type: text/html\n\n"
  Html += "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n<html>\n"
  Html += "<head>\n  <title>...DEMO is running...</title>\n"
  Html += "  <META HTTP-EQUIV=\"refresh\" content=\"%s; url=%s\">\n</head>\n\n" % (Tact,Url)
  Html += "<body>\n<center>\n<br>\n"
  Html += "<span style=\"font-size:22pt; color:Red\">...DEMO is running...</span><br>\n"
  Html += "<br>\n<span style=\"font-size:22pt\">Don't go BACK!</span><br>\n<br>\n"
  Html += "<span style=\"font-size:20pt; color:Blue\">Elapsed Time: %s</span><br>\n" % (Elapsed)
  Html += "</center>\n</body>\n</html>\n"
  return Html


def CurrTime(): #** Creating a string with the current time; ***********************************************#
  Time = time.time()
  (Yr,Mo,Da,Hr,Mi,Sc,WD,DY,ST) = time.localtime(Time)
  LocTime = '%02d:%02d:%02d' % (Hr,Mi,Sc)
  return LocTime


def ElapsedTime(): #** Calculates the elapsed time *********************************************************#
  Time = time.time()
  (Yr,Mo,Da,Hr,Mi,Sc,WD,DY,ST) = time.localtime(Time)
  CTime = (((Hr * 60) + Mi) * 60) + Sc
  Digits = STime.split(":")
  Hr = int(Digits[0])
  Mi = int(Digits[1])
  Sc = int(Digits[2])
  ITime = (((Hr * 60) + Mi) * 60) + Sc
  if (CTime < ITime):
    CTime += 86400
  ETime = CTime - ITime
  Sc = ETime % 60
  ETime = (ETime - Sc) / 60
  Mi = ETime % 60
  Hr = (ETime - Mi) / 60
  ETime = "%02d:%02d:%02d" % (Hr,Mi,Sc)
  return ETime


# ### END SUBROUTINE SECTION ############################################################################## #

# ### START MAIN PROGRAM ################################################################################## #

CGIPath = os.environ['SCRIPT_NAME']
TmpAbs  = "/var/www/tmp"                                                    # Absolute 'tmp' directory path #
TmpVrt  = "/tmp"                                                             # Virtual 'tmp' directory path #
HtmOut  = "%s/demo.html" % (TmpAbs)                                                      # HTML Output File #
HtmTmp  = "%s/demo.html.tmp" % (TmpAbs)                                      # "Temporary" HTML Output File #
Form    = cgi.FieldStorage()
STime   = Form.getvalue('Time',"00:00:00")                                           # "Process Start Time" #
Ticks   = Form.getvalue('Tick',0)                                                       # "Seconds to wait" #
Status  = Form.getvalue('STAT',"0")                                                      # "Process Status" #

sys.stdout = os.fdopen(sys.stdout.fileno(),'w',0)
Elapsed = "00:00:00"                                                            # Initialize "Elapsed Time" #

##### Status = 0: Create the "Start Page" ###################################################################
if (Status == "0"):
  print StartPage()

##### Status = 1: Fork the program, run the long process and create the "Results Page" in CHILD #############
#####             and an "Initial Waiting Page" in PARENT ###################################################
elif (Status == "1"):
  if ((os.path.exists(HtmOut)) and (os.path.getsize(HtmOut) > 0)):
    os.unlink(HtmOut)
  try:
    HTMTMP = open(HtmTmp,"w")
  except IOError:
    ErrorMsg("1",HtmTmp)
  try:
    ForkVal = os.fork()
  except IOError:
    ErrorMsg("0","")
  if (ForkVal == 0):                                                                        # CHILD process #
# If STDOUT, STDERR and STDIN are NOT closed the program remains in the "Start Page" until the long         #
# process is finished, then switches to the "Waiting Page" and immediately switches to the "Results Page"   #
    sys.stdout.close()                                                                       # Close STDOUT #
    sys.stderr.close()                                                                       # Close STDERR #
    sys.stdin.close()                                                                         # Close STDIN #
# Although STDERR is closed the attempt to write into STDERR in 'ProcessPage' function influences the       #
# program behaviour drastically (look into the source code above)                                           #
    Html = ProcessPage()
    HTMTMP.write(Html)
    HTMTMP.close()
    os.rename(HtmTmp,HtmOut)
  else:                                                                                    # PARENT process #
    Elapsed = "00:00:00"
    Url = "%s?STAT=2&Time=%s" % (CGIPath,CurrTime())
    print WaitingPage(Url,1)

##### Status = 2: Create the "Waiting Page" #################################################################
elif (Status == "2"):
  Elapsed = ElapsedTime()
  if ((os.path.exists(HtmOut)) and (os.path.getsize(HtmOut) > 0)):
    Url = "%s/demo.html" % (TmpVrt)
    print WaitingPage(Url,0)
  else:
    Url = "%s?STAT=2&Time=%s" % (CGIPath,STime)
    print WaitingPage(Url,1)
  sys.exit(0)

# ### END MAIN PROGRAM #################################################################################### #
Wer kann mir helfen?

Noch eine Bitte: mir wurde auch schon gesagt, dass ich das Problem mit Ajax lösen kann, aber mir geht es um dieses Skript, auch weil ich das eigentliche Problem verstehen will (zumindest soweit das meine beschränkten geistigen Mittel zulassen :wink: ), und nicht noch eine weitere Programmiersprache / Technologie lernen will. Auch habe ich nur sehr beschränkten Einblick in die objektorientierte Programmierung. Wenn mir jemand also zeigen kann, wie ich mit geringen Änderungen das hier vorgestellte Beispielskript zum Laufen bringen kann, wäre ich ihm/ihr sehr dankbar.

Mit meinen besten Grüßen und Wünschen
Euer

chanklaus
BlackJack

@chanklaus: Warum ist ein einziges Skript eine elegantere Lösung als mehrere kleine die jeweils nur einen abgegrenzten, überschaubaren Teil der Aufgabe lösen?

Und warum muss es unbedingt ein `fork` sein? IMHO ist das eigentliche Problem, dass man sich nicht mit solch low-level-Kram rumschlagen sollte, wenn man nicht absolut muss. Ich würde da eher einen neuen Prozess mit dem `subprocess`-Modul starten. Kann ja auch das selbe Skript sein, muss man halt per Argumentübergabe dafür sorgen, dass es in dem Fall weiss wie es sich zu verhalten hat.

So allgemein: Wirf mal einen Blick in den Style Guide a.k.a. PEP8.

Der ganze Code auf Modulebene sollte in einer Funktion verschwinden. Ich habe da mindestens eine Variable gesehen die nicht sauber über Argumente in eine Funktion kommt, sondern einfach so als "gottgegeben" verwendet wird.

Klammern um Bedingungen sind in Python überflüssig.

Man kann Zeichenketten auch mit ' einfassen, das hätte Dir eine ganze Menge \" innerhalb der Zeichenketten erspart. Ausserdem werden literale Zeichenketten, die nur durch "whitespace" getrennt sind, vom Compiler automatisch zu einer zusammengefügt. Man könnte also mehrzeilige Texte auch so angeben:

Code: Alles auswählen

SPAM = ('Eine Zeile,\n'
        'und noch eine Zeile, diesmal mit "Anführungszeichen".')
Antworten