Індексування Sphinx з віддаленого сервера засобами PHP

    Доброго часу доби, дорогі читачі!
 
Хочу розповісти вам про цікаву завданню, яка стала переді мною в рамках проекту і, природно, про її вирішення.
 
Вихідні дані:
Стандартний набір LAMP (далі СС),
Yii framework (версія тут не важлива),
віддалений сервер (далі УС), на якому встановлений демон Sphinx, searchd.
На УС створений користувач з правами рута (але не сам рут).
На СС встановлений модуль ssh2_mod для PHP.
 
Відразу обмовлюся, в цій статті я не буду розписувати особливості Sphinx, кому цікаво, можуть почитати офіційний мануал sphinxsearch.com / docs / current.html .
Обмежуся тільки загальною інформацією.
 
Отже, Sphinx — пошуковий демон, в моєму випадку працює з MySQL. Основна особливість — він індексує базу по певних запитах (описаним в конфіги сфінкса), і результат вибірки зберігає в свої файли. Щоб інформація була актуальною (в MySQL можливо і додавання і редагування записів), потрібно запускати індексацію сфінкса. Тоді, він зробить повторну вибірку і збереже її собі.
 
Завдання:
Запускати індексацію сфінкса на УС.
Причина саме віддаленого запуску полягає в тому, що необхідно запускати команди по крону з конкретними параметрами, обумовленими в коді. Крони запускаються з СС.
Тобто на сервері запускається крон, метод якого виконує індексацію на УС.
 
Єдине рішення, яке знайшов — використання ssh2_mod для apache2 (кому цікаво, мануал по установці на CentOS можна глянути тут www.stableit.ru/2010/12/ssh2-php-centos-55-pecl.html ).
 
Подивився мануал по ssh2 (http://www.php.net/manual/en/book.ssh2.php), знайшов чудову функцію ssh2_exec, яка на вхід приймає поточну сесію і команду, але, як виявилося, вона має ряд обмежень.
Наприклад, при спробі виконання команди indexer — all — rotate для дельта індексу я отримував помилку:
 
 
WARNING: failed to open pid_file '/var/run/sphinx/searchd.pid'.
WARNING: indices NOT rotated.

 
Ця помилка означає, що моєму користувачеві не вистачає прав для виконання rotate (а у мене юзер з правами рута, sudo-s), хоча з консолі безпосередньо я спокійно виконував цю команду без жодних помилок.
Далі я вирішив пошукати ще, і виявив, що можна емулювати введення команд через термінал (функція ssh2_shell). За допомогою стандарного потоку і фукнции fwrite можна писати команди в «термінал» та отримувати на виході такий же стандарний вихідний потік, тобто результат, що видається терміналом. Відбувається шляхом построчного зчитування з вихідного потоку за допомогою fgets.
 
Все добре, перевірка виконання дельта індексу пройшла успішно, я зрадів, але…
«АЛЕ» відбулося, коли я спробував виконати індексацію основного індексу (порядку 400К записів, виконується кілька хвилин). Виявилося, що вихідний потік обривається при найменшій затримці виконання команди в терміналі. Простою мовою, коли вводиш команду, і термінал «замислюється». У результаті у мене залишалися «недоіндексірованние» файли.
 
Вирішив погуглити, як народ вирішує проблеми, натрапив на шматок коду, прямо в мане по ssh2 на php.net. Автор розв'язку пропонував ставити маркери початку і закінчення команди (echo '[start]'; $ command; echo '[end]') і встановити max_execution_time для скрипта.
Код наведено нижче.
 
 
$ip = 'ip_address'; 

$user = 'username'; 

$pass = 'password'; 

$connection = ssh2_connect($ip); 
ssh2_auth_password($connection,$user,$pass); 
$shell = ssh2_shell($connection,"bash"); 

//Trick is in the start and end echos which can be executed in both *nix and windows systems. 
//Do add 'cmd /C' to the start of $cmd if on a windows system. 
$cmd = "echo '[start]';your commands here;echo '[end]'"; 
$output = user_exec($shell,$cmd); 

fclose($shell); 

function user_exec($shell,$cmd) { 
  fwrite($shell,$cmd . "\n"); 
  $output = ""; 
  $start = false; 
  $start_time = time(); 
  $max_time = 2; //time in seconds 
  while(((time()-$start_time) < $max_time)) { 
    $line = fgets($shell); 
    if(!strstr($line,$cmd)) { 
      if(preg_match('/\[start\]/',$line)) { 
        $start = true; 
      }elseif(preg_match('/\[end\]/',$line)) { 
        return $output; 
      }elseif($start){ 
        $output[] = $line; 
      } 
    } 
  } 
} 

 
Як мені здалося, хороше рішення, але…
Тут АЛЕ полягало в умові preg_match. При виведенні інформації в $ output пишеться все, що дає на вихід термінал. Вищеописана проблема з «задумався терміналом» знову стала актуальною, тому що при паузі на термінал виводилася команда виведення маркера завершення echo '[end]' (саме сама команда, а не результат виконання). Все вирішилося шляхом додавання обмеження початку і кінця рядка в preg_match:
 

preg_match('/^\[start\]\s*$/',$line)

та перевірки на is_string для $ line.
 
Залишалося тільки подрехтовать напилком, і, вуаля, в проекті на Yii був створений компонент, який є свого роду прошарком для ssh2 функцій.
 
 
<?php
class SshException extends CException {}

/**
 * Class Ssh
 * It is a base class for the simplify a ssh connection management
 * and related commands execution
 *
 * @author Ivanenko Vladyslav
 */
class Ssh
{
    const EXEC_TYPE_EXEC = 'exec'; // type for ssh2_exec()
    const EXEC_TYPE_SHELL = 'shell'; // type for ssh2_shell()

    const START_MARK = '__start__';
    const FINISH_MARK = '__finish__';

    const MAX_EXECUTION_TIME = 1800; // max script execution time in sec

    private $user;
    private $password;
    private $host;
    private $port;

    private $shellType = 'bash'; // shell type
    private $shell = null; //shell identificator

    private $ssh = null; //connection

    private $execType;

    /**
     * Construct
     *
     * @param null $user
     * @param null $password
     * @param null $host
     */
    public function __construct($user = null, $password = null, $host = null, $port = null)
    {
        $config = Yii::app()->params['ssh'];
        $params = array('user', 'password', 'host', 'port');

        foreach($params as $param) {
            if(isset(${$param}) && !is_null(${$param})) {
                $this->{$param} = ${$param};
            } else {
                $this->{$param} = @$config[$param];
            }
        }

        return true;
    }

    /**
     * Connect to Ssh
     *
     * @return resource
     * @throws SshException
     */
    public function connect()
    {
        $this->ssh = @ssh2_connect($this->host, $this->port);
        if(empty($this->ssh)) {
            throw new SshException('Cant connect to ssh');
        }

        if(empty($this->execType)) {
            $this->execType = self::EXEC_TYPE_SHELL;
        }

        return $this->ssh;
    }

    /**
     * Login to ssh
     *
     * @throws SshException
     * @return bool
     */
    public function login()
    {
        if(!@ssh2_auth_password($this->ssh, $this->user, $this->password)) {
            throw new SshException('Cant login by ssh');
        }

        return true;
    }

    /**
     * Exec command by ssh
     *
     * @param $cmd
     * @param $type
     *
     * @return string
     * @throws SshException
     */
    public function exec($cmd, $type = self::EXEC_TYPE_SHELL)
    {
        if(is_null($this->ssh)) {
            $this->connect();
            $this->login();
        }
        $this->execType = $type;
        switch($this->execType) {
            case self::EXEC_TYPE_EXEC: $result = $this->execCommand($cmd); break;
            case self::EXEC_TYPE_SHELL: $result = $this->execByShell($cmd); break;
            default: throw new SshException('Incorrect exec type'); break;
        }

        return $result;
    }

    /**
     * Executes command by the direct ssh2_exec
     *
     * @param $command
     *
     * @return string
     * @throws SshException
     */
    private function execCommand($command)
    {
        if (!($stream = ssh2_exec($this->ssh, $command))) {
            throw new SshException('Ssh command failed');
        }
        stream_set_blocking($stream, true);
        $data = "";
        while ($buf = fread($stream, 4096)) {
            $data .= $buf;
        }
        fclose($stream);

        return $data;
    }

    /**
     * Executes command within the shell opening
     *
     * @param $command
     *
     * @return string
     */
    private function execByShell($command)
    {
        $this->openShell();
        return $this->writeShell($command);
    }

    /**
     * opens shell
     *
     * @throws SshException
     */
    private function openShell()
    {
        if(is_null($this->shell)) {
            // here is hardcoded width and height, you can change them.
            $this->shell = @ssh2_shell($this->ssh,  $this->shellType, null, 80, 40, SSH2_TERM_UNIT_CHARS);
        }

        if( !$this->shell ) {
            throw new SshException('SSH shell command failed');
        }
    }

    /**
     *
     * Write the command to the open shell
     *
     * @param $cmd
     * @param int $maxExecTime in sec
     *
     * @return string
     */
    private function writeShell($cmd, $maxExecTime = self::MAX_EXECUTION_TIME)
    {
        // write start marker
        fwrite($this->shell, $this->getMarker(self::START_MARK));
        // write command
        fwrite($this->shell, $cmd . PHP_EOL);
        // write end marker
        fwrite($this->shell, $this->getMarker(self::FINISH_MARK));
        stream_set_blocking($this->shell, true);
        sleep(1);
        $output = "";
        $start = false;
        // define the time until the script can be executed
        $timeUntil = time() + $maxExecTime;

        while(true) {
            if(time() > $timeUntil) {
                break;
            }
            $line = fgets($this->shell, 4096);
            // if any delay is happened while command is processing
            if(!is_string($line)) {
                sleep(1);
                continue;
            }
            // define the start executed command
            if(preg_match('/^' . self::START_MARK . '\s*$/', $line)) {
                $start = true;
            } elseif(preg_match('/^' . self::FINISH_MARK . '\s*$/', $line)) {  // define the last executed command
                break;
            } elseif($start) {
                // add console output to the script output data
                $output .= $line;
            }
        }

        return $output;
    }

    /**
     * Disconnect from ssh
     */
    public function disconnect() {
        $this->exec('exit');
        $this->ssh = null;
        if(!is_null($this->shell)) {
            fclose($this->shell);
        }
    }

    /**
     * Disconnect in destruct
     */
    public function __destruct() {
        $this->disconnect();
    }

    /**
     * Returns marker command
     *
     * @param string $type
     *
     * @return string
     */
    private function getMarker($type = self::START_MARK)
    {
        return 'echo "' . $type . '"' . PHP_EOL;
    }

}

 
П.С. Цей клас можна розширити, адже ssh2 не обмежується тільки двома функціями з виконання команд, є ще й функції для роботи з файлами, і інші типи авторизації і т.д. і т.п.
 
Спасибі за увагу, сподіваюся, стаття буде корисною.
Буду радий почути будь-які відгуки та конструктивну критику!
 
Автор: Владислав Іваненко, PHP Developer Zfort Group
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.