La première version de mon script ne pouvait donc pas fonctionner :
<?php $databaseClient->lockTable(); $databaseClient->extractData(); $continue = false; while ($continue !== true) { $continue = (trim(fgets(STDIN) === ‘continue’); } // After this point, the database connection may be dead $databaseClient->updateData(); $databaseClient->unlockTable(); ?>
J’ai donc cherché une solution afin que la connexion entre ma base de données et mon script soit maintenue active tant que le script était en attente d’une réponse de l’utilisateur.
Or, il se trouve que l’entrée standard, matérialisée par la constante STDIN
lorsque PHP est exécuté en ligne de commande, est un flux.
Et il se trouve que PHP dispose nativement d’une fonction permettant à un script d’attendre qu’un ou plusieurs flux soient disponibles pour l’écriture ou la lecture pendant un temps défini, à savoir stream_select()
.
Il m’a donc suffi de définir dans un tableau le seul et unique flux que je désirais lire, soit STDIN
, et de le lui passer comme premier argument.
J’ai également défini deux autres variables que j’ai initialisées à NULL
car les trois premiers arguments de stream_select()
doivent être des références vers des tableaux, sous peine d’obtenir un avertissement de la part de PHP lors de l’exécution du script.
Enfin, j’ai défini via son quatrième argument le temps que stream_select()
doit attendre avant de rendre la main au script, à savoir dans mon cas 5 secondes.
Une fois cela effectué, il n’y a plus qu’à faire envoyer au script une requête anodine
du style « SELECT 1 » à la base de données ou bien de poursuivre la mise à jour des données en base en fonction de la valeur de retour de stream_select().
Cette dernière renvoie en effet 0 lorsqu’aucun des flux qui lui sont passés en argument ne sont modifiés ou bien le nombre de flux modifiés s’ils existent.
La seconde version de mon script était donc la suivante :
<?php $databaseClient->lockTable(); $databaseClient->extractData(); $continue = false; $read = array(STDIN); $write = null; $except = null; while ($continue !== true) { if (stream_select($read, $write, $except, 5) == 0) { $databaseClient->query(‘SELECT 1’); $read = array(STDIN); } else { $continue = (trim(fgets(STDIN) === ‘continue’); } } // After this point, the database connection is always OK $databaseClient->updateData(); $databaseClient->unlockTable(); ?>
Sauf qu’elle ne marchait toujours pas, car par défaut, STDIN
est un flux bloquant.
En clair, tant que l’utilisateur n’a pas saisi le caractère \n
, il ne rend pas la main et bloque indéfiniment l’exécution du script, indépendamment de la valeur du quatrième argument de stream_select()
.
Pour remédier à cela, il faut donc indiquer à PHP que ce flux ne doit pas être bloquant, à l’aide de stream_set_blocking()
, et bien évidemment, il ne faut pas oublier de le rendre à nouveau bloquant une fois que cela n’est plus nécessaire, sous peine d’avoir une surprise lors de l’exécution de la suite du script s’il est à nouveau utilisé.
La version définitive du script est donc la suivante :
<?php $databaseClient->lockTable(); $databaseClient->extractData(); $continue = false; $read = array(STDIN); $write = null; $except = null; stream_set_blocking(STDIN, 0); while ($continue !== true) { if (stream_select($read, $write, $except, 5) == 0) { $databaseClient->query(‘SELECT 1’); $read = array(STDIN); } else { $continue = (trim(fgets(STDIN) === ‘continue’); } } // After this point, the database connection is always OK stream_set_blocking(STDIN, 1); $databaseClient->updateData(); $databaseClient->unlockTable(); ?>
Évidemment, ce mécanisme peut être mis en œuvre pour faire autre chose qu’entretenir une activité sur une connexion de base de données, et avec un peu d’ingéniosité, il peut même être rendu totalement générique :
<?php function waitContinue($message, $timeout, closure $timeoutCallback) { $read = array(STDIN); $write = null; $except = null; $answer = ''; fwrite(STDOUT, trim($message) . ': '); stream_set_blocking(STDIN, 0); while ($answer != 'continue') { if (stream_select($read, $write, $except, $timeout) != 0) { $answer = trim(fgets(STDIN)); } else { $timeoutCallback(); $read = array(STDIN); } } stream_set_blocking(STDIN, 1); } ?>
Cette fonction reçoit ainsi respectivement comme premier, second et troisième argument le message devant être affiché à l’utilisateur, la valeur du délai d’attente et une fermeture lexicale permettant de définir le comportement de la fonction à l’expiration du délai d’attente.
5 réactions
1 De Adirelle - 14/06/2013, 10:07
stream_select demande 3 variables en entrée/sortie parce qu'il en modifie le contenu pour indiquer quels flux sont disponibles pour des opérations non-bloquantes. Ça ne pourrait donc pas fonctionner avec des valeurs constantes. C'est tout à fait cohérent avec la fonction Unix select (http://en.wikipedia.org/wiki/Select...) dont stream_select est très probablement l'adaptation en PHP.
2 De mageekguy - 14/06/2013, 10:44
@Adirelle : Quel scoop !
3 De Greg - 15/06/2013, 22:25
Hello. Merci pour cet article.
Pourquoi faut t'il utiliser les streams non bloquant ? J'ai lu et relus l'article, mais je ne comprends toujours pas. J'ai fait le test en local, en activant et désactivant les stream bloquant, j'ai le même résultat dans les deux cas. Je n'arrive pas a reproduire les cas: "En clair, tant que l’utilisateur n’a pas saisi le caractère \n, il ne rend pas la main et bloque indéfiniment l’exécution du script...."
4 De mageekguy - 21/06/2013, 20:48
@Greg : Si tu ne rends pas STDIN non bloquant, stream_select() retourne 1 immédiatement et le script bloque alors sur le fgets(STDIN) jusqu'à une intervention de l'utilisateur en ligne de commande.
Et dans ce cas, la connexion vers le serveur de base de données n'est pas rafraichît, ce qui est contraire à l'effet recherché.
Si tu rends STDIN non bloquant, stream_select() attend qu'un événement se produise sur STDIN pour permettre au script de se poursuivre jusqu'à l'expiration de son timeout.
Et dans ce cas, le script rafraichît la connexion vers la base de données.
Du point de vue de l'utilisateur, le comportement du script est donc identique dans les deux cas, mais techniquement, ils ont un comportement très différent.
5 De greg - 21/06/2013, 21:29
Merci de la réponse. mais j'avais Fait le test en exécutant des 'echo toto'. Et ceux ci s'affichaient bien dans mon term. J'avais copié ton dernier exemple. Je vais essayer de creuser un peu plus.