Completed
Pull Request — master (#42)
by
unknown
02:15
created

Shell::getOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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