upnp: use dedicated io_context if none provided

Change-Id: I8482f636007e3625576f5fc9f6f95c3d5be6d2bb
diff --git a/include/upnp/upnp_context.h b/include/upnp/upnp_context.h
index cabfbba..3f24bca 100644
--- a/include/upnp/upnp_context.h
+++ b/include/upnp/upnp_context.h
@@ -107,6 +107,8 @@
     UPnPContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger);
     ~UPnPContext();
 
+    std::shared_ptr<asio::io_context> createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger);
+
     // Terminate the instance.
     void shutdown();
 
@@ -311,6 +313,9 @@
 
     // Shutdown synchronization
     bool shutdownComplete_ {false};
+
+    // Thread
+    std::unique_ptr<std::thread> ioContextRunner_;
 };
 
 } // namespace upnp
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
index 335f70b..9d79f49 100644
--- a/src/upnp/upnp_context.cpp
+++ b/src/upnp/upnp_context.cpp
@@ -44,7 +44,7 @@
 constexpr static uint16_t UPNP_UDP_PORT_MAX {UPNP_UDP_PORT_MIN + 5000};
 
 UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, const std::shared_ptr<dht::log::Logger>& logger)
- : mappingListUpdateTimer_(*ioContext), ctx(ioContext), logger_(logger)
+ : ctx(createIoContext(ioContext, logger)), mappingListUpdateTimer_(*ioContext), logger_(logger)
 {
     if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
 
@@ -55,6 +55,25 @@
     ctx->post([this] { init(); });
 }
 
+std::shared_ptr<asio::io_context>
+UPnPContext::createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger) {
+    if (ctx) {
+        return ctx;
+    } else {
+        if (logger) logger->debug("UPnPContext: starting dedicated io_context thread");
+        auto ioCtx = std::make_shared<asio::io_context>();
+        ioContextRunner_ = std::make_unique<std::thread>([ioCtx, l=logger]() {
+            try {
+                auto work = asio::make_work_guard(*ioCtx);
+                ioCtx->run();
+            } catch (const std::exception& ex) {
+                if (l) l->error("Unexpected io_context thread exception: {}", ex.what());
+            }
+        });
+        return ioCtx;
+    }
+}
+
 void
 UPnPContext::shutdown(std::condition_variable& cv)
 {
@@ -75,6 +94,13 @@
         shutdownComplete_ = true;
         cv.notify_one();
     }
+
+    if (ioContextRunner_) {
+        if (logger_) logger_->debug("UPnPContext: stopping io_context thread");
+        ctx->stop();
+        ioContextRunner_->join();
+        ioContextRunner_.reset();
+    }
 }
 
 void