Introduce ExitType

This commit is contained in:
Henri Chain 2021-02-24 16:13:21 +01:00 committed by Zbigniew Jędrzejewski-Szmek
parent 82a335db83
commit cb0e818f7c
Notes: Zbigniew Jędrzejewski-Szmek 2021-04-22 09:22:27 +02:00
description from the pull request:

This PR introduces `ExitType=main|cgroup` for services. Similar to how `Type`
specifies the launch of a service, `ExitType` is concerned with how systemd
determines that a service exited.

- If set to `main` (the current behavior), the service manager will consider
  the unit stopped when the main process exits.

- The `cgroup` exit type is meant for applications whose forking model is not
  known ahead of time and which might not have a specific main process. The
  service will stay running as long as at least one process in the cgroup is
  running. The exit status of the service is that of the last process in the
  cgroup to exit. This is intended for transient or automatically generated
  services, such as graphical applications inside of a desktop environment.

Motivation for this is #16805. `xdg-autostart-generator` was amended to use
`ExitType=cgroup`, which fixes https://bugs.kde.org/show_bug.cgi?id=433299, a
bug where some generated autostart app services that forked not launching
(systemd kills the entire cgroup after the main process exited).
18 changed files with 194 additions and 10 deletions

View file

@ -303,6 +303,7 @@ Most service unit settings are available for transient units.
✓ ExecStartPre=
✓ ExecStop=
✓ ExecStopPost=
✓ ExitType=
✓ FileDescriptorStoreMax=
✓ GuessMainPID=
✓ NonBlocking=

View file

@ -2250,6 +2250,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s Type = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s ExitType = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s Restart = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PIDFile = '...';
@ -2808,6 +2810,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<!--property Type is not documented!-->
<!--property ExitType is not documented!-->
<!--property Restart is not documented!-->
<!--property PIDFile is not documented!-->
@ -3320,6 +3324,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<variablelist class="dbus-property" generated="True" extra-ref="Type"/>
<variablelist class="dbus-property" generated="True" extra-ref="ExitType"/>
<variablelist class="dbus-property" generated="True" extra-ref="Restart"/>
<variablelist class="dbus-property" generated="True" extra-ref="PIDFile"/>

View file

@ -255,6 +255,31 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>ExitType=</varname></term>
<listitem>
<para>Configures the process exit type for this service unit. One of <option>main</option> or
<option>cgroup</option>:</para>
<itemizedlist>
<listitem><para>If set to <option>main</option> (the default), the service manager
will consider the unit stopped when the main process, which is determined according to the `Type`, exits.
</para></listitem>
<listitem><para>The <option>cgroup</option> exit type is meant for applications whose forking model is not
known ahead of time and which might not have a specific main process. The service will stay running as long
as at least one process in the cgroup is running. The exit status of the service is that of the last
process in the cgroup to exit.</para></listitem>
</itemizedlist>
<para>It is generally recommended to use <varname>ExitType=</varname><option>main</option> when a service has
a known forking model and a main process can reliably be determined. <varname>ExitType=</varname>
<option>cgroup</option> is well suited for transient or automatically generated services, such as graphical
applications inside of a desktop environment.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>RemainAfterExit=</varname></term>

View file

@ -78,7 +78,7 @@ _systemd_run() {
-p|--property)
local comps='CPUAccounting= MemoryAccounting= BlockIOAccounting= SendSIGHUP=
SendSIGKILL= MemoryLimit= CPUShares= BlockIOWeight= User= Group=
DevicePolicy= KillMode= DeviceAllow= BlockIOReadBandwidth=
DevicePolicy= KillMode= ExitType= DeviceAllow= BlockIOReadBandwidth=
BlockIOWriteBandwidth= BlockIODeviceWeight= Nice= Environment=
KillSignal= RestartKillSignal= FinalKillSignal= LimitCPU= LimitFSIZE= LimitDATA=
LimitSTACK= LimitCORE= LimitRSS= LimitNOFILE= LimitAS= LimitNPROC=

View file

@ -45,7 +45,7 @@ _arguments \
{-p+,--property=}'[Set unit property]:NAME=VALUE:(( \
CPUAccounting= MemoryAccounting= BlockIOAccounting= SendSIGHUP= \
SendSIGKILL= MemoryLimit= CPUShares= BlockIOWeight= User= Group= \
DevicePolicy= KillMode= DeviceAllow= BlockIOReadBandwidth= \
DevicePolicy= KillMode= ExitType= DeviceAllow= BlockIOReadBandwidth= \
BlockIOWriteBandwidth= BlockIODeviceWeight= Nice= Environment= \
KillSignal= RestartKillSignal= FinalKillSignal= LimitCPU= LimitFSIZE= LimitDATA= \
LimitSTACK= LimitCORE= LimitRSS= LimitNOFILE= LimitAS= LimitNPROC= \

View file

@ -27,6 +27,7 @@
#include "unit.h"
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_type, service_type, ServiceType);
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_exit_type, service_exit_type, ServiceExitType);
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_result, service_result, ServiceResult);
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_restart, service_restart, ServiceRestart);
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_notify_access, notify_access, NotifyAccess);
@ -192,6 +193,7 @@ int bus_service_method_mount_image(sd_bus_message *message, void *userdata, sd_b
const sd_bus_vtable bus_service_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_PROPERTY("Type", "s", property_get_type, offsetof(Service, type), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ExitType", "s", property_get_exit_type, offsetof(Service, exit_type), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Restart", "s", property_get_restart, offsetof(Service, restart), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PIDFile", "s", NULL, offsetof(Service, pid_file), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("NotifyAccess", "s", property_get_notify_access, offsetof(Service, notify_access), SD_BUS_VTABLE_PROPERTY_CONST),
@ -377,6 +379,7 @@ static int bus_set_transient_std_fd(
}
static BUS_DEFINE_SET_TRANSIENT_PARSE(notify_access, NotifyAccess, notify_access_from_string);
static BUS_DEFINE_SET_TRANSIENT_PARSE(service_type, ServiceType, service_type_from_string);
static BUS_DEFINE_SET_TRANSIENT_PARSE(service_exit_type, ServiceExitType, service_exit_type_from_string);
static BUS_DEFINE_SET_TRANSIENT_PARSE(service_restart, ServiceRestart, service_restart_from_string);
static BUS_DEFINE_SET_TRANSIENT_PARSE(oom_policy, OOMPolicy, oom_policy_from_string);
static BUS_DEFINE_SET_TRANSIENT_STRING_WITH_CHECK(bus_name, sd_bus_service_name_is_valid);
@ -414,6 +417,9 @@ static int bus_service_set_transient_property(
if (streq(name, "Type"))
return bus_set_transient_service_type(u, name, &s->type, message, flags, error);
if (streq(name, "ExitType"))
return bus_set_transient_service_exit_type(u, name, &s->exit_type, message, flags, error);
if (streq(name, "OOMPolicy"))
return bus_set_transient_oom_policy(u, name, &s->oom_policy, message, flags, error);

View file

@ -359,6 +359,7 @@ Service.StartLimitAction, config_parse_emergency_action,
Service.FailureAction, config_parse_emergency_action, 0, offsetof(Unit, failure_action)
Service.RebootArgument, config_parse_unit_string_printf, 0, offsetof(Unit, reboot_arg)
Service.Type, config_parse_service_type, 0, offsetof(Service, type)
Service.ExitType, config_parse_service_exit_type, 0, offsetof(Service, exit_type)
Service.Restart, config_parse_service_restart, 0, offsetof(Service, restart)
Service.PermissionsStartOnly, config_parse_bool, 0, offsetof(Service, permissions_start_only)
Service.RootDirectoryStartOnly, config_parse_bool, 0, offsetof(Service, root_directory_start_only)

View file

@ -130,6 +130,7 @@ DEFINE_CONFIG_PARSE_ENUM(config_parse_protect_home, protect_home, ProtectHome, "
DEFINE_CONFIG_PARSE_ENUM(config_parse_protect_system, protect_system, ProtectSystem, "Failed to parse protect system value");
DEFINE_CONFIG_PARSE_ENUM(config_parse_runtime_preserve_mode, exec_preserve_mode, ExecPreserveMode, "Failed to parse runtime directory preserve mode");
DEFINE_CONFIG_PARSE_ENUM(config_parse_service_type, service_type, ServiceType, "Failed to parse service type");
DEFINE_CONFIG_PARSE_ENUM(config_parse_service_exit_type, service_exit_type, ServiceExitType, "Failed to parse service exit type");
DEFINE_CONFIG_PARSE_ENUM(config_parse_service_restart, service_restart, ServiceRestart, "Failed to parse service restart specifier");
DEFINE_CONFIG_PARSE_ENUM(config_parse_service_timeout_failure_mode, service_timeout_failure_mode, ServiceTimeoutFailureMode, "Failed to parse timeout failure mode");
DEFINE_CONFIG_PARSE_ENUM(config_parse_socket_bind, socket_address_bind_ipv6_only_or_bool, SocketAddressBindIPv6Only, "Failed to parse bind IPv6 only value");
@ -5748,6 +5749,7 @@ void unit_dump_config_items(FILE *f) {
{ config_parse_unit_deps, "UNIT [...]" },
{ config_parse_exec, "PATH [ARGUMENT [...]]" },
{ config_parse_service_type, "SERVICETYPE" },
{ config_parse_service_exit_type, "SERVICEEXITTYPE" },
{ config_parse_service_restart, "SERVICERESTART" },
{ config_parse_service_timeout_failure_mode, "TIMEOUTMODE" },
{ config_parse_kill_mode, "KILLMODE" },

View file

@ -32,6 +32,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_service_timeout);
CONFIG_PARSER_PROTOTYPE(config_parse_service_timeout_abort);
CONFIG_PARSER_PROTOTYPE(config_parse_service_timeout_failure_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_service_type);
CONFIG_PARSER_PROTOTYPE(config_parse_service_exit_type);
CONFIG_PARSER_PROTOTYPE(config_parse_service_restart);
CONFIG_PARSER_PROTOTYPE(config_parse_socket_bindtodevice);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_output);

View file

@ -1621,18 +1621,25 @@ static int control_pid_good(Service *s) {
return s->control_pid > 0;
}
static int cgroup_good(Service *s) {
int r;
static int cgroup_empty(Service *s) {
assert(s);
/* Returns 0 if the cgroup is empty or doesn't exist, > 0 if it is exists and is populated, < 0 if we can't
* figure it out */
/* Returns 0 if there is no cgroup, > 0 if is empty or doesn't exist, < 0 if we can't figure it out */
if (!UNIT(s)->cgroup_path)
return 0;
r = cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, UNIT(s)->cgroup_path);
return cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, UNIT(s)->cgroup_path);
}
static int cgroup_good(Service *s) {
int r;
/* Returns 0 if the cgroup is empty or doesn't exist, > 0 if it is exists and is populated, < 0 if we can't
* figure it out */
r = cgroup_empty(s);
if (r < 0)
return r;
@ -3398,7 +3405,14 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) {
else
assert_not_reached("Unknown code");
if (s->main_pid == pid) {
/* Services with ExitType=cgroup ignore the main PID for purposes of exit status */
if (s->exit_type == SERVICE_EXIT_CGROUP && s->main_pid == pid) {
service_unwatch_main_pid(s);
s->main_pid_known = false;
}
if ((s->exit_type == SERVICE_EXIT_MAIN && s->main_pid == pid) ||
(s->exit_type == SERVICE_EXIT_CGROUP && cgroup_empty(s) && !control_pid_good(s))) {
/* Forking services may occasionally move to a new PID.
* As long as they update the PID file before exiting the old
* PID, they're fine. */
@ -3431,7 +3445,7 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) {
unit_log_process_exit(
u,
"Main process",
s->exit_type == SERVICE_EXIT_CGROUP ? "Last process" : "Main process",
service_exec_command_to_string(SERVICE_EXEC_START),
f == SERVICE_SUCCESS,
code, status);
@ -4448,6 +4462,13 @@ static const char* const service_type_table[_SERVICE_TYPE_MAX] = {
DEFINE_STRING_TABLE_LOOKUP(service_type, ServiceType);
static const char* const service_exit_type_table[_SERVICE_EXIT_TYPE_MAX] = {
[SERVICE_EXIT_MAIN] = "main",
[SERVICE_EXIT_CGROUP] = "cgroup",
};
DEFINE_STRING_TABLE_LOOKUP(service_exit_type, ServiceExitType);
static const char* const service_exec_command_table[_SERVICE_EXEC_COMMAND_MAX] = {
[SERVICE_EXEC_CONDITION] = "ExecCondition",
[SERVICE_EXEC_START_PRE] = "ExecStartPre",

View file

@ -35,6 +35,13 @@ typedef enum ServiceType {
_SERVICE_TYPE_INVALID = -EINVAL,
} ServiceType;
typedef enum ServiceExitType {
SERVICE_EXIT_MAIN, /* we consider the main PID when deciding if the service exited */
SERVICE_EXIT_CGROUP, /* we wait for the last process in the cgroup to exit */
_SERVICE_EXIT_TYPE_MAX,
_SERVICE_EXIT_TYPE_INVALID = -EINVAL,
} ServiceExitType;
typedef enum ServiceExecCommand {
SERVICE_EXEC_CONDITION,
SERVICE_EXEC_START_PRE,
@ -97,6 +104,7 @@ struct Service {
Unit meta;
ServiceType type;
ServiceExitType exit_type;
ServiceRestart restart;
ExitStatusSet restart_prevent_status;
ExitStatusSet restart_force_status;
@ -226,6 +234,9 @@ ServiceRestart service_restart_from_string(const char *s) _pure_;
const char* service_type_to_string(ServiceType i) _const_;
ServiceType service_type_from_string(const char *s) _pure_;
const char* service_exit_type_to_string(ServiceExitType i) _const_;
ServiceExitType service_exit_type_from_string(const char *s) _pure_;
const char* service_exec_command_to_string(ServiceExecCommand i) _const_;
ServiceExecCommand service_exec_command_from_string(const char *s) _pure_;

View file

@ -1947,6 +1947,7 @@ static int bus_append_service_property(sd_bus_message *m, const char *field, con
if (STR_IN_SET(field, "PIDFile",
"Type",
"ExitType",
"Restart",
"BusName",
"NotifyAccess",

View file

@ -599,6 +599,7 @@ int xdg_autostart_service_generate_unit(
fprintf(f,
"\n[Service]\n"
"Type=exec\n"
"ExitType=cgroup\n"
"ExecStart=:%s\n"
"Restart=no\n"
"TimeoutSec=5s\n"

View file

@ -0,0 +1 @@
../TEST-01-BASIC/Makefile

6
test/TEST-56-EXIT-TYPE/test.sh Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
TEST_DESCRIPTION="test ExitType=cgroup"
. $TEST_BASE_DIR/test-functions
do_test "$@" 56

View file

@ -94,6 +94,7 @@ ExecStartPre=
ExecStop=
ExecStopPost=
ExecStopPre=
ExitType=
FailureAction=
FileDescriptorName=
FileDescriptorStoreMax=

View file

@ -0,0 +1,6 @@
[Unit]
Description=TEST-56-EXIT-TYPE
[Service]
ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
Type=oneshot

94
test/units/testsuite-56.sh Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -ex
systemd-analyze log-level debug
# Multiple level process tree, parent process stays up
cat >/tmp/test56-exit-cgroup.sh <<EOF
#!/usr/bin/env bash
set -eux
# process tree: systemd -> sleep
sleep infinity &
disown
# process tree: systemd -> bash -> bash -> sleep
((sleep infinity); true) &
# process tree: systemd -> bash -> sleep
sleep infinity
EOF
chmod +x /tmp/test56-exit-cgroup.sh
# service should be stopped cleanly
(sleep 1; systemctl stop one) &
systemd-run --wait --unit=one -p ExitType=cgroup /tmp/test56-exit-cgroup.sh
# same thing with a truthy exec condition
(sleep 1; systemctl stop two) &
systemd-run --wait --unit=two -p ExitType=cgroup -p ExecCondition=true /tmp/test56-exit-cgroup.sh
# false exec condition: systemd-run should exit immediately with status code: 1
! systemd-run --wait --unit=three -p ExitType=cgroup -p ExecCondition=false /tmp/test56-exit-cgroup.sh
# service should exit uncleanly
(sleep 1; systemctl kill --signal 9 four) &
! systemd-run --wait --unit=four -p ExitType=cgroup /tmp/test56-exit-cgroup.sh
# Multiple level process tree, parent process exits quickly
cat >/tmp/test56-exit-cgroup-parentless.sh <<EOF
#!/usr/bin/env bash
set -eux
# process tree: systemd -> sleep
sleep infinity &
# process tree: systemd -> bash -> sleep
((sleep infinity); true) &
EOF
chmod +x /tmp/test56-exit-cgroup-parentless.sh
# service should be stopped cleanly
(sleep 1; systemctl stop five) &
systemd-run --wait --unit=five -p ExitType=cgroup /tmp/test56-exit-cgroup-parentless.sh
# service should exit uncleanly
(sleep 1; systemctl kill --signal 9 six) &
! systemd-run --wait --unit=six -p ExitType=cgroup /tmp/test56-exit-cgroup-parentless.sh
# Multiple level process tree, parent process exits uncleanly but last process exits cleanly
cat >/tmp/test56-exit-cgroup-clean.sh <<EOF
#!/usr/bin/env bash
set -eux
# process tree: systemd -> bash -> sleep
(sleep 1; true) &
exit 255
EOF
chmod +x /tmp/test56-exit-cgroup-clean.sh
# service should exit cleanly and be garbage-collected
systemd-run --wait --unit=seven -p ExitType=cgroup /tmp/test56-exit-cgroup-clean.sh
# Multiple level process tree, parent process exits cleanly but last process exits uncleanly
cat >/tmp/test56-exit-cgroup-unclean.sh <<EOF
#!/usr/bin/env bash
set -eux
# process tree: systemd -> bash -> sleep
(sleep 1; exit 255) &
EOF
chmod +x /tmp/test56-exit-cgroup-unclean.sh
# service should exit uncleanly after 1 second
! systemd-run --wait --unit=eight -p ExitType=cgroup /tmp/test56-exit-cgroup-unclean.sh
systemd-analyze log-level info
echo OK > /testok
exit 0