Passed
Pull Request — master (#9)
by
unknown
02:17
created

Operation::getLoggingFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Covert;
5
6
use Closure;
7
use Covert\Utils\FunctionReflection;
8
use Covert\Utils\OperatingSystem;
9
use Exception;
10
11
class Operation
12
{
13
    /**
14
     * The absolute path to the autoload.php file.
15
     *
16
     * @var string
17
     */
18
    private $autoload;
19
20
    /**
21
     * The absolute path to the output log file.
22
     *
23
     * @var bool|string
24
     */
25
    private $logging;
26
27
    /**
28
     * The process ID (pid) of the background task.
29
     *
30
     * @var int|null
31
     */
32
    private $processId;
33
34
    /**
35
     * Create a new operation instance.
36
     *
37
     * @param null $processId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $processId is correct as it would always require null to be passed?
Loading history...
38
     *
39
     * @throws \Exception
40
     */
41
    public function __construct($processId = null)
42
    {
43
        try {
44
            // If we run UnitTests this will throw Exception.
45
            $this->setAutoloadFile(__DIR__.'/../../../autoload.php');
46
        } catch (Exception $e) {
47
            // Set it to false whene running UnitTests
48
            $this->setAutoloadFile(false);
49
        }
50
51
        $this->setLoggingFile(false);
52
        $this->processId = $processId;
53
    }
54
55
    /**
56
     * Statically create an instance of an operation from an existing
57
     * process ID.
58
     *
59
     * @param int $processId
60
     *
61
     * @throws \Exception
62
     *
63
     * @return self
64
     */
65
    public static function withId(int $processId): self
66
    {
67
        return new self($processId);
68
    }
69
70
    /**
71
     * Execute the process.
72
     *
73
     * @param \Closure $closure The anonymous function to execute.
74
     *
75
     * @throws \ReflectionException
76
     *
77
     * @return self
78
     */
79
    public function execute(Closure $closure): self
80
    {
81
        $temporaryFile = tempnam(sys_get_temp_dir(), 'covert');
82
        $temporaryContent = '<?php'.PHP_EOL.PHP_EOL;
83
84
        if ($this->autoload !== false) {
0 ignored issues
show
introduced by
The condition $this->autoload !== false is always true.
Loading history...
85
            $temporaryContent .= "require('$this->autoload');".PHP_EOL.PHP_EOL;
86
        }
87
88
        $temporaryContent .= FunctionReflection::toString($closure).PHP_EOL.PHP_EOL;
89
        $temporaryContent .= 'unlink(__FILE__);'.PHP_EOL.PHP_EOL;
90
        $temporaryContent .= 'exit;';
91
92
        file_put_contents($temporaryFile, $temporaryContent);
93
94
        $this->processId = $this->executeFile($temporaryFile);
95
96
        return $this;
97
    }
98
99
    /**
100
     * Check the operating system call appropriate execution method.
101
     *
102
     * @param string $file The absolute path to the executing file.
103
     *
104
     * @throws \Exception
105
     *
106
     * @return int
107
     */
108
    private function executeFile(string $file): int
109
    {
110
        if (OperatingSystem::isWindows()) {
111
            return $this->runCommandForWindows($file);
112
        }
113
114
        return $this->runCommandForNix($file);
115
    }
116
117
    /**
118
     * Execute the shell process for the Windows platform.
119
     *
120
     * @param string $file The absolute path to the executing file.
121
     *
122
     * @throws \Exception
123
     *
124
     * @return int
125
     */
126
    private function runCommandForWindows(string $file): int
127
    {
128
        if ($this->getLoggingFile()) {
129
            $stdoutPipe = ['file', $this->getLoggingFile(), 'w'];
130
            $stderrPipe = ['file', $this->getLoggingFile(), 'w'];
131
        } else {
132
            $stdoutPipe = fopen('NUL', 'c');
133
            $stderrPipe = fopen('NUL', 'c');
134
        }
135
136
        $desc = [
137
            ['pipe', 'r'],
138
            $stdoutPipe,
139
            $stderrPipe,
140
        ];
141
142
        $cmd = "START /b php {$file}";
143
144
        $handle = proc_open(
145
            $cmd,
146
            $desc,
147
            $pipes,
148
            getcwd()
149
        );
150
151
        if (!is_resource($handle)) {
152
            throw new Exception('Could not create a background resource. Try using a better operating system.');
153
        }
154
155
        $pid = proc_get_status($handle)['pid'];
156
157
        try {
158
            proc_close($handle);
159
            $resource = array_filter(explode(' ', shell_exec("wmic process get parentprocessid, processid | find \"$pid\"") ?? ''));
160
            array_pop($resource);
161
            $pid = end($resource);
162
        } catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
163
        }
164
165
        return (int) $pid;
166
    }
167
168
    /**
169
     * Execute the shell process for the *nix platform.
170
     *
171
     * @param string $file The absolute path to the executing file.
172
     *
173
     * @return int
174
     */
175
    private function runCommandForNix(string $file): int
176
    {
177
        $cmd = "php {$file} ";
178
179
        if (!$this->getLoggingFile()) {
180
            $cmd .= '> /dev/null 2>&1 & echo $!';
181
        } else {
182
            $cmd .= "> {$this->getLoggingFile()} & echo $!";
183
        }
184
185
        return (int) shell_exec($cmd);
186
    }
187
188
    /**
189
     * Set a custom path to the autoload.php file.
190
     *
191
     * @param string|bool $autoload The absolute path to autoload.php file
192
     *
193
     * @throws \Exception
194
     *
195
     * @return self
196
     */
197
    public function setAutoloadFile($autoload): self
198
    {
199
        if ($autoload !== false) {
200
            if (!$autoload = realpath($autoload)) {
0 ignored issues
show
Bug introduced by
It seems like $autoload can also be of type true; however, parameter $path of realpath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

200
            if (!$autoload = realpath(/** @scrutinizer ignore-type */ $autoload)) {
Loading history...
201
                throw new Exception("The autoload path '{$autoload}' doesn't exist.");
202
            }
203
        }
204
205
        $this->autoload = $autoload;
0 ignored issues
show
Documentation Bug introduced by
It seems like $autoload can also be of type boolean. However, the property $autoload is declared as type string. 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...
206
207
        return $this;
208
    }
209
210
    /**
211
     * Set a custom path to the output logging file.
212
     *
213
     * @param string|bool $logging The absolute path to the output logging file.
214
     *
215
     * @return self
216
     */
217
    public function setLoggingFile($logging): self
218
    {
219
        $this->logging = $logging;
220
221
        return $this;
222
    }
223
224
    /**
225
     * Get a custom path to the output logging file.
226
     *
227
     * @return string|bool
228
     */
229
    public function getLoggingFile()
230
    {
231
        return $this->logging;
232
    }
233
234
    /**
235
     * Get the process ID of the task running as a system process.
236
     *
237
     * @return int|null
238
     */
239
    public function getProcessId()
240
    {
241
        return $this->processId;
242
    }
243
244
    /**
245
     * Returns true if the process ID is still active.
246
     *
247
     * @return bool
248
     */
249
    public function isRunning(): bool
250
    {
251
        $processId = $this->getProcessId();
252
253
        if (OperatingSystem::isWindows()) {
254
            $pids = shell_exec("wmic process get processid | find \"{$processId}\"") ?? '';
255
            $resource = array_filter(explode(' ', $pids));
256
257
            $isRunning = count($resource) > 0 && $processId == reset($resource);
258
        } else {
259
            $isRunning = (bool) posix_getsid($processId);
260
        }
261
262
        return $isRunning;
263
    }
264
265
    /**
266
     * Kill the current operation process if it is running.
267
     *
268
     * @return self
269
     */
270
    public function kill(): self
271
    {
272
        if ($this->isRunning()) {
273
            $processId = $this->getProcessId();
274
275
            if (OperatingSystem::isWindows()) {
276
                $cmd = "taskkill /pid {$processId} -t -f";
277
            } else {
278
                $cmd = "kill -9 {$processId}";
279
            }
280
281
            shell_exec($cmd);
282
        }
283
284
        return $this;
285
    }
286
}
287