Utilisation de Cron via PHP - Partie 1
Cron est un outil bien connu sur Linux et macOS qui permet de lancer des tâches périodiquement et automatiquement. Sur Mac, il est conseillé depuis de nombreuses occurrences du système d'utiliser launchd/launchctl à la place.
Cependant, si vous n'avez aucune idée de ce qu'est Cron, cet article n'aura aucun intérêt pour vous.
Le principe pour lire et écrire le fichier présent dans crontab est simple, il suffit via PHP de lancer la commande que vous utiliseriez dans un terminal. Sur Linux, l'écriture de tâches iront dans le crontab du compte utilisateur en charge du serveur web. Par exemple, sur Debian/Ubuntu/Raspbian, ce sera www-data.
Sur Mac, si vous utilisez la version d'Apache incluse dans macOS, ce sera l'utilisateur _www. Par contre, si vous utilisez Apache depuis Homebrew, cela utilisera le crontab de votre compte, puisque vous êtes l'utilisateur lançant Apache2 depuis la gestion des services Homebrew. Attention, donc dans ce cas, cela peut se mélanger avec vos autres tâches personnelles.
Mais revenons au coeur de notre sujet. Voici comment lire le contenu de crontab depuis votre terminal
crontab -l
Depuis PHP, il s'agit donc d'exécuter
exec('crontab -l');
Mais il faut récupérer les données
exec('crontab -l', $jobs);
Ainsi toutes les lignes retournées par la commandes se retrouveront une à une dans le tableau $jobs.
Pour écrire les tâches dans crontab, c'est légèrement plus compliqué, car il est nécessaire d'écrire un fichier temporaire avant, puis d'appliquer le fichier dans Cron.
exec('crontab '.$file);
Où $file est le chemin complet du fichier.
Et voilà, vous êtes capable d'ajouter des tâches depuis votre serveur web dans Cron.
Gestion avancée de Cron via PHP
Avec ce que nous venons de voir, nous pouvons aisément lire le fichier crontab, ajouter une nouvelle ligne avec une nouvelle tâche, et ensuite ré-écrire le fichier. Cela permet de ne passer effacer les tâches existantes, tout en en ajoutant de nouvelle.
Si cela fonctionne, il y a toujours une limite, ou une faille. En effet, si vous souhaitez enlever la ligne de vous venez d'ajouter, cela peut se révéler un peu compliqué.
Pour rendre plus aisé l'utilisation de Cron via PHP, voici comment écrire une librairie qui permette de gérer tout cela bien plus facilement. L'article détaillera la construction de la librairie pour montrer la méthodologie utilisée.
A la base, j'ai écris cette librarie pour CaMykS, le CMS Open Source qui propulse le site, vous pourrez donc trouver la librairie au complet dans son code source, disponible sur Git.
Pour réaliser cet outil, j'ai choisi de créer une librairie sous la forme de deux objets. Le premier objet est l'éditeur en lui même, tandis que le second ne gérera qu'une tâche.
Créons la base de notre fichier, avec nos deux objets
final class CrontabEditor {
}
final class CrontabJob {
}
Maintenant, il réfléchir à nos besoins, commençons par notre éditeur.
Reprenons les bases, il faut d'abord être capable de lire et d'écrire les tâches dans Cron.
final class CrontabEditor {
/**
* Load crontab jobs.
* @return boolean success
*/
public function load_jobs() {
}
/**
* Save crontab jobs.
* @return boolean success
*/
public function save_jobs() {
}
}
Nous verrons le contenu des méthodes un peu plus tard, finissons d'abord de définir vos besoins.
Ensuite, il est nécessaire de stocker les tâches une à une, sous forme d'objets CrontabJob et de les gérer, à savoir ajouter et retirer une tâche. Pour cela vous verrez que nous allons ajouter une notion d'identifiant à nos tâches.
final class CrontabEditor {
/**
* @var array $jobs
* @brief List of current crontab jobs.
*/
var $jobs = array();
/**
* Load crontab jobs.
* @return boolean success
*/
public function load_jobs() {
}
/**
* Save crontab jobs.
* @return boolean success
*/
public function save_jobs() {
}
/**
* Set job, by adding it or replacing existing one.
* @param mixed $details
* @param string $identifier
* @return success
*/
public function set_job($details, $identifier=null) {
}
/**
* Delete job by identifier.
* @param string $identifier
* @return success
*/
public function delete_job($identifier) {
}
}
Nous avons les bases, que nous pouvons désormais remplir nos méthodes. Commençons par la première : load_jobs
Cette fonction doit, dans un premier temps, s'assurer que la liste des tâches est vide.
/* Reset object job list */
$this->jobs = array();
Puis lire le fichier, et éventuellement retourner une erreur si cela n'a pas fonctionné.
/* Read crontab job list */
if (!exec('crontab -l', $jobs))
return false;
Enfin, transformer toutes les lignes trouvées en tâche CrontabJob, et comme vous pouvez le voir, nous laissons le travail de décorticage de la ligne au sous-objet.
/* Create job object from crontab job */
foreach ($jobs as $details) {
$job = new CrontabJob($details);
$this->jobs[$job->identifier] = $job;
}
Pour finalement retourner un succès
/* Return success */
return true;
Voici la méthode au complet
/**
* Load crontab jobs.
* @return boolean success
*/
public function load_jobs() {
/* Reset object job list */
$this->jobs = array();
/* Read crontab job list */
if (!exec('crontab -l', $jobs))
return false;
}
/* Create job object from crontab job */
foreach ($jobs as $details) {
$job = new CrontabJob($details);
$this->jobs[$job->identifier] = $job;
}
/* Return success */
return true;
}
Normalement, il n'y a rien de compliquer à ce niveau là.
Maintenant, écrivons la méthode qui écrit le fichier de cron save_jobs
La méthode doit d'abord initialiser le contenu qui sera envoyé à Cron.
/* Initialise output */
$output = '';
Ensuite, regardons s'il y a des tâches à insérer dans la liste. S'il n'y a pas ou plus, autant simplement réinitialiser le fichier de Cron.
/* No job in list, reset crontab */
if (count($this->jobs) === 0)
return (exec('crontab -r') === '');
Sinon, ajoutons tous les tâches. Encore une fois, on fait confiance au sous-objet, ici pour fournir une ligne de Cron fonctionnelle.
/* Add all job to output */
foreach ($this->jobs as $job)
$output .= $job->output();
Comme je vous l'indiquai avant, il faut faut écrire le contenu dans un fichier temporaire. Pour s'assurer que le nom du fichier n'existe pas, on ajoute un facteur aléatoire.
/* Write temporary file */
$rand = rand();
$file = '/tmp/crontab_'.$rand.'.txt';
if (!file_put_contents($file, $output))
return false;
Enfin, on demande à crontab d'écrire le fichier.
/* Write crontab job list */
if (!exec('crontab '.$file) == '')
return false;
Si aucune des lignes au dessous n'a renvoyé d'erreur, on retourne true comme résultat de la fonction.
/* Return success */
return true;
Voici la méthode au complet
/**
* Save crontab jobs.
* @return boolean success
*/
public function save_jobs() {
/* Initialise output */
$output = '';
/* No job in list, reset crontab */
if (count($this->jobs) === 0)
return (exec('crontab -r') === '');
/* Add all job to output */
foreach ($this->jobs as $job)
$output .= $job->output();
/* Write temporary file */
$rand = rand();
$file = '/tmp/crontab_'.$rand.'.txt';
if (!file_put_contents($file, $output))
return false;
/* Write crontab job list */
if (!exec('crontab '.$file) == '')
return false;
/* Return success */
return true;
}
Jetons maintenant un oeil à la fonction qui permet d'ajouter ou de modifier une tâche. Cette méthode prends en charge 2 paramètres. Le premier est le contenu de la tâche, tandis que le second, optionnel, détermine un identifiant pour la tâche.
En voici le contenu. Nous commençons par vérifier le contenu du détail de la tâche. Si celle ci est déjà un CrontabJob, nous la prenons telle qu'elle a été envoyé, sinon, nous créons un job à partir de ses informations.
/* Check details is a job */
if (is_object($details) and get_class($details) === 'CrontabJob') {
$job = $details;
if (!is_null($identifier))
$job->set_identifier($identifier);
} else
$job = new CrontabJob($details, $identifier);
Nous vérifions que la tâche est valide, peut importe sa source. Le sous-objet est, une nouvelle fois, le mieux placé pour réaliser cette action.
/* Check job */
if (!$job->is_valid())
return false;
Nous l'ajoutons à la liste de nos tâches, en utilisant son identifiant. Si l'identifiant existe déjà, la tâche sera alors remplacée par la nouvelle.
/* Add job to job list */
$this->jobs[$job->identifier] = $job;
Enfin, nous retournons l'information du succès de l'ajout.
/* Return success */
return true;
Et comme d'habitude, voici la méthode dans son ensemble
/**
* Set job, by adding it or replacing existing one.
* @param mixed $details
* @param string $identifier
* @return success
*/
public function set_job($details, $identifier=null) {
/* Check details is a job */
if (is_object($details) and get_class($details) === 'CrontabJob') {
$job = $details;
if (!is_null($identifier))
$job->set_identifier($identifier);
} else
$job = new CrontabJob($details, $identifier);
/* Check job */
if (!$job->is_valid())
return false;
/* Add job to job list */
$this->jobs[$job->identifier] = $job;
/* Return success */
return true;
}
Dernière méthode pour cet objet, nous allons lui permettre de supprimer une tâche. Pour cela, nous la retrouverons par son identifiant. L'identifiant est d'ailleurs le seul et unique paramètre de la fonction.
La fonction ne fait pas grand chose. D'abord, elle vérifie que l'identifier existe vraiment dans la liste.
/* Check identifier */
if (!isset($this->jobs[$identifier]))
return false;
Puis, elle retire la ligne correspondante du tableau des tâches.
/* Remove job */
unset($this->jobs[$identifier]);
Enfin elle indique en retour, que l'action s'est bien faite.
/* Return success */
return true;
Hop, la voici dans son entièreté.
/**
* Delete job by identifier.
* @param string $identifier
* @return success
*/
public function delete_job($identifier) {
/* Check identifier */
if (!isset($this->jobs[$identifier]))
return false;
/* Remove job */
unset($this->jobs[$identifier]);
/* Return success */
return true;
}
Notre librairie principale n'est, au final, pas bien compliquée. Mais elle permettra de gérer facilement l'ajout et la suppression de tâche dans notre Cron. Pour cela, il faut d'abord que l'on gère notre sous-objet CrontabJob. Cela sera pour le prochain article qui ne devrait pas trop tarder à arriver.
Cependant, si vous êtes pressé ou curieux, vous pouvez toujours aller faire un tour dans les sources de la librairie écrite pour CaMykS. La librairie présente dans le CMS est légèrement plus complexe que celle présentée dans l'article, parce qu'elle répond à d'autres besoins, et qu'elle s'intègre dans le CMS. Ne vous étonnez donc pas d'y trouver des différences. Ces mécanismes peuvent cependant être intéressants, car ils permettent par exemple de charger et d'écrire automatiquement le fichier de crontab, de supprimer une tâche sans identifiant, mais aussi protéger la liste des tâches, j'en passe et des meilleures. N'hésitez donc pas à y jeter un oeil, et éventuellement à indiquer dans les commentaires si l'une ou plusieurs des mécaniques devaient être détaillées dans un nouvel article.