summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--man/org.freedesktop.sysupdate1.xml487
-rw-r--r--man/rules/meson.build5
-rw-r--r--man/systemd-sysupdate.xml1
-rw-r--r--man/systemd-sysupdated.service.xml54
-rw-r--r--meson.build1
-rw-r--r--po/POTFILES.in1
-rw-r--r--src/shared/bus-locator.c6
-rw-r--r--src/shared/bus-locator.h1
-rw-r--r--src/sysupdate/meson.build16
-rw-r--r--src/sysupdate/org.freedesktop.sysupdate1.conf88
-rw-r--r--src/sysupdate/org.freedesktop.sysupdate1.policy74
-rw-r--r--src/sysupdate/org.freedesktop.sysupdate1.service14
-rw-r--r--src/sysupdate/sysupdate-util.h2
-rw-r--r--src/sysupdate/sysupdate.c2
-rw-r--r--src/sysupdate/sysupdated.c1911
-rw-r--r--units/meson.build5
-rw-r--r--units/systemd-sysupdated.service.in30
17 files changed, 2697 insertions, 1 deletions
diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml
new file mode 100644
index 0000000000..ac0e9152a1
--- /dev/null
+++ b/man/org.freedesktop.sysupdate1.xml
@@ -0,0 +1,487 @@
+<?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>
diff --git a/man/rules/meson.build b/man/rules/meson.build
index fda14d55bd..abe2b1e92f 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -65,6 +65,7 @@ manpages = [
['org.freedesktop.portable1', '5', [], 'ENABLE_PORTABLED'],
['org.freedesktop.resolve1', '5', [], 'ENABLE_RESOLVE'],
['org.freedesktop.systemd1', '5', [], ''],
+ ['org.freedesktop.sysupdate1', '5', [], 'ENABLE_SYSUPDATE'],
['org.freedesktop.timedate1', '5', [], 'ENABLE_TIMEDATED'],
['os-release', '5', ['extension-release', 'initrd-release'], ''],
['pam_systemd', '8', [], 'HAVE_PAM'],
@@ -1100,6 +1101,10 @@ manpages = [
'systemd-sysupdate.service',
'systemd-sysupdate.timer'],
'ENABLE_SYSUPDATE'],
+ ['systemd-sysupdated.service',
+ '8',
+ ['systemd-sysupdated'],
+ 'ENABLE_SYSUPDATE'],
['systemd-sysusers', '8', ['systemd-sysusers.service'], ''],
['systemd-sysv-generator', '8', [], 'HAVE_SYSV_COMPAT'],
['systemd-time-wait-sync.service',
diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml
index f77bd3d0d9..dffe835c04 100644
--- a/man/systemd-sysupdate.xml
+++ b/man/systemd-sysupdate.xml
@@ -322,6 +322,7 @@
<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
index 0000000000..b7a4f3942a
--- /dev/null
+++ b/man/systemd-sysupdated.service.xml
@@ -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>
diff --git a/meson.build b/meson.build
index cef6ab9cb8..5e0b666c64 100644
--- a/meson.build
+++ b/meson.build
@@ -277,6 +277,7 @@ conf.set_quoted('SYSTEMD_LANGUAGE_FALLBACK_MAP', pkgdatadir / 'lang
conf.set_quoted('SYSTEMD_MAKEFS_PATH', libexecdir / 'systemd-makefs')
conf.set_quoted('SYSTEMD_PULL_PATH', libexecdir / 'systemd-pull')
conf.set_quoted('SYSTEMD_SHUTDOWN_BINARY_PATH', libexecdir / 'systemd-shutdown')
+conf.set_quoted('SYSTEMD_SYSUPDATE_PATH', libexecdir / 'systemd-sysupdate')
conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir)
conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent')
conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 16899fd5f9..d9c602cf20 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -11,5 +11,6 @@ src/machine/org.freedesktop.machine1.policy
src/network/org.freedesktop.network1.policy
src/portable/org.freedesktop.portable1.policy
src/resolve/org.freedesktop.resolve1.policy
+src/sysupdate/org.freedesktop.sysupdate1.policy
src/timedate/org.freedesktop.timedate1.policy
src/core/dbus-unit.c
diff --git a/src/shared/bus-locator.c b/src/shared/bus-locator.c
index ff7a872bdb..80d2b5371c 100644
--- a/src/shared/bus-locator.c
+++ b/src/shared/bus-locator.c
@@ -63,6 +63,12 @@ const BusLocator* const bus_systemd_mgr = &(BusLocator){
.interface = "org.freedesktop.systemd1.Manager"
};
+const BusLocator* const bus_sysupdate_mgr = &(BusLocator){
+ .destination = "org.freedesktop.sysupdate1",
+ .path = "/org/freedesktop/sysupdate1",
+ .interface = "org.freedesktop.sysupdate1.Manager"
+};
+
const BusLocator* const bus_timedate = &(BusLocator){
.destination = "org.freedesktop.timedate1",
.path = "/org/freedesktop/timedate1",
diff --git a/src/shared/bus-locator.h b/src/shared/bus-locator.h
index 4f50a9727f..8116aa27c0 100644
--- a/src/shared/bus-locator.h
+++ b/src/shared/bus-locator.h
@@ -20,6 +20,7 @@ extern const BusLocator* const bus_oom_mgr;
extern const BusLocator* const bus_portable_mgr;
extern const BusLocator* const bus_resolve_mgr;
extern const BusLocator* const bus_systemd_mgr;
+extern const BusLocator* const bus_sysupdate_mgr;
extern const BusLocator* const bus_timedate;
extern const BusLocator* const bus_timesync_mgr;
diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build
index b1b1204a2a..8bd422fc43 100644
--- a/src/sysupdate/meson.build
+++ b/src/sysupdate/meson.build
@@ -30,4 +30,20 @@ executables += [
threads,
],
},
+ libexec_template + {
+ 'name' : 'systemd-sysupdated',
+ 'dbus' : true,
+ 'conditions' : ['ENABLE_SYSUPDATE'],
+ 'sources' : files('sysupdated.c'),
+ 'dependencies' : threads,
+ },
]
+
+if conf.get('ENABLE_SYSUPDATE') == 1
+ install_data('org.freedesktop.sysupdate1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.sysupdate1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.sysupdate1.policy',
+ install_dir : polkitpolicydir)
+endif
diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf
new file mode 100644
index 0000000000..30cb1eec24
--- /dev/null
+++ b/src/sysupdate/org.freedesktop.sysupdate1.conf
@@ -0,0 +1,88 @@
+<?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
index 0000000000..7c1b94333c
--- /dev/null
+++ b/src/sysupdate/org.freedesktop.sysupdate1.policy
@@ -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
index 0000000000..67e1a29078
--- /dev/null
+++ b/src/sysupdate/org.freedesktop.sysupdate1.service
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.sysupdate1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.sysupdate1.service
diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h
index fdd6c8318e..56339a87b1 100644
--- a/src/sysupdate/sysupdate-util.h
+++ b/src/sysupdate/sysupdate-util.h
@@ -3,3 +3,5 @@
#pragma once
int reboot_now(void);
+
+#define SD_SYSTEMD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0)
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index aded7dde0b..dee8348bdb 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -1543,7 +1543,7 @@ static int run(int argc, char *argv[]) {
return r;
/* SIGCHLD signal must be blocked for sd_event_add_child to work */
- BLOCK_SIGNALS(SIGCHLD);
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
return sysupdate_main(argc, argv);
}
diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c
new file mode 100644
index 0000000000..e2c3d7e102
--- /dev/null
+++ b/src/sysupdate/sysupdated.c
@@ -0,0 +1,1911 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-bus.h"
+#include "sd-json.h"
+
+#include "build-path.h"
+#include "bus-error.h"
+#include "bus-get-properties.h"
+#include "bus-label.h"
+#include "bus-log-control-api.h"
+#include "bus-polkit.h"
+#include "bus-util.h"
+#include "common-signal.h"
+#include "discover-image.h"
+#include "env-util.h"
+#include "escape.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hashmap.h"
+#include "log.h"
+#include "main-func.h"
+#include "memfd-util.h"
+#include "mkdir-label.h"
+#include "os-util.h"
+#include "process-util.h"
+#include "service-util.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "sysupdate-util.h"
+
+typedef struct Manager {
+ sd_event *event;
+ sd_bus *bus;
+
+ Hashmap *targets;
+
+ uint64_t last_job_id;
+ Hashmap *jobs;
+
+ Hashmap *polkit_registry;
+
+ sd_event_source *notify_event;
+} Manager;
+
+/* Forward declare so that jobs can call it on exit */
+static void manager_check_idle(Manager *m);
+
+typedef enum TargetClass {
+ /* These should try to match ImageClass from src/basic/os-util.h */
+ TARGET_MACHINE = IMAGE_MACHINE,
+ TARGET_PORTABLE = IMAGE_PORTABLE,
+ TARGET_SYSEXT = IMAGE_SYSEXT,
+ TARGET_CONFEXT = IMAGE_CONFEXT,
+ _TARGET_CLASS_IS_IMAGE_CLASS_MAX,
+
+ /* sysupdate-specific classes */
+ TARGET_HOST = _TARGET_CLASS_IS_IMAGE_CLASS_MAX,
+ TARGET_COMPONENT,
+
+ _TARGET_CLASS_MAX,
+ _TARGET_CLASS_INVALID = -EINVAL,
+} TargetClass;
+
+/* Let's ensure when the number of classes is updated things are updated here too */
+assert_cc((int) _IMAGE_CLASS_MAX == (int) _TARGET_CLASS_IS_IMAGE_CLASS_MAX);
+
+typedef struct Target {
+ Manager *manager;
+
+ TargetClass class;
+ char *name;
+ char *path;
+
+ char *id;
+ ImageType image_type;
+ bool busy;
+} Target;
+
+typedef enum JobType {
+ JOB_LIST,
+ JOB_DESCRIBE,
+ JOB_CHECK_NEW,
+ JOB_UPDATE,
+ JOB_VACUUM,
+ _JOB_TYPE_MAX,
+ _JOB_TYPE_INVALID = -EINVAL,
+} JobType;
+
+typedef struct Job Job;
+
+typedef int (*JobReady)(sd_bus_message *msg, const Job *job);
+typedef int (*JobComplete)(sd_bus_message *msg, const Job *job, sd_json_variant *response, sd_bus_error *error);
+
+struct Job {
+ Manager *manager;
+ Target *target;
+
+ uint64_t id;
+ char *object_path;
+
+ JobType type;
+ bool offline;
+ char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */
+
+ unsigned progress_percent;
+
+ sd_event_source *child;
+ int stdout_fd;
+ int status_errno;
+ unsigned n_cancelled;
+
+ sd_json_variant *json;
+
+ JobComplete complete_cb; /* Callback called on job exit */
+ sd_bus_message *dbus_msg;
+ JobReady detach_cb; /* Callback called when job has started. Detaches the job to run in the background */
+};
+
+static const char* const target_class_table[_TARGET_CLASS_MAX] = {
+ [TARGET_MACHINE] = "machine",
+ [TARGET_PORTABLE] = "portable",
+ [TARGET_SYSEXT] = "sysext",
+ [TARGET_CONFEXT] = "confext",
+ [TARGET_COMPONENT] = "component",
+ [TARGET_HOST] = "host",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass);
+
+static const char* const job_type_table[_JOB_TYPE_MAX] = {
+ [JOB_LIST] = "list",
+ [JOB_DESCRIBE] = "describe",
+ [JOB_CHECK_NEW] = "check-new",
+ [JOB_UPDATE] = "update",
+ [JOB_VACUUM] = "vacuum",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType);
+
+static Job *job_free(Job *j) {
+ if (!j)
+ return NULL;
+
+ if (j->manager)
+ assert_se(hashmap_remove(j->manager->jobs, &j->id) == j);
+
+ free(j->object_path);
+ free(j->version);
+
+ sd_json_variant_unref(j->json);
+
+ sd_bus_message_unref(j->dbus_msg);
+
+ sd_event_source_disable_unref(j->child);
+ safe_close(j->stdout_fd);
+
+ return mfree(j);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Job*, job_free);
+DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(job_hash_ops, uint64_t, uint64_hash_func, uint64_compare_func,
+ Job, job_free);
+
+static int job_new(JobType type, Target *t, sd_bus_message *msg, JobComplete complete_cb, Job **ret) {
+ _cleanup_(job_freep) Job *j = NULL;
+ int r;
+
+ assert(t);
+ assert(ret);
+
+ j = new(Job, 1);
+ if (!j)
+ return -ENOMEM;
+
+ *j = (Job) {
+ .type = type,
+ .target = t,
+ .id = t->manager->last_job_id + 1,
+ .stdout_fd = -EBADF,
+ .complete_cb = complete_cb,
+ .dbus_msg = sd_bus_message_ref(msg),
+ };
+
+ if (asprintf(&j->object_path, "/org/freedesktop/sysupdate1/job/_%" PRIu64, j->id) < 0)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(&t->manager->jobs, &job_hash_ops, &j->id, j);
+ if (r < 0)
+ return r;
+
+ j->manager = t->manager;
+
+ t->manager->last_job_id = j->id;
+
+ *ret = TAKE_PTR(j);
+ return 0;
+}
+
+static int job_parse_child_output(int _fd, sd_json_variant **ret) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ /* Take ownership of the passed fd */
+ _cleanup_close_ int fd = _fd;
+ _cleanup_fclose_ FILE *f = NULL;
+ struct stat st;
+ int r;
+
+ assert(ret);
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat stdout fd: %m");
+
+ assert(S_ISREG(st.st_mode));
+
+ if (st.st_size == 0) {
+ log_warning("No output from child job, ignoring");
+ return 0;
+ }
+
+ if (lseek(fd, SEEK_SET, 0) == (off_t) -1)
+ return log_error_errno(errno, "Failed to seek to beginning of memfd: %m");
+
+ f = take_fdopen(&fd, "r");
+ if (!f)
+ return log_error_errno(errno, "Failed to reopen memfd: %m");
+
+ r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static void job_on_ready(Job *j) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *msg = NULL;
+ int r;
+
+ assert(j);
+
+ /* Some jobs run in the background as we return the job ID to the dbus caller (i.e. for the Update
+ * method). However, the worker will perform some sanity-checks on startup which would be valuable
+ * as dbus errors. So, we wait for the worker to signal via READY=1 that it has completed its sanity
+ * checks and we should continue the job in the background. */
+
+ if (!j->detach_cb)
+ return;
+
+ assert(j->dbus_msg);
+ msg = TAKE_PTR(j->dbus_msg);
+
+ j->complete_cb = NULL;
+
+ r = j->detach_cb(msg, j);
+ if (r < 0)
+ log_warning_errno(r, "Failed to run callback on job ready event, ignoring: %m");
+}
+
+static void job_on_errno(Job *j, char *b) {
+ /* Take ownership of donated buffer */
+ _cleanup_free_ char *buf = TAKE_PTR(b);
+ int r;
+
+ assert(j);
+ assert_se(buf);
+
+ r = parse_errno(buf);
+ if (r < 0) {
+ log_warning_errno(r, "Got invalid errno value, ignoring: %m");
+ return;
+ }
+
+ j->status_errno = r;
+
+ log_debug_errno(r, "Got errno from job %" PRIu64 ": %i (%m)", j->id, r);
+}
+
+static void job_on_progress(Job *j, char *b) {
+ /* Take ownership of donated buffer */
+ _cleanup_free_ char *buf = TAKE_PTR(b);
+ unsigned progress;
+ int r;
+
+ assert(j);
+ assert_se(buf);
+
+ r = safe_atou(buf, &progress);
+ if (r < 0 || progress > 100) {
+ log_warning("Got invalid percent value, ignoring.");
+ return;
+ }
+
+ j->progress_percent = progress;
+ (void) sd_bus_emit_properties_changed(j->manager->bus, j->object_path,
+ "org.freedesktop.sysupdate1.Job",
+ "Progress", NULL);
+
+ log_debug("Got percentage from job %" PRIu64 ": %u%%", j->id, j->progress_percent);
+}
+
+static void job_on_version(Job *j, char *version) {
+ assert(j);
+ assert_se(version);
+
+ /* Take ownership of donated memory */
+ free_and_replace(j->version, version);
+
+ log_debug("Got version from job %" PRIu64 ": %s ", j->id, j->version);
+}
+
+static int job_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ Job *j = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+ Manager *manager = j->manager;
+ int r;
+
+ assert(j);
+ assert(s);
+ assert(si);
+
+ if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) {
+ assert(j->target->busy);
+ j->target->busy = false;
+ }
+
+ if (si->si_code != CLD_EXITED) {
+ assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED));
+ sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED,
+ "Job terminated abnormally with signal %s.",
+ signal_to_string(si->si_status));
+ } else if (si->si_status != EXIT_SUCCESS)
+ if (j->status_errno != 0)
+ sd_bus_error_set_errno(&error, j->status_errno);
+ else
+ sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED,
+ "Job failed with exit code %i.", si->si_status);
+ else {
+ r = job_parse_child_output(TAKE_FD(j->stdout_fd), &json);
+ if (r < 0)
+ sd_bus_error_set_errnof(&error, r, "Failed to parse JSON: %m");
+ }
+
+ /* Only send notification of exit if the job was actually detached */
+ if (j->detach_cb) {
+ r = sd_bus_emit_signal(
+ j->manager->bus,
+ "/org/freedesktop/sysupdate1",
+ "org.freedesktop.sysupdate1.Manager",
+ "JobRemoved",
+ "toi",
+ j->id,
+ j->object_path,
+ j->status_errno != 0 ? -j->status_errno : si->si_status);
+ if (r < 0)
+ log_warning_errno(r, "Cannot emit JobRemoved message, ignoring: %m");
+ }
+
+ if (j->dbus_msg && j->complete_cb) {
+ if (sd_bus_error_is_set(&error)) {
+ log_warning("Bus error occurred, ignoring callback for job: %s", error.message);
+ sd_bus_reply_method_error(j->dbus_msg, &error);
+ } else {
+ r = j->complete_cb(j->dbus_msg, j, json, &error);
+ if (r < 0) {
+ log_warning_errno(r, "Error during execution of job callback: %s", bus_error_message(&error, r));
+ sd_bus_reply_method_errno(j->dbus_msg, r, &error);
+ }
+ }
+ }
+
+ job_free(j);
+
+ if (manager)
+ manager_check_idle(manager);
+
+ return 0;
+}
+
+static inline const char* sysupdate_binary_path(void) {
+ return secure_getenv("SYSTEMD_SYSUPDATE_PATH") ?: SYSTEMD_SYSUPDATE_PATH;
+}
+
+static int target_get_argument(Target *t, char **ret) {
+ _cleanup_free_ char *target_arg = NULL;
+
+ assert(t);
+ assert(ret);
+
+ if (t->class != TARGET_HOST) {
+ if (t->class == TARGET_COMPONENT)
+ target_arg = strjoin("--component=", t->name);
+ else if (IN_SET(t->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME))
+ target_arg = strjoin("--root=", t->path);
+ else if (IN_SET(t->image_type, IMAGE_RAW, IMAGE_BLOCK))
+ target_arg = strjoin("--image=", t->path);
+ else
+ assert_not_reached();
+ if (!target_arg)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(target_arg);
+ return 0;
+}
+
+static int job_start(Job *j) {
+ _cleanup_close_ int stdout_fd = -EBADF;
+ _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
+ int r;
+
+ assert(j);
+
+ if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM) && j->target->busy)
+ return log_notice_errno(SYNTHETIC_ERRNO(EBUSY), "Target %s busy, ignoring job.", j->target->name);
+
+ stdout_fd = memfd_new("sysupdate-stdout");
+ if (stdout_fd < 0)
+ return log_error_errno(stdout_fd, "Failed to create memfd: %m");
+
+ r = pidref_safe_fork_full("(sd-sysupdate)",
+ (int[]) { -EBADF, stdout_fd, STDERR_FILENO }, NULL, 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|
+ FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
+ if (r < 0)
+ return r; /* FORK_LOG means pidref_safe_fork_full will handle the logging */
+ if (r == 0) {
+ /* Child */
+
+ _cleanup_free_ char *target_arg = NULL;
+ const char *cmd[] = {
+ "systemd-sysupdate",
+ "--json=short",
+ NULL, /* maybe --verify=no */
+ NULL, /* maybe --component=, --root=, or --image= */
+ NULL, /* maybe --offline */
+ NULL, /* list, check-new, update, vacuum */
+ NULL, /* maybe version (for list, update) */
+ NULL
+ };
+ size_t k = 2;
+
+ if (setenv("NOTIFY_SOCKET", "/run/systemd/sysupdate/notify", /* overwrite= */ 1) < 0) {
+ log_error_errno(errno, "setenv() failed: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
+ cmd[k++] = "--verify=no"; /* For testing */
+
+ r = setenv_systemd_exec_pid(true);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m");
+
+ r = target_get_argument(j->target, &target_arg);
+ if (r < 0) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+ if (target_arg)
+ cmd[k++] = target_arg;
+
+ if (j->offline)
+ cmd[k++] = "--offline";
+
+ switch (j->type) {
+ case JOB_LIST:
+ cmd[k++] = "list";
+ break;
+
+ case JOB_DESCRIBE:
+ cmd[k++] = "list";
+ assert(!isempty(j->version));
+ cmd[k++] = j->version;
+ break;
+
+ case JOB_CHECK_NEW:
+ cmd[k++] = "check-new";
+ break;
+
+ case JOB_UPDATE:
+ cmd[k++] = "update";
+ cmd[k++] = empty_to_null(j->version);
+ break;
+
+ case JOB_VACUUM:
+ cmd[k++] = "vacuum";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *s = NULL;
+
+ s = quote_command_line((char**) cmd, SHELL_ESCAPE_EMPTY);
+ if (!s) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+
+ log_debug("Spawning worker for job %" PRIu64 ": %s", j->id, s);
+ }
+
+ r = invoke_callout_binary(sysupdate_binary_path(), (char *const *) cmd);
+ log_error_errno(r, "Failed to execute systemd-sysupdate: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ r = event_add_child_pidref(j->manager->event, &j->child, &pid, WEXITED, job_on_exit, j);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add child process to event loop: %m");
+
+ r = sd_event_source_set_child_process_own(j->child, true);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed to take ownership of child process: %m");
+ TAKE_PIDREF(pid);
+
+ j->stdout_fd = TAKE_FD(stdout_fd);
+
+ if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM))
+ j->target->busy = true;
+
+ return 0;
+}
+
+static int job_cancel(Job *j) {
+ int r;
+
+ assert(j);
+
+ r = sd_event_source_send_child_signal(j->child, j->n_cancelled < 3 ? SIGTERM : SIGKILL,
+ NULL, 0);
+ if (r < 0)
+ return r;
+
+ j->n_cancelled++;
+ return 0;
+}
+
+static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Job *j = ASSERT_PTR(userdata);
+ const char *action;
+ int r;
+
+ assert(msg);
+
+ switch (j->type) {
+ case JOB_LIST:
+ case JOB_DESCRIBE:
+ case JOB_CHECK_NEW:
+ action = "org.freedesktop.sysupdate1.check";
+ break;
+
+ case JOB_UPDATE:
+ if (j->version)
+ action = "org.freedesktop.sysupdate1.update-to-version";
+ else
+ action = "org.freedesktop.sysupdate1.update";
+ break;
+
+ case JOB_VACUUM:
+ action = "org.freedesktop.sysupdate1.vacuum";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ r = bus_verify_polkit_async(
+ msg,
+ action,
+ /* details= */ NULL,
+ &j->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_cancel(j);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(msg, NULL);
+}
+
+static BUS_DEFINE_PROPERTY_GET_ENUM(job_property_get_type, job_type, JobType);
+
+static int job_object_find(
+ sd_bus *bus,
+ const char *path,
+ const char *iface,
+ void *userdata,
+ void **ret,
+ sd_bus_error *error) {
+
+ Manager *m = ASSERT_PTR(userdata);
+ Job *j;
+ const char *p;
+ uint64_t id;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(ret);
+
+ p = startswith(path, "/org/freedesktop/sysupdate1/job/_");
+ if (!p)
+ return 0;
+
+ r = safe_atou64(p, &id);
+ if (r < 0 || id == 0)
+ return 0;
+
+ j = hashmap_get(m->jobs, &id);
+ if (!j)
+ return 0;
+
+ *ret = j;
+ return 1;
+}
+
+static int job_node_enumerator(
+ sd_bus *bus,
+ const char *path,
+ void *userdata,
+ char ***nodes,
+ sd_bus_error *error) {
+
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Job *j;
+ unsigned k = 0;
+
+ l = new0(char*, hashmap_size(m->jobs) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(j, m->jobs) {
+ l[k] = strdup(j->object_path);
+ if (!l[k])
+ return -ENOMEM;
+ k++;
+ }
+
+ *nodes = TAKE_PTR(l);
+ return 1;
+}
+
+static const sd_bus_vtable job_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("Id", "t", NULL, offsetof(Job, id), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Type", "s", job_property_get_type, offsetof(Job, type), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Offline", "b", bus_property_get_bool, offsetof(Job, offline), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Progress", "u", bus_property_get_unsigned, offsetof(Job, progress_percent), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+
+ SD_BUS_METHOD("Cancel", NULL, NULL, job_method_cancel, SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+static const BusObjectImplementation job_object = {
+ "/org/freedesktop/sysupdate1/job",
+ "org.freedesktop.sysupdate1.Job",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({job_vtable, job_object_find}),
+ .node_enumerator = job_node_enumerator,
+};
+
+static Target *target_free(Target *t) {
+ if (!t)
+ return NULL;
+
+ free(t->name);
+ free(t->path);
+ free(t->id);
+
+ return mfree(t);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Target*, target_free);
+DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(target_hash_ops, char, string_hash_func, string_compare_func,
+ Target, target_free);
+
+static int target_new(Manager *m, TargetClass class, const char *name, const char *path, Target **ret) {
+ _cleanup_(target_freep) Target *t = NULL;
+ int r;
+
+ assert(m);
+ assert(ret);
+
+ t = new(Target, 1);
+ if (!t)
+ return -ENOMEM;
+
+ *t = (Target) {
+ .manager = m,
+ .class = class,
+ .image_type = _IMAGE_TYPE_INVALID,
+ };
+
+ t->name = strdup(name);
+ if (!t->name)
+ return -ENOMEM;
+
+ t->path = strdup(path);
+ if (!t->path)
+ return -ENOMEM;
+
+ if (class == TARGET_HOST)
+ t->id = strdup("host"); /* This is what appears in the object path */
+ else
+ t->id = strjoin(target_class_to_string(class), ":", name);
+ if (!t->id)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(t);
+ return 0;
+}
+
+static int sysupdate_run_simple(sd_json_variant **ret, ...) {
+ _cleanup_close_pair_ int pipe[2] = EBADF_PAIR;
+ _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ int r;
+
+ r = pipe2(pipe, O_CLOEXEC);
+ if (r < 0)
+ return -errno;
+
+ r = pidref_safe_fork_full("(sd-sysupdate)",
+ (int[]) { -EBADF, pipe[1], STDERR_FILENO },
+ NULL, 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|
+ FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG,
+ &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ va_list ap;
+ char *arg;
+ _cleanup_strv_free_ char **args = NULL;
+
+ if (strv_extend(&args, "systemd-sysupdate") < 0) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+
+ if (strv_extend(&args, "--json=short") < 0) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+
+ va_start(ap, ret);
+ while ((arg = va_arg(ap, char*))) {
+ r = strv_extend(&args, arg);
+ if (r < 0)
+ break;
+ }
+ va_end(ap);
+ if (r < 0) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *s = NULL;
+
+ s = quote_command_line((char**) args, SHELL_ESCAPE_EMPTY);
+ if (!s) {
+ log_oom();
+ _exit(EXIT_FAILURE);
+ }
+
+ log_debug("Spawning sysupdate: %s", s);
+ }
+
+ r = invoke_callout_binary(sysupdate_binary_path(), args);
+ log_error_errno(r, "Failed to execute systemd-sysupdate: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ pipe[1] = safe_close(pipe[1]);
+ f = take_fdopen(&pipe[0], "r");
+ if (!f)
+ return -errno;
+
+ r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static BUS_DEFINE_PROPERTY_GET_ENUM(target_property_get_class, target_class, TargetClass);
+
+#define log_sysupdate_bad_json(verb, msg) \
+ log_debug("Invalid JSON response from 'systemd-sysupdate %s': %s", verb, msg)
+
+static int target_method_list_finish(
+ sd_bus_message *msg,
+ const Job *j,
+ sd_json_variant *json,
+ sd_bus_error *error) {
+
+ sd_json_variant *v;
+ _cleanup_strv_free_ char **versions = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int r;
+
+ assert(json);
+
+ v = sd_json_variant_by_key(json, "all");
+ if (!v) {
+ log_sysupdate_bad_json("list", "Missing key 'all'");
+ return -EINVAL;
+ }
+
+ r = sd_json_variant_strv(v, &versions);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_strv(reply, versions);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int target_method_list(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(job_freep) Job *j = NULL;
+ int r;
+ uint64_t flags;
+
+ assert(msg);
+
+ r = sd_bus_message_read(msg, "t", &flags);
+ if (r < 0)
+ return r;
+
+ const char *details[] = {
+ "class", target_class_to_string(t->class),
+ "name", t->name,
+ "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)),
+ NULL
+ };
+
+ r = bus_verify_polkit_async(
+ msg,
+ "org.freedesktop.sysupdate1.check",
+ details,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_new(JOB_LIST, t, msg, target_method_list_finish, &j);
+ if (r < 0)
+ return r;
+
+ j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE);
+
+ r = job_start(j);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
+ TAKE_PTR(j); /* Avoid job from being killed & freed */
+
+ return 1;
+}
+
+static int target_method_describe_finish(
+ sd_bus_message *msg,
+ const Job *j,
+ sd_json_variant *json,
+ sd_bus_error *error) {
+ _cleanup_free_ char *text = NULL;
+ int r;
+
+ assert(json);
+
+ r = sd_json_variant_format(json, 0, &text);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(msg, "s", text);
+}
+
+static int target_method_describe(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(job_freep) Job *j = NULL;
+ const char *version;
+ int r;
+ uint64_t flags;
+
+ assert(msg);
+
+ r = sd_bus_message_read(msg, "st", &version, &flags);
+ if (r < 0)
+ return r;
+
+ if (isempty(version))
+ return -EINVAL;
+
+ const char *details[] = {
+ "class", target_class_to_string(t->class),
+ "name", t->name,
+ "version", version,
+ "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)),
+ NULL
+ };
+
+ r = bus_verify_polkit_async(
+ msg,
+ "org.freedesktop.sysupdate1.check",
+ details,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_new(JOB_DESCRIBE, t, msg, target_method_describe_finish, &j);
+ if (r < 0)
+ return r;
+
+ j->version = strdup(version);
+ if (!j->version)
+ return log_oom();
+
+ j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE);
+
+ r = job_start(j);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
+ TAKE_PTR(j); /* Avoid job from being killed & freed */
+
+ return 1;
+}
+
+static int target_method_check_new_finish(
+ sd_bus_message *msg,
+ const Job *j,
+ sd_json_variant *json,
+ sd_bus_error *error) {
+ const char *reply;
+
+ assert(json);
+
+ sd_json_variant *v = sd_json_variant_by_key(json, "available");
+ if (!v) {
+ log_sysupdate_bad_json("check-new", "Missing key 'available'");
+ return -EINVAL;
+ }
+
+ if (sd_json_variant_is_null(v))
+ reply = "";
+ else
+ reply = sd_json_variant_string(v);
+ if (!reply)
+ return -EINVAL;
+
+ return sd_bus_reply_method_return(msg, "s", reply);
+}
+
+static int target_method_check_new(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(job_freep) Job *j = NULL;
+ int r;
+
+ assert(msg);
+
+ const char *details[] = {
+ "class", target_class_to_string(t->class),
+ "name", t->name,
+ "offline", "0",
+ NULL
+ };
+
+ r = bus_verify_polkit_async(
+ msg,
+ "org.freedesktop.sysupdate1.check",
+ details,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_new(JOB_CHECK_NEW, t, msg, target_method_check_new_finish, &j);
+ if (r < 0)
+ return r;
+
+ r = job_start(j);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
+ TAKE_PTR(j); /* Avoid job from being killed & freed */
+
+ return 1;
+}
+
+static int target_method_update_finished_early(
+ sd_bus_message *msg,
+ const Job *j,
+ sd_json_variant *json,
+ sd_bus_error *error) {
+
+ /* Called when job finishes w/ a successful exit code, but before any work begins.
+ * This happens when there is no candidate (i.e. we're already up-to-date), or
+ * specified update is already installed. */
+ return sd_bus_error_setf(error, "org.freedesktop.sysupdate1.NoCandidate",
+ "Job exited successfully with no work to do, assume already updated");
+}
+
+static int target_method_update_detach(sd_bus_message *msg, const Job *j) {
+ int r;
+
+ assert(msg);
+ assert(j);
+
+ r = sd_bus_reply_method_return(msg, "sto", j->version, j->id, j->object_path);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ return 0;
+}
+
+static int target_method_update(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(job_freep) Job *j = NULL;
+ const char *version, *action;
+ uint64_t flags;
+ int r;
+
+ assert(msg);
+
+ r = sd_bus_message_read(msg, "st", &version, &flags);
+ if (r < 0)
+ return r;
+
+ if (flags != 0)
+ return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(EINVAL), "Flags argument must be 0: %m");
+
+ if (isempty(version))
+ action = "org.freedesktop.sysupdate1.update";
+ else
+ action = "org.freedesktop.sysupdate1.update-to-version";
+
+ const char *details[] = {
+ "class", target_class_to_string(t->class),
+ "name", t->name,
+ "version", version,
+ NULL
+ };
+
+ r = bus_verify_polkit_async(
+ msg,
+ action,
+ details,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_new(JOB_UPDATE, t, msg, target_method_update_finished_early, &j);
+ if (r < 0)
+ return r;
+ j->detach_cb = target_method_update_detach;
+
+ j->version = strdup(version);
+ if (!j->version)
+ return -ENOMEM;
+
+ r = job_start(j);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
+ TAKE_PTR(j);
+
+ return 1;
+}
+
+static int target_method_vacuum_finish(
+ sd_bus_message *msg,
+ const Job *j,
+ sd_json_variant *json,
+ sd_bus_error *error) {
+
+ uint64_t instances;
+
+ assert(json);
+
+ instances = sd_json_variant_unsigned(sd_json_variant_by_key(json, "removed"));
+
+ return sd_bus_reply_method_return(msg, "u", instances);
+}
+
+static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(job_freep) Job *j = NULL;
+ int r;
+
+ assert(msg);
+
+ const char *details[] = {
+ "class", target_class_to_string(t->class),
+ "name", t->name,
+ NULL
+ };
+
+ r = bus_verify_polkit_async(
+ msg,
+ "org.freedesktop.sysupdate1.vacuum",
+ details,
+ &t->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = job_new(JOB_VACUUM, t, msg, target_method_vacuum_finish, &j);
+ if (r < 0)
+ return r;
+
+ r = job_start(j);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
+ TAKE_PTR(j); /* Avoid job from being killed & freed */
+
+ return 1;
+}
+
+static int target_method_get_version(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_free_ char *target_arg = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ sd_json_variant *version_json;
+ int r;
+
+ r = target_get_argument(t, &target_arg);
+ if (r < 0)
+ return r;
+
+ r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL);
+ if (r < 0)
+ return r;
+
+ version_json = sd_json_variant_by_key(v, "current");
+ if (!version_json) {
+ log_sysupdate_bad_json("list", "Missing key 'current'");
+ return -EINVAL;
+ }
+
+ if (sd_json_variant_is_null(version_json))
+ return sd_bus_reply_method_return(msg, "s", "");
+
+ if (!sd_json_variant_is_string(version_json)) {
+ log_sysupdate_bad_json("list", "Expected string value for key 'current'");
+ return -EINVAL;
+ }
+
+ return sd_bus_reply_method_return(msg, "s", sd_json_variant_string(version_json));
+}
+
+static int target_get_appstream(Target *t, char ***ret) {
+ _cleanup_free_ char *target_arg = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ sd_json_variant *appstream_url_json;
+ int r;
+
+ r = target_get_argument(t, &target_arg);
+ if (r < 0)
+ return r;
+
+ r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL);
+ if (r < 0)
+ return r;
+
+ appstream_url_json = sd_json_variant_by_key(v, "appstream_urls");
+ if (!appstream_url_json) {
+ log_sysupdate_bad_json("list", "Missing key 'appstream_urls'");
+ return -EINVAL;
+ }
+
+ r = sd_json_variant_strv(appstream_url_json, ret);
+ if (r < 0) {
+ log_sysupdate_bad_json("list", "Expected array of strings for key 'appstream_urls'");
+ return r;
+ }
+
+ return 0;
+}
+
+static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ Target *t = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_strv_free_ char **appstream_urls = NULL;
+ int r;
+
+ r = target_get_appstream(t, &appstream_urls);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_strv(reply, appstream_urls);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+ _cleanup_strv_free_ char **components = NULL;
+ _cleanup_free_ char *target_arg = NULL;
+ sd_json_variant *v;
+ bool have_default;
+ int r;
+
+ if (t) {
+ r = target_get_argument(t, &target_arg);
+ if (r < 0)
+ return r;
+ }
+
+ r = sysupdate_run_simple(&json, "components", target_arg, NULL);
+ if (r < 0)
+ return r;
+
+ v = sd_json_variant_by_key(json, "default");
+ if (!v)
+ return -EINVAL;
+ have_default = sd_json_variant_boolean(v);
+
+ v = sd_json_variant_by_key(json, "components");
+ if (!v)
+ return -EINVAL;
+ r = sd_json_variant_strv(v, &components);
+ if (r < 0)
+ return r;
+
+ if (ret_components)
+ *ret_components = TAKE_PTR(components);
+ if (ret_have_default)
+ *ret_have_default = have_default;
+ return 0;
+}
+
+static int manager_ensure_targets(Manager *m);
+
+static int target_object_find(
+ sd_bus *bus,
+ const char *path,
+ const char *iface,
+ void *userdata,
+ void **found,
+ sd_bus_error *error) {
+
+ Manager *m = ASSERT_PTR(userdata);
+ Target *t;
+ _cleanup_free_ char *e = NULL;
+ const char *p;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(found);
+
+ p = startswith(path, "/org/freedesktop/sysupdate1/target/");
+ if (!p)
+ return 0;
+
+ e = bus_label_unescape(p);
+ if (!e)
+ return -ENOMEM;
+
+ r = manager_ensure_targets(m);
+ if (r < 0)
+ return r;
+
+ t = hashmap_get(m->targets, e);
+ if (!t)
+ return 0;
+
+ *found = t;
+ return 1;
+}
+
+static char *target_bus_path(Target *t) {
+ _cleanup_free_ char *e = NULL;
+
+ assert(t);
+
+ e = bus_label_escape(t->id);
+ if (!e)
+ return NULL;
+
+ return strjoin("/org/freedesktop/sysupdate1/target/", e);
+}
+
+static int target_node_enumerator(
+ sd_bus *bus,
+ const char *path,
+ void *userdata,
+ char ***nodes,
+ sd_bus_error *error) {
+
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Target *t;
+ unsigned k = 0;
+ int r;
+
+ r = manager_ensure_targets(m);
+ if (r < 0)
+ return r;
+
+ l = new0(char*, hashmap_size(m->targets) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(t, m->targets) {
+ l[k] = target_bus_path(t);
+ if (!l[k])
+ return -ENOMEM;
+ k++;
+ }
+
+ *nodes = TAKE_PTR(l);
+ return 1;
+}
+
+static const sd_bus_vtable target_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("Class", "s", target_property_get_class,
+ offsetof(Target, class), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Name", "s", NULL, offsetof(Target, name),
+ SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Path", "s", NULL, offsetof(Target, path),
+ SD_BUS_VTABLE_PROPERTY_CONST),
+
+ SD_BUS_METHOD_WITH_ARGS("List",
+ SD_BUS_ARGS("t", flags),
+ SD_BUS_RESULT("as", versions),
+ target_method_list,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("Describe",
+ SD_BUS_ARGS("s", version, "t", flags),
+ SD_BUS_RESULT("s", json),
+ target_method_describe,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("CheckNew",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("s", new_version),
+ target_method_check_new,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("Update",
+ SD_BUS_ARGS("s", new_version, "t", flags),
+ SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
+ target_method_update,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("Vacuum",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("u", count),
+ target_method_vacuum,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("GetAppStream",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("as", appstream),
+ target_method_get_appstream,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("GetVersion",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("s", version),
+ target_method_get_version,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+static const BusObjectImplementation target_object = {
+ "/org/freedesktop/sysupdate1/target",
+ "org.freedesktop.sysupdate1.Target",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({target_vtable, target_object_find}),
+ .node_enumerator = target_node_enumerator,
+};
+
+static Manager *manager_free(Manager *m) {
+ if (!m)
+ return NULL;
+
+ hashmap_free(m->targets);
+ hashmap_free(m->jobs);
+
+ m->bus = sd_bus_flush_close_unref(m->bus);
+ sd_event_source_unref(m->notify_event);
+ sd_event_unref(m->event);
+
+ return mfree(m);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager *, manager_free);
+
+static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ char buf[NOTIFY_BUFFER_MAX+1];
+ struct iovec iovec = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf)-1,
+ };
+ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control;
+ struct msghdr msghdr = {
+ .msg_iov = &iovec,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ struct ucred *ucred;
+ Manager *m = ASSERT_PTR(userdata);
+ Job *j;
+ ssize_t n;
+ char *p;
+
+ n = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
+ if (n < 0) {
+ if (ERRNO_IS_TRANSIENT(n))
+ return 0;
+ return (int) n;
+ }
+
+ cmsg_close_all(&msghdr);
+
+ if (msghdr.msg_flags & MSG_TRUNC) {
+ log_warning("Got overly long notification datagram, ignoring.");
+ return 0;
+ }
+
+ ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred);
+ if (!ucred || ucred->pid <= 0) {
+ log_warning("Got notification datagram lacking credential information, ignoring.");
+ return 0;
+ }
+
+ HASHMAP_FOREACH(j, m->jobs) {
+ pid_t pid;
+ assert_se(sd_event_source_get_child_pid(j->child, &pid) >= 0);
+
+ if (ucred->pid == pid)
+ break;
+ }
+
+ if (!j) {
+ log_warning("Got notification datagram from unexpected peer, ignoring.");
+ return 0;
+ }
+
+ buf[n] = 0;
+
+ p = find_line_startswith(buf, "X_SYSUPDATE_VERSION=");
+ if (p) {
+ p = strdupcspn(p, "\n");
+ if (p)
+ job_on_version(j, p);
+ }
+
+ p = find_line_startswith(buf, "ERRNO=");
+ if (p) {
+ p = strdupcspn(p, "\n");
+ if (p)
+ job_on_errno(j, p);
+ }
+
+ p = find_line_startswith(buf, "X_SYSUPDATE_PROGRESS=");
+ if (p) {
+ p = strdupcspn(p, "\n");
+ if (p)
+ job_on_progress(j, p);
+ }
+
+ /* Should come last, since this might actually detach the job */
+ if (find_line_startswith(buf, "READY=1"))
+ job_on_ready(j);
+
+ return 0;
+}
+
+static int manager_new(Manager **ret) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_close_ int notify_fd = -EBADF;
+ static const union sockaddr_union sa = {
+ .un.sun_family = AF_UNIX,
+ .un.sun_path = "/run/systemd/sysupdate/notify",
+ };
+ int r;
+
+ assert(ret);
+
+ m = new0(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_set_watchdog(m->event, true);
+
+ r = sd_event_set_signal_exit(m->event, true);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK,
+ sigrtmin18_handler, NULL);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL);
+ if (r < 0)
+ log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m");
+
+ r = sd_bus_default_system(&m->bus);
+ if (r < 0)
+ return r;
+
+ notify_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (notify_fd < 0)
+ return -errno;
+
+ (void) mkdir_parents_label(sa.un.sun_path, 0755);
+ (void) sockaddr_un_unlink(&sa.un);
+
+ if (bind(notify_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0)
+ return -errno;
+
+ r = setsockopt_int(notify_fd, SOL_SOCKET, SO_PASSCRED, true);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(m->event, &m->notify_event, notify_fd, EPOLLIN, manager_on_notify, m);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(m->notify_event, "notify-socket");
+
+ r = sd_event_source_set_io_fd_own(m->notify_event, true);
+ if (r < 0)
+ return r;
+ TAKE_FD(notify_fd);
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+static int manager_enumerate_image_class(Manager *m, TargetClass class) {
+ _cleanup_hashmap_free_ Hashmap *images = NULL;
+ Image *image;
+ int r;
+
+ images = hashmap_new(&image_hash_ops);
+ if (!images)
+ return -ENOMEM;
+
+ r = image_discover((ImageClass) class, NULL, images);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(image, images) {
+ Target *t = NULL;
+ bool have = false;
+
+ if (IMAGE_IS_HOST(image))
+ continue; /* We already enroll the host ourselves */
+
+ r = target_new(m, class, image->name, image->path, &t);
+ if (r < 0)
+ return r;
+ t->image_type = image->type;
+
+ r = target_list_components(t, NULL, &have);
+ if (r < 0)
+ return r;
+ if (!have) {
+ log_debug("Skipping %s because it has no default component", image->path);
+ continue;
+ }
+ }
+
+ return 0;
+}
+
+static int manager_enumerate_components(Manager *m) {
+ _cleanup_strv_free_ char **components = NULL;
+ bool have_default;
+ Target *t;
+ int r;
+
+ r = target_list_components(NULL, &components, &have_default);
+ if (r < 0)
+ return r;
+
+ if (have_default) {
+ r = target_new(m, TARGET_HOST, "host", "sysupdate.d", &t);
+ if (r < 0)
+ return r;
+ }
+
+ STRV_FOREACH(component, components) {
+ _cleanup_free_ char *path = NULL;
+
+ path = strjoin("sysupdate.", *component, ".d");
+ if (!path)
+ return -ENOMEM;
+
+ r = target_new(m, TARGET_COMPONENT, *component, path, &t);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int manager_enumerate_targets(Manager *m) {
+ static const TargetClass discoverable_classes[] = {
+ TARGET_MACHINE,
+ TARGET_PORTABLE,
+ TARGET_SYSEXT,
+ TARGET_CONFEXT,
+ };
+ int r;
+
+ assert(m);
+
+ FOREACH_ARRAY(class, discoverable_classes, ELEMENTSOF(discoverable_classes)) {
+ r = manager_enumerate_image_class(m, *class);
+ if (r < 0)
+ log_warning_errno(r, "Failed to enumerate %ss, ignoring: %m",
+ target_class_to_string(*class));
+ }
+
+ return manager_enumerate_components(m);
+}
+
+static int manager_ensure_targets(Manager *m) {
+ assert(m);
+
+ if (!hashmap_isempty(m->targets))
+ return 0;
+
+ return manager_enumerate_targets(m);
+}
+
+static int method_list_targets(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Target *t;
+ int r;
+
+ assert(msg);
+
+ r = manager_ensure_targets(m);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(sso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(t, m->targets) {
+ _cleanup_free_ char *bus_path = NULL;
+
+ bus_path = target_bus_path(t);
+ if (!bus_path)
+ return -ENOMEM;
+
+ r = sd_bus_message_append(reply, "(sso)",
+ target_class_to_string(t->class),
+ t->name,
+ bus_path);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_list_jobs(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Job *j;
+ int r;
+
+ assert(msg);
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(tsuo)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(j, m->jobs) {
+ r = sd_bus_message_append(reply, "(tsuo)",
+ j->id,
+ job_type_to_string(j->type),
+ j->progress_percent,
+ j->object_path);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_list_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
+ _cleanup_strv_free_ char **urls = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ Target *t;
+ int r;
+
+ assert(msg);
+
+ r = manager_ensure_targets(m);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(t, m->targets) {
+ _cleanup_strv_free_ char **target_appstream = NULL;
+ r = target_get_appstream(t, &target_appstream);
+ if (r < 0)
+ return r;
+
+ r = strv_extend_strv(&urls, target_appstream, true);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_new_method_return(msg, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_strv(reply, urls);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static const sd_bus_vtable manager_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_METHOD_WITH_ARGS("ListTargets",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("a(sso)", targets),
+ method_list_targets,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("ListJobs",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("a(tsuo)", jobs),
+ method_list_jobs,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_METHOD_WITH_ARGS("ListAppStream",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("as", urls),
+ method_list_appstream,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_SIGNAL_WITH_ARGS("JobRemoved",
+ SD_BUS_ARGS("t", id, "o", path, "i", status),
+ 0),
+
+ SD_BUS_VTABLE_END
+};
+
+static const BusObjectImplementation manager_object = {
+ "/org/freedesktop/sysupdate1",
+ "org.freedesktop.sysupdate1.Manager",
+ .vtables = BUS_VTABLES(manager_vtable),
+ .children = BUS_IMPLEMENTATIONS(&job_object, &target_object),
+};
+
+static int manager_add_bus_objects(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = bus_add_implementation(m->bus, &manager_object, m);
+ if (r < 0)
+ return r;
+
+ r = bus_log_control_api_register(m->bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.sysupdate1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(m->bus, m->event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ return 0;
+}
+
+static bool manager_is_idle(void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+
+ return hashmap_isempty(m->jobs);
+}
+
+static void manager_check_idle(Manager *m) {
+ assert(m);
+
+ if (!hashmap_isempty(m->jobs))
+ return;
+
+ hashmap_clear(m->targets);
+ log_debug("Cleared target cache");
+}
+
+static int manager_run(Manager *m) {
+ assert(m);
+
+ return bus_event_loop_with_idle(m->event,
+ m->bus,
+ "org.freedesktop.sysupdate1",
+ DEFAULT_EXIT_USEC,
+ manager_is_idle,
+ m);
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ log_setup();
+
+ r = service_parse_argv("systemd-sysupdated.service",
+ "System update management service.",
+ BUS_IMPLEMENTATIONS(&manager_object,
+ &log_control_object),
+ argc, argv);
+ if (r <= 0)
+ return r;
+
+ umask(0022);
+
+ /* SIGCHLD signal must be blocked for sd_event_add_child to work */
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
+
+ r = manager_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate manager object: %m");
+
+ r = manager_add_bus_objects(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add bus objects: %m");
+
+ r = manager_run(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/units/meson.build b/units/meson.build
index bdc34e6f2c..004c5f92d4 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -641,6 +641,11 @@ units = [
'conditions' : ['ENABLE_SYSUPDATE'],
},
{
+ 'file' : 'systemd-sysupdated.service.in',
+ 'conditions' : ['ENABLE_SYSUPDATE'],
+ 'symlinks' : ['dbus-org.freedesktop.sysupdate1.service'],
+ },
+ {
'file' : 'systemd-sysupdate.timer',
'conditions' : ['ENABLE_SYSUPDATE'],
},
diff --git a/units/systemd-sysupdated.service.in b/units/systemd-sysupdated.service.in
new file mode 100644
index 0000000000..28671fbc54
--- /dev/null
+++ b/units/systemd-sysupdated.service.in
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=System Update Service
+Documentation=man:systemd-sysupdated.service(8)
+Documentation=man:org.freedesktop.sysupdate1(5)
+
+[Service]
+ExecStart={{LIBEXECDIR}}/systemd-sysupdated
+BusName=org.freedesktop.sysupdate1
+KillMode=mixed
+CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE
+NoNewPrivileges=yes
+MemoryDenyWriteExecute=yes
+ProtectHostname=yes
+RestrictRealtime=yes
+RestrictNamespaces=net
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+SystemCallFilter=@system-service @mount
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+LockPersonality=yes
+{{SERVICE_WATCHDOG}}