Exec shell asynchrone en PHP

199

J'ai un script PHP qui doit invoquer un script shell mais ne se soucie pas du tout de la sortie. Le script shell effectue un certain nombre d'appels SOAP et est lent à terminer, donc je ne veux pas ralentir la demande PHP pendant qu'il attend une réponse. En fait, la requête PHP devrait pouvoir se terminer sans terminer le processus shell.

Je l' ai regardé dans les différents exec(), shell_exec(), pcntl_fork(), etc. fonctions, mais aucun d'entre eux semblent offrir exactement ce que je veux. (Ou, s'ils le font, je ne sais pas trop comment.) Des suggestions?

AdamTheHutt
la source
Peu importe la solution que vous choisissez, vous devriez également envisager d'utiliser niceet ioniced'empêcher le script shell de submerger votre système (par exemple /usr/bin/ionice -c3 /usr/bin/nice -n19)
rinogo
Duplication possible de php exécuter un processus d'arrière-plan
Sean the Bean

Réponses:

218

S'il "ne se soucie pas de la sortie", ne pourrait-on pas appeler l'exec du script avec l' &arrière-plan du processus?

EDIT - incorporant ce que @ AdamTheHut a commenté à ce post, vous pouvez l'ajouter à un appel à exec:

" > /dev/null 2>/dev/null &"

Cela redirigera stdio(en premier >) et stderr( 2>) vers /dev/nullet s'exécutera en arrière-plan.

Il existe d'autres façons de faire la même chose, mais c'est la plus simple à lire.


Une alternative à la double redirection ci-dessus:

" &> /dev/null &"
garenne
la source
11
Cela semble fonctionner, mais il faut un peu plus qu'une esperluette. Je l'ai fait fonctionner en ajoutant "> / dev / null 2> / dev / null &" à l'appel exec (). Même si je dois admettre que je ne sais pas exactement ce que cela fait.
AdamTheHutt
2
Certainement la voie à suivre si vous voulez tirer et oublier avec php et apache. De nombreux environnements de production Apache et PHP auront désactivé pcntl_fork ().
méthode
9
Juste une note pour &> /dev/null &, xdebug ne générera pas de journaux si vous l'utilisez. Vérifiez stackoverflow.com/questions/4883171/…
kapeels
5
@MichaelJMulligan, il ferme les descripteurs de fichiers. Cela dit, malgré les gains d'efficacité, avec le recul, l'utilisation /dev/nullest la meilleure pratique, car l'écriture sur des FD fermés provoque des erreurs, tandis que les tentatives de lecture ou d'écriture pour /dev/nullne rien faire en silence.
Charles Duffy
3
Les scripts d'arrière-plan seront tués lors du redémarrage d'Apache ... sachez-le pour les très longs travaux ou si vous êtes malchanceux en termes de timing et que vous vous demandez pourquoi votre travail a disparu ...
Julien
53

Je à pour cela, car il commence vraiment à un processus indépendant.

<?php
    `echo "the command"|at now`;
?>
Czimi
la source
4
dans certaines situations, c'est absolument la meilleure solution. c'était le seul qui a fonctionné pour moi pour libérer un "sudo reboot" ("echo 'sleep 3; sudo reboot' | at now") à partir d'un webgui ET terminer le rendu de la page .. sur openbsd
Kaii
1
si l'utilisateur que vous exécutez apache (généralement www-data) n'a pas les autorisations à utiliser atet que vous ne pouvez pas le configurer, vous pouvez essayer d'utiliser <?php exec('sudo sh -c "echo \"command\" | at now" ');Si commandcontient des guillemets, voir escapeshellarg pour vous éviter des maux de tête
Julien
1
Bien y penser, faire exécuter sudo à sh n'est pas la meilleure idée, car cela donne à sudo un accès root à tout. Je revins à utiliser echo "sudo command" | at nowet commenter www-dataen/etc/at.deny
Julien
@Julien vous pourriez peut-être créer un script shell avec la commande sudo et le lancer à la place. tant que vous ne transmettez aucune valeur soumise par l'utilisateur à ce script
Garet Claborn
26

Pour tous les utilisateurs de Windows: j'ai trouvé un bon moyen d'exécuter un script PHP asynchrone (en fait, cela fonctionne avec presque tout).

Il est basé sur les commandes popen () et pclose (). Et fonctionne bien à la fois sur Windows et Unix.

function execInBackground($cmd) {
    if (substr(php_uname(), 0, 7) == "Windows"){
        pclose(popen("start /B ". $cmd, "r")); 
    }
    else {
        exec($cmd . " > /dev/null &");  
    }
} 

Code d'origine de: http://php.net/manual/en/function.exec.php#86329

LucaM
la source
cela n'exécute qu'un fichier, ne fonctionne pas si vous utilisez php / python / node etc
david valentino
@davidvalentino Correct, et c'est très bien! Si vous souhaitez exécuter un script PHP / Pyhton / NodeJS, vous devez en fait appeler l'exécutable et lui passer votre script. Par exemple: vous ne mettez pas dans votre terminal myscript.jsmais à la place, vous écrirez node myscript.js. C'est-à-dire: node est l'exécutable, myscript.js est, enfin, le script à exécuter. Il y a une énorme différence entre l'exécutable et le script.
LucaM
ce n'est pas un problème dans ce cas, dans d'autres cas, comme le besoin d'exécuter laravel artisan, php artisanmettez simplement un commentaire ici, donc pas besoin de savoir pourquoi cela ne fonctionnera pas avec les commandes
david valentino
22

Sous Linux, vous pouvez effectuer les opérations suivantes:

$cmd = 'nohup nice -n 10 php -f php/file.php > log/file.log & printf "%u" $!';
$pid = shell_exec($cmd);

Cela exécutera la commande à l'invite de commande, puis renverra simplement le PID, que vous pouvez vérifier pour> 0 pour vous assurer qu'il fonctionne.

Cette question est similaire: PHP a-t-il du filetage?

Darryl Hein
la source
Cette réponse serait plus facile à lire si vous incluiez uniquement l'essentiel (en éliminant le action=generate var1_id=23 var2_id=35 gen_id=535segment). De plus, puisque OP a demandé l'exécution d'un script shell, vous n'avez pas besoin des parties spécifiques à PHP. Le code final serait:$cmd = 'nohup nice -n 10 /path/to/script.sh > /path/to/log/file.log & printf "%u" $!';
rinogo
En outre, comme une note de quelqu'un qui a été "là avant", toute personne lisant ceci pourrait envisager d'utiliser non seulement nicemais aussi ionice.
rinogo
1
Que signifie "% u" $! faire exactement?
Twigs
@Twigs &exécute le code précédent en arrière-plan, puis printfest utilisé pour la sortie formatée de la $!variable qui contient le PID
NoChecksum
1
Merci beaucoup, j'ai essayé toutes sortes de solutions que je trouvais pour sortir dans un journal avec un appel shell asynchrone PHP et la vôtre est la seule qui remplissait tous les critères.
Josh Powlison
6

Sous Linux, vous pouvez démarrer un processus dans un nouveau thread indépendant en ajoutant une esperluette à la fin de la commande

mycommand -someparam somevalue &

Sous Windows, vous pouvez utiliser la commande DOS "start"

start mycommand -someparam somevalue
Leo
la source
2
Sous Linux, le parent peut toujours bloquer jusqu'à ce que l'enfant ait fini de s'exécuter s'il essaie de lire à partir d'un descripteur de fichier ouvert détenu par le sous-processus (c'est-à-dire stdout), donc ce n'est pas une solution complète.
Charles Duffy
1
startCommande testée sur Windows, elle ne s'exécute pas de manière asynchrone ... Pourriez-vous inclure la source d'où vous avez obtenu ces informations?
Alph.Dev
@ Alph.Dev, jetez un œil à ma réponse si vous utilisez Windows: stackoverflow.com/a/40243588/1412157
LucaM
@mynameis Votre réponse montre exactement pourquoi la commande de démarrage ne fonctionnait PAS. C'est à cause du /Bparamètre. Je l'ai expliqué ici: stackoverflow.com/a/34612967/1709903
Alph.Dev
6

la bonne façon (!) de le faire est de

  1. fourchette()
  2. setsid ()
  3. execve ()

fork forks, setsid indique au processus en cours de devenir maître (pas de parent), execve indique au processus appelant d'être remplacé par celui appelé. afin que le parent puisse arrêter sans affecter l'enfant.

 $pid=pcntl_fork();
 if($pid==0)
 {
   posix_setsid();
   pcntl_exec($cmd,$args,$_ENV);
   // child becomes the standalone detached process
 }

 // parent's stuff
 exit();

la source
6
Le problème avec pcntl_fork () est que vous n'êtes pas censé l'utiliser lors de l'exécution sous un serveur Web, comme le fait l'OP (en plus, l'OP a déjà essayé cela).
Guss
6

J'ai utilisé ça ...

/** 
 * Asynchronously execute/include a PHP file. Does not record the output of the file anywhere.  
 * Relies on the PHP_PATH config constant.
 *
 * @param string $filename  file to execute
 * @param string $options   (optional) arguments to pass to file via the command line
 */ 
function asyncInclude($filename, $options = '') {
    exec(PHP_PATH . " -f {$filename} {$options} >> /dev/null &");
}

(où PHP_PATHest un const défini comme define('PHP_PATH', '/opt/bin/php5')similaire ou similaire)

Il passe des arguments via la ligne de commande. Pour les lire en PHP, voir argv .

philfreo
la source
4

La seule façon dont j'ai trouvé que cela fonctionnait vraiment pour moi était:

shell_exec('./myscript.php | at now & disown')
Gordon Forsythe
la source
3
'disown' est un Bash intégré et ne fonctionne pas avec shell_exec () de cette façon. J'ai essayé shell_exec("/usr/local/sbin/command.sh 2>&1 >/dev/null | at now & disown")et tout ce que je reçois est:sh: 1: disown: not found
Thomas Daugaard
4

J'ai également trouvé Symfony Process Component utile pour cela.

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
// ... run process in background
$process->start();

// ... do other things

// ... if you need to wait
$process->wait();

// ... do things after the process has finished

Découvrez comment cela fonctionne dans son référentiel GitHub .

Anton Pelykh
la source
2
Attention: si vous n'attendez pas, le processus sera
interrompu à la
Outil parfait, basé sur proc_*des fonctions internes.
MAChitgarha
2

Vous pouvez également exécuter le script PHP en tant que démon ou cronjob :#!/usr/bin/php -q

Ronald Conco
la source
1

Utilisez un fifo nommé.

#!/bin/sh
mkfifo trigger
while true; do
    read < trigger
    long_running_task
done

Ensuite, chaque fois que vous souhaitez démarrer la tâche longue, écrivez simplement une nouvelle ligne (non bloquante dans le fichier déclencheur.

Tant que votre entrée est inférieure à PIPE_BUFet qu'il s'agit d'une seule write()opération, vous pouvez écrire des arguments dans le fifo et les faire apparaître comme $REPLYdans le script.

géocar
la source
1

sans utiliser la file d'attente, vous pouvez utiliser proc_open()comme ceci:

    $descriptorspec = array(
        0 => array("pipe", "r"),
        1 => array("pipe", "w"),
        2 => array("pipe", "w")    //here curaengine log all the info into stderror
    );
    $command = 'ping stackoverflow.com';
    $process = proc_open($command, $descriptorspec, $pipes);
Kris Roofe
la source