Add sdbusplus::asio coroutine method handling

Adding the server-side of the coroutine path allows yielding
asynchronous method handling via coroutines. This means that a method
handler can call a yielding dbus call (or other asio-based asynchronous
call) without blocking the rest of the process.

The call path might look something like this:

service.thing/object/path/interface.my-method()
    - do something
    - yield_method_call(other.service, /other/path,
                 other.interface, other-method)
    <yields to other coroutine>

execute other code in another context

    <returns some time later with dbus call's response>
    - use response from other method
    <- return my-method response

This also changes the asio-example, pulling it apart into a
client/server model so it is more clear about how to use the yielding
async method handling and yielding async method calls.

Change-Id: I23ccf7a9a8dff787be78929959c1f018280a0392
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
diff --git a/example/asio-example.cpp b/example/asio-example.cpp
index 9819eba..b4f6265 100644
--- a/example/asio-example.cpp
+++ b/example/asio-example.cpp
@@ -7,22 +7,47 @@
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/asio/sd_event.hpp>
 #include <sdbusplus/bus.hpp>
+#include <sdbusplus/exception.hpp>
 #include <sdbusplus/server.hpp>
 #include <sdbusplus/timer.hpp>
 
 using variant = sdbusplus::message::variant<int, std::string>;
+
 int foo(int test)
 {
+    std::cout << "foo(" << test << ") -> " << (test + 1) << "\n";
     return ++test;
 }
 
+// called from coroutine context, can make yielding dbus calls
+int fooYield(boost::asio::yield_context yield,
+             std::shared_ptr<sdbusplus::asio::connection> conn, int test)
+{
+    // fetch the real value from testFunction
+    boost::system::error_code ec;
+    std::cout << "fooYield(yield, " << test << ")...\n";
+    int testCount = conn->yield_method_call<int>(
+        yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+        "xyz.openbmc_project.test", "TestFunction", test);
+    if (ec || testCount != (test + 1))
+    {
+        std::cout << "call to foo failed: ec = " << ec << '\n';
+        return -1;
+    }
+    std::cout << "yielding call to foo OK! (-> " << testCount << ")\n";
+    return testCount;
+}
+
 int methodWithMessage(sdbusplus::message::message& m, int test)
 {
+    std::cout << "methodWithMessage(m, " << test << ") -> " << (test + 1)
+              << "\n";
     return ++test;
 }
 
 int voidBar(void)
 {
+    std::cout << "voidBar() -> 42\n";
     return 42;
 }
 
@@ -66,21 +91,75 @@
     }
 }
 
-void do_start_async_method_call_two(
-    std::shared_ptr<sdbusplus::asio::connection> conn,
-    boost::asio::yield_context yield)
+void do_start_async_ipmi_call(std::shared_ptr<sdbusplus::asio::connection> conn,
+                              boost::asio::yield_context yield)
+{
+    auto method = conn->new_method_call("xyz.openbmc_project.asio-test",
+                                        "/xyz/openbmc_project/test",
+                                        "xyz.openbmc_project.test", "execute");
+    constexpr uint8_t netFn = 6;
+    constexpr uint8_t lun = 0;
+    constexpr uint8_t cmd = 1;
+    std::map<std::string, variant> options = {{"username", variant("admin")},
+                                              {"privilege", variant(4)}};
+    std::vector<uint8_t> commandData = {4, 3, 2, 1};
+    method.append(netFn, lun, cmd, commandData, options);
+    boost::system::error_code ec;
+    sdbusplus::message::message reply = conn->async_send(method, yield[ec]);
+    std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, std::vector<uint8_t>>
+        tupleOut;
+    try
+    {
+        reply.read(tupleOut);
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        std::cerr << "failed to unpack; sig is " << reply.get_signature()
+                  << "\n";
+    }
+    auto& [rnetFn, rlun, rcmd, cc, responseData] = tupleOut;
+    std::vector<uint8_t> expRsp = {1, 2, 3, 4};
+    if (rnetFn == uint8_t(netFn + 1) && rlun == lun && rcmd == cmd && cc == 0 &&
+        responseData == expRsp)
+    {
+        std::cerr << "ipmi call returns OK!\n";
+    }
+    else
+    {
+        std::cerr << "ipmi call returns unexpected response\n";
+    }
+}
+
+auto ipmiInterface(boost::asio::yield_context yield, uint8_t netFn, uint8_t lun,
+                   uint8_t cmd, std::vector<uint8_t>& data,
+                   const std::map<std::string, variant>& options)
+{
+    std::vector<uint8_t> reply = {1, 2, 3, 4};
+    uint8_t cc = 0;
+    std::cerr << "ipmiInterface:execute(" << int(netFn) << int(cmd) << ")\n";
+    return std::make_tuple(uint8_t(netFn + 1), lun, cmd, cc, reply);
+}
+
+void do_start_async_to_yield(std::shared_ptr<sdbusplus::asio::connection> conn,
+                             boost::asio::yield_context yield)
 {
     boost::system::error_code ec;
-    int32_t testCount;
-    std::string testValue;
-    std::tie(testCount, testValue) =
-        conn->yield_method_call<int32_t, std::string>(
+    int testValue = 0;
+    try
+    {
+        testValue = conn->yield_method_call<int>(
             yield[ec], "xyz.openbmc_project.asio-test",
             "/xyz/openbmc_project/test", "xyz.openbmc_project.test",
-            "TestMethod", int32_t(42));
-    if (!ec && testCount == 42 && testValue == "success: 42")
+            "TestYieldFunction", int(41));
+    }
+    catch (sdbusplus::exception::SdBusError& e)
     {
-        std::cout << "async call to TestMethod serialized via yield OK!\n";
+        std::cout << "oops: " << e.what() << "\n";
+    }
+    if (!ec && testValue == 42)
+    {
+        std::cout
+            << "yielding call to TestYieldFunction serialized via yield OK!\n";
     }
     else
     {
@@ -88,60 +167,12 @@
     }
 }
 
-int main()
+int server()
 {
-    using GetSubTreeType = std::vector<std::pair<
-        std::string,
-        std::vector<std::pair<std::string, std::vector<std::string>>>>>;
-    using message = sdbusplus::message::message;
     // setup connection to dbus
     boost::asio::io_service io;
     auto conn = std::make_shared<sdbusplus::asio::connection>(io);
 
-    // test async method call and async send
-    auto mesg =
-        conn->new_method_call("xyz.openbmc_project.ObjectMapper",
-                              "/xyz/openbmc_project/object_mapper",
-                              "xyz.openbmc_project.ObjectMapper", "GetSubTree");
-
-    static const auto depth = 2;
-    static const std::vector<std::string> interfaces = {
-        "xyz.openbmc_project.Sensor.Value"};
-    mesg.append("/xyz/openbmc_project/Sensors", depth, interfaces);
-
-    conn->async_send(mesg, [](boost::system::error_code ec, message& ret) {
-        std::cout << "async_send callback\n";
-        if (ec || ret.is_method_error())
-        {
-            std::cerr << "error with async_send\n";
-            return;
-        }
-        GetSubTreeType data;
-        ret.read(data);
-        for (auto& item : data)
-        {
-            std::cout << item.first << "\n";
-        }
-    });
-
-    conn->async_method_call(
-        [](boost::system::error_code ec, GetSubTreeType& subtree) {
-            std::cout << "async_method_call callback\n";
-            if (ec)
-            {
-                std::cerr << "error with async_method_call\n";
-                return;
-            }
-            for (auto& item : subtree)
-            {
-                std::cout << item.first << "\n";
-            }
-        },
-        "xyz.openbmc_project.ObjectMapper",
-        "/xyz/openbmc_project/object_mapper",
-        "xyz.openbmc_project.ObjectMapper", "GetSubTree",
-        "/org/openbmc/control", 2, std::vector<std::string>());
-
     // test object server
     conn->request_name("xyz.openbmc_project.asio-test");
     auto server = sdbusplus::asio::object_server(conn);
@@ -191,12 +222,99 @@
 
     iface->register_method("TestFunction", foo);
 
+    // fooYield has boost::asio::yield_context as first argument
+    // so will be executed in coroutine context if called
+    iface->register_method("TestYieldFunction",
+                           [conn](boost::asio::yield_context yield, int val) {
+                               return fooYield(yield, conn, val);
+                           });
+
     iface->register_method("TestMethodWithMessage", methodWithMessage);
 
     iface->register_method("VoidFunctionReturnsInt", voidBar);
 
+    iface->register_method("execute", ipmiInterface);
+
     iface->initialize();
-    iface->set_property("int", 45);
+
+    io.run();
+
+    return 0;
+}
+
+int client()
+{
+    using GetSubTreeType = std::vector<std::pair<
+        std::string,
+        std::vector<std::pair<std::string, std::vector<std::string>>>>>;
+    using message = sdbusplus::message::message;
+
+    // setup connection to dbus
+    boost::asio::io_service io;
+    auto conn = std::make_shared<sdbusplus::asio::connection>(io);
+
+    int ready = 0;
+    while (!ready)
+    {
+        auto readyMsg = conn->new_method_call(
+            "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+            "xyz.openbmc_project.test", "VoidFunctionReturnsInt");
+        try
+        {
+            message intMsg = conn->call(readyMsg);
+            intMsg.read(ready);
+        }
+        catch (sdbusplus::exception::SdBusError& e)
+        {
+            ready = 0;
+            // pause to give the server a chance to start up
+            usleep(10000);
+        }
+    }
+
+    // test async method call and async send
+    auto mesg =
+        conn->new_method_call("xyz.openbmc_project.ObjectMapper",
+                              "/xyz/openbmc_project/object_mapper",
+                              "xyz.openbmc_project.ObjectMapper", "GetSubTree");
+
+    static const auto depth = 2;
+    static const std::vector<std::string> interfaces = {
+        "xyz.openbmc_project.Sensor.Value"};
+    mesg.append("/xyz/openbmc_project/Sensors", depth, interfaces);
+
+    conn->async_send(mesg, [](boost::system::error_code ec, message& ret) {
+        std::cout << "async_send callback\n";
+        if (ec || ret.is_method_error())
+        {
+            std::cerr << "error with async_send\n";
+            return;
+        }
+        GetSubTreeType data;
+        ret.read(data);
+        for (auto& item : data)
+        {
+            std::cout << item.first << "\n";
+        }
+    });
+
+    conn->async_method_call(
+        [](boost::system::error_code ec, GetSubTreeType& subtree) {
+            std::cout << "async_method_call callback\n";
+            if (ec)
+            {
+                std::cerr << "error with async_method_call\n";
+                return;
+            }
+            for (auto& item : subtree)
+            {
+                std::cout << item.first << "\n";
+            }
+        },
+        "xyz.openbmc_project.ObjectMapper",
+        "/xyz/openbmc_project/object_mapper",
+        "xyz.openbmc_project.ObjectMapper", "GetSubTree",
+        "/org/openbmc/control", 2, std::vector<std::string>());
 
     // sd_events work too using the default event loop
     phosphor::Timer t1([]() { std::cerr << "*** tock ***\n"; });
@@ -212,9 +330,53 @@
         do_start_async_method_call_one(conn, yield);
     });
     boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
-        do_start_async_method_call_two(conn, yield);
+        do_start_async_ipmi_call(conn, yield);
     });
+    boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
+        do_start_async_to_yield(conn, yield);
+    });
+
+    conn->async_method_call(
+        [](boost::system::error_code ec, int32_t testValue) {
+            if (ec)
+            {
+                std::cerr << "TestYieldFunction returned error with "
+                             "async_method_call (ec = "
+                          << ec << ")\n";
+                return;
+            }
+            std::cout << "TestYieldFunction return " << testValue << "\n";
+        },
+        "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+        "xyz.openbmc_project.test", "TestYieldFunction", int32_t(41));
     io.run();
 
     return 0;
 }
+
+int main(int argc, const char* argv[])
+{
+    if (argc == 1)
+    {
+        int pid = fork();
+        if (pid == 0)
+        {
+            return client();
+        }
+        else if (pid > 0)
+        {
+            return server();
+        }
+        return pid;
+    }
+    if (std::string(argv[1]) == "--server")
+    {
+        return server();
+    }
+    if (std::string(argv[1]) == "--client")
+    {
+        return client();
+    }
+    std::cout << "usage: " << argv[0] << " [--server | --client]\n";
+    return -1;
+}