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