За последние 24 часа нас посетили 31052 программиста и 1821 робот. Сейчас ищут 1015 программистов ...

обертка для задач, запускаемых по крону

Тема в разделе "Прочие вопросы по PHP", создана пользователем alexey_baranov, 28 май 2009.

  1. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    Доброго времени суток!

    Сегодня в сто первый раз сталкнулся с проблемой, что какая- то задача, вызываемая ночью по крону не работает, а я об этом узнаю спустя несколько дней. Эти кроновские скрипты отличаются от обычных. Я с ними намучился. Они, во- первых, не вызываются через апач, и,во- вторых, результат их работы не виден на экране. Так что вы не скоро узнаете, если он не сработал.

    Самый смешной случай был, когда я случайно открыл одну XML- выгрузку, еженочно генерируемую по крону и обнаружил, что это файл двухмесячной давности. Эта выгрузка потом загружалась сторонней статистической программой и на основании ее данных высокое руководство судит о работе всей нашей организации. Хорошо, что отчет они видят в виде двух статистических цифр и они думали, что раз цифры не изменяются, значит мы работаем очень стабильно.

    Давно собирался, но только сегодня в очередной раз кинутый занялся этой проблемой, и создал себе помощника - небольшую библиотечку. Класец Cron, который будет выполнять такие скрипты и вызывает обработчик ошибок, если они конечно там возникнут и сообщать мне. Работать будет так:

    PHP:
    1. <?php
    2.  
    3. class FuckingBuggingCronTask implements Cron_Task{
    4.     function run() {
    5.         //тут сам скрипт, который раньше запускался по крону
    6.         include 'cron_tumbit.php';
    7.     }
    8. }
    9.  
    10. $cron= new Cron();
    11. $cron->setErrorHandler(new Cron_ErrorMailer());
    12. $cron->runTask(new FuckingBuggingCronTask());


    вот исходники
    PHP:
    1. <?php
    2.  
    3. /**
    4.  * Description of Cron
    5.  *
    6.  */
    7. class Cron extends Model {
    8.     /**
    9.      *
    10.      * @var Cron_ErrorHandler Перехватчик ошибок, возникших во время выполнения задачи
    11.      */
    12.     private $errorHandler;
    13.    
    14.     function __construct($errorHandler) {
    15.         $this->setErrorHandler($errorHandler);
    16.     }
    17.     /**
    18.      * Перехватчик ошибок, возникших во время выполнения задачи
    19.      *
    20.      * @return Cron_ErrorHandler
    21.      */
    22.     public function getErrorHandler() {
    23.         return $this->errorHandler;
    24.     }
    25.     /**
    26.      * @param Cron_ErrorHandler $errorHandler
    27.      */
    28.     public function setErrorHandler($errorHandler) {
    29.         $this->errorHandler = $errorHandler;
    30.     }
    31.     /**
    32.      * запускает задачу
    33.      * в случае, если во время ее выполнения происходит ошибка,
    34.      * отдает ее обработчику ошибок
    35.      * любой вывод считается варнингом и тоже обрабатывает обработчиком
    36.      *
    37.      * @var Cron_Task $task Задача, которую нужно выполнить крону
    38.      */
    39.     function runTask(Cron_Task $task) {
    40.         try {
    41.             ob_start();
    42.             $task->run();
    43.  
    44.             //во время выполнения таска произошел варнинг
    45.             if ($output= ob_get_clean()) {
    46.                 $e= new Exception($output);
    47.                 $this->getErrorHandler()->OnError($task, $e);
    48.             }
    49.  
    50.         //во время выполнения таска произошела ошибка
    51.         } catch (Exception $e) {
    52.             $this->getErrorHandler()->OnError($task, $e);
    53.         }
    54.     }
    55. }
    56.  
    57. /**
    58.  * Задача, запускаемая по крону
    59.  */
    60. Interface Cron_Task {
    61.     function run();
    62. }
    63.  
    64. /**
    65.  * Перехватчик ошибок возникших во время выполнения задачи крона
    66.  * обрабатыввает ошибки возникшие во время выполнения задачи
    67.  */
    68. Interface Cron_ErrorHandler {
    69.     /**
    70.      *
    71.      * @param Cron_Task $task
    72.      * @param Exception $error
    73.      */
    74.     function OnError($task, $error);
    75. }
    76.  
    77. /**
    78.  * Перехватчик ошибок внутри задачи, который отсылает ошибки админу на почту
    79.  */
    80. class Cron_ErrorMailer implements Cron_ErrorHandler {
    81.     /**
    82.      *
    83.      * @param Cron_Task $task
    84.      * @param Exception $error
    85.      */
    86.     function OnError($task, $error) {
    87.         $mailer= new AutoMailer();
    88.         $mailer->AddAddress(ADMINMAIL);
    89.         $mailer->Subject= "Ошибка во время выполнения в задачи крона!";
    90.         $mailer->Body= "
    91.            <p>Во время выполнения задачи крона произошла ошибка</p>
    92.            <p>&nbsp;</p>
    93.            <p>Класс задачи: ".get_class($task)."</p>
    94.            <p>Класс ошибки: ".get_class($error)."</p>
    95.            <p>Сообщение: {$e->getMessage()}</p>
    96.            <p>Файл: {$e->getFile()}</p>
    97.            <p>Строка: {$e->getLine()}</p>
    98.            <p>Стэк:</p>
    99.            <pre>$e->getTraceAsString()</pre>
    100.            ";
    101.         try{
    102.             $mailer->Send();
    103.         }
    104.         catch(MailException $e) {}
    105.     }
    106. }

    Исходники наверняка в багах, потому что я их еще ни разу не запускал. сегодня время остается только чтобы открыть тему. Смысл все равно понятен. Кто что думает? может я вообще зря все это делаю, потому что кто- то уже написал такую библиотечку или есть какое- то другое решение средствами крона и пхп?
     
  2. Вльдемар

    Вльдемар Активный пользователь

    С нами с:
    20 май 2006
    Сообщения:
    635
    Симпатии:
    0
    Адрес:
    Белхород
    по идее вот это должно сохранить вывод выполненного скрипта по крону в файл

    Код (Text):
    1.  
    2. php /www/htdocs/file.php >> log.log
    А файл потом можно отправить потом по почте, опять таки по крону :)
     
  3. zheka_13

    zheka_13 Активный пользователь

    С нами с:
    1 май 2009
    Сообщения:
    71
    Симпатии:
    0
    этот FuckingBuggingCronTask может не сработать по крону :)
     
  4. kostyl

    kostyl Guest

    zheka_13
    +1
     
  5. iliavlad

    iliavlad Активный пользователь

    С нами с:
    24 янв 2009
    Сообщения:
    1.689
    Симпатии:
    4
    А как вы их пробовали вызывать через апач? Или может быть вызываются не через апач?)

    Скрипт может в базу или файл записать
    Утром открыли страничку со статистикой и посмотрели, кто выполнился, а кто нет.
     
  6. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    да, косяков у такого подхода много.
    во-первых, крон может вообще не вызвать задачу, потому что сисадмин снес его и забыл что он зачем- то был нужен. она не выполнится, но я об этом ничего не узнаю
    во- вторых, есть ошибки, которые не перехватываются try- catch, например нет файла или нет такого класса, такие ошибки админу тоже не уйдут.

    мне очень понравились логи в базе, потому что они решают обе эти проблемы.
    но и у логов есть один недостаток- их надо каждый день просматривать. Поэтому, совместив оба этих подхода, получил вот такого франкенштейна. использовать планирую все так же

    PHP:
    1. <?php
    2.   class FuckingBuggingCronTask implements Cron_Task{
    3.       function run() {
    4.           //тут сам скрипт, который раньше запускался по крону
    5.           include 'cron_tumbit.php';
    6.       }
    7.   }
    8.  
    9.   $cron= new Cron();
    10.   $cron->addEventHendler(new Cron_ErrorMailer());
    11.   $cron->addEventHendler(new Cron_DBLogger());
    12.   $cron->runTask(new FuckingBuggingCronTask());

    исходники (в багах)
    PHP:
    1.  
    2. <?php
    3. <?php
    4. /**
    5.  * Description of Cron
    6.  *
    7.  */
    8. class Cron extends Model {
    9.     /**
    10.      *
    11.      * @var array ICron_EventHandler массив обработчиков крона. вызываются при запуске задачи, ошибке во время задачи и завершении задачи
    12.      */
    13.     private $eventHandlers= array();
    14.    
    15.     function __construct($eventHandlers= array()) {
    16.         foreach($eventHandlers as $eventHandler)
    17.             $this->addEventHandler($eventHandler);
    18.     }
    19.     /**
    20.      * массив обработчиков крона. вызываются при запуске задачи, ошибке во время задачи и завершении задачи
    21.      *
    22.      * @return array Cron_EventHandler
    23.      */
    24.     public function getEventHandlers() {
    25.         return $this->eventHandlers;
    26.     }
    27.     /**
    28.      * @param Cron_EventHandler $eventHandler
    29.      */
    30.     function addEventHandler($eventHandler) {
    31.         $this->eventHandlers[] = $eventHandler;
    32.     }
    33.     /**
    34.      * запускает задачу
    35.      * основные события передает обработчикам событий:
    36.      * запуск, ошибки и варнинги, завершение задачи
    37.      * любой вывод считается варнингом и тоже обрабатывает обработчикам
    38.      *
    39.      * @var Cron_Task $task Задача, которую нужно выполнить крону
    40.      */
    41.     function runTask(Cron_Task $task) {
    42.         foreach($this->getEventHandlers() as $eventHandler)
    43.             $eventHandler->OnRun($task);
    44.         try {
    45.             ob_start();
    46.             $taskResult= $task->run();
    47.  
    48.             //во время выполнения таска произошел варнинг
    49.             if ($output= ob_get_clean()) {
    50.                 $e= new Exception($output);
    51.                 foreach($this->getEventHandlers() as $eventHandler)
    52.                     $eventHandler->OnError($task, $e);
    53.                 return;
    54.             }
    55.  
    56.         //во время выполнения таска произошела ошибка
    57.         } catch (Exception $e) {
    58.             foreach($this->getEventHandlers() as $eventHandler)
    59.                 $eventHandler->OnError($task, $e);
    60.             return;
    61.         }
    62.  
    63.         //нормальное завершение
    64.         foreach($this->getEventHandlers() as $eventHandler)
    65.             $eventHandler->OnComplete($task, $taskResult?$taskResult:new Cron_TaskResult());
    66.     }
    67. }
    68. /**
    69.  * Результат задачи, который будет передан логеру
    70.  */
    71. final class Cron_TaskResult{
    72.     public $code= 0;
    73.     public $message= 'Выполнено';
    74. }
    75. /**
    76.  * Задача, запускаемая по крону
    77.  */
    78. abstract class Cron_Task {
    79.     /**
    80.      *
    81.      * @var int идентификатор задачи
    82.      */
    83.     private $id;
    84.     function __construct() {
    85.         $this->id= 'время в милисекундах';
    86.     }
    87.     function getId() {
    88.         return $this->id;
    89.     }
    90.     /**
    91.      * @return Cron_TaskResult
    92.      */
    93.     abstract function run();
    94. }
    95.  
    96. /**
    97.  * Перехватчик ошибок возникших во время выполнения задачи крона
    98.  * обрабатыввает ошибки возникшие во время выполнения задачи
    99.  */
    100. Interface ICron_EventHandler {
    101.     /**
    102.      * Вызывается при старте задачи
    103.      * @param Cron_Task $task
    104.      */
    105.     function OnRun(Cron_Task $task);
    106.     /**
    107.      * Вызывается при возникновении ошибки в задаче
    108.      *
    109.      * @param Cron_Task $task
    110.      * @param Exception $error
    111.      */
    112.     function OnError(Cron_Task $task, Exception $e);
    113.     /**
    114.      * Вызывается при нормальном завершении задачи
    115.      *
    116.      * @param Cron_Task $task
    117.      * @param Cron_Result $taskResult
    118.      */
    119.     function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult);
    120. }
    121. /**
    122.  *  обработчик событий, который отсылает ошибки внутри задачи админу на почту
    123.  */
    124. class Cron_ErrorMailer implements ICron_EventHandler {
    125.     function OnRun(Cron_Task $task){}
    126.     function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult){}
    127.  
    128.     function OnError($task, $error) {
    129.         $mailer= new AutoMailer();
    130.         $mailer->AddAddress(ADMINMAIL);
    131.         $mailer->Subject= "Ошибка во время выполнения в задачи крона!";
    132.         $mailer->Body= "
    133.            <p>Во время выполнения задачи крона произошла ошибка</p>
    134.            <p>&nbsp;</p>
    135.            <p>Класс задачи: ".get_class($task)."</p>
    136.            <p>Класс ошибки: ".get_class($error)."</p>
    137.            <p>Сообщение: {$e->getMessage()}</p>
    138.            <p>Файл: {$e->getFile()}</p>
    139.            <p>Строка: {$e->getLine()}</p>
    140.            <p>Стэк:</p>
    141.            <pre>$e->getTraceAsString()</pre>
    142.            ";
    143.         try{
    144.             $mailer->Send();
    145.         }
    146.         catch(MailException $e) {}
    147.     }
    148. }
    149.  
    150. /**
    151.  * Логгер крона, который всю информацию сохраняет в базе
    152.  * таблица cron_logs
    153.  */
    154. class Cron_DBLogger extends Model implements ICron_EventHandler {
    155.     function OnRun(Cron_Task $task){
    156.         //бла бла бла задача и время старта уходят в базу
    157.     }
    158.     function OnError(Cron_Task $task, Exception $e){
    159.         //ошибка уходит в базу
    160.     }
    161.     function OnComplete(Cron_Task $task, Cron_TaskResult $taskResult){
    162.         //бла бла бла результат тоже в базу
    163.     }
    164.     /**
    165.      * Очищает свои накопленные логи
    166.     */
    167.     function clear(){
    168.         //delete from cron_logs
    169.     }
    170.     /**
    171.      * возвращает все логи с $from по $to, включительно в виде плоского массива {task, run, errorcode, errormessage, errorfile, errorline,  errortrace, resultcode, resultmessage}
    172.      *
    173.      * @param Date $from
    174.      * @param Date $to
    175.      *
    176.      * @return array {task, run, errorcode, errormessage, errorfile, errorline,  errortrace, resultcode, resultmessage}
    177.      */
    178.     function get($from, $to){
    179.         //select from cron_logs where between $from $to
    180.     }
    181. }
    182.  
    Естественно, в уме предполагаю вьюшку для DBLoger а в виде таблицы с красными строками, у которых есть эррор или нет резалта, что означает крон оборвался где- то посередине на require_once или "нет такого класса". Какие есть мнения?
     
  7. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    сразу видно, я не сисадмин ни в одном глазу, если не знал этого. а эта >> добавляет в файл или пересоздает его по новой?
    а вообще тоже ничего, главное просто, как колесо
     
  8. Frozen

    Frozen Активный пользователь

    С нами с:
    20 окт 2008
    Сообщения:
    540
    Симпатии:
    0
    Адрес:
    Москва
    добавляет
     
  9. iliavlad

    iliavlad Активный пользователь

    С нами с:
    24 янв 2009
    Сообщения:
    1.689
    Симпатии:
    4
    Код не смотрел, но по поводу
    какая разница - почта, файл, смска или страничка, на которой список скриптов, которые не запускались?

    это всё логи и их надо смотреть)
     
  10. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Я обычно при запуске крона пишу в служебную таблицу запись, что такая-то задача запустилась тогда-то. Таким образом можно в WEB интерфейсе админки проверять когда последний раз запускался cron и сигнализировать пользователю если что-то не так.

    Насчёт нету файла - тут уж пользователь сам виноват - надо обязаловкой проверять file_exists, is_readable, is_writeable и.т.д.
    P.S. Можно записывать started в начале, а в конце менять на completed. Так мы узнаем об ошибках, даже если упадёт скрипт в неотлавливаемую ошибку.
     
  11. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    пишешь из ПХП, правильно я понял? если так, это как раз то, что я и собираюсь делать при помощи обертки.

    а что ты думаешь по поводу php /www/htdocs/file.php >> log.log ?
     
  12. zheka_13

    zheka_13 Активный пользователь

    С нами с:
    1 май 2009
    Сообщения:
    71
    Симпатии:
    0
    тут я так понял человеку требуется знать выполнился скрипт вообще или нет и каков результат без просмотра логов и всего такого
    Самый простой и безотказный вариант:
    в конце скрипта поставить пару функций отсылки результата на почту и на смс, можно даже звонок на тел организовать для самых извращенных нелюбителей читать логи :)
    итого получаем - если выполнилось, то пришло, если не выполнилось - то не пришло.
    вот и вся кухня и никакие мега классы писать не надо...
     
  13. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    да какие мега-классы? ровно это и написано, если использовать Cron + Cron_ErrorMailer

    просто чтобы каждый раз в каждом кроновском скрипте не писать эти отсылки и звонки и решился написать эту обертку. это реально удобнее, чем захламлять смысловой код кроновского скрипта всякими отсылками и звонками, которые по смыслу как бы за рамками этого скрипта.

    php /www/htdocs/file.php >> log.log очень нравится своей банальностью.
     
  14. MiksIr

    MiksIr Активный пользователь

    С нами с:
    29 ноя 2006
    Сообщения:
    2.339
    Симпатии:
    44
    Крон может высылать весь вывод скрипта на почту. Таким образом любая ошибка или варнинг придет на почту. Если в скрипте делать вывод об успешном завершении задачи - можно получать успех на почту и таким образом проверять, что "работает". Мы делаем как Psih, пишем в базу, но это работает, ясно дело, когда у вас свой планировщик, а крон - это так, обертку с номером задачи дергать.
     
  15. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    Понятно. Задача в том, чтобы оставить скрипты как есть прикладными скриптами без дополнительной нагрузки всякими служебными приблудами. И придумать способ как в 100% случаев сбоев получать Alarm! на свою почту. Получать сообщения в случаях успешного выполнения (это когда oputput пустой) не желательно. Потому что зачем оно мне надо да? Спам какой-то, ну выполнился и выполнился. Зачем мне эти письма каждое утро?

    А вот это вот не понял
    еще раз другими словами можно?

    Если как Psih, то Cron + Cron_DBLogger, и у меня тоже все будет ок! ну и Cron_ErrorMailer впридачу, чтобы с утра первым делом почту открыл и сразу увидел, что что-то не так.


    А почему никто не делает php /www/htdocs/file.php >> log.log ?
     
  16. MiksIr

    MiksIr Активный пользователь

    С нами с:
    29 ноя 2006
    Сообщения:
    2.339
    Симпатии:
    44
    Ну когда у вас есть список задач в базе и вы дергаете в кроне cron.php id-задачи то можно писать в эту же таблицу по id задачи дату последнего выполнения. Если же у вас набор скриптов - тут уже сложно как-то унифицировать.

    Теперь, у каждого скрипта есть два "канала" вывода - STDOUT и STDERR. На первый посылаются все ваши print-ы из скрипта. На второй - сообщения об ошибках (PHP-ые ну или вы сами можете туда вывод делать)

    На 100% получить информацию, что задача вообще не запускалась - нельзя. Ибо, что бы что-то периодически проверять, нужно так же периодически запускать проверку.. которая тоже может не запуститься и т.д. И вообще, случаи "админ снес крон", это из серии кирипича на голову - можно одевать каску, но и уронить могут кувалду. Так что в рассмотрение не берется.

    Т.е. отсекать нужно варианты, когда крон дернул скрипт, но вышла ошибка. В этом случае или сам крон или PHP генерит ошибку и посылает ее на STDERR. ЕЕ можно получать на почту. Обычный вывод скрипта, если он вдруг есть, можно "занулить" через >/dev/null

    Код (Text):
    1. А почему никто не делает php /www/htdocs/file.php >> log.log ?
    А какой в этом смысл? Этот файл никто не будет читать. Ошибки нужно получать как можно быстрее, а следить что "работает" нужно по конечному результату скрипта (например, где-то у себя в админке проверять timestamp вашего XML-я и предупреждать, если файл давно не менялся).
     
  17. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    если я правильно понял, то это что-то вроде моего abstract class Cron_Task с его уникальным айдишником

    это да, перебрал. К тому же такие случаи лежат в зоне ответственности сисадмина, поэтому правильнее бедет, если об этом будет ломать голову сам сисадмин. про то что крон "забыл" вызвать скрипт навсегда забываем.
     
  18. Вльдемар

    Вльдемар Активный пользователь

    С нами с:
    20 май 2006
    Сообщения:
    635
    Симпатии:
    0
    Адрес:
    Белхород
    Если не делать >> log.log, то результат приходит мне на почту
    результат выполнения своей последней задачи я направляю в /dev/null ибо результат мне не нужен
    И по хорошему лучше повесить дополнительное оповещение (почта, смс, аська) при выполнении задачи некорректно, и потом уже лезть читать логи.
     
  19. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    Получить систему, которая гарантированно сообщает об ошибках сама можно только обрабатывая STDERR снаружи от ПХП, потому что в самом ПХП невозможно перехватить ошибки типа "банан тебе а не require_once" и "нет такого класса". Так что только PHP обертками тут не обойдешься.

    От Cron + Cron_DBLogger (Psih style) тоже не в восторге, потому что его надо открывать и просматривать каждый день, а я лентяй в хорошем смысле. С таким же успехом я могу STDERR отправлять в файл при помощи >> , сделать ссылку на рабочем столе на него и с утра проверять не появилось ли там чего нового.
     
  20. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    alexey_baranov
    Ну почему-же, проверь существование файла через file_exists перед require и выдай ошибку, если такого нету :)
     
  21. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    Допустим существование самого cron_tumbit.php можно проверить по file_exists, но он рекварит пяток других файлов, а каждый из них еще пяток и т.д. кроновский файл может подключать больше сотни файлов. Их ты как проверять будешь по file_exists?

    А про рекваре я беспокоюсь потому что на той неделе именно она оборвала ночной скрипт после того как я технично подредактировал include_path.

    Если вводить какую- то страховочную систему, которая сообщает о ночных багах, то только такую которая работает во всех 100% ситуациях. Если система обрабатывает все ошибки но не файл нот экзист и класс нот экзист, то это какая-то полумера. Ее саму придется страховать. А зачем она тогда нужна? Я пока что ума не приложу, как из PHP перехватить эти две ошибки. Если это сделать, можно запускать любой кроновский скрипт из под PHP-обертки.
     
  22. читать про __autoload() ?
     
  23. alexey_baranov

    alexey_baranov Активный пользователь

    С нами с:
    3 фев 2009
    Сообщения:
    647
    Симпатии:
    0
    Адрес:
    Сургут
    autoload тут не поможет. Из __autoload(), насколько я знаю, запрещено выбрасывать эксепшины. И сам file not exists trycatch-ом не перехватывается.
     
  24. http://www.onphp5.com/article/61

     
  25. iliavlad

    iliavlad Активный пользователь

    С нами с:
    24 янв 2009
    Сообщения:
    1.689
    Симпатии:
    4
    Тестирование наверное помогло бы.