# HG changeset patch # User Dan # Date 1222226653 14400 # Node ID d643bfb862d82a48e89d9ed7aa8db85a4c1a5d83 # Parent b7f1952cef8d1f766f32f7e327313ddf1b2bc58d Replaced multithreading in WebServer with a full multithreading library that properly handles IPC and child management diff -r b7f1952cef8d -r d643bfb862d8 greyhound.php --- a/greyhound.php Mon Sep 01 17:03:44 2008 -0400 +++ b/greyhound.php Tue Sep 23 23:24:13 2008 -0400 @@ -30,47 +30,52 @@ @ini_set('display_errors', 'on'); -// include files -require('functions.php'); - // get the root define('GREY_ROOT', dirname(__FILE__)); -// ignore this, it allows using a different config file when a Mercurial repository -// exists in Greyhound's root directory (to allow the devs to have their own config -// separate from the default) - -if ( @is_dir(GREY_ROOT . '/.hg') ) - require(GREY_ROOT . '/config.dev.php'); -else - require(GREY_ROOT . '/config.php'); - -// create directories -@mkdir('./compiled'); - // what kind of terminal do we have? $use_colors = ( @in_array(@$_SERVER['TERM'], array('linux', 'xterm', 'vt100')) ) ? true : false; +require(GREY_ROOT . '/functions.php'); // start up... status('Starting Greyhound Web Control v' . GREY_VERSION); status('loading files'); -require('webserver.php'); +require_once(GREY_ROOT . '/webserver.php'); define('SMARTY_DIR', GREY_ROOT . '/smarty/'); -require(GREY_ROOT . '/smarty/Smarty.class.php'); -require(GREY_ROOT . '/playlist.php'); -require(GREY_ROOT . '/json.php'); -require(GREY_ROOT . '/ajax.php'); -require(GREY_ROOT . '/imagetools.php'); -require(GREY_ROOT . '/sessions.php'); +require_once(GREY_ROOT . '/smarty/Smarty.class.php'); +require_once(GREY_ROOT . '/playlist.php'); +require_once(GREY_ROOT . '/json.php'); +require_once(GREY_ROOT . '/ajax.php'); +require_once(GREY_ROOT . '/uiconfig.php'); +require_once(GREY_ROOT . '/imagetools.php'); +require_once(GREY_ROOT . '/sessions.php'); + +// +// LOAD OUR CONFIG +// Amarok launches Greyhound with a different wd than Greyhound's root. This means +// that we can drop our own config (set up from the web UI) in there and load that +// instead of the default config, which comes with greyhound. +// + +grey_reload_config(); + +// create directories +@mkdir('./compiled'); // signal handler function sigterm($signal) { - global $httpd; + global $httpd, $avahi_process; if ( !defined('HTTPD_WS_CHILD') ) + { status("Caught SIGTERM, cleaning up."); + if ( is_resource($avahi_process) ) + { + @proc_terminate($avahi_process); + } + } exit(0); } @@ -108,24 +113,53 @@ status('starting PhpHttpd'); $httpd = new WebServer($ip, $port); + // if we have avahi and proc_open support, publish the service (new) + if ( $allowcontrol && function_exists('proc_open') && $path = which('avahi-publish') ) + { + // get our current hostname (hack, sort of) + $hostfile = tempnam('hostname', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' )); + system("hostname > '$hostfile' 2>/dev/null"); + $hostname = trim(@file_get_contents($hostfile)); + unlink($hostfile); + if ( !empty($hostname) ) + { + status('Publishing service on local network with Avahi'); + $descriptorspec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + $thisuser = get_current_user(); + + $avahi_command = "'$path' -s \"{$thisuser}'s\"' AmaroK playlist on $hostname' _greyhound._tcp $port"; + $avahi_process = proc_open($avahi_command, $descriptorspec, $avahi_pipes); + if ( !$avahi_process ) + { + warning('proc_open() failed; could not start announcement of service on Avahi network'); + } + } + } + // setup handlers status('initializing handlers'); $httpd->add_handler('index', 'function', 'amarok_playlist'); $httpd->add_handler('login', 'function', 'greyhound_login_page'); $httpd->add_handler('logout', 'function', 'greyhound_logout'); + $httpd->add_handler('config', 'function', 'greyhound_config'); $httpd->add_handler('action.json', 'function', 'ajax_request_handler'); $httpd->add_handler('artwork', 'function', 'artwork_request_handler'); $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'); $httpd->add_handler('spacer.gif', 'file', GREY_ROOT . '/spacer.gif'); + $httpd->threader->ipc_register('reloadconfig', 'grey_reload_config'); // 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 ) - { + // if ( $allow_fork ) + // { status('Preloading themes'); $dh = @opendir(GREY_ROOT . '/themes'); @@ -138,7 +172,7 @@ 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'; @@ -164,13 +198,5 @@ // we've got an httpd instance; rebuild the playlist rebuild_playlist(); - - // if this is the parent, also ask the children to rebuild. - if ( !defined('HTTPD_WS_CHILD') ) - { - foreach ( $httpd->child_list as $pid ) - { - posix_kill($pid, SIGUSR1); - } - } } + diff -r b7f1952cef8d -r d643bfb862d8 multithreading.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/multithreading.php Tue Sep 23 23:24:13 2008 -0400 @@ -0,0 +1,336 @@ +event_sigchld(); + } + } +} + +/** + * Global signal handler for SIGUSR2. + */ + +function Threader_SigUsr2() +{ + global $threader_instances; + foreach ( $threader_instances as &$mt ) + { + if ( is_object($mt) ) + { + $parchild = $mt->is_child() ? 'child' : 'parent'; + $mt->event_sigusr2(); + } + } +} + +/** + * List of Threader instances. Needed for global handling of signals. + * @var array + */ + +global $threader_instances; +$threader_instances = array(); + +/** + * Tools for emulating multi-threaded operation in PHP scripts. + * @package Amarok + * @subpackage WebControl + * @author Dan Fuhry + * @license GNU General Public License + */ + +class Threader +{ + + /** + * Return value of fork() if the process is a child. + * @const int + */ + + const FORK_CHILD = -1; + + /** + * Set to true if this is a child process. No exceptions. + * @var bool + * @access private + */ + + private $is_child = false; + + /** + * Sockets for inter-process communication. + * @var array + * @access private + */ + + protected $ipc_sockets = array(); + + /** + * Socket for communication with the parent. Obviously only used after calling fork(). + * @var resource + * @access private + */ + + protected $parent_sock = false; + + /** + * Services_JSON instance. + * @var object + * @access private + */ + + protected $json = false; + + /** + * PID of the parent process. + * @var int + * @access private + */ + + protected $parent_pid = 1; + + /** + * List of actions for IPC events. + * @var array + * @access private + */ + + protected $ipc_actions = array(); + + /** + * Constructor. Sets up signal handlers. Nothing to see here, move along. + */ + + public function __construct() + { + global $threader_instances; + + declare(ticks=1); + + $threader_instances[] =& $this; + + pcntl_signal(SIGUSR2, 'Threader_SigUsr2'); + pcntl_signal(SIGCHLD, 'Threader_SigChld'); + + $this->json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); + $this->parent_pid = getmypid(); + } + + /** + * Forks the current process. See your system's fork(2) man page for details. + * @return int FORK_CHILD if child process, PID of child if parent process. Returns false on failure. + */ + + public function fork() + { + // create our new sockets for IPC + $socket_pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + // fork (emoticon of the day: --ipc_sockets[$fork_result] = $socket_pair[1]; + return $fork_result; + } + else + { + // we are the child. + fclose($socket_pair[1]); + $this->parent_sock = $socket_pair[0]; + $this->is_child = true; + return FORK_CHILD; + } + } + + /** + * Are we a child? + * @return bool + */ + + public function is_child() + { + return $this->is_child; + } + + /** + * Register an action so that when it is fired over IPC, a custom function can be called. + * @param string Action + * @param callback Function to call + * @return true on success, false on failure + */ + + function ipc_register($action, $callback) + { + if ( !is_string($action) || empty($action) || !is_callable($callback) ) + { + return false; + } + $this->ipc_actions[$action] = $callback; + return true; + } + + /** + * Send through an IPC event. If this is a child, it only notifies the parent; if we're the parent, all children are notified. + * @param array Data to be sent through. This must be an associative array containing an "action" key at minimum. If this a key "propagate" set to true, a parent that receives this will propagate the message to all children. + * @return null + */ + + function ipc_send($data) + { + if ( !isset($data['action']) ) + { + return false; + } + $data = $this->json->encode($data); + if ( $this->is_child() ) + { + fwrite($this->parent_sock, "$data\n"); + // signal the parent that we've got an event + posix_kill($this->parent_pid, SIGUSR2); + } + else + { + // signal each child + foreach ( $this->ipc_sockets as $pid => $socket ) + { + fwrite($socket, "$data\n"); + posix_kill($pid, SIGUSR2); + } + } + return null; + } + + /** + * Handler for SIGCHLD events. + * @access private + */ + + function event_sigchld() + { + // this should never happen to children. + if ( $this->is_child() ) + { + return null; + } + + // wait for child to exit. + pcntl_wait($status); + // for each child PID, kill with signal 0 (effectively, test if process is alive) + // if posix_kill fails, it's dead so remove it from the list. + foreach ( $this->ipc_sockets as $pid => $socket ) + { + if ( !@posix_kill($pid, 0) ) + { + // signal failed. + fclose($socket); + unset($this->ipc_sockets[$pid]); + } + } + } + + /** + * Handler for IPC events. + * @access private + */ + + function event_sigusr2() + { + if ( $this->is_child() ) + { + // this is easy - the parent sent the signal. + $command = rtrim(fgets($this->parent_sock, 102400), "\n"); + } + else + { + // since we can't find which PID sent the signal, set the timeout to a very small amount + // of time and try to read; if we get something, awesome. + foreach ( $this->ipc_sockets as $pid => $socket ) + { + // 1000 microseconds = 1/80th of the time it takes you to blink. + @stream_set_timeout($socket, 0, 1000); + $command = rtrim(@fgets($socket, 102400), "\n"); + if ( !empty($command) ) + { + break; + } + } + } + if ( empty($command) ) + { + // hmm, got a sigusr2 without an incoming command. oh well, ignore. + return null; + } + $command = $this->json->decode($command); + if ( !isset($command['action']) ) + { + // no action = no way to figure out how to handle this. + return null; + } + if ( !isset($this->ipc_actions[$command['action']]) ) + { + // action not registered + return null; + } + // should we propagate this event? + if ( !$this->is_child() && isset($command['propagate']) && $command['propagate'] === true ) + { + $this->ipc_send($command); + } + // we're good + @call_user_func($this->ipc_actions[$command['action']], $command, $this); + } + + /** + * Kills all child processes. + * @access public + */ + + public function kill_all_children() + { + foreach ( $this->ipc_sockets as $pid => $socket ) + { + $socklen = count($this->ipc_sockets); + posix_kill($pid, SIGTERM); + // wait until we are conscious of this child's death + while ( count($this->ipc_sockets) >= $socklen ) + { + usleep(20000); + } + } + } + +} + +?> diff -r b7f1952cef8d -r d643bfb862d8 webserver.php --- a/webserver.php Mon Sep 01 17:03:44 2008 -0400 +++ b/webserver.php Tue Sep 23 23:24:13 2008 -0400 @@ -13,6 +13,8 @@ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. */ +require('multithreading.php'); + /** * Version of the server * @const string @@ -60,6 +62,13 @@ var $bind_address = '127.0.0.1'; /** + * Port we're listening on + * @var int + */ + + var $port = 8080; + + /** * Socket abstraction object * @var object */ @@ -130,6 +139,13 @@ var $allow_fork = true; /** + * Multi-threading manager. + * @var object + */ + + var $threader = false; + + /** * Keep-alive support uses this to track what the client requested. * Only used if $allow_fork is set to true. * @var bool @@ -180,6 +196,14 @@ var $parent_pid = 0; /** + * List of IPC request handlers + * @var array + * @access private + */ + + var $ipc_handlers = array(); + + /** * Sockets for parent and child to communicate * @var resource * @var resource @@ -189,6 +213,13 @@ var $child_sock = null; /** + * Switched on when a graceful reboot event is sent. + * @var bool + */ + + var $reboot_sent = false; + + /** * Constructor. * @param string IPv4 address to bind to * @param int Port number @@ -201,12 +232,6 @@ @set_time_limit(0); @ini_set('memory_limit', '128M'); - // do we have socket functions? - if ( !function_exists('socket_create') ) - { - burnout('System does not support socket functions. Please rebuild your PHP or install an appropriate extension.'); - } - // make sure we're not running as root // note that if allow_root is true, you must specify a UID/GID (or user/group) to switch to once the socket is bound $allow_root = ( $port < 1024 ) ? true : false; @@ -275,8 +300,10 @@ } $this->bind_address = $address; + $this->port = $port; $this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n"; $this->parent_pid = getmypid(); + $this->threader = new Threader(); // create a UUID $uuid_base = md5(microtime() . ( function_exists('mt_rand') ? mt_rand() : rand() )); @@ -294,7 +321,7 @@ function __destruct() { - if ( !defined('HTTPD_WS_CHILD') && $this->socket_initted ) + if ( !$this->threader->is_child() && $this->socket_initted ) { if ( function_exists('status') ) status('WebServer: destroying socket'); @@ -305,18 +332,138 @@ { if ( function_exists('status') ) status('WebServer: asking all children to exit'); - $this->send_ipc_event("die _"); + $this->threader->kill_all_children(); } + } + } + + /** + * Reboot the server. Useful for applying new settings. + * @param string Optional, new IP address to bind to + * @param int Optional, new port to bind to + * @param bool Optional, whether to allow forking or not + */ + + function reboot($addr = null, $port = null, $allow_fork = null) + { + if ( function_exists('status') ) + status('Reboot request has been received'); + + $addr = ( !is_string($addr) ) ? $this->bind_address : $addr; + $port = ( !is_int($port) ) ? $this->port : $port; + $fork = ( !is_bool($allow_fork) ) ? $this->allow_fork : $allow_fork; + + // + // REBOOTING IS A COMPLICATED THING. + // We need to ask all children to close any existing connections so + // that all relevant socket resources can be freed. Then we need to + // call the constructor again to respawn the server, and finally + // re-enter the server loop. + // + // However, reboot() is often called from a PHP-based handler. This + // means that some config page probably still needs to be sent. What + // we can do here is send an IPC event that fires the actual reboot, + // then return to allow the current page to finish up. We also need + // to signal the current process to shut down any existing keep- + // alive connections. This can be accomplished by setting in_keepalive + // to false. + // + + // Kill the entire child after this response is sent + $this->in_keepalive = false; + + // If we're the parent process, we need to know that a reboot event + // was fired, and thus the server's main socket needs to be destroyed + // and recreated. This is just done with another boolean switch. + $this->reboot_sent = true; + + // this is really to track if there are any children + $oldfork = $this->allow_fork; + + // Set our new server flags + $this->bind_address = $addr; + $this->port = $port; + $this->allow_fork = $fork; + + // If we're a child, we have to tell the parent what the hell is + // going on, and then get out of here as quickly as possible + // (and other children should do the same). If this is a child, + // fire an IPC reboot event. Else, fire a "die all" event + if ( $this->threader->is_child() ) + { + if ( function_exists('status') ) + status('Signaling parent with parameter changes (fork = ' . intval($fork) . ') + reboot request'); + // this is the child + $this->threader->ipc_send(array( + 'action' => 'ws_reboot', + 'addr' => $addr, + 'port' => $port, + 'fork' => $fork + )); + } + else if ( !$this->threader->is_child() && $oldfork ) + { + if ( function_exists('status') ) + status('Waiting on all children'); - // that last operation should have been asynchronous, so shut everything down now - @socket_shutdown($this->parent_sock); - @socket_close($this->parent_sock); + // this is the parent, and there are children present + $this->threader->kill_all_children(); + + // all children are dead, we are ok to respawn + $this->respawn(); + } + else + { + // this is a childless parent; delay any action until the current + // request has been sent (do nothing now) } - else if ( defined('HTTPD_WS_CHILD') ) + } + + /** + * Respawns the server. All children should be dead, and any client + * connections must be closed. + */ + + function respawn() + { + $this->reboot_sent = false; + + if ( function_exists('status') ) + status('Respawn event sent'); + $this->server->destroy(); + unset($this->server); + + // try to spawn up to 10 times + for ( $i = 0; $i < 10; $i++ ) { - @socket_shutdown($this->child_sock); - @socket_close($this->child_sock); + try + { + $this->__construct($this->bind_address, $this->port); + } + catch ( Exception $e ) + { + if ( $i == 9 ) + { + if ( function_exists('burnout') ) + { + burnout("Couldn't respawn because one of the child processes did not die, and thus the port was not freed."); + } + exit(1); + } + if ( function_exists('status') ) + { + status("Respawn failed, retrying in 2 seconds"); + } + usleep(2000000); + continue; + } + break; } + + if ( function_exists('status') ) + status('Respawn is complete, entering server loop with bind_address = ' . $this->bind_address . ' allow_fork = ' . strval(intval($this->allow_fork))); + + // all handlers should already be set up, so just break out and we should automatically continue the server loop } /** @@ -325,44 +472,29 @@ 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 ) { + ## + ## STAGE 0: CLEANUP FROM PREVIOUS RUN + ## + // if this is a child process, we're finished - close up shop - if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive ) + if ( $this->threader->is_child() && !$this->in_keepalive ) { if ( function_exists('status') ) status('Exiting child process'); $remote->destroy(); - // let the parent know that we're out of here - $this->send_ipc_event("exit " . getmypid()); - - // bye exit(0); } + ## + ## STAGE 1: LISTENER AND INIT + ## + // wait for connection... - // trick from http://us.php.net/manual/en/function.socket-accept.php - if ( !defined('HTTPD_WS_CHILD') ) + if ( !$this->threader->is_child() ) { $remote = $this->server->accept(); } @@ -376,42 +508,27 @@ // fork off if possible if ( function_exists('pcntl_fork') && $this->allow_fork && !$this->in_keepalive ) { - $pid = pcntl_fork(); - if ( $pid == -1 ) + if ( $this->threader->fork() == FORK_CHILD ) { - // do nothing; continue responding to request in single-threaded mode + // this is the child + define('HTTPD_WS_CHILD', 1); } - else if ( $pid ) + else { // we are the parent, continue listening $remote->soft_shutdown(); $this->child_list[] = $pid; continue; } - else - { - // this is the child - define('HTTPD_WS_CHILD', 1); - - // 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')); - } - } } $this->in_keepalive = false; $this->headers_sent = false; $this->in_scriptlet = false; - // - // READ THE REQUEST - // + ## + ## STAGE 2: READ 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 @@ -427,7 +544,7 @@ if ( $start_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) || $remote->is_eof() ) { // request expired -- end the process here - if ( !defined('HTTPD_WS_CHILD') ) + if ( !$this->threader->is_child() ) $remote->destroy(); continue 2; @@ -445,10 +562,14 @@ $last_line = $line; } + ## + ## STAGE 3: PARSE REQUEST AND HEADERS + ## + // parse request $client_headers = trim($client_headers); - if ( isset($last_finish_time) && empty($client_headers) && defined('HTTPD_WS_CHILD') && $last_finish_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) ) + if ( isset($last_finish_time) && empty($client_headers) && $this->threader->is_child() && $last_finish_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) ) { status('[debug] keep-alive connection timed out (checkpoint 2)'); continue; // will jump back to the start of the loop and kill the child process @@ -466,7 +587,7 @@ $method =& $match[1]; $uri =& $match[2]; - // set client headers + // set client header SERVER variables foreach ( $_SERVER as $key => $_ ) { if ( preg_match('/^HTTP_/', $key) ) @@ -482,7 +603,7 @@ } // enable keep-alive if requested - if ( isset($_SERVER['HTTP_CONNECTION']) && defined('HTTPD_WS_CHILD') ) + if ( isset($_SERVER['HTTP_CONNECTION']) && $this->threader->is_child() ) { $this->in_keepalive = ( strtolower($_SERVER['HTTP_CONNECTION']) === 'keep-alive' ); } @@ -515,133 +636,7 @@ $_FILES = array(); if ( $method == 'POST' ) { - // read POST data - if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) ) - { - // this is a multipart request - $boundary =& $match[1]; - $mode = 'data'; - $last_line = ''; - $i = 0; - while ( $data = $remote->read_normal(8388608) ) - { - $data_trim = trim($data, "\r\n"); - if ( $mode != 'data' ) - { - $data = str_replace("\r", '', $data); - } - if ( ( $data_trim === "--$boundary" || $data_trim === "--$boundary--" ) && $i > 0 ) - { - // trim off the first LF and the last CRLF - $currval_data = substr($currval_data, 1, strlen($currval_data)-3); - - // this is the end of a part of the message; parse it into either $_POST or $_FILES - if ( is_string($have_a_file) ) - { - - // write data to a temporary file - $errcode = UPLOAD_ERR_OK; - $tempfile = tempnam('phpupload', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' )); - if ( $fh = @fopen($tempfile, 'w') ) - { - if ( empty($have_a_file) ) - { - $errcode = UPLOAD_ERR_NO_FILE; - } - else - { - fwrite($fh, $currval_data); - } - fclose($fh); - } - else - { - $errcode = UPLOAD_ERR_CANT_WRITE; - } - $_FILES[$currval_name] = array( - 'name' => $have_a_file, - 'type' => $currval_type, - 'size' => filesize($tempfile), - 'tmp_name' => $tempfile, - 'error' => $errcode - ); - } - else - { - $_POST[$currval_name] = $currval_data; - } - } - - if ( $data_trim === "--$boundary" ) - { - // switch from "data" mode to "headers" mode - $currval_name = ''; - $currval_data = ''; - $currval_type = ''; - $have_a_file = false; - $mode = 'headers'; - } - else if ( $data_trim === "--$boundary--" ) - { - // end of request - break; - } - else if ( ( empty($data_trim) && empty($last_line) ) && $mode == 'headers' ) - { - // start of data - $mode = 'data'; - } - else if ( $mode == 'headers' ) - { - // read header - // we're only looking for Content-Disposition and Content-Type - if ( preg_match('#^Content-Disposition: form-data; name="([^"\a\t\r\n]+)"(?:; filename="([^"\a\t\r\n]+)")?#i', $data_trim, $match) ) - { - // content-disposition header, set name and mode. - $currval_name = $match[1]; - if ( isset($match[2]) ) - { - $have_a_file = $match[2]; - } - else - { - $have_a_file = false; - } - } - else if ( preg_match('#^Content-Type: ([a-z0-9-]+/[a-z0-9/-]+)$#i', $data_trim, $match) ) - { - $currval_type = $match[1]; - } - } - else if ( $mode == 'data' ) - { - $currval_data .= $data; - } - $last_line = $data_trim; - $i++; - } - } - else - { - if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) ) - { - $postdata = $remote->read_binary(intval($_SERVER['HTTP_CONTENT_LENGTH'])); - } - else - { - $postdata = $remote->read_normal(8388608); - } - if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) ) - { - if ( isset($matches[1]) ) - { - foreach ( $matches[0] as $i => $_ ) - { - $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true; - } - } - } - } + $this->parse_post_data($remote); } // parse URI @@ -659,6 +654,7 @@ // get remote IP and port $remote->get_peer_info($_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']); + // process $_GET $_GET = array(); if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $params, $matches) ) { @@ -671,10 +667,15 @@ } } + // Parse GET, POST, and FILES into multi-depth arrays $_GET = $this->parse_multi_depth_array($_GET); $_POST = $this->parse_multi_depth_array($_POST); $_FILES = $this->parse_multi_depth_array($_FILES); + ## + ## STAGE 4: HANDLER RESOLUTION + ## + // init handler $handler = false; @@ -731,8 +732,16 @@ } } + ## + ## STAGE 5: HANDLER CALL + ## + $this->send_standard_response($remote, $handler, $uri, $params); + ## + ## STAGE 6: CLEANUP + ## + // now that we're done sending the response, delete any temporary uploaded files if ( !empty($_FILES) ) { @@ -745,22 +754,194 @@ } } - if ( !$this->in_keepalive && defined('HTTPD_WS_CHILD') ) + if ( !$this->in_keepalive && $this->threader->is_child() ) { // connection: close // continue on to the shutdown handler continue; } - else if ( defined('HTTPD_WS_CHILD') ) + else if ( $this->threader->is_child() ) { - // if ( defined('HTTPD_WS_CHILD') ) + // if ( $this->threader->is_child() ) // status('Continuing connection'); // $remote->write("\r\n\r\n"); $last_finish_time = microtime(true); } else { + // standalone process $remote->destroy(); + + // if a reboot was fired and we're running in single-process mode, now is the time to respawn + if ( !$this->threader->is_child() && $this->reboot_sent ) + { + $this->respawn(); + } + } + } + } + + /** + * Parse POST data and format $_POST and $_FILES. + * @param resource Remote socket + */ + + function parse_post_data($remote) + { + $postdata = ''; + + // read POST data + if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) ) + { + // this is a multipart request + $boundary =& $match[1]; + $mode = 'data'; + $last_line = ''; + $i = 0; + while ( $data = $remote->read_normal(8388608) ) + { + $data_trim = trim($data, "\r\n"); + if ( $mode != 'data' ) + { + $data = str_replace("\r", '', $data); + } + if ( ( $data_trim === "--$boundary" || $data_trim === "--$boundary--" ) && $i > 0 ) + { + // trim off the first LF and the last CRLF + if ( HTTPD_SOCKET_LAYER == 'Raw' ) + $currval_data = substr($currval_data, 1, strlen($currval_data)-3); + else + $currval_data = substr($currval_data, 0, strlen($currval_data)-2); + + // this is the end of a part of the message; parse it into either $_POST or $_FILES + if ( is_string($have_a_file) ) + { + // write data to a temporary file + $errcode = UPLOAD_ERR_OK; + $tempfile = tempnam('phpupload', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' )); + if ( $fh = @fopen($tempfile, 'w') ) + { + if ( empty($have_a_file) ) + { + $errcode = UPLOAD_ERR_NO_FILE; + } + else + { + fwrite($fh, $currval_data); + } + fclose($fh); + } + else + { + $errcode = UPLOAD_ERR_CANT_WRITE; + } + $_FILES[$currval_name] = array( + 'name' => $have_a_file, + 'type' => $currval_type, + 'size' => filesize($tempfile), + 'tmp_name' => $tempfile, + 'error' => $errcode + ); + } + else + { + if ( preg_match('/\[\]$/', $currval_name) ) + { + if ( !isset($_POST[$currval_name]) || ( isset($_POST[$currval_name]) && !is_array($_POST[$currval_name]) ) ) + $_POST[$currval_name] = array(); + + $_POST[$currval_name][] = $currval_data; + } + else + { + $_POST[$currval_name] = $currval_data; + } + } + } + + if ( $data_trim === "--$boundary" ) + { + // switch from "data" mode to "headers" mode + $currval_name = ''; + $currval_data = ''; + $currval_type = ''; + $have_a_file = false; + $mode = 'headers'; + } + else if ( $data_trim === "--$boundary--" ) + { + // end of request + break; + } + else if ( ( empty($data_trim) && ( ( HTTPD_SOCKET_LAYER == 'Raw' && empty($last_line) ) || HTTPD_SOCKET_LAYER != 'Raw' ) ) && $mode == 'headers' ) + { + // start of data + $mode = 'data'; + } + else if ( $mode == 'headers' ) + { + // read header + // we're only looking for Content-Disposition and Content-Type + if ( preg_match('#^Content-Disposition: form-data; name="([^"\a\t\r\n]+)"(?:; filename="([^"\a\t\r\n]+)")?#i', $data_trim, $match) ) + { + // content-disposition header, set name and mode. + $currval_name = $match[1]; + if ( isset($match[2]) ) + { + $have_a_file = $match[2]; + } + else + { + $have_a_file = false; + } + } + else if ( preg_match('#^Content-Type: ([a-z0-9-]+/[a-z0-9/-]+)$#i', $data_trim, $match) ) + { + $currval_type = $match[1]; + } + } + else if ( $mode == 'data' ) + { + $currval_data .= $data; + } + $last_line = $data_trim; + $i++; + } + } + else + { + if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) ) + { + $postdata = $remote->read_binary(intval($_SERVER['HTTP_CONTENT_LENGTH'])); + } + else + { + $postdata = $remote->read_normal(8388608); + } + if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]%-]+)(=[^ &]+)?/i', $postdata, $matches) ) + { + if ( isset($matches[1]) ) + { + foreach ( $matches[0] as $i => $_ ) + { + $currval_name =& $matches[2][$i]; + $currval_data = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true; + $currval_name = urldecode($currval_name); + + if ( preg_match('/\[\]$/', $currval_name) ) + { + $basename = preg_replace('/\[\]$/', '', $currval_name); + if ( !isset($_POST[$basename]) || ( isset($_POST[$basename]) && !is_array($_POST[$basename]) ) ) + $_POST[$basename] = array(); + + $_POST[$basename][] = $currval_data; + } + else + { + $_POST[$currval_name] = $currval_data; + } + } + } } } } @@ -1603,7 +1784,7 @@ { foreach ( $array as $key => $value ) { - if ( preg_match('/^([^\[\]]+)\[([^\]]+)\]/', $key, $match) ) + if ( preg_match('/^([^\[\]]+)\[([^\]]*)\]/', $key, $match) ) { $parent =& $match[1]; $child =& $match[2]; @@ -1611,7 +1792,14 @@ { $array[$parent] = array(); } - $array[$parent][$child] = $value; + if ( empty($child) ) + { + $array[$parent][] = $value; + } + else + { + $array[$parent][$child] = $value; + } unset($array[$key]); $array[$parent] = $this->parse_multi_depth_array($array[$parent]); } @@ -1625,85 +1813,33 @@ 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 'set_addr': + $this->bind_address = $param; + break; + case 'set_port': + $this->port = intval($param); + break; + case 'set_fork': + $this->allow_fork = ( $param == '1' ); + break; + case 'reboot': + if ( !$this->threader->is_child() ) { - 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 ) + list(, $addr, $port, $fork) = explode(' ', $line); + $fork = ( $fork === '1' ); + $this->reboot($addr, intval($port), $fork); + } + break; + default: + if ( isset($this->ipc_handlers[$action]) ) { - if ( $pid === intval($param) ) - { - unset($this->child_list[$i]); - $this->child_list = array_values($this->child_list); - break; - } + @call_user_func($this->ipc_handlers[$action], $line); } 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); - } - } - } - } /** @@ -1717,6 +1853,12 @@ function tcp_listen($address, $port) { + // do we have socket functions? + if ( !function_exists('socket_create') ) + { + burnout('System does not support socket functions. Please rebuild your PHP or install an appropriate extension.'); + } + $this->sock = @socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); if ( !$this->sock ) throw new Exception('Could not create socket'); @@ -1823,6 +1965,12 @@ function tcp_listen($address, $port) { + // does PHP support this? + if ( !function_exists('stream_socket_server') ) + { + burnout('System does not support stream functions. Please rebuild your PHP or install an appropriate extension.'); + } + $this->sock = @stream_socket_server("tcp://$address:$port", $errno, $errstr); if ( !$this->sock ) throw new Exception("Could not create the socket: error $errno: $errstr"); @@ -1837,13 +1985,16 @@ { @stream_socket_shutdown($this->sock, STREAM_SHUT_RDWR); } - fclose($this->sock); + while ( !@fclose($this->sock) ) + { + usleep(100000); + } } } function accept() { - // the goal of a custom accept() with *_select() is to tick every 5 seconds to allow signals. + // the goal of a custom accept() with *_select() is to tick every 200ms 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);