Hi, On Thu, Nov 16, 2017 at 12:59 PM, Luiz Augusto von Dentz <luiz.dentz@xxxxxxxxx> wrote: > From: Luiz Augusto von Dentz <luiz.von.dentz@xxxxxxxxx> > > This add initial bt_shell helper which can be used to create shell-like > command line tools. > --- > v3: Add submenu changes > > Makefile.tools | 3 +- > src/shared/shell.c | 570 +++++++++++++++++++++++++++++++++++++++++++++++++++++ > src/shared/shell.h | 67 +++++++ > 3 files changed, 639 insertions(+), 1 deletion(-) > create mode 100644 src/shared/shell.c > create mode 100644 src/shared/shell.h > > diff --git a/Makefile.tools b/Makefile.tools > index 561302fa1..dc2902cb7 100644 > --- a/Makefile.tools > +++ b/Makefile.tools > @@ -8,7 +8,8 @@ client_bluetoothctl_SOURCES = client/main.c \ > client/advertising.h \ > client/advertising.c \ > client/gatt.h client/gatt.c \ > - monitor/uuid.h monitor/uuid.c > + monitor/uuid.h monitor/uuid.c \ > + src/shared/shell.h src/shared/shell.c > client_bluetoothctl_LDADD = gdbus/libgdbus-internal.la src/libshared-glib.la \ > @GLIB_LIBS@ @DBUS_LIBS@ -lreadline > endif > diff --git a/src/shared/shell.c b/src/shared/shell.c > new file mode 100644 > index 000000000..7db629bf1 > --- /dev/null > +++ b/src/shared/shell.c > @@ -0,0 +1,570 @@ > +/* > + * > + * BlueZ - Bluetooth protocol stack for Linux > + * > + * Copyright (C) 2017 Intel Corporation. All rights reserved. > + * > + * > + * This library is free software; you can redistribute it and/or > + * modify it under the terms of the GNU Lesser General Public > + * License as published by the Free Software Foundation; either > + * version 2.1 of the License, or (at your option) any later version. > + * > + * This library 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 > + * Lesser General Public License for more details. > + * > + * You should have received a copy of the GNU Lesser General Public > + * License along with this library; if not, write to the Free Software > + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA > + * > + */ > + > +#ifdef HAVE_CONFIG_H > +#include <config.h> > +#endif > + > +#include <stdio.h> > +#include <errno.h> > +#include <unistd.h> > +#include <stdlib.h> > +#include <stdbool.h> > +#include <signal.h> > +#include <sys/signalfd.h> > + > +#include <readline/readline.h> > +#include <readline/history.h> > +#include <glib.h> > + > +#include "src/shared/io.h" > +#include "src/shared/util.h" > +#include "src/shared/queue.h" > +#include "src/shared/shell.h" > + > +#define CMD_LENGTH 48 > +#define print_text(color, fmt, args...) \ > + printf(color fmt COLOR_OFF "\n", ## args) > +#define print_menu(cmd, args, desc) \ > + printf(COLOR_HIGHLIGHT "%s %-*s " COLOR_OFF "%s\n", \ > + cmd, (int)(CMD_LENGTH - strlen(cmd)), args, desc) > + > +static GMainLoop *main_loop; > +static gboolean option_version = FALSE; > + > +static struct { > + struct io *input; > + > + bool saved_prompt; > + bt_shell_prompt_input_func saved_func; > + void *saved_user_data; > + > + const struct bt_shell_menu_entry *menu; > + /* TODO: Add submenus support */ > +} data; > + > +static void shell_print_menu(void); > + > +static void cmd_version(const char *arg) > +{ > + bt_shell_printf("Version %s\n", VERSION); > +} > + > +static void cmd_quit(const char *arg) > +{ > + g_main_loop_quit(main_loop); > +} > + > +static void cmd_help(const char *arg) > +{ > + shell_print_menu(); > +} > + > +static const struct bt_shell_menu_entry default_menu[] = { > + { "version", NULL, cmd_version, "Display version" }, > + { "quit", NULL, cmd_quit, "Quit program" }, > + { "exit", NULL, cmd_quit, "Quit program" }, > + { "help", NULL, cmd_help, > + "Display help about this program" }, > + { } > +}; > + > +static void shell_print_menu(void) > +{ > + const struct bt_shell_menu_entry *entry; > + > + if (!data.menu) > + return; > + > + print_text(COLOR_HIGHLIGHT, "Available commands:"); > + print_text(COLOR_HIGHLIGHT, "-------------------"); > + for (entry = data.menu; entry->cmd; entry++) { > + print_menu(entry->cmd, entry->arg ? : "", entry->desc ? : ""); > + } > + > + for (entry = default_menu; entry->cmd; entry++) { > + print_menu(entry->cmd, entry->arg ? : "", entry->desc ? : ""); > + } > +} > + > +static void shell_exec(const char *cmd, const char *arg) > +{ > + const struct bt_shell_menu_entry *entry; > + > + if (!data.menu || !cmd) > + return; > + > + for (entry = data.menu; entry->cmd; entry++) { > + if (strcmp(cmd, entry->cmd)) > + continue; > + > + if (entry->func) { > + entry->func(arg); > + return; > + } > + } > + > + for (entry = default_menu; entry->cmd; entry++) { > + if (strcmp(cmd, entry->cmd)) > + continue; > + > + if (entry->func) { > + entry->func(arg); > + return; > + } > + } > + > + print_text(COLOR_HIGHLIGHT, "Invalid command"); > +} > + > +void bt_shell_printf(const char *fmt, ...) > +{ > + va_list args; > + bool save_input; > + char *saved_line; > + int saved_point; > + > + save_input = !RL_ISSTATE(RL_STATE_DONE); > + > + if (save_input) { > + saved_point = rl_point; > + saved_line = rl_copy_text(0, rl_end); > + if (!data.saved_prompt) { > + rl_save_prompt(); > + rl_replace_line("", 0); > + rl_redisplay(); > + } > + } > + > + va_start(args, fmt); > + vprintf(fmt, args); > + va_end(args); > + > + if (save_input) { > + if (!data.saved_prompt) > + rl_restore_prompt(); > + rl_replace_line(saved_line, 0); > + rl_point = saved_point; > + rl_forced_update_display(); > + free(saved_line); > + } > +} > + > +void bt_shell_hexdump(const unsigned char *buf, size_t len) > +{ > + static const char hexdigits[] = "0123456789abcdef"; > + char str[68]; > + size_t i; > + > + if (!len) > + return; > + > + str[0] = ' '; > + > + for (i = 0; i < len; i++) { > + str[((i % 16) * 3) + 1] = ' '; > + str[((i % 16) * 3) + 2] = hexdigits[buf[i] >> 4]; > + str[((i % 16) * 3) + 3] = hexdigits[buf[i] & 0xf]; > + str[(i % 16) + 51] = isprint(buf[i]) ? buf[i] : '.'; > + > + if ((i + 1) % 16 == 0) { > + str[49] = ' '; > + str[50] = ' '; > + str[67] = '\0'; > + bt_shell_printf("%s\n", str); > + str[0] = ' '; > + } > + } > + > + if (i % 16 > 0) { > + size_t j; > + for (j = (i % 16); j < 16; j++) { > + str[(j * 3) + 1] = ' '; > + str[(j * 3) + 2] = ' '; > + str[(j * 3) + 3] = ' '; > + str[j + 51] = ' '; > + } > + str[49] = ' '; > + str[50] = ' '; > + str[67] = '\0'; > + bt_shell_printf("%s\n", str); > + } > +} > + > +void bt_shell_prompt_input(const char *label, const char *msg, > + bt_shell_prompt_input_func func, void *user_data) > +{ > + /* Normal use should not prompt for user input to the value a second > + * time before it releases the prompt, but we take a safe action. */ > + if (data.saved_prompt) > + return; > + > + rl_save_prompt(); > + rl_message(COLOR_RED "[%s]" COLOR_OFF " %s ", label, msg); > + > + data.saved_prompt = true; > + data.saved_func = func; > + data.saved_user_data = user_data; > +} > + > +int bt_shell_release_prompt(const char *input) > +{ > + bt_shell_prompt_input_func func; > + void *user_data; > + > + if (!data.saved_prompt) > + return -1; > + > + data.saved_prompt = false; > + > + rl_restore_prompt(); > + > + func = data.saved_func; > + user_data = data.saved_user_data; > + > + data.saved_func = NULL; > + data.saved_user_data = NULL; > + > + func(input, user_data); > + > + return 0; > +} > + > +static void rl_handler(char *input) > +{ > + char *cmd, *arg; > + > + if (!input) { > + rl_insert_text("quit"); > + rl_redisplay(); > + rl_crlf(); > + g_main_loop_quit(main_loop); > + return; > + } > + > + if (!strlen(input)) > + goto done; > + > + if (!bt_shell_release_prompt(input)) > + goto done; > + > + if (history_search(input, -1)) > + add_history(input); > + > + cmd = strtok_r(input, " ", &arg); > + if (!cmd) > + goto done; > + > + if (arg) { > + int len = strlen(arg); > + if (len > 0 && arg[len - 1] == ' ') > + arg[len - 1] = '\0'; > + } > + > + shell_exec(cmd, arg); > +done: > + free(input); > +} > + > +static char *cmd_generator(const char *text, int state) > +{ > + static const struct bt_shell_menu_entry *entry; > + static int index, len; > + const char *cmd; > + > + if (!state) { > + entry = default_menu; > + index = 0; > + len = strlen(text); > + } > + > + while ((cmd = entry[index].cmd)) { > + index++; > + > + if (!strncmp(cmd, text, len)) > + return strdup(cmd); > + } > + > + if (state) > + return NULL; > + > + entry = data.menu; > + index = 0; > + > + return cmd_generator(text, 1); > +} > + > +static char **menu_completion(const struct bt_shell_menu_entry *entry, > + const char *text, char *input_cmd) > +{ > + char **matches = NULL; > + > + for (entry = data.menu; entry->cmd; entry++) { > + if (strcmp(entry->cmd, input_cmd)) > + continue; > + > + if (!entry->gen) > + continue; > + > + rl_completion_display_matches_hook = entry->disp; > + matches = rl_completion_matches(text, entry->gen); > + break; > + } > + > + return matches; > +} > + > +static char **shell_completion(const char *text, int start, int end) > +{ > + char **matches = NULL; > + > + if (!data.menu) > + return NULL; > + > + if (start > 0) { > + char *input_cmd; > + > + input_cmd = strndup(rl_line_buffer, start - 1); > + matches = menu_completion(default_menu, text, input_cmd); > + if (!matches) > + matches = menu_completion(data.menu, text, > + input_cmd); > + > + free(input_cmd); > + } else { > + rl_completion_display_matches_hook = NULL; > + matches = rl_completion_matches(text, cmd_generator); > + } > + > + if (!matches) > + rl_attempted_completion_over = 1; > + > + return matches; > +} > + > +static bool io_hup(struct io *io, void *user_data) > +{ > + g_main_loop_quit(main_loop); > + > + return false; > +} > + > +static bool signal_read(struct io *io, void *user_data) > +{ > + static bool terminated = false; > + struct signalfd_siginfo si; > + ssize_t result; > + int fd; > + > + fd = io_get_fd(io); > + > + result = read(fd, &si, sizeof(si)); > + if (result != sizeof(si)) > + return false; > + > + switch (si.ssi_signo) { > + case SIGINT: > + if (data.input) { > + rl_replace_line("", 0); > + rl_crlf(); > + rl_on_new_line(); > + rl_redisplay(); > + break; > + } > + > + /* > + * If input was not yet setup up that means signal was received > + * while daemon was not yet running. Since user is not able > + * to terminate client by CTRL-D or typing exit treat this as > + * exit and fall through. > + */ > + > + /* fall through */ > + case SIGTERM: > + if (!terminated) { > + rl_replace_line("", 0); > + rl_crlf(); > + g_main_loop_quit(main_loop); > + } > + > + terminated = true; > + break; > + } > + > + return false; > +} > + > +static struct io *setup_signalfd(void) > +{ > + struct io *io; > + sigset_t mask; > + int fd; > + > + sigemptyset(&mask); > + sigaddset(&mask, SIGINT); > + sigaddset(&mask, SIGTERM); > + > + if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) { > + perror("Failed to set signal mask"); > + return 0; > + } > + > + fd = signalfd(-1, &mask, 0); > + if (fd < 0) { > + perror("Failed to create signal descriptor"); > + return 0; > + } > + > + io = io_new(fd); > + > + io_set_close_on_destroy(io, true); > + io_set_read_handler(io, signal_read, NULL, NULL); > + io_set_disconnect_handler(io, io_hup, NULL, NULL); > + > + return io; > +} > + > +static GOptionEntry options[] = { > + { "version", 'v', 0, G_OPTION_ARG_NONE, &option_version, > + "Show version information and exit" }, > + { NULL }, > +}; > + > +static void rl_init(void) > +{ > + setlinebuf(stdout); > + rl_attempted_completion_function = shell_completion; > + > + rl_erase_empty_line = 1; > + rl_callback_handler_install(NULL, rl_handler); > +} > + > +void bt_shell_init(int *argc, char ***argv) > +{ > + GOptionContext *context; > + GError *error = NULL; > + > + context = g_option_context_new(NULL); > + g_option_context_add_main_entries(context, options, NULL); > + > + if (g_option_context_parse(context, argc, argv, &error) == FALSE) { > + if (error != NULL) { > + g_printerr("%s\n", error->message); > + g_error_free(error); > + } else > + g_printerr("An unknown error occurred\n"); > + exit(1); > + } > + > + g_option_context_free(context); > + > + if (option_version == TRUE) { > + g_print("%s\n", VERSION); > + exit(EXIT_SUCCESS); > + } > + > + main_loop = g_main_loop_new(NULL, FALSE); > + > + rl_init(); > +} > + > +static void rl_cleanup(void) > +{ > + rl_message(""); > + rl_callback_handler_remove(); > +} > + > +void bt_shell_run(void) > +{ > + struct io *signal; > + > + signal = setup_signalfd(); > + > + g_main_loop_run(main_loop); > + > + bt_shell_release_prompt(""); > + bt_shell_detach(); > + > + io_destroy(signal); > + > + g_main_loop_unref(main_loop); > + main_loop = NULL; > + > + rl_cleanup(); > +} > + > +bool bt_shell_set_menu(const struct bt_shell_menu_entry *menu) > +{ > + if (data.menu || !menu) > + return false; > + > + data.menu = menu; > + > + return true; > +} > + > +void bt_shell_set_prompt(const char *string) > +{ > + if (!main_loop) > + return; > + > + rl_set_prompt(string); > + printf("\r"); > + rl_on_new_line(); > + rl_redisplay(); > +} > + > +static bool input_read(struct io *io, void *user_data) > +{ > + rl_callback_read_char(); > + return true; > +} > + > +bool bt_shell_attach(int fd) > +{ > + struct io *io; > + > + /* TODO: Allow more than one input? */ > + if (data.input) > + return false; > + > + io = io_new(fd); > + > + io_set_read_handler(io, input_read, NULL, NULL); > + io_set_disconnect_handler(io, io_hup, NULL, NULL); > + > + data.input = io; > + > + return true; > +} > + > +bool bt_shell_detach(void) > +{ > + if (!data.input) > + return false; > + > + io_destroy(data.input); > + data.input = NULL; > + > + return true; > +} > diff --git a/src/shared/shell.h b/src/shared/shell.h > new file mode 100644 > index 000000000..843335784 > --- /dev/null > +++ b/src/shared/shell.h > @@ -0,0 +1,67 @@ > +/* > + * > + * BlueZ - Bluetooth protocol stack for Linux > + * > + * Copyright (C) 2017 Intel Corporation. All rights reserved. > + * > + * > + * This library is free software; you can redistribute it and/or > + * modify it under the terms of the GNU Lesser General Public > + * License as published by the Free Software Foundation; either > + * version 2.1 of the License, or (at your option) any later version. > + * > + * This library 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 > + * Lesser General Public License for more details. > + * > + * You should have received a copy of the GNU Lesser General Public > + * License along with this library; if not, write to the Free Software > + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA > + * > + */ > + > +#define COLOR_OFF "\x1B[0m" > +#define COLOR_RED "\x1B[0;91m" > +#define COLOR_GREEN "\x1B[0;92m" > +#define COLOR_YELLOW "\x1B[0;93m" > +#define COLOR_BLUE "\x1B[0;94m" > +#define COLOR_BOLDGRAY "\x1B[1;30m" > +#define COLOR_BOLDWHITE "\x1B[1;37m" > +#define COLOR_HIGHLIGHT "\x1B[1;39m" > + > +typedef void (*bt_shell_menu_cb_t)(const char *arg); > +typedef char * (*bt_shell_menu_gen_t)(const char *text, int state); > +typedef void (*bt_shell_menu_disp_t) (char **matches, int num_matches, > + int max_length); > +typedef void (*bt_shell_prompt_input_func) (const char *input, void *user_data); > + > +struct bt_shell_menu_entry { > + const char *cmd; > + const char *arg; > + bt_shell_menu_cb_t func; > + const char *desc; > + bt_shell_menu_gen_t gen; > + bt_shell_menu_disp_t disp; > +}; > + > +void bt_shell_init(int *argc, char ***argv); > + > +void bt_shell_run(void); > + > +bool bt_shell_set_menu(const struct bt_shell_menu_entry *menu); > + > +void bt_shell_set_prompt(const char *string); > + > +void bt_shell_printf(const char *fmt, > + ...) __attribute__((format(printf, 1, 2))); > +void bt_shell_hexdump(const unsigned char *buf, size_t len); > + > +void bt_shell_prompt_input(const char *label, const char *msg, > + bt_shell_prompt_input_func func, void *user_data); > +int bt_shell_release_prompt(const char *input); > + > +bool bt_shell_attach(int fd); > +bool bt_shell_detach(void); > + > +void bt_shell_cleanup(void); > -- > 2.13.6 Applied. -- Luiz Augusto von Dentz -- To unsubscribe from this list: send the line "unsubscribe linux-bluetooth" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html