From 516c38a750f166320387084f38868b7d02f64777 Mon Sep 17 00:00:00 2001
From: Rusty Russell <rusty@rustcorp.com.au>
Date: Fri, 3 Sep 2021 19:46:21 +0930
Subject: [PATCH] lightningd: call shutdown plugin when we dynamic shutdown a
 single one.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
---
 lightningd/plugin_control.c    | 62 +++++++++++++++++++++++++++++-----
 tests/plugins/test_libplugin.c | 20 +++++++++++
 tests/test_plugin.py           | 22 ++++++++++++
 3 files changed, 95 insertions(+), 9 deletions(-)

diff --git a/lightningd/plugin_control.c b/lightningd/plugin_control.c
index 06aaf3f42..db8836c2b 100644
--- a/lightningd/plugin_control.c
+++ b/lightningd/plugin_control.c
@@ -1,4 +1,5 @@
 #include <ccan/opt/opt.h>
+#include <lightningd/notification.h>
 #include <lightningd/options.h>
 #include <lightningd/plugin_control.h>
 #include <lightningd/plugin_hook.h>
@@ -101,11 +102,44 @@ plugin_dynamic_startdir(struct plugin_command *pcmd, const char *dir_path)
 	return command_still_pending(pcmd->cmd);
 }
 
+static struct command_result *plugin_stop(struct command *cmd, struct plugin *p,
+					  bool kill)
+{
+	struct json_stream *response;
+	const char *stopmsg = tal_fmt(NULL, "Successfully stopped %s.",
+				      p->shortname);
+
+	if (kill)
+		plugin_kill(p, LOG_INFORM, "stopped by lightningd via RPC");
+
+	response = json_stream_success(cmd);
+	json_add_string(response, "command", "stop");
+	json_add_string(response, "result", take(stopmsg));
+	return command_success(cmd, response);
+}
+
+/* If plugin stops itself, we end up here. */
+static void plugin_stopped(struct plugin *p, struct command *cmd)
+{
+	plugin_stop(cmd, p, false);
+}
+
+struct plugin_stop_timeout {
+	struct command *cmd;
+	struct plugin *p;
+};
+
+static void plugin_stop_timeout(struct plugin_stop_timeout *pst)
+{
+	log_unusual(pst->p->log, "Timeout on shutdown: killing anyway");
+	tal_del_destructor2(pst->p, plugin_stopped, pst->cmd);
+	plugin_stop(pst->cmd, pst->p, true);
+}
+
 static struct command_result *
 plugin_dynamic_stop(struct command *cmd, const char *plugin_name)
 {
 	struct plugin *p;
-	struct json_stream *response;
 
 	list_for_each(&cmd->ld->plugins->plugins, p, list) {
 		if (plugin_paths_match(p->cmd, plugin_name)) {
@@ -114,14 +148,24 @@ plugin_dynamic_stop(struct command *cmd, const char *plugin_name)
 				                    "%s cannot be managed when "
 				                    "lightningd is up",
 				                    plugin_name);
-			plugin_kill(p, LOG_INFORM,
-				    "stopped by lightningd via RPC");
-			response = json_stream_success(cmd);
-			json_add_string(response, "command", "stop");
-			json_add_string(response, "result",
-			                take(tal_fmt(NULL, "Successfully stopped %s.",
-			                             plugin_name)));
-			return command_success(cmd, response);
+
+			/* If it's interested in clean shutdown, tell it. */
+			if (notify_plugin_shutdown(cmd->ld, p)) {
+				struct plugin_stop_timeout *pst;
+
+				/* Kill in 30 seconds if it doesn't exit. */
+				pst = tal(p, struct plugin_stop_timeout);
+				pst->p = p;
+				pst->cmd = cmd;
+				notleak(new_reltimer(cmd->ld->timers, pst,
+						     time_from_sec(30),
+						     plugin_stop_timeout,
+						     pst));
+
+				tal_add_destructor2(p, plugin_stopped, cmd);
+				return command_still_pending(cmd);
+			}
+			return plugin_stop(cmd, p, true);
 		}
 	}
 
diff --git a/tests/plugins/test_libplugin.c b/tests/plugins/test_libplugin.c
index ef62ab963..39774e64d 100644
--- a/tests/plugins/test_libplugin.c
+++ b/tests/plugins/test_libplugin.c
@@ -6,6 +6,7 @@
 
 const char *name_option;
 static bool self_disable = false;
+static bool dont_shutdown = false;
 
 static struct command_result *json_helloworld(struct command *cmd,
 					      const char *buf,
@@ -56,6 +57,18 @@ static void json_connected(struct command *cmd,
 		   json_strdup(tmpctx, buf, idtok));
 }
 
+static void json_shutdown(struct command *cmd,
+			  const char *buf,
+			  const jsmntok_t *params)
+{
+	plugin_log(cmd->plugin, LOG_DBG, "shutdown called");
+
+	if (dont_shutdown)
+		return;
+
+	plugin_exit(cmd->plugin, 0);
+}
+
 static struct command_result *testrpc_cb(struct command *cmd,
 					 const char *buf,
 					 const jsmntok_t *params,
@@ -137,6 +150,9 @@ static const struct plugin_hook hooks[] = { {
 static const struct plugin_notification notifs[] = { {
 		"connect",
 		json_connected,
+	}, {
+		"shutdown",
+		json_shutdown
 	}
 };
 
@@ -159,5 +175,9 @@ int main(int argc, char *argv[])
 				  "flag",
 				  "Whether to disable.",
 				  flag_option, &self_disable),
+		    plugin_option("dont_shutdown",
+				  "flag",
+				  "Whether to timeout when asked to shutdown.",
+				  flag_option, &dont_shutdown),
 		    NULL);
 }
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index ae284c5c7..b590ba680 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -2550,3 +2550,25 @@ plugin.run()
     n.daemon.wait_for_log(r"Plugin changed, needs restart.")
     n.daemon.wait_for_log(r"test_restart_on_update 2")
     n.stop()
+
+
+def test_plugin_shutdown(node_factory):
+    """test 'shutdown' notification"""
+    p = os.path.join(os.getcwd(), "tests/plugins/test_libplugin")
+    l1 = node_factory.get_node(options={'plugin': p})
+
+    l1.rpc.plugin_stop(p)
+    l1.daemon.wait_for_log(r"test_libplugin: shutdown called")
+    # FIXME: clean this up!
+    l1.daemon.wait_for_log(r"test_libplugin: Killing plugin: exited during normal operation")
+
+    # Now try timeout.
+    l1.rpc.plugin_start(p, dont_shutdown=True)
+    l1.rpc.plugin_stop(p)
+    l1.daemon.wait_for_log(r"test_libplugin: shutdown called")
+    l1.daemon.wait_for_log(r"test_libplugin: Timeout on shutdown: killing anyway")
+
+    # Now, should also shutdown on finish.
+    l1.rpc.plugin_start(p)
+    l1.rpc.stop()
+    l1.daemon.wait_for_log(r"test_libplugin: shutdown called")