subprocess kehrt nicht zurück in CGIHTTPServer

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Samstag 24. Juli 2010, 09:39

Normalerweise kehrt `subprocess.Popen([r"notepad.exe"])` sofort zurück und mein Python-Programm läuft weiter. Wenn ich das Ganze im CGIHTTPServer als CGI-Script versuche, öffnet subprocess.Popen() mein Programm und wartet, bis es wieder geschlossen ist. Erst wenn ich dsa Fenster manuell schließe, läuft das CGI-Script weiter.

Ich verstehe nicht wieso. Lässt sich das beheben?

Code: Alles auswählen

# -*- coding: utf-8 -*-
import cgi
import cgitb
cgitb.enable()

print "Content-type: text/html"
print

# end of CGI header

import subprocess

APP = r"notepad.exe"
process = subprocess.Popen([APP])
pid = process.pid
print pid
Meinen HTTPServer hab ich mir so gestrickt:

Code: Alles auswählen

# -*- coding: utf-8 -*-

"""A stoppable, threaded Python HTTP Webserver.
Attention: CGIHTTPServer cannot handle path names including spaces, see http://bugs.python.org/issue1535504
"""

import time
import socket
import threading
import SocketServer
import BaseHTTPServer
import CGIHTTPServer

class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
	pass

class PythonWebserver(ThreadedHTTPServer):
	def __init__(self, address="", port=8000):
		ThreadedHTTPServer.__init__(self, (address, port), CGIHTTPServer.CGIHTTPRequestHandler)
		self.__serverThread = threading.Thread(target=self.serve_forever)
		self.__serverThread.setDaemon(True)
	
	def start(self):
		self.__serverThread.start()

def main():
	httpd = PythonWebserver()
	httpd.start()
	raw_input("PRESS ANY KEY TO STOP SERVER")
	httpd.shutdown()

if __name__ == "__main__":
	main()
Und das Python CGI-Script rufe ich dann so auf: http://localhost:8000/cgi-bin/test.py
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Samstag 24. Juli 2010, 20:00

Ich habe weiter getestet und alle Threading-Sachen entfernt. Mein HTTP-Server sieht nun so aus. Es geht aber trotzdem nicht, der Effekt ist der gleiche. Nun stehe ich wirklich völlig auf dem Schlauch... und hoffe auf eure guten Ideen :)

Code: Alles auswählen

# -*- coding: utf-8 -*-

import BaseHTTPServer
import CGIHTTPServer

class PythonWebserver(BaseHTTPServer.HTTPServer):
	def __init__(self, address="", port=8000):
		BaseHTTPServer.HTTPServer.__init__(self, (address, port), CGIHTTPServer.CGIHTTPRequestHandler)

def main():
	httpd = PythonWebserver()
	print "PRESS CTRL+C TO STOP SERVER"
	httpd.serve_forever()

if __name__ == "__main__":
	main()
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Sonntag 25. Juli 2010, 16:29

Ich habe alles noch weiter zurück entwickelt und nun auch CGI ausgeschlossen. Und siehe da, es geht. Aber nur, wenn der RequestHandler diese Arbeit übernimmt. Ich hätte es aber lieber als CGI, weil ich modular weiter bauen möchte...

Bleibt die Frage, wieso das direkt im RequestHandler funktioniert, nicht aber als CGI? Das CGI-Script wird letztlich auch nur als separates Python-Script gestartet, also z.B. "C:\Python26\python.exe C:\MyPath\cgi-bin\MyScript.py". Wenn ich genau das standalone starte, funktioniert's ja auch...

Trotzdem hier der Code dazu:

Code: Alles auswählen

import SimpleHTTPServer
import SocketServer
import subprocess

PORT = 8000

APP = [r"notepad.exe"]

class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler):
	def do_GET(self):
		process = subprocess.Popen(APP)
		pid = process.pid
		self.send_response(200)
		self.send_header("Content-type", "text/plain")
		self.end_headers()
		self.wfile.write(pid)

httpd = SocketServer.TCPServer(("", PORT), Handler)

print "serving at port", PORT
httpd.serve_forever()
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Sonntag 25. Juli 2010, 16:46

Noch eine Frage zum Threading: Ich bin auf diesen Beitrag gestoßen. Dort wird `SocketServer.ThreadingTCPServer` genutzt, dafür muss anscheinend zusätzlich noch `server_bind()` überschrieben werdenn. Ich nutze stattdessen:

Code: Alles auswählen

class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
	pass
Was ist besser? Wo liegen die Unterschiede?
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Sonntag 25. Juli 2010, 16:58

droptix hat geschrieben:Bleibt die Frage, wieso das direkt im RequestHandler funktioniert, nicht aber als CGI? Das CGI-Script wird letztlich auch nur als separates Python-Script gestartet, also z.B. "C:\Python26\python.exe C:\MyPath\cgi-bin\MyScript.py". Wenn ich genau das standalone starte, funktioniert's ja auch...
Vielleicht ist das ja genau der Grund? Das CGI-Script wird als neuer Sub-Prozess gestartet, der wiederum einen weiteren Kind-Prozess startet. Das CGI-Script ist demnach der Eltern-Prozess. Und der Eltern-Prozess muss immer solange leben, bis das Kind fertig ist. Da in meinem Fall aber lediglich Notepad als Kind gestartet wird, wird der Eltern-Prozess nicht beendet, solange Notepad nicht wieder geschlossen wird. Das könnte bedeuten, dass das CGI-Script nicht abschließen kann, solange das Fenster geöffnet bleibt.

Es ist trotzdem nicht ganz logisch für mich und auch nur eine Vermutung, denn aus meiner Sicht müsste das CGI-Script trotzdem weiter laufen und nicht komplett hängen...
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Montag 26. Juli 2010, 08:38

droptix hat geschrieben:Es ist trotzdem nicht ganz logisch für mich und auch nur eine Vermutung, denn aus meiner Sicht müsste das CGI-Script trotzdem weiter laufen und nicht komplett hängen...
Es hängt auch nicht, wie ich feststelle. Es wird eben nur nicht beendet. Und da der `CGIHTTPServer.CGIHTTPRequestHandler` das Python-CGI-Script nunmal als separaten Prozess startet, kann dieser nicht zurück kehren, solange der Kind-Prozess noch läuft.

Das Dumme ist nur, dass der Webserver keine "Zwischenergebnisse" ausgibt, also meine `print`-Anweisungen im CGI-Script. Für mein Verständnis sollte das CGI-Script besser über `exec` ausgeführt werden, eingepackt in ein try-except.

Lässt sich das mit wenig Aufwand realisieren? Würden mir Nachteile daraus entstehen?
Leonidas
Administrator
Beiträge: 16024
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Montag 26. Juli 2010, 09:57

droptix hat geschrieben:
droptix hat geschrieben:Es ist trotzdem nicht ganz logisch für mich und auch nur eine Vermutung, denn aus meiner Sicht müsste das CGI-Script trotzdem weiter laufen und nicht komplett hängen...
Es hängt auch nicht, wie ich feststelle. Es wird eben nur nicht beendet. Und da der `CGIHTTPServer.CGIHTTPRequestHandler` das Python-CGI-Script nunmal als separaten Prozess startet, kann dieser nicht zurück kehren, solange der Kind-Prozess noch läuft.
Natürlich, sonst könnte er ja auch die Ausgabe des Prozesses nicht an den HTTP-Client schicken.
droptix hat geschrieben:Das Dumme ist nur, dass der Webserver keine "Zwischenergebnisse" ausgibt, also meine `print`-Anweisungen im CGI-Script.
Hast du es mit flushen des Stdout probiert? Kann aber auch sein dass der Python CGI HTTP Server sowas nicht kann, dann kannst du einen anderen Server probieren
My god, it's full of CARs! | Leonidasvoice vs Modvoice
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Montag 26. Juli 2010, 10:48

Ich hatte soeben folgende Idee, und die scheint zu funktionieren:

Code: Alles auswählen

class RequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
	def run_cgi(self):
		# backup settings
		BAK_fork = self.have_fork
		BAK_popen2 = self.have_popen2
		BAK_popen3 = self.have_popen2
		# simulate an operating system not supporting fork or popen
		self.have_fork = False # UNIX
		self.have_popen2 = self.have_popen3 = False # Windows
		# handle request as usual
		CGIHTTPServer.CGIHTTPRequestHandler.run_cgi(self)
		# restore settings
		self.have_fork = BAK_fork
		self.have_popen2 = BAK_popen2
		self.have_popen2 = BAK_popen3
Ich "kopiere" den `CGIHTTPServer.CGIHTTPRequestHandler` und verändere ihn dahingehend, dass ich ein unbekanntes Betriebssystem vortäusche. In seiner Methode `run_cgi()` steht ganz unten:

Code: Alles auswählen

def run_cgi(self):
	"""Execute a CGI script."""
	# [...]
	if self.have_fork:
		# Unix -- fork as we should
		# [...]
	elif self.have_popen2 or self.have_popen3:
		# Windows -- use popen2 or popen3 to create a subprocess
		# [...]
	else:
		# Other O.S. -- execute script in this process
		# [...]
Ich täusche also vor, dass Python die Funktionen `os.fork()`, `os.popen2()` und `os.popen3()` nicht kennt. Damit wird das CGI-Script direkt über `execfile` ausgeführt.

Um so wenig wie möglich Unfug zu betreiben, sichere ich mir vorher die Werte und stelle sie anschließend wieder her, um den Rest des Request Handlers nicht zu stören.

Könnte sich das sonst irgendwie negativ auswirken? Ich meine, es wird schon seinen Sinn haben, dass CGI-Scripte am besten über einen eigenen Prozess ausgeführt werden, oder?
tordmor
User
Beiträge: 100
Registriert: Donnerstag 20. November 2008, 10:29
Wohnort: Stuttgart

Montag 26. Juli 2010, 11:03

droptix hat geschrieben:Ich täusche also vor, dass Python die Funktionen `os.fork()`, `os.popen2()` und `os.popen3()` nicht kennt. Damit wird das CGI-Script direkt über `execfile` ausgeführt.

Um so wenig wie möglich Unfug zu betreiben, sichere ich mir vorher die Werte und stelle sie anschließend wieder her, um den Rest des Request Handlers nicht zu stören.
Wäre es nicht einfacher, den entsprechenden Teil aus CGIHTTPRequestHandler.run_cgi zu kopieren und die überschriebene methode gar nicht mehr aufzurufen?
http://www.felix-benner.com
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Montag 26. Juli 2010, 12:12

tordmor hat geschrieben:Wäre es nicht einfacher, den entsprechenden Teil aus CGIHTTPRequestHandler.run_cgi zu kopieren und die überschriebene methode gar nicht mehr aufzurufen?
Das dachte ich zunächst auch, aber `run_cgi()` ist sehr lang. Ich wollte nicht soviel rumpfuschen und das auch für zukünftige Python-Versionen und damit auch evtl. verbesserte Varianten von CGIHTTPServer gängig halten. Ich weiß aber wie gesagt nicht, ob das kurzzeitige Überschreiben der fork und popen Eigenschaften sonstige Nachteile hat.

[Edit:] Ich habe das mit der geänderten Datei `CGIHTTPServer.py` probiert und der folgende Effekt ist der gleiche:

Ganz sauber läuft es übrigens trotzdem noch nicht: Wenn dsa CGI-Script durchgelaufen ist und mir die Konsole auch das erwartete Feedback gibt:
CGI script exited OK
... selbst dann steht mein Browser ewig auf "Laden...", also er erwartet das Ende der Antwort. Die bekommt er aber erst, wenn ich mein Notepad-Fenster schließe. Also wieder so ein ähnliches Verhalten.

Das Blöde dabei ist, dass die Browser-Anzeige dann sofort auf "Seite wurde nicht gefunden" umschaltet, so als wäre die Antwort fehlerhaft oder der Server nicht mehr erreichbar. Mein Webserver läuft aber und kann neue anfragen entgegen nehmen.

Ich vermute, dass etwas mit der Antwort nicht stimmt.
Antworten