Если вы не знаете, но хотите узнать, как сделать программу, которая постоянно висит в памяти и что-то нужное периодически выполняет, и при этом вы пишете на языке perl — попытаюсь рассказать об этом как можно более понятно.
Я сам люблю подробные инструкции, которые подразумевают, что человек, её читающий, не обязан знать всё, что было пропущено. Пусть лучше он сам пропустит то, что знает и так. Тем более, что о чём-то он может иметь ошибочное представление. Ну а чтобы не было скучно, совместим этот пример с ещё одной темой — icq, чтобы у нас получился icq-bot.
Для начала — о демонах.
Несмотря на развитие perl под win32 и даже появившуюся возможность эмуляции функции fork, я поведу речь о unix. Всё равно модуль, необходимый для общения с сервером icq, под win32 не заточен.
Проще говоря, в юниксе каждая исполнямая программа порождает процесс с определённым номером. Программа может запускать другие программы — в этом случае она становится родительской, а запущенные — дочерними. Если родительская программа прекращает работать — дочерние также прекращают работу.
В то же время, icq-bot должен работать постоянно, независимо от того, откуда его запустили, и прекратила ли работу запустившая этот процесс другая программа. Например, я залогинился на сервер по ssh, из при помощи bash запустил бота. После того, как я закрою bash и ssh-соединение, бот должен продолжать работу, как ни в чём не бывало.
Этой цели служит функция fork, которая запускает дочерний процесс, не связанный с тем терминалом, из которого запустили родительский. Следом за этим необходимо вызвать функцию setsid, которая завершает работу над обособлением нашего процесса.
После этих действий новорожденному демону, если он хочет быть хорошим демоном, необходимо сменить рабочую директорию на корневую — на тот случай, если он запустился с раздела, который был подключен через mount. Иначе раздел будет занят, и его невозможно будет отключить.
Кроме этого, поскольку ввод с терминала и вывод на терминал демоном производится не будут — необходимо перенаправить эти стандартные каналы в нуль. Если вы хотите отслеживать работу бота, то можно перенаправить вывод в лог-файл.
Самая хитрый этап — это использование функции fork. Она создаёт ещё один точно такой же процесс. Единственное отличие между ними — первый был родительским, и должен завершиться, а второй — дочерний, и должен остаться. Поэтому процессу необходима самоидентификация — кто я?
Она достигается проверкой результата, который возвращает функция fork. Если она вернула ненулевое значение — это pid нового дочернего процесса, который был запущен. Значит, я — родитель, и я ухожу. Если же pid нулевой — значит, я дочка, и я работаю дальше.
В результате, функция демонизации процесса выглядит следующим образом:
sub Daemonize {
return if ($^O eq 'MSWin32'); # под виндой мы не работаем, мы там играем
chdir '/' or die "Can't chdir to /: $!"; # не мешаем unmount
umask 0; # устанавливаем разрешения открываемых файлов по умолчанию
open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; # перенаправим ввод в нуль
open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!"; # перенаправим вывод в нуль
open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!"; # перенаправим вывод ошибок в нуль
defined(my $pid = fork) or die "Can't fork: $!"; # пытаемся выполнить fork
exit if $pid; # я родитель, если получен id дочернего процесса
setsid or die "Can't start a new session: $!"; # обосабливаемся
}
Обработка сообщений
ICQ-бот по идее должен отправлять какие-то сообщения и обрабатывать полученные. Сделаем простой вариант бота, который работает уведомителем — отправляет куда надо поступающие от системы сообщения, а на входящие сообщения просто выдаёт стандартный ответ. И для наглядности сделаем обработку одной команды — quit.
А дальше уже всё зависит от вашей фантазии — обрабатывать входящие сообщения можно как угодно, и тут можно напридумывать много интересного — от выдачи характеристик системы по команде до искусственного интеллекта.
Проще всего получать сообщения для рассылки из файлов. Нужно завести отдельную директорию для файлов с исходящими сообщениями. Программы, которым нужно отправлять сообщения, будут складывать их в эту папку.
Боту останется её периодически проверять и при обнаружении новых сообщений — отправлять их.
Сделаем так — в папке будут создаваться файлы вида nnn.rrr, где nnn — это icq UIN получателя, а rrr — это случайный номер, чтобы файлы не перезаписывались, если в один момент на один номер свалится несколько сообщений.
Итого — нам надо просканировать папку “входящие”, и для каждого файла отправить его содержимое на номер, указанный в имени файла.
sub CheckTasks {
# проверим задания
my($file, $path, $text, $size, $recipient);
$path="~username/icq/icq_tasks"; # директория с входящими
opendir DIR, $path;
for $file (grep /^\d+\.\d+$/, readdir DIR) { # файлы вида nnn.rrr
$file=~/^(\d+)\.\d+$/;
$recipient=$1; # цифры до точки - это UIN
$size=(stat("$path/$file"))[7];
if ($size>0 && $size<200) { # если размер не слишком велик и не слишком мал
$text=ReadFile("$path/$file"); # читаем содержимое
unlink("$path/$file"); # удаляем
$oscar -> send_im($recipient, $text); # отправляем. О том, кто такой oscar - попозже
}
}
}
Основная программа
Вспомогательные функции мы почти все сделали. Теперь сделаем основную программу.
Для взаимодействия с сервером ICQ я использую модуль Net::OSCAR. Это объектный модуль, поэтому нам нужно создать объект, который соединится с сервером, будет общаться с ним, и периодически пинать сервер, чтобы он не отсоединил нас. Кроме того, необходимо проверять статус соединения, и в случае разрыва соединяться заново. Ну и конечно, отправлять и принимать сообщения.
Поехали:
#!/usr/bin/perl -w
use Net::OSCAR; # подключаем модуль
use strict; # примерные программисты используют strict
use POSIX qw(setsid); # одна функция из модуля POSIX, необходимая для демонизации
Daemonize(); # демонизируемся
my($UIN, $PASSWORD, $oscar, $t);
$UIN='123456'; # UIN для бота
$PASSWORD='mypass'; # пароль для UIN
$oscar = Net::OSCAR -> new(); # создаём объект для общения с icq сервером
$oscar -> set_callback_im_in(\&send_answer);
# назначаем функцию для ответа на входящие сообщения - об этом ниже
$t=0; # переменная для отслеживания текущего времени
while (1) { # вечный цикл
if (!$oscar->is_on && (time()-$t)>120) { # Мы не в сети уже более 2 минут!
$oscar->signon($UIN, $PASSWORD); # Законнектимся снова
$t=time(); # Точка отсчёта
}
$oscar->do_one_loop(); # Эта внутренняя функция модуля пинает сервер
CheckTasks() if ($oscar->is_on); # Если мы в сети - обработаем исходящие сообщения
sleep(5); # Ждём 5 секунд, мы же не хотим слишком часто долбать сервер
}
Поясню подробнее некоторые пункты:
Модуль занимается обработкой событий, одним из которых является поступление входящего сообщения. Чтобы на такие сообщения бот мог посылать ответы, мы вешаем на это событие вызов функции send_answer():
$oscar -> set_callback_im_in(\&send_answer)
Она может выглядеть вот так:
sub send_answer() {
my($oscar, $sender, $msg) = @_;
# функции автоматически передаются три параметра -
# ссылка на объект, номер отправителя сообщения и само сообщение
if ($msg eq "quit") { # простой обработчик команд. команда одна - отбой
$oscar -> signoff(); # отсоединиться
exit(); # пока-пока
}
$oscar -> send_im($sender, 'Я бот, интеллекта не имею, общаться не могу.');
# ответ на любое другое сообщение
}
Итак, мы создаем обработчик входящих и занимаемся обработкой исходящих в бесконечном цикле. Чтобы проверять, в сети ли мы, используем флажок $oscar->is_on
А чтобы не долбить слишком часто запросами на коннект, используем таймер — в $t хранится время последней попытки соединения. В том числе, этот участок сработает и после запуска программы, для установления первого соединения с сервером.
Если мы в сети, периодически вызываем метод $oscar->do_one_loop(), который поддерживает наш бот в залогиненном состоянии.
И если мы в сети, проверяем папку входящих сообщений, и при необходимости отправляем их.
Компонуем, сохраняем, запускаем. Простейший icq-бот готов! Я использую такого бота для отсылки уведомлений мне с сервера.
А что теперь?
Из недостатков данного бота отмечу, что модуль Net::OSCAR не умеет отсылать сообщения пользователям, находящимся в офлайне.
В качестве домашнего задания предлагаю следующее:
— на четвёрочку — сделать так, чтобы при первом запуске бот коннектился к icq сразу, а не ждал 2 минуты
— на четвёрочку с плюсом — написать функцию ReadFile
— на пятерочку — разобраться в документации Net::OSCAR, научить бот определять, находится ли получатель сообщения в офлайне и не отправлять ему сообщения в офлайн.