Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php declare(strict_types=1);
2
 
3
/*
4
 * This file is part of the Monolog package.
5
 *
6
 * (c) Jordi Boggiano <j.boggiano@seld.be>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Monolog\Handler;
13
 
14
use Monolog\Logger;
15
 
16
/**
17
 * Stores to any socket - uses fsockopen() or pfsockopen().
18
 *
19
 * @author Pablo de Leon Belloc <pablolb@gmail.com>
20
 * @see    http://php.net/manual/en/function.fsockopen.php
21
 *
22
 * @phpstan-import-type Record from \Monolog\Logger
23
 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
24
 */
25
class SocketHandler extends AbstractProcessingHandler
26
{
27
    /** @var string */
28
    private $connectionString;
29
    /** @var float */
30
    private $connectionTimeout;
31
    /** @var resource|null */
32
    private $resource;
33
    /** @var float */
34
    private $timeout;
35
    /** @var float */
36
    private $writingTimeout;
37
    /** @var ?int */
38
    private $lastSentBytes = null;
39
    /** @var ?int */
40
    private $chunkSize;
41
    /** @var bool */
42
    private $persistent;
43
    /** @var ?int */
44
    private $errno = null;
45
    /** @var ?string */
46
    private $errstr = null;
47
    /** @var ?float */
48
    private $lastWritingAt = null;
49
 
50
    /**
51
     * @param string     $connectionString  Socket connection string
52
     * @param bool       $persistent        Flag to enable/disable persistent connections
53
     * @param float      $timeout           Socket timeout to wait until the request is being aborted
54
     * @param float      $writingTimeout    Socket timeout to wait until the request should've been sent/written
55
     * @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been
56
     *                                      established
57
     * @param int|null   $chunkSize         Sets the chunk size. Only has effect during connection in the writing cycle
58
     *
59
     * @throws \InvalidArgumentException    If an invalid timeout value (less than 0) is passed.
60
     */
61
    public function __construct(
62
        string $connectionString,
63
        $level = Logger::DEBUG,
64
        bool $bubble = true,
65
        bool $persistent = false,
66
        float $timeout = 0.0,
67
        float $writingTimeout = 10.0,
68
        ?float $connectionTimeout = null,
69
        ?int $chunkSize = null
70
    ) {
71
        parent::__construct($level, $bubble);
72
        $this->connectionString = $connectionString;
73
 
74
        if ($connectionTimeout !== null) {
75
            $this->validateTimeout($connectionTimeout);
76
        }
77
 
78
        $this->connectionTimeout = $connectionTimeout ?? (float) ini_get('default_socket_timeout');
79
        $this->persistent = $persistent;
80
        $this->validateTimeout($timeout);
81
        $this->timeout = $timeout;
82
        $this->validateTimeout($writingTimeout);
83
        $this->writingTimeout = $writingTimeout;
84
        $this->chunkSize = $chunkSize;
85
    }
86
 
87
    /**
88
     * Connect (if necessary) and write to the socket
89
     *
90
     * {@inheritDoc}
91
     *
92
     * @throws \UnexpectedValueException
93
     * @throws \RuntimeException
94
     */
95
    protected function write(array $record): void
96
    {
97
        $this->connectIfNotConnected();
98
        $data = $this->generateDataStream($record);
99
        $this->writeToSocket($data);
100
    }
101
 
102
    /**
103
     * We will not close a PersistentSocket instance so it can be reused in other requests.
104
     */
105
    public function close(): void
106
    {
107
        if (!$this->isPersistent()) {
108
            $this->closeSocket();
109
        }
110
    }
111
 
112
    /**
113
     * Close socket, if open
114
     */
115
    public function closeSocket(): void
116
    {
117
        if (is_resource($this->resource)) {
118
            fclose($this->resource);
119
            $this->resource = null;
120
        }
121
    }
122
 
123
    /**
124
     * Set socket connection to be persistent. It only has effect before the connection is initiated.
125
     */
126
    public function setPersistent(bool $persistent): self
127
    {
128
        $this->persistent = $persistent;
129
 
130
        return $this;
131
    }
132
 
133
    /**
134
     * Set connection timeout.  Only has effect before we connect.
135
     *
136
     * @see http://php.net/manual/en/function.fsockopen.php
137
     */
138
    public function setConnectionTimeout(float $seconds): self
139
    {
140
        $this->validateTimeout($seconds);
141
        $this->connectionTimeout = $seconds;
142
 
143
        return $this;
144
    }
145
 
146
    /**
147
     * Set write timeout. Only has effect before we connect.
148
     *
149
     * @see http://php.net/manual/en/function.stream-set-timeout.php
150
     */
151
    public function setTimeout(float $seconds): self
152
    {
153
        $this->validateTimeout($seconds);
154
        $this->timeout = $seconds;
155
 
156
        return $this;
157
    }
158
 
159
    /**
160
     * Set writing timeout. Only has effect during connection in the writing cycle.
161
     *
162
     * @param float $seconds 0 for no timeout
163
     */
164
    public function setWritingTimeout(float $seconds): self
165
    {
166
        $this->validateTimeout($seconds);
167
        $this->writingTimeout = $seconds;
168
 
169
        return $this;
170
    }
171
 
172
    /**
173
     * Set chunk size. Only has effect during connection in the writing cycle.
174
     */
175
    public function setChunkSize(int $bytes): self
176
    {
177
        $this->chunkSize = $bytes;
178
 
179
        return $this;
180
    }
181
 
182
    /**
183
     * Get current connection string
184
     */
185
    public function getConnectionString(): string
186
    {
187
        return $this->connectionString;
188
    }
189
 
190
    /**
191
     * Get persistent setting
192
     */
193
    public function isPersistent(): bool
194
    {
195
        return $this->persistent;
196
    }
197
 
198
    /**
199
     * Get current connection timeout setting
200
     */
201
    public function getConnectionTimeout(): float
202
    {
203
        return $this->connectionTimeout;
204
    }
205
 
206
    /**
207
     * Get current in-transfer timeout
208
     */
209
    public function getTimeout(): float
210
    {
211
        return $this->timeout;
212
    }
213
 
214
    /**
215
     * Get current local writing timeout
216
     *
217
     * @return float
218
     */
219
    public function getWritingTimeout(): float
220
    {
221
        return $this->writingTimeout;
222
    }
223
 
224
    /**
225
     * Get current chunk size
226
     */
227
    public function getChunkSize(): ?int
228
    {
229
        return $this->chunkSize;
230
    }
231
 
232
    /**
233
     * Check to see if the socket is currently available.
234
     *
235
     * UDP might appear to be connected but might fail when writing.  See http://php.net/fsockopen for details.
236
     */
237
    public function isConnected(): bool
238
    {
239
        return is_resource($this->resource)
240
            && !feof($this->resource);  // on TCP - other party can close connection.
241
    }
242
 
243
    /**
244
     * Wrapper to allow mocking
245
     *
246
     * @return resource|false
247
     */
248
    protected function pfsockopen()
249
    {
250
        return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
251
    }
252
 
253
    /**
254
     * Wrapper to allow mocking
255
     *
256
     * @return resource|false
257
     */
258
    protected function fsockopen()
259
    {
260
        return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
261
    }
262
 
263
    /**
264
     * Wrapper to allow mocking
265
     *
266
     * @see http://php.net/manual/en/function.stream-set-timeout.php
267
     *
268
     * @return bool
269
     */
270
    protected function streamSetTimeout()
271
    {
272
        $seconds = floor($this->timeout);
273
        $microseconds = round(($this->timeout - $seconds) * 1e6);
274
 
275
        if (!is_resource($this->resource)) {
276
            throw new \LogicException('streamSetTimeout called but $this->resource is not a resource');
277
        }
278
 
279
        return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds);
280
    }
281
 
282
    /**
283
     * Wrapper to allow mocking
284
     *
285
     * @see http://php.net/manual/en/function.stream-set-chunk-size.php
286
     *
287
     * @return int|bool
288
     */
289
    protected function streamSetChunkSize()
290
    {
291
        if (!is_resource($this->resource)) {
292
            throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource');
293
        }
294
 
295
        if (null === $this->chunkSize) {
296
            throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set');
297
        }
298
 
299
        return stream_set_chunk_size($this->resource, $this->chunkSize);
300
    }
301
 
302
    /**
303
     * Wrapper to allow mocking
304
     *
305
     * @return int|bool
306
     */
307
    protected function fwrite(string $data)
308
    {
309
        if (!is_resource($this->resource)) {
310
            throw new \LogicException('fwrite called but $this->resource is not a resource');
311
        }
312
 
313
        return @fwrite($this->resource, $data);
314
    }
315
 
316
    /**
317
     * Wrapper to allow mocking
318
     *
319
     * @return mixed[]|bool
320
     */
321
    protected function streamGetMetadata()
322
    {
323
        if (!is_resource($this->resource)) {
324
            throw new \LogicException('streamGetMetadata called but $this->resource is not a resource');
325
        }
326
 
327
        return stream_get_meta_data($this->resource);
328
    }
329
 
330
    private function validateTimeout(float $value): void
331
    {
332
        if ($value < 0) {
333
            throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
334
        }
335
    }
336
 
337
    private function connectIfNotConnected(): void
338
    {
339
        if ($this->isConnected()) {
340
            return;
341
        }
342
        $this->connect();
343
    }
344
 
345
    /**
346
     * @phpstan-param FormattedRecord $record
347
     */
348
    protected function generateDataStream(array $record): string
349
    {
350
        return (string) $record['formatted'];
351
    }
352
 
353
    /**
354
     * @return resource|null
355
     */
356
    protected function getResource()
357
    {
358
        return $this->resource;
359
    }
360
 
361
    private function connect(): void
362
    {
363
        $this->createSocketResource();
364
        $this->setSocketTimeout();
365
        $this->setStreamChunkSize();
366
    }
367
 
368
    private function createSocketResource(): void
369
    {
370
        if ($this->isPersistent()) {
371
            $resource = $this->pfsockopen();
372
        } else {
373
            $resource = $this->fsockopen();
374
        }
375
        if (is_bool($resource)) {
376
            throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
377
        }
378
        $this->resource = $resource;
379
    }
380
 
381
    private function setSocketTimeout(): void
382
    {
383
        if (!$this->streamSetTimeout()) {
384
            throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
385
        }
386
    }
387
 
388
    private function setStreamChunkSize(): void
389
    {
390
        if ($this->chunkSize && !$this->streamSetChunkSize()) {
391
            throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()");
392
        }
393
    }
394
 
395
    private function writeToSocket(string $data): void
396
    {
397
        $length = strlen($data);
398
        $sent = 0;
399
        $this->lastSentBytes = $sent;
400
        while ($this->isConnected() && $sent < $length) {
401
            if (0 == $sent) {
402
                $chunk = $this->fwrite($data);
403
            } else {
404
                $chunk = $this->fwrite(substr($data, $sent));
405
            }
406
            if ($chunk === false) {
407
                throw new \RuntimeException("Could not write to socket");
408
            }
409
            $sent += $chunk;
410
            $socketInfo = $this->streamGetMetadata();
411
            if (is_array($socketInfo) && $socketInfo['timed_out']) {
412
                throw new \RuntimeException("Write timed-out");
413
            }
414
 
415
            if ($this->writingIsTimedOut($sent)) {
416
                throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)");
417
            }
418
        }
419
        if (!$this->isConnected() && $sent < $length) {
420
            throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)");
421
        }
422
    }
423
 
424
    private function writingIsTimedOut(int $sent): bool
425
    {
426
        // convert to ms
427
        if (0.0 == $this->writingTimeout) {
428
            return false;
429
        }
430
 
431
        if ($sent !== $this->lastSentBytes) {
432
            $this->lastWritingAt = microtime(true);
433
            $this->lastSentBytes = $sent;
434
 
435
            return false;
436
        } else {
437
            usleep(100);
438
        }
439
 
440
        if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) {
441
            $this->closeSocket();
442
 
443
            return true;
444
        }
445
 
446
        return false;
447
    }
448
}