From de036920a4d1e2ab5a3ed3343e2d0f316b3c69ad Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Sat, 7 Aug 2010 23:28:13 +0200 Subject: [PATCH] * 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 --- src/IDF/Middleware.php | 1 + src/IDF/Scm/Monotone.php | 360 +---------------- src/IDF/Scm/Monotone/Stdio.php | 381 ++++++++++++++++++ src/IDF/Scm/Monotone/Usher.php | 249 ++++++++++++ src/IDF/Views/Admin.php | 167 +++++++- src/IDF/conf/idf.php-dist | 20 +- src/IDF/conf/urls.php | 23 ++ src/IDF/templates/idf/gadmin/base.html | 13 +- src/IDF/templates/idf/gadmin/usher/base.html | 6 + .../idf/gadmin/usher/connections.html | 19 + .../templates/idf/gadmin/usher/control.html | 32 ++ src/IDF/templates/idf/gadmin/usher/index.html | 52 +++ 12 files changed, 951 insertions(+), 372 deletions(-) create mode 100644 src/IDF/Scm/Monotone/Stdio.php create mode 100644 src/IDF/Scm/Monotone/Usher.php create mode 100644 src/IDF/templates/idf/gadmin/usher/base.html create mode 100644 src/IDF/templates/idf/gadmin/usher/connections.html create mode 100644 src/IDF/templates/idf/gadmin/usher/control.html create mode 100644 src/IDF/templates/idf/gadmin/usher/index.html diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php index e0436b6..cee2270 100644 --- a/src/IDF/Middleware.php +++ b/src/IDF/Middleware.php @@ -111,6 +111,7 @@ function IDF_Middleware_ContextPreProcessor($request) $request->project); $c = array_merge($c, $request->rights); } + $c['usherConfigured'] = Pluf::f("mtn_usher", null) !== null; return $c; } diff --git a/src/IDF/Scm/Monotone.php b/src/IDF/Scm/Monotone.php index de061b4..b58a277 100644 --- a/src/IDF/Scm/Monotone.php +++ b/src/IDF/Scm/Monotone.php @@ -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 - */ -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) ? "" : $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 diff --git a/src/IDF/Scm/Monotone/Stdio.php b/src/IDF/Scm/Monotone/Stdio.php new file mode 100644 index 0000000..c1628f7 --- /dev/null +++ b/src/IDF/Scm/Monotone/Stdio.php @@ -0,0 +1,381 @@ + + */ +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) ? "" : $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; + } +} + diff --git a/src/IDF/Scm/Monotone/Usher.php b/src/IDF/Scm/Monotone/Usher.php new file mode 100644 index 0000000..7c86e0a --- /dev/null +++ b/src/IDF/Scm/Monotone/Usher.php @@ -0,0 +1,249 @@ + + */ +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; + } +} + diff --git a/src/IDF/Views/Admin.php b/src/IDF/Views/Admin.php index f7525a8..b69f851 100644 --- a/src/IDF/Views/Admin.php +++ b/src/IDF/Views/Admin.php @@ -66,7 +66,7 @@ class IDF_Views_Admin 'name' => __('Name'), array('id', 'IDF_Views_Admin_projectSize', __('Repository Size')), ); - $pag->configure($list_display, array(), + $pag->configure($list_display, array(), array('shortname')); $pag->extra_classes = array('', '', 'right'); $pag->items_per_page = 25; @@ -211,8 +211,8 @@ class IDF_Views_Admin array('last_login', 'Pluf_Paginator_DateYMDHM', __('Last Login')), ); $pag->extra_classes = array('', '', 'a-c', 'a-c', 'a-c', 'a-c'); - $pag->configure($list_display, - array('login', 'last_name', 'email'), + $pag->configure($list_display, + array('login', 'last_name', 'email'), array('login', 'last_login')); $pag->items_per_page = 50; $pag->no_results_text = __('No users were found.'); @@ -225,7 +225,7 @@ class IDF_Views_Admin ), $request); } - + /** * Not validated users. */ @@ -314,6 +314,159 @@ class IDF_Views_Admin ), $request); } + + /** + * Usher servers overview + * + */ + public $usher_precond = array('Pluf_Precondition::staffRequired'); + public function usher($request, $match) + { + $title = __('Usher management'); + $servers = array(); + foreach (IDF_Scm_Monotone_Usher::getServerList() as $server) + { + $servers[] = (object)array( + "name" => $server, + "status" => IDF_Scm_Monotone_Usher::getStatus($server), + ); + } + + return Pluf_Shortcuts_RenderToResponse( + 'idf/gadmin/usher/index.html', + array( + 'page_title' => $title, + 'servers' => $servers, + ), + $request + ); + } + + /** + * Usher control + * + */ + public $usherControl_precond = array('Pluf_Precondition::staffRequired'); + public function usherControl($request, $match) + { + $title = __('Usher control'); + $action = $match[1]; + + if (!empty($action)) + { + if (!in_array($action, array("reload", "shutdown", "startup"))) + { + throw new Pluf_HTTP_Error404(); + } + + $msg = null; + if ($action == "reload") + { + IDF_Scm_Monotone_Usher::reload(); + $msg = __('Usher configuration has been reloaded'); + } + else if ($action == "shutdown") + { + IDF_Scm_Monotone_Usher::shutDown(); + $msg = __('Usher has been shut down'); + } + else + { + IDF_Scm_Monotone_Usher::startUp(); + $msg = __('Usher has been started up'); + } + + $request->user->setMessage($msg); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usherControl', array('')); + return new Pluf_HTTP_Response_Redirect($url); + } + + return Pluf_Shortcuts_RenderToResponse( + 'idf/gadmin/usher/control.html', + array( + 'page_title' => $title, + 'status' => IDF_Scm_Monotone_Usher::getStatus(), + ), + $request + ); + } + + /** + * Usher control + * + */ + public $usherServerControl_precond = array('Pluf_Precondition::staffRequired'); + public function usherServerControl($request, $match) + { + $server = $match[1]; + if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) + { + throw new Pluf_HTTP_Error404(); + } + + $action = $match[2]; + if (!in_array($action, array("start", "stop", "kill"))) + { + throw new Pluf_HTTP_Error404(); + } + + $msg = null; + if ($action == "start") + { + IDF_Scm_Monotone_Usher::startServer($server); + $msg = sprintf(__('The server "%s" has been started'), $server); + } + else if ($action == "stop") + { + IDF_Scm_Monotone_Usher::stopServer($server); + $msg = sprintf(__('The server "%s" has been stopped'), $server); + } + else + { + IDF_Scm_Monotone_Usher::killServer($server); + $msg = sprintf(__('The server "%s" has been killed'), $server); + } + + $request->user->setMessage($msg); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher'); + return new Pluf_HTTP_Response_Redirect($url); + } + + /** + * Open connections for a configured server + * + */ + public $usherServerConnections_precond = array('Pluf_Precondition::staffRequired'); + public function usherServerConnections($request, $match) + { + $server = $match[1]; + if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) + { + throw new Pluf_HTTP_Error404(); + } + + $title = sprintf(__('Open connections for "%s"'), $server); + + $connections = IDF_Scm_Monotone_Usher::getConnectionList($server); + if (count($connections) == 0) + { + $request->user->setMessage(sprintf( + __('no connections for server "%s"'), $server + )); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher'); + return new Pluf_HTTP_Response_Redirect($url); + } + + return Pluf_Shortcuts_RenderToResponse( + 'idf/gadmin/usher/connections.html', + array( + 'page_title' => $title, + 'server' => $server, + 'connections' => $connections, + ), + $request + ); + } } function IDF_Views_Admin_bool($field, $item) @@ -326,7 +479,7 @@ function IDF_Views_Admin_bool($field, $item) /** * Display the size of the project. * - * @param string Field + * @param string Field * @param IDF_Project * @return string */ @@ -406,8 +559,8 @@ function IDF_Views_Admin_getForgeDbSize() } switch (Pluf::f('db_engine')) { case 'PostgreSQL': - $sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS -TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn + $sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS +TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn WHERE pg_table_is_visible(pgc.oid) IS TRUE AND relkind = \'r\' AND pgc.relnamespace = pgn.oid AND pgn.nspname NOT IN (\'information_schema\', \'pg_catalog\')'; diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index d759f94..390ce36 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -103,7 +103,7 @@ $cfg['mtn_opts'] = array('--no-workspace', '--norc'); # Then download and configure 'usher' # (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher) # which acts as proxy in front of all single project databases. -# Usher's server name should be mapped to the project's short name, +# Usher's server names should be mapped to the project's short names, # so you end up with something like this for every project: # # server "project" @@ -137,9 +137,25 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; # Note that you need to setup the hook 'get_remote_automate_permitted' for # each remotely accessible database. A full HOWTO set this up is beyond this # scope, please refer to the documentation of monotone and / or ask on the -# mailing list / IRC channel (irc.oftc.net/#monotone) +# mailing list (monotone-users@nongnu.org) or IRC channel +# (irc.oftc.net/#monotone) # $cfg['mtn_db_access'] = 'remote'; +# +# If configured, this allows basic control of a running usher process +# via the forge administration +# +# 'host' and 'port' must be set to the specific bits from usher's +# configured 'adminaddr', 'user' and 'pass' must match the values set for +# the configured 'userpass' combination +# +#$cfg['mtn_usher'] = array( +# 'host' => 'localhost', +# 'port' => 12345, +# 'user' => 'admin', +# 'pass' => 'admin', +#); +# # Mercurial repositories path #$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index f6fef8b..1d9b56f 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -386,6 +386,29 @@ $ctl[] = array('regex' => '#^/admin/users/(\d+)/$#', 'model' => 'IDF_Views_Admin', 'method' => 'userUpdate'); +if (Pluf::f("mtn_usher", null) !== null) +{ + $ctl[] = array('regex' => '#^/admin/usher/$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'usher'); + + $ctl[] = array('regex' => '#^/admin/usher/control/(.*)$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'usherControl'); + + $ctl[] = array('regex' => '#^/admin/usher/server/(.+)/control/(.+)$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'usherServerControl'); + + $ctl[] = array('regex' => '#^/admin/usher/server/(.+)/connections/$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'usherServerConnections'); +} + // ---------- UTILITY VIEWS ------------------------------- $ctl[] = array('regex' => '#^/register/$#', diff --git a/src/IDF/templates/idf/gadmin/base.html b/src/IDF/templates/idf/gadmin/base.html index 827b9ef..1858682 100644 --- a/src/IDF/templates/idf/gadmin/base.html +++ b/src/IDF/templates/idf/gadmin/base.html @@ -43,18 +43,21 @@
{trans 'Projects'} {trans 'People'} + {if $usherConfigured} + {trans 'Usher'} + {/if}
{block subtabs}{/block}
-

{block title}{$page_title}{/block}

+

{block title}{$page_title}{/block}

-
+
-
+
{if $user and $user.id}{getmsgs $user}{/if} -
{block body}{/block}
-
+
{block body}{/block}
+
{block context}{/block}
diff --git a/src/IDF/templates/idf/gadmin/usher/base.html b/src/IDF/templates/idf/gadmin/usher/base.html new file mode 100644 index 0000000..e2849e7 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/base.html @@ -0,0 +1,6 @@ +{extends "idf/gadmin/base.html"} +{block tabprojects} class="active"{/block} +{block subtabs} +{trans 'Configured servers'} | +{trans 'Usher control'} +{/block} diff --git a/src/IDF/templates/idf/gadmin/usher/connections.html b/src/IDF/templates/idf/gadmin/usher/connections.html new file mode 100644 index 0000000..bde9e2f --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/connections.html @@ -0,0 +1,19 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsherServerConnections=true}{/block} + +{block body} + + + + + +{foreach $connections as $connection} + + + + +{/foreach} +
{trans "address"}{trans "port"}
{$connection.address}{$connection.port}
+{/block} + diff --git a/src/IDF/templates/idf/gadmin/usher/control.html b/src/IDF/templates/idf/gadmin/usher/control.html new file mode 100644 index 0000000..e7d6802 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/control.html @@ -0,0 +1,32 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsherControl=true}{/block} + +{block body} +

+{trans 'current server status:'} {$status} | +{if $status == "SHUTDOWN"} + {trans 'startup'} +{else} + {trans 'shutdown'} +{/if} +

+ +

{trans 'reload server configuration:'} + {trans 'reload'} +

+{/block} + +{block context} +
+

{trans 'Status explanation'}

+
    +
  • ACTIVE n: {trans 'active with n total open connections'}
  • +
  • WAITING: {trans 'waiting for new connections'}
  • +
  • SHUTTINGDOWN: {trans 'usher is being shut down, not accepting connections'}
  • +
  • SHUTDOWN: {trans 'usher is shut down, all local servers are stopped and not accepting connections'}
  • +
+ +
+{/block} + diff --git a/src/IDF/templates/idf/gadmin/usher/index.html b/src/IDF/templates/idf/gadmin/usher/index.html new file mode 100644 index 0000000..c81c134 --- /dev/null +++ b/src/IDF/templates/idf/gadmin/usher/index.html @@ -0,0 +1,52 @@ +{extends "idf/gadmin/usher/base.html"} + +{block docclass}yui-t3{assign $inUsher=true}{/block} + +{block body} + + + + + + +{foreach $servers as $server} + + + + +{/foreach} +
{trans "server name"}{trans "status"}{trans "action"}
{$server.name}{$server.status} + {if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)} + + {trans 'stop'} + {elseif $server.status == "STOPPED"} + + {trans 'start'} + {/if} + {if preg_match("/ACTIVE|WAITING|SLEEPING|STOPPING/", $server.status)} + | + {trans 'kill'} + {/if} + {if preg_match("/STOPPING|ACTIVE/", $server.status)} + | + {trans 'active connections'} + {/if} +
+{/block} + +{block context} +
+

{trans 'Status explanation'}

+
    +
  • REMOTE: {trans 'remote server without open connections'}
  • +
  • ACTIVE n: {trans 'server with n open connections'}
  • +
  • WAITING: {trans 'local server running, without open connections'}
  • +
  • SLEEPING: {trans 'local server not running, waiting for connections'}
  • +
  • STOPPING n: {trans 'local server is about to stop, n connections still open'}
  • +
  • STOPPED: {trans 'local server not running, not accepting connections'}
  • +
  • SHUTDOWN: {trans 'usher is shut down, not running and not accepting connections'}
  • +
+ +
+{/block} +