xref: /PHP-8.1/sapi/fpm/tests/logreader.inc (revision 5a4520bc)
1<?php
2
3namespace FPM;
4
5class LogReader
6{
7    /**
8     * Log debugging.
9     *
10     * @var bool
11     */
12    private bool $debug;
13
14    /**
15     * Log descriptor.
16     *
17     * @var string|null
18     */
19    private ?string $currentSourceName;
20
21    /**
22     * Log descriptors.
23     *
24     * @var LogSource[]
25     */
26    private array $sources = [];
27
28    /**
29     * Log reader constructor.
30     *
31     * @param bool $debug
32     */
33    public function __construct(bool $debug = false)
34    {
35        $this->debug = $debug;
36    }
37
38    /**
39     * Returns log descriptor source.
40     *
41     * @return LogSource
42     * @throws \Exception
43     */
44    private function getSource(): LogSource
45    {
46        if ( ! $this->currentSourceName) {
47            throw new \Exception('Log descriptor is not set');
48        }
49
50        return $this->sources[$this->currentSourceName];
51    }
52
53    /**
54     * Set current stream source and create it if it does not exist.
55     *
56     * @param string   $name   Stream name.
57     * @param resource $stream The actual stream.
58     */
59    public function setStreamSource(string $name, $stream)
60    {
61        $this->currentSourceName = $name;
62        if ( ! isset($this->sources[$name])) {
63            $this->sources[$name] = new LogStreamSource($stream);
64        }
65    }
66
67    /**
68     * Set file source as current and create it if it does not exist.
69     *
70     * @param string $name     Source name.
71     * @param string $filePath Source file path.s
72     */
73    public function setFileSource(string $name, string $filePath)
74    {
75        $this->currentSourceName = $name;
76        if ( ! isset($this->sources[$name])) {
77            $this->sources[$name] = new LogFileSource($filePath);
78        }
79    }
80
81    /**
82     * Get a single log line.
83     *
84     * @param int  $timeoutSeconds      Read timeout in seconds
85     * @param int  $timeoutMicroseconds Read timeout in microseconds
86     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
87     *
88     * @return null|string
89     * @throws \Exception
90     */
91    public function getLine(
92        int $timeoutSeconds = 3,
93        int $timeoutMicroseconds = 0,
94        bool $throwOnTimeout = false
95    ): ?string {
96        $line = $this->getSource()->getLine(
97            $timeoutSeconds,
98            $timeoutMicroseconds,
99            $throwOnTimeout
100        );
101        $this->trace(is_null($line) ? "LINE - null" : "LINE: $line");
102
103        return $line;
104    }
105
106    /**
107     * Print separation line.
108     */
109    public function printSeparator(): void
110    {
111        echo str_repeat('-', 68) . "\n";
112    }
113
114    /**
115     * Print all logs.
116     */
117    public function printLogs(): void
118    {
119        $hasMultipleDescriptors = count($this->sources) > 1;
120        echo "LOGS:\n";
121        foreach ($this->sources as $name => $source) {
122            if ($hasMultipleDescriptors) {
123                echo ">>> source: $name\n";
124            }
125            $this->printSeparator();
126            foreach ($source->getAllLines() as $line) {
127                echo $line;
128            }
129            $this->printSeparator();
130        }
131    }
132
133    /**
134     * Print error and logs.
135     *
136     * @param string|null $errorMessage Error message to print before the logs.
137     *
138     * @return false
139     */
140    public function printError(?string $errorMessage): bool
141    {
142        if (is_null($errorMessage)) {
143            return false;
144        }
145        echo "ERROR: " . $errorMessage . "\n\n";
146        $this->printLogs();
147        echo "\n";
148
149        return false;
150    }
151
152    /**
153     * Read log until matcher matches the log message or there are no more logs.
154     *
155     * @param callable    $matcher             Callback to identify a match
156     * @param string|null $notFoundMessage     Error message if matcher does not succeed.
157     * @param bool        $checkAllLogs        Whether to also check past logs.
158     * @param int|null    $timeoutSeconds      Timeout in seconds for reading of all messages.
159     * @param int|null    $timeoutMicroseconds Additional timeout in microseconds for reading of all messages.
160     *
161     * @return bool
162     * @throws \Exception
163     */
164    public function readUntil(
165        callable $matcher,
166        string $notFoundMessage = null,
167        bool $checkAllLogs = false,
168        int $timeoutSeconds = null,
169        int $timeoutMicroseconds = null
170    ): bool {
171        if (is_null($timeoutSeconds) && is_null($timeoutMicroseconds)) {
172            $timeoutSeconds      = 3;
173            $timeoutMicroseconds = 0;
174        } elseif (is_null($timeoutSeconds)) {
175            $timeoutSeconds = 0;
176        } elseif (is_null($timeoutMicroseconds)) {
177            $timeoutMicroseconds = 0;
178        }
179
180        $startTime = microtime(true);
181        $endTime   = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
182        if ($checkAllLogs) {
183            foreach ($this->getSource()->getAllLines() as $line) {
184                if ($matcher($line)) {
185                    return true;
186                }
187            }
188        }
189
190        do {
191            if (microtime(true) > $endTime) {
192                return $this->printError($notFoundMessage);
193            }
194            $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds);
195            if ($line === null || microtime(true) > $endTime) {
196                return $this->printError($notFoundMessage);
197            }
198        } while ( ! $matcher($line));
199
200        return true;
201    }
202
203    /**
204     * Print tracing message - only in debug .
205     *
206     * @param string $msg Message to print.
207     */
208    private function trace(string $msg): void
209    {
210        if ($this->debug) {
211            print "LogReader - $msg";
212        }
213    }
214}
215
216class LogTimoutException extends \Exception
217{
218}
219
220abstract class LogSource
221{
222    /**
223     * Get single line from the source.
224     *
225     * @param int  $timeoutSeconds      Read timeout in seconds
226     * @param int  $timeoutMicroseconds Read timeout in microseconds
227     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
228     *
229     * @return string|null
230     * @throws LogTimoutException
231     */
232    public abstract function getLine(
233        int $timeoutSeconds,
234        int $timeoutMicroseconds,
235        bool $throwOnTimeout = false
236    ): ?string;
237
238    /**
239     * Get all lines that has been returned by getLine() method.
240     *
241     * @return string[]
242     */
243    public abstract function getAllLines(): array;
244}
245
246class LogStreamSource extends LogSource
247{
248    /**
249     * @var resource
250     */
251    private $stream;
252
253    /**
254     * @var array
255     */
256    private array $lines = [];
257
258    public function __construct($stream)
259    {
260        $this->stream = $stream;
261    }
262
263    /**
264     * Get single line from the stream.
265     *
266     * @param int  $timeoutSeconds      Read timeout in seconds
267     * @param int  $timeoutMicroseconds Read timeout in microseconds
268     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
269     *
270     * @return string|null
271     * @throws LogTimoutException
272     */
273    public function getLine(
274        int $timeoutSeconds,
275        int $timeoutMicroseconds,
276        bool $throwOnTimeout = false
277    ): ?string {
278        if (feof($this->stream)) {
279            return null;
280        }
281        $read   = [$this->stream];
282        $write  = null;
283        $except = null;
284        if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) {
285            $line          = fgets($this->stream);
286            $this->lines[] = $line;
287
288            return $line;
289        } else {
290            if ($throwOnTimeout) {
291                throw new LogTimoutException('Timout exceeded when reading line');
292            }
293
294            return null;
295        }
296    }
297
298    /**
299     * Get all stream read lines.
300     *
301     * @return string[]
302     */
303    public function getAllLines(): array
304    {
305        return $this->lines;
306    }
307}
308
309class LogFileSource extends LogSource
310{
311    /**
312     * @var string
313     */
314    private string $filePath;
315
316    /**
317     * @var int
318     */
319    private int $position;
320
321    /**
322     * @var array
323     */
324    private array $lines = [];
325
326    public function __construct(string $filePath)
327    {
328        $this->filePath = $filePath;
329        $this->position = 0;
330    }
331
332    /**
333     * Get single line from the file.
334     *
335     * @param int  $timeoutSeconds      Read timeout in seconds
336     * @param int  $timeoutMicroseconds Read timeout in microseconds
337     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
338     *
339     * @return string|null
340     * @throws LogTimoutException
341     */
342    public function getLine(
343        int $timeoutSeconds,
344        int $timeoutMicroseconds,
345        bool $throwOnTimeout = false
346    ): ?string {
347        $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
348        while ($this->position >= count($this->lines)) {
349            if (is_file($this->filePath)) {
350                $lines = file($this->filePath);
351                if ($lines === false) {
352                    return null;
353                }
354                $this->lines = $lines;
355                if ($this->position < count($lines)) {
356                    break;
357                }
358            }
359            usleep(50_000);
360            if (microtime(true) > $endTime) {
361                if ($throwOnTimeout) {
362                    throw new LogTimoutException('Timout exceeded when reading line');
363                }
364
365                return null;
366            }
367        }
368
369        return $this->lines[$this->position++];
370    }
371
372    /**
373     * Get all returned lines from the file.
374     *
375     * @return string[]
376     */
377    public function getAllLines(): array
378    {
379        return array_slice($this->lines, 0, $this->position);
380    }
381}
382