Completed
Pull Request — master (#39)
by
unknown
01:31
created

Shell::execute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 18
rs 9.9666
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_TERMINATED = 'terminated';
30
31
    /** @var string Command to be executed */
32
    protected $command;
33
34
    /** @var array Descriptor to be passed for proc_open */
35
    protected $descriptors;
36
37
    /** @var int Exit code of the process once it has been terminated */
38
    protected $exitCode = null;
39
40
    /** @var string Input for stdin */
41
    protected $input;
42
43
    /** @var array Pointers to stdin, stdout & stderr */
44
    protected $pipes = null;
45
46
    /** @var resource The actual process resource returned from proc_open */
47
    protected $process = null;
48
49
    /** @var string Current state of the shell execution */
50
    protected $state = self::STATE_READY;
51
52
    /** @var string Status of the process as returned from proc_get_status */
53
    protected $processStatus = null;
54
55
    public function __construct(string $command, string $input = null)
56
    {
57
        if (!\function_exists('proc_open')) {
58
            throw new RuntimeException('Required proc_open could not be found in your PHP setup');
59
        }
60
61
        $this->command = $command;
62
        $this->input   = $input;
63
    }
64
65
    protected function getDescriptors()
66
    {
67
        $out = '\\' === \DIRECTORY_SEPARATOR ? ['file', 'NUL', 'w'] : ['pipe', 'w'];
68
69
        return [
70
            self::STDIN_DESCRIPTOR_KEY  => ['pipe', 'r'],
71
            self::STDOUT_DESCRIPTOR_KEY => $out,
72
            self::STDERR_DESCRIPTOR_KEY => $out,
73
        ];
74
    }
75
76
    protected function setInput()
77
    {
78
        \fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input);
79
    }
80
81
    protected function updateProcessStatus()
82
    {
83
        if (self::STATE_STARTED !== $this->state) {
84
            return;
85
        }
86
87
        $this->processStatus = \proc_get_status($this->process);
0 ignored issues
show
Documentation Bug introduced by
It seems like proc_get_status($this->process) of type false or array is incompatible with the declared type string of property $processStatus.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
88
89
        if ($this->processStatus['running'] === false && $this->exitCode === null) {
90
            $this->exitCode = $this->processStatus['exitcode'];
91
        }
92
    }
93
94
    protected function closePipes()
95
    {
96
        \fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]);
97
        \fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
98
        \fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
99
    }
100
101
    public function execute()
102
    {
103
        if ($this->isRunning()) {
104
            throw new RuntimeException('Process is already running');
105
        }
106
107
        $this->descriptors = $this->getDescriptors();
108
109
        $this->process = proc_open($this->command, $this->descriptors, $this->pipes);
0 ignored issues
show
Documentation Bug introduced by
It seems like proc_open($this->command...criptors, $this->pipes) 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...
110
111
        if (!\is_resource($this->process)) {
112
            throw new RuntimeException('Bad program could not be started.');
113
        }
114
115
        $this->state = self::STATE_STARTED;
116
117
        $this->setInput();
118
        $this->updateProcessStatus();
119
    }
120
121
    public function getState()
122
    {
123
        $this->updateProcessStatus();
124
125
        return $this->state;
126
    }
127
128
    public function getOutput()
129
    {
130
        return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]);
131
    }
132
133
    public function getErrorOutput()
134
    {
135
        return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]);
136
    }
137
138
    public function getExitCode()
139
    {
140
        $this->updateProcessStatus();
141
142
        return $this->exitCode;
143
    }
144
145
    public function isRunning()
146
    {
147
        if (self::STATE_STARTED !== $this->state) {
148
            return false;
149
        }
150
151
        $this->updateProcessStatus();
152
153
        return $this->processStatus['running'];
154
    }
155
156
    public function getProcessId()
157
    {
158
        return $this->isRunning() ? $this->processStatus['pid'] : null;
159
    }
160
161
    public function stop()
162
    {
163
        $this->closePipes();
164
165
        if (\is_resource($this->process)) {
166
            \proc_close($this->process);
167
        }
168
169
        $this->state    = self::STATE_TERMINATED;
170
        $this->exitCode = $this->processStatus['exitcode'];
171
172
        return $this->exitCode;
173
    }
174
175
    public function kill()
176
    {
177
        return \proc_terminate($this->process);
178
    }
179
180
    public function __destruct()
181
    {
182
        $this->stop();
183
    }
184
}