Wtf ?
Catcher les erreurs fatales en PHP
Samedi 6 février 2010, par //
un peu de technique
Version imprimable
Le coté chiant de PHP qui se veut objet est qu’une partie des functions sont procédurales et se terminent par des erreurs, et l’autre est objet avec des trys catchs possibles, mais que certains objets utilisant des fonctions dans leur coeurs génèrent des erreurs non interceptables (ex PDO qui défaille quand il y a trop de connexions à la base de données).
La grande question est peut-on catcher les erreurs non catchables, et profitons-en pour faire un tour de PHP et de ce qui est non dit.
Le problème
Quand on fait un eval, contrairement à perl (on utilise le eval pour faire du try catch), on meurt. Le script suivant teste toutes mes réussites pour catcher les erreurs fatals . Une erreur fatale dans un eval est fatale au script l’appelant.
Le problème c’est que l’on aimerait bien les intercepter.
Ceci est à éxécuter avec une version récente de PHP (5.x.y) de la date d’aujourd’hui
<?
$index=1;
function sig_handler($signo) {
static $index;
print "hello you $signo:${errno}x$index";
$index++;
$signo==17 and print("oh a sigchild! would a Child have died ?\n");
}
foreach (array(SIGALRM, SIGHUP,SIGKILL, SIGCHLD, SIGTERM, SIGCHLD, SIGSTOP) as $signal) {
pcntl_signal($signal, "sig_handler");
}
declare(ticks = 1);
echo "bourrin
";
$code = <<<EOF
<?
print "ici on va mourrir bravement\n";
exit(-1);
print "on passe pas ici\n*************************";
?>
EOF;
system("echo '$code' | php");
print "encore en vie ???****\n";
print(" alive ! ");
$i=3;
while ($i--) {
print "YOU kill me with kill -SIGHUP/SIGINT/SIGTERM\n";
sleep(1);
print "$i sec left\n";
}
print("Do I stay alive ? \n");
sleep(1);
$pid = pcntl_fork();
if($pid == -1) {
print("not expecting to go there in normal situation");
die('could not fork');
}
else if ($pid) {
// positive value means we're in the parent.
// do whatever parents do
print "<$pid>Forking one time
";
$status=false;
pcntl_wait($status);
print("DV::status::$status\n");
print_r($status);
} else {
print ("luke<$pid>: I am young and trying\n");
sleep(1);
return;
// zero value means we're in the child.
// do whatever children do
// (e.g. download the files, then exit)
}
$pid = pcntl_fork();
if($pid == -1) {
print("not expecting to go there in normal situation");
print("not expecting to go there in normal situation");
die('could not fork');
}
else if ($pid) {
// positive value means we're in the parent.
// do whatever parents do
print "<$pid>DarkVader: we wait 10 seconds
DV:I protected myself using SIGCHLD
";
$status=false;
pcntl_wait($status);
print("DV::status::$status\n");
print_r($status);
} else {
print ("luke<$pid>: I am young and gonna crash\n");
sleep(1);
print "\nnow I kill me with exit -1 \n";
exit(-1);
// zero value means we're in the child.
// do whatever children do
// (e.g. download the files, then exit)
}
echo "\n${pid}I SURVIVED THE DARK SIDE OF THE FORCE WON!!!";
eval ("die(1);");
print("staying alive !");(Merci Ben)
Maintenant cherchons une solution générique :
<?
class Catcher{
public static $mongrel="Aucun";
private $tag;
public function __construct($args=array("explain" => false)){
foreach ($args as $k => $v) {
$this->$k=$v;
}
$this->tag=get_class($this) . "::";
$this->tell("Creating Catcher");
}
public $explain=false;
private $safe_sex=false;
private $son=false;
public function son() { return $this->son; }
public function masturbation(){
print($this->tag."playing with myself\n");
}
public $grand_son=0;
public function back_room() {
print($this->tag."playing with others\n");
$from_a_contaminated_partner=-1;
$this->grand_son=1;
self::$mongrel="Weired";
print self::$mongrel . "\n";
exit($from_a_contaminated_partner);
}
private function condom() {
$explain=$this->explain;
$err_is_human;
$referer=$this;
function sig_handler($signo) {
global $referer,$explain;
$err_is_human=error_get_last();
$errno=$err_is_human["type"];
$explain and print("SH::hello you $signo:${errno}\n");
if( $signo==SIGCHLD) {
$explain and print("SH::oh a Child have died ? Always use condoms!\n");
}
print_r($err_is_human);
}
pcntl_signal(SIGCHLD, "sig_handler");
declare(ticks = 1);
$this->tell("We protect ourselves against kids");
$this->safe_sex=true;
}
public function tell($story) {
$this->explain and print $this->tag ."$story\n";
}
public function __call($name, $args) {
$this->safe_sex or $this->condom();
$pid = pcntl_fork();
if($pid == -1) {
$this->tell("not explainecting to go there in normal situation");
throw(new Exception( $this->tag ."cant fork"));
}
if ($pid) {
$this->tell("<$pid>::I am DarkVador::watching my dangerous child");
$this->tell("<$pid>::DV::$name is called with");
$this->tell("<$pid>::DV::" . print_r($args,true));
$status=0;
pcntl_wait($status);
$this->tell("<$pid>::DV::status of my son is <<$status>>");
if($status!=0) {
throw new Exception($this->tag . "FatalErrorCaught::$name" );
}
}
} else {
$sontext="<$pid>::Luke";
$this->tell("$sontext::I am young and intrepid\n");
$this->tell("$sontext::I DO call $name now!");
$matches=array();
preg_match("/safer_(.*)/",$name, $matches)
and call_user_func_array(array($this,$matches[1]), $args)
or call_user_func_array($name, $args);
$this->tell("<$pid>::Luke::my child life is over");
die($this->explain ? ($this->tag . "let me die in peace, may the force be with you\n") : "");
}
}
}Et maintenant testons la solution :
<?
include("catcher.php");
$c = new Catcher(array("explain" => true));
$val=0;
function user_f(&$ret_val,$args) {
global $val;
print "\nHarmless called by luke (normaly)\n";
$val=0;
$val=$ret_val=count($args);
return 42;
}
function user_die(&$ret_val,$args,$die=false) {
global $val;
$val=0;
print "\nHarmfull should be called by luke\n";
$val=$ret_val=count($args);
$die and exit(1);
}
$i=0;
$r=0;
$res=0;
try {
$res=$c->user_f(&$r, array( 1 , 2 , 3));
}
catch(Exception $e) {
print "$e\nIMPOSSIBLE\n";
}
$c->son() and die("arg Que fous le fils ici Mais VRAIMENT ????\n");
print "caught res : $res\n";
$r=0;
print "\n1:<<<<<<<<<<$val??$r>>>>>>>>>>>$i\n";
$i++;
try {
print($c->user_die(&$r, array( 1 , 2 , 3),true));
}
catch (Exception $e) {
print "$e\nA new Victory of the DARK SIDE\n";
}
$c->son() and die("arg WTF!!!!!!");
$c->masturbation();
try {
$c->safer_back_room();
}
catch (Exception $e) {
print "$e\nAnother Victory of the DARK SIDE my son dies, not me\n";
}
print "DONT expect return un less we share memory" . $c->grand_son . "\n";
print "unless you are strong hearted enough to play unsafe " . Catcher::$mongrel ."\n";
print "\n2:<<<<<<<<<<$val??$r>>>>>>>>>>>$i\n";On s’aperçoit que :
- on peut catcher les erreurs systèmes ;
- que cela à 2 coûts.
Premièrement un fork ça coûte cher en mémoire et système, deuxièmement on ne peut pas récupérer de valeurs.
Néanmoins, je crois que cela peut m’être utile ...
La poussière sous le tapis de PHP
Rien ne vous a étonné si vous avez essayé la même chose ?
declare(ticks=1)La page de manuel de PHP est très élusive sur le sujet. On dirait un flag modifiant le comportement internet de l’interpréteur. Il est peut être utiliser pour par exemple utiliser un débogueur.
Sans cette ligne, l’interception de signal ne marche pas. Et bizzarement, on peut se protéger des signaux que l’on reçoit, mais pas de ceux que l’on émet.
un strace -f php premier_script.php vous montrera les forks, c’est très instructif.
Voilà, amusez vous bien, le code et son execution devrait suffire :)
La classe permet d’exécuter toute fonction arbitraire qui est appelée comme une méthode de la classe Catcher. (Il faut quand même qu’elle existe dans le user space).
Et si on veut protéger ses méthodes internes il suffit de les préfixer avec safer_ et la classe appelle alors sa méthode.
Execution du script de test de la classe
Catcher::Creating Catcher
Catcher::We protect ourselves against kids
Catcher::<3726>::I am DarkVador::watching my dangerous child
Catcher::<3726>::DV::user_f is called with
Catcher::<3726>::DV::Array
(
[0] => 0
[1] => Array
(
[0] => 1
[1] => 2
[2] => 3
)
)
Catcher::<0>::Luke::I am young and intrepid
Catcher::<0>::Luke::I DO call user_f now!
Harmless called by luke (normaly)
Catcher::<0>::Luke::my child life is over
Catcher::let me die in peace, may the force be with you
Catcher::<3726>::DV::status of my son is <<0>>
caught res :
1:<<<<<<<<<<0??0>>>>>>>>>>>0
Catcher::<3727>::I am DarkVador::watching my dangerous child
Catcher::<3727>::DV::user_die is called with
Catcher::<3727>::DV::Array
(
[0] => 0
[1] => Array
(
[0] => 1
[1] => 2
[2] => 3
)
[2] => 1
)
Catcher::<0>::Luke::I am young and intrepid
Catcher::<0>::Luke::I DO call user_die now!
Harmfull should be called by luke
Catcher::<3727>::DV::status of my son is <<256>>
exception 'Exception' with message 'Catcher::FatalErrorCaught::user_die' in /home/jul/src/php_exp/fatal/catcher.php:67
Stack trace:
#0 [internal function]: Catcher->__call('user_die', Array)
#1 /home/jul/src/php_exp/fatal/proof.php(35): Catcher->user_die(0, Array, true)
#2 {main}
A new Victory of the DARK SIDE
Catcher::playing with myself
Catcher::<0>::Luke::I am young and intrepid
Catcher::<0>::Luke::I DO call safer_back_room now!
Catcher::playing with others
Weired
Catcher::<3728>::I am DarkVador::watching my dangerous child
Catcher::<3728>::DV::safer_back_room is called with
Catcher::<3728>::DV::Array
(
)
Catcher::<3728>::DV::status of my son is <<65280>>
exception 'Exception' with message 'Catcher::FatalErrorCaught::safer_back_room' in /home/jul/src/php_exp/fatal/catcher.php:67
Stack trace:
#0 [internal function]: Catcher->__call('safer_back_room', Array)
#1 /home/jul/src/php_exp/fatal/proof.php(44): Catcher->safer_back_room()
#2 {main}
Another Victory of the DARK SIDE my son dies, not me
DONT expect return un less we share memory0
unless you are strong hearted enough to play unsafe Aucun
2:<<<<<<<<<<0??0>>>>>>>>>>>1