]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
varlinkctl: add new varlinkctl tool
authorLennart Poettering <lennart@poettering.net>
Fri, 22 Sep 2023 20:44:28 +0000 (22:44 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 6 Oct 2023 09:49:38 +0000 (11:49 +0200)
man/busctl.xml
man/rules/meson.build
man/varlinkctl.xml [new file with mode: 0644]
meson.build
src/varlinkctl/meson.build [new file with mode: 0644]
src/varlinkctl/varlinkctl.c [new file with mode: 0644]

index ce23dd1b33b88033b98fa5718fe94bce9ba53c41..cc404529ae041f76be9d483081d3e2427da2e0bf 100644 (file)
@@ -582,6 +582,7 @@ o "/org/freedesktop/systemd1/job/42684"</programlisting>
       <citerefentry project='dbus'><refentrytitle>dbus-daemon</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
       <ulink url="https://www.freedesktop.org/wiki/Software/dbus">D-Bus</ulink>,
       <citerefentry><refentrytitle>sd-bus</refentrytitle><manvolnum>3</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
       <citerefentry project='die-net'><refentrytitle>wireshark</refentrytitle><manvolnum>1</manvolnum></citerefentry>
index 3454668cab9456fd737bbeb09f497be0c20f3040..3265db9c2dc6bb8b4da35b0adb8b328bc604193a 100644 (file)
@@ -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 (file)
index 0000000..5d0ec6a
--- /dev/null
@@ -0,0 +1,315 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="varlinkctl"
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>varlinkctl</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>varlinkctl</refentrytitle>
+    <manvolnum>1</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>varlinkctl</refname>
+    <refpurpose>Introspect with and invoke Varlink services</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">info</arg>
+      <arg choice="plain"><replaceable>ADDRESS</replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">list-interfaces</arg>
+      <arg choice="plain"><replaceable>ADDRESS</replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">introspect</arg>
+      <arg choice="plain"><replaceable>ADDRESS</replaceable></arg>
+      <arg choice="plain"><replaceable>INTERFACE</replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">call</arg>
+      <arg choice="plain"><replaceable>ADDRESS</replaceable></arg>
+      <arg choice="plain"><replaceable>METHOD</replaceable></arg>
+      <arg choice="opt"><replaceable>PARAMETERS</replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">validate-idl</arg>
+      <arg choice="opt"><replaceable>FILE</replaceable></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>varlinkctl</command> may be used to introspect and invoke <ulink
+    url="https://varlink.org/">Varlink</ulink> services.</para>
+
+    <para>Services are referenced by one of the following:</para>
+
+    <itemizedlist>
+      <listitem><para>A Varlink service reference starting with the <literal>unix:</literal> string, followed
+      by an absolute <constant>AF_UNIX</constant> path, or by <literal>@</literal> and an arbitrary string
+      (the latter for referencing sockets in the abstract namespace).</para></listitem>
+
+      <listitem><para>A Varlink service reference starting with the <literal>exec:</literal> string, followed
+      by an absolute path of a binary to execute.</para></listitem>
+    </itemizedlist>
+
+    <para>For convenience these two simpler (redundant) service address syntaxes are also supported:</para>
+
+    <itemizedlist>
+      <listitem><para>A file system path to an <constant>AF_UNIX</constant> socket, either absolute
+      (i.e. begins with <literal>/</literal>) or relative (in which case it must begin with
+      <literal>./</literal>).</para></listitem>
+
+      <listitem><para>A file system path to an executable, either absolute or relative (as above, must begin
+      with <literal>/</literal>, resp. <literal>./</literal>).</para></listitem>
+    </itemizedlist>
+  </refsect1>
+
+  <refsect1>
+    <title>Commands</title>
+
+    <para>The following commands are understood:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><command>info</command> <replaceable>ADDRESS</replaceable></term>
+
+        <listitem><para>Show brief information about the specified service, including vendor name and list of
+        implemented interfaces. Expects a service address in the formats described above.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>list-interfaces</command> <replaceable>ADDRESS</replaceable></term>
+
+        <listitem><para>Show list of interfaces implemented by the specified service. Expects a service
+        address in the formats described above.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>introspect</command> <replaceable>ADDRESS</replaceable> <replaceable>INTERFACE</replaceable></term>
+
+        <listitem><para>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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>call</command> <replaceable>ADDRESS</replaceable> <replaceable>METHOD</replaceable> [<replaceable>ARGUMENTS</replaceable>]</term>
+
+        <listitem><para>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 <literal>{}</literal>.</para>
+
+        <para>The reply parameters are written as JSON object to STDOUT.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>validate-idl</command> [<replaceable>FILE</replaceable>]</term>
+
+        <listitem><para>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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>help</command></term>
+
+        <listitem><para>Show command syntax help.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>The following options are understood:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>--more</option></term>
+
+        <listitem><para>When used with <command>call</command>: expect multiple method replies. If this flag is
+        set the method call is sent with the <constant>more</constant> 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.</para>
+
+        <para>If this mode is enabled output is automatically switched to JSON-SEQ mode, so that individual
+        reply objects can be easily discerned.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--oneway</option></term>
+
+        <listitem><para>When used with <command>call</command>: do not expect a method reply. If this flag
+        is set the method call is sent with the <constant>oneway</constant> flag set (the command exits
+        immediately after), which tells the service not to generate a reply.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--json=</option><replaceable>MODE</replaceable></term>
+
+        <listitem>
+          <para>Selects the JSON output formatting, one of <literal>pretty</literal> (for nicely indented,
+          colorized output) or <literal>short</literal> (for terse output with minimal whitespace and no
+          newlines), defaults to <literal>short</literal>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>-j</option></term>
+
+        <listitem>
+          <para>Equivalent to <option>--json=pretty</option> when invoked interactively from a terminal. Otherwise
+          equivalent to <option>--json=short</option>, in particular when the output is piped to some other
+          program.</para>
+
+        <xi:include href="version-info.xml" xpointer="v255"/>
+        </listitem>
+      </varlistentry>
+
+      <xi:include href="standard-options.xml" xpointer="no-pager" />
+      <xi:include href="standard-options.xml" xpointer="help" />
+      <xi:include href="standard-options.xml" xpointer="version" />
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Examples</title>
+
+    <example>
+      <title>Investigating a Service</title>
+
+      <para>The following three commands inspect the <literal>io.systemd.Resolve</literal> service
+      implemented by
+      <citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      listing general service information and implemented interfaces, and then displaying the interface
+      definition of its primary interface:</para>
+
+      <programlisting>$ 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,
+        …</programlisting>
+
+    <para>(Interface definition has been truncated in the example above, in the interest of brevity.)</para>
+    </example>
+
+    <example>
+      <title>Invoking a Method</title>
+
+      <para>The following command resolves a hostname via <citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>'s <function>ResolveHostname</function>  method call.</para>
+
+      <programlisting>$ 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
+}</programlisting>
+    </example>
+
+    <example>
+      <title>Investigating a Service Executable</title>
+
+      <para>The following comand inspects the <filename>/usr/lib/systemd/systemd-pcrextend</filename>
+      executable and the IPC APIs it provides. It then invokes a method on it:</para>
+
+      <programlisting># 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"}'
+{}</programlisting>
+    </example>
+
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+
+    <para>
+      <citerefentry><refentrytitle>busctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <ulink url="https://varlink.org/">Varlink</ulink>
+    </para>
+  </refsect1>
+</refentry>
index 88bc1367e05069847a243284206ed5cb7a6ff761..5e9ca285acd70517b8f4e25a500c3450132a8170 100644 (file)
@@ -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 (file)
index 0000000..c1074dc
--- /dev/null
@@ -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 (file)
index 0000000..a656bfc
--- /dev/null
@@ -0,0 +1,520 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#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, "<argv[4]>", 0, &jp, &line, &column);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse parameters at <argv[4]>:%u:%u: %m", line, column);
+        } else {
+                r = json_parse_file_at(stdin, AT_FDCWD, "<stdin>", 0, &jp, &line, &column);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse parameters at <stdin>:%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 = "<stdin>";
+        }
+
+        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);