Recevoir les mails de periodic sous Mac OS X Server 10.5

Les BSDistes de tout poil sont habitués aux emails envoyés chaque nuit, chaque semaine, et chaque mois à l'issue du lancement des scripts periodic. Sous Mac OS X, le résultat de ces scripts en par défaut renvoyé dans les fichiers /var/log/daily.out /var/log/weekly.out et /var/log/monthly.out. Néanmoins, l'administrateur avisé aura tôt fait de les diriger vers son mail en utilisant un fichier /etc/periodic.conf.local comme celui-ci :

daily_output=root
weekly_output=root
monthly_output=root

La formule fonctionne parfaitement pour FreeBSD ou Mac OS X 10.4, mais pas pour Mac OS X Server 10.5.4. Launchd semble présenter un bug qui l'empêche de gérer la création du mail post-periodic. On lit alors cette erreur dans /var/log/system.log :

Jul 29 03:17:42 myserver com.apple.launchd[1] (com.apple.periodic-daily[...]):
  Stray process with PGID equal to this dead job: PID ... PPID 1 sendmail

C'est fâcheux, mais ne nous laissons pas abattre, car il existe une solution. Contrairement à ce qu'on peut lire à droite et à gauche, il ne faut pas modifier la configuration de Postfix, et laisser les références à Cyrus tranquilles. La solution est plutôt du côté de launchd. Certains ont mis en évidence que le mail sera bien généré si au lieu d'exécuter simplement le periodic, on exécute en plus et juste après une petite pause.
Après quelques tests, j'ai trouvé que c'est une solution assez satisfaisante. Sur le plan fonctionnel elle est parfaite, mais elle n'est pas idéale, car elle impose de modifier des fichiers fournis par Apple. Donc cette correction est susceptible d'être perdue au détour d'une mise à jour du système.

J'ai choisi de modifier les plist de launchd correspondant aux lancements de periodic :

[edit] : En réalité, la modification des plists com.apple.periodic* n'a pas donné le résultat escompté sur le terrain. Sur mon serveur de test, les mails de periodic étaient bien envoyés, mais sur mes serveurs de production, 3 machines sur 4 n'ont pas réussi à envoyer les mails pour le daily. Par ailleurs, le nombre d'erreurs dans les log système a sensiblement augmenté.

J'ai donc décidé de restaurer les fichiers com.apple.periodic* dans leur état d'origine et de modifier à la place la commande periodic. J'ai renommé /usr/sbin/periodic en /usr/sbin/periodic_orig, puis j'ai créé un script shell nommé /usr/sbin/periodic :

#!/bin/bash 
/usr/sbin/periodic_orig $@ 
sleep 1

Ainsi, le lancement par launchd de la commande `periodic daily` va en réalité lancer `/usr/sbin/periodic_orig daily` (donc le script periodic original), puis va lancer `sleep 1`, ce qui suffit à launchd pour pouvoir générer le mail de résultat de periodic.

J'ai comme l'impression qu'avec launchd, on n'a pas fini d'en baver...

Sauvegarder des bases MySQL

Dans de précédents articles j'ai répondu brièvement aux questions de l'installation de MySQL 5 sur Mac OS X 10.5 et de la création d'un plist de démarrage launchd pour MySQL. J'ai aussi donné quelques ficelles pour trouver les points de blocage habituels au fonctionnement de MySQL.
Il me reste donc à aborder le problème des sauvegardes. Cet article s'adresse uniquement aux utilisateurs d'un serveur MySQL qui sont root/admin de leur serveur, si vous avez un serveur MySQL chez un hébergeur, alors vous pouvez passer votre chemin.

Le script que je propose ci-dessous doit être lancé en root, une fois par jour. Il fait une sauvegarde de chaque base de données qu'il trouve, et il conserve cette sauvegarde pendant 7 jours. En cas de pépin, il vous est donc possible de restaurer une ou plusieurs bases de données, en remontant de maximum 7 jours dans le passé.
Vous devez adapter absolument les lignes 2 à 5 pour indiquer les chemins suivants :

  • mysqlroot : chemin du dossier contenant les bases de données sur le serveur
  • monbkp : chemin du dossier contenant les sauvegardes des bases sur le serveur
  • hotcopy : chemin de l'exécutable mysqlhotcopy sur le serveur
  • mydump : chemin de l'exécutable mysqldump sur le serveur

Vous devez aussi remplacer LE_PASS_MYSQL par le mot de passe de root de MySQL. Attention, il n'y a pas d'espace entre -p et LE_PASS_MYSQL pour la commande mysqldump, mais il y a bien un espace entre le -p et LE_PASS_MYSQL pour la commande mysqlhotcopy.

Le script supporte un argument ("dump"). Si il est lancé avec cet argument, alors il utilisera mysqldump pour faire la sauvegarde. Vous obtenez alors un dump de chaque base, c'est à dire un fichier texte "plat" contenant les instructions SQL nécessaires à la reconstruction de la base de données. C'est la méthode que je recommande car le fichier obtenu peut être injecté dans presque n'importe quel serveur MySQL. Par ailleurs, le fichier obtenu est plus petit et plus facile à compresser.
Si le script est lancé sans l'argument "dump", alors la méthode de sauvegarde utilisée est mysqlhotcopy. Ce programme duplique physiquement le répertoire de chaque base de données. L'avantage c'est que la sauvegarde est prête à l'emploi, il n'est pas nécessaire de la ré-injecter dans le serveur. L'inconvénient, c'est qu'il vous sera probablement impossible d'utiliser cette sauvegarde sur un autre serveur que le votre, et dans la même version de MySQL. Si vous souhaitez archiver vos sauvegardes de bases de données et pouvoir les restaurer quelques mois ou années plus tard, il ne faut pas utiliser mysqlhotcopy.

Déroulement du script :

  1. création d'un dossier temporaire /tmp/mysql
  2. pour chaque base de données, création d'un dump ou d'une "hotcopy"
  3. archivage et compression de chaque dump/hotcopy (en .tgz)
  4. suppression de la version non-compressée
  5. déplacement de la version compressée vers le dossier de sauvegarde
  6. suppression du dossier temporaire /tmp/mysql
#!/bin/sh
mysqlroot=/var/db/mysql
monbkp=/backup/MYSQL
hotcopy=/usr/local/bin/mysqlhotcopy
mydump=/usr/local/bin/mysqldump

madate=`date "+%Y-%m-%d"`
monjour=`date "+%w"`

echo "lancement du backup des bases de donnees MySQL..."
echo

mkdir -m 0777 /tmp/mysql

cd $mysqlroot
for directory in *
do
if [ $directory != "" ]; then
if [ -d "$mysqlroot/$directory" ]; then
  echo -n "backup de $directory : "
  case $* in
    dump)
    $mydump -u root -pLE_PASS_MYSQL --opt $directory > "/tmp/mysql/$directory"
    ;;
    *)
    $hotcopy -u root -p LE_PASS_MYSQL -q "$directory" /tmp/mysql
    ;;
  esac
  if [ $? = 0 ]; then
    tar -czf "$monbkp/$madate$directory.tgz" -C /tmp/mysql/ "$directory"
    if [ $? = 0 ]; then
      rm -f "$monbkp/$directory.${monjour}.tgz"
      rm -r "/tmp/mysql/$directory"
      mv "$monbkp/$madate$directory.tgz" "$monbkp/$directory.${monjour}.tgz"
      echo "ok"
    else
      echo "Erreur targz".
    fi
  else
    echo "Erreur export".
  fi
fi
fi
done

rm -fR /tmp/mysql/

 

Pour lancer ce script toutes les nuits, j'utilise une crontab (car mon mysqld est installé sur un serveur FreeBSD). Le script est enregistré dans /usr/local/bin/ sous le nom BKP_SQL.sh. Voilà la ligne en question :

30 5 * * * /usr/local/bin/BKP_SQL.sh

Et si je souhaite faire des dumps plutôt que des "hotcopy" :

30 5 * * * /usr/local/bin/BKP_SQL.sh dump

 
Note : ce script n'est pas du tout une référence de fiabilité et encore moins d'élégance, néanmoins, je l'ai créé en 2003 et depuis il tourne tous les jours. Je n'ai jamais eu de problème avec.

Note 2 : selon le système, votre environnement, ... il vous faudra peut être indiquer le chemin complet pour l'application tar.

CF Extreme IV : lecteur externe et performances

Il y a déjà quelques temps, j'avais testé une carte CF Ultra II 1 Go et une CF Extreme IV 2 Go de SanDisk. Je reviens avec un autre test (rapide) : les mêmes cartes en lecture et écriture dans un lecteur externe FireWire Sandisk.

La méthodologie de ces tests est sommaire et barbare. J'ai choisi d'utiliser la commande dd pour lire et écrire des fichiers de 500 et 800 Mo sur les deux cartes. Le lecteur est connecté en FireWire 2 (800 Mbits/s théoriques, soient 100 Mo/s). Voici un exemple de commande utilisée pour l'écriture :

time dd if=/dev/zero bs=8k count=100000 of=/Volumes/EOS_DIGITAL/fichier

et un exemple de commande utilisée pour la lecture :

time dd bs=64k if=/Volumes/EOS_DIGITAL/fichier | dd of=/dev/null

Résultats d'écriture

  • 12,43 Mo/s pour la carte Ultra II
  • 30,42 Mo/s pour la carte Extreme IV

Résultats de lecture

  • 13,17 Mo/s pour la carte Ultra II
  • 64,93 Mo/s pour la carte Extreme IV

Je n'ai pas torturé les cartes pour en tirer les meilleurs performances, mais l'Extreme IV se dégage sans difficulté devant l'Ultra II avec des performances de lecture dépassant nettement les 40 Mo/s annoncés par le constructeur. Ce résultat est hautement suspect. J'ai donc refait le test de lecture de la carte Extreme IV avec des vrais fichiers RAW.

for image in /Volumes/EOS_DIGITAL/*.CR2; do
   time dd bs=64k if=${image} | dd of=/dev/null
done

La première exécution de cette commande donne environ 33 Mo/s pour les 39 fichiers RAW. La seconde lecture donne par contre une moyenne de 74 Mo/s. Sans doute une farce de l'OS (Mac OS X 10.5.2) et de ses multiples caches d'optimisation/accélération.
Dans le cadre de l'utilisation photographique d'une carte CF, l'ordinateur avec son lecteur externe va servir à télécharger les photos une fois, et effacer la carte. On peut donc oublier les résultats farfelus et ne retenir que le déjà très honorable 33 Mo/s.
Pour utiliser régulièrement cette carte avec de lecteur pour décharger mes photos, je peux témoigner du confort énorme qu'apporte ces performances.

MySQL 5 : le checklist en cas de pépin

Comme les commentaires le montrent, si l'installation de MySQL peut prendre 5 minutes et se dérouler comme un charme, le moindre problème peut vite bloquer le débutant pendant des jours. Et plus le débutant se débat, plus il fait des dégâts sur son système.
Je vous propose donc ici une petite checklist des choses à vérifier si votre installation de MySQL sur Mac OS X tourne mal.

1) Si vous avez installé une ou plusieurs autres versions de MySQL et que vous souhaitez repartir de zéro, le plus simple est de localiser tous les éléments liés à MySQL et de les supprimer. On utilise pour cela la base locate, qu'il convient de mettre à jour au préalable :

sudo /usr/libexec/locate.updatedb 
locate -i mysql

ensuite faites le tri dans ce que vous voyez, et supprimez les éléments appartenant aux anciennes installation de MySQL (sans doute dans /usr/local/, /Library/LaunchDaemon/, /Library/StartupItems/, ...)

2) Vérifiez votre PATH : les binaires de MySQL doivent se trouver dans votre PATH pour être exécutables sans indiquer leur chemin complet. Si la commande which mysql ne renvoie rien, ajoutez /usr/local/mysql/bin/ à votre PATH.

3) Vérifiez que le serveur mysqld est lancé (ps auxwww | grep mysqld) ou se lance bien (sudo /usr/local/mysql/bin/mysqld_safe).

4) Vérifiez que le socket du serveur existe quand il est lancé :

netstat -f unix | grep mysql 
ls -l /tmp/mysql.sock

Si l'une de ces deux commandes ne retourne pas le chemin du socket, alors soit le socket est ouvert par le serveur mais supprimé par un autre process, soit le socket n'est pas ouvert par le serveur car il existe déjà.

5) Vérifiez que vous essayez bien d'accéder au serveur MySQL à partir d'un logiciel qui saura s'y connecter (php doit connaître le chemin du socket, le client mysql aussi). Si vous tentez une connexion réseau, vérifier aussi que mysqld ouvre un port réseau (netstat -alnf inet | grep 3306).

6) Jetez un œil aux fichiers de log de MySQL. Si le serveur ne se lance pas du tout, cela ne vous aidera probablement pas, mais si il se lance et quitte inopinément, ou si il se lance dans un état inutilisable, vous trouverez probablement des informations intéressantes dans ces fichiers. Pour l'installation de MySQL via le package officiel, les fichiers de log se trouvent normalement dans /usr/local/mysql/data/, sous le nom de localhost.err ou de votre-machine.err.

Je compléterai cette liste au fil du temps... N'hésitez pas à faire des suggestions !

MySQL 5 : le plist de démarrage

Dans un précédent article j'ai exposé l'installation triviale d'un MySQL 5 sur une base de Mac OS X 10.5 propre. J'ai aussi mentionné le StartupItem fourni de base avec le package MySQL. Ce StartupItem étant conçu pour Mac OS X 10.4, il est intéressant de voir comment on doit maintenant lancer un serveur MySQL sur Leopard.

Si vous aviez installé le script de démarrage de MySQL 5 mentionné dans le précédent article, il faudra le désactiver ou le supprimer. Pour le supprimer, vous pouvez exécuter les commandes suivantes dans le terminal :

sudo /Library/StartupItems/MySQLCOM/MySQLCOM stop 
sudo rm -r /Library/StartupItems/MySQLCOM 
sudo rm /tmp/mysql.sock

Ensuite, vous pouvez créer le fichier /Library/LaunchDaemons/org.mysql.mysqld.plist avec votre éditeur favori, et coller dedans :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>org.mysql.mysqld</string>
	<key>OnDemand</key>
	<false/>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/mysql/bin/mysqld</string>
		<string>--socket=/tmp/mysql.sock</string>
		<string>--user=mysql</string>
		<string>--port=3306</string>
		<string>--datadir=/usr/local/mysql/data</string>
		<string>--pid-file=/usr/local/mysql/data/localhost.pid</string>
		<string>--bind-address=127.0.0.1</string>
	</array>
	<key>ServiceIPC</key>
	<false/>
</dict>
</plist>

Une fois que ce fichier est sauvé, il faut s'assurer qu'il appartient bien à root, sinon launchctl vous envoie sur les roses (cf. les commentaires de Pierre29). Dans le doute, exécutez la commande

sudo chown root /Library/LaunchDaemons/org.mysql.mysqld.plist

Vous pouvez ensuite activer ce plist par la commande

sudo launchctl load /Library/LaunchDaemons/org.mysql.mysqld.plist

MySQL devrait être alors lancé automatiquement pour vous. Il placera son socket dans /tmp/ et autorisera aussi les connexions TCP sur l'interface 127.0.0.1.

Je vous renvoie à la documentation de launchctl pour le reste des options et commandes disponibles et pour les détails sur l'utilisation de launchd. Votre serveur MySQL est désormais géré proprement par launchd, exactement comme sur un Mac OS X Serveur.

edit : ajout du chown root sur le fichier

MySQL 5 sur Mac OS X 10.5 en 5 minutes

Ça arrive toujours par vague, il suffit de suivre un peu des newsgroups comme fr.comp.mac-os.x pour s'en apercevoir. Un type un peu désespéré se pointe avec plein de questions sur comment installer MySQL sur son nouvel OS, puis c'est s'escalade, la surenchère à qui proposera la solution la plus compliquée ou l'idée la plus saugrenue. Bien sûr au départ c'est toujours la faute du système, ou celle de MySQL. C'est rarement celle de tous ces tutoriels ou de ces conseils mal avisés glanés sur les forums les plus inattendus. Et non, bien sûr ce n'est jamais la faute du pauvre bougre qui va copier-coller dans son terminal, sans les comprendre, les commandes dictées par des inconnus.
Pourtant, installer un MySQL 5 sur Mac OS X est d'une simplicité enfantine. C'est comme à l'école des fans : même les moins doués gagnent à la fin. Je ne vais pas détailler toutes les contortions cérébrales qu'il faut faire pour rater cette installation, on trouve suffisamment d'exemples sur les forums, et ce ne serait pas chic de ma part de moquer la paresse intellectuelle de certains.

La première étape pour une installation réussie, c'est de télécharger le package officiel fourni par MySQL. Ne cédez pas à la tentation de compiler vous même MySQL, si vous avez besoin d'aide pour installer un pkg, faites preuve de bon sens et oubliez immédiatement la compilation. On trouve le précieux paquet dans la zone "Developer" du site mysql.com. Cliquez sur "MySQL Community Server" dans la marge de gauche, et dans la nouvelle page qui se charge, trouvez le lien intitulé "Mac OS X (package format)". Je ne donne pas les liens directs, car ils sont susceptibles de changer au fil des versions de MySQL.
Vous voilà en face d'une liste de packages pour Mac OS X : Mac OS X (package format) downloads. Choisissez celui qui correspond le plus à votre version du système. Actuellement pour un G5 en Mac OS X 10.4 ou 10.5 ce sera "Mac OS X 10.4 (PowerPC, 64-bit)" par exemple.

Une fois l'image disque téléchargée et montée vous voici en face de deux packages, d'un "PrefPane", et d'un readme :

  • mysql-5.0.45-osx10.4-powerpc-64bit.pkg (dans mon cas)
  • MySQLStartupItem.pkg
  • MySQL.prefPane
  • ReadMe.txt

Le premier package installe tout MySQL sur votre machine. Le second installe un StartupItem à l'ancienne (comprendre : pré-launchd). Pour un lanceur à la mode de launchd : moderne et qui marche bien, voyez cet article. Le PrefPane permet d'installer un tableau de bord MySQL dans les préférences système.
Muni d'un log/pass d'administrateur vous pouvez maintenant installer les packages par simple double-click. Jusque là, pas besoin de comprendre la théorie de la relativité. Si vous avez su taper votre mot de passe, vous avez maintenant installé MySQL.
De la même manière, double-cliquez sur le PrefPane pour l'installer. Vous pouvez l'installer pour vous uniquement, ou pour tous les utilisateurs de la machine. C'est sans incidence sur le résultat, faites comme vous préférez. Ce tableau de bord est de toute manière partiellement non-fonctionnel sous Leopard (je ne l'ai pas testé sous Tiger). Il ne permet pas de lancer ou d'arrêter MySQL via le bouton de son interface. Par contre, il permet d'activer ou non le lancement automatique du serveur MySQL au démarrage de la machine.
À partir de là, votre serveur MySQL est complètement fonctionnel. Si vous avez opté pour un lancement automatique au démarrage, vous pouvez maintenant rebooter pour vérifier que cela fonctionne, tout en vous félicitant d'avoir installé MySQL 5 en moins de 5 minutes, sans taper une seule ligne de commande dans votre terminal. Si vous souhaitez lancer le serveur sans redémarrer et que, comme dans mon cas, le bouton ad hoc du tableau de bord de fonctionne pas, vous pouvez le faire via le terminal :

  • cochez la case pour lancer MySQL automatiquement au démarrage (cela édite pour vous un fichier de configuration qui autorise aussi le lancement manuel)
  • tapez dans une fenêtre de terminal la commande
    sudo /Library/StartupItems/MySQLCOM/MySQLCOM start

Normalement, le tableau de bord doit maintenant refléter l'état du serveur, et indiquer que le serveur est fonctionnel. Si ce n'est pas le cas, c'est que vous êtes parvenu à rater une des étapes précédentes.

Désormais, il est judicieux de tester la connexion au serveur. Ouvrez une fenêtre de terminal, et taper la commande suivante :

/usr/local/mysql/bin/mysql -u root

Cela doit vous donner le résultat suivant :

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 200
Server version: 5.0.45 MySQL Community Server (GPL)

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> 

Vous êtes alors connecté à votre serveur mysql local. Il est important de noter que les packages officiels de MySQL sont compilés pour stocker le socket de connexion dans /tmp/mysql.sock, et non pas dans /var/mysql/mysql.sock. Le PHP fourni par Apple quant à lui, attend ce socket dans /var/mysql/mysql.sock. Si vous souhaitez faire fonctionner des scripts PHP, vous devrez indiquer à ce dernier le chemin réel de votre socket MySQL. On fait cela en éditant le fichier /private/etc/php.ini. Ouvrez ce fichier (probablement inexistant) avec votre éditeur de texte favori, et insérez la ligne mysql.default_socket = /tmp/mysql.sock. Maintenant, PHP doit trouver tout seul le socket de MySQL. C'est facile à vérifier dans le terminal :

php -r 'mysql_connect(localhost, root, ""); echo mysql_result(mysql_query("SELECT 2+2;"),0)." ";'

Si tout fonctionne, le résultat affiché sera "4". Si cela ne fonctionne pas, c'est probablement que vous êtes incapable d'utiliser MySQL de toute manière :).

edit : en cas de pépin, faites un tour vers la checklist.

Passer de cron à launchd, épisode 2

On l'a vu dans l'épisode 1 de "Passer de cron à launchd", la syntaxe de launchd est vraiment très verbeuse par rapport à celle des crontabs. De plus, launchd pose rapidement des problèmes quand il s'agit de définir des périodicités un peu excentriques ou sophistiquées.
Par exemple, si dans cron je veux créer une tâche qui s'exécutera toutes les 30 minutes tous les jours, j'écris ceci :

*/30 * * * *      /ma/commande

Alors, crond va lancer /ma/commande à 0h00, 0h30, 1h00, ... 23h30 tous les jours.
Pour faire la même chose avec launchd J'ai a priori deux possibilités : StartInterval et StartCalendarInterval. StartInterval est simple et permet de régler en 2 lignes mon intervalle de temps :

<key>StartInterval</key>
<integer>1800</integer>

Seulement, il existe une limitation de taille. StartInterval ne sait pas qu'il doit commencer à 0 minute pour assurer le rythme "heure + 0 minute, heure + 30 minutes". Si le plist launchd est chargé à 7h42, heure du démarrage de la machine par exemple, et bien la première exécution va se produire immédiatement. La prochaine aura bien lieu 30 minutes après, et ainsi de suite. On aura donc les heures de lancement suitantes : 7h42, 8h12, 8h42, ...

Si vous avez besoin d'un lancement à heure fixe, il faut donc oublier complètement StartInterval et vous rabattre sur StartCalendarInterval. Ce dernier impose les dates et heures de lancement, mais il souffre de deux gros défauts.
Premier défaut, et non des moindres : jusqu'à Mac OS X 10.5 le comportement de StartCalendarInterval était bugué, ce qui le rendait pratiquement inutilisable. D'ailleurs, je n'ai pas de certitude absolue sur son bon fonctionnement dans Leopard. Le second défaut, c'est la verbosité de sa syntaxe. Reprenons l'exemple ci-dessus, et transposons-le à StartCalendarInterval. On obtient le code suivant, juste pour la gestion de la périodicité :

<key>StartCalendarInterval</key>
<array>
	<dict>
	    <key>Minute</key>
	    <integer>0</integer>
	</dict>
	<dict>
        <key>Minute</key>
        <integer>30</integer>
	</dict>
</array>

Maintenant, imaginez le nombre de lignes nécessaires pour imposer un lancement aux minutes 0, 10, 20, 30, 40, et 50 : 27 lignes sans compter la gestion de la commande, de l'utilisateur, du header xml..., là où une seule ligne de crontab suffit.

Passons aux choses sérieuses avec un exemple de la vraie vie. Je veux lancer un script de sauvegarde sur ma machine professionnelle, à 12h30 et à 18h30, du lundi au vendredi. Avec cron, c'est simplissime :

30 12,18 * * 1,2,3,4,5      /mon/script/de/sauvegarde

Maintenant, si on veut reproduire la même chose avec launchd, on obtient un fichier xml très long. Je peux placer dans le même bloc dict au maximum une fois chaque mot clé (ici Minute, Hour, Weekday). Heureusement, les astérisques dans la crontab ne sont pas reportées dans launchd car un mot clé absent dans le plist correspond à une astérisque dans la crontab. Je n'ai qu'une valeur pour le mot clé Minute, j'ai deux valeurs pour le mot clé Hour, et 5 pour Weekday. Donc je vais avoir 2 blocs dict pour chaque jour (un pour chaque heure), et chaque bloc va faire 8 lignes. J'ai 5 jours, et 2 blocs de 8 lignes pour chacun, donc j'obtiens 80 lignes de xml uniquement pour la définition de la périodicité ! Ci-dessous, les 16 premières lignes :

<dict>
	<key>Weekday</key>
	<integer>1</integer>
	<key>Hour</key>
	<integer>12</integer>
	<key>Minute</key>
	<integer>30</integer>
</dict>
<dict>
	<key>Weekday</key>
	<integer>1</integer>
	<key>Hour</key>
	<integer>18</integer>
	<key>Minute</key>
	<integer>30</integer>
</dict>

Merci Apple pour cette simplification.

Passer de cron à launchd

Il existe des tas de bonnes raisons d'utiliser cron plutôt que launchd pour lancer des tâches périodiques sur un Mac OS X. La première étant l'écrasante complexité de launchd face à cron. Néanmoins, Apple fait presque tout pour nous décourager d'utiliser cron, donc il est peut être temps de jeter un œil à launchd. J'ai déjà montré comment ce dernier permet de créer des tunnels ssh à la demande pour la connexion d'applications. Voyons maintenant comment il peut aussi être utilisé, dans une certaine mesure, pour s'acquitter de tâches périodiques.

Vous êtes peut être arrivé sur cette page à cause d'un vilain message dans vos fichiers de log système :

...cron...Could not setup Mach task special port 9: (os/kern) no access

Si vous trouvez ce message dans votre fichier /var/log/system.log, c'est que vous utilisez, peut être à votre insu, des crontabs. Les crontabs sont des fichiers de configuration pour cron. Le système râle pour vous faire comprendre à demi-mot qu'il serait temps d'utiliser launchd, et de laisser cron à vos ancêtres.

Généralement, une crontab se présente sous cette forme (sortie de la commande crontab -l) :

# Collect system information every minute
* * * * *       /Users/patpro/bin/cpu_load.sh >/dev/null
# Status graphs generation
*/15 * * * *    /Users/patpro/bin/cpu_load_graphs.sh >/dev/null

J'ai donc ici un script lancé toutes les minutes, et un script lancé toutes les 15 minutes.

La première complexité de launchd c'est que nous allons avoir un fichier de configuration de plusieurs lignes pour chaque ligne de la configuration de cron. Chaque action gérée par launchd fait l'objet d'une déclaration dans un fichier XML. Pour les actions périodiques de type crontab, il faut que launchd les prennent en charge dès le démarrage de la machine. Il faut aussi que les scripts ou applications soient lancés indépendamment de l'utilisateur. Pour remplir ces conditions, il faut que les fichiers XML soient placés dans /Library/LaunchDaemons/.
Voici le fichier XML correspondant à la première ligne de ma crontab, il se nomme ma.crontab.cpu_load.plist et est enregistré dans /Library/LaunchDaemons/ :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
                        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>ma.crontab.cpu_load</string>
        <key>UserName</key>
        <string>patpro</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Users/patpro/bin/cpu_load.sh</string>
        </array>
        <key>StartInterval</key>
        <integer>60</integer>
</dict>
</plist>

Le champs Label doit être unique, on reprend en général le nom du fichier, ici ma.crontab.cpu_load.plist, en omettant le suffix .plist.
Le Username correspond au login de l'utilisateur sous le quel sera lancée la commande.
ProgramArguments est un tableau qui peut contenir plusieurs arguments. Le premier argument (string) est toujours le nom de la commande à exécuter. Les strings suivants (facultatifs) sont les arguments que l'on souhaite passer à la commande.
Pour finir, StartInterval est le nombre de secondes qui sépare chaque lancement de la commande. Il ne peut pas être inférieur à 60, car launchd ne scrute sa liste de lancement que toutes les 60 secondes. Pour la gestion de la périodicité de date (tous les jours à 3h, ou le dimanche à 12h...) on utilise à la place de StartInterval le mot clé StartCalendarInterval (voir le man de launchd.plist pour la syntaxe).

De la même manière, la seconde ligne de ma crontab se traduirait par un fichier ma.crontab.cpu_load_graphs.plist au contenu suivant :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
                        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>ma.crontab.cpu_load_graphs</string>
        <key>UserName</key>
        <string>patpro</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Users/patpro/bin/cpu_load_graphs.sh</string>
        </array>
        <key>StartInterval</key>
        <integer>900</integer>
</dict>
</plist>

Il est possible d'ajouter une directive à ces fichiers plist si on souhaite logger les erreurs d'exécution :

        <key>StandardErrorPath</key>
        <string>/tmp/sortie-err.log</string>

Une fois que l'on a créé tous les fichiers adéquats dans /Library/LaunchDaemons/, on peut activer les plists launchd (sans oublier de neutraliser les crontabs) :

sudo launchctl load /Library/LaunchDaemons/ma.crontab.cpu_load*

Il peut arriver que certaines crontabs soient trop complexes pour launchd. Par exemple, je pense que launchd ne sait pas gérer une périodicité de lancement d'une fois toutes les 15 minutes du lundi au vendredi. J'avoue néanmoins que je n'ai pas vérifié ;)
edit : launchd peut gérer cela, mais ce n'est pas élégant, voir l'épisode 2.