diff options
Diffstat (limited to 'src/utils/knotc/interactive.c')
-rw-r--r-- | src/utils/knotc/interactive.c | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/src/utils/knotc/interactive.c b/src/utils/knotc/interactive.c new file mode 100644 index 0000000..a03b8d3 --- /dev/null +++ b/src/utils/knotc/interactive.c @@ -0,0 +1,450 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <histedit.h> + +#include "knot/common/log.h" +#include "utils/common/lookup.h" +#include "utils/knotc/interactive.h" +#include "utils/knotc/commands.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/string.h" + +#define PROGRAM_NAME "knotc" +#define HISTORY_FILE ".knotc_history" + +extern params_t params; + +typedef struct { + const char **args; + int count; + bool dname; +} dup_check_ctx_t; + +static void cmds_lookup(EditLine *el, const char *str, size_t str_len) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + // Fill the lookup with command names. + for (const cmd_desc_t *desc = cmd_table; desc->name != NULL; desc++) { + ret = lookup_insert(&lookup, desc->name, NULL); + if (ret != KNOT_EOK) { + goto cmds_lookup_finish; + } + } + + (void)lookup_complete(&lookup, str, str_len, el, true); + +cmds_lookup_finish: + lookup_deinit(&lookup); +} + +static void remove_duplicates(lookup_t *lookup, dup_check_ctx_t *check_ctx) +{ + if (check_ctx == NULL) { + return; + } + + knot_dname_txt_storage_t dname = ""; + for (int i = 0; i < check_ctx->count; i++) { + const char *arg = (check_ctx->args)[i]; + size_t len = strlen(arg); + if (check_ctx->dname && len > 1 && arg[len - 1] != '.') { + strlcat(dname, arg, sizeof(dname)); + strlcat(dname, ".", sizeof(dname)); + arg = dname; + } + (void)lookup_remove(lookup, arg); + } +} + +static void local_zones_lookup(EditLine *el, const char *str, size_t str_len, + dup_check_ctx_t *check_ctx) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + knot_dname_txt_storage_t buff; + + // Fill the lookup with local zone names. + for (conf_iter_t iter = conf_iter(conf(), C_ZONE); + iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) { + conf_val_t val = conf_iter_id(conf(), &iter); + char *name = knot_dname_to_str(buff, conf_dname(&val), sizeof(buff)); + + ret = lookup_insert(&lookup, name, NULL); + if (ret != KNOT_EOK) { + conf_iter_finish(conf(), &iter); + goto local_zones_lookup_finish; + } + } + + remove_duplicates(&lookup, check_ctx); + (void)lookup_complete(&lookup, str, str_len, el, true); + +local_zones_lookup_finish: + lookup_deinit(&lookup); +} + +static void list_separators(EditLine *el, const char *separators) +{ + lookup_t lookup; + if (lookup_init(&lookup) != KNOT_EOK) { + return; + } + + size_t count = strlen(separators); + for (int i = 0; i < count; i++) { + char sep[2] = { separators[i] }; + (void)lookup_insert(&lookup, sep, NULL); + } + (void)lookup_complete(&lookup, "", 0, el, false); + + lookup_deinit(&lookup); +} + +static bool rmt_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const char *item, const char *id, + dup_check_ctx_t *check_ctx, bool add_space, const char *flags, + knot_ctl_idx_t idx) +{ + const cmd_desc_t *desc = cmd_table; + while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) { + desc++; + } + assert(desc->name != NULL); + + knot_ctl_data_t query = { + [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd), + [KNOT_CTL_IDX_SECTION] = section, + [KNOT_CTL_IDX_ITEM] = item, + [KNOT_CTL_IDX_ID] = id, + [KNOT_CTL_IDX_FLAGS] = flags + }; + + lookup_t lookup; + knot_ctl_t *ctl = NULL; + bool found = false; + + if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK || + knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK || + knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK || + lookup_init(&lookup) != KNOT_EOK) { + unset_ctl(ctl); + return found; + } + + while (true) { + knot_ctl_type_t type; + knot_ctl_data_t reply; + + if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) { + goto rmt_lookup_finish; + } + + if (type != KNOT_CTL_TYPE_DATA && type != KNOT_CTL_TYPE_EXTRA) { + break; + } + + const char *error = reply[KNOT_CTL_IDX_ERROR]; + if (error != NULL) { + printf("\nnotice: (%s)\n", error); + goto rmt_lookup_finish; + } + + // Insert the received name into the lookup. + if (lookup_insert(&lookup, reply[idx], NULL) != KNOT_EOK) { + goto rmt_lookup_finish; + } + } + + remove_duplicates(&lookup, check_ctx); + if (lookup_complete(&lookup, str, str_len, el, add_space) == KNOT_EOK && + str != NULL && strcmp(lookup.found.key, str) == 0) { + found = true; + } + +rmt_lookup_finish: + lookup_deinit(&lookup); + unset_ctl(ctl); + + return found; +} + +static bool id_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const cmd_desc_t *cmd_desc, + dup_check_ctx_t *ctx, bool add_space, bool zones) +{ + char flags[4] = ""; + if (zones) { + strlcat(flags, CTL_FLAG_LIST_ZONES, sizeof(flags)); + } else if (cmd_desc->flags & CMD_FREQ_TXN) { + strlcat(flags, CTL_FLAG_LIST_TXN, sizeof(flags)); + } + + return rmt_lookup(el, str, str_len, section, NULL, NULL, ctx, add_space, + flags, KNOT_CTL_IDX_ID); +} + +static void val_lookup(EditLine *el, const char *str, size_t str_len, + const char *section, const char *item, const char *id, + dup_check_ctx_t *ctx, bool list_schema) +{ + char flags[4] = CTL_FLAG_LIST_TXN; + if (list_schema) { + strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags)); + } + + (void)rmt_lookup(el, str, str_len, section, item, id, ctx, true, + flags, KNOT_CTL_IDX_DATA); +} + +static bool list_lookup(EditLine *el, const char *str, const char *section) +{ + const char *flags = CTL_FLAG_LIST_SCHEMA; + knot_ctl_idx_t idx = (section == NULL) ? KNOT_CTL_IDX_SECTION : KNOT_CTL_IDX_ITEM; + + return rmt_lookup(el, str, strlen(str), section, NULL, NULL, NULL, + section != NULL, flags, idx); +} + +static void item_lookup(EditLine *el, const char *str, const cmd_desc_t *cmd_desc) +{ + // List all sections. + if (str == NULL) { + (void)list_lookup(el, "", NULL); + return; + } + + // Check for id specification. + char *id = (strchr(str, '[')); + if (id != NULL) { + char *section = strndup(str, id - str); + + // Check for completed id specification. + char *id_stop = (strchr(id, ']')); + if (id_stop != NULL) { + // Complete the item name. + if (*(id_stop + 1) == '.') { + (void)list_lookup(el, id_stop + 2, section); + } else { + list_separators(el, "."); + } + } else { + // Complete the section id. + if (id_lookup(el, id + 1, strlen(id + 1), section, cmd_desc, + NULL, false, false)) { + list_separators(el, "]"); + } + } + + free(section); + } else { + // Check for item specification. + char *dot = (strchr(str, '.')); + if (dot != NULL) { + // Complete the item name. + char *section = strndup(str, dot - str); + (void)list_lookup(el, dot + 1, section); + free(section); + } else { + // Complete the section name. + if (list_lookup(el, str, NULL)) { + list_separators(el, "[."); + } + } + } +} + +static unsigned char complete(EditLine *el, int ch) +{ + int argc, token, pos; + const char **argv; + + const LineInfo *li = el_line(el); + Tokenizer *tok = tok_init(NULL); + + // Parse the line. + int ret = tok_line(tok, li, &argc, &argv, &token, &pos); + if (ret != 0) { + goto complete_exit; + } + + // Show possible commands. + if (argc == 0) { + print_commands(); + goto complete_exit; + } + + // Complete the command name. + if (token == 0) { + cmds_lookup(el, argv[0], pos); + goto complete_exit; + } + + // Find the command descriptor. + const cmd_desc_t *desc = cmd_table; + while (desc->name != NULL && strcmp(desc->name, argv[0]) != 0) { + desc++; + } + if (desc->name == NULL) { + goto complete_exit; + } + + // Finish if a command with no or unsupported arguments. + if (desc->flags == CMD_FNONE || desc->flags == CMD_FREAD || + desc->flags == CMD_FWRITE) { + goto complete_exit; + } + + ret = set_config(desc, ¶ms); + if (ret != KNOT_EOK) { + goto complete_exit; + } + + // Complete the zone name. + if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) { + if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) { + goto complete_exit; + } + + dup_check_ctx_t ctx = { &argv[1], token - 1, true }; + if (desc->flags & CMD_FREAD) { + local_zones_lookup(el, argv[token], pos, &ctx); + } else { + id_lookup(el, argv[token], pos, "zone", desc, &ctx, true, true); + } + goto complete_exit; + // Complete the section/id/item name or item value. + } else if (desc->flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) { + if (token == 1) { + item_lookup(el, argv[1], desc); + } else if (desc->flags & CMD_FOPT_DATA) { + char section[YP_MAX_TXT_KEY_LEN + 1] = ""; + char item[YP_MAX_TXT_KEY_LEN + 1] = ""; + char id[KNOT_DNAME_TXT_MAXLEN + 1] = ""; + + assert(YP_MAX_TXT_KEY_LEN == 127); + assert(KNOT_DNAME_TXT_MAXLEN == 1004); + if (sscanf(argv[1], "%127[^[][%1004[^]]].%127s", section, id, item) == 3 || + sscanf(argv[1], "%127[^.].%127s", section, item) == 2) { + dup_check_ctx_t ctx = { &argv[2], token - 2 }; + val_lookup(el, argv[token], pos, section, item, id, + &ctx, desc->flags & CMD_FLIST_SCHEMA); + } + } + goto complete_exit; + } + +complete_exit: + conf_update(NULL, CONF_UPD_FNONE); + tok_reset(tok); + tok_end(tok); + + return CC_REDISPLAY; +} + +static char *prompt(EditLine *el) +{ + return PROGRAM_NAME"> "; +} + +int interactive_loop(params_t *process_params) +{ + char *hist_file = NULL; + const char *home = getenv("HOME"); + if (home != NULL) { + hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE); + } + if (hist_file == NULL) { + log_notice("failed to get home directory"); + } + + EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr); + if (el == NULL) { + log_error("interactive mode not available"); + free(hist_file); + return KNOT_ERROR; + } + + History *hist = history_init(); + if (hist == NULL) { + log_error("interactive mode not available"); + el_end(el); + free(hist_file); + return KNOT_ERROR; + } + + HistEvent hev = { 0 }; + history(hist, &hev, H_SETSIZE, 1000); + history(hist, &hev, H_SETUNIQUE, 1); + el_set(el, EL_HIST, history, hist); + history(hist, &hev, H_LOAD, hist_file); + + el_set(el, EL_TERMINAL, NULL); + el_set(el, EL_EDITOR, "emacs"); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_SIGNAL, 1); + el_source(el, NULL); + + // Warning: these two el_sets()'s always leak -- in libedit2 library! + // For more details see this commit's message. + el_set(el, EL_ADDFN, PROGRAM_NAME"-complete", + "Perform "PROGRAM_NAME" completion.", complete); + el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL); + + int count; + const char *line; + while ((line = el_gets(el, &count)) != NULL && count > 0) { + Tokenizer *tok = tok_init(NULL); + + // Tokenize the current line. + int argc; + const char **argv; + const LineInfo *li = el_line(el); + int ret = tok_line(tok, li, &argc, &argv, NULL, NULL); + if (ret == 0 && argc != 0) { + history(hist, &hev, H_ENTER, line); + history(hist, &hev, H_SAVE, hist_file); + + // Process the command. + ret = process_cmd(argc, argv, process_params); + } + + tok_reset(tok); + tok_end(tok); + + // Check for the exit command. + if (ret == KNOT_CTL_ESTOP) { + break; + } + } + + history_end(hist); + free(hist_file); + + el_end(el); + + return KNOT_EOK; +} |