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

JBZoo / Cli / 34579555474

20 Jul 2026 08:59PM UTC coverage: 82.146%. Remained the same
34579555474

push

github

web-flow
Merge pull request #37 from JBZoo/release/8.0

8.0.0 — PHP 8.3+ floor & lock-step major

3 of 3 new or added lines in 3 files covered. (100.0%)

11 existing lines in 2 files now uncovered.

1072 of 1305 relevant lines covered (82.15%)

235.21 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 JBZoo\Cli\ProcessManager\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
/**
29
 * @psalm-suppress UnusedClass
30
 */
31
abstract class CliCommandMultiProc extends CliCommand
32
{
33
    private const int PM_DEFAULT_INTERVAL    = 100;
34
    private const int PM_DEFAULT_START_DELAY = 1;
35
    private const int PM_DEFAULT_TIMEOUT     = 7200;
36

37
    private array                      $procPool    = [];
38
    private ?ProgressBarProcessManager $progressBar = null;
39

40
    abstract protected function executeOneProcess(string $pmThreadId): int;
41

42
    /**
43
     * @return string[]
44
     */
45
    abstract protected function getListOfChildIds(): array;
46

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

89
        parent::configure();
864 ✔
90
    }
91

92
    /**
93
     * @phan-suppress PhanPluginPossiblyStaticProtectedMethod
94
     */
95
    protected function beforeStartAllProcesses(): void
96
    {
97
        // noop
98
    }
24 ✔
99

100
    /**
101
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
102
     * @phan-suppress PhanPluginPossiblyStaticProtectedMethod
103
     * @phan-suppress PhanUnusedProtectedNoOverrideMethodParameter
104
     * @psalm-suppress PossiblyUnusedParam
105
     */
106
    protected function afterFinishAllProcesses(array $procPool): void
107
    {
108
        // noop
109
    }
×
110

111
    protected function executeAction(): int
112
    {
113
        $pmProcId = $this->getOptString('pm-proc-id');
144 ✔
114
        if ($pmProcId !== '') {
144 ✔
115
            return $this->executeOneProcess($pmProcId);
120 ✔
116
        }
117

118
        return $this->executeMultiProcessAction();
24 ✔
119
    }
120

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

134
        $procManager = $this->initProcManager($procNum, $this->gePmInterval(), $this->getPmStartDelay());
24 ✔
135

136
        $procListIds = $this->getListOfChildIds();
24 ✔
137

138
        if (!$this->outputMode->isProgressBarDisabled()) {
24 ✔
139
            $this->progressBar = new ProgressBarProcessManager($this->outputMode);
×
140
            $this->progressBar->setMax(\count($procListIds));
×
141
            $this->progressBar->start();
×
142
        }
143

144
        foreach ($procListIds as $procListId) {
24 ✔
145
            $childProcess = $this->createSubProcess($procListId);
24 ✔
146
            $procManager->addProcess($childProcess);
24 ✔
147
        }
148

149
        $this->beforeStartAllProcesses();
24 ✔
150
        $procManager->waitForAllProcesses();
24 ✔
151
        if ($this->progressBar !== null) {
24 ✔
152
            $this->progressBar->finish();
×
153
            $this->_('');
×
154
        }
155

156
        $this->afterFinishAllProcesses($this->procPool);
24 ✔
157

158
        $errorList = $this->getErrorList();
24 ✔
159
        if (\count($errorList) > 0) {
24 ✔
160
            throw new Exception(\implode("\n" . \str_repeat('-', 60) . "\n", $errorList));
6 ✔
161
        }
162

163
        $warningList = $this->getWarningList();
18 ✔
164
        if (\count($warningList) > 0) {
18 ✔
165
            $this->_(\implode("\n" . \str_repeat('-', 60) . "\n", $warningList), OutLvl::WARNING);
×
166
        }
167

168
        return 0;
18 ✔
169
    }
170

171
    private function initProcManager(
172
        int $numberOfParallelProcesses,
173
        int $pollInterval,
174
        int $processStartDelay,
175
    ): ProcessManager {
176
        $finishCallback = function (Process $process): void {
24 ✔
177
            $virtProcId = \spl_object_id($process);
24 ✔
178

179
            $exitCode    = $process->getExitCode();
24 ✔
180
            $errorOutput = \trim($process->getErrorOutput());
24 ✔
181
            $stdOutput   = \trim($process->getOutput());
24 ✔
182

183
            $this->procPool[$virtProcId]['time_end']  = \microtime(true);
24 ✔
184
            $this->procPool[$virtProcId]['exit_code'] = $exitCode;
24 ✔
185
            $this->procPool[$virtProcId]['std_out']   = $stdOutput;
24 ✔
186

187
            if ($exitCode > 0 || $errorOutput !== '') {
24 ✔
188
                $this->procPool[$virtProcId]['err_out'] = $errorOutput;
6 ✔
189
            }
190

191
            if ($this->progressBar !== null) {
24 ✔
192
                $this->progressBar->advance();
×
193
            }
194
        };
24 ✔
195

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

208
                $virtProcId                                     = \spl_object_id($process);
×
209
                $this->procPool[$virtProcId]['reached_timeout'] = true;
×
210
            });
24 ✔
211
    }
212

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

221
        foreach (\array_keys($options) as $optionKey) {
24 ✔
222
            if (!$this->getDefinition()->getOption((string)$optionKey)->acceptValue()) {
24 ✔
223
                $options[$optionKey] = null;
24 ✔
224
            }
225
        }
226

227
        unset($options['ansi']);
24 ✔
228
        $options['no-ansi']        = null;
24 ✔
229
        $options['no-interaction'] = null;
24 ✔
230
        $options['pm-proc-id']     = $procId;
24 ✔
231

232
        // Prepare $argument list from the parent process
233
        $arguments     = $this->outputMode->getInput()->getArguments();
24 ✔
234
        $argumentsList = [];
24 ✔
235

236
        foreach ($arguments as $argKey => $argValue) {
24 ✔
237
            if (\is_array($argValue)) {
24 ✔
238
                continue;
×
239
            }
240

241
            if ($argKey !== 'command') {
24 ✔
242
                $argumentsList[] = '"' . \addcslashes((string)$argValue, '"') . '"';
24 ✔
243
            }
244
        }
245

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

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

277
        return $process;
24 ✔
278
    }
279

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

298
            return $acc;
24 ✔
299
        }, []);
24 ✔
300
    }
301

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

313
            return $acc;
18 ✔
314
        }, []);
18 ✔
315
    }
316

317
    private function getMaxTimeout(): int
318
    {
319
        $pmMaxTimeout = $this->getOptInt('pm-max-timeout');
24 ✔
320

321
        return $pmMaxTimeout > 0 ? $pmMaxTimeout : self::PM_DEFAULT_TIMEOUT;
24 ✔
322
    }
323

324
    private function gePmInterval(): int
325
    {
326
        $pmInterval = $this->getOptInt('pm-interval');
24 ✔
327

328
        return $pmInterval > 0 ? $pmInterval : self::PM_DEFAULT_INTERVAL;
24 ✔
329
    }
330

331
    private function getPmStartDelay(): int
332
    {
333
        $pmStartDelay = $this->getOptInt('pm-start-delay');
24 ✔
334

335
        return $pmStartDelay > 0 ? $pmStartDelay : self::PM_DEFAULT_START_DELAY;
24 ✔
336
    }
337

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

343
        if ($pmMax === 'auto') {
24 ✔
UNCOV
344
            return $cpuCores;
×
345
        }
346

347
        $pmMaxInt = \abs(int($pmMax));
24 ✔
348

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

© 2026 Coveralls, Inc