diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md
index 9f69a3162a..9fa856ec21 100644
--- a/docs/TRANSIENT-SETTINGS.md
+++ b/docs/TRANSIENT-SETTINGS.md
@@ -303,6 +303,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 aff43217e1..614871bce2 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -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 {
+
+
@@ -3320,6 +3324,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
diff --git a/man/systemd.service.xml b/man/systemd.service.xml
index 350bc5f8e5..50d1a1d85d 100644
--- a/man/systemd.service.xml
+++ b/man/systemd.service.xml
@@ -255,6 +255,31 @@
+
+ ExitType=
+
+
+ Configures the process exit type for this service unit. One of or
+ :
+
+
+ If set to (the default), the service manager
+ will consider the unit stopped when the main process, which is determined according to the `Type`, exits.
+
+
+ The 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.
+
+
+ It is generally recommended to use ExitType= when a service has
+ a known forking model and a main process can reliably be determined. ExitType=
+ is well suited for transient or automatically generated services, such as graphical
+ applications inside of a desktop environment.
+
+
+
RemainAfterExit=
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 bd70c13ba3..d7af226105 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 f7cdb51eba..73906ebab7 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.m4 b/src/core/load-fragment-gperf.gperf.m4
index 21bbcffe41..5ef785c0de 100644
--- a/src/core/load-fragment-gperf.gperf.m4
+++ b/src/core/load-fragment-gperf.gperf.m4
@@ -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)
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index c6fc4fe083..ead7f7bf11 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -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" },
diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h
index b8a6d5fead..4746a8a792 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 df3ed05cfc..550db40631 100644
--- a/src/core/service.c
+++ b/src/core/service.c
@@ -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",
diff --git a/src/core/service.h b/src/core/service.h
index af474aa40e..92c1caf794 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 a75178068b..84d4729334 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -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",
diff --git a/src/xdg-autostart-generator/xdg-autostart-service.c b/src/xdg-autostart-generator/xdg-autostart-service.c
index 1528432f43..00d8b76539 100644
--- a/src/xdg-autostart-generator/xdg-autostart-service.c
+++ b/src/xdg-autostart-generator/xdg-autostart-service.c
@@ -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"
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..fc321e4141
--- /dev/null
+++ b/test/TEST-56-EXIT-TYPE/test.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+TEST_DESCRIPTION="test ExitType=cgroup"
+. $TEST_BASE_DIR/test-functions
+
+do_test "$@" 56
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index 55204463c8..310dbd6add 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -94,6 +94,7 @@ ExecStartPre=
ExecStop=
ExecStopPost=
ExecStopPre=
+ExitType=
FailureAction=
FileDescriptorName=
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..9de03e1dea
--- /dev/null
+++ b/test/units/testsuite-56.sh
@@ -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 < 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 < 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 < 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 < 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