Replaced multithreading in WebServer with a full multithreading library that properly handles IPC and child management
authorDan
Tue, 23 Sep 2008 23:24:13 -0400
changeset 48 d643bfb862d8
parent 47 b7f1952cef8d
child 49 d62212462f9b
Replaced multithreading in WebServer with a full multithreading library that properly handles IPC and child management
greyhound.php
multithreading.php
webserver.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);
-    }
-  }
 }
+
--- /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 @@
+<?php
+
+/**
+ * Multi-threading (well sort of) tools
+ * 
+ * Greyhound - real web management for Amarok
+ * Copyright (C) 2008 Dan Fuhry
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require_once('json.php');
+
+/**
+ * Global signal handler for SIGCHLD.
+ */
+
+function Threader_SigChld()
+{
+  global $threader_instances;
+  foreach ( $threader_instances as &$mt )
+  {
+    if ( is_object($mt) )
+    {
+      $mt->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 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
+ */
+
+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: --<E)
+    $fork_result = pcntl_fork();
+    if ( $fork_result == -1 )
+    {
+      // fork failed.
+      return false;
+    }
+    else if ( $fork_result )
+    {
+      // we are the parent - register the child
+      fclose($socket_pair[0]);
+      $this->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);
+      }
+    }
+  }
+  
+}
+
+?>
--- 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);