Indefero

Indefero Git Source Tree


Root/src/IDF/Scm/Monotone/Stdio.php

<?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) 2008-2011 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 ***** */

require_once 'IDF/Scm/Monotone/IStdio.php';

/**
 * 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 implements IDF_Scm_Monotone_IStdio
{
    /** 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();
    }

    /**
     * Returns a string with additional options which are passed to
     * an mtn instance connecting to remote databases
     *
     * @return string
     */
    private function _getAuthOptions()
    {
        $prjconf = $this->project->getConf();
        $name = $prjconf->getVal('mtn_client_key_name', false);
        $hash = $prjconf->getVal('mtn_client_key_hash', false);

        if (!$name || !$hash) {
            throw new IDF_Scm_Exception(sprintf(
                __('Monotone client key name or hash not in project conf.')
            ));
        }

        $keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
        if (!file_exists($keydir)) {
            if (!mkdir($keydir)) {
                throw new IDF_Scm_Exception(sprintf(
                    __('The key directory %s could not be created.'), $keydir
                ));
            }
        }

        // in case somebody cleaned out the cache, we restore the key here
        $keyfile = $keydir . '/' . $name .'.'. $hash;
        if (!file_exists($keyfile)) {
            $data = $prjconf->getVal('mtn_client_key_data');
            if (!file_put_contents($keyfile, $data, LOCK_EX)) {
                throw new IDF_Scm_Exception(sprintf(
                    __('Could not write client key "%s"'), $keyfile
                ));
            }
        }

        return sprintf('--keydir=%s --key=%s ',
               escapeshellarg($keydir),
               escapeshellarg($hash)
        );
    }

    /**
     * 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));
        }

        if ($remote_db_access) {
            $cmd .= $this->_getAuthOptions();
            $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]);
        $write = $except = null;
        $streamsChanged = stream_select(
            $read, $write, $except, 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;
    }
}

Archive Download this file

Page rendered in 0.09849s using 11 queries.