cli: add -c/--commando support.

It's easier to type:

```
lightning-cli --commando=03ce2d830369fc903ffec52ca1d7aba095c3cf5d17175b6c9a3ff058f6aece37bc:V08OylkJ2ZZPClAXbTaxrXJ9YpKnmucJxcQI-wvIGiE9MA== invoice any "Invoice Label" "Invoice Description"
lightning-cli --commando=03ce2d830369fc903ffec52ca1d7aba095c3cf5d17175b6c9a3ff058f6aece37bc:V08OylkJ2ZZPClAXbTaxrXJ9YpKnmucJxcQI-wvIGiE9MA== commando amount_msat=100000 label="invoice label" description="invoice description"
```

Than:

```
commando 03ce2d830369fc903ffec52ca1d7aba095c3cf5d17175b6c9a3ff058f6aece37bc invoice '["any", "Invoice Label", "Invoice Description"]' V08OylkJ2ZZPClAXbTaxrXJ9YpKnmucJxcQI-wvIGiE9MA==
commando 03ce2d830369fc903ffec52ca1d7aba095c3cf5d17175b6c9a3ff058f6aece37bc invoice '{"amount_msat": "100000", "label": "invoice label", "description": "invoice description"}' V08OylkJ2ZZPClAXbTaxrXJ9YpKnmucJxcQI-wvIGiE9MA==
```

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: cli: `--commando=peerid:rune` (or `-c peerid:rune`) as convenient shortcut for running commando commands.
This commit is contained in:
Rusty Russell 2023-01-03 14:53:28 +10:30
parent 3f0c5b985b
commit 404e961bad
3 changed files with 183 additions and 6 deletions

View file

@ -12,6 +12,7 @@
#include <ccan/tal/str/str.h>
#include <common/configdir.h>
#include <common/json_command.h>
#include <common/node_id.h>
#include <common/status_levels.h>
#include <common/utils.h>
#include <common/version.h>
@ -613,6 +614,29 @@ static void opt_log_stderr_exit_usage(const char *fmt, ...)
exit(ERROR_USAGE);
}
struct commando {
const char *peer_id;
const char *rune;
};
static char *opt_set_commando(const char *arg, struct commando **commando)
{
size_t idlen = strcspn(arg, ":");
*commando = tal(NULL, struct commando);
/* We don't use common/node_id.c here, to keep dependencies minimal */
if (idlen != PUBKEY_CMPR_LEN * 2)
return "Invalid peer id";
(*commando)->peer_id = tal_strndup(*commando, arg, idlen);
if (arg[idlen] == '\0')
(*commando)->rune = NULL;
else
(*commando)->rune = tal_strdup(*commando, arg + idlen + 1);
return NULL;
}
int main(int argc, char *argv[])
{
setup_locale();
@ -633,6 +657,7 @@ int main(int argc, char *argv[])
enum log_level notification_level = LOG_INFORM;
bool last_was_progress = false;
char *command = NULL, *filter = NULL;
struct commando *commando = NULL;
err_set_progname(argv[0]);
jsmn_init(&parser);
@ -665,12 +690,18 @@ int main(int argc, char *argv[])
opt_register_arg("-l|--filter", opt_set_charp,
opt_show_charp, &filter,
"Set JSON reply filter");
opt_register_arg("-c|--commando", opt_set_commando,
NULL, &commando,
"Send this as a commando command to nodeid:rune");
opt_register_version();
opt_early_parse(argc, argv, opt_log_stderr_exit_usage);
opt_parse(&argc, argv, opt_log_stderr_exit_usage);
/* Make sure this is parented correctly if set! */
tal_steal(ctx, commando);
method = argv[1];
if (!method) {
char *usage = opt_usage(argv[0], NULL);
@ -682,7 +713,7 @@ int main(int argc, char *argv[])
/* Launch a manpage if we have a help command with an argument. We do
* not need to have lightningd running in this case. */
if (streq(method, "help") && format == DEFAULT_FORMAT && argc >= 3) {
if (streq(method, "help") && format == DEFAULT_FORMAT && argc >= 3 && !commando) {
command = argv[2];
char *page = tal_fmt(ctx, "lightning-%s", command);
@ -724,16 +755,34 @@ int main(int argc, char *argv[])
else
idstr = tal_fmt(ctx, "cli:%s#%i", method, getpid());
if (notification_level <= LOG_LEVEL_MAX)
/* FIXME: commando should support notifications! */
if (notification_level <= LOG_LEVEL_MAX && !commando)
enable_notifications(fd);
cmd = tal_fmt(ctx,
"{ \"jsonrpc\" : \"2.0\", \"method\" : \"%s\", \"id\" : \"%s\",",
json_escape(ctx, method)->s, idstr);
if (filter)
commando ? "commando" : json_escape(ctx, method)->s,
idstr);
if (filter && !commando)
tal_append_fmt(&cmd, "\"filter\": %s,", filter);
tal_append_fmt(&cmd, " \"params\" :");
if (commando) {
tal_append_fmt(&cmd, "{"
" \"peer_id\": \"%s\","
" \"method\": \"%s\",",
commando->peer_id,
json_escape(ctx, method)->s);
if (filter) {
tal_append_fmt(&cmd, "\"filter\": %s,", filter);
}
if (commando->rune) {
tal_append_fmt(&cmd, " \"rune\": \"%s\",",
commando->rune);
}
tal_append_fmt(&cmd, " \"params\": ");
}
if (input == DEFAULT_INPUT) {
/* Hacky autodetect; only matters if more than single arg */
if (argc > 2 && strchr(argv[2], '='))
@ -764,6 +813,10 @@ int main(int argc, char *argv[])
tal_append_fmt(&cmd, "] }");
}
/* For commando, "params" we just populated is inside real "params" */
if (commando)
tal_append_fmt(&cmd, "}");
toks = json_parse_simple(ctx, cmd, strlen(cmd));
if (toks == NULL)
errx(ERROR_USAGE,

View file

@ -69,7 +69,7 @@ field without parsing JSON.
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 `#
`.
`. (Note: currently not supported with `--commando`).
* **--filter**/**-l**=*JSON*
@ -84,12 +84,21 @@ be changed using `-F`, `-R`, `-J`, `-H` etc.
Print version number to standard output and exit.
* **allow-deprecated-apis**=*BOOL*
* **--allow-deprecated-apis**=*BOOL*
Enable deprecated options. It defaults to *true*, but you should set
it to *false* when testing to ensure that an upgrade won't break your
configuration.
* **--commando**/**-c**=**peerid**:**rune**
Convenience option to indicate that this command should be wrapped
in a `commando` command to be sent to the connected peer with id
`peerid`, using rune `rune`. This also means that any `--filter` is
handed via commando to the remote peer to reduce its output (which it
will do it it is v23.02 or newer), rather than trying to do so
locally. Note that currently `-N` is not supported by commando.
COMMANDS
--------

View file

@ -1044,6 +1044,121 @@ def test_cli(node_factory):
assert [l for l in lines if not re.search(r'^help\[[0-9]*\].', l)] == ['format-hint=simple']
def test_cli_commando(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False,
opts={'log-level': 'io'})
rune = l2.rpc.commando_rune()['rune']
# Invalid peer id.
val = subprocess.run(['cli/lightning-cli',
'--commando=00',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'help'])
assert val.returncode == 3
# Valid peer id, but needs rune!
val = subprocess.run(['cli/lightning-cli',
'--commando={}'.format(l2.info['id']),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'help'])
assert val.returncode == 1
# This works!
out = subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'help']).decode('utf-8')
# Test some known output.
assert 'help [command]\n List available commands, or give verbose help on one {command}' in out
# Check JSON id is as expected
l1.daemon.wait_for_log(r'jsonrpc#[0-9]*: "cli:help#[0-9]*"\[IN\]')
# And through l2...
l2.daemon.wait_for_log(r'jsonrpc#[0-9]*: "cli:help#[0-9]*/cln:commando#[0-9]*/commando:help#[0-9]*"\[IN\]')
# Test keyword input (forced)
out = subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'-J', '-k',
'help', 'command=help']).decode('utf-8')
j, _ = json.JSONDecoder().raw_decode(out)
assert 'help [command]' in j['help'][0]['verbose']
# Test ordered input (forced)
out = subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'-J', '-o',
'help', 'help']).decode('utf-8')
j, _ = json.JSONDecoder().raw_decode(out)
assert 'help [command]' in j['help'][0]['verbose']
# Test filtering
out = subprocess.check_output(['cli/lightning-cli',
'-c', '{}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'-J', '--filter={"help":[{"command":true}]}',
'help', 'help']).decode('utf-8')
j, _ = json.JSONDecoder().raw_decode(out)
assert j == {'help': [{'command': 'help [command]'}]}
# Test missing parameters.
try:
# This will error due to missing parameters.
# We want to check if lightningd will crash.
out = subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'-J', '-o',
'sendpay']).decode('utf-8')
except Exception:
pass
# Test it escapes JSON completely in both method and params.
# cli turns " into \", reply turns that into \\\".
out = subprocess.run(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'x"[]{}'],
stdout=subprocess.PIPE)
assert 'Unknown command' in out.stdout.decode('utf-8')
subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'invoice', '123000', 'l"[]{}', 'd"[]{}']).decode('utf-8')
# Check label is correct, and also that cli's keyword parsing works.
out = subprocess.check_output(['cli/lightning-cli',
'--commando={}:{}'.format(l2.info['id'], rune),
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'-k',
'listinvoices', 'label=l"[]{}']).decode('utf-8')
j = json.loads(out)
assert only_one(j['invoices'])['label'] == 'l"[]{}'
def test_daemon_option(node_factory):
"""
Make sure --daemon at least vaguely works!