Completed
Push — master ( cdeef2...f03c61 )
by Justin
03:14
created

SocketHandler::getWritingTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Monolog package.
5
 *
6
 * (c) Jordi Boggiano <[email protected]>
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 <[email protected]>
20
 * @see    http://php.net/manual/en/function.fsockopen.php
21
 */
22
class SocketHandler extends AbstractProcessingHandler
23
{
24
    private $connectionString;
25
    private $connectionTimeout;
26
    private $resource;
27
    private $timeout = 0;
28
    private $writingTimeout = 10;
29
    private $lastSentBytes = null;
30
    private $persistent = false;
31
    private $errno;
32
    private $errstr;
33
    private $lastWritingAt;
34
35
    /**
36
     * @param string  $connectionString Socket connection string
37
     * @param int     $level            The minimum logging level at which this handler will be triggered
38
     * @param Boolean $bubble           Whether the messages that are handled can bubble up the stack or not
39
     */
40
    public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true)
41
    {
42
        parent::__construct($level, $bubble);
43
        $this->connectionString = $connectionString;
44
        $this->connectionTimeout = (float) ini_get('default_socket_timeout');
45
    }
46
47
    /**
48
     * Connect (if necessary) and write to the socket
49
     *
50
     * @param array $record
51
     *
52
     * @throws \UnexpectedValueException
53
     * @throws \RuntimeException
54
     */
55
    protected function write(array $record)
56
    {
57
        $this->connectIfNotConnected();
58
        $data = $this->generateDataStream($record);
59
        $this->writeToSocket($data);
60
    }
61
62
    /**
63
     * We will not close a PersistentSocket instance so it can be reused in other requests.
64
     */
65
    public function close()
66
    {
67
        if (!$this->isPersistent()) {
68
            $this->closeSocket();
69
        }
70
    }
71
72
    /**
73
     * Close socket, if open
74
     */
75
    public function closeSocket()
76
    {
77
        if (is_resource($this->resource)) {
78
            fclose($this->resource);
79
            $this->resource = null;
80
        }
81
    }
82
83
    /**
84
     * Set socket connection to nbe persistent. It only has effect before the connection is initiated.
85
     *
86
     * @param bool $persistent
87
     */
88
    public function setPersistent($persistent)
89
    {
90
        $this->persistent = (boolean) $persistent;
91
    }
92
93
    /**
94
     * Set connection timeout.  Only has effect before we connect.
95
     *
96
     * @param float $seconds
97
     *
98
     * @see http://php.net/manual/en/function.fsockopen.php
99
     */
100
    public function setConnectionTimeout($seconds)
101
    {
102
        $this->validateTimeout($seconds);
103
        $this->connectionTimeout = (float) $seconds;
104
    }
105
106
    /**
107
     * Set write timeout. Only has effect before we connect.
108
     *
109
     * @param float $seconds
110
     *
111
     * @see http://php.net/manual/en/function.stream-set-timeout.php
112
     */
113
    public function setTimeout($seconds)
114
    {
115
        $this->validateTimeout($seconds);
116
        $this->timeout = (float) $seconds;
117
    }
118
119
    /**
120
     * Set writing timeout. Only has effect during connection in the writing cycle.
121
     *
122
     * @param float $seconds 0 for no timeout
123
     */
124
    public function setWritingTimeout($seconds)
125
    {
126
        $this->validateTimeout($seconds);
127
        $this->writingTimeout = (float) $seconds;
128
    }
129
130
    /**
131
     * Get current connection string
132
     *
133
     * @return string
134
     */
135
    public function getConnectionString()
136
    {
137
        return $this->connectionString;
138
    }
139
140
    /**
141
     * Get persistent setting
142
     *
143
     * @return bool
144
     */
145
    public function isPersistent()
146
    {
147
        return $this->persistent;
148
    }
149
150
    /**
151
     * Get current connection timeout setting
152
     *
153
     * @return float
154
     */
155
    public function getConnectionTimeout()
156
    {
157
        return $this->connectionTimeout;
158
    }
159
160
    /**
161
     * Get current in-transfer timeout
162
     *
163
     * @return float
164
     */
165
    public function getTimeout()
166
    {
167
        return $this->timeout;
168
    }
169
170
    /**
171
     * Get current local writing timeout
172
     *
173
     * @return float
174
     */
175
    public function getWritingTimeout()
176
    {
177
        return $this->writingTimeout;
178
    }
179
180
    /**
181
     * Check to see if the socket is currently available.
182
     *
183
     * UDP might appear to be connected but might fail when writing.  See http://php.net/fsockopen for details.
184
     *
185
     * @return bool
186
     */
187
    public function isConnected()
188
    {
189
        return is_resource($this->resource)
190
            && !feof($this->resource);  // on TCP - other party can close connection.
191
    }
192
193
    /**
194
     * Wrapper to allow mocking
195
     */
196
    protected function pfsockopen()
197
    {
198
        return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
199
    }
200
201
    /**
202
     * Wrapper to allow mocking
203
     */
204
    protected function fsockopen()
205
    {
206
        return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
207
    }
208
209
    /**
210
     * Wrapper to allow mocking
211
     *
212
     * @see http://php.net/manual/en/function.stream-set-timeout.php
213
     */
214
    protected function streamSetTimeout()
215
    {
216
        $seconds = floor($this->timeout);
217
        $microseconds = round(($this->timeout - $seconds) * 1e6);
218
219
        return stream_set_timeout($this->resource, $seconds, $microseconds);
0 ignored issues
show
Bug introduced by
$seconds of type double is incompatible with the type integer expected by parameter $seconds of stream_set_timeout(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

219
        return stream_set_timeout($this->resource, /** @scrutinizer ignore-type */ $seconds, $microseconds);
Loading history...
Bug introduced by
$microseconds of type double is incompatible with the type integer expected by parameter $microseconds of stream_set_timeout(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

219
        return stream_set_timeout($this->resource, $seconds, /** @scrutinizer ignore-type */ $microseconds);
Loading history...
220
    }
221
222
    /**
223
     * Wrapper to allow mocking
224
     */
225
    protected function fwrite($data)
226
    {
227
        return @fwrite($this->resource, $data);
228
    }
229
230
    /**
231
     * Wrapper to allow mocking
232
     */
233
    protected function streamGetMetadata()
234
    {
235
        return stream_get_meta_data($this->resource);
236
    }
237
238
    private function validateTimeout($value)
239
    {
240
        $ok = filter_var($value, FILTER_VALIDATE_FLOAT);
241
        if ($ok === false || $value < 0) {
242
            throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
243
        }
244
    }
245
246
    private function connectIfNotConnected()
247
    {
248
        if ($this->isConnected()) {
249
            return;
250
        }
251
        $this->connect();
252
    }
253
254
    protected function generateDataStream($record)
255
    {
256
        return (string) $record['formatted'];
257
    }
258
259
    /**
260
     * @return resource|null
261
     */
262
    protected function getResource()
263
    {
264
        return $this->resource;
265
    }
266
267
    private function connect()
268
    {
269
        $this->createSocketResource();
270
        $this->setSocketTimeout();
271
    }
272
273
    private function createSocketResource()
274
    {
275
        if ($this->isPersistent()) {
276
            $resource = $this->pfsockopen();
277
        } else {
278
            $resource = $this->fsockopen();
279
        }
280
        if (!$resource) {
0 ignored issues
show
introduced by
$resource is of type resource, thus it always evaluated to false.
Loading history...
281
            throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
282
        }
283
        $this->resource = $resource;
284
    }
285
286
    private function setSocketTimeout()
287
    {
288
        if (!$this->streamSetTimeout()) {
289
            throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
290
        }
291
    }
292
293
    private function writeToSocket($data)
294
    {
295
        $length = strlen($data);
296
        $sent = 0;
297
        $this->lastSentBytes = $sent;
298
        while ($this->isConnected() && $sent < $length) {
299
            if (0 == $sent) {
300
                $chunk = $this->fwrite($data);
301
            } else {
302
                $chunk = $this->fwrite(substr($data, $sent));
303
            }
304
            if ($chunk === false) {
305
                throw new \RuntimeException("Could not write to socket");
306
            }
307
            $sent += $chunk;
308
            $socketInfo = $this->streamGetMetadata();
309
            if ($socketInfo['timed_out']) {
310
                throw new \RuntimeException("Write timed-out");
311
            }
312
313
            if ($this->writingIsTimedOut($sent)) {
314
                throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)");
315
            }
316
        }
317
        if (!$this->isConnected() && $sent < $length) {
318
            throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)");
319
        }
320
    }
321
322
    private function writingIsTimedOut($sent)
323
    {
324
        $writingTimeout = (int) floor($this->writingTimeout);
325
        if (0 === $writingTimeout) {
326
            return false;
327
        }
328
329
        if ($sent !== $this->lastSentBytes) {
330
            $this->lastWritingAt = time();
331
            $this->lastSentBytes = $sent;
332
333
            return false;
334
        } else {
335
            usleep(100);
336
        }
337
338
        if ((time() - $this->lastWritingAt) >= $writingTimeout) {
339
            $this->closeSocket();
340
341
            return true;
342
        }
343
344
        return false;
345
    }
346
}
347