mirror of
https://github.com/Qbix/webserver.git
synced 2026-07-22 07:57:23 +02:00
Initial release — pure PHP web server, 55-73% of nginx throughput
Standalone server with zero dependencies beyond PHP 8.1+. Static files with in-memory response cache, keep-alive, TCP_NODELAY, gzip/brotli, WebSocket, live dashboard, rate limiting. Includes PHAR builder and GitHub Actions CI for static binaries (Linux x86_64, Linux ARM64, macOS x86_64, macOS Apple Silicon).
This commit is contained in:
+225
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Qbix Server — pure PHP web server.
|
||||
*
|
||||
* Standalone: php server.php --root=./web --port=8080
|
||||
* With Qbix: php server.php --app=/path/to/myapp --port=8080
|
||||
*
|
||||
* Options:
|
||||
* --root=DIR Document root (default: ./web)
|
||||
* --app=DIR Qbix app directory (loads full Q framework)
|
||||
* --host=IP Bind address (default: 0.0.0.0)
|
||||
* --port=PORT Listen port (default: 8080)
|
||||
* --workers=N Pre-fork PHP workers (default: 0 = in-process)
|
||||
* --config=FILE JSON config file to load
|
||||
* --pid=PATH Write PID file
|
||||
* --debug Enable verbose logging
|
||||
* --version Print version and exit
|
||||
* --help Print usage and exit
|
||||
*/
|
||||
|
||||
define('QBIX_SERVER_VERSION', '1.0.0');
|
||||
|
||||
// ── Parse CLI args ──────────────────────────────────
|
||||
|
||||
$opts = array(
|
||||
'root' => null,
|
||||
'app' => null,
|
||||
'host' => '0.0.0.0',
|
||||
'port' => 8080,
|
||||
'workers' => 0,
|
||||
'config' => null,
|
||||
'pid' => null,
|
||||
'debug' => false,
|
||||
);
|
||||
|
||||
foreach ($argv as $i => $arg) {
|
||||
if ($i === 0) continue;
|
||||
if ($arg === '--help' || $arg === '-h') {
|
||||
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n\n";
|
||||
echo "Usage: php server.php [options]\n\n";
|
||||
echo "Options:\n";
|
||||
echo " --root=DIR Document root (default: ./web)\n";
|
||||
echo " --app=DIR Qbix app directory (uses full Q framework)\n";
|
||||
echo " --host=IP Bind address (default: 0.0.0.0)\n";
|
||||
echo " --port=PORT Listen port (default: 8080)\n";
|
||||
echo " --workers=N Pre-fork workers (default: 0 = in-process)\n";
|
||||
echo " --config=FILE JSON config file\n";
|
||||
echo " --pid=PATH PID file path\n";
|
||||
echo " --debug Verbose logging\n";
|
||||
echo " --version Print version\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($arg === '--version' || $arg === '-v') {
|
||||
echo "Qbix Server v" . QBIX_SERVER_VERSION . "\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($arg === '--debug') {
|
||||
$opts['debug'] = true;
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^--(\w+)=(.+)$/', $arg, $m)) {
|
||||
$opts[$m[1]] = $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Determine mode: standalone vs Qbix app ──────────
|
||||
|
||||
$qbixMode = false;
|
||||
|
||||
if ($opts['app']) {
|
||||
// Qbix app mode — load the full framework
|
||||
$appDir = realpath($opts['app']);
|
||||
if (!$appDir || !is_dir($appDir)) {
|
||||
fwrite(STDERR, "Error: app directory not found: {$opts['app']}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Look for Q.inc.php
|
||||
$qInc = $appDir . '/scripts/Q.inc.php';
|
||||
if (!file_exists($qInc)) {
|
||||
// Try Platform path
|
||||
$qInc = $appDir . '/../Platform/scripts/Q.inc.php';
|
||||
}
|
||||
if (file_exists($qInc)) {
|
||||
define('APP_DIR', $appDir);
|
||||
require_once $qInc;
|
||||
$qbixMode = true;
|
||||
} else {
|
||||
fwrite(STDERR, "Warning: Q.inc.php not found, running in standalone mode\n");
|
||||
}
|
||||
|
||||
$webDir = $appDir . '/web';
|
||||
} else {
|
||||
$webDir = $opts['root'] ?: (getcwd() . '/web');
|
||||
}
|
||||
|
||||
if (!$qbixMode) {
|
||||
// Standalone mode — load minimal Q shim
|
||||
require_once __DIR__ . '/src/Q.php';
|
||||
}
|
||||
|
||||
$webDir = realpath($webDir);
|
||||
if (!$webDir || !is_dir($webDir)) {
|
||||
fwrite(STDERR, "Error: document root not found: " . ($opts['root'] ?: './web') . "\n");
|
||||
fwrite(STDERR, "Create a web/ directory or use --root=DIR\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ── Load config ─────────────────────────────────────
|
||||
|
||||
// Default server config
|
||||
$defaultConfig = array(
|
||||
'Q' => array(
|
||||
'webserver' => array(
|
||||
'keepAlive' => array('max' => 100, 'timeout' => 15),
|
||||
'timeout' => array('read' => 30),
|
||||
'maxConnections' => 1024,
|
||||
'fileCache' => array(
|
||||
'maxSize' => 67108864,
|
||||
'maxFile' => 1048576,
|
||||
'checkInterval' => 1,
|
||||
),
|
||||
'rateLimit' => array(
|
||||
'enabled' => false,
|
||||
'requests' => 100,
|
||||
'window' => 60,
|
||||
'burstRequests' => 20,
|
||||
'burstWindow' => 1,
|
||||
),
|
||||
),
|
||||
'web' => array(
|
||||
'cache' => array(
|
||||
'enabled' => true,
|
||||
'defaultTtl' => 0,
|
||||
'components' => array('enabled' => false),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Apply defaults
|
||||
foreach ($defaultConfig as $k1 => $v1) {
|
||||
if (is_array($v1)) {
|
||||
foreach ($v1 as $k2 => $v2) {
|
||||
if (is_array($v2)) {
|
||||
foreach ($v2 as $k3 => $v3) {
|
||||
if (is_array($v3)) {
|
||||
foreach ($v3 as $k4 => $v4) {
|
||||
if (Q_Config::get($k1, $k2, $k3, $k4, null) === null) {
|
||||
Q_Config::set($k1, $k2, $k3, $k4, $v4);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Q_Config::get($k1, $k2, $k3, null) === null) {
|
||||
Q_Config::set($k1, $k2, $k3, $v3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User config file
|
||||
if ($opts['config']) {
|
||||
Q_Config::load($opts['config']);
|
||||
}
|
||||
|
||||
// Config from app directory
|
||||
$appConfig = dirname($webDir) . '/config/server.json';
|
||||
if (file_exists($appConfig)) {
|
||||
Q_Config::load($appConfig);
|
||||
}
|
||||
|
||||
// ── PID file ────────────────────────────────────────
|
||||
|
||||
if ($opts['pid']) {
|
||||
file_put_contents($opts['pid'], getmypid());
|
||||
register_shutdown_function(function () use ($opts) {
|
||||
@unlink($opts['pid']);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Request logging ─────────────────────────────────
|
||||
|
||||
$colors = array(
|
||||
2 => "\033[32m", // green for 2xx
|
||||
3 => "\033[33m", // yellow for 3xx
|
||||
4 => "\033[31m", // red for 4xx
|
||||
5 => "\033[31m", // red for 5xx
|
||||
);
|
||||
$reset = "\033[0m";
|
||||
|
||||
Q_WebServer::$onRequest = function ($method, $uri, $status, $ms) use ($colors, $reset, $opts) {
|
||||
$color = $colors[(int)($status / 100)] ?? '';
|
||||
$time = date('H:i:s');
|
||||
echo "$time {$color}{$status}{$reset} $method $uri ({$ms}ms)\n";
|
||||
};
|
||||
|
||||
// ── Start server ────────────────────────────────────
|
||||
|
||||
echo "\n";
|
||||
echo " ┌──────────────────────────────────────┐\n";
|
||||
echo " │ Qbix Server v" . QBIX_SERVER_VERSION . str_repeat(' ', 22 - strlen(QBIX_SERVER_VERSION)) . "│\n";
|
||||
echo " ├──────────────────────────────────────┤\n";
|
||||
echo " │ http://{$opts['host']}:{$opts['port']}" . str_repeat(' ', max(0, 24 - strlen("{$opts['host']}:{$opts['port']}"))) . "│\n";
|
||||
echo " │ Root: " . basename($webDir) . str_repeat(' ', max(0, 29 - strlen(basename($webDir)))) . "│\n";
|
||||
echo " │ Mode: " . ($qbixMode ? 'Qbix Platform' : 'Standalone') . str_repeat(' ', $qbixMode ? 16 : 19) . "│\n";
|
||||
echo " │ PHP: " . ($opts['workers'] ? $opts['workers'] . ' workers' : 'in-process') . str_repeat(' ', max(0, 30 - strlen($opts['workers'] ? $opts['workers'] . ' workers' : 'in-process'))) . "│\n";
|
||||
echo " ├──────────────────────────────────────┤\n";
|
||||
echo " │ Dashboard: /Q/dashboard │\n";
|
||||
echo " │ Health: /Q/health │\n";
|
||||
echo " │ Ctrl+C to stop │\n";
|
||||
echo " └──────────────────────────────────────┘\n";
|
||||
echo "\n";
|
||||
|
||||
Q_WebServer::start(
|
||||
$webDir,
|
||||
$opts['host'],
|
||||
(int) $opts['port'],
|
||||
(int) $opts['workers']
|
||||
);
|
||||
|
||||
Q_WebServer::run();
|
||||
Reference in New Issue
Block a user