Вывод systemd-analyze dot --user ‘i3.service’ | dot -Tpng | imv -
Как-то раз, листая сообщения в профильном systemd чате, в телеграм, я наткнулся на следующий кусок man systemd.special
…
xdg-desktop-autostart.target
The XDG specification defines a way to autostart applications using XDG desktop files.
systemd ships systemd-xdg-autostart-generator(8) for the XDG desktop files in autostart
directories. Desktop Environments can opt-in to use this service by adding a Wants=dependency
on xdg-desktop-autostart.target.
О как интересно, подумалось мне. Можно реализовать функционал полноценныхDesktop Environments
, по автоматическому запуску приложений, при старте. А у меня как раз i3wm, который таковым не является и которому такой функционал не помешал бы. Надо это дело исследовать. Тогда я ещё не знал во что ввязался. Как оказалось, не всё так просто.
Переменные XDG, freedesktop.org, desktop-файлы и autostart
Пользователям полноценных линуксовых графических окружений (KDE, Gnome, Mate etc) прекрасно известна возможность автозапуска приложений при логине пользователя в систему, разработанную инициативной группой Freedesktop.org (ранее X Desktop Group, или XDG), подобная той, что существует, например, в Windows. Данный функционал обеспечивается обычными *.desktop
файлами, но лежащими по определённым путям:
$XDG_CONFIG_DIRS/autostart/
(/etc/xdg/autostart/
по умолчанию) — общесистемная директория, для всех пользователей. Туда, обычно, попадают десктоп файлы при установке софта пакетным менеджером.$XDG_CONFIG_HOME/autostart/
($HOME/.config/autostart/
по умолчанию) — Пользовательская директория, имеющая больший приоритет, нежели общесистемная, то есть если в пользовательской лежит десктоп файл с таким же именем, что и в общесистемной, будет использован именно пользовательский файл.
Если в этих переменных семейства XDG directories не указано иное, или эти переменные отсутствуют (так происходит в большинстве классических дистрибутивов, привет NixOS!), будут использованы значения по умолчанию.
Итак, с директориями определились. Файлы в них можно:
- Симлинкнуть из стандартных путей:
$XDG_DATA_DIRS/applications/
(/usr/share/applications/
по умолчанию) или из пользовательского$XDG_DATA_HOME/applications/
(~/.local/share/applications
по умолчанию), куда, кстати, любят класть свои файлы Steam, Itch.io или Wine. - Можно создать самому, написав десктоп файлы руками.
- Можно нажать галочку «Запускать при старте системы», в каком-нибудь софте, например, в телеграм клиенте и тогда уже софт сам создаст в
$XDG_CONFIG_HOME/autostart/
свой файл.
Всё хорошо. Одно плохо. Это не работает, как минимум, в Leftwm, Spectrwm, xmonad, bspwm, dwm (без патчей точно) и, разумеется, в любимом i3wm. Просто потому, что у них отсутствует session manager. И вот тут мы переходим к самому интересному. Встречайте! systemd!
Systemd как спасательный круг тайловых (и не очень) оконных менеджеров
Эта глава будет самой объёмной. Тут мы разберёмся кто и как может помочь разобрать залежи desktop файлов, кто, как и когда их запустит, и при чём тут вообще systemd. Поехали!
▍ Developers, developers, developers! Генераторы, генераторы, генераторы!
Systemd, как известно, это не только система инициализации, логгирования событий, но и набор готовых дополнительных утилит, готовых сервисов с их юнитами, система управления сетью, and more… Среди прочего systemd может выступать в качестве системного менеджера для пользовательских сервисов — юнитов, работающих в пространстве пользователя. То есть после логина пользователя в систему запускается ещё один экземпляр /usr/lib/systemd
только уже от пользователя и позволяет запускать юниты в пространстве пользователя, с наследованием его окружения и правами.
Среди других интересных и полезных вещей в systemd есть такая штука, как генераторы. Маленькие утилиты запускаемые на раннем этапе загрузки системы или сразу после логина пользователя и выполняющие динамическую генерацию юнитов и/или их конфигов. Например, есть генератор, который читает /etc/fstab
и на его основе генерирует *.mount
юниты. Или генератор, который вычитывает файлы *.conf
из /etc/environment.d/
и $HOME/.config/environment.d/
и на их основе собирает переменные которые пользователь видит набирая команду env
и которые наследуются всеми пользовательскими юнитами. Среди прочего есть и генератор, который пробегает по $XDG_CONFIG_DIRS/autostart
и $XDG_CONFIG_HOME/autostart
, вычитывает *.desktop
файлы, генерирует пользовательские *.service
юниты и кладёт их в /run/user/<UID>/systemd/generator.late
.
Всё хорошо и замечательно, но есть одно но. Если есть сервисы, их должен кто-то вовремя запустить. То есть запустить ровно тогда, когда будет запущена графическая оболочка… Если посмотреть, произвольный такой юнит, мы увидим там упоминание target-а graphical-session.target
(Юнит на основе десктоп файла апплета управления Bluetooth cat /run/user/1000/systemd/generator.late/app-blueman@autostart.service
):
# Automatically generated by systemd-xdg-autostart-generator
[Unit]
Documentation=man:systemd-xdg-autostart-generator(8)
SourcePath=/etc/xdg/autostart/blueman.desktop
PartOf=graphical-session.target
Description=Blueman Applet
After=graphical-session.target
[Service]
Type=exec
ExecStart=:/usr/bin/blueman-applet
Restart=no
TimeoutSec=5s
Slice=app.slice
Хорошо, но что это за graphical-session.target? В выводе systemctl --user --type=target
, если выполнить команду из-под i3wm никакого такого таргета не наблюдается. А вот если запустить из-под, например, Gnome, то вполне:
UNIT LOAD ACTIVE SUB DESCRIPTION
basic.target loaded active active Basic System
default.target loaded active active Main User Target
gnome-session-initialized.target loaded active active GNOME Session is initialized
gnome-session-manager.target loaded active active GNOME Session Manager is ready
gnome-session-pre.target loaded active active Tasks to be run before GNOME Session starts
gnome-session-x11-services.target loaded active active GNOME session X11 services
gnome-session-x11.target loaded active active GNOME X11 Session
gnome-session-x11@zorin.target loaded active active GNOME X11 Session (session: gnome)
gnome-session.target loaded active active GNOME Session
gnome-session@gnome.target loaded active active GNOME Session (session: gnome)
graphical-session-pre.target loaded active active Session services which should run early before the graphical session is brought up
graphical-session.target loaded active active Current graphical user session
org.gnome.SettingsDaemon.A11ySettings.target loaded active active GNOME accessibility target
org.gnome.SettingsDaemon.Color.target loaded active active GNOME color management target
org.gnome.SettingsDaemon.Datetime.target loaded active active GNOME date & time target
org.gnome.SettingsDaemon.Housekeeping.target loaded active active GNOME maintenance of expirable data target
org.gnome.SettingsDaemon.Keyboard.target loaded active active GNOME keyboard configuration target
org.gnome.SettingsDaemon.MediaKeys.target loaded active active GNOME keyboard shortcuts target
org.gnome.SettingsDaemon.Power.target loaded active active GNOME power management target
org.gnome.SettingsDaemon.PrintNotifications.target loaded active active GNOME printer notifications target
org.gnome.SettingsDaemon.Rfkill.target loaded active active GNOME RFKill support target
org.gnome.SettingsDaemon.ScreensaverProxy.target loaded active active GNOME FreeDesktop screensaver target
org.gnome.SettingsDaemon.Sharing.target loaded active active GNOME file sharing target
org.gnome.SettingsDaemon.Smartcard.target loaded active active GNOME smartcard target
org.gnome.SettingsDaemon.Sound.target loaded active active GNOME sound sample caching target
org.gnome.SettingsDaemon.Wacom.target loaded active active GNOME Wacom tablet support target
org.gnome.SettingsDaemon.XSettings.target loaded active active GNOME XSettings target
org.gnome.Shell.target loaded active active GNOME Shell
paths.target loaded active active Paths
sockets.target loaded active active Sockets
timers.target loaded active active Timers
LOAD = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB = The low-level unit activation state, values depend on unit type.
31 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.
И что же со всем этим делать и как быть? Как получить заветный target?
▍ Графическая оболочка тоже сервис. Подсматриваем в Gnome
Если в Gnome запустить systemctl --user --type=service
можно заметить интересный сервис:
UNIT LOAD ACTIVE SUB DESCRIPTION
at-spi-dbus-bus.service loaded active running Accessibility services bus
dbus.service loaded active running D-Bus User Message Bus
...
gnome-shell-x11.service loaded active running GNOME Shell on X11
...
Становится всё интереснее. Значит Gnome запускается как systemd сервис (gnome-shell-x11.service
). Ну а уж из сервиса можно реализовывать любые зависимости. В принципе ожидаемо. Но как реализовывать такое для произвольной графической оболочки, которая не заточена под такие тонкие извращения? Надеемся на то не должно быть сложно… Перво-наперво смотрим в юнит (systemctl cat --user gnome-shell-x11.service
) и понимаем, что ничего не понимаем и что мы немножко попали…
# /usr/lib/systemd/user/gnome-shell-x11.service
[Unit]
Description=GNOME Shell on X11
# On X11, try to show the GNOME Session Failed screen
OnFailure=gnome-shell-disable-extensions.service gnome-session-failed.target
OnFailureJobMode=replace
CollectMode=inactive-or-failed
RefuseManualStart=on
RefuseManualStop=on
After=gnome-session-manager.target
Requisite=gnome-session-initialized.target
PartOf=gnome-session-initialized.target
Before=gnome-session-initialized.target
# The units already conflict because they use the same BusName
#Conflicts=gnome-shell-wayland.service
# Limit startup frequency more than the default
StartLimitIntervalSec=15s
StartLimitBurst=3
[Service]
Type=notify
ExecStart=/usr/bin/gnome-shell
# Exit code 1 means we are probably *not* dealing with an extension failure
SuccessExitStatus=1
# On X11 we want to restart on-success (Alt+F2 + r) and on-failure.
Restart=always
Ладно, чёрт с ним, идём смотреть в *.desktop
файл xsessions(cat /usr/share/xsessions/gnome.desktop
)…
[Desktop Entry]
Name=GNOME
Comment=This session logs you into GNOME
Exec=env GNOME_SHELL_SESSION_MODE=gnome /usr/bin/gnome-session --systemd --session=gnome
TryExec=/usr/bin/gnome-shell
Type=Application
DesktopNames=GNOME
X-GDM-SessionRegisters=true
X-Ubuntu-Gettext-Domain=gnome-session-3.0
… и понимаем, что попали несколько серьёзнее чем хотелось бы и что гном, в данном случае, нам мало чем поможет. Он изначально заточен под работу с systemd. Идём в эти наши интернеты.
▍ Выходим на финишную прямую. Пишем враппер, юнит и наконец удачно стартуем
Не буду затягивать и утомлять читателя подробностями того, как и где приходилось выуживать информацию по крупицам. Это были и маны и ArchWiki
и чёрт его знает что ещё. Лучше сразу приведу готовые, в меру откомментированные файлы.
Итак, копируем дефолтный /usr/share/xsessions/i3.desktop
в /usr/share/xsessions/i3-systemd.desktop
и немного модифицируем.
[Desktop Entry]
### Было: ###
# Name=i3
### Стало: ###
Name=i3 via systemd
Comment=improved dynamic tiling window manager
### Было: ###
# Exec=i3
# TryExec=i3
### Стало: ###
Exec=i3-service
TryExec=i3-service
Type=Application
X-LightDM-DesktopName=i3
DesktopNames=i3
Keywords=tiling;wm;windowmanager;window;manager;
Теперь нам нужно написать враппер i3-service который будет подготавливать окружение и запускать i3wm в качестве сервиса. Ну и, разумеется, сам i3.service
файл тоже должен быть написан. Итак враппер /usr/local/bin/i3-service
:
#!/usr/bin/env sh
# Импортируем и загружаем в d-bus сессию переменные из логин менеджера.
/etc/X11/xinit/xinitrc.d/50-systemd-user.sh
systemctl --user import-environment XDG_SEAT XDG_VTNR XDG_SESSION_ID XDG_SESSION_TYPE XDG_SESSION_CLASS
if command -v dbus-update-activation-environment >/dev/null 2>&1; then
dbus-update-activation-environment XDG_SEAT XDG_VTNR XDG_SESSION_ID XDG_SESSION_TYPE XDG_SESSION_CLASS
fi
# Загружаем иксовые ресурсы.
userresources=$HOME/.Xresources
usermodmap=$HOME/.Xmodmap
sysresources=/etc/X11/xinit/.Xresources
sysmodmap=/etc/X11/xinit/.Xmodmap
if [ -f $sysresources ]; then
xrdb -merge $sysresources
fi
if [ -f $sysmodmap ]; then
xmodmap $sysmodmap
fi
if [ -f "$userresources" ]; then
xrdb -merge "$userresources"
fi
if [ -f "$usermodmap" ]; then
xmodmap "$usermodmap"
fi
# Запускаем xinitrc* скрипты.
if [ -d /etc/X11/xinit/xinitrc.d ] ; then
for f in /etc/X11/xinit/xinitrc.d/?*.sh ; do
[ -x "$f" ] && . "$f"
done
unset f
fi
# И собственно запускаем сервис.
exec systemctl --wait --user start i3.service
Ну и наконец вишенка на нашем торте, сам /etc/systemd/user/i3.service
:
[Unit]
Description=i3wm tiling window manager for X
Documentation=man:i3(5)
Wants=graphical-session-pre.target
After=graphical-session-pre.target
# Самое главное, биндимся к таргету графической сессии..
BindsTo=graphical-session.target
PartOf=graphical-session.target
# ... и не забываем включить таргет, ради которого всё затевалось.
Before=xdg-desktop-autostart.target
Wants=xdg-desktop-autostart.target
[Service]
Type=simple
# Запускаем i3 через d-bus launcher. Мы же хотим, чтоб у нас работал d-bus?
ExecStart=/usr/bin/dbus-launch --sh-syntax --exit-with-session i3 --shmlog-size 0
Restart=on-failure
RestartSec=5
TimeoutStopSec=10
- Записываемся.
- Для проверки добавляем в автозагрузку, например, тот же клиент телеграма.
- Идём на перезагрузку и в дисплейном менеджере при старте системы, выбираем пункт «i3 via systemd»
Что в итоге?
- Работает автозагрузка, прямо как в каком-нить гноме.
- Бонусом получили
graphical-session.target
, к которому можно биндить сервисы, зависящие от запущенной графической оболочки. Например, до этого у меня падал, при загрузке юнит clipboard manager-а, в результате приходилось костылять таймаут… Теперь не падает. - Можно выкинуть из конфига i3 всё, что запускается при старте (Директива
exec --no-startup-id
и это вот всё) и упаковать в отдельные аккуратные пользовательские*.service
и по-человечески рулить ими в процессе работы. Например, отключать и включать lockscreen простымsystemctl --user start
/stop
- Для автозагружаемых юнитов, сгенерённых из
*.desktop
файлов, в самих этих файлах их можно отключать, добавив строчкуHidden=true
Ну и вообще, приятно быть первооткрывателем. Ибо в процессе гугления и чтения манов, готового рецепта обнаружено не было. Так что любители wm, не относящиеся к systemd хейтерам. Пробуйте. За месяц использования был замечен ровно один косяк. Не работает gvgs-*
функционал в pcman-fm, если его запустить хоткеем из i3
Но если запустить из rofi, волшебным образом всё начинает работать. Возможно я забыл импортировать какую-то переменную в d-bus Ну и, чтоб не копипастить, ссылка на гитхаб.