Shell::__destruct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
3
/*
4
 * This file is part of the PHP-CLI package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Cli\Helper;
13
14
use Ahc\Cli\Exception\RuntimeException;
15
16
/**
17
 * A thin proc_open wrapper to execute shell commands.
18
 *
19
 * With some inspirations from symfony/process.
20
 *
21
 * @author  Sushil Gupta <[email protected]>
22
 * @license MIT
23
 *
24
 * @link    https://github.com/adhocore/cli
25
 */
26
class Shell
27
{
28
    const STDIN_DESCRIPTOR_KEY  = 0;
29
    const STDOUT_DESCRIPTOR_KEY = 1;
30
    const STDERR_DESCRIPTOR_KEY = 2;
31
32
    const STATE_READY      = 'ready';
33
    const STATE_STARTED    = 'started';
34
    const STATE_CLOSED     = 'closed';
35
    const STATE_TERMINATED = 'terminated';
36
37
    /** @var bool Whether to wait for the process to finish or return instantly */
38
    protected $async = false;
39
40
    /** @var string Command to be executed */
41
    protected $command;
42
43
    /** @var string Current working directory */
44
    protected $cwd = null;
45
46
    /** @var array Descriptor to be passed for proc_open */
47
    protected $descriptors;
48
49
    /** @var array An array of environment variables */
50
    protected $env = null;
51
52
    /** @var int Exit code of the process once it has been terminated */
53
    protected $exitCode = null;
54
55
    /** @var string Input for stdin */
56
    protected $input;
57
58
    /** @var array Other options to be passed for proc_open */
59
    protected $otherOptions = [];
60
61
    /** @var array Pointers to stdin, stdout & stderr */
62
    protected $pipes = null;
63
64
    /** @var resource The actual process resource returned from proc_open */
65
    protected $process = null;
66
67
    /** @var int Process starting time in unix timestamp */
68
    protected $processStartTime;
69
70
    /** @var array Status of the process as returned from proc_get_status */
71
    protected $processStatus = null;
72
73
    /** @var float Default timeout for the process in seconds with microseconds */
74
    protected $processTimeout = null;
75
76
    /** @var string Current state of the shell execution, set from this class, NOT for proc_get_status */
77
    protected $state = self::STATE_READY;
78
79
    public function __construct(string $command, string $input = null)
80
    {
81
        // @codeCoverageIgnoreStart
82
        if (!\function_exists('proc_open')) {
83
            throw new RuntimeException('Required proc_open could not be found in your PHP setup.');
84
        }
85
        // @codeCoverageIgnoreEnd
86
87
        $this->command = $command;
88
        $this->input   = $input;
89
    }
90
91
    protected function getDescriptors(): array
92
    {
93
        $out = $this->isWindows() ? ['file', 'NUL', 'w'] : ['pipe', 'w'];
94
95
        return [
96
            self::STDIN_DESCRIPTOR_KEY  => ['pipe', 'r'],
97
            self::STDOUT_DESCRIPTOR_KEY => $out,
98
            self::STDERR_DESCRIPTOR_KEY => $out,
99
        ];
100
    }
101
102
    protected function isWindows(): bool
103
    {
104
        return '\\' === \DIRECTORY_SEPARATOR;
105
    }
106
107
    protected function setInput()
108
    {
109
        \fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input);
110
    }
111
112
    protected function updateProcessStatus()
113
    {
114
        if ($this->state !== self::STATE_STARTED) {
115
            return;
116
        }
117
118
        $this->processStatus = \proc_get_status($this->process);
0 ignored issues
show
Documentation Bug introduced by
It seems like proc_get_status($this->process) can also be of type false. However, the property $processStatus is declared as type array. 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...
119
120
        if ($this->processStatus['running'] === false && $this->exitCode === null) {
121
            $this->exitCode = $this->processStatus['exitcode'];
122
        }
123
    }
124
125
    protected function closePipes()
126
    {
127
        \fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
128
        \fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
129
        \fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
130
    }
131
132
    protected function wait()
133
    {
134
        while ($this->isRunning()) {
135
            usleep(5000);
136
            $this->checkTimeout();
137
        }
138
139
        return $this->exitCode;
140
    }
141
142
    protected function checkTimeout()
143
    {
144
        if ($this->processTimeout === null) {
145
            return;
146
        }
147
148
        $executionDuration = \microtime(true) - $this->processStartTime;
149
150
        if ($executionDuration > $this->processTimeout) {
151
            $this->kill();
152
153
            throw new RuntimeException('Timeout occurred, process terminated.');
154
        }
155
        // @codeCoverageIgnoreStart
156
    }
157
158
    // @codeCoverageIgnoreEnd
159
160
    public function setOptions(string $cwd = null, array $env = null, float $timeout = null, array $otherOptions = []): self
161
    {
162
        $this->cwd            = $cwd;
163
        $this->env            = $env;
164
        $this->processTimeout = $timeout;
165
        $this->otherOptions   = $otherOptions;
166
167
        return $this;
168
    }
169
170
    public function execute(bool $async = false): self
171
    {
172
        if ($this->isRunning()) {
173
            throw new RuntimeException('Process is already running.');
174
        }
175
176
        $this->descriptors      = $this->getDescriptors();
177
        $this->processStartTime = \microtime(true);
178
179
        $this->process = \proc_open($this->command, $this->descriptors, $this->pipes, $this->cwd, $this->env, $this->otherOptions);
0 ignored issues
show
Documentation Bug introduced by
It seems like proc_open($this->command...v, $this->otherOptions) can also be of type false. However, the property $process is declared as type resource. 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...
180
        $this->setInput();
181
182
        // @codeCoverageIgnoreStart
183
        if (!\is_resource($this->process)) {
184
            throw new RuntimeException('Bad program could not be started.');
185
        }
186
        // @codeCoverageIgnoreEnd
187
188
        $this->state = self::STATE_STARTED;
189
190
        $this->updateProcessStatus();
191
192
        if ($this->async = $async) {
193
            $this->setOutputStreamNonBlocking();
194
        } else {
195
            $this->wait();
196
        }
197
198
        return $this;
199
    }
200
201
    private function setOutputStreamNonBlocking(): bool
202
    {
203
        return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false);
204
    }
205
206
    public function getState(): string
207
    {
208
        return $this->state;
209
    }
210
211
    public function getOutput(): string
212
    {
213
        return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
214
    }
215
216
    public function getErrorOutput(): string
217
    {
218
        return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
219
    }
220
221
    public function getExitCode()
222
    {
223
        $this->updateProcessStatus();
224
225
        return $this->exitCode;
226
    }
227
228
    public function isRunning(): bool
229
    {
230
        if (self::STATE_STARTED !== $this->state) {
231
            return false;
232
        }
233
234
        $this->updateProcessStatus();
235
236
        return $this->processStatus['running'];
237
    }
238
239
    public function getProcessId()
240
    {
241
        return $this->isRunning() ? $this->processStatus['pid'] : null;
242
    }
243
244
    public function stop()
245
    {
246
        $this->closePipes();
247
248
        if (\is_resource($this->process)) {
249
            \proc_close($this->process);
250
        }
251
252
        $this->state = self::STATE_CLOSED;
253
254
        $this->exitCode = $this->processStatus['exitcode'];
255
256
        return $this->exitCode;
257
    }
258
259
    public function kill()
260
    {
261
        if (\is_resource($this->process)) {
262
            \proc_terminate($this->process);
263
        }
264
265
        $this->state = self::STATE_TERMINATED;
266
    }
267
268
    public function __destruct()
269
    {
270
        // If async (run in background) => we don't care if it ever closes
271
        // Otherwise, waited already till it ends itself or timeout occurs, in which case kill it
272
    }
273
}
274