/* SPDX-License-Identifier: GPL-2.0 */ /** \file client.c The client program used to connect to para_server. * * The bulk of the code of this file is the completers because interactive * sessions are implemented in \ref interactive.c and the authentication * handshake (shared with para_audiod(1)) is implemented in \ref * client_common.c. This file contains no non-static functions except main(). */ /** \cond doxygen_ignore */ #include #include #include "client.lsg.h" #include "para.h" #include "list.h" #include "sched.h" #include "crypt.h" #include "string.h" #include "stdin.h" #include "stdout.h" #include "client.h" #include "buffer_tree.h" #include "fd.h" #include "error.h" DEFINE_PARA_ERRLIST; static struct sched *sched; static struct client_task *ct; static struct stdin_task *sit; static struct stdout_task *sot; static int client_loglevel = LL_ERROR; DEFINE_STDERR_LOGGER(stderr_log, client_loglevel); __printf_2_3 void (*para_log)(int, const char*, ...) = stderr_log; #ifdef HAVE_READLINE #include "interactive.h" #include "server_cmd.lsg.h" struct exec_task { struct task *task; struct btr_node *btrn; char *result_buf; size_t result_size; }; static void exec_pre_monitor(struct sched *s, void *context) { struct exec_task *et = context; int ret = btr_node_status(et->btrn, 0, BTR_NT_LEAF); if (ret != 0) sched_min_delay(s); } static int exec_post_monitor(__a_unused struct sched *s, void *context) { struct exec_task *et = context; struct btr_node *btrn = et->btrn; char *buf; size_t sz; int ret; ret = btr_node_status(btrn, 0, BTR_NT_LEAF); if (ret <= 0) return ret; sz = btr_next_buffer(btrn, &buf); if (sz <= 1) goto out; et->result_buf = para_realloc(et->result_buf, et->result_size + sz - 1); memcpy(et->result_buf + et->result_size - 1, buf, sz - 1); et->result_size += sz - 1; et->result_buf[et->result_size - 1] = '\0'; out: btr_consume(btrn, sz); return 0; } /* Called from the line handler and the completers. This overwrites ct->lpr. */ static int create_merged_lpr(const char *line) { const struct lls_command *cmd = CLIENT_CMD_PTR; int argc, ret; char *cmdline, **argv, *errctx; struct lls_parse_result *argv_lpr; static struct lls_parse_result *orig_lpr; if (!orig_lpr) orig_lpr = ct->lpr; ct->lpr = NULL; cmdline = make_message("-- %s", line); ret = create_shifted_argv(cmdline, " ", &argv); free(cmdline); if (ret < 0) return ret; argc = ret; if (argc == 2) { /* no words (only blanks in line) */ free_argv(argv); return 0; } argv[0] = para_strdup("--"); /* * The original lpr for the interactive session has no non-option * arguments. We create a fresh lpr from the words in "line" and merge * it with the original lpr. */ ret = lls(lls_parse(argc, argv, cmd, &argv_lpr, &errctx)); free_argv(argv); if (ret < 0) goto fail; ret = lls(lls_merge(orig_lpr, argv_lpr, cmd, &ct->lpr, &errctx)); lls_free_parse_result(argv_lpr, cmd); if (ret < 0) goto fail; return 1; fail: if (errctx) PARA_ERROR_LOG("%s\n", para_strerror(-ret)); free(errctx); assert(ret < 0); return ret; } /* called from completers */ static int execute_client_command(const char *cmd, char **result) { int ret; struct sched *command_sched = sched_new(NULL); struct exec_task exec_task = { .result_buf = para_strdup(""), .result_size = 1, }; struct lls_parse_result *old_lpr = ct->lpr; *result = NULL; ret = create_merged_lpr(cmd); if (ret <= 0) goto out; exec_task.btrn = btr_new_node(&(struct btr_node_description) EMBRACE(.name = "exec_collect")); exec_task.task = task_register(&(struct task_info) { .name = "client exec", .pre_monitor = exec_pre_monitor, .post_monitor = exec_post_monitor, .context = &exec_task, }, command_sched); ret = client_connect(ct, command_sched, NULL, exec_task.btrn); if (ret < 0) goto out; schedule(command_sched); sched_shutdown(command_sched); lls_free_parse_result(ct->lpr, CLIENT_CMD_PTR); ct->lpr = old_lpr; *result = exec_task.result_buf; btr_remove_node(&exec_task.btrn); ret = 1; out: btr_remove_node(&exec_task.btrn); if (ret < 0) free(exec_task.result_buf); return ret; } static int extract_matches_from_command_pfx(const char *word, char *cmd, const char *pfx, char ***matches) { char *buf, **argv; int argc, ret; ret = execute_client_command(cmd, &buf); if (ret < 0) return ret; ret = create_argv(buf, "\n", &argv); free(buf); if (ret < 0) return ret; argc = ret; if (pfx) { char **nargv = arr_alloc(argc + 1, sizeof(char *)); for (unsigned n = 0; n < argc; n++) nargv[n] = make_message("%s%s", pfx, argv[n]); nargv[argc] = NULL; free_argv(argv); argv = nargv; } ret = i9e_extract_completions(word, argv, matches); free_argv(argv); return ret; } static int extract_matches_from_command(const char *word, char *cmd, char ***matches) { return extract_matches_from_command_pfx(word, cmd, NULL, matches); } static int complete_attributes(const char *word, char ***matches) { return extract_matches_from_command(word, "lsatt", matches); } static void complete_addblob(__a_unused const char *blob_type, __a_unused struct i9e_completion_info *ci, __a_unused struct i9e_completion_result *cr) { cr->filename_completion_desired = true; } static void generic_blob_complete(const char *blob_type, struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char cmd[20]; sprintf(cmd, "ls%s", blob_type); extract_matches_from_command(ci->word, cmd, &cr->matches); } static void complete_catblob(const char *blob_type, struct i9e_completion_info *ci, struct i9e_completion_result *cr) { generic_blob_complete(blob_type, ci, cr); } static void complete_lsblob(const char *blob_type, struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_LSIMG_OPTS, NULL}; if (ci->word[0] == '-') return i9e_complete_option(opts, ci, cr); generic_blob_complete(blob_type, ci, cr); } static void complete_rmblob(const char *blob_type, struct i9e_completion_info *ci, struct i9e_completion_result *cr) { generic_blob_complete(blob_type, ci, cr); } static void complete_mvblob(const char *blob_type, struct i9e_completion_info *ci, struct i9e_completion_result *cr) { unsigned num = i9e_get_nonopt_argnum(NULL, ci); if (num == 1 || num == 2) generic_blob_complete(blob_type, ci, cr); } /* these don't need any completions */ I9E_DUMMY_COMPLETER(ff); I9E_DUMMY_COMPLETER(hup); I9E_DUMMY_COMPLETER(jmp); I9E_DUMMY_COMPLETER(next); I9E_DUMMY_COMPLETER(nomore); I9E_DUMMY_COMPLETER(pause); I9E_DUMMY_COMPLETER(play); I9E_DUMMY_COMPLETER(si); I9E_DUMMY_COMPLETER(term); I9E_DUMMY_COMPLETER(stop); I9E_DUMMY_COMPLETER(addatt); static const struct i9e_completer completers[]; static void ll_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { i9e_ll_completer(ci, cr); } static void help_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_HELP_OPTS, NULL}; if (ci->word[0] == '-') { i9e_complete_option(opts, ci, cr); return; } if (i9e_get_nonopt_argnum(opts, ci) == 1) /* arg #0 is "help" */ cr->matches = i9e_complete_commands(ci->word, completers); } static void stat_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_STAT_OPTS, NULL}; i9e_complete_option(opts, ci, cr); } static void version_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_VERSION_OPTS, NULL}; i9e_complete_option(opts, ci, cr); } static void sender_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char *senders[] = {"http", "udp", NULL}; char *http_cmds[] = {"on", "off", "allow", "deny", "help", NULL}; char *udp_cmds[] ={"on", "off", "add", "delete", "help", NULL}; char *sender; char **cmds; //PARA_CRIT_LOG("wn: %d\n", ci->word_num); if (ci->word_num == 0 || ci->word_num > 3) return; if (ci->word_num == 1 || (ci->word_num == 2 && *ci->word != '\0')) { i9e_extract_completions(ci->word, senders, &cr->matches); return; } sender = ci->argv[1]; //PARA_CRIT_LOG("sender: %s\n", sender); if (strcmp(sender, "http") == 0) cmds = http_cmds; else if (strcmp(sender, "udp") == 0) cmds = udp_cmds; else return; i9e_extract_completions(ci->word, cmds, &cr->matches); } static void add_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_ADD_OPTS, NULL}; if (ci->word[0] == '-') i9e_complete_option(opts, ci, cr); cr->filename_completion_desired = true; } static void complete_mops(const char *word, const char *pfx, char ***matches) { char *mood_buf, *pl_buf, **moods, **playlists; char **mops; int num_moods, num_pl, i, n, ret; ret = execute_client_command("lsmood", &mood_buf); if (ret < 0) return; ret = execute_client_command("lspl", &pl_buf); if (ret < 0) goto free_mood_buf; ret = create_argv(mood_buf, "\n", &moods); if (ret < 0) goto free_pl_buf; num_moods = ret; ret = create_argv(pl_buf, "\n", &playlists); if (ret < 0) goto free_moods; num_pl = ret; n = num_moods + num_pl; mops = arr_alloc(n + 1, sizeof(char *)); for (i = 0; i < num_moods; i++) mops[i] = make_message("%sm/%s", pfx, moods[i]); for (i = 0; i < num_pl; i++) mops[num_moods + i] = make_message("%sp/%s", pfx, playlists[i]); mops[n] = NULL; i9e_extract_completions(word, mops, matches); free_argv(mops); free_argv(playlists); free_moods: free_argv(moods); free_pl_buf: free(pl_buf); free_mood_buf: free(mood_buf); } static void ls_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_LS_OPTS, NULL}; int num = i9e_cword_is_option_arg(opts, ci); if (num >= 0) { const char *opt = opts[num]; if (!strcmp(opt, "-a=") || !strcmp(opt, "--admissible=")) return complete_mops(ci->word, opt, &cr->matches); if (!strcmp(opt, "-l=") || !strcmp(opt, "--listing-mode=")) { cr->matches = arr_alloc(5, sizeof(char *)); cr->matches[0] = make_message("%ss", opt); cr->matches[1] = make_message("%sl", opt); cr->matches[2] = make_message("%sv", opt); cr->matches[3] = make_message("%sp", opt); cr->matches[4] = NULL; return; } if (!strcmp(opt, "-s=") || !strcmp(opt, "--sort=")) { cr->matches = arr_alloc(11, sizeof(char *)); cr->matches[0] = make_message("%sl", opt); cr->matches[1] = make_message("%sn", opt); cr->matches[2] = make_message("%sf", opt); cr->matches[3] = make_message("%sc", opt); cr->matches[4] = make_message("%si", opt); cr->matches[5] = make_message("%sy", opt); cr->matches[6] = make_message("%sb", opt); cr->matches[7] = make_message("%sd", opt); cr->matches[8] = make_message("%sa", opt); cr->matches[9] = make_message("%sh", opt); cr->matches[10] = NULL; return; } } if (ci->word[0] == '-') i9e_complete_option(opts, ci, cr); cr->filename_completion_desired = true; } static void lsatt_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_LSATT_OPTS, NULL}; i9e_complete_option(opts, ci, cr); } static void mvatt_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { unsigned num = i9e_get_nonopt_argnum(NULL, ci); if (num == 1 || num == 2) complete_attributes(ci->word, &cr->matches); } static void rmatt_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { complete_attributes(ci->word, &cr->matches); } static void check_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_CHECK_OPTS, NULL}; i9e_complete_option(opts, ci, cr); } static void rm_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_RM_OPTS, NULL}; if (ci->word[0] == '-') { i9e_complete_option(opts, ci, cr); return; } cr->filename_completion_desired = true; } static void touch_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_TOUCH_OPTS, NULL}; int num = i9e_cword_is_option_arg(opts, ci); const char *opt; if (num < 0) { /* no option arg, is it an option? */ if (ci->word[0] == '-') return i9e_complete_option(opts, ci, cr); cr->filename_completion_desired = true; return; } opt = opts[num]; /* only attribute option args can be completed */ if (strcmp(opt, "-s=") && strcmp(opt, "--set-attribute=") && strcmp(opt, "-u=") && strcmp(opt, "--unset-attribute=")) return; if (strncmp(opt, ci->word, strlen(opt))) { /* complete on unprefixed attributes (-s att syntax) */ extract_matches_from_command(ci->word, "lsatt", &cr->matches); return; } /* complete on prefixed attributes (-s=att syntax) */ extract_matches_from_command_pfx(ci->word, "lsatt", opt, &cr->matches); return; } static void cpsi_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_CPSI_OPTS, NULL}; if (ci->word[0] == '-') i9e_complete_option(opts, ci, cr); cr->filename_completion_desired = true; } static void select_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_SERVER_CMD_SELECT_OPTS, NULL}; if (ci->word[0] == '-') return i9e_complete_option(opts, ci, cr); if (i9e_get_nonopt_argnum(opts, ci) == 1) /* arg #0 is "select" */ complete_mops(ci->word, "", &cr->matches); } #define DEFINE_BLOB_COMPLETER(cmd, blob_type) \ static void cmd ## blob_type ## _completer( \ struct i9e_completion_info *ci, \ struct i9e_completion_result *cr) \ {complete_ ## cmd ## blob(#blob_type, ci, cr);} DEFINE_BLOB_COMPLETER(add, mood) DEFINE_BLOB_COMPLETER(add, lyr) DEFINE_BLOB_COMPLETER(add, img) DEFINE_BLOB_COMPLETER(add, pl) DEFINE_BLOB_COMPLETER(cat, mood) DEFINE_BLOB_COMPLETER(cat, lyr) DEFINE_BLOB_COMPLETER(cat, img) DEFINE_BLOB_COMPLETER(cat, pl) DEFINE_BLOB_COMPLETER(ls, mood) DEFINE_BLOB_COMPLETER(ls, lyr) DEFINE_BLOB_COMPLETER(ls, img) DEFINE_BLOB_COMPLETER(ls, pl) DEFINE_BLOB_COMPLETER(rm, mood) DEFINE_BLOB_COMPLETER(rm, lyr) DEFINE_BLOB_COMPLETER(rm, img) DEFINE_BLOB_COMPLETER(rm, pl) DEFINE_BLOB_COMPLETER(mv, mood) DEFINE_BLOB_COMPLETER(mv, lyr) DEFINE_BLOB_COMPLETER(mv, img) DEFINE_BLOB_COMPLETER(mv, pl) static int client_i9e_line_handler(char *line) { int ret; static bool first = true; if (first) first = false; else lls_free_parse_result(ct->lpr, CLIENT_CMD_PTR); ret = create_merged_lpr(line); if (ret <= 0) return ret; ret = client_connect(ct, sched, NULL, NULL); if (ret < 0) return ret; i9e_attach_to_stdout(ct->btrn[0]); return 1; } static const struct i9e_completer completers[] = { #define LSG_SERVER_CMD_CMD(_name) {.name = #_name, \ .completer = _name ## _completer} LSG_SERVER_CMD_SUBCOMMANDS #undef LSG_SERVER_CMD_CMD {.name = NULL} }; __noreturn static void interactive_session(void) { int ret; struct sigaction act; struct i9e_client_info ici = { .fds = {0, 1, 2}, .prompt = "para_client> ", .line_handler = client_i9e_line_handler, .loglevel = client_loglevel, .completers = completers, }; PARA_NOTICE_LOG("\n%s\n", version_text("client")); if (CLIENT_OPT_GIVEN(HISTORY_FILE, ct->lpr)) ici.history_file = para_strdup(CLIENT_OPT_STRING_VAL( HISTORY_FILE, ct->lpr)); else { char *dot_para = create_dot_paraslash(); ici.history_file = make_message("%s/client.history", dot_para); free(dot_para); } act.sa_handler = i9e_signal_dispatch; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, NULL); sched = sched_new(i9e_poll); ret = i9e_open(&ici, sched); if (ret < 0) goto out; para_log = i9e_log; ret = schedule(sched); sched_shutdown(sched); i9e_close(); para_log = stderr_log; out: free(ici.history_file); if (ret < 0) PARA_ERROR_LOG("%s\n", para_strerror(-ret)); exit(ret < 0? EXIT_FAILURE : EXIT_SUCCESS); //client_close(ct); } __noreturn static void print_completions(void) { int ret; ret = i9e_print_completions(completers); exit(ret <= 0? EXIT_FAILURE : EXIT_SUCCESS); } #else /* HAVE_READLINE */ __noreturn static void interactive_session(void) { PARA_EMERG_LOG("interactive sessions not available\n"); exit(EXIT_FAILURE); } __noreturn static void print_completions(void) { PARA_EMERG_LOG("command completion not available\n"); exit(EXIT_FAILURE); } #endif /* HAVE_READLINE */ struct supervisor_task { bool stdout_task_started; struct task *task; }; static int supervisor_post_monitor(struct sched *s, void *context) { struct supervisor_task *svt = context; int ret = task_status(ct->task); if (ret < 0) return ret; if (!svt->stdout_task_started && ct->status == CL_EXECUTING) { stdout_register(sot, s); svt->stdout_task_started = true; return 1; } if (ct->status == CL_SENDING) { stdin_register(sit, s); return -E_TASK_STARTED; } return 0; } static struct supervisor_task supervisor_task; /** \endcond * * The client program to connect to para_server. * * \param argc Options are defined in the client lopsub suite. * \param argv The client suite defines no subcommands. * * If --complete is specified, possible completions are determined by executing * a second instance of para_client(1). For example, if attribute completion * is needed, the lsatt server subcommand is run. * * If run with no arguments, an interactive session is started. In this * case we call \ref i9e_open() of \ref interactive.c, which implements * interactive sessions, including command line editing and tab completion. * * Otherwise, the client task and the supervisor task are started. The former * communicates with para_server while the latter monitors whether the client * task intends to read from stdin or write to stdout. * * Once it has been determined whether the client command corresponds to a * stdin command (addmood, addimg, ..), either the stdin task or the stdout * task is set up to replace the supervisor task. * * \return EXIT_SUCCESS or EXIT_FAILURE * * \sa \ref client_open(), \ref stdin.c, \ref stdout.c, para_client(1), * para_server(1). */ int main(int argc, char *argv[]) { int ret; crypt_init(); ret = client_parse_config(argc, argv, &ct); if (ret < 0) goto out; client_loglevel = CLIENT_OPT_UINT32_VAL(LOGLEVEL, ct->lpr); if (CLIENT_OPT_GIVEN(COMPLETE, ct->lpr)) print_completions(); if (ret == 0) interactive_session(); /* does not return */ sched = sched_new(NULL); /* * We add buffer tree nodes for stdin and stdout even though * only one of them will be needed. This simplifies the code * a bit wrt. to the buffer tree setup. */ sit = stdin_new(); ret = client_connect(ct, sched, stdin_btrn(sit), NULL); if (ret < 0) goto free_sit; sot = stdout_new(ct->btrn[0]); supervisor_task.task = task_register(&(struct task_info) { .name = "supervisor", .post_monitor = supervisor_post_monitor, .context = &supervisor_task, }, sched); ret = schedule(sched); if (ret >= 0) { ret = task_status(ct->task); if (ret < 0) { switch (ret) { /* these are not errors */ case -E_SERVER_CMD_SUCCESS: case -E_EOF: ret = 0; break; default: ret = -E_SERVER_CMD_FAILURE; } } } sched_shutdown(sched); crypt_shutdown(); stdout_free(sot); free_sit: stdin_free(sit); out: if (ret < 0) PARA_ERROR_LOG("%s\n", para_strerror(-ret)); client_close(ct); return ret < 0? EXIT_FAILURE : EXIT_SUCCESS; }