Tasso & As Vozes

Adeus shell_exec()

· 6 minutos

Como converter um PDF em imagens PNG ou JPEG, usando PHP? Como converter arquivos de vídeo enviados via upload em formatos para a web, como WebM ou FLV, usando PHP? Como realizar uma tarefa administrativa no servidor, usando PHP?

Muitas vezes nos fazemos essas perguntas e passamos por algumas decepções ao procurar soluções. A primeira é descobrir que nem sempre podemos resolver usando única e exclusivamente a linguagem e/ou tecnologia que usamos em nossos projetos: PHP não foi construída para executar tarefas longas e que demandam alto processamento, como converter formatos de imagem, áudio e vídeo. Até podemos considerar o uso de extensões (como a ImageMagick), mas geralmente o modo rápido de conseguir o que se quer é através de programas externos, como avconv e convert.

A principal dificuldade esperada ao realizar a integração entre scripts PHP e programas externos é a perda de compatibilidade nas diversas plataformas em que PHP é executável. A maioria dos exemplos de uso das funções exec(), shell_exec() e passthru() utiliza programas do ecossistema Unix e, ao menos no Brasil, o número de desenvolvedores PHP que rodam Windows em suas estações de trabalho é grande. Para eles, usar programas externos é adicionar ao seu código algo que só poderá ser testado em servidores de teste ou desenvolvimento uma realidade que vem mudando, a passos de formiga, com a adoção de ferramentas como Vagrant e Docker.

Então é simples usar programas externos? Não. Todo programa é executado como um novo processo do sistema, chamado pelo shell através de uma linha de comando. E neste contexto, existem algumas coisas a se considerar:

  1. Deve ser garantido que nenhum número abusivo de processos seja executado;
  2. Deve ser garantido que nenhum dado enviado execute código arbitrário;
  3. Todo processo possui uma stream de entrada de dados, uma stream de saída normal e uma stream de saída de erros;
  4. Alguns processos exigem interação do usuário através da stream de entrada;
  5. Alguns processos são longos;
  6. Alguns processos escrevem dados em formatos complexos nas streams de saída;
  7. Processos retornam um código de status de fim de execução que indica erros ocorridos durante a execução.

Vamos ver que opções as funções padrão do PHP nos fornecem para trabalhar com processos:

string shell_exec(string $cmd) ou `$cmd`

É a função ideal para processos invocados com uma linha de comando simples e que escrevem apenas uma linha de texto na stream de saída.

  1. Somente um processo pode ser executado por vez, a menos que se utilizem mecanismos de execução paralela presentes no shell (e.g. start convert doc.pdf images.jpg em Windows).
  2. Você depende de escapeshellarg($arg) para escapar argumentos vindos da entrada de usuário. Usar ou não usar é por sua conta e risco. Para o operador de execução (` `) é impossível passar dados de usuário, como se a definição da linha de comando do processo fosse constante.
  3. Tudo o que foi escrito na stream de saída é retornado pela função. Em caso de erro, entretanto, é retornado o valor NULL, mesmo que dados tenham sido escritos.
  4. Não permite escrever dados na stream de entrada.
  5. Você não pode definir um tempo máximo para a execução do processo, embora fique limitado pelo limite de tempo de execução do próprio script PHP.
  6. Você precisa parsear toda a stream de saída obtida através do retorno da função/expressão.
  7. Você não obtém o código de status. Se ele for zero, a função retorna o conteúdo da stream de saída; caso contrário, NULL é retornado.

string exec(string $command[, array &$output[, int &$return_var]])

Esta função adiciona um grau maior de controle sob o que é escrito na stream de saída e sobre os códigos de status.

  1. Somente um processo pode ser executado por vez, a menos que se utilizem mecanismos de execução paralela presentes no shell.
  2. Você depende de escapeshellarg($arg) para escapar argumentos vindos da entrada de usuário. Usar ou não usar é por sua conta e risco.
  3. Você pode ter acesso ao que foi escrito na stream de saída através do array $output ou apenas da última linha escrita através do retorno da função.
  4. Não permite escrever dados na stream de entrada.
  5. Você não pode definir um tempo máximo para a execução do processo, embora fique limitado pelo limite de tempo de execução do próprio script PHP.
  6. Você precisa parsear toda a stream de saída obtida através de $output.
  7. Você obtém o código de status através de $return_var.

void passthru(string $command[, int &$return_var])

O uso mais comum desta função é quando o script PHP funciona como um simples proxy para um programa externo, como um gerador de imagens ou compilador.

  1. Somente um processo pode ser executado por vez, a menos que se utilizem mecanismos de execução paralela presentes no shell.
  2. Você depende de escapeshellarg($arg) para escapar argumentos vindos da entrada de usuário. Usar ou não usar é por sua conta e risco.
  3. A stream de saída do processo é redirecionada para a stream de saída do script PHP. Se você quiser capturar a saída, vai ter que utilizar mecanismos de output buffering (ob_start()).
  4. Não permite escrever dados na stream de entrada.
  5. Você não pode definir um tempo máximo para a execução do processo, embora fique limitado pelo limite de tempo de execução do próprio script PHP.
  6. Você precisa parsear toda a stream de saída obtida através de output buffering.
  7. Você obtém o código de status através de $return_var.

resource proc_open(string $cmd, array $descriptorspec, array &$pipes[, string $cwd[, array $env[, array $other_options]]])

E aqui temos o maior controle possível de processos (inclusive do diretório de trabalho do processo, via $cwd) através de scripts PHP.

  1. Esta função é não-bloqueante, de modo que você pode iniciar quantos processos julgar adequado. Vale o bom senso para garantir que uma quantidade não-abusiva de processos sejam rodados paralelamente.
  2. Você depende de escapeshellarg($arg) para escapar argumentos vindos da entrada de usuário. Usar ou não usar é por sua conta e risco.
  3. Você tem acesso a stdin, stdout e stderr através do array $pipes, como se fossem ponteiros de arquivo tradicionais.
  4. stdin é uma stream de escrita, onde você pode escrever usando funções simples como fwrite().
  5. O controle de timeout dos processos pode ser implementado, já que o processo roda paralelamente ao script.
  6. A leitura dos dados escritos em stdout e stderr não difere da leitura de um arquivo; assim, as mesmas técnicas empregadas para parsear um arquivo podem ser aplicadas, seja de forma integral (ler toda a stream e interpretar o formato), seja de forma contínua (ler a stream linha a linha e interpretar durante a execução).
  7. Você pode ter o código de status a partir do retorno de proc_close($process).

Tudo é complicado

Pelo que se percebe, a complexidade para se executar certos processos via PHP é semelhante à própria complexidade do processo, i.e., um processo que exige interação com todas as streams, timeout e controle do código de status vai demandar o uso de uma função muito complexa. Veja você mesmo:

<?php
$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("file", "/tmp/error-output.txt", "a") // stderr is a file to write to
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('php', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
    // $pipes now looks like this:
    // 0 => writeable handle connected to child stdin
    // 1 => readable handle connected to child stdout
    // Any error output will be appended to /tmp/error-output.txt

    fwrite($pipes[0], '<?php print_r($_ENV); ?>');
    fclose($pipes[0]);

    echo stream_get_contents($pipes[1]);
    fclose($pipes[1]);

    // It is important that you close any pipes before calling
    // proc_close in order to avoid a deadlock
    $return_value = proc_close($process);

    echo "command returned $return_value\n";
}

Nesse exemplo, abrimos o processo php, escrevemos na stream de entrada o código <?php print_r($_ENV); ?> e lemos o resultado da execução na stream de saída do processo. Nenhum mecanismo de timeout é implementado, bem como nenhum controle de erros é realizado através da stream de erros e do código de status, e mesmo assim temos um código assustador para programadores novatos.

Symfony Process ao resgate

Streams, código de status, timeout, diretório de trabalho, argumentos escapados... Muitos são os componentes e detalhes presentes na execução de um processo. Para nossa sorte, o componente Symfony Process provê um mecanismo simples para execução e controle de processos (e ao contrário do que muitos pensam não é necessário estar utilizando o framework Symfony para usufruir deste componente). Vamos recriar o exemplo supracitado para demonstrar isso.

Primeiramente, você pode baixar as classes do Symfony Process de modo tradicional, mas isso é desaconselhado; prefira fazer o controle deste e de demais códigos de terceiros através do Composer, um gerenciador de dependências para PHP que se tornou praticamente o padrão da indústria. Caso você ainda não esteja utilizando no seu projeto, execute no terminal

$ composer init

E forneça informações básicas do seu projeto. Assim que o arquivo composer.json estiver disponível, execute

$ composer require symfony/process

Para adicionar ao seu projeto a última versão do componente Symfony Process. Todo o código fica disponível no diretório vendor/symfony/process e o arquivo composer.lock é criado para registrar qual a versão utilizada.

Com a library em mãos, vamos recriar o exemplo passo-a-passo. Escreva, num arquivo chamado exemplo.php, as seguintes linhas:

<?php
require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Process\Process;

A primeira linha vai adicionar o autoloader do Composer, tornando acessíveis todas as classes do Symfony Process. A linha seguinte permite que a classe Symfony\Component\Process\Process possa ser chamada apenas de Process no script. Ainda no mesmo arquivo, escreva:

$process = new Process('php');

Essa linha não executa o processo php de imediato, apenas prepara uma instância da classe Process que representa um processo antes, durante, e após sua execução.

$process->setInput('<?php print_r($_ENV); ?>');

Aqui foi definido qual conteúdo será escrito em stdin que o processo seja executado (esse método lança RuntimeException se é executado depois que o processo é executado).

$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Aqui o processo é executado de fato, de forma síncrona (para executar o processo de forma asíncrona, $process->start() deve ser invocado no lugar de $process->run()), mas com um adendo: toda saída gerada pelo processo, em stdout e stderr, é passada imediatamente para o callable passado como parâmetro do método run() (neste caso, é uma closure). O callable deve aceitar dois parâmetros: $type, que indica se a saída foi escrita em stdout ou stderr; $buffer, que contém o texto escrito.

E... Isso é tudo. Você não precisa fechar o processo, muito menos pará-lo. Segue o exemplo completo:

<?php
require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Process\Process;

$process = new Process('php');
$process->setInput('<?php print_r($_ENV); ?>');
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Agora vá!

Espero que este artigo tenha instigado o leitor investir um pouco de atenção ao componente Symfony Process. As possibilidades com ele são interessantes, principalmente se o seu desejo for de adicionar poder de fogo à sua aplicação web. Em breve devo demonstrar mais casos de uso, como invocação de convert, avconv e rsync. Até lá!