Immae's blog

egrep -ri TODO /etc

Manage your session with systemd

Languages: French

Introduction

A lot of distribution are making their way to "systemd" as an alternative to System V.

One of the advantages of systemd is that it offers a service management for users, and it is this system that I plan to present here to handle our session. Systemd is still under active development, including the user part. This guide reflects the big change that happened in version 206 of systemd on which most current tutorial are based. This setup has been tested on version 212.

When a user session starts (either remotely via ssh or local), an instance of systemd --user starts for that user. This instance has the role to start user services, in a similar way to process 1 but dedicated to the user. Note that by default these services disappear with the user session

Starting systemd --user

Before version 206 (as far as I can tell), it was the user's responsibility to start (or not) this instance. Now she cannot anymore be started by the user and it must be run by root.

It is started automatically at login provided that the pam_systemd module is active in pam for the requested session. For that, we add a line of the form

-session   optional   pam_systemd.so

The "-" indicates that it is not essential to the session. In other words: if it fails to start we don't care). With Archlinux, the file /etc/pam.d/system-login already contains this line and concerns any kind of connection (local and remote).

Systemd arrangement

Systemd separates the services and session between slice/scope/service, making heavy use of the cgroups feature (it is a way to group a collection of processes and their possible children without possible escape). A typical system running systemd then looks like that (you can obtain it with systemd-cgls):

Note that the services of the user ar started in a context separate to the session itself. This has to be remembered, for instance for environment variables scopes.

As soon as all the session-c*.scope of a user are exited, the corresponding user@.service will also be stopped, including all the sub-services. This is a point to remember! For instance, by default, using "timer" unit files of systemd as a replacement for cron won't work for the user if he's not there!
We'll see later how to deal with that.

User services

Comparison

User services are handled in exactly the same way as system services, except that the units are sought in different folders (see man systemd.unit). When systemd --user starts, unit default.target is started and has the role to start other necessary services.

systemctl --user and dbus

Services managed by systemd (whether system or user) are managed primarily through the command systemctl (--user), which strongly depends on a correct dbus installation. First thing to do is thus to ensure that the dbus daemon runs, both at system level (system bus) and user level (session bus). For the system part, we simply need to add a dependency to user@.service. We will also normalize the path to the session bus socket by giving the appropriate environment variable DBUS_SESSION_BUS_ADDRESS. Since user@.service is a parent of all user services, we can use it to transmit the variable

# /etc/systemd/system/user@.service.d/dbus_env.conf
[Unit]
Wants=dbus.service

[Service]
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%I/bus

Note

Line 2 of the file indicates that we require that the system dbus is started, while line 5 concerns the environment variable for the user's session bus. Don't confuse these services, they correspond to two different buses.

In the sequence, we will define the different services that we will need. They can be defined or activated globally or by user's choice. This depends both on the location of the unit file (/etc/systemd/user/ vs $HOME/.config/systemd/user) and on the way it is activated (with --global vs --user). Here I will define the services and sockets associated to the session dbus globally, but I will let the user chose whether he turns them on or note l'utilisateur :

# /etc/systemd/user/dbus.service
[Unit]
Description=D-Bus Message Bus
Requires=dbus.socket

[Service]
ExecStart=/usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation
ExecReload=/usr/bin/dbus-send --print-reply --session --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig
Restart=always
RestartSec=1

(Note that the main difference between this unit and the corresponding system unit resides in the use of --session instead of --system)

# /etc/systemd/user/dbus.socket
[Unit]
Description=D-Bus Message Bus Socket
Before=sockets.target

[Socket]
ListenStream=/run/user/%U/bus

[Install]
WantedBy=default.target

(default.target is the default activated target by systemd).

From now on, the hardest part is done. We will create a unit my_services.target alias of default.target

# $HOME/.config/systemd/user/my_services.target
[Unit]
Description=My services
Wants=dbus.service
AllowIsolate=true

[Install]
Alias=default.target

and then we create unit files in the same folder. Then, we activate them using the well know command systemctl, but with an argument --user. The rest of the article consists essentially in tricks and interesting specific cases.

Important notice

The directive After doesn't have the same effect for a service unit and a target unit. A service will be started only once all the unit quoted in After are ready (for the definition of ready, see man systemd.service, bloc Type=), while for a target it is assumed that the services are part of it (and can then be started concurrently to other services).

User services that last

By default, services associated to an user (everything that is below user@1001.service in the tree above) are stopped when the user disconnects. This includes timers, that we would like to use as a replacement for crontab.

We can bypass that with this command:

loginctl enable-linger user

In that case, user@.service will be started as soon as boot for the corresponding user, even when he is not there.

I won't go any further into details on how to define the equivelnt of crontabs for systemd, this kind of feature is already well explained in manual pages as well as systemd-dedicated websites (see man systemd.timer). The trick given here deals with the issue of having crontabs equivalent that last for an user even when he's not there.

Important notice

Usually crontabs send emails when there is output. As far as I know there is no such feature in systemd services and everything is logged in the journal.

Environment variables

Environment variables can become a true headache when we use services. For instance, how can we transmit variables necessary to the services like GPG_AGENT_INFO or SSH_AUTH_SOCK or even DISPLAY. One way to do it is to specify in the unit file a directive of the form

Environment=DISPLAY=:0

or

EnvironmentFile=/file/to/load

The problem is that this configuration is static, and must be defined for each service separately or globally in a file like /etc/systemd/system/user@.service.d/environment.conf. We also lose the usual dynamism of configuration scripts.

Here, we will make use of the command systemctl --user import-environment to solve our problem. This command permits to import environment variables that will be included in any subsequent service. We start by creating a service setenv.service that will start a script loading the environment. As far as I am concerned, I have to different environment depending on whether I am in command line or graphical. My service file then looks like that:

# $HOME/.config/systemd/user/setenv@.service
[Unit]
Description=Set environment
Wants=dbus.service gpg-agent.service ssh-agent.service
After=dbus.service gpg-agent.service ssh-agent.service

[Service]
Type=oneshot
Environment=SSH_AUTH_SOCK=%t/ssh_auth_sock
ExecStart=%h/bin/systemd_setenv %i

And then I activate services setenv@type.service depending on my needs. Here I also defined an environment variable inside the unit. The only reason for that is that the part "%t" (/run/user/1000/) is easier to find in a unit file...

Here is an example script to define my environment:

# $HOME/bin/systemd_setenv
#!/bin/zsh

if [ "x$1" = "xcommon" ]; then
        . /etc/zsh/zprofile
        source $HOME/.gpg-agent-info
        export GPG_AGENT_INFO
        systemctl --user import-environment
        systemctl --user unset-environment PWD OLDPWD SHLVL _ MANAGERPID
elif [ "x$1" = "xgraphical" ]; then
        export XDG_CONFIG_HOME="$HOME/.config/"
        export SAL_USE_VCLPLUGIN=gtk
        export XDG_MENU_PREFIX="lxde-"
        export DISPLAY=:0
        systemctl --user import-environment XDG_CONFIG_HOME SAL_USE_VCLPLUGIN XDG_MENU_PREFIX DISPLAY
fi

As I mentionned before, I have two different environments depending on whether I started a graphical session or simply a command line connection. Note that even if I leave the graphical environment, the variables are still there! We could make use of the command

systemctl --user unset-environment VARIABLE1 VARIABLE2

in the same way to destroy variables after use (in which case the setenv service has to be adapted).

The *-agents

Services that are worth starting no matter if we are in a graphical or command line remote session are ssh-agent and gpg-agent. However we need to think to transmit environment variables to the process that are interested (see Above section for more information).

The service envoy should also be considered to manage these agents. I prefer to separate both agent, which is not possible with envoy.

For ssh-agent, we can directly indicate in the command where we want to put the socket, which makes things easier:

# $HOME/.config/systemd/user/ssh-agent.service
[Unit]
Description=ssh-agent
ConditionFileIsExecutable=/usr/bin/ssh-agent

[Service]
ExecStart=/usr/bin/ssh-agent -d -a %t/ssh_auth_sock
Restart=always

[Install]
WantedBy=my_services.target

For gpg-agent, we can only tell him to save the informations in a file: the socket won't end in a predictable path.

# $HOME/.config/systemd/user/gpg-agent.service
[Unit]
Description=gpg-agent
ConditionFileIsExecutable=/usr/bin/gpg-agent

[Service]
ExecStart=/usr/bin/gpg-agent --daemon --write-env-file %h/.gpg-agent-info
Type=forking
Restart=always

[Install]
WantedBy=my_services.target

We can then source the file $HOME/.gpg-agent-info to access the agent socket.

source $HOME/.gpg-agent-info

Note

The fact that we don't have a standard place can be a problem if the agent is restarted: most probably, the socket won't be in the same place, and programs that already started won't be able to contact the agent.

Using systemd to manage your graphical session

It can be tempting to use systemd to manage also your graphical session: defining your window manager as a service and be able to switch to another window manager at will without being obliged to restart your session. This can lead to some problems as we will see later.

Graphical session as a service file

The simplest to do there is to define a unit file graphical.target (or graphical@.target if you want to define several window managers) and activate it in the .xsession script (or whichever is executed when your session starts).
It can be necessary to correctly define the environment variables at that time, in which case each uni will need the service setenv@graphical.service activated as a prerequisite (as indicated before, it it not sufficient to put it int the After directive in graphical.target).

As pointed initialy, it is tempting to define your graphical session as a service. However:
First, the command systemct returns immediatly. Thus, a .xsession file containing only something like that:

# $HOME/.xsession
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
systemctl --user start graphical.target

won't be enough, because your graphical session will disappear as soon as it starts (NB: the export DBUS_SESSION_BUS_ADDRESS is necessary to permit dialog with systemd). There exists programs such as systemd-wait that do exactly what we want to: wait for a service to end.

The second problem can be more annoying, and comes from the difference between the user session and services. If we don't take care, we will end up with something like that:

How can this be a problem? Because the only process considered active there is systemd-wait as we put it in the .xsession file. This can have consequences for instance in right management, see next section about polkit.

Note about slim

As specified on Archlinux website, slim is not compatible with systemd --user (at least it wasn't at the time when this article was writtent). What does that mean, and what can we do about it?

First of all, let's have a closer look to how slim works. slim is a connexion manager, that is, a sort of replacement of login in terminal-like tty's, but with a graphical interface. This has several implications: it has to deal with the problem of starting an X server, and has to launch the user session after his connexion. But he also have to take back control once the user leaves.

As the user logs in, slim start (via pam configuration) an instance of systemd --user, as well as a session script (for instance the .xsession file). It does not have to start an X server, since it is already there.
However when the user logs out however, it doesn't restart everything and reuses the state in which it was before. In the point of view of systemd, it then ends in the "scope" of the preceding user (even if it is still a root process):

Before log out:

  • system.slice
    • slimd.service
      • slim
      • X
  • user.slice
    • user-1000.slice
      • session-c1.scope
        • ...
      • user@1000.service

After log out:

  • system.slice
    • slimd.service
  • user.slice
    • user-1000.slice
      • session-c1.scope
        • slim
        • X
      • user@1000.service


This particular behaviour is particularly annoying for the next users. In Archlinux, slim has been patched recently to avoid this (since slim 1.3.6-4, 2014-04-21), by forcing slim to quit when a session ends (argument -nodaemon), and adding a Restart directive to the systemd unit file.

Note

The Restart directive is on-failure, but the patch makes slim quit with an error on purpose.

Another solution which was the one I used until the patch, if you are not using Archlinux and don't want to patch slim, is to put a line in /etc/slim.conf of the form

# /etc/slim.conf
sessionstop_cmd        systemctl restart slim.service

that will have the effect to restart slim at the end of each session (which essentially does the same as the patch).

Polkit and administrative rights

Systemd distinguishes between two kind of users, that is "active" and "inactive" users. For instance, if two users are both connected on the same computer (physically), then only one of them is actually "active" (unless you have several monitors). It also makes a difference whether the user is "local" or "remote". This information about a session can be obtained with the command

loginctl show-session "session"

where "session" is the id of the session (you can list session ids with loginctl list-sessions).
In particular, and that's where it is important, a service is always inactive.

Polkit is a service that permits to give (temporarily or not) more rights to a user. For instance if you can turn off the computer without typing sudo or entering a password, then most probably polkit plays a role here. These rights are aranged depending on the state of the user, whether he is active or inactive or "any".

Important

Polkit and systemd don't share the same active notions. For polkit, an active/inactive user is local and active/inactive for systemd. Any other user is in any (in particular remote users). This rule is hard-coded in function check_authorization_sync of file src/polkitbackend/polkitbackendinteractiveauthority.c in source code of polkit:

if (session_is_local)
  {
    if (session_is_active)
      implicit_authorization = polkit_action_description_get_implicit_active (action_desc);
    else
      implicit_authorization = polkit_action_description_get_implicit_inactive (action_desc);
  }
else
  {
    implicit_authorization = polkit_action_description_get_implicit_any (action_desc);
  }

I won't go into much more details about polkit here, but we can already see that an xterm started as a service, and one started directly (for instance in your .xsession) won't have the same right for polkit! We thus have to be careful about that when we want to use systemd --user as a session manager, because it will then be unable to shutdown the computer without using sudo.