Lua*

Разработка → Awesome WM и Dbus

Faiver_bes 1 сентября 2014 в 19:41 10,3k
Я думаю ни для кого не будет секретом, что у Awesome есть «узкое место», если мы запускаем внешний скрипт, который например должен считать данные из файла, или интернета и вернуть результат в виджет или саму систему, то мы периодически можем наблюдать явлениие «фриза», т.е. когда система перестает реагировать на нажатия клавиш и мыши до получения результата обработки (правда активный клиент при этом продолжает работать). Чаще всего это происходит при использовании io.popen или awful.util.pread


У меня такая ситуация случалась не однократно, например, при прослушивании музыки в moc/mocp при смене трека у меня вызывается внешний скрипт который получает данные о треке и загружает обложку альбома, если она есть и отображает их. Но периодически (т.к. скрипт тестировался на ноутбуке) система «зависала». Долго не мог понять почему же это происходит, а затем выяснил, что если в этот момент диск сильно нагружен чем нибудь, то данные на чтение ставятся в очередь и в результате, т.к. вывод результата зависит от ответа, система «подвисала». Проблему удалось частично решить через «костыли» в виде вешнего скрипта, который через 'echo $result | awesome-client -' пересылал данные.

Или другой вариант, есть виджеты отображающие свободное место на диске (например раздела /), работающие чаще всего через 'df -h', но если у нас есть на диске раздел ntfs, то периодически он может «отваливаться», и в этот момент система перестает реагировать. Одним из решений является сохранение вывода команды в файл, а затем его чтение оттуда. Но опять же это тот еще «костыль». Согласитесь, очень неприятно, когда для простейших действий приходится изобретать костыли.

А тем временем, в Awesome, существует такая замечательная вещь как DBus, которая позволяет осуществлять взаимодействие различных компонентов системы и приложений.

==Что такое DBus==
D-Bus — это система межпроцессного взаимодействия, которая предоставляет приложениям несколько шин для передачи сообщений. Она обеспечивает бес проблемную связь десктопных приложений между собой и связь между десктопными приложениями и системными сервисами. Поддерживается не только широковещательная рассылка сообщений (сигналов), но и удалённый вызов методов.

Т.е. возможно из Awesome, внешнего приложения или вашего виджета послать запрос на обработку каких либо данных, а получить результат обработки через шину DBus, и наконец вызвать функцию обработчик этого события. И при этом никаких «зависаний», ведь мы не заставляем Awesome ожидать результат.

Для работы с Dbus можно использовать стандартные утилиты 'dbus-send' — для отправки сигналов в приложения из скриптов или оболочки, и 'dbus-monitor' — которая позволяет отслеживать все сигналы посылаемые между приложениями и/или системой. Если же вы хотите получить более полные данные о том какие приложения зарегистрированы в dbus, какие методы они могут вам предоставить, можно воспользоваться сторонней утилитой из комплекта KDE 'qdbus' — это консольная утилита имеющая минимальные зависимости и занимающая чуть менее 1Мб.

Если вы запустите dbus-monitor, то будете отслеживать все сигналы пересылаемые приложениями и системой. Но если вас интересует какой либо конкретный сигнал, то можно отфильтровать вывод:
dbus-monitor "interface='ru.gentoo.kbdd' "

В данном случае мы будем получать сигналы о смене раскладки клавиатуры посылаемые kbdd. За более подробными сведениями обращайтесь LOR. В принципе dbus-monitor удобно использовать для отладки ваших скриптов.

Но нас то интересует возможность взаимодействия наших скриптов и Awesome.

Для посылки сигнала в Awesome вы можете использовать следующий код:
dbus-send --session --dest=org.naquadah.awesome.awful /ru/console/mocp ru.console.mocp.songChanged

Разберем, что здесь и к чему.
--session — указывает на то что используется сессионная (а не системная) шина передачи данных. Т.е. сессионная шина это пользовательская шина, к которой подключаются запущенные от имени пользователя приложения, в то время как системная шина чаще всего не имеет своего пользователя (сервисы HAL, сетевой стек, bluetooth и т.д.)
--dest=org.naquadah.awesome.awful — здесь мы указываем кто будет являться получателем нашего сигнала, в данном случае это Awesome
/ru/console/mocp — уникальное имя объекта (обычно имя сервиса, путь к объекту и интерфейс), в нашем случае создаем его сами.
ru.console.mocp.songChanged — используемый метод, по сути вызываемая функция которая и порождает сигнал, в случае если используется приложение.

Также аналогично можно использовать и посылку сигналов из Awesome различным приложениям, например, чтобы переключить трек в различных плеерах, поменять статус в Pidgin и т.д. При этом преимуществом данного способа будет то, что не требуется запускать копию терминала, и передавать ему команду для обработки, что пусть и не сильно, но нагружает ресурсы системы, на создание терминала, обработку команды в нем, а потом и уничтожение этого терминала. Об этом способе и примерах поговорим чуть позже.

Как видите все достаточно просто. Но ведь этого недостаточно. Одно дело послать сообщение, что отработала такая то функция, а другое передать еще и результат в сигнале. Такая возможность есть, вы можете через Dbus сигналы передавать данные для ваших виджетов или функций.
Например тот же kbdd передает помимо самого сигнала еще и выбранную раскладку ( в виде числа, а в другой функции и название раскладки, поэкспериментируйте). Если же вы используете другой менеджер раскладки, то и там та же ситуация. Наиболее тяжелый сигнал в KDE, там передается очень много информации, в том числе и двоичной.

Поддерживаемые типы данных: string, byte, boolean, int16, uint16, int32, uint32, int64, uint64, dooble, object_path

==Практическая реализация==
===Работа с файловой системой===
Все изменения мы будем вносить только в rc.lua.

Сначала создадим виджет который будет отображать информацию:
--Awesome 3.4
fs_root = widget({type = "textbox"})
fs_root.text = "Занято:"
--Awesome 3.5
fs_root = wibox.widget.textbox()
fs_root:set_text("Занято:")

Затем создадим таймер, который будет запрашивать данные:
fs_timer = timer ({timeout = 600}) --раз в 10 минут
fs_timer:add_singal ("timeout", function () awful.util.spawn_with_shell("dbus-send --session --dest=org.naquadah.awesome.awful /ru/console/df ru.console.df.fsValue string:$(df -h --output='pcent' /home | sed '1d;s/ //g' )" ) end )
fs_timer:start()

если вы используете Awesome 3.5, то просто замените add_singal на connect_signal

И обновляем значение, при получении сигнала:
dbus.request_name("session", "ru.console.df")
dbus.add_match("session", "interface='ru.console.df', member='fsValue' " )
dbus.add_singal("ru.console.df", function (...)
      local data = {...}
      local dbustext = data[2]
      fs_root.text = "Занято: " .. dbustext     --для 3.5 fs_root:set_text("Занято:" .. dbustext)
   end )

Все, перезапускаем Awesome!

Преимуществом данного способа будет то, что Awesome не будет ждать (и соответственно висеть) пока обрабатывается вызванный код.

В случае если вы вызываете одну и ту же команду с разными параметрами, можно вернуть вторым значением этот параметр, и соответственно в самом Awesome его проверить и вызывать нужный обработчик. На нашем примере чуть чуть модифицируем функцию таймера:
path = '/home'
fs_timer:add_singal ("timeout", function () awful.util.spawn_with_shell("dbus-send --session --dest=org.naquadah.awesome.awful /ru/console/df ru.console.df.fsValue string:$(df -h --output='pcent' " ..path.. " | sed '1d;s/ //g' )" string:"..path) end )

dbus.request_name("session", "ru.console.df")
dbus.add_match("session", "interface='ru.console.df', member='fsValue' " )
dbus.add_singal("ru.console.df", function (...)
      local data = {...}
      local dbustext = data[2]
      local dbuspath = data[3] 
      if dbustext == '/' then
        fs_root.text = "Занято: " .. dbustext          --для 3.5 fs_root:set_text("Занято:" .. dbustext)
      elseif dbustext == '/home' then
        fs_home.text = "Занято: " .. dbustext        --для 3.5 fs_home:set_text("Занято:" .. dbustext) 
      end
   end )


===Взаимодействие с mocp===
К сожалению сам mocp не поддерживает dbus, но он может вызывать внешнюю команду при смене трека (и не только, за подробностями к документации).

В конфиге для mocp я добавил свой обработчик:
OnSongChange = "/home/user/script/changesong.sh %f %a %t %d %r %n"

Здесь мы передаем все необходимые значения: путь к файлу, исполнитель, название, время, альбом, для того, чтобы потом не дергать mocp еще раз, чтобы получить эти данные, как указано в изначальных версиях этих скриптов.

Затем, создаем скрипт (changesong.sh) для получения обложки и формирования текста:
#!/bin/bash
# changesong.sh

#файл с обложкой по умолчанию
DEFAULT_COVER="/home/user/Images/no-cover.jpg"

[ -n "$1" ] && FULLDIR=`dirname "$1"`

[ -n "$FULLDIR" ] && COVERS=`ls "$FULLDIR" | grep "\.jpg\|\.png\|\.gif"`

if [ -z "$COVERS" ]; then
	COVERS="$DEFAULT_COVER"
else
	TRYCOVERS=`echo "$COVERS" | grep -i "cover\|front\|folder\|albumart" | head -n 1`

	if [ -z "$TRYCOVERS" ]; then
		TRYCOVERS=`echo "$COVERS" | head -n 1`
		if [ -z "$TRYCOVERS" ]; then
			TRYCOVERS="$DEFAULT_COVER"
		else
			TRYCOVERS="$FULLDIR/$TRYCOVERS"
		fi
	else
		TRYCOVERS="$FULLDIR/$TRYCOVERS"
	fi

	COVERS="$TRYCOVERS"
fi

MTITLE= "

	Исполнитель:	$2
	Название:	$3
	Альбом: 	$5
	Трек:	$6
	Время:  	$4"

dbus-send --session --dest=org.naquadah.awesome.awful /ru/console/mocp ru.console.mocp.songChanged \
		  string:"$MTITLE" \
		  string:"$COVERS"
#обязательно помещаем переменную в кавычки, т.к. иначе некорректно передается строка (особенность bash)

Даем скрипту права на исполнение:
chmod +x changesong.sh

Добавляем обработчик в Awesome:
dbus.request_name("session", "ru.console.mocp"
dbus.add_match("session", "interface='ru.console.mocp', member='songChanged' ")
dbus.add_signal("ru.console.mocp", function(...)
    local data = {...}
    coverart_nf = naughty.notify({icon = data[3], icon_size = 100, text = data[2], position = "bottom_left"})
    end )

Хотя результат и будет тем же самым, что и в изначальном варианте, но разница будет в том, что система не зависнет если жесткий диск будет занят, плюс мы используем один скрипт вместо 2х в первоначальной версии. Также в скрипте мы не производим проверку на состояние mocp (переключение состояния пауза/воспроизведения) и запущенно ли вообще приложение, если вам это необходимо, добавьте соотвествующий код.

==Посылка сигнала из Awesome==
Стандартный Awesome к сожалению не имеет функции для отправки сигналов dbus, по крайней мере на wiki нет ни слова об этой возможности. Поэтому приходится использовать отправку сигналов через shell, например это можно сделать следующим образом (на примере переключателя клавиатуры kbdd):
--виджет клавиатуры
kbdwidget = widget({type = "textbox", name = "kbdwidget"})
kbdwidget.border_color = beautiful.fg_normal
kbdwidget.border_width = 1
kbdwidget.text = '<span color="#F8EC5D"><b> Eng </b></span>'
next_layout=1
function changeKeyboardLayout(keyboard_layout)
    awful.util.spawn( "dbus-send --type=method_call --session --dest=ru.gentoo.KbddService /ru/gentoo/KbddService ru.gentoo.kbdd.set_layout uint32:".. keyboard_layout )
end
dbus.request_name("session", "ru.gentoo.kbdd") 
dbus.add_match("session", "interface='ru.gentoo.kbdd',member='layoutChanged'") 
dbus.add_signal("ru.gentoo.kbdd", function(...) 
	    local data = {...} 
		local layout = data[2] 
		lts = {[0] = '<span color="#F8EC5D"><b> Eng </b></span>', [1] = '<span color="#FF3000"><b> Рус </b></span>'} 
		 kbdwidget.text = " "..lts[layout].." " 
		 if layout == 1
		     then next_layout = 0
		 else
		     next_layout = 1
		end 
	end
				) 
kbdwidget:buttons(awful.util.table.join(awful.button({}, 1, function ()
                changeKeyboardLayout(next_layout)
        end)))

Здесь мы через нажатие на виджет левой кнопкой мыши меняем раскладку клавиатуры, посылая сообщение об этом через dbus. Также необходимо добавить в автозагрузку сам kbdd, иначе ничего не будет работать. Кстати если у вас не работают виджеты в русской раскладке(например после изменения раскладки на русскую при нажатии на виджет не меняется раскладка на английскую) прочтите статью известные проблемы.

==Поиск нужных сигналов приложений==
Большинством приложений можно напрямую управлять через dbus, т.е. можно переключать треки, переводить приложения в полноэкранный режим, менять статусы и т.д. Для получения списка всех возможных сигналов и методов запустите приложение (без этого не произойдет регистрации доступных событий), после чего запустите qdbus, найдите нужный интерфейс, например:
qdbus | grep clementine
Получим следующий вывод:
 org.mpris.MediaPlayer2.clementine
 org.mpris.clementine
Затем, запустим:
qdbus org.mpris.clementine 
Получим следующиее:
/
/Player
/TrackList
/org
/org/mpris
/org/mpris/MediaPlayer2
А затем вызовем:
qdbus org.mpris.clementine /Player

И получим все возможные методы и сигналы для данного приложения. Например, нас интересует переключение на следующий трек, метод для этого выглядит следующим образом:
method void org.freedesktop.MediaPlayer.Next()

В данном случае метод не требует каких либо параметров, поэтому просто вызываем его:
dbus-send --type=method_call --session --dest=org.mpris.clementine /Player org.freedesktop.MediaPlayer.Next

Собственно, все. Дальше экспериментируйте и ищите сами.

==Отслеживание сигналов из скриптов==

Если вы хотите выполнить более сложные задания, чем вызов отдельных методов, то вы можете написать скрипт командной оболочки, содержащий dbus-send команды, или используйте язык более высокого уровня, для упрощения задачи. Существуют D-Bus привязки для Python, Ruby и Java языков.

В следующем примере, будет показан скрипт на Python, который меняет статус в Pidgin на “Away from keyboard”, при активизации скринсейвера. Здесь имеются два аспекта D-Bus: скрипт ждет сигнала от скринсейвера, и затем он вызывает метод в Pidgin.

Сразу оговорюсь, скрипт не мой, ссылка на оригинал приведена ниже, но не описать эту возможность взаимодействия я просто не мог.
pidgin_screensaver.py
#!/usr/bin/env python def pidgin_status_func(state): 
obj = bus.get_object("im.pidgin.purple.PurpleService", 
"/im/pidgin/purple/PurpleObject") 
pidgin = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface") 
status = pidgin.PurpleSavedstatusFind("afk") 
if status == 0: 
status = pidgin.PurpleSavedstatusNew("afk", 5) 
if state: 
pidgin.PurpleSavedstatusSetMessage(status, 
"Away from keyboard") 
pidgin.PurpleSavedstatusActivate(status) 

import dbus, gobject 
from dbus.mainloop.glib import DBusGMainLoop 

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 
bus = dbus.SessionBus() 

bus.add_signal_receiver(pidgin_status_func, 
dbus_interface="org.gnome.ScreenSaver", 
signal_name="ActiveChanged") 

loop = gobject.MainLoop() 
loop.run() 

Давайте разберем этот скрипт. Функция pidgin_status_func устанавливает ваш статус в Pidgin. Она получает объект im/pidgin/purple/PurpleObject и интерфейс im.pidgin.purple.PurpleInterface из сессионной шины. Далее, вызывается метод интерфейса. Он создает новый “saved status” тип, после проверки существования типа статус с именем “afk” (“afk” означает “Away From Keyboard”, и 5 — это вид “away” статуса).

Далее функция проверяет переменную state, которая является аргументом функции pidgin_status_func (я объясню, что означает этот аргумент далее). Если аргумент правдив, то сообщению нового статуса “afk” присваивается значение “Away from keyboard”, и статус активируется. В результате Pidgin показывает ваш статус как “afk", с сообщением “Away from keyboard”.

Теперь мы должны вызвать эту функцию вместе с активизацией скринсейвера. Поэтому, запускаем dbus.mainloop и соединяемся к сессионной шине. Далее добавляем приемник сигнала, который слушает сигнал ActiveChanged от интерфейса org.gnome.ScreenSaver. Если/когда сигнал срабатывает, он вызывает функцию pidgin_status_func. Так как сигнал ActiveChanged имеет булев аргумент, обозначающий текущее состояние заставки (1 — активная, 0 — не активная), то мы используем только один аргумент (state) в функции pidgin_status_func. Для постоянного прослушивания запускаем бесконечный цикл, работающий пока работает скрипт.

Вообще, у многих приложений есть интерфейс dbus, поэтому возможности по их управлению очень обширны, и ограничены только Вашей фантазией и желанием!

==Ссылки по теме==
Введение в DBus OpenNET
D-Bus Tutorial
LOR
Управление Linux десктопом через D-Bus

Update 1. Небольшое обновление: ru.gentoo.kbdd заменен на ru.gentoo.KbddService т.к. менялось только название, без изменения самой раскладки. Дополнительно вынесена отдельно функция по изменению раскладки, т.к. удобно автоматически менять раскладку при вызове Mod4+r или Mod4+p (перед вызовом добавил changeKeyboardLayout(0)
Проголосовать:
+26
Сохранить: