Passed
Push — develop ( 4927f2...cbce63 )
by nguereza
02:13
created

Shell::stop()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Console
5
 *
6
 * Platine Console is a powerful library with support of custom
7
 * style to build command line interface applications
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Console
12
 * Copyright (c) 2017-2020 Jitendra Adhikari
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Shell.php
35
 *
36
 *  The shell class
37
 *
38
 *  @package    Platine\Console\Command
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   http://www.iacademy.cf
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Console\Command;
50
51
use Platine\Console\Exception\RuntimeException;
52
53
/**
54
 * Class Shell
55
 * @package Platine\Console\Command
56
 */
57
class Shell
58
{
59
    public const STDIN_DESCRIPTOR = 0;
60
    public const STDOUT_DESCRIPTOR = 1;
61
    public const STDERR_DESCRIPTOR = 2;
62
63
    public const STATE_READY = 0;
64
    public const STATE_STARTED = 1;
65
    public const STATE_CLOSED = 2;
66
    public const STATE_TERMINATED = 3;
67
68
    /**
69
     * Whether to wait for the process to finish or return instantly
70
     * @var bool
71
     */
72
    protected bool $async = false;
73
74
    /**
75
     * The command to execute
76
     * @var string
77
     */
78
    protected string $command = '';
79
80
    /**
81
     * Current working directory
82
     * @var string|null
83
     */
84
    protected ?string $cwd = null;
85
86
    /**
87
     * The list of descriptors to pass to process
88
     * @var array<int, array<int, string>>
89
     */
90
    protected array $descriptors = [];
91
92
    /**
93
     * List of environment variables
94
     * @var array<string, mixed>|null
95
     */
96
    protected ?array $env = null;
97
98
    /**
99
     * The process exit code
100
     * @var int|null
101
     */
102
    protected ?int $exitCode = null;
103
104
    /**
105
     * The path for input stream
106
     * @var string|null
107
     */
108
    protected ?string $input = null;
109
110
    /**
111
     * Others options to pass to process
112
     * @var array<string, mixed>
113
     */
114
    protected array $options = [];
115
116
    /**
117
     * Pointers to standard input/output/error
118
     * @var array<int, resource>
119
     */
120
    protected array $pipes = [];
121
122
    /**
123
     * The actual process resource returned
124
     * @var resource|false
125
     */
126
    protected $process;
127
128
    /**
129
     * The process start time in Unix timestamp
130
     * @var float
131
     */
132
    protected float $startTime = 0;
133
134
    /**
135
     * Default timeout for the process in seconds with microseconds
136
     * @var float|null
137
     */
138
    protected ?float $timeout = null;
139
140
    /**
141
     * The status list of the process
142
     * @var array<string, mixed>
143
     */
144
    protected array $status = [];
145
146
    /**
147
     * The current process status
148
     * @var int
149
     */
150
    protected int $state = self::STATE_READY;
151
152
    /**
153
     * Create new instance
154
     */
155
    public function __construct()
156
    {
157
        if (!function_exists('proc_open')) {
158
            throw new RuntimeException(
159
                'The "proc_open" could not be found in your PHP setup'
160
            );
161
        }
162
    }
163
164
    /**
165
     * Set the command to be executed
166
     * @param string $command
167
     * @return $this
168
     */
169
    public function setCommand(string $command): self
170
    {
171
        $this->command = $command;
172
173
        return $this;
174
    }
175
176
    /**
177
     * Set the input stream information
178
     * @param string|null $input
179
     * @return $this
180
     */
181
    public function setInput(?string $input): self
182
    {
183
        $this->input = $input;
184
        return $this;
185
    }
186
187
    /**
188
     * Whether the process is running
189
     * @return bool
190
     */
191
    public function isRunning(): bool
192
    {
193
        if ($this->state !== self::STATE_STARTED) {
194
            return false;
195
        }
196
197
        $this->updateStatus();
198
199
        return $this->status['running'];
200
    }
201
202
    /**
203
     * Kill the process
204
     * @return void
205
     */
206
    public function kill(): void
207
    {
208
        if (is_resource($this->process)) {
209
            proc_terminate($this->process);
210
        }
211
212
        $this->state = self::STATE_TERMINATED;
213
    }
214
215
    /**
216
     * Stop the process
217
     * @return int|null
218
     */
219
    public function stop(): ?int
220
    {
221
        $this->closePipes();
222
223
        if (is_resource($this->process)) {
224
            proc_close($this->process);
225
        }
226
227
        $this->state = self::STATE_CLOSED;
228
        $this->exitCode = $this->status['exitcode'];
229
230
        return $this->exitCode;
231
    }
232
233
    /**
234
     * Set options used for process execution
235
     * @param string|null $cwd
236
     * @param array<string, mixed>|null $env
237
     * @param float|null $timeout
238
     * @param array<string, mixed> $options
239
     * @return $this
240
     */
241
    public function setOptions(
242
        ?string $cwd = null,
243
        ?array $env = [],
244
        ?float $timeout = null,
245
        array $options = []
246
    ): self {
247
        $this->cwd = $cwd;
248
        $this->env = $env;
249
        $this->timeout = $timeout;
250
        $this->options = $options;
251
252
        return $this;
253
    }
254
255
    /**
256
     * Execute the process
257
     * @param bool $async
258
     * @return $this
259
     */
260
    public function execute(bool $async = false): self
261
    {
262
        if ($this->isRunning()) {
263
            throw new RuntimeException(sprintf(
264
                'Process [%s] already running',
265
                $this->command
266
            ));
267
        }
268
269
        $this->descriptors = $this->getDescriptors();
270
        $this->startTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $startTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
271
272
        $this->process = proc_open(
273
            $this->command,
274
            $this->descriptors,
275
            $this->pipes,
276
            $this->cwd,
277
            $this->env,
278
            $this->options
279
        );
280
281
        $this->writeInput();
282
283
        if (!is_resource($this->process)) {
284
            throw new RuntimeException(sprintf(
285
                'Bad program [%s] could not be started',
286
                $this->command
287
            ));
288
        }
289
290
        $this->state = self::STATE_STARTED;
291
292
        $this->updateStatus();
293
294
        $this->async = $async;
295
296
        if ($this->async) {
297
            $this->setOutputStreamNonBlocking();
298
        } else {
299
            $this->wait();
300
        }
301
302
        return $this;
303
    }
304
305
    /**
306
     * Return the process current state
307
     * @return int
308
     */
309
    public function getState(): int
310
    {
311
        return $this->state;
312
    }
313
314
    /**
315
     * Return the process command error output
316
     * @return string
317
     */
318
    public function getErrorOutput(): string
319
    {
320
        $output = stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR]);
321
322
        if ($output === false) {
323
            throw new RuntimeException(sprintf(
324
                'Can not get process [%s] error output',
325
                $this->command
326
            ));
327
        }
328
329
        return $output;
330
    }
331
332
    /**
333
     * Return the process command output
334
     * @return string
335
     */
336
    public function getOutput(): string
337
    {
338
        $output = stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR]);
339
340
        if ($output === false) {
341
            throw new RuntimeException(sprintf(
342
                'Can not get process [%s] error output',
343
                $this->command
344
            ));
345
        }
346
347
        return $output;
348
    }
349
350
    /**
351
     * Return the process exit code
352
     * @return int|null
353
     */
354
    public function getExitCode(): ?int
355
    {
356
        $this->updateStatus();
357
358
        return $this->exitCode;
359
    }
360
361
    /**
362
     * Return the process ID
363
     * @return int|null
364
     */
365
    public function getProcessId(): ?int
366
    {
367
        return $this->isRunning() ? $this->status['pid'] : null;
368
    }
369
370
    /**
371
     * Return the descriptors to be used later
372
     * @return array<int, array<int, string>>
373
     */
374
    protected function getDescriptors(): array
375
    {
376
        $out = $this->isWindows()
377
               ? ['pipe', 'w'] // ['file', 'NUL', 'w']
378
               : ['pipe', 'w'];
379
380
        return [
381
         self::STDIN_DESCRIPTOR  => ['pipe', 'r'],
382
         self::STDOUT_DESCRIPTOR => $out,
383
         self::STDERR_DESCRIPTOR => $out,
384
        ];
385
    }
386
387
    /**
388
     * Whether the current Os is Windows
389
     * @return bool
390
     */
391
    protected function isWindows(): bool
392
    {
393
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
394
    }
395
396
    /**
397
     * Write to input stream
398
     * @return void
399
     */
400
    protected function writeInput(): void
401
    {
402
        if ($this->input !== null) {
403
            fwrite($this->pipes[self::STDIN_DESCRIPTOR], $this->input);
404
        }
405
    }
406
407
    /**
408
     * Update the process status
409
     * @return void
410
     */
411
    protected function updateStatus(): void
412
    {
413
        if ($this->state !== self::STATE_STARTED) {
414
            return;
415
        }
416
417
        if (is_resource($this->process)) {
418
            $status = proc_get_status($this->process);
419
            if ($status === false) {
420
                throw new RuntimeException(sprintf(
421
                    'Can not get process [%s] status information',
422
                    $this->command
423
                ));
424
            }
425
426
            $this->status = $status;
427
428
            if ($this->status['running'] === false && $this->exitCode === null) {
429
                $this->exitCode = $this->status['exitcode'];
430
            }
431
        }
432
    }
433
434
    /**
435
     * Close the process pipes
436
     * @return void
437
     */
438
    protected function closePipes(): void
439
    {
440
        fclose($this->pipes[self::STDIN_DESCRIPTOR]);
441
        fclose($this->pipes[self::STDOUT_DESCRIPTOR]);
442
        fclose($this->pipes[self::STDERR_DESCRIPTOR]);
443
    }
444
445
    /**
446
     * Waiting the process to finish
447
     * @return int|null
448
     */
449
    protected function wait(): ?int
450
    {
451
        while ($this->isRunning()) {
452
            usleep(5000);
453
            $this->checkTimeout();
454
        }
455
456
        return $this->exitCode;
457
    }
458
459
    /**
460
     * Check for process execution timeout
461
     * @return void
462
     */
463
    protected function checkTimeout(): void
464
    {
465
        if ($this->timeout === null) {
466
            return;
467
        }
468
469
        $duration = microtime(true) - $this->startTime;
470
471
        if ($duration > $this->timeout) {
472
            throw new RuntimeException(sprintf(
473
                'Process [%s] execution timeout, time [%d sec] expected [%d sec]',
474
                $this->command,
475
                $duration,
476
                $this->timeout
477
            ));
478
        }
479
    }
480
481
    /**
482
     * Set the running process to asynchronous
483
     * @return bool
484
     */
485
    protected function setOutputStreamNonBlocking(): bool
486
    {
487
        return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR], false);
488
    }
489
}
490