Completed
Pull Request — master (#40)
by Jitendra
02:55
created

Shell::kill()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
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 array Status of the process as returned from proc_get_status */
68
    protected $processStatus = null;
69
70
    /** @var int Process starting time in unix timestamp */
71
    protected $processStartTime;
72
73
    /** @var string Current state of the shell execution */
74
    protected $state = self::STATE_READY;
75
76
    /** @var float Default timeout for the process in seconds with microseconds */
77
    protected $processTimeout = null;
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 = '\\' === \DIRECTORY_SEPARATOR ? ['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 setInput()
103
    {
104
        \fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input);
105
    }
106
107
    protected function updateProcessStatus()
108
    {
109
        if ($this->state !== self::STATE_STARTED) {
110
            return;
111
        }
112
113
        $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...
114
115
        if ($this->processStatus['running'] === false && $this->exitCode === null) {
116
            $this->exitCode = $this->processStatus['exitcode'];
117
        }
118
    }
119
120
    protected function closePipes()
121
    {
122
        \fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
123
        \fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
124
        \fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
125
    }
126
127
    protected function wait()
128
    {
129
        while ($this->isRunning()) {
130
            usleep(5000);
131
            $this->checkTimeout();
132
        }
133
134
        return $this->exitCode;
135
    }
136
137
    protected function checkTimeout()
138
    {
139
        if ($this->processTimeout === null) {
140
            return;
141
        }
142
143
        $executionDuration = \microtime(true) - $this->processStartTime;
144
145
        if ($executionDuration > $this->processTimeout) {
146
            $this->kill();
147
148
            throw new RuntimeException('Process timeout occurred, terminated');
149
        }
150
151
        // @codeCoverageIgnoreStart
152
        return;
153
        // @codeCoverageIgnoreEnd
154
    }
155
156
    public function setOptions(string $cwd = null, array $env = null, float $timeout = null, array $otherOptions = []): self
157
    {
158
        $this->cwd            = $cwd;
159
        $this->env            = $env;
160
        $this->processTimeout = $timeout;
161
        $this->otherOptions   = $otherOptions;
162
163
        return $this;
164
    }
165
166
    public function execute(bool $async = false): self
167
    {
168
        if ($this->isRunning()) {
169
            throw new RuntimeException('Process is already running');
170
        }
171
172
        $this->descriptors      = $this->getDescriptors();
173
        $this->processStartTime = \microtime(true);
174
175
        $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...
176
        $this->setInput();
177
178
        // @codeCoverageIgnoreStart
179
        if (!\is_resource($this->process)) {
180
            throw new RuntimeException('Bad program could not be started.');
181
        }
182
        // @codeCoverageIgnoreEnd
183
184
        $this->state = self::STATE_STARTED;
185
186
        $this->updateProcessStatus();
187
188
        if ($this->async = $async) {
189
            $this->setOutputStreamNonBlocking();
190
        } else {
191
            $this->wait();
192
        }
193
194
        return $this;
195
    }
196
197
    private function setOutputStreamNonBlocking(): bool
198
    {
199
        return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false);
200
    }
201
202
    public function getState(): string
203
    {
204
        return $this->state;
205
    }
206
207
    public function getOutput(): string
208
    {
209
        return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
210
    }
211
212
    public function getErrorOutput(): string
213
    {
214
        return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
215
    }
216
217
    public function getExitCode()
218
    {
219
        $this->updateProcessStatus();
220
221
        return $this->exitCode;
222
    }
223
224
    public function isRunning(): bool
225
    {
226
        if (self::STATE_STARTED !== $this->state) {
227
            return false;
228
        }
229
230
        $this->updateProcessStatus();
231
232
        return $this->processStatus['running'];
233
    }
234
235
    public function getProcessId()
236
    {
237
        return $this->isRunning() ? $this->processStatus['pid'] : null;
238
    }
239
240
    public function stop()
241
    {
242
        $this->closePipes();
243
244
        if (\is_resource($this->process)) {
245
            \proc_close($this->process);
246
        }
247
248
        $this->state = self::STATE_CLOSED;
249
250
        $this->exitCode = $this->processStatus['exitcode'];
251
252
        return $this->exitCode;
253
    }
254
255
    public function kill()
256
    {
257
        if (\is_resource($this->process)) {
258
            \proc_terminate($this->process);
259
        }
260
261
        $this->state = self::STATE_TERMINATED;
262
    }
263
264
    public function __destruct()
265
    {
266
        // If async (run in background) => we don't care if it ever closes
267
        // Otherwise, waited already till it runs or timeout occurs, in which case kill it
268
    }
269
}
270