Предисловие
D-bus — это часть современных Linux, предназначеная для обмена сообщениями между процессами.
Она не такая уж и легкая даже на родном C, а уж чтобы перенести ее работу на PHP, понадобилось немало усилий.
Итак, базовые принципы работы d-bus:
Приложение, которое хочет управляться по d-bus, создает так называемую «шину» с произвольным, хотя по возможности читабельным именем, в котором размещаются так называемые «объекты«, в которых размещаются так называемые «интерфейсы«, уже в которых размещаются необходимые элементы управления.
Приложение, которое хочет управлять кем-то по d-bus, подключается к этой шине, делает вызов элемента управления с необходимыми (но не всегда) параметрами, и ожидает, хоть и не обязательно, ответ.
Популярных элементов управления, понимание которых поможет существовать в этом мире велосипедов и костылей, всего три:
Метод. Это элемент, работающий по принципу write-read, которому можно передать какой-либо параметр. Управляемое приложение должно будет отловить вызов этого метода, его параметры, обработать и выдать ответ, который прочитает запрашиваемое приложение.
Например, мы можем взять шину org.freedesktop.DBus, найти в ней объект /org/freedesktop/DBus, и в его интерфейсе org.freedesktop.DBus вызвать метод ListNames без параметров — и в ответ получим список всех зарегистрированных на данный момент шин в системе.
Свойство. Это элемент read-only. Программа может обратиться к другой программе, и получить какую-нибудь информацию практически в реальном времени.
Например рисунок ее иконки. Разумеется, если программа-владелец шины это поддерживает. Кстати чтение свойства происходит тоже при помощи метода. Дело в том, что у каждой шины считается хорошим тоном иметь объект /org/freedesktop/DBus с интерфейсом org.freedesktop.DBus и методом Get, обращением к которому с указанием имени свойства, позволяет просмотреть содержимое этого свойства. Если программа-владелец шины, этот метод не поддерживает — обратиться к ее свойствам будет невозможно.
Сигнал. Этот элемент write для владельца, и read для остальных. При помощи него, другие программы могут отловить изменения программы-владельца шины, если она захочет.
К примеру возьмем организацию системного трея на уровне операционной системы, и мессенджер Telegram. Менеджер системного трея, создает у себя на системной шине сигнал NewIcon, и мониторит на низком уровне изменения мессенджера. Если мессенджер поменял иконку, например когда пришло новое сообщение — менеджер системного трея изменяет сигнал NewIcon, и другие программы при помощи этого сигнала могут понять, что иконка изменилась. Альтернативный метод, в котором программа (или даже не одна) в бесконечном цикле попиксельно читала иконку Telegram и сравнивала ее на изменения — затребовал бы больше вычислительных ресурсов процессора, нежели чтение простого true\false.
По сути, сигнал можно сравнить с таким себе файлом, содержимое которого бесконечно читается в цикле. И только кто-то запишет в файл «1» — это изменение тут же увидит тот, кто этот файл читает.
- Важно: элементы управления не являются автоматическими, и гарантированно функционирующими. За них отвечает программа, зарегистрировавшая себя в d-bus. Если эта программа по каким-то причинам не поддерживает элемент управления, или не сможет ответить (вовремя) на запрос — запрашивающая программа может зависнуть. Для удобства понимания, мы будем рассматривать примеры с использованием бесконечного цикла, однако в реальных условиях вам нужно предусматривать выход из цикла по тайм-ауту и обработку возможных ошибок.
А теперь к практике
1. Мониторинг сигнала при помощи PHP-DBUS
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->add_match("type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged'");
while(true) {
usleep(25000);
if (($dbus->connection_pop_message())and($dbus->message_is_signal("org.freedesktop.DBus","NameOwnerChanged"))) {
echo "Name owners changed \n";
break;
}
}
$dbus->message_unref(DBUS_INCOMING);
$dbus = new Ldbus(DBUS_BUS_SESSION); — устанавливает подключение к сессионной шине
$dbus->add_match(«type=’signal’,interface=’org.freedesktop.DBus’,member=’NameOwnerChanged'»); — добавляет фильтр для сигналов, чтобы реагировать на сигналы только одного типа. Если нужно реагировать на сигналы нескольких типов — их следует добавить по такому же принципу еще одной строкой.
Далее в бесконечном цикле мы проверяем получение нового сообщения на шине, и если это сообщение — наш желаемый сигнал, то реагируем.
2. Чтение какого-нибудь свойства
Как уже указывалось выше, чтобы прочитать свойство, нам нужно дернуть метод Get, с указанием информации о том, какое свойство мы хотим прочитать. Давайте дернем.
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->message_new_method_call(":1.41","/StatusNotifierItem","org.freedesktop.DBus.Properties","Get");
$dbus->message_append_args(DBUS_TYPE_STRING,"org.kde.StatusNotifierItem");
$dbus->message_append_args(DBUS_TYPE_STRING,"Title");
$dbus->connection_send();
usleep(25000);
while(true) {
usleep(25000);
if ($dbus->connection_pop_message()) {
$message = $dbus->message_get_all();
$array = json_decode($message, true);
print_r($array);
}
}
Вывод будет примерно следующий.
Array
(
[message_type] => 2
[path] =>
[interface] =>
[member] =>
[arguments] => Array
(
[0] => Array
(
[type] => variant
[value] => ViberPC
)
)
)
Соответственно, чтобы получить готовое значение свойства «Title», нам нужно вычленить его из массива. Тогда мы
print_r($array);
заменяем на
if ($array["message_type"]==2) {
$title = $array["arguments"][0]["value"];
}
Если нам нужно прочитать сразу несколько свойств одного и того же интерфейса — нет резона несколько раз дергать шину, а лучше воспользоваться встроенным методом GetAll. И тогда код будет выглядеть вот так:
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->message_new_method_call(":1.41","/StatusNotifierItem","org.freedesktop.DBus.Properties","GetAll");
$dbus->message_append_args(DBUS_TYPE_STRING,"org.kde.StatusNotifierItem");
$dbus->connection_send();
usleep(25000);
while(true) {
usleep(25000);
if ($dbus->connection_pop_message()) {
$message = $dbus->message_get_all();
$array = json_decode($message, true);
print_r($array);
}
}
В результате, в массиве окажутся все доступные свойства, к которым можно будет обратиться по имени\уровню многомерного ассоциативного массива.
3. Вызов метода
Здесь все просто, по аналогии с предыдущим пунктом.
В этом примере мы будем вызывать метод ContextMenu интерфейса org.kde.StatusNotifierItem, которому передадим два числовых параметра.
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->message_new_method_call(":1.447","/StatusNotifierItem","org.kde.StatusNotifierItem","ContextMenu");
$dbus->message_append_args(DBUS_TYPE_INT32,320);
$dbus->message_append_args(DBUS_TYPE_INT32,240);
$dbus->connection_send();
usleep(25000);
while(true) {
usleep(25000);
if ($dbus->connection_pop_message()) {
$message = $dbus->message_get_all();
$array = json_decode($message, true);
print_r($array);
}
}
Данный метод не подразумевает ответа, поэтому в $array будет лежать только сервисная информация.
Отдельным подпунктом стоит выделить типы данных d-bus.
- Как по мне, это большая глупость, потому что в готовом коде, и запросы и ответы все равно приходится переводить из типов d-bus в типы переменных, соответствующих языку программирования. Тогда было бы логичнее просто все хранить и передавать в строковой переменной, которую можно разбирать и приводить к нужным типам, средствами самой программы. Но работаем с тем, что есть.
Типы данных описываются в виде готовых констант, хотя на самом деле имеют обыкновенное числовое значение. На данный момент реализация библиотеки PHP-DBUS поддерживает несколько констант, однако за неимением константы, вы можете свободно пользоваться ее числовым аналогом, взятым из https://dbus.freedesktop.org/doc/dbus-specification.html
DBUS_TYPE_STRING (115)
DBUS_TYPE_ARRAY (97)
DBUS_TYPE_UINT32 (117)
DBUS_TYPE_INT32 (105)
DBUS_TYPE_BOOLEAN (98)
DBUS_TYPE_BYTE (121)
То есть использовать в коде PHP вы можете либо готовую константу, либо ее числовой эквивалент, взятый из скобок в предыдущем описании, или найденный по ссылке выше.
4. Интроспекция
Номинально, интроспекция не относится к элементам управления, однако на ней стоит остановиться отдельно.
Если вкратце — это набор данных в формате XML, который описывает все возможные методы, сигналы, названия, интерфейсы и объекты в пределах шины. Когда вы открываете такую шину просмотрщиком d-bus, и ему нужно вывести структуру шины — он запрашивает метод, называемый Introspect, находящийся по определенному пути, и программа ответственная за такую шину — должна просто вывести в ответ эти XML данные в формате DBUS_TYPE_STRING.
В этом пункте мы рассмотрим получение таких данных.
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->message_new_method_call("org.freedesktop.DBus","/","org.freedesktop.DBus.Introspectable","Introspect");
$dbus->connection_send();
usleep(25000);
while(true) {
usleep(25000);
if ($dbus->connection_pop_message()) {
$message = $dbus->message_get_all();
echo "$message\n";
}
}
В целом, данный пример должен у вас сработать, поскольку шина «org.freedesktop.DBus» — стандартная.
Если вам нужно запросить интроспекцию у другой программы — нужно будет поменять только имя шины, поскольку объект и интерфейс в случае интроспекции — стандартные.
Для чего это нужно?
К примеру, вы не знаете точное имя шины, но знаете название объекта. Тогда вы можете вызвать метод ListNames, он вам вернет список активных на данный момент шин. Затем вы пройдетесь по этому списку в цикле, вызовете для каждой шины интроспекцию, и поищете в полученных данных название вашего объекта при помощи простейшей substr_count($introspect_data, $searching_object_name).
Или например, вы можете узнать существует ли метод\свойство, прежде чем его вызывать — проверка существования, а потом запрос — займут меньше времени и процессорных ресурсов, чем запрос и сообщение об ошибке спустя некоторое время.
5. Получение всех шин, зарегистрированных на данный момент в d-bus.
#!/usr/local/supreme/php/bin/php
<?php
$dbus = new Ldbus(DBUS_BUS_SESSION);
$dbus->message_new_method_call("org.freedesktop.DBus","/","org.freedesktop.DBus","ListNames");
$dbus->connection_send();
usleep(25000);
while(true) {
usleep(25000);
if ($dbus->connection_pop_message()) {
$message = $dbus->message_get_all();
echo "$message\n";
}
}
Вывод будет примерно таким
{"message_type":4,"path":"/org/freedesktop/DBus","interface":"org.freedesktop.DBus","member":"NameAcquired","arguments":[":1.584"]}
{"message_type":2,"path":"","interface":"","member":"","arguments":[["org.freedesktop.DBus",":1.7","org.freedesktop.Notifications",":1.8",":1.9","org.freedesktop.portal.Desktop","org.freedesktop.systemd1","org.gtk.vfs.Daemon","org.pulseaudio.Server","org.freedesktop.impl.portal.desktop.gtk",":1.490","org.a11y.Bus",":1.21",":1.43","org.supreme.Session","org.gnome.keyring","org.gnome.dfeet",":1.25",":1.48",":1.26",":1.27","org.mozilla.firefox.ZGVmYXVsdC1lc3I_",":1.28",":1.29","org.freedesktop.portal.Documents","ca.desrt.dconf",":1.578",":1.479",":1.579","org.PulseAudio1","org.mpris.MediaPlayer2.chromium.instance1229",":1.30",":1.480",":1.31",":1.54",":1.10","org.freedesktop.impl.portal.PermissionStore",":1.32",":1.11",":1.56",":1.12",":1.34",":1.13",":1.35",":1.584",":1.1",":1.14",":1.36",":1.486",":1.2",":1.15","org.freedesktop.secrets",":1.37",":1.487",":1.4",":1.17","org.telegram.desktop._07479a6668c8ab51dff15ca3eea5ef9e",":1.5",":1.18",":1.6",":1.19"]]}
6. Пример создания собственного интерфейса + обработка собственного метода.
#!/usr/local/supreme/php/bin/php
<?php
// Описание нашего интерфейса
$introspection_xml = "
<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"
\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">
<node>
<interface name=\"com.example.MethodCallExample\">
<method name=\"SayHello\">
<arg direction=\"out\" type=\"s\" name=\"response\"/>
</method>
</interface>
<interface name=\"org.freedesktop.DBus.Introspectable\">
<method name=\"Introspect\">
<arg direction=\"out\" type=\"s\"/>
</method>
</interface>
</node>
";
// Инициализируем новое подключение d-bus
$dbus = new Ldbus(DBUS_BUS_SESSION);
// Регистрируем шину
$dbus->bus_request_name("org.Supreme.test");
usleep(25000);
while(true) {
usleep(25000);
// Если мы получили сообщение на шине, и оно то которое мы ловим - значит тру
if ($dbus->connection_pop_message()) {
// В $message у нас будет лежать JSON ответа.
$message = json_decode($dbus->message_get_all(),true);
// Хотя в примерах рекомендуется вызывать функцию message_is_method_call для проверки, метод ли это - мы можем парсить непосредственно тело сообщения, чтобы это понять
if (($message["message_type"]==1) and ($message["interface"]=="org.freedesktop.DBus.Introspectable") and ($message["member"]=="Introspect")) {
// Функция формирования ответа на метод
$dbus->message_new_method_return();
// Ответом на метод будет текстовая строка с содержимым интроспекции
$dbus->message_append_args(DBUS_TYPE_STRING,$introspection_xml);
// Шлем сообщение в d-bus
$dbus->connection_send();
}
}
}
Здесь в пример были добавлены комментарии, остается лишь объяснить, как оно работает.
Разберем на примере интроспекции. Интроспекция, как уже говорилось выше — это специальный способ взаимодействия между программами на шине d-bus, позволяющий получать информацию о доступных элементах управления.
Краткая суть интроспекции в том, что программа (например qdbusviewer, или d-feet), желающая получить информацию об интерфейсе объекта — вызывает определенный метод, на который владелец объекта должен ответить текстом разметки своего объекта в формате XML. Программа этот ответ разбирает, и выводит данные в удобном человеко-читаемом формате.
- Важно: хотя обработка этого метода и является крайне желательной — метод не является обязательным. Программа-владелец объекта может обрабатывать сообщения самостоятельно, а программа желающая взаимодействовать с владельцем — может использовать заведомо известные методы и сигналы. Однако при попытке просмотреть общую информацию о таком объекте — программа выдаст ошибку, либо зависнет на время тайм-аута.
Итак, как устроена обработка методов.
Сперва мы создаем шину. После этого мы висим в цикле, и отлавливаем пришедшие нам сообщения.
Как только мы получили сообщение — мы разбираем его на предмет вызова метода в целом, и нашего метода в частности.
Если это наш метод — то формируем ответ, в данном случае это текстовая строка с XML-содержимым, описывающая наш интерфейс. Ну и отправляем в шину.
Все. Программа, запросившая метод — получит ответ. Выглядеть это будет вот так:

Полное описание функций библиотеки
bus_request_name(string $bus_name)
Регистрирует новую шину в системе d-bus. Не забывайте, шина существует до тех пор, пока существует программа ее создавшая.
add_match(string $rule)
Добавляет фильтр в систему чтения сообщений. Можно добавлять несколько фильтр, при помощи нескольких вызовов этой функции.
$rule — должно соответствовать описанию из https://dbus.freedesktop.org/doc/dbus-specification.html, но типичный пример применения:
type=’signal’,interface=’org.freedesktop.DBus’,member=’NameOwnerChanged’, где type — фильтруемый элемент управления, interface — интерфейс, где этот элемент управления присутствует, а member — название элемента управления.
remove_match(string $rule)
Удаляет фильтр из системы.
$rule — должно точно соответствовать добавленному ранее, иначе проигнорируется. Поэтому есть смысл сохранять вводимые правила в переменную, при помощи которой сможете эти правила удалить впоследствии.
connection_pop_message()
Проверяет, есть ли на данное время сообщение в системе d-bus.
Чтобы наверняка отловить пришедшее сообщение, эту функцию нужно запускать в цикле с очень небольшой задержкой, любой лаг может привести к тому, что сообщение будет пропущено.
connection_send()
Шлет сформированный специальным образом запрос на шину d-bus
bool message_is_signal()
Проверяет, является ли сообщение присутствующее в данный момент в системе — сигналом
message_unref(int type)
Очищает сообщение.
Из-за ограничения языка PHP, мы не можем обращаться к этим объектам напрямую из PHP, поэтому храним их в памяти класса. После того, как сообщение было сформировано или получено, оно остается в памяти. Если его не очистить, то при следующей проверке сообщения, оно будет присутствовать даже если фактически не было получено.
Для удобства использования, сообщения были разделены на два вида — отправляемые и получаемые. Отправляемые сообщения мы формируем перед вызовом элементов, как правило при помощи функции message_append_args. Получаемые сообщения у нас формируются в ответе, при помощи функции message_get_all.
type может принимать DBUS_INCOMING или DBUS_OUTGOING в зависимости от того, какие сообщения мы хотим очистить.
message_new_method_call(string $bus, string $object, string $interface, string $method)
Формирует запрос к методу.
Обратите внимание, данная функция не вызывает метод, а лишь формирует его. Вызов происходит функцией connection_send
string message_get_all()
Возвращает доступное в системе сообщение в формате JSON.
message_append_args(int type, void value)
Формирует аргумент перед вызовом метода.
В зависимости от типа, value может быть как string, так и int
Возможные константы, были описаны выше.
message_get_sender()
Возвращает отправителя сигнала.
bool message_is_method_call(string interface, string method)
Возвращает true, если сообщение, полученное после connection_pop_message — запрос метода
message_new_method_return()
Подготавливает сообщения к ответу на метод
Стоит немного разъяснить методику программирования под d-bus. Она не проста.
Работа с шиной и ее элементами управления — не ограничены специфическими функциями, типа «вызвать метод X с параметрами Y». Вместо этого, в программирование ввели несколько ключевых концепций.
Наверное одна из самых главных концепций — это сообщение, или message.
Фактически, это те самые параметры, только применяемые не в контексте функции, а формируемые до, или разбираемые после. Тип этих параметров в недрах d-bus, не текстовый, и не цифровой, а специальным образом подготовленный.
Вот например, если в классических случаях программирования, мы пишем нечто вроде send("Message")
, то в случае с d-bus, мы должны сначала запаковать нашу строку в сообщение, и уже потом его отправить
message = function_to_create_message(STRING_MESSAGE, "Message")
send(message)
Создавая этот биндинг, мы бы конечно могли упаковать это все в одну-две функции, и тогда бы оно было проще.
Но мы постарались соответствовать программированию на языке оригинала — С, и хотя конечный код получается сложнее — он же получается и гибче.