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; +}