[PATCH BlueZ 1/2] shared/shell: Add initial implementation

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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.
---
 Makefile.tools     |   3 +-
 src/shared/shell.c | 567 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/shared/shell.h |  67 +++++++
 3 files changed, 636 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..2cd1ea3fb
--- /dev/null
+++ b/src/shared/shell.c
@@ -0,0 +1,567 @@
+/*
+ *
+ *  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/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 {
+	unsigned int input;
+
+	bool saved_prompt;
+	bt_shell_prompt_input_func saved_func;
+	void *saved_user_data;
+
+	const struct bt_shell_menu_entry *current;
+	/* 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 cmd_table[] = {
+	{ "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.current)
+		return;
+
+	print_text(COLOR_HIGHLIGHT, "Available commands:");
+	print_text(COLOR_HIGHLIGHT, "-------------------");
+	for (entry = data.current; entry->cmd; entry++) {
+		print_menu(entry->cmd, entry->arg ? : "", entry->desc ? : "");
+	}
+
+	for (entry = cmd_table; 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.current || !cmd)
+		return;
+
+	for (entry = data.current; entry->cmd; entry++) {
+		if (strcmp(cmd, entry->cmd))
+			continue;
+
+		if (entry->func) {
+			entry->func(arg);
+			return;
+		}
+	}
+
+	for (entry = cmd_table; 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) && !data.saved_prompt;
+
+	if (save_input) {
+		saved_point = rl_point;
+		saved_line = rl_copy_text(0, rl_end);
+		rl_save_prompt();
+		rl_replace_line("", 0);
+		rl_redisplay();
+	}
+
+	va_start(args, fmt);
+	vprintf(fmt, args);
+	va_end(args);
+
+	if (save_input) {
+		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_clear_message();
+	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)
+{
+	const struct bt_shell_menu_entry *entry;
+	static int index, len;
+	const char *cmd;
+
+	entry = data.current;
+
+	if (!state) {
+		index = 0;
+		len = strlen(text);
+	}
+
+	while ((cmd = entry[index].cmd)) {
+		index++;
+
+		if (!strncmp(cmd, text, len))
+			return strdup(cmd);
+	}
+
+	return NULL;
+}
+
+static char **shell_completion(const char *text, int start, int end)
+{
+	char **matches = NULL;
+
+	if (!data.current)
+		return NULL;
+
+	if (start > 0) {
+		const struct bt_shell_menu_entry *entry;
+		char *input_cmd;
+
+		input_cmd = strndup(rl_line_buffer, start - 1);
+		for (entry = data.current; 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;
+		}
+
+		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 gboolean signal_handler(GIOChannel *channel, GIOCondition condition,
+							gpointer user_data)
+{
+	static bool terminated = false;
+	struct signalfd_siginfo si;
+	ssize_t result;
+	int fd;
+
+	if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
+		g_main_loop_quit(main_loop);
+		return FALSE;
+	}
+
+	fd = g_io_channel_unix_get_fd(channel);
+
+	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 TRUE;
+}
+
+static guint setup_signalfd(void)
+{
+	GIOChannel *channel;
+	guint source;
+	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;
+	}
+
+	channel = g_io_channel_unix_new(fd);
+
+	g_io_channel_set_close_on_unref(channel, TRUE);
+	g_io_channel_set_encoding(channel, NULL, NULL);
+	g_io_channel_set_buffered(channel, FALSE);
+
+	source = g_io_add_watch(channel,
+				G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+				signal_handler, NULL);
+
+	g_io_channel_unref(channel);
+
+	return source;
+}
+
+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)
+{
+	guint signal;
+
+	signal = setup_signalfd();
+
+	g_main_loop_run(main_loop);
+
+	bt_shell_release_prompt("");
+	bt_shell_detach();
+
+	g_source_remove(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.current || !menu)
+		return false;
+
+	data.current = 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 gboolean input_handler(GIOChannel *channel, GIOCondition condition,
+							gpointer user_data)
+{
+	if (condition & G_IO_IN) {
+		rl_callback_read_char();
+		return TRUE;
+	}
+
+	if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) {
+		g_main_loop_quit(main_loop);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool bt_shell_attach(int fd)
+{
+	GIOChannel *channel;
+
+	/* TODO: Allow more than one input? */
+	if (data.input > 0)
+		return false;
+
+	channel = g_io_channel_unix_new(fd);
+
+	data.input = g_io_add_watch(channel,
+				G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+				input_handler, NULL);
+
+	g_io_channel_unref(channel);
+
+	return data.input > 0;
+}
+
+bool bt_shell_detach(void)
+{
+	if (!data.input)
+		return false;
+
+	g_source_remove(data.input);
+	data.input = 0;
+
+	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

--
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



[Index of Archives]     [Bluez Devel]     [Linux Wireless Networking]     [Linux Wireless Personal Area Networking]     [Linux ATH6KL]     [Linux USB Devel]     [Linux Media Drivers]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [Big List of Linux Books]

  Powered by Linux