Доброго времени суток, дорогие читатели!
Хочу рассказать вам об интересной задаче, которая стала передо мной в рамках проекта и, естественно, о ее решении.
Исходные данные:
Стандартный набор 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 для дельта индекса я получал ошибку:
Эта ошибка означает, что моему пользователю не хватает прав для выполнения rotate (а у меня юзер с правами рута, sudo -s), хотя из консоли напрямую я спокойно выполнял эту команду безо всяких ошибок.
Далее я решил поискать еще, и обнаружил, что можно эмулировать ввод команд через терминал (функция ssh2_shell). С помощью стандарного потока и фукнции fwrite можно писать команды в «терминал» и получать на выходе такой же стандарный выходной поток, т.е. результат, выдаваемый терминалом. Происходит путем построчного считывания из выходного потока при помощи fgets.
Все хорошо, проверка выполнения дельта индекса прошла успешно, я обрадовался, но…
«НО» произошло, когда я попытался выполнить индексацию основного индекса (порядка 400к записей, выполняется несколько минут). Оказалось, что выходной поток обрывается при малейшей задержке выполнения команды в терминале. Простым языком, когда вводишь команду, и терминал «задумывается». В итоге у меня оставались «недоиндексированные» файлы.
Решил погуглить, как народ решает проблемы, натолкнулся на кусок кода, прямо в мане по ssh2 на php.net. Автор решения предлагал ставить маркеры начала и окончания команды (echo '[start]'; $command; echo '[end]') и установить max_execution_time для скрипта.
Код приведен ниже.
Как мне показалось, хорошее решение, но…
Здесь НО заключалось в условии preg_match. При выводе информации в $output пишется все, что дает на выход терминал. Вышеописанная проблема с «задумавшимся терминалом» снова стала актуальной, т.к. при паузе на терминал выводилась команда вывода маркера завершения echo '[end]' (именно сама команда, а не результат выполнения). Все решилось путем добавления ограничения начала и конца строки в preg_match:
и проверки на is_string для $line.
Оставалось только подрехтовать напильником, и, вуаля, в проекте на Yii был создан компонент, который является своего рода прослойкой для ssh2 функций.
П.С. Этот класс можно расширить, ведь ssh2 не ограничивается только двумя функциями по выполнению команд, есть еще и функции для работы с файлами, и другие типы авторизации и т.д. и т.п.
Спасибо за внимание, надеюсь, статья будет полезной.
Буду рад услышать любые отзывы и конструктивную критику!
Автор: Владислав Иваненко, PHP Developer Zfort Group
Хочу рассказать вам об интересной задаче, которая стала передо мной в рамках проекта и, естественно, о ее решении.
Исходные данные:
Стандартный набор 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