• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Cli / 14065334400

02 Feb 2025 08:23PM UTC coverage: 81.315% (-0.5%) from 81.796%
14065334400

push

github

web-flow
Add PHP 8.4 compatibility and improve testing configurations (#29)

Update GitHub Actions workflow to include PHP 8.4 in the testing matrix.
Modify the test file to define supported PHP versions in a centralized
method, ensuring consistency. Additionally, create a phpstan.neon file
for improved static analysis with strict rules.

These changes enhance the project's compatibility with the latest PHP
version, ensure better test coverage, and facilitate improved code
quality through static analysis.

1014 of 1247 relevant lines covered (81.32%)

322.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.57
/src/CliCommandMultiProc.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Cli.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Cli
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Cli;
18

19
use BluePsyduck\SymfonyProcessManager\ProcessManager;
20
use JBZoo\Cli\ProgressBars\ProgressBarProcessManager;
21
use JBZoo\Utils\Cli as CliUtils;
22
use JBZoo\Utils\Sys;
23
use Symfony\Component\Console\Input\InputOption;
24
use Symfony\Component\Process\Process;
25

26
use function JBZoo\Utils\int;
27

28
abstract class CliCommandMultiProc extends CliCommand
29
{
30
    private const PM_DEFAULT_INTERVAL    = 100;
31
    private const PM_DEFAULT_START_DELAY = 1;
32
    private const PM_DEFAULT_TIMEOUT     = 7200;
33

34
    private array                      $procPool    = [];
35
    private ?ProgressBarProcessManager $progressBar = null;
36

37
    abstract protected function executeOneProcess(string $pmThreadId): int;
38

39
    /**
40
     * @return string[]
41
     */
42
    abstract protected function getListOfChildIds(): array;
43

44
    /**
45
     * {@inheritDoc}
46
     */
47
    protected function configure(): void
48
    {
49
        $this
1,144✔
50
            ->addOption(
1,144✔
51
                'pm-max',
1,144✔
52
                null,
1,144✔
53
                InputOption::VALUE_REQUIRED,
1,144✔
54
                'Process Manager. The number of processes to execute in parallel (os isolated processes)',
1,144✔
55
                'auto',
1,144✔
56
            )
1,144✔
57
            ->addOption(
1,144✔
58
                'pm-interval',
1,144✔
59
                null,
1,144✔
60
                InputOption::VALUE_REQUIRED,
1,144✔
61
                'Process Manager. The interval to use for polling the processes, in milliseconds',
1,144✔
62
                self::PM_DEFAULT_INTERVAL,
1,144✔
63
            )
1,144✔
64
            ->addOption(
1,144✔
65
                'pm-start-delay',
1,144✔
66
                null,
1,144✔
67
                InputOption::VALUE_REQUIRED,
1,144✔
68
                'Process Manager. The time to delay the start of processes to space them out, in milliseconds',
1,144✔
69
                self::PM_DEFAULT_START_DELAY,
1,144✔
70
            )
1,144✔
71
            ->addOption(
1,144✔
72
                'pm-max-timeout',
1,144✔
73
                null,
1,144✔
74
                InputOption::VALUE_REQUIRED,
1,144✔
75
                'Process Manager. The max timeout for each proccess, in seconds',
1,144✔
76
                self::PM_DEFAULT_TIMEOUT,
1,144✔
77
            )
1,144✔
78
            ->addOption(
1,144✔
79
                'pm-proc-id',
1,144✔
80
                null,
1,144✔
81
                InputOption::VALUE_REQUIRED,
1,144✔
82
                'Process Manager. Unique ID of process to execute one child proccess.',
1,144✔
83
                '',
1,144✔
84
            );
1,144✔
85

86
        parent::configure();
1,144✔
87
    }
88

89
    /**
90
     * @phan-suppress PhanPluginPossiblyStaticProtectedMethod
91
     */
92
    protected function beforeStartAllProcesses(): void
93
    {
94
        // noop
95
    }
32✔
96

97
    /**
98
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
99
     * @phan-suppress PhanPluginPossiblyStaticProtectedMethod
100
     * @phan-suppress PhanUnusedProtectedNoOverrideMethodParameter
101
     */
102
    protected function afterFinishAllProcesses(array $procPool): void
103
    {
104
        // noop
105
    }
×
106

107
    protected function executeAction(): int
108
    {
109
        $pmProcId = $this->getOptString('pm-proc-id');
192✔
110
        if ($pmProcId !== '') {
192✔
111
            return $this->executeOneProcess($pmProcId);
160✔
112
        }
113

114
        return $this->executeMultiProcessAction();
32✔
115
    }
116

117
    protected function executeMultiProcessAction(): int
118
    {
119
        $procNum  = $this->getNumberOfProcesses();
32✔
120
        $cpuCores = CliHelper::getNumberOfCpuCores();
32✔
121
        $this->_("Max number of sub-processes: {$procNum}", OutLvl::DEBUG);
32✔
122
        if ($procNum > $cpuCores) {
32✔
123
            $this->_(
32✔
124
                "The specified number of processes (--pm-max={$procNum}) "
32✔
125
                . "is more than the found number of CPU cores in the system ({$cpuCores}).",
32✔
126
                OutLvl::WARNING,
32✔
127
            );
32✔
128
        }
129

130
        $procManager = $this->initProcManager($procNum, $this->gePmInterval(), $this->getPmStartDelay());
32✔
131

132
        $procListIds = $this->getListOfChildIds();
32✔
133

134
        if (!$this->outputMode->isProgressBarDisabled()) {
32✔
135
            $this->progressBar = new ProgressBarProcessManager($this->outputMode);
×
136
            $this->progressBar->setMax(\count($procListIds));
×
137
            $this->progressBar->start();
×
138
        }
139

140
        foreach ($procListIds as $procListId) {
32✔
141
            $childProcess = $this->createSubProcess($procListId);
32✔
142
            $procManager->addProcess($childProcess);
32✔
143
        }
144

145
        $this->beforeStartAllProcesses();
32✔
146
        $procManager->waitForAllProcesses();
32✔
147
        if ($this->progressBar !== null) {
32✔
148
            $this->progressBar->finish();
×
149
            $this->_('');
×
150
        }
151

152
        $this->afterFinishAllProcesses($this->procPool);
32✔
153

154
        $errorList = $this->getErrorList();
32✔
155
        if (\count($errorList) > 0) {
32✔
156
            throw new Exception(\implode("\n" . \str_repeat('-', 60) . "\n", $errorList));
8✔
157
        }
158

159
        $warningList = $this->getWarningList();
24✔
160
        if (\count($warningList) > 0) {
24✔
161
            $this->_(\implode("\n" . \str_repeat('-', 60) . "\n", $warningList), OutLvl::WARNING);
×
162
        }
163

164
        return 0;
24✔
165
    }
166

167
    private function initProcManager(
168
        int $numberOfParallelProcesses,
169
        int $pollInterval,
170
        int $processStartDelay,
171
    ): ProcessManager {
172
        $finishCallback = function (Process $process): void {
32✔
173
            $virtProcId = \spl_object_id($process);
32✔
174

175
            $exitCode    = $process->getExitCode();
32✔
176
            $errorOutput = \trim($process->getErrorOutput());
32✔
177
            $stdOutput   = \trim($process->getOutput());
32✔
178

179
            $this->procPool[$virtProcId]['time_end']  = \microtime(true);
32✔
180
            $this->procPool[$virtProcId]['exit_code'] = $exitCode;
32✔
181
            $this->procPool[$virtProcId]['std_out']   = $stdOutput;
32✔
182

183
            if ($exitCode > 0 || $errorOutput !== '') {
32✔
184
                $this->procPool[$virtProcId]['err_out'] = $errorOutput;
8✔
185
            }
186

187
            if ($this->progressBar !== null) {
32✔
188
                $this->progressBar->advance();
×
189
            }
190
        };
32✔
191

192
        return (new ProcessManager())
32✔
193
            ->setPollInterval($pollInterval)
32✔
194
            ->setNumberOfParallelProcesses($numberOfParallelProcesses)
32✔
195
            ->setProcessStartDelay($processStartDelay)
32✔
196
            ->setProcessStartCallback(function (Process $process): void {
32✔
197
                $virtProcId                                = \spl_object_id($process);
32✔
198
                $this->procPool[$virtProcId]['time_start'] = \microtime(true);
32✔
199
            })
32✔
200
            ->setProcessFinishCallback($finishCallback)
32✔
201
            ->setProcessTimeoutCallback(function (Process $process) use ($finishCallback): void {
32✔
202
                $finishCallback($process);
×
203

204
                $virtProcId                                     = \spl_object_id($process);
×
205
                $this->procPool[$virtProcId]['reached_timeout'] = true;
×
206
            });
32✔
207
    }
208

209
    private function createSubProcess(string $procId): Process
210
    {
211
        // Prepare option list from the parent process
212
        $options = \array_filter(
32✔
213
            $this->outputMode->getInput()->getOptions(),
32✔
214
            static fn ($optionValue): bool => $optionValue !== false && $optionValue !== '',
32✔
215
        );
32✔
216

217
        foreach (\array_keys($options) as $optionKey) {
32✔
218
            if (!$this->getDefinition()->getOption((string)$optionKey)->acceptValue()) {
32✔
219
                $options[$optionKey] = null;
32✔
220
            }
221
        }
222

223
        unset($options['ansi']);
32✔
224
        $options['no-ansi']        = null;
32✔
225
        $options['no-interaction'] = null;
32✔
226
        $options['pm-proc-id']     = $procId;
32✔
227

228
        // Prepare $argument list from the parent process
229
        $arguments     = $this->outputMode->getInput()->getArguments();
32✔
230
        $argumentsList = [];
32✔
231

232
        foreach ($arguments as $argKey => $argValue) {
32✔
233
            if (\is_array($argValue)) {
32✔
234
                continue;
×
235
            }
236

237
            /** @var string $argValue */
238
            if ($argKey !== 'command') {
32✔
239
                /** @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal */
240
                $argumentsList[] = '"' . \addcslashes($argValue, '"') . '"';
32✔
241
            }
242
        }
243

244
        // Build full command line
245
        $process = Process::fromShellCommandline(
32✔
246
            CliUtils::build(
32✔
247
                \implode(
32✔
248
                    ' ',
32✔
249
                    \array_filter([
32✔
250
                        Sys::getBinary(),
32✔
251
                        CliHelper::getBinPath(),
32✔
252
                        $this->getName(),
32✔
253
                        \implode(' ', $argumentsList),
32✔
254
                    ]),
32✔
255
                ),
32✔
256
                $options,
32✔
257
            ),
32✔
258
            CliHelper::getRootPath(),
32✔
259
            null,
32✔
260
            null,
32✔
261
            $this->getMaxTimeout(),
32✔
262
        );
32✔
263

264
        $this->procPool[\spl_object_id($process)] = [
32✔
265
            'command'         => $process->getCommandLine(),
32✔
266
            'proc_id'         => $procId,
32✔
267
            'exit_code'       => null,
32✔
268
            'std_out'         => null,
32✔
269
            'err_out'         => null,
32✔
270
            'reached_timeout' => false,
32✔
271
            'time_start'      => null,
32✔
272
            'time_end'        => null,
32✔
273
        ];
32✔
274

275
        return $process;
32✔
276
    }
277

278
    private function getErrorList(): array
279
    {
280
        return \array_reduce($this->procPool, function (array $acc, array $procInfo): array {
32✔
281
            if ($procInfo['reached_timeout']) {
32✔
282
                $acc[] = \implode("\n", [
×
283
                    "Command : {$procInfo['command']}",
×
284
                    "Error   : The process with ID \"{$procInfo['proc_id']}\""
×
285
                    . " exceeded the timeout of {$this->getMaxTimeout()} seconds.",
×
286
                ]);
×
287
            } elseif ($procInfo['err_out'] && $procInfo['exit_code'] > 0) {
32✔
288
                $acc[] = \implode("\n", [
8✔
289
                    "Command : {$procInfo['command']}",
8✔
290
                    "Code    : {$procInfo['exit_code']}",
8✔
291
                    "Error   : {$procInfo['err_out']}",
8✔
292
                    "StdOut  : {$procInfo['std_out']}",
8✔
293
                ]);
8✔
294
            }
295

296
            return $acc;
32✔
297
        }, []);
32✔
298
    }
299

300
    private function getWarningList(): array
301
    {
302
        return \array_reduce($this->procPool, static function (array $acc, array $procInfo): array {
24✔
303
            if ($procInfo['err_out'] && $procInfo['exit_code'] === 0) {
24✔
304
                $acc[] = \implode("\n", [
×
305
                    "Command : {$procInfo['command']}",
×
306
                    "Warning : {$procInfo['err_out']}",
×
307
                    "StdOut  : {$procInfo['std_out']}",
×
308
                ]);
×
309
            }
310

311
            return $acc;
24✔
312
        }, []);
24✔
313
    }
314

315
    private function getMaxTimeout(): int
316
    {
317
        $pmMaxTimeout = $this->getOptInt('pm-max-timeout');
32✔
318

319
        return $pmMaxTimeout > 0 ? $pmMaxTimeout : self::PM_DEFAULT_TIMEOUT;
32✔
320
    }
321

322
    private function gePmInterval(): int
323
    {
324
        $pmInterval = $this->getOptInt('pm-interval');
32✔
325

326
        return $pmInterval > 0 ? $pmInterval : self::PM_DEFAULT_INTERVAL;
32✔
327
    }
328

329
    private function getPmStartDelay(): int
330
    {
331
        $pmStartDelay = $this->getOptInt('pm-start-delay');
32✔
332

333
        return $pmStartDelay > 0 ? $pmStartDelay : self::PM_DEFAULT_START_DELAY;
32✔
334
    }
335

336
    private function getNumberOfProcesses(): int
337
    {
338
        $pmMax    = \strtolower($this->getOptString('pm-max'));
32✔
339
        $cpuCores = CliHelper::getNumberOfCpuCores();
32✔
340

341
        if ($pmMax === 'auto') {
32✔
342
            return $cpuCores;
×
343
        }
344

345
        $pmMaxInt = \abs(int($pmMax));
32✔
346

347
        return $pmMaxInt > 0 ? $pmMaxInt : $cpuCores;
32✔
348
    }
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc