--- 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);