xref: /PHP-8.4/sapi/fpm/tests/logreader.inc (revision 330cc5cd)
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 = null;
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        if (empty($this->sources)) {
120            return;
121        }
122        $hasMultipleDescriptors = count($this->sources) > 1;
123        echo "\nLOGS:\n";
124        foreach ($this->sources as $name => $source) {
125            if ($hasMultipleDescriptors) {
126                echo ">>> source: $name\n";
127            }
128            $this->printSeparator();
129            foreach ($source->getAllLines() as $line) {
130                echo $line;
131            }
132            $this->printSeparator();
133        }
134        echo "\n";
135    }
136
137    /**
138     * Print error and logs.
139     *
140     * @param string|null $errorMessage Error message to print before the logs.
141     *
142     * @return false
143     */
144    public function printError(?string $errorMessage): bool
145    {
146        if (is_null($errorMessage)) {
147            return false;
148        }
149        echo "ERROR: " . $errorMessage . "\n";
150        $this->printLogs();
151
152        return false;
153    }
154
155    /**
156     * Read log until matcher matches the log message or there are no more logs.
157     *
158     * @param callable    $matcher             Callback to identify a match
159     * @param string|null $notFoundMessage     Error message if matcher does not succeed.
160     * @param bool        $checkAllLogs        Whether to also check past logs.
161     * @param int|null    $timeoutSeconds      Timeout in seconds for reading of all messages.
162     * @param int|null    $timeoutMicroseconds Additional timeout in microseconds for reading of all messages.
163     *
164     * @return bool
165     * @throws \Exception
166     */
167    public function readUntil(
168        callable $matcher,
169        ?string $notFoundMessage = null,
170        bool $checkAllLogs = false,
171        ?int $timeoutSeconds = null,
172        ?int $timeoutMicroseconds = null
173    ): bool {
174        if (is_null($timeoutSeconds) && is_null($timeoutMicroseconds)) {
175            $timeoutSeconds      = 3;
176            $timeoutMicroseconds = 0;
177        } elseif (is_null($timeoutSeconds)) {
178            $timeoutSeconds = 0;
179        } elseif (is_null($timeoutMicroseconds)) {
180            $timeoutMicroseconds = 0;
181        }
182
183        $startTime = microtime(true);
184        $endTime   = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
185        if ($checkAllLogs) {
186            foreach ($this->getSource()->getAllLines() as $line) {
187                if ($matcher($line)) {
188                    return true;
189                }
190            }
191        }
192
193        do {
194            if (microtime(true) > $endTime) {
195                return $this->printError($notFoundMessage);
196            }
197            $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds);
198            if ($line === null || microtime(true) > $endTime) {
199                return $this->printError($notFoundMessage);
200            }
201        } while ( ! $matcher($line));
202
203        return true;
204    }
205
206    /**
207     * Print tracing message - only in debug .
208     *
209     * @param string $msg Message to print.
210     */
211    private function trace(string $msg): void
212    {
213        if ($this->debug) {
214            print "LogReader - $msg";
215        }
216    }
217}
218
219class LogTimoutException extends \Exception
220{
221}
222
223abstract class LogSource
224{
225    /**
226     * Get single line from the source.
227     *
228     * @param int  $timeoutSeconds      Read timeout in seconds
229     * @param int  $timeoutMicroseconds Read timeout in microseconds
230     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
231     *
232     * @return string|null
233     * @throws LogTimoutException
234     */
235    public abstract function getLine(
236        int $timeoutSeconds,
237        int $timeoutMicroseconds,
238        bool $throwOnTimeout = false
239    ): ?string;
240
241    /**
242     * Get all lines that has been returned by getLine() method.
243     *
244     * @return string[]
245     */
246    public abstract function getAllLines(): array;
247}
248
249class LogStreamSource extends LogSource
250{
251    /**
252     * @var resource
253     */
254    private $stream;
255
256    /**
257     * @var array
258     */
259    private array $lines = [];
260
261    public function __construct($stream)
262    {
263        $this->stream = $stream;
264    }
265
266    /**
267     * Get single line from the stream.
268     *
269     * @param int  $timeoutSeconds      Read timeout in seconds
270     * @param int  $timeoutMicroseconds Read timeout in microseconds
271     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
272     *
273     * @return string|null
274     * @throws LogTimoutException
275     */
276    public function getLine(
277        int $timeoutSeconds,
278        int $timeoutMicroseconds,
279        bool $throwOnTimeout = false
280    ): ?string {
281        if (feof($this->stream)) {
282            return null;
283        }
284        $read   = [$this->stream];
285        $write  = null;
286        $except = null;
287        if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) {
288            $line          = fgets($this->stream);
289            $this->lines[] = $line;
290
291            return $line;
292        } else {
293            if ($throwOnTimeout) {
294                throw new LogTimoutException('Timout exceeded when reading line');
295            }
296
297            return null;
298        }
299    }
300
301    /**
302     * Get all stream read lines.
303     *
304     * @return string[]
305     */
306    public function getAllLines(): array
307    {
308        return $this->lines;
309    }
310}
311
312class LogFileSource extends LogSource
313{
314    /**
315     * @var string
316     */
317    private string $filePath;
318
319    /**
320     * @var int
321     */
322    private int $position;
323
324    /**
325     * @var array
326     */
327    private array $lines = [];
328
329    public function __construct(string $filePath)
330    {
331        $this->filePath = $filePath;
332        $this->position = 0;
333    }
334
335    /**
336     * Get single line from the file.
337     *
338     * @param int  $timeoutSeconds      Read timeout in seconds
339     * @param int  $timeoutMicroseconds Read timeout in microseconds
340     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
341     *
342     * @return string|null
343     * @throws LogTimoutException
344     */
345    public function getLine(
346        int $timeoutSeconds,
347        int $timeoutMicroseconds,
348        bool $throwOnTimeout = false
349    ): ?string {
350        $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
351        while ($this->position >= count($this->lines)) {
352            if (is_file($this->filePath)) {
353                $lines = file($this->filePath);
354                if ($lines === false) {
355                    return null;
356                }
357                $this->lines = $lines;
358                if ($this->position < count($lines)) {
359                    break;
360                }
361            }
362            usleep(50_000);
363            if (microtime(true) > $endTime) {
364                if ($throwOnTimeout) {
365                    throw new LogTimoutException('Timout exceeded when reading line');
366                }
367
368                return null;
369            }
370        }
371
372        return $this->lines[$this->position++];
373    }
374
375    /**
376     * Get all returned lines from the file.
377     *
378     * @return string[]
379     */
380    public function getAllLines(): array
381    {
382        return array_slice($this->lines, 0, $this->position);
383    }
384}
385