From bf2c741fd772af6f04b4fa234ada2d364f9a5d6c Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Thu, 29 Jun 2023 22:58:41 -0400 Subject: [PATCH 1/3] sysupdate: Implement systemd-sysupdated dbus service Co-authored-by: Tom Coldrick Co-authored-by: Abderrahim Kitouni --- man/org.freedesktop.sysupdate1.xml | 487 +++++ man/rules/meson.build | 5 + man/systemd-sysupdate.xml | 1 + man/systemd-sysupdated.service.xml | 54 + meson.build | 1 + po/POTFILES.in | 1 + src/shared/bus-locator.c | 6 + src/shared/bus-locator.h | 1 + src/sysupdate/meson.build | 16 + src/sysupdate/org.freedesktop.sysupdate1.conf | 88 + .../org.freedesktop.sysupdate1.policy | 74 + .../org.freedesktop.sysupdate1.service | 14 + src/sysupdate/sysupdate-util.h | 2 + src/sysupdate/sysupdate.c | 2 +- src/sysupdate/sysupdated.c | 1911 +++++++++++++++++ units/meson.build | 5 + units/systemd-sysupdated.service.in | 30 + 17 files changed, 2697 insertions(+), 1 deletion(-) create mode 100644 man/org.freedesktop.sysupdate1.xml create mode 100644 man/systemd-sysupdated.service.xml create mode 100644 src/sysupdate/org.freedesktop.sysupdate1.conf create mode 100644 src/sysupdate/org.freedesktop.sysupdate1.policy create mode 100644 src/sysupdate/org.freedesktop.sysupdate1.service create mode 100644 src/sysupdate/sysupdated.c create mode 100644 units/systemd-sysupdated.service.in diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml new file mode 100644 index 0000000000..ac0e9152a1 --- /dev/null +++ b/man/org.freedesktop.sysupdate1.xml @@ -0,0 +1,487 @@ + + + + + + + org.freedesktop.sysupdate1 + systemd + + + + org.freedesktop.sysupdate1 + 5 + + + + org.freedesktop.sysupdate1 + The D-Bus interface of systemd-sysupdated + + + + Introduction + + + systemd-sysupdated.service8 + is a system service that allows unprivileged clients to update the system. This page describes the D-Bus + interface. + + + + The Manager Object + + The service exposes the following interfaces on the Manager object on the bus: + + +node /org/freedesktop/sysupdate1 { + interface org.freedesktop.sysupdate1.Manager { + methods: + ListTargets(out a(sso) targets); + ListJobs(out a(tsuo) jobs); + ListAppStream(out as urls); + signals: + JobRemoved(t id, + o path, + i status); + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + + + + + + + + + + + + + + + + + + + + Methods + + ListTargets() returns a list all known update targets. It returns + an array of structures which consist of a string indicating the target's class (see Target's + Class property below for an explanation of the possible values), a string + with the name of the target, and the target object path. + + ListJobs() returns a list all ongoing jobs. It returns + an array of structures which consist of a numeric job ID, a string indicating the job type (see Job's + Type property below for an explanation of the possible values), the job's progress, + and the job's object path. + + ListAppStream() returns an array of all the appstream catalog URLs that this + service knows about. See Target's GetAppStream() method below for more + details. + + + + Signals + + The JobRemoved() signal is sent each time a job finishes, + is canceled or fails. It also carries the job ID and object path, followed by a numeric status + code. If the status is zero, the job has succeed. A positive status should be treated as an + exit code (i.e. EXIT_FAILURE), and a negative status should be treated as a + negative errno-style error code (i.e. -EINVAL). + + + + + The Target Object + + A target is a component of the system (i.e. the host itself, a sysext, a confext, etc.) that + can be updated by + systemd-sysupdate8. + + + The service exposes the following interfaces on Target objects on the bus: + + +node /org/freedesktop/sysupdate1/target/host { + interface org.freedesktop.sysupdate1.Target { + methods: + List(in t flags, + out as versions); + Describe(in s version, + in t flags, + out s json); + CheckNew(out s new_version); + Update(in s new_version, + in t flags, + out s new_version, + out t job_id, + out o job_path); + Vacuum(out u count); + GetAppStream(out as appstream); + GetVersion(out s version); + properties: + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Class = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Name = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Path = '...'; + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Methods + + List() returns a list of versions available for this target. The + flags argument can be used to pass additional options, with bit 0 mapping to + . When is true, this method returns only the versions + installed locally. Otherwise, this method pulls metadata from the network and returns all versions + available for this target. Use Describe() to query more information about each + version returned by this method. + + Describe() returns all known information about a given version as a JSON + object. The version argument is used to pass the version to be described. Additional + options may be passed through the flags argument. The only supported value currently + is SD_SYSTEMD_SYSUPDATE_OFFLINE, which prevents the call from accessing the network + and restricts results to locally installed versions. This flag is defined as follows: + + +#define SD_SYSTEMD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) + + + The returned JSON object contains several known keys. More keys may be added in the future. The + currently known keys are as follows: + + + + version + A string containing the version number. + + + + newest + A boolean indicating whether this version is the latest available for the target. + + + + available + A boolean indicating whether this version is available for download. + + + + installed + A boolean indicating whether this version is installed locally. + + + + obsolete + A boolean indicating whether this version is considered obsolete by the service, + and is therefore disallowed from being installed. + + + + protected + A boolean indicating whether this version is exempt from deletion by a + Vacuum() operation. + + + + changelog_urls + A list of strings that contain user-presentable URLs to ChangeLogs associated with + this version. + + + + CheckNew() checks if a newer version is available for this target. This + method pulls metadata from the network. If a newer version is found, this method returns the + version number. If no newer version is found, this method returns an empty string. Use + Describe() to query more information about the version returned by this method. + + + Update() installs an update for this target. If a + new_version is specified, that is the version that gets installed. Otherwise, the + latest version is installed. The flags argument is added for future + extensibility. No flags are currently defined, and the argument is required to be set to + 0. Unlike all the other methods in this interface, Update() + does not wait for its job to complete. Instead, it returns the job's numeric ID and object path as soon + as the job begins, so that the caller can listen for progress updates or cancel the operation. This + method also returns the version the target will be updated to, for cases where no version was specified + by the caller. This method pulls both metadata and payload data from the network. Listen for the + Manager's JobRemoved() signal to detect when the job is complete. + + Vacuum() deletes old installed versions of this target to free up space. + It returns the number of instances that have been deleted. + + GetAppStream() returns a list of HTTP/HTTPS URLs to this target's + appstream catalog + XML files. If this target has no appstream catalogs, the method will return an empty list. These + catalog files can be used by software centers (such as GNOME Software or KDE Discover) to present rich + metadata about the target, including a display name, changelog, icon, and more. The returned catalogs + will include special metadata to allow the + software center to correctly associate the catalogs with this target. + + GetVersion() returns the current version of this target, if any. The current + version is the newest version that is installed. Note that this isn't necessarily the same thing as the + booted or currently-in-use version of the target. For example, on the host system the booted version + is the current version most of the time, but if an update is installed and pending a reboot it will + become the current version instead. You can query the booted version of the host system via + IMAGE_VERSION in /etc/os-release. If the target has no current + version, the function will return an empty string. + + + + + Properties + + The Class property exposes the class of this target, which describes + where it was enumerated. Possible values include: machine for containers and + virtual machines managed by + systemd-machined.service8, + portable for portable services, + sysext for system extensions managed by + systemd-sysext8, + confext for configuration extensions managed by + systemd-confext8, + component for components accepted by the option of + systemd-sysupdate8, + and host for the host system itself. At most one target will have a class of + host. + + The Path property exposes more detail about where this target was found. + For machine, portable, extension, and + confext targets, this is the file path to the image. For component + and host targets, this is the name of a + sysupdate.d5 + directory. + + The Name property exposes the name of this target. Note that the name is + unique within a class but is not necessarily unique between classes. For instance, it is possible + to have both a portable target named foobar and an + extension target named foobar, but it is not possible to have + two portable targets named foobar. + + + + + Security + + Method calls on this service are authenticated via + polkit. + + List(), Describe(), and CheckNew() + use the polkit action org.freedesktop.sysupdate1.check. + By default, this action is permitted without administrator authentication. + + Update() uses the polkit action + org.freedesktop.sysupdate1.update when no version is specified. + By default, this action is permitted without administrator authentication. When a version is + specified, org.freedesktop.sysupdate1.update-to-version is + used instead. By default, this alternate action requires administrator authentication. + + Vacuum() uses the polkit action + org.freedesktop.sysupdate1.vacuum. By default, this action requires + administrator authentication. + + GetAppStream() and GetVersion() are unauthenticated and + may be called by anybody. + + All methods called on this interface expose additional variables to the polkit rules. + class contains the class of the Target being acted upon, and name + contains the name of the same Target. Additionally, each method exposes its arguments to the + rule. Arguments containing flags are unwrapped into a variable-per-flag; for example, the + SD_SYSTEMD_SYSUPDATE_OFFLINE flag is exposed as a variable named + offline. + + + + + The Job Object + + A job is an ongoing operation, started by one of the methods on a Target object. + + The service exposes the following interfaces on Job objects on the bus: + + +node /org/freedesktop/sysupdate1/job/_1 { + interface org.freedesktop.sysupdate1.Job { + methods: + Cancel(); + properties: + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly t Id = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Type = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b Offline = ...; + readonly u Progress = ...; + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Methods + + The Cancel() method may be used to cancel the job. It takes no + parameters. + + + + Properties + + The Id property exposes the numeric job ID of the job object. + + The Type property exposes the type of operation (one of: list, + describe, check-new, update, or vacuum). + + + The Offline property exposes whether the job is permitted to access + the network or not. + + The Progress property exposes the current progress of the job as a value + between 0 and 100. It is only available for update jobs; for all other jobs + it is always 0. + + + + Security + + Cancel() uses the polkit action that corresponds to the method + that started this job. For instance, trying to cancel a list job will + require polkit to permit the org.freedesktop.sysupdate1.check + action. + + + + + Examples + + + Introspect <interfacename>org.freedesktop.sysupdate1.Manager</interfacename> on the bus + + $ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1 + + + + + Introspect <interfacename>org.freedesktop.sysupdate1.Target</interfacename> on the bus + + $ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1/target/host + + + + + Introspect <interfacename>org.freedesktop.sysupdate1.Job</interfacename> on the bus + + $ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1/job/_1 + + + + + + + History + + The Manager Object + ListTargets(), + ListJobs(), + ListAppStream(), and + JobRemoved() were added in version 257. + + + The Target Object + List(), + Describe(), + CheckNew(), + Update(), + Vacuum(), + GetAppStream(), + GetVersion(), + Class, + Name, and + Path were added in version 257. + + + The Job Object + Cancel(), + Id, + Type, + Offline, and + Progress were added in version 257. + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index fda14d55bd..abe2b1e92f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -65,6 +65,7 @@ manpages = [ ['org.freedesktop.portable1', '5', [], 'ENABLE_PORTABLED'], ['org.freedesktop.resolve1', '5', [], 'ENABLE_RESOLVE'], ['org.freedesktop.systemd1', '5', [], ''], + ['org.freedesktop.sysupdate1', '5', [], 'ENABLE_SYSUPDATE'], ['org.freedesktop.timedate1', '5', [], 'ENABLE_TIMEDATED'], ['os-release', '5', ['extension-release', 'initrd-release'], ''], ['pam_systemd', '8', [], 'HAVE_PAM'], @@ -1100,6 +1101,10 @@ manpages = [ 'systemd-sysupdate.service', 'systemd-sysupdate.timer'], 'ENABLE_SYSUPDATE'], + ['systemd-sysupdated.service', + '8', + ['systemd-sysupdated'], + 'ENABLE_SYSUPDATE'], ['systemd-sysusers', '8', ['systemd-sysusers.service'], ''], ['systemd-sysv-generator', '8', [], 'HAVE_SYSV_COMPAT'], ['systemd-time-wait-sync.service', diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index f77bd3d0d9..dffe835c04 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -322,6 +322,7 @@ systemd1 sysupdate.d5 + systemd-sysupdated.service8 systemd-repart8 diff --git a/man/systemd-sysupdated.service.xml b/man/systemd-sysupdated.service.xml new file mode 100644 index 0000000000..b7a4f3942a --- /dev/null +++ b/man/systemd-sysupdated.service.xml @@ -0,0 +1,54 @@ + + + + + + + + systemd-sysupdated.service + systemd + + + + systemd-sysupdated.service + 8 + + + + systemd-sysupdated.service + systemd-sysupdated + System Update Service + + + + systemd-sysupdated.service + /usr/lib/systemd/systemd-sysupdated + + + + Description + + systemd-sysupdated is a system service that allows unprivileged + clients to update the system. It works by scanning the system for updateable "targets" (i.e. + portable services, sysexts, sysupdate components, etc.) and exposing them on the bus. Each + target then has methods that translate directly into invocations of + systemd-sysupdate8. + + + See + org.freedesktop.sysupdate15 + and + org.freedesktop.LogControl15 + for a description of the D-Bus API. + + + + See Also + + systemd1, + systemd-sysupdate8 + + + + diff --git a/meson.build b/meson.build index cef6ab9cb8..5e0b666c64 100644 --- a/meson.build +++ b/meson.build @@ -277,6 +277,7 @@ conf.set_quoted('SYSTEMD_LANGUAGE_FALLBACK_MAP', pkgdatadir / 'lang conf.set_quoted('SYSTEMD_MAKEFS_PATH', libexecdir / 'systemd-makefs') conf.set_quoted('SYSTEMD_PULL_PATH', libexecdir / 'systemd-pull') conf.set_quoted('SYSTEMD_SHUTDOWN_BINARY_PATH', libexecdir / 'systemd-shutdown') +conf.set_quoted('SYSTEMD_SYSUPDATE_PATH', libexecdir / 'systemd-sysupdate') conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir) conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent') conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper') diff --git a/po/POTFILES.in b/po/POTFILES.in index 16899fd5f9..d9c602cf20 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,5 +11,6 @@ src/machine/org.freedesktop.machine1.policy src/network/org.freedesktop.network1.policy src/portable/org.freedesktop.portable1.policy src/resolve/org.freedesktop.resolve1.policy +src/sysupdate/org.freedesktop.sysupdate1.policy src/timedate/org.freedesktop.timedate1.policy src/core/dbus-unit.c diff --git a/src/shared/bus-locator.c b/src/shared/bus-locator.c index ff7a872bdb..80d2b5371c 100644 --- a/src/shared/bus-locator.c +++ b/src/shared/bus-locator.c @@ -63,6 +63,12 @@ const BusLocator* const bus_systemd_mgr = &(BusLocator){ .interface = "org.freedesktop.systemd1.Manager" }; +const BusLocator* const bus_sysupdate_mgr = &(BusLocator){ + .destination = "org.freedesktop.sysupdate1", + .path = "/org/freedesktop/sysupdate1", + .interface = "org.freedesktop.sysupdate1.Manager" +}; + const BusLocator* const bus_timedate = &(BusLocator){ .destination = "org.freedesktop.timedate1", .path = "/org/freedesktop/timedate1", diff --git a/src/shared/bus-locator.h b/src/shared/bus-locator.h index 4f50a9727f..8116aa27c0 100644 --- a/src/shared/bus-locator.h +++ b/src/shared/bus-locator.h @@ -20,6 +20,7 @@ extern const BusLocator* const bus_oom_mgr; extern const BusLocator* const bus_portable_mgr; extern const BusLocator* const bus_resolve_mgr; extern const BusLocator* const bus_systemd_mgr; +extern const BusLocator* const bus_sysupdate_mgr; extern const BusLocator* const bus_timedate; extern const BusLocator* const bus_timesync_mgr; diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index b1b1204a2a..8bd422fc43 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -30,4 +30,20 @@ executables += [ threads, ], }, + libexec_template + { + 'name' : 'systemd-sysupdated', + 'dbus' : true, + 'conditions' : ['ENABLE_SYSUPDATE'], + 'sources' : files('sysupdated.c'), + 'dependencies' : threads, + }, ] + +if conf.get('ENABLE_SYSUPDATE') == 1 + install_data('org.freedesktop.sysupdate1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.sysupdate1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.sysupdate1.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf new file mode 100644 index 0000000000..30cb1eec24 --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.conf @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sysupdate/org.freedesktop.sysupdate1.policy b/src/sysupdate/org.freedesktop.sysupdate1.policy new file mode 100644 index 0000000000..7c1b94333c --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.policy @@ -0,0 +1,74 @@ + + + + + + + + The systemd Project + https://systemd.io + + + + + Check for system updates + Authentication is required to check for system updates + + auth_admin + auth_admin + yes + + + + + Install system updates + Authentication is required to install system updates + + auth_admin + auth_admin + yes + + + + + Install specific system version + Authentication is required to update the system to a specific (possibly old) version + + auth_admin + auth_admin + auth_admin_keep + + + + + Cleanup old system updates + Authentication is required to cleanup old system updates + + auth_admin + auth_admin + auth_admin_keep + + + + diff --git a/src/sysupdate/org.freedesktop.sysupdate1.service b/src/sysupdate/org.freedesktop.sysupdate1.service new file mode 100644 index 0000000000..67e1a29078 --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.service @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[D-BUS Service] +Name=org.freedesktop.sysupdate1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.sysupdate1.service diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h index fdd6c8318e..56339a87b1 100644 --- a/src/sysupdate/sysupdate-util.h +++ b/src/sysupdate/sysupdate-util.h @@ -3,3 +3,5 @@ #pragma once int reboot_now(void); + +#define SD_SYSTEMD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index aded7dde0b..dee8348bdb 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -1543,7 +1543,7 @@ static int run(int argc, char *argv[]) { return r; /* SIGCHLD signal must be blocked for sd_event_add_child to work */ - BLOCK_SIGNALS(SIGCHLD); + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); return sysupdate_main(argc, argv); } diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c new file mode 100644 index 0000000000..e2c3d7e102 --- /dev/null +++ b/src/sysupdate/sysupdated.c @@ -0,0 +1,1911 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-bus.h" +#include "sd-json.h" + +#include "build-path.h" +#include "bus-error.h" +#include "bus-get-properties.h" +#include "bus-label.h" +#include "bus-log-control-api.h" +#include "bus-polkit.h" +#include "bus-util.h" +#include "common-signal.h" +#include "discover-image.h" +#include "env-util.h" +#include "escape.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "hashmap.h" +#include "log.h" +#include "main-func.h" +#include "memfd-util.h" +#include "mkdir-label.h" +#include "os-util.h" +#include "process-util.h" +#include "service-util.h" +#include "signal-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "sysupdate-util.h" + +typedef struct Manager { + sd_event *event; + sd_bus *bus; + + Hashmap *targets; + + uint64_t last_job_id; + Hashmap *jobs; + + Hashmap *polkit_registry; + + sd_event_source *notify_event; +} Manager; + +/* Forward declare so that jobs can call it on exit */ +static void manager_check_idle(Manager *m); + +typedef enum TargetClass { + /* These should try to match ImageClass from src/basic/os-util.h */ + TARGET_MACHINE = IMAGE_MACHINE, + TARGET_PORTABLE = IMAGE_PORTABLE, + TARGET_SYSEXT = IMAGE_SYSEXT, + TARGET_CONFEXT = IMAGE_CONFEXT, + _TARGET_CLASS_IS_IMAGE_CLASS_MAX, + + /* sysupdate-specific classes */ + TARGET_HOST = _TARGET_CLASS_IS_IMAGE_CLASS_MAX, + TARGET_COMPONENT, + + _TARGET_CLASS_MAX, + _TARGET_CLASS_INVALID = -EINVAL, +} TargetClass; + +/* Let's ensure when the number of classes is updated things are updated here too */ +assert_cc((int) _IMAGE_CLASS_MAX == (int) _TARGET_CLASS_IS_IMAGE_CLASS_MAX); + +typedef struct Target { + Manager *manager; + + TargetClass class; + char *name; + char *path; + + char *id; + ImageType image_type; + bool busy; +} Target; + +typedef enum JobType { + JOB_LIST, + JOB_DESCRIBE, + JOB_CHECK_NEW, + JOB_UPDATE, + JOB_VACUUM, + _JOB_TYPE_MAX, + _JOB_TYPE_INVALID = -EINVAL, +} JobType; + +typedef struct Job Job; + +typedef int (*JobReady)(sd_bus_message *msg, const Job *job); +typedef int (*JobComplete)(sd_bus_message *msg, const Job *job, sd_json_variant *response, sd_bus_error *error); + +struct Job { + Manager *manager; + Target *target; + + uint64_t id; + char *object_path; + + JobType type; + bool offline; + char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */ + + unsigned progress_percent; + + sd_event_source *child; + int stdout_fd; + int status_errno; + unsigned n_cancelled; + + sd_json_variant *json; + + JobComplete complete_cb; /* Callback called on job exit */ + sd_bus_message *dbus_msg; + JobReady detach_cb; /* Callback called when job has started. Detaches the job to run in the background */ +}; + +static const char* const target_class_table[_TARGET_CLASS_MAX] = { + [TARGET_MACHINE] = "machine", + [TARGET_PORTABLE] = "portable", + [TARGET_SYSEXT] = "sysext", + [TARGET_CONFEXT] = "confext", + [TARGET_COMPONENT] = "component", + [TARGET_HOST] = "host", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass); + +static const char* const job_type_table[_JOB_TYPE_MAX] = { + [JOB_LIST] = "list", + [JOB_DESCRIBE] = "describe", + [JOB_CHECK_NEW] = "check-new", + [JOB_UPDATE] = "update", + [JOB_VACUUM] = "vacuum", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType); + +static Job *job_free(Job *j) { + if (!j) + return NULL; + + if (j->manager) + assert_se(hashmap_remove(j->manager->jobs, &j->id) == j); + + free(j->object_path); + free(j->version); + + sd_json_variant_unref(j->json); + + sd_bus_message_unref(j->dbus_msg); + + sd_event_source_disable_unref(j->child); + safe_close(j->stdout_fd); + + return mfree(j); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Job*, job_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(job_hash_ops, uint64_t, uint64_hash_func, uint64_compare_func, + Job, job_free); + +static int job_new(JobType type, Target *t, sd_bus_message *msg, JobComplete complete_cb, Job **ret) { + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(t); + assert(ret); + + j = new(Job, 1); + if (!j) + return -ENOMEM; + + *j = (Job) { + .type = type, + .target = t, + .id = t->manager->last_job_id + 1, + .stdout_fd = -EBADF, + .complete_cb = complete_cb, + .dbus_msg = sd_bus_message_ref(msg), + }; + + if (asprintf(&j->object_path, "/org/freedesktop/sysupdate1/job/_%" PRIu64, j->id) < 0) + return -ENOMEM; + + r = hashmap_ensure_put(&t->manager->jobs, &job_hash_ops, &j->id, j); + if (r < 0) + return r; + + j->manager = t->manager; + + t->manager->last_job_id = j->id; + + *ret = TAKE_PTR(j); + return 0; +} + +static int job_parse_child_output(int _fd, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + /* Take ownership of the passed fd */ + _cleanup_close_ int fd = _fd; + _cleanup_fclose_ FILE *f = NULL; + struct stat st; + int r; + + assert(ret); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat stdout fd: %m"); + + assert(S_ISREG(st.st_mode)); + + if (st.st_size == 0) { + log_warning("No output from child job, ignoring"); + return 0; + } + + if (lseek(fd, SEEK_SET, 0) == (off_t) -1) + return log_error_errno(errno, "Failed to seek to beginning of memfd: %m"); + + f = take_fdopen(&fd, "r"); + if (!f) + return log_error_errno(errno, "Failed to reopen memfd: %m"); + + r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON: %m"); + + *ret = TAKE_PTR(v); + return 0; +} + +static void job_on_ready(Job *j) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *msg = NULL; + int r; + + assert(j); + + /* Some jobs run in the background as we return the job ID to the dbus caller (i.e. for the Update + * method). However, the worker will perform some sanity-checks on startup which would be valuable + * as dbus errors. So, we wait for the worker to signal via READY=1 that it has completed its sanity + * checks and we should continue the job in the background. */ + + if (!j->detach_cb) + return; + + assert(j->dbus_msg); + msg = TAKE_PTR(j->dbus_msg); + + j->complete_cb = NULL; + + r = j->detach_cb(msg, j); + if (r < 0) + log_warning_errno(r, "Failed to run callback on job ready event, ignoring: %m"); +} + +static void job_on_errno(Job *j, char *b) { + /* Take ownership of donated buffer */ + _cleanup_free_ char *buf = TAKE_PTR(b); + int r; + + assert(j); + assert_se(buf); + + r = parse_errno(buf); + if (r < 0) { + log_warning_errno(r, "Got invalid errno value, ignoring: %m"); + return; + } + + j->status_errno = r; + + log_debug_errno(r, "Got errno from job %" PRIu64 ": %i (%m)", j->id, r); +} + +static void job_on_progress(Job *j, char *b) { + /* Take ownership of donated buffer */ + _cleanup_free_ char *buf = TAKE_PTR(b); + unsigned progress; + int r; + + assert(j); + assert_se(buf); + + r = safe_atou(buf, &progress); + if (r < 0 || progress > 100) { + log_warning("Got invalid percent value, ignoring."); + return; + } + + j->progress_percent = progress; + (void) sd_bus_emit_properties_changed(j->manager->bus, j->object_path, + "org.freedesktop.sysupdate1.Job", + "Progress", NULL); + + log_debug("Got percentage from job %" PRIu64 ": %u%%", j->id, j->progress_percent); +} + +static void job_on_version(Job *j, char *version) { + assert(j); + assert_se(version); + + /* Take ownership of donated memory */ + free_and_replace(j->version, version); + + log_debug("Got version from job %" PRIu64 ": %s ", j->id, j->version); +} + +static int job_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Job *j = ASSERT_PTR(userdata); + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + Manager *manager = j->manager; + int r; + + assert(j); + assert(s); + assert(si); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) { + assert(j->target->busy); + j->target->busy = false; + } + + if (si->si_code != CLD_EXITED) { + assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)); + sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, + "Job terminated abnormally with signal %s.", + signal_to_string(si->si_status)); + } else if (si->si_status != EXIT_SUCCESS) + if (j->status_errno != 0) + sd_bus_error_set_errno(&error, j->status_errno); + else + sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, + "Job failed with exit code %i.", si->si_status); + else { + r = job_parse_child_output(TAKE_FD(j->stdout_fd), &json); + if (r < 0) + sd_bus_error_set_errnof(&error, r, "Failed to parse JSON: %m"); + } + + /* Only send notification of exit if the job was actually detached */ + if (j->detach_cb) { + r = sd_bus_emit_signal( + j->manager->bus, + "/org/freedesktop/sysupdate1", + "org.freedesktop.sysupdate1.Manager", + "JobRemoved", + "toi", + j->id, + j->object_path, + j->status_errno != 0 ? -j->status_errno : si->si_status); + if (r < 0) + log_warning_errno(r, "Cannot emit JobRemoved message, ignoring: %m"); + } + + if (j->dbus_msg && j->complete_cb) { + if (sd_bus_error_is_set(&error)) { + log_warning("Bus error occurred, ignoring callback for job: %s", error.message); + sd_bus_reply_method_error(j->dbus_msg, &error); + } else { + r = j->complete_cb(j->dbus_msg, j, json, &error); + if (r < 0) { + log_warning_errno(r, "Error during execution of job callback: %s", bus_error_message(&error, r)); + sd_bus_reply_method_errno(j->dbus_msg, r, &error); + } + } + } + + job_free(j); + + if (manager) + manager_check_idle(manager); + + return 0; +} + +static inline const char* sysupdate_binary_path(void) { + return secure_getenv("SYSTEMD_SYSUPDATE_PATH") ?: SYSTEMD_SYSUPDATE_PATH; +} + +static int target_get_argument(Target *t, char **ret) { + _cleanup_free_ char *target_arg = NULL; + + assert(t); + assert(ret); + + if (t->class != TARGET_HOST) { + if (t->class == TARGET_COMPONENT) + target_arg = strjoin("--component=", t->name); + else if (IN_SET(t->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) + target_arg = strjoin("--root=", t->path); + else if (IN_SET(t->image_type, IMAGE_RAW, IMAGE_BLOCK)) + target_arg = strjoin("--image=", t->path); + else + assert_not_reached(); + if (!target_arg) + return -ENOMEM; + } + + *ret = TAKE_PTR(target_arg); + return 0; +} + +static int job_start(Job *j) { + _cleanup_close_ int stdout_fd = -EBADF; + _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL; + int r; + + assert(j); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM) && j->target->busy) + return log_notice_errno(SYNTHETIC_ERRNO(EBUSY), "Target %s busy, ignoring job.", j->target->name); + + stdout_fd = memfd_new("sysupdate-stdout"); + if (stdout_fd < 0) + return log_error_errno(stdout_fd, "Failed to create memfd: %m"); + + r = pidref_safe_fork_full("(sd-sysupdate)", + (int[]) { -EBADF, stdout_fd, STDERR_FILENO }, NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); + if (r < 0) + return r; /* FORK_LOG means pidref_safe_fork_full will handle the logging */ + if (r == 0) { + /* Child */ + + _cleanup_free_ char *target_arg = NULL; + const char *cmd[] = { + "systemd-sysupdate", + "--json=short", + NULL, /* maybe --verify=no */ + NULL, /* maybe --component=, --root=, or --image= */ + NULL, /* maybe --offline */ + NULL, /* list, check-new, update, vacuum */ + NULL, /* maybe version (for list, update) */ + NULL + }; + size_t k = 2; + + if (setenv("NOTIFY_SOCKET", "/run/systemd/sysupdate/notify", /* overwrite= */ 1) < 0) { + log_error_errno(errno, "setenv() failed: %m"); + _exit(EXIT_FAILURE); + } + + if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0) + cmd[k++] = "--verify=no"; /* For testing */ + + r = setenv_systemd_exec_pid(true); + if (r < 0) + log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m"); + + r = target_get_argument(j->target, &target_arg); + if (r < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + if (target_arg) + cmd[k++] = target_arg; + + if (j->offline) + cmd[k++] = "--offline"; + + switch (j->type) { + case JOB_LIST: + cmd[k++] = "list"; + break; + + case JOB_DESCRIBE: + cmd[k++] = "list"; + assert(!isempty(j->version)); + cmd[k++] = j->version; + break; + + case JOB_CHECK_NEW: + cmd[k++] = "check-new"; + break; + + case JOB_UPDATE: + cmd[k++] = "update"; + cmd[k++] = empty_to_null(j->version); + break; + + case JOB_VACUUM: + cmd[k++] = "vacuum"; + break; + + default: + assert_not_reached(); + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + + s = quote_command_line((char**) cmd, SHELL_ESCAPE_EMPTY); + if (!s) { + log_oom(); + _exit(EXIT_FAILURE); + } + + log_debug("Spawning worker for job %" PRIu64 ": %s", j->id, s); + } + + r = invoke_callout_binary(sysupdate_binary_path(), (char *const *) cmd); + log_error_errno(r, "Failed to execute systemd-sysupdate: %m"); + _exit(EXIT_FAILURE); + } + + r = event_add_child_pidref(j->manager->event, &j->child, &pid, WEXITED, job_on_exit, j); + if (r < 0) + return log_error_errno(r, "Failed to add child process to event loop: %m"); + + r = sd_event_source_set_child_process_own(j->child, true); + if (r < 0) + return log_error_errno(r, "Event loop failed to take ownership of child process: %m"); + TAKE_PIDREF(pid); + + j->stdout_fd = TAKE_FD(stdout_fd); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) + j->target->busy = true; + + return 0; +} + +static int job_cancel(Job *j) { + int r; + + assert(j); + + r = sd_event_source_send_child_signal(j->child, j->n_cancelled < 3 ? SIGTERM : SIGKILL, + NULL, 0); + if (r < 0) + return r; + + j->n_cancelled++; + return 0; +} + +static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Job *j = ASSERT_PTR(userdata); + const char *action; + int r; + + assert(msg); + + switch (j->type) { + case JOB_LIST: + case JOB_DESCRIBE: + case JOB_CHECK_NEW: + action = "org.freedesktop.sysupdate1.check"; + break; + + case JOB_UPDATE: + if (j->version) + action = "org.freedesktop.sysupdate1.update-to-version"; + else + action = "org.freedesktop.sysupdate1.update"; + break; + + case JOB_VACUUM: + action = "org.freedesktop.sysupdate1.vacuum"; + break; + + default: + assert_not_reached(); + } + + r = bus_verify_polkit_async( + msg, + action, + /* details= */ NULL, + &j->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_cancel(j); + if (r < 0) + return r; + + return sd_bus_reply_method_return(msg, NULL); +} + +static BUS_DEFINE_PROPERTY_GET_ENUM(job_property_get_type, job_type, JobType); + +static int job_object_find( + sd_bus *bus, + const char *path, + const char *iface, + void *userdata, + void **ret, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + Job *j; + const char *p; + uint64_t id; + int r; + + assert(bus); + assert(path); + assert(ret); + + p = startswith(path, "/org/freedesktop/sysupdate1/job/_"); + if (!p) + return 0; + + r = safe_atou64(p, &id); + if (r < 0 || id == 0) + return 0; + + j = hashmap_get(m->jobs, &id); + if (!j) + return 0; + + *ret = j; + return 1; +} + +static int job_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Job *j; + unsigned k = 0; + + l = new0(char*, hashmap_size(m->jobs) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(j, m->jobs) { + l[k] = strdup(j->object_path); + if (!l[k]) + return -ENOMEM; + k++; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static const sd_bus_vtable job_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Id", "t", NULL, offsetof(Job, id), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Type", "s", job_property_get_type, offsetof(Job, type), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Offline", "b", bus_property_get_bool, offsetof(Job, offline), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Progress", "u", bus_property_get_unsigned, offsetof(Job, progress_percent), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_METHOD("Cancel", NULL, NULL, job_method_cancel, SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation job_object = { + "/org/freedesktop/sysupdate1/job", + "org.freedesktop.sysupdate1.Job", + .fallback_vtables = BUS_FALLBACK_VTABLES({job_vtable, job_object_find}), + .node_enumerator = job_node_enumerator, +}; + +static Target *target_free(Target *t) { + if (!t) + return NULL; + + free(t->name); + free(t->path); + free(t->id); + + return mfree(t); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Target*, target_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(target_hash_ops, char, string_hash_func, string_compare_func, + Target, target_free); + +static int target_new(Manager *m, TargetClass class, const char *name, const char *path, Target **ret) { + _cleanup_(target_freep) Target *t = NULL; + int r; + + assert(m); + assert(ret); + + t = new(Target, 1); + if (!t) + return -ENOMEM; + + *t = (Target) { + .manager = m, + .class = class, + .image_type = _IMAGE_TYPE_INVALID, + }; + + t->name = strdup(name); + if (!t->name) + return -ENOMEM; + + t->path = strdup(path); + if (!t->path) + return -ENOMEM; + + if (class == TARGET_HOST) + t->id = strdup("host"); /* This is what appears in the object path */ + else + t->id = strjoin(target_class_to_string(class), ":", name); + if (!t->id) + return -ENOMEM; + + r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t); + if (r < 0) + return r; + + *ret = TAKE_PTR(t); + return 0; +} + +static int sysupdate_run_simple(sd_json_variant **ret, ...) { + _cleanup_close_pair_ int pipe[2] = EBADF_PAIR; + _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + r = pipe2(pipe, O_CLOEXEC); + if (r < 0) + return -errno; + + r = pidref_safe_fork_full("(sd-sysupdate)", + (int[]) { -EBADF, pipe[1], STDERR_FILENO }, + NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, + &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + va_list ap; + char *arg; + _cleanup_strv_free_ char **args = NULL; + + if (strv_extend(&args, "systemd-sysupdate") < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + if (strv_extend(&args, "--json=short") < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + va_start(ap, ret); + while ((arg = va_arg(ap, char*))) { + r = strv_extend(&args, arg); + if (r < 0) + break; + } + va_end(ap); + if (r < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + + s = quote_command_line((char**) args, SHELL_ESCAPE_EMPTY); + if (!s) { + log_oom(); + _exit(EXIT_FAILURE); + } + + log_debug("Spawning sysupdate: %s", s); + } + + r = invoke_callout_binary(sysupdate_binary_path(), args); + log_error_errno(r, "Failed to execute systemd-sysupdate: %m"); + _exit(EXIT_FAILURE); + } + + pipe[1] = safe_close(pipe[1]); + f = take_fdopen(&pipe[0], "r"); + if (!f) + return -errno; + + r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON: %m"); + + *ret = TAKE_PTR(v); + return 0; +} + +static BUS_DEFINE_PROPERTY_GET_ENUM(target_property_get_class, target_class, TargetClass); + +#define log_sysupdate_bad_json(verb, msg) \ + log_debug("Invalid JSON response from 'systemd-sysupdate %s': %s", verb, msg) + +static int target_method_list_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + sd_json_variant *v; + _cleanup_strv_free_ char **versions = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + assert(json); + + v = sd_json_variant_by_key(json, "all"); + if (!v) { + log_sysupdate_bad_json("list", "Missing key 'all'"); + return -EINVAL; + } + + r = sd_json_variant_strv(v, &versions); + if (r < 0) + return r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(reply, versions); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int target_method_list(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + uint64_t flags; + + assert(msg); + + r = sd_bus_message_read(msg, "t", &flags); + if (r < 0) + return r; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)), + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_LIST, t, msg, target_method_list_finish, &j); + if (r < 0) + return r; + + j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE); + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); /* Avoid job from being killed & freed */ + + return 1; +} + +static int target_method_describe_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + _cleanup_free_ char *text = NULL; + int r; + + assert(json); + + r = sd_json_variant_format(json, 0, &text); + if (r < 0) + return r; + + return sd_bus_reply_method_return(msg, "s", text); +} + +static int target_method_describe(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + const char *version; + int r; + uint64_t flags; + + assert(msg); + + r = sd_bus_message_read(msg, "st", &version, &flags); + if (r < 0) + return r; + + if (isempty(version)) + return -EINVAL; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "version", version, + "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)), + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_DESCRIBE, t, msg, target_method_describe_finish, &j); + if (r < 0) + return r; + + j->version = strdup(version); + if (!j->version) + return log_oom(); + + j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE); + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); /* Avoid job from being killed & freed */ + + return 1; +} + +static int target_method_check_new_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + const char *reply; + + assert(json); + + sd_json_variant *v = sd_json_variant_by_key(json, "available"); + if (!v) { + log_sysupdate_bad_json("check-new", "Missing key 'available'"); + return -EINVAL; + } + + if (sd_json_variant_is_null(v)) + reply = ""; + else + reply = sd_json_variant_string(v); + if (!reply) + return -EINVAL; + + return sd_bus_reply_method_return(msg, "s", reply); +} + +static int target_method_check_new(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(msg); + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "offline", "0", + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_CHECK_NEW, t, msg, target_method_check_new_finish, &j); + if (r < 0) + return r; + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); /* Avoid job from being killed & freed */ + + return 1; +} + +static int target_method_update_finished_early( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + /* Called when job finishes w/ a successful exit code, but before any work begins. + * This happens when there is no candidate (i.e. we're already up-to-date), or + * specified update is already installed. */ + return sd_bus_error_setf(error, "org.freedesktop.sysupdate1.NoCandidate", + "Job exited successfully with no work to do, assume already updated"); +} + +static int target_method_update_detach(sd_bus_message *msg, const Job *j) { + int r; + + assert(msg); + assert(j); + + r = sd_bus_reply_method_return(msg, "sto", j->version, j->id, j->object_path); + if (r < 0) + return bus_log_parse_error(r); + + return 0; +} + +static int target_method_update(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + const char *version, *action; + uint64_t flags; + int r; + + assert(msg); + + r = sd_bus_message_read(msg, "st", &version, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(EINVAL), "Flags argument must be 0: %m"); + + if (isempty(version)) + action = "org.freedesktop.sysupdate1.update"; + else + action = "org.freedesktop.sysupdate1.update-to-version"; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "version", version, + NULL + }; + + r = bus_verify_polkit_async( + msg, + action, + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_UPDATE, t, msg, target_method_update_finished_early, &j); + if (r < 0) + return r; + j->detach_cb = target_method_update_detach; + + j->version = strdup(version); + if (!j->version) + return -ENOMEM; + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); + + return 1; +} + +static int target_method_vacuum_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + uint64_t instances; + + assert(json); + + instances = sd_json_variant_unsigned(sd_json_variant_by_key(json, "removed")); + + return sd_bus_reply_method_return(msg, "u", instances); +} + +static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(msg); + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.vacuum", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_VACUUM, t, msg, target_method_vacuum_finish, &j); + if (r < 0) + return r; + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); /* Avoid job from being killed & freed */ + + return 1; +} + +static int target_method_get_version(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_free_ char *target_arg = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + sd_json_variant *version_json; + int r; + + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + + r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL); + if (r < 0) + return r; + + version_json = sd_json_variant_by_key(v, "current"); + if (!version_json) { + log_sysupdate_bad_json("list", "Missing key 'current'"); + return -EINVAL; + } + + if (sd_json_variant_is_null(version_json)) + return sd_bus_reply_method_return(msg, "s", ""); + + if (!sd_json_variant_is_string(version_json)) { + log_sysupdate_bad_json("list", "Expected string value for key 'current'"); + return -EINVAL; + } + + return sd_bus_reply_method_return(msg, "s", sd_json_variant_string(version_json)); +} + +static int target_get_appstream(Target *t, char ***ret) { + _cleanup_free_ char *target_arg = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + sd_json_variant *appstream_url_json; + int r; + + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + + r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL); + if (r < 0) + return r; + + appstream_url_json = sd_json_variant_by_key(v, "appstream_urls"); + if (!appstream_url_json) { + log_sysupdate_bad_json("list", "Missing key 'appstream_urls'"); + return -EINVAL; + } + + r = sd_json_variant_strv(appstream_url_json, ret); + if (r < 0) { + log_sysupdate_bad_json("list", "Expected array of strings for key 'appstream_urls'"); + return r; + } + + return 0; +} + +static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_strv_free_ char **appstream_urls = NULL; + int r; + + r = target_get_appstream(t, &appstream_urls); + if (r < 0) + return r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(reply, appstream_urls); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + _cleanup_strv_free_ char **components = NULL; + _cleanup_free_ char *target_arg = NULL; + sd_json_variant *v; + bool have_default; + int r; + + if (t) { + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + } + + r = sysupdate_run_simple(&json, "components", target_arg, NULL); + if (r < 0) + return r; + + v = sd_json_variant_by_key(json, "default"); + if (!v) + return -EINVAL; + have_default = sd_json_variant_boolean(v); + + v = sd_json_variant_by_key(json, "components"); + if (!v) + return -EINVAL; + r = sd_json_variant_strv(v, &components); + if (r < 0) + return r; + + if (ret_components) + *ret_components = TAKE_PTR(components); + if (ret_have_default) + *ret_have_default = have_default; + return 0; +} + +static int manager_ensure_targets(Manager *m); + +static int target_object_find( + sd_bus *bus, + const char *path, + const char *iface, + void *userdata, + void **found, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + Target *t; + _cleanup_free_ char *e = NULL; + const char *p; + int r; + + assert(bus); + assert(path); + assert(found); + + p = startswith(path, "/org/freedesktop/sysupdate1/target/"); + if (!p) + return 0; + + e = bus_label_unescape(p); + if (!e) + return -ENOMEM; + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + t = hashmap_get(m->targets, e); + if (!t) + return 0; + + *found = t; + return 1; +} + +static char *target_bus_path(Target *t) { + _cleanup_free_ char *e = NULL; + + assert(t); + + e = bus_label_escape(t->id); + if (!e) + return NULL; + + return strjoin("/org/freedesktop/sysupdate1/target/", e); +} + +static int target_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + unsigned k = 0; + int r; + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + l = new0(char*, hashmap_size(m->targets) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(t, m->targets) { + l[k] = target_bus_path(t); + if (!l[k]) + return -ENOMEM; + k++; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static const sd_bus_vtable target_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Class", "s", target_property_get_class, + offsetof(Target, class), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Name", "s", NULL, offsetof(Target, name), + SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Path", "s", NULL, offsetof(Target, path), + SD_BUS_VTABLE_PROPERTY_CONST), + + SD_BUS_METHOD_WITH_ARGS("List", + SD_BUS_ARGS("t", flags), + SD_BUS_RESULT("as", versions), + target_method_list, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Describe", + SD_BUS_ARGS("s", version, "t", flags), + SD_BUS_RESULT("s", json), + target_method_describe, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("CheckNew", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", new_version), + target_method_check_new, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Update", + SD_BUS_ARGS("s", new_version, "t", flags), + SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path), + target_method_update, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Vacuum", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("u", count), + target_method_vacuum, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("GetAppStream", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("as", appstream), + target_method_get_appstream, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("GetVersion", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", version), + target_method_get_version, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation target_object = { + "/org/freedesktop/sysupdate1/target", + "org.freedesktop.sysupdate1.Target", + .fallback_vtables = BUS_FALLBACK_VTABLES({target_vtable, target_object_find}), + .node_enumerator = target_node_enumerator, +}; + +static Manager *manager_free(Manager *m) { + if (!m) + return NULL; + + hashmap_free(m->targets); + hashmap_free(m->jobs); + + m->bus = sd_bus_flush_close_unref(m->bus); + sd_event_source_unref(m->notify_event); + sd_event_unref(m->event); + + return mfree(m); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager *, manager_free); + +static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + char buf[NOTIFY_BUFFER_MAX+1]; + struct iovec iovec = { + .iov_base = buf, + .iov_len = sizeof(buf)-1, + }; + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr msghdr = { + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct ucred *ucred; + Manager *m = ASSERT_PTR(userdata); + Job *j; + ssize_t n; + char *p; + + n = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); + if (n < 0) { + if (ERRNO_IS_TRANSIENT(n)) + return 0; + return (int) n; + } + + cmsg_close_all(&msghdr); + + if (msghdr.msg_flags & MSG_TRUNC) { + log_warning("Got overly long notification datagram, ignoring."); + return 0; + } + + ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred); + if (!ucred || ucred->pid <= 0) { + log_warning("Got notification datagram lacking credential information, ignoring."); + return 0; + } + + HASHMAP_FOREACH(j, m->jobs) { + pid_t pid; + assert_se(sd_event_source_get_child_pid(j->child, &pid) >= 0); + + if (ucred->pid == pid) + break; + } + + if (!j) { + log_warning("Got notification datagram from unexpected peer, ignoring."); + return 0; + } + + buf[n] = 0; + + p = find_line_startswith(buf, "X_SYSUPDATE_VERSION="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_version(j, p); + } + + p = find_line_startswith(buf, "ERRNO="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_errno(j, p); + } + + p = find_line_startswith(buf, "X_SYSUPDATE_PROGRESS="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_progress(j, p); + } + + /* Should come last, since this might actually detach the job */ + if (find_line_startswith(buf, "READY=1")) + job_on_ready(j); + + return 0; +} + +static int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_close_ int notify_fd = -EBADF; + static const union sockaddr_union sa = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/sysupdate/notify", + }; + int r; + + assert(ret); + + m = new0(Manager, 1); + if (!m) + return -ENOMEM; + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + (void) sd_event_set_watchdog(m->event, true); + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK, + sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_bus_default_system(&m->bus); + if (r < 0) + return r; + + notify_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (notify_fd < 0) + return -errno; + + (void) mkdir_parents_label(sa.un.sun_path, 0755); + (void) sockaddr_un_unlink(&sa.un); + + if (bind(notify_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) + return -errno; + + r = setsockopt_int(notify_fd, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return r; + + r = sd_event_add_io(m->event, &m->notify_event, notify_fd, EPOLLIN, manager_on_notify, m); + if (r < 0) + return r; + + (void) sd_event_source_set_description(m->notify_event, "notify-socket"); + + r = sd_event_source_set_io_fd_own(m->notify_event, true); + if (r < 0) + return r; + TAKE_FD(notify_fd); + + *ret = TAKE_PTR(m); + return 0; +} + +static int manager_enumerate_image_class(Manager *m, TargetClass class) { + _cleanup_hashmap_free_ Hashmap *images = NULL; + Image *image; + int r; + + images = hashmap_new(&image_hash_ops); + if (!images) + return -ENOMEM; + + r = image_discover((ImageClass) class, NULL, images); + if (r < 0) + return r; + + HASHMAP_FOREACH(image, images) { + Target *t = NULL; + bool have = false; + + if (IMAGE_IS_HOST(image)) + continue; /* We already enroll the host ourselves */ + + r = target_new(m, class, image->name, image->path, &t); + if (r < 0) + return r; + t->image_type = image->type; + + r = target_list_components(t, NULL, &have); + if (r < 0) + return r; + if (!have) { + log_debug("Skipping %s because it has no default component", image->path); + continue; + } + } + + return 0; +} + +static int manager_enumerate_components(Manager *m) { + _cleanup_strv_free_ char **components = NULL; + bool have_default; + Target *t; + int r; + + r = target_list_components(NULL, &components, &have_default); + if (r < 0) + return r; + + if (have_default) { + r = target_new(m, TARGET_HOST, "host", "sysupdate.d", &t); + if (r < 0) + return r; + } + + STRV_FOREACH(component, components) { + _cleanup_free_ char *path = NULL; + + path = strjoin("sysupdate.", *component, ".d"); + if (!path) + return -ENOMEM; + + r = target_new(m, TARGET_COMPONENT, *component, path, &t); + if (r < 0) + return r; + } + + return 0; +} + +static int manager_enumerate_targets(Manager *m) { + static const TargetClass discoverable_classes[] = { + TARGET_MACHINE, + TARGET_PORTABLE, + TARGET_SYSEXT, + TARGET_CONFEXT, + }; + int r; + + assert(m); + + FOREACH_ARRAY(class, discoverable_classes, ELEMENTSOF(discoverable_classes)) { + r = manager_enumerate_image_class(m, *class); + if (r < 0) + log_warning_errno(r, "Failed to enumerate %ss, ignoring: %m", + target_class_to_string(*class)); + } + + return manager_enumerate_components(m); +} + +static int manager_ensure_targets(Manager *m) { + assert(m); + + if (!hashmap_isempty(m->targets)) + return 0; + + return manager_enumerate_targets(m); +} + +static int method_list_targets(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + int r; + + assert(msg); + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(sso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(t, m->targets) { + _cleanup_free_ char *bus_path = NULL; + + bus_path = target_bus_path(t); + if (!bus_path) + return -ENOMEM; + + r = sd_bus_message_append(reply, "(sso)", + target_class_to_string(t->class), + t->name, + bus_path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_list_jobs(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Job *j; + int r; + + assert(msg); + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(tsuo)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(j, m->jobs) { + r = sd_bus_message_append(reply, "(tsuo)", + j->id, + job_type_to_string(j->type), + j->progress_percent, + j->object_path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_list_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_strv_free_ char **urls = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + int r; + + assert(msg); + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + HASHMAP_FOREACH(t, m->targets) { + _cleanup_strv_free_ char **target_appstream = NULL; + r = target_get_appstream(t, &target_appstream); + if (r < 0) + return r; + + r = strv_extend_strv(&urls, target_appstream, true); + if (r < 0) + return r; + } + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(reply, urls); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_METHOD_WITH_ARGS("ListTargets", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(sso)", targets), + method_list_targets, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("ListJobs", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(tsuo)", jobs), + method_list_jobs, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("ListAppStream", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("as", urls), + method_list_appstream, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_SIGNAL_WITH_ARGS("JobRemoved", + SD_BUS_ARGS("t", id, "o", path, "i", status), + 0), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation manager_object = { + "/org/freedesktop/sysupdate1", + "org.freedesktop.sysupdate1.Manager", + .vtables = BUS_VTABLES(manager_vtable), + .children = BUS_IMPLEMENTATIONS(&job_object, &target_object), +}; + +static int manager_add_bus_objects(Manager *m) { + int r; + + assert(m); + + r = bus_add_implementation(m->bus, &manager_object, m); + if (r < 0) + return r; + + r = bus_log_control_api_register(m->bus); + if (r < 0) + return r; + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.sysupdate1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + return 0; +} + +static bool manager_is_idle(void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + return hashmap_isempty(m->jobs); +} + +static void manager_check_idle(Manager *m) { + assert(m); + + if (!hashmap_isempty(m->jobs)) + return; + + hashmap_clear(m->targets); + log_debug("Cleared target cache"); +} + +static int manager_run(Manager *m) { + assert(m); + + return bus_event_loop_with_idle(m->event, + m->bus, + "org.freedesktop.sysupdate1", + DEFAULT_EXIT_USEC, + manager_is_idle, + m); +} + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + r = service_parse_argv("systemd-sysupdated.service", + "System update management service.", + BUS_IMPLEMENTATIONS(&manager_object, + &log_control_object), + argc, argv); + if (r <= 0) + return r; + + umask(0022); + + /* SIGCHLD signal must be blocked for sd_event_add_child to work */ + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Failed to allocate manager object: %m"); + + r = manager_add_bus_objects(m); + if (r < 0) + return log_error_errno(r, "Failed to add bus objects: %m"); + + r = manager_run(m); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/units/meson.build b/units/meson.build index bdc34e6f2c..004c5f92d4 100644 --- a/units/meson.build +++ b/units/meson.build @@ -640,6 +640,11 @@ units = [ 'file' : 'systemd-sysupdate.service.in', 'conditions' : ['ENABLE_SYSUPDATE'], }, + { + 'file' : 'systemd-sysupdated.service.in', + 'conditions' : ['ENABLE_SYSUPDATE'], + 'symlinks' : ['dbus-org.freedesktop.sysupdate1.service'], + }, { 'file' : 'systemd-sysupdate.timer', 'conditions' : ['ENABLE_SYSUPDATE'], diff --git a/units/systemd-sysupdated.service.in b/units/systemd-sysupdated.service.in new file mode 100644 index 0000000000..28671fbc54 --- /dev/null +++ b/units/systemd-sysupdated.service.in @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Update Service +Documentation=man:systemd-sysupdated.service(8) +Documentation=man:org.freedesktop.sysupdate1(5) + +[Service] +ExecStart={{LIBEXECDIR}}/systemd-sysupdated +BusName=org.freedesktop.sysupdate1 +KillMode=mixed +CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE +NoNewPrivileges=yes +MemoryDenyWriteExecute=yes +ProtectHostname=yes +RestrictRealtime=yes +RestrictNamespaces=net +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes +{{SERVICE_WATCHDOG}} From ec15bb71c2ccd57702325a6e6626053a343f3511 Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Thu, 13 Jul 2023 20:23:12 -0400 Subject: [PATCH 2/3] sysupdate: Implement updatectl This is the command-line tool to manage systemd-sysudpated Co-authored-by: Tom Coldrick Co-authored-by: Abderrahim Kitouni --- man/rules/meson.build | 1 + man/systemd-sysupdated.service.xml | 1 + man/updatectl.xml | 207 +++++ src/sysupdate/meson.build | 12 + src/sysupdate/updatectl.c | 1274 ++++++++++++++++++++++++++++ 5 files changed, 1495 insertions(+) create mode 100644 man/updatectl.xml create mode 100644 src/sysupdate/updatectl.c diff --git a/man/rules/meson.build b/man/rules/meson.build index abe2b1e92f..a42143ec7c 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 b7a4f3942a..f28cc6c97f 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 0000000000..144fc5a418 --- /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 <command>list</command> + + $ 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 8bd422fc43..9186a454f2 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 0000000000..39957ea1a0 --- /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); From b8b38e3da67d6e76a3ef7fcc37b4884ee7fd0c18 Mon Sep 17 00:00:00 2001 From: Tom Coldrick Date: Thu, 20 Jun 2024 10:26:24 +0100 Subject: [PATCH 3/3] sysupdate: Add integration test for updatectl updates --- test/TEST-72-SYSUPDATE/meson.build | 1 + test/units/TEST-72-SYSUPDATE.sh | 75 +++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/test/TEST-72-SYSUPDATE/meson.build b/test/TEST-72-SYSUPDATE/meson.build index 8dec5f37e7..77370ce458 100644 --- a/test/TEST-72-SYSUPDATE/meson.build +++ b/test/TEST-72-SYSUPDATE/meson.build @@ -3,5 +3,6 @@ integration_tests += [ integration_test_template + { 'name' : fs.name(meson.current_source_dir()), + 'vm' : true, }, ] diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index 5e658e0485..3f4aa5ce8a 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -6,8 +6,10 @@ set -eux set -o pipefail SYSUPDATE=/lib/systemd/systemd-sysupdate +SYSUPDATED=/lib/systemd/systemd-sysupdated SECTOR_SIZES=(512 4096) WORKDIR="$(mktemp -d /var/tmp/test-72-XXXXXX)" +CONFIGDIR="/run/sysupdate.d" BACKING_FILE="$WORKDIR/joined.raw" export SYSTEMD_ESP_PATH="$WORKDIR/esp" export SYSTEMD_XBOOTLDR_PATH="$WORKDIR/xbootldr" @@ -28,6 +30,17 @@ if [[ ! -e /dev/loop-control ]]; then SECTOR_SIZES=(512) fi +# Set up sysupdated drop-in pointing at the correct definitions and setting +# no verification of images. +mkdir -p /run/systemd/system/systemd-sysupdated.service.d +cat >/run/systemd/system/systemd-sysupdated.service.d/override.conf<"$WORKDIR/defs/01-first.conf" <"$CONFIGDIR/01-first.conf" <"$WORKDIR/defs/02-second.conf" <"$CONFIGDIR/02-second.conf" <"$WORKDIR/defs/03-third.conf" <"$CONFIGDIR/03-third.conf" <"$WORKDIR/defs/04-fourth.conf" <"$CONFIGDIR/04-fourth.conf" <"$WORKDIR/defs/05-fifth.conf" <"$CONFIGDIR/05-fifth.conf" <"$WORKDIR/defs/02-second.conf" <"$CONFIGDIR/02-second.conf" <"$WORKDIR/defs/03-third.conf" <"$CONFIGDIR/03-third.conf" <