Compare commits

...

12 Commits

Author SHA1 Message Date
2b18ebf314 Implement tab completion under libedit
I haven't tested it with real wide characters but it will have to do.
I wasn't even sure if this piece of crap could be coerced into doing
this at first, so it's a win for me.

It uses a variation of the code in degesch where we /don't/ want to
print the list of candidates on partial failure.

Updates #1
2020-10-13 03:58:26 +02:00
5d2cd01db0 json-rpc-test-server: fix a potential memory leak 2020-10-13 02:08:53 +02:00
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
4 changed files with 323 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

@@ -141,6 +141,8 @@ struct input
void (*on_input) (char *line, void *user_data);
/// User requested external line editing
void (*on_run_editor) (const char *line, void *user_data);
/// Tab completion generator, returns locale encoding strings or NULL
char *(*complete_start_word) (const char *text, int state);
};
struct input_vtable
@@ -246,16 +248,42 @@ 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)
{
(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, g_input_rl->super.complete_start_word);
}
static void
input_rl_start (struct input *input, const char *program_name)
{
@@ -268,6 +296,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 +378,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,19 +387,16 @@ 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_set_prompt (self->prompt);
if (self->prompt_shown > 0)
rl_redisplay ();
}
@@ -625,6 +652,8 @@ input_el_install_prompt (struct input_el *self)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static unsigned char input_el_on_complete (EditLine *editline, int key);
static void
input_el_start (struct input *input, const char *program_name)
{
@@ -652,6 +681,10 @@ 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);
el_set (self->editline, EL_ADDFN,
"complete", "Complete word", input_el_on_complete);
el_set (self->editline, EL_BIND, "\t", "complete", NULL);
// Source the user's defaults file
el_source (self->editline, NULL);
@@ -745,8 +778,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);
@@ -781,6 +813,121 @@ input_el_ding (struct input *input)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
input_el_collate (const void *a, const void *b)
{
return strcoll (*(const char **) a, *(const char **) b);
}
static struct strv
input_el_collect_candidates (struct input_el *self, const char *word)
{
struct strv v = strv_make ();
int i = 0; char *candidate = NULL;
while ((candidate = self->super.complete_start_word (word, i++)))
strv_append_owned (&v, candidate);
qsort (v.vector, v.len, sizeof *v.vector, input_el_collate);
return v;
}
static void
input_el_print_candidates (struct input_el *self, const struct strv *v)
{
EditLine *editline = self->editline;
// This insanity seems to be required to make it reprint the prompt
const LineInfoW *info = el_wline (editline);
int from_cursor_until_end = info->lastchar - info->cursor;
el_cursor (editline, from_cursor_until_end);
el_insertstr (editline, "\n");
input_el_redisplay (self);
el_wdeletestr (editline, 1);
el_set (editline, EL_REFRESH);
input_el_hide (&self->super);
for (size_t i = 0; i < v->len; i++)
printf ("%s\n", v->vector[i]);
input_el_show (&self->super);
el_cursor (editline, -from_cursor_until_end);
}
static void
input_el_insert_common_prefix (EditLine *editline, const struct strv *v)
{
char *p[v->len]; memcpy (p, v->vector, sizeof p);
mbstate_t state[v->len]; memset (state, 0, sizeof state);
wchar_t want[2] = {}; size_t len;
while ((len = mbrtowc (&want[0], p[0], strlen (p[0]), &state[0])) > 0)
{
p[0] += len;
for (size_t i = 1; i < v->len; i++)
{
wchar_t found = 0;
if ((len = mbrtowc (&found, p[i], strlen (p[i]), &state[i])) <= 0
|| found != want[0])
return;
p[i] += len;
}
el_winsertstr (editline, want);
}
}
static unsigned char
input_el_on_complete (EditLine *editline, int key)
{
(void) key;
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
// First prepare what Readline would have normally done for us...
const LineInfo *info_mb = el_line (editline);
int len = info_mb->lastchar - info_mb->buffer;
int point = info_mb->cursor - info_mb->buffer;
char *word = xstrndup (info_mb->buffer, len);
int start = point;
while (start && !isspace_ascii (word[start - 1]))
start--;
// Only complete the first word, when we're at the end of it
if (start != 0
|| (word[point] && !isspace_ascii (word[point]))
|| (point && isspace_ascii (word[point - 1])))
{
free (word);
return CC_REFRESH_BEEP;
}
word[point] = '\0';
int word_len = mbstowcs (NULL, word, 0);
struct strv v = input_el_collect_candidates (self, word);
free (word);
if (!v.len)
{
strv_free (&v);
return CC_REFRESH_BEEP;
}
// Remove the original word and replace it with the best (sub)match
el_wdeletestr (editline, word_len);
if (v.len == 1)
{
el_insertstr (editline, v.vector[0]);
el_insertstr (editline, " ");
strv_free (&v);
return CC_REFRESH;
}
input_el_insert_common_prefix (editline, &v);
input_el_print_candidates (self, &v);
strv_free (&v);
return CC_REFRESH_BEEP;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
input_el_load_history (struct input *input, const char *filename,
struct error **e)
@@ -852,8 +999,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 +1075,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 +1385,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 +2863,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 +3018,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 +3107,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 +3222,74 @@ 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);
}
static char *
complete_method_name (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;
}
// --- 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 +3309,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 +3434,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 +3519,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 +3555,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;
@@ -3435,7 +3612,9 @@ main (int argc, char *argv[])
g_ctx.input->user_data = &g_ctx;
g_ctx.input->on_input = process_input;
g_ctx.input->on_run_editor = run_editor;
g_ctx.input->complete_start_word = complete_method_name;
g_ctx.methods = str_map_make (NULL);
init_colors (&g_ctx);
load_configuration (&g_ctx);
@@ -3515,6 +3694,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 +3717,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);

View File

@@ -329,6 +329,7 @@ fcgi_muxer_on_get_values
nv_parser.output = &values;
fcgi_nv_parser_push (&nv_parser, parser->content.str, parser->content.len);
fcgi_nv_parser_free (&nv_parser);
const char *key = NULL;
// No real-world servers seem to actually use multiplexing

Submodule liberty updated: 1a76b2032e...e029aae1d3