СЕРАФИМ ПИКАЛОВ
PHP: делаем отладку на стороне клиента, или Операции под наркозом
У клиента что-то сломалось, а у нас всё работает. Проклиная всё на свете, идём в код к клиенту. Исправлять нужно срочно, отладчика нет, а скрипты продолжают эксплуатироваться пользователями. Как искать ошибку, ничего не ломая, как отладить исправленный код, как не показать никому ничего лишнего?
Все мы прекрасно знаем, что любой код перед установкой клиенту должен быть тщательно оттестирован, все ошибочные ситуации описаны, и сообщения о них должны быть максимально информативны.
С другой стороны, все мы знаем, что теория и практика – две разные вещи. Обнаружив ошибку, которую на тестовой версии системы смоделировать не удалось, мы сталкиваемся с необходимостью редактировать код, уже установленный клиенту и скорее всего выполняющийся в этот момент. Тут возникает необходимость сделать всё как можно более незаметно и безопасно.
О некоторых методах того, как это сделать, мы поговорим в этой статье.
Условия работы
В рассматриваемой ситуации нам придётся работать со скриптами, которые уже установлены у клиента и могут быть востребованы в любой момент.
Поэтому необходимо ввести некоторые ограничения:
- Нам необходимо локализовать и исправить ошибку, не испортив правильно работающие скрипты.
- Процесс нашей отладки должен быть виден только нам, пользователи не должны ничего заметить.
- Мы не можем установить или перенастроить ПО на сервере, т.е. использование сторонних отладочных систем невозможно, но мы можем добавлять новые или редактировать старые php-скрипты.
- Мы имеем только ssh (и/или telnet) и http-доступ.
Теперь полностью прояснив, что нам можно, а чего нельзя, попробуем сделать нашу работу наиболее комфортной и продуктивной.
Идентификация разработчика
В процессе поиска и исправления ошибки нам придётся менять порядок работы программы и получать различную информацию.
Безусловно, можно пойти старым проверенным способом и выводить отладку в error_log, но этот метод заставит нас метаться между браузером и файлом лога, замедлит выполнение скрипта для всех пользователей и засорит лог, т.к. исполняться он будет для всех одинаково. Но даже смирившись с такими неудобствами, нельзя забывать, что код, который вы вводите в скрипт, также не застрахован от ошибок.
Ответ на поставленную проблему напрашивается сам собой: мы хотим, чтобы скрипт однозначно определял, кто перед ним – разработчик или обычный пользователь, и в соответствии с этим работал по-разному. Для этого отладочный код выведем в условный блок, условием входа в который будет то, что пользователь является разработчиком.
О том, как это установить смотрим ниже.
Специальная GET-переменная
Довольно небезопасный способ, но нередко встречаемый. Смысл заключается в помещении отладочного кода в условный блок, проверяющий, установлена ли определённая глобальная переменная.
Например, так:
//---код скрипта
if (isset($_GET[‘debug’])) {
echo ‘Debug info!’;
}
//---код скрипта
Плюс метода – быстрота доступа к режиму отладки.
Минус в том, что, зная имя переменной, отладку может увидеть любой желающий, кроме того, метод не подходит, если POST передает параметры скрипту.
Идентификация по IP
Очень удобный и довольно надёжный способ. Как и во всех описанных тут методах, отладочный код помещается в условный блок, условие – совпадение IP-пользователя с установленным разработчиком.
Пример:
If ($_SERVER[‘REMOTE_ADDR’]==='123.234.234.15') {
Echo ‘Debug info2!’;
}
Плюс – вывод отладки не зависит от того, что мы передаём скрипту.
Минусы в следующем. Во-первых, не редактируя код, мы не можем отключить отладку, т.е. для того чтобы посмотреть, как было и как стало, нам придётся редактировать код.
Во-вторых, при использовании proxy-сервером REMOTE_ADDR однозначно не определяет компьютер. Но даже проверка на значение HTTP_X_FORWARDED_FOR не даёт 100% гарантии, т.к. эти значения зависят от того, что возвращает веб-сервер и proxy, а доверять им можно только отчасти.
Идентификация по cookie
Пожалуй, самый надёжный способ определить в пользователе разработчика – поставить условие на исполнение отладочного блока – наличие определённой в cookie переменной.
Пример:
if (!empty($_COOKIE[‘debug’])) {
echo ‘Debug info3!’;
}
Плюс в том, что отладку будет видеть только тот пользователь, у которого установлена cookie для данного ресурса с определённой переменной. Это позволяет достаточно просто давать доступ к отладочной информации только тем, кому это разрешено. Также очень удобно переключаться между режимами, выставляя и убирая cookieпеременную.
Минусы в том, что опять же возможно получение несанкционированного доступа к отладочной информации. Если злоумышленник узнает, какая cookie-переменная управляет выводом, он может сформировать такую же cookie, но этого достаточно просто избежать, меняя имя переменной, или способом, описанным чуть ниже.
Для удобства управлением отладки можно написать и установить у клиента скрипт по установке и удалению нужной cookie-переменной.
Комбинированный способ
Два вышеописанных способа можно скомбинировать в один и таким образом избежать проблем, связанных как с неуникальным IP, так и с кражей cookies.
Пример:
if (!empty($_COOKIE['DBG'])
&&($_SERVER[‘REMOTE_ADDR’]=== '123.234.234.15')) {
echo ‘Debug4’;
}
В принципе, используя скрипт установки cookie-переменной со случайным именем из предыдущего раздела с авторизацией в начале работы, можно IP вносить как значение в cookie (параноики могут его ещё и кодировать), и тогда отладочный блок примет более удобный и универсальный вид.
Теперь управлять режимом выполнения удалённого скрипта можно, не изменяя код.
Время жизни отладки
Чтобы, исправив ошибку и случайно забыв об одном затерявшемся блоке, потом не ломать голову, почему это у нас всё работает не так, удобно ограничить время жизни отладки.
Если использовать метод с cookie, то тут всё просто – указываем время жизни cookie в функции setcookie, и дело в шляпе (null означает время жизни до закрытия браузера). Но если этот способ нам по каким-то причинам не подходит, то можно ограничить время жизни отладочного блока определённым сроком после последней модификации файла, например, так:
if ( ( time() - filemtime(__FILE__) ) < BLOCK_LIFE_TIME) {
//…
}
где BLOCK_LIFE_TIME – константа, определяющая временной промежуток. Кстати, подключив фантазию, можно, например, сделать систему оповещения о забытой отладке.
Всё вышеописанное очень удобно собрать в одну или несколько вспомогательных функций и подгружать их как include при необходимости.
Подставной файл
Зачастую количество отладочных блоков превышает допустимый порог, и скрипт превращается в смесь разрозненных кусков кода. Чтобы этого не было, можно использовать метод подставного скрипта. Суть метода в том, что мы разделяем наш скрипт на стабильный и нестабильный. Доступ к нашему скрипту осуществляется через специальный подставной скрипт, который определяет, что за пользователь перед ним и подгружает стабильный или нестабильный код.
Например, мы ведём серьёзные работы со скриптом myscript.php.
Переименуем стабильную версию в myscript.php.stable, а нестабильную – в myscript.php.unstable и создадим следующий подставной скрипт myscript.php:
<?php
$d_file=__FILE__;
//is_debug функция, проверяющая, кто использует скрипт – обычный пользователь или разработчик
if (is_debug()) {
onyma_include($d_file.'.unstable');
}
else {
onyma_include($d_file.'.stable');
}
?>
Таким образом, каждый будет работать с той версией, которая необходима.
Нетрудно заметить, что приведённый подставной скрипт является универсальным и при использовании с другими скриптами не требует ничего, кроме изменения своего имени. Так же обратите внимание на то, что мы меняем расширения у скриптов, и без специальной настройки веб-сервера их можно будет загрузить как обычный текст!
Отслеживание ошибки
Теперь, когда мы можем делать почти всё что захотим, не боясь быть замеченными, поговорим непосредственно о поиске самой ошибки.
Ошибка PHP
Рассмотрим самую простую ситуацию, когда интерпретатор сообщает нам о возникновении ошибки в конкретной строке конкретного файла.
В этом случае удобней всего установить свой обработчик ошибкой (error_handler) и внутри вызвать стандартную PHP-функцию debug_backtrace() (однозначно определит путь к ошибке), вывести значения элементов суперглобального массива $GLOBALS и параметра error_context.
Пример:
If (is_debug()) {
Function debug_eh($errno, $errstr, $errfile, $errline, $errcontext) {
/*вывод необходимой информации*/
print_r(debug_backtrace());
print_r($GLOBALS);
print_r($error_context);
}
$eh=set_error_handler(‘debug_eh’);
}
// место перед возникновением ошибки
If (is_debug()) {
set_error_handler($eh);
}
Таким образом, мы получим полную информацию о том, где и как возникла ошибка. Очень часто достаточно просто вывести debug_backtrace в месте её возникновения.
Ошибка в алгоритме
Теперь рассмотрим задачку посложней. Допустим, код выполняется без ошибок, но результат не совпадает с нашими ожиданиями. Очевидно, что ошибка где-то в алгоритме, но как же её локализовать?
Для начала нужно чётко определить, что мы должны получить, и чем ожидаемый результат отличается от полученного.
Свести различия нужно к какому-нибудь конкретному тезису, к примеру: «Тут мы должны были увидеть две строки, а видим только одну». После этого нам придётся провести анализ кода и определить переменные, от которых зависит вывод данных строк.
Сделать это можно, к примеру, такими способами:
Ограничиваем код выводом двух отладочных сообщений и сдвигаем их друг к другу до тех пор, пока они не будут обрамлять строки, считаемые нами ошибочными.
Второй способ заключается в анализе вывода скрипта. Для этого включаем буферизацию вывода и обработчик на каждый тик PHP:
<?php
define('FIND_PATERN','/some_str/');
function find_str() {
if (preg_match(FIND_PATERN,ob_get_contents())) {
$db=debug_backtrace();
print_r($db);
unregister_tick_function('find_str');
}
ob_flush(); // Это необходимо для сбрасывания буфера
}
ob_start();
register_tick_function('find_str');
// тут можно поставить побольше, хотя от этого зависит точность места определения
declare (ticks=1);
//код программы...
ob_end_flush();
?>
Таким образом, на каждом тике мы проверяем буфер вывода, ищем там необходимую подстроку, выводим путь к месту, где она выводится, и отключаем анализатор, чтобы не выполнять напрасную работу.
Подобным же методом можно найти место вывода и формирования нужной строки или выполнения некоторой операции, а также отслеживать «жизнь» глобальных переменных (только глобальных) – своего рода Watch-функция отладчика.
Реализация отладчиков
В последнем описанном приёме мы использовали конструкцию declare для отслеживания истории жизни переменной, по сути дела, мы сделали аналог функции watch в отладчике.
Далее мы рассмотрим, как с помощью этой конструкции реализовать простенькое подобие отладчика.
Сетевой отладчик
Для реализации сетевого отладчика нам будет необходимо, чтобы PHP был собран с поддержкой сокетов. В качестве сервера управления отладкой проще всего выбрать приложение терминального типа (например, в Linux удобно использовать netcat), которое будет запускаться в качестве сервера, ожидающего соединения, к примеру, на порт 9998. Клиентом будет наш отлаживаемый скрипт, который будет получать команды и возвращать результаты действия, а функцию отладки реализуем с помощью тик-функции.
Схема работы будет следующая: при запуске скрипта создаётся сокетное соединения с сервером отладки. После этого при каждом тике запускается нами определённая функция отладки, которая посылает данные о текущем состоянии к серверу и ждёт ответа – команды:
- s (step) – один шаг вперёд.
- bp (break point) – точка останова. Определяется как номер строки и имя файла.
- st (stop) – немедленная остановка выполнения.
- g (go) – выполнить скрипт до конца.
После получения одной из описанных команд отладчик её обрабатывает и выполняет необходимые действия.
Приведем код:
<?php
// Отключим буферизацию, чтобы видеть пошаговое выполнение программы
ob_implicit_flush();
// Убираем лимит времени на выполнение скрипта, т.к. это может воспрепятствовать пошаговому выполнению
set_time_limit(0);
// Функция посылки сообщения отладчику
function D_write($msg) {
global $fp;
if (is_resource($fp)) {
fwrite($fp,"$msg\n");
}
}
// Функция принятия сообщения от отладчика
function D_read() {
global $fp;
// Приглашение на ввод команды
D_write('Enter command = [g, s, bp [line number]:[file name], st]');
if (is_resource($fp)) {
return fread($fp, 8192);
}
}
// Функция отладки
function debug_func() {
global $fp;
static $GO=false;
static $STOP_LINE,$STOP_FILE;
$db=debug_backtrace();
// Проверка: не достигли ли мы точки останова, (break point) и если нет, то уходим из функции
// отладки (функцию is_bp реализуйте сами)
if (!is_bp($db,$STOP_FILE,$STOP_LINE)) return;
// Посылаем текущее состояние. Сюда же можно добавить другую необходимую информацию
D_write(print_r($db,1));
// Ожидаем команду от сервера
$ret=D_read();
// Анализируем поступившую команду
switch (substr($ret,0,2)) {
case 'st': {
fclose($fp);
exit('Stop by debuger<br>');
}
case 'bp': {
// Выставляем точку остановки
list($STOP_LINE,$STOP_FILE)=split(':',substr($ret,3,strlen($ret)-4));
break;
}
case “g\n”: {
// Так как необходимо выполнить скрипт до конца без остановки,
// убираем вызов функции отладки
unregister_tick_function ('debug_func');
break;
}
case “s\n”:
default: {
break;
}
}
}
//подключаемся к серверу управления отладкой
$fp = fsockopen('10.0.2.145', 9998);
register_tick_function('debug_func'); //включаем отладчик
declare (ticks=1);
/*Код программы*/
fclose($fp);
?>
Этот довольно простой код с лёгкостью можно расширить, обезопасить, введя авторизацию, и реализовать как отдельный include.
Отладчик без использования сокетов
Вполне может сложиться ситуация, что клиент, у которого мы установили систему, в целях безопасности установил PHP без сокетов, в таком случае описанный ранее отладчик работать не будет.
Реализуем такой же по функциональности код, но без использования сокетов.
Поскольку соединение с удалённой машиной создать нельзя, для передачи данных мы будем использовать файловую систему, а точнее – один файл.
Схема работы теперь приобретет следующий вид: при каждом тике в браузер выдается информация о текущем состоянии, и запускается нами определённая функция отладки, которая ожидает появление в определённой директории определённого файла, как только файл появляется, из него считывается команда, и он удаляется.
Управление отладкой можно реализовать двумя способам.
Создаётся отдельный скрипт, который генерирует интерфейс управления и создаёт файл с командой.
Или интерфейс управления формируется в начале отлаживаемого скрипта и по средствам AJAX обращается к другому скрипту, создающему файл с необходимой командой. То есть в самом начале выполнения в браузер отсылается небольшой AJAX-движок и несколько управляющих кнопок. При нажатии на определённую кнопку с помощью JavaScript посылается определённая команда к скрипту, единственная функция которого – создать файл и поместить туда эту команду.
Второй способ немного удобней, т.к. управление происходит из того же окна, где и выполняется отлаживаемый скрипт.
Привидём код, опустив аналогичные чати:
// Начало такое же
// Функция посылки сообщения о процессе отладки
function D_write($msg) {
// Для простоты будем считать, что такой вывод не нарушит структуру документа
echo "<script language=JavaScript>";
// Будем выводить отладку в отдельный div (другой вариант, например, писать информацию
// во временный файл, вставлять его с помощью iframe и тут этот iframe перегружать)
echo "dbg_div=document.getElementById('dbg_div');";
echo "dbg_div.innerHTML=dbg_div.innerHTML+'<HR>$msg';";
echo "</script>";
}
// Функция принятия сообщения от отладчика
function D_read() {
global $fp;
while (!file_exists($fp)) ;
// Думаю, длина команды не более 600 байт
return fread($fp, 600);
}
// debug_func() практически не изменился, за исключением того, что после обработки полученной команды
// необходимо зачистить (удалить) файл с командой, чтобы она повторно не обрабатывалась
// Вместо сокета открываем файл
$fp = fopen('dbg_cmd.txt', 'r');
// Вставляем управляющие элементы в начало скрипта.
// В ajax.js помещаем AJAX-движок с функцией Send2Dbg(cmd) отправки команды скрипту dbgfile.php, который создаёт
// dbg_cmd.txt и пишет туда команду
echo "<script language='JavaScript' src=ajax.js></script>"
echo '<div id="dbg_cntrls">';
echo '<a href="nojavascript... Send2Dbg(\'st\');">STOP</a> ';
echo '<a href="nojavascript... Send2Dbg(\'g\');">GO</a> ';
// и т.д.
echo '</div>';
echo '<div id="dbg_dib"></div>';
// Далее всё так же
Нельзя забывать, что подгружать эти отладчики необходимо, только если к скрипту обратился программист. Как это сделать, мы уже описали ранее.
Заключение
Мы разобрали несколько несложных приемов по реализации невидимой работы с уже используемым кодом. Будем надеяться, описанные выше способы помогут вам в реализации и улучшении собственных механизмов исправления ошибок на работающей системе, но в любом случае всё описанное выше призвано решать проблемы, которые не должны появляется, их необходимо предотвращать ещё на этапе разработки. Серьёзное тестирование и хорошо реализованная система сообщения об ошибках могут значительно облегчить поддержку. Так что лучше оставьте эти методы на самый крайний случай.
Приложение
Определяем основные термины
Мы будем оперировать некоторыми понятиями, значения которых зависят от контекста. Во избежание неточностей определим их.
Ошибкой в этой статье будем считать как PHP-ошибку (Fatal error, Warning, Notice), так и ошибку в алгоритме программы (т.е. когда интерпретатор без проблем выполнил скрипт, но результат выполнения не соответствует нашим ожиданиям).
Под отслеживанием ошибки будет подразумеваться процесс сбора максимального количества информации, такой как значения переменных скрипта в момент возникновения ошибки и путь к ошибке. Тут надо сделать пояснение. Как правило, крупные проекты разбиты на множество отдельных скриптов и часто имеют сложную вложенность, из-за этого информация интерпретатора PHP о месте возникновения ошибки для нас недостаточна. Действительно, представим ситуацию, когда сообщение об ошибке указывает на одну из часто используемых функций, в какой-либо общеиспользуемой библиотеке. Это знание нам мало чем поможет, если функция достаточно часто используется, намного важней для нас найти место её вызова. Информацию о том, где вызывается эта функция в основном скрипте (и/или подгружается скрипт, где эта функция была вызвана), будем называть путём к ошибке.
Под отладкой будем подразумевать любые действия по изменению и анализу кода для получения правильно работающего скрипта.