From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH v3 1/6] shared/shell: Add initial implementation
Date: Thu, 16 Nov 2017 12:59:20 +0200 [thread overview]
Message-ID: <20171116105925.2117-1-luiz.dentz@gmail.com> (raw)
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
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
next reply other threads:[~2017-11-16 10:59 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-11-16 10:59 Luiz Augusto von Dentz [this message]
2017-11-16 10:59 ` [PATCH v3 2/6] client: Make use of bt_shell Luiz Augusto von Dentz
2017-11-16 10:59 ` [PATCH v3 3/6] shared/shell: Add submenu support Luiz Augusto von Dentz
2017-11-16 10:59 ` [PATCH v3 4/6] client: Move advertise related commands to a submenu Luiz Augusto von Dentz
2017-11-16 10:59 ` [PATCH v3 5/6] client: Move scan " Luiz Augusto von Dentz
2017-11-16 10:59 ` [PATCH v3 6/6] client: Move gatt " Luiz Augusto von Dentz
2017-11-17 14:12 ` [PATCH v3 1/6] shared/shell: Add initial implementation Luiz Augusto von Dentz
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20171116105925.2117-1-luiz.dentz@gmail.com \
--to=luiz.dentz@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).