summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2022-01-11 20:45:42 +0100
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2022-01-11 20:52:54 +0100
commit31b7eefb6c3c8e3da74ef18d864e64d72f542e16 (patch)
tree0fa0190390d4ee8f5afd0a90dc8d71093c9382da
parenta82b93092bdd3901a22375a820bfa09db8a39978 (diff)
downloadsystemd-31b7eefb6c3c8e3da74ef18d864e64d72f542e16.tar.gz
Reintroduce ExitTypev249.8
This 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. This is intended for transient or automatically generated services, such as graphical applications inside of a desktop environment. Motivation for this is #16805. The original PR (#18782) was reverted (#20073) after realizing that the exit status of "the last process in the cgroup" can't reliably be known (#19385) This version instead uses the main process exit status if there is one and just listens to the cgroup empty event otherwise. The advantages of a service with `ExitType=cgroup` over scopes are: - Integrated logging / stdout redirection - Avoids the race / synchronisation issue between launch and scope creation - More extensive use of drop-ins and thus distro-level configuration: by moving from scopes to services we can have drop ins that will affect properties that can only be set during service creation, like `OOMPolicy` and security-related properties - It makes systemd-xdg-autostart-generator usable by fixing [1], as obviously only services can be used in the generator, not scopes. [1] https://bugs.kde.org/show_bug.cgi?id=433299 (cherry picked from commit 596e447076b27d103a30c26a68626e9820ac705b)
-rw-r--r--docs/TRANSIENT-SETTINGS.md1
-rw-r--r--man/org.freedesktop.systemd1.xml6
-rw-r--r--man/systemd.service.xml25
-rw-r--r--shell-completion/bash/systemd-run2
-rw-r--r--shell-completion/zsh/_systemd-run2
-rw-r--r--src/core/dbus-service.c6
-rw-r--r--src/core/load-fragment-gperf.gperf.in1
-rw-r--r--src/core/load-fragment.c2
-rw-r--r--src/core/load-fragment.h1
-rw-r--r--src/core/service.c132
-rw-r--r--src/core/service.h11
-rw-r--r--src/shared/bus-unit-util.c1
l---------test/TEST-56-EXIT-TYPE/Makefile1
-rwxr-xr-xtest/TEST-56-EXIT-TYPE/test.sh9
-rw-r--r--test/fuzz/fuzz-unit-file/directives.service1
-rw-r--r--test/units/testsuite-56.service6
-rwxr-xr-xtest/units/testsuite-56.sh79
17 files changed, 226 insertions, 60 deletions
diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md
index 3a75627ca9..070ad5a285 100644
--- a/docs/TRANSIENT-SETTINGS.md
+++ b/docs/TRANSIENT-SETTINGS.md
@@ -304,6 +304,7 @@ Most service unit settings are available for transient units.
✓ ExecStartPre=
✓ ExecStop=
✓ ExecStopPost=
+✓ ExitType=
✓ FileDescriptorStoreMax=
✓ GuessMainPID=
✓ NonBlocking=
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index eb5245b62d..f76f751db4 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -2298,6 +2298,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 = '...';
@@ -2864,6 +2866,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!-->
@@ -3382,6 +3386,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"/>
diff --git a/man/systemd.service.xml b/man/systemd.service.xml
index 884260a215..9bc3826800 100644
--- a/man/systemd.service.xml
+++ b/man/systemd.service.xml
@@ -256,6 +256,31 @@
</varlistentry>
<varlistentry>
+ <term><varname>ExitType=</varname></term>
+
+ <listitem>
+ <para>Specifies when the manager should consider the service to be finished. 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
+ <varname>Type=</varname>, exits. Consequently, it cannot be used with
+ <varname>Type=</varname><option>oneshot</option>.</para></listitem>
+
+ <listitem><para>If set to <option>cgroup</option>, the service will be considered running as long as at
+ least one process in the cgroup has not exited.</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 meant for applications whose forking model is not known ahead of time and which
+ might not have a specific main process. It 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>
<listitem><para>Takes a boolean value that specifies whether
diff --git a/shell-completion/bash/systemd-run b/shell-completion/bash/systemd-run
index 76b9700f79..c5db8b77bd 100644
--- a/shell-completion/bash/systemd-run
+++ b/shell-completion/bash/systemd-run
@@ -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=
diff --git a/shell-completion/zsh/_systemd-run b/shell-completion/zsh/_systemd-run
index 934834b94b..7568ed4840 100644
--- a/shell-completion/zsh/_systemd-run
+++ b/shell-completion/zsh/_systemd-run
@@ -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= \
diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c
index 02628cd39e..13d555041e 100644
--- a/src/core/dbus-service.c
+++ b/src/core/dbus-service.c
@@ -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);
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index 42441eab6e..d343145fa9 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -383,6 +383,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)
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index 399a759ad0..9f7889a252 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -135,6 +135,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");
@@ -5844,6 +5845,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" },
diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h
index 45e9c397e4..d722041f96 100644
--- a/src/core/load-fragment.h
+++ b/src/core/load-fragment.h
@@ -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);
diff --git a/src/core/service.c b/src/core/service.c
index 701c145565..9e1602950d 100644
--- a/src/core/service.c
+++ b/src/core/service.c
@@ -588,6 +588,9 @@ static int service_verify(Service *s) {
if (s->type == SERVICE_ONESHOT && !exit_status_set_is_empty(&s->restart_force_status))
return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Service has RestartForceStatus= set, which isn't allowed for Type=oneshot services. Refusing.");
+ if (s->type == SERVICE_ONESHOT && s->exit_type == SERVICE_EXIT_CGROUP)
+ return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Service has ExitType=cgroup set, which isn't allowed for Type=oneshot services. Refusing.");
+
if (s->type == SERVICE_DBUS && !s->bus_name)
return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Service is of type D-Bus but no D-Bus service name has been specified. Refusing.");
@@ -3273,6 +3276,9 @@ static void service_notify_cgroup_empty_event(Unit *u) {
break;
}
+ if (s->exit_type == SERVICE_EXIT_CGROUP && main_pid_good(s) <= 0)
+ service_enter_start_post(s);
+
_fallthrough_;
case SERVICE_START_POST:
if (s->pid_file_pathspec &&
@@ -3461,79 +3467,82 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) {
service_run_next_main(s);
} else {
-
- /* The service exited, so the service is officially gone. */
s->main_command = NULL;
- switch (s->state) {
-
- case SERVICE_START_POST:
- case SERVICE_RELOAD:
- /* If neither main nor control processes are running then
- * the current state can never exit cleanly, hence immediately
- * terminate the service. */
- if (control_pid_good(s) <= 0)
- service_enter_stop(s, f);
+ /* Services with ExitType=cgroup do not act on main PID exiting,
+ * unless the cgroup is already empty */
+ if (s->exit_type == SERVICE_EXIT_MAIN || cgroup_good(s) <= 0) {
+ /* The service exited, so the service is officially gone. */
+ switch (s->state) {
+
+ case SERVICE_START_POST:
+ case SERVICE_RELOAD:
+ /* If neither main nor control processes are running then
+ * the current state can never exit cleanly, hence immediately
+ * terminate the service. */
+ if (control_pid_good(s) <= 0)
+ service_enter_stop(s, f);
+
+ /* Otherwise need to wait until the operation is done. */
+ break;
- /* Otherwise need to wait until the operation is done. */
- break;
+ case SERVICE_STOP:
+ /* Need to wait until the operation is done. */
+ break;
- case SERVICE_STOP:
- /* Need to wait until the operation is done. */
- break;
+ case SERVICE_START:
+ if (s->type == SERVICE_ONESHOT) {
+ /* This was our main goal, so let's go on */
+ if (f == SERVICE_SUCCESS)
+ service_enter_start_post(s);
+ else
+ service_enter_signal(s, SERVICE_STOP_SIGTERM, f);
+ break;
+ } else if (s->type == SERVICE_NOTIFY) {
+ /* Only enter running through a notification, so that the
+ * SERVICE_START state signifies that no ready notification
+ * has been received */
+ if (f != SERVICE_SUCCESS)
+ service_enter_signal(s, SERVICE_STOP_SIGTERM, f);
+ else if (!s->remain_after_exit || s->notify_access == NOTIFY_MAIN)
+ /* The service has never been and will never be active */
+ service_enter_signal(s, SERVICE_STOP_SIGTERM, SERVICE_FAILURE_PROTOCOL);
+ break;
+ }
- case SERVICE_START:
- if (s->type == SERVICE_ONESHOT) {
- /* This was our main goal, so let's go on */
- if (f == SERVICE_SUCCESS)
- service_enter_start_post(s);
- else
- service_enter_signal(s, SERVICE_STOP_SIGTERM, f);
- break;
- } else if (s->type == SERVICE_NOTIFY) {
- /* Only enter running through a notification, so that the
- * SERVICE_START state signifies that no ready notification
- * has been received */
- if (f != SERVICE_SUCCESS)
- service_enter_signal(s, SERVICE_STOP_SIGTERM, f);
- else if (!s->remain_after_exit || s->notify_access == NOTIFY_MAIN)
- /* The service has never been and will never be active */
- service_enter_signal(s, SERVICE_STOP_SIGTERM, SERVICE_FAILURE_PROTOCOL);
+ _fallthrough_;
+ case SERVICE_RUNNING:
+ service_enter_running(s, f);
break;
- }
- _fallthrough_;
- case SERVICE_RUNNING:
- service_enter_running(s, f);
- break;
+ case SERVICE_STOP_WATCHDOG:
+ case SERVICE_STOP_SIGTERM:
+ case SERVICE_STOP_SIGKILL:
- case SERVICE_STOP_WATCHDOG:
- case SERVICE_STOP_SIGTERM:
- case SERVICE_STOP_SIGKILL:
+ if (control_pid_good(s) <= 0)
+ service_enter_stop_post(s, f);
- if (control_pid_good(s) <= 0)
- service_enter_stop_post(s, f);
+ /* If there is still a control process, wait for that first */
+ break;
- /* If there is still a control process, wait for that first */
- break;
+ case SERVICE_STOP_POST:
- case SERVICE_STOP_POST:
+ if (control_pid_good(s) <= 0)
+ service_enter_signal(s, SERVICE_FINAL_SIGTERM, f);
- if (control_pid_good(s) <= 0)
- service_enter_signal(s, SERVICE_FINAL_SIGTERM, f);
+ break;
- break;
+ case SERVICE_FINAL_WATCHDOG:
+ case SERVICE_FINAL_SIGTERM:
+ case SERVICE_FINAL_SIGKILL:
- case SERVICE_FINAL_WATCHDOG:
- case SERVICE_FINAL_SIGTERM:
- case SERVICE_FINAL_SIGKILL:
-
- if (control_pid_good(s) <= 0)
- service_enter_dead(s, f, true);
- break;
+ if (control_pid_good(s) <= 0)
+ service_enter_dead(s, f, true);
+ break;
- default:
- assert_not_reached("Uh, main process died at wrong time.");
+ default:
+ assert_not_reached("Process died at the wrong time");
+ }
}
}
@@ -4491,6 +4500,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",
diff --git a/src/core/service.h b/src/core/service.h
index 6d931c3d5e..0d51fc3153 100644
--- a/src/core/service.h
+++ b/src/core/service.h
@@ -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_;
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index d3a5b25d18..31a6c63f0c 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -1992,6 +1992,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",
diff --git a/test/TEST-56-EXIT-TYPE/Makefile b/test/TEST-56-EXIT-TYPE/Makefile
new file mode 120000
index 0000000000..e9f93b1104
--- /dev/null
+++ b/test/TEST-56-EXIT-TYPE/Makefile
@@ -0,0 +1 @@
+../TEST-01-BASIC/Makefile \ No newline at end of file
diff --git a/test/TEST-56-EXIT-TYPE/test.sh b/test/TEST-56-EXIT-TYPE/test.sh
new file mode 100755
index 0000000000..0f84dca1ba
--- /dev/null
+++ b/test/TEST-56-EXIT-TYPE/test.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -e
+
+TEST_DESCRIPTION="test ExitType=cgroup"
+
+# shellcheck source=test/test-functions
+. "${TEST_BASE_DIR:?}/test-functions"
+
+do_test "$@"
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index de7d2c7daf..b5df300a6b 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -160,6 +160,7 @@ ExecStartPost=
ExecStartPre=
ExecStop=
ExecStopPost=
+ExitType=
ExtensionImages=
FailureAction=
FileDescriptorStoreMax=
diff --git a/test/units/testsuite-56.service b/test/units/testsuite-56.service
new file mode 100644
index 0000000000..d8ad589ca0
--- /dev/null
+++ b/test/units/testsuite-56.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=TEST-56-EXIT-TYPE
+
+[Service]
+ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
+Type=oneshot
diff --git a/test/units/testsuite-56.sh b/test/units/testsuite-56.sh
new file mode 100755
index 0000000000..b167320615
--- /dev/null
+++ b/test/units/testsuite-56.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+set -eux
+
+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) &
+
+systemd-notify --ready
+
+# process tree: systemd -> bash -> sleep
+sleep infinity
+EOF
+chmod +x /tmp/test56-exit-cgroup.sh
+
+# service should be stopped cleanly
+systemd-run --wait --unit=one -p Type=notify -p ExitType=cgroup \
+ -p ExecStartPost='bash -c "systemctl stop one &"' \
+ /tmp/test56-exit-cgroup.sh
+
+# same thing with a truthy exec condition
+systemd-run --wait --unit=two -p Type=notify -p ExitType=cgroup \
+ -p ExecCondition=true \
+ -p ExecStartPost='bash -c "systemctl stop two &"' \
+ /tmp/test56-exit-cgroup.sh
+
+# false exec condition: systemd-run should exit immediately with status code: 1
+systemd-run --wait --unit=three -p Type=notify -p ExitType=cgroup \
+ -p ExecCondition=false \
+ /tmp/test56-exit-cgroup.sh \
+ && { echo 'unexpected success'; exit 1; }
+
+# service should exit uncleanly (main process exits with SIGKILL)
+systemd-run --wait --unit=four -p Type=notify -p ExitType=cgroup \
+ -p ExecStartPost='bash -c "systemctl kill --signal 9 four &"' \
+ /tmp/test56-exit-cgroup.sh \
+ && { echo 'unexpected success'; exit 1; }
+
+
+# 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) &
+
+systemd-notify --ready
+EOF
+chmod +x /tmp/test56-exit-cgroup-parentless.sh
+
+# service should be stopped cleanly
+systemd-run --wait --unit=five -p Type=notify -p ExitType=cgroup \
+ -p ExecStartPost='bash -c "systemctl stop five &"' \
+ /tmp/test56-exit-cgroup-parentless.sh
+
+# service should still exit cleanly despite SIGKILL (the main process already exited cleanly)
+systemd-run --wait --unit=six -p Type=notify -p ExitType=cgroup \
+ -p ExecStartPost='bash -c "systemctl kill --signal 9 six &"' \
+ /tmp/test56-exit-cgroup-parentless.sh
+
+
+systemd-analyze log-level info
+
+echo OK >/testok
+
+exit 0