diff --git a/include/go_plugin/server.hpp b/include/go_plugin/server.hpp index 6a4cb72..cf84018 100644 --- a/include/go_plugin/server.hpp +++ b/include/go_plugin/server.hpp @@ -1,8 +1,12 @@ #pragma once +#include #include +#include #include #include +#include +#include #include #include @@ -85,8 +89,8 @@ class PluginServer { bool Start(std::string* error = nullptr); /** - * Blocks until the server stops (i.e. until the host process exits or - * Shutdown() is called). + * Blocks until the server stops (i.e. until Shutdown() is called or the + * host process exits — see the parent-death watchdog started by Start()). */ void Wait(); @@ -97,9 +101,24 @@ class PluginServer { int port() const { return port_; } private: + // Watches for the host (parent) process disappearing. go-plugin hosts stop a + // plugin by killing it, but when the host itself dies unexpectedly (e.g. it + // is SIGKILLed) nothing signals the plugin and Wait() would block forever, + // orphaning the process. The watchdog detects reparenting (getppid changes) + // and calls Shutdown(), mirroring the Go go-plugin server's built-in + // orphan protection. Started by Start(), stopped by Shutdown()/destruction. + void StartParentWatchdog(); + void StopParentWatchdog(); + ServeConfig config_; std::unique_ptr server_; int port_ = 0; + + std::thread parent_watchdog_; + std::mutex watchdog_mu_; + std::condition_variable watchdog_cv_; + bool watchdog_stop_ = false; + pid_t host_pid_ = 0; }; /** Returned by the Serve() convenience function. */ diff --git a/src/server.cpp b/src/server.cpp index bf61a52..5f86038 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -90,6 +91,7 @@ int PickPort() { PluginServer::PluginServer(ServeConfig config) : config_(std::move(config)) {} PluginServer::~PluginServer() { + StopParentWatchdog(); if (server_) { server_->Shutdown(); server_->Wait(); @@ -103,6 +105,11 @@ bool PluginServer::Start(std::string *out_error) { return false; }; + // Record the launching host's pid up front, before any startup work could let + // it reparent us — this is the baseline the parent-death watchdog compares + // against. The watchdog itself only starts once startup succeeds (below). + host_pid_ = getppid(); + // ── 1. Magic cookie check ──────────────────────────────────────────── if (!ValidateMagicCookie(config_.handshake)) { return fail("magic cookie mismatch: plugin not invoked by a go-plugin host " @@ -160,6 +167,10 @@ bool PluginServer::Start(std::string *out_error) { << "|" << server_cert_b64 << "\n"; out.flush(); + // Startup succeeded and Wait() is about to become the plugin's blocking point: + // start watching the host (host_pid_, captured at the top of Start()). + StartParentWatchdog(); + return true; } @@ -169,10 +180,44 @@ void PluginServer::Wait() { } void PluginServer::Shutdown() { + // Stop (and, for external callers, join) the watchdog before tearing down the + // server, so no watchdog thread outlives Shutdown() for an external caller. + StopParentWatchdog(); if (server_) server_->Shutdown(); } +void PluginServer::StartParentWatchdog() { + parent_watchdog_ = std::thread([this]() { + std::unique_lock lock(watchdog_mu_); + while (!watchdog_stop_) { + // Reparenting (parent pid changes, typically to the reaper/init) means the + // host that launched us is gone. Drive the same Shutdown() the host would + // have, so we don't linger as an orphan. + if (getppid() != host_pid_) { + lock.unlock(); + std::cerr << "go-plugin: host process exited, shutting down" << std::endl; + Shutdown(); + return; + } + watchdog_cv_.wait_for(lock, std::chrono::seconds(1)); + } + }); +} + +void PluginServer::StopParentWatchdog() { + { + std::lock_guard lock(watchdog_mu_); + watchdog_stop_ = true; + } + watchdog_cv_.notify_all(); + // The watchdog reaches here via its own Shutdown() call on the host-death + // path; it must never join itself (that deadlocks). The destructor joins it. + if (parent_watchdog_.joinable() && + parent_watchdog_.get_id() != std::this_thread::get_id()) + parent_watchdog_.join(); +} + // ── Serve convenience function ──────────────────────────────────────────────── ServeResult Serve(const ServeConfig &config) { diff --git a/vcpkg.json b/vcpkg.json index 134f976..2eec772 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "go-plugin-cpp", - "version": "0.1.0", + "version": "0.1.1", "dependencies": [ "grpc", "openssl",