# HG changeset patch # User Dan # Date 1219555732 14400 # Node ID 65e70ada71c917f56562b7030be82ec6aee71670 # Parent 70ef461bbffac3d143c5c7305dcf7d12ff144a00 Major changes to webserver backend. All socket functions are abstracted to allow support for stream_* which seems to be both more widely supported and better at handling blocking and timeouts, at the cost of a small bit of speed. Keep-Alive times out properly and thanks to a bit of IPC code from stream_create_pair(), zombie children are mostly eliminated by proper pcntl_wait() being called when a child shuts down normally, and children die within 0.2sec if the parent receives a SIGTERM or SIGINT, even if the children are waiting on the socket. diff -r 70ef461bbffa -r 65e70ada71c9 webserver.php --- a/webserver.php Sun Aug 24 01:26:20 2008 -0400 +++ b/webserver.php Sun Aug 24 01:28:52 2008 -0400 @@ -36,6 +36,12 @@ define('HTTPD_ICON_FILE', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII='); /** + * Abstraction layer to use + */ + +define('HTTPD_SOCKET_LAYER', 'Stream'); + +/** * Simple but full-featured embedded web server written in PHP. * @package Amarok * @subpackage WebControl @@ -54,11 +60,11 @@ var $bind_address = '127.0.0.1'; /** - * Socket resource - * @var resource + * Socket abstraction object + * @var object */ - var $sock = null; + var $server = null; /** * Server string @@ -174,6 +180,15 @@ var $parent_pid = 0; /** + * Sockets for parent and child to communicate + * @var resource + * @var resource + */ + + var $parent_sock = null; + var $child_sock = null; + + /** * Constructor. * @param string IPv4 address to bind to * @param int Port number @@ -244,16 +259,9 @@ } $socket_do_root = ( $allow_root ) ? function_exists('posix_geteuid') : false; - $this->sock = @socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); - if ( !$this->sock ) - throw new Exception('Could not create socket'); - $result = @socket_bind($this->sock, $address, $port); - if ( !$result ) - throw new Exception("Could not bind to $address:$port"); - $this->socket_initted = true; - $result = @socket_listen($this->sock, SOMAXCONN); - if ( !$result ) - throw new Exception("Could not listen for connections $address:$port"); + $class = 'Socket_' . HTTPD_SOCKET_LAYER; + $this->server = new $class(); + $this->server->tcp_listen($address, $port); // if running as root and we made it here, switch credentials if ( $socket_do_root ) @@ -277,6 +285,7 @@ substr($uuid_base, 12, 4) . '-' . substr($uuid_base, 16, 4) . '-' . substr($uuid_base, 20, 20); + } /** @@ -289,13 +298,24 @@ { if ( function_exists('status') ) status('WebServer: destroying socket'); - // http://us3.php.net/manual/en/function.socket-bind.php - if ( !@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1) ) + $this->server->destroy(); + + // tell all children to shut down + if ( $this->allow_fork ) { - echo socket_strerror(socket_last_error($sock)) . "\n"; + if ( function_exists('status') ) + status('WebServer: asking all children to exit'); + $this->send_ipc_event("die _"); } - @socket_shutdown($this->sock, 2); - @socket_close($this->sock); + + // that last operation should have been asynchronous, so shut everything down now + @socket_shutdown($this->parent_sock); + @socket_close($this->parent_sock); + } + else if ( defined('HTTPD_WS_CHILD') ) + { + @socket_shutdown($this->child_sock); + @socket_close($this->child_sock); } } @@ -305,6 +325,24 @@ function serve() { + // If we're allowed to use multithreading, set up to handle SIGUSR2 which waits on the child + if ( function_exists('pcntl_signal') && $this->allow_fork ) + { + // required for signal handling to work + declare(ticks=1); + + // trap SIGTERM + pcntl_signal(SIGUSR2, array(&$this, '_ipc_event')); + + if ( !($sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) ) + { + throw new Exception("Could not set up private IPC socket. Reason: " . socket_strerror(socket_last_error())); + } + + $this->parent_sock =& $sockets[0]; + $this->child_sock =& $sockets[1]; + } + while ( true ) { // if this is a child process, we're finished - close up shop @@ -312,8 +350,13 @@ { if ( function_exists('status') ) status('Exiting child process'); - @socket_shutdown($remote); - @socket_close($remote); + + $remote->destroy(); + + // let the parent know that we're out of here + $this->send_ipc_event("exit " . getmypid()); + + // bye exit(0); } @@ -321,19 +364,9 @@ // trick from http://us.php.net/manual/en/function.socket-accept.php 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; - } + $remote = $this->server->accept(); } - + if ( !$remote ) { $this->in_keepalive = false; @@ -351,7 +384,7 @@ else if ( $pid ) { // we are the parent, continue listening - socket_close($remote); + $remote->soft_shutdown(); $this->child_list[] = $pid; continue; } @@ -359,8 +392,16 @@ { // this is the child define('HTTPD_WS_CHILD', 1); - @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1); - socket_close($this->sock); + + // setup to handle signals + if ( function_exists('pcntl_signal') ) + { + // required for signal handling to work + declare(ticks=1); + + // trap SIGTERM + pcntl_signal(SIGUSR2, array(&$this, '_ipc_event')); + } } } @@ -368,39 +409,77 @@ $this->headers_sent = false; $this->in_scriptlet = false; - // read request + // + // READ THE REQUEST + // + + // this is a complicated situation because we need to keep enough ticks going to properly handle + // signals, meaning we can't use stream_set_timeout() and instead need to rely on our own timing + // logic. setting the timeout to a short period, say 200,000 usec, we can minimize CPU usage and + // have a good response time. + + $remote->set_timeout(0, 200000); + $start_time = microtime(true); + $client_headers = ''; + $last_line = ''; + while ( true ) + { + if ( $start_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) || $remote->is_eof() ) + { + // request expired -- end the process here + if ( !defined('HTTPD_WS_CHILD') ) + $remote->destroy(); + + continue 2; + } + $line = strval($remote->read_normal()); + $line = str_replace("\r", "", $line); + + // raw layer wants to send 2 empty lines through, stream layer wants to send one. + $last_line_check = ( HTTPD_SOCKET_LAYER != 'Raw' || ( HTTPD_SOCKET_LAYER == 'Raw' && $last_line == "\n" ) ); + if ( $line == "\n" && $last_line_check ) + // we have two newlines in a row, break out since we have a full request + break; + + $client_headers .= $line; + $last_line = $line; + } + + /* $last_line = ''; $client_headers = ''; if ( defined('HTTPD_WS_CHILD') ) { - @socket_set_timeout($remote, HTTPD_KEEP_ALIVE_TIMEOUT); + $remote->set_timeout(1); } - if ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) ) + if ( $line = $remote->read_normal() ) { do { $line = str_replace("\r", "", $line); if ( empty($line) ) continue; - if ( $line == "\n" && $last_line == "\n" ) + $last_line_check = ( HTTPD_SOCKET_LAYER != 'Raw' || ( HTTPD_SOCKET_LAYER == 'Raw' && $last_line == "\n" ) ); + if ( $line == "\n" && $last_line_check ) break; $client_headers .= $line; $last_line = $line; + $line = $remote->read_normal() } - while ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) ); + while ( true ); } else { if ( defined('HTTPD_WS_CHILD') ) { - $md = @socket_get_status($remote); - if ( @$md['timed_out'] ) + if ( $remote->timed_out() ) { status('[debug] keep-alive connection timed out'); continue; // will jump back to the start of the loop and kill the child process } } } + */ // parse request $client_headers = trim($client_headers); @@ -467,7 +546,7 @@ $mode = 'data'; $last_line = ''; $i = 0; - while ( $data = socket_read($remote, 8388608, PHP_NORMAL_READ) ) + while ( $data = $remote->read_normal(8388608) ) { $data_trim = trim($data, "\r\n"); if ( $mode != 'data' ) @@ -569,11 +648,11 @@ { if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) ) { - $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ); + $postdata = $remote->read_binary(intval($_SERVER['HTTP_CONTENT_LENGTH'])); } else { - $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ); + $postdata = $remote->read_normal(8388608); } if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) ) { @@ -601,7 +680,7 @@ $_SERVER['REQUEST_METHOD'] = $method; // get remote IP and port - socket_getpeername($remote, $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']); + $remote->get_peer_info($_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']); $_GET = array(); if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $params, $matches) ) @@ -691,25 +770,20 @@ if ( !$this->in_keepalive && defined('HTTPD_WS_CHILD') ) { - // if ( defined('HTTPD_WS_CHILD') ) - // status('Closing connection'); - @socket_shutdown($remote); - @socket_close($remote); - if ( function_exists('status') ) - status('Exiting child process'); - exit(0); + // connection: close + // continue on to the shutdown handler + continue; } else if ( defined('HTTPD_WS_CHILD') ) { // if ( defined('HTTPD_WS_CHILD') ) // status('Continuing connection'); - // @socket_write($remote, "\r\n\r\n"); + // $remote->write("\r\n\r\n"); $last_finish_time = microtime(true); } else { - @socket_shutdown($remote); - @socket_close($remote); + $remote->destroy(); } } } @@ -743,15 +817,15 @@ $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: $connection\r\n"); - @socket_write($socket, "Content-Type: $contenttype\r\n"); + $socket->write("HTTP/1.1 $http_code $reason_code\r\n"); + $socket->write("Server: $this->server_string"); + $socket->write("Connection: $connection\r\n"); + $socket->write("Content-Type: $contenttype\r\n"); if ( !empty($headers) ) { - @socket_write($socket, "$headers\r\n"); + $socket->write("$headers\r\n"); } - @socket_write($socket, "\r\n"); + $socket->write("\r\n"); } /** @@ -831,7 +905,7 @@ $sz = strlen($contents); $this->send_client_headers($socket, 200, 'text/html', "Content-length: $sz\r\n"); - @socket_write($socket, $contents); + $socket->write($contents); return true; } @@ -858,7 +932,7 @@ // send body while ( $blk = @fread($fh, 768000) ) { - @socket_write($socket, $blk); + $socket->write($blk); } fclose($fh); return true; @@ -909,7 +983,7 @@ // send body while ( $blk = @fread($fh, 768000) ) { - @socket_write($socket, $blk); + $socket->write($blk); } fclose($fh); return true; @@ -983,7 +1057,7 @@ // $output = dechex(strlen($output)) . "\r\n$output"; // write body - @socket_write($socket, $output); + $socket->write($output); $this->headers_sent = false; @@ -1056,7 +1130,7 @@ $lm_date = date('r', filemtime(__FILE__)); $size = strlen($image_data); $this->send_client_headers($socket, 200, $type, "Last-Modified: $lm_date\r\nContent-Length: $size"); - @socket_write($socket, $image_data); + $socket->write($image_data); } else { @@ -1126,7 +1200,7 @@ $this->send_client_headers($socket, 200, 'text/html', "Content-Length: $len"); // write to the socket - @socket_write($socket, $contents); + $socket->write($contents); return true; break; @@ -1191,7 +1265,7 @@ $headers = 'Content-length: ' . strlen($html); $this->send_client_headers($socket, $http_code, 'text/html', $headers); - @socket_write($socket, $html); + $socket->write($html); } /** @@ -1528,6 +1602,286 @@ return $array; } + /** + * Handle an IPC event. Called only upon SIGUSR2. + */ + + function _ipc_event() + { + $pid = getmypid() . ':' . $this->parent_pid; + + // decide which socket to use + if ( defined('HTTPD_WS_CHILD') ) + $sock =& $this->parent_sock; + else + $sock =& $this->child_sock; + + // try to read the event + // this sometimes gets hung up because socket_set_timeout() doesn't seem to work on its own set of + // functions (it only works on PHP's normal streams) + if ( $line = @fgets($sock, 1024) ) + { + $line = trim($line); + list($action, $param) = explode(' ', $line); + switch($action) + { + case 'exit': + // this is to prevent zombie children + pcntl_waitpid(intval($param), $status); + // we know this child is dead now, remove them from the list + foreach ( $this->child_list as $i => $pid ) + { + if ( $pid === intval($param) ) + { + unset($this->child_list[$i]); + $this->child_list = array_values($this->child_list); + break; + } + } + break; + case 'die': + // only do this if this is a child (both security and design) + if ( defined('HTTPD_WS_CHILD') ) + { + if ( function_exists('status') ) + { + status('Received shutdown request, complying'); + } + $this->send_ipc_event("exit " . getmypid()); + exit(0); + } + break; + default: + break; + } + } + } + + /** + * Send an IPC event. + * @param string Data to write to the socket, newline will be added automatically + */ + + function send_ipc_event($data) + { + if ( defined('HTTPD_WS_CHILD') ) + $sock =& $this->parent_sock; + else + $sock =& $this->child_sock; + + $data = rtrim($data, "\r\n") . "\n"; + @fwrite($sock, $data); + + // if we're a child, signal the parent + if ( defined('HTTPD_WS_CHILD') ) + { + posix_kill($this->parent_pid, SIGUSR2); + } + // if we're the parent, signal all children + else + { + foreach ( $this->child_list as $pid ) + { + posix_kill($pid, SIGUSR2); + } + } + } + +} + +/** + * Socket abstraction layer - low-level socket functions (socket_*) + */ + +class Socket_Raw +{ + var $sock; + var $socket_initted = false; + + function tcp_listen($address, $port) + { + $this->sock = @socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); + if ( !$this->sock ) + throw new Exception('Could not create socket'); + $result = @socket_bind($this->sock, $address, $port); + if ( !$result ) + throw new Exception("Could not bind to $address:$port"); + $this->socket_initted = true; + $result = @socket_listen($this->sock, SOMAXCONN); + if ( !$result ) + throw new Exception("Could not listen for connections $address:$port"); + + $this->socket_initted = true; + } + + function destroy() + { + if ( $this->socket_initted ) + { + // http://us3.php.net/manual/en/function.socket-bind.php + if ( !@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1) ) + { + echo socket_strerror(socket_last_error($this->sock)) . "\n"; + } + @socket_shutdown($this->sock, 2); + @socket_close($this->sock); + } + } + + function accept() + { + $remote = false; + $timeout = 5; + switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) { + case 2: + return false; + case 1: + $remote = @socket_accept($this->sock); + $return = new Socket_Raw(); + $return->sock = $remote; + $return->socket_initted = true; + return $return; + break; + case 0: + return false; + } + } + + /** + * Closes the socket but doesn't destroy it. + */ + + function soft_shutdown() + { + @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1); + socket_close($this->sock); + } + + function set_timeout($timeout, $usec = false) + { + // doesn't work in this. + } + + function read_normal($length = 1024) + { + return @socket_read($this->sock, $length, PHP_NORMAL_READ); + } + + function read_binary($length = 1024) + { + return @socket_read($this->sock, $length, PHP_BINARY_READ); + } + + function timed_out() + { + $md = @socket_get_status($this->sock); + return ( isset($md['timed_out']) ) ? $md['timed_out'] : false; + } + + function get_peer_info(&$addr, &$port) + { + socket_getpeername($this->sock, $addr, $port); + } + + function write($data) + { + return @socket_write($this->sock, $data); + } + + function is_eof() + { + // feof() not supported + return false; + } +} + +/** + * Socket abstraction layer - PHP stream support + */ + +class Socket_Stream +{ + var $sock; + var $socket_initted = false; + + function tcp_listen($address, $port) + { + $this->sock = @stream_socket_server("tcp://$address:$port", $errno, $errstr); + if ( !$this->sock ) + throw new Exception("Could not create the socket: error $errno: $errstr"); + } + + function destroy() + { + if ( $this->socket_initted ) + { + // PHP >= 5.2.1 + if ( function_exists('stream_socket_shutdown') ) + { + @stream_socket_shutdown($this->sock, STREAM_SHUT_RDWR); + } + fclose($this->sock); + } + } + + function accept() + { + // the goal of a custom accept() with *_select() is to tick every 5 seconds to allow signals. + stream_set_blocking($this->sock, 1); + $timeout = 5; + $selection = @stream_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout); + if ( !$selection ) + { + return false; + } + $remote = stream_socket_accept($this->sock); + $return = new Socket_Stream(); + $return->sock = $remote; + $return->socket_initted = true; + return $return; + } + + function soft_shutdown() + { + fclose($this->sock); + } + + function set_timeout($timeout, $usec = false) + { + return ( $usec ) ? @stream_set_timeout($this->sock, 0, $usec) : @stream_set_timeout($this->sock, $timeout); + } + + function read_normal($length = 1024) + { + return @fgets($this->sock, $length); + } + + function read_binary($length = 1024) + { + return @fread($this->sock, $length); + } + + function timed_out() + { + $md = @stream_get_meta_data($this->sock); + return ( isset($md['timed_out']) ) ? $md['timed_out'] : false; + } + + function get_peer_info(&$addr, &$port) + { + $peer = stream_socket_get_name($this->sock, true); + list($addr, $port) = explode(':', $peer); + } + + function write($data) + { + return @fwrite($this->sock, $data); + } + + function is_eof() + { + return feof($this->sock); + } } /**