diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 120ffbc8ef..689ca3ec88 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -1898,6 +1898,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly s CollectMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly as Refs = ['...', ...]; + readonly a(ss) ActivationDetails = [...]; }; interface org.freedesktop.DBus.Peer { ... }; interface org.freedesktop.DBus.Introspectable { ... }; @@ -2229,6 +2230,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -2406,6 +2409,18 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { Transient contains a boolean that indicates whether the unit was created as a transient unit (i.e. via CreateTransientUnit() on the manager object). + + ActivationDetails contains a list of string pairs, key and value, that + describe the event that caused the unit to be activated, if any. The key describes the information + (e.g.: trigger_unit, with value foo.service). This is only filled + in if the unit was triggered by a Path or Timer unit, and it is + only provided in a best effort fashion: it is not guaranteed to be set, and it is not guaranteed to be + the only trigger. It is only guaranteed to be a valid trigger that caused the activation job to be + enqueued and complete successfully. The key value pairs correspond (in lowercase) to the environment + variables described in the Environment Variables Set on Triggered Units section in + systemd.exec1. + Note that new key value pair may be added at any time in future versions. Existing entries will not be + removed. @@ -10650,6 +10665,8 @@ node /org/freedesktop/systemd1/job/666 { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s JobType = '...'; readonly s State = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(ss) ActivationDetails = [...]; }; interface org.freedesktop.DBus.Peer { ... }; interface org.freedesktop.DBus.Introspectable { ... }; @@ -10681,6 +10698,8 @@ node /org/freedesktop/systemd1/job/666 { + + @@ -10709,6 +10728,9 @@ node /org/freedesktop/systemd1/job/666 { State refers to the job's state and is one of waiting and running. The former indicates that a job is currently queued but has not begun to execute yet. The latter indicates that a job is currently being executed. + + ActivationDetails has the same content as the property of the same name under + the org.freedesktop.systemd1.Unit interface. diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index d9da864bc2..2e843be2e5 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -3695,6 +3695,21 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX system. + + $TRIGGER_UNIT + $TRIGGER_PATH + $TRIGGER_TIMER_REALTIME_USEC + $TRIGGER_TIMER_MONOTONIC_USEC + + If the unit was activated dynamically (e.g.: a corresponding path unit or timer 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/man/systemd.timer.xml b/man/systemd.timer.xml index 49bcb18be5..953faa9b33 100644 --- a/man/systemd.timer.xml +++ b/man/systemd.timer.xml @@ -366,6 +366,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/basic/glob-util.c b/src/basic/glob-util.c index e026b29478..fd60a6eda2 100644 --- a/src/basic/glob-util.c +++ b/src/basic/glob-util.c @@ -47,17 +47,28 @@ int safe_glob(const char *path, int flags, glob_t *pglob) { return 0; } -int glob_exists(const char *path) { +int glob_first(const char *path, char **ret_first) { _cleanup_globfree_ glob_t g = {}; int k; assert(path); k = safe_glob(path, GLOB_NOSORT|GLOB_BRACE, &g); - if (k == -ENOENT) + if (k == -ENOENT) { + if (ret_first) + *ret_first = NULL; return false; + } if (k < 0) return k; + + if (ret_first) { + char *first = strdup(g.gl_pathv[0]); + if (!first) + return log_oom_debug(); + *ret_first = first; + } + return true; } diff --git a/src/basic/glob-util.h b/src/basic/glob-util.h index fc86e990dd..7ca26cc27f 100644 --- a/src/basic/glob-util.h +++ b/src/basic/glob-util.h @@ -10,7 +10,9 @@ /* Note: this function modifies pglob to set various functions. */ int safe_glob(const char *path, int flags, glob_t *pglob); -int glob_exists(const char *path); +/* Note: which match is returned depends on the implementation/system and not guaranteed to be stable */ +int glob_first(const char *path, char **ret_first); +#define glob_exists(path) glob_first(path, NULL) int glob_extend(char ***strv, const char *path, int flags); int glob_non_glob_prefix(const char *path, char **ret); diff --git a/src/core/dbus-job.c b/src/core/dbus-job.c index de474e6d4e..7b1438266b 100644 --- a/src/core/dbus-job.c +++ b/src/core/dbus-job.c @@ -7,6 +7,7 @@ #include "bus-util.h" #include "dbus-job.h" #include "dbus-unit.h" +#include "dbus-util.h" #include "dbus.h" #include "job.h" #include "log.h" @@ -136,6 +137,7 @@ const sd_bus_vtable bus_job_vtable[] = { SD_BUS_PROPERTY("Unit", "(so)", property_get_unit, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("JobType", "s", property_get_type, offsetof(Job, type), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("State", "s", property_get_state, offsetof(Job, state), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("ActivationDetails", "a(ss)", bus_property_get_activation_details, offsetof(Job, activation_details), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_VTABLE_END }; diff --git a/src/core/dbus-unit.c b/src/core/dbus-unit.c index ee013e1bc5..7a16471758 100644 --- a/src/core/dbus-unit.c +++ b/src/core/dbus-unit.c @@ -951,6 +951,7 @@ const sd_bus_vtable bus_unit_vtable[] = { SD_BUS_PROPERTY("InvocationID", "ay", bus_property_get_id128, offsetof(Unit, invocation_id), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("CollectMode", "s", property_get_collect_mode, offsetof(Unit, collect_mode), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("Refs", "as", property_get_refs, 0, 0), + SD_BUS_PROPERTY("ActivationDetails", "a(ss)", bus_property_get_activation_details, offsetof(Unit, activation_details), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_METHOD_WITH_ARGS("Start", SD_BUS_ARGS("s", mode), diff --git a/src/core/dbus-util.c b/src/core/dbus-util.c index 264a4f55b6..edfa0eb69a 100644 --- a/src/core/dbus-util.c +++ b/src/core/dbus-util.c @@ -228,3 +228,35 @@ int bus_read_mount_options( return 0; } + +int bus_property_get_activation_details( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ActivationDetails **details = ASSERT_PTR(userdata); + _cleanup_strv_free_ char **pairs = NULL; + int r; + + assert(reply); + + r = activation_details_append_pair(*details, &pairs); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(ss)"); + if (r < 0) + return r; + + STRV_FOREACH_PAIR(key, value, pairs) { + r = sd_bus_message_append(reply, "(ss)", *key, *value); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} diff --git a/src/core/dbus-util.h b/src/core/dbus-util.h index 799136737b..e12631a0e2 100644 --- a/src/core/dbus-util.h +++ b/src/core/dbus-util.h @@ -251,3 +251,5 @@ static inline int bus_set_transient_usec_fix_0(Unit *u, const char *name, usec_t int bus_verify_manage_units_async_full(Unit *u, const char *verb, int capability, const char *polkit_message, bool interactive, sd_bus_message *call, sd_bus_error *error); int bus_read_mount_options(sd_bus_message *message, sd_bus_error *error, MountOptions **ret_options, char **ret_format_str, const char *separator); + +int bus_property_get_activation_details(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error); diff --git a/src/core/job.c b/src/core/job.c index 6653dbde84..dd8d858bd2 100644 --- a/src/core/job.c +++ b/src/core/job.c @@ -104,6 +104,8 @@ Job* job_free(Job *j) { sd_bus_track_unref(j->bus_track); strv_free(j->deserialized_clients); + activation_details_unref(j->activation_details); + return mfree(j); } @@ -180,9 +182,13 @@ static void job_merge_into_installed(Job *j, Job *other) { assert(j->installed); assert(j->unit == other->unit); - if (j->type != JOB_NOP) + if (j->type != JOB_NOP) { assert_se(job_type_merge_and_collapse(&j->type, other->type, j->unit) == 0); - else + + /* Keep the oldest ActivationDetails, if any */ + if (!j->activation_details) + j->activation_details = TAKE_PTR(other->activation_details); + } else assert(other->type == JOB_NOP); j->irreversible = j->irreversible || other->irreversible; @@ -776,6 +782,7 @@ static void job_emit_done_message(Unit *u, uint32_t job_id, JobType t, JobResult } static int job_perform_on_unit(Job **j) { + ActivationDetails *a; uint32_t id; Manager *m; JobType t; @@ -795,10 +802,11 @@ static int job_perform_on_unit(Job **j) { u = (*j)->unit; t = (*j)->type; id = (*j)->id; + a = (*j)->activation_details; switch (t) { case JOB_START: - r = unit_start(u); + r = unit_start(u, a); break; case JOB_RESTART: @@ -1160,6 +1168,8 @@ int job_serialize(Job *j, FILE *f) { bus_track_serialize(j->bus_track, f, "subscribed"); + activation_details_serialize(j->activation_details, f); + /* End marker */ fputc('\n', f); return 0; @@ -1257,6 +1267,11 @@ int job_deserialize(Job *j, FILE *f) { else if (streq(l, "subscribed")) { if (strv_extend(&j->deserialized_clients, v) < 0) return log_oom(); + + } else if (startswith(l, "activation-details")) { + if (activation_details_deserialize(l, v, &j->activation_details) < 0) + log_debug("Failed to parse job ActivationDetails element: %s", v); + } else log_debug("Unknown job serialization key: %s", l); } @@ -1636,3 +1651,11 @@ int job_compare(Job *a, Job *b, UnitDependencyAtom assume_dep) { else return -1; } + +void job_set_activation_details(Job *j, ActivationDetails *info) { + /* Existing (older) ActivationDetails win, newer ones are discarded. */ + if (!j || j->activation_details || !info) + return; /* Nothing to do. */ + + j->activation_details = activation_details_ref(info); +} diff --git a/src/core/job.h b/src/core/job.h index c033c8a4fa..0305e0ea44 100644 --- a/src/core/job.h +++ b/src/core/job.h @@ -10,6 +10,7 @@ #include "unit-name.h" #include "unit.h" +typedef struct ActivationDetails ActivationDetails; typedef struct Job Job; typedef struct JobDependency JobDependency; typedef enum JobType JobType; @@ -151,6 +152,9 @@ struct Job { unsigned run_queue_idx; + /* If the job had a specific trigger that needs to be advertised (eg: a path unit), store it. */ + ActivationDetails *activation_details; + bool installed:1; bool in_run_queue:1; bool matters_to_anchor:1; @@ -243,3 +247,5 @@ JobResult job_result_from_string(const char *s) _pure_; const char* job_type_to_access_method(JobType t); int job_compare(Job *a, Job *b, UnitDependencyAtom assume_dep); + +void job_set_activation_details(Job *j, ActivationDetails *info); 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/service.c b/src/core/service.c index a715a1d1dc..fa37207dcc 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -1641,6 +1641,16 @@ static int service_spawn_internal( } } + if (UNIT(s)->activation_details) { + r = activation_details_append_env(UNIT(s)->activation_details, &our_env); + if (r < 0) + return r; + /* The number of env vars added here can vary, rather than keeping the allocation block in + * sync manually, these functions simply use the strv methods to append to it, so we need + * to update n_env when we are done in case of future usage. */ + n_env += r; + } + r = unit_set_exec_params(UNIT(s), &exec_params); if (r < 0) return r; diff --git a/src/core/timer.c b/src/core/timer.c index 9de325ba66..b89d593b75 100644 --- a/src/core/timer.c +++ b/src/core/timer.c @@ -576,8 +576,10 @@ fail: } static void timer_enter_running(Timer *t) { + _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(t); @@ -593,11 +595,20 @@ static void timer_enter_running(Timer *t) { return; } - r = manager_add_job(UNIT(t)->manager, JOB_START, trigger, JOB_REPLACE, NULL, &error, NULL); + details = activation_details_new(UNIT(t)); + if (!details) { + r = -ENOMEM; + goto fail; + } + + r = manager_add_job(UNIT(t)->manager, JOB_START, trigger, JOB_REPLACE, NULL, &error, &job); if (r < 0) goto fail; dual_timestamp_get(&t->last_trigger); + ACTIVATION_DETAILS_TIMER(details)->last_trigger = t->last_trigger; + + job_set_activation_details(job, details); if (t->stamp_path) touch_file(t->stamp_path, true, t->last_trigger.realtime, UID_INVALID, GID_INVALID, MODE_INVALID); @@ -892,6 +903,91 @@ static int timer_can_start(Unit *u) { return 1; } +static void activation_details_timer_serialize(ActivationDetails *details, FILE *f) { + ActivationDetailsTimer *t = ACTIVATION_DETAILS_TIMER(details); + + assert(details); + assert(f); + assert(t); + + (void) serialize_dual_timestamp(f, "activation-details-timer-last-trigger", &t->last_trigger); +} + +static int activation_details_timer_deserialize(const char *key, const char *value, ActivationDetails **details) { + int r; + + assert(key); + assert(value); + + if (!details || !*details) + return -EINVAL; + + ActivationDetailsTimer *t = ACTIVATION_DETAILS_TIMER(*details); + if (!t) + return -EINVAL; + + if (!streq(key, "activation-details-timer-last-trigger")) + return -EINVAL; + + r = deserialize_dual_timestamp(value, &t->last_trigger); + if (r < 0) + return r; + + return 0; +} + +static int activation_details_timer_append_env(ActivationDetails *details, char ***strv) { + ActivationDetailsTimer *t = ACTIVATION_DETAILS_TIMER(details); + int r; + + assert(details); + assert(strv); + assert(t); + + if (!dual_timestamp_is_set(&t->last_trigger)) + return 0; + + r = strv_extendf(strv, "TRIGGER_TIMER_REALTIME_USEC=%" USEC_FMT, t->last_trigger.realtime); + if (r < 0) + return r; + + r = strv_extendf(strv, "TRIGGER_TIMER_MONOTONIC_USEC=%" USEC_FMT, t->last_trigger.monotonic); + if (r < 0) + return r; + + return 2; /* Return the number of variables added to the env block */ +} + +static int activation_details_timer_append_pair(ActivationDetails *details, char ***strv) { + ActivationDetailsTimer *t = ACTIVATION_DETAILS_TIMER(details); + int r; + + assert(details); + assert(strv); + assert(t); + + if (!dual_timestamp_is_set(&t->last_trigger)) + return 0; + + r = strv_extend(strv, "trigger_timer_realtime_usec"); + if (r < 0) + return r; + + r = strv_extendf(strv, "%" USEC_FMT, t->last_trigger.realtime); + if (r < 0) + return r; + + r = strv_extend(strv, "trigger_timer_monotonic_usec"); + if (r < 0) + return r; + + r = strv_extendf(strv, "%" USEC_FMT, t->last_trigger.monotonic); + if (r < 0) + return r; + + return 2; /* Return the number of pairs added to the env block */ +} + static const char* const timer_base_table[_TIMER_BASE_MAX] = { [TIMER_ACTIVE] = "OnActiveSec", [TIMER_BOOT] = "OnBootSec", @@ -954,3 +1050,12 @@ const UnitVTable timer_vtable = { .can_start = timer_can_start, }; + +const ActivationDetailsVTable activation_details_timer_vtable = { + .object_size = sizeof(ActivationDetailsTimer), + + .serialize = activation_details_timer_serialize, + .deserialize = activation_details_timer_deserialize, + .append_env = activation_details_timer_append_env, + .append_pair = activation_details_timer_append_pair, +}; diff --git a/src/core/timer.h b/src/core/timer.h index 551e283341..914e42580e 100644 --- a/src/core/timer.h +++ b/src/core/timer.h @@ -2,6 +2,7 @@ #pragma once typedef struct Timer Timer; +typedef struct ActivationDetailsTimer ActivationDetailsTimer; #include "calendarspec.h" #include "unit.h" @@ -64,11 +65,17 @@ struct Timer { char *stamp_path; }; +struct ActivationDetailsTimer { + ActivationDetails meta; + dual_timestamp last_trigger; +}; + #define TIMER_MONOTONIC_CLOCK(t) ((t)->wake_system ? CLOCK_BOOTTIME_ALARM : CLOCK_MONOTONIC) void timer_free_values(Timer *t); extern const UnitVTable timer_vtable; +extern const ActivationDetailsVTable activation_details_timer_vtable; const char *timer_base_to_string(TimerBase i) _const_; TimerBase timer_base_from_string(const char *s) _pure_; @@ -77,3 +84,4 @@ const char* timer_result_to_string(TimerResult i) _const_; TimerResult timer_result_from_string(const char *s) _pure_; DEFINE_CAST(TIMER, Timer); +DEFINE_ACTIVATION_DETAILS_CAST(ACTIVATION_DETAILS_TIMER, ActivationDetailsTimer, TIMER); diff --git a/src/core/unit.c b/src/core/unit.c index 6242f045b4..5f1c6109b0 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -41,6 +41,7 @@ #include "path-util.h" #include "process-util.h" #include "rm-rf.h" +#include "serialize.h" #include "set.h" #include "signal-util.h" #include "sparse-endian.h" @@ -807,6 +808,8 @@ Unit* unit_free(Unit *u) { set_free_free(u->aliases); free(u->id); + activation_details_unref(u->activation_details); + return mfree(u); } @@ -1188,6 +1191,9 @@ int unit_merge(Unit *u, Unit *other) { other->load_state = UNIT_MERGED; other->merged_into = u; + if (!u->activation_details) + u->activation_details = activation_details_ref(other->activation_details); + /* If there is still some data attached to the other node, we * don't need it anymore, and can free it. */ if (other->load_state != UNIT_STUB) @@ -1861,7 +1867,7 @@ static bool unit_verify_deps(Unit *u) { * -ESTALE: This unit has been started before and can't be started a second time * -ENOENT: This is a triggering unit and unit to trigger is not loaded */ -int unit_start(Unit *u) { +int unit_start(Unit *u, ActivationDetails *details) { UnitActiveState state; Unit *following; int r; @@ -1918,7 +1924,7 @@ int unit_start(Unit *u) { following = unit_following(u); if (following) { log_unit_debug(u, "Redirecting start request from %s to %s.", u->id, following->id); - return unit_start(following); + return unit_start(following, details); } /* Check our ability to start early so that failure conditions don't cause us to enter a busy loop. */ @@ -1939,6 +1945,9 @@ int unit_start(Unit *u) { unit_add_to_dbus_queue(u); unit_cgroup_freezer_action(u, FREEZER_THAW); + if (!u->activation_details) /* Older details object wins */ + u->activation_details = activation_details_ref(details); + return UNIT_VTABLE(u)->start(u); } @@ -5919,3 +5928,154 @@ int unit_get_dependency_array(const Unit *u, UnitDependencyAtom atom, Unit ***re assert(n <= INT_MAX); return (int) n; } + +const ActivationDetailsVTable * const activation_details_vtable[_UNIT_TYPE_MAX] = { + [UNIT_PATH] = &activation_details_path_vtable, + [UNIT_TIMER] = &activation_details_timer_vtable, +}; + +ActivationDetails *activation_details_new(Unit *trigger_unit) { + _cleanup_free_ ActivationDetails *details = NULL; + + assert(trigger_unit); + assert(trigger_unit->type != _UNIT_TYPE_INVALID); + assert(trigger_unit->id); + + details = malloc0(activation_details_vtable[trigger_unit->type]->object_size); + if (!details) + return NULL; + + *details = (ActivationDetails) { + .n_ref = 1, + .trigger_unit_type = trigger_unit->type, + }; + + details->trigger_unit_name = strdup(trigger_unit->id); + if (!details->trigger_unit_name) + return NULL; + + if (ACTIVATION_DETAILS_VTABLE(details)->init) + ACTIVATION_DETAILS_VTABLE(details)->init(details, trigger_unit); + + return TAKE_PTR(details); +} + +static ActivationDetails *activation_details_free(ActivationDetails *details) { + if (!details) + return NULL; + + if (ACTIVATION_DETAILS_VTABLE(details)->done) + ACTIVATION_DETAILS_VTABLE(details)->done(details); + + free(details->trigger_unit_name); + + return mfree(details); +} + +void activation_details_serialize(ActivationDetails *details, FILE *f) { + if (!details || details->trigger_unit_type == _UNIT_TYPE_INVALID) + return; + + (void) serialize_item(f, "activation-details-unit-type", unit_type_to_string(details->trigger_unit_type)); + if (details->trigger_unit_name) + (void) serialize_item(f, "activation-details-unit-name", details->trigger_unit_name); + if (ACTIVATION_DETAILS_VTABLE(details)->serialize) + ACTIVATION_DETAILS_VTABLE(details)->serialize(details, f); +} + +int activation_details_deserialize(const char *key, const char *value, ActivationDetails **details) { + assert(key); + assert(value); + assert(details); + + if (!*details) { + UnitType t; + + if (!streq(key, "activation-details-unit-type")) + return -EINVAL; + + t = unit_type_from_string(value); + if (t == _UNIT_TYPE_INVALID) + return -EINVAL; + + *details = malloc0(activation_details_vtable[t]->object_size); + if (!*details) + return -ENOMEM; + + **details = (ActivationDetails) { + .n_ref = 1, + .trigger_unit_type = t, + }; + + return 0; + } + + if (streq(key, "activation-details-unit-name")) { + (*details)->trigger_unit_name = strdup(value); + if (!(*details)->trigger_unit_name) + return -ENOMEM; + + return 0; + } + + if (ACTIVATION_DETAILS_VTABLE(*details)->deserialize) + return ACTIVATION_DETAILS_VTABLE(*details)->deserialize(key, value, details); + + return -EINVAL; +} + +int activation_details_append_env(ActivationDetails *details, char ***strv) { + int r = 0; + + assert(strv); + + if (!details) + return 0; + + if (!isempty(details->trigger_unit_name)) { + char *s = strjoin("TRIGGER_UNIT=", details->trigger_unit_name); + if (!s) + return -ENOMEM; + + r = strv_consume(strv, TAKE_PTR(s)); + if (r < 0) + return r; + } + + if (ACTIVATION_DETAILS_VTABLE(details)->append_env) { + r = ACTIVATION_DETAILS_VTABLE(details)->append_env(details, strv); + if (r < 0) + return r; + } + + return r + !isempty(details->trigger_unit_name); /* Return the number of variables added to the env block */ +} + +int activation_details_append_pair(ActivationDetails *details, char ***strv) { + int r = 0; + + assert(strv); + + if (!details) + return 0; + + if (!isempty(details->trigger_unit_name)) { + r = strv_extend(strv, "trigger_unit"); + if (r < 0) + return r; + + r = strv_extend(strv, details->trigger_unit_name); + if (r < 0) + return r; + } + + if (ACTIVATION_DETAILS_VTABLE(details)->append_env) { + r = ACTIVATION_DETAILS_VTABLE(details)->append_pair(details, strv); + if (r < 0) + return r; + } + + return r + !isempty(details->trigger_unit_name); /* Return the number of pairs added to the strv */ +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(ActivationDetails, activation_details, activation_details_free); diff --git a/src/core/unit.h b/src/core/unit.h index 4dabbb9a3c..fc8edaade5 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -110,6 +110,75 @@ typedef union UnitDependencyInfo { } _packed_; } UnitDependencyInfo; +/* Store information about why a unit was activated. + * We start with trigger units (.path/.timer), eventually it will be expanded to include more metadata. */ +typedef struct ActivationDetails { + unsigned n_ref; + UnitType trigger_unit_type; + char *trigger_unit_name; +} ActivationDetails; + +/* For casting an activation event into the various unit-specific types */ +#define DEFINE_ACTIVATION_DETAILS_CAST(UPPERCASE, MixedCase, UNIT_TYPE) \ + static inline MixedCase* UPPERCASE(ActivationDetails *a) { \ + if (_unlikely_(!a || a->trigger_unit_type != UNIT_##UNIT_TYPE)) \ + return NULL; \ + \ + return (MixedCase*) a; \ + } + +/* For casting the various unit types into a unit */ +#define ACTIVATION_DETAILS(u) \ + ({ \ + typeof(u) _u_ = (u); \ + ActivationDetails *_w_ = _u_ ? &(_u_)->meta : NULL; \ + _w_; \ + }) + +ActivationDetails *activation_details_new(Unit *trigger_unit); +ActivationDetails *activation_details_ref(ActivationDetails *p); +ActivationDetails *activation_details_unref(ActivationDetails *p); +void activation_details_serialize(ActivationDetails *p, FILE *f); +int activation_details_deserialize(const char *key, const char *value, ActivationDetails **info); +int activation_details_append_env(ActivationDetails *info, char ***strv); +int activation_details_append_pair(ActivationDetails *info, char ***strv); +DEFINE_TRIVIAL_CLEANUP_FUNC(ActivationDetails*, activation_details_unref); + +typedef struct ActivationDetailsVTable { + /* How much memory does an object of this activation type need */ + size_t object_size; + + /* This should reset all type-specific variables. This should not allocate memory, and is called + * with zero-initialized data. It should hence only initialize variables that need to be set != 0. */ + void (*init)(ActivationDetails *info, Unit *trigger_unit); + + /* This should free all type-specific variables. It should be idempotent. */ + void (*done)(ActivationDetails *info); + + /* This should serialize all type-specific variables. */ + void (*serialize)(ActivationDetails *info, FILE *f); + + /* This should deserialize all type-specific variables, one at a time. */ + int (*deserialize)(const char *key, const char *value, ActivationDetails **info); + + /* This should format the type-specific variables for the env block of the spawned service, + * and return the number of added items. */ + int (*append_env)(ActivationDetails *info, char ***strv); + + /* This should append type-specific variables as key/value pairs for the D-Bus property of the job, + * and return the number of added pairs. */ + int (*append_pair)(ActivationDetails *info, char ***strv); +} ActivationDetailsVTable; + +extern const ActivationDetailsVTable * const activation_details_vtable[_UNIT_TYPE_MAX]; + +static inline const ActivationDetailsVTable* ACTIVATION_DETAILS_VTABLE(const ActivationDetails *a) { + assert(a); + assert(a->trigger_unit_type < _UNIT_TYPE_MAX); + + return activation_details_vtable[a->trigger_unit_type]; +} + /* Newer LLVM versions don't like implicit casts from large pointer types to smaller enums, hence let's add * explicit type-safe helpers for that. */ static inline UnitDependency UNIT_DEPENDENCY_FROM_PTR(const void *p) { @@ -363,6 +432,9 @@ typedef struct Unit { JobMode on_success_job_mode; JobMode on_failure_job_mode; + /* If the job had a specific trigger that needs to be advertised (eg: a path unit), store it. */ + ActivationDetails *activation_details; + /* Tweaking the GC logic */ CollectMode collect_mode; @@ -813,7 +885,7 @@ bool unit_can_start(Unit *u) _pure_; bool unit_can_stop(Unit *u) _pure_; bool unit_can_isolate(Unit *u) _pure_; -int unit_start(Unit *u); +int unit_start(Unit *u, ActivationDetails *details); int unit_stop(Unit *u); int unit_reload(Unit *u); diff --git a/src/test/test-bpf-firewall.c b/src/test/test-bpf-firewall.c index 3b99c5aaec..d655058d3d 100644 --- a/src/test/test-bpf-firewall.c +++ b/src/test/test-bpf-firewall.c @@ -176,7 +176,7 @@ int main(int argc, char *argv[]) { assert_se(r >= 0); - assert_se(unit_start(u) >= 0); + assert_se(unit_start(u, NULL) >= 0); while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) assert_se(sd_event_run(m->event, UINT64_MAX) >= 0); @@ -201,7 +201,7 @@ int main(int argc, char *argv[]) { SERVICE(u)->type = SERVICE_ONESHOT; u->load_state = UNIT_LOADED; - assert_se(unit_start(u) >= 0); + assert_se(unit_start(u, NULL) >= 0); while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) assert_se(sd_event_run(m->event, UINT64_MAX) >= 0); diff --git a/src/test/test-bpf-foreign-programs.c b/src/test/test-bpf-foreign-programs.c index 061426f287..0445c39855 100644 --- a/src/test/test-bpf-foreign-programs.c +++ b/src/test/test-bpf-foreign-programs.c @@ -246,7 +246,7 @@ static int test_bpf_cgroup_programs(Manager *m, const char *unit_name, const Tes SERVICE(u)->type = SERVICE_ONESHOT; u->load_state = UNIT_LOADED; - r = unit_start(u); + r = unit_start(u, NULL); if (r < 0) return log_error_errno(r, "Unit start failed %m"); diff --git a/src/test/test-bpf-lsm.c b/src/test/test-bpf-lsm.c index 9709053d0a..e1df62f1a6 100644 --- a/src/test/test-bpf-lsm.c +++ b/src/test/test-bpf-lsm.c @@ -39,7 +39,7 @@ static int test_restrict_filesystems(Manager *m, const char *unit_name, const ch SERVICE(u)->type = SERVICE_ONESHOT; u->load_state = UNIT_LOADED; - r = unit_start(u); + r = unit_start(u, NULL); if (r < 0) return log_error_errno(r, "Unit start failed %m"); diff --git a/src/test/test-execute.c b/src/test/test-execute.c index 82e4cf5e54..0283caeca6 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -210,7 +210,7 @@ static void _test(const char *file, unsigned line, const char *func, assert_se(unit_name); assert_se(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit) >= 0); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); check_main_result(file, line, func, m, unit, status_expected, code_expected); } #define test(m, unit_name, status_expected, code_expected) \ @@ -223,7 +223,7 @@ static void _test_service(const char *file, unsigned line, const char *func, assert_se(unit_name); assert_se(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit) >= 0); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); check_service_result(file, line, func, m, unit, result_expected); } #define test_service(m, unit_name, result_expected) \ diff --git a/src/test/test-glob-util.c b/src/test/test-glob-util.c index ec8b74f48f..566b68b589 100644 --- a/src/test/test-glob-util.c +++ b/src/test/test-glob-util.c @@ -13,6 +13,27 @@ #include "tests.h" #include "tmpfile-util.h" +TEST(glob_first) { + char *first, name[] = "/tmp/test-glob_first.XXXXXX"; + int fd = -1; + int r; + + fd = mkostemp_safe(name); + assert_se(fd >= 0); + close(fd); + + r = glob_first("/tmp/test-glob_first*", &first); + assert_se(r == 1); + assert_se(streq(name, first)); + first = mfree(first); + + r = unlink(name); + assert_se(r == 0); + r = glob_first("/tmp/test-glob_first*", &first); + assert_se(r == 0); + assert_se(first == NULL); +} + TEST(glob_exists) { char name[] = "/tmp/test-glob_exists.XXXXXX"; int fd = -1; diff --git a/src/test/test-path.c b/src/test/test-path.c index 7fb1f7363c..a440ca08b4 100644 --- a/src/test/test-path.c +++ b/src/test/test-path.c @@ -136,7 +136,7 @@ static void test_path_exists(Manager *m) { path = PATH(unit); service = service_for_path(m, path, NULL); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -170,7 +170,7 @@ static void test_path_existsglob(Manager *m) { path = PATH(unit); service = service_for_path(m, path, NULL); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -205,7 +205,7 @@ static void test_path_changed(Manager *m) { path = PATH(unit); service = service_for_path(m, path, NULL); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -247,7 +247,7 @@ static void test_path_modified(Manager *m) { path = PATH(unit); service = service_for_path(m, path, NULL); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -288,7 +288,7 @@ static void test_path_unit(Manager *m) { path = PATH(unit); service = service_for_path(m, path, "path-mycustomunit.service"); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -319,7 +319,7 @@ static void test_path_directorynotempty(Manager *m) { assert_se(access(test_path, F_OK) < 0); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0) return; @@ -356,7 +356,7 @@ static void test_path_makedirectory_directorymode(Manager *m) { assert_se(access(test_path, F_OK) < 0); - assert_se(unit_start(unit) >= 0); + assert_se(unit_start(unit, NULL) >= 0); /* Check if the directory has been created */ assert_se(access(test_path, F_OK) >= 0); diff --git a/src/test/test-socket-bind.c b/src/test/test-socket-bind.c index dadd2c0dff..d7d9110343 100644 --- a/src/test/test-socket-bind.c +++ b/src/test/test-socket-bind.c @@ -78,7 +78,7 @@ static int test_socket_bind( SERVICE(u)->type = SERVICE_ONESHOT; u->load_state = UNIT_LOADED; - r = unit_start(u); + r = unit_start(u, NULL); if (r < 0) return log_error_errno(r, "Unit start failed %m"); diff --git a/test/TEST-63-ISSUE-17433/Makefile b/test/TEST-63-PATH/Makefile similarity index 100% rename from test/TEST-63-ISSUE-17433/Makefile rename to test/TEST-63-PATH/Makefile diff --git a/test/TEST-63-ISSUE-17433/test.sh b/test/TEST-63-PATH/test.sh similarity index 100% rename from test/TEST-63-ISSUE-17433/test.sh rename to test/TEST-63-PATH/test.sh 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-07.sh b/test/units/testsuite-07.sh index 5e9fe64ea9..95ebe3876f 100755 --- a/test/units/testsuite-07.sh +++ b/test/units/testsuite-07.sh @@ -8,6 +8,9 @@ set -o pipefail cat >/lib/systemd/system/my.service </testok' diff --git a/test/units/testsuite-63.sh b/test/units/testsuite-63.sh new file mode 100755 index 0000000000..7ee7fc1513 --- /dev/null +++ b/test/units/testsuite-63.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +systemctl log-level debug + +# Test that a path unit continuously triggering a service that fails condition checks eventually fails with +# the trigger-limit-hit error. +rm -f /tmp/nonexistent +systemctl start test63.path +touch /tmp/test63 + +# Make sure systemd has sufficient time to hit the trigger limit for test63.path. +sleep 2 +test "$(systemctl show test63.service -P ActiveState)" = inactive +test "$(systemctl show test63.service -P Result)" = success +test "$(systemctl show test63.path -P ActiveState)" = failed +test "$(systemctl show test63.path -P Result)" = trigger-limit-hit + +# Test that starting the service manually doesn't affect the path unit. +rm -f /tmp/test63 +systemctl reset-failed +systemctl start test63.path +systemctl start test63.service +test "$(systemctl show test63.service -P ActiveState)" = inactive +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