From 4603e719873ccbb555f82a19fdacf6cc4a51bdab Mon Sep 17 00:00:00 2001 From: Markus Makela Date: Fri, 18 Nov 2016 14:15:59 +0200 Subject: [PATCH] MXS-929: Add domain function registration A module can register a function to a domain. These function can then be called by external actors enabling the modules to expand their functionality beyond the module API. Each module should use its own domain e.g the library name. Currently, the functions do not return results. The possible next step would be to alter the function entry point to return a result set of sorts. This would allow the modules to convey structured information to the client information which would handle the formatting of the result. Although this sounds good, it is not required for the implementation of MXS-929. --- include/maxscale/modulecmd.h | 171 ++++++++++++++ server/core/CMakeLists.txt | 2 +- server/core/modulecmd.c | 389 +++++++++++++++++++++++++++++++ server/core/test/CMakeLists.txt | 3 + server/core/test/testmodulecmd.c | 185 +++++++++++++++ 5 files changed, 749 insertions(+), 1 deletion(-) create mode 100644 include/maxscale/modulecmd.h create mode 100644 server/core/modulecmd.c create mode 100644 server/core/test/testmodulecmd.c diff --git a/include/maxscale/modulecmd.h b/include/maxscale/modulecmd.h new file mode 100644 index 000000000..7c1b0088c --- /dev/null +++ b/include/maxscale/modulecmd.h @@ -0,0 +1,171 @@ +#pragma once +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl. + * + * Change Date: 2019-07-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +/** + * @file module_command.h Module driven commands + * + * This header describes the structures and functions used to register new + * functions for modules. It allows modules to introduce custom commands that + * are registered into a module specific domain. These commands can then be + * accessed from multiple different client interfaces without implementing the + * same functionality again. + */ + +#include +#include +#include +#include +#include +#include +#include + +MXS_BEGIN_DECLS + +/** + * The argument type + * + * First 8 bits are reserved for argument type, bits 9 through 32 are reserved + * for argument options and bits 33 through 64 are reserved for future use. + */ +typedef uint64_t modulecmd_arg_type_t; + +/** + * Argument types for the registered functions, the first 8 bits of + * the modulecmd_arg_type_t type. An argument can be of only one type. + */ +#define MODULECMD_ARG_NONE 0 +#define MODULECMD_ARG_STRING 1 +#define MODULECMD_ARG_BOOLEAN 2 +#define MODULECMD_ARG_SERVICE 3 +#define MODULECMD_ARG_SERVER 4 +#define MODULECMD_ARG_SESSION 5 +#define MODULECMD_ARG_DCB 6 +#define MODULECMD_ARG_MONITOR 7 +#define MODULECMD_ARG_FILTER 8 + +/** + * Options for arguments, bits 9 through 32 + */ +#define MODULECMD_ARG_OPTIONAL (1 << 8) /**< The argument is optional */ + +/** + * Helper macros + */ +#define MODULECMD_GET_TYPE(type) ((type & 0xff)) +#define MODULECMD_ARG_IS_REQUIRED(type) ((type & MODULECMD_ARG_OPTIONAL) == 0) + +/** Argument list node */ +struct arg_node +{ + modulecmd_arg_type_t type; + union + { + char *string; + bool boolean; + SERVICE *service; + SERVER *server; + SESSION *session; + DCB *dcb; + MONITOR *monitor; + FILTER_DEF *filter; + } value; +}; + +/** Argument list */ +typedef struct +{ + int argc; + struct arg_node *argv; +} MODULECMD_ARG; + +/** + * The function signature for the module commands. + * + * The number of arguments will always be the maximum number of arguments the + * module requested. If an argument had the MODULECMD_ARG_OPTIONAL flag, and + * the argument was not provided, the type of the argument will be + * MODULECMD_ARG_NONE. + * + * @param argv Argument list + * @return True on success, false on error + */ +typedef bool (*MODULECMDFN)(const MODULECMD_ARG *argv); + +/** + * A registered command + */ +typedef struct modulecmd +{ + char *identifier; /**< Unique identifier */ + char *domain; /**< Command domain */ + MODULECMDFN func; /**< The registered function */ + int arg_count; /**< Number of arguments */ + modulecmd_arg_type_t *arg_types; /**< Argument types */ + struct modulecmd *next; /**< Next command */ +} MODULECMD; + +/** + * @brief Register a new command + * + * This function registers a new command into the domain. + * + * @param domain Command domain + * @param identifier The unique identifier for this command + * @param entry_point The actual entry point function + * @param argc Maximum number of arguments + * @param argv Array of argument types of size @c argc + * @return True if the module was successfully registered, false on error + */ +bool modulecmd_register_command(const char *domain, const char *identifier, + MODULECMDFN entry_point, int argc, modulecmd_arg_type_t *argv); + +/** + * @brief Find a registered command + * + * @param domain Command domain + * @param identifier Command identifier + * @return Registered command or NULL if no command was found + */ +const MODULECMD* modulecmd_find_command(const char *domain, const char *identifier); + +/** + * @brief Parse arguments for a command + * + * @param cmd Command for which the parameters are parsed + * @param argc Number of arguments + * @param argv Argument list in string format of size @c argc + * @return Parsed arguments or NULL on error + */ +MODULECMD_ARG* modulecmd_arg_parse(const MODULECMD *cmd, int argc, const char **argv); + +/** + * @brief Free parsed arguments returned by modulecmd_arg_parse + * @param arg Arguments to free + */ +void modulecmd_arg_free(MODULECMD_ARG *arg); + +/** + * @brief Call a registered command + * + * This calls a registered command in a specific domain. There are no guarantees + * on the length of the call or whether it will block. All of this depends on the + * module and what the command does. + * + * @param cmd Command to call + * @param args Parsed command arguments + * @return True on success, false on error + */ +bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args); + +MXS_END_DECLS diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt index 4e6207424..d232fe559 100644 --- a/server/core/CMakeLists.txt +++ b/server/core/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(maxscale-common SHARED adminusers.c alloc.c authenticator.c atomic.c buffer.c config.c dcb.c filter.c externcmd.c gwbitmask.c gwdirs.c hashtable.c hint.c housekeeper.c listmanager.c load_utils.c log_manager.cc maxscale_pcre2.c memlog.c misc.c mlist.c modutil.c monitor.c queuemanager.c query_classifier.c poll.c random_jkiss.c resultset.c secrets.c server.c service.c session.c spinlock.c thread.c users.c utils.c skygw_utils.cc statistics.c listener.c gw_ssl.c mysql_utils.c mysql_binlog.c) +add_library(maxscale-common SHARED adminusers.c alloc.c authenticator.c atomic.c buffer.c config.c dcb.c filter.c externcmd.c gwbitmask.c gwdirs.c hashtable.c hint.c housekeeper.c listmanager.c load_utils.c log_manager.cc maxscale_pcre2.c memlog.c misc.c mlist.c modutil.c monitor.c queuemanager.c query_classifier.c poll.c random_jkiss.c resultset.c secrets.c server.c service.c session.c spinlock.c thread.c users.c utils.c skygw_utils.cc statistics.c listener.c gw_ssl.c mysql_utils.c mysql_binlog.c modulecmd.c) target_link_libraries(maxscale-common ${MARIADB_CONNECTOR_LIBRARIES} ${LZMA_LINK_FLAGS} ${PCRE2_LIBRARIES} ${CURL_LIBRARIES} ssl pthread crypt dl crypto inih z rt m stdc++) diff --git a/server/core/modulecmd.c b/server/core/modulecmd.c new file mode 100644 index 000000000..cf5d4515e --- /dev/null +++ b/server/core/modulecmd.c @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl. + * + * Change Date: 2019-07-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +#include +#include +#include +#include + +/** + * A registered domain + */ +typedef struct modulecmd_domain +{ + char *domain; /**< The domain */ + MODULECMD *commands; /**< List of registered commands */ + struct modulecmd_domain *next; /**< Next domain */ +} MODULECMD_DOMAIN; + +/** + * Internal functions + */ + +/** The global list of registered domains */ +static MODULECMD_DOMAIN *modulecmd_domains = NULL; +static SPINLOCK modulecmd_lock = SPINLOCK_INIT; + +static MODULECMD_DOMAIN* domain_create(const char *domain) +{ + MODULECMD_DOMAIN *rval = MXS_MALLOC(sizeof(*rval)); + char *dm = MXS_STRDUP(domain); + + if (rval && dm) + { + rval->domain = dm; + rval->commands = NULL; + rval->next = NULL; + } + else + { + MXS_FREE(rval); + MXS_FREE(dm); + rval = NULL; + } + + return rval; +} + +static void domain_free(MODULECMD_DOMAIN *dm) +{ + if (dm) + { + MXS_FREE(dm->domain); + MXS_FREE(dm); + } +} + +static MODULECMD_DOMAIN* get_or_create_domain(const char *domain) +{ + + MODULECMD_DOMAIN *dm; + + for (dm = modulecmd_domains; dm; dm = dm->next) + { + if (strcmp(dm->domain, domain) == 0) + { + return dm; + } + } + + if ((dm = domain_create(domain))) + { + dm->next = modulecmd_domains; + modulecmd_domains = dm; + } + + return dm; +} + +static MODULECMD* command_create(const char *identifier, const char *domain, + MODULECMDFN entry_point, int argc, + modulecmd_arg_type_t* argv) +{ + MODULECMD *rval = MXS_MALLOC(sizeof(*rval)); + char *id = MXS_STRDUP(identifier); + char *dm = MXS_STRDUP(domain); + modulecmd_arg_type_t *types = MXS_MALLOC(sizeof(*types) * argc); + + if (rval && id && dm && types) + { + for (int i = 0; i < argc; i++) + { + types[i] = argv[i]; + } + + rval->func = entry_point; + rval->identifier = id; + rval->domain = dm; + rval->arg_types = types; + rval->arg_count = argc; + rval->next = NULL; + } + else + { + MXS_FREE(rval); + MXS_FREE(id); + MXS_FREE(dm); + MXS_FREE(types); + rval = NULL; + } + + return rval; +} + +static void command_free(MODULECMD *cmd) +{ + if (cmd) + { + MXS_FREE(cmd->identifier); + MXS_FREE(cmd->domain); + MXS_FREE(cmd->arg_types); + MXS_FREE(cmd); + } +} + +static bool domain_has_command(MODULECMD_DOMAIN *dm, const char *id) +{ + for (MODULECMD *cmd = dm->commands; cmd; cmd = cmd->next) + { + if (strcmp(cmd->identifier, id) == 0) + { + return true; + } + } + return false; +} + +static bool process_argument(modulecmd_arg_type_t type, const char* value, + struct arg_node *arg) +{ + bool rval = false; + + if (!MODULECMD_ARG_IS_REQUIRED(type) && value == NULL) + { + arg->type = MODULECMD_ARG_NONE; + rval = true; + } + else if (value) + { + switch (MODULECMD_GET_TYPE(type)) + { + case MODULECMD_ARG_NONE: + arg->type = MODULECMD_ARG_NONE; + rval = true; + break; + + case MODULECMD_ARG_STRING: + if ((arg->value.string = MXS_STRDUP(value))) + { + arg->type = MODULECMD_ARG_STRING; + rval = true; + } + break; + + case MODULECMD_ARG_BOOLEAN: + { + int truthval = config_truth_value((char*)value); + if (truthval != -1) + { + arg->value.boolean = truthval; + arg->type = MODULECMD_ARG_BOOLEAN; + rval = true; + } + } + break; + + case MODULECMD_ARG_SERVICE: + if ((arg->value.service = service_find((char*)value))) + { + arg->type = MODULECMD_ARG_SERVICE; + rval = true; + } + break; + + case MODULECMD_ARG_SERVER: + if ((arg->value.server = server_find_by_unique_name(value))) + { + arg->type = MODULECMD_ARG_SERVER; + rval = true; + } + break; + + case MODULECMD_ARG_SESSION: + // TODO: Implement this + break; + + case MODULECMD_ARG_DCB: + // TODO: Implement this + break; + + case MODULECMD_ARG_MONITOR: + if ((arg->value.monitor = monitor_find((char*)value))) + { + arg->type = MODULECMD_ARG_MONITOR; + rval = true; + } + break; + + case MODULECMD_ARG_FILTER: + if ((arg->value.filter = filter_find((char*)value))) + { + arg->type = MODULECMD_ARG_FILTER; + rval = true; + } + break; + + default: + ss_dassert(false); + MXS_ERROR("Undefined argument type: %0lx", type); + break; + } + } + + return rval; +} + +static MODULECMD_ARG* modulecmd_arg_create(int argc) +{ + MODULECMD_ARG* arg = MXS_MALLOC(sizeof(*arg)); + struct arg_node *argv = MXS_CALLOC(argc, sizeof(*argv)); + + if (arg && argv) + { + arg->argc = argc; + arg->argv = argv; + } + else + { + MXS_FREE(argv); + MXS_FREE(arg); + arg = NULL; + } + + return arg; +} + +static void free_argument(struct arg_node *arg) +{ + switch (arg->type) + { + case MODULECMD_ARG_STRING: + MXS_FREE(arg->value.string); + break; + + default: + break; + } +} + +/** + * Public functions declared in modulecmd.h + */ + +bool modulecmd_register_command(const char *domain, const char *identifier, + MODULECMDFN entry_point, int argc, modulecmd_arg_type_t *argv) +{ + bool rval = false; + spinlock_acquire(&modulecmd_lock); + + MODULECMD_DOMAIN *dm = get_or_create_domain(domain); + + if (dm) + { + if (domain_has_command(dm, identifier)) + { + MXS_ERROR("Command '%s' in domain '%s' was registered more than once.", + identifier, domain); + } + else + { + MODULECMD *cmd = command_create(identifier, domain, entry_point, argc, argv); + + if (cmd) + { + cmd->next = dm->commands; + dm->commands = cmd; + rval = true; + } + } + } + + spinlock_release(&modulecmd_lock); + + return rval; +} + +const MODULECMD* modulecmd_find_command(const char *domain, const char *identifier) +{ + MODULECMD *rval = NULL; + spinlock_acquire(&modulecmd_lock); + + for (MODULECMD_DOMAIN *dm = modulecmd_domains; dm; dm = dm->next) + { + if (strcmp(domain, dm->domain) == 0) + { + for (MODULECMD *cmd = dm->commands; cmd; cmd = cmd->next) + { + if (strcmp(cmd->identifier, identifier) == 0) + { + rval = cmd; + break; + } + } + break; + } + } + + spinlock_release(&modulecmd_lock); + return rval; +} + +MODULECMD_ARG* modulecmd_arg_parse(const MODULECMD *cmd, int argc, const char **argv) +{ + int argc_min = 0; + + for (int i = 0; i < cmd->arg_count; i++) + { + if (MODULECMD_ARG_IS_REQUIRED(cmd->arg_types[i])) + { + argc_min++; + } + } + + MODULECMD_ARG* arg = NULL; + + if (argc >= argc_min && argc <= cmd->arg_count) + { + arg = modulecmd_arg_create(cmd->arg_count); + bool error = false; + + if (arg) + { + for (int i = 0; i < cmd->arg_count && i < argc; i++) + { + if (!process_argument(cmd->arg_types[i], argv[i], &arg->argv[i])) + { + MXS_ERROR("Failed to parse argument %d: %s", i + 1, argv[i] ? argv[i] : "NULL"); + error = true; + } + } + + if (error) + { + modulecmd_arg_free(arg); + arg = NULL; + } + } + } + + return arg; +} + +void modulecmd_arg_free(MODULECMD_ARG* arg) +{ + if (arg) + { + for (int i = 0; i < arg->argc; i++) + { + free_argument(&arg->argv[i]); + } + + MXS_FREE(arg->argv); + MXS_FREE(arg); + } +} + +bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args) +{ + return cmd->func(args); +} diff --git a/server/core/test/CMakeLists.txt b/server/core/test/CMakeLists.txt index f12f22ca5..1dd858aa6 100644 --- a/server/core/test/CMakeLists.txt +++ b/server/core/test/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(test_users testusers.c) add_executable(testfeedback testfeedback.c) add_executable(testmaxscalepcre2 testmaxscalepcre2.c) add_executable(testmemlog testmemlog.c) +add_executable(testmodulecmd testmodulecmd.c) target_link_libraries(test_adminusers maxscale-common) target_link_libraries(test_buffer maxscale-common) target_link_libraries(test_dcb maxscale-common) @@ -38,6 +39,7 @@ target_link_libraries(test_users maxscale-common) target_link_libraries(testfeedback maxscale-common) target_link_libraries(testmaxscalepcre2 maxscale-common) target_link_libraries(testmemlog maxscale-common) +target_link_libraries(testmodulecmd maxscale-common) add_test(TestAdminUsers test_adminusers) add_test(TestBuffer test_buffer) add_test(TestDCB test_dcb) @@ -58,6 +60,7 @@ add_test(TestServer test_server) add_test(TestService test_service) add_test(TestSpinlock test_spinlock) add_test(TestUsers test_users) +add_test(TestModulecmd testmodulecmd) # This test requires external dependencies and thus cannot be run # as a part of the core test set diff --git a/server/core/test/testmodulecmd.c b/server/core/test/testmodulecmd.c new file mode 100644 index 000000000..7a1832aa8 --- /dev/null +++ b/server/core/test/testmodulecmd.c @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl. + * + * Change Date: 2019-07-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +/** + * Test modulecmd.h functionality + */ + +#include + +#define TEST(a, b) do{if (!(a)){printf("%s:%d "b"\n", __FILE__, __LINE__);return 1;}}while(false) + +static bool ok = false; + +bool test_fn(const MODULECMD_ARG *arg) +{ + + ok = (arg->argc == 2 && strcmp(arg->argv[0].value.string, "Hello") == 0 && + arg->argv[1].value.boolean); + + return true; +} + +int test_arguments() +{ + const char *params1[] = {"Hello", "true"}; + const char *params2[] = {"Hello", "1"}; + + const char *wrong_params1[] = {"Hi", "true"}; + const char *wrong_params2[] = {"Hello", "false"}; + + const char *bad_params1[] = {"Hello", "World!"}; + const char *bad_params2[] = {"Hello", NULL}; + const char *bad_params3[] = {NULL, NULL}; + const char *bad_params4[] = {NULL, "World!"}; + + const char *ns = "test_arguments"; + const char *id = "test_arguments"; + modulecmd_arg_type_t args1[] = {MODULECMD_ARG_STRING, MODULECMD_ARG_BOOLEAN}; + + /** + * Test command creation + */ + + TEST(modulecmd_find_command(ns, id) == NULL, "The registered command should not yet be found"); + + TEST(modulecmd_register_command(ns, id, test_fn, 2, args1), + "Registering a command should succeed"); + + TEST(!modulecmd_register_command(ns, id, test_fn, 2, args1), + "Registering the command a second time should fail"); + + const MODULECMD *cmd = modulecmd_find_command(ns, id); + TEST(cmd, "The registered command should be found"); + + /** + * Test bad arguments + */ + + TEST(modulecmd_arg_parse(cmd, 0, NULL) == NULL, "Passing no arguments should fail"); + TEST(modulecmd_arg_parse(cmd, 1, params1) == NULL, "Passing one argument should fail"); + TEST(modulecmd_arg_parse(cmd, 3, params1) == NULL, "Passing three arguments should fail"); + + TEST(modulecmd_arg_parse(cmd, 2, bad_params1) == NULL, "Passing bad arguments should fail"); + TEST(modulecmd_arg_parse(cmd, 2, bad_params2) == NULL, "Passing bad arguments should fail"); + TEST(modulecmd_arg_parse(cmd, 2, bad_params3) == NULL, "Passing bad arguments should fail"); + TEST(modulecmd_arg_parse(cmd, 2, bad_params4) == NULL, "Passing bad arguments should fail"); + + /** + * Test valid arguments + */ + + MODULECMD_ARG* alist = modulecmd_arg_parse(cmd, 2, params1); + TEST(alist, "Arguments should be parsed"); + + TEST(modulecmd_call_command(cmd, alist), "Module call should be successful"); + TEST(ok, "Function should receive right parameters"); + + ok = false; + + TEST(modulecmd_call_command(cmd, alist), "Second Module call should be successful"); + TEST(ok, "Function should receive right parameters"); + + + ok = false; + modulecmd_arg_free(alist); + + TEST((alist = modulecmd_arg_parse(cmd, 2, params2)), "Arguments should be parsed"); + + TEST(modulecmd_call_command(cmd, alist), "Module call should be successful"); + TEST(ok, "Function should receive right parameters"); + + modulecmd_arg_free(alist); + + /** + * Test valid but wrong arguments + */ + TEST((alist = modulecmd_arg_parse(cmd, 2, wrong_params1)), "Arguments should be parsed"); + TEST(modulecmd_call_command(cmd, alist), "Module call should be successful"); + TEST(!ok, "Function should receive wrong parameters"); + modulecmd_arg_free(alist); + + TEST((alist = modulecmd_arg_parse(cmd, 2, wrong_params2)), "Arguments should be parsed"); + TEST(modulecmd_call_command(cmd, alist), "Module call should be successful"); + TEST(!ok, "Function should receive wrong parameters"); + modulecmd_arg_free(alist); + + return 0; +} + +bool test_fn2(const MODULECMD_ARG *arg) +{ + return true; +} + +int test_optional_arguments() +{ + const char *params1[] = {"Hello", "true"}; + const char *params2[] = {NULL, "true"}; + const char *params3[] = {"Hello", NULL}; + const char *params4[] = {NULL, NULL}; + + const char *ns = "test_optional_arguments"; + const char *id = "test_optional_arguments"; + modulecmd_arg_type_t args1[] = + { + MODULECMD_ARG_STRING | MODULECMD_ARG_OPTIONAL, + MODULECMD_ARG_BOOLEAN | MODULECMD_ARG_OPTIONAL + }; + + TEST(modulecmd_register_command(ns, id, test_fn2, 2, args1), + "Registering a command should succeed"); + + const MODULECMD *cmd = modulecmd_find_command(ns, id); + TEST(cmd, "The registered command should be found"); + + MODULECMD_ARG *arg = modulecmd_arg_parse(cmd, 2, params1); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 2, params2); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 2, params3); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 2, params4); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 1, params1); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 1, params2); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + arg = modulecmd_arg_parse(cmd, 0, params1); + TEST(arg, "Parsing arguments should succeed"); + modulecmd_arg_free(arg); + + return 0; +} + +int main(int argc, char **argv) +{ + int rc = 0; + + rc += test_arguments(); + rc += test_optional_arguments(); + + return rc; +}