208 lines
6.7 KiB
PHP
208 lines
6.7 KiB
PHP
|
<?php
|
||
|
|
||
|
return [
|
||
|
'description' => 'Run Kirby dev server and Tailwind watcher',
|
||
|
'command' => static function ($cli): void {
|
||
|
$cwd = getcwd();
|
||
|
|
||
|
// Resolve paths
|
||
|
$router = $cwd . '/vendor/getkirby/cms/router.php';
|
||
|
$docroot = $cli->arg('docroot') ?? 'public';
|
||
|
$docrootPath = $cwd . '/' . $docroot;
|
||
|
|
||
|
// Basic checks
|
||
|
if (!is_file($router)) {
|
||
|
$cli->error('Kirby router not found. Make sure dependencies are installed: vendor/getkirby/cms/router.php');
|
||
|
return;
|
||
|
}
|
||
|
if (!is_dir($docrootPath)) {
|
||
|
$cli->error("Document root not found: {$docrootPath}");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Helper function to check if a port is available
|
||
|
$isPortAvailable = function (string $host, int $port): bool {
|
||
|
$socket = @fsockopen($host, $port, $errno, $errstr, 1);
|
||
|
if ($socket) {
|
||
|
fclose($socket);
|
||
|
return false; // Port is occupied
|
||
|
}
|
||
|
return true; // Port is available
|
||
|
};
|
||
|
|
||
|
$host = 'localhost';
|
||
|
$startPort = 8000;
|
||
|
$maxPort = 8100;
|
||
|
$port = $startPort;
|
||
|
|
||
|
while ($port <= $maxPort) {
|
||
|
if ($isPortAvailable($host, $port)) {
|
||
|
break;
|
||
|
}
|
||
|
$port++;
|
||
|
}
|
||
|
|
||
|
if ($port > $maxPort) {
|
||
|
$cli->error("No available ports found between {$startPort} and {$maxPort}");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$cli->info("Using port {$port} for PHP dev server");
|
||
|
|
||
|
$phpCmd = sprintf(
|
||
|
'php -S %s:%s -t %s %s',
|
||
|
escapeshellarg($host),
|
||
|
escapeshellarg($port),
|
||
|
escapeshellarg($docrootPath),
|
||
|
escapeshellarg($router)
|
||
|
);
|
||
|
|
||
|
$npmCmd = 'npm run watch';
|
||
|
|
||
|
$cli->info("Starting PHP dev server on http://{$host}:{$port} (docroot: {$docroot})");
|
||
|
|
||
|
// Helper to start a process with pipes
|
||
|
$start = static function (string $cmd) use ($cwd) {
|
||
|
$descriptorSpec = [
|
||
|
0 => ['pipe', 'r'], // stdin
|
||
|
1 => ['pipe', 'w'], // stdout
|
||
|
2 => ['pipe', 'w'], // stderr
|
||
|
];
|
||
|
|
||
|
$proc = proc_open($cmd, $descriptorSpec, $pipes, $cwd);
|
||
|
|
||
|
if (!is_resource($proc)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Non-blocking I/O
|
||
|
stream_set_blocking($pipes[1], false);
|
||
|
stream_set_blocking($pipes[2], false);
|
||
|
|
||
|
return [
|
||
|
'proc' => $proc,
|
||
|
'pipes' => $pipes,
|
||
|
'cmd' => $cmd,
|
||
|
];
|
||
|
};
|
||
|
|
||
|
$processes = [];
|
||
|
|
||
|
$phpProc = $start($phpCmd);
|
||
|
if ($phpProc === null) {
|
||
|
$cli->error('Failed to start PHP dev server.');
|
||
|
return;
|
||
|
}
|
||
|
$processes['php'] = $phpProc;
|
||
|
|
||
|
$npmProc = $start($npmCmd);
|
||
|
if ($npmProc === null) {
|
||
|
$cli->warning('Could not start npm watcher. Is Node/npm installed and in your PATH?');
|
||
|
} else {
|
||
|
$processes['npm'] = $npmProc;
|
||
|
}
|
||
|
|
||
|
// Graceful termination
|
||
|
$terminateAll = static function () use (&$processes, $cli): void {
|
||
|
$cli->line("");
|
||
|
foreach ($processes as $name => $p) {
|
||
|
// Check if the resource is still valid before calling proc_get_status
|
||
|
if (is_resource($p['proc'])) {
|
||
|
$status = proc_get_status($p['proc']);
|
||
|
if ($status && $status['running']) {
|
||
|
$cli->line("Stopping {$name}...");
|
||
|
// Try graceful terminate
|
||
|
@proc_terminate($p['proc']);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Close pipes regardless of process status
|
||
|
foreach ($p['pipes'] as $pipe) {
|
||
|
if (is_resource($pipe)) {
|
||
|
@fclose($pipe);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Final close if the resource is still valid
|
||
|
if (is_resource($p['proc'])) {
|
||
|
@proc_close($p['proc']);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Handle Ctrl+C if pcntl is available
|
||
|
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
|
||
|
pcntl_async_signals(true);
|
||
|
pcntl_signal(SIGINT, static function () use ($terminateAll) {
|
||
|
$terminateAll();
|
||
|
// Exit cleanly after terminating children
|
||
|
exit(130);
|
||
|
});
|
||
|
// Optional: also handle SIGTERM
|
||
|
pcntl_signal(SIGTERM, static function () use ($terminateAll) {
|
||
|
$terminateAll();
|
||
|
exit(143);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Ensure cleanup on normal shutdown
|
||
|
register_shutdown_function($terminateAll);
|
||
|
|
||
|
// Stream outputs until all children exit
|
||
|
$prefixColor = [
|
||
|
'php' => fn($s) => "\033[1;34m🐘 [php]\033[0m $s",
|
||
|
'npm' => fn($s) => "\033[1;32m [npm]\033[0m $s",
|
||
|
];
|
||
|
|
||
|
while (true) {
|
||
|
$running = false;
|
||
|
|
||
|
foreach ($processes as $name => $p) {
|
||
|
// Check if the resource is still valid
|
||
|
if (!is_resource($p['proc'])) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$status = proc_get_status($p['proc']);
|
||
|
if (!$status) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$running = $running || $status['running'];
|
||
|
|
||
|
// Read stdout
|
||
|
if (is_resource($p['pipes'][1])) {
|
||
|
$out = @stream_get_contents($p['pipes'][1]);
|
||
|
if ($out !== false && $out !== '') {
|
||
|
foreach (preg_split('/\R/', rtrim($out)) as $line) {
|
||
|
if ($line !== '') {
|
||
|
$cli->line(($prefixColor[$name])($line));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read stderr
|
||
|
if (is_resource($p['pipes'][2])) {
|
||
|
$err = @stream_get_contents($p['pipes'][2]);
|
||
|
if ($err !== false && $err !== '') {
|
||
|
foreach (preg_split('/\R/', rtrim($err)) as $line) {
|
||
|
if ($line !== '') {
|
||
|
$cli->error(($prefixColor[$name])($line));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!$running) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Small sleep to avoid busy-waiting
|
||
|
usleep(50_000);
|
||
|
}
|
||
|
|
||
|
$cli->success('All processes exited.');
|
||
|
}
|
||
|
];
|