Compare commits
2 Commits
c0316a84d3
...
67d67875b4
Author | SHA1 | Date |
---|---|---|
James R T | 67d67875b4 | |
James Raphael Tiovalen | 1a34e4709f |
|
@ -498,6 +498,7 @@ Most path unit settings are available to transient units.
|
||||||
Unit=
|
Unit=
|
||||||
✓ MakeDirectory=
|
✓ MakeDirectory=
|
||||||
✓ DirectoryMode=
|
✓ DirectoryMode=
|
||||||
|
✓ DeactivationToggle=
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install Section
|
## Install Section
|
||||||
|
|
|
@ -10715,6 +10715,8 @@ node /org/freedesktop/systemd1/unit/cups_2epath {
|
||||||
readonly t TriggerLimitIntervalUSec = ...;
|
readonly t TriggerLimitIntervalUSec = ...;
|
||||||
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
|
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
|
||||||
readonly u TriggerLimitBurst = ...;
|
readonly u TriggerLimitBurst = ...;
|
||||||
|
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
|
||||||
|
readonly b DeactivationToggle = ...;
|
||||||
};
|
};
|
||||||
interface org.freedesktop.DBus.Peer { ... };
|
interface org.freedesktop.DBus.Peer { ... };
|
||||||
interface org.freedesktop.DBus.Introspectable { ... };
|
interface org.freedesktop.DBus.Introspectable { ... };
|
||||||
|
@ -10731,6 +10733,8 @@ node /org/freedesktop/systemd1/unit/cups_2epath {
|
||||||
|
|
||||||
<!--property TriggerLimitBurst is not documented!-->
|
<!--property TriggerLimitBurst is not documented!-->
|
||||||
|
|
||||||
|
<!--property DeactivationToggle is not documented!-->
|
||||||
|
|
||||||
<!--Autogenerated cross-references for systemd.directives, do not edit-->
|
<!--Autogenerated cross-references for systemd.directives, do not edit-->
|
||||||
|
|
||||||
<variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.systemd1.Unit"/>
|
<variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.systemd1.Unit"/>
|
||||||
|
@ -10755,6 +10759,8 @@ node /org/freedesktop/systemd1/unit/cups_2epath {
|
||||||
|
|
||||||
<variablelist class="dbus-property" generated="True" extra-ref="TriggerLimitBurst"/>
|
<variablelist class="dbus-property" generated="True" extra-ref="TriggerLimitBurst"/>
|
||||||
|
|
||||||
|
<variablelist class="dbus-property" generated="True" extra-ref="DeactivationToggle"/>
|
||||||
|
|
||||||
<!--End of Autogenerated section-->
|
<!--End of Autogenerated section-->
|
||||||
|
|
||||||
<refsect2>
|
<refsect2>
|
||||||
|
@ -12488,6 +12494,10 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
|
||||||
<title>Timer Objects</title>
|
<title>Timer Objects</title>
|
||||||
<para><varname>DeferReactivation</varname> was added in version 257.</para>
|
<para><varname>DeferReactivation</varname> was added in version 257.</para>
|
||||||
</refsect2>
|
</refsect2>
|
||||||
|
<refsect2>
|
||||||
|
<title>Path Objects</title>
|
||||||
|
<para><varname>DeactivationToggle</varname> was added in version 257.</para>
|
||||||
|
</refsect2>
|
||||||
</refsect1>
|
</refsect1>
|
||||||
|
|
||||||
<refsect1>
|
<refsect1>
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<para>A unit configuration file whose name ends in
|
<para>A unit configuration file whose name ends in
|
||||||
<literal>.path</literal> encodes information about a path
|
<literal>.path</literal> encodes information about a path
|
||||||
monitored by systemd, for path-based activation.</para>
|
monitored by systemd, for path-based activation and deactivation.</para>
|
||||||
|
|
||||||
<para>This man page lists the configuration options specific to
|
<para>This man page lists the configuration options specific to
|
||||||
this unit type. See
|
this unit type. See
|
||||||
|
@ -206,6 +206,21 @@
|
||||||
|
|
||||||
<xi:include href="version-info.xml" xpointer="v250"/></listitem>
|
<xi:include href="version-info.xml" xpointer="v250"/></listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
<varlistentry>
|
||||||
|
<term><varname>DeactivationToggle=</varname></term>
|
||||||
|
|
||||||
|
<listitem><para>Takes a boolean argument. If true, the
|
||||||
|
triggered unit is stopped and the path unit goes back to
|
||||||
|
waiting state when the path it watches is removed for
|
||||||
|
<varname>PathExists=</varname>, when there are no matching
|
||||||
|
paths for <varname>PathExistsGlob=</varname>, or when the
|
||||||
|
directory is empty for <varname>DirectoryNotEmpty=</varname>.
|
||||||
|
This option is ignored for <varname>PathChanged=</varname> and
|
||||||
|
<varname>PathModified=</varname> settings. Defaults to
|
||||||
|
<option>false</option>.</para>
|
||||||
|
|
||||||
|
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||||
|
</varlistentry>
|
||||||
</variablelist>
|
</variablelist>
|
||||||
|
|
||||||
<xi:include href="systemd.service.xml" xpointer="shared-unit-options" />
|
<xi:include href="systemd.service.xml" xpointer="shared-unit-options" />
|
||||||
|
|
|
@ -49,6 +49,7 @@ const sd_bus_vtable bus_path_vtable[] = {
|
||||||
SD_BUS_PROPERTY("Result", "s", property_get_result, offsetof(Path, result), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
SD_BUS_PROPERTY("Result", "s", property_get_result, offsetof(Path, result), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
||||||
SD_BUS_PROPERTY("TriggerLimitIntervalUSec", "t", bus_property_get_usec, offsetof(Path, trigger_limit.interval), SD_BUS_VTABLE_PROPERTY_CONST),
|
SD_BUS_PROPERTY("TriggerLimitIntervalUSec", "t", bus_property_get_usec, offsetof(Path, trigger_limit.interval), SD_BUS_VTABLE_PROPERTY_CONST),
|
||||||
SD_BUS_PROPERTY("TriggerLimitBurst", "u", bus_property_get_unsigned, offsetof(Path, trigger_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST),
|
SD_BUS_PROPERTY("TriggerLimitBurst", "u", bus_property_get_unsigned, offsetof(Path, trigger_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST),
|
||||||
|
SD_BUS_PROPERTY("DeactivationToggle", "b", bus_property_get_bool, offsetof(Path, deactivation_toggle), SD_BUS_VTABLE_PROPERTY_CONST),
|
||||||
SD_BUS_VTABLE_END
|
SD_BUS_VTABLE_END
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,6 +142,9 @@ static int bus_path_set_transient_property(
|
||||||
if (streq(name, "TriggerLimitIntervalUSec"))
|
if (streq(name, "TriggerLimitIntervalUSec"))
|
||||||
return bus_set_transient_usec(u, name, &p->trigger_limit.interval, message, flags, error);
|
return bus_set_transient_usec(u, name, &p->trigger_limit.interval, message, flags, error);
|
||||||
|
|
||||||
|
if (streq(name, "DeactivationToggle"))
|
||||||
|
return bus_set_transient_bool(u, name, &p->deactivation_toggle, message, flags, error);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -586,6 +586,7 @@ Path.MakeDirectory, config_parse_bool,
|
||||||
Path.DirectoryMode, config_parse_mode, 0, offsetof(Path, directory_mode)
|
Path.DirectoryMode, config_parse_mode, 0, offsetof(Path, directory_mode)
|
||||||
Path.TriggerLimitIntervalSec, config_parse_sec, 0, offsetof(Path, trigger_limit.interval)
|
Path.TriggerLimitIntervalSec, config_parse_sec, 0, offsetof(Path, trigger_limit.interval)
|
||||||
Path.TriggerLimitBurst, config_parse_unsigned, 0, offsetof(Path, trigger_limit.burst)
|
Path.TriggerLimitBurst, config_parse_unsigned, 0, offsetof(Path, trigger_limit.burst)
|
||||||
|
Path.DeactivationToggle, config_parse_bool, 0, offsetof(Path, deactivation_toggle)
|
||||||
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Slice') }}
|
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Slice') }}
|
||||||
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Scope') }}
|
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Scope') }}
|
||||||
{{ KILL_CONTEXT_CONFIG_ITEMS('Scope') }}
|
{{ KILL_CONTEXT_CONFIG_ITEMS('Scope') }}
|
||||||
|
|
|
@ -47,11 +47,14 @@ int path_spec_watch(PathSpec *s, sd_event_io_handler_t handler) {
|
||||||
bool exists = false;
|
bool exists = false;
|
||||||
char *slash, *oldslash = NULL;
|
char *slash, *oldslash = NULL;
|
||||||
int r;
|
int r;
|
||||||
|
Path *p;
|
||||||
|
|
||||||
assert(s);
|
assert(s);
|
||||||
assert(s->unit);
|
assert(s->unit);
|
||||||
assert(handler);
|
assert(handler);
|
||||||
|
|
||||||
|
p = PATH(s->unit);
|
||||||
|
|
||||||
path_spec_unwatch(s);
|
path_spec_unwatch(s);
|
||||||
|
|
||||||
s->inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
|
s->inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
|
||||||
|
@ -82,9 +85,11 @@ int path_spec_watch(PathSpec *s, sd_event_io_handler_t handler) {
|
||||||
*cut = '\0';
|
*cut = '\0';
|
||||||
|
|
||||||
flags = IN_MOVE_SELF | IN_DELETE_SELF | IN_CREATE | IN_MOVED_TO;
|
flags = IN_MOVE_SELF | IN_DELETE_SELF | IN_CREATE | IN_MOVED_TO;
|
||||||
|
SET_FLAG(flags, IN_DELETE | IN_MOVED_FROM, p && p->deactivation_toggle);
|
||||||
} else {
|
} else {
|
||||||
cut = NULL;
|
cut = NULL;
|
||||||
flags = flags_table[s->type];
|
flags = flags_table[s->type];
|
||||||
|
SET_FLAG(flags, IN_DELETE | IN_MOVED_FROM, p && p->deactivation_toggle && s->type == PATH_DIRECTORY_NOT_EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If this is a symlink watch both the symlink inode and where it points to. If the inode is
|
/* If this is a symlink watch both the symlink inode and where it points to. If the inode is
|
||||||
|
@ -134,7 +139,10 @@ int path_spec_watch(PathSpec *s, sd_event_io_handler_t handler) {
|
||||||
char tmp2 = *cut2;
|
char tmp2 = *cut2;
|
||||||
*cut2 = '\0';
|
*cut2 = '\0';
|
||||||
|
|
||||||
(void) inotify_add_watch(s->inotify_fd, s->path, IN_MOVE_SELF);
|
flags = IN_MOVE_SELF;
|
||||||
|
SET_FLAG(flags, IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM, p && p->deactivation_toggle);
|
||||||
|
|
||||||
|
(void) inotify_add_watch(s->inotify_fd, s->path, flags);
|
||||||
/* Error is ignored, the worst can happen is we get spurious events. */
|
/* Error is ignored, the worst can happen is we get spurious events. */
|
||||||
|
|
||||||
*cut2 = tmp2;
|
*cut2 = tmp2;
|
||||||
|
@ -427,14 +435,16 @@ static void path_dump(Unit *u, FILE *f, const char *prefix) {
|
||||||
"%sMakeDirectory: %s\n"
|
"%sMakeDirectory: %s\n"
|
||||||
"%sDirectoryMode: %04o\n"
|
"%sDirectoryMode: %04o\n"
|
||||||
"%sTriggerLimitIntervalSec: %s\n"
|
"%sTriggerLimitIntervalSec: %s\n"
|
||||||
"%sTriggerLimitBurst: %u\n",
|
"%sTriggerLimitBurst: %u\n"
|
||||||
|
"%sDeactivationToggle: %s\n",
|
||||||
prefix, path_state_to_string(p->state),
|
prefix, path_state_to_string(p->state),
|
||||||
prefix, path_result_to_string(p->result),
|
prefix, path_result_to_string(p->result),
|
||||||
prefix, trigger ? trigger->id : "n/a",
|
prefix, trigger ? trigger->id : "n/a",
|
||||||
prefix, yes_no(p->make_directory),
|
prefix, yes_no(p->make_directory),
|
||||||
prefix, p->directory_mode,
|
prefix, p->directory_mode,
|
||||||
prefix, FORMAT_TIMESPAN(p->trigger_limit.interval, USEC_PER_SEC),
|
prefix, FORMAT_TIMESPAN(p->trigger_limit.interval, USEC_PER_SEC),
|
||||||
prefix, p->trigger_limit.burst);
|
prefix, p->trigger_limit.burst,
|
||||||
|
prefix, yes_no(p->deactivation_toggle));
|
||||||
|
|
||||||
LIST_FOREACH(spec, s, p->specs)
|
LIST_FOREACH(spec, s, p->specs)
|
||||||
path_spec_dump(s, f, prefix);
|
path_spec_dump(s, f, prefix);
|
||||||
|
@ -555,6 +565,8 @@ static void path_enter_running(Path *p, char *trigger_path) {
|
||||||
job_set_activation_details(job, details);
|
job_set_activation_details(job, details);
|
||||||
|
|
||||||
path_set_state(p, PATH_RUNNING);
|
path_set_state(p, PATH_RUNNING);
|
||||||
|
|
||||||
|
if (!p->deactivation_toggle)
|
||||||
path_unwatch(p);
|
path_unwatch(p);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -563,6 +575,34 @@ fail:
|
||||||
path_enter_dead(p, PATH_FAILURE_RESOURCES);
|
path_enter_dead(p, PATH_FAILURE_RESOURCES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void path_deactivate_unit(Path *p) {
|
||||||
|
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||||
|
Unit *trigger;
|
||||||
|
Job *job;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(p);
|
||||||
|
|
||||||
|
trigger = UNIT_TRIGGER(UNIT(p));
|
||||||
|
if (!trigger) {
|
||||||
|
log_unit_error(UNIT(p), "Unit to trigger vanished.");
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
r = manager_add_job(UNIT(p)->manager, JOB_STOP, trigger, JOB_REPLACE, NULL, &error, &job);
|
||||||
|
if (r < 0) {
|
||||||
|
log_unit_warning(UNIT(p), "Failed to queue unit stop job: %s", bus_error_message(&error, r));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
path_set_state(p, PATH_WAITING);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
path_enter_dead(p, PATH_FAILURE_RESOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
static bool path_check_good(Path *p, bool initial, bool from_trigger_notify, char **ret_trigger_path) {
|
static bool path_check_good(Path *p, bool initial, bool from_trigger_notify, char **ret_trigger_path) {
|
||||||
assert(p);
|
assert(p);
|
||||||
assert(ret_trigger_path);
|
assert(ret_trigger_path);
|
||||||
|
@ -578,19 +618,30 @@ static void path_enter_waiting(Path *p, bool initial, bool from_trigger_notify)
|
||||||
_cleanup_free_ char *trigger_path = NULL;
|
_cleanup_free_ char *trigger_path = NULL;
|
||||||
Unit *trigger;
|
Unit *trigger;
|
||||||
int r;
|
int r;
|
||||||
|
bool watch_not_needed = !p->deactivation_toggle || p->state == PATH_RUNNING;
|
||||||
|
bool trigger_unit_is_running;
|
||||||
|
|
||||||
if (p->trigger_notify_event_source)
|
if (p->trigger_notify_event_source)
|
||||||
(void) event_source_disable(p->trigger_notify_event_source);
|
(void) event_source_disable(p->trigger_notify_event_source);
|
||||||
|
|
||||||
/* If the triggered unit is already running, so are we */
|
/* If the triggered unit is already running, so are we */
|
||||||
trigger = UNIT_TRIGGER(UNIT(p));
|
trigger = UNIT_TRIGGER(UNIT(p));
|
||||||
if (trigger && !UNIT_IS_INACTIVE_OR_DEACTIVATING(unit_active_state(trigger))) {
|
trigger_unit_is_running = trigger && !UNIT_IS_INACTIVE_OR_DEACTIVATING(unit_active_state(trigger));
|
||||||
|
if (watch_not_needed && trigger_unit_is_running) {
|
||||||
|
if (p->deactivation_toggle && p->state == PATH_RUNNING) {
|
||||||
|
if (!path_check_good(p, true, from_trigger_notify, &trigger_path))
|
||||||
|
/* If the path is removed, stop the unit */
|
||||||
|
path_deactivate_unit(p);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
path_set_state(p, PATH_RUNNING);
|
path_set_state(p, PATH_RUNNING);
|
||||||
path_unwatch(p);
|
path_unwatch(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path_check_good(p, initial, from_trigger_notify, &trigger_path)) {
|
if (watch_not_needed && path_check_good(p, initial, from_trigger_notify, &trigger_path)) {
|
||||||
log_unit_debug(UNIT(p), "Got triggered.");
|
log_unit_debug(UNIT(p), "Got triggered.");
|
||||||
path_enter_running(p, trigger_path);
|
path_enter_running(p, trigger_path);
|
||||||
return;
|
return;
|
||||||
|
@ -608,9 +659,18 @@ static void path_enter_waiting(Path *p, bool initial, bool from_trigger_notify)
|
||||||
* recheck */
|
* recheck */
|
||||||
|
|
||||||
if (path_check_good(p, false, from_trigger_notify, &trigger_path)) {
|
if (path_check_good(p, false, from_trigger_notify, &trigger_path)) {
|
||||||
|
if (p->deactivation_toggle && trigger_unit_is_running) {
|
||||||
|
path_set_state(p, PATH_RUNNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log_unit_debug(UNIT(p), "Got triggered.");
|
log_unit_debug(UNIT(p), "Got triggered.");
|
||||||
path_enter_running(p, trigger_path);
|
path_enter_running(p, trigger_path);
|
||||||
return;
|
return;
|
||||||
|
} else if (p->deactivation_toggle && trigger_unit_is_running) {
|
||||||
|
/* If the path is removed, stop the unit */
|
||||||
|
path_deactivate_unit(p);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
path_set_state(p, PATH_WAITING);
|
path_set_state(p, PATH_WAITING);
|
||||||
|
|
|
@ -67,6 +67,8 @@ struct Path {
|
||||||
RateLimit trigger_limit;
|
RateLimit trigger_limit;
|
||||||
|
|
||||||
sd_event_source *trigger_notify_event_source;
|
sd_event_source *trigger_notify_event_source;
|
||||||
|
|
||||||
|
bool deactivation_toggle;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ActivationDetailsPath {
|
struct ActivationDetailsPath {
|
||||||
|
|
|
@ -2315,7 +2315,7 @@ static int bus_append_mount_property(sd_bus_message *m, const char *field, const
|
||||||
static int bus_append_path_property(sd_bus_message *m, const char *field, const char *eq) {
|
static int bus_append_path_property(sd_bus_message *m, const char *field, const char *eq) {
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
if (streq(field, "MakeDirectory"))
|
if (STR_IN_SET(field, "MakeDirectory", "DeactivationToggle"))
|
||||||
return bus_append_parse_boolean(m, field, eq);
|
return bus_append_parse_boolean(m, field, eq);
|
||||||
|
|
||||||
if (streq(field, "DirectoryMode"))
|
if (streq(field, "DirectoryMode"))
|
||||||
|
|
|
@ -22,7 +22,8 @@ typedef void (*test_function_t)(Manager *m);
|
||||||
|
|
||||||
static int setup_test(Manager **m) {
|
static int setup_test(Manager **m) {
|
||||||
char **tests_path = STRV_MAKE("exists", "existsglobFOOBAR", "changed", "modified", "unit",
|
char **tests_path = STRV_MAKE("exists", "existsglobFOOBAR", "changed", "modified", "unit",
|
||||||
"directorynotempty", "makedirectory");
|
"directorynotempty", "makedirectory", "deactivationtoggle",
|
||||||
|
"deactivationtoggle_secondary");
|
||||||
Manager *tmp = NULL;
|
Manager *tmp = NULL;
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
|
@ -378,6 +379,371 @@ static void test_path_makedirectory_directorymode(Manager *m) {
|
||||||
(void) rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL);
|
(void) rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_path_deactivationtoggle_exists(Manager *m) {
|
||||||
|
const char *test_file, *test_file2;
|
||||||
|
const char *test_path = "/tmp/test-path_deactivationtoggle/";
|
||||||
|
const char *test_path2 = "/tmp/test-path_deactivationtoggle_secondary/";
|
||||||
|
const char *path_name = "path-deactivationtoggle-exists.path";
|
||||||
|
const char *service_name = "path-deactivationtoggle.service";
|
||||||
|
Unit *unit = NULL;
|
||||||
|
Path *path = NULL;
|
||||||
|
Service *service = NULL;
|
||||||
|
|
||||||
|
assert_se(m);
|
||||||
|
|
||||||
|
assert_se(manager_load_startable_unit_or_warn(m, path_name, NULL, &unit) >= 0);
|
||||||
|
|
||||||
|
path = PATH(unit);
|
||||||
|
service = service_for_path(m, path, service_name);
|
||||||
|
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
test_file = strjoina(test_path, "test_file");
|
||||||
|
test_file2 = strjoina(test_path2, "test_file");
|
||||||
|
|
||||||
|
assert_se(mkdir_p(test_path, 0755) >= 0);
|
||||||
|
assert_se(mkdir_p(test_path2, 0755) >= 0);
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
assert_se(rename(test_file2, test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If file exists before path is started, service is stopped when file is removed */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Service is stopped when file is renamed */
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file2, test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and file does not exist, it is stopped */
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and file exists, path unit is running */
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running first, service is stopped when file is removed */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_path_deactivationtoggle_existsglob(Manager *m) {
|
||||||
|
const char *test_file, *test_file2, *test_file3, *test_file4;
|
||||||
|
const char *test_path = "/tmp/test-path_deactivationtoggle/";
|
||||||
|
const char *test_path2 = "/tmp/test-path_deactivationtoggle_secondary/";
|
||||||
|
const char *path_name = "path-deactivationtoggle-existsglob.path";
|
||||||
|
const char *service_name = "path-deactivationtoggle.service";
|
||||||
|
Unit *unit = NULL;
|
||||||
|
Path *path = NULL;
|
||||||
|
Service *service = NULL;
|
||||||
|
|
||||||
|
assert_se(m);
|
||||||
|
|
||||||
|
assert_se(manager_load_startable_unit_or_warn(m, path_name, NULL, &unit) >= 0);
|
||||||
|
|
||||||
|
path = PATH(unit);
|
||||||
|
service = service_for_path(m, path, service_name);
|
||||||
|
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
test_file = strjoina(test_path, "test_file_globFOO");
|
||||||
|
test_file2 = strjoina(test_path, "test_file_globBAR");
|
||||||
|
test_file3 = strjoina(test_path2, "test_file_globFOO");
|
||||||
|
test_file4 = strjoina(test_path2, "test_file_globBAR");
|
||||||
|
|
||||||
|
assert_se(mkdir_p(test_path, 0755) >= 0);
|
||||||
|
assert_se(mkdir_p(test_path2, 0755) >= 0);
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file3) >= 0);
|
||||||
|
assert_se(touch(test_file4) >= 0);
|
||||||
|
assert_se(rename(test_file3, test_file) >= 0);
|
||||||
|
assert_se(rename(test_file4, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file3) >= 0);
|
||||||
|
assert_se(rename(test_file2, test_file4) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file3, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_file4, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If files exist before path is started, service is stopped when files are removed */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Service is stopped when files are renamed */
|
||||||
|
assert_se(rename(test_file, test_file3) >= 0);
|
||||||
|
assert_se(rename(test_file2, test_file4) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file3, test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file3) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file3, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_file4, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and files do not exist, it is stopped */
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and files exist, path unit is running */
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running first, service is stopped when files are removed */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_path_deactivationtoggle_directorynotempty(Manager *m) {
|
||||||
|
const char *test_file, *test_file2;
|
||||||
|
const char *test_path = "/tmp/test-path_deactivationtoggle/";
|
||||||
|
const char *test_path2 = "/tmp/test-path_deactivationtoggle_secondary/";
|
||||||
|
const char *path_name = "path-deactivationtoggle-directorynotempty.path";
|
||||||
|
const char *service_name = "path-deactivationtoggle.service";
|
||||||
|
Unit *unit = NULL;
|
||||||
|
Path *path = NULL;
|
||||||
|
Service *service = NULL;
|
||||||
|
|
||||||
|
assert_se(m);
|
||||||
|
|
||||||
|
assert_se(manager_load_startable_unit_or_warn(m, path_name, NULL, &unit) >= 0);
|
||||||
|
|
||||||
|
path = PATH(unit);
|
||||||
|
service = service_for_path(m, path, service_name);
|
||||||
|
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
test_file = strjoina(test_path, "test_file");
|
||||||
|
test_file2 = strjoina(test_path2, "test_file");
|
||||||
|
|
||||||
|
assert_se(mkdir_p(test_path, 0755) >= 0);
|
||||||
|
assert_se(mkdir_p(test_path2, 0755) >= 0);
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file2) >= 0);
|
||||||
|
assert_se(rename(test_file2, test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
assert_se(rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(mkdir_p(test_path, 0755) >= 0);
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If directory is not empty before path is started, service is stopped when file is removed */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Service is stopped when directory is empty due to file rename */
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file2, test_file) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(rename(test_file, test_file2) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
assert_se(rm_rf(test_file2, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and directory is empty, it is stopped */
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
|
||||||
|
assert_se(unit_start(UNIT(service), NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_DEAD, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running and directory is not empty, path unit is running */
|
||||||
|
assert_se(touch(test_file) >= 0);
|
||||||
|
assert_se(unit_start(unit, NULL) >= 0);
|
||||||
|
if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* If service is running first, service is stopped when directory is empty */
|
||||||
|
assert_se(rm_rf(test_file, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
|
||||||
|
if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert_se(unit_stop(unit) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
static const test_function_t tests[] = {
|
static const test_function_t tests[] = {
|
||||||
test_path_exists,
|
test_path_exists,
|
||||||
|
@ -387,6 +753,9 @@ int main(int argc, char *argv[]) {
|
||||||
test_path_unit,
|
test_path_unit,
|
||||||
test_path_directorynotempty,
|
test_path_directorynotempty,
|
||||||
test_path_makedirectory_directorymode,
|
test_path_makedirectory_directorymode,
|
||||||
|
test_path_deactivationtoggle_exists,
|
||||||
|
test_path_deactivationtoggle_existsglob,
|
||||||
|
test_path_deactivationtoggle_directorynotempty,
|
||||||
NULL,
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ ConditionSecurity=
|
||||||
ConditionUser=
|
ConditionUser=
|
||||||
ConditionVirtualization=
|
ConditionVirtualization=
|
||||||
Conflicts=
|
Conflicts=
|
||||||
|
DeactivationToggle=
|
||||||
DefaultDependencies=
|
DefaultDependencies=
|
||||||
DefaultInstance=
|
DefaultInstance=
|
||||||
DeferAcceptSec=
|
DeferAcceptSec=
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Test DeactivationToggle with DirectoryNotEmpty
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
DirectoryNotEmpty=/tmp/test-path_deactivationtoggle/
|
||||||
|
Unit=path-deactivationtoggle.service
|
||||||
|
DeactivationToggle=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,12 @@
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Test DeactivationToggle with PathExists
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathExists=/tmp/test-path_deactivationtoggle/test_file
|
||||||
|
Unit=path-deactivationtoggle.service
|
||||||
|
DeactivationToggle=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,12 @@
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Test DeactivationToggle with PathExistsGlob
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathExistsGlob=/tmp/test-path_deactivationtoggle/test_file_glob*
|
||||||
|
Unit=path-deactivationtoggle.service
|
||||||
|
DeactivationToggle=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,10 @@
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Service Test for Path units
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=sleep infinity
|
||||||
|
Type=exec
|
||||||
|
RemainAfterExit=true
|
Loading…
Reference in New Issue