From f0b2ea63f4e6876839a05dd05bc8bc338d8e8abb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 19 Jan 2026 17:16:54 +0000 Subject: [PATCH] sysupdated: Add Acquire() and Install() D-Bus methods 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 Helps: https://github.com/systemd/systemd/issues/34814 --- man/org.freedesktop.sysupdate1.xml | 48 +++- src/sysupdate/org.freedesktop.sysupdate1.conf | 8 + src/sysupdate/sysupdated.c | 206 +++++++++++++++++- 3 files changed, 252 insertions(+), 10 deletions(-) diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml index a34ca18c5ce..08fc729c509 100644 --- a/man/org.freedesktop.sysupdate1.xml +++ b/man/org.freedesktop.sysupdate1.xml @@ -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 { + + + + @@ -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 JobRemoved() signal to detect when the job is complete. + Acquire() downloads an update for this target, if one is available. If a + new_version is specified, that is the version that gets downloaded. Otherwise, the + latest version is downloaded. Call Install() to install the acquired update. + The flags argument is added for future extensibility. No flags are currently + defined, and the argument is required to be set to 0. This method pulls both + metadata and payload data from the network. + + Install() installs an already-acquired update for this target. If a + new_version is specified, that is the version that gets installed, assuming it has + already been acquired. Otherwise, the latest acquired version is installed. The + flags argument is added for future extensibility. No flags are currently defined, + and the argument is required to be set to 0. + + Unlike all the other methods in this interface, Acquire() and + Install() 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 + JobRemoved() signal to detect when the job is complete. + Vacuum() deletes old installed versions of this target to free up space. It returns the number of instances that have been deleted. @@ -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 50-systemd-sysupdate-enabled.conf. This method only changes configuration files; to actually apply the changes, clients will need to - call Update(). + call Update() (or Acquire() and Install()). 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 GetVersion()). @@ -397,7 +431,8 @@ node /org/freedesktop/sysupdate1/target/host { use the polkit action org.freedesktop.sysupdate1.check. By default, this action is permitted without administrator authentication. - Update() uses the polkit action + Update(), Acquire() and Install() + use the polkit action org.freedesktop.sysupdate1.update when no version is specified. By default, this action is permitted without administrator authentication. When a version is specified, org.freedesktop.sysupdate1.update-to-version is @@ -496,14 +531,15 @@ node /org/freedesktop/sysupdate1/job/_1 { The Type property exposes the type of operation (one of: list, describe, check-new, - update, vacuum, or describe-feature). + update, acquire, install, + vacuum, or describe-feature). The Offline property exposes whether the job is permitted to access the network or not. The Progress property exposes the current progress of the job as a value - between 0 and 100. It is only available for update jobs; for all other jobs - it is always 0. + between 0 and 100. It is only available for update, acquire and + install jobs; for all other jobs it is always 0. @@ -563,6 +599,8 @@ node /org/freedesktop/sysupdate1/job/_1 { Describe(), CheckNew(), Update(), + Acquire(), + Install(), Vacuum(), GetAppStream(), GetVersion(), diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf index da45c020174..c98acb5736c 100644 --- a/src/sysupdate/org.freedesktop.sysupdate1.conf +++ b/src/sysupdate/org.freedesktop.sysupdate1.conf @@ -66,6 +66,14 @@ send_interface="org.freedesktop.sysupdate1.Target" send_member="Update"/> + + + + diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c index b0b7d4aeff6..57a5cb04574 100644 --- a/src/sysupdate/sysupdated.c +++ b/src/sysupdate/sysupdated.c @@ -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), -- 2.47.3