#include #include #include #include #include /* A dummy structure used to give multiple arguments to callbacks. */ struct dynamic_plugin { struct plugin *plugin; struct command *cmd; }; /** * Returned by all subcommands on success. */ static struct command_result *plugin_dynamic_list_plugins(struct command *cmd) { struct json_stream *response; struct plugin *p; response = json_stream_success(cmd); json_array_start(response, "plugins"); list_for_each(&cmd->ld->plugins->plugins, p, list) { json_object_start(response, NULL); json_add_string(response, "name", p->cmd); json_add_bool(response, "active", p->plugin_state == CONFIGURED); json_object_end(response); } json_array_end(response); return command_success(cmd, response); } /** * Returned by all subcommands on error. */ static struct command_result * plugin_dynamic_error(struct dynamic_plugin *dp, const char *error) { if (dp->plugin) plugin_kill(dp->plugin, "%s", error); else log_info(dp->cmd->ld->log, "%s", error); return command_fail(dp->cmd, JSONRPC2_INVALID_PARAMS, "%s: %s", dp->plugin ? dp->plugin->cmd : "unknown plugin", error); } static void plugin_dynamic_timeout(struct dynamic_plugin *dp) { plugin_dynamic_error(dp, "Timed out while waiting for plugin response"); } static void plugin_dynamic_config_callback(const char *buffer, const jsmntok_t *toks, const jsmntok_t *idtok, struct dynamic_plugin *dp) { struct plugin *p; dp->plugin->plugin_state = CONFIGURED; /* Reset the timer only now so that we are either configured, or * killed. */ tal_free(dp->plugin->timeout_timer); list_for_each(&dp->plugin->plugins->plugins, p, list) { if (p->plugin_state != CONFIGURED) return; } /* No plugin unconfigured left, return the plugin list */ was_pending(plugin_dynamic_list_plugins(dp->cmd)); } /** * Send the init message to the plugin. We don't care about its response, * but it's considered the last part of the handshake : once it responds * it is considered configured. */ static void plugin_dynamic_config(struct dynamic_plugin *dp) { struct jsonrpc_request *req; req = jsonrpc_request_start(dp->plugin, "init", dp->plugin->log, plugin_dynamic_config_callback, dp); plugin_populate_init_request(dp->plugin, req); jsonrpc_request_end(req); plugin_request_send(dp->plugin, req); } static void plugin_dynamic_manifest_callback(const char *buffer, const jsmntok_t *toks, const jsmntok_t *idtok, struct dynamic_plugin *dp) { if (!plugin_parse_getmanifest_response(buffer, toks, idtok, dp->plugin)) return was_pending(plugin_dynamic_error(dp, "Gave a bad response to getmanifest")); if (!dp->plugin->dynamic) return was_pending(plugin_dynamic_error(dp, "Not a dynamic plugin")); /* We got the manifest, now send the init message */ plugin_dynamic_config(dp); } /** * This starts a plugin : spawns the process, connect its stdout and stdin, * then sends it a getmanifest request. */ static struct command_result *plugin_start(struct dynamic_plugin *dp) { int stdin, stdout; mode_t prev_mask; char **p_cmd; struct jsonrpc_request *req; struct plugin *p = dp->plugin; p->dynamic = true; p_cmd = tal_arrz(NULL, char *, 2); p_cmd[0] = p->cmd; /* In case the plugin create files, this is a better default. */ prev_mask = umask(dp->cmd->ld->initial_umask); p->pid = pipecmdarr(&stdin, &stdout, &pipecmd_preserve, p_cmd); umask(prev_mask); if (p->pid == -1) return plugin_dynamic_error(dp, "Error running command"); else log_debug(dp->cmd->ld->plugins->log, "started(%u) %s", p->pid, p->cmd); tal_free(p_cmd); p->buffer = tal_arr(p, char, 64); p->stop = false; /* Give the plugin 20 seconds to respond to `getmanifest`, so we don't hang * too long on the RPC caller. */ p->timeout_timer = new_reltimer(dp->cmd->ld->timers, dp, time_from_sec((10)), plugin_dynamic_timeout, dp); /* Create two connections, one read-only on top of the plugin's stdin, and one * write-only on its stdout. */ io_new_conn(p, stdout, plugin_stdout_conn_init, p); io_new_conn(p, stdin, plugin_stdin_conn_init, p); req = jsonrpc_request_start(p, "getmanifest", p->log, plugin_dynamic_manifest_callback, dp); jsonrpc_request_end(req); plugin_request_send(p, req); return command_still_pending(dp->cmd); } /** * Called when trying to start a plugin through RPC, it starts the plugin and * will give a result 20 seconds later at the most. */ static struct command_result * plugin_dynamic_start(struct command *cmd, const char *plugin_path) { struct dynamic_plugin *dp; dp = tal(cmd, struct dynamic_plugin); dp->cmd = cmd; dp->plugin = plugin_register(cmd->ld->plugins, plugin_path); if (!dp->plugin) return plugin_dynamic_error(dp, "Is already registered"); return plugin_start(dp); } /** * Called when trying to start a plugin directory through RPC, it registers * all contained plugins recursively and then starts them. */ static struct command_result * plugin_dynamic_startdir(struct command *cmd, const char *dir_path) { const char *err; struct plugin *p; /* If the directory is empty */ bool found; err = add_plugin_dir(cmd->ld->plugins, dir_path, false); if (err) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s", err); found = false; list_for_each(&cmd->ld->plugins->plugins, p, list) { if (p->plugin_state == UNCONFIGURED) { found = true; struct dynamic_plugin *dp = tal(cmd, struct dynamic_plugin); dp->plugin = p; dp->cmd = cmd; plugin_start(dp); } } if (!found) plugin_dynamic_list_plugins(cmd); return command_still_pending(cmd); } 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)) { if (!p->dynamic) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "%s cannot be managed when " "lightningd is up", plugin_name); plugin_hook_unregister_all(p); plugin_kill(p, "%s stopped by lightningd via RPC", plugin_name); tal_free(p); response = json_stream_success(cmd); if (deprecated_apis) json_add_string(response, "", take(tal_fmt(NULL, "Successfully stopped %s.", plugin_name))); json_add_string(response, "result", take(tal_fmt(NULL, "Successfully stopped %s.", plugin_name))); return command_success(cmd, response); } } return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Could not find plugin %s", plugin_name); } /** * Look for additions in the default plugin directory. */ static struct command_result * plugin_dynamic_rescan_plugins(struct command *cmd) { bool found; struct plugin *p; /* This will not fail on "already registered" error. */ plugins_add_default_dir(cmd->ld->plugins); found = false; list_for_each(&cmd->ld->plugins->plugins, p, list) { if (p->plugin_state == UNCONFIGURED) { struct dynamic_plugin *dp = tal(cmd, struct dynamic_plugin); dp->plugin = p; dp->cmd = cmd; plugin_start(dp); found = true; } } if (!found) return plugin_dynamic_list_plugins(cmd); return command_still_pending(cmd); } /** * 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) { const char *subcmd; subcmd = param_subcommand(cmd, buffer, params, "start", "stop", "startdir", "rescan", "list", NULL); if (!subcmd) return command_param_failed(); 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(); return plugin_dynamic_stop(cmd, plugin_name); } else if (streq(subcmd, "start")) { const char *plugin_path; if (!param(cmd, buffer, params, p_req("subcommand", param_ignore, cmd), p_req("plugin", param_string, &plugin_path), NULL)) return command_param_failed(); if (access(plugin_path, X_OK) == 0) return plugin_dynamic_start(cmd, plugin_path); 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) return plugin_dynamic_startdir(cmd, dir_path); 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(); return plugin_dynamic_rescan_plugins(cmd); } else if (streq(subcmd, "list")) { if (!param(cmd, buffer, params, p_req("subcommand", param_ignore, cmd), NULL)) return command_param_failed(); return plugin_dynamic_list_plugins(cmd); } /* subcmd must be one of the above: param_subcommand checked it! */ abort(); } 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" " adds a new plugin to c-lightning\n" "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);