Compare commits

..

10 Commits

Author SHA1 Message Date
ee79249d23 json-rpc-shell.adoc: update WebSocket notes
https://github.com/open-rpc/client-js also uses WebSockets,
although they don't seem to support notifications (in general).
2020-10-10 05:20:31 +02:00
160d23018a Bump liberty
resolve_relative_runtime_unique_filename() used to have a bug.
2020-10-10 05:09:11 +02:00
fed2892ee1 Readline: add trivial OpenRPC support
So far hidden under a switch and only for this frontend.
2020-10-10 05:09:10 +02:00
667b01cb73 Reorder help message entries a bit
Should be both more useful and more alphabetic this way.
2020-10-10 02:57:14 +02:00
20c8578084 Fix use of possibly uninitialised memory 2020-10-10 02:57:14 +02:00
57a3b4e990 Split make_json_rpc_call() in half 2020-10-10 02:57:13 +02:00
e4d1529b4d Slightly refactor make_json_rpc_call() 2020-10-10 02:57:13 +02:00
897a263ee7 Readline: make M-Enter insert a newline
Before, it was only possible with C-v C-j but it's too useful
to require such an awkward method.

There is a precedent in, e.g., zsh and fish for the new binding.
2020-10-09 20:41:37 +02:00
84702fa47d Fix handling terminal resizes while the terminal is suspended
GNU Readline has a misfeature.
2020-10-09 20:21:52 +02:00
b315892249 Readline: fix a dormant bug in prompt changes
For details, see a similar change in degesch from uirc3.
2020-10-09 20:17:17 +02:00
3 changed files with 208 additions and 142 deletions

View File

@@ -113,8 +113,7 @@ newlines.
WebSockets
~~~~~~~~~~
The JSON-RPC 2.0 specification doesn't say almost anything about underlying
transports. As far as the author is aware, he is the only person combining it
with WebSockets. The way it's implemented here is that every request is sent as
transports. The way it's implemented here is that every request is sent as
a single text message. If it has an "id" field, i.e., it's not just
a notification, the client waits for a message from the server in response.
Should any message arrive unexpectedly, you will receive a warning.

View File

@@ -246,16 +246,30 @@ input_rl_on_run_editor (int count, int key)
return 0;
}
static int
input_rl_newline_insert (int count, int key)
{
(void) count;
(void) key;
rl_insert_text ("\n");
return 0;
}
static int
input_rl_on_startup (void)
{
rl_add_defun ("run-editor", input_rl_on_run_editor, -1);
rl_bind_keyseq ("\\ee", rl_named_function ("run-editor"));
rl_add_defun ("newline-insert", input_rl_newline_insert, -1);
rl_bind_keyseq ("\\e\\r", rl_named_function ("newline-insert"));
return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static char **app_readline_completion (const char *text, int start, int end);
static void
input_rl_start (struct input *input, const char *program_name)
{
@@ -268,6 +282,9 @@ input_rl_start (struct input *input, const char *program_name)
rl_readline_name = slash ? ++slash : program_name;
rl_startup_hook = input_rl_on_startup;
rl_catch_sigwinch = false;
rl_change_environment = false;
rl_attempted_completion_function = app_readline_completion;
hard_assert (self->prompt != NULL);
rl_callback_handler_install (self->prompt, input_rl_on_input);
@@ -347,8 +364,7 @@ input_rl_show (struct input *input)
rl_replace_line (self->saved_line, false);
rl_point = self->saved_point;
rl_mark = self->saved_mark;
free (self->saved_line);
self->saved_line = NULL;
cstr_set (&self->saved_line, NULL);
rl_redisplay ();
}
@@ -357,20 +373,17 @@ static void
input_rl_set_prompt (struct input *input, char *prompt)
{
struct input_rl *self = (struct input_rl *) input;
free (self->prompt);
self->prompt = prompt;
cstr_set (&self->prompt, prompt);
if (!self->active)
if (!self->active || self->prompt_shown <= 0)
return;
// First reset the prompt to work around a bug in readline
rl_set_prompt ("");
if (self->prompt_shown > 0)
rl_redisplay ();
rl_redisplay ();
rl_set_prompt (self->prompt);
if (self->prompt_shown > 0)
rl_redisplay ();
rl_redisplay ();
}
static bool
@@ -652,6 +665,8 @@ input_el_start (struct input *input, const char *program_name)
"run-editor", "Run editor to edit line", input_el_on_run_editor);
el_set (self->editline, EL_BIND, "M-e", "run-editor", NULL);
// TODO: implement method name completion for editline (see degesch)
// Source the user's defaults file
el_source (self->editline, NULL);
@@ -745,8 +760,7 @@ static void
input_el_set_prompt (struct input *input, char *prompt)
{
struct input_el *self = (struct input_el *) input;
free (self->prompt);
self->prompt = prompt;
cstr_set (&self->prompt, prompt);
if (self->prompt_shown > 0)
input_el_redisplay (self);
@@ -852,8 +866,7 @@ input_el_on_tty_readable (struct input *input)
self->prompt_shown = 0;
self->super.on_input (self->entered_line, self->super.user_data);
free (self->entered_line);
self->entered_line = NULL;
cstr_set (&self->entered_line, NULL);
// Forbid editline from trying to erase the old prompt (or worse)
// and let it redisplay the prompt in its clean state
@@ -929,12 +942,14 @@ static struct app_context
struct backend *backend; ///< Our current backend
char *editor_filename; ///< File for input line editor
struct str_map methods; ///< Methods detected via OpenRPC
struct config config; ///< Program configuration
enum color_mode color_mode; ///< Colour output mode
bool compact; ///< Whether to not pretty print
bool verbose; ///< Print requests
bool trust_all; ///< Don't verify peer certificates
bool openrpc; ///< OpenRPC method name completion
bool null_as_id; ///< JSON null is used as an ID
int64_t next_id; ///< Next autogenerated ID
@@ -1237,10 +1252,9 @@ on_config_attribute_change (struct config_item *item)
ssize_t id = attr_by_name (item->schema->name);
if (id != -1)
{
free (ctx->attrs[id]);
ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL
? ctx->attrs_defaults[id]
: item->value.string.str);
: item->value.string.str));
}
}
@@ -2716,6 +2730,7 @@ static void
resume_terminal (struct app_context *ctx)
{
ctx->input->vtable->prepare (ctx->input, true);
ctx->input->vtable->on_terminal_resized (ctx->input);
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->show (ctx->input);
}
@@ -2870,6 +2885,82 @@ fail:
return success;
}
static void
maybe_print_verbose (struct app_context *ctx, intptr_t attribute,
char *utf8, size_t len)
{
if (!ctx->verbose)
return;
char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, len, NULL);
if (!term)
{
print_error ("%s: %s", "verbose", "character conversion failed");
return;
}
ctx->input->vtable->hide (ctx->input);
print_attributed (ctx, stdout, attribute, "%s", term);
fputs ("\n", stdout);
free (term);
ctx->input->vtable->show (ctx->input);
}
static struct error *
json_rpc_call_raw (struct app_context *ctx,
const char *method, json_t *id, json_t *params, struct str *buf)
{
json_t *request = json_object ();
json_object_set_new (request, "jsonrpc", json_string ("2.0"));
json_object_set_new (request, "method", json_string (method));
if (id) json_object_set (request, "id", id);
if (params) json_object_set (request, "params", params);
char *req_utf8 = json_dumps (request, 0);
json_decref (request);
maybe_print_verbose (ctx, ATTR_OUTGOING, req_utf8, -1);
struct error *error = NULL;
ctx->backend->vtable->make_call (ctx->backend, req_utf8,
id != NULL /* expect_content */, buf, &error);
free (req_utf8);
if (error)
return error;
maybe_print_verbose (ctx, ATTR_INCOMING, buf->str, buf->len + 1);
return NULL;
}
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params, const char *pipeline)
{
struct str buf = str_make ();
struct error *e = json_rpc_call_raw (ctx, method, id, params, &buf);
if (e)
{
print_error ("%s", e->message);
error_free (e);
}
else if (!process_response (ctx, id, &buf, pipeline))
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1 /* null byte */, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else if (!ctx->verbose /* already printed */)
printf ("%s: %s\n", "raw response data", s);
free (s);
}
str_free (&buf);
}
static bool
is_valid_json_rpc_id (json_t *v)
{
@@ -2883,73 +2974,6 @@ is_valid_json_rpc_params (json_t *v)
return json_is_array (v) || json_is_object (v);
}
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params, const char *pipeline)
{
json_t *request = json_object ();
json_object_set_new (request, "jsonrpc", json_string ("2.0"));
json_object_set_new (request, "method", json_string (method));
if (id) json_object_set (request, "id", id);
if (params) json_object_set (request, "params", params);
char *req_utf8 = json_dumps (request, 0);
if (ctx->verbose)
{
char *req_term = iconv_xstrdup
(ctx->term_from_utf8, req_utf8, -1, NULL);
if (!req_term)
print_error ("%s: %s", "verbose", "character conversion failed");
else
{
print_attributed (ctx, stdout, ATTR_OUTGOING, "%s", req_term);
fputs ("\n", stdout);
}
free (req_term);
}
struct str buf = str_make ();
struct error *e = NULL;
if (!ctx->backend->vtable->make_call
(ctx->backend, req_utf8, id != NULL, &buf, &e))
{
print_error ("%s", e->message);
error_free (e);
goto fail;
}
if (ctx->verbose)
{
char *buf_term =
iconv_xstrdup (ctx->term_from_utf8, buf.str, buf.len, NULL);
if (!buf_term)
print_error ("%s: %s", "verbose", "character conversion failed");
else
{
print_attributed (ctx, stdout, ATTR_INCOMING, "%s", buf_term);
fputs ("\n", stdout);
}
free (buf_term);
}
if (!process_response (ctx, id, &buf, pipeline))
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1 /* null byte */, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else if (!ctx->verbose /* already printed */)
printf ("%s: %s\n", "raw response data", s);
free (s);
}
fail:
str_free (&buf);
free (req_utf8);
json_decref (request);
}
static void
process_input (char *user_input, void *user_data)
{
@@ -3065,56 +3089,94 @@ fail:
free (input);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// --- OpenRPC information extraction ------------------------------------------
static void
init_openrpc (struct app_context *ctx)
{
if (!ctx->openrpc)
return;
json_t *id = json_integer (ctx->next_id++);
struct str buf = str_make ();
struct error *e = json_rpc_call_raw (ctx, "rpc.discover", id, NULL, &buf);
json_decref (id);
// Just optimistically punch through, I don't have time for this shit
json_error_t error;
json_t *response = NULL, *result = NULL, *value = NULL;
if (!e && !(response = json_loadb (buf.str, buf.len, 0, &error)))
error_set (&e, "parse failure: %s", error.text);
else if (!(result = json_object_get (response, "result"))
|| !(result = json_object_get (result, "methods")))
error_set (&e, "unsupported");
else
{
const char *name = NULL;
for (size_t i = 0; (value = json_array_get (result, i)); i++)
if ((value = json_object_get (value, "name"))
&& (name = json_string_value (value)))
str_map_set (&ctx->methods, name, (void *) 1);
}
json_decref (response);
if (e)
{
print_error ("OpenRPC: %s", e->message);
error_free (e);
}
str_free (&buf);
}
// --- GNU Readline user actions -----------------------------------------------
#ifdef HAVE_READLINE
static char *
app_readline_complete (const char *text, int state)
{
static struct str_map_iter iter;
if (!state)
iter = str_map_iter_make (&g_ctx.methods);
char *input;
size_t len;
if (!(input = iconv_xstrdup (g_ctx.term_to_utf8, (char *) text, -1, &len)))
{
print_error ("character conversion failed for `%s'", "user input");
return NULL;
}
char *match = NULL;
while (str_map_iter_next (&iter)
&& (strncasecmp_ascii (input, iter.link->key, len - 1 /* XXX */)
|| !(match = iconv_xstrdup (g_ctx.term_from_utf8,
iter.link->key, iter.link->key_length + 1, NULL))))
;
free (input);
return match;
}
static char **
app_readline_completion (const char *text, int start, int end)
{
(void) end;
// Only customize matches for the first token, which is the method name
if (start)
return NULL;
// Don't iterate over filenames and stuff in this case
rl_attempted_completion_over = true;
return rl_completion_matches (text, app_readline_complete);
}
#endif // HAVE_READLINE
// --- Main program ------------------------------------------------------------
// The ability to use an external editor on the input line has been shamelessly
// copypasted from degesch with minor changes only.
/// This differs from the non-unique version in that we expect the filename
/// to be something like a pattern for mkstemp(), so the resulting path can
/// reside in a system-wide directory with no risk of a conflict.
static char *
resolve_relative_runtime_unique_filename (const char *filename)
{
struct str path = str_make ();
const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
const char *tmpdir = getenv ("TMPDIR");
if (runtime_dir && *runtime_dir == '/')
str_append (&path, runtime_dir);
else if (tmpdir && *tmpdir == '/')
str_append (&path, tmpdir);
else
str_append (&path, "/tmp");
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
// Try to create the file's ancestors;
// typically the user will want to immediately create a file in there
const char *last_slash = strrchr (path.str, '/');
if (last_slash && last_slash != path.str)
{
char *copy = xstrndup (path.str, last_slash - path.str);
(void) mkdir_with_parents (copy, NULL);
free (copy);
}
return str_steal (&path);
}
static bool
xwrite (int fd, const char *data, size_t len, struct error **e)
{
size_t written = 0;
while (written < len)
{
ssize_t res = write (fd, data + written, len - written);
if (res >= 0)
written += res;
else if (errno != EINTR)
FAIL ("%s", strerror (errno));
}
return true;
}
static bool
dump_line_to_file (const char *line, char *template, struct error **e)
{
@@ -3134,7 +3196,7 @@ static char *
try_dump_line_to_file (const char *line)
{
char *template = resolve_filename
("input.XXXXXX", resolve_relative_runtime_unique_filename);
("input.XXXXXX", resolve_relative_runtime_template);
struct error *e = NULL;
if (dump_line_to_file (line, template, &e))
@@ -3259,8 +3321,7 @@ on_child (EV_P_ ev_child *handle, int revents)
else
process_edited_input (ctx);
free (ctx->editor_filename);
ctx->editor_filename = NULL;
cstr_set (&ctx->editor_filename, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -3345,19 +3406,21 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv,
{
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help message and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 'n', "null-as-id", NULL, 0, "JSON null is used as an `id'" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
{ 'c', "compact-output", NULL, 0, "do not pretty-print responses" },
{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
{ 'v', "verbose", NULL, 0, "print raw requests and responses" },
{ 'C', "color", "WHEN", OPT_LONG_ONLY,
"colorize output: never, always, or auto" },
{ 'n', "null-as-id", NULL, 0, "JSON null is used as an `id'" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
// So far you have to explicitly enable this rather than disable
{ 'O', "openrpc", NULL, 0, "method name completion using OpenRPC" },
{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
{ 'v', "verbose", NULL, 0, "print raw requests and responses" },
{ 'w', "write-default-cfg", "FILENAME",
OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
"write a default configuration file and exit" },
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help message and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
@@ -3379,6 +3442,7 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv,
exit (EXIT_SUCCESS);
case 'o': *origin = optarg; break;
case 'O': ctx->openrpc = true; break;
case 'n': ctx->null_as_id = true; break;
case 'c': ctx->compact = true; break;
case 't': ctx->trust_all = true; break;
@@ -3436,6 +3500,7 @@ main (int argc, char *argv[])
g_ctx.input->on_input = process_input;
g_ctx.input->on_run_editor = run_editor;
g_ctx.methods = str_map_make (NULL);
init_colors (&g_ctx);
load_configuration (&g_ctx);
@@ -3515,6 +3580,7 @@ main (int argc, char *argv[])
g_ctx.input->vtable->start (g_ctx.input, PROGRAM_NAME);
ev_set_userdata (EV_DEFAULT_ &g_ctx);
init_openrpc (&g_ctx);
ev_run (EV_DEFAULT_ 0);
// User has terminated the program, let's save the history and clean up
@@ -3537,6 +3603,7 @@ main (int argc, char *argv[])
iconv_close (g_ctx.term_from_utf8);
iconv_close (g_ctx.term_to_utf8);
str_map_free (&g_ctx.methods);
config_free (&g_ctx.config);
free_terminal ();
ev_loop_destroy (EV_DEFAULT);

Submodule liberty updated: 1a76b2032e...e029aae1d3