Passed
Push — develop ( add880...26f5dd )
by nguereza
02:36
created

Shell::execute()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 26
nc 4
nop 1
dl 0
loc 43
rs 9.504
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>
95
     */
96
    protected array $env = [];
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
     * @param string $command
155
     * @param string|null $input
156
     */
157
    public function __construct(string $command, ?string $input = null)
158
    {
159
        if (!function_exists('proc_open')) {
160
            throw new RuntimeException(
161
                'The "proc_open" could not be found in your PHP setup'
162
            );
163
        }
164
165
        $this->command = $command;
166
        $this->input = $input;
167
    }
168
169
    /**
170
     * Whether the process is running
171
     * @return bool
172
     */
173
    public function isRunning(): bool
174
    {
175
        if ($this->state !== self::STATE_STARTED) {
176
            return false;
177
        }
178
179
        $this->updateStatus();
180
181
        return $this->status['running'];
182
    }
183
184
    /**
185
     * Kill the process
186
     * @return void
187
     */
188
    public function kill(): void
189
    {
190
        if (is_resource($this->process)) {
191
            proc_terminate($this->process);
192
        }
193
194
        $this->state = self::STATE_TERMINATED;
195
    }
196
197
    /**
198
     * Stop the process
199
     * @return int|null
200
     */
201
    public function stop(): ?int
202
    {
203
        $this->closePipes();
204
205
        if (is_resource($this->process)) {
206
            proc_close($this->process);
207
        }
208
209
        $this->state = self::STATE_CLOSED;
210
        $this->exitCode = $this->status['exitcode'];
211
212
        return $this->exitCode;
213
    }
214
215
    /**
216
     * Set options used for process execution
217
     * @param string|null $cwd
218
     * @param array<string, mixed> $env
219
     * @param float|null $timeout
220
     * @param array<string, mixed> $options
221
     * @return $this
222
     */
223
    public function setOptions(
224
        ?string $cwd = null,
225
        array $env = [],
226
        ?float $timeout = null,
227
        array $options = []
228
    ): self {
229
        $this->cwd = $cwd;
230
        $this->env = $env;
231
        $this->timeout = $timeout;
232
        $this->options = $options;
233
234
        return $this;
235
    }
236
237
    /**
238
     * Execute the process
239
     * @param bool $async
240
     * @return $this
241
     */
242
    public function execute(bool $async = false): self
243
    {
244
        if ($this->isRunning()) {
245
            throw new RuntimeException(sprintf(
246
                'Process [%s] already running',
247
                $this->command
248
            ));
249
        }
250
251
        $this->descriptors = $this->getDescriptors();
252
        $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...
253
254
        $this->process = proc_open(
255
            $this->command,
256
            $this->descriptors,
257
            $this->pipes,
258
            $this->cwd,
259
            $this->env,
260
            $this->options
261
        );
262
263
        $this->setInput();
264
265
        if (!is_resource($this->process)) {
266
            throw new RuntimeException(sprintf(
267
                'Bad program [%s] could not be started',
268
                $this->command
269
            ));
270
        }
271
272
        $this->state = self::STATE_STARTED;
273
274
        $this->updateStatus();
275
276
        $this->async = $async;
277
278
        if ($this->async) {
279
            $this->setOutputStreamNonBlocking();
280
        } else {
281
            $this->wait();
282
        }
283
284
        return $this;
285
    }
286
287
    /**
288
     * Return the process current state
289
     * @return int
290
     */
291
    public function getState(): int
292
    {
293
        return $this->state;
294
    }
295
296
    /**
297
     * Return the process command error output
298
     * @return string
299
     */
300
    public function getErrorOutput(): string
301
    {
302
        $output = stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR]);
303
304
        if ($output === false) {
305
            throw new RuntimeException(sprintf(
306
                'Can not get process [%s] error output',
307
                $this->command
308
            ));
309
        }
310
311
        return $output;
312
    }
313
314
    /**
315
     * Return the process command output
316
     * @return string
317
     */
318
    public function getOutput(): string
319
    {
320
        $output = stream_get_contents($this->pipes[self::STDOUT_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 exit code
334
     * @return int|null
335
     */
336
    public function getExitCode(): ?int
337
    {
338
        $this->updateStatus();
339
340
        return $this->exitCode;
341
    }
342
343
    /**
344
     * Return the process ID
345
     * @return int|null
346
     */
347
    public function getProcessId(): ?int
348
    {
349
        return $this->isRunning() ? $this->status['pid'] : null;
350
    }
351
352
    /**
353
     * Return the descriptors to be used later
354
     * @return array<int, array<int, string>>
355
     */
356
    protected function getDescriptors(): array
357
    {
358
        $out = $this->isWindows()
359
               ? ['file', 'NUL', 'w']
360
               : ['pipe', 'w'];
361
362
        return [
363
         self::STDIN_DESCRIPTOR  => ['pipe', 'r'],
364
         self::STDOUT_DESCRIPTOR => $out,
365
         self::STDERR_DESCRIPTOR => $out,
366
        ];
367
    }
368
369
    /**
370
     * Whether the current Os is Windows
371
     * @return bool
372
     */
373
    protected function isWindows(): bool
374
    {
375
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
376
    }
377
378
    /**
379
     * Set the input stream information
380
     * @return void
381
     */
382
    protected function setInput(): void
383
    {
384
        if ($this->input !== null) {
385
            fwrite($this->pipes[self::STDIN_DESCRIPTOR], $this->input);
386
        }
387
    }
388
389
    /**
390
     * Update the process status
391
     * @return void
392
     */
393
    protected function updateStatus(): void
394
    {
395
        if ($this->state !== self::STATE_STARTED) {
396
            return;
397
        }
398
399
        if (is_resource($this->process)) {
400
            $status = proc_get_status($this->process);
401
            if ($status === false) {
402
                throw new RuntimeException(sprintf(
403
                    'Can not get process [%s] status information',
404
                    $this->command
405
                ));
406
            }
407
408
            $this->status = $status;
409
410
            if ($this->status['running'] === false && $this->exitCode === null) {
411
                $this->exitCode = $this->status['exitcode'];
412
            }
413
        }
414
    }
415
416
    /**
417
     * Close the process pipes
418
     * @return void
419
     */
420
    protected function closePipes(): void
421
    {
422
        fclose($this->pipes[self::STDIN_DESCRIPTOR]);
423
        fclose($this->pipes[self::STDOUT_DESCRIPTOR]);
424
        fclose($this->pipes[self::STDERR_DESCRIPTOR]);
425
    }
426
427
    /**
428
     * Waiting the process to finish
429
     * @return int|null
430
     */
431
    protected function wait(): ?int
432
    {
433
        while ($this->isRunning()) {
434
            usleep(5000);
435
            $this->checkTimeout();
436
        }
437
438
        return $this->exitCode;
439
    }
440
441
    /**
442
     * Check for process execution timeout
443
     * @return void
444
     */
445
    protected function checkTimeout(): void
446
    {
447
        if ($this->timeout === null) {
448
            return;
449
        }
450
451
        $duration = microtime(true) - $this->startTime;
452
453
        if ($duration > $this->timeout) {
454
            throw new RuntimeException(sprintf(
455
                'Process [%s] execution timeout, time [%d sec] expected [%d sec]',
456
                $this->command,
457
                $duration,
458
                $this->timeout
459
            ));
460
        }
461
    }
462
463
    /**
464
     * Set the running process to asynchronous
465
     * @return bool
466
     */
467
    protected function setOutputStreamNonBlocking(): bool
468
    {
469
        return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR], false);
470
    }
471
}
472