diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index d9da864bc2..2acb737c38 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -3695,6 +3695,19 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX
system.
+
+ $TRIGGER_UNIT
+ $TRIGGER_PATH
+
+ If the unit was activated dynamically (e.g.: a corresponding path unit), the
+ unit that triggered it and other type-dependent information will be passed via these variables. Note that
+ this information is provided in a best-effort way. For example, multiple triggers happening one after
+ another will be coalesced and only one will be reported, with no guarantee as to which one it will be.
+ Because of this, in most cases this variable will be primarily informational, i.e. useful for debugging
+ purposes, is lossy, and should not be relied upon to propagate a comprehensive reason for activation.
+
+
+
For system services, when PAMName= is enabled and pam_systemd is part
diff --git a/man/systemd.path.xml b/man/systemd.path.xml
index 4e4cd9137a..f143208cb4 100644
--- a/man/systemd.path.xml
+++ b/man/systemd.path.xml
@@ -208,6 +208,10 @@
See Also
+ Environment variables with details on the trigger will be set for triggered units. See the
+ Environment Variables Set on Triggered Units section in
+ systemd.exec1
+ for more details.
systemd1,
systemctl1,
diff --git a/src/core/path.c b/src/core/path.c
index 69bbddf158..2810e30573 100644
--- a/src/core/path.c
+++ b/src/core/path.c
@@ -197,9 +197,13 @@ int path_spec_fd_event(PathSpec *s, uint32_t revents) {
return 0;
}
-static bool path_spec_check_good(PathSpec *s, bool initial, bool from_trigger_notify) {
+static bool path_spec_check_good(PathSpec *s, bool initial, bool from_trigger_notify, char **ret_trigger_path) {
+ _cleanup_free_ char *trigger = NULL;
bool b, good = false;
+ assert(s);
+ assert(ret_trigger_path);
+
switch (s->type) {
case PATH_EXISTS:
@@ -207,7 +211,7 @@ static bool path_spec_check_good(PathSpec *s, bool initial, bool from_trigger_no
break;
case PATH_EXISTS_GLOB:
- good = glob_exists(s->path) > 0;
+ good = glob_first(s->path, &trigger) > 0;
break;
case PATH_DIRECTORY_NOT_EMPTY: {
@@ -229,6 +233,15 @@ static bool path_spec_check_good(PathSpec *s, bool initial, bool from_trigger_no
;
}
+ if (good) {
+ if (!trigger) {
+ trigger = strdup(s->path);
+ if (!trigger)
+ (void) log_oom_debug();
+ }
+ *ret_trigger_path = TAKE_PTR(trigger);
+ }
+
return good;
}
@@ -494,9 +507,11 @@ static void path_enter_dead(Path *p, PathResult f) {
path_set_state(p, p->result != PATH_SUCCESS ? PATH_FAILED : PATH_DEAD);
}
-static void path_enter_running(Path *p) {
+static void path_enter_running(Path *p, char *trigger_path) {
+ _cleanup_(activation_details_unrefp) ActivationDetails *details = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
Unit *trigger;
+ Job *job;
int r;
assert(p);
@@ -518,10 +533,22 @@ static void path_enter_running(Path *p) {
return;
}
- r = manager_add_job(UNIT(p)->manager, JOB_START, trigger, JOB_REPLACE, NULL, &error, NULL);
+ details = activation_details_new(UNIT(p));
+ if (!details) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ r = free_and_strdup(&(ACTIVATION_DETAILS_PATH(details))->trigger_path_filename, trigger_path);
if (r < 0)
goto fail;
+ r = manager_add_job(UNIT(p)->manager, JOB_START, trigger, JOB_REPLACE, NULL, &error, &job);
+ if (r < 0)
+ goto fail;
+
+ job_set_activation_details(job, details);
+
path_set_state(p, PATH_RUNNING);
path_unwatch(p);
@@ -532,17 +559,19 @@ fail:
path_enter_dead(p, PATH_FAILURE_RESOURCES);
}
-static bool path_check_good(Path *p, bool initial, bool from_trigger_notify) {
+static bool path_check_good(Path *p, bool initial, bool from_trigger_notify, char **ret_trigger_path) {
assert(p);
+ assert(ret_trigger_path);
LIST_FOREACH(spec, s, p->specs)
- if (path_spec_check_good(s, initial, from_trigger_notify))
+ if (path_spec_check_good(s, initial, from_trigger_notify, ret_trigger_path))
return true;
return false;
}
static void path_enter_waiting(Path *p, bool initial, bool from_trigger_notify) {
+ _cleanup_free_ char *trigger_path = NULL;
Unit *trigger;
int r;
@@ -554,9 +583,9 @@ static void path_enter_waiting(Path *p, bool initial, bool from_trigger_notify)
return;
}
- if (path_check_good(p, initial, from_trigger_notify)) {
+ if (path_check_good(p, initial, from_trigger_notify, &trigger_path)) {
log_unit_debug(UNIT(p), "Got triggered.");
- path_enter_running(p);
+ path_enter_running(p, trigger_path);
return;
}
@@ -568,9 +597,9 @@ static void path_enter_waiting(Path *p, bool initial, bool from_trigger_notify)
* might have appeared/been removed by now, so we must
* recheck */
- if (path_check_good(p, false, from_trigger_notify)) {
+ if (path_check_good(p, false, from_trigger_notify, &trigger_path)) {
log_unit_debug(UNIT(p), "Got triggered.");
- path_enter_running(p);
+ path_enter_running(p, trigger_path);
return;
}
@@ -759,7 +788,7 @@ static int path_dispatch_io(sd_event_source *source, int fd, uint32_t revents, v
goto fail;
if (changed)
- path_enter_running(p);
+ path_enter_running(p, found->path);
else
path_enter_waiting(p, false, false);
@@ -832,6 +861,89 @@ static int path_can_start(Unit *u) {
return 1;
}
+static void activation_details_path_done(ActivationDetails *details) {
+ ActivationDetailsPath *p = ASSERT_PTR(ACTIVATION_DETAILS_PATH(details));
+
+ p->trigger_path_filename = mfree(p->trigger_path_filename);
+}
+
+static void activation_details_path_serialize(ActivationDetails *details, FILE *f) {
+ ActivationDetailsPath *p = ASSERT_PTR(ACTIVATION_DETAILS_PATH(details));
+
+ assert(f);
+
+ if (p->trigger_path_filename)
+ (void) serialize_item(f, "activation-details-path-filename", p->trigger_path_filename);
+}
+
+static int activation_details_path_deserialize(const char *key, const char *value, ActivationDetails **details) {
+ int r;
+
+ assert(key);
+ assert(value);
+
+ if (!details || !*details)
+ return -EINVAL;
+
+ ActivationDetailsPath *p = ACTIVATION_DETAILS_PATH(*details);
+ if (!p)
+ return -EINVAL;
+
+ if (!streq(key, "activation-details-path-filename"))
+ return -EINVAL;
+
+ r = free_and_strdup(&p->trigger_path_filename, value);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int activation_details_path_append_env(ActivationDetails *details, char ***strv) {
+ ActivationDetailsPath *p = ACTIVATION_DETAILS_PATH(details);
+ char *s;
+ int r;
+
+ assert(details);
+ assert(strv);
+ assert(p);
+
+ if (isempty(p->trigger_path_filename))
+ return 0;
+
+ s = strjoin("TRIGGER_PATH=", p->trigger_path_filename);
+ if (!s)
+ return -ENOMEM;
+
+ r = strv_consume(strv, TAKE_PTR(s));
+ if (r < 0)
+ return r;
+
+ return 1; /* Return the number of variables added to the env block */
+}
+
+static int activation_details_path_append_pair(ActivationDetails *details, char ***strv) {
+ ActivationDetailsPath *p = ACTIVATION_DETAILS_PATH(details);
+ int r;
+
+ assert(details);
+ assert(strv);
+ assert(p);
+
+ if (isempty(p->trigger_path_filename))
+ return 0;
+
+ r = strv_extend(strv, "trigger_path");
+ if (r < 0)
+ return r;
+
+ r = strv_extend(strv, p->trigger_path_filename);
+ if (r < 0)
+ return r;
+
+ return 1; /* Return the number of pairs added to the env block */
+}
+
static const char* const path_type_table[_PATH_TYPE_MAX] = {
[PATH_EXISTS] = "PathExists",
[PATH_EXISTS_GLOB] = "PathExistsGlob",
@@ -890,3 +1002,13 @@ const UnitVTable path_vtable = {
.can_start = path_can_start,
};
+
+const ActivationDetailsVTable activation_details_path_vtable = {
+ .object_size = sizeof(ActivationDetailsPath),
+
+ .done = activation_details_path_done,
+ .serialize = activation_details_path_serialize,
+ .deserialize = activation_details_path_deserialize,
+ .append_env = activation_details_path_append_env,
+ .append_pair = activation_details_path_append_pair,
+};
diff --git a/src/core/path.h b/src/core/path.h
index d835c24166..c76103cc12 100644
--- a/src/core/path.h
+++ b/src/core/path.h
@@ -3,6 +3,7 @@
typedef struct Path Path;
typedef struct PathSpec PathSpec;
+typedef struct ActivationDetailsPath ActivationDetailsPath;
#include "unit.h"
@@ -66,9 +67,15 @@ struct Path {
RateLimit trigger_limit;
};
+struct ActivationDetailsPath {
+ ActivationDetails meta;
+ char *trigger_path_filename;
+};
+
void path_free_specs(Path *p);
extern const UnitVTable path_vtable;
+extern const ActivationDetailsVTable activation_details_path_vtable;
const char* path_type_to_string(PathType i) _const_;
PathType path_type_from_string(const char *s) _pure_;
@@ -77,3 +84,4 @@ const char* path_result_to_string(PathResult i) _const_;
PathResult path_result_from_string(const char *s) _pure_;
DEFINE_CAST(PATH, Path);
+DEFINE_ACTIVATION_DETAILS_CAST(ACTIVATION_DETAILS_PATH, ActivationDetailsPath, PATH);
diff --git a/src/core/unit.c b/src/core/unit.c
index 18d9ba85de..4255e0cf1f 100644
--- a/src/core/unit.c
+++ b/src/core/unit.c
@@ -5930,6 +5930,7 @@ int unit_get_dependency_array(const Unit *u, UnitDependencyAtom atom, Unit ***re
}
const ActivationDetailsVTable * const activation_details_vtable[_UNIT_TYPE_MAX] = {
+ [UNIT_PATH] = &activation_details_path_vtable,
};
ActivationDetails *activation_details_new(Unit *trigger_unit) {
diff --git a/test/testsuite-63.units/test63-glob.path b/test/testsuite-63.units/test63-glob.path
new file mode 100644
index 0000000000..5f237a9fcf
--- /dev/null
+++ b/test/testsuite-63.units/test63-glob.path
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Path]
+PathExistsGlob=/tmp/test63-glob*
diff --git a/test/testsuite-63.units/test63-glob.service b/test/testsuite-63.units/test63-glob.service
new file mode 100644
index 0000000000..3f49dd4620
--- /dev/null
+++ b/test/testsuite-63.units/test63-glob.service
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Service]
+ExecStartPre=sh -c 'test "$TRIGGER_PATH" = /tmp/test63-glob-foo'
+ExecStartPre=sh -c 'test "$TRIGGER_UNIT" = test63-glob.path'
+ExecStart=systemd-notify --ready
+RemainAfterExit=yes
+Type=notify
diff --git a/test/testsuite-63.units/test63.service b/test/testsuite-63.units/test63.service
index 1a8721d82c..01a928b8d6 100644
--- a/test/testsuite-63.units/test63.service
+++ b/test/testsuite-63.units/test63.service
@@ -3,4 +3,6 @@
ConditionPathExists=/tmp/nonexistent
[Service]
+ExecStartPre=sh -c 'test "$TRIGGER_PATH" = /tmp/test63'
+ExecStartPre=sh -c 'test "$TRIGGER_UNIT" = test63.path'
ExecStart=true
diff --git a/test/units/testsuite-63.sh b/test/units/testsuite-63.sh
index 20d93936b3..7ee7fc1513 100755
--- a/test/units/testsuite-63.sh
+++ b/test/units/testsuite-63.sh
@@ -28,6 +28,19 @@ test "$(systemctl show test63.service -P Result)" = success
test "$(systemctl show test63.path -P ActiveState)" = active
test "$(systemctl show test63.path -P Result)" = success
+# Test that glob matching works too, with $TRIGGER_PATH
+systemctl start test63-glob.path
+touch /tmp/test63-glob-foo
+timeout 60 bash -c 'while ! systemctl -q is-active test63-glob.service; do sleep .2; done'
+test "$(systemctl show test63-glob.service -P ActiveState)" = active
+test "$(systemctl show test63-glob.service -P Result)" = success
+
+test "$(busctl --json=short get-property org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/test63_2dglob_2eservice org.freedesktop.systemd1.Unit ActivationDetails)" = '{"type":"a(ss)","data":[["trigger_unit","test63-glob.path"],["trigger_path","/tmp/test63-glob-foo"]]}'
+
+systemctl stop test63-glob.path test63-glob.service
+
+test "$(busctl --json=short get-property org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/test63_2dglob_2eservice org.freedesktop.systemd1.Unit ActivationDetails)" = '{"type":"a(ss)","data":[]}'
+
systemctl log-level info
echo OK >/testok