utility/sdbus: Add bus processing workaround

Provide a wrapper for others to use that works around a memory leak that
we have with processing dbus matches in an event loop.

Change-Id: I944e9c2547844b507216e334bd013f26b8a547da
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/.gitignore b/.gitignore
index 327ea97..860c423 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 /build*/
-/subprojects/*/
+/subprojects/*
+!/subprojects/googletest.wrap
+!/subprojects/sdbusplus.wrap
+!/subprojects/stdplus.wrap
diff --git a/src/meson.build b/src/meson.build
index a9e0620..e736633 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -70,4 +70,5 @@
 
 install_headers(
   'sdeventplus/utility/timer.hpp',
+  'sdeventplus/utility/sdbus.hpp',
   subdir: 'sdeventplus/utility')
diff --git a/src/sdeventplus/utility/sdbus.hpp b/src/sdeventplus/utility/sdbus.hpp
new file mode 100644
index 0000000..bd4c5ce
--- /dev/null
+++ b/src/sdeventplus/utility/sdbus.hpp
@@ -0,0 +1,43 @@
+#include <sdbusplus/bus.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/exception.hpp>
+#include <stdexcept>
+
+namespace sdeventplus::utility
+{
+
+/**
+ * @brief Run and event loop with a given bus, bug workaround
+ * @detail There is a known issue with systemd not correctly dispatching
+ *         all of the events for a given bus. We need a workaround to
+ *         prevent memory leaks.
+ *         https://github.com/systemd/systemd/issues/22046
+ */
+inline int loopWithBus(Event& event, sdbusplus::bus_t& bus)
+{
+    bus.attach_event(event.get(), SD_EVENT_PRIORITY_NORMAL);
+    try
+    {
+        do
+        {
+            if (bus.is_open())
+            {
+                // Process all outstanding bus events before running the loop.
+                // This prevents the sd-bus handling logic from leaking memory.
+                while (bus.process_discard() > 0)
+                    ;
+            }
+        } while (event.run(std::nullopt) > 0);
+    }
+    catch (const SdEventError& e)
+    {
+        if (e.code().value() == ESTALE)
+        {
+            return event.get_exit_code();
+        }
+        throw;
+    }
+    throw std::runtime_error("Unknown sd-event terminaton");
+}
+
+} // namespace sdeventplus::utility
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
deleted file mode 100644
index 6847ae5..0000000
--- a/subprojects/fmt.wrap
+++ /dev/null
@@ -1,3 +0,0 @@
-[wrap-git]
-url = https://github.com/fmtlib/fmt
-revision = HEAD
diff --git a/subprojects/sdbusplus.wrap b/subprojects/sdbusplus.wrap
new file mode 100644
index 0000000..edd9a31
--- /dev/null
+++ b/subprojects/sdbusplus.wrap
@@ -0,0 +1,7 @@
+[wrap-git]
+url = https://github.com/openbmc/sdbusplus.git
+revision = HEAD
+
+[provide]
+sdbusplus = sdbusplus_dep
+program_names = sdbus++, sdbus++-gen-meson
diff --git a/test/meson.build b/test/meson.build
index 29704ca..b2cdbc1 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -31,11 +31,20 @@
   'source/io',
   'source/signal',
   'source/time',
+  'utility/sdbus',
   'utility/timer',
 ]
 
 foreach t : tests
-  test(t, executable(t.underscorify(), t + '.cpp',
-                     implicit_include_directories: false,
-                     dependencies: [sdeventplus_dep, gtest, gmock]))
+  test(t,
+    executable(
+      t.underscorify(),
+      t + '.cpp',
+      implicit_include_directories: false,
+      dependencies: [
+        dependency('sdbusplus'),
+        sdeventplus_dep,
+        gtest,
+        gmock,
+      ]))
 endforeach
diff --git a/test/utility/sdbus.cpp b/test/utility/sdbus.cpp
new file mode 100644
index 0000000..dda33c3
--- /dev/null
+++ b/test/utility/sdbus.cpp
@@ -0,0 +1,43 @@
+#include <gtest/gtest.h>
+#include <sdeventplus/source/event.hpp>
+#include <sdeventplus/utility/sdbus.hpp>
+
+namespace sdeventplus::utility
+{
+
+struct LoopWithBus : testing::Test
+{
+    Event event = Event::get_new();
+    sdbusplus::bus_t bus = sdbusplus::bus::new_bus();
+};
+
+TEST_F(LoopWithBus, ImmediateExit)
+{
+    event.exit(0);
+    EXPECT_EQ(0, loopWithBus(event, bus));
+}
+
+TEST_F(LoopWithBus, DelayedExit)
+{
+    source::Defer(event, [](source::EventBase& b) {
+        b.get_event().exit(1);
+    }).set_floating(true);
+    EXPECT_EQ(1, loopWithBus(event, bus));
+}
+
+TEST_F(LoopWithBus, ExitSources)
+{
+    int d1 = 0, d2 = 0;
+    source::Exit(event, [&](source::EventBase&) { d1 = 1; }).set_floating(true);
+    source::Defer(event, [&](source::EventBase& b) {
+        source::Exit(event, [&](source::EventBase&) {
+            d2 = 2;
+        }).set_floating(true);
+        b.get_event().exit(3);
+    }).set_floating(true);
+    EXPECT_EQ(3, loopWithBus(event, bus));
+    EXPECT_EQ(1, d1);
+    EXPECT_EQ(2, d2);
+}
+
+} // namespace sdeventplus::utility