From 9ccb51c93f95a5d12c49f38d41a62089bb9f9aff Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 22 Jan 2016 06:41:48 +1030 Subject: [PATCH] daemon: UNIX domain socket for JSON-based control. Also taken from pettycoin. Signed-off-by: Rusty Russell --- daemon/Makefile | 2 + daemon/jsonrpc.c | 456 ++++++++++++++++++++++++++++++++++++++++++++ daemon/jsonrpc.h | 47 +++++ daemon/lightningd.c | 18 ++ 4 files changed, 523 insertions(+) create mode 100644 daemon/jsonrpc.c create mode 100644 daemon/jsonrpc.h diff --git a/daemon/Makefile b/daemon/Makefile index 0b7f67edd..0f487585c 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -9,6 +9,7 @@ daemon-all: daemon/lightningd DAEMON_SRC := \ daemon/configdir.c \ daemon/json.c \ + daemon/jsonrpc.c \ daemon/lightningd.c \ daemon/log.c \ daemon/pseudorand.c \ @@ -21,6 +22,7 @@ DAEMON_JSMN_HEADERS := daemon/jsmn/jsmn.h DAEMON_HEADERS := \ daemon/configdir.h \ daemon/json.h \ + daemon/jsonrpc.h \ daemon/lightningd.h \ daemon/log.h \ daemon/pseudorand.h \ diff --git a/daemon/jsonrpc.c b/daemon/jsonrpc.c new file mode 100644 index 000000000..b07fcb590 --- /dev/null +++ b/daemon/jsonrpc.c @@ -0,0 +1,456 @@ +/* Code for JSON_RPC API */ +/* eg: { "method" : "dev-echo", "params" : [ "hello", "Arabella!" ], "id" : "1" } */ +#include "json.h" +#include "jsonrpc.h" +#include "lightningd.h" +#include "log.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct json_output { + struct list_node list; + const char *json; +}; + +static void finish_jcon(struct io_conn *conn, struct json_connection *jcon) +{ + log_info(jcon->log, "Closing (%s)", strerror(errno)); +} + +static char *json_help(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *response); + +static const struct json_command help_command = { + "help", + json_help, + "describe commands", + "[] if specified gives details about a single command." +}; + +static char *json_echo(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *response) +{ + json_object_start(response, NULL); + json_add_num(response, "num", params->size); + json_add_literal(response, "echo", + json_tok_contents(jcon->buffer, params), + json_tok_len(params)); + json_object_end(response); + return NULL; +} + +static const struct json_command echo_command = { + "dev-echo", + json_echo, + "echo parameters", + "Simple echo test for developers" +}; + +static char *json_stop(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *response) +{ + jcon->stop = true; + json_add_string(response, NULL, "Shutting down"); + return NULL; +} + +static const struct json_command stop_command = { + "stop", + json_stop, + "Shutdown the lightningd process", + "What part of shutdown wasn't clear?" +}; + +struct log_info { + enum log_level level; + struct json_result *response; + unsigned int num_skipped; +}; + +static void add_skipped(struct log_info *info) +{ + if (info->num_skipped) { + json_array_start(info->response, NULL); + json_add_string(info->response, "type", "SKIPPED"); + json_add_num(info->response, "num_skipped", info->num_skipped); + json_array_end(info->response); + info->num_skipped = 0; + } +} + +static void json_add_time(struct json_result *result, const char *fieldname, + struct timespec ts) +{ + char timebuf[100]; + + sprintf(timebuf, "%lu.%09u", + (unsigned long)ts.tv_sec, + (unsigned)ts.tv_nsec); + json_add_string(result, fieldname, timebuf); +} + +static void log_to_json(unsigned int skipped, + struct timerel diff, + enum log_level level, + const char *prefix, + const char *log, + struct log_info *info) +{ + info->num_skipped += skipped; + + if (level < info->level) { + info->num_skipped++; + return; + } + + add_skipped(info); + + json_array_start(info->response, NULL); + json_add_string(info->response, "type", + level == LOG_BROKEN ? "BROKEN" + : level == LOG_UNUSUAL ? "UNUSUAL" + : level == LOG_INFORM ? "INFO" + : level == LOG_DBG ? "DEBUG" + : level == LOG_IO ? "IO" + : "UNKNOWN"); + json_add_time(info->response, "time", diff.ts); + json_add_string(info->response, "source", prefix); + if (level == LOG_IO) { + if (log[0]) + json_add_string(info->response, "direction", "IN"); + else + json_add_string(info->response, "direction", "OUT"); + + json_add_hex(info->response, "data", log+1, tal_count(log)-1); + } else + json_add_string(info->response, "log", log); + + json_array_end(info->response); +} + +static char *json_getlog(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *response) +{ + struct log_info info; + struct log_record *lr = jcon->state->log_record; + jsmntok_t *level; + + json_get_params(jcon->buffer, params, "level", &level, NULL); + + info.response = response; + info.num_skipped = 0; + + if (!level) + info.level = LOG_INFORM; + else if (json_tok_streq(jcon->buffer, level, "io")) + info.level = LOG_IO; + else if (json_tok_streq(jcon->buffer, level, "debug")) + info.level = LOG_DBG; + else if (json_tok_streq(jcon->buffer, level, "info")) + info.level = LOG_INFORM; + else if (json_tok_streq(jcon->buffer, level, "unusual")) + info.level = LOG_UNUSUAL; + else + return "Invalid level param"; + + json_object_start(response, NULL); + json_add_time(response, "creation_time", log_init_time(lr)->ts); + json_add_num(response, "bytes_used", (unsigned int)log_used(lr)); + json_add_num(response, "bytes_max", (unsigned int)log_max_mem(lr)); + json_object_start(response, "log"); + log_each_line(lr, log_to_json, &info); + json_object_end(response); + json_object_end(response); + return NULL; +} + +static const struct json_command getlog_command = { + "getlog", + json_getlog, + "Get logs, with optional level: [io|debug|info|unusual]", + "Returns log array" +}; + +static const struct json_command *cmdlist[] = { + &help_command, + &stop_command, + &getlog_command, + /* Developer/debugging options. */ + &echo_command, +}; + +static char *json_help(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *response) +{ + unsigned int i; + + json_array_start(response, NULL); + for (i = 0; i < ARRAY_SIZE(cmdlist); i++) { + json_add_object(response, + "command", JSMN_STRING, + cmdlist[i]->name, + "description", JSMN_STRING, + cmdlist[i]->description, + NULL); + } + json_array_end(response); + return NULL; +} + +static const struct json_command *find_cmd(const char *buffer, + const jsmntok_t *tok) +{ + unsigned int i; + + /* cmdlist[i]->name can be NULL in test code. */ + for (i = 0; i < ARRAY_SIZE(cmdlist); i++) + if (cmdlist[i]->name + && json_tok_streq(buffer, tok, cmdlist[i]->name)) + return cmdlist[i]; + return NULL; +} + +/* Returns NULL if it's a fatal error. */ +static char *parse_request(struct json_connection *jcon, const jsmntok_t tok[]) +{ + const jsmntok_t *method, *id, *params; + const struct json_command *cmd; + char *error; + struct json_result *result; + + if (tok[0].type != JSMN_OBJECT) { + log_unusual(jcon->log, "Expected {} for json command"); + return NULL; + } + + method = json_get_member(jcon->buffer, tok, "method"); + params = json_get_member(jcon->buffer, tok, "params"); + id = json_get_member(jcon->buffer, tok, "id"); + + if (!id || !method || !params) { + log_unusual(jcon->log, "json: No %s", + !id ? "id" : (!method ? "method" : "params")); + return NULL; + } + + if (id->type != JSMN_STRING && id->type != JSMN_PRIMITIVE) { + log_unusual(jcon->log, "Expected string/primitive for id"); + return NULL; + } + + if (method->type != JSMN_STRING) { + log_unusual(jcon->log, "Expected string for method"); + return NULL; + } + + cmd = find_cmd(jcon->buffer, method); + if (!cmd) { + return tal_fmt(jcon, + "{ \"result\" : null," + " \"error\" : \"Unknown command '%.*s'\"," + " \"id\" : %.*s }\n", + (int)(method->end - method->start), + jcon->buffer + method->start, + json_tok_len(id), + json_tok_contents(jcon->buffer, id)); + } + + if (params->type != JSMN_ARRAY && params->type != JSMN_OBJECT) { + log_unusual(jcon->log, "Expected array or object for params"); + return NULL; + } + + result = new_json_result(jcon); + error = cmd->dispatch(jcon, params, result); + if (error) { + char *quote; + + /* Remove " */ + while ((quote = strchr(error, '"')) != NULL) + *quote = '\''; + + return tal_fmt(jcon, + "{ \"result\" : null," + " \"error\" : \"%s\"," + " \"id\" : %.*s }\n", + error, + json_tok_len(id), + json_tok_contents(jcon->buffer, id)); + } + return tal_fmt(jcon, + "{ \"result\" : %s," + " \"error\" : null," + " \"id\" : %.*s }\n", + json_result_string(result), + json_tok_len(id), + json_tok_contents(jcon->buffer, id)); +} + +static struct io_plan *write_json(struct io_conn *conn, + struct json_connection *jcon) +{ + struct json_output *out; + + out = list_pop(&jcon->output, struct json_output, list); + if (!out) { + if (jcon->stop) { + log_unusual(jcon->log, "JSON-RPC shutdown"); + /* Return us to toplevel lightningd.c */ + io_break(jcon->state); + return io_close(conn); + } + + /* Reader can go again now. */ + io_wake(jcon); + return io_out_wait(conn, jcon, write_json, jcon); + } + + jcon->outbuf = tal_steal(jcon, out->json); + tal_free(out); + + log_io(jcon->log, false, jcon->outbuf, strlen(jcon->outbuf)); + return io_write(conn, + jcon->outbuf, strlen(jcon->outbuf), write_json, jcon); +} + +static struct io_plan *read_json(struct io_conn *conn, + struct json_connection *jcon) +{ + jsmntok_t *toks; + bool valid; + struct json_output *out; + + log_io(jcon->log, true, jcon->buffer + jcon->used, jcon->len_read); + + /* Resize larger if we're full. */ + jcon->used += jcon->len_read; + if (jcon->used == tal_count(jcon->buffer)) + tal_resize(&jcon->buffer, jcon->used * 2); + +again: + toks = json_parse_input(jcon->buffer, jcon->used, &valid); + if (!toks) { + if (!valid) { + log_unusual(jcon->state->base_log, + "Invalid token in json input: '%.*s'", + (int)jcon->used, jcon->buffer); + return io_close(conn); + } + /* We need more. */ + goto read_more; + } + + /* Empty buffer? (eg. just whitespace). */ + if (tal_count(toks) == 1) { + jcon->used = 0; + goto read_more; + } + + out = tal(jcon, struct json_output); + out->json = parse_request(jcon, toks); + if (!out->json) + return io_close(conn); + + /* Remove first {}. */ + memmove(jcon->buffer, jcon->buffer + toks[0].end, + tal_count(jcon->buffer) - toks[0].end); + jcon->used -= toks[0].end; + tal_free(toks); + + /* Queue for writing, and wake writer. */ + list_add_tail(&jcon->output, &out->list); + io_wake(jcon); + + /* See if we can parse the rest. */ + goto again; + +read_more: + tal_free(toks); + return io_read_partial(conn, jcon->buffer + jcon->used, + tal_count(jcon->buffer) - jcon->used, + &jcon->len_read, read_json, jcon); +} + +static struct io_plan *jcon_connected(struct io_conn *conn, + struct lightningd_state *state) +{ + struct json_connection *jcon; + + jcon = tal(state, struct json_connection); + jcon->state = state; + jcon->used = 0; + jcon->len_read = 64; + jcon->buffer = tal_arr(jcon, char, jcon->len_read); + jcon->stop = false; + jcon->log = new_log(jcon, state->log_record, "%sjcon fd %i:", + log_prefix(state->base_log), io_conn_fd(conn)); + list_head_init(&jcon->output); + + io_set_finish(conn, finish_jcon, jcon); + + return io_duplex(conn, + io_read_partial(conn, jcon->buffer, + tal_count(jcon->buffer), + &jcon->len_read, read_json, jcon), + write_json(conn, jcon)); +} + +static struct io_plan *incoming_jcon_connected(struct io_conn *conn, + struct lightningd_state *state) +{ + log_info(state->base_log, "Connected json input"); + return jcon_connected(conn, state); +} + +void setup_jsonrpc(struct lightningd_state *state, const char *rpc_filename) +{ + struct sockaddr_un addr; + int fd, old_umask; + + if (streq(rpc_filename, "")) + return; + + if (streq(rpc_filename, "/dev/tty")) { + fd = open(rpc_filename, O_RDWR); + if (fd == -1) + err(1, "Opening %s", rpc_filename); + io_new_conn(state, fd, jcon_connected, state); + return; + } + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (strlen(rpc_filename) + 1 > sizeof(addr.sun_path)) + errx(1, "rpc filename '%s' too long", rpc_filename); + strcpy(addr.sun_path, rpc_filename); + addr.sun_family = AF_UNIX; + + /* Of course, this is racy! */ + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) + errx(1, "rpc filename '%s' in use", rpc_filename); + unlink(rpc_filename); + + /* This file is only rw by us! */ + old_umask = umask(0177); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr))) + err(1, "Binding rpc socket to '%s'", rpc_filename); + umask(old_umask); + + if (listen(fd, 1) != 0) + err(1, "Listening on '%s'", rpc_filename); + + io_new_listener(state, fd, incoming_jcon_connected, state); +} diff --git a/daemon/jsonrpc.h b/daemon/jsonrpc.h new file mode 100644 index 000000000..2f3f30ce2 --- /dev/null +++ b/daemon/jsonrpc.h @@ -0,0 +1,47 @@ +#ifndef LIGHTNING_DAEMON_JSONRPC_H +#define LIGHTNING_DAEMON_JSONRPC_H +#include "config.h" +#include "json.h" +#include + +struct json_connection { + /* The global state */ + struct lightningd_state *state; + + /* Logging for this json connection. */ + struct log *log; + + /* The buffer (required to interpret tokens). */ + char *buffer; + + /* Internal state: */ + /* How much is already filled. */ + size_t used; + /* How much has just been filled. */ + size_t len_read; + + /* We've been told to stop. */ + bool stop; + + struct list_head output; + const char *outbuf; +}; + +struct json_command { + const char *name; + char *(*dispatch)(struct json_connection *jcon, + const jsmntok_t *params, + struct json_result *result); + const char *description; + const char *help; +}; + +/* Add notification about something. */ +void json_notify(struct json_connection *jcon, const char *result); + +/* For initialization */ +void setup_jsonrpc(struct lightningd_state *state, const char *rpc_filename); + +/* Commands (from other files) */ + +#endif /* LIGHTNING_DAEMON_JSONRPC_H */ diff --git a/daemon/lightningd.c b/daemon/lightningd.c index 3cc1d34ee..876aaabac 100644 --- a/daemon/lightningd.c +++ b/daemon/lightningd.c @@ -1,10 +1,15 @@ #include "configdir.h" +#include "jsonrpc.h" #include "lightningd.h" #include "log.h" +#include "timeout.h" +#include #include +#include #include #include #include +#include #include #include #include @@ -46,6 +51,7 @@ static void tal_freefn(void *ptr) int main(int argc, char *argv[]) { struct lightningd_state *state = lightningd_state(); + struct timer *expired; err_set_progname(argv[0]); opt_set_alloc(opt_allocfn, tal_reallocfn, tal_freefn); @@ -85,7 +91,19 @@ int main(int argc, char *argv[]) if (argc != 1) errx(1, "no arguments accepted"); + /* Create RPC socket (if any) */ + setup_jsonrpc(state, state->rpc_filename); + log_info(state->base_log, "Hello world!"); + + /* If io_loop returns NULL, either a timer expired, or all fds closed */ + while (!io_loop(&state->timers, &expired) && expired) { + struct timeout *to; + + to = container_of(expired, struct timeout, timer); + to->cb(to->arg); + } + tal_free(state); opt_free_table(); return 0;