xref: /PHP-8.0/sapi/fpm/tests/logreader.inc (revision 1c5844aa)
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    private 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         $timeoutSeconds      Timeout in seconds for reading of all messages.
159     * @param int         $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 = 3,
169        int $timeoutMicroseconds = 0
170    ): bool {
171        $startTime = microtime(true);
172        $endTime   = $startTime + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
173        if ($checkAllLogs) {
174            foreach ($this->getSource()->getAllLines() as $line) {
175                if ($matcher($line)) {
176                    return true;
177                }
178            }
179        }
180
181        do {
182            if (microtime(true) > $endTime) {
183                return $this->printError($notFoundMessage);
184            }
185            $line = $this->getLine($timeoutSeconds, $timeoutMicroseconds);
186            if ($line === null || microtime(true) > $endTime) {
187                return $this->printError($notFoundMessage);
188            }
189        } while ( ! $matcher($line));
190
191        return true;
192    }
193
194    /**
195     * Print tracing message - only in debug .
196     *
197     * @param string $msg Message to print.
198     */
199    private function trace(string $msg): void
200    {
201        if ($this->debug) {
202            print "LogReader - $msg";
203        }
204    }
205}
206
207class LogTimoutException extends \Exception
208{
209}
210
211abstract class LogSource
212{
213    /**
214     * Get single line from the source.
215     *
216     * @param int  $timeoutSeconds      Read timeout in seconds
217     * @param int  $timeoutMicroseconds Read timeout in microseconds
218     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
219     *
220     * @return string|null
221     * @throws LogTimoutException
222     */
223    public abstract function getLine(
224        int $timeoutSeconds,
225        int $timeoutMicroseconds,
226        bool $throwOnTimeout = false
227    ): ?string;
228
229    /**
230     * Get all lines that has been returned by getLine() method.
231     *
232     * @return string[]
233     */
234    public abstract function getAllLines(): array;
235}
236
237class LogStreamSource extends LogSource
238{
239    /**
240     * @var resource
241     */
242    private $stream;
243
244    /**
245     * @var array
246     */
247    private array $lines = [];
248
249    public function __construct($stream)
250    {
251        $this->stream = $stream;
252    }
253
254    /**
255     * Get single line from the stream.
256     *
257     * @param int  $timeoutSeconds      Read timeout in seconds
258     * @param int  $timeoutMicroseconds Read timeout in microseconds
259     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
260     *
261     * @return string|null
262     * @throws LogTimoutException
263     */
264    public function getLine(
265        int $timeoutSeconds,
266        int $timeoutMicroseconds,
267        bool $throwOnTimeout = false
268    ): ?string {
269        if (feof($this->stream)) {
270            return null;
271        }
272        $read   = [$this->stream];
273        $write  = null;
274        $except = null;
275        if (stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds)) {
276            $line          = fgets($this->stream);
277            $this->lines[] = $line;
278
279            return $line;
280        } else {
281            if ($throwOnTimeout) {
282                throw new LogTimoutException('Timout exceeded when reading line');
283            }
284
285            return null;
286        }
287    }
288
289    /**
290     * Get all stream read lines.
291     *
292     * @return string[]
293     */
294    public function getAllLines(): array
295    {
296        return $this->lines;
297    }
298}
299
300class LogFileSource extends LogSource
301{
302    /**
303     * @var string
304     */
305    private string $filePath;
306
307    /**
308     * @var int
309     */
310    private int $position;
311
312    /**
313     * @var array
314     */
315    private array $lines = [];
316
317    public function __construct(string $filePath)
318    {
319        $this->filePath = $filePath;
320        $this->position = 0;
321    }
322
323    /**
324     * Get single line from the file.
325     *
326     * @param int  $timeoutSeconds      Read timeout in seconds
327     * @param int  $timeoutMicroseconds Read timeout in microseconds
328     * @param bool $throwOnTimeout      Whether to throw an exception on timeout
329     *
330     * @return string|null
331     * @throws LogTimoutException
332     */
333    public function getLine(
334        int $timeoutSeconds,
335        int $timeoutMicroseconds,
336        bool $throwOnTimeout = false
337    ): ?string {
338        $endTime = microtime(true) + $timeoutSeconds + ($timeoutMicroseconds / 1_000_000);
339        while ($this->position >= count($this->lines)) {
340            if (is_file($this->filePath)) {
341                $lines = file($this->filePath);
342                if ($lines === false) {
343                    return null;
344                }
345                $this->lines = $lines;
346                if ($this->position < count($lines)) {
347                    break;
348                }
349            }
350            usleep(50_000);
351            if (microtime(true) > $endTime) {
352                if ($throwOnTimeout) {
353                    throw new LogTimoutException('Timout exceeded when reading line');
354                }
355
356                return null;
357            }
358        }
359
360        return $this->lines[$this->position++];
361    }
362
363    /**
364     * Get all returned lines from the file.
365     *
366     * @return string[]
367     */
368    public function getAllLines(): array
369    {
370        return array_slice($this->lines, 0, $this->position);
371    }
372}
373