4 votes

Redirige le tube acquis par proc_open() vers un fichier pour le reste de la durée du processus.

Dites, en PHP J'ai un tas de tests unitaires. Disons qu'ils nécessitent l'exécution d'un service.

Idéalement, je veux que mon script de bootstrap :

  • démarrer ce service
  • attendre que le service atteigne un état souhaité
  • donner le contrôle au cadre de test unitaire de son choix pour exécuter les tests
  • faire le ménage à la fin des tests, en mettant fin au service de manière élégante, le cas échéant
  • mettre en place un moyen de capturer toutes les sorties du service en cours de route pour la journalisation et le débogage.

J'utilise actuellement proc_open() pour initialiser mon service, capturer la sortie en utilisant le mécanisme de pipe, vérifier que le service arrive à l'état dont j'ai besoin en examinant la sortie.

Cependant, à ce stade, je suis bloqué - comment puis-je capturer le reste de la sortie (y compris STDERR) pour le reste de la durée du script, tout en permettant à mes tests unitaires de s'exécuter ?

Je peux penser à quelques solutions potentiellement longues, mais avant d'investir du temps dans leur recherche, j'aimerais savoir si quelqu'un d'autre a été confronté à ce problème et quelles solutions il a trouvées, le cas échéant, sans influencer la réponse.

Editar:

Voici une version réduite de la classe que j'initialise dans mon script bootstrap (avec new ServiceRunner ), à titre de référence :

<?php

namespace Tests;

class ServiceRunner
{
    /**
     * @var resource[]
     */
    private $servicePipes;

    /**
     * @var resource
     */
    private $serviceProc;

    /**
     * @var resource
     */
    private $temp;

    public function __construct()
    {
        // Open my log output buffer
        $this->temp = fopen('php://temp', 'r+');

        fputs(STDERR,"Launching Service.\n");
        $this->serviceProc      = proc_open('/path/to/service', [
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w"),
        ], $this->servicePipes);

        // Set the streams to non-blocking, so stream_select() works
        stream_set_blocking($this->servicePipes[1], false);
        stream_set_blocking($this->servicePipes[2], false);

        // Set up array of pipes to select on
        $readables = [$this->servicePipes[1], $this->servicePipes[2]);

        while(false !== ($streams = stream_select($read = $readables, $w = [], $e = [], 1))) {
            // Iterate over pipes that can be read from
            foreach($read as $stream) {
                // Fetch a line of input, and append to my output buffer
                if($line = stream_get_line($stream, 8192, "\n")) {
                    fputs($this->temp, $line."\n");
                }

                // Break out of both loops if the service has attained the desired state
                if(strstr($line, 'The Service is Listening' ) !== false) {
                    break 2;
                }

                // If the service has closed one of its output pipes, remove them from those we're selecting on
                if($line === false && feof($stream)) {
                    $readables = array_diff($readables, [$stream]);
                }
            }
        }

        /* SOLUTION REQUIRED SOLUTION REQUIRED SOLUTION REQUIRED SOLUTION REQUIRED */
        /* Set up the pipes to be redirected to $this->temp here */

        register_shutdown_function([$this, 'shutDown']);
    }

    public function shutDown()
    {
        fputs(STDERR,"Closing...\n");
        fclose($this->servicePipes[0]);
        proc_terminate($this->serviceProc, SIGINT);
        fclose($this->servicePipes[1]);
        fclose($this->servicePipes[2]);
        proc_close($this->serviceProc);
        fputs(STDERR,"Closed service\n");

        $logFile = fopen('log.txt', 'w');

        rewind($this->temp);
        stream_copy_to_stream($this->temp, $logFile);

        fclose($this->temp);
        fclose($logFile);
    }
}

1voto

Ruslan Osmanov Points 13305

Supposons que le service soit implémenté comme service.sh shell script avec le contenu suivant :

#!/bin/bash -
for i in {1..4} ; do
  printf 'Step %d\n' $i
  printf 'Step Error %d\n' $i >&2
  sleep 0.7
done

printf '%s\n' 'The service is listening'

for i in {1..4} ; do
  printf 'Output %d\n' $i
  printf 'Output Error %d\n' $i >&2
  sleep 0.2
done

echo 'Done'

Le script émule le processus de démarrage, imprime le message indiquant que le service est prêt, et imprime quelques sorties après le démarrage.

Puisque vous n'effectuez pas les tests unitaires tant que le "marqueur de service prêt" n'est pas lu, je ne vois pas de raison particulière de le faire de manière asynchrone. Si vous voulez exécuter un processus (mise à jour de l'interface utilisateur, etc.) en attendant le service, je vous suggère d'utiliser une extension comportant des fonctions asynchrones ( pthreads , ev , événement etc.).

Cependant, s'il n'y a que deux choses à faire de manière asynchrone, alors pourquoi ne pas bifurquer un processus ? Le service peut s'exécuter dans le processus parent, et les tests unitaires peuvent être lancés dans le processus enfant :

<?php
$cmd = './service.sh';
$desc = [
  1 => [ 'pipe', 'w' ],
  2 => [ 'pipe', 'w' ],
];
$proc = proc_open($cmd, $desc, $pipes);
if (!is_resource($proc)) {
  die("Failed to open process for command $cmd");
}

$service_ready_marker = 'The service is listening';
$got_service_ready_marker = false;

// Wait until service is ready
for (;;) {
  $output_line = stream_get_line($pipes[1], PHP_INT_MAX, PHP_EOL);
  echo "Read line: $output_line\n";
  if ($output_line === false) {
    break;
  }
  if ($output_line == $service_ready_marker) {
    $got_service_ready_marker = true;
    break;
  }

  if ($error_line = stream_get_line($pipes[2], PHP_INT_MAX, PHP_EOL)) {
    $startup_errors []= $error_line;
  }
}

if (!empty($startup_errors)) {
  fprintf(STDERR, "Startup Errors: <<<\n%s\n>>>\n", implode(PHP_EOL, $startup_errors));
}

if ($got_service_ready_marker) {
  echo "Got service ready marker\n";
  $pid = pcntl_fork();
  if ($pid == -1) {
    fprintf(STDERR, "failed to fork a process\n");

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);
  } elseif ($pid) {
    // parent process

    // capture the output from the service
    $output = stream_get_contents($pipes[1]);
    $errors = stream_get_contents($pipes[2]);

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    // Use the captured output
    if ($output) {
      file_put_contents('/tmp/service.output', $output);
    }
    if ($errors) {
      file_put_contents('/tmp/service.errors', $errors);
    }

    echo "Parent: waiting for child processes to finish...\n";
    pcntl_wait($status);
    echo "Parent: done\n";
  } else {
    // child process

    // Cleanup
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    // Run unit tests
    echo "Child: running unit tests...\n";
    usleep(5e6);
    echo "Child: done\n";
  }
}

Exemple de sortie

Read line: Step 1
Read line: Step 2
Read line: Step 3
Read line: Step 4
Read line: The service is listening
Startup Errors: <<<
Step Error 1
Step Error 2
Step Error 3
Step Error 4
>>>
Got service ready marker
Child: running unit tests...
Parent: waiting for child processes to finish...
Child: done
Parent: done

0voto

Adam Points 4201

Vous pouvez utiliser le pcntl_fork() pour bifurquer le processus actuel afin de réaliser les deux tâches et attendre la fin des tests :

 <?php
 // [launch service here]
 $pid = pcntl_fork();
 if ($pid == -1) {
      die('error');
 } else if ($pid) {
      // [read output here]
      // then wait for the unit tests to end (see below)
      pcntl_wait($status);
      // [gracefully finishing service]
 } else {
      // [unit tests here]
 }

 ?>

0voto

Benjamin Points 909

Ce que j'ai fini par faire, après avoir atteint le point où le service a été initialisé correctement, c'est de rediriger les tuyaux du processus déjà ouvert en tant qu'entrée standard vers un fichier de type cat par tuyau, également ouvert par proc_open() (aidé par cette réponse ).

Ce n'était pas toute l'histoire, car je suis arrivé à ce point et j'ai réalisé que le processus asynchrone se suspendait après un certain temps en raison du remplissage de la mémoire tampon du flux.

La partie essentielle dont j'avais besoin (après avoir configuré les flux en mode non bloquant auparavant) était de faire passer les flux en mode bloquant, de manière à ce que la mémoire tampon se déverse dans la mémoire de réception. cat correctement.

Pour compléter le code de ma question :

// Iterate over the streams that are stil open
foreach(array_reverse($readables) as $stream) {
    // Revert the blocking mode
    stream_set_blocking($stream, true);
    $cmd = 'cat';

    // Receive input from an output stream for the previous process,
    // Send output into the internal unified output buffer
    $pipes = [
        0 => $stream,
        1 => $this->temp,
        2 => array("file", "/dev/null", 'w'),
    ];

    // Launch the process
    $this->cats[] = proc_open($cmd, $pipes, $outputPipes = []);
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X