execute: add support for XDG_STATE_HOME for placing service state data in --user mode

This adds support for the new XDG_STATE_HOME env var that was added to
the xdg basedir spec. Previously, because the basedir spec didn't know
the concept we'd alias the backing dir for StateDirectory= to the one
for ConfigurationDirectory= when runnin in --user mode. With this change
we'll make separate. This brings us various benefits, such as proper
"systemctl clean" support, where we can clear service state separately
from service configuration, now in user mode too.

This does not come without complications: retaining compatibility with
older setups is difficult, because we cannot possibly identitfy which
files in existing populated config dirs are actually "state" and which
one are true" configuration.

Hence let's deal with this pragmatically: if we detect that a service
that has both dirs configured only has the configuration dir existing,
then symlink the state dir to the configuration dir to retain
compatibility.

This is not great, but it's the only somewhat reasonable way out I can
see.

Fixes: #25739
This commit is contained in:
Lennart Poettering 2023-06-26 22:42:33 +02:00
parent d5602c1632
commit f9c91932b4
6 changed files with 65 additions and 10 deletions

View file

@ -1378,7 +1378,7 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
<row>
<entry><varname>StateDirectory=</varname></entry>
<entry><filename>/var/lib/</filename></entry>
<entry><varname>$XDG_CONFIG_HOME</varname></entry>
<entry><varname>$XDG_STATE_HOME</varname></entry>
<entry><varname>$STATE_DIRECTORY</varname></entry>
</row>
<row>
@ -1390,7 +1390,7 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
<row>
<entry><varname>LogsDirectory=</varname></entry>
<entry><filename>/var/log/</filename></entry>
<entry><varname>$XDG_CONFIG_HOME</varname><filename>/log/</filename></entry>
<entry><varname>$XDG_STATE_HOME</varname><filename>/log/</filename></entry>
<entry><varname>$LOGS_DIRECTORY</varname></entry>
</row>
<row>

View file

@ -2131,7 +2131,7 @@ Note that this setting is <emphasis>not</emphasis> influenced by the <varname>Us
<row>
<entry><literal>%L</literal></entry>
<entry>Log directory root</entry>
<entry>This is either <filename>/var/log</filename> (for the system manager) or the path <literal>$XDG_CONFIG_HOME</literal> resolves to with <filename index="false">/log</filename> appended (for user managers).</entry>
<entry>This is either <filename>/var/log</filename> (for the system manager) or the path <varname>$XDG_STATE_HOME</varname> resolves to with <filename index="false">/log</filename> appended (for user managers).</entry>
</row>
<xi:include href="standard-specifiers.xml" xpointer="m"/>
<xi:include href="standard-specifiers.xml" xpointer="M"/>
@ -2171,7 +2171,7 @@ Note that this setting is <emphasis>not</emphasis> influenced by the <varname>Us
<row>
<entry><literal>%S</literal></entry>
<entry>State directory root</entry>
<entry>This is either <filename>/var/lib</filename> (for the system manager) or the path <literal>$XDG_CONFIG_HOME</literal> resolves to (for user managers).</entry>
<entry>This is either <filename>/var/lib</filename> (for the system manager) or the path <varname>$XDG_STATE_HOME</varname> resolves to (for user managers).</entry>
</row>
<row>
<entry><literal>%t</literal></entry>

View file

@ -2511,6 +2511,61 @@ static int setup_exec_directory(
if (r < 0)
goto fail;
if (IN_SET(type, EXEC_DIRECTORY_STATE, EXEC_DIRECTORY_LOGS) && params->runtime_scope == RUNTIME_SCOPE_USER) {
/* If we are in user mode, and a configuration directory exists but a state directory
* doesn't exist, then we likely are upgrading from an older systemd version that
* didn't know the more recent addition to the xdg-basedir spec: the $XDG_STATE_HOME
* directory. In older systemd versions EXEC_DIRECTORY_STATE was aliased to
* EXEC_DIRECTORY_CONFIGURATION, with the advent of $XDG_STATE_HOME is is now
* seperated. If a service has both dirs configured but only the configuration dir
* exists and the state dir does not, we assume we are looking at an update
* situation. Hence, create a compatibility symlink, so that all expectations are
* met.
*
* (We also do something similar with the log directory, which still doesn't exist in
* the xdg basedir spec. We'll make it a subdir of the state dir.) */
/* this assumes the state dir is always created before the configuration dir */
assert_cc(EXEC_DIRECTORY_STATE < EXEC_DIRECTORY_LOGS);
assert_cc(EXEC_DIRECTORY_LOGS < EXEC_DIRECTORY_CONFIGURATION);
r = laccess(p, F_OK);
if (r == -ENOENT) {
_cleanup_free_ char *q = NULL;
/* OK, we know that the state dir does not exist. Let's see if the dir exists
* under the configuration hierarchy. */
if (type == EXEC_DIRECTORY_STATE)
q = path_join(params->prefix[EXEC_DIRECTORY_CONFIGURATION], context->directories[type].items[i].path);
else if (type == EXEC_DIRECTORY_LOGS)
q = path_join(params->prefix[EXEC_DIRECTORY_CONFIGURATION], "log", context->directories[type].items[i].path);
else
assert_not_reached();
if (!q) {
r = -ENOMEM;
goto fail;
}
r = laccess(q, F_OK);
if (r >= 0) {
/* It does exist! This hence looks like an update. Symlink the
* configuration directory into the state directory. */
r = symlink_idempotent(q, p, /* make_relative= */ true);
if (r < 0)
goto fail;
log_notice("Unit state directory %s missing but matching configuration directory %s exists, assuming update from systemd 253 or older, creating compatibility symlink.", p, q);
continue;
} else if (r != -ENOENT)
log_warning_errno(r, "Unable to detect whether unit configuration directory '%s' exists, assuming not: %m", q);
} else if (r < 0)
log_warning_errno(r, "Unable to detect whether unit state directory '%s' is missing, assuming it is: %m", p);
}
if (exec_directory_is_private(context, type)) {
/* So, here's one extra complication when dealing with DynamicUser=1 units. In that
* case we want to avoid leaving a directory around fully accessible that is owned by

View file

@ -720,9 +720,9 @@ static int manager_setup_prefix(Manager *m) {
static const struct table_entry paths_user[_EXEC_DIRECTORY_TYPE_MAX] = {
[EXEC_DIRECTORY_RUNTIME] = { SD_PATH_USER_RUNTIME, NULL },
[EXEC_DIRECTORY_STATE] = { SD_PATH_USER_CONFIGURATION, NULL },
[EXEC_DIRECTORY_STATE] = { SD_PATH_USER_STATE_PRIVATE, NULL },
[EXEC_DIRECTORY_CACHE] = { SD_PATH_USER_STATE_CACHE, NULL },
[EXEC_DIRECTORY_LOGS] = { SD_PATH_USER_CONFIGURATION, "log" },
[EXEC_DIRECTORY_LOGS] = { SD_PATH_USER_STATE_PRIVATE, "log" },
[EXEC_DIRECTORY_CONFIGURATION] = { SD_PATH_USER_CONFIGURATION, NULL },
};

View file

@ -209,8 +209,8 @@ int unit_full_printf_full(const Unit *u, const char *format, size_t max_length,
* %C: the cache directory root (e.g. /var/cache or $XDG_CACHE_HOME)
* %d: the credentials directory ($CREDENTIALS_DIRECTORY)
* %E: the configuration directory root (e.g. /etc or $XDG_CONFIG_HOME)
* %L: the log directory root (e.g. /var/log or $XDG_CONFIG_HOME/log)
* %S: the state directory root (e.g. /var/lib or $XDG_CONFIG_HOME)
* %L: the log directory root (e.g. /var/log or $XDG_STATE_HOME/log)
* %S: the state directory root (e.g. /var/lib or $XDG_STATE_HOME)
* %t: the runtime directory root (e.g. /run or $XDG_RUNTIME_DIR)
*
* %h: the homedir of the running user

View file

@ -5,7 +5,7 @@ Description=Test for specifiers
[Service]
Type=oneshot
ExecStart=sh -c 'test %t = $$XDG_RUNTIME_DIR'
ExecStart=sh -c 'test %S = %h/.config'
ExecStart=sh -c 'test %S = %h/.local/state'
ExecStart=sh -c 'test %C = %h/.cache'
ExecStart=sh -c 'test %L = %h/.config/log'
ExecStart=sh -c 'test %L = %h/.local/state/log'
ExecStart=sh -c 'test %E = %h/.config'