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

JBZoo / Cli / 7685763438

28 Jan 2024 12:35PM UTC coverage: 80.746% (-3.1%) from 83.803%
7685763438

push

github

web-flow
Add support for PHP 8.3 and update Symfony dependencies to ^6.4 (#24)

* Add support for PHP 8.3 and update Symfony dependencies to ^6.4
* Fix default value handling in CliCommand

0 of 3 new or added lines in 1 file covered. (0.0%)

41 existing lines in 5 files now uncovered.

952 of 1179 relevant lines covered (80.75%)

234.54 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
876✔
50
            ->addOption(
876✔
51
                'pm-max',
876✔
52
                null,
876✔
53
                InputOption::VALUE_REQUIRED,
876✔
54
                'Process Manager. The number of processes to execute in parallel (os isolated processes)',
876✔
55
                'auto',
876✔
56
            )
876✔
57
            ->addOption(
876✔
58
                'pm-interval',
876✔
59
                null,
876✔
60
                InputOption::VALUE_REQUIRED,
876✔
61
                'Process Manager. The interval to use for polling the processes, in milliseconds',
876✔
62
                self::PM_DEFAULT_INTERVAL,
876✔
63
            )
876✔
64
            ->addOption(
876✔
65
                'pm-start-delay',
876✔
66
                null,
876✔
67
                InputOption::VALUE_REQUIRED,
876✔
68
                'Process Manager. The time to delay the start of processes to space them out, in milliseconds',
876✔
69
                self::PM_DEFAULT_START_DELAY,
876✔
70
            )
876✔
71
            ->addOption(
876✔
72
                'pm-max-timeout',
876✔
73
                null,
876✔
74
                InputOption::VALUE_REQUIRED,
876✔
75
                'Process Manager. The max timeout for each proccess, in seconds',
876✔
76
                self::PM_DEFAULT_TIMEOUT,
876✔
77
            )
876✔
78
            ->addOption(
876✔
79
                'pm-proc-id',
876✔
80
                null,
876✔
81
                InputOption::VALUE_REQUIRED,
876✔
82
                'Process Manager. Unique ID of process to execute one child proccess.',
876✔
83
                '',
876✔
84
            );
876✔
85

86
        parent::configure();
876✔
87
    }
88

89
    /**
90
     * @phan-suppress PhanPluginPossiblyStaticProtectedMethod
91
     */
92
    protected function beforeStartAllProcesses(): void
93
    {
94
        // noop
95
    }
24✔
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
UNCOV
105
    }
×
106

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

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

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

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

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

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

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

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

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

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

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

164
        return 0;
18✔
165
    }
166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

275
        return $process;
24✔
276
    }
277

278
    private function getErrorList(): array
279
    {
280
        return \array_reduce($this->procPool, function (array $acc, array $procInfo): array {
24✔
281
            if ($procInfo['reached_timeout']) {
24✔
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.",
×
UNCOV
286
                ]);
×
287
            } elseif ($procInfo['err_out'] && $procInfo['exit_code'] > 0) {
24✔
288
                $acc[] = \implode("\n", [
6✔
289
                    "Command : {$procInfo['command']}",
6✔
290
                    "Code    : {$procInfo['exit_code']}",
6✔
291
                    "Error   : {$procInfo['err_out']}",
6✔
292
                    "StdOut  : {$procInfo['std_out']}",
6✔
293
                ]);
6✔
294
            }
295

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

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

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

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

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

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

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

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

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

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

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

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

347
        return $pmMaxInt > 0 ? $pmMaxInt : $cpuCores;
24✔
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

© 2025 Coveralls, Inc