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