Browse Source

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 <rusty@rustcorp.com.au>
Changelog-Added: cli: print notifications and progress bars if commands provide them.
travis-experimental
Rusty Russell 4 years ago
parent
commit
41290a436f
  1. 1
      cli/Makefile
  2. 174
      cli/lightning-cli.c
  3. 11
      cli/test/run-human-mode.c
  4. 11
      cli/test/run-large-input.c
  5. 11
      cli/test/run-remove-hint.c
  6. 8
      doc/lightning-cli.1
  7. 6
      doc/lightning-cli.1.md

1
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

174
cli/lightning-cli.c

@ -15,6 +15,7 @@
#include <common/json.h>
#include <common/json_command.h>
#include <common/memleak.h>
#include <common/status_levels.h>
#include <common/utils.h>
#include <common/version.h>
#include <libgen.h>
@ -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 <params>");
opt_register_noarg("-o|--order", opt_set_ordered, &input,
"Use params in order for <params>");
opt_register_arg("-N|--notifications", opt_set_level,
opt_show_level, &notification_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");

11
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;

11
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();

11
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"

8
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

6
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.

Loading…
Cancel
Save