Фоновое выполнение скрипта на PHP без crontab
Прислано AMD на Ноябрь 15 2011 08:52:26
Озадачили меня тут написать демона на PHP. Т.е. скрипт, который будет заданное количество раз в заданное количество часов в случайное время (всегда случайное) выполнять определенные действия, и все это без использования cron'a.

До этого никогда не заморачивался, а тут после постановки задачи, начал было думать что так нельзя, что php скрипт надо вызывать браузером…ну задача то поставлена, надо выполнять.

Первая мысль — отключить ограничение времени выполнения скрипта. Запрещено хостером.

Вторая мысль — яваскриптом повторять аякс-запрос периодически (да хоть раз в секунду). — нельзя (требование заказчика).

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

1. Пачка сигарет, ночь, гугл, доки, книги, мануалы….
goto 1…

На выходе получаю:
Задача_1:
Реализовать генератор времен выполнения скрипта, исходя из заданных количества раз и количества часов. Хранить где-то эти времена.

Задача_2:
Работать после закрытия браузера

Задача_3:
Не вылетать после окончания ограничения времени выполнения скрипта

Задача_4:
Выполнять в нужное время какие-то действия.

Итак…
Пишем в конфиге исходные данные:

session_start();  // Старт сессии
$num_starts = 120; // Количество запусков скрипта за промежуток времени
$hours = 1; // Количество часов, в течение которых нужно запускать скрипт $num_starts раз.
$time_sec = $hours*3600; // Количество секунд в цикле запусков
$time_to_start = array(); // Собственно, массив с временами запусков
ignore_user_abort(1);   // Игнорировать обрыв связи с браузером 


Далее пишем функцию, которая поможет нам сгенерировать времена запуска.
В ней мы генерируем случайное число от 0 до количества секунд в исходном интервале.
/******
* @desc  Генерируем интервал между запусками.
*/
function add_time2start() {
    global $time_sec, $time_to_start;
    $new_time = time()+rand(0, $time_sec);
    if (!in_array($new_time, $time_to_start)) {   // Если такого времени в массиве нет - добавим
        $time_to_start[] = $new_time;
    } else {
        add_time2start(); // Если такое время уже есть - генерируем заново.
    }
}


Далее сгенерируем и запишем в сессию массив времен запуска. Предварительно отсортируем массив по возрастанию, чтобы сначала шло раннее время (машину времени я еще не успел создать).

$k = 1;
if ($_SESSION["num_st"] == "" || $_SESSION["num_st"][$num_starts-1] < time()) {   // проверка, что в сессию не записаны данные и что эти данные не устарели.
    do {
        add_time2start($k);                                        
        $k++;
    } while ($k < = $num_starts);
    sort($time_to_start, SORT_NUMERIC);       
    $_SESSION["num_st"] = $time_to_start;
}


Теперь надо заставить скрипт работать, не обращая внимания на максимальное время выполнения, установленное сервером.
Принцип таков:
1) Определяем время начала работы скрипта;
2) Определяем установленное ограничение на время выполнения.
3) Запускаем цикл, внутри которого считаем текущее время и вычисляем общее время работы скрипта, сверяем текущее время со значениями в массиве времен запуска, и если совпадение есть, выполняем заданные действия (у меня они в файле exec.php). Для запуска файлов используем сокеты.
4) Повторяем цикл пока время работы скрипта не приблизится к максимально разрешенному. Я поставил — пока до максимального времени не останется 5 секунд.

Итак… считаем начальные данные по времени:

$start_time = microtime(); // Узнаем время запуска скрипта   
$start_array = explode(" ",$start_time); // Разделяем секунды и миллисекунды
$start_time = $start_array[1]; // получаем стартовое время скрипта
$max_exec = ini_get("max_execution_time"); //Получаем максимально возможное время работы скрипта

Собственно, цикл. Комментарии в коде.

do{
    $nowtime = time();  // Текущее время
    //// Если текущее время есть в массиве с временами выполнения скрипта......
    if (in_array($nowtime, $_SESSION["num_st"])) {
        // Сокетом цепляемся к файлу с основным содержанием действий
        $http = fsockopen('test.ru',80);
        /// заодно передаем ему данные сессии и время когда он должен сработать
        fputs($http, "GET http://test.ru/exec.php?".session_name()."=".session_id()."&nowtime=$nowtime HTTP/1.0\r\n"); 
        fputs($http, "Host: test.ru\r\n");
        fputs($http, "\r\n");
        fclose($http);
    } //// выполнили заданное действие
    // Узнаем текущее время чтобы проверить, дальше ли вести цикл или перезапустить
        $now_time = microtime();
        $now_array = explode(" ",$now_time);
        $now_time = $now_array[1];
        // вычитаем из текущего времени начальное начальное
        $exec_time = $now_time - $start_time+$exec_time;
        /// тормозимся на секунду
        usleep(1000000);
        //Остановка скрипта, работающего в фоновом режиме. Я другого способа не придумал.
        if (file_exists("stop.txt")) exit;
        //Проверяем время работы, если до конца работы скрипта
        //осталось менее 5 секунд, завершаем работу цикла. 
} while($exec_time < ($max_exec - 5));


Ну и, если разрешенное время подходит к концу, то завершаем цикл и благополучно запускаем этот же скрипт другие процессом (в 5 секунд точно уложимся)

// Запускаем этот же скрипт новым процессом и завершаем работу текущего
$http = fsockopen('test.ru',80);
fputs($http, "GET http://test.ru/index.php?".session_name()."=".session_id()."&bu=$max_exec HTTP/1.0\r\n");
fputs($http, "Host: test.ru\r\n");
fputs($http, "\r\n");
fclose($http); 


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

Когда дописал все, озадачился полезным применением…Использовать его можно как службу. Он может следить за чем-то в сети и уведомлять Вас, например, по почте. И не надо никаких cron'ов.

Скрипт можно еще оптимизировать — доработкой не занимался.
Кстати, вот от чего я не смог оторваться — браузер все же придется открыть, чтобы изначально запустить скрипт. +++