Manage your session with systemd
Languages: French
May 26, 2014
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
):
- system.slice
- service_system1.service
- service_system2.service
- user.slice
- user-1000.slice
- session-c1.scope
- Program 1
- Program 2
- session-c2.scope
- session-c3.scope
- user@1000.service
- systemd --user
- service_user1000_1.service
- service_user1000_2.service
- service_user1000_3.service
- session-c1.scope
- user-1001.slice
- session-c4.scope
- Program 1
- Program 2
- session-c5.scope
- session-c6.scope
- user@1001.service
- systemd --user
- service_user1001_1.service
- service_user1001_2.service
- service_user1001_3.service
- session-c4.scope
- user-1000.slice
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 *-agent
s
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:
- user-1000.slice
- session-c1.scope
- systemd-wait -q --user graphique.service failed inactive
- user@1000.service
- systemd --user
- graphique.service
- openbox
- firefox
- xterm
- xterm
- service_2.service
- service_3.service
- session-c1.scope
activethere 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
- slimd.service
- user.slice
- user-1000.slice
- session-c1.scope
- ...
- user@1000.service
- session-c1.scope
- user-1000.slice
After log out:
- system.slice
- slimd.service
- user.slice
- user-1000.slice
- session-c1.scope
- slim
- X
- user@1000.service
- session-c1.scope
- user-1000.slice
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
.