From 631803cccd8112b420ad74195d78b0f1ae029ac7 Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Wed, 19 Jun 2024 21:05:04 -0400 Subject: [PATCH] sysupdate: Repair incomplete versions in-place A previous commit made sysupdate recognize installed versions where some transfers are missing. This commit teaches sysupdate how to correctly repair these incomplete versions. Previously, if you had a incomplete installation of the OS booted, and ran sysupdate in an attempt to repair it, sysupdate would make things worse by creating copies of the currently-booted partitions in the inactive slots. Then at boot you have two identical partitions, with identical labels an UUIDs, and end up with a mess. With this commit, sysupdate is able to recognize situations where it can simply download the missing transfers and leave the rest of the system undistrubed. Partial fix for https://github.com/systemd/systemd/issues/33339 --- man/org.freedesktop.sysupdate1.xml | 3 +- src/sysupdate/sysupdate-transfer.c | 3 +- src/sysupdate/sysupdate.c | 68 ++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml index 3acae3d701c..79f718f93c4 100644 --- a/man/org.freedesktop.sysupdate1.xml +++ b/man/org.freedesktop.sysupdate1.xml @@ -226,7 +226,8 @@ node /org/freedesktop/sysupdate1/target/host { incomplete A boolean indicating whether this version is incomplete, which means that it is missing some file. Note that only installed incomplete versions will be offered by the service; - versions that are incomplete on the server-side are completely ignored. + versions that are incomplete on the server-side are completely ignored. Incomplete versions can + be repaired in-place by calling Update() on that version. diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c index 69bba00cabb..f7d9a043fc6 100644 --- a/src/sysupdate/sysupdate-transfer.c +++ b/src/sysupdate/sysupdate-transfer.c @@ -1075,8 +1075,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi assert(t); assert(i); - assert(i->resource); - assert(t == container_of(i->resource, Transfer, source)); + assert(i->resource == &t->source); assert(cb); /* Does this instance already exist in the target? Then we don't need to acquire anything */ diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 9b56bf1a8f2..04fd003299e 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -325,11 +325,36 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags /* See if we already have this update set in our table */ FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) { UpdateSet *u = *update_set; + if (strverscmp_improved(u->version, cursor) != 0) continue; - /* We only store the instances we found first, but we remember we also found it again */ + /* Merge in what we've learned and continue onto the next version */ + + if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE)) { + assert(u->n_instances == c->n_transfers); + + /* Incomplete updates will have picked NULL instances for the transfers that + * are missing. Now we have more information, so let's try to fill them in. */ + + for (size_t j = 0; j < u->n_instances; j++) { + if (!u->instances[j]) + u->instances[j] = cursor_instances[j]; + + /* Make sure that the list is full if the update is AVAILABLE */ + assert(flags != UPDATE_AVAILABLE || u->instances[j]); + } + } + u->flags |= flags | extra_flags; + + /* If this is the newest installed version, that is incomplete and just became marked + * as available, and if there is no other candidate available, we promote this to be + * the candidate. */ + if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) && + !c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE)) + c->candidate = u; + skip = true; newest_found = true; break; @@ -367,7 +392,8 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags } /* Newest installed is newer than or equal to candidate? Then suppress the candidate */ - if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0) + if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) && + c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0) c->candidate = NULL; return 0; @@ -736,6 +762,10 @@ static int context_vacuum( FOREACH_ARRAY(tr, c->transfers, c->n_transfers) { Transfer *t = *tr; + /* Don't bother clearing out space if we're not going to be downloading anything */ + if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version)) + continue; + r = transfer_vacuum(t, space, extra_protected_version); if (r < 0) return r; @@ -866,7 +896,9 @@ static int context_apply( us = c->candidate; } - if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { + if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE)) + log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version); + else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { log_info("Selected update '%s' is already installed. Skipping update.", us->version); if (ret_applied) @@ -874,13 +906,12 @@ static int context_apply( return 0; } + if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version); if (FLAGS_SET(us->flags, UPDATE_OBSOLETE)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version); - assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE); - if (!FLAGS_SET(us->flags, UPDATE_NEWEST)) log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version); if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0) @@ -913,8 +944,17 @@ static int context_apply( assert(us->n_instances == c->n_transfers); for (size_t i = 0; i < c->n_transfers; i++) { - r = transfer_acquire_instance(c->transfers[i], us->instances[i], - context_on_acquire_progress, c); + Instance *inst = us->instances[i]; + Transfer *t = c->transfers[i]; + + assert(inst); /* ditto */ + + if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */ + assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE)); + continue; + } + + r = transfer_acquire_instance(t, inst, context_on_acquire_progress, c); if (r < 0) return r; } @@ -926,7 +966,13 @@ static int context_apply( "STATUS=Installing '%s'.", us->version); for (size_t i = 0; i < c->n_transfers; i++) { - r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root); + Instance *inst = us->instances[i]; + Transfer *t = c->transfers[i]; + + if (inst->resource == &t->target) + continue; + + r = transfer_install_instance(t, inst, arg_root); if (r < 0) return r; } @@ -1160,6 +1206,12 @@ static int verb_update(int argc, char **argv, void *userdata) { return reboot_now(); } + if (strverscmp_improved(applied->version, booted_version) == 0 && + FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) { + log_notice("Currently booted version was incomplete and has been repaired, rebooting."); + return reboot_now(); + } + log_info("Booted version is newer or identical to newly installed version, not rebooting."); }