«Freelance watchdog» — дружим Perl и D-Bus

17 апреля 2011 г.

Здравствуйте! Я думаю многих фрилансеров, как и меня, заботит вопрос как не пропустить интересный проект и при этом не проводить часы например на сайте 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
Вешаем свой скрипт на событие блокировки экрана

Теги:
рубрика Linux, Программирование
  • Похожие статьи
  • Предыдущие из рубрики