From d408a53f78b04a2f4a0a460e147626ae59118f01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:44:28 +0200 Subject: [PATCH] varlinkctl: add new varlinkctl tool --- man/busctl.xml | 1 + man/rules/meson.build | 1 + man/varlinkctl.xml | 315 ++++++++++++++++++++++ meson.build | 1 + src/varlinkctl/meson.build | 13 + src/varlinkctl/varlinkctl.c | 520 ++++++++++++++++++++++++++++++++++++ 6 files changed, 851 insertions(+) create mode 100644 man/varlinkctl.xml create mode 100644 src/varlinkctl/meson.build create mode 100644 src/varlinkctl/varlinkctl.c diff --git a/man/busctl.xml b/man/busctl.xml index ce23dd1b33..cc404529ae 100644 --- a/man/busctl.xml +++ b/man/busctl.xml @@ -582,6 +582,7 @@ o "/org/freedesktop/systemd1/job/42684" dbus-daemon1, D-Bus, sd-bus3, + varlinkctl1, systemd1, machinectl1, wireshark1 diff --git a/man/rules/meson.build b/man/rules/meson.build index 3454668cab..3265db9c2d 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1249,6 +1249,7 @@ manpages = [ ['systemd-user-runtime-dir', 'user-runtime-dir@.service'], ''], ['userdbctl', '1', [], 'ENABLE_USERDB'], + ['varlinkctl', '1', [], ''], ['vconsole.conf', '5', [], 'ENABLE_VCONSOLE'], ['veritytab', '5', [], 'HAVE_LIBCRYPTSETUP'] ] diff --git a/man/varlinkctl.xml b/man/varlinkctl.xml new file mode 100644 index 0000000000..5d0ec6ace7 --- /dev/null +++ b/man/varlinkctl.xml @@ -0,0 +1,315 @@ + + + + + + + + varlinkctl + systemd + + + + varlinkctl + 1 + + + + varlinkctl + Introspect with and invoke Varlink services + + + + + varlinkctl + OPTIONS + info + ADDRESS + + + + varlinkctl + OPTIONS + list-interfaces + ADDRESS + + + + varlinkctl + OPTIONS + introspect + ADDRESS + INTERFACE + + + + varlinkctl + OPTIONS + call + ADDRESS + METHOD + PARAMETERS + + + + varlinkctl + OPTIONS + validate-idl + FILE + + + + + Description + + varlinkctl may be used to introspect and invoke Varlink services. + + Services are referenced by one of the following: + + + A Varlink service reference starting with the unix: string, followed + by an absolute AF_UNIX path, or by @ and an arbitrary string + (the latter for referencing sockets in the abstract namespace). + + A Varlink service reference starting with the exec: string, followed + by an absolute path of a binary to execute. + + + For convenience these two simpler (redundant) service address syntaxes are also supported: + + + A file system path to an AF_UNIX socket, either absolute + (i.e. begins with /) or relative (in which case it must begin with + ./). + + A file system path to an executable, either absolute or relative (as above, must begin + with /, resp. ./). + + + + + Commands + + The following commands are understood: + + + + info ADDRESS + + Show brief information about the specified service, including vendor name and list of + implemented interfaces. Expects a service address in the formats described above. + + + + + + list-interfaces ADDRESS + + Show list of interfaces implemented by the specified service. Expects a service + address in the formats described above. + + + + + + introspect ADDRESS INTERFACE + + Show interface definition of the specified interface provided by the specified + service. Expects a service address in the formats described above and a Varlink interface + name. + + + + + + call ADDRESS METHOD [ARGUMENTS] + + Call the specified method of the specified service. Expects a service address in the + format described above, a fully qualified Varlink method name, and a JSON arguments object. If the + arguments object is not specified, it is read from STDIN instead. To pass an empty list of + parameters, specify the empty object {}. + + The reply parameters are written as JSON object to STDOUT. + + + + + + validate-idl [FILE] + + Reads a Varlink interface definition file, parses and validates it, then outputs it + with syntax highlighting. This checks for syntax and internal consistency of the interface. Expects a + file name to read the interface definition from. If omitted reads the interface definition from + STDIN. + + + + + + help + + Show command syntax help. + + + + + + + + Options + + The following options are understood: + + + + + + When used with call: expect multiple method replies. If this flag is + set the method call is sent with the more flag set, which tells the service to + generate multiple replies, if needed. The command remains running until the service sends a reply + message that indicates it is the last in the series. This flag should be set only for method calls + that support this mechanism. + + If this mode is enabled output is automatically switched to JSON-SEQ mode, so that individual + reply objects can be easily discerned. + + + + + + + + When used with call: do not expect a method reply. If this flag + is set the method call is sent with the oneway flag set (the command exits + immediately after), which tells the service not to generate a reply. + + + + + + MODE + + + Selects the JSON output formatting, one of pretty (for nicely indented, + colorized output) or short (for terse output with minimal whitespace and no + newlines), defaults to short. + + + + + + + + + + Equivalent to when invoked interactively from a terminal. Otherwise + equivalent to , in particular when the output is piped to some other + program. + + + + + + + + + + + + + Examples + + + Investigating a Service + + The following three commands inspect the io.systemd.Resolve service + implemented by + systemd-resolved.service8, + listing general service information and implemented interfaces, and then displaying the interface + definition of its primary interface: + + $ varlinkctl info /run/systemd/resolve/io.systemd.Resolve + Vendor: The systemd Project + Product: systemd (systemd-resolved) + Version: 254 (254-1522-g4790521^) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.Resolve + org.varlink.service +$ varlinkctl list-interfaces /run/systemd/resolve/io.systemd.Resolve +io.systemd +io.systemd.Resolve +org.varlink.service +$ varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve +interface io.systemd.Resolve +type ResolvedAddress( + ifindex: ?int, + … + + (Interface definition has been truncated in the example above, in the interest of brevity.) + + + + Invoking a Method + + The following command resolves a hostname via systemd-resolved.service8's ResolveHostname method call. + + $ varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name":"systemd.io","family":2}' -j +{ + "addresses" : [ + { + "ifindex" : 2, + "family" : 2, + "address" : [ + 185, + 199, + 111, + 153 + ] + } + ], + "name" : "systemd.io", + "flags" : 1048577 +} + + + + Investigating a Service Executable + + The following comand inspects the /usr/lib/systemd/systemd-pcrextend + executable and the IPC APIs it provides. It then invokes a method on it: + + # varlinkctl info /usr/lib/systemd/systemd-pcrextend + Vendor: The systemd Project + Product: systemd (systemd-pcrextend) + Version: 254 (254-1536-g97734fb) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.PCRExtend + org.varlink.service +# varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend +interface io.systemd.PCRExtend + +method Extend( + pcr: int, + text: ?string, + data: ?string +) -> () +# varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"pcr":15,"text":"foobar"}' +{} + + + + + + See Also + + + busctl1, + Varlink + + + diff --git a/meson.build b/meson.build index 88bc1367e0..5e9ca285ac 100644 --- a/meson.build +++ b/meson.build @@ -2193,6 +2193,7 @@ subdir('src/update-done') subdir('src/update-utmp') subdir('src/user-sessions') subdir('src/userdb') +subdir('src/varlinkctl') subdir('src/vconsole') subdir('src/veritysetup') subdir('src/volatile-root') diff --git a/src/varlinkctl/meson.build b/src/varlinkctl/meson.build new file mode 100644 index 0000000000..c1074dc50a --- /dev/null +++ b/src/varlinkctl/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +varlinkctl_sources = files( + 'varlinkctl.c', +) + +executables += [ + executable_template + { + 'name' : 'varlinkctl', + 'public' : true, + 'sources' : varlinkctl_sources, + }, +] diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c new file mode 100644 index 0000000000..a656bfcaf0 --- /dev/null +++ b/src/varlinkctl/varlinkctl.c @@ -0,0 +1,520 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "build.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "main-func.h" +#include "pager.h" +#include "parse-argument.h" +#include "path-util.h" +#include "pretty-print.h" +#include "terminal-util.h" +#include "varlink.h" +#include "verbs.h" +#include "version.h" + +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static VarlinkMethodFlags arg_method_flags = 0; + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("varlinkctl", "1", &link); + if (r < 0) + return log_oom(); + + pager_open(arg_pager_flags); + + printf("%1$s [OPTIONS...] COMMAND ...\n\n" + "%5$sIntrospect Varlink Services.%6$s\n" + "\n%3$sCommands:%4$s\n" + " info ADDRESS Show service information\n" + " list-interfaces ADDRESS\n" + " List interfaces implemented by service\n" + " introspect ADDRESS INTERFACE\n" + " Show interface definition\n" + " call ADDRESS METHOD [PARAMS]\n" + " Invoke method\n" + " validate-idl [FILE] Validate interface description\n" + " help Show this help\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --more Request multiple responses\n" + " --oneway Do not request response\n" + " --json=MODE Output as JSON\n" + " -j Same as --json=pretty on tty, --json=short otherwise\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + return help(); +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_MORE, + ARG_ONEWAY, + ARG_JSON, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "more", no_argument, NULL, ARG_MORE }, + { "oneway", no_argument, NULL, ARG_ONEWAY }, + { "json", required_argument, NULL, ARG_JSON }, + {}, + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_MORE: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_ONEWAY) | VARLINK_METHOD_MORE; + break; + + case ARG_ONEWAY: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_MORE) | VARLINK_METHOD_ONEWAY; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'j': + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + /* If more than one reply is expected, imply JSON-SEQ output */ + if (FLAGS_SET(arg_method_flags, VARLINK_METHOD_MORE)) + arg_json_format_flags |= JSON_FORMAT_SEQ; + + return 1; +} + +static int varlink_connect_auto(Varlink **ret, const char *where) { + int r; + + assert(ret); + assert(where); + + if (STARTSWITH_SET(where, "/", "./")) { /* If the string starts with a slash or dot slash we use it as a file system path */ + _cleanup_close_ int fd = -EBADF; + struct stat st; + + fd = open(where, O_PATH|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", where); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", where); + + /* Is this a socket in the fs? Then connect() to it. */ + if (S_ISSOCK(st.st_mode)) { + r = varlink_connect_address(ret, FORMAT_PROC_FD_PATH(fd)); + if (r < 0) + return log_error_errno(r, "Failed to connect to '%s': %m", where); + + return 0; + } + + /* Is this an executable binary? Then fork it off. */ + if (S_ISREG(st.st_mode) && (st.st_mode & 0111)) { + r = varlink_connect_exec(ret, where, STRV_MAKE(where)); /* Ideally we'd use FORMAT_PROC_FD_PATH(fd) here too, but that breaks the #! logic */ + if (r < 0) + return log_error_errno(r, "Failed to spawn '%s' process: %m", where); + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unrecognized path '%s' is neither an AF_UNIX socket, nor an executable binary.", where); + } + + /* Otherwise assume this is an URL */ + r = varlink_connect_url(ret, where); + if (r < 0) + return log_error_errno(r, "Failed to connect to URL '%s': %m", where); + + return 0; +} + +typedef struct GetInfoData { + const char *vendor; + const char *product; + const char *version; + const char *url; + char **interfaces; +} GetInfoData; + +static void get_info_data_done(GetInfoData *d) { + assert(d); + + d->interfaces = strv_free(d->interfaces); +} + +static int verb_info(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url; + int r; + + assert(argc == 2); + url = argv[1]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_call(vl, "org.varlink.service.GetInfo", NULL, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInfo() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInfo() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "vendor", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, vendor), JSON_MANDATORY }, + { "product", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, product), JSON_MANDATORY }, + { "version", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, version), JSON_MANDATORY }, + { "url", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, url), JSON_MANDATORY }, + { "interfaces", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(GetInfoData, interfaces), JSON_MANDATORY }, + {} + }; + _cleanup_(get_info_data_done) GetInfoData data = {}; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &data); + if (r < 0) + return r; + + strv_sort(data.interfaces); + + if (streq_ptr(argv[0], "list-interfaces")) { + STRV_FOREACH(i, data.interfaces) + puts(*i); + } else { + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new_vertical(); + if (!t) + return log_oom(); + + r = table_add_many( + t, + TABLE_FIELD, "Vendor", + TABLE_STRING, data.vendor, + TABLE_FIELD, "Product", + TABLE_STRING, data.product, + TABLE_FIELD, "Version", + TABLE_STRING, data.version, + TABLE_FIELD, "URL", + TABLE_STRING, data.url, + TABLE_SET_URL, data.url, + TABLE_FIELD, "Interfaces", + TABLE_STRV, data.interfaces); + if (r < 0) + return table_log_add_error(r); + + r = table_print(t, NULL); + if (r < 0) + return table_log_print_error(r); + } + } else { + JsonVariant *v; + + v = streq_ptr(argv[0], "list-interfaces") ? + json_variant_by_key(reply, "interfaces") : reply; + + json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; +} + +typedef struct GetInterfaceDescriptionData { + const char *description; +} GetInterfaceDescriptionData; + +static int verb_introspect(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *interface; + int r; + + assert(argc == 3); + url = argv[1]; + interface = argv[2]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_callb(vl, "org.varlink.service.GetInterfaceDescription", &reply, &error, NULL, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("interface", interface))); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInterfaceDescription() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInterfaceDescription() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "description", JSON_VARIANT_STRING, json_dispatch_const_string, 0, JSON_MANDATORY }, + {} + }; + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + const char *description = NULL; + unsigned line = 0, column = 0; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &description); + if (r < 0) + return r; + + /* Try to parse the returned description, so that we can add syntax highlighting */ + r = varlink_idl_parse(ASSERT_PTR(description), &line, &column, &vi); + if (r < 0) { + log_warning_errno(r, "Failed to parse returned interface description at %u:%u, showing raw interface description: %m", line, column); + + fputs(description, stdout); + if (!endswith(description, "\n")) + fputs("\n", stdout); + } else { + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + } + } else + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + + return 0; +} + +static int reply_callback( + Varlink *link, + JsonVariant *parameters, + const char *error, + VarlinkReplyFlags flags, + void *userdata) { + + int r; + + assert(link); + + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call failed: %s", error); + } else + r = 0; + + json_variant_dump(parameters, arg_json_format_flags, stdout, NULL); + return r; +} + +static int verb_call(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *jp = NULL; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *method, *parameter; + unsigned line = 0, column = 0; + int r; + + assert(argc >= 3); + assert(argc <= 4); + url = argv[1]; + method = argv[2]; + parameter = argc > 3 && !streq(argv[3], "-") ? argv[3] : NULL; + + arg_json_format_flags &= ~JSON_FORMAT_OFF; + + if (parameter) { + r = json_parse_with_source(parameter, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } else { + r = json_parse_file_at(stdin, AT_FDCWD, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + if (arg_method_flags & VARLINK_METHOD_ONEWAY) { + r = varlink_send(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + r = varlink_flush(vl); + if (r < 0) + return log_error_errno(r, "Failed to flush Varlink connection: %m"); + + } else if (arg_method_flags & VARLINK_METHOD_MORE) { + + varlink_set_userdata(vl, (void*) method); + + r = varlink_bind_reply(vl, reply_callback); + if (r < 0) + return log_error_errno(r, "Failed to bind reply callback: %m"); + + r = varlink_observe(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + for (;;) { + r = varlink_is_idle(vl); + if (r < 0) + return log_error_errno(r, "Failed to check if varlink connection is idle: %m"); + if (r > 0) + break; + + r = varlink_process(vl); + if (r < 0) + return log_error_errno(r, "Failed to process varlink connection: %m"); + if (r != 0) + continue; + + r = varlink_wait(vl, USEC_INFINITY); + if (r < 0) + return log_error_errno(r, "Failed to wait for varlink connection events: %m"); + } + } else { + JsonVariant *reply = NULL; + const char *error = NULL; + + r = varlink_call(vl, method, jp, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + /* If the server returned an error to us, then fail, but first output the associated parameters */ + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call %s() failed: %s", method, error); + } else + r = 0; + + pager_open(arg_pager_flags); + + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + return r; + } + + return 0; +} + +static int verb_validate_idl(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + _cleanup_free_ char *text = NULL; + const char *fname; + unsigned line = 1, column = 1; + int r; + + fname = argc > 1 ? argv[1] : NULL; + + if (fname) { + r = read_full_file(fname, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description file '%s': %m", fname); + } else { + r = read_full_stream(stdin, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description from stdin: %m"); + + fname = ""; + } + + r = varlink_idl_parse(text, &line, &column, &vi); + if (r < 0) + return log_error_errno(r, "%s:%u:%u: Failed to parse interface description: %m", fname, line, column); + + r = varlink_idl_consistent(vi, LOG_ERR); + if (r < 0) + return r; + + pager_open(arg_pager_flags); + + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + + return 0; +} + +static int varlinkctl_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "info", 2, 2, 0, verb_info }, + { "list-interfaces", 2, 2, 0, verb_info }, + { "introspect", 3, 3, 0, verb_introspect }, + { "call", 3, 4, 0, verb_call }, + { "validate-idl", 1, 2, 0, verb_validate_idl }, + { "help", VERB_ANY, VERB_ANY, 0, verb_help }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return varlinkctl_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run);