Completed
Pull Request — master (#43)
by
unknown
02:07
created

Shell::wait()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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
156
        // @codeCoverageIgnoreStart
157
158
        // @codeCoverageIgnoreEnd
159
    }
160
161
    public function setOptions(string $cwd = null, array $env = null, float $timeout = null, array $otherOptions = []): self
162
    {
163
        $this->cwd            = $cwd;
164
        $this->env            = $env;
165
        $this->processTimeout = $timeout;
166
        $this->otherOptions   = $otherOptions;
167
168
        return $this;
169
    }
170
171
    public function execute(bool $async = false): self
172
    {
173
        if ($this->isRunning()) {
174
            throw new RuntimeException('Process is already running.');
175
        }
176
177
        $this->descriptors      = $this->getDescriptors();
178
        $this->processStartTime = \microtime(true);
179
180
        $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...
181
        $this->setInput();
182
183
        // @codeCoverageIgnoreStart
184
        if (!\is_resource($this->process)) {
185
            throw new RuntimeException('Bad program could not be started.');
186
        }
187
        // @codeCoverageIgnoreEnd
188
189
        $this->state = self::STATE_STARTED;
190
191
        $this->updateProcessStatus();
192
193
        if ($this->async = $async) {
194
            $this->setOutputStreamNonBlocking();
195
        } else {
196
            $this->wait();
197
        }
198
199
        return $this;
200
    }
201
202
    private function setOutputStreamNonBlocking(): bool
203
    {
204
        return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false);
205
    }
206
207
    public function getState(): string
208
    {
209
        return $this->state;
210
    }
211
212
    public function getOutput(): string
213
    {
214
        return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
215
    }
216
217
    public function getErrorOutput(): string
218
    {
219
        return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
220
    }
221
222
    public function getExitCode()
223
    {
224
        $this->updateProcessStatus();
225
226
        return $this->exitCode;
227
    }
228
229
    public function isRunning(): bool
230
    {
231
        if (self::STATE_STARTED !== $this->state) {
232
            return false;
233
        }
234
235
        $this->updateProcessStatus();
236
237
        return $this->processStatus['running'];
238
    }
239
240
    public function getProcessId()
241
    {
242
        return $this->isRunning() ? $this->processStatus['pid'] : null;
243
    }
244
245
    public function stop()
246
    {
247
        $this->closePipes();
248
249
        if (\is_resource($this->process)) {
250
            \proc_close($this->process);
251
        }
252
253
        $this->state = self::STATE_CLOSED;
254
255
        $this->exitCode = $this->processStatus['exitcode'];
256
257
        return $this->exitCode;
258
    }
259
260
    public function kill()
261
    {
262
        if (\is_resource($this->process)) {
263
            \proc_terminate($this->process);
264
        }
265
266
        $this->state = self::STATE_TERMINATED;
267
    }
268
269
    public function __destruct()
270
    {
271
        // If async (run in background) => we don't care if it ever closes
272
        // Otherwise, waited already till it ends itself or timeout occurs, in which case kill it
273
    }
274
}
275