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