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

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