srchub-old

srchub-old Mercurial Source Tree


Root/indefero/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', '') .
               escapeshellarg(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;
    }
}

Source at commit 23012f799d08 created 10 years 19 days ago.
By Nathan Adams, Adding syntax highlighter to base.html

Archive Download this file

Branches

Tags

Page rendered in 0.85182s using 11 queries.