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

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