| <?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;␊ |
| }␊ |
| }␊ |
| ␊ |