From 41290a436fcbc2ff29f28306a436a1a6c2cc9841 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 12 Oct 2020 16:03:50 +1030 Subject: [PATCH] lightning-cli: print notifications (with '# ' prefix) if received. This can be suppressed with -N. Note that we wull get an error with older lightningd, but we ignore it anyway. Signed-off-by: Rusty Russell Changelog-Added: cli: print notifications and progress bars if commands provide them. --- cli/Makefile | 1 + cli/lightning-cli.c | 174 +++++++++++++++++++++++++++++++++++-- cli/test/run-human-mode.c | 11 ++- cli/test/run-large-input.c | 11 ++- cli/test/run-remove-hint.c | 11 ++- doc/lightning-cli.1 | 8 +- doc/lightning-cli.1.md | 6 ++ 7 files changed, 207 insertions(+), 15 deletions(-) diff --git a/cli/Makefile b/cli/Makefile index a032ee79b..635a8d4e9 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -10,6 +10,7 @@ LIGHTNING_CLI_COMMON_OBJS := \ common/configdir.o \ common/json.o \ common/json_stream.o \ + common/status_levels.o \ common/utils.o \ common/version.o diff --git a/cli/lightning-cli.c b/cli/lightning-cli.c index 628684f37..16852e749 100644 --- a/cli/lightning-cli.c +++ b/cli/lightning-cli.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -470,6 +471,135 @@ static enum format choose_format(const char *resp, return format; } +static bool handle_notify(const char *buf, jsmntok_t *toks, + enum log_level notification_level, + bool *last_was_progress) +{ + const jsmntok_t *id, *method, *params; + + if (toks->type != JSMN_OBJECT) + return false; + + id = json_get_member(buf, toks, "id"); + if (id) + return false; + + method = json_get_member(buf, toks, "method"); + if (!method) + return false; + + params = json_get_member(buf, toks, "params"); + if (!params) + return false; + + /* Print nothing if --notifications=none */ + if (notification_level == LOG_LEVEL_MAX + 1) + return true; + + /* We try to be robust if malformed */ + if (json_tok_streq(buf, method, "message")) { + const jsmntok_t *message, *leveltok; + enum log_level level; + + leveltok = json_get_member(buf, params, "level"); + if (!leveltok + || !log_level_parse(buf + leveltok->start, + leveltok->end - leveltok->start, + &level) + || level < notification_level) + return true; + + if (*last_was_progress) + printf("\n"); + *last_was_progress = false; + message = json_get_member(buf, params, "message"); + if (!message) + return true; + + printf("# %.*s\n", + message->end - message->start, + buf + message->start); + } else if (json_tok_streq(buf, method, "progress")) { + const jsmntok_t *num, *total, *stage; + u32 n, tot; + char bar[60 + 1]; + char totstr[STR_MAX_CHARS(u32)]; + + num = json_get_member(buf, params, "num"); + total = json_get_member(buf, params, "total"); + if (!num || !total) + return true; + if (!json_to_u32(buf, num, &n) + || !json_to_u32(buf, total, &tot)) + return true; + + /* Ph3ar my gui skillz! */ + printf("\r# "); + stage = json_get_member(buf, params, "stage"); + if (stage) { + u32 stage_num, stage_total; + json_to_u32(buf, json_get_member(buf, stage, "num"), + &stage_num); + json_to_u32(buf, json_get_member(buf, stage, "total"), + &stage_total); + snprintf(totstr, sizeof(totstr), "%u", stage_total); + printf("Stage %*u/%s ", + (int)strlen(totstr), stage_num+1, totstr); + } + snprintf(totstr, sizeof(totstr), "%u", tot); + printf("%*u/%s ", (int)strlen(totstr), n+1, totstr); + memset(bar, ' ', sizeof(bar)-1); + memset(bar, '=', (double)strlen(bar) / (tot-1) * n); + bar[sizeof(bar)-1] = '\0'; + printf("|%s|", bar); + /* Leave bar there if it's finished. */ + if (n+1 == tot) { + printf("\n"); + *last_was_progress = false; + } else { + fflush(stdout); + *last_was_progress = true; + } + } + + return true; +} + +static void enable_notifications(int fd) +{ + const char *enable = "{ \"jsonrpc\": \"2.0\", \"method\": \"notifications\", \"id\": 0, \"params\": { \"enable\": true } }"; + char rbuf[100]; + + if (!write_all(fd, enable, strlen(enable))) + err(ERROR_TALKING_TO_LIGHTNINGD, "Writing enable command"); + + /* We get a very simple response, ending in \n\n. */ + memset(rbuf, 0, sizeof(rbuf)); + while (!strends(rbuf, "\n\n")) { + size_t len = strlen(rbuf); + if (read(fd, rbuf + len, sizeof(rbuf) - len) < 0) + err(ERROR_TALKING_TO_LIGHTNINGD, + "Reading enable response"); + } +} + +static char *opt_set_level(const char *arg, enum log_level *level) +{ + if (streq(arg, "none")) + *level = LOG_LEVEL_MAX + 1; + else if (!log_level_parse(arg, strlen(arg), level)) + return "Invalid level"; + return NULL; +} + +static void opt_show_level(char buf[OPT_SHOW_LEN], const enum log_level *level) +{ + if (*level == LOG_LEVEL_MAX + 1) + strncpy(buf, "none", OPT_SHOW_LEN-1); + else + strncpy(buf, log_level_name(*level), OPT_SHOW_LEN-1); +} + int main(int argc, char *argv[]) { setup_locale(); @@ -487,6 +617,8 @@ int main(int argc, char *argv[]) int parserr; enum format format = DEFAULT_FORMAT; enum input input = DEFAULT_INPUT; + enum log_level notification_level = LOG_INFORM; + bool last_was_progress = false; char *command = NULL; err_set_progname(argv[0]); @@ -514,6 +646,9 @@ int main(int argc, char *argv[]) "Use format key=value for "); opt_register_noarg("-o|--order", opt_set_ordered, &input, "Use params in order for "); + opt_register_arg("-N|--notifications", opt_set_level, + opt_show_level, ¬ification_level, + "Set notification level, or none"); opt_register_version(); @@ -567,6 +702,10 @@ int main(int argc, char *argv[]) "Connecting to '%s'", rpc_filename); idstr = tal_fmt(ctx, "lightning-cli-%i", getpid()); + + if (notification_level <= LOG_LEVEL_MAX) + enable_notifications(fd); + cmd = tal_fmt(ctx, "{ \"jsonrpc\" : \"2.0\", \"method\" : \"%s\", \"id\" : \"%s\", \"params\" :", json_escape(ctx, method)->s, idstr); @@ -607,6 +746,7 @@ int main(int argc, char *argv[]) /* Start with 1000 characters, 100 tokens. */ resp = tal_arr(ctx, char, 1000); toks = tal_arr(ctx, jsmntok_t, 100); + toks[0].type = JSMN_UNDEFINED; off = 0; parserr = 0; @@ -632,19 +772,34 @@ int main(int argc, char *argv[]) case JSMN_ERROR_INVAL: errx(ERROR_TALKING_TO_LIGHTNINGD, "Malformed response '%s'", resp); - case JSMN_ERROR_NOMEM: { + case JSMN_ERROR_NOMEM: /* Need more tokens, double it */ if (!tal_resize(&toks, tal_count(toks) * 2)) oom_dump(fd, resp, off); break; - } case JSMN_ERROR_PART: - /* Need more data: make room if necessary */ - if (off == tal_bytelen(resp) - 1) { - if (!tal_resize(&resp, tal_count(resp) * 2)) - oom_dump(fd, resp, off); + /* We may actually have a complete token! */ + if (toks[0].type == JSMN_UNDEFINED || toks[0].end == -1) { + /* Need more data: make room if necessary */ + if (off == tal_bytelen(resp) - 1) { + if (!tal_resize(&resp, tal_count(resp) * 2)) + oom_dump(fd, resp, off); + } + break; + } + /* Otherwise fall through... */ + default: + if (handle_notify(resp, toks, notification_level, + &last_was_progress)) { + /* +2 for \n\n */ + size_t len = toks[0].end - toks[0].start + 2; + memmove(resp, resp + len, off - len); + off -= len; + jsmn_init(&parser); + toks[0].type = JSMN_UNDEFINED; + /* Don't force another read! */ + parserr = JSMN_ERROR_NOMEM; } - break; } } @@ -652,7 +807,10 @@ int main(int argc, char *argv[]) errx(ERROR_TALKING_TO_LIGHTNINGD, "Non-object response '%s'", resp); - /* This can rellocate toks, so call before getting pointers to tokens */ + if (last_was_progress) + printf("\n"); + + /* This can reallocate toks, so call before getting pointers to tokens */ format = choose_format(resp, &toks, method, command, format); result = json_get_member(resp, toks, "result"); error = json_get_member(resp, toks, "error"); diff --git a/cli/test/run-human-mode.c b/cli/test/run-human-mode.c index 1f702aa01..a30828246 100644 --- a/cli/test/run-human-mode.c +++ b/cli/test/run-human-mode.c @@ -90,6 +90,13 @@ void json_add_member(struct json_stream *js UNNEEDED, char *json_member_direct(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, size_t extra UNNEEDED) { fprintf(stderr, "json_member_direct called!\n"); abort(); } +/* Generated stub for log_level_name */ +const char *log_level_name(enum log_level level UNNEEDED) +{ fprintf(stderr, "log_level_name called!\n"); abort(); } +/* Generated stub for log_level_parse */ +bool log_level_parse(const char *levelstr UNNEEDED, size_t len UNNEEDED, + enum log_level *level UNNEEDED) +{ fprintf(stderr, "log_level_parse called!\n"); abort(); } /* Generated stub for towire_amount_msat */ void towire_amount_msat(u8 **pptr UNNEEDED, const struct amount_msat msat UNNEEDED) { fprintf(stderr, "towire_amount_msat called!\n"); abort(); } @@ -162,11 +169,11 @@ int main(int argc UNUSED, char *argv[]) { setup_locale(); - char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "-H", "listconfigs", NULL }; + char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "-H", "listconfigs", "-N", "none", NULL }; response_off = 0; max_read_return = -1; - assert(test_main(4, fake_argv) == 0); + assert(test_main(6, fake_argv) == 0); assert(!taken_any()); take_cleanup(); return 0; diff --git a/cli/test/run-large-input.c b/cli/test/run-large-input.c index ddb09a2f8..4e02cd487 100644 --- a/cli/test/run-large-input.c +++ b/cli/test/run-large-input.c @@ -90,6 +90,13 @@ void json_add_member(struct json_stream *js UNNEEDED, char *json_member_direct(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, size_t extra UNNEEDED) { fprintf(stderr, "json_member_direct called!\n"); abort(); } +/* Generated stub for log_level_name */ +const char *log_level_name(enum log_level level UNNEEDED) +{ fprintf(stderr, "log_level_name called!\n"); abort(); } +/* Generated stub for log_level_parse */ +bool log_level_parse(const char *levelstr UNNEEDED, size_t len UNNEEDED, + enum log_level *level UNNEEDED) +{ fprintf(stderr, "log_level_parse called!\n"); abort(); } /* Generated stub for towire_amount_msat */ void towire_amount_msat(u8 **pptr UNNEEDED, const struct amount_msat msat UNNEEDED) { fprintf(stderr, "towire_amount_msat called!\n"); abort(); } @@ -171,7 +178,7 @@ int main(int argc UNUSED, char *argv[]) { setup_locale(); - char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "test", NULL }; + char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "test", "-N", "none", NULL }; /* sizeof() is an overestimate, but we don't care. */ response = tal_arr(NULL, char, @@ -196,7 +203,7 @@ int main(int argc UNUSED, char *argv[]) response_off = 0; max_read_return = -1; - assert(test_main(3, fake_argv) == 0); + assert(test_main(5, fake_argv) == 0); tal_free(response); assert(!taken_any()); take_cleanup(); diff --git a/cli/test/run-remove-hint.c b/cli/test/run-remove-hint.c index fbfaef8e2..42e00ab16 100644 --- a/cli/test/run-remove-hint.c +++ b/cli/test/run-remove-hint.c @@ -93,6 +93,13 @@ void json_add_member(struct json_stream *js UNNEEDED, char *json_member_direct(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, size_t extra UNNEEDED) { fprintf(stderr, "json_member_direct called!\n"); abort(); } +/* Generated stub for log_level_name */ +const char *log_level_name(enum log_level level UNNEEDED) +{ fprintf(stderr, "log_level_name called!\n"); abort(); } +/* Generated stub for log_level_parse */ +bool log_level_parse(const char *levelstr UNNEEDED, size_t len UNNEEDED, + enum log_level *level UNNEEDED) +{ fprintf(stderr, "log_level_parse called!\n"); abort(); } /* Generated stub for towire_amount_msat */ void towire_amount_msat(u8 **pptr UNNEEDED, const struct amount_msat msat UNNEEDED) { fprintf(stderr, "towire_amount_msat called!\n"); abort(); } @@ -168,10 +175,10 @@ int main(int argc UNUSED, char *argv[]) { setup_locale(); - char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "test", NULL }; + char *fake_argv[] = { argv[0], "--lightning-dir=/tmp/", "test", "-N", "none", NULL }; output = tal_strdup(NULL, ""); - assert(test_main(3, fake_argv) == 0); + assert(test_main(5, fake_argv) == 0); assert(streq(output, "channels=\n" "\n" diff --git a/doc/lightning-cli.1 b/doc/lightning-cli.1 index 439add145..34a1dd49f 100644 --- a/doc/lightning-cli.1 +++ b/doc/lightning-cli.1 @@ -61,6 +61,12 @@ This is useful for simple scripts which want to find a specific output field without parsing JSON\. + \fB--notifications\fR/\fB-N\fR=\fILEVEL\fR +If \fILEVEL\fR is 'none', then never print out notifications\. Otherwise, +print out notifications of \fILEVEL\fR or above (one of \fBio\fR, \fBdebug\fR, +\fBinfo\fR (the default), \fBunusual\fR or \fBbroken\fR: they are prefixed with \fB#\fR\. + + \fB--help\fR/\fB-h\fR Pretty-print summary of options to standard output and exit\. The format can be changed using -F, -R, -J, -H etc\. @@ -118,4 +124,4 @@ Main web site: \fIhttps://github.com/ElementsProject/lightning\fR Note: the modules in the ccan/ directory have their own licenses, but the rest of the code is covered by the BSD-style MIT license\. -\" SHA256STAMP:0269a00171f6ff2bbcb772b9367e05787345faf6457e75141978999fc0103d4a +\" SHA256STAMP:b626a2499bd231acc33bf1c279c62de2a7ad2046c6c63965b97f74ec4e861365 diff --git a/doc/lightning-cli.1.md b/doc/lightning-cli.1.md index 25f3aa150..80c5ba25b 100644 --- a/doc/lightning-cli.1.md +++ b/doc/lightning-cli.1.md @@ -54,6 +54,12 @@ Return JSON result in flattened one-per-line output, e.g. `{ "help": This is useful for simple scripts which want to find a specific output field without parsing JSON. + **--notifications**/**-N**=*LEVEL* +If *LEVEL* is 'none', then never print out notifications. Otherwise, +print out notifications of *LEVEL* or above (one of `io`, `debug`, +`info` (the default), `unusual` or `broken`: they are prefixed with `# +`. + **--help**/**-h** Pretty-print summary of options to standard output and exit. The format can be changed using -F, -R, -J, -H etc.