]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: Implement systemd-sysupdated dbus service
authorAdrian Vovk <adrianvovk@gmail.com>
Fri, 30 Jun 2023 02:58:41 +0000 (22:58 -0400)
committerTom Coldrick <thomas.coldrick@codethink.co.uk>
Wed, 21 Aug 2024 08:31:41 +0000 (09:31 +0100)
Co-authored-by: Tom Coldrick <thomas.coldrick@codethink.co.uk>
Co-authored-by: Abderrahim Kitouni <abderrahim.kitouni@codethink.co.uk>
17 files changed:
man/org.freedesktop.sysupdate1.xml [new file with mode: 0644]
man/rules/meson.build
man/systemd-sysupdate.xml
man/systemd-sysupdated.service.xml [new file with mode: 0644]
meson.build
po/POTFILES.in
src/shared/bus-locator.c
src/shared/bus-locator.h
src/sysupdate/meson.build
src/sysupdate/org.freedesktop.sysupdate1.conf [new file with mode: 0644]
src/sysupdate/org.freedesktop.sysupdate1.policy [new file with mode: 0644]
src/sysupdate/org.freedesktop.sysupdate1.service [new file with mode: 0644]
src/sysupdate/sysupdate-util.h
src/sysupdate/sysupdate.c
src/sysupdate/sysupdated.c [new file with mode: 0644]
units/meson.build
units/systemd-sysupdated.service.in [new file with mode: 0644]

diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml
new file mode 100644 (file)
index 0000000..ac0e915
--- /dev/null
@@ -0,0 +1,487 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" >
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="org.freedesktop.sysupdate1" conditional='ENABLE_SYSUPDATE'
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+  <refentryinfo>
+    <title>org.freedesktop.sysupdate1</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>org.freedesktop.sysupdate1</refentrytitle>
+    <manvolnum>5</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>org.freedesktop.sysupdate1</refname>
+    <refpurpose>The D-Bus interface of systemd-sysupdated</refpurpose>
+  </refnamediv>
+
+  <refsect1>
+    <title>Introduction</title>
+
+    <para>
+    <citerefentry><refentrytitle>systemd-sysupdated.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    is a system service that allows unprivileged clients to update the system. This page describes the D-Bus
+    interface.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>The Manager Object</title>
+
+    <para>The service exposes the following interfaces on the Manager object on the bus:</para>
+
+    <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1" interface="org.freedesktop.sysupdate1.Manager">
+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 { ... };
+};
+    </programlisting>
+
+    <!--Autogenerated cross-references for systemd.directives, do not edit-->
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Manager"/>
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Manager"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="ListTargets()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="ListJobs()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="ListAppStream()"/>
+
+    <variablelist class="dbus-signal" generated="True" extra-ref="JobRemoved()"/>
+
+    <!--End of Autogenerated section-->
+
+    <refsect2>
+      <title>Methods</title>
+
+      <para><function>ListTargets()</function> 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
+      <varname>Class</varname> property below for an explanation of the possible values), a string
+      with the name of the target, and the target object path.</para>
+
+      <para><function>ListJobs()</function> 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
+      <varname>Type</varname> property below for an explanation of the possible values), the job's progress,
+      and the job's object path.</para>
+
+      <para><function>ListAppStream()</function> returns an array of all the appstream catalog URLs that this
+      service knows about. See Target's <varname>GetAppStream()</varname> method below for more
+      details.</para>
+    </refsect2>
+
+    <refsect2>
+      <title>Signals</title>
+
+      <para>The <function>JobRemoved()</function> 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. <literal>EXIT_FAILURE</literal>), and a negative status should be treated as a
+      negative errno-style error code (i.e. <literal>-EINVAL</literal>).</para>
+    </refsect2>
+  </refsect1>
+
+  <refsect1>
+    <title>The Target Object</title>
+
+    <para>A target is a component of the system (i.e. the host itself, a sysext, a confext, etc.) that
+    can be updated by
+    <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+    </para>
+
+    <para>The service exposes the following interfaces on Target objects on the bus:</para>
+
+    <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1/target/host" interface="org.freedesktop.sysupdate1.Target">
+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 { ... };
+};
+    </programlisting>
+
+    <!--Autogenerated cross-references for systemd.directives, do not edit-->
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Target"/>
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Target"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="List()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="Describe()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="CheckNew()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="Update()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="Vacuum()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="GetAppStream()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="GetVersion()"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Class"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Name"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Path"/>
+
+    <!--End of Autogenerated section-->
+
+    <refsect2>
+      <title>Methods</title>
+
+      <para><function>List()</function> returns a list of versions available for this target.  The
+      <varname>flags</varname> argument can be used to pass additional options, with bit 0 mapping to
+      <option>offline</option>.  When <option>offline</option> 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 <function>Describe()</function> to query more information about each
+      version returned by this method.</para>
+
+      <para><function>Describe()</function> returns all known information about a given version as a JSON
+      object. The <varname>version</varname> argument is used to pass the version to be described. Additional
+      options may be passed through the <varname>flags</varname> argument. The only supported value currently
+      is <varname>SD_SYSTEMD_SYSUPDATE_OFFLINE</varname>, which prevents the call from accessing the network
+      and restricts results to locally installed versions. This flag is defined as follows:</para>
+
+      <programlisting>
+#define SD_SYSTEMD_SYSUPDATE_OFFLINE    (UINT64_C(1) &lt;&lt; 0)
+      </programlisting>
+
+      <para>The returned JSON object contains several known keys. More keys may be added in the future. The
+      currently known keys are as follows:</para>
+
+      <variablelist>
+        <varlistentry>
+          <term>version</term>
+          <listitem><para>A string containing the version number.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>newest</term>
+          <listitem><para>A boolean indicating whether this version is the latest available for the target.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>available</term>
+          <listitem><para>A boolean indicating whether this version is available for download.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>installed</term>
+          <listitem><para>A boolean indicating whether this version is installed locally.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>obsolete</term>
+          <listitem><para>A boolean indicating whether this version is considered obsolete by the service,
+          and is therefore disallowed from being installed.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>protected</term>
+          <listitem><para>A boolean indicating whether this version is exempt from deletion by a
+          <function>Vacuum()</function> operation.</para></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term>changelog_urls</term>
+          <listitem><para>A list of strings that contain user-presentable URLs to ChangeLogs associated with
+          this version.</para></listitem>
+        </varlistentry>
+      </variablelist>
+      
+      <para><function>CheckNew()</function> 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
+      <function>Describe()</function> to query more information about the version returned by this method.
+      </para>
+
+      <para><function>Update()</function> installs an update for this target. If a
+      <varname>new_version</varname> is specified, that is the version that gets installed. Otherwise, the
+      latest version is installed. The <varname>flags</varname> argument is added for future
+      extensibility. No flags are currently defined, and the argument is required to be set to
+      <literal>0</literal>. Unlike all the other methods in this interface, <function>Update()</function>
+      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 <function>JobRemoved()</function> signal to detect when the job is complete.</para>
+
+      <para><function>Vacuum()</function> deletes old installed versions of this target to free up space.
+      It returns the number of instances that have been deleted.</para>
+
+      <para><function>GetAppStream()</function> returns a list of HTTP/HTTPS URLs to this target's
+      <ulink url="https://wwww.freedesktop.org/software/appstream/docs/chap-CatalogData.html">appstream catalog</ulink>
+      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 <ulink url="https://systemd.io/APPSTREAM_BUNDLE">special metadata</ulink> to allow the
+      software center to correctly associate the catalogs with this target.</para>
+
+      <para><function>GetVersion()</function> 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
+      <varname>IMAGE_VERSION</varname> in <filename>/etc/os-release</filename>. If the target has no current
+      version, the function will return an empty string.</para>
+
+    </refsect2>
+
+    <refsect2>
+      <title>Properties</title>
+
+      <para>The <varname>Class</varname> property exposes the class of this target, which describes
+      where it was enumerated. Possible values include: <literal>machine</literal> for containers and
+      virtual machines managed by
+      <citerefentry><refentrytitle>systemd-machined.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <literal>portable</literal> for <ulink url="https://systemd.io/PORTABLE_SERVICES">portable services</ulink>,
+      <literal>sysext</literal> for system extensions managed by
+      <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <literal>confext</literal> for configuration extensions managed by
+      <citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <literal>component</literal> for components accepted by the <option>--component=</option> option of
+      <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      and <literal>host</literal> for the host system itself. At most one target will have a class of
+      <literal>host</literal>.</para>
+
+      <para>The <varname>Path</varname> property exposes more detail about where this target was found.
+      For <literal>machine</literal>, <literal>portable</literal>, <literal>extension</literal>, and
+      <literal>confext</literal> targets, this is the file path to the image. For <literal>component</literal>
+      and <literal>host</literal> targets, this is the name of a
+      <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+      directory.</para>
+
+      <para>The <varname>Name</varname> 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 <literal>portable</literal> target named <literal>foobar</literal> and an
+      <literal>extension</literal> target named <literal>foobar</literal>, but it is not possible to have
+      two <literal>portable</literal> targets named <literal>foobar</literal>.</para>
+
+    </refsect2>
+
+    <refsect2>
+      <title>Security</title>
+
+      <para>Method calls on this service are authenticated via
+      <ulink url="https://www.freedesktop.org/software/polkit/docs/latest/">polkit</ulink>.</para>
+
+      <para><function>List()</function>, <function>Describe()</function>, and <function>CheckNew()</function>
+      use the polkit action <interfacename>org.freedesktop.sysupdate1.check</interfacename>.
+      By default, this action is permitted without administrator authentication.</para>
+
+      <para><function>Update()</function> uses the polkit action
+      <interfacename>org.freedesktop.sysupdate1.update</interfacename> when no version is specified.
+      By default, this action is permitted without administrator authentication. When a version is
+      specified, <interfacename>org.freedesktop.sysupdate1.update-to-version</interfacename> is
+      used instead. By default, this alternate action requires administrator authentication.</para>
+
+      <para><function>Vacuum()</function> uses the polkit action
+      <interfacename>org.freedesktop.sysupdate1.vacuum</interfacename>. By default, this action requires
+      administrator authentication.</para>
+
+      <para><function>GetAppStream()</function> and <function>GetVersion()</function> are unauthenticated and
+      may be called by anybody.</para>
+
+      <para>All methods called on this interface expose additional variables to the polkit rules.
+      <literal>class</literal> contains the class of the Target being acted upon, and <literal>name</literal>
+      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
+      <literal>SD_SYSTEMD_SYSUPDATE_OFFLINE</literal> flag is exposed as a variable named
+      <literal>offline</literal>.</para>
+    </refsect2>
+  </refsect1>
+
+  <refsect1>
+    <title>The Job Object</title>
+
+    <para>A job is an ongoing operation, started by one of the methods on a Target object.</para>
+
+    <para>The service exposes the following interfaces on Job objects on the bus:</para>
+
+    <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1/job/_1" interface="org.freedesktop.sysupdate1.Job">
+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 { ... };
+};
+    </programlisting>
+
+
+    <!--Autogenerated cross-references for systemd.directives, do not edit-->
+
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Job"/>
+
+
+    <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Job"/>
+
+
+    <variablelist class="dbus-method" generated="True" extra-ref="Cancel()"/>
+
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Id"/>
+
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Type"/>
+
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Offline"/>
+
+
+    <variablelist class="dbus-property" generated="True" extra-ref="Progress"/>
+
+
+    <!--End of Autogenerated section-->
+
+
+    <refsect2>
+      <title>Methods</title>
+
+      <para>The <function>Cancel()</function> method may be used to cancel the job. It takes no
+      parameters.</para>
+    </refsect2>
+
+    <refsect2>
+      <title>Properties</title>
+
+      <para>The <varname>Id</varname> property exposes the numeric job ID of the job object.</para>
+
+      <para>The <varname>Type</varname> property exposes the type of operation (one of: <literal>list</literal>,
+      <literal>describe</literal>, <literal>check-new</literal>, <literal>update</literal>, or <literal>vacuum</literal>).
+      </para>
+
+      <para>The <varname>Offline</varname> property exposes whether the job is permitted to access
+      the network or not.</para>
+
+      <para>The <varname>Progress</varname> property exposes the current progress of the job as a value
+      between 0 and 100. It is only available for <literal>update</literal> jobs; for all other jobs
+      it is always 0.</para>
+    </refsect2>
+
+    <refsect2>
+      <title>Security</title>
+
+      <para><function>Cancel()</function> uses the polkit action that corresponds to the method
+      that started this job. For instance, trying to cancel a <literal>list</literal> job will
+      require polkit to permit the <interfacename>org.freedesktop.sysupdate1.check</interfacename>
+      action.</para>
+    </refsect2>
+  </refsect1>
+
+  <refsect1>
+    <title>Examples</title>
+
+    <example>
+      <title>Introspect <interfacename>org.freedesktop.sysupdate1.Manager</interfacename> on the bus</title>
+
+      <programlisting>$ gdbus introspect --system \
+  --dest org.freedesktop.sysupdate1 \
+  --object-path /org/freedesktop/sysupdate1
+      </programlisting>
+    </example>
+
+    <example>
+      <title>Introspect <interfacename>org.freedesktop.sysupdate1.Target</interfacename> on the bus</title>
+
+      <programlisting>$ gdbus introspect --system \
+  --dest org.freedesktop.sysupdate1 \
+  --object-path /org/freedesktop/sysupdate1/target/host
+      </programlisting>
+    </example>
+
+    <example>
+      <title>Introspect <interfacename>org.freedesktop.sysupdate1.Job</interfacename> on the bus</title>
+
+      <programlisting>$ gdbus introspect --system \
+  --dest org.freedesktop.sysupdate1 \
+  --object-path /org/freedesktop/sysupdate1/job/_1
+      </programlisting>
+    </example>
+  </refsect1>
+
+  <xi:include href="org.freedesktop.locale1.xml" xpointer="versioning"/>
+  <refsect1>
+    <title>History</title>
+    <refsect2>
+      <title>The Manager Object</title>
+      <para><function>ListTargets()</function>,
+      <function>ListJobs()</function>,
+      <function>ListAppStream()</function>, and
+      <function>JobRemoved()</function> were added in version 257.</para>
+    </refsect2>
+    <refsect2>
+      <title>The Target Object</title>
+      <para><function>List()</function>,
+      <function>Describe()</function>,
+      <function>CheckNew()</function>,
+      <function>Update()</function>,
+      <function>Vacuum()</function>,
+      <function>GetAppStream()</function>,
+      <function>GetVersion()</function>,
+      <varname>Class</varname>,
+      <varname>Name</varname>, and
+      <varname>Path</varname> were added in version 257.</para>
+    </refsect2>
+    <refsect2>
+      <title>The Job Object</title>
+      <para><function>Cancel()</function>,
+      <varname>Id</varname>,
+      <varname>Type</varname>,
+      <varname>Offline</varname>, and
+      <varname>Progress</varname> were added in version 257.</para>
+    </refsect2>
+  </refsect1>
+</refentry>
index fda14d55bd5f989e8f3f15acb2b710fcc81635d5..abe2b1e92fc9a92a2768e298488aca310d639e26 100644 (file)
@@ -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',
index f77bd3d0d9813ad3631c4e235d10b0a06a595a7d..dffe835c04a0122a950b92bff786e446f922c5c6 100644 (file)
     <para><simplelist type="inline">
       <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-sysupdated.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
     </simplelist></para>
   </refsect1>
diff --git a/man/systemd-sysupdated.service.xml b/man/systemd-sysupdated.service.xml
new file mode 100644 (file)
index 0000000..b7a4f39
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-sysupdated.service" conditional='ENABLE_SYSUPDATE'>
+
+  <refentryinfo>
+    <title>systemd-sysupdated.service</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-sysupdated.service</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-sysupdated.service</refname>
+    <refname>systemd-sysupdated</refname>
+    <refpurpose>System Update Service</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><filename>systemd-sysupdated.service</filename></para>
+    <para><filename>/usr/lib/systemd/systemd-sysupdated</filename></para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>systemd-sysupdated</command> 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
+    <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+    </para>
+
+    <para>See
+    <citerefentry><refentrytitle>org.freedesktop.sysupdate1</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+    and
+    <citerefentry><refentrytitle>org.freedesktop.LogControl1</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+    for a description of the D-Bus API.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    </para>
+  </refsect1>
+
+</refentry>
index cef6ab9cb82313abf632ccab37dd5337edbdd716..5e0b666c64b17633a708387184de393e83f02995 100644 (file)
@@ -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')
index 16899fd5f9f4dc738123a03673a04bb448309ae7..d9c602cf20d8af41edb6ef9555c732e5aa2cdc99 100644 (file)
@@ -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
index ff7a872bdb3310a6ccade423f97c8e7736c26520..80d2b5371c9bfcc55bb990dc225da00d1cea6675 100644 (file)
@@ -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",
index 4f50a9727f560c34d742bbeeb375aa554fac08cb..8116aa27c0a8e1a25deeffdd5a1ea65eb746ac54 100644 (file)
@@ -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;
 
index b1b1204a2ab5947c18dcab3709830173c863c286..8bd422fc433043acde9095dc37041753801ce1f6 100644 (file)
@@ -30,4 +30,20 @@ executables += [
                         threads,
                 ],
         },
+        libexec_template + {
+                'name' : 'systemd-sysupdated',
+                'dbus' : true,
+                'conditions' : ['ENABLE_SYSUPDATE'],
+                'sources' : files('sysupdated.c'),
+                'dependencies' : threads,
+        },
 ]
+
+if conf.get('ENABLE_SYSUPDATE') == 1
+        install_data('org.freedesktop.sysupdate1.conf',
+                     install_dir : dbuspolicydir)
+        install_data('org.freedesktop.sysupdate1.service',
+                     install_dir : dbussystemservicedir)
+        install_data('org.freedesktop.sysupdate1.policy',
+                     install_dir : polkitpolicydir)
+endif
diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf
new file mode 100644 (file)
index 0000000..30cb1ee
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+        "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!--
+  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.
+-->
+
+<busconfig>
+
+        <policy user="root">
+                <allow own="org.freedesktop.sysupdate1"/>
+                <allow send_destination="org.freedesktop.sysupdate1"/>
+                <allow receive_sender="org.freedesktop.sysupdate1"/>
+        </policy>
+
+        <policy context="default">
+                <deny send_destination="org.freedesktop.sysupdate1"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.DBus.Introspectable"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.DBus.Peer"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.DBus.Properties"
+                       send_member="Get"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.DBus.Properties"
+                       send_member="GetAll"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Manager"
+                       send_member="ListTargets"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Manager"
+                       send_member="ListJobs"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Manager"
+                       send_member="ListAppStream"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="List"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="Describe"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="CheckNew"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="Update"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="Vacuum"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="GetAppstream"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="GetVersion"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Job"
+                       send_member="Cancel"/>
+
+                <allow receive_sender="org.freedesktop.sysupdate1"/>
+        </policy>
+
+</busconfig>
diff --git a/src/sysupdate/org.freedesktop.sysupdate1.policy b/src/sysupdate/org.freedesktop.sysupdate1.policy
new file mode 100644 (file)
index 0000000..7c1b943
--- /dev/null
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+        "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!--
+  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.
+-->
+
+<policyconfig>
+
+        <vendor>The systemd Project</vendor>
+        <vendor_url>https://systemd.io</vendor_url>
+
+        <!--
+            SECURITY: the default policy allows any user with an active session on the local console to check
+            for updates and update the system to the latest version without extra authentication.
+            Depending on the use case it might make sense to request authentication here, or add a polkit
+            rule to only allow access to these actions for members of a given group.
+
+            The default policy matches prior art in distributions and system update managers. To update a
+            system, for example: packagekit requires only a user with an active session, eos-updater needs
+            a user at the console, and rpm-ostree (generally) needs an "administrative user" at the computer.
+            Without this default, distributions hoping to use sysupdate as an update mechanism will have to
+            set the policy to it anyhow.
+        -->
+
+        <action id="org.freedesktop.sysupdate1.check">
+                <description gettext-domain="systemd">Check for system updates</description>
+                <message gettext-domain="systemd">Authentication is required to check for system updates</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>yes</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.sysupdate1.update">
+                <description gettext-domain="systemd">Install system updates</description>
+                <message gettext-domain="systemd">Authentication is required to install system updates</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>yes</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.sysupdate1.update-to-version">
+                <description gettext-domain="systemd">Install specific system version</description>
+                <message gettext-domain="systemd">Authentication is required to update the system to a specific (possibly old) version</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.sysupdate1.vacuum">
+                <description gettext-domain="systemd">Cleanup old system updates</description>
+                <message gettext-domain="systemd">Authentication is required to cleanup old system updates</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+</policyconfig>
diff --git a/src/sysupdate/org.freedesktop.sysupdate1.service b/src/sysupdate/org.freedesktop.sysupdate1.service
new file mode 100644 (file)
index 0000000..67e1a29
--- /dev/null
@@ -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
index fdd6c8318e7c8d5df1b4b1108cdd8dbdb92a069c..56339a87b15233da086ada6cf3e2883fd1c6ce79 100644 (file)
@@ -3,3 +3,5 @@
 #pragma once
 
 int reboot_now(void);
+
+#define SD_SYSTEMD_SYSUPDATE_OFFLINE  (UINT64_C(1) << 0)
index aded7dde0b4306a6a7849b54de9a65fa4fa1cd89..dee8348bdb8644f08d2a8c1a6c29c54c8c8d5085 100644 (file)
@@ -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 (file)
index 0000000..e2c3d7e
--- /dev/null
@@ -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);
index bdc34e6f2c826979e1b2c707155eb9754f3fb11b..004c5f92d4558a7f21d4edf2f4eaa88bbdd13263 100644 (file)
@@ -640,6 +640,11 @@ units = [
           'file' : 'systemd-sysupdate.service.in',
           'conditions' : ['ENABLE_SYSUPDATE'],
         },
+        {
+          'file' : 'systemd-sysupdated.service.in',
+          'conditions' : ['ENABLE_SYSUPDATE'],
+          'symlinks' : ['dbus-org.freedesktop.sysupdate1.service'],
+        },
         {
           'file' : 'systemd-sysupdate.timer',
           'conditions' : ['ENABLE_SYSUPDATE'],
diff --git a/units/systemd-sysupdated.service.in b/units/systemd-sysupdated.service.in
new file mode 100644 (file)
index 0000000..28671fb
--- /dev/null
@@ -0,0 +1,30 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=System Update Service
+Documentation=man:systemd-sysupdated.service(8)
+Documentation=man:org.freedesktop.sysupdate1(5)
+
+[Service]
+ExecStart={{LIBEXECDIR}}/systemd-sysupdated
+BusName=org.freedesktop.sysupdate1
+KillMode=mixed
+CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE
+NoNewPrivileges=yes
+MemoryDenyWriteExecute=yes
+ProtectHostname=yes
+RestrictRealtime=yes
+RestrictNamespaces=net
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+SystemCallFilter=@system-service @mount
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+LockPersonality=yes
+{{SERVICE_WATCHDOG}}