Added multi-threading/forking/keep-alive support to webserver. w00t, feeling all POSIX-happy today!
--- a/functions.php Mon Mar 31 07:40:30 2008 -0400
+++ b/functions.php Wed Apr 02 00:23:51 2008 -0400
@@ -39,7 +39,8 @@
function status($msg)
{
$h = @fopen('php://stderr', 'w');
- fwrite($h, "\x1B[32;1m[Greyhound] \x1B[32;0m$msg\x1B[0m\n");
+ $label = ( defined('HTTPD_WS_CHILD') ) ? 'Child ' . getmypid() : ' Greyhound ';
+ fwrite($h, "\x1B[32;1m[$label] \x1B[32;0m$msg\x1B[0m\n");
fclose($h);
}
--- a/greyhound.php Mon Mar 31 07:40:30 2008 -0400
+++ b/greyhound.php Wed Apr 02 00:23:51 2008 -0400
@@ -25,9 +25,22 @@
pcntl_signal(SIGINT, 'sigterm');
}
+//
+// CONFIGURATION
+//
+
+// Listen on all interfaces. If this is false, it will only listen on
+// 127.0.0.1 (the loopback interface)
$public = true;
+// Allow control of playback. By default this is turned on but if you
+// set this to false, it will only display the playlist.
$allowcontrol = true;
+// The default theme. This should be a name of a directory in ./themes.
$theme = 'funkymonkey';
+// Allow forking when an HTTP request is received. This has advantages
+// and disadvantages. If this experimental option is enabled, it will
+// result in faster responses and load times but more memory usage.
+$allow_fork = true;
@ini_set('display_errors', 'on');
@@ -42,7 +55,7 @@
// start up...
-status('Starting WebControl v0.1-hg');
+status('Starting Greyhound Web Control v0.1-hg');
status('loading files');
require('webserver.php');
@@ -99,7 +112,28 @@
$httpd->add_handler('scripts', 'dir', GREY_ROOT . '/scripts');
$httpd->add_handler('favicon.ico', 'file', GREY_ROOT . '/amarok_icon.ico');
$httpd->add_handler('apple-touch-icon.png', 'file', GREY_ROOT . '/apple-touch-icon.png');
+ // load all themes if forking is enabled
+ // Themes are loaded when the playlist is requested. This is fine for
+ // single-threaded operation, but if the playlist handler is only loaded
+ // in a child process, we need to preload all themes into the parent before
+ // children can respond to theme resource requests.
+ if ( $allow_fork )
+ {
+ status('Preloading themes');
+
+ $dh = @opendir(GREY_ROOT . '/themes');
+ if ( !$dh )
+ burnout('Could not open themes directory');
+ while ( $dir = @readdir($dh) )
+ {
+ if ( $dir == '.' || $dir == '..' )
+ continue;
+ if ( is_dir( GREY_ROOT . "/themes/$dir" ) )
+ load_theme($dir);
+ }
+ }
$httpd->allow_dir_list = true;
+ $httpd->allow_fork = ( $allow_fork ) ? true : false;
$httpd->default_document = 'index';
status("Entering main server loop - ^C to interrupt, listening on port $port");
@@ -109,4 +143,3 @@
{
burnout("Exception caught while running webserver:\n$e");
}
-
--- a/webserver.php Mon Mar 31 07:40:30 2008 -0400
+++ b/webserver.php Wed Apr 02 00:23:51 2008 -0400
@@ -95,6 +95,21 @@
var $allow_dir_list = false;
/**
+ * Switch to control forking support.
+ * @var bool
+ */
+
+ var $allow_fork = true;
+
+ /**
+ * Keep-alive support uses this to track what the client requested.
+ * Only used if $allow_fork is set to true.
+ * @var bool
+ */
+
+ var $in_keepalive = false;
+
+ /**
* Constructor.
* @param string IPv4 address to bind to
* @param int Port number
@@ -130,8 +145,12 @@
function __destruct()
{
- status('WebServer: destroying socket');
- @socket_close($this->sock);
+ if ( !defined('HTTPD_WS_CHILD') )
+ {
+ status('WebServer: destroying socket');
+ @socket_shutdown($this->sock, 2);
+ @socket_close($this->sock);
+ }
}
/**
@@ -142,24 +161,59 @@
{
while ( true )
{
+ // if this is a child process, we're finished - close up shop
+ if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive )
+ {
+ exit(0);
+ }
+
// wait for connection...
// trick from http://us.php.net/manual/en/function.socket-accept.php
- $remote = false;
- switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), 5)) {
- case 2:
- break;
- case 1:
- $remote = @socket_accept($this->sock);
- break;
- case 0:
- break;
+ if ( !defined('HTTPD_WS_CHILD') )
+ {
+ $remote = false;
+ $timeout = 5;
+ switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) {
+ case 2:
+ break;
+ case 1:
+ $remote = @socket_accept($this->sock);
+ break;
+ case 0:
+ break;
+ }
}
if ( !$remote )
{
+ $this->in_keepalive = false;
continue;
}
+ // fork off if possible
+ if ( function_exists('pcntl_fork') && $this->allow_fork && !$this->in_keepalive )
+ {
+ $pid = pcntl_fork();
+ if ( $pid == -1 )
+ {
+ // do nothing; continue responding to request in single-threaded mode
+ }
+ else if ( $pid )
+ {
+ // we are the parent, continue listening
+ $remote = false;
+ continue;
+ }
+ else
+ {
+ // this is the child
+ define('HTTPD_WS_CHILD', 1);
+ $this->sock = false;
+ }
+ }
+
+ $this->in_keepalive = false;
+
// read request
$last_line = '';
$client_headers = '';
@@ -198,6 +252,12 @@
$_SERVER[$key] = $match[2];
}
+ // enable keep-alive if requested
+ if ( isset($_SERVER['HTTP_CONNECTION']) && defined('HTTPD_WS_CHILD') )
+ {
+ $this->in_keepalive = ( $_SERVER['HTTP_CONNECTION'] === 'keep-alive' );
+ }
+
if ( isset($_SERVER['HTTP_AUTHORIZATION']) )
{
$data = $_SERVER['HTTP_AUTHORIZATION'];
@@ -285,7 +345,19 @@
$this->send_standard_response($remote, $handler, $uri, $params);
- @socket_close($remote);
+ if ( !$this->in_keepalive )
+ {
+ // if ( defined('HTTPD_WS_CHILD') )
+ // status('Closing connection');
+ @socket_close($remote);
+ exit(0);
+ }
+ else
+ {
+ // if ( defined('HTTPD_WS_CHILD') )
+ // status('Continuing connection');
+ @socket_write($remote, "\r\n\r\n");
+ }
}
}
@@ -302,15 +374,17 @@
global $http_responses;
$reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
+ $_SERVER['HTTP_USER_AGENT'] = ( isset($_SERVER['HTTP_USER_AGENT']) ) ? $_SERVER['HTTP_USER_AGENT'] : '(no user agent)';
status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}");
$headers = str_replace("\r\n", "\n", $headers);
$headers = str_replace("\n", "\r\n", $headers);
$headers = preg_replace("#[\r\n]+$#", '', $headers);
+ $connection = ( $this->in_keepalive ) ? 'keep-alive' : 'close';
@socket_write($socket, "HTTP/1.1 $http_code $reason_code\r\n");
@socket_write($socket, "Server: $this->server_string");
- @socket_write($socket, "Connection: close\r\n");
+ @socket_write($socket, "Connection: $connection\r\n");
@socket_write($socket, "Content-Type: $contenttype\r\n");
if ( !empty($headers) )
{
@@ -518,6 +592,7 @@
return true;
}
+ $this->header("Content-length: " . strlen($output));
$headers = implode("\r\n", $this->response_headers);
// write headers