1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Dazzle\SSH\Driver; |
4
|
|
|
|
5
|
|
|
use Dazzle\Event\BaseEventEmitterTrait; |
6
|
|
|
use Dazzle\Loop\Timer\TimerInterface; |
7
|
|
|
use Dazzle\Loop\LoopAwareTrait; |
8
|
|
|
use Dazzle\SSH\Driver\Shell\ShellResource; |
9
|
|
|
use Dazzle\SSH\SSH2; |
10
|
|
|
use Dazzle\SSH\SSH2DriverInterface; |
11
|
|
|
use Dazzle\SSH\SSH2Interface; |
12
|
|
|
use Dazzle\SSH\SSH2ResourceInterface; |
13
|
|
|
use Dazzle\Throwable\Exception\Logic\ResourceUndefinedException; |
14
|
|
|
use Dazzle\Throwable\Exception\Runtime\ExecutionException; |
15
|
|
|
use Dazzle\Throwable\Exception\Runtime\ReadException; |
16
|
|
|
|
17
|
|
|
class Shell implements SSH2DriverInterface |
18
|
|
|
{ |
19
|
|
|
use BaseEventEmitterTrait; |
20
|
|
|
use LoopAwareTrait; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var int |
24
|
|
|
*/ |
25
|
|
|
const BUFFER_SIZE = 4096; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var SSH2Interface |
29
|
|
|
*/ |
30
|
|
|
protected $ssh2; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var resource |
34
|
|
|
*/ |
35
|
|
|
protected $conn; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var float |
39
|
|
|
*/ |
40
|
|
|
protected $interval; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var resource |
44
|
|
|
*/ |
45
|
|
|
protected $resource; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var ShellResource[]|SSH2ResourceInterface[] |
49
|
|
|
*/ |
50
|
|
|
protected $resources; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var bool |
54
|
|
|
*/ |
55
|
|
|
protected $paused; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var TimerInterface|null |
59
|
|
|
*/ |
60
|
|
|
private $timer; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @var int |
64
|
|
|
*/ |
65
|
|
|
private $resourcesCounter; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @var string |
69
|
|
|
*/ |
70
|
|
|
private $buffer; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @var string |
74
|
|
|
*/ |
75
|
|
|
private $prefix; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @param SSH2Interface $ssh2 |
79
|
|
|
* @param resource $conn |
80
|
|
|
* @param float $interval |
81
|
|
|
*/ |
82
|
23 |
View Code Duplication |
public function __construct(SSH2Interface $ssh2, $conn, $interval = 1e-1) |
|
|
|
|
83
|
|
|
{ |
84
|
23 |
|
$this->ssh2 = $ssh2; |
85
|
23 |
|
$this->conn = $conn; |
86
|
23 |
|
$this->interval = $interval; |
87
|
|
|
|
88
|
23 |
|
$this->loop = $ssh2->getLoop(); |
89
|
|
|
|
90
|
23 |
|
$this->resource = null; |
91
|
23 |
|
$this->resources = []; |
92
|
23 |
|
$this->paused = true; |
93
|
|
|
|
94
|
23 |
|
$this->timer = null; |
95
|
23 |
|
$this->resourcesCounter = 0; |
96
|
23 |
|
$this->buffer = ''; |
97
|
23 |
|
$this->prefix = ''; |
98
|
|
|
|
99
|
23 |
|
$this->resume(); |
100
|
23 |
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* |
104
|
|
|
*/ |
105
|
13 |
|
public function __destruct() |
106
|
|
|
{ |
107
|
13 |
|
$this->disconnect(); |
108
|
13 |
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @override |
112
|
|
|
* @inheritDoc |
113
|
|
|
*/ |
114
|
3 |
|
public function getName() |
115
|
|
|
{ |
116
|
3 |
|
return SSH2::DRIVER_SHELL; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @override |
121
|
|
|
* @inheritDoc |
122
|
|
|
*/ |
123
|
6 |
View Code Duplication |
public function connect() |
|
|
|
|
124
|
|
|
{ |
125
|
6 |
|
if ($this->resource !== null) |
126
|
|
|
{ |
127
|
1 |
|
return; |
128
|
|
|
} |
129
|
|
|
|
130
|
5 |
|
$shell = $this->createConnection($this->conn); |
131
|
|
|
|
132
|
5 |
|
if (!$shell || !is_resource($shell)) |
133
|
|
|
{ |
134
|
2 |
|
$this->emit('error', [ $this, new ExecutionException('SSH2:Shell could not be connected.') ]); |
135
|
2 |
|
return; |
136
|
|
|
} |
137
|
|
|
|
138
|
3 |
|
$this->resource = $shell; |
139
|
|
|
|
140
|
3 |
|
$this->emit('connect', [ $this ]); |
141
|
3 |
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* @override |
145
|
|
|
* @inheritDoc |
146
|
|
|
*/ |
147
|
18 |
View Code Duplication |
public function disconnect() |
|
|
|
|
148
|
|
|
{ |
149
|
18 |
|
if ($this->resource === null || !is_resource($this->resource)) |
150
|
|
|
{ |
151
|
15 |
|
return; |
152
|
|
|
} |
153
|
|
|
|
154
|
5 |
|
$this->pause(); |
155
|
|
|
|
156
|
5 |
|
foreach ($this->resources as $resource) |
157
|
|
|
{ |
158
|
1 |
|
$resource->close(); |
159
|
|
|
} |
160
|
|
|
|
161
|
5 |
|
$this->handleDisconnect(); |
162
|
5 |
|
$this->emit('disconnect', [ $this ]); |
163
|
5 |
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @override |
167
|
|
|
* @inheritDoc |
168
|
|
|
*/ |
169
|
5 |
|
public function isConnected() |
170
|
|
|
{ |
171
|
5 |
|
return $this->resource !== null && is_resource($this->resource); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @override |
176
|
|
|
* @inheritDoc |
177
|
|
|
*/ |
178
|
5 |
View Code Duplication |
public function pause() |
|
|
|
|
179
|
|
|
{ |
180
|
5 |
|
if (!$this->paused) |
181
|
|
|
{ |
182
|
4 |
|
$this->paused = true; |
183
|
|
|
|
184
|
4 |
|
if ($this->timer !== null) |
185
|
|
|
{ |
186
|
4 |
|
$this->timer->cancel(); |
187
|
4 |
|
$this->timer = null; |
188
|
|
|
} |
189
|
|
|
} |
190
|
5 |
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* @override |
194
|
|
|
* @inheritDoc |
195
|
|
|
*/ |
196
|
23 |
View Code Duplication |
public function resume() |
|
|
|
|
197
|
|
|
{ |
198
|
23 |
|
if ($this->paused) |
199
|
|
|
{ |
200
|
23 |
|
$this->paused = false; |
201
|
|
|
|
202
|
23 |
|
if ($this->timer === null) |
203
|
|
|
{ |
204
|
23 |
|
$this->timer = $this->loop->addPeriodicTimer($this->interval, [ $this, 'handleHeartbeat' ]); |
205
|
|
|
} |
206
|
|
|
} |
207
|
23 |
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @override |
211
|
|
|
* @inheritDoc |
212
|
|
|
*/ |
213
|
6 |
|
public function isPaused() |
214
|
|
|
{ |
215
|
6 |
|
return $this->paused; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* @override |
220
|
|
|
* @inheritDoc |
221
|
|
|
*/ |
222
|
2 |
|
public function open($resource = null, $flags = 'r') |
223
|
|
|
{ |
224
|
2 |
|
if (!$this->isConnected()) |
225
|
|
|
{ |
226
|
|
|
throw new ResourceUndefinedException('Tried to open resource before establishing SSH2 connection!'); |
227
|
|
|
} |
228
|
|
|
|
229
|
2 |
|
$resource = new ShellResource($this, $this->resource); |
230
|
|
|
$resource->on('open', function(SSH2ResourceInterface $resource) { |
231
|
|
|
$this->emit('resource:open', [ $this, $resource ]); |
232
|
2 |
|
}); |
233
|
|
|
$resource->on('close', function(SSH2ResourceInterface $resource) { |
234
|
2 |
|
$this->removeResource($resource->getId()); |
235
|
2 |
|
$this->emit('resource:close', [ $this, $resource ]); |
236
|
2 |
|
}); |
237
|
|
|
|
238
|
2 |
|
$this->resources[$resource->getId()] = $resource; |
239
|
2 |
|
$this->resourcesCounter++; |
240
|
2 |
|
$this->resume(); |
241
|
|
|
|
242
|
2 |
|
return $resource; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Handle data. |
247
|
|
|
* |
248
|
|
|
* @internal |
249
|
|
|
*/ |
250
|
2 |
|
public function handleHeartbeat() |
251
|
|
|
{ |
252
|
2 |
|
if (fwrite($this->resource, "\n") === 0) |
253
|
|
|
{ |
254
|
|
|
return $this->ssh2->disconnect(); |
255
|
|
|
} |
256
|
|
|
|
257
|
2 |
|
$this->handleRead(); |
258
|
2 |
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Handle incoming data. |
262
|
|
|
* |
263
|
|
|
* @internal |
264
|
|
|
*/ |
265
|
2 |
|
protected function handleRead() |
266
|
|
|
{ |
267
|
2 |
|
if ($this->paused) |
268
|
|
|
{ |
269
|
|
|
return; |
270
|
|
|
} |
271
|
|
|
|
272
|
2 |
|
$data = @fread($this->resource, static::BUFFER_SIZE); |
273
|
|
|
|
274
|
2 |
|
if ($data === false || $data === '') |
275
|
|
|
{ |
276
|
|
|
return; |
277
|
|
|
} |
278
|
|
|
|
279
|
2 |
|
$this->buffer .= $data; |
280
|
|
|
|
281
|
2 |
|
while ($this->buffer !== '') |
282
|
|
|
{ |
283
|
2 |
|
if ($this->prefix !== '' && !isset($this->resources[$this->prefix])) |
284
|
|
|
{ |
285
|
|
|
$this->prefix = ''; |
286
|
|
|
} |
287
|
|
|
|
288
|
2 |
|
if ($this->prefix === '') |
289
|
|
|
{ |
290
|
2 |
|
if (!preg_match('/([a-zA-Z0-9]{32})\r?\n(.*)/s', $this->buffer, $matches)) |
291
|
|
|
{ |
292
|
2 |
|
return; |
293
|
|
|
} |
294
|
|
|
|
295
|
2 |
|
$this->prefix = $matches[1]; |
296
|
2 |
|
$this->buffer = $matches[2]; |
297
|
|
|
} |
298
|
|
|
|
299
|
2 |
|
$resource = $this->resources[$this->prefix]; |
300
|
2 |
|
$data = ''; |
301
|
2 |
|
$status = -1; |
302
|
2 |
|
$successSuffix = $resource->getSuccessSuffix(); |
|
|
|
|
303
|
2 |
|
$failureSuffix = $resource->getFailureSuffix(); |
|
|
|
|
304
|
|
|
|
305
|
2 |
|
$this->buffer = preg_replace_callback( |
306
|
2 |
|
sprintf('/(.*)(%s|%s):(\d*)\r?\n/s', $successSuffix, $failureSuffix), |
307
|
2 |
|
function($matches) use($resource, &$data, &$status) { |
308
|
2 |
|
$data = $matches[1]; |
309
|
2 |
|
$status = (int) $matches[3]; |
310
|
2 |
|
return ''; |
311
|
2 |
|
}, |
312
|
2 |
|
$this->buffer |
313
|
|
|
); |
314
|
|
|
|
315
|
2 |
|
if ($status === -1) |
316
|
|
|
{ |
317
|
|
|
$data = $this->buffer; |
318
|
|
|
$this->buffer = ''; |
319
|
|
|
} |
320
|
|
|
else |
321
|
|
|
{ |
322
|
2 |
|
$this->removeResource($this->prefix); |
323
|
2 |
|
$this->prefix = ''; |
324
|
|
|
} |
325
|
|
|
|
326
|
2 |
|
$parts = str_split($data, $resource->getBufferSize()); |
327
|
2 |
|
unset($data); |
328
|
|
|
|
329
|
2 |
|
foreach ($parts as &$part) |
330
|
|
|
{ |
331
|
2 |
|
$resource->emit('data', [ $resource, $part ]); |
332
|
|
|
} |
333
|
2 |
|
unset($parts); |
334
|
2 |
|
unset($part); |
335
|
|
|
|
336
|
2 |
|
if ($status === 0) |
337
|
|
|
{ |
338
|
2 |
|
$resource->emit('end', [ $resource ]); |
339
|
2 |
|
$resource->close(); |
340
|
|
|
} |
341
|
|
|
else if ($status > 0) |
342
|
|
|
{ |
343
|
|
|
$resource->emit('error', [ $resource, new ReadException($status) ]); |
344
|
|
|
$resource->close(); |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
$this->handleRead(); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* |
353
|
|
|
*/ |
354
|
3 |
|
protected function handleDisconnect() |
355
|
|
|
{ |
356
|
3 |
|
@fclose($this->resource); |
|
|
|
|
357
|
3 |
|
$this->resource = null; |
358
|
3 |
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @param resource $conn |
362
|
|
|
* @return resource |
363
|
|
|
*/ |
364
|
2 |
|
protected function createConnection($conn) |
365
|
|
|
{ |
366
|
2 |
|
return @ssh2_shell($conn); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Remove resource from known collection. |
371
|
|
|
* |
372
|
|
|
* @param string $prefix |
373
|
|
|
*/ |
374
|
2 |
View Code Duplication |
private function removeResource($prefix) |
|
|
|
|
375
|
|
|
{ |
376
|
2 |
|
if (!isset($this->resources[$prefix])) |
377
|
|
|
{ |
378
|
2 |
|
return; |
379
|
|
|
} |
380
|
|
|
|
381
|
2 |
|
unset($this->resources[$prefix]); |
382
|
2 |
|
$this->resourcesCounter--; |
383
|
|
|
|
384
|
2 |
|
if ($this->resourcesCounter === 0) |
385
|
|
|
{ |
386
|
2 |
|
$this->pause(); |
387
|
|
|
} |
388
|
2 |
|
} |
389
|
|
|
} |
390
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.