ugrás a tartalomhoz

PHP STDERR átirányítás

janoszen · 2011. Már. 14. (H), 20.15
Sziasztok!

Éppen azon próbálkozom, hogy PHP-ban daemont írjak. Egészen sok problémát már megoldottam, de egyet nem sikerült. Ha a PHP-ban becsukom az STDERR-t, akkor utána hiába nyitom újra, nem ugyanaz lesz a file descriptor number, ezért EBADF (Bad file descriptor) hibával elszáll. (Ezt egyékbént nem egészen értem, hogy miért.)

Normális működés esetén a PHP-nak nem kellene használnia az STDERR-t, de szeretnék lekezelni minden hibalehetőséget.

Mit javasoltok erre?

(Egyébként a cucc open source lesz, rövidesen szeretném majd prezentálni itt a Weblaboron is.)

Teszt kód, itt STDOUT-al megvalósítva:

<?php
ini_set('display_errors', true);
error_reporting(E_ALL);
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
fopen('/tmp/stdin', 'rw');
fopen('/tmp/stdout', 'rw');
fopen('/tmp/stderr', 'rw');
echo('test');
Strace kimenet:
munmap(0x7f57246bb000, 207)             = 0
close(3)                                = 0
munmap(0x7f57247f0000, 4096)            = 0
close(0)                                = 0
munmap(0x7f57246bd000, 4096)            = 0
close(1)                                = 0
munmap(0x7f57246bc000, 4096)            = 0
close(2)                                = 0
gettimeofday({1300125804, 921571}, NULL) = 0
lstat("/tmp/stdin", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
lstat("/tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0
open("/tmp/stdin", O_RDONLY)            = 0
fstat(0, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
lseek(0, 0, SEEK_CUR)                   = 0
close(0)                                = 0
gettimeofday({1300125804, 922135}, NULL) = 0
lstat("/tmp/stdout", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
open("/tmp/stdout", O_RDONLY)           = 0
fstat(0, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
lseek(0, 0, SEEK_CUR)                   = 0
close(0)                                = 0
gettimeofday({1300125804, 922565}, NULL) = 0
lstat("/tmp/stderr", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
open("/tmp/stderr", O_RDONLY)           = 0
fstat(0, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
lseek(0, 0, SEEK_CUR)                   = 0
close(0)                                = 0
write(1, "test", 4)                     = -1 EBADF (Bad file descriptor)
munmap(0x7f57246be000, 528384)          = 0
 
1

Amit én csinálok: Nem

gphilip · 2011. Már. 14. (H), 20.29
Amit én csinálok:

Nem nyitok újra semmilyen descriptort (kicsit hacky-nak érzem ezt az "átirányítás" dolgot), hanem csak nyomok egy ignore_user_abort(true);-t, ha esetleg bármilyen véletlen kimenet menne a scriptből, akkor mégse álljon le az egész szó nélkül.

Ha ugyanis egy CLI-ből futott scriptben bezárod az STDOUT-ot és a scripted történetesen mégis tol valamit a kimenetre, a PHP azt hiszi, hogy weboldalt hajt végre és a kliens bezárta a kapcsolatot (nem tud írni az STDOUT-ra) így mindenféle üzenet nélkül leáll.

Mindneképpen szükséged van nyitott STDERR-re?

Ezen kívül a blogodon írott daemon implementációhoz (remek cikk egyébként!) még egy minimális adalék: miután a processz session és pg leader lesz, még egyszer forkolnod kell (kilépni a szülő processzből), hogy "elereszd" a tty-t.
2

Nem feltétlen

janoszen · 2011. Már. 14. (H), 20.41
Alapvető feltevés az, hogy sem az STDOUT-ra, sem az STDERR-re nem fog semmi menni, hacsak hiba folytán nem, viszont szeretném elkerülni, hogy elcrasheljen az egész egy bad file descriptor hibával. Meg alapvetően ha nem PHP-ban, hanem mondjuk C-ben, Perlben, Pythonban írnám meg, akkor ez lenne a helyes eljárás.

Ami a blogomon írt daemon implementációt illeti, legjobb tudomásom szerint a második fork csak ahhoz kell, hogy ne kelljen zombi processzeket kezelni, mivel ha a process group leader kilép, akkor onnantól az init adoptálja a gyerekprocesszeket. Ergó a programozói (egészséges) lustaság egyik fajtája. Én inkább kezelek zombikat.

Szerk: megnéztem ignore_user_abort-tal is, becsukott FD-vel tök mindegy, be van-e kapcsolva, így is, meg úgy is elhasal. Ergó nem használ semmit.

Szerk 2: Utána néztem a double forknak, itt van egy egész jó leírás róla: http://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon
7

Második fork témában:

gphilip · 2011. Már. 14. (H), 21.34
Második fork témában: http://bit.ly/e67CvW
8

Terminál

janoszen · 2011. Már. 14. (H), 21.37
Ergó ha nem kérek terminált, akkor nem lesz belőle probléma. OK, én is ezt gondoltam. A kérdés az, hogy implicit módon mikor lehet terminált kérni akaratlanul is?
12

Valószínűleg soha, de ha már

gphilip · 2011. Már. 14. (H), 22.26
Valószínűleg soha, de ha már a bombabiztos megoldásokról beszélünk... :) mellesleg ha az osztály, amit közzéteszel, általános felhasználásra íródik, akkor nem árt mindenre eshetőségre felkészülni.

Mondjuk egy socket server esetén például nem feltétlen kell "megdögleni" a szülő processzben, hanem egy pcntl_wait-tel figyelni a meghaló forkokat és szépen újakat indítani a helyükre (asszonyról lemászás elkerülése végett ;))
14

Guardian

janoszen · 2011. Már. 14. (H), 23.30
Igen, azt hiszem, ezt hívják guardian processnek.
9

Szerk: megnéztem

gphilip · 2011. Már. 14. (H), 21.44
Szerk: megnéztem ignore_user_abort-tal is, becsukott FD-vel tök mindegy, be van-e kapcsolva, így is, meg úgy is elhasal. Ergó nem használ semmit.


Nekem csukott STDOUT-ra való írásnál abszolút tökéletesen működik, STDERR-rel pedig a záró echo lefut, igaz, warninggal:

<?php

fclose( STDERR );

if ( $fp = fopen( 'php://stderr', 'a' ) ) 
{
    fwrite( $fp, "Test stderr\n" );
    fclose( $fp );
}

echo "All OK\n";
[root@wcphp test]# php test.php

Warning: fopen(php://stderr): failed to open stream: operation failed in /var/phpprojects/php_tracker/test.php on line 5

Call Stack:
    0.0035     320116   1. {main}() /var/phpprojects/php_tracker/test.php:0
    0.0036     319896   2. fopen(string(12), string(1)) /var/phpprojects/php_tracker/test.php:5

All OK
[root@wcphp test]#
10

Ja...

janoszen · 2011. Már. 14. (H), 21.50
Én az STDOUT-ot is becsuktam, na az már ezek szerint problémás. A fenti teszt kódot nézd meg mondjuk két echo paranccsal strace-ban az ignore_user_abort-tal az elején, annak el kéne hasalnia.
11

Mégsem hasal el

gphilip · 2011. Már. 14. (H), 22.12
Mégsem hasal el :(

<?php

fclose( STDOUT );
fclose( STDERR );

ignore_user_abort( true );

if ( $fp = fopen( 'php://stderr', 'a' ) ) 
{
    fwrite( $fp, "Test stderr\n" );
    fclose( $fp );
}

echo "Test stdout";

file_put_contents( 'test_was_ok_' . date( 'YmdHis' ), 'ok' );
[root@wcphp test]# ls
test.php
[root@wcphp test]# php test.php
[root@wcphp test]# ls
test.php  test_was_ok_20110314211156
[root@wcphp test]#


ignore_user_abort nélkül viszont igen:

<?php

fclose( STDOUT );
fclose( STDERR );

if ( $fp = fopen( 'php://stderr', 'a' ) ) 
{
    fwrite( $fp, "Test stderr\n" );
    fclose( $fp );
}

echo "Test stdout";

file_put_contents( 'test_was_ok_' . date( 'YmdHis' ), 'ok' );
[root@wcphp test]# ls
test.php
[root@wcphp test]# php test.php
[root@wcphp test]# ls
test.php
[root@wcphp test]#


[root@wcphp test]# php -v
PHP 5.3.5 (cli) (built: Jan  7 2011 18:29:01)
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies
    with eAccelerator v0.9.6.1, Copyright (c) 2004-2010 eAccelerator, by eAccelerator
    with Xdebug v2.1.0, Copyright (c) 2002-2010, by Derick Rethans
[root@wcphp test]#
16

Jogos

janoszen · 2011. Már. 14. (H), 23.37
Jogos, ezt benéztem. Úgy tűnik, a PHP ha már egyszer hibát kapott, akkor onnantól kezdve az összes echo() megy a levesbe, a syscallt már meg sem próbálja, ezért nem láttam a strace-ben.

Ergó tied a pont, probléma megoldva. Nem úgy, ahogy szerettem volna, de jó lesz.
17

Király, izgatottan várjuk a

gphilip · 2011. Már. 15. (K), 00.06
Király, izgatottan várjuk a kódot! :)
18

Van

janoszen · 2011. Már. 15. (K), 00.11
Igazából már van, de meglehetősen... fragmentált állapotaban. Ha akarsz kódot túrni, az svn.janoszen.com-on nézz szét az fw repóban.
3

Parent-child process

Ifju · 2011. Már. 14. (H), 20.42
Ha nem muszáj ugyanannak a processznek megmaradnia, akkor érdemes megpróbálni, hogy az indított process-ben lezárod a kimeneteket, ezzel detach-olod magad a konzoltol, majd indíts egy gyerek folyamatot, annak pedig nem kell piszkálni már a kimenetét.
4

Nem kell

janoszen · 2011. Már. 14. (H), 20.44
És ez miben segít? Az STDOUT/STDERR nem lesz valid file descriptor, ergó ha valaki megpróbál rá írni, akkor jön az ominózus hiba.

Vagy arra gondolsz, hogy a gyerekprocesszt indítsam popen-nel és kapjam el az STD ki/bemeneteit?
5

Valóban... error_handler()

Ifju · 2011. Már. 14. (H), 20.58
Valóban... error_handler() használata nem lenne megoldás?
6

De.

janoszen · 2011. Már. 14. (H), 21.18
De, de ismersz. :) Még a saját programkódomban sem bízom meg annyira, hogy ne akarjak legalább két szintű védelmet az esetleges hibaforrásokra. Ez nem egy PHP script, ami fehér oldalt ad, a következő frissítésre pedig jó lesz, hanem egy daemon. Ergó ha ez lehal, akkor egy kedves barátom szava járása szerint "hajnali kettőkor le kell mászni az asszonyról" és meg újra be kell röffenteni.
13

Milyen verziójú PHP?

Pred · 2011. Már. 14. (H), 22.37
Ahogy ismerlek, ezt a kört már bejártad, de milyen verziójú PHP-n futtatod? Több verziónál is (5.1.4, 5.1.5, 5.0.5) létezik az a hiba, hogy nem lehet lezárni egyes streameket.

http://bugs.php.net/bug.php?id=38199

Emellett csak kis stilisztikai dolog:
STDIN csak olvasható, STDOUT és STDERR pedig csak írható, bár nem hiszem, hogy ez problémát okozna.
15

5.3

janoszen · 2011. Már. 14. (H), 23.32
5.3-nál alább nem adjuk mostanság.
19

monit

Hodicska Gergely · 2011. Már. 15. (K), 00.26
Szvsz ha PHP-ban írsz daemont, akkor kb. nincs esélyed tökéletes megoldást készíteni. Persze sok esetben lehet nagyon jó eredményt elérni, de azért akárhogyis, nem erre lett tervezve, ezért külső védelemre van szükséged, ami pl. lehet egy monit. Eleve PHP-ban egészségesebb X "kör" után kilépni, és újból elindulni. A leglehetetlenebb helyeken lehet memory leak például.
20

A memory leak-ekre lehet

gphilip · 2011. Már. 15. (K), 02.59
A memory leak-ekre lehet szintén jó megoldás ha a szülő process figyeli a gyerekeit, amiket újraindít, ha leállnak (Guardian? tényleg így hívják?). Egy-egy process explicit meghalhat X iteráció után (pl. 50 klienst szolgált ki), így a memóriaterülete felszabadul, de rögtön új fork indul helyette.

Ilyenkor csak arra kell figyelni, hogy a szülő tuti ne okozzon galibát, szóval lehetőleg a legminimálisabb kód kerüljön a fork elé és alaposan át kell nézni a pcntl_work környékén a loopot.
21

Monit

janoszen · 2011. Már. 15. (K), 07.27
Na, a monit az egy önálló történet, azzal voltak szívásaim.

Ami a PHP-t illeti, nekem van olyan daemonom és nem tűnt úgy, mint ha eszeveszett módon memleakelne, pedig dolgozik rendesen. Persze az is hozzá tartozik, hogy mindenhol explicit módon unsetelve vannak a dolgok, stb. Talán azért, mert az 5.3-ban sok mindent javítottak az enginen. Ha memleakel, akkor majd max nekiesek debuggerrel és kikeresem, hogy hol van, úgyis ideje lenne, hogy valaki végre rendbe rakja a PHP-t.
22

memleak

Hodicska Gergely · 2011. Már. 15. (K), 14.44
Az egy dolog, hogy egy adott script épp működik, az meg másik dolog, hogy általánosan kell-e számítanod arra, hogy előfordulhat memory leak. PHP-nél szvsz igen, és nem biztos, hogy belefér, hogy minden módosítás után hosszú tesztek következzenek, célszerűbb, ha olyan a környezet, ami kezeli ezt, vagy amit gphilip is javasolt, hogy a szülő figyeli ezt, csak akkor megint kicsit a PHP-ra vagy hagyatkozva, igaz egy jól behatárolható részen.
23

Hobbyprojekt

janoszen · 2011. Már. 16. (Sze), 09.15
Egyrészt hobbyprojekt, másrészt valóban nem tart semeddig egy tekerhető paramétert betenni erre a viselkedésre.
24

Végre volt egy kis időm hogy

gphilip · 2011. Már. 16. (Sze), 11.33
Végre volt egy kis időm hogy pusholjam githubra a projektet, ahol szerepel az én implementációm. Küldtem már blogmarkban, a PHPTracker nevű jószágról van szó. Ez ti. tartalmaz egy seed szervert, ami ugye egy klasszikus socket szerver.

Az absztrakt "forkoló" osztály itt található:
PHPTracker_Threading_Forker

És itt egy implementációja:
PHPTracker_Seeder_Peer

Építő jellegű kritikát szívesen fogadok.

Ha már itt tartunk, ti hogyan egységtesztelnétek egy ilyen osztályt?
25

most talaltam, nagyon

Tyrael · 2011. Ápr. 11. (H), 16.41
most talaltam, nagyon vicces:
http://andytson.com/blog/2010/05/daemonising-a-php-cli-script-on-a-posix-system/
ugy tunik, hogy, ha lezarod a STDIN, STDOUT, STDERR fd-ket, majd _megfelelo sorrendben_ nyitsz uj file descriptor-okat, akkor azok at fogjak venni a STDIN, STDOUT, STDERR helyet...

mukodo pelda, /home/tyrael-t ertelemszeruen at kell irni:

<?php
echo 'example output';
file_put_contents('php://stderr', 'example error');

fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);

unlink('/home/tyrael/std.out');
unlink('/home/tyrael/std.err');

$stdIn = fopen('/dev/null', 'r'); 
$stdOut = fopen('/home/tyrael/std.out', 'w'); 
$stdErr = fopen('/home/tyrael/std.err', 'w'); 

echo 'example output';
file_put_contents('php://stderr', 'example error');
ezen felborultam most.

Tyrael
26

Nem

janoszen · 2011. Ápr. 11. (H), 17.05
Mint a Unixhoz értő ismerősöm felvilágosított, ez valóban így van - Linuxon. Azonban vannak olyan Unixok, amik a file descriptor számot nem sorrendben adják ki, ezért az egyetlen helyes megoldás az, ha dup2() függvényt használod a 0, 1 és 2 FD-k felülírására.
27

halisten en nem hasznalok mas

Tyrael · 2011. Ápr. 11. (H), 23.03
halisten en nem hasznalok mas Unixot, hol fordulna ez elo?
ha Mac-en vagy FreeBSD-n nem megy, az hataresetet, AIX-on meg hasonlon nem tervezek a kozeljovoben sem php-t futtatni. :)

Tyrael
28

Ez valóban így működik, C-ben

gphilip · 2011. Ápr. 12. (K), 21.02
Ez valóban így működik, C-ben is. Éppen ezért szokták a /dev/null-t megnyitogatni a standard be- és kimenetek után, h a szkriped még véletlenül se írjon mondjuk hibaüzeneteket például egy socketre.

Mivel a PHP nem használja az STDERR-t, és normális esetben a daemonod nem ír semmit a kimenetre (display erros off, csak mint a weben), ha ezt nem teszed meg, nem jelent komoly kockázatot.

Viszont az ignore_user_abort fontos, ha esetleg mégis menne a kimenetre valami a scripted ne haljon le.

További magyarázat:
We open/dev/null for standard input, standard output, and standard error. This guarantees that these common descriptors are open, and a read from any of these descriptors returns 0 (EOF), and the kernel just discards anything written to them. The reason for opening these descriptors is so that any library function called by the daemon that assumes it can read from standard input or write to either standard output or standard error will not fail. Such a failure is potentially dangerous. If the daemon ends up opening a socket to a client, that socket descriptor ends up as stdout or stderr and some erroneous call to something like perror then sends unexpected data to a client.
29

biztos hogy nekem akartal

Tyrael · 2011. Ápr. 13. (Sze), 11.43
biztos hogy nekem akartal valaszolni?
tisztaban vagyok vele, hogy hogy mukodik normal esetben az input/output redirection, az egesz szal arrol szolt, hogy nem lehetne-e valahogy azt megoldani, hogy ne a scripted inditasanal legyen ez az atiranyitas elkovetve, hanem ugyanugy mint a damonizalt alkalmazasoknal megszokott, a parent process forkolja ki a workert es detacholja magat az eredeti STDIN/STDOUT/STDERR-rol.
ahogy tobben le is irtak/irtuk, bezarni viszonylag egyszeru ezeket, sima fclose mukodik szepen, viszont ilyenkor ha barmi kodod irna/olvasna ezekrol a file descriptorokrol, akkor elszallna a kodod hibaval.
ezert lenne jobb, ha nem csak bezarni tudnad oket, hanem lecserelni.
egesz eddig nem tudtam rola, hogy lenne lehetoseg a futo php scriptbol meglepni ezt (kulso exec meg ilyesmi nelkul), viszont a fentebb linkelt blogpostban leirja a szerzo, hogy ha lezarod ezeket az FD-ket, majd __megfelelo sorrendben__ nyitsz harom uj FD-t, akkor a PHP ezeket fogja STDIN/STDOUT/STDERR-kent hasznalni.
es ez igy lehetove teszi, amit szerettunk volna.

viszont janoszen(proclub) bedobta, hogy ez nem minden Unix like rendszeren mukodne, mert nem mindegyik osztja az FD-ket szekvencialisan novekvo sorrendben, ezert az ujranyitasos trukk ott nem mukodne valoszinuleg.

ezert irta hogy dup2-t kellene hasznalni, erre irtam, hogy az php-bol nem elerheto kozvetlenul, illetve kerdeztem, hogy mely Unix like rendszerek erintettek ebben, mert ha csak nagyon obscure rendszereken problema ez, akkor engem nem zavar.

ha megis felreertettelek volna, akkor javits ki nyugodtan.

ignore_user_abort pedig csak akkor szukseges, hogyha lezarod de nem lecsereled az FD-ket, mi mar nem errol beszeltunk itt.

Tyrael
30

Tisztázás

janoszen · 2011. Ápr. 14. (Cs), 07.36
Több tanulságot is sikerült leszűrni:

  • Ha nem nyitod újra a 0,1,2-es FD-t, akkor fennáll a veszélye, hogy egy megnyitott másik fájlba fogja a rendszer beleírni az STDOUT illetve STDERR kimeneteidet.
  • Szerintem nem elég az ignore_user_abort, mert nem tudom, hogy az oprendszer kiosztja-e újra a Bad File Descriptor hiba után a megadott FD-t. Ha igen, akkor a fenti hiba veszélye fennáll.
  • Érdemes (lenne) dup2-vel átmásolni a 0,1,2-be az FD-t mert egyrészt nem garantált, hogy sorrendben nyitja újra a Unix, másrészt nem tudhatod, hogy a PHP-ban nem végeznek-e olyan módosítást, ami közben más file descriptorokat nyit. Valószínűtlen, de lehetséges.


Ergó amíg nincs dup2 támogatás a PHP-ban, Tyrael megoldása lesz a jó. Amint nő bele, érdemes azt használni. (Nem kizárt, hogy nekiállok belepatchelni, megfelelő tanuló feladatnak tűnik nekem.)
31

Ha nem nyitod újra a 0,1,2-es

Tyrael · 2011. Ápr. 14. (Cs), 10.43
Ha nem nyitod újra a 0,1,2-es FD-t, akkor fennáll a veszélye, hogy egy megnyitott másik fájlba fogja a rendszer beleírni az STDOUT illetve STDERR kimeneteidet.


igen, ezt nagyon fontos kiemelni.

másrészt nem tudhatod, hogy a PHP-ban nem végeznek-e olyan módosítást, ami közben más file descriptorokat nyit.

ha ugyanott nyitja a kodod az FD-ket ahol 2 sorral feljebb bezarod oket, akkor viszonylag kicsi az eselye, hogy nyilik valami FD, de persze elofordulhat:
- valami hiba miatt lefut egy error_handler
- erkezik egy signal es lefut a ra felhuzott signal handler
- fatal error miatt megall a kod a bezaras utan, de a megnyitas elott, es a register_shutdown_function-nel felhuzott callback, esetleg ugyanez ob_start-tal

szoval ha zarod oket, akkor szerintem is mindenkepp nyiss helyette masikat, de nem linux környezetben inkabb ne a scripten belulrol intezd az atiranyitast.

Tyrael
32

Érdemes (lenne) dup2-vel

Tyrael · 2011. Ápr. 30. (Szo), 13.23
Érdemes (lenne) dup2-vel átmásolni a 0,1,2-be az FD-t mert egyrészt nem garantált, hogy sorrendben nyitja újra a Unix, másrészt nem tudhatod, hogy a PHP-ban nem végeznek-e olyan módosítást, ami közben más file descriptorokat nyit. Valószínűtlen, de lehetséges.


ugy tunik, hogy Unix speci szerint a legalacsonyabb FD-t kell kiosztania az OS-nek.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
"The open() function shall return a file descriptor for the named file that is the lowest file descriptor not currently open for that process."

Tyrael
33

Nem garantálja

janoszen · 2011. Ápr. 30. (Szo), 13.42
Hasznos tudni, de nem garantálja, hogy a PHP nem mókol közben valami FD-ket a háttérben.
34

igen, nem is arra a reszre

Tyrael · 2011. Ápr. 30. (Szo), 14.08
igen, nem is arra a reszre valaszoltam.
btw. inditottam neki egy threadet az internalson:
http://marc.info/?l=php-internals&m=130416511430852&w=2

Tyrael