Add the pieces for a watchdog for dangling plugins

Once we register our plugins here, we should be able to gracefully shut
down any plugins whose remote process got killed.
This commit is contained in:
Robbert van der Helm
2021-05-01 16:45:37 +02:00
parent 167784f93b
commit 0f2db64aeb
4 changed files with 194 additions and 9 deletions
+4
View File
@@ -34,3 +34,7 @@ void HostBridge::handle_win32_events() {
DispatchMessage(&msg);
}
}
void HostBridge::shutdown_if_dangling() {
// TODO: Implement
}
+13 -3
View File
@@ -24,9 +24,11 @@
#include "../utils.h"
/**
* The base for the Wine plugin host bridge interface for all plugin types. This
* only has to be able to handle Win32 and X11 events. Implementations of this
* will actually host a plugin and do all the function call forwarding.
* The base for the Wine plugin host bridge interfaces for all plugin types.
* This mostly concerns event handling, and some common setup like loggers and a
* watchdog timer to let us shut down the sockets when the native host has
* exited while the sockets are still alive. Implementations of this will
* actually host a plugin and do all the function call forwarding.
*/
class HostBridge {
protected:
@@ -86,6 +88,14 @@ class HostBridge {
*/
void handle_win32_events();
/**
* Used as part of the watchdog. This will check whether the remote host
* process this bridge is connected with is still active. If it is not, then
* we'll close the sockets, which will cause this process to exit
* gracefully.
*/
void shutdown_if_dangling();
/**
* The path to the .dll being loaded in the Wine plugin host.
*/
+79 -1
View File
@@ -16,6 +16,10 @@
#include "utils.h"
#include "bridges/common.h"
using namespace std::literals::chrono_literals;
uint32_t WINAPI
win32_thread_trampoline(fu2::unique_function<void()>* entry_point) {
(*entry_point)();
@@ -71,7 +75,18 @@ Win32Timer& Win32Timer::operator=(Win32Timer&& o) {
return *this;
}
MainContext::MainContext() : context(), events_timer(context) {}
MainContext::MainContext()
: context(),
events_timer(context),
watchdog_context(),
watchdog_timer(watchdog_context) {
async_handle_watchdog();
watchdog_handler = Win32Thread([&]() {
set_realtime_priority(false);
watchdog_context.run();
});
}
void MainContext::run() {
context.run();
@@ -85,3 +100,66 @@ void MainContext::update_timer_interval(
std::chrono::steady_clock::duration new_interval) {
timer_interval = new_interval;
}
MainContext::WatchdogGuard::WatchdogGuard(
HostBridge& bridge,
std::set<HostBridge*>& watched_bridges,
std::mutex& watched_bridges_mutex)
: bridge(&bridge),
watched_bridges(watched_bridges),
watched_bridges_mutex(watched_bridges_mutex) {
std::lock_guard lock(watched_bridges_mutex);
watched_bridges.insert(&bridge);
}
MainContext::WatchdogGuard::~WatchdogGuard() {
if (is_active) {
std::lock_guard lock(watched_bridges_mutex.get());
watched_bridges.get().erase(bridge);
}
}
MainContext::WatchdogGuard::WatchdogGuard(WatchdogGuard&& o)
: bridge(std::move(o.bridge)),
watched_bridges(std::move(o.watched_bridges)),
watched_bridges_mutex(std::move(o.watched_bridges_mutex)) {
o.is_active = false;
}
MainContext::WatchdogGuard& MainContext::WatchdogGuard::operator=(
WatchdogGuard&& o) {
bridge = std::move(o.bridge);
watched_bridges = std::move(o.watched_bridges);
watched_bridges_mutex = std::move(o.watched_bridges_mutex);
o.is_active = false;
return *this;
}
MainContext::WatchdogGuard MainContext::register_watchdog(HostBridge& bridge) {
// The guard's constructor and destructor will handle actually registering
// and unregistering the bridge from `watched_bridges`
return WatchdogGuard(bridge, watched_bridges, watched_bridges_mutex);
}
void MainContext::async_handle_watchdog() {
// Try to keep a steady framerate, but add in delays to let other events
// get handled if the GUI message handling somehow takes very long.
watchdog_timer.expires_at(std::chrono::steady_clock::now() + 20s);
watchdog_timer.async_wait([&](const boost::system::error_code& error) {
if (error.failed()) {
return;
}
// When the `WatchdogGuard` field on `HostBridge` gets destroyed, that
// bridge instance will be removed from `watched_bridges`. So if our
// call to `HostBridge::shutdown_if_dangling()` shuts the plugin down,
// the instance will be removed after this lambda exits.
std::lock_guard lock(watched_bridges_mutex);
for (auto& bridge : watched_bridges) {
bridge->shutdown_if_dangling();
}
async_handle_watchdog();
});
}
+98 -5
View File
@@ -21,6 +21,7 @@
#include <future>
#include <memory>
#include <optional>
#include <set>
#ifndef NOMINMAX
#define NOMINMAX
@@ -34,6 +35,9 @@
#include "../common/utils.h"
// Forward declaration for use in our watchdog in `MainContext`
class HostBridge;
/**
* A proxy function that calls `Win32Thread::entry_point` since `CreateThread()`
* is not usable with lambdas directly. Calling the passed function will invoke
@@ -152,11 +156,16 @@ class Win32Timer {
/**
* A wrapper around `boost::asio::io_context()` to serve as the application's
* main IO context. A single instance is shared for all plugins in a plugin
* group so that several important events can be handled on the main thread,
* which can be required because in the Win32 model all GUI related operations
* have to be handled from the same thread. This will be run from the
* application's main thread.
* main IO context, run from the GUI thread. A single instance is shared for all
* plugins in a plugin group so that several important events can be handled on
* the main thread, which can be required because in the Win32 model all GUI
* related operations have to be handled from the same thread.
*
* This also spawns a second IO context in its own thread, which will be used as
* a watchdog to shutdown a plugin instance's sockets when the process that
* spawned it is no longer active. This approach also works with plugin groups
* since closing a plugin's sockets will only cause that one plugin to
* terminate.
*/
class MainContext {
public:
@@ -182,6 +191,52 @@ class MainContext {
void update_timer_interval(
std::chrono::steady_clock::duration new_interval);
/**
* The RAII guard used to register and unregister host bridge instances from
* our watchdog.
*/
class WatchdogGuard {
public:
WatchdogGuard(HostBridge& bridge,
std::set<HostBridge*>& watched_bridges,
std::mutex& watched_bridges_mutex);
~WatchdogGuard();
WatchdogGuard(const WatchdogGuard&) = delete;
WatchdogGuard& operator=(const WatchdogGuard&) = delete;
WatchdogGuard(WatchdogGuard&& o);
WatchdogGuard& operator=(WatchdogGuard&& o);
private:
/**
* Used to facilitate moves.
*/
bool is_active = true;
/**
* The bridge that we will add to the watchdog list when this object
* gets created, and that we'll remove from the list again when this
* object gets destroyed.
*/
HostBridge* bridge;
// References to the same two fields on `MainContext`, so we don't have
// to use `friend`
std::reference_wrapper<std::set<HostBridge*>> watched_bridges;
std::reference_wrapper<std::mutex> watched_bridges_mutex;
};
/**
* Register a bridge instance for our watchdog. We'll periodically check if
* the remote (native) host process that should be connected to the bridge
* instance is still alive, and we'll shut down the bridge if it is not to
* prevent dangling processes. The returned guard should be stored as a
* field in `HostBridge`, and the watchdog will automatically be
* unregistered once this guard drops from scope.
*/
WatchdogGuard register_watchdog(HostBridge& bridge);
/**
* Asynchronously execute a function inside of this main IO context and
* return the results as a future. This is used to make sure that operations
@@ -262,6 +317,16 @@ class MainContext {
boost::asio::io_context context;
private:
/**
* Start a timer to periodically check whether the host processes belong to
* all active plugin bridges are still alive. We will shut down the plugin
* instances where this is not the case, so that this process can gracefully
* terminate. In some cases Unix Domain Sockets are left in a state where
* it's impossible to tell that the remote isn't alive anymore, and where
* `recv()` will just hang indefinitely. We use this watchdog to avoid this.
*/
void async_handle_watchdog();
/**
* The timer used to periodically handle X11 events and Win32 messages.
*/
@@ -276,4 +341,32 @@ class MainContext {
*/
std::chrono::steady_clock::duration timer_interval =
std::chrono::milliseconds(1000) / 60;
/**
* The IO context used for the watchdog described below.
*/
boost::asio::io_context watchdog_context;
/**
* The timer used to periodically check if the host processes are still
* active, so we can shut down a plugin's sockets (and with that the plugin
* itself) when the host has exited and the sockets are somehow not closed
* yet..
*/
boost::asio::steady_timer watchdog_timer;
/**
* All of the bridges we're watching as part of our watchdog. We're storing
* pointers for efficiency's sake, since reference wrappers don't implement
* any comparison operators.
*/
std::set<HostBridge*> watched_bridges;
std::mutex watched_bridges_mutex;
/**
* The thread where we run our watchdog timer, to shut down plugins after
* the native plugin host process they're supposed to be connected to has
* died.
*/
Win32Thread watchdog_handler;
};