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

JBZoo / Cli / 29985090680

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

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