Нажмите "Enter" для перехода к содержанию

PHP-DBUS

Предисловие

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)

Создавая этот биндинг, мы бы конечно могли упаковать это все в одну-две функции, и тогда бы оно было проще.

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