Exec shell assíncrono em PHP

199

Eu tenho um script PHP que precisa chamar um shell script, mas não se importa com a saída. O script de shell faz várias chamadas SOAP e é lento para concluir, portanto, não quero abrandar a solicitação do PHP enquanto aguarda uma resposta. De fato, a solicitação do PHP deve poder sair sem encerrar o processo do shell.

Eu olhei para os vários exec(), shell_exec(), pcntl_fork()funções, etc., mas nenhum deles parecem oferecer exatamente o que eu quero. (Ou, se o fizerem, não está claro para mim como.) Alguma sugestão?

AdamTheHutt
fonte
Não importa qual a solução que você escolher, você também deve considerar o uso nicee ionicepara impedir que o script shell de sobrecarregar o seu sistema (por exemplo /usr/bin/ionice -c3 /usr/bin/nice -n19)
rinogo
Possível duplicado de php executar um processo em segundo plano
Sean Bean

Respostas:

218

Se "não se importa com a saída", o executivo do script não pode ser chamado com o &pano de fundo do processo?

EDIT - incorporando o que o @ AdamTheHut comentou neste post, você pode adicioná-lo a uma chamada para exec:

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

Isso redirecionará ambos stdio(primeiro >) e stderr( 2>) para /dev/nulle será executado em segundo plano.

Existem outras maneiras de fazer a mesma coisa, mas essa é a mais simples de ler.


Uma alternativa ao redirecionamento duplo acima:

" &> /dev/null &"
Warren
fonte
11
Isso parece funcionar, mas precisa de um pouco mais do que um e comercial. Eu o consegui anexando "> / dev / null 2> / dev / null &" à chamada exec (). Embora eu deva admitir que não tenho muita certeza do que isso faz.
21820 AdamTheHutt
2
Definitivamente, o caminho a percorrer se você quiser disparar e esquecer com php e apache. Muitos ambientes Apache e PHP de produção terão o pcntl_fork () desativado.
método
9
Apenas uma observação para &> /dev/null &, xdebug não irá gerar logs se você usar isso. Verifique stackoverflow.com/questions/4883171/…
kapeels
5
@MichaelJMulligan fecha os descritores de arquivo. Dito isto, apesar dos ganhos de eficiência, em retrospectiva, usar /dev/nullé a melhor prática, pois gravar em FDs fechados causa erros, enquanto as tentativas de ler ou gravar /dev/nullsimplesmente não fazem nada silenciosamente.
Charles Duffy
3
Scripts de fundo serão mortos quando reiniciar o apache ... apenas estar ciente disto para trabalhos muito longos ou se você é azarado tempo-sábio e você quer saber porque seu trabalho desapareceu ...
Julien
53

Eu usei a para isso, como ele está realmente começando um processo independente.

<?php
    `echo "the command"|at now`;
?>
Czimi
fonte
4
em algumas situações, essa é absolutamente a melhor solução. que era o único que trabalhou para mim para lançar um "reboot sudo" ( "echo 'sono 3; reinicialização sudo' | no now") a partir de uma interface Web e terminar processar a página .. no OpenBSD
Kaii
1
se o usuário executar o apache (geralmente www-data) não tem as permissões para uso ate você não pode configurá-lo para, você pode tentar usar <?php exec('sudo sh -c "echo \"command\" | at now" ');Se commandcontém aspas, consulte escapeshellarg para poupar dores de cabeça
Julien
1
Bem, pensando bem, fazer o sudo rodar sh não é a melhor ideia, pois basicamente dá ao sudo acesso root a tudo. Eu reverti para usar echo "sudo command" | at nowe comentar www-dataem/etc/at.deny
Julien
@ Julien talvez você possa criar um script de shell com o comando sudo e iniciá-lo. contanto que você não está passando qualquer usuário apresentado valores para o referido roteiro
Garet Claborn
26

Para todos os usuários do Windows: Encontrei uma boa maneira de executar um script PHP assíncrono (na verdade, ele funciona com quase tudo).

É baseado nos comandos popen () e pclose (). E funciona bem no Windows e no 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
fonte
isso apenas executa um arquivo, não funciona se estiver usando php / python / node etc.
david valentino
@davidvalentino Correto, e tudo bem! Se você deseja executar um script PHP / Pyhton / NodeJS, é necessário chamar o executável e passar seu script para ele. Por exemplo: você não coloca no seu terminal, myscript.jsmas escreve node myscript.js. Ou seja: node é o executável, myscript.js é, bem, o script a ser executado. Há uma enorme diferença entre executável e script.
LucaM 16/01
certo isso não é problema nesse caso ,, em outros casos exemplo como a necessidade de executar o laravel artisan, php artisanbasta colocar um comentário aqui para que não seja necessário rastrear por que ele não funcionará com comandos
david valentino
22

No linux, você pode fazer o seguinte:

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

Isso executará o comando no prompt de comando e retornará o PID, que você pode verificar> 0 para garantir que funcionou.

Esta pergunta é semelhante: O PHP tem threading?

Darryl Hein
fonte
Essa resposta seria mais fácil de ler se você incluísse apenas o essencial (eliminando o action=generate var1_id=23 var2_id=35 gen_id=535segmento). Além disso, como o OP perguntou sobre a execução de um script de shell, você não precisa das partes específicas do PHP. O código final seria:$cmd = 'nohup nice -n 10 /path/to/script.sh > /path/to/log/file.log & printf "%u" $!';
rinogo
Além disso, como uma nota de quem "já esteve lá antes", qualquer pessoa que esteja lendo isso pode considerar usar não apenas nicemas também ionice.
Rinogo 2/08
1
O que significa "% u" $! fazer exatamente?
Twigs
@Twigs &pistas anteriores código no fundo, em seguida, printfé usado para a saída formatado da $!variável que contém o PID
NoChecksum
1
Muito obrigado, tentei todos os tipos de soluções que encontrava para gerar um log com uma chamada de shell PHP assíncrona e a sua é a única que atendeu a todos os critérios.
Josh Powlison
6

No Linux, você pode iniciar um processo em um novo encadeamento independente anexando um e comercial no final do comando

mycommand -someparam somevalue &

No Windows, você pode usar o comando DOS "start"

start mycommand -someparam somevalue
Leo
fonte
2
No Linux, o pai ainda pode bloquear até que o filho termine de executar se estiver tentando ler de um identificador de arquivo aberto mantido pelo subprocesso (por exemplo, stdout), portanto, essa não é uma solução completa.
Charles Duffy
1
startComando testado no Windows, ele não é executado de forma assíncrona ... Você poderia incluir a fonte de onde obteve essas informações?
Alph.Dev
@ Alph.Dev, por favor, dê uma olhada na minha resposta se você estiver usando o Windows: stackoverflow.com/a/40243588/1412157
LucaM
@mynameis Sua resposta mostra exatamente por que o comando start NÃO estava funcionando. É por causa do /Bparâmetro. Eu expliquei aqui: stackoverflow.com/a/34612967/1709903
Alph.Dev
6

o caminho certo (!) para fazê-lo é

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

fork garfos, setsid informa o processo atual para se tornar um master (sem pai), execve informa o processo de chamada a ser substituído pelo chamado. para que o pai possa sair sem afetar o filho.

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

 // parent's stuff
 exit();

fonte
6
O problema com pcntl_fork () é que você não deve usá-lo quando estiver executando em um servidor web, como o OP (além disso, o OP já tentou isso).
Guss
6

Eu usei isso ...

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

(onde PHP_PATHé uma constante definida como define('PHP_PATH', '/opt/bin/php5')semelhante ou similar)

Ele passa argumentos através da linha de comando. Para lê-los em PHP, consulte argv .

philfreo
fonte
4

A única maneira que eu descobri que realmente funcionou para mim foi:

shell_exec('./myscript.php | at now & disown')
Gordon Forsythe
fonte
3
'disown' é um Bash embutido e não funciona com shell_exec () dessa maneira. Eu tentei shell_exec("/usr/local/sbin/command.sh 2>&1 >/dev/null | at now & disown")e tudo o que recebo é:sh: 1: disown: not found
Thomas Daugaard 15/01
4

Eu também achei o Symfony Process Component útil para isso.

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

Veja como ele funciona em seu repositório GitHub .

Anton Pelykh
fonte
2
Aviso: se você não esperar, o processo será
interrompido
Ferramenta perfeita, baseada em proc_*funções internas.
MAChitgarha
2

Você também pode executar o script PHP como daemon ou cronjob :#!/usr/bin/php -q

Ronald Conco
fonte
1

Use um fifo nomeado.

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

Depois, sempre que desejar iniciar a tarefa de longa execução, basta escrever uma nova linha (sem bloqueio no arquivo acionador.

Desde que sua entrada seja menor que PIPE_BUFe seja uma write()operação única , você pode escrever argumentos no fifo e fazê-los aparecer como $REPLYno script.

geocar
fonte
1

sem fila de uso, você pode usar o proc_open()seguinte:

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