sysupdate: Split update into acquire and install verbs (#40236)

Using roughly the approach described in
https://gitlab.gnome.org/GNOME/gnome-software/-/merge_requests/2004#note_2145880.
Basically, copying in-progress downloads to a file/partition with a
predictable prefix, and then moving to a predictable ‘pending’ prefix
when ready to install.

Helps: https://github.com/systemd/systemd/issues/34814
This commit is contained in:
Luca Boccassi
2026-02-10 13:11:52 +00:00
committed by GitHub
11 changed files with 622 additions and 120 deletions
+4 -1
View File
@@ -45,8 +45,11 @@ struct Instance {
InstanceMetadata metadata;
/* Where we found the instance */
char *path;
char *path; /* includes the `.sysupdate.partial.` (etc.) prefix, if applicable */
PartitionInfo partition_info;
bool is_partial;
bool is_pending;
};
void instance_metadata_destroy(InstanceMetadata *m);
+27
View File
@@ -16,6 +16,33 @@ void partition_info_destroy(PartitionInfo *p) {
p->device = mfree(p->device);
}
int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src) {
int r;
assert(dest);
assert(src);
r = free_and_strdup_warn(&dest->label, src->label);
if (r < 0)
return r;
r = free_and_strdup_warn(&dest->device, src->device);
if (r < 0)
return r;
dest->partno = src->partno;
dest->start = src->start;
dest->size = src->size;
dest->flags = src->flags;
dest->type = src->type;
dest->uuid = src->uuid;
dest->no_auto = src->no_auto;
dest->read_only = src->read_only;
dest->growfs = src->growfs;
return 0;
}
int read_partition_info(
struct fdisk_context *c,
struct fdisk_table *t,
+1
View File
@@ -36,6 +36,7 @@ typedef struct PartitionInfo {
}
void partition_info_destroy(PartitionInfo *p);
int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src);
int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret);
+52 -5
View File
@@ -80,15 +80,22 @@ static int resource_load_from_directory_recursive(
Resource *rr,
DIR* d,
const char* relpath,
mode_t m) {
const char* relpath_for_matching,
mode_t m,
bool ancestor_is_partial,
bool ancestor_is_pending) {
int r;
for (;;) {
_cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
_cleanup_free_ char *joined = NULL, *rel_joined = NULL;
_cleanup_free_ char *rel_joined_for_matching = NULL;
Instance *instance;
struct dirent *de;
const char *de_d_name_stripped;
struct stat st;
bool is_partial = ancestor_is_partial, is_pending = ancestor_is_pending;
const char *stripped;
errno = 0;
de = readdir_no_dot(d);
@@ -128,11 +135,26 @@ static int resource_load_from_directory_recursive(
if (!(S_ISDIR(st.st_mode) && S_ISREG(m)) && ((st.st_mode & S_IFMT) != m))
continue;
if ((stripped = startswith(de->d_name, ".sysupdate.partial."))) {
de_d_name_stripped = stripped;
is_partial = true;
} else if ((stripped = startswith(de->d_name, ".sysupdate.pending."))) {
de_d_name_stripped = stripped;
is_pending = true;
} else
de_d_name_stripped = de->d_name;
rel_joined = path_join(relpath, de->d_name);
if (!rel_joined)
return log_oom();
r = pattern_match_many(rr->patterns, rel_joined, &extracted_fields);
/* Match against the filename with any `.sysupdate.partial.` (etc.) prefix stripped, so the
* user’s patterns still apply. But don’t use the stripped version in any paths or recursion. */
rel_joined_for_matching = path_join(relpath_for_matching, de_d_name_stripped);
if (!rel_joined_for_matching)
return log_oom();
r = pattern_match_many(rr->patterns, rel_joined_for_matching, &extracted_fields);
if (r == PATTERN_MATCH_RETRY) {
_cleanup_closedir_ DIR *subdir = NULL;
@@ -140,7 +162,7 @@ static int resource_load_from_directory_recursive(
if (!subdir)
continue;
r = resource_load_from_directory_recursive(rr, subdir, rel_joined, m);
r = resource_load_from_directory_recursive(rr, subdir, rel_joined, rel_joined_for_matching, m, is_partial, is_pending);
if (r < 0)
return r;
if (r == 0)
@@ -168,6 +190,9 @@ static int resource_load_from_directory_recursive(
if (instance->metadata.mode == MODE_INVALID)
instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
return 0;
@@ -192,7 +217,7 @@ static int resource_load_from_directory(
return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
}
return resource_load_from_directory_recursive(rr, d, NULL, m);
return resource_load_from_directory_recursive(rr, d, NULL, NULL, m, false, false);
}
static int resource_load_from_blockdev(Resource *rr) {
@@ -219,6 +244,9 @@ static int resource_load_from_blockdev(Resource *rr) {
_cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
_cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
Instance *instance;
const char *pinfo_label_stripped;
bool is_partial = false, is_pending = false;
const char *stripped;
r = read_partition_info(c, t, i, &pinfo);
if (r < 0)
@@ -236,7 +264,18 @@ static int resource_load_from_blockdev(Resource *rr) {
continue;
}
r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields);
/* Match the label with any partial/pending prefix removed so the user’s existing patterns
* match regardless of the instance’s state. */
if ((stripped = startswith(pinfo.label, "PRT#"))) {
pinfo_label_stripped = stripped;
is_partial = true;
} else if ((stripped = startswith(pinfo.label, "PND#"))) {
pinfo_label_stripped = stripped;
is_pending = true;
} else
pinfo_label_stripped = pinfo.label;
r = pattern_match_many(rr->patterns, pinfo_label_stripped, &extracted_fields);
if (r < 0)
return log_error_errno(r, "Failed to match pattern: %m");
if (IN_SET(r, PATTERN_MATCH_NO, PATTERN_MATCH_RETRY))
@@ -262,6 +301,9 @@ static int resource_load_from_blockdev(Resource *rr) {
if (instance->metadata.read_only < 0)
instance->metadata.read_only = instance->partition_info.read_only;
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
return 0;
@@ -539,6 +581,11 @@ static int resource_load_from_web(
memcpy(instance->metadata.sha256sum, h.iov_base, h.iov_len);
instance->metadata.sha256sum_set = true;
}
/* Web resources can only be a source, not a target, so
* can never be partial or pending. */
instance->is_partial = false;
instance->is_pending = false;
}
}
+295 -78
View File
@@ -41,7 +41,6 @@
#include "sysupdate-resource.h"
#include "sysupdate-transfer.h"
#include "time-util.h"
#include "tmpfile-util.h"
#include "web-util.h"
/* Default value for InstancesMax= for fs object targets */
@@ -51,7 +50,8 @@ Transfer* transfer_free(Transfer *t) {
if (!t)
return NULL;
t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
free(t->temporary_partial_path);
free(t->temporary_pending_path);
free(t->id);
@@ -67,6 +67,9 @@ Transfer* transfer_free(Transfer *t) {
strv_free(t->appstream);
partition_info_destroy(&t->partition_info);
free(t->temporary_partial_partition_label);
free(t->temporary_pending_partition_label);
free(t->final_partition_label);
resource_destroy(&t->source);
resource_destroy(&t->target);
@@ -740,6 +743,49 @@ static void transfer_remove_temporary(Transfer *t) {
}
}
static int transfer_instance_vacuum(
Transfer *t,
Instance *instance) {
int r;
assert(t);
assert(instance);
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
r = rm_rf(instance->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", instance->path);
(void) rmdir_parents(instance->path, t->target.path);
break;
case RESOURCE_PARTITION: {
PartitionInfo pinfo = instance->partition_info;
/* label "_empty" means "no contents" for our purposes */
pinfo.label = (char*) "_empty";
log_debug("Relabelling partition '%s' to '%s'.", pinfo.device, pinfo.label);
r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
return 0;
}
int transfer_vacuum(
Transfer *t,
uint64_t space,
@@ -752,7 +798,45 @@ int transfer_vacuum(
transfer_remove_temporary(t);
/* First, calculate how many instances to keep, based on the instance limit — but keep at least one */
/* First, remove any partial or pending instances (unless protected) */
for (size_t i = 0; i < t->target.n_instances;) {
Instance *instance = t->target.instances[i];
assert(instance);
if (!instance->is_pending && !instance->is_partial) {
i++;
continue;
}
/* If this is listed among the protected versions, then let's not remove it */
if (strv_contains(t->protected_versions, instance->metadata.version) ||
(extra_protected_version && streq(extra_protected_version, instance->metadata.version))) {
log_debug("Version '%s' is pending/partial but protected, not removing.", instance->metadata.version);
i++;
continue;
}
assert(instance->resource);
log_info("%s Removing old %s '%s' (%s).",
glyph(GLYPH_RECYCLING),
instance->is_partial ? "partial" : "pending",
instance->path,
resource_type_to_string(instance->resource->type));
r = transfer_instance_vacuum(t, instance);
if (r < 0)
return 0;
instance_free(instance);
memmove(t->target.instances + i, t->target.instances + i + 1, (t->target.n_instances - i - 1) * sizeof(Instance*));
t->target.n_instances--;
count++;
}
/* Second, calculate how many instances to keep, based on the instance limit — but keep at least one */
instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max;
assert(instances_max >= 1);
@@ -840,36 +924,9 @@ int transfer_vacuum(
oldest->path,
resource_type_to_string(oldest->resource->type));
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path);
(void) rmdir_parents(oldest->path, t->target.path);
break;
case RESOURCE_PARTITION: {
PartitionInfo pinfo = oldest->partition_info;
/* label "_empty" means "no contents" for our purposes */
pinfo.label = (char*) "_empty";
r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
r = transfer_instance_vacuum(t, oldest);
if (r < 0)
return 0;
instance_free(oldest);
memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*));
@@ -1111,8 +1168,100 @@ static int run_callout(
return sd_event_loop(event);
}
/* Build the filenames and paths which is normally done by transfer_acquire_instance(), but for partial
* and pending instances which are about to be installed (in which case, transfer_acquire_instance() is
* skipped). */
static int transfer_compute_temporary_paths(Transfer *t, Instance *i, InstanceMetadata *f) {
_cleanup_free_ char *formatted_pattern = NULL, *formatted_partial_pattern = NULL, *formatted_pending_pattern = NULL;
int r;
assert(t);
assert(i);
assert(!t->final_path);
assert(!t->temporary_partial_path);
assert(!t->temporary_pending_path);
assert(!t->final_partition_label);
assert(!t->temporary_partial_partition_label);
assert(!t->temporary_pending_partition_label);
assert(!strv_isempty(t->target.patterns));
/* Format the target name using the first pattern specified */
compile_pattern_fields(t, i, f);
r = pattern_format(t->target.patterns[0], f, &formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to format target pattern: %m");
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
_cleanup_free_ char *final_dir = NULL, *final_filename = NULL, *partial_filename = NULL, *pending_filename = NULL;
if (!path_is_safe(formatted_pattern))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
t->final_path = path_join(t->target.path, formatted_pattern);
if (!t->final_path)
return log_oom();
/* Build the paths for the partial and pending files, which hold the resource while it’s
* being acquired and after it’s been acquired (but before it’s moved to the final_path
* when it’s installed).
*
* Split the filename off the `final_path`, then add a prefix to it for each of partial and
* pending, then join them back on to the same directory. */
r = path_split_prefix_filename(t->final_path, &final_dir, &final_filename);
if (r < 0)
return log_error_errno(r, "Failed to parse path: %m");
if (!strprepend(&partial_filename, ".sysupdate.partial.", final_filename))
return log_oom();
if (!strprepend(&pending_filename, ".sysupdate.pending.", final_filename))
return log_oom();
t->temporary_partial_path = path_join(final_dir, partial_filename);
if (!t->temporary_partial_path)
return log_oom();
t->temporary_pending_path = path_join(final_dir, pending_filename);
if (!t->temporary_pending_path)
return log_oom();
}
if (t->target.type == RESOURCE_PARTITION) {
r = gpt_partition_label_valid(formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
if (!strprepend(&formatted_partial_pattern, "PRT#", formatted_pattern))
return log_oom();
r = gpt_partition_label_valid(formatted_partial_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_partial_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_partial_pattern);
free_and_replace(t->temporary_partial_partition_label, formatted_partial_pattern);
if (!strprepend(&formatted_pending_pattern, "PND#", formatted_pattern))
return log_oom();
r = gpt_partition_label_valid(formatted_pending_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pending_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pending_pattern);
free_and_replace(t->temporary_pending_partition_label, formatted_pending_pattern);
t->final_partition_label = TAKE_PTR(formatted_pattern);
}
return 0;
}
int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) {
_cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
_cleanup_free_ char *digest = NULL;
char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
const char *where = NULL;
InstanceMetadata f;
@@ -1126,48 +1275,35 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Does this instance already exist in the target? Then we don't need to acquire anything */
existing = resource_find_instance(&t->target, i->metadata.version);
if (existing && (existing->is_partial || existing->is_pending))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already partial or pending in the target.", i->path);
if (existing) {
log_info("No need to acquire '%s', already installed.", i->path);
return 0;
}
assert(!t->final_path);
assert(!t->temporary_path);
assert(!strv_isempty(t->target.patterns));
/* Format the target name using the first pattern specified */
compile_pattern_fields(t, i, &f);
r = pattern_format(t->target.patterns[0], &f, &formatted_pattern);
/* Compute up the temporary paths */
r = transfer_compute_temporary_paths(t, i, &f);
if (r < 0)
return log_error_errno(r, "Failed to format target pattern: %m");
return r;
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
r = mkdir_parents(t->temporary_partial_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
if (!path_is_safe(formatted_pattern))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
t->final_path = path_join(t->target.path, formatted_pattern);
if (!t->final_path)
return log_oom();
r = mkdir_parents(t->temporary_pending_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
r = mkdir_parents(t->final_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
if (r < 0)
return log_error_errno(r, "Failed to generate temporary target path: %m");
where = t->final_path;
}
if (t->target.type == RESOURCE_PARTITION) {
r = gpt_partition_label_valid(formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
r = find_suitable_partition(
t->target.path,
i->metadata.size,
@@ -1180,6 +1316,20 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
xsprintf(max_size, "%" PRIu64, t->partition_info.size);
where = t->partition_info.device;
/* Rename the partition to `PRT#<VERSION>` to indicate that a transfer to it is in progress. */
r = free_and_strdup_warn(&t->partition_info.label, t->temporary_partial_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
r = patch_partition(
t->target.path,
&t->partition_info,
t->partition_change);
if (r < 0)
return r;
}
assert(where);
@@ -1218,7 +1368,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
"--direct", /* just copy/unpack the specified file, don't do anything else */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1259,7 +1409,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1276,7 +1426,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1296,7 +1446,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
"--verify", digest, /* validate by explicit SHA256 sum */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1336,7 +1486,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1348,7 +1498,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
bool need_sync = false;
assert(t->temporary_path);
assert(t->temporary_partial_path);
assert(t->temporary_pending_path);
/* Apply file attributes if set */
if (f.mtime != USEC_INFINITY) {
@@ -1356,8 +1507,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
timespec_store(&ts, f.mtime);
if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path);
if (utimensat(AT_FDCWD, t->temporary_partial_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_partial_path);
need_sync = true;
}
@@ -1366,9 +1517,9 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older
* kernels don't support that however, in that case we fall back to chmod(). Not as
* safe, but shouldn't be a problem, given that we don't create symlinks here. */
if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
(!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0))
return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path);
if (fchmodat(AT_FDCWD, t->temporary_partial_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
(!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_partial_path, f.mode) < 0))
return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_partial_path);
need_sync = true;
}
@@ -1376,20 +1527,34 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Synchronize */
if (arg_sync && need_sync) {
if (t->target.type == RESOURCE_REGULAR_FILE)
r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_partial_path);
else {
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
r = syncfs_path(AT_FDCWD, t->temporary_path);
r = syncfs_path(AT_FDCWD, t->temporary_partial_path);
}
if (r < 0)
return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_partial_path);
}
t->install_read_only = f.read_only;
/* Rename the file from `.sysupdate.partial.<VERSION>` to `.sysupdate.pending.<VERSION>` to indicate it’s ready to install. */
log_debug("Renaming resource instance '%s' to '%s'.", t->temporary_partial_path, t->temporary_pending_path);
r = install_file(AT_FDCWD, t->temporary_partial_path,
AT_FDCWD, t->temporary_pending_path,
INSTALL_REPLACE|
(t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
(t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS));
if (r < 0)
return log_error_errno(r, "Failed to move '%s' into pending place: %m", t->temporary_pending_path);
}
if (t->target.type == RESOURCE_PARTITION) {
free_and_replace(t->partition_info.label, formatted_pattern);
/* Now rename the partition again to `PND#<VERSION>` to indicate that the acquire is complete
* and the partition is ready for install. */
r = free_and_strdup_warn(&t->partition_info.label, t->temporary_pending_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
if (f.partition_uuid_set) {
@@ -1416,6 +1581,14 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
t->partition_info.growfs = f.growfs;
t->partition_change |= PARTITION_GROWFS;
}
log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
r = patch_partition(
t->target.path,
&t->partition_info,
t->partition_change);
if (r < 0)
return r;
}
/* For regular file cases the only step left is to install the file in place, which install_file()
@@ -1426,6 +1599,42 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
return 0;
}
int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i) {
InstanceMetadata f;
Instance *existing;
int r;
assert(t);
assert(i);
log_debug("transfer_process_partial_and_pending_instance %s", i->path);
/* Does this instance already exist in the target but isn’t pending? */
existing = resource_find_instance(&t->target, i->metadata.version);
if (existing && !existing->is_pending)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already in the target but is not pending.", i->path);
/* All we need to do is compute the temporary paths. We don’t need to do any of the other work in
* transfer_acquire_instance(). */
r = transfer_compute_temporary_paths(t, i, &f);
if (r < 0)
return r;
/* This is the analogue of find_suitable_partition(), but since finding the suitable partition has
* already happened in the acquire phase, the target should already have that information and it
* should already have been claimed as `PND#`. */
if (t->target.type == RESOURCE_PARTITION) {
assert(i->resource == &t->target);
assert(i->is_pending);
r = partition_info_copy(&t->partition_info, &i->partition_info);
if (r < 0)
return r;
}
return 0;
}
int transfer_install_instance(
Transfer *t,
Instance *i,
@@ -1436,13 +1645,15 @@ int transfer_install_instance(
assert(t);
assert(i);
assert(i->resource);
assert(t == container_of(i->resource, Transfer, source));
assert(i->is_pending || t == container_of(i->resource, Transfer, source));
if (t->temporary_path) {
log_debug("transfer_install_instance %s %s %s %d", i->path, t->temporary_pending_path, t->final_partition_label, t->partition_change);
if (t->temporary_pending_path) {
assert(RESOURCE_IS_FILESYSTEM(t->target.type));
assert(t->final_path);
r = install_file(AT_FDCWD, t->temporary_path,
r = install_file(AT_FDCWD, t->temporary_pending_path,
AT_FDCWD, t->final_path,
INSTALL_REPLACE|
(t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
@@ -1456,11 +1667,17 @@ int transfer_install_instance(
t->final_path,
resource_type_to_string(t->target.type));
t->temporary_path = mfree(t->temporary_path);
t->temporary_pending_path = mfree(t->temporary_pending_path);
}
if (t->partition_change != 0) {
if (t->temporary_pending_partition_label) {
assert(t->target.type == RESOURCE_PARTITION);
assert(t->final_partition_label);
r = free_and_strdup_warn(&t->partition_info.label, t->final_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
r = patch_partition(
t->target.path,
+6 -1
View File
@@ -39,13 +39,17 @@ typedef struct Transfer {
int growfs;
/* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */
char *temporary_path;
char *temporary_partial_path;
char *temporary_pending_path;
char *final_path;
int install_read_only;
/* If we write to a partition in a partition table, the metrics of it */
PartitionInfo partition_info;
PartitionChange partition_change;
char *final_partition_label;
char *temporary_partial_partition_label;
char *temporary_pending_partition_label;
Context *context;
} Transfer;
@@ -63,5 +67,6 @@ int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version);
int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata);
int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i);
int transfer_install_instance(Transfer *t, Instance *i, const char *root);
@@ -9,6 +9,9 @@ const char* update_set_flags_to_color(UpdateSetFlags flags) {
if (flags == 0 || (flags & UPDATE_OBSOLETE))
return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey();
if (flags & (UPDATE_PARTIAL|UPDATE_PENDING))
return ansi_highlight_cyan();
if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_INCOMPLETE))
return ansi_highlight_yellow();
@@ -29,6 +32,9 @@ const char* update_set_flags_to_glyph(UpdateSetFlags flags) {
if (flags == 0 || (flags & UPDATE_OBSOLETE))
return glyph(GLYPH_MULTIPLICATION_SIGN);
if (flags & (UPDATE_PARTIAL|UPDATE_PENDING))
return glyph(GLYPH_DOWNLOAD);
if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
return glyph(GLYPH_BLACK_CIRCLE);
@@ -54,6 +60,18 @@ const char* update_set_flags_to_string(UpdateSetFlags flags) {
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current";
case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current+pending";
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current+partial";
case UPDATE_AVAILABLE|UPDATE_NEWEST:
case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
return "candidate";
@@ -10,6 +10,8 @@ typedef enum UpdateSetFlags {
UPDATE_OBSOLETE = 1 << 3,
UPDATE_PROTECTED = 1 << 4,
UPDATE_INCOMPLETE = 1 << 5,
UPDATE_PARTIAL = 1 << 6,
UPDATE_PENDING = 1 << 7,
} UpdateSetFlags;
const char* update_set_flags_to_color(UpdateSetFlags flags);
+168 -22
View File
@@ -397,7 +397,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
assert(flags == UPDATE_INSTALLED);
match = resource_find_instance(&t->target, cursor);
if (!match) {
if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING))) {
/* When we're looking for installed versions, let's be robust and treat
* an incomplete installation as an installation. Otherwise, there are
* situations that can lead to sysupdate wiping the currently booted OS.
@@ -413,6 +413,14 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
if (strv_contains(t->protected_versions, cursor))
extra_flags |= UPDATE_PROTECTED;
/* Partial or pending updates by definition are not incomplete, they’re
* partial/pending instead */
if (match && match->is_partial)
extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
if (match && match->is_pending)
extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
}
r = free_and_strdup_warn(&boundary, cursor);
@@ -431,7 +439,9 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
/* Merge in what we've learned and continue onto the next version */
if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE)) {
if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
FLAGS_SET(u->flags, UPDATE_PENDING)) {
assert(u->n_instances == c->n_transfers);
/* Incomplete updates will have picked NULL instances for the transfers that
@@ -450,7 +460,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags 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. */
* the candidate. Ignore partial or pending status on the update set. */
if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) &&
!c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE))
c->candidate = u;
@@ -486,7 +496,8 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
c->newest_installed = us;
/* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */
/* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
* It may be partial or pending. */
if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
c->candidate = us;
}
@@ -496,6 +507,11 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
c->candidate = NULL;
/* Newest installed is still pending and no candidate is set? Then it becomes the candidate. */
if (c->newest_installed && FLAGS_SET(c->newest_installed->flags, UPDATE_PENDING) &&
!c->candidate)
c->candidate = c->newest_installed;
return 0;
}
@@ -595,13 +611,14 @@ static int context_show_version(Context *c, const char *version) {
if (!sd_json_format_enabled(arg_json_format_flags))
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %s%s\n"
"Installed: %s%s%s%s\n"
"Available: %s%s\n"
"Protected: %s%s%s\n"
" Obsolete: %s%s%s\n\n",
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
@@ -794,7 +811,7 @@ static int context_show_version(Context *c, const char *version) {
if (!sd_json_format_enabled(arg_json_format_flags)) {
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %s%s%s%s%s\n"
"Installed: %s%s%s%s%s%s%s\n"
"Available: %s%s\n"
"Protected: %s%s%s\n"
" Obsolete: %s%s%s\n",
@@ -802,6 +819,7 @@ static int context_show_version(Context *c, const char *version) {
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
@@ -827,6 +845,8 @@ static int context_show_version(Context *c, const char *version) {
SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)),
SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)),
SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)),
SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
SD_JSON_BUILD_PAIR_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)),
@@ -982,10 +1002,11 @@ static int context_on_acquire_progress(const Transfer *t, const Instance *inst,
overall, n - i, i, inst->metadata.version, overall);
}
static int context_apply(
static int context_process_partial_and_pending(Context *c, const char *version);
static int context_acquire(
Context *c,
const char *version,
UpdateSet **ret_applied) {
const char *version) {
UpdateSet *us = NULL;
int r;
@@ -1000,9 +1021,6 @@ static int context_apply(
if (!c->candidate) {
log_info("No update needed.");
if (ret_applied)
*ret_applied = NULL;
return 0;
}
@@ -1011,11 +1029,16 @@ static int context_apply(
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);
else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
log_info("Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
if (ret_applied)
*ret_applied = NULL;
return 0;
} else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
log_info("Selected update '%s' is already acquired and pending installation.", us->version);
return context_process_partial_and_pending(c, version);
} else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
log_info("Selected update '%s' is already installed. Skipping update.", us->version);
return 0;
}
@@ -1075,14 +1098,106 @@ static int context_apply(
if (arg_sync)
sync();
(void) sd_notifyf(/* unset_environment= */ false,
return 1;
}
/* Check to see if we have an update set acquired and pending installation. */
static int context_process_partial_and_pending(
Context *c,
const char *version) {
UpdateSet *us = NULL;
int r;
assert(c);
if (version) {
us = context_update_set_by_version(c, version);
if (!us)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
} else {
if (!c->candidate) {
log_info("No update needed.");
return 0;
}
us = c->candidate;
}
if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
log_info("Selected update '%s' is already installed. Skipping update.", us->version);
return 0;
}
if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
if (!FLAGS_SET(us->flags, UPDATE_PENDING))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, 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);
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)
log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
log_info("Selected update '%s' for install.", us->version);
/* There should now be one instance picked for each transfer, and the order is the same */
assert(us->n_instances == c->n_transfers);
for (size_t i = 0; i < c->n_transfers; i++) {
Instance *inst = us->instances[i];
Transfer *t = c->transfers[i];
assert(inst);
r = transfer_process_partial_and_pending_instance(t, inst);
if (r < 0)
return r;
}
return 1;
}
static int context_install(
Context *c,
const char *version,
UpdateSet **ret_applied) {
UpdateSet *us = NULL;
int r;
assert(c);
if (version) {
us = context_update_set_by_version(c, version);
if (!us)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
} else {
if (!c->candidate) {
log_info("No update needed.");
return 0;
}
us = c->candidate;
}
(void) sd_notifyf(/* unset_environment=*/ false,
"STATUS=Installing '%s'.", us->version);
for (size_t i = 0; i < c->n_transfers; i++) {
Instance *inst = us->instances[i];
Transfer *t = c->transfers[i];
if (inst->resource == &t->target)
if (inst->resource == &t->target &&
!inst->is_pending)
continue;
r = transfer_install_instance(t, inst, arg_root);
@@ -1173,13 +1288,16 @@ static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **versions = NULL;
const char *current = NULL;
bool current_is_pending = false;
FOREACH_ARRAY(update_set, context->update_sets, context->n_update_sets) {
UpdateSet *us = *update_set;
if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
FLAGS_SET(us->flags, UPDATE_NEWEST))
FLAGS_SET(us->flags, UPDATE_NEWEST)) {
current = us->version;
current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
}
r = strv_extend(&versions, us->version);
if (r < 0)
@@ -1197,7 +1315,7 @@ static int verb_list(int argc, char **argv, void *userdata) {
return log_oom();
}
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("current", current),
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
SD_JSON_BUILD_PAIR_STRV("all", versions),
SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
if (r < 0)
@@ -1409,7 +1527,12 @@ static int verb_vacuum(int argc, char **argv, void *userdata) {
return context_vacuum(context, 0, NULL);
}
static int verb_update(int argc, char **argv, void *userdata) {
typedef enum {
UPDATE_ACTION_ACQUIRE = 1 << 0,
UPDATE_ACTION_INSTALL = 1 << 1,
} UpdateActionFlags;
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
@@ -1443,7 +1566,15 @@ static int verb_update(int argc, char **argv, void *userdata) {
if (r < 0)
return r;
r = context_apply(context, version, &applied);
if (action_flags & UPDATE_ACTION_ACQUIRE)
r = context_acquire(context, version);
else
r = context_process_partial_and_pending(context, version);
if (r < 0)
return r; /* error */
if (action_flags & UPDATE_ACTION_INSTALL && r > 0) /* update needed */
r = context_install(context, version, &applied);
if (r < 0)
return r;
@@ -1468,6 +1599,19 @@ static int verb_update(int argc, char **argv, void *userdata) {
return 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
if (!arg_offline)
flags |= UPDATE_ACTION_ACQUIRE;
return verb_update_impl(argc, argv, flags);
}
static int verb_acquire(int argc, char **argv, void *userdata) {
return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
}
static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
_cleanup_(context_freep) Context* context = NULL;
_cleanup_free_ char *booted_version = NULL;
@@ -1647,6 +1791,7 @@ static int verb_help(int argc, char **argv, void *userdata) {
" features [FEATURE] Show optional features\n"
" check-new Check if there's a new version available\n"
" update [VERSION] Install new version now\n"
" acquire [VERSION] Acquire (download) new version now\n"
" vacuum Make room, by deleting old versions\n"
" pending Report whether a newer version is installed than\n"
" currently booted\n"
@@ -1860,6 +2005,7 @@ static int sysupdate_main(int argc, char *argv[]) {
{ "features", VERB_ANY, 2, 0, verb_features },
{ "check-new", VERB_ANY, 1, 0, verb_check_new },
{ "update", VERB_ANY, 2, 0, verb_update },
{ "acquire", VERB_ANY, 2, 0, verb_acquire },
{ "vacuum", VERB_ANY, 1, 0, verb_vacuum },
{ "reboot", 1, 1, 0, verb_pending_or_reboot },
{ "pending", 1, 1, 0, verb_pending_or_reboot },