aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLaurent Bachelier <laurent@bachelier.name>2011-02-01 01:05:26 +0100
committerLaurent Bachelier <laurent@bachelier.name>2011-02-01 01:05:26 +0100
commite133c2c6b8295dccc006da60dcd19f11b39ac1d5 (patch)
tree94d560b4e0c7c1cc1edbdfe5122448ae1438e945
parentRationalize output messages (diff)
parentDocumentation updates (diff)
downloadsymfttpd-e133c2c6b8295dccc006da60dcd19f11b39ac1d5.tar.xz
Merge branch 'fork'
-rw-r--r--README.md17
-rw-r--r--lib/Color.php128
-rw-r--r--lib/PosixTools.php25
-rw-r--r--lib/Tail.php114
-rw-r--r--lib/Template.php18
-rw-r--r--lib/bootstrap.php1
-rwxr-xr-xspawn113
7 files changed, 392 insertions, 24 deletions
diff --git a/README.md b/README.md
index 2ce4ccf..8a13d1c 100644
--- a/README.md
+++ b/README.md
@@ -71,12 +71,19 @@ by using the symfttpd.conf.php mechanism.
* `--all` or `-A`: Listen on all interfaces (overrides `--bind`)
* `--bind=<port>` or `-b<ip>`: Listen on a specific IP (default is `127.0.0.1`)
* `--path=<path>`: Use a different project path (default is current dir)
+* `--tail` or `t`: Display server logs in the console
+ (like the UNIX `tail` command would do)
+* `--no-color` or `C`: Disable colored output
+ (also automatically disabled if the output is piped)
+* `--single-process` or `-s`: Do not try to run lighttpd in another process
+ (not recommended, you will lose auto-reloading of the rewriting rules)
## mksymlinks
-If you don't want to spend time with repetitive symlink creation each time you set up a new project, then this tool is for you.
+If you don't want to spend time with repetitive symlink creation each time
+you set up a new project, then this tool is for you.
### Quick start
@@ -167,9 +174,11 @@ or if you want a different default application:
include_shell "/path/to/example.com/config/lighttpd.php --default=mobile"
}
-You have to restart lighttpd each time you add a file the the web/
-root. Hopefully, it doesn't happen often. Also, don't forget to run
-`php symfony plugin:publish-assets`, or even better, `mksymlinks` before.
+If symfttpd is running in single-process mode, or you only running an independent
+lighttpd, you have to restart it each time you add a file the the web/ root.
+Hopefully, it doesn't happen often.
+Also, don't forget to run `php symfony plugin:publish-assets`, or even better,
+`mksymlinks` before.
### Available options
diff --git a/lib/Color.php b/lib/Color.php
new file mode 100644
index 0000000..0c47789
--- /dev/null
+++ b/lib/Color.php
@@ -0,0 +1,128 @@
+<?php
+/*
+ * No "get" prefix on the class methods as pretty much
+ * all of them would have it.
+ */
+class Color
+{
+ protected static $enabled = false;
+
+ protected static $fgcolors = array(
+ 'black' => 30,
+ 'blue' => 34,
+ 'brown' => 33,
+ 'cyan' => 36,
+ 'green' => 32,
+ 'grey' => 37,
+ 'purple' => 35,
+ 'red' => 31,
+ 'yellow' => 33,
+ );
+
+ protected static $styles = array(
+ 'normal' => 0, // also "reset"
+ 'bright' => 1, // also "bold"
+ 'underline' => 4,
+ 'blink' => 5,
+ 'inverse' => 6, // also "reverse"
+ );
+
+ protected static $bgcolors = array(
+ 'black' => 40,
+ 'blue' => 44,
+ 'brown' => 43,
+ 'cyan' => 46,
+ 'green' => 42,
+ 'grey' => 47,
+ 'purple' => 45,
+ 'red' => 41,
+ 'yellow' => 43,
+ );
+
+ /**
+ * Get a control code for UNIX-like terminals, if color support is enabled.
+ * @param integer $number
+ * @return string
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function controlCode($number)
+ {
+
+ return self::$enabled ? "\033[".$number.'m' : '';
+ }
+
+ /**
+ * Get a control number from its type and name.
+ *
+ * @param array $list Type, e.g. Color::$fgcolor
+ * @param string $name Name, e.g. 'red'
+ * @return integer Control number, e.g. 31
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ protected static function numberFromList($list, $name)
+ {
+
+ return $list[$name];
+ }
+
+ /**
+ * Get the control code for a foreground color.
+ * @param string $color Color name, e.g. 'red'
+ * @return string Escape sequence
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function fgColor($name)
+ {
+
+ return self::controlCode(self::numberFromList(self::$fgcolors, $name));
+ }
+
+ /**
+ * Get the control code for a background color.
+ * @param string $color Color name, e.g. 'red'
+ * @return string Escape sequence
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function bgColor($name)
+ {
+
+ return self::controlCode(self::numberFromList(self::$bgcolors, $name));
+ }
+
+ /**
+ * Get the control code for a style.
+ * @param string $color Color name, e.g. 'bold'
+ * @return string Escape sequence for this color
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function style($name)
+ {
+
+ return self::controlCode(self::numberFromList(self::$styles, $name));
+ }
+
+ /**
+ * Enable color support.
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function enable()
+ {
+ self::$enabled = true;
+ }
+
+ /**
+ * Disable color support.
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public static function disable()
+ {
+ self::$enabled = false;
+ }
+}
diff --git a/lib/PosixTools.php b/lib/PosixTools.php
index 8970b46..fabd495 100644
--- a/lib/PosixTools.php
+++ b/lib/PosixTools.php
@@ -57,5 +57,30 @@ class PosixTools
self::$custom_path = $custom_path;
}
+ /**
+ * Get a process ID from a file, and kill it, and remove the file either way.
+ * @param string $pidfile Path to PID file
+ * @return boolean Success
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ static public function killPid($pidfile)
+ {
+ if (file_exists($pidfile))
+ {
+ $pid = intval(trim(file_get_contents($pidfile)));
+ unlink($pidfile);
+ if ($pid)
+ {
+ posix_kill($pid, SIGTERM);
+ log_message('Process '.$pid.' killed');
+
+ return true;
+ }
+ }
+ log_message('No running process found', true);
+
+ return false;
+ }
}
diff --git a/lib/Tail.php b/lib/Tail.php
new file mode 100644
index 0000000..00f21ae
--- /dev/null
+++ b/lib/Tail.php
@@ -0,0 +1,114 @@
+<?php
+class Tail
+{
+ protected $path = null;
+ protected $first = true;
+ protected $pos = 0;
+
+ /**
+ * Follow a file
+ * @param string $path
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Read a new line a long as it is possible.
+ * Goes straight to the end the first time if the file exists.
+ * @return mixed a string (including line return), false (EOF) or null (failure)
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public function consume()
+ {
+ $first = $this->first;
+ $this->first = false;
+ if (!is_readable($this->path))
+ {
+
+ return null;
+ }
+ $fd = fopen($this->path, 'r');
+ if ($first)
+ {
+ fseek($fd, 0, SEEK_END);
+ }
+ else
+ {
+ fseek($fd, $this->pos, SEEK_SET);
+ }
+ $line = fgets($fd);
+ $this->pos = ftell($fd);
+
+ if ($line === false)
+ {
+ /* Detect file truncation.
+ * There seem to be no better way, as fseek will accept
+ * to go over EOF and fgets will not handle it differently either.
+ */
+ $stat = fstat($fd);
+ if ($stat['size'] < $this->pos)
+ {
+ // rewind
+ $this->pos = 0;
+ }
+ }
+ fclose($fd);
+
+ return $line;
+ }
+}
+
+class MultiTail
+{
+ protected $tails = array();
+
+ /**
+ * Adds a Tail to watch
+ * @param string $name Unique name
+ * @param Tail $tail
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public function add($name, $tail)
+ {
+ $this->tails[$name] = $tail;
+ }
+
+ /**
+ * Calls consume() on every Tail and displays lines.
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public function consume()
+ {
+ foreach ($this->tails as $name => $tail)
+ {
+ while ($this->display($tail->consume(), $name));
+ }
+ }
+
+ /**
+ * Display a line from a Tail, if it is valid.
+ * @param mixed $line
+ * @param string $name Unique name of the Tail
+ * @return boolean Line validity
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ public function display($line, $name)
+ {
+ if (is_string($line))
+ {
+ echo $name.': '.$line;
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/lib/Template.php b/lib/Template.php
index 89db03e..7ec5b14 100644
--- a/lib/Template.php
+++ b/lib/Template.php
@@ -19,4 +19,22 @@ class Template
return ob_get_clean();
}
+
+ /**
+ * Write the main lighttpd config file
+ * @param array $options
+ * @return boolean|integer Failure or number of bytes written
+ *
+ * @author Laurent Bachelier <laurent@bachelier.name>
+ */
+ static public function writeConfig($options)
+ {
+ $config_file = $options['config_dir'].'/lighttpd.conf';
+
+ return file_put_contents(
+ $config_file,
+ Template::get($options['config_template'], $options)
+ );
+ }
+
}
diff --git a/lib/bootstrap.php b/lib/bootstrap.php
index 0a00738..2f5ea79 100644
--- a/lib/bootstrap.php
+++ b/lib/bootstrap.php
@@ -1,6 +1,7 @@
<?php
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', true);
+set_time_limit(0);
/**
* Display a message on the standard output
diff --git a/spawn b/spawn
index 4260b06..9bfd24e 100755
--- a/spawn
+++ b/spawn
@@ -10,6 +10,8 @@ require dirname(__FILE__).'/lib/FileTools.php';
require dirname(__FILE__).'/lib/PosixTools.php';
require dirname(__FILE__).'/lib/MultiConfig.php';
require dirname(__FILE__).'/lib/Symfony.php';
+require dirname(__FILE__).'/lib/Tail.php';
+require dirname(__FILE__).'/lib/Color.php';
$project_path = Symfony::getProjectPath();
$options = MultiConfig::get();
@@ -20,25 +22,32 @@ $options['bind'] = Argument::get('A', 'all', false)
: Argument::get('b', 'bind', '127.0.0.1');
$options['project_path'] = $project_path;
$options['config_dir'] = $project_path.'/cache/lighttpd';
+$options['config_file'] = $options['config_dir'].'/lighttpd.conf';
$options['log_dir'] = $project_path.'/log/lighttpd';
// hack: .sf files are not removed by symfony cc
$options['pidfile'] = $options['config_dir'].'/.sf';
+$options['restartfile'] = $options['config_dir'].'/.symfttpd_restart';
+$options['tail'] = Argument::get('t', 'tail', false);
+$options['fork'] = !Argument::get('s', 'single-process', false);
+if ($options['fork'] && !function_exists('pcntl_fork'))
+{
+ log_message('Warning: No fork() support.'
+ . 'symfttpd will run in single-process mode.');
+ $options['fork'] = false;
+}
+$options['color'] = !Argument::get('C', 'no-color', false) && posix_isatty(STDOUT);
+if ($options['color'])
+{
+ Color::enable();
+}
if (Argument::get('K', 'kill', false))
{
- if (file_exists($options['pidfile']))
+ if (file_exists($options['restartfile']))
{
- $pid = intval(trim(file_get_contents($options['pidfile'])));
- unlink($options['pidfile']);
- if ($pid)
- {
- posix_kill($pid, SIGTERM);
- log_message('Process '.$pid.' killed');
- exit(0);
- }
+ unlink($options['restartfile']);
}
- log_message('No running process found', true);
- exit(1);
+ exit(!PosixTools::killPid($options['pidfile']));
}
FileTools::mkdirs($options['config_dir']);
@@ -64,24 +73,20 @@ try
}
catch (ExecutableNotFoundError $e)
{
- log_message("Required executable not found.", false);
+ log_message('Required executable not found.', false);
log_message($e->getMessage()
. ' not found in the specified paths: '
. implode(', ', PosixTools::getPaths()), false);
exit(1);
}
-$config_file = $options['config_dir'].'/lighttpd.conf';
-file_put_contents(
- $config_file,
- Template::get($options['config_template'], $options)
-);
+Template::writeConfig($options);
// Pretty information. Nothing interesting code-wise.
log_message('lighttpd started on '
. (strlen($options['bind']) ? $options['bind'] : 'all interfaces')
. ', port '.$options['port'].'.');
-log_message("\nAvailable applications");
+log_message("\nAvailable applications:");
$apps = array();
foreach (new DirectoryIterator($project_path.'/web') as $file)
{
@@ -101,6 +106,74 @@ foreach ($apps as $app)
log_message("\nPress Ctrl+C to stop serving.");
flush();
-passthru($options['lighttpd_cmd'].' -D -f '.escapeshellarg($config_file));
+if (!$options['fork'])
+{
+ passthru($options['lighttpd_cmd'].' -D -f '.escapeshellarg($options['config_file']));
+ log_message('Terminated.');
+}
+else
+{
+ if ($options['tail'])
+ {
+ $multitail = new MultiTail();
+ $multitail->add(Color::fgColor('blue').'lighttpd-access'.Color::style('normal'), new Tail($options['log_dir'].'/access.log'));
+ $multitail->add(Color::fgColor('red').'lighttpd-error'.Color::style('normal'), new Tail($options['log_dir'].'/error.log'));
+ // We have to do it before the fork to capture the startup messages
+ $multitail->consume();
+ }
+ $pid = pcntl_fork();
+ if ($pid)
+ {
+ // Parent process
+ $prev_genconf = null;
+ while (false !== sleep(1))
+ {
+ $handle = popen($options['php_cmd'].' '.$project_path.'/config/lighttpd.php', 'r');
+ $genconf = stream_get_contents($handle);
+ pclose($handle);
+ if ($prev_genconf !== null && $prev_genconf !== $genconf)
+ {
+ touch($options['restartfile']);
+ !PosixTools::killPid($options['pidfile']);
+ }
+ $prev_genconf = $genconf;
+
+ if ($options['tail'])
+ {
+ $multitail->consume();
+ }
-log_message('Terminated');
+ // If the children is defunct, we are finished here
+ if (pcntl_waitpid($pid, $status, WNOHANG))
+ {
+ exit(0);
+ }
+ }
+ }
+ elseif ($pid == 0)
+ {
+ // Child process
+ do
+ {
+ if (file_exists($options['restartfile']))
+ {
+ unlink($options['restartfile']);
+ }
+ passthru($options['lighttpd_cmd'].' -D -f '.escapeshellarg($options['config_file']));
+ if (!file_exists($options['restartfile']))
+ {
+ log_message('Terminated.');
+ }
+ else
+ {
+ log_message(Color::style('bright').'Something in web/ changed. Restarting lighttpd.'.Color::style('normal'));
+ Template::writeConfig($options);
+ }
+ } while (file_exists($options['restartfile']));
+ }
+ else
+ {
+ log_message('Unable to fork!', true);
+ exit(1);
+ }
+}