# HG changeset patch # User Dan # Date 1207110231 14400 # Node ID 2adca0f363fd3873a4cbef68e765c6eeaec07180 # Parent 7a1573676cc4e4076f3a8c6393c6badf77be28ee Added multi-threading/forking/keep-alive support to webserver. w00t, feeling all POSIX-happy today! diff -r 7a1573676cc4 -r 2adca0f363fd functions.php --- 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); } diff -r 7a1573676cc4 -r 2adca0f363fd greyhound.php --- 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"); } - diff -r 7a1573676cc4 -r 2adca0f363fd webserver.php --- 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