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


Комментарии (15)


  1. mikhailian
    18.10.2021 14:55
    +4

    Гм... а я в .xinitrc для таких случаев дописываю строчку вроде chromium & для автостарта. Это проще, нет?


    1. Oxyd Автор
      18.10.2021 15:04
      +3

      Это проще. Но приятнее когда работает весь функционал. К тому-же у меня запуск всей безобразии идет параллельно. Ну и всякое перезапустится, если невзначай упадёт.



      1. mikhailian
        19.10.2021 15:15
        -2

        while true; do chromium; sleep 1; done & будет и перезапускаться, и параллельно выполняться.

        Systemd — позорище. Не потому, что такой сложный. Это простительно. Xorg тоже сложный. А потому, что при всей его сложности и неадекватности путём подковёрных интриг RedHat (не удивлюсь если на пару с АНБ и ФБР) его уже десять лет как пропихивает повсюду, а мы плачем и хаваем.


        1. Oxyd Автор
          19.10.2021 17:42
          +4

          Не знаю, Я вот не плачу. Мне наоборот он очень заходит. Лучшей документации в linux, я пожалуй не вспомню. Особенно учтывая объёмы продукта. А те кто говорят про АНБ и это вот всё... Могу с уверенностю сказать, что они ни разу не заглядывали в сорцы. Даже я заглядывал(хоть ни разу не сишник), что-б понять как работают некоторые вещи. Как ни странно понял.


  1. Bronekalmar
    18.10.2021 23:34
    +1

    i3wm из коробки предлагает автозапуск с помощью exec_always, для большинства задач по автозапуска приложений кажется достаточным, но Ваш подход интересен.


    1. Oxyd Автор
      18.10.2021 23:38
      +2

      exec_always срабатывает при каждом перезапуске i3. Обычно используют просто exec --no-startup-id. При моём способе обеспечивается большая гибкость.


  1. AlienJust
    19.10.2021 05:56

    Ещё почему-то "большие" используют dbus-run-session вместо dbus-launch --exit-with-session


  1. EdheL_evair
    19.10.2021 09:56

    Спасибо. Некоторое время назад также искал вариант запуска иксового приложения от текущего пользователя. В итоге сейчас использую следующий вариант:

    Сервис в /etc/systemd/user вызывается командой systemctl --user .service из файла .desktop в /etc/xdg/autostart

    З.Ы. Правда теперь пытаюсь разобраться, как назначить этому процессу другую группу. Не первичную для пользователя, а дополнительную. Объявление Group= не помогает.


    1. Oxyd Автор
      20.10.2021 00:00

      Не удивительно...

      User=, Group= Set the UNIX user or group that the processes are executed as, respectively. Takes a single user or group name, or a numeric ID as argument. For system services (services run by the system service manager, i.e. managed by PID 1) and for user services of the root user (services managed by root's instance of systemd --user), the default is "root", but User= may be used to specify a different user. For user services of any other user, switching user identity is not permitted.

      Почему нельзя сменить группу, вопрос, на самом деле, интересный. Подозреваю что где-то в коде этот функционал попал под горячую руку вместе с User=


  1. datt
    19.10.2021 09:59

    Хм, я просто ставлю пакет dex и добавляю его в i3wm config:
    exec --no-startup-id dex -ae i3

    И всё работает


    1. Oxyd Автор
      19.10.2021 17:34

      Да, я в процессе натыкался на dex, но это не спортивно, ставить ещё какой-то пакет, когда уже есть нужный функционал, да ещё и с бонусами.


  1. prefrontalCortex
    19.10.2021 11:21

    Как всегда, чудовище systemd легко заменимо банальным легковесным решением, прописываемым в автозапуск вашего wm или .xinitrc: https://github.com/jceb/dex


    1. Oxyd Автор
      19.10.2021 17:46
      +1

      Зачем ещё один бинарник, когда можно просто приспособить имеющееся «чудовище»? ;-)


      1. prefrontalCortex
        19.10.2021 22:43
        -1

        find /usr -type f -name "systemd" | wc -l

        0

        Дякую тобi боже


  1. Shorrer
    19.10.2021 17:34
    +3

    Такая маленькая вещь: в 4.20 в дефолтный конфиг добавили запуск dex --autostart который запустит всё с /etc/xdg/autostart, поэтому это можно считать официальным ответом на автозапуск. Также добавили вызов sd_notify() с READY=1 и теперь можно послушать когда i3 запустился и зависеть от этого. Коммит 5b6a56419051c9cf40d02b3d88df7829b5a616c7 объясняет этим пользоваться.
    Статья от этого менее полезной не стала в любом случае, автору спасибо. Новая версия вышла часов пять назад.