From 5e422a9cea38bd5c7ce54c7bbac612c04418dc41 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
Date: Mon, 1 Apr 2019 18:54:59 +0200
Subject: [PATCH] shared: add generic logic for waiting for a unit to enter
 some state

This is a generic implementation of a client-side logic of waiting until
a unit enters or leaves some state.

This is a more generic implementation of the WaitContext logic currently
in systemctl.c, and is supposed to replace it (a later commit does
this). It's similar to bus-wait-for-jobs.c and we probably should fold
that one into it later on.

This code is more powerful and cleaner than the WaitContext logic
however. In addition to waiting for a unit to exit this also allows us
to wait for a unit to leave the "maintainance" state.

This commit only implements the generic logic, and adds no users of it
yet.

(cherry picked from commit 3572d3df8f822d4cf1601428401a837f723771cf)

Related: #1830861
---
 src/shared/bus-wait-for-units.c | 434 ++++++++++++++++++++++++++++++++
 src/shared/bus-wait-for-units.h |  35 +++
 src/shared/meson.build          |   2 +
 3 files changed, 471 insertions(+)
 create mode 100644 src/shared/bus-wait-for-units.c
 create mode 100644 src/shared/bus-wait-for-units.h

diff --git a/src/shared/bus-wait-for-units.c b/src/shared/bus-wait-for-units.c
new file mode 100644
index 0000000000..d07f491e93
--- /dev/null
+++ b/src/shared/bus-wait-for-units.c
@@ -0,0 +1,434 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "bus-util.h"
+#include "bus-wait-for-units.h"
+#include "hashmap.h"
+#include "string-util.h"
+#include "strv.h"
+#include "unit-def.h"
+
+typedef struct WaitForItem {
+        BusWaitForUnits *parent;
+
+        BusWaitForUnitsFlags flags;
+
+        char *bus_path;
+
+        sd_bus_slot *slot_get_all;
+        sd_bus_slot *slot_properties_changed;
+
+        bus_wait_for_units_unit_callback unit_callback;
+        void *userdata;
+
+        char *active_state;
+        uint32_t job_id;
+        char *clean_result;
+} WaitForItem;
+
+typedef struct BusWaitForUnits {
+        sd_bus *bus;
+        sd_bus_slot *slot_disconnected;
+
+        Hashmap *items;
+
+        bus_wait_for_units_ready_callback ready_callback;
+        void *userdata;
+
+        WaitForItem *current;
+
+        BusWaitForUnitsState state;
+        bool has_failed:1;
+} BusWaitForUnits;
+
+static WaitForItem *wait_for_item_free(WaitForItem *item) {
+        int r;
+
+        if (!item)
+                return NULL;
+
+        if (item->parent) {
+                if (FLAGS_SET(item->flags, BUS_WAIT_REFFED) && item->bus_path && item->parent->bus) {
+                        r = sd_bus_call_method_async(
+                                        item->parent->bus,
+                                        NULL,
+                                        "org.freedesktop.systemd1",
+                                        item->bus_path,
+                                        "org.freedesktop.systemd1.Unit",
+                                        "Unref",
+                                        NULL,
+                                        NULL,
+                                        NULL);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to drop reference to unit %s, ignoring: %m", item->bus_path);
+                }
+
+                assert_se(hashmap_remove(item->parent->items, item->bus_path) == item);
+
+                if (item->parent->current == item)
+                        item->parent->current = NULL;
+        }
+
+        sd_bus_slot_unref(item->slot_properties_changed);
+        sd_bus_slot_unref(item->slot_get_all);
+
+        free(item->bus_path);
+        free(item->active_state);
+        free(item->clean_result);
+
+        return mfree(item);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(WaitForItem*, wait_for_item_free);
+
+static void bus_wait_for_units_clear(BusWaitForUnits *d) {
+        WaitForItem *item;
+
+        assert(d);
+
+        d->slot_disconnected = sd_bus_slot_unref(d->slot_disconnected);
+        d->bus = sd_bus_unref(d->bus);
+
+        while ((item = hashmap_first(d->items))) {
+                d->current = item;
+
+                item->unit_callback(d, item->bus_path, false, item->userdata);
+                wait_for_item_free(item);
+        }
+
+        d->items = hashmap_free(d->items);
+}
+
+static int match_disconnected(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+        BusWaitForUnits *d = userdata;
+
+        assert(m);
+        assert(d);
+
+        log_error("Warning! D-Bus connection terminated.");
+
+        bus_wait_for_units_clear(d);
+
+        if (d->ready_callback)
+                d->ready_callback(d, false, d->userdata);
+        else /* If no ready callback is specified close the connection so that the event loop exits */
+                sd_bus_close(sd_bus_message_get_bus(m));
+
+        return 0;
+}
+
+int bus_wait_for_units_new(sd_bus *bus, BusWaitForUnits **ret) {
+        _cleanup_(bus_wait_for_units_freep) BusWaitForUnits *d = NULL;
+        int r;
+
+        assert(bus);
+        assert(ret);
+
+        d = new(BusWaitForUnits, 1);
+        if (!d)
+                return -ENOMEM;
+
+        *d = (BusWaitForUnits) {
+                .state = BUS_WAIT_SUCCESS,
+                .bus = sd_bus_ref(bus),
+        };
+
+        r = sd_bus_match_signal_async(
+                        bus,
+                        &d->slot_disconnected,
+                        "org.freedesktop.DBus.Local",
+                        NULL,
+                        "org.freedesktop.DBus.Local",
+                        "Disconnected",
+                        match_disconnected, NULL, d);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(d);
+        return 0;
+}
+
+BusWaitForUnits* bus_wait_for_units_free(BusWaitForUnits *d) {
+        if (!d)
+                return NULL;
+
+        bus_wait_for_units_clear(d);
+        sd_bus_slot_unref(d->slot_disconnected);
+        sd_bus_unref(d->bus);
+
+        return mfree(d);
+}
+
+static bool bus_wait_for_units_is_ready(BusWaitForUnits *d) {
+        assert(d);
+
+        if (!d->bus) /* Disconnected? */
+                return true;
+
+        return hashmap_isempty(d->items);
+}
+
+void bus_wait_for_units_set_ready_callback(BusWaitForUnits *d, bus_wait_for_units_ready_callback callback, void *userdata) {
+        assert(d);
+
+        d->ready_callback = callback;
+        d->userdata = userdata;
+}
+
+static void bus_wait_for_units_check_ready(BusWaitForUnits *d) {
+        assert(d);
+
+        if (!bus_wait_for_units_is_ready(d))
+                return;
+
+        d->state = d->has_failed ? BUS_WAIT_FAILURE : BUS_WAIT_SUCCESS;
+
+        if (d->ready_callback)
+                d->ready_callback(d, d->state, d->userdata);
+}
+
+static void wait_for_item_check_ready(WaitForItem *item) {
+        BusWaitForUnits *d;
+
+        assert(item);
+        assert(d = item->parent);
+
+        if (FLAGS_SET(item->flags, BUS_WAIT_FOR_MAINTENANCE_END)) {
+
+                if (item->clean_result && !streq(item->clean_result, "success"))
+                        d->has_failed = true;
+
+                if (!item->active_state || streq(item->active_state, "maintenance"))
+                        return;
+        }
+
+        if (FLAGS_SET(item->flags, BUS_WAIT_NO_JOB) && item->job_id != 0)
+                return;
+
+        if (FLAGS_SET(item->flags, BUS_WAIT_FOR_INACTIVE)) {
+
+                if (streq_ptr(item->active_state, "failed"))
+                        d->has_failed = true;
+                else if (!streq_ptr(item->active_state, "inactive"))
+                        return;
+        }
+
+        if (item->unit_callback) {
+                d->current = item;
+                item->unit_callback(d, item->bus_path, true, item->userdata);
+        }
+
+        wait_for_item_free(item);
+
+        bus_wait_for_units_check_ready(d);
+}
+
+static int property_map_job(
+                sd_bus *bus,
+                const char *member,
+                sd_bus_message *m,
+                sd_bus_error *error,
+                void *userdata) {
+
+        WaitForItem *item = userdata;
+        const char *path;
+        uint32_t id;
+        int r;
+
+        assert(item);
+
+        r = sd_bus_message_read(m, "(uo)", &id, &path);
+        if (r < 0)
+                return r;
+
+        item->job_id = id;
+        return 0;
+}
+
+static int wait_for_item_parse_properties(WaitForItem *item, sd_bus_message *m) {
+
+        static const struct bus_properties_map map[] = {
+                { "ActiveState", "s",    NULL,             offsetof(WaitForItem, active_state) },
+                { "Job",         "(uo)", property_map_job, 0                                   },
+                { "CleanResult", "s",    NULL,             offsetof(WaitForItem, clean_result) },
+                {}
+        };
+
+        int r;
+
+        assert(item);
+        assert(m);
+
+        r = bus_message_map_all_properties(m, map, BUS_MAP_STRDUP, NULL, item);
+        if (r < 0)
+                return r;
+
+        wait_for_item_check_ready(item);
+        return 0;
+}
+
+static int on_properties_changed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+        WaitForItem *item = userdata;
+        const char *interface;
+        int r;
+
+        assert(item);
+
+        r = sd_bus_message_read(m, "s", &interface);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to parse PropertiesChanged signal: %m");
+                return 0;
+        }
+
+        if (!streq(interface, "org.freedesktop.systemd1.Unit"))
+                return 0;
+
+        r = wait_for_item_parse_properties(item, m);
+        if (r < 0)
+                log_debug_errno(r, "Failed to process PropertiesChanged signal: %m");
+
+        return 0;
+}
+
+static int on_get_all_properties(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+        WaitForItem *item = userdata;
+        int r;
+
+        assert(item);
+
+        if (sd_bus_error_is_set(error)) {
+                BusWaitForUnits *d = item->parent;
+
+                d->has_failed = true;
+
+                log_debug_errno(sd_bus_error_get_errno(error), "GetAll() failed for %s: %s",
+                                item->bus_path, error->message);
+
+                d->current = item;
+                item->unit_callback(d, item->bus_path, false, item->userdata);
+                wait_for_item_free(item);
+
+                bus_wait_for_units_check_ready(d);
+                return 0;
+        }
+
+        r = wait_for_item_parse_properties(item, m);
+        if (r < 0)
+                log_debug_errno(r, "Failed to process GetAll method reply: %m");
+
+        return 0;
+}
+
+int bus_wait_for_units_add_unit(
+                BusWaitForUnits *d,
+                const char *unit,
+                BusWaitForUnitsFlags flags,
+                bus_wait_for_units_unit_callback callback,
+                void *userdata) {
+
+        _cleanup_(wait_for_item_freep) WaitForItem *item = NULL;
+        int r;
+
+        assert(d);
+        assert(unit);
+
+        assert(flags != 0);
+
+        r = hashmap_ensure_allocated(&d->items, &string_hash_ops);
+        if (r < 0)
+                return r;
+
+        item = new(WaitForItem, 1);
+        if (!item)
+                return -ENOMEM;
+
+        *item = (WaitForItem) {
+                .flags = flags,
+                .bus_path = unit_dbus_path_from_name(unit),
+                .unit_callback = callback,
+                .userdata = userdata,
+                .job_id = UINT32_MAX,
+        };
+
+        if (!item->bus_path)
+                return -ENOMEM;
+
+        if (!FLAGS_SET(item->flags, BUS_WAIT_REFFED)) {
+                r = sd_bus_call_method_async(
+                                d->bus,
+                                NULL,
+                                "org.freedesktop.systemd1",
+                                item->bus_path,
+                                "org.freedesktop.systemd1.Unit",
+                                "Ref",
+                                NULL,
+                                NULL,
+                                NULL);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to add reference to unit %s: %m", unit);
+
+
+                item->flags |= BUS_WAIT_REFFED;
+        }
+
+        r = sd_bus_match_signal_async(
+                        d->bus,
+                        &item->slot_properties_changed,
+                        "org.freedesktop.systemd1",
+                        item->bus_path,
+                        "org.freedesktop.DBus.Properties",
+                        "PropertiesChanged",
+                        on_properties_changed,
+                        NULL,
+                        item);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to request match for PropertiesChanged signal: %m");
+
+        r = sd_bus_call_method_async(
+                        d->bus,
+                        &item->slot_get_all,
+                        "org.freedesktop.systemd1",
+                        item->bus_path,
+                        "org.freedesktop.DBus.Properties",
+                        "GetAll",
+                        on_get_all_properties,
+                        item,
+                        "s", FLAGS_SET(item->flags, BUS_WAIT_FOR_MAINTENANCE_END) ? NULL : "org.freedesktop.systemd1.Unit");
+        if (r < 0)
+                return log_debug_errno(r, "Failed to request properties of unit %s: %m", unit);
+
+        r = hashmap_put(d->items, item->bus_path, item);
+        if (r < 0)
+                return r;
+
+        d->state = BUS_WAIT_RUNNING;
+        item->parent = d;
+        TAKE_PTR(item);
+        return 0;
+}
+
+int bus_wait_for_units_run(BusWaitForUnits *d) {
+        int r;
+
+        assert(d);
+
+        while (d->state == BUS_WAIT_RUNNING) {
+
+                r = sd_bus_process(d->bus, NULL);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        continue;
+
+                r = sd_bus_wait(d->bus, (uint64_t) -1);
+                if (r < 0)
+                        return r;
+        }
+
+        return d->state;
+}
+
+BusWaitForUnitsState bus_wait_for_units_state(BusWaitForUnits *d) {
+        assert(d);
+
+        return d->state;
+}
diff --git a/src/shared/bus-wait-for-units.h b/src/shared/bus-wait-for-units.h
new file mode 100644
index 0000000000..a20f3d8fd7
--- /dev/null
+++ b/src/shared/bus-wait-for-units.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "macro.h"
+#include "sd-bus.h"
+
+typedef struct BusWaitForUnits BusWaitForUnits;
+
+typedef enum BusWaitForUnitsState {
+        BUS_WAIT_SUCCESS,    /* Nothing to wait for anymore and nothing failed */
+        BUS_WAIT_FAILURE,    /* dito, but something failed */
+        BUS_WAIT_RUNNING,    /* Still something to wait for */
+        _BUS_WAIT_FOR_UNITS_STATE_MAX,
+        _BUS_WAIT_FOR_UNITS_STATE_INVALID = -1,
+} BusWaitForUnitsState;
+
+typedef enum BusWaitForUnitsFlags {
+        BUS_WAIT_FOR_MAINTENANCE_END = 1 << 0, /* Wait until the unit is no longer in maintenance state */
+        BUS_WAIT_FOR_INACTIVE        = 1 << 1, /* Wait until the unit is back in inactive or dead state */
+        BUS_WAIT_NO_JOB              = 1 << 2, /* Wait until there's no more job pending */
+        BUS_WAIT_REFFED              = 1 << 3, /* The unit is already reffed with RefUnit() */
+} BusWaitForUnitsFlags;
+
+typedef void (*bus_wait_for_units_ready_callback)(BusWaitForUnits *d, BusWaitForUnitsState state, void *userdata);
+typedef void (*bus_wait_for_units_unit_callback)(BusWaitForUnits *d, const char *unit_path, bool good, void *userdata);
+
+int bus_wait_for_units_new(sd_bus *bus, BusWaitForUnits **ret);
+BusWaitForUnits* bus_wait_for_units_free(BusWaitForUnits *d);
+
+BusWaitForUnitsState bus_wait_for_units_state(BusWaitForUnits *d);
+void bus_wait_for_units_set_ready_callback(BusWaitForUnits *d, bus_wait_for_units_ready_callback callback, void *userdata);
+int bus_wait_for_units_add_unit(BusWaitForUnits *d, const char *unit, BusWaitForUnitsFlags flags, bus_wait_for_units_unit_callback callback, void *userdata);
+int bus_wait_for_units_run(BusWaitForUnits *d);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(BusWaitForUnits*, bus_wait_for_units_free);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 54e77e9af6..d0a1bba4c6 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -18,6 +18,8 @@ shared_sources = files('''
         bus-unit-util.h
         bus-util.c
         bus-util.h
+        bus-wait-for-units.c
+        bus-wait-for-units.h
         cgroup-show.c
         cgroup-show.h
         clean-ipc.c
