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

valksor / php-dev-build / 19113736544

05 Nov 2025 11:28AM UTC coverage: 18.191% (+0.06%) from 18.133%
19113736544

push

github

k0d3r1s
code cleanup

5 of 27 new or added lines in 7 files covered. (18.52%)

1 existing line in 1 file now uncovered.

372 of 2045 relevant lines covered (18.19%)

0.97 hits per line

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

58.33
/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

21
use function count;
22
use function function_exists;
23
use function pcntl_async_signals;
24
use function pcntl_signal;
25
use function sprintf;
26
use function ucfirst;
27
use function usleep;
28

29
use const SIGINT;
30
use const SIGTERM;
31

32
/**
33
 * Process lifecycle manager for the Valksor build system.
34
 *
35
 * This class manages background processes used in watch mode, providing:
36
 * - Process tracking and health monitoring
37
 * - Graceful shutdown handling with signal management
38
 * - Process status reporting and failure detection
39
 * - Interactive vs non-interactive process execution modes
40
 * - Multi-process coordination and cleanup strategies
41
 *
42
 * The manager ensures all build services (Tailwind, Importmap, Hot Reload) can run
43
 * simultaneously while providing proper cleanup and error handling.
44
 */
45
final class ProcessManager
46
{
47
    /**
48
     * Mapping of service names to process identifiers.
49
     * Used for logging and process identification.
50
     *
51
     * @var array<string,string>
52
     */
53
    private array $processNames = [];
54

55
    /**
56
     * Registry of managed background processes.
57
     * Maps service names to Symfony Process instances for lifecycle management.
58
     *
59
     * @var array<string,Process>
60
     */
61
    private array $processes = [];
62

63
    /**
64
     * Flag indicating whether shutdown has been initiated.
65
     * Prevents duplicate shutdown operations and signals.
66
     */
67
    private bool $shutdown = false;
68

69
    public function __construct(
70
        private readonly ?SymfonyStyle $io = null,
71
    ) {
72
        // Register signal handlers for graceful shutdown
73
        if (function_exists('pcntl_signal')) {
22✔
74
            pcntl_async_signals(true);
22✔
75
            pcntl_signal(SIGINT, [$this, 'handleSignal']);
22✔
76
            pcntl_signal(SIGTERM, [$this, 'handleSignal']);
22✔
77
        }
78
    }
79

80
    /**
81
     * Add a process to track.
82
     *
83
     * @param string  $name    The service name (e.g., 'hot_reload', 'tailwind')
84
     * @param Process $process The background process
85
     */
86
    public function addProcess(
87
        string $name,
88
        Process $process,
89
    ): void {
90
        $this->processes[$name] = $process;
11✔
91
        $this->processNames[$name] = $name;
11✔
92

93
        $this->io?->text(sprintf('[TRACKING] Now monitoring %s process (PID: %d)', $name, $process->getPid()));
11✔
94
    }
95

96
    /**
97
     * Check if all processes are still running.
98
     */
99
    public function allProcessesRunning(): bool
100
    {
101
        foreach ($this->processes as $name => $process) {
2✔
102
            if (!$process->isRunning()) {
2✔
103
                $this->io?->warning(sprintf('[FAILED] Process %s has stopped (exit code: %d)', $name, $process->getExitCode()));
1✔
104

105
                return false;
1✔
106
            }
107
        }
108

109
        return true;
1✔
110
    }
111

112
    /**
113
     * Get count of tracked processes.
114
     */
115
    public function count(): int
116
    {
117
        return count($this->processes);
4✔
118
    }
119

120
    /**
121
     * Display current status of all processes.
122
     */
123
    public function displayStatus(): void
124
    {
125
        if (!$this->io) {
1✔
126
            return;
1✔
127
        }
128

129
        $statuses = $this->getProcessStatuses();
×
130

131
        $this->io->section('Service Status');
×
132

133
        foreach ($statuses as $name => $status) {
×
134
            $statusIcon = $status['running'] ? '✓' : '✗';
×
135
            $statusText = $status['running'] ? 'Running' : 'Stopped';
×
136
            $pidInfo = $status['pid'] ? sprintf(' (PID: %d)', $status['pid']) : '';
×
137

138
            $this->io->text(sprintf(
×
139
                '%s %s: %s%s',
×
140
                $statusIcon,
×
141
                ucfirst($name),
×
142
                $statusText,
×
143
                $pidInfo,
×
144
            ));
×
145
        }
146

147
        $this->io->newLine();
×
148
    }
149

150
    /**
151
     * Get failed processes.
152
     *
153
     * @return array<string,Process>
154
     */
155
    public function getFailedProcesses(): array
156
    {
157
        $failed = [];
1✔
158

159
        foreach ($this->processes as $name => $process) {
1✔
160
            if (!$process->isRunning() && !$process->isSuccessful()) {
1✔
161
                $failed[$name] = $process;
1✔
162
            }
163
        }
164

165
        return $failed;
1✔
166
    }
167

168
    /**
169
     * Get status of all tracked processes.
170
     *
171
     * @return array<string,array{running:bool,exit_code:int|null,pid:int|null}>
172
     */
173
    public function getProcessStatuses(): array
174
    {
175
        $statuses = [];
1✔
176

177
        foreach ($this->processes as $name => $process) {
1✔
178
            $statuses[$name] = [
1✔
179
                'running' => $process->isRunning(),
1✔
180
                'exit_code' => $process->getExitCode(),
1✔
181
                'pid' => $process->getPid(),
1✔
182
            ];
1✔
183
        }
184

185
        return $statuses;
1✔
186
    }
187

188
    /**
189
     * Handle shutdown signals for graceful process termination.
190
     *
191
     * This method is called when SIGINT (Ctrl+C) or SIGTERM signals are received.
192
     * It coordinates the shutdown of all managed processes to ensure clean
193
     * termination and proper resource cleanup.
194
     *
195
     * @param int $signal The signal number (SIGINT or SIGTERM)
196
     *
197
     * @return void This method terminates the process
198
     */
199
    #[NoReturn]
200
    public function handleSignal(
201
        int $signal,
202
    ): void {
203
        // Prevent multiple shutdown attempts
204
        $this->shutdown = true;
×
205

206
        switch ($signal) {
207
            case SIGINT:
208
                // User pressed Ctrl+C - provide clear feedback
209
                if ($this->io) {
×
210
                    $this->io->newLine();
×
211
                    $this->io->warning('[INTERRUPT] Received Ctrl+C - shutting down gracefully...');
×
212
                }
213

214
                break;
×
215

216
            case SIGTERM:
217
                // System or process manager requested termination
218
                $this->io?->warning('[TERMINATE] Received termination signal - shutting down gracefully...');
×
219

220
                break;
×
221
        }
222

223
        // Terminate all managed processes before exiting
224
        $this->terminateAll();
×
225

226
        exit(0);
×
227
    }
228

229
    /**
230
     * Check if any process has failed.
231
     */
232
    public function hasFailedProcesses(): bool
233
    {
234
        foreach ($this->processes as $process) {
1✔
235
            if (!$process->isRunning() && !$process->isSuccessful()) {
1✔
236
                return true;
1✔
237
            }
238
        }
239

240
        return false;
×
241
    }
242

243
    /**
244
     * Check if any processes are being tracked.
245
     */
246
    public function hasProcesses(): bool
247
    {
248
        return !empty($this->processes);
2✔
249
    }
250

251
    /**
252
     * Check if shutdown has been initiated.
253
     */
254
    public function isShutdown(): bool
255
    {
256
        return $this->shutdown;
1✔
257
    }
258

259
    /**
260
     * Remove a stopped process from tracking.
261
     */
262
    public function removeProcess(
263
        string $name,
264
    ): void {
265
        if (isset($this->processes[$name])) {
1✔
266
            unset($this->processes[$name], $this->processNames[$name]);
1✔
267

268
            $this->io?->text(sprintf('[CLEANUP] Removed %s from tracking', $name));
1✔
269
        }
270
    }
271

272
    /**
273
     * Terminate all tracked processes using a graceful shutdown strategy.
274
     *
275
     * This method implements a two-phase termination approach:
276
     * 1. Graceful termination (SIGTERM) with 2-second timeout
277
     * 2. Force kill (SIGKILL) for stubborn processes
278
     *
279
     * This ensures that processes have time to clean up resources and
280
     * complete any in-progress operations before being forcefully stopped.
281
     */
282
    public function terminateAll(): void
283
    {
284
        $this->io?->text('[SHUTDOWN] Terminating all background processes...');
1✔
285

286
        // Phase 1: Graceful termination using SIGTERM (signal 15)
287
        // This allows processes to clean up and exit cleanly
288
        foreach ($this->processes as $name => $process) {
1✔
289
            if ($process->isRunning()) {
1✔
290
                $this->io?->text(sprintf('[STOPPING] Terminating %s process (PID: %d)', $name, $process->getPid()));
×
291
                $process->stop(2); // Send SIGTERM with 2-second timeout for graceful shutdown
×
292
            }
293
        }
294

295
        // Phase 2: Force kill any remaining processes using SIGKILL (signal 9)
296
        // This immediately terminates processes that didn't respond to SIGTERM
297
        foreach ($this->processes as $name => $process) {
1✔
298
            if ($process->isRunning()) {
1✔
299
                $this->io?->warning(sprintf('[FORCE-KILL] Forcefully killing %s process', $name));
×
300
                $process->signal(9); // SIGKILL - cannot be caught or ignored
×
301
            }
302
        }
303

304
        $this->io?->success('[SHUTDOWN] All processes terminated');
1✔
305
    }
306

307
    /**
308
     * Execute a single process with support for interactive and non-interactive modes.
309
     *
310
     * This static method handles different execution scenarios:
311
     * - Interactive mode: Starts process and allows it to run continuously (for watch services)
312
     * - Non-interactive mode: Runs process to completion and returns exit code
313
     * - Timeout handling: Manages processes that are expected to run indefinitely
314
     *
315
     * @param array  $arguments     Command arguments to pass to the console
316
     * @param bool   $isInteractive Whether to run in interactive (background) mode
317
     * @param string $serviceName   Name of the service for logging and user feedback
318
     *
319
     * @return int Command exit code (Command::SUCCESS or Command::FAILURE)
320
     */
321
    public static function executeProcess(
322
        array $arguments,
323
        bool $isInteractive,
324
        string $serviceName = 'Service',
325
    ): int {
326
        $process = new Process(['php', 'bin/console', ...$arguments]);
1✔
327

328
        if ($isInteractive) {
1✔
329
            // Interactive mode - start process and let it run in background
330
            // Used for watch services that should continue running
331
            try {
332
                $process->start();
1✔
333

334
                // Give the process time to initialize and start monitoring
335
                usleep(500000); // 500ms for startup
1✔
336

337
                if ($process->isRunning()) {
1✔
338
                    // Process started successfully and is running in background
339
                    // This is the expected behavior for watch services
340
                    echo sprintf("[RUNNING] %s started and monitoring files for changes\n", $serviceName);
1✔
341

342
                    return Command::SUCCESS;
1✔
343
                }
344

345
                // Process finished quickly (likely an error or quick operation)
346
                // Check if the execution was successful
347
                return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
×
NEW
348
            } catch (ProcessTimedOutException) {
×
349
                // Timeout exception occurs when the process runs longer than expected
350
                // This is normal for watch services - they are designed to run continuously
351
                if ($process->isRunning()) {
×
352
                    // Process is still running despite timeout - let it continue
353
                    return Command::SUCCESS;
×
354
                }
355

356
                // Process stopped during or after the timeout
357
                // Check if it completed successfully before stopping
358
                return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
×
359
            }
360
        } else {
361
            // Non-interactive mode - run process to completion without output
362
            // Used for one-time operations like build commands
363
            $process->run();
×
364

365
            return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
×
366
        }
367
    }
368
}
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