Всё как у больших. Автозагрузка приложений в оконных менеджерах linux

Вывод systemd-analyze dot --user ‘i3.service’ | dot -Tpng | imv -

Вывод 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//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 Ну и, чтоб не копипастить, ссылка на гитхаб.


image-loader.svg

© Habrahabr.ru