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

systemd / systemd / 27993746414

22 Jun 2026 09:22PM UTC coverage: 72.966% (+0.2%) from 72.75%
27993746414

push

github

web-flow
imds: expose imds info fields also as metrics (#42409)

32 of 110 new or added lines in 4 files covered. (29.09%)

7826 existing lines in 84 files now uncovered.

340224 of 466275 relevant lines covered (72.97%)

1291684.45 hits per line

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

74.33
/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 "errno-util.h"
12
#include "fd-util.h"
13
#include "format-table.h"
14
#include "glyph-util.h"
15
#include "hashmap.h"
16
#include "help-util.h"
17
#include "hexdecoct.h"
18
#include "image-policy.h"
19
#include "loop-util.h"
20
#include "main-func.h"
21
#include "mount-util.h"
22
#include "options.h"
23
#include "os-util.h"
24
#include "pager.h"
25
#include "parse-argument.h"
26
#include "parse-util.h"
27
#include "pretty-print.h"
28
#include "sort-util.h"
29
#include "specifier.h"
30
#include "string-util.h"
31
#include "strv.h"
32
#include "sysupdate.h"
33
#include "sysupdate-cleanup.h"
34
#include "sysupdate-feature.h"
35
#include "sysupdate-instance.h"
36
#include "sysupdate-transfer.h"
37
#include "sysupdate-update-set.h"
38
#include "sysupdate-util.h"
39
#include "verbs.h"
40

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

57
STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
649✔
58
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
649✔
59
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
649✔
60
STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
649✔
61
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
649✔
62
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
649✔
63

64
const Specifier specifier_table[] = {
65
        COMMON_SYSTEM_SPECIFIERS,
66
        COMMON_TMP_SPECIFIERS,
67
        {}
68
};
69

70
Context* context_free(Context *c) {
649✔
71
        if (!c)
649✔
72
                return NULL;
73

74
        free(c->component);
649✔
75

76
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers)
5,040✔
77
                transfer_free(*tr);
4,391✔
78
        free(c->transfers);
649✔
79

80
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
1,215✔
81
                transfer_free(*tr);
566✔
82
        free(c->disabled_transfers);
649✔
83

84
        hashmap_free(c->features);
649✔
85

86
        FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
2,989✔
87
                update_set_free(*us);
2,340✔
88
        free(c->update_sets);
649✔
89

90
        hashmap_free(c->web_cache);
649✔
91

92
        safe_close(c->installdb_fd);
649✔
93

94
        return mfree(c);
649✔
95
}
96

97
static Context* context_new(void) {
649✔
98
        Context *c = new(Context, 1);
649✔
99
        if (!c)
649✔
100
                return NULL;
101

102
        *c = (Context) {
649✔
103
                .installdb_fd = -EBADF,
104
        };
105

106
        return c;
649✔
107
}
108

UNCOV
109
static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free);
×
110

111
static int read_definitions(
656✔
112
                Context *c,
113
                const char **dirs,
114
                const char *suffix,
115
                const char *node) {
116

117
        ConfFile **files = NULL;
656✔
118
        Transfer **transfers = NULL, **disabled = NULL;
656✔
119
        size_t n_files = 0, n_transfers = 0, n_disabled = 0;
656✔
120
        int r;
656✔
121

122
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
656✔
123
        CLEANUP_ARRAY(transfers, n_transfers, transfer_free_array);
656✔
124
        CLEANUP_ARRAY(disabled, n_disabled, transfer_free_array);
656✔
125

126
        assert(c);
656✔
127
        assert(dirs);
656✔
128
        assert(suffix);
656✔
129

130
        r = conf_files_list_strv_full(suffix, arg_root,
656✔
131
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
132
                                      dirs, &files, &n_files);
133
        if (r < 0)
656✔
UNCOV
134
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
×
135

136
        FOREACH_ARRAY(i, files, n_files) {
5,613✔
UNCOV
137
                _cleanup_(transfer_freep) Transfer *t = NULL;
×
138
                Transfer **appended;
4,957✔
139
                ConfFile *e = *i;
4,957✔
140

141
                t = transfer_new(c);
4,957✔
142
                if (!t)
4,957✔
UNCOV
143
                        return log_oom();
×
144

145
                r = transfer_read_definition(t, e->result, dirs, c->features);
4,957✔
146
                if (r < 0)
4,957✔
147
                        return r;
148

149
                r = transfer_resolve_paths(t, arg_root, node);
4,957✔
150
                if (r < 0)
4,957✔
151
                        return r;
152

153
                if (t->enabled)
4,957✔
154
                        appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
4,391✔
155
                else
156
                        appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
566✔
157
                if (!appended)
4,957✔
UNCOV
158
                        return log_oom();
×
159
                TAKE_PTR(t);
4,957✔
160
        }
161

162
        c->transfers = TAKE_PTR(transfers);
656✔
163
        c->n_transfers = n_transfers;
656✔
164
        c->disabled_transfers = TAKE_PTR(disabled);
656✔
165
        c->n_disabled_transfers = n_disabled;
656✔
166
        return 0;
656✔
167
}
168

169
static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) {
649✔
170
        _cleanup_strv_free_ char **dirs = NULL;
649✔
171
        int r;
649✔
172

173
        assert(c);
649✔
174

175
        if (arg_definitions)
649✔
176
                dirs = strv_new(arg_definitions);
1✔
177
        else if (c->component) {
648✔
178
                char **l = CONF_PATHS_STRV("");
22✔
179
                size_t i = 0;
22✔
180

181
                dirs = new0(char*, strv_length(l) + 1);
22✔
182
                if (!dirs)
22✔
UNCOV
183
                        return log_oom();
×
184

185
                STRV_FOREACH(dir, l) {
110✔
186
                        char *j;
88✔
187

188
                        j = strjoin(*dir, "sysupdate.", c->component, ".d");
88✔
189
                        if (!j)
88✔
UNCOV
190
                                return log_oom();
×
191

192
                        dirs[i++] = j;
88✔
193
                }
194
        } else
195
                dirs = strv_new(CONF_PATHS("sysupdate.d"));
626✔
196
        if (!dirs)
649✔
UNCOV
197
                return log_oom();
×
198

199
        ConfFile **files = NULL;
649✔
200
        size_t n_files = 0;
649✔
201

202
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
649✔
203

204
        r = conf_files_list_strv_full(".feature", arg_root,
649✔
205
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
206
                                      (const char**) dirs, &files, &n_files);
207
        if (r < 0)
649✔
UNCOV
208
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
×
209

210
        FOREACH_ARRAY(i, files, n_files) {
1,265✔
UNCOV
211
                _cleanup_(feature_unrefp) Feature *f = NULL;
×
212
                ConfFile *e = *i;
616✔
213

214
                f = feature_new();
616✔
215
                if (!f)
616✔
UNCOV
216
                        return log_oom();
×
217

218
                r = feature_read_definition(f, e->result, (const char**) dirs);
616✔
219
                if (r < 0)
616✔
220
                        return r;
221

222
                r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
616✔
223
                if (r < 0)
616✔
UNCOV
224
                        return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
×
225
                TAKE_PTR(f);
616✔
226
        }
227

228
        r = read_definitions(c, (const char**) dirs, ".transfer", node);
649✔
229
        if (r < 0)
649✔
230
                return r;
231

232
        if (c->n_transfers + c->n_disabled_transfers == 0) {
649✔
233
                /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
234
                r = read_definitions(c, (const char**) dirs, ".conf", node);
7✔
235
                if (r < 0)
7✔
236
                        return r;
237

238
                if (c->n_transfers + c->n_disabled_transfers > 0)
7✔
UNCOV
239
                        log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
×
240
        }
241

242
        if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS) &&
649✔
243
            c->n_transfers + (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS) ? 0 : c->n_disabled_transfers) == 0) {
552✔
UNCOV
244
                if (c->component)
×
UNCOV
245
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
246
                                               "No transfer definitions for component '%s' found.",
247
                                               c->component);
248

249
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
250
                                       "No transfer definitions found.");
251
        }
252

253
        return 0;
254
}
255

256
static int context_load_installed_instances(Context *c) {
649✔
257
        int r;
649✔
258

259
        assert(c);
649✔
260

261
        log_info("Discovering installed instances%s", glyph(GLYPH_ELLIPSIS));
1,298✔
262

263
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
5,040✔
264
                Transfer *t = *tr;
4,391✔
265

266
                r = resource_load_instances(
4,391✔
267
                                &t->target,
268
                                arg_verify >= 0 ? arg_verify : t->verify,
4,391✔
269
                                &c->web_cache);
270
                if (r < 0)
4,391✔
271
                        return r;
272
        }
273

274
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
1,215✔
275
                Transfer *t = *tr;
566✔
276

277
                r = resource_load_instances(
566✔
278
                                &t->target,
279
                                arg_verify >= 0 ? arg_verify : t->verify,
566✔
280
                                &c->web_cache);
281
                if (r < 0)
566✔
282
                        return r;
283
        }
284

285
        return 0;
286
}
287

288
static int context_load_available_instances(Context *c) {
414✔
289
        int r;
414✔
290

291
        assert(c);
414✔
292

293
        log_info("Discovering available instances%s", glyph(GLYPH_ELLIPSIS));
828✔
294

295
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
3,207✔
296
                Transfer *t = *tr;
2,802✔
297

298
                r = resource_load_instances(
2,802✔
299
                                &t->source,
300
                                arg_verify >= 0 ? arg_verify : t->verify,
2,802✔
301
                                &c->web_cache);
302
                if (r < 0)
2,802✔
303
                        return r;
304
        }
305

306
        return 0;
307
}
308

309
static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
916✔
310
        _cleanup_free_ char *boundary = NULL;
916✔
311
        bool newest_found = false;
916✔
312
        int r;
916✔
313

314
        assert(c);
916✔
315
        assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
916✔
316

317
        for (;;) {
4,903✔
UNCOV
318
                _cleanup_free_ Instance **cursor_instances = NULL;
×
319
                bool skip = false;
4,903✔
320
                UpdateSetFlags extra_flags = 0;
4,903✔
321
                _cleanup_free_ char *cursor = NULL;
3,987✔
322
                UpdateSet *us = NULL;
4,903✔
323

324
                /* First, let's find the newest version that's older than the boundary. */
325
                FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
36,107✔
326
                        Resource *rr;
31,609✔
327

328
                        assert(*tr);
31,609✔
329

330
                        if (flags == UPDATE_AVAILABLE)
31,609✔
331
                                rr = &(*tr)->source;
16,104✔
332
                        else {
333
                                assert(flags == UPDATE_INSTALLED);
15,505✔
334
                                rr = &(*tr)->target;
15,505✔
335
                        }
336

337
                        FOREACH_ARRAY(inst, rr->instances, rr->n_instances) {
99,291✔
338
                                Instance *i = *inst; /* Sorted newest-to-oldest */
91,689✔
339

340
                                assert(i);
91,689✔
341

342
                                if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
91,689✔
343
                                        continue; /* Not older than the boundary */
67,682✔
344

345
                                if (cursor && strverscmp(i->metadata.version, cursor) <= 0)
24,007✔
346
                                        break; /* Not newer than the cursor. The same will be true for all
347
                                                * subsequent instances (due to sorting) so let's skip to the
348
                                                * next transfer. */
349

350
                                if (free_and_strdup(&cursor, i->metadata.version) < 0)
3,987✔
UNCOV
351
                                        return log_oom();
×
352

353
                                break; /* All subsequent instances will be older than this one */
354
                        }
355

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

364
                if (!cursor) /* We didn't find anything older than the boundary, so we're done. */
4,903✔
365
                        break;
366

367
                cursor_instances = new0(Instance*, c->n_transfers);
3,987✔
368
                if (!cursor_instances)
3,987✔
UNCOV
369
                        return log_oom();
×
370

371
                /* Now let's find all the instances that match the version of the cursor, if we have them */
372
                for (size_t k = 0; k < c->n_transfers; k++) {
30,724✔
373
                        Transfer *t = c->transfers[k];
27,051✔
374
                        Instance *match = NULL;
27,051✔
375

376
                        assert(t);
27,051✔
377

378
                        if (flags == UPDATE_AVAILABLE) {
27,051✔
379
                                match = resource_find_instance(&t->source, cursor);
15,045✔
380
                                if (!match) {
15,045✔
381
                                        /* When we're looking for updates to download, we don't offer
382
                                         * incomplete versions at all. The server wants to send us an update
383
                                         * with parts of the OS missing. For robustness sake, let's not do
384
                                         * that. */
385
                                        skip = true;
386
                                        break;
387
                                }
388
                        } else {
389
                                assert(flags == UPDATE_INSTALLED);
12,006✔
390

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

400
                        cursor_instances[k] = match;
26,737✔
401

402
                        if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
26,737✔
UNCOV
403
                                extra_flags |= UPDATE_OBSOLETE;
×
404

405
                        if (strv_contains(t->protected_versions, cursor))
26,737✔
UNCOV
406
                                extra_flags |= UPDATE_PROTECTED;
×
407

408
                        /* Partial or pending updates by definition are not incomplete, they’re
409
                         * partial/pending instead. While an individual Instance cannot be both partial and
410
                         * pending, an UpdateSet as a whole can contain both partial and pending instances. */
411
                        assert(!match || !(match->is_partial && match->is_pending));
26,737✔
412

413
                        if (match && match->is_partial)
22,925✔
414
                                extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
18✔
415

416
                        if (match && match->is_pending)
22,925✔
417
                                extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
374✔
418
                }
419

420
                r = free_and_strdup_warn(&boundary, cursor);
3,987✔
421
                if (r < 0)
3,987✔
422
                        return r;
423

424
                if (skip)
3,987✔
425
                        continue;
314✔
426

427
                /* See if we already have this update set in our table */
428
                FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
10,570✔
429
                        UpdateSet *u = *update_set;
8,230✔
430

431
                        if (strverscmp_improved(u->version, cursor) != 0)
8,230✔
432
                                continue;
6,897✔
433

434
                        /* Merge in what we've learned and continue onto the next version */
435

436
                        if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
1,333✔
437
                            FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
661✔
438
                            FLAGS_SET(u->flags, UPDATE_PENDING)) {
653✔
439
                                assert(u->n_instances == c->n_transfers);
696✔
440

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

444
                                for (size_t j = 0; j < u->n_instances; j++) {
5,664✔
445
                                        if (!u->instances[j])
4,968✔
446
                                                u->instances[j] = cursor_instances[j];
2,896✔
447

448
                                        /* Make sure that the list is full if the update is AVAILABLE */
449
                                        assert(flags != UPDATE_AVAILABLE || u->instances[j]);
4,968✔
450
                                }
451
                        }
452

453
                        u->flags |= flags | extra_flags;
1,333✔
454

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

462
                        skip = true;
463
                        newest_found = true;
464
                        break;
465
                }
466

467
                if (skip)
32✔
468
                        continue;
1,333✔
469

470
                /* Doesn't exist yet, let's add it */
471
                if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
2,340✔
UNCOV
472
                        return log_oom();
×
473

474
                us = new(UpdateSet, 1);
2,340✔
475
                if (!us)
2,340✔
UNCOV
476
                        return log_oom();
×
477

478
                *us = (UpdateSet) {
2,340✔
479
                        .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
2,340✔
480
                        .version = TAKE_PTR(cursor),
2,340✔
481
                        .instances = TAKE_PTR(cursor_instances),
2,340✔
482
                        .n_instances = c->n_transfers,
2,340✔
483
                };
484

485
                c->update_sets[c->n_update_sets++] = us;
2,340✔
486

487
                newest_found = true;
2,340✔
488

489
                /* Remember which one is the newest installed */
490
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
2,340✔
491
                        c->newest_installed = us;
491✔
492

493
                /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
494
                 * It may be partial or pending. */
495
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
2,340✔
496
                        c->candidate = us;
157✔
497
        }
498

499
        /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
500
        if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) &&
916✔
501
            c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
796✔
502
                c->candidate = NULL;
24✔
503

504
        /* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */
505
        if (c->newest_installed &&
916✔
506
            (c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) &&
876✔
507
            !c->candidate)
98✔
508
                c->candidate = c->newest_installed;
98✔
509

510
        return 0;
511
}
512

513
static int context_discover_update_sets(Context *c) {
511✔
514
        int r;
511✔
515

516
        assert(c);
511✔
517

518
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
1,022✔
519

520
        r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
511✔
521
        if (r < 0)
511✔
522
                return r;
523

524
        if (!arg_offline) {
511✔
525
                log_info("Determining available update sets%s", glyph(GLYPH_ELLIPSIS));
810✔
526

527
                r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
405✔
528
                if (r < 0)
405✔
529
                        return r;
530
        }
531

532
        typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
511✔
533
        return 0;
511✔
534
}
535

UNCOV
536
static int context_show_table(Context *c) {
×
UNCOV
537
        _cleanup_(table_unrefp) Table *t = NULL;
×
UNCOV
538
        int r;
×
539

UNCOV
540
        assert(c);
×
541

UNCOV
542
        t = table_new("", "version", "installed", "available", "assessment");
×
UNCOV
543
        if (!t)
×
UNCOV
544
                return log_oom();
×
545

546
        (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
×
547
        (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
×
548
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
×
549

550
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
×
UNCOV
551
                UpdateSet *us = *update_set;
×
552
                const char *color;
×
553

554
                color = update_set_flags_to_color(us->flags);
×
555

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

UNCOV
571
        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
×
572
}
573

574
static UpdateSet* context_update_set_by_version(Context *c, const char *version) {
104✔
575
        assert(c);
104✔
576
        assert(version);
104✔
577

578
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets)
184✔
579
                if (streq((*update_set)->version, version))
184✔
580
                        return *update_set;
581

582
        return NULL;
583
}
584

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

596
        assert(c);
104✔
597
        assert(version);
104✔
598

599
        us = context_update_set_by_version(c, version);
104✔
600
        if (!us)
104✔
UNCOV
601
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
602

603
        if (arg_json_format_flags & (SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO))
104✔
604
                pager_open(arg_pager_flags);
56✔
605

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

621
        t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
104✔
622
        if (!t)
104✔
UNCOV
623
                return log_oom();
×
624

625
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
104✔
626
        (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
104✔
627
        (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
104✔
628
        (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
104✔
629
        (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
104✔
630
        (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
104✔
631
        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
104✔
632

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

638
        /* Determine if the target will make use of partition/fs attributes for any of the transfers */
639
        FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
848✔
640
                Transfer *tr = *transfer;
744✔
641

642
                if (tr->target.type == RESOURCE_PARTITION)
744✔
643
                        show_partition_columns = true;
208✔
644
                if (RESOURCE_IS_FILESYSTEM(tr->target.type))
744✔
645
                        show_fs_columns = true;
536✔
646

647
                STRV_FOREACH(changelog, tr->changelog) {
744✔
UNCOV
648
                        assert(*changelog);
×
649

UNCOV
650
                        _cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
×
UNCOV
651
                        if (!changelog_url)
×
UNCOV
652
                                return log_oom();
×
653

654
                        /* Avoid duplicates */
UNCOV
655
                        if (strv_contains(changelog_urls, changelog_url))
×
UNCOV
656
                                continue;
×
657

658
                        /* changelog_urls takes ownership of expanded changelog_url */
UNCOV
659
                        r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
×
660
                        if (r < 0)
×
661
                                return log_oom();
×
662
                }
663
        }
664

665
        FOREACH_ARRAY(inst, us->instances, us->n_instances) {
848✔
666
                Instance *i = *inst;
744✔
667

668
                if (!i) {
744✔
669
                        assert(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING));
56✔
670
                        continue;
56✔
671
                }
672

673
                r = table_add_many(t,
688✔
674
                                   TABLE_STRING, resource_type_to_string(i->resource->type),
675
                                   TABLE_PATH, i->path);
676
                if (r < 0)
688✔
UNCOV
677
                        return table_log_add_error(r);
×
678

679
                if (i->metadata.partition_uuid_set) {
688✔
680
                        have_partition_attributes = true;
144✔
681
                        r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
144✔
682
                } else
683
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
544✔
684
                if (r < 0)
688✔
UNCOV
685
                        return table_log_add_error(r);
×
686

687
                if (i->metadata.partition_flags_set) {
688✔
688
                        have_partition_attributes = true;
144✔
689
                        r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
144✔
690
                } else
691
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
544✔
692
                if (r < 0)
688✔
UNCOV
693
                        return table_log_add_error(r);
×
694

695
                if (i->metadata.mtime != USEC_INFINITY) {
688✔
696
                        have_fs_attributes = true;
528✔
697
                        r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
528✔
698
                } else
699
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
160✔
700
                if (r < 0)
688✔
UNCOV
701
                        return table_log_add_error(r);
×
702

703
                if (i->metadata.mode != MODE_INVALID) {
688✔
704
                        have_fs_attributes = true;
528✔
705
                        r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
528✔
706
                } else
707
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
160✔
708
                if (r < 0)
688✔
UNCOV
709
                        return table_log_add_error(r);
×
710

711
                if (i->metadata.size != UINT64_MAX) {
688✔
UNCOV
712
                        have_size = true;
×
UNCOV
713
                        r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
×
714
                } else
715
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
688✔
716
                if (r < 0)
688✔
UNCOV
717
                        return table_log_add_error(r);
×
718

719
                if (i->metadata.tries_done != UINT64_MAX) {
688✔
720
                        have_tries = true;
64✔
721
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
64✔
722
                } else
723
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
624✔
724
                if (r < 0)
688✔
UNCOV
725
                        return table_log_add_error(r);
×
726

727
                if (i->metadata.tries_left != UINT64_MAX) {
688✔
728
                        have_tries = true;
64✔
729
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
64✔
730
                } else
731
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
624✔
732
                if (r < 0)
688✔
UNCOV
733
                        return table_log_add_error(r);
×
734

735
                if (i->metadata.no_auto >= 0) {
688✔
UNCOV
736
                        bool b;
×
737

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

748
                        have_read_only = true;
144✔
749
                        b = i->metadata.read_only;
144✔
750
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
144✔
751
                } else
752
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
544✔
753
                if (r < 0)
688✔
754
                        return table_log_add_error(r);
×
755

756
                if (i->metadata.growfs >= 0) {
688✔
UNCOV
757
                        bool b;
×
758

UNCOV
759
                        have_growfs = true;
×
UNCOV
760
                        b = i->metadata.growfs;
×
UNCOV
761
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
762
                } else
763
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
688✔
764
                if (r < 0)
688✔
UNCOV
765
                        return table_log_add_error(r);
×
766

767
                if (i->metadata.sha256sum_set) {
688✔
768
                        _cleanup_free_ char *formatted = NULL;
16✔
769

770
                        have_sha256 = true;
16✔
771

772
                        formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
16✔
773
                        if (!formatted)
16✔
UNCOV
774
                                return log_oom();
×
775

776
                        r = table_add_cell(t, NULL, TABLE_STRING, formatted);
16✔
777
                } else
778
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
672✔
779
                if (r < 0)
688✔
UNCOV
780
                        return table_log_add_error(r);
×
781
        }
782

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

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

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

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

831
                return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
56✔
832
        } else {
833
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
48✔
834

835
                r = table_to_json(t, &t_json);
48✔
836
                if (r < 0)
48✔
837
                        return log_error_errno(r, "failed to convert table to JSON: %m");
×
838

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

853
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
48✔
854
                if (r < 0)
48✔
UNCOV
855
                        return log_error_errno(r, "Failed to print JSON: %m");
×
856

857
                return 0;
858
        }
859
}
860

861
static int context_vacuum(
109✔
862
                Context *c,
863
                uint64_t space,
864
                const char *extra_protected_version) {
865

866
        size_t disabled_count = 0;
109✔
867
        int r, count = 0;
109✔
868

869
        assert(c);
109✔
870

871
        if (space == 0)
109✔
872
                log_info("Making room%s", glyph(GLYPH_ELLIPSIS));
32✔
873
        else
874
                log_info("Making room for %" PRIu64 " updates%s", space, glyph(GLYPH_ELLIPSIS));
186✔
875

876
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
852✔
877
                Transfer *t = *tr;
743✔
878

879
                /* Don't bother clearing out space if we're not going to be downloading anything */
880
                if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version))
743✔
881
                        continue;
104✔
882

883
                r = transfer_vacuum(t, space, extra_protected_version);
639✔
884
                if (r < 0)
639✔
885
                        return r;
886

887
                count = MAX(count, r);
639✔
888
        }
889

890
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
205✔
891
                r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
96✔
892
                if (r < 0)
96✔
893
                        return r;
894
                if (r > 0)
96✔
895
                        disabled_count++;
8✔
896
        }
897

898
        if (!sd_json_format_enabled(arg_json_format_flags)) {
109✔
899
                if (count > 0 && disabled_count > 0)
83✔
UNCOV
900
                        log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
×
901
                else if (count > 0)
83✔
902
                        log_info("Removed %i instances.", count);
38✔
903
                else if (disabled_count > 0)
45✔
904
                        log_info("Removed %zu disabled transfers.", disabled_count);
8✔
905
                else
906
                        log_info("Found nothing to remove.");
37✔
907
        } else {
908
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
26✔
909

910
                r = sd_json_buildo(&json,
26✔
911
                                   SD_JSON_BUILD_PAIR_INTEGER("removed", count),
912
                                   SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
913
                if (r < 0)
26✔
UNCOV
914
                        return log_error_errno(r, "Failed to create JSON: %m");
×
915

916
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
26✔
917
                if (r < 0)
26✔
UNCOV
918
                        return log_error_errno(r, "Failed to print JSON: %m");
×
919
        }
920

921
        return 0;
922
}
923

924
int context_make_offline(
649✔
925
                Context **ret,
926
                const char *node,
927
                const char *component,
928
                ReadDefinitionsFlags read_definitions_flags) {
929
        _cleanup_(context_freep) Context* context = NULL;
649✔
930
        int r;
649✔
931

932
        assert(ret);
649✔
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();
649✔
938
        if (!context)
649✔
UNCOV
939
                return log_oom();
×
940

941
        r = free_and_strdup_warn(&context->component, component);
649✔
942
        if (r < 0)
649✔
943
                return r;
944

945
        r = context_read_definitions(context, node, read_definitions_flags);
649✔
946
        if (r < 0)
649✔
947
                return r;
948

949
        r = context_load_installed_instances(context);
649✔
950
        if (r < 0)
649✔
951
                return r;
952

953
        *ret = TAKE_PTR(context);
649✔
954
        return 0;
649✔
955
}
956

957
static int context_make_online(
520✔
958
                Context **ret,
959
                const char *node,
960
                const char *component) {
961

962
        _cleanup_(context_freep) Context* context = NULL;
520✔
963
        int r;
520✔
964

965
        assert(ret);
520✔
966

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

970
        r = context_make_offline(
520✔
971
                        &context,
972
                        node,
973
                        component,
974
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
975
        if (r < 0)
520✔
976
                return r;
977

978
        if (!arg_offline) {
520✔
979
                r = context_load_available_instances(context);
414✔
980
                if (r < 0)
414✔
981
                        return r;
982
        }
983

984
        r = context_discover_update_sets(context);
511✔
985
        if (r < 0)
511✔
986
                return r;
987

988
        *ret = TAKE_PTR(context);
511✔
989
        return 0;
511✔
990
}
991

992
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
411✔
993
        const Context *c = ASSERT_PTR(t->context);
411✔
994
        size_t i, n = c->n_transfers;
411✔
995
        uint64_t base, scaled;
411✔
996
        unsigned overall;
411✔
997

998
        for (i = 0; i < n; i++)
1,435✔
999
                if (c->transfers[i] == t)
1,435✔
1000
                        break;
1001
        assert(i < n); /* We should have found the index */
411✔
1002

1003
        base = (100 * 100 * i) / n;
411✔
1004
        scaled = (100 * percentage) / n;
411✔
1005
        overall = (unsigned) ((base + scaled) / 100);
411✔
1006
        assert(overall <= 100);
411✔
1007

1008
        log_debug("Transfer %zu/%zu is %u%% complete (%u%% overall).", i+1, n, percentage, overall);
411✔
1009
        return sd_notifyf(/* unset_environment= */ false, "X_SYSUPDATE_PROGRESS=%u\n"
822✔
1010
                                              "X_SYSUPDATE_TRANSFERS_LEFT=%zu\n"
1011
                                              "X_SYSUPDATE_TRANSFERS_DONE=%zu\n"
1012
                                              "STATUS=Updating to '%s' (%u%% complete).",
1013
                                              overall, n - i, i, inst->metadata.version, overall);
411✔
1014
}
1015

1016
static int context_process_partial_and_pending(Context *c, const char *version);
1017

1018
static int context_acquire(
141✔
1019
                Context *c,
1020
                const char *version) {
1021

1022
        UpdateSet *us = NULL;
141✔
1023
        int r;
141✔
1024

1025
        assert(c);
141✔
1026

1027
        if (version) {
141✔
UNCOV
1028
                us = context_update_set_by_version(c, version);
×
UNCOV
1029
                if (!us)
×
1030
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
141✔
1031
        } else {
1032
                if (!c->candidate) {
141✔
1033
                        log_info("No update needed.");
24✔
1034

1035
                        return 0;
1036
                }
1037

1038
                us = c->candidate;
1039
        }
1040

1041
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
117✔
1042
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
16✔
1043
        else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
101✔
1044
                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✔
1045
        } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
93✔
1046
                log_info("Selected update '%s' is already acquired and pending installation.", us->version);
16✔
1047

1048
                return context_process_partial_and_pending(c, version);
16✔
1049
        } else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
77✔
UNCOV
1050
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1051

1052
                return 0;
1053
        }
1054

1055
        if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
93✔
1056
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
×
1057
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
93✔
1058
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1059

1060
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
93✔
UNCOV
1061
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1062
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
93✔
UNCOV
1063
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1064

1065
        log_info("Selected update '%s' for install.", us->version);
93✔
1066

1067
        _cleanup_free_ InstanceMetadata *metadata = new0(InstanceMetadata, c->n_transfers);
186✔
1068
        if (!metadata)
93✔
UNCOV
1069
                return log_oom();
×
1070

1071
        /* Compute up the temporary paths before vacuuming so we don't vacuum anything if we fail to compute
1072
         * any paths because of failed validations (e.g. exceeding the gpt partition label size). */
1073
        for (size_t i = 0; i < c->n_transfers; i++) {
724✔
1074
                Instance *inst = us->instances[i];
631✔
1075
                Transfer *t = c->transfers[i];
631✔
1076

1077
                assert(inst);
631✔
1078

1079
                r = transfer_compute_temporary_paths(t, inst, metadata + i);
631✔
1080
                if (r < 0)
631✔
1081
                        return r;
1082
        }
1083

1084
        (void) sd_notifyf(/* unset_environment= */ false,
93✔
1085
                          "READY=1\n"
1086
                          "X_SYSUPDATE_VERSION=%s\n"
1087
                          "STATUS=Making room for '%s'.", us->version, us->version);
1088

1089
        /* Let's make some room. We make sure for each transfer we have one free space to fill. While
1090
         * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
1091
         * download succeeded already, in which case we shouldn't remove it just to acquire it again */
1092
        r = context_vacuum(
186✔
1093
                        c,
1094
                        /* space= */ 1,
1095
                        /* extra_protected_version= */ us->version);
93✔
1096
        if (r < 0)
93✔
1097
                return r;
1098

1099
        if (arg_sync)
93✔
1100
                sync();
93✔
1101

1102
        (void) sd_notifyf(/* unset_environment= */ false,
93✔
1103
                          "STATUS=Updating to '%s'.", us->version);
1104

1105
        /* There should now be one instance picked for each transfer, and the order is the same */
1106
        assert(us->n_instances == c->n_transfers);
93✔
1107

1108
        for (size_t i = 0; i < c->n_transfers; i++) {
676✔
1109
                Instance *inst = us->instances[i];
591✔
1110
                Transfer *t = c->transfers[i];
591✔
1111

1112
                assert(inst); /* ditto */
591✔
1113

1114
                if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */
591✔
1115
                        assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
104✔
1116
                        continue;
104✔
1117
                }
1118

1119
                r = transfer_acquire_instance(t, inst, metadata + i, context_on_acquire_progress, c);
487✔
1120
                if (r < 0)
487✔
1121
                        return r;
1122
        }
1123

1124
        if (arg_sync)
85✔
1125
                sync();
85✔
1126

1127
        return 1;
1128
}
1129

1130
/* Check to see if we have an update set acquired and pending installation. */
1131
static int context_process_partial_and_pending(
58✔
1132
                Context *c,
1133
                const char *version) {
1134

1135
        UpdateSet *us = NULL;
58✔
1136
        int r;
58✔
1137

1138
        assert(c);
58✔
1139

1140
        if (version) {
58✔
1141
                us = context_update_set_by_version(c, version);
×
UNCOV
1142
                if (!us)
×
UNCOV
1143
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1144
        } else {
1145
                if (!c->candidate) {
58✔
UNCOV
1146
                        log_info("No update needed.");
×
1147

1148
                        return 0;
1149
                }
1150

1151
                us = c->candidate;
1152
        }
1153

1154
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
58✔
UNCOV
1155
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
×
1156
        else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
58✔
UNCOV
1157
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1158

1159
                return 0;
1160
        }
1161

1162
        if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
58✔
1163
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
2✔
1164
        if (!FLAGS_SET(us->flags, UPDATE_PENDING))
56✔
UNCOV
1165
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version);
×
1166

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

1170
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
56✔
UNCOV
1171
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1172
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
56✔
UNCOV
1173
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1174

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

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

1180
        for (size_t i = 0; i < c->n_transfers; i++) {
454✔
1181
                Instance *inst = us->instances[i];
398✔
1182
                Transfer *t = c->transfers[i];
398✔
1183

1184
                assert(inst);
398✔
1185

1186
                r = transfer_process_partial_and_pending_instance(t, inst);
398✔
1187
                if (r < 0)
398✔
1188
                        return r;
1189
        }
1190

1191
        return 1;
1192
}
1193

1194
static int context_install(
85✔
1195
                Context *c,
1196
                const char *version,
1197
                UpdateSet **ret_applied) {
1198

1199
        UpdateSet *us = NULL;
85✔
1200
        int r;
85✔
1201

1202
        assert(c);
85✔
1203

1204
        if (version) {
85✔
1205
                us = context_update_set_by_version(c, version);
×
UNCOV
1206
                if (!us)
×
UNCOV
1207
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1208
        } else {
1209
                if (!c->candidate) {
85✔
UNCOV
1210
                        log_info("No update needed.");
×
1211

1212
                        return 0;
1213
                }
1214

1215
                us = c->candidate;
1216
        }
1217

1218
        (void) sd_notifyf(/* unset_environment=*/ false,
85✔
1219
                          "READY=1\n"
1220
                          "X_SYSUPDATE_VERSION=%s\n"
1221
                          "STATUS=Installing '%s'.", us->version, us->version);
1222

1223
        for (size_t i = 0; i < c->n_transfers; i++) {
745✔
1224
                Instance *inst = us->instances[i];
575✔
1225
                Transfer *t = c->transfers[i];
575✔
1226

1227
                if (inst->resource == &t->target &&
575✔
1228
                    !inst->is_pending)
460✔
1229
                        continue;
104✔
1230

1231
                r = transfer_install_instance(t, inst, arg_root);
471✔
1232
                if (r < 0)
471✔
1233
                        return r;
1234
        }
1235

1236
        log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
85✔
1237

1238
        (void) sd_notifyf(/* unset_environment= */ false,
85✔
1239
                          "STATUS=Installed '%s'.", us->version);
1240

1241
        if (ret_applied)
85✔
1242
                *ret_applied = us;
85✔
1243

1244
        return 1;
1245
}
1246

1247
static int process_image(
645✔
1248
                bool ro,
1249
                char **ret_mounted_dir,
1250
                LoopDevice **ret_loop_device) {
1251

1252
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
645✔
1253
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
645✔
1254
        int r;
645✔
1255

1256
        assert(ret_mounted_dir);
645✔
1257
        assert(ret_loop_device);
645✔
1258

1259
        if (!arg_image)
645✔
1260
                return 0;
1261

UNCOV
1262
        assert(!arg_root);
×
1263

UNCOV
1264
        r = mount_image_privately_interactively(
×
1265
                        arg_image,
1266
                        arg_image_policy,
1267
                        (ro ? DISSECT_IMAGE_READ_ONLY : 0) |
1268
                        DISSECT_IMAGE_FSCK |
1269
                        DISSECT_IMAGE_MKDIR |
1270
                        DISSECT_IMAGE_GROWFS |
1271
                        DISSECT_IMAGE_RELAX_VAR_CHECK |
1272
                        DISSECT_IMAGE_USR_NO_ROOT |
1273
                        DISSECT_IMAGE_GENERIC_ROOT |
1274
                        DISSECT_IMAGE_REQUIRE_ROOT |
1275
                        DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
1276
                        &mounted_dir,
1277
                        /* ret_dir_fd= */ NULL,
1278
                        &loop_device);
1279
        if (r < 0)
×
1280
                return r;
1281

1282
        arg_root = strdup(mounted_dir);
×
UNCOV
1283
        if (!arg_root)
×
1284
                return log_oom();
×
1285

UNCOV
1286
        *ret_mounted_dir = TAKE_PTR(mounted_dir);
×
UNCOV
1287
        *ret_loop_device = TAKE_PTR(loop_device);
×
1288

UNCOV
1289
        return 0;
×
1290
}
1291

1292
VERB(verb_list, "list", "[VERSION]", VERB_ANY, 2, VERB_DEFAULT,
1293
     "Show installed and available versions");
1294
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
128✔
1295
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
128✔
1296
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
128✔
1297
        _cleanup_(context_freep) Context* context = NULL;
128✔
1298
        _cleanup_strv_free_ char **appstream_urls = NULL;
128✔
1299
        const char *version;
128✔
1300
        int r;
128✔
1301

1302
        assert(argc <= 2);
128✔
1303
        version = argc >= 2 ? argv[1] : NULL;
128✔
1304

1305
        if (arg_component_all)
128✔
UNCOV
1306
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1307

1308
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
128✔
1309
        if (r < 0)
128✔
1310
                return r;
1311

1312
        r = context_make_online(
128✔
1313
                        &context,
1314
                        loop_device ? loop_device->node : NULL,
128✔
1315
                        arg_component);
1316
        if (r < 0)
128✔
1317
                return r;
1318

1319
        if (version)
128✔
1320
                return context_show_version(context, version);
104✔
1321
        else if (!sd_json_format_enabled(arg_json_format_flags))
24✔
UNCOV
1322
                return context_show_table(context);
×
1323
        else {
1324
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
24✔
1325
                _cleanup_strv_free_ char **versions = NULL;
24✔
1326
                const char *current = NULL;
24✔
1327
                bool current_is_pending = false;
24✔
1328

1329
                FOREACH_ARRAY(update_set, context->update_sets, context->n_update_sets) {
112✔
1330
                        UpdateSet *us = *update_set;
88✔
1331

1332
                        if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
88✔
1333
                            FLAGS_SET(us->flags, UPDATE_NEWEST)) {
80✔
1334
                                current = us->version;
24✔
1335
                                current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
24✔
1336
                        }
1337

1338
                        r = strv_extend(&versions, us->version);
88✔
1339
                        if (r < 0)
88✔
1340
                                return log_oom();
×
1341
                }
1342

1343
                FOREACH_ARRAY(tr, context->transfers, context->n_transfers)
144✔
1344
                        STRV_FOREACH(appstream_url, (*tr)->appstream) {
120✔
1345
                                /* Avoid duplicates */
UNCOV
1346
                                if (strv_contains(appstream_urls, *appstream_url))
×
1347
                                        continue;
×
1348

UNCOV
1349
                                r = strv_extend(&appstream_urls, *appstream_url);
×
UNCOV
1350
                                if (r < 0)
×
1351
                                        return log_oom();
×
1352
                        }
1353

1354
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
48✔
1355
                                          SD_JSON_BUILD_PAIR_STRV("all", versions),
1356
                                          SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
1357
                if (r < 0)
24✔
UNCOV
1358
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1359

1360
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
24✔
1361
                if (r < 0)
24✔
UNCOV
1362
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1363

1364
                return 0;
1365
        }
1366
}
1367

1368
VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, 0,
1369
     "Show optional features");
1370
static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) {
16✔
1371
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
16✔
1372
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
16✔
1373
        _cleanup_(context_freep) Context* context = NULL;
16✔
1374
        _cleanup_(table_unrefp) Table *table = NULL;
16✔
1375
        const char *feature_id;
16✔
1376
        Feature *f;
16✔
1377
        int r;
16✔
1378

1379
        assert(argc <= 2);
16✔
1380
        feature_id = argc >= 2 ? argv[1] : NULL;
16✔
1381

1382
        if (arg_component_all)
16✔
UNCOV
1383
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1384

1385
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
16✔
1386
        if (r < 0)
16✔
1387
                return r;
1388

1389
        r = context_make_offline(
16✔
1390
                        &context,
1391
                        loop_device ? loop_device->node : NULL,
16✔
1392
                        arg_component,
1393
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
1394
        if (r < 0)
16✔
1395
                return r;
1396

1397
        if (feature_id) {
16✔
1398
                _cleanup_strv_free_ char **transfers = NULL;
8✔
1399

1400
                f = hashmap_get(context->features, feature_id);
8✔
1401
                if (!f)
8✔
UNCOV
1402
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
1403
                                               "Optional feature not found: %s",
1404
                                               feature_id);
1405

1406
                table = table_new_vertical();
8✔
1407
                if (!table)
8✔
1408
                        return log_oom();
×
1409

1410
                FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
64✔
1411
                        Transfer *t = *tr;
56✔
1412

1413
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
56✔
1414
                                continue;
56✔
1415

UNCOV
1416
                        r = strv_extend(&transfers, t->id);
×
UNCOV
1417
                        if (r < 0)
×
UNCOV
1418
                                return log_oom();
×
1419
                }
1420

1421
                FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) {
16✔
1422
                        Transfer *t = *tr;
8✔
1423

1424
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
8✔
UNCOV
1425
                                continue;
×
1426

1427
                        r = strv_extend(&transfers, t->id);
8✔
1428
                        if (r < 0)
8✔
UNCOV
1429
                                return log_oom();
×
1430
                }
1431

1432
                r = table_add_many(table,
8✔
1433
                                   TABLE_FIELD, "Name",
1434
                                   TABLE_STRING, f->id,
1435
                                   TABLE_FIELD, "Enabled",
1436
                                   TABLE_BOOLEAN, f->enabled);
1437
                if (r < 0)
8✔
UNCOV
1438
                        return table_log_add_error(r);
×
1439

1440
                if (f->description) {
8✔
1441
                        r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
8✔
1442
                        if (r < 0)
8✔
1443
                                return table_log_add_error(r);
×
1444
                }
1445

1446
                if (f->documentation) {
8✔
UNCOV
1447
                        r = table_add_many(table,
×
1448
                                           TABLE_FIELD, "Documentation",
1449
                                           TABLE_STRING, f->documentation,
1450
                                           TABLE_SET_URL, f->documentation);
UNCOV
1451
                        if (r < 0)
×
UNCOV
1452
                                return table_log_add_error(r);
×
1453
                }
1454

1455
                if (f->appstream) {
8✔
UNCOV
1456
                        r = table_add_many(table,
×
1457
                                           TABLE_FIELD, "AppStream",
1458
                                           TABLE_STRING, f->appstream,
1459
                                           TABLE_SET_URL, f->appstream);
UNCOV
1460
                        if (r < 0)
×
UNCOV
1461
                                return table_log_add_error(r);
×
1462
                }
1463

1464
                if (!strv_isempty(transfers)) {
8✔
1465
                        r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
8✔
1466
                        if (r < 0)
8✔
UNCOV
1467
                                return table_log_add_error(r);
×
1468
                }
1469

1470
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
8✔
1471
        } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
8✔
1472
                table = table_new("", "feature", "description", "documentation");
8✔
1473
                if (!table)
8✔
1474
                        return log_oom();
×
1475

1476
                HASHMAP_FOREACH(f, context->features) {
16✔
1477
                        r = table_add_many(table,
8✔
1478
                                           TABLE_BOOLEAN_CHECKMARK, f->enabled,
1479
                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
1480
                                           TABLE_STRING, f->id,
1481
                                           TABLE_STRING, f->description,
1482
                                           TABLE_STRING, f->documentation,
1483
                                           TABLE_SET_URL, f->documentation);
1484
                        if (r < 0)
8✔
UNCOV
1485
                                return table_log_add_error(r);
×
1486
                }
1487

1488
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
8✔
1489
        } else {
UNCOV
1490
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
×
1491
                _cleanup_strv_free_ char **features = NULL;
×
1492

UNCOV
1493
                HASHMAP_FOREACH(f, context->features) {
×
UNCOV
1494
                        r = strv_extend(&features, f->id);
×
UNCOV
1495
                        if (r < 0)
×
UNCOV
1496
                                return log_oom();
×
1497
                }
1498

UNCOV
1499
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
×
UNCOV
1500
                if (r < 0)
×
UNCOV
1501
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1502

UNCOV
1503
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
×
UNCOV
1504
                if (r < 0)
×
UNCOV
1505
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1506
        }
1507

UNCOV
1508
        return 0;
×
1509
}
1510

1511
VERB_NOARG(verb_check_new, "check-new",
1512
           "Check if there's a new version available");
1513
static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) {
201✔
1514
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
201✔
1515
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
201✔
1516
        _cleanup_(context_freep) Context* context = NULL;
201✔
1517
        int r;
201✔
1518

1519
        assert(argc <= 1);
201✔
1520

1521
        if (arg_component_all)
201✔
UNCOV
1522
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1523

1524
        r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
201✔
1525
        if (r < 0)
201✔
1526
                return r;
1527

1528
        r = context_make_online(
201✔
1529
                        &context,
1530
                        loop_device ? loop_device->node : NULL,
201✔
1531
                        arg_component);
1532
        if (r < 0)
201✔
1533
                return r;
1534

1535
        if (!sd_json_format_enabled(arg_json_format_flags)) {
200✔
1536
                if (!context->candidate) {
184✔
1537
                        log_debug("No candidate found.");
96✔
1538
                        return EXIT_FAILURE;
1539
                }
1540

1541
                puts(context->candidate->version);
88✔
1542
        } else {
1543
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
16✔
1544

1545
                if (context->candidate)
16✔
UNCOV
1546
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("available", context->candidate->version));
×
1547
                else
1548
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_NULL("available"));
16✔
1549
                if (r < 0)
16✔
UNCOV
1550
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1551

1552
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
16✔
1553
                if (r < 0)
16✔
UNCOV
1554
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1555
        }
1556

1557
        return EXIT_SUCCESS;
1558
}
1559

1560
typedef enum {
1561
        UPDATE_ACTION_ACQUIRE = 1 << 0,
1562
        UPDATE_ACTION_INSTALL = 1 << 1,
1563
} UpdateActionFlags;
1564

1565
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
192✔
1566
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
192✔
1567
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
192✔
UNCOV
1568
        _cleanup_(context_freep) Context* context = NULL;
×
1569
        _cleanup_free_ char *booted_version = NULL;
192✔
1570
        UpdateSet *applied = NULL;
192✔
1571
        const char *version;
192✔
1572
        int r;
192✔
1573

1574
        assert(argc <= 2);
192✔
1575
        version = argc >= 2 ? argv[1] : NULL;
192✔
1576

1577
        if (arg_component_all)
192✔
1578
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
1✔
1579

1580
        if (arg_instances_max < 2)
191✔
UNCOV
1581
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1582
                                      "The --instances-max argument must be >= 2 while updating");
1583

1584
        if (arg_reboot) {
191✔
1585
                /* If automatic reboot on completion is requested, let's first determine the currently booted image */
1586

UNCOV
1587
                r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
×
UNCOV
1588
                if (r < 0)
×
1589
                        return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
1590
                if (!booted_version)
×
UNCOV
1591
                        return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
×
1592
        }
1593

1594
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
191✔
1595
        if (r < 0)
191✔
1596
                return r;
1597

1598
        r = context_make_online(
191✔
1599
                        &context,
1600
                        loop_device ? loop_device->node : NULL,
191✔
1601
                        arg_component);
1602
        if (r < 0)
191✔
1603
                return r;
1604

1605
        if (action_flags & UPDATE_ACTION_ACQUIRE)
183✔
1606
                r = context_acquire(context, version);
141✔
1607
        else
1608
                r = context_process_partial_and_pending(context, version);
42✔
1609
        if (r < 0)
183✔
1610
                return r;  /* error */
1611

1612
        if (action_flags & UPDATE_ACTION_INSTALL && r > 0)  /* update needed */
165✔
1613
                r = context_install(context, version, &applied);
85✔
1614
        if (r < 0)
165✔
1615
                return r;
1616

1617
        if (r > 0 && arg_reboot) {
165✔
UNCOV
1618
                assert(applied);
×
UNCOV
1619
                assert(booted_version);
×
1620

UNCOV
1621
                if (strverscmp_improved(applied->version, booted_version) > 0) {
×
UNCOV
1622
                        log_notice("Newly installed version is newer than booted version, rebooting.");
×
UNCOV
1623
                        return reboot_now();
×
1624
                }
1625

UNCOV
1626
                if (strverscmp_improved(applied->version, booted_version) == 0 &&
×
UNCOV
1627
                    FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) {
×
UNCOV
1628
                        log_notice("Currently booted version was incomplete and has been repaired, rebooting.");
×
UNCOV
1629
                        return reboot_now();
×
1630
                }
1631

UNCOV
1632
                log_info("Booted version is newer or identical to newly installed version, not rebooting.");
×
1633
        }
1634

1635
        return 0;
1636
}
1637

1638
VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0,
1639
     "Install new version now");
1640
static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) {
124✔
1641
        UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
124✔
1642

1643
        if (!arg_offline)
124✔
1644
                flags |= UPDATE_ACTION_ACQUIRE;
82✔
1645

1646
        return verb_update_impl(argc, argv, flags);
124✔
1647
}
1648

1649
VERB(verb_acquire, "acquire", "[VERSION]", VERB_ANY, 2, 0,
1650
     "Acquire (download) new version now");
1651
static int verb_acquire(int argc, char *argv[], uintptr_t _data, void *userdata) {
68✔
1652
        return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
68✔
1653
}
1654

1655
VERB_NOARG(verb_vacuum, "vacuum",
1656
           "Make room, by deleting old versions");
1657
static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) {
16✔
1658
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
16✔
1659
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
16✔
1660
        _cleanup_(context_freep) Context* context = NULL;
16✔
1661
        int r;
16✔
1662

1663
        assert(argc <= 1);
16✔
1664

1665
        if (arg_component_all)
16✔
UNCOV
1666
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1667

1668
        if (arg_instances_max < 1)
16✔
UNCOV
1669
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1670
                                      "The --instances-max argument must be >= 1 while vacuuming");
1671

1672
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
16✔
1673
        if (r < 0)
16✔
1674
                return r;
1675

1676
        r = context_make_offline(
16✔
1677
                        &context,
1678
                        loop_device ? loop_device->node : NULL,
16✔
1679
                        arg_component,
1680
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
1681
        if (r < 0)
16✔
1682
                return r;
1683

1684
        return context_vacuum(context, 0, NULL);
16✔
1685
}
1686

1687
VERB(verb_pending_or_reboot, "pending", NULL, 1, 1, 0,
1688
     "Report whether a newer version is installed than currently booted");
1689
VERB(verb_pending_or_reboot, "reboot", NULL, 1, 1, 0,
1690
     "Reboot if a newer version is installed than booted");
1691
static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
2✔
1692
        _cleanup_(context_freep) Context* context = NULL;
×
1693
        _cleanup_free_ char *booted_version = NULL;
2✔
1694
        int r;
2✔
1695

1696
        assert(argc == 1);
2✔
1697

1698
        if (arg_image || arg_root)
2✔
UNCOV
1699
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1700
                                       "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]);
1701

1702
        if (arg_component || arg_component_all)
2✔
1703
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
2✔
1704
                                       "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]);
1705

UNCOV
1706
        r = context_make_offline(
×
1707
                        &context,
1708
                        /* node= */ NULL,
1709
                        arg_component,
1710
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
UNCOV
1711
        if (r < 0)
×
1712
                return r;
1713

UNCOV
1714
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
×
1715

UNCOV
1716
        r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
×
UNCOV
1717
        if (r < 0)
×
1718
                return r;
UNCOV
1719
        if (!context->newest_installed)
×
UNCOV
1720
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
×
1721

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

UNCOV
1729
        r = strverscmp_improved(context->newest_installed->version, booted_version);
×
UNCOV
1730
        if (r > 0) {
×
UNCOV
1731
                log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
×
1732
                           context->newest_installed->version, booted_version,
1733
                           streq(argv[0], "pending") ? " Reboot recommended." : "");
1734

UNCOV
1735
                if (streq(argv[0], "reboot"))
×
UNCOV
1736
                        return reboot_now();
×
1737

1738
                return EXIT_SUCCESS;
UNCOV
1739
        } else if (r == 0)
×
UNCOV
1740
                log_info("Newest installed version '%s' matches booted version '%s'.",
×
1741
                         context->newest_installed->version, booted_version);
1742
        else
UNCOV
1743
                log_warning("Newest installed version '%s' is older than booted version '%s'.",
×
1744
                            context->newest_installed->version, booted_version);
1745

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

1749
        return EXIT_SUCCESS;
1750
}
1751

1752
VERB_NOARG(verb_components, "components",
1753
           "Show list of components");
1754
static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
88✔
1755
        _cleanup_(context_freep) Context* context = NULL;
88✔
1756
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
88✔
1757
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
88✔
1758
        bool has_default_component = false;
88✔
1759
        int r;
88✔
1760

1761
        assert(argc <= 1);
88✔
1762

1763
        if (arg_component_all)
88✔
UNCOV
1764
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1765

1766
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
88✔
1767
        if (r < 0)
88✔
1768
                return r;
1769

1770
        r = context_make_offline(
88✔
1771
                        &context,
1772
                        loop_device ? loop_device->node : NULL,
88✔
1773
                        arg_component,
1774
                        /* read_definitions_flags= */ 0);
1775
        if (r < 0)
88✔
1776
                return r;
1777

1778
        _cleanup_strv_free_ char **z = NULL;
88✔
1779
        r = get_component_list(arg_root, &z);
88✔
1780
        if (r < 0)
88✔
UNCOV
1781
                return log_error_errno(r, "Failed to enumerate components: %m");
×
1782

1783
        /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a
1784
         * TARGET_HOST? See target_get_argument() in sysupdated.c */
1785
        has_default_component = (!arg_definitions &&
264✔
1786
                                 !context->component &&
88✔
1787
                                 !arg_root &&
88✔
1788
                                 !arg_image &&
176✔
1789
                                 context->n_transfers > 0);
88✔
1790

1791
        if (!sd_json_format_enabled(arg_json_format_flags)) {
88✔
1792
                if (!has_default_component && strv_isempty(z)) {
×
UNCOV
1793
                        log_info("No components defined.");
×
1794
                        return 0;
1795
                }
1796

1797
                if (has_default_component)
UNCOV
1798
                        printf("%s<default>%s\n",
×
1799
                               ansi_highlight(), ansi_normal());
1800

UNCOV
1801
                STRV_FOREACH(i, z)
×
UNCOV
1802
                        puts(*i);
×
1803
        } else {
1804
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
88✔
1805

1806
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component),
88✔
1807
                                          SD_JSON_BUILD_PAIR_STRV("components", z));
1808
                if (r < 0)
88✔
UNCOV
1809
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1810

1811
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
88✔
1812
                if (r < 0)
88✔
1813
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1814
        }
1815

1816
        return 0;
1817
}
1818

1819
VERB_NOARG(verb_cleanup, "cleanup", "Clean up orphaned files");
1820
static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) {
5✔
1821
        int r;
5✔
1822

1823
        assert(argc <= 1);
5✔
1824

1825
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
5✔
1826
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
5✔
1827
        r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
5✔
1828
        if (r < 0)
5✔
1829
                return r;
1830

1831
        const char *node = loop_device ? loop_device->node : NULL;
5✔
1832

1833
        int ret = 0;
5✔
1834
        RET_GATHER(ret, installdb_cleanup_component(node, arg_component));
5✔
1835

1836
        if (arg_component_all) {
5✔
1837
                _cleanup_strv_free_ char **z = NULL;
2✔
1838
                r = installdb_list_components(&z);
2✔
1839
                if (r < 0)
2✔
1840
                        return log_error_errno(r, "Failed to enumerate components: %m");
×
1841

1842
                STRV_FOREACH(i, z)
6✔
1843
                        RET_GATHER(ret, installdb_cleanup_component(node, *i));
4✔
1844
        }
1845

1846
        return ret;
1847
}
1848

1849
static int help(void) {
×
UNCOV
1850
        _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL;
×
UNCOV
1851
        int r;
×
1852

1853
        r = verbs_get_help_table(&verbs);
×
UNCOV
1854
        if (r < 0)
×
1855
                return r;
1856

1857
        r = option_parser_get_help_table(&common_options);
×
UNCOV
1858
        if (r < 0)
×
1859
                return r;
1860

UNCOV
1861
        r = option_parser_get_help_table_group("Options", &options);
×
1862
        if (r < 0)
×
1863
                return r;
1864

UNCOV
1865
        (void) table_sync_column_widths(0, verbs, common_options, options);
×
1866

UNCOV
1867
        help_cmdline("[OPTIONS…] [VERSION]");
×
UNCOV
1868
        help_abstract("Update OS images.");
×
1869

1870
        help_section("Commands");
×
UNCOV
1871
        r = table_print_or_warn(verbs);
×
UNCOV
1872
        if (r < 0)
×
1873
                return r;
1874

UNCOV
1875
        r = table_print_or_warn(common_options);
×
UNCOV
1876
        if (r < 0)
×
1877
                return r;
1878

1879
        help_section("Options");
×
UNCOV
1880
        r = table_print_or_warn(options);
×
UNCOV
1881
        if (r < 0)
×
1882
                return r;
1883

UNCOV
1884
        help_man_page_reference("systemd-sysupdate", "8");
×
1885
        return 0;
1886
}
1887

UNCOV
1888
VERB_COMMON_HELP_HIDDEN(help);
×
1889

1890
static int parse_argv(int argc, char *argv[], char ***remaining_args) {
649✔
1891
        assert(argc >= 0);
649✔
1892
        assert(argv);
649✔
1893
        assert(remaining_args);
649✔
1894

1895
        OptionParser opts = { argc, argv };
649✔
1896
        int r;
649✔
1897

1898
        FOREACH_OPTION_OR_RETURN(c, &opts)
2,125✔
1899
                switch (c) {
827✔
1900

UNCOV
1901
                OPTION_COMMON_HELP:
×
1902
                        return help();
×
1903

UNCOV
1904
                OPTION_COMMON_VERSION:
×
UNCOV
1905
                        return version();
×
1906

1907
                OPTION_GROUP("Options"):
1908
                        break;
1909

1910
                OPTION('C', "component", "NAME",
21✔
1911
                       "Select component to update"):
1912
                        if (isempty(opts.arg)) {
21✔
UNCOV
1913
                                arg_component = mfree(arg_component);
×
UNCOV
1914
                                arg_component_all = false;
×
UNCOV
1915
                                break;
×
1916
                        }
1917

1918
                        if (!component_name_valid(opts.arg))
21✔
1919
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", opts.arg);
×
1920

1921
                        r = free_and_strdup_warn(&arg_component, opts.arg);
21✔
1922
                        if (r < 0)
21✔
1923
                                return r;
1924

1925
                        arg_component_all = false;
21✔
1926
                        break;
21✔
1927

1928
                OPTION('A', "component-all", NULL, "Process all components"):
3✔
1929

1930
                        arg_component = mfree(arg_component);
3✔
1931
                        arg_component_all = true;
3✔
1932
                        break;
3✔
1933

1934
                OPTION_LONG("definitions", "DIR",
1✔
1935
                            "Find transfer definitions in specified directory"):
1936
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions);
1✔
1937
                        if (r < 0)
1✔
1938
                                return r;
1939
                        break;
1940

1941
                OPTION_LONG("root", "PATH",
×
1942
                            "Operate on an alternate filesystem root"):
1943
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
×
1944
                        if (r < 0)
×
1945
                                return r;
1946
                        break;
1947

1948
                OPTION_LONG("image", "PATH",
×
1949
                            "Operate on disk image as filesystem root"):
1950
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
1951
                        if (r < 0)
×
1952
                                return r;
1953
                        break;
1954

1955
                OPTION_LONG("image-policy", "POLICY",
×
1956
                            "Specify disk image dissection policy"):
1957
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
1958
                        if (r < 0)
×
1959
                                return r;
1960
                        break;
1961

UNCOV
1962
                OPTION_LONG("transfer-source", "PATH",
×
1963
                            "Specify the directory to transfer sources from"):
UNCOV
1964
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_transfer_source);
×
1965
                        if (r < 0)
×
1966
                                return r;
1967

1968
                        break;
1969

UNCOV
1970
                OPTION('m', "instances-max", "INT",
×
1971
                       "How many instances to maintain"):
UNCOV
1972
                        r = safe_atou64(opts.arg, &arg_instances_max);
×
1973
                        if (r < 0)
×
1974
                                return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", opts.arg);
×
1975

1976
                        break;
1977

UNCOV
1978
                OPTION_LONG("sync", "BOOL",
×
1979
                            "Controls whether to sync data to disk"):
UNCOV
1980
                        r = parse_boolean_argument("--sync=", opts.arg, &arg_sync);
×
UNCOV
1981
                        if (r < 0)
×
1982
                                return r;
1983
                        break;
1984

1985
                OPTION_LONG("verify", "BOOL",
457✔
1986
                            "Force signature verification on or off"): {
1987
                        bool b;
457✔
1988

1989
                        r = parse_boolean_argument("--verify=", opts.arg, &b);
457✔
1990
                        if (r < 0)
457✔
UNCOV
1991
                                return r;
×
1992

1993
                        arg_verify = b;
457✔
1994
                        break;
457✔
1995
                }
1996

1997
                OPTION_LONG("reboot", NULL,
1✔
1998
                            "Reboot after updating to newer version"):
1999
                        arg_reboot = true;
1✔
2000
                        break;
1✔
2001

2002
                OPTION_LONG("offline", NULL,
114✔
2003
                            "Do not fetch metadata from the network"):
2004
                        arg_offline = true;
114✔
2005
                        break;
114✔
2006

UNCOV
2007
                OPTION_COMMON_NO_PAGER:
×
UNCOV
2008
                        arg_pager_flags |= PAGER_DISABLE;
×
UNCOV
2009
                        break;
×
2010

UNCOV
2011
                OPTION_COMMON_NO_LEGEND:
×
UNCOV
2012
                        arg_legend = false;
×
UNCOV
2013
                        break;
×
2014

2015
                OPTION_COMMON_JSON:
230✔
2016
                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
230✔
2017
                        if (r <= 0)
230✔
2018
                                return r;
2019

2020
                        break;
2021
                }
2022

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

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

2029
        if (arg_reboot && arg_component)
649✔
2030
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=, as automatic reboots only apply to the booted OS version.");
1✔
2031

2032
        if (arg_definitions && arg_component)
648✔
UNCOV
2033
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
×
2034

2035
        *remaining_args = option_parser_get_args(&opts);
648✔
2036
        return 1;
648✔
2037
}
2038

2039
static int run(int argc, char *argv[]) {
649✔
2040
        int r;
649✔
2041

2042
        log_setup();
649✔
2043

2044
        char **args = NULL;
649✔
2045
        r = parse_argv(argc, argv, &args);
649✔
2046
        if (r <= 0)
649✔
2047
                return r;
649✔
2048

2049
        return dispatch_verb(args, NULL);
648✔
2050
}
2051

2052
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
649✔
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