* src/IDF/Middlewre.php: add a global template variable

"usherConfigured" to denote whether we render links to the usher
control functions in the forge administration
* src/IDF/Scm/Monotone.php: moved IDF_Scm_Monotone_Stdio into
separate file src/IDF/Scm/Monotone/Stdio.php
* src/IDF/Scm/Usher.php: new class to query and modify the state
of a running usher instance
* src/IDF/Views/Admin.php: add actions to query the list of
configured servers, edit their status, view their open connections
and control the state of the usher as a whole
* src/IDF/conf/idf.php-dist: optional usher configuration added;
mail address of monotone-users added; spelling changes
* src/IDF/conf/urls.php: added needed URLs for usher actions
* src/IDF/templates/idf/gadmin/base.html: usher links
This commit is contained in:
Thomas Keller
2010-08-07 23:28:13 +02:00
parent 780267978d
commit de036920a4
12 changed files with 951 additions and 372 deletions

View File

@@ -3,7 +3,7 @@
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,363 +21,7 @@
#
# ***** END LICENSE BLOCK ***** */
/**
* Monotone stdio class
*
* Connects to a monotone process and executes commands via its
* stdio interface
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_Stdio
{
/** this is the most recent STDIO version. The number is output
at the protocol start. Older versions of monotone (prior 0.47)
do not output it and are therefor incompatible */
public static $SUPPORTED_STDIO_VERSION = 2;
private $project;
private $proc;
private $pipes;
private $oob;
private $cmdnum;
private $lastcmd;
/**
* Constructor - starts the stdio process
*
* @param IDF_Project
*/
public function __construct(IDF_Project $project)
{
$this->project = $project;
$this->start();
}
/**
* Destructor - stops the stdio process
*/
public function __destruct()
{
$this->stop();
}
/**
* Starts the stdio process and resets the command counter
*/
public function start()
{
if (is_resource($this->proc))
$this->stop();
$remote_db_access = Pluf::f('mtn_db_access', 'remote') == "remote";
$cmd = Pluf::f('idf_exec_cmd_prefix', '') .
Pluf::f('mtn_path', 'mtn') . ' ';
$opts = Pluf::f('mtn_opts', array());
foreach ($opts as $opt)
{
$cmd .= sprintf('%s ', escapeshellarg($opt));
}
// FIXME: we might want to add an option for anonymous / no key
// access, but upstream bug #30237 prevents that for now
if ($remote_db_access)
{
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
}
else
{
$repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
if (!file_exists($repo))
{
throw new IDF_Scm_Exception(
"repository file '$repo' does not exist"
);
}
$cmd .= sprintf('--db %s automate stdio', escapeshellarg($repo));
}
$descriptors = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
);
$env = array("LANG" => "en_US.UTF-8");
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
null, $env);
if (!is_resource($this->proc))
{
throw new IDF_Scm_Exception("could not start stdio process");
}
$this->_checkVersion();
$this->cmdnum = -1;
}
/**
* Stops the stdio process and closes all pipes
*/
public function stop()
{
if (!is_resource($this->proc))
return;
fclose($this->pipes[0]);
fclose($this->pipes[1]);
fclose($this->pipes[2]);
proc_close($this->proc);
$this->proc = null;
}
/**
* select()'s on stdout and returns true as soon as we got new
* data to read, false if the select() timed out
*
* @return boolean
* @throws IDF_Scm_Exception
*/
private function _waitForReadyRead()
{
if (!is_resource($this->pipes[1]))
return false;
$read = array($this->pipes[1], $this->pipes[2]);
$streamsChanged = stream_select(
$read, $write = null, $except = null, 0, 20000
);
if ($streamsChanged === false)
{
throw new IDF_Scm_Exception(
"Could not select() on read pipe"
);
}
if ($streamsChanged == 0)
{
return false;
}
return true;
}
/**
* Checks the version of the used stdio protocol
*
* @throws IDF_Scm_Exception
*/
private function _checkVersion()
{
$this->_waitForReadyRead();
$version = fgets($this->pipes[1]);
if ($version === false)
{
throw new IDF_Scm_Exception(
"Could not determine stdio version, stderr is:\n".
$this->_readStderr()
);
}
if (!preg_match('/^format-version: (\d+)$/', $version, $m) ||
$m[1] != self::$SUPPORTED_STDIO_VERSION)
{
throw new IDF_Scm_Exception(
"stdio format version mismatch, expected '".
self::$SUPPORTED_STDIO_VERSION."', got '".@$m[1]."'"
);
}
fgets($this->pipes[1]);
}
/**
* Writes a command to stdio
*
* @param array
* @param array
* @throws IDF_Scm_Exception
*/
private function _write(array $args, array $options = array())
{
$cmd = "";
if (count($options) > 0)
{
$cmd = "o";
foreach ($options as $k => $vals)
{
if (!is_array($vals))
$vals = array($vals);
foreach ($vals as $v)
{
$cmd .= strlen((string)$k) . ":" . (string)$k;
$cmd .= strlen((string)$v) . ":" . (string)$v;
}
}
$cmd .= "e ";
}
$cmd .= "l";
foreach ($args as $arg)
{
$cmd .= strlen((string)$arg) . ":" . (string)$arg;
}
$cmd .= "e\n";
if (!fwrite($this->pipes[0], $cmd))
{
throw new IDF_Scm_Exception("could not write '$cmd' to process");
}
$this->lastcmd = $cmd;
$this->cmdnum++;
}
/**
* Reads all output from stderr and returns it
*
* @return string
*/
private function _readStderr()
{
$err = "";
while (($line = fgets($this->pipes[2])) !== false)
{
$err .= $line;
}
return empty($err) ? "<empty>" : $err;
}
/**
* Reads the last output from the stdio process, parses and returns it
*
* @return string
* @throws IDF_Scm_Exception
*/
private function _readStdout()
{
$this->oob = array('w' => array(),
'p' => array(),
't' => array(),
'e' => array());
$output = "";
$errcode = 0;
while (true)
{
if (!$this->_waitForReadyRead())
continue;
$data = array(0,"",0);
$idx = 0;
while (true)
{
$c = fgetc($this->pipes[1]);
if ($c === false)
{
throw new IDF_Scm_Exception(
"No data on stdin, stderr is:\n".
$this->_readStderr()
);
}
if ($c == ':')
{
if ($idx == 2)
break;
++$idx;
continue;
}
if (is_numeric($c))
$data[$idx] = $data[$idx] * 10 + $c;
else
$data[$idx] .= $c;
}
// sanity
if ($this->cmdnum != $data[0])
{
throw new IDF_Scm_Exception(
"command numbers out of sync; ".
"expected {$this->cmdnum}, got {$data[0]}"
);
}
$toRead = $data[2];
$buffer = "";
while ($toRead > 0)
{
$buffer .= fread($this->pipes[1], $toRead);
$toRead = $data[2] - strlen($buffer);
}
switch ($data[1])
{
case 'w':
case 'p':
case 't':
case 'e':
$this->oob[$data[1]][] = $buffer;
continue;
case 'm':
$output .= $buffer;
continue;
case 'l':
$errcode = $buffer;
break 2;
}
}
if ($errcode != 0)
{
throw new IDF_Scm_Exception(
"command '{$this->lastcmd}' returned error code $errcode: ".
implode(" ", $this->oob['e'])
);
}
return $output;
}
/**
* Executes a command over stdio and returns its result
*
* @param array Array of arguments
* @param array Array of options as key-value pairs. Multiple options
* can be defined in sub-arrays, like
* "r" => array("123...", "456...")
* @return string
*/
public function exec(array $args, array $options = array())
{
$this->_write($args, $options);
return $this->_readStdout();
}
/**
* Returns the last out-of-band output for a previously executed
* command as associative array with 'e' (error), 'w' (warning),
* 'p' (progress) and 't' (ticker, unparsed) as keys
*
* @return array
*/
public function getLastOutOfBandOutput()
{
return $this->oob;
}
}
require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
/**
* Monotone scm class

View File

@@ -0,0 +1,381 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it 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.
#
# InDefero 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 more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Monotone stdio class
*
* Connects to a monotone process and executes commands via its
* stdio interface
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_Stdio
{
/** this is the most recent STDIO version. The number is output
at the protocol start. Older versions of monotone (prior 0.47)
do not output it and are therefor incompatible */
public static $SUPPORTED_STDIO_VERSION = 2;
private $project;
private $proc;
private $pipes;
private $oob;
private $cmdnum;
private $lastcmd;
/**
* Constructor - starts the stdio process
*
* @param IDF_Project
*/
public function __construct(IDF_Project $project)
{
$this->project = $project;
$this->start();
}
/**
* Destructor - stops the stdio process
*/
public function __destruct()
{
$this->stop();
}
/**
* Starts the stdio process and resets the command counter
*/
public function start()
{
if (is_resource($this->proc))
$this->stop();
$remote_db_access = Pluf::f('mtn_db_access', 'remote') == "remote";
$cmd = Pluf::f('idf_exec_cmd_prefix', '') .
Pluf::f('mtn_path', 'mtn') . ' ';
$opts = Pluf::f('mtn_opts', array());
foreach ($opts as $opt)
{
$cmd .= sprintf('%s ', escapeshellarg($opt));
}
// FIXME: we might want to add an option for anonymous / no key
// access, but upstream bug #30237 prevents that for now
if ($remote_db_access)
{
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
}
else
{
$repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
if (!file_exists($repo))
{
throw new IDF_Scm_Exception(
"repository file '$repo' does not exist"
);
}
$cmd .= sprintf('--db %s automate stdio', escapeshellarg($repo));
}
$descriptors = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
);
$env = array("LANG" => "en_US.UTF-8");
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
null, $env);
if (!is_resource($this->proc))
{
throw new IDF_Scm_Exception("could not start stdio process");
}
$this->_checkVersion();
$this->cmdnum = -1;
}
/**
* Stops the stdio process and closes all pipes
*/
public function stop()
{
if (!is_resource($this->proc))
return;
fclose($this->pipes[0]);
fclose($this->pipes[1]);
fclose($this->pipes[2]);
proc_close($this->proc);
$this->proc = null;
}
/**
* select()'s on stdout and returns true as soon as we got new
* data to read, false if the select() timed out
*
* @return boolean
* @throws IDF_Scm_Exception
*/
private function _waitForReadyRead()
{
if (!is_resource($this->pipes[1]))
return false;
$read = array($this->pipes[1], $this->pipes[2]);
$streamsChanged = stream_select(
$read, $write = null, $except = null, 0, 20000
);
if ($streamsChanged === false)
{
throw new IDF_Scm_Exception(
"Could not select() on read pipe"
);
}
if ($streamsChanged == 0)
{
return false;
}
return true;
}
/**
* Checks the version of the used stdio protocol
*
* @throws IDF_Scm_Exception
*/
private function _checkVersion()
{
$this->_waitForReadyRead();
$version = fgets($this->pipes[1]);
if ($version === false)
{
throw new IDF_Scm_Exception(
"Could not determine stdio version, stderr is:\n".
$this->_readStderr()
);
}
if (!preg_match('/^format-version: (\d+)$/', $version, $m) ||
$m[1] != self::$SUPPORTED_STDIO_VERSION)
{
throw new IDF_Scm_Exception(
"stdio format version mismatch, expected '".
self::$SUPPORTED_STDIO_VERSION."', got '".@$m[1]."'"
);
}
fgets($this->pipes[1]);
}
/**
* Writes a command to stdio
*
* @param array
* @param array
* @throws IDF_Scm_Exception
*/
private function _write(array $args, array $options = array())
{
$cmd = "";
if (count($options) > 0)
{
$cmd = "o";
foreach ($options as $k => $vals)
{
if (!is_array($vals))
$vals = array($vals);
foreach ($vals as $v)
{
$cmd .= strlen((string)$k) . ":" . (string)$k;
$cmd .= strlen((string)$v) . ":" . (string)$v;
}
}
$cmd .= "e ";
}
$cmd .= "l";
foreach ($args as $arg)
{
$cmd .= strlen((string)$arg) . ":" . (string)$arg;
}
$cmd .= "e\n";
if (!fwrite($this->pipes[0], $cmd))
{
throw new IDF_Scm_Exception("could not write '$cmd' to process");
}
$this->lastcmd = $cmd;
$this->cmdnum++;
}
/**
* Reads all output from stderr and returns it
*
* @return string
*/
private function _readStderr()
{
$err = "";
while (($line = fgets($this->pipes[2])) !== false)
{
$err .= $line;
}
return empty($err) ? "<empty>" : $err;
}
/**
* Reads the last output from the stdio process, parses and returns it
*
* @return string
* @throws IDF_Scm_Exception
*/
private function _readStdout()
{
$this->oob = array('w' => array(),
'p' => array(),
't' => array(),
'e' => array());
$output = "";
$errcode = 0;
while (true)
{
if (!$this->_waitForReadyRead())
continue;
$data = array(0,"",0);
$idx = 0;
while (true)
{
$c = fgetc($this->pipes[1]);
if ($c === false)
{
throw new IDF_Scm_Exception(
"No data on stdin, stderr is:\n".
$this->_readStderr()
);
}
if ($c == ':')
{
if ($idx == 2)
break;
++$idx;
continue;
}
if (is_numeric($c))
$data[$idx] = $data[$idx] * 10 + $c;
else
$data[$idx] .= $c;
}
// sanity
if ($this->cmdnum != $data[0])
{
throw new IDF_Scm_Exception(
"command numbers out of sync; ".
"expected {$this->cmdnum}, got {$data[0]}"
);
}
$toRead = $data[2];
$buffer = "";
while ($toRead > 0)
{
$buffer .= fread($this->pipes[1], $toRead);
$toRead = $data[2] - strlen($buffer);
}
switch ($data[1])
{
case 'w':
case 'p':
case 't':
case 'e':
$this->oob[$data[1]][] = $buffer;
continue;
case 'm':
$output .= $buffer;
continue;
case 'l':
$errcode = $buffer;
break 2;
}
}
if ($errcode != 0)
{
throw new IDF_Scm_Exception(
"command '{$this->lastcmd}' returned error code $errcode: ".
implode(" ", $this->oob['e'])
);
}
return $output;
}
/**
* Executes a command over stdio and returns its result
*
* @param array Array of arguments
* @param array Array of options as key-value pairs. Multiple options
* can be defined in sub-arrays, like
* "r" => array("123...", "456...")
* @return string
*/
public function exec(array $args, array $options = array())
{
$this->_write($args, $options);
return $this->_readStdout();
}
/**
* Returns the last out-of-band output for a previously executed
* command as associative array with 'e' (error), 'w' (warning),
* 'p' (progress) and 't' (ticker, unparsed) as keys
*
* @return array
*/
public function getLastOutOfBandOutput()
{
return $this->oob;
}
}

View File

@@ -0,0 +1,249 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it 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.
#
# InDefero 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 more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Connects with the admininistrative interface of usher,
* the monotone proxy. This class contains only static methods because
* there is really no state to keep between each invocation, as usher
* closes the connection after every command.
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_Usher
{
/**
* Without giving a specific state, returns an array of all servers.
* When a state is given, the array contains only servers which are
* in the given state.
*
* @param string $state One of REMOTE, ACTIVE, WAITING, SLEEPING,
* STOPPING, STOPPED, SHUTTINGDOWN or SHUTDOWN
* @return array
*/
public static function getServerList($state = null)
{
$conn = self::_triggerCommand("LIST $state");
if ($conn == "none")
return array();
return preg_split("/[ ]/", $conn);
}
/**
* Returns an array of all open connections to the given server, or to
* any server if no server is specified.
* If there are no connections to list, an empty array is returned.
*
* Example:
* array("server1" => array(
* array("address" => "192.168.1.0", "port" => "13456"),
* ...
* ),
* "server2" => ...
* )
*
* @param string $server
* @return array
*/
public static function getConnectionList($server = null)
{
$conn = self::_triggerCommand("LISTCONNECTIONS $server");
if ($conn == "none")
return array();
$single_conns = preg_split("/[ ]/", $conn);
$ret = array();
foreach ($single_conns as $conn)
{
preg_match("/\(\w+\)([^:]):(\d+)/", $conn, $matches);
$ret[$matches[1]][] = (object)array(
"server" => $matches[1],
"address" => $matches[2],
"port" => $matches[3],
);
}
return $ret;
}
/**
* Get the status of a particular server, or of the usher as a whole if
* no server is specified.
*
* @param string $server
* @return One of REMOTE, SLEEPING, STOPPING, STOPPED for servers or
* ACTIVE, WAITING, SHUTTINGDOWN or SHUTDOWN for usher itself
*/
public static function getStatus($server = null)
{
return self::_triggerCommand("STATUS $server");
}
/**
* Looks up the name of the server that would be used for an incoming
* connection having the given host and pattern.
*
* @param string $host Host
* @param string $pattern Branch pattern
* @return server name
* @throws IDF_Scm_Exception
*/
public static function matchServer($host, $pattern)
{
$ret = self::_triggerCommand("MATCH $host $pattern");
if (preg_match("/^OK: (.+)/", $ret, $m))
return $m[1];
preg_match("/^ERROR: (.+)/", $ret, $m);
throw new IDF_Scm_Exception("could not match server: ".$m[1]);
}
/**
* Prevent the given local server from receiving further connections,
* and stop it once all connections are closed. The return value will
* be the new status of that server: ACTIVE local servers will become
* STOPPING, and WAITING and SLEEPING serveres become STOPPED.
* Servers in other states are not affected.
*
* @param string $server
* @return string State of the server after the command
*/
public static function stopServer($server)
{
return self::_triggerCommand("STOP $server");
}
/**
* Allow a STOPPED or STOPPING server to receive connections again.
* The return value is the new status of that server: STOPPING servers
* become ACTIVE, and STOPPED servers become SLEEPING. Servers in other
* states are not affected.
*
* @param string $server
* @return string State of the server after the command
*/
public static function startServer($server)
{
return self::_triggerCommand("START $server");
}
/**
* Immediately kill the given local server, dropping any open connections,
* and prevent is from receiving new connections and restarting. The named
* server will immediately change to state STOPPED.
*
* @param string $server
* @return bool True if successful
*/
public static function killServer($server)
{
return self::_triggerCommand("KILL_NOW $server") == "ok";
}
/**
* Do not accept new connections for any servers, local or remote.
*
* @return bool True if successful
*/
public static function shutDown()
{
return self::_triggerCommand("SHUTDOWN") == "ok";
}
/**
* Begin accepting connections after a SHUTDOWN.
*
* @return bool True if successful
*/
public static function startUp()
{
return self::_triggerCommand("STARTUP") == "ok";
}
/**
* Reload the config file, the same as sending SIGHUP.
*
* @return bool True if successful (after the configuration was reloaded)
*/
public static function reload()
{
return self::_triggerCommand("RELOAD") == "ok";
}
private static function _triggerCommand($cmd)
{
$uc = Pluf::f('mtn_usher');
if (empty($uc['host']))
{
throw new IDF_Scm_Exception("usher host is empty");
}
if (!preg_match('/^\d+$/', $uc['port']) ||
$uc['port'] == 0)
{
throw new IDF_Scm_Exception("usher port is invalid");
}
if (empty($uc['user']))
{
throw new IDF_Scm_Exception("usher user is empty");
}
if (empty($uc['pass']))
{
throw new IDF_Scm_Exception("usher pass is empty");
}
$sock = @fsockopen($uc['host'], $uc['port'], $errno, $errstr);
if (!$sock)
{
throw new IDF_Scm_Exception(
"could not connect to usher: $errstr ($errno)"
);
}
fwrite($sock, "USERPASS {$uc['user']} {$uc['pass']}\n");
if (feof($sock))
{
throw new IDF_Scm_Exception(
"usher closed the connection - probably wrong admin ".
"username or password"
);
}
fwrite($sock, "$cmd\n");
$out = "";
while (!feof($sock))
{
$out .= fgets($sock);
}
fclose($sock);
$out = rtrim($out);
if ($out == "unknown command")
{
throw new IDF_Scm_Exception("unknown command: $cmd");
}
return $out;
}
}