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

valksor / php-dev-build / 21323318062

24 Jan 2026 11:21PM UTC coverage: 27.706% (-2.8%) from 30.503%
21323318062

push

github

k0d3r1s
wip

1 of 2 new or added lines in 2 files covered. (50.0%)

909 existing lines in 16 files now uncovered.

791 of 2855 relevant lines covered (27.71%)

0.96 hits per line

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

30.16
/Service/ProcessManager.php
1
<?php declare(strict_types = 1);
2

3
/*
4
 * This file is part of the Valksor package.
5
 *
6
 * (c) Davis Zalitis (k0d3r1s)
7
 * (c) SIA Valksor <packages@valksor.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace ValksorDev\Build\Service;
14

15
use JetBrains\PhpStorm\NoReturn;
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Style\SymfonyStyle;
18
use Symfony\Component\Process\Exception\ProcessTimedOutException;
19
use Symfony\Component\Process\Process;
20
use ValksorDev\Build\Util\ConsoleCommandBuilder;
21

22
use function array_filter;
23
use function array_merge;
24
use function array_shift;
25
use function array_slice;
26
use function count;
27
use function date;
28
use function explode;
29
use function function_exists;
30
use function implode;
31
use function min;
32
use function pcntl_async_signals;
33
use function pcntl_signal;
34
use function sprintf;
35
use function strtoupper;
36
use function time;
37
use function trim;
38
use function ucfirst;
39
use function usleep;
40

41
use const SIGINT;
42
use const SIGTERM;
43

44
/**
45
 * Process lifecycle manager for the Valksor build system.
46
 *
47
 * This class manages background processes used in watch mode, providing:
48
 * - Process tracking and health monitoring
49
 * - Graceful shutdown handling with signal management
50
 * - Process status reporting and failure detection
51
 * - Interactive vs non-interactive process execution modes
52
 * - Multi-process coordination and cleanup strategies
53
 *
54
 * The manager ensures all build services (Tailwind, Importmap, Hot Reload) can run
55
 * simultaneously while providing proper cleanup and error handling.
56
 */
57
final class ProcessManager
58
{
59
    private const int FAILURE_WINDOW_SECONDS = 30;
60

61
    /**
62
     * Configuration for restart logic.
63
     */
64
    private const int MAX_FAILURES_IN_WINDOW = 5;
65

66
    /**
67
     * Maximum number of lines to keep in output buffers per service.
68
     * Prevents memory exhaustion from long-running services.
69
     */
70
    private const int MAX_OUTPUT_LINES = 100;
71
    // 1 hour
72

73
    /**
74
     * Error output buffers for capturing service stderr output.
75
     * Maps service names to arrays of recent error lines with timestamps.
76
     *
77
     * @var array<string,array<string>>
78
     */
79
    private array $errorBuffers = [];
80

81
    /**
82
     * Track failure timestamps for restart logic.
83
     * Maps process names to arrays of failure timestamps.
84
     *
85
     * @var array<string,array<int>>
86
     */
87
    private array $failureHistory = [];
88

89
    /**
90
     * Output buffers for capturing service stdout output.
91
     * Maps service names to arrays of recent output lines with timestamps.
92
     *
93
     * @var array<string,array<string>>
94
     */
95
    private array $outputBuffers = [];
96

97
    /**
98
     * Store original command arguments for restart functionality.
99
     * Maps process names to their command arguments.
100
     *
101
     * @var array<string,array<string>>
102
     */
103
    private array $processArgs = [];
104

105
    /**
106
     * Mapping of service names to process identifiers.
107
     * Used for logging and process identification.
108
     *
109
     * @var array<string,string>
110
     */
111
    private array $processNames = [];
112

113
    /**
114
     * Track parent-child relationships between processes.
115
     * Maps child process names to parent process names.
116
     *
117
     * @var array<string,string|null>
118
     */
119
    private array $processParents = [];
120

121
    /**
122
     * Registry of managed background processes.
123
     * Maps service names to Symfony Process instances for lifecycle management.
124
     *
125
     * @var array<string,Process>
126
     */
127
    private array $processes = [];
128

129
    /**
130
     * Flag indicating whether shutdown has been initiated.
131
     * Prevents duplicate shutdown operations and signals.
132
     */
133
    private bool $shutdown = false;
134

135
    public function __construct(
136
        private readonly ?SymfonyStyle $io = null,
137
        private readonly ?ConsoleCommandBuilder $commandBuilder = null,
138
    ) {
139
        // Register signal handlers for graceful shutdown
140
        if (function_exists('pcntl_signal')) {
24✔
141
            pcntl_async_signals(true);
24✔
142
            pcntl_signal(SIGINT, [$this, 'handleSignal']);
24✔
143
            pcntl_signal(SIGTERM, [$this, 'handleSignal']);
24✔
144
        }
145
    }
146

147
    /**
148
     * Add a process to track.
149
     *
150
     * @param string  $name    The service name (e.g., 'hot_reload', 'tailwind')
151
     * @param Process $process The background process
152
     */
153
    public function addProcess(
154
        string $name,
155
        Process $process,
156
    ): void {
157
        $this->processes[$name] = $process;
12✔
158
        $this->processNames[$name] = $name;
12✔
159

160
        // Initialize output buffers for this service
161
        $this->outputBuffers[$name] = [];
12✔
162
        $this->errorBuffers[$name] = [];
12✔
163

164
        $this->io?->text(sprintf('[TRACKING] Now monitoring %s process (PID: %d)', $name, $process->getPid()));
12✔
165
    }
166

167
    /**
168
     * Check if all processes are still running.
169
     */
170
    public function allProcessesRunning(): bool
171
    {
172
        foreach ($this->processes as $name => $process) {
2✔
173
            if (!$process->isRunning()) {
2✔
174
                $this->io?->warning(sprintf('[FAILED] Process %s has stopped (exit code: %d)', $name, $process->getExitCode()));
1✔
175

176
                return false;
1✔
177
            }
178
        }
179

180
        return true;
1✔
181
    }
182

183
    /**
184
     * Clear failure history for a process after successful run.
185
     */
186
    public function clearFailureHistory(
187
        string $processName,
188
    ): void {
UNCOV
189
        unset($this->failureHistory[$processName]);
×
UNCOV
190
        $this->io?->text(sprintf('[CLEAN] Cleared failure history for %s', $processName));
×
191
    }
192

193
    /**
194
     * Clear output buffers for a service.
195
     *
196
     * @param string $serviceName The service name
197
     */
198
    public function clearOutputBuffer(
199
        string $serviceName,
200
    ): void {
UNCOV
201
        $this->outputBuffers[$serviceName] = [];
×
UNCOV
202
        $this->errorBuffers[$serviceName] = [];
×
203

UNCOV
204
        $this->io?->text(sprintf('[CLEANUP] Cleared output buffer for %s', $serviceName));
×
205
    }
206

207
    /**
208
     * Get count of tracked processes.
209
     */
210
    public function count(): int
211
    {
212
        return count($this->processes);
4✔
213
    }
214

215
    /**
216
     * Display current status of all processes.
217
     */
218
    public function displayStatus(): void
219
    {
220
        if (!$this->io) {
1✔
221
            return;
1✔
222
        }
223

224
        // First, capture any new output from running processes
225
        $this->captureProcessOutput();
×
226

UNCOV
227
        $statuses = $this->getProcessStatuses();
×
228

229
        $this->io->section('Service Status');
×
230

231
        foreach ($statuses as $name => $status) {
×
232
            $statusIcon = $status['running'] ? '✓' : '✗';
×
233
            $statusText = $status['running'] ? 'Running' : 'Stopped';
×
234
            $pidInfo = $status['pid'] ? sprintf(' (PID: %d)', $status['pid']) : '';
×
235

UNCOV
236
            $this->io->text(sprintf(
×
237
                '%s %s: %s%s',
×
UNCOV
238
                $statusIcon,
×
UNCOV
239
                ucfirst($name),
×
240
                $statusText,
×
UNCOV
241
                $pidInfo,
×
UNCOV
242
            ));
×
243

244
            // Display recent output for this service
UNCOV
245
            $this->displayServiceOutput($name);
×
246
        }
247

UNCOV
248
        $this->io->newLine();
×
249
    }
250

251
    /**
252
     * Execute a single process with support for interactive and non-interactive modes.
253
     *
254
     * This method handles different execution scenarios:
255
     * - Interactive mode: Starts process and allows it to run continuously (for watch services)
256
     * - Non-interactive mode: Runs process to completion and returns exit code
257
     * - Timeout handling: Manages processes that are expected to run indefinitely
258
     *
259
     * @param array  $arguments     Command arguments to pass to the console
260
     * @param bool   $isInteractive Whether to run in interactive (background) mode
261
     * @param string $serviceName   Name of the service for logging and user feedback
262
     *
263
     * @return int Command exit code (Command::SUCCESS or Command::FAILURE)
264
     */
265
    public function executeProcess(
266
        array $arguments,
267
        bool $isInteractive,
268
        string $serviceName = 'Service',
269
    ): int {
270
        $process = new Process(['php', 'bin/console', ...$arguments]);
1✔
271

272
        if ($isInteractive) {
1✔
273
            // Interactive mode - start process and let it run in background
274
            // Used for watch services that should continue running
275
            try {
276
                // Enable output capture for interactive processes
277
                $process->start();
1✔
278

279
                // Give the process time to initialize and start monitoring
280
                usleep(500000); // 500ms for startup
1✔
281

282
                if ($process->isRunning()) {
1✔
283
                    // Process started successfully and is running in background
284
                    // This is the expected behavior for watch services
285
                    echo sprintf("[RUNNING] %s started and monitoring files for changes\n", $serviceName);
×
286

UNCOV
287
                    return Command::SUCCESS;
×
288
                }
289

290
                // Process finished quickly (likely an error or quick operation)
291
                // Check if the execution was successful
292
                return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
1✔
UNCOV
293
            } catch (ProcessTimedOutException) {
×
294
                // Timeout exception occurs when the process runs longer than expected
295
                // This is normal for watch services - they are designed to run continuously
UNCOV
296
                if ($process->isRunning()) {
×
297
                    // Process is still running despite timeout - let it continue
UNCOV
298
                    return Command::SUCCESS;
×
299
                }
300

301
                // Process stopped during or after the timeout
302
                // Check if it completed successfully before stopping
UNCOV
303
                return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
×
304
            }
305
        } else {
306
            // Non-interactive mode - run process to completion without output
307
            // Used for one-time operations like build commands
UNCOV
308
            $process->run();
×
309

UNCOV
310
            return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
×
311
        }
312
    }
313

314
    /**
315
     * Get failed processes.
316
     *
317
     * @return array<string,Process>
318
     */
319
    public function getFailedProcesses(): array
320
    {
321
        $failed = [];
1✔
322

323
        foreach ($this->processes as $name => $process) {
1✔
324
            // Consider any non-running process as failed, regardless of exit code
325
            // Long-running services like SSE should never exit on their own
326
            if (!$process->isRunning()) {
1✔
327
                $failed[$name] = $process;
1✔
328
            }
329
        }
330

331
        return $failed;
1✔
332
    }
333

334
    /**
335
     * Get the command arguments for a process.
336
     *
337
     * @return array<string>
338
     */
339
    public function getProcessArgs(
340
        string $processName,
341
    ): array {
UNCOV
342
        return $this->processArgs[$processName] ?? [];
×
343
    }
344

345
    /**
346
     * Get the parent process for a given process.
347
     */
348
    public function getProcessParent(
349
        string $processName,
350
    ): ?string {
UNCOV
351
        return $this->processParents[$processName] ?? null;
×
352
    }
353

354
    /**
355
     * Get status of all tracked processes.
356
     *
357
     * @return array<string,array{running:bool,exit_code:int|null,pid:int|null}>
358
     */
359
    public function getProcessStatuses(): array
360
    {
361
        $statuses = [];
1✔
362

363
        foreach ($this->processes as $name => $process) {
1✔
364
            $statuses[$name] = [
1✔
365
                'running' => $process->isRunning(),
1✔
366
                'exit_code' => $process->getExitCode(),
1✔
367
                'pid' => $process->getPid(),
1✔
368
            ];
1✔
369
        }
370

371
        return $statuses;
1✔
372
    }
373

374
    /**
375
     * Get recent output for a service.
376
     *
377
     * @param string $serviceName The service name
378
     * @param int    $limit       Maximum number of lines to return
379
     *
380
     * @return array<string>
381
     */
382
    public function getRecentOutput(
383
        string $serviceName,
384
        int $limit = 10,
385
    ): array {
UNCOV
386
        $output = $this->outputBuffers[$serviceName] ?? [];
×
UNCOV
387
        $errors = $this->errorBuffers[$serviceName] ?? [];
×
388

389
        // Combine and get most recent lines
UNCOV
390
        $allOutput = array_merge($errors, $output);
×
391

392
        return array_slice($allOutput, -$limit);
×
393
    }
394

395
    /**
396
     * Get the root parent process by traversing up the hierarchy.
397
     */
398
    public function getRootParent(
399
        string $processName,
400
    ): ?string {
401
        $current = $processName;
×
UNCOV
402
        $visited = [];
×
403

404
        while (null !== $current && !isset($visited[$current])) {
×
UNCOV
405
            $visited[$current] = true;
×
UNCOV
406
            $parent = $this->processParents[$current] ?? null;
×
407

UNCOV
408
            if (null === $parent) {
×
UNCOV
409
                return $current; // This is the root
×
410
            }
UNCOV
411
            $current = $parent;
×
412
        }
413

UNCOV
414
        return null; // Circular reference detected
×
415
    }
416

417
    /**
418
     * Handle process failure by restarting the appropriate command.
419
     *
420
     * @param string $failedProcessName The name of the failed process
421
     *
422
     * @return int Command exit code
423
     */
424
    public function handleProcessFailure(
425
        string $failedProcessName,
426
    ): int {
UNCOV
427
        $this->recordFailure($failedProcessName);
×
428

429
        // Determine which process to restart (parent or self)
UNCOV
430
        $processToRestart = $this->getRootParent($failedProcessName) ?? $failedProcessName;
×
431

UNCOV
432
        $this->io?->warning(sprintf('[FAILURE] Process %s failed, restarting %s...', $failedProcessName, $processToRestart));
×
433

UNCOV
434
        return $this->restartProcess($processToRestart);
×
435
    }
436

437
    /**
438
     * Handle shutdown signals for graceful process termination.
439
     *
440
     * This method is called when SIGINT (Ctrl+C) or SIGTERM signals are received.
441
     * It coordinates the shutdown of all managed processes to ensure clean
442
     * termination and proper resource cleanup.
443
     *
444
     * @param int $signal The signal number (SIGINT or SIGTERM)
445
     *
446
     * @return void This method terminates the process
447
     */
448
    #[NoReturn]
449
    public function handleSignal(
450
        int $signal,
451
    ): void {
452
        // Prevent multiple shutdown attempts
453
        $this->shutdown = true;
×
454

455
        switch ($signal) {
456
            case SIGINT:
457
                // User pressed Ctrl+C - provide clear feedback
UNCOV
458
                if ($this->io) {
×
459
                    $this->io->newLine();
×
UNCOV
460
                    $this->io->warning('[INTERRUPT] Received Ctrl+C - shutting down gracefully...');
×
461
                }
462

463
                break;
×
464

465
            case SIGTERM:
466
                // System or process manager requested termination
UNCOV
467
                $this->io?->warning('[TERMINATE] Received termination signal - shutting down gracefully...');
×
468

UNCOV
469
                break;
×
470
        }
471

472
        // Terminate all managed processes before exiting
UNCOV
473
        $this->terminateAll();
×
474

UNCOV
475
        exit(0);
×
476
    }
477

478
    /**
479
     * Check if any process has failed.
480
     */
481
    public function hasFailedProcesses(): bool
482
    {
483
        return array_any($this->processes, static fn ($process) => !$process->isRunning() && !$process->isSuccessful());
1✔
484
    }
485

486
    /**
487
     * Check if any processes are being tracked.
488
     */
489
    public function hasProcesses(): bool
490
    {
491
        return !empty($this->processes);
2✔
492
    }
493

494
    /**
495
     * Check if shutdown has been initiated.
496
     */
497
    public function isShutdown(): bool
498
    {
499
        return $this->shutdown;
1✔
500
    }
501

502
    /**
503
     * Record a failure for a process with timestamp.
504
     */
505
    public function recordFailure(
506
        string $processName,
507
    ): void {
UNCOV
508
        $this->failureHistory[$processName][] = time();
×
UNCOV
509
        $this->io?->warning(sprintf('[FAILURE] Recorded failure for %s (total: %d)', $processName, count($this->failureHistory[$processName])));
×
510
    }
511

512
    /**
513
     * Remove a stopped process from tracking.
514
     */
515
    public function removeProcess(
516
        string $name,
517
    ): void {
518
        if (isset($this->processes[$name])) {
1✔
519
            unset(
1✔
520
                $this->processes[$name],
1✔
521
                $this->processNames[$name],
1✔
522
                $this->outputBuffers[$name],
1✔
523
                $this->errorBuffers[$name],
1✔
524
            );
1✔
525

526
            $this->io?->text(sprintf('[CLEANUP] Removed %s from tracking', $name));
1✔
527
        }
528
    }
529

530
    /**
531
     * Restart a process using its stored arguments.
532
     *
533
     * @param string $processName The process to restart
534
     *
535
     * @return int Command exit code
536
     */
537
    public function restartProcess(
538
        string $processName,
539
    ): int {
UNCOV
540
        $args = $this->getProcessArgs($processName);
×
541

UNCOV
542
        if (empty($args)) {
×
UNCOV
543
            $this->io?->error(sprintf('[RESTART] No arguments stored for %s, cannot restart', $processName));
×
544

545
            return Command::FAILURE;
×
546
        }
547

UNCOV
548
        if (!$this->shouldRestartProcess($processName)) {
×
UNCOV
549
            $this->terminateAll();
×
550

551
            exit(Command::FAILURE);
×
552
        }
553

554
        // Add exponential backoff delay based on failure count
555
        $failureCount = count($this->failureHistory[$processName] ?? []);
×
556
        $backoffDelay = min(5 * 2 ** ($failureCount - 1), 30); // Max 30 seconds
×
557

UNCOV
558
        if ($failureCount > 0) {
×
559
            $this->io?->text(sprintf('[RESTART] Waiting %d seconds before restart (failure #%d)', $backoffDelay, $failureCount));
×
UNCOV
560
            usleep($backoffDelay * 1000000); // Convert to microseconds
×
561
        }
562

UNCOV
563
        $this->io?->warning(sprintf('[RESTART] Restarting process %s...', $processName));
×
564

565
        // Only terminate the failed process, not all processes
566
        $this->terminateProcess($processName);
×
567

568
        // Start the new process
569
        $process = new Process($args);
×
UNCOV
570
        $process->start();
×
571

572
        // Give it time to initialize
UNCOV
573
        usleep(500000);
×
574

UNCOV
575
        if ($process->isRunning()) {
×
576
            $this->io?->success(sprintf('[RESTART] Successfully restarted %s', $processName));
×
577

UNCOV
578
            return Command::SUCCESS;
×
579
        }
580
        $this->recordFailure($processName);
×
UNCOV
581
        $this->io?->error(sprintf('[RESTART] Failed to restart %s (exit code: %d)', $processName, $process->getExitCode()));
×
582

583
        // If restart failed, try again or give up
UNCOV
584
        return $this->restartProcess($processName);
×
585
    }
586

587
    /**
588
     * Store the original command arguments for a process.
589
     *
590
     * @param string        $processName The process name
591
     * @param array<string> $args        The command arguments
592
     */
593
    public function setProcessArgs(
594
        string $processName,
595
        array $args,
596
    ): void {
597
        $this->processArgs[$processName] = $args;
11✔
598
        $this->io?->text(sprintf('[ARGS] Stored arguments for %s: %s', $processName, implode(' ', $args)));
11✔
599
    }
600

601
    /**
602
     * Set the parent process for a given process.
603
     *
604
     * @param string      $processName The child process name
605
     * @param string|null $parentName  The parent process name, or null if it's a root process
606
     */
607
    public function setProcessParent(
608
        string $processName,
609
        ?string $parentName = null,
610
    ): void {
611
        $this->processParents[$processName] = $parentName;
11✔
612
        $this->io?->text(sprintf('[PARENT] Set %s as parent of %s', $parentName ?? 'root', $processName));
11✔
613
    }
614

615
    /**
616
     * Check if a process should be restarted based on failure history.
617
     */
618
    public function shouldRestartProcess(
619
        string $processName,
620
    ): bool {
621
        $now = time();
×
UNCOV
622
        $failures = $this->failureHistory[$processName] ?? [];
×
623

624
        // Remove old failures outside the window
UNCOV
625
        $recentFailures = array_filter($failures, static fn ($timestamp) => $now - $timestamp <= self::FAILURE_WINDOW_SECONDS);
×
626

627
        // Update failure history with only recent failures
628
        $this->failureHistory[$processName] = $recentFailures;
×
629

630
        // Check if we have too many recent failures
UNCOV
631
        if (count($recentFailures) >= self::MAX_FAILURES_IN_WINDOW) {
×
UNCOV
632
            $this->io?->error(sprintf('[GIVE_UP] Too many failures for %s (%d in %d seconds), giving up', $processName, count($recentFailures), self::FAILURE_WINDOW_SECONDS));
×
633

UNCOV
634
            return false;
×
635
        }
636

UNCOV
637
        return true;
×
638
    }
639

640
    /**
641
     * Terminate all tracked processes using a graceful shutdown strategy.
642
     *
643
     * This method implements a two-phase termination approach:
644
     * 1. Graceful termination (SIGTERM) with 2-second timeout
645
     * 2. Force kill (SIGKILL) for stubborn processes
646
     *
647
     * This ensures that processes have time to clean up resources and
648
     * complete any in-progress operations before being forcefully stopped.
649
     */
650
    public function terminateAll(): void
651
    {
652
        $this->io?->text('[SHUTDOWN] Terminating all background processes...');
1✔
653

654
        // Phase 1: Graceful termination using SIGTERM (signal 15)
655
        // This allows processes to clean up and exit cleanly
656
        foreach ($this->processes as $name => $process) {
1✔
657
            if ($process->isRunning()) {
1✔
UNCOV
658
                $this->io?->text(sprintf('[STOPPING] Terminating %s process (PID: %d)', $name, $process->getPid()));
×
UNCOV
659
                $process->stop(2); // Send SIGTERM with 2-second timeout for graceful shutdown
×
660
            }
661
        }
662

663
        // Phase 2: Force kill any remaining processes using SIGKILL (signal 9)
664
        // This immediately terminates processes that didn't respond to SIGTERM
665
        foreach ($this->processes as $name => $process) {
1✔
666
            if ($process->isRunning()) {
1✔
UNCOV
667
                $this->io?->warning(sprintf('[FORCE-KILL] Forcefully killing %s process', $name));
×
UNCOV
668
                $process->signal(9); // SIGKILL - cannot be caught or ignored
×
669
            }
670
        }
671

672
        $this->io?->success('[SHUTDOWN] All processes terminated');
1✔
673
    }
674

675
    /**
676
     * Terminate a specific process gracefully.
677
     *
678
     * @param string $processName The name of the process to terminate
679
     */
680
    public function terminateProcess(
681
        string $processName,
682
    ): void {
UNCOV
683
        if (!isset($this->processes[$processName])) {
×
UNCOV
684
            $this->io?->warning(sprintf('[STOPPING] Process %s not found in registry', $processName));
×
685

UNCOV
686
            return;
×
687
        }
688

UNCOV
689
        $process = $this->processes[$processName];
×
690

UNCOV
691
        if (!$process->isRunning()) {
×
UNCOV
692
            $this->io?->text(sprintf('[STOPPING] Process %s is not running', $processName));
×
693

UNCOV
694
            return;
×
695
        }
696

UNCOV
697
        $this->io?->text(sprintf('[STOPPING] Terminating %s process (PID: %d)', $processName, $process->getPid()));
×
698

699
        // Phase 1: Graceful termination using SIGTERM (signal 15)
700
        $process->stop(2); // Send SIGTERM with 2-second timeout for graceful shutdown
×
701

702
        // Phase 2: Force kill if still running using SIGKILL (signal 9)
703
        if ($process->isRunning()) {
×
UNCOV
704
            $this->io?->warning(sprintf('[FORCE-KILL] Forcefully killing %s process', $processName));
×
UNCOV
705
            $process->signal(9); // SIGKILL - cannot be caught or ignored
×
706
        } else {
UNCOV
707
            $this->io?->success(sprintf('[STOPPING] Successfully terminated %s process', $processName));
×
708
        }
709
    }
710

711
    /**
712
     * Add output line to the appropriate buffer with rotation.
713
     *
714
     * @param string $serviceName The service name
715
     * @param string $output      The output line(s)
716
     * @param bool   $isError     Whether this is error output
717
     */
718
    private function addOutputToBuffer(
719
        string $serviceName,
720
        string $output,
721
        bool $isError,
722
    ): void {
UNCOV
723
        if ($isError) {
×
UNCOV
724
            $buffer = &$this->errorBuffers[$serviceName];
×
725
        } else {
726
            $buffer = &$this->outputBuffers[$serviceName];
×
727
        }
728

729
        // Split output into lines and add with timestamps
730
        $lines = explode("\n", trim($output));
×
731
        $timestamp = date('H:i:s');
×
732

UNCOV
733
        foreach ($lines as $line) {
×
734
            if ('' === $line) {
×
UNCOV
735
                continue;
×
736
            }
737

738
            $buffer[] = sprintf('[%s] %s', $timestamp, $line);
×
739

740
            // Rotate buffer if it exceeds maximum size
UNCOV
741
            if (count($buffer) > self::MAX_OUTPUT_LINES) {
×
UNCOV
742
                array_shift($buffer); // Remove oldest line
×
743
            }
744
        }
745
    }
746

747
    /**
748
     * Capture and buffer output from all running processes.
749
     * This method reads any new output from processes and stores it in buffers.
750
     */
751
    private function captureProcessOutput(): void
752
    {
UNCOV
753
        foreach ($this->processes as $name => $process) {
×
UNCOV
754
            if (!$process->isRunning()) {
×
755
                continue;
×
756
            }
757

758
            // Capture stdout output
UNCOV
759
            $output = $process->getIncrementalOutput();
×
760

UNCOV
761
            if ('' !== $output) {
×
762
                $this->addOutputToBuffer($name, $output, false);
×
763
            }
764

765
            // Capture stderr output
UNCOV
766
            $errorOutput = $process->getIncrementalErrorOutput();
×
767

UNCOV
768
            if ('' !== $errorOutput) {
×
UNCOV
769
                $this->addOutputToBuffer($name, $errorOutput, true);
×
770
            }
771
        }
772
    }
773

774
    /**
775
     * Display recent output for a specific service.
776
     *
777
     * @param string $serviceName The service name
778
     */
779
    private function displayServiceOutput(
780
        string $serviceName,
781
    ): void {
782
        $output = $this->outputBuffers[$serviceName] ?? [];
×
783

784
        // Display error output first (more important)
UNCOV
785
        foreach ($this->errorBuffers[$serviceName] ?? [] as $errorLine) {
×
786
            $this->io?->text(sprintf('  <error>[%s] %s</error>', strtoupper($serviceName), $errorLine));
×
787
        }
788

789
        // Display regular output
UNCOV
790
        foreach ($output as $outputLine) {
×
UNCOV
791
            $this->io?->text(sprintf('  [%s] %s', strtoupper($serviceName), $outputLine));
×
792
        }
793
    }
794
}
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