«Freelance watchdog» — дружим Perl и D-Bus
Здравствуйте! Я думаю многих фрилансеров, как и меня, заботит вопрос как не пропустить интересный проект и при этом не проводить часы например на сайте free-lance.ru обновляя список проектов и ожидая того самого, который заинтересует. В какой-то момент мне надоело тратить время впустую и я решил раз и навсегда покончить с подобным безделием. Быстро сделал простой парсер и добавил его вызов в Cron с периодичностью в минуту я принялся экспериментировать с наглядным выводом результатов. Перепробовав несколько вариантов информирования себя я узнал что утилита wall рассылающая сообщения на все открытые в данный момент консоли загадочным образом искажает русский текст в utf8, а любимый osd_cat отказывается выводить сообщения если его вызывать из скрипта запускаемого через cron, я остановился на выводе сообщений через KNotify, стандартную службу системных уведомлений в KDE использующую D-Bus — интерфейс межпроцессного взаимодействия являющийся частью проекта freedesktop.org, подробнее о котором можно прочитать тут: Статья на Википедии, Страница проекта на freedesktop.org. В данной статье я хочу остановиться именно на работе с D-Bus.
Достаточно быстро я нашёл в cpan модули, которые предоставляют интерфейс для работы с D-Bus, их оказалось достаточно много. Из всего разнообразия мне понадобилось два: Net::DBus который предоставляет доступ к базовым функциям и Net::DBus::Reactor, который позволяет повесить обработчик на события возвращаемые от KNotify. Для работы с D-Bus сначала необходимо определить адрес сокета, через который происходит общение c сервисом. Он присутствует в переменных окружения программ запускаемых из KDE, однако при старте скрипта из Cron оказалось что данная переменная отсутствует. Вот эта переменная:
$ echo $DBUS_SESSION_BUS_ADDRESS unix:abstract=/tmp/dbus-90YGBbKyPJ,guid=d4f4494988f1826aea58b612000000bd
Узнать её значение можно как минимум из двух мест, во первых она прописана в файле содержащем переменные окружения любого процесса, использующего D-Bus, например того-же самого KNotify:
$ ps aux | grep knotify 1000 13742 0.0 2.4 149464 50716 ? Sl Apr09 0:01 /usr/bin/knotify4 1000 16306 0.0 0.0 5228 708 pts/4 S+ 01:27 0:00 grep --colour=auto knotify $ grep -z DBUS_SESSION_BUS_ADDRESS /proc/13742/environ DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-90YGBbKyPJ,guid=d4f4494988f1826aea58b612000000bd
Ключ -z в данном случае нужен так как в файле environ записи разделены не символом переноса строки а ноль-байтом.
Во вторых то-же самое значение можно увидеть в файле сессии самого сервиса D-Bus, который лежит в директории .dbus/session-bus/ находящейся в домашней папке пользователя, из-под которого выполнен вход в KDE:
$ cat ~/.dbus/session-bus/e0828276ff34f7799cd0f6670000605a-0 # This file allows processes on the machine with id e0828276ff34f7799cd0f6670000605a using # display :0 to find the D-Bus session bus with the below address. # If the DBUS_SESSION_BUS_ADDRESS environment variable is set, it will # be used rather than this file. # See "man dbus-launch" for more details. DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-90YGBbKyPJ,guid=d4f4494988f1826aea58b612000000bd DBUS_SESSION_BUS_PID=13601 DBUS_SESSION_BUS_WINDOWID=4194305
Так как выполнять как минимум две системных команды для поиска адреса мне показалось много я решил обойтись одной командой, пойти вторым путём и читать это значение из файла:
if($ENV{DBUS_SESSION_BUS_ADDRESS} eq ""){ my $dbus = `grep "^DBUS_SESSION_BUS_ADDRESS" $homedir.dbus/session-bus/*`; $dbus =~m/DBUS_SESSION_BUS_ADDRESS=(.*)/; $ENV{DBUS_SESSION_BUS_ADDRESS}=$1; }
Далее необходимо подключиться к шине к отослать сообщение KNotify, которая должна отобразить его во всплывающем окне. К сожалению найти приверы общения с KNotify мне не удалось, поэтому чтобы определиться в каком виде и куда (с точки зрения D-Bus) отсылать сообщения, я воспользовался утилитой dbus-monitor, которая выводит на консоль все сообщения передаваемые по шине D-Bus, а также программой qdbusviewer, которая позволяет отсылать сообщения и видеть ответы на них. Итак, по порядку. Запускаю dbus-monitor и жду пока кто-то из моих контактов icq пришлёт мне сообщение, в этот момент KNotify отображает окошко, а dbus-monitor выдаёт лог обмена сообщениями между Kopete и KNotify, из которого меня заинтересовало следующее (Привожу сразу со своими #комментариями что для чего нужно):
... method call sender=:1.33 -> dest=org.kde.knotify serial=1994 path=/Notify; interface=org.kde.KNotify; member=event #Название сервиса, объекта и метода, описано ниже string "kopete_contact_incoming" #тип события string "kopete" #Название приложения вызывающего метод array [ #Сдесь передаются данные специфические для Kopete, у меня этот массив пустой variant string "contact" variant string "{c661d146-c5bd-4feb-983b-9e5900a66307}" variant string "group" variant string "10" ] string "Incoming message from <i>Zendo</i>" #Заголовок всплывающего окна string "+" #Текст в окне, можно использовать теги и вставлять картинки через тег array [ #Для чего этот массив я не знаю ] array [ #Тут мы указываем название кнопок в окне, если они нужны string "Просмотреть" string "Проигнорировать" ] int32 0 #Это и следующее число у меня равно 0 int64 46689228 ... ... ...<b>#Окошко закрылось при нажатии на кнопку</b> signal sender=:1.15 -> dest=(null destination) serial=1203 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=ActionInvoked uint32 99 string "1" signal sender=:1.13 -> dest=(null destination) serial=1473 path=/Notify; interface=org.kde.KNotify; member=notificationActivated #Ответ от KNotify При нажатии на кнопку int32 1015 int32 1 #Номер нажатой кнопки ... ... ... <b>#Окошко закрылось по таймауту</b> signal sender=:1.13 -> dest=(null destination) serial=1545 path=/Notify; interface=org.kde.KNotify; member=notificationClosed int32 1075
Итак, я узнал следующие интересующие меня параметры: название сервиса org.kde.knotify, объект непосредственно отвечающий за приём сообщений /Notify, и вызываемый метод этого объекта member=Notify, также получил пример данных передаваемых методу и пример возвращаемых значений при нажатии на кнопку «Просмотреть» в окне KNotify (Последние три строки из лога). Далее используя полученные данные и метод научного тыка я окончательно определил какие поля за что отвечают. В результате появился вот такой скриптик:
#!/usr/bin/perl use LWP::UserAgent; use utf8; use strict; use Net::DBus; use Net::DBus::Reactor; my $wfi = "(?:линукс|админ(?!к)|linux|rh|red\s?hat|centos|ubuntu|postfix|apache)"; my $url = "http://www.free-lance.ru"; my $ua = LWP::UserAgent->new(timeout => 30); my $response = $ua->request(HTTP::Request->new('GET', $url)); my $code = $response->code; my $homedir="/home/cherick/"; my $i; my @match_proj; my %proj_ignore; if($ENV{DBUS_SESSION_BUS_ADDRESS} eq ""){ my $dbus = `grep "^DBUS_SESSION_BUS_ADDRESS" $homedir.dbus/session-bus/*`; $dbus =~m/DBUS_SESSION_BUS_ADDRESS=(.*)/; $ENV{DBUS_SESSION_BUS_ADDRESS}=$1; } if($code == 200){ my $content = $response->decoded_content; open(IGNORE, "</home/cherick/scripts/fl_watchdog.ignore") or die "can't open fl_watchdog.ignore"; while (){ if(m/^([0-9]+)$/){ $proj_ignore{$1} = $1; } } close IGNORE; while ($content=~m/<div class="project.*href="\/projects\/\?pid=([0-9]+)">([^<]*$wfi[^<]*)/ig){ if($proj_ignore{$1} ne $1){push @match_proj, {content => $2, pid => $1};} } my $proj_count = $#match_proj; if($proj_count > 0){ my $bus = Net::DBus->session; my $myservice = $bus->export_service("org.kde.flwd"); my $service = $bus->get_service("org.kde.knotify"); my $object = $service->get_object("/Notify", "org.kde.KNotify"); $object->connect_to_signal('notificationActivated', sub{ my $t1 = shift; my $t2 = shift; open(IGNORE, ">>/home/cherick/test_scripts/fl_watchdog.ignore") or die "can't write to fl_watchdog.ignore"; for($i=0; $i<$proj_count; $i++){ print IGNORE "$match_proj[$i]{pid}\n"; } close IGNORE; if($t2 eq "1"){ for($i=0; $i<$proj_count; $i++){ qx"firefox <a href="http://www.free-lance.ru/projects/?pid=">www.free-lance.ru/projects/?pid=</a>$match_proj[$i]{pid}"; } } if($t2 eq "2"){ } exit(0); }); $object->connect_to_signal('notificationClosed', sub{ exit(0); }); my $proj_list = ''; for(my $i=0; $i<$proj_count; $i++){ $proj_list .= "$match_proj[$i]{content} "; } $object->event("warning", "kde", [], "Новые проекты:","$proj_list", [], ["Просмотреть", "Игнор"], 30*1000, 0); my $reactor = Net::DBus::Reactor->main(); $reactor->run(); exit(0); } }
При появлении интересных проектов выводится окно с двумя кнопками: «Просмотреть» и «Игнор». При нажатии кнопки «Просмотреть» скрипт открывает ссылки на проекты в Фаерфоксе, при нажатии «Игнор» просто выходим из скрипта. Так-же при нажатии на любую из кнопок в файл fl_watchdog.ignore заносятся PID найденых проектов и в следующий раз эти проекты игнорируются.
Также попутно посмотрев интерфейсы Kopete выяснил что в принципе через D-Bus можно отсылать сообщения, менять статусы и делать ещё много чего интересного в Kopete из своего скрипта. А мой Firefox 3.6.13 хоть и собран у меня с флагом dbus почему-то никакого интерфейса не имеет, что для меня остаётся загадкой.
Что можно почитать по теме:
Модули на cpan.org и примеры работы с ними
Пример работы с Pidgin через D-Bus
Вешаем свой скрипт на событие блокировки экрана