Operation::executeFile()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 1
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
     * Information is process running.
44
     *
45
     * @var bool
46
     */
47
    private $isRunning = null;
48
49
    /**
50
     * Create a new operation instance.
51
     *
52
     * @param null|int $processId
53
     *
54
     * @throws \Exception
55
     */
56
    public function __construct($processId = null)
57
    {
58
        try {
59
            // If we run UnitTests this will throw Exception.
60
            $this->setAutoloadFile(__DIR__.'/../../../autoload.php');
61
        } catch (Exception $e) {
62
            // Set it to false whene running UnitTests
63
            $this->setAutoloadFile(false);
64
        }
65
66
        $this->setLoggingFile(false);
67
        $this->processId = $processId;
68
    }
69
70
    /**
71
     * Statically create an instance of an operation from an existing
72
     * process ID.
73
     *
74
     * @param int $processId
75
     *
76
     * @throws \Exception
77
     *
78
     * @return self
79
     */
80
    public static function withId(int $processId): self
81
    {
82
        return new self($processId);
83
    }
84
85
    /**
86
     * Execute the process.
87
     *
88
     * @param \Closure $closure The anonymous function to execute.
89
     *
90
     * @throws \ReflectionException
91
     *
92
     * @return self
93
     */
94
    public function execute(Closure $closure): self
95
    {
96
        $temporaryFile = tempnam(sys_get_temp_dir(), 'covert');
97
        $temporaryContent = '<?php'.PHP_EOL.PHP_EOL;
98
99
        if (!empty($this->autoload)) {
100
            $temporaryContent .= "require('$this->autoload');".PHP_EOL.PHP_EOL;
101
        }
102
103
        $temporaryContent .= FunctionReflection::toString($closure).PHP_EOL.PHP_EOL;
104
        $temporaryContent .= 'unlink(__FILE__);'.PHP_EOL.PHP_EOL;
105
        $temporaryContent .= 'exit;';
106
107
        file_put_contents($temporaryFile, $temporaryContent);
108
109
        $this->processId = $this->executeFile($temporaryFile);
110
111
        return $this;
112
    }
113
114
    /**
115
     * Check the operating system call appropriate execution method.
116
     *
117
     * @param string $file The absolute path to the executing file.
118
     *
119
     * @throws \Exception
120
     *
121
     * @return int
122
     */
123
    private function executeFile(string $file): int
124
    {
125
        $this->isRunning = true;
126
127
        if (OperatingSystem::isWindows()) {
128
            return $this->runCommandForWindows($file);
129
        }
130
131
        return $this->runCommandForNix($file);
132
    }
133
134
    /**
135
     * Execute the shell process for the Windows platform.
136
     *
137
     * @param string $file The absolute path to the executing file.
138
     *
139
     * @throws \Exception
140
     *
141
     * @return int
142
     */
143
    private function runCommandForWindows(string $file): int
144
    {
145
        if ($this->getLoggingFile()) {
146
            $stdoutPipe = ['file', $this->getLoggingFile(), 'w'];
147
            $stderrPipe = ['file', $this->getLoggingFile(), 'w'];
148
        } else {
149
            $stdoutPipe = fopen('NUL', 'c');
150
            $stderrPipe = fopen('NUL', 'c');
151
        }
152
153
        $desc = [
154
            ['pipe', 'r'],
155
            $stdoutPipe,
156
            $stderrPipe,
157
        ];
158
159
        $cmd = 'START /b '.$this->getCommand()." {$file}";
160
161
        $handle = proc_open(
162
            $cmd,
163
            $desc,
164
            $pipes,
165
            getcwd()
166
        );
167
168
        if (!is_resource($handle)) {
169
            throw new Exception('Could not create a background resource. Try using a better operating system.');
170
        }
171
172
        $pid = proc_get_status($handle)['pid'];
173
        proc_close($handle);
174
        $pid = shell_exec('powershell.exe -Command "(Get-CimInstance -Class Win32_Process -Filter \'parentprocessid='.$pid.'\').processid"');
175
176
        return (int) $pid;
177
    }
178
179
    /**
180
     * Execute the shell process for the *nix platform.
181
     *
182
     * @param string $file The absolute path to the executing file.
183
     *
184
     * @return int
185
     */
186
    private function runCommandForNix(string $file): int
187
    {
188
        $cmd = $this->getCommand()." {$file} ";
189
190
        if (!$this->getLoggingFile()) {
191
            $cmd .= '> /dev/null 2>&1 & echo $!';
192
        } else {
193
            $cmd .= "> {$this->getLoggingFile()} & echo $!";
194
        }
195
196
        return (int) shell_exec($cmd);
197
    }
198
199
    /**
200
     * Set a custom path to the autoload.php file.
201
     *
202
     * @param string|bool $autoload The absolute path to autoload.php file
203
     *
204
     * @throws \Exception
205
     *
206
     * @return self
207
     */
208
    public function setAutoloadFile($autoload): self
209
    {
210
        if (is_string($autoload)) {
211
            if (!$autoload = realpath($autoload)) {
212
                throw new Exception("The autoload path '{$autoload}' doesn't exist.");
213
            }
214
215
            $this->autoload = $autoload;
216
        }
217
218
        return $this;
219
    }
220
221
    /**
222
     * Set a custom path to the output logging file.
223
     *
224
     * @param string|bool $logging The absolute path to the output logging file.
225
     *
226
     * @return self
227
     */
228
    public function setLoggingFile($logging): self
229
    {
230
        $this->logging = $logging;
231
232
        return $this;
233
    }
234
235
    /**
236
     * Get a custom path to the output logging file.
237
     *
238
     * @return string|bool
239
     */
240
    public function getLoggingFile()
241
    {
242
        return $this->logging;
243
    }
244
245
    /**
246
     * Get command to run PHP.
247
     *
248
     * @return string
249
     */
250
    public function getCommand()
251
    {
252
        return $this->command;
253
    }
254
255
    /**
256
     * Set command to run PHP.
257
     *
258
     * @param string $command
259
     */
260
    public function setCommand($command)
261
    {
262
        $this->command = $command;
263
    }
264
265
    /**
266
     * Get the process ID of the task running as a system process.
267
     *
268
     * @return int|null
269
     */
270
    public function getProcessId()
271
    {
272
        return $this->processId;
273
    }
274
275
    /**
276
     * Returns true if the process ID is still active.
277
     *
278
     * @return bool
279
     */
280
    public function isRunning(): bool
281
    {
282
        /*
283
         * If we do not check it before or last time process was running,
284
         * check its current status, otherwise it was running and was ended.
285
         */
286
        if ($this->isRunning === null || $this->isRunning === true) {
287
            if ($processId = $this->getProcessId()) {
288
                if (OperatingSystem::isWindows()) {
289
                    $this->isRunning = !empty(shell_exec('powershell.exe -Command "Get-CimInstance -Class Win32_Process -Filter \'processid='.$processId.'\'"'));
290
                } else {
291
                    $this->isRunning = (bool) posix_getsid($processId);
292
                }
293
            }
294
        }
295
296
        return (bool) $this->isRunning;
297
    }
298
299
    /**
300
     * Kill the current operation process if it is running.
301
     *
302
     * @return self
303
     */
304
    public function kill(): self
305
    {
306
        if ($this->isRunning()) {
307
            $processId = $this->getProcessId();
308
309
            if (OperatingSystem::isWindows()) {
310
                $cmd = "taskkill /pid {$processId} -t -f";
311
            } else {
312
                $cmd = "kill -9 {$processId}";
313
            }
314
315
            shell_exec($cmd);
316
        }
317
318
        return $this;
319
    }
320
}
321