diff --git a/man/rules/meson.build b/man/rules/meson.build
index abe2b1e92fc..a42143ec7cf 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1282,6 +1282,7 @@ manpages = [
['udev_new', '3', ['udev_ref', 'udev_unref'], ''],
['udevadm', '8', [], ''],
['ukify', '1', [], 'ENABLE_UKIFY'],
+ ['updatectl', '1', [], 'ENABLE_SYSUPDATE'],
['user@.service',
'5',
['systemd-user-runtime-dir', 'user-runtime-dir@.service'],
diff --git a/man/systemd-sysupdated.service.xml b/man/systemd-sysupdated.service.xml
index b7a4f3942a2..f28cc6c97f5 100644
--- a/man/systemd-sysupdated.service.xml
+++ b/man/systemd-sysupdated.service.xml
@@ -48,6 +48,7 @@
systemd1,
systemd-sysupdate8
+ updatectl1
diff --git a/man/updatectl.xml b/man/updatectl.xml
new file mode 100644
index 00000000000..144fc5a4182
--- /dev/null
+++ b/man/updatectl.xml
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+ updatectl
+ systemd
+
+
+
+ updatectl
+ 1
+
+
+
+ updatectl
+ Control the system update service
+
+
+
+
+ updatectl
+ OPTIONS
+ COMMAND
+ TARGET
+
+
+
+
+ Description
+
+ updatectl may be used to check for and install system updates managed by
+ systemd-sysupdated.service8.
+
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+ list [TARGET[@VERSION]]
+
+ Show information about targets and their versions.
+
+ When no TARGET is specified, this command lists all
+ available targets. When a TARGET is specified without a
+ VERSION, this command lists all known versions of the
+ specified target. If a VERSION is specified, this command
+ lists all known information about the specific version.
+
+ See the example below for details of the output.
+
+
+
+
+
+ check [TARGET…]
+
+ Check if any updates are available for the specified targets. If no targets
+ are specified, all available targets will be checked for updates.
+
+ See the example below for details of the output.
+
+
+
+
+
+ update [TARGET[@VERSION]…]
+
+ Update the specified targets to the specified versions. If a target
+ is specified without a version, then it will be updated to the latest version. If no targets are
+ specified, then all available targets will be updated to the latest version.
+
+
+
+
+
+ vacuum [TARGET…]
+
+ Clean up old versions of the specified targets. If no targets are specified,
+ all available targets will be vacuumed.
+
+
+
+
+
+
+
+
+
+
+ Options
+
+ The following commands are understood:
+
+
+
+
+
+ When used with the update command, reboots the system
+ after updates finish applying. If any update fails, the system will not reboot.
+
+
+
+
+
+
+
+ When used with the list command, disables fetching
+ metadata from the network. This makes the list command only return
+ information that is available locally (i.e. about versions already installed on the system).
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ Examples
+
+ Note that these examples are just here for demonstration purposes, and the output of these commands
+ is free to change. These commands are intended for display to a user. If you'd like machine-readable
+ output, use the
+ org.freedesktop.sysupdate15
+ D-Bus API directly.
+
+
+ Output from list
+
+ $ updatectl list
+TARGET VERSION PATH
+host 48 sysupdate.d
+machine:fedora 38 /var/lib/machines/fedora.raw
+component:shim 15.7 sysupdate.shim.d
+$ updatectl list host
+ VERSION STATUS
+↻ 50 candidate
+ 49 available
+● 48 current
+ 47 available
+ 46 available
+ 45 available
+[...]
+× 25 available+obsolete
+× 24 available+obsolete
+× 23 available+obsolete
+[...]
+$ updatectl list host@49
+↻ Version: 50
+ State: candidate
+ ChangeLog: https://vendor.com/os/v50.html
+
+TYPE PATH PTUUID PTFLAGS SHA256
+url-file http://dl.vendor.com/os/uki_50 - - 90f6534282dd720f7a222fa781086990dc9c83e5c7499f085970a8e75e3ac349
+url-file http://dl.vendor.com/os/usr_50_981a5b84-a301-c819-f681-3e575fe16f16 981a5b84-a301-c819-f681-3e575fe16f16 - c0596ab1095258ec6f16c7c281a50d71c419a9f587c1ef858cfbbb69fb0a16f3
+url-file http://dl.vendor.com/os/verity_50_2f8d0f3b-f80a-6ddc-a556-3722bfbb5b79 2f8d0f3b-f80a-6ddc-a556-3722bfbb5b79 - e1e90a128e038b3a53455e55d1ca717c743aba31fe6b4b4624109df0243c6338
+url-file http://dl.vendor.com/os/verity_sig_50 - - ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356
+
+
+
+
+ Checking for and installing updates
+
+ $ updatectl check
+TARGET UPDATE
+host 48 → 50
+machine:fedora 38 → 40
+$ updatectl update host machine:fedora@39
+[...]
+✓ host@50
+✓ machine:fedora@39
+
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd-sysupdate8
+ systemd-sysupdated.service8
+ sysupdate.d5,
+
+
+
+
diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build
index 8bd422fc433..9186a454f2a 100644
--- a/src/sysupdate/meson.build
+++ b/src/sysupdate/meson.build
@@ -13,6 +13,12 @@ systemd_sysupdate_sources = files(
'sysupdate.c',
)
+systemd_updatectl_sources = files(
+ 'sysupdate-update-set-flags.c',
+ 'sysupdate-util.c',
+ 'updatectl.c',
+)
+
executables += [
libexec_template + {
'name' : 'systemd-sysupdate',
@@ -37,6 +43,12 @@ executables += [
'sources' : files('sysupdated.c'),
'dependencies' : threads,
},
+ executable_template + {
+ 'name' : 'updatectl',
+ 'public' : true,
+ 'sources' : systemd_updatectl_sources,
+ 'conditions' : ['ENABLE_SYSUPDATE'],
+ },
]
if conf.get('ENABLE_SYSUPDATE') == 1
diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c
new file mode 100644
index 00000000000..39957ea1a01
--- /dev/null
+++ b/src/sysupdate/updatectl.c
@@ -0,0 +1,1274 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+
+#include "sd-bus.h"
+#include "sd-json.h"
+
+#include "build.h"
+#include "bus-error.h"
+#include "bus-label.h"
+#include "bus-locator.h"
+#include "bus-map-properties.h"
+#include "bus-util.h"
+#include "errno-list.h"
+#include "format-table.h"
+#include "json-util.h"
+#include "main-func.h"
+#include "pager.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "sysupdate-update-set-flags.h"
+#include "sysupdate-util.h"
+#include "terminal-util.h"
+#include "verbs.h"
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_reboot = false;
+static bool arg_offline = false;
+static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+static char *arg_host = NULL;
+
+#define SYSUPDATE_TARGET_INTERFACE "org.freedesktop.sysupdate1.Target"
+
+typedef struct Version {
+ char *version;
+ UpdateSetFlags flags;
+ char **changelog;
+ char *contents_json;
+} Version;
+
+static void version_done(Version *v) {
+ assert(v);
+
+ v->version = mfree(v->version);
+ v->changelog = strv_free(v->changelog);
+ v->flags = 0;
+ v->contents_json = mfree(v->contents_json);
+}
+
+typedef struct Operation {
+ void *userdata;
+
+ sd_bus *bus;
+ sd_event *event;
+ unsigned *remaining;
+
+ const char *target_path;
+ const char *target_id;
+
+ uint64_t job_id;
+ char *job_path;
+ sd_event_source *job_interrupt_source;
+ sd_bus_slot *job_properties_slot;
+ sd_bus_slot *job_finished_slot;
+} Operation;
+
+static Operation* operation_free(Operation *p) {
+ if (!p)
+ return NULL;
+
+ assert(*p->remaining > 0);
+ *p->remaining -= 1;
+ if (*p->remaining == 0)
+ /* We want to crash the program if we can't exit the loop
+ * cleanly, otherwise it will just hang */
+ assert_se(sd_event_exit(p->event, 0) >= 0);
+
+ free(p->job_path);
+
+ sd_event_source_disable_unref(p->job_interrupt_source);
+ sd_bus_slot_unref(p->job_properties_slot);
+ sd_bus_slot_unref(p->job_finished_slot);
+
+ return mfree(p);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_free);
+
+static Operation* operation_new(
+ void *userdata,
+ sd_bus *bus,
+ unsigned *remaining,
+ const char *target_path,
+ const char *target_id) {
+
+ _cleanup_(operation_freep) Operation *o = NULL;
+
+ o = new(Operation, 1);
+ if (!o)
+ return NULL;
+
+ *o = (Operation) {
+ .userdata = userdata,
+ .bus = bus,
+ .event = sd_bus_get_event(bus),
+ .remaining = remaining,
+ .target_path = target_path,
+ .target_id = target_id,
+ };
+ return TAKE_PTR(o);
+}
+
+static int ensure_targets(sd_bus *bus, char **argv, char ***ret_targets) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_strv_free_ char **targets = NULL;
+ int r;
+
+ assert(bus);
+ assert(ret_targets);
+
+ if (strv_isempty(argv)) {
+ const char *class, *name, *path;
+
+ r = bus_call_method(bus, bus_sysupdate_mgr, "ListTargets", &error, &reply, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call ListTargets: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_enter_container(reply, 'a', "(sso)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ while ((r = sd_bus_message_read(reply, "(sso)", &class, &name, &path)) > 0) {
+ _cleanup_free_ char *id = NULL;
+
+ if (streq(class, "host"))
+ id = strdup("host");
+ else
+ id = strjoin(class, ":", name);
+ if (!id)
+ return log_oom();
+
+ r = strv_consume(&targets, TAKE_PTR(id));
+ if (r < 0)
+ return log_oom();
+ }
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ } else {
+ r = strv_extend_strv(&targets, argv, true);
+ if (r < 0)
+ return log_oom();
+ }
+
+ *ret_targets = TAKE_PTR(targets);
+ return 0;
+}
+
+static int parse_target(
+ const char *in,
+ char **ret_bus_path,
+ char **ret_version) {
+ _cleanup_free_ char *id = NULL, *version = NULL, *escaped = NULL, *objpath = NULL;
+ const char *s;
+
+ /*
+ * Parses the TARGET[@VERSION] syntax from the command line into
+ * a bus object path and an optional version number.
+ */
+
+ assert(in);
+ assert(ret_bus_path);
+ assert(ret_version);
+
+ s = strrchr(in, '@');
+ if (s) {
+ version = strdup(s + 1);
+ if (!version)
+ return -ENOMEM;
+ id = strndup(in, s - in);
+ } else
+ id = strdup(in);
+ if (!id)
+ return -ENOMEM;
+
+ escaped = bus_label_escape(id);
+ if (!escaped)
+ return -ENOMEM;
+
+ objpath = strjoin("/org/freedesktop/sysupdate1/target/", escaped);
+ if (!objpath)
+ return -ENOMEM;
+
+ *ret_bus_path = TAKE_PTR(objpath);
+ *ret_version = TAKE_PTR(version);
+ return 0;
+}
+
+static int parse_targets(
+ char **targets,
+ size_t *ret_n,
+ char ***ret_bus_paths,
+ char ***ret_versions) {
+ _cleanup_strv_free_ char **bus_paths = NULL;
+ _cleanup_strv_free_ char **versions = NULL;
+ size_t n = 0;
+ int r;
+
+ assert(ret_bus_paths);
+ assert(ret_n);
+
+ if (strv_isempty(targets))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No targets found.");
+
+ STRV_FOREACH(id, targets) {
+ _cleanup_free_ char *bus_path = NULL, *version = NULL;
+
+ r = parse_target(*id, &bus_path, &version);
+ if (r < 0)
+ return log_oom();
+
+ if (version && !ret_versions)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unexpected version specifier in target: %s",
+ *id);
+
+ r = strv_extend(&bus_paths, strempty(bus_path));
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extend(&versions, strempty(version));
+ if (r < 0)
+ return log_oom();
+
+ n++;
+ }
+
+ *ret_n = n;
+ *ret_bus_paths = TAKE_PTR(bus_paths);
+ if (ret_versions)
+ *ret_versions = TAKE_PTR(versions);
+ return 0;
+}
+
+static int log_bus_error(int r, const sd_bus_error *error, const char *target, const char *action) {
+ assert(action);
+
+ if (r == 0) {
+ assert(sd_bus_error_is_set(error));
+ r = sd_bus_error_get_errno(error);
+ }
+
+ if (sd_bus_error_has_name(error, SD_BUS_ERROR_UNKNOWN_OBJECT)) {
+ if (target)
+ return log_error_errno(r, "Invalid target: %s", target);
+ return log_error_errno(r, "Invalid target");
+ }
+
+ if (target)
+ return log_error_errno(r, "Failed to %s for '%s': %s", action, target,
+ bus_error_message(error, r));
+ return log_error_errno(r, "Failed to %s: %s", action, bus_error_message(error, r));
+}
+
+static int list_targets(sd_bus *bus) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
+ size_t n;
+ int r;
+
+ assert(bus);
+
+ r = ensure_targets(bus, /* argv= */ NULL, &targets);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find targets: %m");
+
+ r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse targets: %m");
+
+ table = table_new("target", "version", "path");
+ if (!table)
+ return log_oom();
+
+ for (size_t i = 0; i < n; i++) {
+ char *version = NULL;
+ _cleanup_free_ char *path = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+
+ r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination,
+ target_paths[i], SYSUPDATE_TARGET_INTERFACE,
+ "GetVersion", &error, &reply, NULL);
+ if (r < 0)
+ return log_bus_error(r, &error, targets[i], "get current version");
+ r = sd_bus_message_read_basic(reply, 's', &version);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_get_property_string(bus, bus_sysupdate_mgr->destination,
+ target_paths[i], SYSUPDATE_TARGET_INTERFACE,
+ "Path", &error, &path);
+ if (r < 0)
+ return log_bus_error(r, &error, targets[i], "get target bus path");
+
+ r = table_add_many(table,
+ TABLE_STRING, targets[i],
+ TABLE_STRING, empty_to_dash(version),
+ TABLE_STRING, path);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
+}
+
+static int parse_describe(sd_bus_message *reply, Version *ret) {
+ Version v = {};
+ char *version_json = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL, *contents_json = NULL;
+ bool newest = false, available = false, installed = false, obsolete = false, protected = false;
+ int r;
+
+ assert(reply);
+ assert(ret);
+
+ r = sd_bus_message_read_basic(reply, 's', &version_json);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_json_parse(version_json, 0, &json, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ assert(sd_json_variant_is_object(json));
+
+ r = sd_json_dispatch(json,
+ (const sd_json_dispatch_field[]) {
+ { "version", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, PTR_TO_SIZE(&v.version), 0 },
+ { "newest", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, PTR_TO_SIZE(&newest), 0 },
+ { "available", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, PTR_TO_SIZE(&available), 0 },
+ { "installed", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, PTR_TO_SIZE(&installed), 0 },
+ { "obsolete", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, PTR_TO_SIZE(&obsolete), 0 },
+ { "protected", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, PTR_TO_SIZE(&protected), 0 },
+ { "changelog_urls", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, PTR_TO_SIZE(&v.changelog), 0 },
+ { "contents", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, PTR_TO_SIZE(&contents_json), 0 },
+ {},
+ },
+ SD_JSON_ALLOW_EXTENSIONS,
+ /* userdata= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ SET_FLAG(v.flags, UPDATE_NEWEST, newest);
+ SET_FLAG(v.flags, UPDATE_AVAILABLE, available);
+ SET_FLAG(v.flags, UPDATE_INSTALLED, installed);
+ SET_FLAG(v.flags, UPDATE_OBSOLETE, obsolete);
+ SET_FLAG(v.flags, UPDATE_PROTECTED, protected);
+
+ r = sd_json_variant_format(contents_json, 0, &v.contents_json);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format JSON for contents: %m");
+
+ *ret = TAKE_STRUCT(v);
+ return 0;
+}
+
+static int list_versions_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ Table *table = ASSERT_PTR(op->userdata);
+ const sd_bus_error *e;
+ _cleanup_(version_done) Version v = {};
+ _cleanup_free_ char *version_link = NULL;
+ const char *color;
+ int r;
+
+ assert(reply);
+
+ e = sd_bus_message_get_error(reply);
+ if (e)
+ return log_bus_error(0, e, NULL, "call Describe");
+
+ r = parse_describe(reply, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse Describe output: %m");
+
+ color = update_set_flags_to_color(v.flags);
+
+ if (urlify_enabled() && !strv_isempty(v.changelog)) {
+ version_link = strjoin(v.version, special_glyph(SPECIAL_GLYPH_EXTERNAL_LINK));
+ if (!version_link)
+ return log_oom();
+ }
+
+ r = table_add_many(table,
+ TABLE_STRING, update_set_flags_to_glyph(v.flags),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, version_link ?: v.version,
+ TABLE_SET_COLOR, color,
+ TABLE_SET_URL, strv_isempty(v.changelog) ? NULL : v.changelog[0],
+ TABLE_STRING, update_set_flags_to_string(v.flags),
+ TABLE_SET_COLOR, color);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int list_versions(sd_bus *bus, const char *target_path) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_strv_free_ char **versions = NULL;
+ unsigned remaining = 0;
+ int r;
+
+ r = sd_bus_call_method(
+ bus,
+ bus_sysupdate_mgr->destination,
+ target_path,
+ SYSUPDATE_TARGET_INTERFACE,
+ "List",
+ &error,
+ &reply,
+ "t",
+ arg_offline ? SD_SYSTEMD_SYSUPDATE_OFFLINE : 0);
+ if (r < 0)
+ return log_bus_error(r, &error, NULL, "call List");
+
+ r = sd_bus_message_read_strv(reply, &versions);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ table = table_new("", "version", "status");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_sort(table, 1);
+ (void) table_set_reverse(table, 1, true);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ r = sd_bus_attach_event(bus, event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ r = sd_event_set_signal_exit(event, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up interrupt handler: %m");
+
+ STRV_FOREACH(version, versions) {
+ _cleanup_(operation_freep) Operation *op = NULL;
+ op = operation_new(table, bus, &remaining, NULL, NULL);
+ if (!op)
+ return log_oom();
+
+ r = sd_bus_call_method_async(bus,
+ NULL,
+ bus_sysupdate_mgr->destination,
+ target_path,
+ SYSUPDATE_TARGET_INTERFACE,
+ "Describe",
+ list_versions_finished,
+ op,
+ "st",
+ *version,
+ arg_offline ? SD_SYSTEMD_SYSUPDATE_OFFLINE : 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call Describe: %m");
+ TAKE_PTR(op);
+
+ remaining++;
+ }
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start event loop: %m");
+
+ return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
+}
+
+static int describe(sd_bus *bus, const char *target_path, const char *version) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+ _cleanup_(version_done) Version v = {};
+ sd_json_variant *entry;
+ const char *color;
+ int r;
+
+ r = sd_bus_call_method(
+ bus,
+ bus_sysupdate_mgr->destination,
+ target_path,
+ SYSUPDATE_TARGET_INTERFACE,
+ "Describe",
+ &error,
+ &reply,
+ "st",
+ version,
+ arg_offline ? SD_SYSTEMD_SYSUPDATE_OFFLINE : 0);
+ if (r < 0)
+ return log_bus_error(r, &error, NULL, "call Describe");
+
+ r = parse_describe(reply, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse Describe output: %m");
+
+ color = strempty(update_set_flags_to_color(v.flags));
+
+ printf("%s%s%s Version: %s\n"
+ " State: %s%s%s\n",
+ color,
+ update_set_flags_to_glyph(v.flags),
+ ansi_normal(),
+ v.version,
+ color,
+ update_set_flags_to_string(v.flags),
+ ansi_normal());
+
+ STRV_FOREACH(url, v.changelog) {
+ _cleanup_free_ char *changelog_link = NULL;
+
+ r = terminal_urlify(*url, NULL, &changelog_link);
+ if (r < 0)
+ return log_error_errno(r, "Could not urlify link to change-log: %m");
+
+ printf("ChangeLog: %s\n", strna(changelog_link));
+ }
+ printf("\n");
+
+ r = sd_json_parse(v.contents_json, 0, &json, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ assert(sd_json_variant_is_array(json));
+
+ JSON_VARIANT_ARRAY_FOREACH(entry, json) {
+ assert(sd_json_variant_is_object(entry));
+ const char *key;
+ sd_json_variant *value;
+
+ if (!table) {
+ table = table_new_raw(sd_json_variant_elements(entry) / 2);
+ if (!table)
+ return log_oom();
+
+ JSON_VARIANT_OBJECT_FOREACH(key, value, entry) {
+
+ r = table_add_cell(table, NULL, TABLE_HEADER, key);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+
+ JSON_VARIANT_OBJECT_FOREACH(key, value, entry) {
+ TableDataType type;
+ uint64_t number;
+ bool boolean;
+ const void *data;
+
+ if (sd_json_variant_is_string(value)) {
+ type = TABLE_STRING;
+ assert_se(data = sd_json_variant_string(value));
+ } else if (sd_json_variant_is_unsigned(value)) {
+ type = TABLE_UINT64;
+ number = sd_json_variant_unsigned(value);
+ data = &number;
+ } else if (sd_json_variant_is_boolean(value)) {
+ type = TABLE_BOOLEAN;
+ boolean = sd_json_variant_boolean(value);
+ data = &boolean;
+ } else if (sd_json_variant_is_null(value)) {
+ type = TABLE_EMPTY;
+ data = NULL;
+ } else
+ assert_not_reached();
+
+ if (streq(key, "ptflags"))
+ type = TABLE_UINT64_HEX;
+ else if (streq(key, "size"))
+ type = TABLE_SIZE;
+ else if (streq(key, "mode"))
+ type = TABLE_MODE;
+ else if (streq(key, "mtime"))
+ type = TABLE_TIMESTAMP;
+
+ r = table_add_cell(table, NULL, type, data);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+
+ return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
+}
+
+static int verb_list(int argc, char **argv, void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_free_ char *target_path = NULL, *version = NULL;
+ int r;
+
+ if (argc == 1)
+ return list_targets(bus);
+
+ r = parse_target(argv[1], &target_path, &version);
+ if (r < 0)
+ return log_oom();
+
+ if (version)
+ return describe(bus, target_path, version);
+ else
+ return list_versions(bus, target_path);
+}
+
+static int check_describe_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ Table *table = ASSERT_PTR(op->userdata);
+ _cleanup_(version_done) Version v = {};
+ _cleanup_free_ char *update = NULL;
+ const sd_bus_error *e;
+ sd_bus_error error = {};
+ const char *lnk = NULL;
+ char *current;
+ int r;
+
+ assert(reply);
+
+ e = sd_bus_message_get_error(reply);
+ if (e)
+ return log_bus_error(0, e, NULL, "call Describe");
+
+ r = parse_describe(reply, &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse output of Describe: %m");
+
+ r = sd_bus_call_method(
+ op->bus,
+ bus_sysupdate_mgr->destination,
+ op->target_path,
+ SYSUPDATE_TARGET_INTERFACE,
+ "GetVersion",
+ &error,
+ &reply,
+ NULL);
+ if (r < 0)
+ return log_bus_error(r, &error, op->target_id, "get current version");
+
+ r = sd_bus_message_read_basic(reply, 's', ¤t);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (urlify_enabled() && !strv_isempty(v.changelog))
+ lnk = special_glyph(SPECIAL_GLYPH_EXTERNAL_LINK);
+
+ update = strjoin(empty_to_dash(current), " ",
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), " ",
+ v.version, strempty(lnk));
+ if (!update)
+ return log_oom();
+
+ r = table_add_many(table,
+ TABLE_STRING, op->target_id,
+ TABLE_STRING, update,
+ TABLE_SET_URL, strv_isempty(v.changelog) ? NULL : v.changelog[0]);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+static int check_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ const sd_bus_error *e;
+ const char *new_version = NULL;
+ int r;
+
+ assert(reply);
+
+ e = sd_bus_message_get_error(reply);
+ if (e)
+ return log_bus_error(0, e, op->target_id, "call CheckNew");
+
+ r = sd_bus_message_read(reply, "s", &new_version);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (isempty(new_version))
+ return 0;
+
+ r = sd_bus_call_method_async(op->bus,
+ NULL,
+ bus_sysupdate_mgr->destination,
+ op->target_path,
+ SYSUPDATE_TARGET_INTERFACE,
+ "Describe",
+ check_describe_finished,
+ op,
+ "st",
+ new_version,
+ arg_offline ? SD_SYSTEMD_SYSUPDATE_OFFLINE : 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call Describe: %m");
+ TAKE_PTR(op);
+
+ return 0;
+}
+
+static int verb_check(int argc, char **argv, void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
+ size_t n;
+ unsigned remaining = 0;
+ int r;
+
+ r = ensure_targets(bus, argv + 1, &targets);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find targets: %m");
+
+ r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse targets: %m");
+
+ table = table_new("target", "update");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_sort(table, 0);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ r = sd_bus_attach_event(bus, event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ r = sd_event_set_signal_exit(event, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up interrupt handler: %m");
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_(operation_freep) Operation *op = NULL;
+ op = operation_new(table, bus, &remaining, target_paths[i], targets[i]);
+ if (!op)
+ return log_oom();
+
+ r = sd_bus_call_method_async(bus, NULL, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "CheckNew", check_finished, op, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call CheckNew for target %s: %m", targets[i]);
+ TAKE_PTR(op);
+
+ remaining++;
+ }
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start event loop: %m");
+
+ return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
+}
+
+#define UPDATE_PROGRESS_FAILED INT_MIN
+/* Make sure it doesn't overlap w/ errno values */
+assert_cc(UPDATE_PROGRESS_FAILED < -ERRNO_MAX);
+
+static int update_render_progress(sd_event_source *source, void *userdata) {
+ OrderedHashmap *map = ASSERT_PTR(userdata);
+ const char *target;
+ void *p;
+ unsigned total;
+ size_t n;
+ bool exiting;
+
+ exiting = sd_event_get_state(sd_event_source_get_event(source)) == SD_EVENT_EXITING;
+
+ total = 0;
+ n = ordered_hashmap_size(map);
+
+ if (n == 0)
+ return 0;
+
+ if (!terminal_is_dumb()) {
+ for (size_t i = 0; i <= n; i++)
+ fputs("\n", stderr); /* Possibly scroll the terminal to make room (including total)*/
+
+ fprintf(stderr, "\e[%zuF", n+1); /* Go back */
+
+ fputs("\e7", stderr); /* Save cursor position */
+ fputs("\e[?25l", stderr); /* Hide cursor */
+ }
+
+ ORDERED_HASHMAP_FOREACH_KEY(p, target, map) {
+ int progress = PTR_TO_INT(p);
+
+ if (progress == UPDATE_PROGRESS_FAILED) {
+ fprintf(stderr, "%s %s\n", RED_CROSS_MARK(), target);
+ total += 100;
+ } else if (progress == -EALREADY) {
+ fprintf(stderr, "%s %s (Already up-to-date)\n", GREEN_CHECK_MARK(), target);
+ n--; /* Don't consider this target in the total */
+ } else if (progress < 0) {
+ fprintf(stderr, "%s %s (%s)\n", RED_CROSS_MARK(), target, STRERROR(progress));
+ total += 100;
+ } else {
+ draw_progress_bar(target, progress);
+ fputs("\n", stderr);
+ total += progress;
+ }
+ }
+
+ if (n > 1) {
+ draw_progress_bar("TOTAL", (double) total / n);
+ fputs("\n", stderr);
+ }
+
+ if (!terminal_is_dumb()) {
+ if (exiting)
+ fputs("\e[?25h", stderr); /* Show cursor again */
+ else
+ fputs("\e8", stderr); /* Restore cursor position */
+ } else if (!exiting)
+ fputs("------\n", stderr);
+
+ fflush(stderr);
+ return 0;
+}
+
+static int update_properties_changed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ Operation *op = ASSERT_PTR(userdata);
+ OrderedHashmap *map = ASSERT_PTR(op->userdata);
+ const char *interface;
+ uint32_t progress = UINT32_MAX;
+ static const struct bus_properties_map prop_map[] = {
+ { "Progress", "u", NULL, 0 },
+ {}
+ };
+ int r;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "s", &interface);
+ if (r < 0) {
+ bus_log_parse_error_debug(r);
+ return 0;
+ }
+
+ if (!streq(interface, "org.freedesktop.sysupdate1.Job"))
+ return 0;
+
+ r = bus_message_map_all_properties(m, prop_map, /* flags= */ 0, error, &progress);
+ if (r < 0)
+ return 0; /* map_all_properties does the debug logging internally... */
+
+ if (progress == UINT_MAX)
+ return 0;
+
+ r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR((int) progress));
+ if (r < 0)
+ log_debug_errno(r, "Failed to update hashmap: %m");
+ return 0;
+}
+
+static int update_finished(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ OrderedHashmap *map = ASSERT_PTR(op->userdata);
+ uint64_t id;
+ int r, status;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "toi", &id, NULL, &status);
+ if (r < 0) {
+ bus_log_parse_error_debug(r);
+ return 0;
+ }
+
+ if (id != op->job_id) {
+ TAKE_PTR(op);
+ return 0;
+ }
+
+ if (status == 0) /* success */
+ status = 100;
+ else if (status > 0) /* exit status without errno */
+ status = UPDATE_PROGRESS_FAILED; /* i.e. EXIT_FAILURE */
+ /* else errno */
+
+ r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(status));
+ if (r < 0)
+ log_debug_errno(r, "Failed to update hashmap: %m");
+ return 0;
+}
+
+static int update_interrupted(sd_event_source *source, void *userdata) {
+ /* Since the event loop is exiting, we will never recieve the JobRemoved
+ * signal. So, we must free the userdata here. */
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ OrderedHashmap *map = ASSERT_PTR(op->userdata);
+ int r;
+
+ r = sd_bus_call_method(op->bus,
+ bus_sysupdate_mgr->destination,
+ op->job_path,
+ "org.freedesktop.sysupdate1.Job",
+ "Cancel",
+ &error, /* reply= */ NULL,
+ NULL);
+ if (r < 0)
+ return log_bus_error(r, &error, NULL, "call Cancel");
+
+ r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(-ECANCELED));
+ if (r < 0)
+ log_debug_errno(r, "Failed to update hashmap: %m");
+
+ return 0;
+}
+
+static int update_started(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
+ _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
+ OrderedHashmap *map = ASSERT_PTR(op->userdata);
+ const sd_bus_error *e;
+ _cleanup_free_ char *key = NULL;
+ const char *new_version, *job_path;
+ int r;
+
+ assert(reply);
+
+ e = sd_bus_message_get_error(reply);
+ if (e) {
+ r = -sd_bus_error_get_errno(e);
+
+ key = strdup(op->target_id);
+ if (!key)
+ return log_oom();
+ r = ordered_hashmap_put(map, key, INT_TO_PTR(r));
+ if (r < 0)
+ return log_error_errno(r, "Failed to update hashmap: %m");
+ TAKE_PTR(key);
+
+ return 0;
+ }
+
+ r = sd_bus_message_read(reply, "sto", &new_version, &op->job_id, &job_path);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ op->job_path = strdup(job_path);
+ if (!op->job_path)
+ return log_oom();
+ if (isempty(new_version))
+ new_version = "latest";
+
+ /* Register this job into the hashmap. This will give it a progress bar */
+ if (strchr(op->target_id, '@'))
+ key = strdup(op->target_id);
+ else
+ key = strjoin(op->target_id, "@", new_version);
+ if (!key)
+ return log_oom();
+ r = ordered_hashmap_put(map, key, INT_TO_PTR(0)); /* takes ownership of key */
+ if (r < 0)
+ return log_error_errno(r, "Failed to add target to tracking map: %m");
+ op->target_id = TAKE_PTR(key); /* just borrowing */
+
+ /* Cancel the job if the event loop exits */
+ r = sd_event_add_exit(op->event, &op->job_interrupt_source, update_interrupted, op);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up interrupt handler: %m");
+
+ /* We need to cancel the job before the final iteration of the renderer runs */
+ r = sd_event_source_set_priority(op->job_interrupt_source, SD_EVENT_PRIORITY_IMPORTANT);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set interrupt priority: %m");
+
+ /* Register for progress notifications */
+ r = sd_bus_match_signal_async(
+ op->bus,
+ &op->job_properties_slot,
+ bus_sysupdate_mgr->destination,
+ job_path,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ update_properties_changed,
+ NULL,
+ op);
+ if (r < 0)
+ return log_bus_error(r, NULL, op->target_id, "listen for PropertiesChanged");
+
+ TAKE_PTR(op); /* update_finished/update_interrupted take ownership of the data */
+
+ return 0;
+}
+
+static int verb_update(int argc, char **argv, void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *render_exit = NULL;
+ _cleanup_ordered_hashmap_free_ OrderedHashmap *map = NULL;
+ _cleanup_strv_free_ char **targets = NULL, **versions = NULL, **target_paths = NULL;
+ size_t n;
+ unsigned remaining = 0;
+ void *p;
+ bool did_anything = false;
+ int r;
+
+ r = ensure_targets(bus, argv + 1, &targets);
+ if (r < 0)
+ return log_error_errno(r, "Could not find targets: %m");
+
+ r = parse_targets(targets, &n, &target_paths, &versions);
+ if (r < 0)
+ return log_error_errno(r, "Could not parse targets: %m");
+
+ map = ordered_hashmap_new(&string_hash_ops_free);
+ if (!map)
+ return log_oom();
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ r = sd_bus_attach_event(bus, event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ r = sd_event_set_signal_exit(event, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up interrupt handler: %m");
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_(operation_freep) Operation *op = NULL;
+ op = operation_new(map, bus, &remaining, target_paths[i], targets[i]);
+ if (!op)
+ return log_oom();
+
+ /* Sign up for notification when the associated job finishes */
+ r = bus_match_signal_async(
+ op->bus, &op->job_finished_slot, bus_sysupdate_mgr, "JobRemoved", update_finished, NULL, op);
+ if (r < 0)
+ return log_bus_error(r, NULL, op->target_id, "listen for JobRemoved");
+
+ r = sd_bus_call_method_async(
+ bus,
+ NULL,
+ bus_sysupdate_mgr->destination,
+ target_paths[i],
+ SYSUPDATE_TARGET_INTERFACE,
+ "Update",
+ update_started,
+ op,
+ "st",
+ versions[i],
+ 0LU);
+ if (r < 0)
+ return log_bus_error(r, NULL, targets[i], "call Update");
+ TAKE_PTR(op);
+
+ remaining++;
+ }
+
+ /* Set up the rendering */
+ r = sd_event_add_post(event, NULL, update_render_progress, map);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add progress rendering callback: %m");
+
+ r = sd_event_add_exit(event, &render_exit, update_render_progress, map);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add exit callback: %m");
+
+ r = sd_event_source_set_priority(render_exit, SD_EVENT_PRIORITY_IDLE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set priority of update job");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start event loop");
+
+ ORDERED_HASHMAP_FOREACH(p, map) {
+ r = PTR_TO_INT(p);
+ if (r == -EALREADY)
+ continue;
+ if (r == UPDATE_PROGRESS_FAILED)
+ return EXIT_FAILURE;
+ if (r < 0)
+ return r;
+
+ did_anything = true;
+ }
+
+ if (arg_reboot) {
+ if (did_anything)
+ return reboot_now();
+ log_info("Nothing was updated... skipping reboot.");
+ }
+
+ return 0;
+}
+
+static int verb_vacuum(int argc, char **argv, void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
+ size_t n;
+ int r;
+
+ r = ensure_targets(bus, argv + 1, &targets);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find targets: %m");
+
+ r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse targets: %m");
+
+ for (size_t i = 0; i < n; i++) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ uint32_t count;
+
+ r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
+ if (r < 0)
+ return log_bus_error(r, &error, targets[i], "call Vacuum");
+
+ r = sd_bus_message_read(reply, "u", &count);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ log_info("Deleted %u instance(s) of %s.\n", count, targets[i]);
+ }
+ return 0;
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("updatectl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] [VERSION]\n"
+ "\n%5$sManage system updates.%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " list [TARGET[@VERSION]] List available targets and versions\n"
+ " check [TARGET...] Check for updates\n"
+ " update [TARGET[@VERSION]...] Install updates\n"
+ " vacuum [TARGET...] Clean up old updates\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ "\n%3$sOptions:%4$s\n"
+ " --reboot Reboot after updating to newer version\n"
+ " --offline Do not fetch metadata from the network\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\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 parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_REBOOT,
+ ARG_OFFLINE,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "host", required_argument, NULL, 'H' },
+ { "reboot", no_argument, NULL, ARG_REBOOT },
+ { "offline", no_argument, NULL, ARG_OFFLINE },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hH:M:", 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_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case ARG_REBOOT:
+ arg_reboot = true;
+ break;
+
+ case ARG_OFFLINE:
+ arg_offline = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ static const Verb verbs[] = {
+ { "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
+ { "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
+ { "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
+ { "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
+ {}
+ };
+
+ setlocale(LC_ALL, "");
+ log_setup();
+
+ (void) signal(SIGWINCH, columns_lines_cache_reset);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, &bus);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ if (arg_transport == BUS_TRANSPORT_LOCAL)
+ polkit_agent_open();
+
+ return dispatch_verb(argc, argv, verbs, bus);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);