2021-12-04 12:23:56 +01:00
|
|
|
#include "config.h"
|
2022-06-17 06:48:48 +02:00
|
|
|
#include <ccan/tal/path/path.h>
|
2021-09-16 07:00:42 +02:00
|
|
|
#include <ccan/tal/str/str.h>
|
|
|
|
#include <common/json_command.h>
|
|
|
|
#include <common/json_tok.h>
|
|
|
|
#include <common/memleak.h>
|
|
|
|
#include <common/param.h>
|
|
|
|
#include <common/timeout.h>
|
|
|
|
#include <errno.h>
|
2021-09-03 12:16:21 +02:00
|
|
|
#include <lightningd/notification.h>
|
2019-06-29 13:47:48 +02:00
|
|
|
#include <lightningd/plugin_control.h>
|
2021-09-16 07:00:42 +02:00
|
|
|
#include <unistd.h>
|
2019-06-29 13:47:48 +02:00
|
|
|
|
2019-09-12 01:26:51 +02:00
|
|
|
/* A dummy structure used to give multiple arguments to callbacks. */
|
2021-06-16 03:02:17 +02:00
|
|
|
struct plugin_command {
|
2019-09-12 01:26:51 +02:00
|
|
|
struct command *cmd;
|
2021-06-16 03:02:17 +02:00
|
|
|
const char *subcmd;
|
2019-09-12 01:26:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returned by all subcommands on success.
|
|
|
|
*/
|
2021-06-16 03:02:17 +02:00
|
|
|
static struct command_result *plugin_dynamic_list_plugins(struct plugin_command *pcmd,
|
2020-05-05 03:13:14 +02:00
|
|
|
const struct plugins *plugins)
|
2019-09-12 01:26:51 +02:00
|
|
|
{
|
|
|
|
struct json_stream *response;
|
2020-05-05 03:13:14 +02:00
|
|
|
const struct plugin *p;
|
2019-09-12 01:26:51 +02:00
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
response = json_stream_success(pcmd->cmd);
|
|
|
|
json_add_string(response, "command", pcmd->subcmd);
|
2019-09-12 01:26:51 +02:00
|
|
|
json_array_start(response, "plugins");
|
2020-05-05 03:13:14 +02:00
|
|
|
list_for_each(&plugins->plugins, p, list) {
|
2019-09-12 01:26:51 +02:00
|
|
|
json_object_start(response, NULL);
|
|
|
|
json_add_string(response, "name", p->cmd);
|
|
|
|
json_add_bool(response, "active",
|
2020-05-04 19:19:05 +02:00
|
|
|
p->plugin_state == INIT_COMPLETE);
|
2019-09-12 01:26:51 +02:00
|
|
|
json_object_end(response);
|
|
|
|
}
|
|
|
|
json_array_end(response);
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_success(pcmd->cmd, response);
|
2019-09-12 01:26:51 +02:00
|
|
|
}
|
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
struct command_result *plugin_cmd_killed(struct plugin_command *pcmd,
|
2020-05-05 03:13:14 +02:00
|
|
|
struct plugin *plugin, const char *msg)
|
|
|
|
{
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_fail(pcmd->cmd, PLUGIN_ERROR, "%s: %s", plugin->cmd, msg);
|
2020-05-05 03:13:14 +02:00
|
|
|
}
|
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
struct command_result *plugin_cmd_succeeded(struct plugin_command *pcmd,
|
2020-05-05 03:13:14 +02:00
|
|
|
struct plugin *plugin)
|
|
|
|
{
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_list_plugins(pcmd, plugin->plugins);
|
2020-05-05 03:13:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
struct command_result *plugin_cmd_all_complete(struct plugins *plugins,
|
2021-06-16 03:02:17 +02:00
|
|
|
struct plugin_command *pcmd)
|
2020-05-05 03:13:14 +02:00
|
|
|
{
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_list_plugins(pcmd, plugins);
|
2020-05-05 03:13:14 +02:00
|
|
|
}
|
|
|
|
|
2019-09-12 01:26:51 +02:00
|
|
|
/**
|
|
|
|
* Called when trying to start a plugin through RPC, it starts the plugin and
|
2020-05-05 03:15:52 +02:00
|
|
|
* will give a result 60 seconds later at the most (once init completes).
|
2019-09-12 01:26:51 +02:00
|
|
|
*/
|
|
|
|
static struct command_result *
|
2021-06-16 03:02:17 +02:00
|
|
|
plugin_dynamic_start(struct plugin_command *pcmd, const char *plugin_path,
|
2020-12-14 05:58:35 +01:00
|
|
|
const char *buffer, const jsmntok_t *params)
|
2019-09-12 01:26:51 +02:00
|
|
|
{
|
2021-06-16 03:02:17 +02:00
|
|
|
struct plugin *p = plugin_register(pcmd->cmd->ld->plugins, plugin_path, pcmd, false, buffer, params);
|
2020-05-05 03:14:32 +02:00
|
|
|
const char *err;
|
2019-09-12 01:26:51 +02:00
|
|
|
|
2020-05-05 03:13:56 +02:00
|
|
|
if (!p)
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_fail(pcmd->cmd, JSONRPC2_INVALID_PARAMS,
|
2022-06-15 07:47:57 +02:00
|
|
|
"%s: %s", plugin_path,
|
|
|
|
errno ? strerror(errno) : "already registered");
|
2019-09-12 01:26:51 +02:00
|
|
|
|
2020-05-05 03:13:56 +02:00
|
|
|
/* This will come back via plugin_cmd_killed or plugin_cmd_succeeded */
|
2020-05-05 03:14:32 +02:00
|
|
|
err = plugin_send_getmanifest(p);
|
|
|
|
if (err)
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_fail(pcmd->cmd, PLUGIN_ERROR,
|
2020-05-05 03:14:32 +02:00
|
|
|
"%s: %s",
|
|
|
|
plugin_path, err);
|
2020-05-05 03:13:56 +02:00
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_still_pending(pcmd->cmd);
|
2019-09-12 01:26:51 +02:00
|
|
|
}
|
|
|
|
|
2019-09-14 17:35:00 +02:00
|
|
|
/**
|
|
|
|
* Called when trying to start a plugin directory through RPC, it registers
|
|
|
|
* all contained plugins recursively and then starts them.
|
|
|
|
*/
|
|
|
|
static struct command_result *
|
2021-06-16 03:02:17 +02:00
|
|
|
plugin_dynamic_startdir(struct plugin_command *pcmd, const char *dir_path)
|
2019-09-14 17:35:00 +02:00
|
|
|
{
|
|
|
|
const char *err;
|
2020-05-05 03:13:56 +02:00
|
|
|
struct command_result *res;
|
2019-09-14 17:35:00 +02:00
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
err = add_plugin_dir(pcmd->cmd->ld->plugins, dir_path, false);
|
2019-09-14 17:35:00 +02:00
|
|
|
if (err)
|
2021-06-16 03:02:17 +02:00
|
|
|
return command_fail(pcmd->cmd, JSONRPC2_INVALID_PARAMS, "%s", err);
|
2019-09-14 17:35:00 +02:00
|
|
|
|
2020-05-05 03:13:56 +02:00
|
|
|
/* If none added, this calls plugin_cmd_all_complete immediately */
|
2021-06-16 03:02:17 +02:00
|
|
|
res = plugin_register_all_complete(pcmd->cmd->ld, pcmd);
|
2020-05-05 03:13:56 +02:00
|
|
|
if (res)
|
|
|
|
return res;
|
2019-09-14 17:35:00 +02:00
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
plugins_send_getmanifest(pcmd->cmd->ld->plugins);
|
|
|
|
return command_still_pending(pcmd->cmd);
|
2019-09-14 17:35:00 +02:00
|
|
|
}
|
|
|
|
|
2021-09-03 12:16:21 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-09-15 18:46:01 +02:00
|
|
|
static struct command_result *
|
|
|
|
plugin_dynamic_stop(struct command *cmd, const char *plugin_name)
|
|
|
|
{
|
|
|
|
struct plugin *p;
|
|
|
|
|
|
|
|
list_for_each(&cmd->ld->plugins->plugins, p, list) {
|
|
|
|
if (plugin_paths_match(p->cmd, plugin_name)) {
|
|
|
|
if (!p->dynamic)
|
|
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
|
|
"%s cannot be managed when "
|
|
|
|
"lightningd is up",
|
|
|
|
plugin_name);
|
2021-09-03 12:16:21 +02:00
|
|
|
|
|
|
|
/* 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);
|
2019-09-15 18:46:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
|
|
"Could not find plugin %s", plugin_name);
|
|
|
|
}
|
|
|
|
|
2019-09-15 18:51:41 +02:00
|
|
|
/**
|
|
|
|
* Look for additions in the default plugin directory.
|
|
|
|
*/
|
|
|
|
static struct command_result *
|
2021-06-16 03:02:17 +02:00
|
|
|
plugin_dynamic_rescan_plugins(struct plugin_command *pcmd)
|
2019-09-15 18:51:41 +02:00
|
|
|
{
|
2020-05-05 03:13:56 +02:00
|
|
|
struct command_result *res;
|
2019-09-15 18:51:41 +02:00
|
|
|
|
|
|
|
/* This will not fail on "already registered" error. */
|
2021-06-16 03:02:17 +02:00
|
|
|
plugins_add_default_dir(pcmd->cmd->ld->plugins);
|
2019-09-15 18:51:41 +02:00
|
|
|
|
2020-05-05 03:13:56 +02:00
|
|
|
/* If none added, this calls plugin_cmd_all_complete immediately */
|
2021-06-16 03:02:17 +02:00
|
|
|
res = plugin_register_all_complete(pcmd->cmd->ld, pcmd);
|
2020-05-05 03:13:56 +02:00
|
|
|
if (res)
|
|
|
|
return res;
|
2019-09-15 18:51:41 +02:00
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
plugins_send_getmanifest(pcmd->cmd->ld->plugins);
|
|
|
|
return command_still_pending(pcmd->cmd);
|
2019-09-15 18:51:41 +02:00
|
|
|
}
|
|
|
|
|
2019-06-29 13:47:48 +02:00
|
|
|
/**
|
|
|
|
* A plugin command which permits to control plugins without restarting
|
|
|
|
* lightningd. It takes a subcommand, and an optional subcommand parameter.
|
|
|
|
*/
|
|
|
|
static struct command_result *json_plugin_control(struct command *cmd,
|
|
|
|
const char *buffer,
|
|
|
|
const jsmntok_t *obj UNNEEDED,
|
|
|
|
const jsmntok_t *params)
|
|
|
|
{
|
2021-06-16 03:02:17 +02:00
|
|
|
struct plugin_command *pcmd;
|
2019-06-29 13:47:48 +02:00
|
|
|
const char *subcmd;
|
|
|
|
subcmd = param_subcommand(cmd, buffer, params,
|
2019-09-15 18:51:41 +02:00
|
|
|
"start", "stop", "startdir",
|
|
|
|
"rescan", "list", NULL);
|
2019-06-29 13:47:48 +02:00
|
|
|
if (!subcmd)
|
|
|
|
return command_param_failed();
|
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
pcmd = tal(cmd, struct plugin_command);
|
|
|
|
pcmd->cmd = cmd;
|
|
|
|
pcmd->subcmd = subcmd;
|
|
|
|
|
2019-06-29 13:47:48 +02:00
|
|
|
if (streq(subcmd, "stop")) {
|
|
|
|
const char *plugin_name;
|
|
|
|
|
|
|
|
if (!param(cmd, buffer, params,
|
|
|
|
p_req("subcommand", param_ignore, cmd),
|
|
|
|
p_req("plugin", param_string, &plugin_name),
|
|
|
|
NULL))
|
|
|
|
return command_param_failed();
|
|
|
|
|
2019-09-15 18:46:01 +02:00
|
|
|
return plugin_dynamic_stop(cmd, plugin_name);
|
2019-06-29 13:47:48 +02:00
|
|
|
} else if (streq(subcmd, "start")) {
|
|
|
|
const char *plugin_path;
|
2020-12-14 05:58:35 +01:00
|
|
|
jsmntok_t *mod_params;
|
2019-06-29 13:47:48 +02:00
|
|
|
|
|
|
|
if (!param(cmd, buffer, params,
|
|
|
|
p_req("subcommand", param_ignore, cmd),
|
|
|
|
p_req("plugin", param_string, &plugin_path),
|
2020-12-14 05:58:35 +01:00
|
|
|
p_opt_any(),
|
2019-06-29 13:47:48 +02:00
|
|
|
NULL))
|
|
|
|
return command_param_failed();
|
|
|
|
|
2020-12-14 05:58:35 +01:00
|
|
|
/* Manually parse any remaining options (only for objects,
|
|
|
|
* since plugin options must be explicitly named!). */
|
|
|
|
if (params->type == JSMN_ARRAY) {
|
|
|
|
if (params->size != 2)
|
|
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
|
|
"Extra parameters must be in object");
|
|
|
|
mod_params = NULL;
|
|
|
|
} else {
|
|
|
|
mod_params = json_tok_copy(cmd, params);
|
|
|
|
|
|
|
|
json_tok_remove(&mod_params, mod_params,
|
|
|
|
json_get_member(buffer, mod_params,
|
|
|
|
"subcommand") - 1, 1);
|
|
|
|
json_tok_remove(&mod_params, mod_params,
|
|
|
|
json_get_member(buffer, mod_params,
|
|
|
|
"plugin") - 1, 1);
|
|
|
|
}
|
2022-06-17 06:48:48 +02:00
|
|
|
if (access(plugin_path, X_OK) != 0)
|
|
|
|
plugin_path = path_join(cmd,
|
|
|
|
cmd->ld->plugins->default_dir, plugin_path);
|
2019-06-29 13:47:48 +02:00
|
|
|
if (access(plugin_path, X_OK) == 0)
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_start(pcmd, plugin_path,
|
2020-12-14 05:58:35 +01:00
|
|
|
buffer, mod_params);
|
2019-06-29 13:47:48 +02:00
|
|
|
else
|
|
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
|
|
"%s is not executable: %s",
|
|
|
|
plugin_path, strerror(errno));
|
|
|
|
} else if (streq(subcmd, "startdir")) {
|
|
|
|
const char *dir_path;
|
|
|
|
|
|
|
|
if (!param(cmd, buffer, params,
|
|
|
|
p_req("subcommand", param_ignore, cmd),
|
|
|
|
p_req("directory", param_string, &dir_path),
|
|
|
|
NULL))
|
|
|
|
return command_param_failed();
|
|
|
|
|
|
|
|
if (access(dir_path, F_OK) == 0)
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_startdir(pcmd, dir_path);
|
2019-06-29 13:47:48 +02:00
|
|
|
else
|
|
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
|
|
"Could not open %s", dir_path);
|
|
|
|
} else if (streq(subcmd, "rescan")) {
|
|
|
|
if (!param(cmd, buffer, params,
|
|
|
|
p_req("subcommand", param_ignore, cmd),
|
|
|
|
NULL))
|
|
|
|
return command_param_failed();
|
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_rescan_plugins(pcmd);
|
2019-06-29 13:47:48 +02:00
|
|
|
} else if (streq(subcmd, "list")) {
|
|
|
|
if (!param(cmd, buffer, params,
|
|
|
|
p_req("subcommand", param_ignore, cmd),
|
|
|
|
NULL))
|
|
|
|
return command_param_failed();
|
|
|
|
|
2021-06-16 03:02:17 +02:00
|
|
|
return plugin_dynamic_list_plugins(pcmd, cmd->ld->plugins);
|
2019-06-29 13:47:48 +02:00
|
|
|
}
|
|
|
|
|
2019-09-15 18:51:41 +02:00
|
|
|
/* subcmd must be one of the above: param_subcommand checked it! */
|
|
|
|
abort();
|
2019-06-29 13:47:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static const struct json_command plugin_control_command = {
|
|
|
|
"plugin",
|
|
|
|
"plugin",
|
|
|
|
json_plugin_control,
|
|
|
|
"Control plugins (start, stop, startdir, rescan, list)",
|
|
|
|
.verbose = "Usage :\n"
|
|
|
|
"plugin start /path/to/a/plugin\n"
|
2022-04-06 07:09:48 +02:00
|
|
|
" adds a new plugin to Core Lightning\n"
|
2019-06-29 13:47:48 +02:00
|
|
|
"plugin stop plugin_name\n"
|
|
|
|
" stops an already registered plugin\n"
|
|
|
|
"plugin startdir /path/to/a/plugin_dir/\n"
|
|
|
|
" adds a new plugin directory\n"
|
|
|
|
"plugin rescan\n"
|
|
|
|
" loads not-already-loaded plugins from the default plugins dir\n"
|
|
|
|
"plugin list\n"
|
|
|
|
" lists all active plugins\n"
|
|
|
|
"\n"
|
|
|
|
};
|
|
|
|
AUTODATA(json_command, &plugin_control_command);
|