Stefan: Perl-Skript mit "fork" funktioniert nicht als CGI

Hallo liebe Netzgemeinde, und en besonderes "Hallo" an seth_not@home, der mir schon beim letzten Mal (http://forum.de.selfhtml.org/archiv/2007/5/t152146/) behilflich war.

Auch dieses Mal klemmt es wieder beim Thema "fork".

An der Bash läuft mein Skript; Starte ich es jedoch als CGI, terminiert es nicht und malt mir endlos die Wartenachricht in den Browser.

Hier das Skriptchen:
------------------------------------------------------
#!/usr/bin/perl -w

Testskript fuer einzelnen "fork"

use CGI::Carp qw(fatalsToBrowser);
use POSIX;

print "Content-type: text/html\n\n";
print qq~
<HTML>
<HEAD><TITLE>Test</TITLE></HEAD>
<BODY>
<tt>~;
$|=1;
pipe READFD,WRITEFD;
my $oh= select(WRITEFD); select($oh);
my $Kind_pid = fork();

if($Kind_pid) {
 #Teil 1
 use POSIX ":sys_wait_h";
 my $waitcount=1;
 do {
  $Kind_pid = waitpid(-1,&WNOHANG);
  #Wartenachricht hier
  print " ...".$waitcount; # Hier zaehlt er als CGI endlos hoch
  $waitcount++;
  sleep 2;
 } until $Kind_pid == -1;
 close(WRITEFD);
 @returnvalue=<READFD>;
}
else {
 #Teil 2
 my $cvscan='';
 sleep 10; # kuenstlich ausbremsen, damit Teil 1 was zu tun hat
 my @childvs=echo "foobar"; # beliebiger Befehl
 # der Befehl wird laut "ps ax" auch beim Aufruf als CGI ausgefuehrt und beendet
 close(READFD);
 foreach $cvs (@childvs){
  #print $cvs."<br>\n";
  print WRITEFD $cvs;
 }
 exit(0);
}

#Weitere Verarbeitung hier
print "\nAusgabe hier\n";
print @returnvalue;
print qq~
</tt>
</BODY>
</HTML>
~;
------------------------------------------------------

Und nachfolgend noch ein bisschen Debug-Info...

------------------------------------------------------
Aufruf an der Bash:
user@host# perl cgi-bin/test.pl
Content-type: text/html

<HTML>
<HEAD><TITLE>Test</TITLE></HEAD>
<BODY>
<tt> ...1 ...2 ...3 ...4 ...5 ...6 ...7
Ausgabe hier
foobar

</tt>
</BODY>
</HTML>

-->Alles OK
------------------------------------------------------
Ausgabe im Browser (Quelltextanzeige)

<HTML>
<HEAD><TITLE>Test</TITLE></HEAD>
<BODY>
<tt> ...1 ...2 ...3 ...4 ...5 ...6 ...7 ...8 ...9 ...10 ...11 ...12 ...13 ...14 ...15

-->Mit anderen Worten, er terminiert nicht!
------------------------------------------------------
Verwendeter Webserver:
Package: thttpd
Priority: optional
Section: web
Installed-Size: 216
Maintainer: Daniel Baumann daniel.baumann@panthera-systems.net
Architecture: i386
Version: 2.23beta1-5
Provides: httpd, httpd-cgi
Depends: libc6 (>= 2.3.6-6), logrotate
Suggests: thttpd-util
Filename: pool/main/t/thttpd/thttpd_2.23beta1-5_i386.deb
Size: 54482
MD5sum: 18e7b8b8e80975a13b6ef3b9770cecb6
SHA1: 46a6f70d960e95f8b6e35aa992a6655ac9b2d5ff
SHA256: 2dc5e8fcf217e2fd97f90a6c1537d0f124e409b4d90810b31e4da48463e0803f
Description: tiny/turbo/throttling HTTP server
thttpd is a small, fast secure webserver. It features CGI support,
URL-traffic-based throttling and basic authentication.
thttpd has a very small memory footprint as it only forks itself in order
to execute CGI scripts. It is designed to be as fast as fully featured
web-servers and it performs extremely well under high load.
.
This package contains the thttpd server. For thttpd support programs see
the thttpd-util package.
Tag: interface::daemon, network::server, protocol::http, role::program, web::server, works-with::text, works-with-format::html
------------------------------------------------------
Config-Datei des Webservers:
user@host# cat /etc/thttpd/thttpd.conf
port=80
nochroot
user=www-data
logfile=/var/log/thttpd.log
throttles=/etc/thttpd/throttle.conf
dir=/var/www
cgipat=/cgi-bin/*
------------------------------------------------------
Dateirechte:
user@host# ls -lah cgi-bin
insgesamt 4,0K
drwxr-xr-x 2 root root 72 2007-07-10 14:50 .
drwxr-xr-x 4 root root 96 2007-07-10 14:46 ..
-rwxr-xr-x 1 root root 932 2007-07-10 14:50 test.pl
------------------------------------------------------
Verzeichnisrechte:
user@host# ls -lah
insgesamt 0
drwxr-xr-x 4 root root 96 2007-07-10 14:46 .
drwxr-xr-x 14 root root 336 2007-07-10 14:45 ..
drwxr-xr-x 2 root root 72 2007-07-10 14:50 cgi-bin
drwxr-xr-x 2 root root 48 2006-10-31 20:16 users
------------------------------------------------------
Perl-Version:
user@host# perl -v

This is perl, v5.8.8 built for i486-linux-gnu-thread-multi

Copyright 1987-2006, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl". If you have access to the
Internet, point your browser at http://www.perl.org/, the Perl Home Page.
------------------------------------------------------

In der Hoffnung auf Erleuchtung aus der Community,

Stefan

  1. An der Bash läuft mein Skript; Starte ich es jedoch als CGI, terminiert es nicht und malt mir endlos die Wartenachricht in den Browser.

    Also bei mir läuft das Script sowohl in der Shell als auch unter Serverbedingungen problemlos. Ich nutze WinXP Home, ActivePerl 5.8.8 und TinyWeb. Könnte also am OS oder an der verwendeten Serversoftware liegen, seth schrieb was von einem Bug.

    Siechfred

    --
    Ein Selbständiger ist jemand, der bereit ist, 16 Stunden am Tag zu arbeiten, nur um nicht 8 Stunden für einen Anderen arbeiten zu müssen.
    1. Also bei mir läuft das Script sowohl in der Shell als auch unter Serverbedingungen problemlos. Ich nutze WinXP Home, ActivePerl 5.8.8 und TinyWeb. Könnte also am OS oder an der verwendeten Serversoftware liegen, seth schrieb was von einem Bug.

      Könnte es vielleicht noch jemand unter Linux und der von mir angegebenen Perl-Version, aber mit einem Apachen testen, oder mit "meiner" thttpd-Version, aber dafür mit einer anderen Perl-Version?

      Würde mich über Rückmeldungen freuen.

      Gruß Stefan

    2. Nachtrag: Der Bug, auf den seth sich bezog, war im Beispielskript von den selfhtml-Seiten; in meinem Skript ist er behoben (behebt man ihn nicht, terminiert das Programm auch an der Shell nicht). Daran dürfte es also nicht liegen.

  2. Ich sehe in dem Script einige Probleme:

    * "use strict;" fehlt, dadurch verdeckt Perl Flüchtigkeitsfehler. Vor weiteren Versuchen, denn eigentlichen Fehler zu beheben, sollte das Script erst einmal auf strict umgestellt und alle von strict gemeldeten Probleme behoben werden.
    * -T fehlt in der #!-Zeile.  CGIs funktionieren auch ohne -T, aber mit -T wird die Katastrophenwahrscheinlichkeit deutlich kleiner.
    * fork() hat drei mögliche Rückgabewerte, nicht nur zwei: 0 im Child, PID im Parent, undef bei Fehlern. Letzeres ist nicht berücksichtigt.
    * Backticks (``) involvieren die Shell, das ist in CGIs insbesondere mit Eingaben vom Benutzer in der Regel nicht gewünscht, weil es fast immer riesige Sicherheitslöcher öffnet.
    * waitpid(-1) wartet auf alle Kindprozesse, nicht nur auf den gerade gestarteten. Das sollte hier eigentlich harmlos sein.
    * Der Rückgabewert von waitpid() ist nicht notwendigerweise -1, siehe perlfunc-Manpage.
    * Es wird nirgendwo $ENV{'PATH'} gesetzt, das aufgerufene Programm wird aber auch nicht mit einer absoluten Pfadangabe aufgerufen. Welchen Wert hat $ENV{'PATH'} für vom Webserver gestartete CGIs? Welchen Wert hat $ENV{'PATH'} in der Benutzer-Shell? (-T hätte dieses Problem sofort gemeldet!)
    * Die perlipc-Manpage dokumentiert unter "Safe Pipe Opens", wie sichere IPC mit einer Kopie des Prozesses oder auch mit einem anderen Prozess funktioniert. Das ist der offiziell empfohlene Weg, um I/O mit anderen Programmen zu machen.

    Alexander

    1. Hallo Alexander,

      danke für Deine Tipps.

      use strict und das -T werde ich einbauen, $ENV{'PATH'} werde ich mir auch noch ausgeben lassen, und das "undef" bei fork() werde ich ebenfalls noch prüfen. Das Ergebnis werde ich dann posten.

      Der Hinweis wegen den Backticks ist sicherlich generell berechtigt, in meinem Fall werden aber keine Benutzereingaben eingefügt. Es soll ganz stupide ein und das selbe Programm ausgeführt werden, und der Dateiname der temporären Datei, auf die es zugreift, wird vom Skript bestimmt, und nicht vom Anwender.

      * waitpid(-1) wartet auf alle Kindprozesse, nicht nur auf den gerade gestarteten. Das sollte hier eigentlich harmlos sein.

      Wie kann ich das Skript denn auf genau den gerade gestarteten warten lassen?

      * Der Rückgabewert von waitpid() ist nicht notwendigerweise -1, siehe perlfunc-Manpage.

      So tief wollte ich eigentlich gar nicht in die Materie einsteigen. Meine Vorlage war dieses Beispiel aus selfhtml: http://de.selfhtml.org/perl/funktionen/systemaufrufe.htm#waitpid
      Falls Du das Beispiel für so schlecht hältst - vielleicht könntest Du eine bessere Version davon einstellen, dann haben alle etwas davon?

      * Die perlipc-Manpage dokumentiert unter "Safe Pipe Opens", wie sichere IPC mit einer Kopie des Prozesses oder auch mit einem anderen Prozess funktioniert. Das ist der offiziell empfohlene Weg, um I/O mit anderen Programmen zu machen.

      Hm, ich *dachte* eigentlich, dass ich die Kommunikationsgeschichte auch aus einem selfhtml-Beispiel hatte, aber so spontan finde ich es nicht mehr.
      Ich habe mir auf Deinen Rat hin mal http://www.ccsf.edu/Pub/Perl/perlipc/Safe_Pipe_Opens.html angeschaut, aber irgendwie schafft das bei mir eher noch mehr Verwirrung. :-(

      Ich will doch einfach nur, dass mein Hauptprogramm eine sich wiederholende Wartemeldung ausgibt, bis der externe Befehl sein Ergebnis zurückgeliefert hat, und anschließend will ich dieses Ergebnis weiterverarbeiten. Ist das wirklich so schwer?

      Gruß
      Stefan

      1. Hallo Alexander,

        danke für Deine Tipps.

        use strict und das -T werde ich einbauen, $ENV{'PATH'} werde ich mir auch noch ausgeben lassen, und das "undef" bei fork() werde ich ebenfalls noch prüfen. Das Ergebnis werde ich dann posten.

        Guter Plan.

        Der Hinweis wegen den Backticks ist sicherlich generell berechtigt, in meinem Fall werden aber keine Benutzereingaben eingefügt. Es soll ganz stupide ein und das selbe Programm ausgeführt werden, und der Dateiname der temporären Datei, auf die es zugreift, wird vom Skript bestimmt, und nicht vom Anwender.

        Ist der Name vorhersagbar? Dann hast Du immer noch ein Problem, wenn auch ein anderes.

        * waitpid(-1) wartet auf alle Kindprozesse, nicht nur auf den gerade gestarteten. Das sollte hier eigentlich harmlos sein.

        Wie kann ich das Skript denn auf genau den gerade gestarteten warten lassen?

        Siehe Dokumentation zu waitpid.

        * Der Rückgabewert von waitpid() ist nicht notwendigerweise -1, siehe perlfunc-Manpage.

        So tief wollte ich eigentlich gar nicht in die Materie einsteigen.

        Würdest Du dich nach 20 Minuten am MS Flight Simulator ans Steuer einer 747 wagen, um sie von Frankfurt nach NY zu fliegen? Du wirst kaum darum herumkommen, wenigstens die essenziellen Teile der Perl-Dokumentation durchzuarbeiten. Damit kommst Du zwar auch nicht nach NY, aber zumindest darfst Du dann schonmal aufs Rollfeld gehen und vielleicht sogar aus dem Cockpit-Fenster sehen. ;-)

        Meine Vorlage war dieses Beispiel aus selfhtml: http://de.selfhtml.org/perl/funktionen/systemaufrufe.htm#waitpid
        Falls Du das Beispiel für so schlecht hältst - vielleicht könntest Du eine bessere Version davon einstellen, dann haben alle etwas davon?

        Unnötig, die Perl-Dokumentation ist für diesen Fall ausreichend und auch auf Deutsch verfügbar.

        * Die perlipc-Manpage dokumentiert unter "Safe Pipe Opens", wie sichere IPC mit einer Kopie des Prozesses oder auch mit einem anderen Prozess funktioniert. Das ist der offiziell empfohlene Weg, um I/O mit anderen Programmen zu machen.

        Hm, ich *dachte* eigentlich, dass ich die Kommunikationsgeschichte auch aus einem selfhtml-Beispiel hatte, aber so spontan finde ich es nicht mehr.

        Ich habe den Verdacht, Du planst ein Haus, in dem Du Stücke aus den Konstruktionsplänen von Sanssoussi, dem Eiffelturm, der Berliner Mauer, dem Telemichel und der Titanic rausreißt und mit reichlich Kleister auf dem Plan vom VW Käfer zusammenklebst. Das Ergebnis wird in aller Regel nicht zu Deinem Traumhaus führen.

        Ich habe mir auf Deinen Rat hin mal http://www.ccsf.edu/Pub/Perl/perlipc/Safe_Pipe_Opens.html angeschaut, aber irgendwie schafft das bei mir eher noch mehr Verwirrung. :-(

        Probier's mal mit http://perldoc.perl.org/ bzw. http://perldoc.perl.org/perlipc.html, dort ist die Dokumentation nicht zu Konfetti verarbeitet.

        Ich will doch einfach nur, dass mein Hauptprogramm eine sich wiederholende Wartemeldung ausgibt, bis der externe Befehl sein Ergebnis zurückgeliefert hat, und anschließend will ich dieses Ergebnis weiterverarbeiten. Ist das wirklich so schwer?

        Wenn Du Dich mit der Dokumentation ernsthaft befaßt, nein.

        Wobei mir noch nicht so ganz in den Kopf will, warum Du den Webserver mit zusätzlicher Arbeit belasten willst, während ein ohnehin langsames Programm läuft. Wenn Du dem Benutzer Aktivität vorgaukeln willst, benutze ein animiertes GIF, das Du nach Ende der langen Arbeit ausblendest oder ersetzt. Oder verarbeite die Ausgabe des Kindprozesses direkt im CGI, noch während der Kindprozess die Ausgabe anliefert. Das dürfte in aller Regel die effizienteste Lösung sein.

        Alexander

        1. [Backticks now considered harmful]

          [...] Es soll ganz stupide ein und das selbe Programm ausgeführt werden, und der Dateiname der temporären Datei, auf die es zugreift, wird vom Skript bestimmt, und nicht vom Anwender.
          Ist der Name vorhersagbar? Dann hast Du immer noch ein Problem, wenn auch ein anderes.

          Ja, ich habe immer noch ein Problem damit, dass mein Programm nicht da terminiert wo ich es terminieren lassen möchte ;-).

          Du spielst wahrscheinlich darauf an, dass man die Dateinamen dann erraten kann und somit ein unberechtigter Dritter eine nicht für ihn bestimmte Datei abrufen kann.
          Mein Skript ist aber nur für ein Intranet gedacht (das was ich schlussendlich vorhabe, macht auf einem Server im Internet prinzipbedingt keinen Sinn), von daher halte ich es für ausreichend, den Namen aus der Prozeß-ID und dem Timestamp zusammenzusetzen - zumal der Anwender diese Datei sowieso nie unter diesem Namen zu sehen oder gar zu fassen bekommt (Das Verzeichnis liegt auch außerhalb des webserver-roots). Er bekommt später einen "gesäuberten" (d.h. alles was nicht \w, \d oder . ist, wird zum "_") Originaldateinamen zurück.

          So tief wollte ich eigentlich gar nicht in die Materie einsteigen.

          Würdest Du dich nach 20 Minuten am MS Flight Simulator ans Steuer einer 747 wagen, um sie von Frankfurt nach NY zu fliegen? Du wirst kaum darum herumkommen, wenigstens die essenziellen Teile der Perl-Dokumentation durchzuarbeiten. Damit kommst Du zwar auch nicht nach NY, aber zumindest darfst Du dann schonmal aufs Rollfeld gehen und vielleicht sogar aus dem Cockpit-Fenster sehen. ;-)

          Witzig, dass Du genau diese Strecke ansprichst. Die bin ich tatsächlich schonmal als Passagier geflogen (allerdings in einem Airbus) - und genau deswegen habe ich mich aus Neugierde mal am MSFS in die Boeing 747 (Airbus hatte er nicht :-() gesetzt und diese Strecke eingestellt, als er mir -ulkigerweise wirklich für grob 20 Minuten- zur Verfügung stand.
          In 20 Minuten schafft man es natürlich nicht "rüber", so dass ich die Landung nicht auch noch testen konnte (*Angeblich* können die Teile sogar mittlerweile eine nahezu vollautomatische Landung hinlegen, wenn der Flughafen die entsprechende Technik dazu hat, und EDDF bzw. KJFK gehören da dazu - das Feature hätte ich wirklich gern mal gesehen), aber Take-Off und in der Luft bleiben funktioniert schon nach 20 Minuten erstaunlich gut.
          Der Trick: Nicht alles selbst machen versuchen, sondern auf den Autopilot zurückgreifen, der bei diesen Vögeln nämlich auch komplexere Manöver beherrscht als nur den aktuellen Kurs zu halten, wenn man ihn mit den entsprechenden Daten (z.B. gewünschte Steigrate oder gewünschte Höhe) füttert.*

          Das gleiche Prinzip sehe ich auch bei Perl: Man muss nicht alles bis ins letzte Fitzelchen verstanden haben, um sein Ziel zu erreichen, sondern erreicht auch schon viel, indem man versucht, die Zusammenhänge zu verstehen, und sich quasi in Modulbauweise die Teile zusammensucht, die man braucht.

          Meine Vorlage war dieses Beispiel aus selfhtml: http://de.selfhtml.org/perl/funktionen/systemaufrufe.htm#waitpid
          Falls Du das Beispiel für so schlecht hältst - vielleicht könntest Du eine bessere Version davon einstellen, dann haben alle etwas davon?
          Unnötig, die Perl-Dokumentation ist für diesen Fall ausreichend und auch auf Deutsch verfügbar.

          Na ja, das Statement finde ich etwas gewagt. Selbst wenn Du der große Perl-Gott bist und Dir diese Doku gereicht hat, heißt das noch lange nicht, dass es jeder auf dem gleichen Weg schafft wie Du.
          Etwas Starthilfe wäre für den einen oder anderen sicherlich hilfreich - und je besser die Beispiele, desto weniger "doofe" Fragen kommen im Forum auf.

          [...]

          Ich habe den Verdacht, Du planst ein Haus, in dem Du Stücke aus den Konstruktionsplänen von Sanssoussi, dem Eiffelturm, der Berliner Mauer, dem Telemichel und der Titanic rausreißt und mit reichlich Kleister auf dem Plan vom VW Käfer zusammenklebst. Das Ergebnis wird in aller Regel nicht zu Deinem Traumhaus führen.

          Du hast die Teile identifiziert,aber Du hältst den Plan verkehrt herum (macht nix, passiert jedem Mal, auch bei IKEA-Plänen *grins*). Der Käfer fährt auf der Innenseite der Berliner Mauer herum, die ringförmig zusammengepappt den Eiffelturm umkreist. Da er die selbe Geschwindigkeit hat wie die Mauer, nur in entgegengesetzter Richtung, scheint er im Vergleich zum Eiffelturm stillzustehen. Somit kann man über die am Turm seitlich angeklebte Bugspitze der Titanic problemlos in den Käfer wechseln und auch wieder aussteigen. Sanssouci ist das Kassenhäuschen, und den Telemichel haben wir sogar noch übrig - magst ihn haben?
          (Nimm's mir nicht krumm, wir haben heute >37° im Office, da kommt man auf solche Ideen. Normalerweise hätte ich nur den Käfer auf einer zum Möbiusband zusammengeklebten Berliner Mauer fahren lassen)

          [SafePipeOpens-Links]
          Danke, werde ich mir anschauen.

          [...]

          Wobei mir noch nicht so ganz in den Kopf will, warum Du den Webserver mit zusätzlicher Arbeit belasten willst, während ein ohnehin langsames Programm läuft. Wenn Du dem Benutzer Aktivität vorgaukeln willst, benutze ein animiertes GIF, das Du nach Ende der langen Arbeit ausblendest oder ersetzt. Oder verarbeite die Ausgabe des Kindprozesses direkt im CGI, noch während der Kindprozess die Ausgabe anliefert. Das dürfte in aller Regel die effizienteste Lösung sein.

          Ich hatte das Programm ursprünglich ohne Fork im Einsatz.
          Das Problem dabei ist, dass die Ausgabe des externen Programms schlagartig am Ende erfolgt, und nicht Stück für Stück.
          In der neuen Version ist die Startzeit des Programms aber so übermäßig angewachsen, dass es zwischen Browser und Server zu einem Timeout kommt, bevor das externe Programm sein Ergebnis zurückliefern kann.
          (Wobei das Problem früher schon auftreten konnte, wenn eine besonders große Datei übergeben wurde, aber da das selten bis gar nicht vorkam, war es da nicht störend.)
          Deswegen wollte ich die Verbindung am Leben erhalten, indem ich ihm vom einen Prozeßteil aus so lange die "Bitte Warten"-Nachricht schicke, bis er im anderen Prozeßteil zu einem Ergebnis gekommen ist.

          Gruß Stefan

          *Bitte keine Belehrung, dass es ziemlich sinnfrei ist, einen Simulator anzuschmeißen, nur um sich dann vom Autopilot durch die Gegend schaukeln zu lassen, anstatt selber den Stick in die Hand zu nehmen. Es war damals einfach nur "just for fun". ;-)
          Und um Deine ursprüngliche Frage zu beantworten, auch wenn sie eher rhetorisch gemeint war: Ich würde mich natürlich sicher nicht an die Kontrollen einer echten Maschine setzen, solange keine Notwendigkeit besteht. Aber schon nach dem bisschen Sim-Erfahrung würde ich, wenn ich in so einem Vogel in der Luft bin und ein Notfall eintritt, eher versuchen, mir via Funk Unterstützung vom nächsten Airport zu holen und einen Landeversuch hinlegen, als mich sinnlos zu besaufen und auf den sicheren Tod zu warten. ;-)

          1. Hallo Stefan!

            [...]
            Na ja, das Statement finde ich etwas gewagt. Selbst wenn Du der große Perl-Gott bist und Dir diese Doku gereicht hat, heißt das noch lange nicht, dass es jeder auf dem gleichen Weg schafft wie Du.
            Etwas Starthilfe wäre für den einen oder anderen sicherlich hilfreich - und je besser die Beispiele, desto weniger "doofe" Fragen kommen im Forum auf.
            [...]

            Und viele Statements in Deinem Posting, die ich unterschreiben kann.
            Manchem hier wurde schon mal Cargo Cult Programming[1] vorgeworfen, weil sie es gewagt haben, in einer Antwort den Aufruf einer Sub noch mit &sub; vorzuschlagen... Die Erklärung, warum es so ist, kam erst auf Nachfrage.

            Deswegen ist in Deinem Posting als Einziger der Ausdruck Perl-"Gott" falsch gewählt, denn meine allgemeine, mir überlieferte Vorstellung von Gott ist die einer Gestalt, die ihren Geschöpfen zur Seite steht. Aber mag sein, dass manche hier das Alte Testament für glaubwürdiger halten, als die Egangilen - wich auch immer, ich halte mich da raus. Sowohl in Glaubens- als auch in Programmierfragen.

            In Entweder "Einführung in Perl" oder "Programming mit Perl" gibt es eine dieser vielen, meist ganz lustigen Fußnoten, die - ich bin zu dumm, mir in Bücher Lesezeichen[2] zu machen, wie ich im Internet die entsprechende Seite gebookmarkt hätte - leider nicht mehr auf Anhieb finde, deren Wortlaut aber sinngemäß bedeutet, dass es nicht drauf ankommt, wie ein Perl-Programm aussieht, sondern darauf, dass es zur Zufriedenheit aller einfach seine Arbeit tut.

            [1] Trotz der Richtigkeit eines jeden Vorwurfs kommt es drauf an, wie ein solcher 'rüberkommt. Wenn ich behaupten würde, STDIN sei eine Möglichkeit, den Speicher frei zu geben, weil es für mich "Send This Data In Nirvana" bedeutet, würde ich eine gesalzene und sogar noch gepfefferte Zurechtweisung verdienen.
            [2] Dazu müsste ich aus chronischem Papiermangel aus anderen Büchern Seiten ausreißen oder zu gewissen Papierrollen greifen... ach nee, ich kauf' mir lieber ein Notizblock für's nächste interessante Buch ;)

            Viele Grüße aus Frankfurt/Main,
            Patrick

            --

            _ - jenseits vom delirium - _
            [link:hatehtehpehdoppelpunktslashslashwehwehwehpunktatomicminuseggspunktcomslash]
            Nichts ist unmöglich? Doch!
            Heute schon gegökt?
          2. Wobei mir noch nicht so ganz in den Kopf will, warum Du den Webserver mit zusätzlicher Arbeit belasten willst, während ein ohnehin langsames Programm läuft. Wenn Du dem Benutzer Aktivität vorgaukeln willst, benutze ein animiertes GIF, das Du nach Ende der langen Arbeit ausblendest oder ersetzt. Oder verarbeite die Ausgabe des Kindprozesses direkt im CGI, noch während der Kindprozess die Ausgabe anliefert. Das dürfte in aller Regel die effizienteste Lösung sein.

            Ich hatte das Programm ursprünglich ohne Fork im Einsatz.
            Das Problem dabei ist, dass die Ausgabe des externen Programms schlagartig am Ende erfolgt, und nicht Stück für Stück.
            In der neuen Version ist die Startzeit des Programms aber so übermäßig angewachsen, dass es zwischen Browser und Server zu einem Timeout kommt, bevor das externe Programm sein Ergebnis zurückliefern kann.
            (Wobei das Problem früher schon auftreten konnte, wenn eine besonders große Datei übergeben wurde, aber da das selten bis gar nicht vorkam, war es da nicht störend.)
            Deswegen wollte ich die Verbindung am Leben erhalten, indem ich ihm vom einen Prozeßteil aus so lange die "Bitte Warten"-Nachricht schicke, bis er im anderen Prozeßteil zu einem Ergebnis gekommen ist.

            Also für mich klingt das nach verlorener Liebesmühe. Ich kenn mich mit fork nicht aus, aber wir reden über ein CGI Skript, das über vom Server gestartet und u.U. auch vorzeitig beendet wird. Du versuchst hiermit ja dieses Limit zu verlängern, ich kenn mich mit diesen Systemnahen Oprationen nicht aus, fände es aber merkwürdig wenn dies so gehen würde. Ich denke mal das fork im zusammenhang mit CGI Skripten soweiso selten eine sinnvolle Sache ist.

            Struppi.

          3. Ich hatte das Programm ursprünglich ohne Fork im Einsatz.
            Das Problem dabei ist, dass die Ausgabe des externen Programms schlagartig am Ende erfolgt, und nicht Stück für Stück.

            Das ist im CGI-/HTTP-Kontext völlig normal: Bevor die Serverantwort verschickt wird, wird sie vom Server komplett zusammengebaut. Sowas könnte man via Non-parsed Header umgehen, siehe u.a.:

            http://www.unix.org.ua/orelly/linux/cgi/ch03_03.htm#ch03-10-fm2xml
            http://www.wdvl.com/Authoring/Scripting/Tutorial/nph.html
            http://perldoc.perl.org/CGI.html#USING-NPH-SCRIPTS

            In der neuen Version ist die Startzeit des Programms aber so übermäßig angewachsen, dass es zwischen Browser und Server zu einem Timeout kommt, bevor das externe Programm sein Ergebnis zurückliefern kann.

            Dann setze den Wert für's Timeout hoch, das sollte doch gehen, wenn du dich in einem Intranet bewegst, oder?

            Siechfred

            --
            Ein Selbständiger ist jemand, der bereit ist, 16 Stunden am Tag zu arbeiten, nur um nicht 8 Stunden für einen Anderen arbeiten zu müssen.
    2. Hallo Alexander, hier nun die vorläufigen Ergebnisse.

      Das Skript sieht jetzt so aus:
      ------------------------------------------------------
      #!/usr/bin/perl -w -T
      use strict; # strict und -T: Eingebaut auf Alexanders Rat
      use CGI::Carp qw(fatalsToBrowser);
      use POSIX;
      print "Content-type: text/html\n\n";
      print "ENV_PATH: $ENV{'PATH'}<br>"; # Eingebaut auf Alexanders Rat

      print qq~
      <HTML>
      <HEAD><TITLE>Test</TITLE></HEAD>
      <BODY>
      <tt>~;
      my @returnvalue=();
      $|=1;
      pipe READFD,WRITEFD;
      my $oh= select(WRITEFD); select($oh);
      my $Kind_pid = fork();

      die "Nicht geforkt: $!\n" unless defined $Kind_pid; # Eingebaut auf Alexanders Rat

      if($Kind_pid) {
      use POSIX ":sys_wait_h";
      my $waitcount=1;
      do {
      my $Kind_pid = waitpid(-1,&WNOHANG);
      #Wartenachricht hier
      print " ...".$waitcount;
      $waitcount++;
      sleep 2;
      } until $Kind_pid == -1;
      close(WRITEFD);
      @returnvalue=<READFD>;
      }
      else {
      my $cvs='';
      sleep 10;
      my @cvscan=/bin/echo "foobar";
      close(READFD);
      foreach $cvs (@cvscan){
      #print $cvs."<br>\n";
      print WRITEFD $cvs;
      }
      close STDOUT;
      exit(0);
      }

      #Weitere Verarbeitung hier
      print "\nAusgabe hier\n";
      print @returnvalue;
      print qq~
      </tt>
      </BODY>
      </HTML>~;

      Wenn ich es jetzt an der Kommandozeile aufrufe, sieht es nach einer Verschlimmbesserung aus, denn es terminiert nun auch dort wieder nicht:
      ------------------------------------------------------
      perl -T /var/www/cgi-bin/test.pl
      Content-type: text/html

      ENV_PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/usr/local/sbin:/usr/local/bin:/usr/games<br>
      <HTML>
      <HEAD><TITLE>Test</TITLE></HEAD>
      <BODY>
      <tt> ...1 ...2 ...3 ...4 ...5 ...6<h1>Software error:</h1>
      <pre>Insecure $ENV{PATH} while running with -T switch at /var/www/cgi-bin/test.pl line 38.
      </pre>
      <p>
      For help, please send mail to this site's webmaster, giving this error message
      and the time and date of the error.

      </p>
      [Thu Jul 12 16:26:33 2007] test.pl: Insecure $ENV{PATH} while running with -T switch at /var/www/cgi-bin/test.pl line 38.
      ...7 ...8 ...9 ...10 ...11 ...12 ...13
      ------------------------------------------------------
      Aber Du kannst mir sicherlich sagen, was ich gegen diese Fehlermeldung konkret unternehmen muss? Ich hatte die Hoffnung, es würde reichen, den absoluten Pfad zur ausführbaren Datei anzugeben, aber scheinbar mag er da was anderes nicht.

      Hier noch die Ausgabe beim Start via Webserver, wo er ebenfalls nicht terminiert:
      ------------------------------------------------------
      ENV_PATH: /usr/local/bin:/bin:/usr/bin:/usr/lib/cgi-bin<br>
      <HTML>
      <HEAD><TITLE>Test</TITLE></HEAD>
      <BODY>
      <tt> ...1 ...2 ...3 ...4 ...5<h1>Software error:</h1>

      <pre>Insecure $ENV{PATH} while running with -T switch at test.pl line 38.
      </pre>
      <p>
      For help, please send mail to this site's webmaster, giving this error message
      and the time and date of the error.

      </p>
      ...6[Thu Jul 12 16:29:25 2007] test.pl: Insecure $ENV{PATH} while running with -T switch at test.pl line 38.
      ...7 ...8 ...9 ...10 ...11 ...12 ...13
      ------------------------------------------------------

      Bin auf die nächsten Lösungsansätze gespannt...

      Gruß Stefan

      1. Wenn ich es jetzt an der Kommandozeile aufrufe, sieht es nach einer Verschlimmbesserung aus, denn es terminiert nun auch dort wieder nicht:

        perl -T /var/www/cgi-bin/test.pl

        <h1>Software error:</h1>
        <pre>Insecure $ENV{PATH} while running with -T switch at /var/www/cgi-bin/test.pl line 38.

        Aber Du kannst mir sicherlich sagen, was ich gegen diese Fehlermeldung konkret unternehmen muss?

        Siehe perlsec

        Ich hatte die Hoffnung, es würde reichen, den absoluten Pfad zur ausführbaren Datei anzugeben, aber scheinbar mag er da was anderes nicht.

        Doch, $ENV{'PATH'} gefällt dem Taint-Modus nicht. Der ist nämlich reichlich paranoid, und das mit Recht. Wer garantiert Dir denn, dass das Programm, das Du startest, auch überall absolute Pfade benutzt? Richtig, niemand. Deswegen muß dafür gesorgt werden, dass nur "sichere" Verzeichnisse nach evtl. weiteren Kindprozessen durchsucht werden. Ähnliche Probleme werden noch einige weitere Variablen machen, aufgrund der Unart vieler externer Programme, andere Programme über die Shell zu starten.

        Hier noch die Ausgabe beim Start via Webserver, wo er ebenfalls nicht terminiert:

        ENV_PATH: /usr/local/bin:/bin:/usr/bin:/usr/lib/cgi-bin<br>
        <HTML>
        <HEAD><TITLE>Test</TITLE></HEAD>
        <BODY>
        <tt> ...1 ...2 ...3 ...4 ...5<h1>Software error:</h1>

        <pre>Insecure $ENV{PATH} while running with -T switch at test.pl line 38.

        Terminiert doch perfekt. ;-)

        OK, der Kindprozess vom CGI vielleicht nicht, oder vielleicht bekommt es das CGI auch einfach nicht mit. Bist Du sicher, dass nach dem Ende des CGI-Prozesses der Kindprozess noch läuft?

        Laß statt der dummen Zahlen mal eine aktuelle Prozessliste rausschreiben (ps -ef oder ps aux, je nach Betriebssystem).

        Wenn der Kindprozess schnell wieder verschwindet, aber dein CGI weiterläuft, bekommt es vom Kindstod nichs mit.

        1. Doch, $ENV{'PATH'} gefällt dem Taint-Modus nicht. Der ist nämlich reichlich paranoid, und das mit Recht. Wer garantiert Dir denn, dass das Programm, das Du startest, auch überall absolute Pfade benutzt?

          "-v" bitte. Wenn ich "/bin/foobar /tmp/tempfile" ausführe, wo kommt dann noch ein Pfad her, der mir Probleme machen könnte? Ich lerne ja gerne dazu, aber Dazu solltest Du Dein Wissen so vermitteln, dass man es als "Unwürdiger" ;-) versteht.

          [...]

          <pre>Insecure $ENV{PATH} while running with -T switch at test.pl line 38.

          Terminiert doch perfekt. ;-)

          Eben nicht. Wenn Du ein paar Zeilen weiter drunter nachschaust, siehst Du, dass der Zähler da munter weitermarschiert. *grumpf*

          [...]

          Laß statt der dummen Zahlen mal eine aktuelle Prozessliste rausschreiben (ps -ef oder ps aux, je nach Betriebssystem).

          Will do. Ergebnis demnächst in diesem Theater, äh, Thread.

          Gruß Stefan

          1. Doch, $ENV{'PATH'} gefällt dem Taint-Modus nicht. Der ist nämlich reichlich paranoid, und das mit Recht. Wer garantiert Dir denn, dass das Programm, das Du startest, auch überall absolute Pfade benutzt?

            "-v" bitte. Wenn ich "/bin/foobar /tmp/tempfile" ausführe, wo kommt dann noch ein Pfad her, der mir Probleme machen könnte? Ich lerne ja gerne dazu, aber Dazu solltest Du Dein Wissen so vermitteln, dass man es als "Unwürdiger" ;-) versteht.

            Um mal aus man 3 execvp zu zitieren:

            The  functions  execlp() and execvp() will duplicate the actions of the shell in searching for an executable file  if  the  specified  filename does  not  contain  a slash (/) character.  The search path is the path specified in the environment by the PATH variable.   If  this  variable isn't specified, the default path ":/bin:/usr/bin" is used.

            Wenn /bin/foobar irgendwo execvp() oder execlp() aufruft, und sei es nur, weil ein übermüdeter Coder für irgendeine über 10 Umwege eingelinkte Library gerade unlink() vergessen hatte und stattdessen fork() && execvp("rm","rm","-f",tmpfile,NULL) gehackt hat, wird die PATH-Environment-Variable durchsucht.

            popen(3) geht GRUNDSÄTZLICH über die Shell, schlimmer noch, popen() überläßt auch das Parsen der Kommandozeile der Shell. Zitat:

            The command argument is a pointer to a null-terminated string containing  a shell command line.  This command is passed to /bin/sh using the c flag; interpretation, if any, is performed by the shell.

            Alexander

        2. Hallo Alexander,

          OK, der Kindprozess vom CGI vielleicht nicht, oder vielleicht bekommt es das CGI auch einfach nicht mit. Bist Du sicher, dass nach dem Ende des CGI-Prozesses der Kindprozess noch läuft?

          Laß statt der dummen Zahlen mal eine aktuelle Prozessliste rausschreiben (ps -ef oder ps aux, je nach Betriebssystem).

          Wenn der Kindprozess schnell wieder verschwindet, aber dein CGI weiterläuft, bekommt es vom Kindstod nichs mit.

          Erstmal habe ich den Path bereinigt:
          -----------------------------------------------------------------------------

          #!/usr/bin/perl -w -T
          use strict;
          use CGI::Carp qw(fatalsToBrowser);
          use POSIX;
          print "Content-type: text/html\n\n";

          $ENV{'PATH'}='/bin:/usr/bin';

          print qq~
          <HTML>
          <HEAD><TITLE>Test</TITLE></HEAD>
          <BODY>
          <tt>~;
          my @returnvalue=();
          $|=1;
          pipe READFD,WRITEFD;
          my $oh= select(WRITEFD); select($oh);
          my $Kind_pid = fork();

          die "Aua: $!\n" unless defined $Kind_pid;

          if($Kind_pid) {
          use POSIX ":sys_wait_h";
          my $waitcount=1;
          do {
          my $Kind_pid = waitpid(-1,&WNOHANG);
          #Wartenachricht hier
          #print /bin/ps aux;
          print "...".$waitcount;
          $waitcount++;
          sleep 2;
          } until $Kind_pid == -1;
          close(WRITEFD);
          @returnvalue=<READFD>;
          }
          else {
          my $cvs='';
          sleep 10;
          my @cvscan=/bin/echo "foobar";
          close(READFD);
          foreach $cvs (@cvscan){
          #print $cvs."<br>\n";
          print WRITEFD $cvs;
          }
          close STDOUT;
          exit(0);
          }

          #Weitere Verarbeitung hier
          print "\nAusgabe hier\n";
          print @returnvalue;
          print qq~
          </tt>
          </BODY>
          </HTML>~;
          -----------------------------------------------------------------------------
          Aufruf an der Kommandozeile:
          perl -T test.pl
          Content-type: text/html

          <HTML>
          <HEAD><TITLE>Test</TITLE></HEAD>
          <BODY>
          <tt>...1...2...3...4...5...6...7...8...9...10...11...12...13...14
          (Ctrl-C), denn es terminiert wieder nicht :-(
          -----------------------------------------------------------------------------

          Dann Dein Vorschlag bzgl. ps aux:
          -----------------------------------------------------------------------------
          geänderter Abschnitt:
          if($Kind_pid) {
          use POSIX ":sys_wait_h";

          my $waitcount=1;

          do {
          my $Kind_pid = waitpid(-1,&WNOHANG);
          #Wartenachricht hier
          print /bin/ps aux;

          print "...".$waitcount;

          $waitcount++;

          sleep 2;
          } until $Kind_pid == -1;
          close(WRITEFD);
          @returnvalue=<READFD>;
          }
          -----------------------------------------------------------------------------
          perl -T test.pl
          Content-type: text/html

          <HTML>
          <HEAD><TITLE>Test</TITLE></HEAD>
          <BODY>
          <tt>
          [viel Output gekürzt]
          USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
          [...]
          root 3561 6.6 0.7 6796 3076 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3563 0.0 0.7 6504 2856 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3570 0.0 0.2 4268 944 /UNIONFS/dev/pts/0 R+ 08:23 0:00 /bin/ps aux
          USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
          [...]
          root 3561 5.0 0.7 6796 3076 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3563 0.0 0.7 6504 2856 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3573 0.0 0.2 4268 944 /UNIONFS/dev/pts/0 R+ 08:23 0:00 /bin/ps aux
          USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
          [...]
          root 3561 4.0 0.7 6796 3076 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3563 0.0 0.7 6504 2856 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3576 0.0 0.2 4268 944 /UNIONFS/dev/pts/0 R+ 08:23 0:00 /bin/ps aux
          USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
          [...]
          root 3561 3.4 0.7 6796 3076 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
          root 3580 0.0 0.2 4268 944 /UNIONFS/dev/pts/0 R+ 08:23 0:00 /bin/ps aux
          (Ctrl-C)

          Der zweite Teil beendet sich, aber die Warteschleife bekommt's nicht mit.

          So, jetzt warte ich auf Erleuchtung, großer Meister.

          Gruß Stefan

          1. #!/usr/bin/perl -w -T
            use strict;
            use CGI::Carp qw(fatalsToBrowser);
            use POSIX;
            print "Content-type: text/html\n\n";

            Die CGI-Spec verlangt hier CR + LF statt "\n". Die CGI-Library würde sich darum automatisch kümmern ...

            $ENV{'PATH'}='/bin:/usr/bin';

            perlsec sagt, dass noch mehr Environment-Variablen ausgemistet werden sollten.

            print qq~
            <HTML>
            <HEAD><TITLE>Test</TITLE></HEAD>
            <BODY>
            <tt>~;

            Auch das würde die CGI-Library erledigen.

            my @returnvalue=();
            $|=1;
            pipe READFD,WRITEFD;
            my $oh= select(WRITEFD); select($oh);

            Was genau soll diese Zeile (^^^) bewirken?

            my $Kind_pid = fork();

            die "Aua: $!\n" unless defined $Kind_pid;

            if($Kind_pid) {
            use POSIX ":sys_wait_h";

            Wenn Du das so schreibst, erwartest Du vermutlich, dass die POSIX-Libraray nur im Parent geladen wird. Das passiert aber nicht. Siehe perlfunc.

            my $waitcount=1;
            do {
            my $Kind_pid = waitpid(-1,&WNOHANG);
            #Wartenachricht hier
            #print /bin/ps aux;
            print "...".$waitcount;
            $waitcount++;
            sleep 2;
            } until $Kind_pid == -1;
            close(WRITEFD);
            @returnvalue=<READFD>;
            }
            else {
            my $cvs='';
            sleep 10;
            my @cvscan=/bin/echo "foobar";
            close(READFD);
            foreach $cvs (@cvscan){
            #print $cvs."<br>\n";
            print WRITEFD $cvs;
            }
            close STDOUT;
            exit(0);
            }

            #Weitere Verarbeitung hier
            print "\nAusgabe hier\n";
            print @returnvalue;
            print qq~
            </tt>
            </BODY>
            </HTML>~;

            Aufruf an der Kommandozeile:
            perl -T test.pl

            Es sollte perl -T -w test.pl sein.

            Content-type: text/html

            <HTML>
            <HEAD><TITLE>Test</TITLE></HEAD>
            <BODY>
            <tt>...1...2...3...4...5...6...7...8...9...10...11...12...13...14
            (Ctrl-C), denn es terminiert wieder nicht :-(

            Dann Dein Vorschlag bzgl. ps aux:

            geänderter Abschnitt:
            if($Kind_pid) {
            use POSIX ":sys_wait_h";

            my $waitcount=1;

            do {
            my $Kind_pid = waitpid(-1,&WNOHANG);
            #Wartenachricht hier
            print /bin/ps aux;

            print "...".$waitcount;

            $waitcount++;

            sleep 2;
            } until $Kind_pid == -1;
            close(WRITEFD);
            @returnvalue=<READFD>;
            }

            perl -T test.pl
            Content-type: text/html

            <HTML>
            <HEAD><TITLE>Test</TITLE></HEAD>
            <BODY>
            <tt>
            [viel Output gekürzt]
            USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
            [...]
            root 3561 6.6 0.7 6796 3076 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl

            /UNIONFS? Was ist das für eine Testumgebung? Knoppix?

            root 3563 0.0 0.7 6504 2856 /UNIONFS/dev/pts/0 S+ 08:23 0:00 perl -T test.pl
            root 3570 0.0 0.2 4268 944 /UNIONFS/dev/pts/0 R+ 08:23 0:00 /bin/ps aux

            Wo ist /bin/echo ?
            Warum läuft das Perl-Script als root und nicht als wwwrun (oder wie auch immer der Webserver-Account heißen mag)?

            Der zweite Teil beendet sich, aber die Warteschleife bekommt's nicht mit.

            So, jetzt warte ich auf Erleuchtung, großer Meister.

            pipe rauswerfen, Safe Pipe Opens aus perlipc benutzen. Im Kindprozess kein großartiges Theater veranstalte (zweiter fork durch ``, Ausgabe nochmals puffern), sondern schlicht und ergreifend "exec $prog,@args; die 'exec failed';". Nicht mehr.

            Wenn Webserver bzw. Browser wirklich in ein Timeout hineinlaufen, das aufgerufene Programm beschleunigen, z.B. indem Du es etappenweise mit jeweils weniger Daten fütterst oder den Algorithmus gründlich optimierst.

            Ein anderer Plan wäre, mit dem ersten Aufruf das lang laufende Programm vollständig im Hintergrund zu starten (siehe "Complete Dissociation of Child from Parent" in perlipc), alle Ausgaben in eine Datei umzuleiten und dann per CGI und HTTP refresh darauf warten, dass das langsame Programm terminiert. Letzteres sicher und zuverlässig zu erkennen ist nicht ganz trivial, am sichersten dürfte wohl eine Signaldatei sein, die mit Ende des langsamen Programms gelöscht oder angelegt wird. Ein einfaches Shell-Script könnte das als Wrapper erledigen, dabei dient die Ausgabedatei selbst als Marker:

            #!/bin/bash
            PATH=/bin:/usr/bin
            rm -f output.txt
            schnecke "$@" > output.tmp 2> errors.txt < /dev/null
            mv output.tmp output.txt

            Sobald output.txt existiert, ist "schnecke" fertig, und die 100. Instanz des CGIs kann output.txt und error.txt auswerten statt den Nutzer wieder um 10 Sekunden zu vertrösten.

            Alexander