Différences entre versions de « Php daemon »
(13 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 9 : | Ligne 9 : | ||
= Mise en place = | = Mise en place = | ||
+ | == Le projet sous Eclipse == | ||
+ | Nous allons créer un répertoire ''src'' pour contenir les scripts. Sous ''src'', nous allons créer un répertoire ''class'' qui contiendra toutes les classes que nous allons créer. | ||
+ | |||
==Réception des signaux système== | ==Réception des signaux système== | ||
− | Nous allons créer une classe | + | Nous allons créer une classe ''Daemon'' qui va répondre aux [https://fr.wikipedia.org/wiki/Interruption_(informatique) interruptions] envoyées par le système. |
Celles qui vont nous intéresser sont : | Celles qui vont nous intéresser sont : | ||
* SIGINT : signal d’interruption déclenché par ''ctrl+c''; | * SIGINT : signal d’interruption déclenché par ''ctrl+c''; | ||
Ligne 17 : | Ligne 20 : | ||
* SIGHUP : signal utilisé pour redémarrer un processus; | * SIGHUP : signal utilisé pour redémarrer un processus; | ||
− | + | Pour que le code soit réutilisable, la classe ''Daemon'' sera [https://fr.wikipedia.org/wiki/Classe_abstraite abstraite] : | |
<source lang="php" style="font-size:110%"> | <source lang="php" style="font-size:110%"> | ||
<?php | <?php | ||
Ligne 86 : | Ligne 89 : | ||
$this->onStart (); | $this->onStart (); | ||
while ( $this->isRunning ) { | while ( $this->isRunning ) { | ||
+ | // Appel du dispatcher pour traiter les signaux en attente | ||
+ | pcntl_signal_dispatch(); | ||
$this->run (); | $this->run (); | ||
} | } | ||
$this->onStop (); | $this->onStop (); | ||
+ | } | ||
+ | /** | ||
+ | * True if the daemon is running | ||
+ | */ | ||
+ | public function isRunning(){ | ||
+ | return $this->isRunning; | ||
} | } | ||
/** | /** | ||
Ligne 194 : | Ligne 205 : | ||
// Processus fils (démon) | // Processus fils (démon) | ||
// On fait du processus fils un chef de session | // On fait du processus fils un chef de session | ||
− | if (posix_setsid () == - 1) { | + | if ((posix_setsid ()) == - 1) { |
// Détachement échoué | // Détachement échoué | ||
exit ( 1 ); | exit ( 1 ); | ||
Ligne 201 : | Ligne 212 : | ||
cli_set_process_title($this->name); | cli_set_process_title($this->name); | ||
// On écrit le PID du processus dans un fichier | // On écrit le PID du processus dans un fichier | ||
− | file_put_contents("/var/run/".$this->name.".pid", | + | file_put_contents("/var/run/".$this->name.".pid", getmypid()); |
$this->onStart (); | $this->onStart (); | ||
while ( $this->isRunning ) { | while ( $this->isRunning ) { | ||
Ligne 213 : | Ligne 224 : | ||
Si la fonction [http://php.net/manual/fr/function.pcntl-fork.php ''pcntl_fork''] renvoie ''-1'', les forks ne sont pas supportés (eg. sur ''Windows''), sinon, dans le processus père la variable ''$pid'' correspond au numéro de PID du processus fils et dans le fils elle aura la valeur ''0''. | Si la fonction [http://php.net/manual/fr/function.pcntl-fork.php ''pcntl_fork''] renvoie ''-1'', les forks ne sont pas supportés (eg. sur ''Windows''), sinon, dans le processus père la variable ''$pid'' correspond au numéro de PID du processus fils et dans le fils elle aura la valeur ''0''. | ||
− | On remarque l'utilisation de la fonction [http://php.net/manual/fr/function.posix-setsid.php ''posix_setsid()''] qui va permettre au processus fils de devenir ''chef de session'' (fils du processus ''init''). | + | On remarque l'utilisation de la fonction [http://php.net/manual/fr/function.posix-setsid.php ''posix_setsid()''] qui va permettre au processus fils de devenir ''chef de session'' (fils du processus ''init'' ou ''systemD''). |
Pour finir en beauté, on utilise la fonction [http://php.net/manual/fr/function.cli-set-process-title.php ''cli_set_process_title()''] qui permet de modifier le nom du processus tel qu'il apparaît dans les commande ''top'' ou ''ps''. | Pour finir en beauté, on utilise la fonction [http://php.net/manual/fr/function.cli-set-process-title.php ''cli_set_process_title()''] qui permet de modifier le nom du processus tel qu'il apparaît dans les commande ''top'' ou ''ps''. | ||
Ligne 337 : | Ligne 348 : | ||
</pre> | </pre> | ||
= Enregistrement dans le chargeur de démarrage = | = Enregistrement dans le chargeur de démarrage = | ||
− | En suivant ces [[Start_stop_daemon#Transformer_un_programme_en_sous-syst.C3.A8me|indications]] on arrive à faire le script suivant : | + | En suivant ces [[Start_stop_daemon#Transformer_un_programme_en_sous-syst.C3.A8me|indications]] on arrive à faire le script ''/etc/init.d/server'' suivant : |
<source lang="bash" > | <source lang="bash" > | ||
#!/bin/sh | #!/bin/sh | ||
Ligne 371 : | Ligne 382 : | ||
kill $(cat ${PIDFILE}) &> /dev/null | kill $(cat ${PIDFILE}) &> /dev/null | ||
RETVAL=$? | RETVAL=$? | ||
− | [ "$RETVAL" = 0 ] && rm -f /var/lock/subsys/$prog | + | [ "$RETVAL" = 0 ] && rm -f /var/lock/subsys/$prog ${PIDFILE} |
showResult | showResult | ||
} | } | ||
Ligne 421 : | Ligne 432 : | ||
exit $RETVAL | exit $RETVAL | ||
</source> | </source> | ||
− | Pour ensuite pouvoir | + | Pour ensuite pouvoir faire : |
<pre> | <pre> | ||
# service server start | # service server start | ||
Ligne 431 : | Ligne 442 : | ||
Starting server: [ OK ] | Starting server: [ OK ] | ||
</pre> | </pre> | ||
− | + | ainsi que : | |
<pre> | <pre> | ||
# chkconfig server on | # chkconfig server on |
Version actuelle datée du 1 janvier 2019 à 01:44
Introduction
Définition
Un daemon [...], parfois traduit par démon, désigne un type de programme informatique, un processus ou un ensemble de processus qui s'exécute en arrière-plan plutôt que sous le contrôle direct d'un utilisateur. Wikipedia
Principe
Pour qu'un processus soit considéré comme un démon, il faut :
- qu'il puisse répondre au signaux du système ;
- qu'il continue son exécution avec ou sans contrôle utilisateur.
Mise en place
Le projet sous Eclipse
Nous allons créer un répertoire src pour contenir les scripts. Sous src, nous allons créer un répertoire class qui contiendra toutes les classes que nous allons créer.
Réception des signaux système
Nous allons créer une classe Daemon qui va répondre aux interruptions envoyées par le système. Celles qui vont nous intéresser sont :
- SIGINT : signal d’interruption déclenché par ctrl+c;
- SIGTERM : signal de terminaison déclenché par la commande kill;
- SIGCHLD : signal utilisé pour gérer les processus fils (fork)
- SIGHUP : signal utilisé pour redémarrer un processus;
Pour que le code soit réutilisable, la classe Daemon sera abstraite :
<?php
abstract class Daemon {
protected $name;
private $isRunning = true;
private $signals = array (
SIGTERM,
SIGINT,
SIGCHLD,
SIGHUP
);
/**
* Class used to handle POSIX signals and fork from the current process
*
* @param string $name
* <p>The name of the class</p>
* @param array $signals
* <p>An array containing additional POSIX signals to handle [optionel] </p>
*/
protected function __construct($name, array $signals = array()) {
$this->name = $name;
if (! empty ( $signals )) {
$this->signals = array_merge ( $this->signals, $signals );
}
// Permet au script PHP de s'éxécuter indéfiniment
set_time_limit ( 0 );
$this->registerSignals ();
}
/**
* Used to register POSIX signals
*/
private function registerSignals() {
declare ( ticks = 1 )
;
foreach ( $this->signals as $signal ) {
@pcntl_signal ( $signal, array (
'Daemon',
'handleSignal'
) );
}
}
/**
* Used to handle properly SIGINT, SIGTERM, SIGCHLD and SIGHUP
*
* @param string $signal
*/
protected function handleSignal($signal) {
if ($signal == SIGTERM || $signal == SIGINT) {
// Gestion de l'extinction
$this->isRunning = false;
} else if ($signal == SIGHUP) {
// Gestion du redémarrage
$this->onStop ();
$this->onStart ();
} else if ($signal == SIGCHLD) {
// Gestion des processus fils
pcntl_waitpid ( - 1, $status, WNOHANG );
} else {
// Gestion des autres signaux
$this->handleOtherSignals ( $signal );
}
}
/**
* Launch the infinite loop executing the ''run'' abstract method
*/
protected function start() {
$this->onStart ();
while ( $this->isRunning ) {
// Appel du dispatcher pour traiter les signaux en attente
pcntl_signal_dispatch();
$this->run ();
}
$this->onStop ();
}
/**
* True if the daemon is running
*/
public function isRunning(){
return $this->isRunning;
}
/**
* Override to implement the code run infinetly
*/
protected abstract function run();
/**
* Override to execute code before the ''run'' method on daemon start
*/
protected abstract function onStart();
/**
* Override to execute code after the ''run'' method on daemon shutdown
*/
protected abstract function onStop();
/**
* Override to handle additional POSIX signals
*
* @param int $signal
* <p>Signal sent by interrupt</p>
*/
protected abstract function handleOtherSignals($signal);
}
?>
La classe Daemon ne peut pas être instanciée car elle est abstraite ce qui permet de définir les méthodes abstraites onStart, onStop, run et handleOtherSignals. Le code de ces méthodes est spécifique à chaque démon et c'est pour cela que l'utilisateur qui va étendre la classe Daemon devra les surcharger.
Cela nous permet dans la classe Daemon de seulement coder le comportement du démon et non ce qu'il fait.
Utilisation
Voici une classe Server qui implémente la classe Daemon :
<?php
class Server extends Daemon {
public function __construct() {
// Ici on souhaite gérer les signaux SIGUSR1 et SIGUSR2 en plus
parent::__construct ( "server", array (
SIGUSR1,
SIGUSR2
) );
// Démarrage du démon
parent::start ();
}
public function run() {
// Le code qui s'exécute infiniment
echo "On tourne !\n";
sleep ( 5 );
}
public function onStart() {
echo "Démarrage du processus avec le pid " . getmypid () . "\n";
}
public function onStop() {
echo "Arrêt du processus avec le pid " . getmypid () . "\n";
}
public function handleOtherSignals($signal) {
echo "Signal non géré par la classe Daemon : " . $signal . "\n";
}
}
?>
Et enfin, un fichier qui nous permet d'instancier et de démarrer la classe Server :
<?php
include_once 'class/Daemon.class.php';
include_once 'class/Server.class.php';
new Server();
Lorsque vous démarrer le script dans Eclipse ou dans un terminal vous devriez voir cela :
Démarrage du processus avec le pid 82108 On tourne ! On tourne ! On tourne !
Et lorsque on exécute la commande suivante dans un autre terminal :
# kill 82108
Le message suivant apparaît :
Arrêt du processus avec le pid 82108
Détachement du processus courant
Lorsque l'on exécute le script start.php, celui-ci ne rend pas la mains, ce qui pose problème si on veut un vrai démon. Nous allons donc utiliser la fonction pcntl_fork.
La modification se fait dans la fonction start() :
protected function start() {
$pid = pcntl_fork ();
if ($pid == - 1) {
// Erreur
return false;
} else if ($pid) {
// Processus courant (père)
} else {
// Processus fils (démon)
// On fait du processus fils un chef de session
if ((posix_setsid ()) == - 1) {
// Détachement échoué
exit ( 1 );
}
// On change le nom du processus
cli_set_process_title($this->name);
// On écrit le PID du processus dans un fichier
file_put_contents("/var/run/".$this->name.".pid", getmypid());
$this->onStart ();
while ( $this->isRunning ) {
$this->run ();
}
$this->onStop ();
}
}
Si la fonction pcntl_fork renvoie -1, les forks ne sont pas supportés (eg. sur Windows), sinon, dans le processus père la variable $pid correspond au numéro de PID du processus fils et dans le fils elle aura la valeur 0.
On remarque l'utilisation de la fonction posix_setsid() qui va permettre au processus fils de devenir chef de session (fils du processus init ou systemD).
Pour finir en beauté, on utilise la fonction cli_set_process_title() qui permet de modifier le nom du processus tel qu'il apparaît dans les commande top ou ps.
# ps -ef | grep server | grep -v grep root 3587 1 0 12:04 ? 00:00:00 server
Journalisation de l'activité
On vient de faire un démon qui va donc tourner de manière autonome mais le seul problème qui persiste est : comment savoir ce qu'il se passe ??
Le démon se détache de la console, on peut fermer le terminal et de facto le processus devient headless...
La solution est de faire écrire le démon dans un fichier texte pour pouvoir suivre son activité indépendamment du terminal dans lequel on se trouve.
Les niveaux de journalisation
On attache très souvent à l'événement journalisé ce qu'on appel un niveau qui va permettre de déterminer sa criticité. De manière générale, les niveaux suivants sont les plus utilisés :
- DEBUG : informations détaillées sur le fonctionnement de l'application ;
- INFO : informations globales sur le fonctionnement de l'application ;
- WARN : situation potentiellement dangereuse ;
- ERROR : erreurs qui peuvent permettre à l'application de continuer de fonctionner;
- FATAL : erreurs irrécupérables, l'application s'arrête ;
- OFF : journalisation désactivé.
Les niveaux sont inclusifs de haut en bas, c'est à dire que le niveau DEBUG inclut tous les autres niveaux et que le niveau FATAL exclut tous les autres niveaux.
Classe Logger
L'une des problématique majeure de la journalisation est l'accès coururent. En effet, il se pourrait, que plusieurs processus veuillent journaliser en même temps dans le même fichier. Pour éviter cela, rien de plus simple, il suffit d'utiliser le patron de conception singleton qui va nous permettre de garantir qu'une seule instance de la classe Logger sera créée.
<?php
class Logger {
const LOG_LEVEL_OFF = 5;
const LOG_LEVEL_FATAL = 4;
const LOG_LEVEL_ERROR = 3;
const LOG_LEVEL_WARNING = 2;
const LOG_LEVEL_INFO = 1;
const LOG_LEVEL_DEBUG = 0;
// Fichier de journalisation par défaut
private $logFile = "php.log";
// Niveau de journalisation par défaut
private $logLevel = self::LOG_LEVEL_DEBUG;
// Instance de Logger
private static $instance;
/**
* Used to retrieve the Logger instance
*
* @return Logger
*/
private static function getInstance() {
if (self::$instance == NULL) {
self::$instance = new Logger ();
}
return self::$instance;
}
/**
* Used to change the log file
*
* @param string $filename
*/
public static function setLogFile($filename) {
self::getInstance ()->logFile = $filename;
}
/**
* Used to change the log level
*
* @param int $logLevel
*/
public static function setLogLevel($logLevel) {
self::getInstance ()->logLevel = $logLevel;
}
/**
* Log an event according to its level
*
* @param string $message
* The event to log
* @param int $level
* The event level, DEBUG by default
* @return boolean TRUE if event is logged, FALSE otherwise
*/
public static function log($message, $level = self::LOG_LEVEL_DEBUG) {
// On vérifie si l'événement doit être journalisé
if ($level < self::getInstance ()->logLevel || $level === self::LOG_LEVEL_OFF) {
return false;
}
// On respect le format de journalisation ''syslog''
$message = date ( "M j H:i:s" ) . " : " . trim ( $message ) . "\n";
file_put_contents ( self::getInstance ()->logFile, $message, FILE_APPEND );
return true;
}
}
Il faut maintenant modifier le code de la classe Server pour utiliser le Logger.
Tout d'abord on spécifie le fichier utilisé par notre démon dans le constructeur de la classe Server :
// Création du Logger
Logger::setLogFile("/var/log/server.log");
Ensuite il faut modifier chaque echo :
echo "On tourne !\n";
Par :
Logger::log("On tourne !");
Notez bien le faite que l'on enlève le \n à la fin de la chaîne de caractères (il est ajouté par la fonction Logger::log).
On peut maintenant relancer notre démon et faire un tail sur le fichier /var/log/server.log pour voir l'activité du processus :
# tail -f /var/log/server.log Mar 17 18:57:08 : Démarrage du processus avec le pid 8541 Mar 17 18:57:08 : On tourne ! Mar 17 18:57:13 : On tourne ! Mar 17 18:57:18 : On tourne ! Mar 17 18:57:23 : On tourne !
Enregistrement dans le chargeur de démarrage
En suivant ces indications on arrive à faire le script /etc/init.d/server suivant :
#!/bin/sh
#
# /etc/init.d/monsystem
# Fichier sous-système pour le serveur "server"
#
# chkconfig: 2345 95 05
# description: démon server
#
# processname: server
# config: /etc/monsystem/monsystem.conf
# pidfile: /var/run/server.pid
# fichier source des fonctions
. /etc/rc.d/init.d/functions
RETVAL=0
prog="server"
DIR="/root/workspace/Daemon/src"
PIDFILE="/var/run/server.pid"
start() {
echo -n $"Starting $prog:"
/usr/bin/php -f ${DIR}'/start.php'
RETVAL=$?
[ "$RETVAL" = 0 ] && touch /var/lock/subsys/$prog
showResult
}
stop() {
echo -n $"Stopping $prog:"
kill $(cat ${PIDFILE}) &> /dev/null
RETVAL=$?
[ "$RETVAL" = 0 ] && rm -f /var/lock/subsys/$prog ${PIDFILE}
showResult
}
reload() {
echo -n $"Reloading $prog:"
kill -1 $(cat ${PIDFILE}) &> /dev/null
RETVAL=$?
showResult
}
showResult() {
# On affiche le retour du script : [ OK ] ou [ ÉCHOUÉ ]
[ $RETVAL -eq 0 ] && success || failure
echo
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
reload)
reload
;;
condrestart)
if [ -f /var/lock/subsys/$prog ] ; then
stop
# On évite les accès concurent en patientant
# avant de démarrer à nouveau le sous-système
sleep 3
start
fi
;;
status)
status $prog
RETVAL=$?
;;
*)
echo $"Usage: $0 {start|stop|restart|reload|condrestart|status}"
RETVAL=1
esac
exit $RETVAL
Pour ensuite pouvoir faire :
# service server start Starting server: [ OK ] # service server stop Stopping server: [ OK ] # service server restart Stopping server: [ÉCHOUÉ] Starting server: [ OK ]
ainsi que :
# chkconfig server on # chkconfig --list server server 0:arrêt 1:arrêt 2:marche 3:marche 4:marche 5:marche 6:arrêt