From 3639c2aa23bae4a9fb5b81aef46ac16c2ef0722d Mon Sep 17 00:00:00 2001 From: devgianlu Date: Tue, 21 Jul 2026 15:09:47 +0200 Subject: [PATCH] feat: self-shutdown when the host process exits Add a parent-death watchdog to PluginServer: when the go-plugin host dies without killing the plugin (e.g. the host is SIGKILLed), nothing signals the plugin and Wait() would block forever, orphaning the process. Detect reparenting (getppid changes) and drive Shutdown(), mirroring the Go go-plugin server's built-in orphan protection. Co-Authored-By: Claude Opus 4.8 --- include/go_plugin/server.hpp | 23 ++++++++++++++++-- src/server.cpp | 45 ++++++++++++++++++++++++++++++++++++ vcpkg.json | 2 +- 3 files changed, 67 insertions(+), 3 deletions(-) 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",