Exec asíncrono de shell en PHP

199

Tengo un script PHP que necesita invocar un script de shell pero no me importa en absoluto la salida. El script de shell realiza varias llamadas SOAP y tarda en completarse, por lo que no quiero ralentizar la solicitud de PHP mientras espera una respuesta. De hecho, la solicitud de PHP debería poder salir sin terminar el proceso de shell.

He mirado en las diversas exec(), shell_exec(), pcntl_fork(), etc. funciones, pero ninguna de ellas parece ofrecer exactamente lo que quiero. (O, si lo hacen, no me queda claro cómo). ¿Alguna sugerencia?

AdamTheHutt
fuente
Independientemente de la solución que elija, también debe considerar usar nicey ioniceevitar que el script de shell abrume su sistema (p /usr/bin/ionice -c3 /usr/bin/nice -n19. Ej. )
rinogo
Posible duplicado de php ejecuta un proceso en segundo plano
Sean the Bean

Respuestas:

218

Si "no le importa la salida", ¿no se podría llamar al ejecutivo del script con &el proceso de fondo?

EDITAR : incorporando lo que @ AdamTheHut comentó en esta publicación, puede agregar esto a una llamada a exec:

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

Eso redirigirá stdio(primero >) y stderr( 2>) a /dev/nully se ejecutará en segundo plano.

Hay otras formas de hacer lo mismo, pero esta es la más simple de leer.


Una alternativa a la doble redirección anterior:

" &> /dev/null &"
madriguera
fuente
11
Esto parece funcionar, pero necesita un poco más que un ampersand. Lo hice funcionar agregando "> / dev / null 2> / dev / null &" a la llamada exec (). Aunque tengo que admitir que no estoy exactamente seguro de lo que hace.
AdamTheHutt
2
Definitivamente el camino a seguir si quieres disparar y olvidarte con php y apache. Una gran cantidad de entornos de producción Apache y PHP tendrán pcntl_fork () deshabilitado.
método
9
Solo una nota para &> /dev/null &, xdebug no generará registros si usa esto. Consulte stackoverflow.com/questions/4883171/…
kapeels
55
@MichaelJMulligan cierra los descriptores de archivo. Dicho esto, a pesar de las ganancias de eficiencia, en retrospectiva, el uso /dev/nulles la mejor práctica, ya que escribir en FD cerrados causa errores, mientras que los intentos de leer o escribir /dev/nullsimplemente no hacen nada en silencio.
Charles Duffy
3
Los scripts de fondo se eliminarán al reiniciar Apache ... solo tenga en cuenta esto para trabajos muy largos o si tiene mala suerte en cuanto al tiempo y se pregunta por qué desapareció su trabajo ...
Julien
53

Solía al por esto, ya que está empezando un proceso independiente.

<?php
    `echo "the command"|at now`;
?>
Czimi
fuente
44
En algunas situaciones, esta es absolutamente la mejor solución. fue el único que funcionó para mí para lanzar un "reinicio de sudo" ("echo 'sleep 3; sudo reiniciar' | ahora") desde un webgui Y terminar de mostrar la página .. en openbsd
Kaii
1
si el usuario con el que ejecuta Apache (por lo general www-data) no tiene los permisos para usar aty no puede configurarlo, puede intentar usar <?php exec('sudo sh -c "echo \"command\" | at now" ');If commandcontiene comillas, vea escapeshellarg para evitar dolores de cabeza
Julien
1
Bien pensando en ello, hacer que sudo ejecute sh no es la mejor idea, ya que básicamente le da a sudo un acceso raíz a todo. Volví a usar echo "sudo command" | at nowy comentar www-dataen/etc/at.deny
Julien
@Julien tal vez podrías hacer un script de shell con el comando sudo y ejecutarlo en su lugar. siempre y cuando no pase ningún valor enviado por el usuario a dicho script
Garet Claborn el
26

Para todos los usuarios de Windows: encontré una buena manera de ejecutar un script PHP asincrónico (en realidad funciona con casi todo).

Se basa en los comandos popen () y pclose (). Y funciona bien tanto en Windows como en Unix.

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

Código original de: http://php.net/manual/en/function.exec.php#86329

LucaM
fuente
esto solo ejecuta un archivo, no funciona si usa php / python / node, etc.
david valentino
@davidvalentino Correcto, ¡y está bien! Si desea ejecutar un script PHP / Pyhton / NodeJS, debe llamar al ejecutable y pasarle su script. Por ejemplo: no pones en tu terminal, myscript.jssino que escribirás node myscript.js. Es decir: nodo es el ejecutable, myscript.js es, bueno, el script para ejecutar. Hay una gran diferencia entre ejecutable y script.
LucaM
correcto, eso no es problema en ese caso, en otros casos, como la necesidad de ejecutar laravel artisan, php artisansolo ponga un comentario aquí, así que no es necesario rastrear por qué no funciona con comandos
david valentino
22

En linux puedes hacer lo siguiente:

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

Esto ejecutará el comando en el símbolo del sistema y luego solo devolverá el PID, que puede verificar> 0 para asegurarse de que funcionó.

Esta pregunta es similar: ¿PHP tiene subprocesos?

Darryl Hein
fuente
Esta respuesta sería más fácil de leer si incluyera solo lo esencial (eliminando el action=generate var1_id=23 var2_id=35 gen_id=535segmento). Además, dado que OP le preguntó sobre cómo ejecutar un script de shell, no necesita las porciones específicas de PHP. El código final sería:$cmd = 'nohup nice -n 10 /path/to/script.sh > /path/to/log/file.log & printf "%u" $!';
rinogo
Además, como una nota de alguien que "ha estado allí antes", cualquiera que lea esto podría considerar usar no solo nicesino también ionice.
rinogo
1
¿Qué significa "% u" $! hacer exactamente?
Ramitas
@Twigs &ejecuta el código anterior en segundo plano, luego printfse usa para la salida formateada de la $!variable que contiene el PID
NoChecksum
1
Muchas gracias, probé todo tipo de soluciones que estaba encontrando para enviar a un registro con una llamada asincrónica de PHP y la suya es la única que cumple con todos los criterios.
Josh Powlison
6

En Linux, puede iniciar un proceso en un nuevo hilo independiente agregando un ampersand al final del comando

mycommand -someparam somevalue &

En Windows, puede usar el comando "iniciar" DOS

start mycommand -someparam somevalue
León
fuente
2
En Linux, el padre todavía puede bloquear hasta que el niño haya terminado de ejecutarse si está tratando de leer desde un identificador de archivo abierto en el subproceso (es decir, stdout), por lo que esta no es una solución completa.
Charles Duffy
1
startComando probado en Windows, no se ejecuta de forma asíncrona ... ¿Podría incluir la fuente de donde obtuvo esa información?
Alph.Dev
@ Alph.Dev eche un vistazo a mi respuesta si está utilizando Windows: stackoverflow.com/a/40243588/1412157
LucaM
@mynameis Su respuesta muestra exactamente por qué el comando de inicio NO estaba funcionando. Es por el /Bparámetro. Lo he explicado aquí: stackoverflow.com/a/34612967/1709903
Alph.Dev
6

la forma correcta (!) de hacerlo es

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

fork forks, setsid le dice al proceso actual que se convierta en uno maestro (no padre), execve le dice al proceso de llamada que sea reemplazado por el llamado. para que el padre pueda renunciar sin afectar al niño.

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

 // parent's stuff
 exit();

fuente
66
El problema con pcntl_fork () es que se supone que no debe usarlo cuando se ejecuta bajo un servidor web, como lo hace el OP (además, el OP ya lo ha intentado).
Guss
6

Usé esto ...

/** 
 * 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 &");
}

(donde PHP_PATHse define una constante como define('PHP_PATH', '/opt/bin/php5')o similar)

Pasa argumentos a través de la línea de comando. Para leerlos en PHP, vea argv .

philfreo
fuente
4

La única forma en que encontré que realmente funcionó para mí fue:

shell_exec('./myscript.php | at now & disown')
Gordon Forsythe
fuente
3
'disown' es un Bash incorporado y no funciona con shell_exec () de esta manera. Lo intenté shell_exec("/usr/local/sbin/command.sh 2>&1 >/dev/null | at now & disown")y todo lo que obtengo es:sh: 1: disown: not found
Thomas Daugaard
4

También encontré que Symfony Process Component es útil para esto.

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

Vea cómo funciona en su repositorio de GitHub .

Anton Pelykh
fuente
2
Advertencia: si no espera, el proceso se
cancelará
Herramienta perfecta, que se basa en proc_*funciones internas.
MAChitgarha
2

También puede ejecutar el script PHP como daemon o cronjob :#!/usr/bin/php -q

Ronald Conco
fuente
1

Use un fifo con nombre.

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

Luego, cuando desee comenzar la tarea de larga duración, simplemente escriba una nueva línea (sin bloqueo en el archivo desencadenante).

Siempre que su entrada sea menor PIPE_BUFy sea una write()operación única , puede escribir argumentos en el fifo y hacer que aparezcan como $REPLYen el script.

geocar
fuente
1

sin cola de uso, puede usar el proc_open()siguiente:

    $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
fuente