/* * json-rpc-shell.c: trivial JSON-RPC 2.0 shell * * Copyright (c) 2014 - 2015, PÅ™emysl Janouch * All rights reserved. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ /// Some arbitrary limit for the history file #define HISTORY_LIMIT 10000 // String constants for all attributes we use for output #define ATTR_PROMPT "attr_prompt" #define ATTR_RESET "attr_reset" #define ATTR_WARNING "attr_warning" #define ATTR_ERROR "attr_error" #define ATTR_INCOMING "attr_incoming" #define ATTR_OUTGOING "attr_outgoing" // User data for logger functions to enable formatted logging #define print_fatal_data ATTR_ERROR #define print_error_data ATTR_ERROR #define print_warning_data ATTR_WARNING #include "config.h" #include "utils.c" #include #include #include #include #include #include #include #include #include #include // --- Configuration (application-specific) ------------------------------------ static struct config_item g_config_table[] = { { ATTR_PROMPT, NULL, "Terminal attributes for the prompt" }, { ATTR_RESET, NULL, "String to reset terminal attributes" }, { ATTR_WARNING, NULL, "Terminal attributes for warnings" }, { ATTR_ERROR, NULL, "Terminal attributes for errors" }, { ATTR_INCOMING, NULL, "Terminal attributes for incoming traffic" }, { ATTR_OUTGOING, NULL, "Terminal attributes for outgoing traffic" }, { NULL, NULL, NULL } }; // --- Main program ------------------------------------------------------------ enum color_mode { COLOR_AUTO, ///< Autodetect if colours are available COLOR_ALWAYS, ///< Always use coloured output COLOR_NEVER ///< Never use coloured output }; static struct app_context { CURL *curl; ///< cURL handle char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer struct str_map config; ///< Program configuration enum color_mode color_mode; ///< Colour output mode bool pretty_print; ///< Whether to pretty print bool verbose; ///< Print requests bool trust_all; ///< Don't verify peer certificates bool auto_id; ///< Use automatically generated ID's int64_t next_id; ///< Next autogenerated ID iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding } g_ctx; // --- Attributed output ------------------------------------------------------- static struct { bool initialized; ///< Terminal is available bool stdout_is_tty; ///< `stdout' is a terminal bool stderr_is_tty; ///< `stderr' is a terminal char *color_set[8]; ///< Codes to set the foreground colour } g_terminal; static bool init_terminal (void) { int tty_fd = -1; if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) tty_fd = STDERR_FILENO; if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) tty_fd = STDOUT_FILENO; int err; if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) return false; // Make sure all terminal features used by us are supported if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode) { del_curterm (cur_term); return false; } for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground, i, 0, 0, 0, 0, 0, 0, 0, 0)); return g_terminal.initialized = true; } static void free_terminal (void) { if (!g_terminal.initialized) return; for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) free (g_terminal.color_set[i]); del_curterm (cur_term); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef int (*terminal_printer_fn) (int); static int putchar_stderr (int c) { return fputc (c, stderr); } static terminal_printer_fn get_attribute_printer (FILE *stream) { if (stream == stdout && g_terminal.stdout_is_tty) return putchar; if (stream == stderr && g_terminal.stderr_is_tty) return putchar_stderr; return NULL; } static void vprint_attributed (struct app_context *ctx, FILE *stream, const char *attribute, const char *fmt, va_list ap) { terminal_printer_fn printer = get_attribute_printer (stream); if (!attribute) printer = NULL; if (printer) { const char *value = str_map_find (&ctx->config, attribute); tputs (value, 1, printer); } vfprintf (stream, fmt, ap); if (printer) { const char *value = str_map_find (&ctx->config, ATTR_RESET); tputs (value, 1, printer); } } static void print_attributed (struct app_context *ctx, FILE *stream, const char *attribute, const char *fmt, ...) { va_list ap; va_start (ap, fmt); vprint_attributed (ctx, stream, attribute, fmt, ap); va_end (ap); } static void log_message_attributed (void *user_data, const char *quote, const char *fmt, va_list ap) { FILE *stream = stderr; print_attributed (&g_ctx, stream, user_data, "%s", quote); vprint_attributed (&g_ctx, stream, user_data, fmt, ap); fputs ("\n", stream); } static void init_colors (struct app_context *ctx) { // Use escape sequences from terminfo if possible, and SGR as a fallback if (init_terminal ()) { const char *attrs[][2] = { { ATTR_PROMPT, enter_bold_mode }, { ATTR_RESET, exit_attribute_mode }, { ATTR_WARNING, g_terminal.color_set[3] }, { ATTR_ERROR, g_terminal.color_set[1] }, { ATTR_INCOMING, "" }, { ATTR_OUTGOING, "" }, }; for (size_t i = 0; i < N_ELEMENTS (attrs); i++) str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); } else { const char *attrs[][2] = { { ATTR_PROMPT, "\x1b[1m" }, { ATTR_RESET, "\x1b[0m" }, { ATTR_WARNING, "\x1b[33m" }, { ATTR_ERROR, "\x1b[31m" }, { ATTR_INCOMING, "" }, { ATTR_OUTGOING, "" }, }; for (size_t i = 0; i < N_ELEMENTS (attrs); i++) str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); } switch (ctx->color_mode) { case COLOR_ALWAYS: g_terminal.stdout_is_tty = true; g_terminal.stderr_is_tty = true; break; case COLOR_AUTO: if (!g_terminal.initialized) { case COLOR_NEVER: g_terminal.stdout_is_tty = false; g_terminal.stderr_is_tty = false; } } g_log_message_real = log_message_attributed; } // --- Configuration loading --------------------------------------------------- static bool read_hexa_escape (const char **cursor, struct str *output) { int i; char c, code = 0; for (i = 0; i < 2; i++) { c = tolower (*(*cursor)); if (c >= '0' && c <= '9') code = (code << 4) | (c - '0'); else if (c >= 'a' && c <= 'f') code = (code << 4) | (c - 'a' + 10); else break; (*cursor)++; } if (!i) return false; str_append_c (output, code); return true; } static bool read_octal_escape (const char **cursor, struct str *output) { int i; char c, code = 0; for (i = 0; i < 3; i++) { c = *(*cursor); if (c < '0' || c > '7') break; code = (code << 3) | (c - '0'); (*cursor)++; } if (!i) return false; str_append_c (output, code); return true; } static bool read_string_escape_sequence (const char **cursor, struct str *output, struct error **e) { int c; switch ((c = *(*cursor)++)) { case '?': str_append_c (output, '?'); break; case '"': str_append_c (output, '"'); break; case '\\': str_append_c (output, '\\'); break; case 'a': str_append_c (output, '\a'); break; case 'b': str_append_c (output, '\b'); break; case 'f': str_append_c (output, '\f'); break; case 'n': str_append_c (output, '\n'); break; case 'r': str_append_c (output, '\r'); break; case 't': str_append_c (output, '\t'); break; case 'v': str_append_c (output, '\v'); break; case 'e': case 'E': str_append_c (output, '\x1b'); break; case 'x': case 'X': if (!read_hexa_escape (cursor, output)) { error_set (e, "invalid hexadecimal escape"); return false; } break; case '\0': error_set (e, "premature end of escape sequence"); return false; default: (*cursor)--; if (!read_octal_escape (cursor, output)) { error_set (e, "unknown escape sequence"); return false; } } return true; } static bool unescape_string (const char *s, struct str *output, struct error **e) { int c; while ((c = *s++)) { if (c != '\\') str_append_c (output, c); else if (!read_string_escape_sequence (&s, output, e)) return false; } return true; } static void load_config (struct app_context *ctx) { // TODO: employ a better configuration file format, so that we don't have // to do this convoluted post-processing anymore. struct str_map map; str_map_init (&map); map.free = free; struct error *e = NULL; if (!read_config_file (&map, &e)) { print_error ("error loading configuration: %s", e->message); error_free (e); exit (EXIT_FAILURE); } struct str_map_iter iter; str_map_iter_init (&iter, &map); while (str_map_iter_next (&iter)) { struct error *e = NULL; struct str value; str_init (&value); if (!unescape_string (iter.link->data, &value, &e)) { print_error ("error reading configuration: %s: %s", iter.link->key, e->message); error_free (e); exit (EXIT_FAILURE); } str_map_set (&ctx->config, iter.link->key, str_steal (&value)); } str_map_free (&map); } // --- Main program ------------------------------------------------------------ #define PARSE_FAIL(...) \ BLOCK_START \ print_error (__VA_ARGS__); \ goto fail; \ BLOCK_END static bool parse_response (struct app_context *ctx, struct str *buf) { json_error_t e; json_t *response; if (!(response = json_loadb (buf->str, buf->len, JSON_DECODE_ANY, &e))) { print_error ("failed to parse the response: %s", e.text); return false; } bool success = false; if (!json_is_object (response)) PARSE_FAIL ("the response is not a JSON object"); json_t *v; if (!(v = json_object_get (response, "jsonrpc"))) print_warning ("`%s' field not present in response", "jsonrpc"); else if (!json_is_string (v) || strcmp (json_string_value (v), "2.0")) print_warning ("invalid `%s' field in response", "jsonrpc"); json_t *result = json_object_get (response, "result"); json_t *error = json_object_get (response, "error"); json_t *data = NULL; if (!result && !error) PARSE_FAIL ("neither `result' nor `error' present in response"); if (result && error) // Prohibited by the specification but happens in real life (null) print_warning ("both `result' and `error' present in response"); if (error) { if (!json_is_object (error)) PARSE_FAIL ("invalid `%s' field in response", "error"); json_t *code = json_object_get (error, "code"); json_t *message = json_object_get (error, "message"); if (!code) PARSE_FAIL ("missing `%s' field in error response", "code"); if (!message) PARSE_FAIL ("missing `%s' field in error response", "message"); if (!json_is_integer (code)) PARSE_FAIL ("invalid `%s' field in error response", "code"); if (!json_is_string (message)) PARSE_FAIL ("invalid `%s' field in error response", "message"); json_int_t code_val = json_integer_value (code); char *utf8 = xstrdup_printf ("error response: %" JSON_INTEGER_FORMAT " (%s)", code_val, json_string_value (message)); char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); free (utf8); if (!s) print_error ("character conversion failed for `%s'", "error"); else printf ("%s\n", s); free (s); data = json_object_get (error, "data"); } if (data) { char *utf8 = json_dumps (data, JSON_ENCODE_ANY); char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); free (utf8); if (!s) print_error ("character conversion failed for `%s'", "error data"); else printf ("error data: %s\n", s); free (s); } if (result) { int flags = JSON_ENCODE_ANY; if (ctx->pretty_print) flags |= JSON_INDENT (2); char *utf8 = json_dumps (result, flags); char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); free (utf8); if (!s) print_error ("character conversion failed for `%s'", "result"); else print_attributed (ctx, stdout, ATTR_INCOMING, "%s\n", s); free (s); } success = true; fail: json_decref (response); return success; } static bool is_valid_json_rpc_id (json_t *v) { return json_is_string (v) || json_is_integer (v) || json_is_real (v) || json_is_null (v); // These two shouldn't be used } static bool is_valid_json_rpc_params (json_t *v) { return json_is_array (v) || json_is_object (v); } static bool isspace_ascii (int c) { return strchr (" \f\n\r\t\v", c); } static size_t write_callback (char *ptr, size_t size, size_t nmemb, void *user_data) { struct str *buf = user_data; str_append_data (buf, ptr, size * nmemb); return size * nmemb; } #define RPC_FAIL(...) \ BLOCK_START \ print_error (__VA_ARGS__); \ goto fail; \ BLOCK_END static bool try_advance (const char **p, const char *text) { size_t len = strlen (text); if (strncmp (*p, text, len)) return false; *p += len; return true; } static bool validate_content_type (const char *type) { const char *content_types[] = { "application/json-rpc", // obsolete "application/json" }; const char *tails[] = { "; charset=utf-8", "; charset=UTF-8", "" }; bool found = false; for (size_t i = 0; i < N_ELEMENTS (content_types); i++) if ((found = try_advance (&type, content_types[i]))) break; if (!found) return false; for (size_t i = 0; i < N_ELEMENTS (tails); i++) if ((found = try_advance (&type, tails[i]))) break; if (!found) return false; return !*type; } static void make_json_rpc_call (struct app_context *ctx, const char *method, json_t *id, json_t *params) { 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\n", req_term); free (req_term); } struct str buf; str_init (&buf); if (curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDS, req_utf8) || curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t) -1) || curl_easy_setopt (ctx->curl, CURLOPT_WRITEDATA, &buf) || curl_easy_setopt (ctx->curl, CURLOPT_WRITEFUNCTION, write_callback)) RPC_FAIL ("cURL setup failed"); CURLcode ret; if ((ret = curl_easy_perform (ctx->curl))) RPC_FAIL ("HTTP request failed: %s", ctx->curl_error); long code; char *type; if (curl_easy_getinfo (ctx->curl, CURLINFO_RESPONSE_CODE, &code) || curl_easy_getinfo (ctx->curl, CURLINFO_CONTENT_TYPE, &type)) RPC_FAIL ("cURL info retrieval failed"); if (code != 200) RPC_FAIL ("unexpected HTTP response code: %ld", code); bool success = false; if (id) { if (!type) print_warning ("missing `Content-Type' header"); else if (!validate_content_type (type)) print_warning ("unexpected `Content-Type' header: %s", type); success = parse_response (ctx, &buf); } else { printf ("[Notification]\n"); if (buf.len) print_warning ("we have been sent data back for a notification"); else success = true; } if (!success) { char *s = iconv_xstrdup (ctx->term_from_utf8, buf.str, buf.len + 1, NULL); if (!s) print_error ("character conversion failed for `%s'", "raw response data"); else printf ("%s: %s\n", "raw response data", s); free (s); } fail: str_free (&buf); free (req_utf8); json_decref (request); } static void process_input (struct app_context *ctx, char *user_input) { char *input; size_t len; if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len))) { print_error ("character conversion failed for `%s'", "user input"); goto fail; } // Cut out the method name first char *p = input; while (*p && isspace_ascii (*p)) p++; // No input if (!*p) goto fail; char *method = p; while (*p && !isspace_ascii (*p)) p++; if (*p) *p++ = '\0'; // Now we go through this madness, just so that the order can be arbitrary json_error_t e; size_t args_len = 0; json_t *args[2] = { NULL, NULL }, *id = NULL, *params = NULL; while (true) { // Jansson is too stupid to just tell us that there was nothing while (*p && isspace_ascii (*p)) p++; if (!*p) break; if (args_len == N_ELEMENTS (args)) { print_error ("too many arguments"); goto fail_parse; } if (!(args[args_len] = json_loadb (p, len - (p - input), JSON_DECODE_ANY | JSON_DISABLE_EOF_CHECK, &e))) { print_error ("failed to parse JSON value: %s", e.text); goto fail_parse; } p += e.position; args_len++; } for (size_t i = 0; i < args_len; i++) { json_t **target; if (is_valid_json_rpc_id (args[i])) target = &id; else if (is_valid_json_rpc_params (args[i])) target = ¶ms; else { print_error ("unexpected value at index %zu", i); goto fail_parse; } if (*target) { print_error ("cannot specify multiple `id' or `params'"); goto fail_parse; } *target = json_incref (args[i]); } if (!id && ctx->auto_id) id = json_integer (ctx->next_id++); make_json_rpc_call (ctx, method, id, params); fail_parse: if (id) json_decref (id); if (params) json_decref (params); for (size_t i = 0; i < args_len; i++) json_decref (args[i]); fail: free (input); } static void on_winch (EV_P_ ev_signal *handle, int revents) { (void) loop; (void) handle; (void) revents; // This fucks up big time on terminals with automatic wrapping such as // rxvt-unicode or newer VTE when the current line overflows, however we // can't do much about that rl_resize_terminal (); } static void on_readline_input (char *line) { if (!line) { rl_callback_handler_remove (); ev_break (EV_DEFAULT_ EVBREAK_ONE); return; } if (*line) add_history (line); // Stupid readline forces us to use a global variable process_input (&g_ctx, line); free (line); } static void on_tty_readable (EV_P_ ev_io *handle, int revents) { (void) loop; (void) handle; if (revents & EV_READ) rl_callback_read_char (); } static void parse_program_arguments (struct app_context *ctx, int argc, char **argv, char **origin, char **endpoint) { static const struct opt opts[] = { { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 'a', "auto-id", NULL, 0, "automatic `id' fields" }, { 'o', "origin", "O", 0, "set the HTTP Origin header" }, { 'p', "pretty", NULL, 0, "pretty-print the responses" }, { 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" }, { 'v', "verbose", NULL, 0, "print the request before sending" }, { 'c', "color", "WHEN", OPT_LONG_ONLY, "colorize output: never, always, or auto" }, { 'w', "write-default-cfg", "FILENAME", OPT_OPTIONAL_ARG | OPT_LONG_ONLY, "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh; opt_handler_init (&oh, argc, argv, opts, "ENDPOINT", "Trivial JSON-RPC shell."); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { case 'd': g_debug_mode = true; break; case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 'o': *origin = optarg; break; case 'a': ctx->auto_id = true; break; case 'p': ctx->pretty_print = true; break; case 't': ctx->trust_all = true; break; case 'v': ctx->verbose = true; break; case 'c': if (!strcasecmp (optarg, "never")) ctx->color_mode = COLOR_NEVER; else if (!strcasecmp (optarg, "always")) ctx->color_mode = COLOR_ALWAYS; else if (!strcasecmp (optarg, "auto")) ctx->color_mode = COLOR_AUTO; else { print_error ("`%s' is not a valid value for `%s'", optarg, "color"); exit (EXIT_FAILURE); } break; case 'w': call_write_default_config (optarg, g_config_table); exit (EXIT_SUCCESS); default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } argc -= optind; argv += optind; if (argc != 1) { opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } *endpoint = argv[0]; opt_handler_free (&oh); } int main (int argc, char *argv[]) { str_map_init (&g_ctx.config); g_ctx.config.free = free; char *origin = NULL; char *endpoint = NULL; parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint); init_colors (&g_ctx); load_config (&g_ctx); if (strncmp (endpoint, "http://", 7) && strncmp (endpoint, "https://", 8)) exit_fatal ("the endpoint address must begin with" " either `http://' or `https://'"); CURL *curl; if (!(g_ctx.curl = curl = curl_easy_init ())) exit_fatal ("cURL initialization failed"); struct curl_slist *headers = NULL; headers = curl_slist_append (headers, "Content-Type: application/json"); if (origin) { origin = xstrdup_printf ("Origin: %s", origin); headers = curl_slist_append (headers, origin); } if (curl_easy_setopt (curl, CURLOPT_POST, 1L) || curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1L) || curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, g_ctx.curl_error) || curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headers) || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, g_ctx.trust_all ? 0L : 1L) || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST, g_ctx.trust_all ? 0L : 2L) || curl_easy_setopt (curl, CURLOPT_URL, endpoint)) exit_fatal ("cURL setup failed"); // We only need to convert to and from the terminal encoding setlocale (LC_CTYPE, ""); char *encoding = nl_langinfo (CODESET); #ifdef __linux__ // XXX: not quite sure if this is actually desirable // TODO: instead retry with JSON_ENSURE_ASCII encoding = xstrdup_printf ("%s//TRANSLIT", encoding); #endif // __linux__ if ((g_ctx.term_from_utf8 = iconv_open (encoding, "UTF-8")) == (iconv_t) -1 || (g_ctx.term_to_utf8 = iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1) exit_fatal ("creating the UTF-8 conversion object failed: %s", strerror (errno)); char *data_home = getenv ("XDG_DATA_HOME"), *home = getenv ("HOME"); if (!data_home || *data_home != '/') { if (!home) exit_fatal ("where is your $HOME, kid?"); data_home = xstrdup_printf ("%s/.local/share", home); } using_history (); stifle_history (HISTORY_LIMIT); char *history_path = xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home); (void) read_history (history_path); char *prompt; if (!get_attribute_printer (stdout)) prompt = xstrdup_printf ("json-rpc> "); else { // XXX: to be completely correct, we should use tputs, but we cannot const char *prompt_attrs = str_map_find (&g_ctx.config, ATTR_PROMPT); const char *reset_attrs = str_map_find (&g_ctx.config, ATTR_RESET); prompt = xstrdup_printf ("%c%s%cjson-rpc> %c%s%c", RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE, RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE); } // readline 6.3 doesn't immediately redraw the terminal upon reception // of SIGWINCH, so we must run it in an event loop to remediate that struct ev_loop *loop = EV_DEFAULT; if (!loop) exit_fatal ("libev initialization failed"); ev_signal winch_watcher; ev_io tty_watcher; ev_signal_init (&winch_watcher, on_winch, SIGWINCH); ev_signal_start (EV_DEFAULT_ &winch_watcher); ev_io_init (&tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ); ev_io_start (EV_DEFAULT_ &tty_watcher); rl_catch_sigwinch = false; rl_callback_handler_install (prompt, on_readline_input); ev_run (loop, 0); putchar ('\n'); ev_loop_destroy (loop); // User has terminated the program, let's save the history and clean up char *dir = xstrdup (history_path); (void) mkdir_with_parents (dirname (dir), NULL); free (dir); if (write_history (history_path)) print_error ("writing the history file `%s' failed: %s", history_path, strerror (errno)); free (history_path); iconv_close (g_ctx.term_from_utf8); iconv_close (g_ctx.term_to_utf8); curl_slist_free_all (headers); free (origin); curl_easy_cleanup (curl); str_map_free (&g_ctx.config); free_terminal (); return EXIT_SUCCESS; }