• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 25705508282

11 May 2026 11:07PM UTC coverage: 72.65% (+0.1%) from 72.511%
25705508282

push

github

bluca
firstboot,sysinstall,hostnamed: always show FANCY_NAME=

This makes sure that whenever we want to show the OS name we can show
the fancy name. Thus this moves the escaping/validation of the fancy
name out of hostnamed into generic code, and then makes use of it in
sysinstall,firstboot,prompt-util.

17 of 36 new or added lines in 6 files covered. (47.22%)

2673 existing lines in 72 files now uncovered.

327104 of 450245 relevant lines covered (72.65%)

1200575.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.19
/src/sysupdate/sysupdate.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <unistd.h>
4

5
#include "sd-daemon.h"
6

7
#include "build.h"
8
#include "conf-files.h"
9
#include "constants.h"
10
#include "dissect-image.h"
11
#include "format-table.h"
12
#include "glyph-util.h"
13
#include "hexdecoct.h"
14
#include "image-policy.h"
15
#include "loop-util.h"
16
#include "main-func.h"
17
#include "mount-util.h"
18
#include "options.h"
19
#include "os-util.h"
20
#include "pager.h"
21
#include "parse-argument.h"
22
#include "parse-util.h"
23
#include "path-util.h"
24
#include "pretty-print.h"
25
#include "set.h"
26
#include "sort-util.h"
27
#include "specifier.h"
28
#include "string-util.h"
29
#include "strv.h"
30
#include "sysupdate.h"
31
#include "sysupdate-feature.h"
32
#include "sysupdate-instance.h"
33
#include "sysupdate-transfer.h"
34
#include "sysupdate-update-set.h"
35
#include "sysupdate-util.h"
36
#include "utf8.h"
37
#include "verbs.h"
38

39
static char *arg_definitions = NULL;
40
bool arg_sync = true;
41
uint64_t arg_instances_max = UINT64_MAX;
42
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
43
static PagerFlags arg_pager_flags = 0;
44
static bool arg_legend = true;
45
char *arg_root = NULL;
46
static char *arg_image = NULL;
47
static bool arg_reboot = false;
48
static char *arg_component = NULL;
49
static int arg_verify = -1;
50
static ImagePolicy *arg_image_policy = NULL;
51
static bool arg_offline = false;
52
char *arg_transfer_source = NULL;
53

54
STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
660✔
55
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
660✔
56
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
660✔
57
STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
660✔
58
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
660✔
59
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
660✔
60

61
const Specifier specifier_table[] = {
62
        COMMON_SYSTEM_SPECIFIERS,
63
        COMMON_TMP_SPECIFIERS,
64
        {}
65
};
66

67
typedef struct Context {
68
        Transfer **transfers;
69
        size_t n_transfers;
70

71
        Transfer **disabled_transfers;
72
        size_t n_disabled_transfers;
73

74
        Hashmap *features; /* Defined features, keyed by ID */
75

76
        UpdateSet **update_sets;
77
        size_t n_update_sets;
78

79
        UpdateSet *newest_installed, *candidate;
80

81
        Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
82
} Context;
83

84
static Context* context_free(Context *c) {
574✔
85
        if (!c)
574✔
86
                return NULL;
87

88
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers)
3,460✔
89
                transfer_free(*tr);
2,886✔
90
        free(c->transfers);
574✔
91

92
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
1,092✔
93
                transfer_free(*tr);
518✔
94
        free(c->disabled_transfers);
574✔
95

96
        hashmap_free(c->features);
574✔
97

98
        FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
2,900✔
99
                update_set_free(*us);
2,326✔
100
        free(c->update_sets);
574✔
101

102
        hashmap_free(c->web_cache);
574✔
103

104
        return mfree(c);
574✔
105
}
106

107
DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free);
1,690✔
108

109
static Context* context_new(void) {
574✔
110
        /* For now, no fields to initialize non-zero */
111
        return new0(Context, 1);
574✔
112
}
113

114
static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free);
×
115

116
static int read_definitions(
574✔
117
                Context *c,
118
                const char **dirs,
119
                const char *suffix,
120
                const char *node) {
121

122
        ConfFile **files = NULL;
574✔
123
        Transfer **transfers = NULL, **disabled = NULL;
574✔
124
        size_t n_files = 0, n_transfers = 0, n_disabled = 0;
574✔
125
        int r;
574✔
126

127
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
574✔
128
        CLEANUP_ARRAY(transfers, n_transfers, transfer_free_array);
574✔
129
        CLEANUP_ARRAY(disabled, n_disabled, transfer_free_array);
574✔
130

131
        assert(c);
574✔
132
        assert(dirs);
574✔
133
        assert(suffix);
574✔
134

135
        r = conf_files_list_strv_full(suffix, arg_root,
574✔
136
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
137
                                      dirs, &files, &n_files);
138
        if (r < 0)
574✔
139
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
×
140

141
        FOREACH_ARRAY(i, files, n_files) {
3,978✔
142
                _cleanup_(transfer_freep) Transfer *t = NULL;
×
143
                Transfer **appended;
3,404✔
144
                ConfFile *e = *i;
3,404✔
145

146
                t = transfer_new(c);
3,404✔
147
                if (!t)
3,404✔
148
                        return log_oom();
×
149

150
                r = transfer_read_definition(t, e->result, dirs, c->features);
3,404✔
151
                if (r < 0)
3,404✔
152
                        return r;
153

154
                r = transfer_resolve_paths(t, arg_root, node);
3,404✔
155
                if (r < 0)
3,404✔
156
                        return r;
157

158
                if (t->enabled)
3,404✔
159
                        appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
2,886✔
160
                else
161
                        appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
518✔
162
                if (!appended)
3,404✔
163
                        return log_oom();
×
164
                TAKE_PTR(t);
3,404✔
165
        }
166

167
        c->transfers = TAKE_PTR(transfers);
574✔
168
        c->n_transfers = n_transfers;
574✔
169
        c->disabled_transfers = TAKE_PTR(disabled);
574✔
170
        c->n_disabled_transfers = n_disabled;
574✔
171
        return 0;
574✔
172
}
173

174
static int context_read_definitions(Context *c, const char* node, bool requires_enabled_transfers) {
574✔
175
        _cleanup_strv_free_ char **dirs = NULL;
574✔
176
        int r;
574✔
177

178
        assert(c);
574✔
179

180
        if (arg_definitions)
574✔
181
                dirs = strv_new(arg_definitions);
×
182
        else if (arg_component) {
574✔
183
                char **l = CONF_PATHS_STRV("");
8✔
184
                size_t i = 0;
8✔
185

186
                dirs = new0(char*, strv_length(l) + 1);
8✔
187
                if (!dirs)
8✔
188
                        return log_oom();
×
189

190
                STRV_FOREACH(dir, l) {
40✔
191
                        char *j;
32✔
192

193
                        j = strjoin(*dir, "sysupdate.", arg_component, ".d");
32✔
194
                        if (!j)
32✔
195
                                return log_oom();
×
196

197
                        dirs[i++] = j;
32✔
198
                }
199
        } else
200
                dirs = strv_new(CONF_PATHS("sysupdate.d"));
566✔
201
        if (!dirs)
574✔
202
                return log_oom();
×
203

204
        ConfFile **files = NULL;
574✔
205
        size_t n_files = 0;
574✔
206

207
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
574✔
208

209
        r = conf_files_list_strv_full(".feature", arg_root,
574✔
210
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
211
                                      (const char**) dirs, &files, &n_files);
212
        if (r < 0)
574✔
213
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
×
214

215
        FOREACH_ARRAY(i, files, n_files) {
1,140✔
216
                _cleanup_(feature_unrefp) Feature *f = NULL;
×
217
                ConfFile *e = *i;
566✔
218

219
                f = feature_new();
566✔
220
                if (!f)
566✔
221
                        return log_oom();
×
222

223
                r = feature_read_definition(f, e->result, (const char**) dirs);
566✔
224
                if (r < 0)
566✔
225
                        return r;
226

227
                r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
566✔
228
                if (r < 0)
566✔
229
                        return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
×
230
                TAKE_PTR(f);
566✔
231
        }
232

233
        r = read_definitions(c, (const char**) dirs, ".transfer", node);
574✔
234
        if (r < 0)
574✔
235
                return r;
236

237
        if (c->n_transfers + c->n_disabled_transfers == 0) {
574✔
238
                /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
239
                r = read_definitions(c, (const char**) dirs, ".conf", node);
×
240
                if (r < 0)
×
241
                        return r;
242

243
                if (c->n_transfers + c->n_disabled_transfers > 0)
×
244
                        log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
×
245
        }
246

247
        if (c->n_transfers + (requires_enabled_transfers ? 0 : c->n_disabled_transfers) == 0) {
574✔
248
                if (arg_component)
×
249
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
250
                                               "No transfer definitions for component '%s' found.",
251
                                               arg_component);
252

253
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
254
                                       "No transfer definitions found.");
255
        }
256

257
        return 0;
258
}
259

260
static int context_load_installed_instances(Context *c) {
574✔
261
        int r;
574✔
262

263
        assert(c);
574✔
264

265
        log_info("Discovering installed instances%s", glyph(GLYPH_ELLIPSIS));
1,148✔
266

267
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
3,460✔
268
                Transfer *t = *tr;
2,886✔
269

270
                r = resource_load_instances(
2,886✔
271
                                &t->target,
272
                                arg_verify >= 0 ? arg_verify : t->verify,
2,886✔
273
                                &c->web_cache);
274
                if (r < 0)
2,886✔
275
                        return r;
276
        }
277

278
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
1,092✔
279
                Transfer *t = *tr;
518✔
280

281
                r = resource_load_instances(
518✔
282
                                &t->target,
283
                                arg_verify >= 0 ? arg_verify : t->verify,
518✔
284
                                &c->web_cache);
285
                if (r < 0)
518✔
286
                        return r;
287
        }
288

289
        return 0;
290
}
291

292
static int context_load_available_instances(Context *c) {
408✔
293
        int r;
408✔
294

295
        assert(c);
408✔
296

297
        log_info("Discovering available instances%s", glyph(GLYPH_ELLIPSIS));
816✔
298

299
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
2,410✔
300
                Transfer *t = *tr;
2,010✔
301

302
                r = resource_load_instances(
2,010✔
303
                                &t->source,
304
                                arg_verify >= 0 ? arg_verify : t->verify,
2,010✔
305
                                &c->web_cache);
306
                if (r < 0)
2,010✔
307
                        return r;
308
        }
309

310
        return 0;
311
}
312

313
static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
934✔
314
        _cleanup_free_ char *boundary = NULL;
934✔
315
        bool newest_found = false;
934✔
316
        int r;
934✔
317

318
        assert(c);
934✔
319
        assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
934✔
320

321
        for (;;) {
4,616✔
322
                _cleanup_free_ Instance **cursor_instances = NULL;
×
323
                bool skip = false;
4,616✔
324
                UpdateSetFlags extra_flags = 0;
4,616✔
325
                _cleanup_free_ char *cursor = NULL;
3,682✔
326
                UpdateSet *us = NULL;
4,616✔
327

328
                /* First, let's find the newest version that's older than the boundary. */
329
                FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
25,730✔
330
                        Resource *rr;
21,514✔
331

332
                        assert(*tr);
21,514✔
333

334
                        if (flags == UPDATE_AVAILABLE)
21,514✔
335
                                rr = &(*tr)->source;
11,658✔
336
                        else {
337
                                assert(flags == UPDATE_INSTALLED);
9,856✔
338
                                rr = &(*tr)->target;
9,856✔
339
                        }
340

341
                        FOREACH_ARRAY(inst, rr->instances, rr->n_instances) {
65,738✔
342
                                Instance *i = *inst; /* Sorted newest-to-oldest */
60,754✔
343

344
                                assert(i);
60,754✔
345

346
                                if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
60,754✔
347
                                        continue; /* Not older than the boundary */
44,224✔
348

349
                                if (cursor && strverscmp(i->metadata.version, cursor) <= 0)
16,530✔
350
                                        break; /* Not newer than the cursor. The same will be true for all
351
                                                * subsequent instances (due to sorting) so let's skip to the
352
                                                * next transfer. */
353

354
                                if (free_and_strdup(&cursor, i->metadata.version) < 0)
3,682✔
355
                                        return log_oom();
×
356

357
                                break; /* All subsequent instances will be older than this one */
358
                        }
359

360
                        if (flags == UPDATE_AVAILABLE && !cursor)
21,514✔
361
                                break; /* This transfer didn't have a version older than the boundary,
362
                                        * so any older-than-boundary version that might exist in a different
363
                                        * transfer must always be incomplete. For reasons described below,
364
                                        * we don't include incomplete versions for AVAILABLE updates. So we
365
                                        * are completely done looking. */
366
                }
367

368
                if (!cursor) /* We didn't find anything older than the boundary, so we're done. */
4,616✔
369
                        break;
370

371
                cursor_instances = new0(Instance*, c->n_transfers);
3,682✔
372
                if (!cursor_instances)
3,682✔
373
                        return log_oom();
×
374

375
                /* Now let's find all the instances that match the version of the cursor, if we have them */
376
                for (size_t k = 0; k < c->n_transfers; k++) {
21,770✔
377
                        Transfer *t = c->transfers[k];
18,402✔
378
                        Instance *match = NULL;
18,402✔
379

380
                        assert(t);
18,402✔
381

382
                        if (flags == UPDATE_AVAILABLE) {
18,402✔
383
                                match = resource_find_instance(&t->source, cursor);
11,232✔
384
                                if (!match) {
11,232✔
385
                                        /* When we're looking for updates to download, we don't offer
386
                                         * incomplete versions at all. The server wants to send us an update
387
                                         * with parts of the OS missing. For robustness sake, let's not do
388
                                         * that. */
389
                                        skip = true;
390
                                        break;
391
                                }
392
                        } else {
393
                                assert(flags == UPDATE_INSTALLED);
7,170✔
394

395
                                match = resource_find_instance(&t->target, cursor);
7,170✔
396
                                if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING)))
7,170✔
397
                                        /* When we're looking for installed versions, let's be robust and treat
398
                                         * an incomplete installation as an installation. Otherwise, there are
399
                                         * situations that can lead to sysupdate wiping the currently booted OS.
400
                                         * See https://github.com/systemd/systemd/issues/33339 */
401
                                        extra_flags |= UPDATE_INCOMPLETE;
1,922✔
402
                        }
403

404
                        cursor_instances[k] = match;
18,088✔
405

406
                        if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
18,088✔
407
                                extra_flags |= UPDATE_OBSOLETE;
×
408

409
                        if (strv_contains(t->protected_versions, cursor))
18,088✔
410
                                extra_flags |= UPDATE_PROTECTED;
×
411

412
                        /* Partial or pending updates by definition are not incomplete, they’re
413
                         * partial/pending instead. While an individual Instance cannot be both partial and
414
                         * pending, an UpdateSet as a whole can contain both partial and pending instances. */
415
                        assert(!match || !(match->is_partial && match->is_pending));
18,088✔
416

417
                        if (match && match->is_partial)
16,106✔
418
                                extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
20✔
419

420
                        if (match && match->is_pending)
16,106✔
421
                                extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
276✔
422
                }
423

424
                r = free_and_strdup_warn(&boundary, cursor);
3,682✔
425
                if (r < 0)
3,682✔
426
                        return r;
427

428
                if (skip)
3,682✔
429
                        continue;
314✔
430

431
                /* See if we already have this update set in our table */
432
                FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
9,240✔
433
                        UpdateSet *u = *update_set;
6,914✔
434

435
                        if (strverscmp_improved(u->version, cursor) != 0)
6,914✔
436
                                continue;
5,872✔
437

438
                        /* Merge in what we've learned and continue onto the next version */
439

440
                        if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
1,042✔
441
                            FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
660✔
442
                            FLAGS_SET(u->flags, UPDATE_PENDING)) {
652✔
443
                                assert(u->n_instances == c->n_transfers);
406✔
444

445
                                /* Incomplete updates will have picked NULL instances for the transfers that
446
                                 * are missing. Now we have more information, so let's try to fill them in. */
447

448
                                for (size_t j = 0; j < u->n_instances; j++) {
2,506✔
449
                                        if (!u->instances[j])
2,100✔
450
                                                u->instances[j] = cursor_instances[j];
1,404✔
451

452
                                        /* Make sure that the list is full if the update is AVAILABLE */
453
                                        assert(flags != UPDATE_AVAILABLE || u->instances[j]);
2,100✔
454
                                }
455
                        }
456

457
                        u->flags |= flags | extra_flags;
1,042✔
458

459
                        /* If this is the newest installed version, that is incomplete and just became marked
460
                         * as available, and if there is no other candidate available, we promote this to be
461
                         * the candidate. Ignore partial or pending status on the update set. */
462
                        if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) &&
1,042✔
463
                            !c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE))
32✔
464
                                c->candidate = u;
32✔
465

466
                        skip = true;
467
                        newest_found = true;
468
                        break;
469
                }
470

471
                if (skip)
32✔
472
                        continue;
1,042✔
473

474
                /* Doesn't exist yet, let's add it */
475
                if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
2,326✔
UNCOV
476
                        return log_oom();
×
477

478
                us = new(UpdateSet, 1);
2,326✔
479
                if (!us)
2,326✔
UNCOV
480
                        return log_oom();
×
481

482
                *us = (UpdateSet) {
2,326✔
483
                        .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
2,326✔
484
                        .version = TAKE_PTR(cursor),
2,326✔
485
                        .instances = TAKE_PTR(cursor_instances),
2,326✔
486
                        .n_instances = c->n_transfers,
2,326✔
487
                };
488

489
                c->update_sets[c->n_update_sets++] = us;
2,326✔
490

491
                newest_found = true;
2,326✔
492

493
                /* Remember which one is the newest installed */
494
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
2,326✔
495
                        c->newest_installed = us;
516✔
496

497
                /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
498
                 * It may be partial or pending. */
499
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
2,326✔
500
                        c->candidate = us;
152✔
501
        }
502

503
        /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
504
        if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) &&
934✔
505
            c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
818✔
506
                c->candidate = NULL;
24✔
507

508
        /* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */
509
        if (c->newest_installed &&
934✔
510
            (c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) &&
900✔
511
            !c->candidate)
100✔
512
                c->candidate = c->newest_installed;
100✔
513

514
        return 0;
515
}
516

517
static int context_discover_update_sets(Context *c) {
534✔
518
        int r;
534✔
519

520
        assert(c);
534✔
521

522
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
1,068✔
523

524
        r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
534✔
525
        if (r < 0)
534✔
526
                return r;
527

528
        if (!arg_offline) {
534✔
529
                log_info("Determining available update sets%s", glyph(GLYPH_ELLIPSIS));
800✔
530

531
                r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
400✔
532
                if (r < 0)
400✔
533
                        return r;
534
        }
535

536
        typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
534✔
537
        return 0;
534✔
538
}
539

540
static int context_show_table(Context *c) {
×
UNCOV
541
        _cleanup_(table_unrefp) Table *t = NULL;
×
542
        int r;
×
543

544
        assert(c);
×
545

546
        t = table_new("", "version", "installed", "available", "assessment");
×
547
        if (!t)
×
548
                return log_oom();
×
549

550
        (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
×
551
        (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
×
552
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
×
553

554
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
×
UNCOV
555
                UpdateSet *us = *update_set;
×
556
                const char *color;
×
557

UNCOV
558
                color = update_set_flags_to_color(us->flags);
×
559

UNCOV
560
                r = table_add_many(t,
×
561
                                   TABLE_STRING,    update_set_flags_to_glyph(us->flags),
562
                                   TABLE_SET_COLOR, color,
563
                                   TABLE_STRING,    us->version,
564
                                   TABLE_SET_COLOR, color,
565
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
566
                                   TABLE_SET_COLOR, color,
567
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
568
                                   TABLE_SET_COLOR, color,
569
                                   TABLE_STRING,    update_set_flags_to_string(us->flags),
570
                                   TABLE_SET_COLOR, color);
571
                if (r < 0)
×
UNCOV
572
                        return table_log_add_error(r);
×
573
        }
574

UNCOV
575
        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
×
576
}
577

578
static UpdateSet* context_update_set_by_version(Context *c, const char *version) {
104✔
579
        assert(c);
104✔
580
        assert(version);
104✔
581

582
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets)
184✔
583
                if (streq((*update_set)->version, version))
184✔
584
                        return *update_set;
585

586
        return NULL;
587
}
588

589
static int context_show_version(Context *c, const char *version) {
104✔
590
        bool show_fs_columns = false, show_partition_columns = false,
104✔
591
                have_fs_attributes = false, have_partition_attributes = false,
104✔
592
                have_size = false, have_tries = false, have_no_auto = false,
104✔
593
                have_read_only = false, have_growfs = false, have_sha256 = false;
104✔
594
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
104✔
595
        _cleanup_(table_unrefp) Table *t = NULL;
104✔
596
        _cleanup_strv_free_ char **changelog_urls = NULL;
104✔
597
        UpdateSet *us;
104✔
598
        int r;
104✔
599

600
        assert(c);
104✔
601
        assert(version);
104✔
602

603
        us = context_update_set_by_version(c, version);
104✔
604
        if (!us)
104✔
UNCOV
605
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
606

607
        if (arg_json_format_flags & (SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO))
104✔
608
                pager_open(arg_pager_flags);
56✔
609

610
        if (!sd_json_format_enabled(arg_json_format_flags))
104✔
611
                printf("%s%s%s Version: %s\n"
448✔
612
                       "    State: %s%s%s\n"
613
                       "Installed: %s%s%s%s\n"
614
                       "Available: %s%s\n"
615
                       "Protected: %s%s%s\n"
616
                       " Obsolete: %s%s%s\n\n",
617
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
56✔
618
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
56✔
619
                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
64✔
620
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
104✔
621
                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
104✔
622
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
56✔
623
                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
56✔
624

625
        t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
104✔
626
        if (!t)
104✔
UNCOV
627
                return log_oom();
×
628

629
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
104✔
630
        (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
104✔
631
        (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
104✔
632
        (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
104✔
633
        (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
104✔
634
        (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
104✔
635
        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
104✔
636

637
        /* Starting in v257, these fields would be automatically formatted with underscores. This would have
638
         * been a breaking change, so to avoid that let's hard-code their original names. */
639
        (void) table_set_json_field_name(t, 7, "tries-done");
104✔
640
        (void) table_set_json_field_name(t, 8, "tries-left");
104✔
641

642
        /* Determine if the target will make use of partition/fs attributes for any of the transfers */
643
        FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
640✔
644
                Transfer *tr = *transfer;
536✔
645

646
                if (tr->target.type == RESOURCE_PARTITION)
536✔
647
                        show_partition_columns = true;
208✔
648
                if (RESOURCE_IS_FILESYSTEM(tr->target.type))
536✔
649
                        show_fs_columns = true;
328✔
650

651
                STRV_FOREACH(changelog, tr->changelog) {
536✔
652
                        assert(*changelog);
×
653

UNCOV
654
                        _cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
×
655
                        if (!changelog_url)
×
656
                                return log_oom();
×
657

658
                        /* Avoid duplicates */
659
                        if (strv_contains(changelog_urls, changelog_url))
×
660
                                continue;
×
661

662
                        /* changelog_urls takes ownership of expanded changelog_url */
UNCOV
663
                        r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
×
UNCOV
664
                        if (r < 0)
×
UNCOV
665
                                return log_oom();
×
666
                }
667
        }
668

669
        FOREACH_ARRAY(inst, us->instances, us->n_instances) {
640✔
670
                Instance *i = *inst;
536✔
671

672
                if (!i) {
536✔
673
                        assert(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING));
40✔
674
                        continue;
40✔
675
                }
676

677
                r = table_add_many(t,
496✔
678
                                   TABLE_STRING, resource_type_to_string(i->resource->type),
679
                                   TABLE_PATH, i->path);
680
                if (r < 0)
496✔
UNCOV
681
                        return table_log_add_error(r);
×
682

683
                if (i->metadata.partition_uuid_set) {
496✔
684
                        have_partition_attributes = true;
144✔
685
                        r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
144✔
686
                } else
687
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
352✔
688
                if (r < 0)
496✔
UNCOV
689
                        return table_log_add_error(r);
×
690

691
                if (i->metadata.partition_flags_set) {
496✔
692
                        have_partition_attributes = true;
144✔
693
                        r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
144✔
694
                } else
695
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
352✔
696
                if (r < 0)
496✔
UNCOV
697
                        return table_log_add_error(r);
×
698

699
                if (i->metadata.mtime != USEC_INFINITY) {
496✔
700
                        have_fs_attributes = true;
336✔
701
                        r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
336✔
702
                } else
703
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
160✔
704
                if (r < 0)
496✔
UNCOV
705
                        return table_log_add_error(r);
×
706

707
                if (i->metadata.mode != MODE_INVALID) {
496✔
708
                        have_fs_attributes = true;
336✔
709
                        r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
336✔
710
                } else
711
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
160✔
712
                if (r < 0)
496✔
713
                        return table_log_add_error(r);
×
714

715
                if (i->metadata.size != UINT64_MAX) {
496✔
UNCOV
716
                        have_size = true;
×
717
                        r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
×
718
                } else
719
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
496✔
720
                if (r < 0)
496✔
UNCOV
721
                        return table_log_add_error(r);
×
722

723
                if (i->metadata.tries_done != UINT64_MAX) {
496✔
724
                        have_tries = true;
64✔
725
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
64✔
726
                } else
727
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
432✔
728
                if (r < 0)
496✔
UNCOV
729
                        return table_log_add_error(r);
×
730

731
                if (i->metadata.tries_left != UINT64_MAX) {
496✔
732
                        have_tries = true;
64✔
733
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
64✔
734
                } else
735
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
432✔
736
                if (r < 0)
496✔
UNCOV
737
                        return table_log_add_error(r);
×
738

739
                if (i->metadata.no_auto >= 0) {
496✔
740
                        bool b;
×
741

UNCOV
742
                        have_no_auto = true;
×
UNCOV
743
                        b = i->metadata.no_auto;
×
744
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
745
                } else
746
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
496✔
747
                if (r < 0)
496✔
UNCOV
748
                        return table_log_add_error(r);
×
749
                if (i->metadata.read_only >= 0) {
496✔
750
                        bool b;
144✔
751

752
                        have_read_only = true;
144✔
753
                        b = i->metadata.read_only;
144✔
754
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
144✔
755
                } else
756
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
352✔
757
                if (r < 0)
496✔
UNCOV
758
                        return table_log_add_error(r);
×
759

760
                if (i->metadata.growfs >= 0) {
496✔
761
                        bool b;
×
762

UNCOV
763
                        have_growfs = true;
×
UNCOV
764
                        b = i->metadata.growfs;
×
765
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
766
                } else
767
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
496✔
768
                if (r < 0)
496✔
UNCOV
769
                        return table_log_add_error(r);
×
770

771
                if (i->metadata.sha256sum_set) {
496✔
772
                        _cleanup_free_ char *formatted = NULL;
16✔
773

774
                        have_sha256 = true;
16✔
775

776
                        formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
16✔
777
                        if (!formatted)
16✔
UNCOV
778
                                return log_oom();
×
779

780
                        r = table_add_cell(t, NULL, TABLE_STRING, formatted);
16✔
781
                } else
782
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
480✔
783
                if (r < 0)
496✔
UNCOV
784
                        return table_log_add_error(r);
×
785
        }
786

787
        /* Hide the fs/partition columns if we don't have any data to show there */
788
        if (!have_fs_attributes)
104✔
789
                show_fs_columns = false;
8✔
790
        if (!have_partition_attributes)
104✔
791
                show_partition_columns = false;
792

793
        if (!show_partition_columns)
72✔
794
                (void) table_hide_column_from_display(t, 2, 3);
32✔
795
        if (!show_fs_columns)
104✔
796
                (void) table_hide_column_from_display(t, 4, 5);
8✔
797
        if (!have_size)
104✔
798
                (void) table_hide_column_from_display(t, 6);
104✔
799
        if (!have_tries)
104✔
800
                (void) table_hide_column_from_display(t, 7, 8);
40✔
801
        if (!have_no_auto)
104✔
802
                (void) table_hide_column_from_display(t, 9);
104✔
803
        if (!have_read_only)
104✔
804
                (void) table_hide_column_from_display(t, 10);
32✔
805
        if (!have_growfs)
104✔
806
                (void) table_hide_column_from_display(t, 11);
104✔
807
        if (!have_sha256)
104✔
808
                (void) table_hide_column_from_display(t, 12);
96✔
809

810
        if (!sd_json_format_enabled(arg_json_format_flags)) {
104✔
811
                printf("%s%s%s Version: %s\n"
504✔
812
                       "    State: %s%s%s\n"
813
                       "Installed: %s%s%s%s%s%s%s\n"
814
                       "Available: %s%s\n"
815
                       "Protected: %s%s%s\n"
816
                       " Obsolete: %s%s%s\n",
817
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
56✔
818
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
56✔
819
                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
64✔
820
                       FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
72✔
821
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
104✔
822
                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
104✔
823
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
56✔
824
                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
56✔
825

826
                STRV_FOREACH(url, changelog_urls) {
56✔
827
                        _cleanup_free_ char *changelog_link = NULL;
×
UNCOV
828
                        r = terminal_urlify(*url, NULL, &changelog_link);
×
UNCOV
829
                        if (r < 0)
×
UNCOV
830
                                return log_oom();
×
UNCOV
831
                        printf("ChangeLog: %s\n", changelog_link);
×
832
                }
833
                printf("\n");
56✔
834

835
                return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
56✔
836
        } else {
837
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
48✔
838

839
                r = table_to_json(t, &t_json);
48✔
840
                if (r < 0)
48✔
UNCOV
841
                        return log_error_errno(r, "failed to convert table to JSON: %m");
×
842

843
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("version", us->version),
48✔
844
                                          SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)),
845
                                          SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
846
                                          SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
847
                                          SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)),
848
                                          SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)),
849
                                          SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
850
                                          SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
851
                                          SD_JSON_BUILD_PAIR_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)),
852
                                          SD_JSON_BUILD_PAIR_STRV("changelogUrls", changelog_urls),
853
                                          SD_JSON_BUILD_PAIR_VARIANT("contents", t_json));
854
                if (r < 0)
48✔
855
                        return log_error_errno(r, "Failed to create JSON: %m");
×
856

857
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
48✔
858
                if (r < 0)
48✔
UNCOV
859
                        return log_error_errno(r, "Failed to print JSON: %m");
×
860

861
                return 0;
862
        }
863
}
864

865
static int context_vacuum(
104✔
866
                Context *c,
867
                uint64_t space,
868
                const char *extra_protected_version) {
869

870
        size_t disabled_count = 0;
104✔
871
        int r, count = 0;
104✔
872

873
        assert(c);
104✔
874

875
        if (space == 0)
104✔
876
                log_info("Making room%s", glyph(GLYPH_ELLIPSIS));
32✔
877
        else
878
                log_info("Making room for %" PRIu64 " updates%s", space, glyph(GLYPH_ELLIPSIS));
176✔
879

880
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
632✔
881
                Transfer *t = *tr;
528✔
882

883
                /* Don't bother clearing out space if we're not going to be downloading anything */
884
                if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version))
528✔
885
                        continue;
72✔
886

887
                r = transfer_vacuum(t, space, extra_protected_version);
456✔
888
                if (r < 0)
456✔
889
                        return r;
890

891
                count = MAX(count, r);
456✔
892
        }
893

894
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
200✔
895
                r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
96✔
896
                if (r < 0)
96✔
897
                        return r;
898
                if (r > 0)
96✔
899
                        disabled_count++;
8✔
900
        }
901

902
        if (!sd_json_format_enabled(arg_json_format_flags)) {
104✔
903
                if (count > 0 && disabled_count > 0)
78✔
UNCOV
904
                        log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
×
905
                else if (count > 0)
78✔
906
                        log_info("Removed %i instances.", count);
38✔
907
                else if (disabled_count > 0)
40✔
908
                        log_info("Removed %zu disabled transfers.", disabled_count);
8✔
909
                else
910
                        log_info("Found nothing to remove.");
32✔
911
        } else {
912
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
26✔
913

914
                r = sd_json_buildo(&json,
26✔
915
                                   SD_JSON_BUILD_PAIR_INTEGER("removed", count),
916
                                   SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
917
                if (r < 0)
26✔
918
                        return log_error_errno(r, "Failed to create JSON: %m");
×
919

920
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
26✔
921
                if (r < 0)
26✔
UNCOV
922
                        return log_error_errno(r, "Failed to print JSON: %m");
×
923
        }
924

925
        return 0;
926
}
927

928
static int context_make_offline(Context **ret, const char *node, bool requires_enabled_transfers) {
574✔
929
        _cleanup_(context_freep) Context* context = NULL;
574✔
930
        int r;
574✔
931

932
        assert(ret);
574✔
933

934
        /* Allocates a context object and initializes everything we can initialize offline, i.e. without
935
         * checking on the update source (i.e. the Internet) what versions are available */
936

937
        context = context_new();
574✔
938
        if (!context)
574✔
UNCOV
939
                return log_oom();
×
940

941
        r = context_read_definitions(context, node, requires_enabled_transfers);
574✔
942
        if (r < 0)
574✔
943
                return r;
944

945
        r = context_load_installed_instances(context);
574✔
946
        if (r < 0)
574✔
947
                return r;
948

949
        *ret = TAKE_PTR(context);
574✔
950
        return 0;
574✔
951
}
952

953
static int context_make_online(Context **ret, const char *node) {
542✔
954
        _cleanup_(context_freep) Context* context = NULL;
542✔
955
        int r;
542✔
956

957
        assert(ret);
542✔
958

959
        /* Like context_make_offline(), but also communicates with the update source looking for new
960
         * versions (as long as --offline is not specified on the command line). */
961

962
        r = context_make_offline(&context, node, /* requires_enabled_transfers= */ true);
542✔
963
        if (r < 0)
542✔
964
                return r;
965

966
        if (!arg_offline) {
542✔
967
                r = context_load_available_instances(context);
408✔
968
                if (r < 0)
408✔
969
                        return r;
970
        }
971

972
        r = context_discover_update_sets(context);
534✔
973
        if (r < 0)
534✔
974
                return r;
975

976
        *ret = TAKE_PTR(context);
534✔
977
        return 0;
534✔
978
}
979

980
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
315✔
981
        const Context *c = ASSERT_PTR(t->context);
315✔
982
        size_t i, n = c->n_transfers;
315✔
983
        uint64_t base, scaled;
315✔
984
        unsigned overall;
315✔
985

986
        for (i = 0; i < n; i++)
844✔
987
                if (c->transfers[i] == t)
844✔
988
                        break;
989
        assert(i < n); /* We should have found the index */
315✔
990

991
        base = (100 * 100 * i) / n;
315✔
992
        scaled = (100 * percentage) / n;
315✔
993
        overall = (unsigned) ((base + scaled) / 100);
315✔
994
        assert(overall <= 100);
315✔
995

996
        log_debug("Transfer %zu/%zu is %u%% complete (%u%% overall).", i+1, n, percentage, overall);
315✔
997
        return sd_notifyf(/* unset_environment= */ false, "X_SYSUPDATE_PROGRESS=%u\n"
630✔
998
                                              "X_SYSUPDATE_TRANSFERS_LEFT=%zu\n"
999
                                              "X_SYSUPDATE_TRANSFERS_DONE=%zu\n"
1000
                                              "STATUS=Updating to '%s' (%u%% complete).",
1001
                                              overall, n - i, i, inst->metadata.version, overall);
315✔
1002
}
1003

1004
static int context_process_partial_and_pending(Context *c, const char *version);
1005

1006
static int context_acquire(
136✔
1007
                Context *c,
1008
                const char *version) {
1009

1010
        UpdateSet *us = NULL;
136✔
1011
        int r;
136✔
1012

1013
        assert(c);
136✔
1014

1015
        if (version) {
136✔
UNCOV
1016
                us = context_update_set_by_version(c, version);
×
UNCOV
1017
                if (!us)
×
1018
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
136✔
1019
        } else {
1020
                if (!c->candidate) {
136✔
1021
                        log_info("No update needed.");
24✔
1022

1023
                        return 0;
1024
                }
1025

1026
                us = c->candidate;
1027
        }
1028

1029
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
112✔
1030
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
16✔
1031
        else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
96✔
1032
                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
8✔
1033
        } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
88✔
1034
                log_info("Selected update '%s' is already acquired and pending installation.", us->version);
16✔
1035

1036
                return context_process_partial_and_pending(c, version);
16✔
1037
        } else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
72✔
UNCOV
1038
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1039

1040
                return 0;
1041
        }
1042

1043
        if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
88✔
1044
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
×
1045
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
88✔
UNCOV
1046
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1047

1048
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
88✔
1049
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1050
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
88✔
UNCOV
1051
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1052

1053
        log_info("Selected update '%s' for install.", us->version);
88✔
1054

1055
        _cleanup_free_ InstanceMetadata *metadata = new0(InstanceMetadata, c->n_transfers);
176✔
1056
        if (!metadata)
88✔
UNCOV
1057
                return log_oom();
×
1058

1059
        /* Compute up the temporary paths before vacuuming so we don't vacuum anything if we fail to compute
1060
         * any paths because of failed validations (e.g. exceeding the gpt partition label size). */
1061
        for (size_t i = 0; i < c->n_transfers; i++) {
536✔
1062
                Instance *inst = us->instances[i];
448✔
1063
                Transfer *t = c->transfers[i];
448✔
1064

1065
                assert(inst);
448✔
1066

1067
                r = transfer_compute_temporary_paths(t, inst, metadata + i);
448✔
1068
                if (r < 0)
448✔
1069
                        return r;
1070
        }
1071

1072
        (void) sd_notifyf(/* unset_environment= */ false,
88✔
1073
                          "READY=1\n"
1074
                          "X_SYSUPDATE_VERSION=%s\n"
1075
                          "STATUS=Making room for '%s'.", us->version, us->version);
1076

1077
        /* Let's make some room. We make sure for each transfer we have one free space to fill. While
1078
         * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
1079
         * download succeeded already, in which case we shouldn't remove it just to acquire it again */
1080
        r = context_vacuum(
176✔
1081
                        c,
1082
                        /* space= */ 1,
1083
                        /* extra_protected_version= */ us->version);
88✔
1084
        if (r < 0)
88✔
1085
                return r;
1086

1087
        if (arg_sync)
88✔
1088
                sync();
88✔
1089

1090
        (void) sd_notifyf(/* unset_environment= */ false,
88✔
1091
                          "STATUS=Updating to '%s'.", us->version);
1092

1093
        /* There should now be one instance picked for each transfer, and the order is the same */
1094
        assert(us->n_instances == c->n_transfers);
88✔
1095

1096
        for (size_t i = 0; i < c->n_transfers; i++) {
504✔
1097
                Instance *inst = us->instances[i];
424✔
1098
                Transfer *t = c->transfers[i];
424✔
1099

1100
                assert(inst); /* ditto */
424✔
1101

1102
                if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */
424✔
1103
                        assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
72✔
1104
                        continue;
72✔
1105
                }
1106

1107
                r = transfer_acquire_instance(t, inst, metadata + i, context_on_acquire_progress, c);
352✔
1108
                if (r < 0)
352✔
1109
                        return r;
1110
        }
1111

1112
        if (arg_sync)
80✔
1113
                sync();
80✔
1114

1115
        return 1;
1116
}
1117

1118
/* Check to see if we have an update set acquired and pending installation. */
1119
static int context_process_partial_and_pending(
58✔
1120
                Context *c,
1121
                const char *version) {
1122

1123
        UpdateSet *us = NULL;
58✔
1124
        int r;
58✔
1125

1126
        assert(c);
58✔
1127

1128
        if (version) {
58✔
1129
                us = context_update_set_by_version(c, version);
×
UNCOV
1130
                if (!us)
×
UNCOV
1131
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1132
        } else {
1133
                if (!c->candidate) {
58✔
UNCOV
1134
                        log_info("No update needed.");
×
1135

1136
                        return 0;
1137
                }
1138

1139
                us = c->candidate;
1140
        }
1141

1142
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
58✔
1143
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
×
1144
        else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
58✔
UNCOV
1145
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1146

1147
                return 0;
1148
        }
1149

1150
        if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
58✔
1151
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
2✔
1152
        if (!FLAGS_SET(us->flags, UPDATE_PENDING))
56✔
UNCOV
1153
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version);
×
1154

1155
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
56✔
UNCOV
1156
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1157

1158
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
56✔
1159
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1160
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
56✔
UNCOV
1161
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1162

1163
        log_info("Selected update '%s' for install.", us->version);
56✔
1164

1165
        /* There should now be one instance picked for each transfer, and the order is the same */
1166
        assert(us->n_instances == c->n_transfers);
56✔
1167

1168
        for (size_t i = 0; i < c->n_transfers; i++) {
342✔
1169
                Instance *inst = us->instances[i];
286✔
1170
                Transfer *t = c->transfers[i];
286✔
1171

1172
                assert(inst);
286✔
1173

1174
                r = transfer_process_partial_and_pending_instance(t, inst);
286✔
1175
                if (r < 0)
286✔
1176
                        return r;
1177
        }
1178

1179
        return 1;
1180
}
1181

1182
static int context_install(
80✔
1183
                Context *c,
1184
                const char *version,
1185
                UpdateSet **ret_applied) {
1186

1187
        UpdateSet *us = NULL;
80✔
1188
        int r;
80✔
1189

1190
        assert(c);
80✔
1191

1192
        if (version) {
80✔
1193
                us = context_update_set_by_version(c, version);
×
UNCOV
1194
                if (!us)
×
UNCOV
1195
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1196
        } else {
1197
                if (!c->candidate) {
80✔
UNCOV
1198
                        log_info("No update needed.");
×
1199

1200
                        return 0;
1201
                }
1202

1203
                us = c->candidate;
1204
        }
1205

1206
        (void) sd_notifyf(/* unset_environment=*/ false,
80✔
1207
                          "READY=1\n"
1208
                          "X_SYSUPDATE_VERSION=%s\n"
1209
                          "STATUS=Installing '%s'.", us->version, us->version);
1210

1211
        for (size_t i = 0; i < c->n_transfers; i++) {
568✔
1212
                Instance *inst = us->instances[i];
408✔
1213
                Transfer *t = c->transfers[i];
408✔
1214

1215
                if (inst->resource == &t->target &&
408✔
1216
                    !inst->is_pending)
328✔
1217
                        continue;
72✔
1218

1219
                r = transfer_install_instance(t, inst, arg_root);
336✔
1220
                if (r < 0)
336✔
1221
                        return r;
1222
        }
1223

1224
        log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
80✔
1225

1226
        (void) sd_notifyf(/* unset_environment= */ false,
80✔
1227
                          "STATUS=Installed '%s'.", us->version);
1228

1229
        if (ret_applied)
80✔
1230
                *ret_applied = us;
80✔
1231

1232
        return 1;
1233
}
1234

1235
static int process_image(
660✔
1236
                bool ro,
1237
                char **ret_mounted_dir,
1238
                LoopDevice **ret_loop_device) {
1239

1240
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
660✔
1241
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
660✔
1242
        int r;
660✔
1243

1244
        assert(ret_mounted_dir);
660✔
1245
        assert(ret_loop_device);
660✔
1246

1247
        if (!arg_image)
660✔
1248
                return 0;
1249

1250
        assert(!arg_root);
×
1251

UNCOV
1252
        r = mount_image_privately_interactively(
×
1253
                        arg_image,
1254
                        arg_image_policy,
1255
                        (ro ? DISSECT_IMAGE_READ_ONLY : 0) |
1256
                        DISSECT_IMAGE_FSCK |
1257
                        DISSECT_IMAGE_MKDIR |
1258
                        DISSECT_IMAGE_GROWFS |
1259
                        DISSECT_IMAGE_RELAX_VAR_CHECK |
1260
                        DISSECT_IMAGE_USR_NO_ROOT |
1261
                        DISSECT_IMAGE_GENERIC_ROOT |
1262
                        DISSECT_IMAGE_REQUIRE_ROOT |
1263
                        DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
1264
                        &mounted_dir,
1265
                        /* ret_dir_fd= */ NULL,
1266
                        &loop_device);
UNCOV
1267
        if (r < 0)
×
1268
                return r;
1269

1270
        arg_root = strdup(mounted_dir);
×
UNCOV
1271
        if (!arg_root)
×
1272
                return log_oom();
×
1273

UNCOV
1274
        *ret_mounted_dir = TAKE_PTR(mounted_dir);
×
1275
        *ret_loop_device = TAKE_PTR(loop_device);
×
1276

UNCOV
1277
        return 0;
×
1278
}
1279

1280
VERB(verb_list, "list", "[VERSION]", VERB_ANY, 2, VERB_DEFAULT,
1281
     "Show installed and available versions");
1282
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
156✔
1283
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
156✔
1284
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
156✔
1285
        _cleanup_(context_freep) Context* context = NULL;
156✔
1286
        _cleanup_strv_free_ char **appstream_urls = NULL;
156✔
1287
        const char *version;
156✔
1288
        int r;
156✔
1289

1290
        assert(argc <= 2);
156✔
1291
        version = argc >= 2 ? argv[1] : NULL;
156✔
1292

1293
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
156✔
1294
        if (r < 0)
156✔
1295
                return r;
1296

1297
        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
156✔
1298
        if (r < 0)
156✔
1299
                return r;
1300

1301
        if (version)
156✔
1302
                return context_show_version(context, version);
104✔
1303
        else if (!sd_json_format_enabled(arg_json_format_flags))
52✔
UNCOV
1304
                return context_show_table(context);
×
1305
        else {
1306
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
52✔
1307
                _cleanup_strv_free_ char **versions = NULL;
52✔
1308
                const char *current = NULL;
52✔
1309
                bool current_is_pending = false;
52✔
1310

1311
                FOREACH_ARRAY(update_set, context->update_sets, context->n_update_sets) {
210✔
1312
                        UpdateSet *us = *update_set;
158✔
1313

1314
                        if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
158✔
1315
                            FLAGS_SET(us->flags, UPDATE_NEWEST)) {
142✔
1316
                                current = us->version;
50✔
1317
                                current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
50✔
1318
                        }
1319

1320
                        r = strv_extend(&versions, us->version);
158✔
1321
                        if (r < 0)
158✔
UNCOV
1322
                                return log_oom();
×
1323
                }
1324

1325
                FOREACH_ARRAY(tr, context->transfers, context->n_transfers)
314✔
1326
                        STRV_FOREACH(appstream_url, (*tr)->appstream) {
262✔
1327
                                /* Avoid duplicates */
UNCOV
1328
                                if (strv_contains(appstream_urls, *appstream_url))
×
1329
                                        continue;
×
1330

1331
                                r = strv_extend(&appstream_urls, *appstream_url);
×
UNCOV
1332
                                if (r < 0)
×
UNCOV
1333
                                        return log_oom();
×
1334
                        }
1335

1336
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
102✔
1337
                                          SD_JSON_BUILD_PAIR_STRV("all", versions),
1338
                                          SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
1339
                if (r < 0)
52✔
UNCOV
1340
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1341

1342
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
52✔
1343
                if (r < 0)
52✔
UNCOV
1344
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1345

1346
                return 0;
1347
        }
1348
}
1349

1350
VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, 0,
1351
     "Show optional features");
1352
static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) {
16✔
1353
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
16✔
1354
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
16✔
1355
        _cleanup_(context_freep) Context* context = NULL;
16✔
1356
        _cleanup_(table_unrefp) Table *table = NULL;
16✔
1357
        const char *feature_id;
16✔
1358
        Feature *f;
16✔
1359
        int r;
16✔
1360

1361
        assert(argc <= 2);
16✔
1362
        feature_id = argc >= 2 ? argv[1] : NULL;
16✔
1363

1364
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
16✔
1365
        if (r < 0)
16✔
1366
                return r;
1367

1368
        r = context_make_offline(&context, loop_device ? loop_device->node : NULL, /* requires_enabled_transfers= */ false);
16✔
1369
        if (r < 0)
16✔
1370
                return r;
1371

1372
        if (feature_id) {
16✔
1373
                _cleanup_strv_free_ char **transfers = NULL;
8✔
1374

1375
                f = hashmap_get(context->features, feature_id);
8✔
1376
                if (!f)
8✔
UNCOV
1377
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
1378
                                               "Optional feature not found: %s",
1379
                                               feature_id);
1380

1381
                table = table_new_vertical();
8✔
1382
                if (!table)
8✔
UNCOV
1383
                        return log_oom();
×
1384

1385
                FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
48✔
1386
                        Transfer *t = *tr;
40✔
1387

1388
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
40✔
1389
                                continue;
40✔
1390

1391
                        r = strv_extend(&transfers, t->id);
×
UNCOV
1392
                        if (r < 0)
×
UNCOV
1393
                                return log_oom();
×
1394
                }
1395

1396
                FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) {
16✔
1397
                        Transfer *t = *tr;
8✔
1398

1399
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
8✔
UNCOV
1400
                                continue;
×
1401

1402
                        r = strv_extend(&transfers, t->id);
8✔
1403
                        if (r < 0)
8✔
UNCOV
1404
                                return log_oom();
×
1405
                }
1406

1407
                r = table_add_many(table,
8✔
1408
                                   TABLE_FIELD, "Name",
1409
                                   TABLE_STRING, f->id,
1410
                                   TABLE_FIELD, "Enabled",
1411
                                   TABLE_BOOLEAN, f->enabled);
1412
                if (r < 0)
8✔
UNCOV
1413
                        return table_log_add_error(r);
×
1414

1415
                if (f->description) {
8✔
1416
                        r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
8✔
1417
                        if (r < 0)
8✔
UNCOV
1418
                                return table_log_add_error(r);
×
1419
                }
1420

1421
                if (f->documentation) {
8✔
UNCOV
1422
                        r = table_add_many(table,
×
1423
                                           TABLE_FIELD, "Documentation",
1424
                                           TABLE_STRING, f->documentation,
1425
                                           TABLE_SET_URL, f->documentation);
UNCOV
1426
                        if (r < 0)
×
UNCOV
1427
                                return table_log_add_error(r);
×
1428
                }
1429

1430
                if (f->appstream) {
8✔
UNCOV
1431
                        r = table_add_many(table,
×
1432
                                           TABLE_FIELD, "AppStream",
1433
                                           TABLE_STRING, f->appstream,
1434
                                           TABLE_SET_URL, f->appstream);
UNCOV
1435
                        if (r < 0)
×
UNCOV
1436
                                return table_log_add_error(r);
×
1437
                }
1438

1439
                if (!strv_isempty(transfers)) {
8✔
1440
                        r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
8✔
1441
                        if (r < 0)
8✔
UNCOV
1442
                                return table_log_add_error(r);
×
1443
                }
1444

1445
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
8✔
1446
        } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
8✔
1447
                table = table_new("", "feature", "description", "documentation");
8✔
1448
                if (!table)
8✔
UNCOV
1449
                        return log_oom();
×
1450

1451
                HASHMAP_FOREACH(f, context->features) {
16✔
1452
                        r = table_add_many(table,
8✔
1453
                                           TABLE_BOOLEAN_CHECKMARK, f->enabled,
1454
                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
1455
                                           TABLE_STRING, f->id,
1456
                                           TABLE_STRING, f->description,
1457
                                           TABLE_STRING, f->documentation,
1458
                                           TABLE_SET_URL, f->documentation);
1459
                        if (r < 0)
8✔
UNCOV
1460
                                return table_log_add_error(r);
×
1461
                }
1462

1463
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
8✔
1464
        } else {
UNCOV
1465
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
×
1466
                _cleanup_strv_free_ char **features = NULL;
×
1467

1468
                HASHMAP_FOREACH(f, context->features) {
×
1469
                        r = strv_extend(&features, f->id);
×
UNCOV
1470
                        if (r < 0)
×
UNCOV
1471
                                return log_oom();
×
1472
                }
1473

1474
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
×
UNCOV
1475
                if (r < 0)
×
1476
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1477

1478
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
×
UNCOV
1479
                if (r < 0)
×
UNCOV
1480
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1481
        }
1482

UNCOV
1483
        return 0;
×
1484
}
1485

1486
VERB_NOARG(verb_check_new, "check-new",
1487
           "Check if there's a new version available");
1488
static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) {
200✔
1489
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
200✔
1490
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
200✔
1491
        _cleanup_(context_freep) Context* context = NULL;
200✔
1492
        int r;
200✔
1493

1494
        assert(argc <= 1);
200✔
1495

1496
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
200✔
1497
        if (r < 0)
200✔
1498
                return r;
1499

1500
        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
200✔
1501
        if (r < 0)
200✔
1502
                return r;
1503

1504
        if (!sd_json_format_enabled(arg_json_format_flags)) {
200✔
1505
                if (!context->candidate) {
184✔
1506
                        log_debug("No candidate found.");
96✔
1507
                        return EXIT_FAILURE;
1508
                }
1509

1510
                puts(context->candidate->version);
88✔
1511
        } else {
1512
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
16✔
1513

1514
                if (context->candidate)
16✔
UNCOV
1515
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("available", context->candidate->version));
×
1516
                else
1517
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_NULL("available"));
16✔
1518
                if (r < 0)
16✔
UNCOV
1519
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1520

1521
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
16✔
1522
                if (r < 0)
16✔
UNCOV
1523
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1524
        }
1525

1526
        return EXIT_SUCCESS;
1527
}
1528

1529
typedef enum {
1530
        UPDATE_ACTION_ACQUIRE = 1 << 0,
1531
        UPDATE_ACTION_INSTALL = 1 << 1,
1532
} UpdateActionFlags;
1533

1534
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
186✔
1535
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
186✔
1536
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
186✔
UNCOV
1537
        _cleanup_(context_freep) Context* context = NULL;
×
1538
        _cleanup_free_ char *booted_version = NULL;
186✔
1539
        UpdateSet *applied = NULL;
186✔
1540
        const char *version;
186✔
1541
        int r;
186✔
1542

1543
        assert(argc <= 2);
186✔
1544
        version = argc >= 2 ? argv[1] : NULL;
186✔
1545

1546
        if (arg_instances_max < 2)
186✔
UNCOV
1547
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1548
                                      "The --instances-max argument must be >= 2 while updating");
1549

1550
        if (arg_reboot) {
186✔
1551
                /* If automatic reboot on completion is requested, let's first determine the currently booted image */
1552

1553
                r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
×
1554
                if (r < 0)
×
1555
                        return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
UNCOV
1556
                if (!booted_version)
×
UNCOV
1557
                        return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
×
1558
        }
1559

1560
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
186✔
1561
        if (r < 0)
186✔
1562
                return r;
1563

1564
        r = context_make_online(&context, loop_device ? loop_device->node : NULL);
186✔
1565
        if (r < 0)
186✔
1566
                return r;
1567

1568
        if (action_flags & UPDATE_ACTION_ACQUIRE)
178✔
1569
                r = context_acquire(context, version);
136✔
1570
        else
1571
                r = context_process_partial_and_pending(context, version);
42✔
1572
        if (r < 0)
178✔
1573
                return r;  /* error */
1574

1575
        if (action_flags & UPDATE_ACTION_INSTALL && r > 0)  /* update needed */
160✔
1576
                r = context_install(context, version, &applied);
80✔
1577
        if (r < 0)
160✔
1578
                return r;
1579

1580
        if (r > 0 && arg_reboot) {
160✔
UNCOV
1581
                assert(applied);
×
1582
                assert(booted_version);
×
1583

1584
                if (strverscmp_improved(applied->version, booted_version) > 0) {
×
UNCOV
1585
                        log_notice("Newly installed version is newer than booted version, rebooting.");
×
UNCOV
1586
                        return reboot_now();
×
1587
                }
1588

1589
                if (strverscmp_improved(applied->version, booted_version) == 0 &&
×
1590
                    FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) {
×
UNCOV
1591
                        log_notice("Currently booted version was incomplete and has been repaired, rebooting.");
×
UNCOV
1592
                        return reboot_now();
×
1593
                }
1594

UNCOV
1595
                log_info("Booted version is newer or identical to newly installed version, not rebooting.");
×
1596
        }
1597

1598
        return 0;
1599
}
1600

1601
VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0,
1602
     "Install new version now");
1603
static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) {
118✔
1604
        UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
118✔
1605

1606
        if (!arg_offline)
118✔
1607
                flags |= UPDATE_ACTION_ACQUIRE;
76✔
1608

1609
        return verb_update_impl(argc, argv, flags);
118✔
1610
}
1611

1612
VERB(verb_acquire, "acquire", "[VERSION]", VERB_ANY, 2, 0,
1613
     "Acquire (download) new version now");
1614
static int verb_acquire(int argc, char *argv[], uintptr_t _data, void *userdata) {
68✔
1615
        return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
68✔
1616
}
1617

1618
VERB_NOARG(verb_vacuum, "vacuum",
1619
           "Make room, by deleting old versions");
1620
static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) {
16✔
1621
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
16✔
1622
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
16✔
1623
        _cleanup_(context_freep) Context* context = NULL;
16✔
1624
        int r;
16✔
1625

1626
        assert(argc <= 1);
16✔
1627

1628
        if (arg_instances_max < 1)
16✔
UNCOV
1629
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1630
                                      "The --instances-max argument must be >= 1 while vacuuming");
1631

1632
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
16✔
1633
        if (r < 0)
16✔
1634
                return r;
1635

1636
        r = context_make_offline(&context, loop_device ? loop_device->node : NULL, /* requires_enabled_transfers= */ false);
16✔
1637
        if (r < 0)
16✔
1638
                return r;
1639

1640
        return context_vacuum(context, 0, NULL);
16✔
1641
}
1642

1643
VERB(verb_pending_or_reboot, "pending", NULL, 1, 1, 0,
1644
     "Report whether a newer version is installed than currently booted");
1645
VERB(verb_pending_or_reboot, "reboot", NULL, 1, 1, 0,
1646
     "Reboot if a newer version is installed than booted");
1647
static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
×
1648
        _cleanup_(context_freep) Context* context = NULL;
×
UNCOV
1649
        _cleanup_free_ char *booted_version = NULL;
×
1650
        int r;
×
1651

1652
        assert(argc == 1);
×
1653

UNCOV
1654
        if (arg_image || arg_root)
×
UNCOV
1655
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1656
                                       "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]);
1657

UNCOV
1658
        r = context_make_offline(&context, /* node= */ NULL, /* requires_enabled_transfers= */ true);
×
UNCOV
1659
        if (r < 0)
×
1660
                return r;
1661

1662
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
×
1663

UNCOV
1664
        r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
×
1665
        if (r < 0)
×
1666
                return r;
UNCOV
1667
        if (!context->newest_installed)
×
1668
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
×
1669

UNCOV
1670
        r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
×
1671
        if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable
×
1672
                    * if we see what the first argument is about */
1673
                return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
UNCOV
1674
        if (!booted_version)
×
1675
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
×
1676

1677
        r = strverscmp_improved(context->newest_installed->version, booted_version);
×
UNCOV
1678
        if (r > 0) {
×
UNCOV
1679
                log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
×
1680
                           context->newest_installed->version, booted_version,
1681
                           streq(argv[0], "pending") ? " Reboot recommended." : "");
1682

UNCOV
1683
                if (streq(argv[0], "reboot"))
×
UNCOV
1684
                        return reboot_now();
×
1685

1686
                return EXIT_SUCCESS;
UNCOV
1687
        } else if (r == 0)
×
UNCOV
1688
                log_info("Newest installed version '%s' matches booted version '%s'.",
×
1689
                         context->newest_installed->version, booted_version);
1690
        else
UNCOV
1691
                log_warning("Newest installed version '%s' is older than booted version '%s'.",
×
1692
                            context->newest_installed->version, booted_version);
1693

UNCOV
1694
        if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */
×
UNCOV
1695
                return EXIT_FAILURE;
×
1696

1697
        return EXIT_SUCCESS;
1698
}
1699

1700
static int component_name_valid(const char *c) {
32✔
1701
        _cleanup_free_ char *j = NULL;
32✔
1702

1703
        /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */
1704

1705
        if (isempty(c))
64✔
1706
                return false;
1707

1708
        if (string_has_cc(c, NULL))
32✔
1709
                return false;
1710

1711
        if (!utf8_is_valid(c))
32✔
1712
                return false;
1713

1714
        j = strjoin("sysupdate.", c, ".d");
32✔
1715
        if (!j)
32✔
1716
                return -ENOMEM;
1717

1718
        return filename_is_valid(j);
32✔
1719
}
1720

1721
VERB_NOARG(verb_components, "components",
1722
           "Show list of components");
1723
static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
86✔
1724
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
86✔
1725
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
86✔
1726
        _cleanup_set_free_ Set *names = NULL;
86✔
1727
        bool has_default_component = false;
86✔
1728
        int r;
86✔
1729

1730
        assert(argc <= 1);
86✔
1731

1732
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
86✔
1733
        if (r < 0)
86✔
1734
                return r;
1735

1736
        ConfFile **directories = NULL;
86✔
1737
        size_t n_directories = 0;
86✔
1738

1739
        CLEANUP_ARRAY(directories, n_directories, conf_file_free_array);
86✔
1740

1741
        r = conf_files_list_strv_full(".d", arg_root, CONF_FILES_DIRECTORY|CONF_FILES_WARN,
172✔
1742
                                      (const char * const *) CONF_PATHS_STRV(""), &directories, &n_directories);
86✔
1743
        if (r < 0)
86✔
UNCOV
1744
                return log_error_errno(r, "Failed to enumerate directories: %m");
×
1745

1746
        FOREACH_ARRAY(i, directories, n_directories) {
2,260✔
1747
                ConfFile *e = *i;
2,174✔
1748

1749
                if (streq(e->filename, "sysupdate.d")) {
2,174✔
1750
                        has_default_component = true;
86✔
1751
                        continue;
86✔
1752
                }
1753

1754
                const char *s = startswith(e->filename, "sysupdate.");
2,088✔
1755
                if (!s)
2,088✔
1756
                        continue;
2,064✔
1757

1758
                const char *a = endswith(s, ".d");
24✔
1759
                if (!a)
24✔
1760
                        continue;
×
1761

1762
                _cleanup_free_ char *n = strndup(s, a - s);
×
1763
                if (!n)
24✔
UNCOV
1764
                        return log_oom();
×
1765

1766
                r = component_name_valid(n);
24✔
1767
                if (r < 0)
24✔
1768
                        return log_error_errno(r, "Unable to validate component name '%s': %m", n);
×
1769
                if (r == 0)
24✔
UNCOV
1770
                        continue;
×
1771

1772
                r = set_ensure_put(&names, &string_hash_ops_free, n);
24✔
1773
                if (r < 0 && r != -EEXIST)
24✔
UNCOV
1774
                        return log_error_errno(r, "Failed to add component '%s' to set: %m", n);
×
1775
                TAKE_PTR(n);
1776
        }
1777

1778
        /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */
1779
        _cleanup_free_ char **z = set_get_strv(names);
172✔
1780
        if (!z)
86✔
UNCOV
1781
                return log_oom();
×
1782

1783
        strv_sort(z);
86✔
1784

1785
        if (!sd_json_format_enabled(arg_json_format_flags)) {
86✔
UNCOV
1786
                if (!has_default_component && set_isempty(names)) {
×
UNCOV
1787
                        log_info("No components defined.");
×
1788
                        return 0;
1789
                }
1790

1791
                if (has_default_component)
UNCOV
1792
                        printf("%s<default>%s\n",
×
1793
                               ansi_highlight(), ansi_normal());
1794

UNCOV
1795
                STRV_FOREACH(i, z)
×
UNCOV
1796
                        puts(*i);
×
1797
        } else {
1798
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
86✔
1799

1800
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component),
86✔
1801
                                          SD_JSON_BUILD_PAIR_STRV("components", z));
1802
                if (r < 0)
86✔
UNCOV
1803
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1804

1805
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
86✔
1806
                if (r < 0)
86✔
UNCOV
1807
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1808
        }
1809

1810
        return 0;
1811
}
1812

1813
static int help(void) {
×
1814
        _cleanup_free_ char *link = NULL;
×
UNCOV
1815
        _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL;
×
1816
        int r;
×
1817

1818
        r = terminal_urlify_man("systemd-sysupdate", "8", &link);
×
UNCOV
1819
        if (r < 0)
×
1820
                return log_oom();
×
1821

UNCOV
1822
        r = verbs_get_help_table(&verbs);
×
UNCOV
1823
        if (r < 0)
×
1824
                return r;
1825

UNCOV
1826
        r = option_parser_get_help_table(&common_options);
×
UNCOV
1827
        if (r < 0)
×
1828
                return r;
1829

UNCOV
1830
        r = option_parser_get_help_table_group("Options", &options);
×
UNCOV
1831
        if (r < 0)
×
1832
                return r;
1833

1834
        (void) table_sync_column_widths(0, verbs, common_options, options);
×
1835

UNCOV
1836
        printf("%s [OPTIONS...] [VERSION]\n"
×
1837
               "\n%sUpdate OS images.%s\n"
1838
               "\n%sCommands:%s\n",
1839
               program_invocation_short_name,
1840
               ansi_highlight(), ansi_normal(),
1841
               ansi_underline(), ansi_normal());
1842

UNCOV
1843
        r = table_print_or_warn(verbs);
×
UNCOV
1844
        if (r < 0)
×
1845
                return r;
1846

UNCOV
1847
        r = table_print_or_warn(common_options);
×
UNCOV
1848
        if (r < 0)
×
1849
                return r;
1850

1851
        printf("\n%sOptions:%s\n", ansi_underline(), ansi_normal());
×
UNCOV
1852
        r = table_print_or_warn(options);
×
UNCOV
1853
        if (r < 0)
×
1854
                return r;
1855

UNCOV
1856
        printf("\nSee the %s for details.\n", link);
×
1857
        return 0;
1858
}
1859

UNCOV
1860
VERB_COMMON_HELP_HIDDEN(help);
×
1861

1862
static int parse_argv(int argc, char *argv[], char ***remaining_args) {
660✔
1863
        assert(argc >= 0);
660✔
1864
        assert(argv);
660✔
1865
        assert(remaining_args);
660✔
1866

1867
        OptionParser opts = { argc, argv };
660✔
1868
        int r;
660✔
1869

1870
        FOREACH_OPTION_OR_RETURN(c, &opts)
2,176✔
1871
                switch (c) {
856✔
1872

UNCOV
1873
                OPTION_COMMON_HELP:
×
1874
                        return help();
×
1875

UNCOV
1876
                OPTION_COMMON_VERSION:
×
UNCOV
1877
                        return version();
×
1878

1879
                OPTION_GROUP("Options"):
1880
                        break;
1881

1882
                OPTION('C', "component", "NAME",
8✔
1883
                       "Select component to update"):
1884
                        if (isempty(opts.arg)) {
8✔
UNCOV
1885
                                arg_component = mfree(arg_component);
×
UNCOV
1886
                                break;
×
1887
                        }
1888

1889
                        r = component_name_valid(opts.arg);
8✔
1890
                        if (r < 0)
8✔
1891
                                return log_error_errno(r, "Failed to determine if component name is valid: %m");
×
1892
                        if (r == 0)
8✔
UNCOV
1893
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", opts.arg);
×
1894

1895
                        r = free_and_strdup_warn(&arg_component, opts.arg);
8✔
1896
                        if (r < 0)
8✔
1897
                                return r;
1898

1899
                        break;
1900

1901
                OPTION_LONG("definitions", "DIR",
×
1902
                            "Find transfer definitions in specified directory"):
UNCOV
1903
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions);
×
UNCOV
1904
                        if (r < 0)
×
1905
                                return r;
1906
                        break;
1907

1908
                OPTION_LONG("root", "PATH",
×
1909
                            "Operate on an alternate filesystem root"):
UNCOV
1910
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
×
UNCOV
1911
                        if (r < 0)
×
1912
                                return r;
1913
                        break;
1914

1915
                OPTION_LONG("image", "PATH",
×
1916
                            "Operate on disk image as filesystem root"):
UNCOV
1917
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
UNCOV
1918
                        if (r < 0)
×
1919
                                return r;
1920
                        break;
1921

1922
                OPTION_LONG("image-policy", "POLICY",
×
1923
                            "Specify disk image dissection policy"):
UNCOV
1924
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
UNCOV
1925
                        if (r < 0)
×
1926
                                return r;
1927
                        break;
1928

1929
                OPTION_LONG("transfer-source", "PATH",
×
1930
                            "Specify the directory to transfer sources from"):
UNCOV
1931
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_transfer_source);
×
UNCOV
1932
                        if (r < 0)
×
1933
                                return r;
1934

1935
                        break;
1936

1937
                OPTION('m', "instances-max", "INT",
×
1938
                       "How many instances to maintain"):
1939
                        r = safe_atou64(opts.arg, &arg_instances_max);
×
UNCOV
1940
                        if (r < 0)
×
UNCOV
1941
                                return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", opts.arg);
×
1942

1943
                        break;
1944

1945
                OPTION_LONG("sync", "BOOL",
×
1946
                            "Controls whether to sync data to disk"):
UNCOV
1947
                        r = parse_boolean_argument("--sync=", opts.arg, &arg_sync);
×
UNCOV
1948
                        if (r < 0)
×
1949
                                return r;
1950
                        break;
1951

1952
                OPTION_LONG("verify", "BOOL",
450✔
1953
                            "Force signature verification on or off"): {
1954
                        bool b;
450✔
1955

1956
                        r = parse_boolean_argument("--verify=", opts.arg, &b);
450✔
1957
                        if (r < 0)
450✔
UNCOV
1958
                                return r;
×
1959

1960
                        arg_verify = b;
450✔
1961
                        break;
450✔
1962
                }
1963

1964
                OPTION_LONG("reboot", NULL,
×
1965
                            "Reboot after updating to newer version"):
UNCOV
1966
                        arg_reboot = true;
×
UNCOV
1967
                        break;
×
1968

1969
                OPTION_LONG("offline", NULL,
142✔
1970
                            "Do not fetch metadata from the network"):
1971
                        arg_offline = true;
142✔
1972
                        break;
142✔
1973

1974
                OPTION_COMMON_NO_PAGER:
×
UNCOV
1975
                        arg_pager_flags |= PAGER_DISABLE;
×
1976
                        break;
×
1977

1978
                OPTION_COMMON_NO_LEGEND:
×
UNCOV
1979
                        arg_legend = false;
×
UNCOV
1980
                        break;
×
1981

1982
                OPTION_COMMON_JSON:
256✔
1983
                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
256✔
1984
                        if (r <= 0)
256✔
1985
                                return r;
1986

1987
                        break;
1988
                }
1989

1990
        if (arg_image && arg_root)
660✔
UNCOV
1991
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
×
1992

1993
        if ((arg_image || arg_root) && arg_reboot)
660✔
UNCOV
1994
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
×
1995

1996
        if (arg_definitions && arg_component)
660✔
UNCOV
1997
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
×
1998

1999
        *remaining_args = option_parser_get_args(&opts);
660✔
2000
        return 1;
660✔
2001
}
2002

2003
static int run(int argc, char *argv[]) {
660✔
2004
        int r;
660✔
2005

2006
        log_setup();
660✔
2007

2008
        char **args = NULL;
660✔
2009
        r = parse_argv(argc, argv, &args);
660✔
2010
        if (r <= 0)
660✔
2011
                return r;
660✔
2012

2013
        return dispatch_verb_with_args(args, NULL);
660✔
2014
}
2015

2016
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
660✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc