1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace RedirectionIO\Client\Sdk; |
4
|
|
|
|
5
|
|
|
use Psr\Log\LoggerInterface; |
6
|
|
|
use Psr\Log\NullLogger; |
7
|
|
|
use RedirectionIO\Client\Sdk\Command\LogCommand; |
8
|
|
|
use RedirectionIO\Client\Sdk\Command\MatchCommand; |
9
|
|
|
use RedirectionIO\Client\Sdk\Exception\AgentNotFoundException; |
10
|
|
|
use RedirectionIO\Client\Sdk\Exception\BadConfigurationException; |
11
|
|
|
use RedirectionIO\Client\Sdk\Exception\ExceptionInterface; |
12
|
|
|
use RedirectionIO\Client\Sdk\HttpMessage\Request; |
13
|
|
|
use RedirectionIO\Client\Sdk\HttpMessage\Response; |
14
|
|
|
|
15
|
|
|
class Client |
16
|
|
|
{ |
17
|
|
|
private $connections; |
18
|
|
|
private $timeout; |
19
|
|
|
private $debug; |
20
|
|
|
private $logger; |
21
|
|
|
private $currentConnection; |
22
|
|
|
private $currentConnectionName; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @param int $timeout |
26
|
13 |
|
* @param bool $debug |
27
|
|
|
*/ |
28
|
13 |
|
public function __construct(array $connections, $timeout = 10000, $debug = false, LoggerInterface $logger = null) |
29
|
1 |
|
{ |
30
|
|
|
if (!$connections) { |
|
|
|
|
31
|
|
|
throw new BadConfigurationException('At least one connection is required.'); |
32
|
13 |
|
} |
33
|
13 |
|
|
34
|
13 |
|
foreach ($connections as $name => $connection) { |
35
|
13 |
|
$this->connections[$name] = [ |
36
|
|
|
'remote_socket' => $connection, |
37
|
13 |
|
'retries' => 2, |
38
|
|
|
]; |
39
|
13 |
|
} |
40
|
13 |
|
|
41
|
13 |
|
$this->timeout = $timeout; |
42
|
13 |
|
$this->debug = $debug; |
43
|
|
|
$this->logger = $logger ?: new NullLogger(); |
44
|
9 |
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
9 |
|
* @deprecated findRedirect() is deprecated since version 0.2 and will be removed in 0.3. Use request(new MatchCommand()) instead. |
48
|
9 |
|
*/ |
49
|
9 |
|
public function findRedirect(Request $request) |
50
|
9 |
|
{ |
51
|
9 |
|
@trigger_error('findRedirect() is deprecated since version 0.2 and will be removed in 0.3. Use request(new MatchCommand()) instead.', E_USER_DEPRECATED); |
52
|
9 |
|
|
53
|
9 |
|
return $this->request(new MatchCommand($request)); |
54
|
|
|
} |
55
|
|
|
|
56
|
9 |
|
/** |
57
|
9 |
|
* @deprecated log() is deprecated since version 0.2 and will be removed in 0.3. Use request(new LogCommand()) instead. |
58
|
3 |
|
*/ |
59
|
1 |
|
public function log(Request $request, Response $response) |
60
|
|
|
{ |
61
|
|
|
@trigger_error('log() is deprecated since version 0.2 and will be removed in 0.3. Use request(new LogCommand()) instead.', E_USER_DEPRECATED); |
62
|
2 |
|
|
63
|
|
|
$this->request(new LogCommand($request, $response)); |
64
|
|
|
|
65
|
7 |
|
return true; |
66
|
1 |
|
} |
67
|
|
|
|
68
|
|
|
public function request(Command\CommandInterface $command) |
69
|
6 |
|
{ |
70
|
|
|
try { |
71
|
6 |
|
return $this->doRequest($command); |
72
|
|
|
} catch (ExceptionInterface $exception) { |
73
|
|
|
if ($this->debug) { |
74
|
|
|
throw $exception; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
return null; |
78
|
|
|
} |
79
|
6 |
|
} |
80
|
6 |
|
|
81
|
|
|
private function doRequest(Command\CommandInterface $command) |
82
|
6 |
|
{ |
83
|
|
|
$connection = $this->getConnection(); |
84
|
|
|
|
85
|
|
|
$toSend = $command->getName()."\0".$command->getRequest()."\0"; |
86
|
6 |
|
$sent = $this->box('doSend', false, [$connection, $toSend]); |
87
|
6 |
|
|
88
|
6 |
|
if (false === $sent) { |
89
|
|
|
$this->logger->debug('Impossible to send content to the connection.', [ |
90
|
6 |
|
'options' => $this->connections[$this->currentConnectionName], |
91
|
|
|
]); |
92
|
|
|
|
93
|
3 |
|
--$this->connections[$this->currentConnectionName]['retries']; |
94
|
|
|
$this->currentConnection = null; |
95
|
|
|
$this->box('disconnect', null, [$connection]); |
96
|
3 |
|
|
97
|
3 |
|
return $this->doRequest($command); |
98
|
3 |
|
} |
99
|
3 |
|
|
100
|
3 |
|
if (!$command->hasResponse()) { |
101
|
3 |
|
return null; |
102
|
3 |
|
} |
103
|
3 |
|
|
104
|
|
|
$received = $this->box('doGet', false, [$connection]); |
105
|
3 |
|
|
106
|
|
|
// false: the persistent connection is stale |
107
|
|
|
if (false === $received) { |
108
|
|
|
$this->logger->debug('Impossible to get content from the connection.', [ |
109
|
3 |
|
'options' => $this->connections[$this->currentConnectionName], |
110
|
|
|
]); |
111
|
|
|
|
112
|
|
|
--$this->connections[$this->currentConnectionName]['retries']; |
113
|
|
|
$this->currentConnection = null; |
114
|
3 |
|
$this->box('disconnect', null, [$connection]); |
115
|
2 |
|
|
116
|
2 |
|
return $this->doRequest($command); |
117
|
1 |
|
} |
118
|
1 |
|
|
119
|
|
|
return $command->parseResponse(trim($received)); |
120
|
1 |
|
} |
121
|
|
|
|
122
|
|
|
private function getConnection() |
123
|
|
|
{ |
124
|
12 |
|
if (null !== $this->currentConnection) { |
125
|
|
|
return $this->currentConnection; |
126
|
12 |
|
} |
127
|
|
|
|
128
|
8 |
|
foreach ($this->connections as $name => $connection) { |
129
|
8 |
|
if ($connection['retries'] <= 0) { |
130
|
|
|
continue; |
131
|
|
|
} |
132
|
8 |
|
|
133
|
|
|
$this->logger->debug('New connection chosen. Trying to connect.', [ |
134
|
|
|
'connection' => $connection, |
135
|
|
|
'name' => $name, |
136
|
|
|
]); |
137
|
|
|
|
138
|
|
|
$connection = $this->box('doConnect', false, [$connection]); |
139
|
|
|
|
140
|
|
|
if (false === $connection) { |
141
|
|
|
$this->logger->debug('Impossible to connect to the connection.', [ |
142
|
|
|
'connection' => $connection, |
143
|
8 |
|
'name' => $name, |
144
|
|
|
]); |
145
|
|
|
|
146
|
8 |
|
$this->connections[$name]['retries'] = 0; |
147
|
1 |
|
|
148
|
1 |
|
continue; |
149
|
1 |
|
} |
150
|
|
|
|
151
|
1 |
|
$this->logger->debug('New connection approved.', [ |
152
|
1 |
|
'connection' => $connection, |
153
|
|
|
'name' => $name, |
154
|
1 |
|
]); |
155
|
|
|
|
156
|
|
|
stream_set_timeout($connection, 0, $this->timeout); |
157
|
8 |
|
|
158
|
|
|
$this->currentConnection = $connection; |
159
|
|
|
$this->currentConnectionName = $name; |
160
|
12 |
|
|
161
|
|
|
return $connection; |
162
|
12 |
|
} |
163
|
2 |
|
|
164
|
|
|
$this->logger->error('Can not find an agent.', [ |
165
|
|
|
'connections_options' => $this->connections, |
166
|
12 |
|
]); |
167
|
12 |
|
|
168
|
1 |
|
throw new AgentNotFoundException(); |
169
|
|
|
} |
170
|
|
|
|
171
|
12 |
|
private function doConnect($options) |
172
|
12 |
|
{ |
173
|
12 |
|
if (\PHP_VERSION_ID >= 70100) { |
174
|
12 |
|
$context = stream_context_create([ |
175
|
|
|
'socket' => [ |
176
|
12 |
|
'tcp_nodelay' => true, |
177
|
|
|
], |
178
|
12 |
|
]); |
179
|
6 |
|
} else { |
180
|
6 |
|
$context = stream_context_create(); |
181
|
6 |
|
} |
182
|
6 |
|
|
183
|
|
|
$connection = stream_socket_client( |
184
|
6 |
|
$options['remote_socket'], |
185
|
|
|
$errNo, |
186
|
6 |
|
$errMsg, |
187
|
|
|
1, // This value is not used but it should not be 0 |
188
|
|
|
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, |
189
|
8 |
|
$context |
190
|
8 |
|
); |
191
|
8 |
|
|
192
|
8 |
|
stream_set_blocking($connection, 1); |
|
|
|
|
193
|
|
|
|
194
|
8 |
|
return $connection; |
195
|
|
|
} |
196
|
8 |
|
|
197
|
8 |
|
private function disconnect($connection) |
198
|
|
|
{ |
199
|
8 |
|
fclose($connection); |
200
|
5 |
|
} |
201
|
|
|
|
202
|
5 |
|
private function doSend($connection, $content) |
203
|
5 |
|
{ |
204
|
5 |
|
return $this->fwrite($connection, $content); |
205
|
|
|
} |
206
|
5 |
|
|
207
|
|
|
private function doGet($connection) |
208
|
|
|
{ |
209
|
12 |
|
$buffer = ''; |
210
|
|
|
|
211
|
12 |
|
$retries = 10; |
212
|
12 |
|
|
213
|
12 |
|
while (true) { |
214
|
12 |
|
if (feof($connection)) { |
215
|
12 |
|
return false; |
216
|
12 |
|
} |
217
|
12 |
|
|
218
|
|
|
$char = fread($connection, 1); |
219
|
|
|
|
220
|
8 |
|
if (false === $char) { |
221
|
|
|
return false; |
222
|
8 |
|
} |
223
|
|
|
|
224
|
|
|
// On timeout char is empty. But it's also possible PHP did not |
225
|
8 |
|
// "see" the data yet, so let's retry few times |
226
|
|
|
if ('' === $char) { |
227
|
8 |
|
if (!$retries) { |
228
|
|
|
return false; |
229
|
|
|
} |
230
|
12 |
|
--$retries; |
231
|
|
|
|
232
|
12 |
|
continue; |
233
|
|
|
} |
234
|
|
|
|
235
|
12 |
|
if ("\0" === $char) { |
236
|
12 |
|
return $buffer; |
237
|
6 |
|
} |
238
|
|
|
|
239
|
6 |
|
$buffer .= $char; |
240
|
6 |
|
} |
241
|
6 |
|
} |
242
|
6 |
|
|
243
|
6 |
|
private function box($method, $defaultReturnValue = null, array $args = []) |
244
|
6 |
|
{ |
245
|
|
|
set_error_handler(__CLASS__.'::handleInternalError'); |
246
|
|
|
|
247
|
12 |
|
try { |
248
|
|
|
$returnValue = \call_user_func_array([$this, $method], $args); |
249
|
12 |
|
} catch (\ErrorException $exception) { |
250
|
|
|
$returnValue = $defaultReturnValue; |
251
|
|
|
|
252
|
6 |
|
$this->logger->warning('Impossible to execute a boxed call.', [ |
253
|
|
|
'method' => $method, |
254
|
6 |
|
'default_return_value' => $defaultReturnValue, |
255
|
|
|
'args' => $args, |
256
|
|
|
'exception' => $exception, |
257
|
|
|
]); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
restore_error_handler(); |
261
|
|
|
|
262
|
|
|
return $returnValue; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
private static function handleInternalError($type, $message, $file, $line) |
266
|
|
|
{ |
267
|
8 |
|
throw new \ErrorException($message, 0, $type, $file, $line); |
268
|
|
|
} |
269
|
8 |
|
|
270
|
|
|
/** |
271
|
|
|
* Replace fwrite behavior as API is broken in PHP. |
272
|
8 |
|
* |
273
|
8 |
|
* @see https://secure.phabricator.com/rPHU69490c53c9c2ef2002bc2dd4cecfe9a4b080b497 |
274
|
|
|
* |
275
|
|
|
* @param resource $stream The stream resource |
276
|
|
|
* @param string $bytes Bytes written in the stream |
277
|
8 |
|
* |
278
|
|
|
* @return bool|int false if pipe is broken, number of bytes written otherwise |
279
|
|
|
*/ |
280
|
|
|
private function fwrite($stream, $bytes) |
281
|
|
|
{ |
282
|
|
|
if (!\strlen($bytes)) { |
283
|
|
|
return 0; |
284
|
|
|
} |
285
|
|
|
$result = @fwrite($stream, $bytes); |
286
|
|
|
if (0 !== $result) { |
287
|
|
|
// In cases where some bytes are witten (`$result > 0`) or |
288
|
|
|
// an error occurs (`$result === false`), the behavior of fwrite() is |
289
|
|
|
// correct. We can return the value as-is. |
290
|
|
|
return $result; |
291
|
|
|
} |
292
|
|
|
// If we make it here, we performed a 0-length write. Try to distinguish |
293
|
|
|
// between EAGAIN and EPIPE. To do this, we're going to `stream_select()` |
294
|
|
|
// the stream, write to it again if PHP claims that it's writable, and |
295
|
|
|
// consider the pipe broken if the write fails. |
296
|
|
|
$read = []; |
297
|
|
|
$write = [$stream]; |
298
|
|
|
$except = []; |
299
|
|
|
@stream_select($read, $write, $except, 0); |
|
|
|
|
300
|
|
|
if (!$write) { |
301
|
|
|
// The stream isn't writable, so we conclude that it probably really is |
302
|
|
|
// blocked and the underlying error was EAGAIN. Return 0 to indicate that |
303
|
|
|
// no data could be written yet. |
304
|
|
|
return 0; |
305
|
|
|
} |
306
|
|
|
// If we make it here, PHP **just** claimed that this stream is writable, so |
307
|
|
|
// perform a write. If the write also fails, conclude that these failures are |
308
|
|
|
// EPIPE or some other permanent failure. |
309
|
|
|
$result = @fwrite($stream, $bytes); |
310
|
|
|
if (0 !== $result) { |
311
|
|
|
// The write worked or failed explicitly. This value is fine to return. |
312
|
|
|
return $result; |
313
|
|
|
} |
314
|
|
|
// We performed a 0-length write, were told that the stream was writable, and |
315
|
|
|
// then immediately performed another 0-length write. Conclude that the pipe |
316
|
|
|
// is broken and return `false`. |
317
|
|
|
return false; |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.