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 +#include #include #include #include @@ -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")