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 org.freedesktop.sysupdate1.Manager on the bus
+
+ $ gdbus introspect --system \
+ --dest org.freedesktop.sysupdate1 \
+ --object-path /org/freedesktop/sysupdate1
+
+
+
+
+ Introspect org.freedesktop.sysupdate1.Target on the bus
+
+ $ gdbus introspect --system \
+ --dest org.freedesktop.sysupdate1 \
+ --object-path /org/freedesktop/sysupdate1/target/host
+
+
+
+
+ Introspect org.freedesktop.sysupdate1.Job 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..a42143ec7c 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',
@@ -1277,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-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..f28cc6c97f
--- /dev/null
+++ b/man/systemd-sysupdated.service.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ 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
+ 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 list
+
+ $ updatectl list
+TARGET VERSION PATH
+host 48 sysupdate.d
+machine:fedora 38 /var/lib/machines/fedora.raw
+component:shim 15.7 sysupdate.shim.d
+$ updatectl list host
+ VERSION STATUS
+↻ 50 candidate
+ 49 available
+● 48 current
+ 47 available
+ 46 available
+ 45 available
+[...]
+× 25 available+obsolete
+× 24 available+obsolete
+× 23 available+obsolete
+[...]
+$ updatectl list host@49
+↻ Version: 50
+ State: candidate
+ ChangeLog: https://vendor.com/os/v50.html
+
+TYPE PATH PTUUID PTFLAGS SHA256
+url-file http://dl.vendor.com/os/uki_50 - - 90f6534282dd720f7a222fa781086990dc9c83e5c7499f085970a8e75e3ac349
+url-file http://dl.vendor.com/os/usr_50_981a5b84-a301-c819-f681-3e575fe16f16 981a5b84-a301-c819-f681-3e575fe16f16 - c0596ab1095258ec6f16c7c281a50d71c419a9f587c1ef858cfbbb69fb0a16f3
+url-file http://dl.vendor.com/os/verity_50_2f8d0f3b-f80a-6ddc-a556-3722bfbb5b79 2f8d0f3b-f80a-6ddc-a556-3722bfbb5b79 - e1e90a128e038b3a53455e55d1ca717c743aba31fe6b4b4624109df0243c6338
+url-file http://dl.vendor.com/os/verity_sig_50 - - ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356
+
+
+
+
+ Checking for and installing updates
+
+ $ updatectl check
+TARGET UPDATE
+host 48 → 50
+machine:fedora 38 → 40
+$ updatectl update host machine:fedora@39
+[...]
+✓ host@50
+✓ machine:fedora@39
+
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd-sysupdate8
+ systemd-sysupdated.service8
+ sysupdate.d5,
+
+
+
+
diff --git a/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..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',
@@ -30,4 +36,26 @@ executables += [
threads,
],
},
+ libexec_template + {
+ 'name' : 'systemd-sysupdated',
+ 'dbus' : true,
+ 'conditions' : ['ENABLE_SYSUPDATE'],
+ '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
+ 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/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);
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" <