]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdated: Add Acquire() and Install() D-Bus methods
authorPhilip Withnall <pwithnall@gnome.org>
Mon, 19 Jan 2026 17:16:54 +0000 (17:16 +0000)
committerPhilip Withnall <pwithnall@gnome.org>
Mon, 23 Feb 2026 16:35:01 +0000 (16:35 +0000)
These mirror the `sysupdate acquire` and `sysupdate update --offline`
verbs, which are themselves a split of `sysupdate update` into
downloading and installing stages.

The existing `sysupdated` `Update()` method is kept for now, for
convenience. It might be dropped in future.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814

man/org.freedesktop.sysupdate1.xml
src/sysupdate/org.freedesktop.sysupdate1.conf
src/sysupdate/sysupdated.c

index a34ca18c5ce3fe9f535177981d36584b7b0ed2cb..08fc729c509712b22bb5b35a3a5bd84ae605acfc 100644 (file)
@@ -125,6 +125,16 @@ node /org/freedesktop/sysupdate1/target/host {
              out s new_version,
              out t job_id,
              out o job_path);
+      Acquire(in  s new_version,
+              in  t flags,
+              out s new_version,
+              out t job_id,
+              out o job_path);
+      Install(in  s new_version,
+              in  t flags,
+              out s new_version,
+              out t job_id,
+              out o job_path);
       Vacuum(out u instances,
              out u disabled_transfers);
       GetAppStream(out as appstream);
@@ -165,6 +175,10 @@ node /org/freedesktop/sysupdate1/target/host {
 
     <variablelist class="dbus-method" generated="True" extra-ref="Update()"/>
 
+    <variablelist class="dbus-method" generated="True" extra-ref="Acquire()"/>
+
+    <variablelist class="dbus-method" generated="True" extra-ref="Install()"/>
+
     <variablelist class="dbus-method" generated="True" extra-ref="Vacuum()"/>
 
     <variablelist class="dbus-method" generated="True" extra-ref="GetAppStream()"/>
@@ -272,6 +286,26 @@ node /org/freedesktop/sysupdate1/target/host {
       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>Acquire()</function> downloads an update for this target, if one is available. If a
+      <varname>new_version</varname> is specified, that is the version that gets downloaded. Otherwise, the
+      latest version is downloaded. Call <function>Install()</function> to install the acquired update.
+      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>. This method pulls both
+      metadata and payload data from the network.</para>
+
+      <para><function>Install()</function> installs an already-acquired update for this target. If a
+      <varname>new_version</varname> is specified, that is the version that gets installed, assuming it has
+      already been acquired. Otherwise, the latest acquired 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>.</para>
+
+      <para>Unlike all the other methods in this interface, <function>Acquire()</function> and
+      <function>Install()</function> do not wait for their jobs to complete. Instead, they return 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. These methods also return the version the target will be updated to,
+      for cases where no version was specified by the caller. 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>
 
@@ -347,7 +381,7 @@ node /org/freedesktop/sysupdate1/target/host {
       preemptive decisions to be made about features that are planned to appear in future releases of the OS.
       The drop-in will have a filename of <literal>50-systemd-sysupdate-enabled.conf</literal>.
       This method only changes configuration files; to actually apply the changes, clients will need to
-      call <function>Update()</function>.
+      call <function>Update()</function> (or <function>Acquire()</function> and <function>Install()</function>).
       Depending on the exact needs of the client, it can choose to update the system to the latest available
       version, or it can extend the newest existing installation in-place (by passing in the version returned
       by <varname>GetVersion()</varname>).
@@ -397,7 +431,8 @@ node /org/freedesktop/sysupdate1/target/host {
       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
+      <para><function>Update()</function>, <function>Acquire()</function> and <function>Install()</function>
+      use 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
@@ -496,14 +531,15 @@ node /org/freedesktop/sysupdate1/job/_1 {
 
       <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>, <literal>vacuum</literal>, or <literal>describe-feature</literal>).</para>
+      <literal>update</literal>, <literal>acquire</literal>, <literal>install</literal>,
+      <literal>vacuum</literal>, or <literal>describe-feature</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>
+      between 0 and 100. It is only available for <literal>update</literal>, <literal>acquire</literal> and
+      <literal>install</literal> jobs; for all other jobs it is always 0.</para>
     </refsect2>
 
     <refsect2>
@@ -563,6 +599,8 @@ node /org/freedesktop/sysupdate1/job/_1 {
       <function>Describe()</function>,
       <function>CheckNew()</function>,
       <function>Update()</function>,
+      <function>Acquire()</function>,
+      <function>Install()</function>,
       <function>Vacuum()</function>,
       <function>GetAppStream()</function>,
       <function>GetVersion()</function>,
index da45c020174382e1ac7377566a75ef25ca9af891..c98acb5736cc9ed12065bb37153d5f710f9c5159 100644 (file)
                        send_interface="org.freedesktop.sysupdate1.Target"
                        send_member="Update"/>
 
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="Acquire"/>
+
+                <allow send_destination="org.freedesktop.sysupdate1"
+                       send_interface="org.freedesktop.sysupdate1.Target"
+                       send_member="Install"/>
+
                 <allow send_destination="org.freedesktop.sysupdate1"
                        send_interface="org.freedesktop.sysupdate1.Target"
                        send_member="Vacuum"/>
index b0b7d4aeff6c4817a9957c328d961c850c178eb6..57a5cb0457434d6338234848d3da3c40e09ede52 100644 (file)
@@ -101,6 +101,8 @@ typedef enum JobType {
         JOB_DESCRIBE,
         JOB_CHECK_NEW,
         JOB_UPDATE,
+        JOB_ACQUIRE,
+        JOB_INSTALL,
         JOB_VACUUM,
         JOB_DESCRIBE_FEATURE,
         _JOB_TYPE_MAX,
@@ -121,7 +123,7 @@ struct Job {
 
         JobType type;
         bool offline;
-        char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */
+        char *version; /* Passed into sysupdate for JOB_DESCRIBE, JOB_UPDATE, JOB_ACQUIRE and JOB_INSTALL */
         char *feature; /* Passed into sysupdate for JOB_DESCRIBE_FEATURE */
 
         unsigned progress_percent;
@@ -154,6 +156,8 @@ static const char* const job_type_table[_JOB_TYPE_MAX] = {
         [JOB_DESCRIBE]         = "describe",
         [JOB_CHECK_NEW]        = "check-new",
         [JOB_UPDATE]           = "update",
+        [JOB_ACQUIRE]          = "acquire",
+        [JOB_INSTALL]          = "install",
         [JOB_VACUUM]           = "vacuum",
         [JOB_DESCRIBE_FEATURE] = "describe-feature",
 };
@@ -222,7 +226,7 @@ static int job_new(JobType type, Target *t, sd_bus_message *msg, JobComplete com
 
 /* Is Job in the set of jobs which require Target.busy to be set so they run exclusively? */
 static bool job_requires_busy(Job *j) {
-        return IN_SET(j->type, JOB_UPDATE, JOB_VACUUM);
+        return IN_SET(j->type, JOB_UPDATE, JOB_ACQUIRE, JOB_INSTALL, JOB_VACUUM);
 }
 
 static int job_parse_child_output(int _fd, sd_json_variant **ret) {
@@ -458,8 +462,8 @@ static int job_start(Job *j) {
                         NULL, /* maybe --verify=no */
                         NULL, /* maybe --component=, --root=, or --image= */
                         NULL, /* maybe --offline */
-                        NULL, /* list, check-new, update, vacuum, features */
-                        NULL, /* maybe version (for list, update), maybe feature (features) */
+                        NULL, /* list, check-new, acquire, update, vacuum, features */
+                        NULL, /* maybe version (for list, acquire, update), maybe feature (features) */
                         NULL
                 };
                 size_t k = 2;
@@ -484,7 +488,7 @@ static int job_start(Job *j) {
                 if (target_arg)
                         cmd[k++] = target_arg;
 
-                if (j->offline)
+                if (j->offline || j->type == JOB_INSTALL)  /* install is implemented as `update --offline` */
                         cmd[k++] = "--offline";
 
                 switch (j->type) {
@@ -507,6 +511,16 @@ static int job_start(Job *j) {
                         cmd[k++] = empty_to_null(j->version);
                         break;
 
+                case JOB_ACQUIRE:
+                        cmd[k++] = "acquire";
+                        cmd[k++] = empty_to_null(j->version);
+                        break;
+
+                case JOB_INSTALL:
+                        cmd[k++] = "update";  /* install is implemented as `update --offline` */
+                        cmd[k++] = empty_to_null(j->version);
+                        break;
+
                 case JOB_VACUUM:
                         cmd[k++] = "vacuum";
                         break;
@@ -587,6 +601,8 @@ static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *
                 break;
 
         case JOB_UPDATE:
+        case JOB_ACQUIRE:
+        case JOB_INSTALL:
                 if (j->version)
                         action = "org.freedesktop.sysupdate1.update-to-version";
                 else
@@ -1152,6 +1168,174 @@ static int target_method_update(sd_bus_message *msg, void *userdata, sd_bus_erro
         return 1;
 }
 
+static int target_method_acquire_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 acquired. */
+        return sd_bus_error_setf(error, BUS_ERROR_NO_UPDATE_CANDIDATE,
+                                 "Job exited successfully with no work to do, assume already acquired");
+}
+
+static int target_method_acquire_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_acquire(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_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
+
+        /* We don’t have a separate polkit action for acquire/install as they are both effectively (part of)
+         * an update anyway. */
+        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_ACQUIRE, t, msg, target_method_acquire_finished_early, &j);
+        if (r < 0)
+                return r;
+        j->detach_cb = target_method_acquire_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_install_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, BUS_ERROR_NO_UPDATE_CANDIDATE,
+                                 "Job exited successfully with no work to do, assume already installed");
+}
+
+static int target_method_install_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_install(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_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
+
+        /* We don’t have a separate polkit action for acquire/install as they are both effectively (part of)
+         * an update anyway. */
+        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_INSTALL, t, msg, target_method_install_finished_early, &j);
+        if (r < 0)
+                return r;
+        j->detach_cb = target_method_install_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,
@@ -1591,6 +1775,18 @@ static const sd_bus_vtable target_vtable[] = {
                                 target_method_update,
                                 SD_BUS_VTABLE_UNPRIVILEGED),
 
+        SD_BUS_METHOD_WITH_ARGS("Acquire",
+                                SD_BUS_ARGS("s", new_version, "t", flags),
+                                SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
+                                target_method_acquire,
+                                SD_BUS_VTABLE_UNPRIVILEGED),
+
+        SD_BUS_METHOD_WITH_ARGS("Install",
+                                SD_BUS_ARGS("s", new_version, "t", flags),
+                                SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
+                                target_method_install,
+                                SD_BUS_VTABLE_UNPRIVILEGED),
+
         SD_BUS_METHOD_WITH_ARGS("Vacuum",
                                 SD_BUS_NO_ARGS,
                                 SD_BUS_RESULT("u", instances, "u", disabled_transfers),