458 lines
10 KiB
C
458 lines
10 KiB
C
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
#include <arpa/inet.h>
|
|
#include <contrib/ccan/asprintf/asprintf.h>
|
|
#include <editline/readline.h>
|
|
#include <errno.h>
|
|
#include <histedit.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#define HISTORY_FILE "kresc_history"
|
|
#define PROGRAM_NAME "kresc"
|
|
|
|
FILE *g_tty = NULL; //!< connection to the daemon
|
|
|
|
static char *run_cmd(const char *cmd, size_t * out_len);
|
|
|
|
const char *prompt(EditLine * e)
|
|
{
|
|
return PROGRAM_NAME "> ";
|
|
}
|
|
|
|
bool starts_with(const char *a, const char *b)
|
|
{
|
|
if (strncmp(a, b, strlen(b)) == 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
//! Returns Lua name of type of value, NULL on error. Puts length of type in name_len;
|
|
char *get_type_name(const char *value)
|
|
{
|
|
if (value == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = 0; value[i]; i++) {
|
|
if (value[i] == ')') {
|
|
//Return NULL to prevent unexpected function call
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
char *cmd = afmt("type(%s)", value);
|
|
if (!cmd) {
|
|
perror("While tab-completing.");
|
|
return NULL;
|
|
}
|
|
|
|
size_t name_len;
|
|
char *type = run_cmd(cmd, &name_len);
|
|
if (!type) {
|
|
return NULL;
|
|
}
|
|
free(cmd);
|
|
|
|
if (starts_with(type, "[")) {
|
|
//Return "nil" on non-valid name.
|
|
free(type);
|
|
return strdup("nil");
|
|
} else {
|
|
type[strlen(type) - 1] = '\0';
|
|
return type;
|
|
}
|
|
}
|
|
|
|
static void complete_function(EditLine * el)
|
|
{
|
|
//Add left parenthesis to function name.
|
|
el_insertstr(el, "(");
|
|
}
|
|
|
|
static void complete_members(EditLine * el, const char *str,
|
|
const char *str_type, int str_len, char *dot)
|
|
{
|
|
char *table = strdup(str);
|
|
if (!table) {
|
|
perror("While tab-completing");
|
|
return;
|
|
}
|
|
//Get only the table name (without partial member name).
|
|
if (dot) {
|
|
*(table + (dot - str)) = '\0';
|
|
}
|
|
//Insert a dot after the table name.
|
|
if (!strncmp(str_type, "table", 5)) {
|
|
el_insertstr(el, ".");
|
|
str_len++;
|
|
}
|
|
//Check if the substring before dot is a valid table name.
|
|
const char *t_type = get_type_name(table);
|
|
if (t_type && !strncmp("table", t_type, 5)) {
|
|
//Get string of members of the table.
|
|
char *cmd =
|
|
afmt
|
|
("do local s=\"\"; for i in pairs(%s) do s=s..i..\"\\n\" end return(s) end",
|
|
table);
|
|
if (!cmd) {
|
|
perror("While tab-completing.");
|
|
goto complete_members_exit;
|
|
}
|
|
size_t members_len;
|
|
char *members = run_cmd(cmd, &members_len);
|
|
free(cmd);
|
|
if (!members) {
|
|
perror("While communication with daemon");
|
|
goto complete_members_exit;
|
|
}
|
|
//Split members by newline.
|
|
char *members_tok = strdup(members);
|
|
free(members);
|
|
if (!members_tok) {
|
|
goto complete_members_exit;
|
|
}
|
|
char *token = strtok(members_tok, "\n");
|
|
int matches = 0;
|
|
char *lastmatch = NULL;
|
|
if (!dot || dot - str + 1 == strlen(str)) {
|
|
//Prints all members.
|
|
while (token) {
|
|
char *member = afmt("%s.%s", table, token);
|
|
const char *member_type = get_type_name(member);
|
|
if (member && member_type) {
|
|
printf("\n%s (%s)", member, member_type);
|
|
free(member);
|
|
free((void *)member_type);
|
|
} else if (member) {
|
|
printf("\n%s", member);
|
|
free(member);
|
|
}
|
|
token = strtok(NULL, "\n");
|
|
matches++;
|
|
}
|
|
} else {
|
|
//Print members matching the current line.
|
|
while (token) {
|
|
if (starts_with(token, dot + 1)) {
|
|
const char *member_type =
|
|
get_type_name(afmt
|
|
("%s.%s", table,
|
|
token));
|
|
if (member_type) {
|
|
printf("\n%s.%s (%s)", table,
|
|
token, member_type);
|
|
free((void *)member_type);
|
|
} else {
|
|
printf("\n%s.%s", table, token);
|
|
}
|
|
lastmatch = token;
|
|
matches++;
|
|
}
|
|
token = strtok(NULL, "\n");
|
|
}
|
|
|
|
//Complete matching member.
|
|
if (matches == 1) {
|
|
el_deletestr(el, str_len);
|
|
el_insertstr(el, table);
|
|
el_insertstr(el, ".");
|
|
el_insertstr(el, lastmatch);
|
|
}
|
|
}
|
|
if (matches > 1) {
|
|
printf("\n");
|
|
}
|
|
free(members_tok);
|
|
}
|
|
|
|
complete_members_exit:
|
|
free(table);
|
|
if(t_type) {
|
|
free((void*)t_type);
|
|
}
|
|
}
|
|
|
|
static void complete_globals(EditLine * el, const char *str, int str_len)
|
|
{
|
|
//Parse Lua globals.
|
|
size_t globals_len;
|
|
char *globals = run_cmd("_G.__orig_name_list", &globals_len);
|
|
if (!globals) {
|
|
perror("While tab-completing");
|
|
return;
|
|
}
|
|
//Show possible globals.
|
|
char *globals_tok = strdup(globals);
|
|
free(globals);
|
|
if (!globals_tok) {
|
|
return;
|
|
}
|
|
char *token = strtok(globals_tok, "\n");
|
|
int matches = 0;
|
|
char *lastmatch = NULL;
|
|
while (token) {
|
|
if (str && starts_with(token, str)) {
|
|
char *name = get_type_name(token);
|
|
printf("\n%s (%s)", token, name);
|
|
free(name);
|
|
lastmatch = token;
|
|
matches++;
|
|
}
|
|
token = strtok(NULL, "\n");
|
|
}
|
|
if (matches > 1) {
|
|
printf("\n");
|
|
}
|
|
//Complete matching global.
|
|
if (matches == 1) {
|
|
el_deletestr(el, str_len);
|
|
el_insertstr(el, lastmatch);
|
|
}
|
|
free(globals_tok);
|
|
}
|
|
|
|
static unsigned char complete(EditLine * el, int ch)
|
|
{
|
|
int argc, pos;
|
|
const char **argv;
|
|
const LineInfo *li = el_line(el);
|
|
Tokenizer *tok = tok_init(NULL);
|
|
|
|
//Tokenize current line.
|
|
int ret = tok_line(tok, li, &argc, &argv, NULL, &pos);
|
|
|
|
if (ret != 0) {
|
|
perror("While tab-completing.");
|
|
goto complete_exit;
|
|
}
|
|
//Show help.
|
|
if (argc == 0) {
|
|
size_t help_len;
|
|
char *help = run_cmd("help()", &help_len);
|
|
if (help) {
|
|
printf("\n%s", help);
|
|
free(help);
|
|
} else {
|
|
perror("While communication with daemon");
|
|
}
|
|
goto complete_exit;
|
|
}
|
|
|
|
if (argc > 1) {
|
|
goto complete_exit;
|
|
}
|
|
//Get name of type of current line.
|
|
const char *type = get_type_name(argv[0]);
|
|
|
|
if (!type) {
|
|
goto complete_exit;
|
|
}
|
|
//Get position of last dot in current line (useful for parsing table).
|
|
char *dot = strrchr(argv[0], '.');
|
|
|
|
if (strncmp(type, "table", 5) != 0 && !dot) {
|
|
//Line is not a name of some table and there is no dot in it.
|
|
complete_globals(el, argv[0], pos);
|
|
} else if ((dot && strncmp(type, "nil", 3) == 0)
|
|
|| strncmp(type, "table", 5) == 0) {
|
|
//Current line (or part of it) is a name of some table.
|
|
complete_members(el, argv[0], type, pos, dot);
|
|
} else if (strncmp(type, "function", 8) == 0) {
|
|
//Current line is a function.
|
|
complete_function(el);
|
|
}
|
|
if (type) {
|
|
free((void *)type);
|
|
}
|
|
|
|
complete_exit:
|
|
tok_reset(tok);
|
|
tok_end(tok);
|
|
return CC_REDISPLAY;
|
|
}
|
|
|
|
//! Initialize connection to the daemon; return 0 on success.
|
|
static int init_tty(const char *path)
|
|
{
|
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd < 0)
|
|
return 1;
|
|
|
|
struct sockaddr_un addr;
|
|
addr.sun_family = AF_UNIX;
|
|
size_t plen = strlen(path);
|
|
if (plen + 1 > sizeof(addr.sun_path)) {
|
|
fprintf(stderr, "Path too long\n");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
memcpy(addr.sun_path, path, plen + 1);
|
|
if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr))) {
|
|
perror("While connecting to daemon");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
g_tty = fdopen(fd, "r+");
|
|
if (!g_tty) {
|
|
perror("While opening TTY");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
|
|
// Switch to binary mode and consume the text "> ".
|
|
if (fprintf(g_tty, "__binary\n") < 0 || !fread(&addr, 2, 1, g_tty)
|
|
|| fflush(g_tty)) {
|
|
perror("While initializing TTY");
|
|
fclose(g_tty);
|
|
g_tty = NULL;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//! Run a command on the daemon; return the answer or NULL on failure, puts answer length to out_len.
|
|
static char *run_cmd(const char *cmd, size_t * out_len)
|
|
{
|
|
if (!g_tty || !cmd)
|
|
return NULL;
|
|
if (fprintf(g_tty, "%s", cmd) < 0 || fflush(g_tty))
|
|
return NULL;
|
|
uint32_t len;
|
|
if (!fread(&len, sizeof(len), 1, g_tty))
|
|
return NULL;
|
|
len = ntohl(len);
|
|
if (!len)
|
|
return NULL;
|
|
char *msg = malloc(1 + (size_t) len);
|
|
if (!msg)
|
|
return NULL;
|
|
if (len && !fread(msg, len, 1, g_tty)) {
|
|
free(msg);
|
|
return NULL;
|
|
}
|
|
msg[len] = '\0';
|
|
*out_len = len;
|
|
return msg;
|
|
}
|
|
|
|
static int interact(void)
|
|
{
|
|
EditLine *el;
|
|
History *hist;
|
|
int count;
|
|
const char *line;
|
|
HistEvent ev;
|
|
el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
|
|
el_set(el, EL_PROMPT, prompt);
|
|
el_set(el, EL_EDITOR, "emacs");
|
|
el_set(el, EL_ADDFN, PROGRAM_NAME "-complete",
|
|
"Perform " PROGRAM_NAME " completion.", complete);
|
|
el_set(el, EL_BIND, "^I", PROGRAM_NAME "-complete", NULL);
|
|
|
|
hist = history_init();
|
|
if (hist == 0) {
|
|
perror("While initializing command history");
|
|
return 1;
|
|
}
|
|
history(hist, &ev, H_SETSIZE, 800);
|
|
el_set(el, EL_HIST, history, hist);
|
|
|
|
char *hist_file = NULL;
|
|
|
|
char *data_home = getenv("XDG_DATA_HOME");
|
|
|
|
//Check whether $XDG_DATA_HOME is set.
|
|
if (!data_home || *data_home == '\0') {
|
|
const char *home = getenv("HOME"); //This should be set on any POSIX compliant OS, even for nobody
|
|
|
|
//Create necessary folders.
|
|
char *dirs[3] =
|
|
{ afmt("%s/.local", home), afmt("%s/.local/share", home),
|
|
afmt("%s/.local/share/knot-resolver/", home)
|
|
};
|
|
bool ok = true;
|
|
for (int i = 0; i < 3; i++) {
|
|
if (mkdir(dirs[i], 0755) && errno != EEXIST) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
if (ok) {
|
|
hist_file =
|
|
afmt("%s/.local/share/knot-resolver/" HISTORY_FILE, home);
|
|
}
|
|
} else {
|
|
if (!mkdir(afmt("%s/knot-resolver/", data_home), 0755)
|
|
|| errno == EEXIST) {
|
|
hist_file = afmt("%s/knot-resolver/" HISTORY_FILE, data_home);
|
|
}
|
|
}
|
|
|
|
//Load history file
|
|
if (hist_file) {
|
|
history(hist, &ev, H_LOAD, hist_file);
|
|
} else {
|
|
perror("While opening history file");
|
|
}
|
|
|
|
while (1) {
|
|
line = el_gets(el, &count);
|
|
if (count > 0) {
|
|
history(hist, &ev, H_ENTER, line);
|
|
size_t msg_len;
|
|
char *msg = run_cmd(line, &msg_len);
|
|
if (!msg) {
|
|
perror("While communication with daemon");
|
|
history_end(hist);
|
|
el_end(el);
|
|
free(msg);
|
|
return 1;
|
|
}
|
|
printf("%s", msg);
|
|
if (msg_len > 0 && msg[msg_len - 1] != '\n') {
|
|
printf("\n");
|
|
}
|
|
if (hist_file) {
|
|
history(hist, &ev, H_SAVE, hist_file);
|
|
}
|
|
free(msg);
|
|
}
|
|
}
|
|
history_end(hist);
|
|
free(hist_file);
|
|
el_end(el);
|
|
if (feof(stdin))
|
|
return 0;
|
|
perror("While reading input");
|
|
return 1;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
fprintf(stderr, "Warning! %s is highly experimental, use at own risk.\n", argv[0]);
|
|
fprintf(stderr, "Please tell authors what features you expect from client utility.\n");
|
|
if (argc != 2) {
|
|
fprintf(stderr, "Usage: %s tty/xxxxx\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
int res = init_tty(argv[1]);
|
|
|
|
if (!res)
|
|
res = interact();
|
|
|
|
if (g_tty)
|
|
fclose(g_tty);
|
|
return res;
|
|
}
|