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

systemd / systemd / 28272947092

26 Jun 2026 08:38PM UTC coverage: 72.893% (+0.2%) from 72.703%
28272947092

push

github

poettering
sysupdate: Address review feedback on CheckNew varlink scaffolding

Follow-up to #42422:

 - Rename process_image() to context_process_image(), since it now
   operates on a Context object.
 - Use IN_SET() in image_type_can_sysupdate() instead of a switch.
 - Name the return parameters of context_list_components() ret_xyz, per
   our coding style.
 - Drop a redundant "else" after a return in vl_method_check_new().

9 of 11 new or added lines in 1 file covered. (81.82%)

12567 existing lines in 144 files now uncovered.

341026 of 467845 relevant lines covered (72.89%)

1339355.33 hits per line

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

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

3
#include <unistd.h>
4

5
#include "sd-bus.h"
6
#include "sd-daemon.h"
7
#include "sd-json.h"
8
#include "sd-varlink.h"
9

10
#include "build.h"
11
#include "bus-polkit.h"
12
#include "conf-files.h"
13
#include "constants.h"
14
#include "discover-image.h"
15
#include "dissect-image.h"
16
#include "env-util.h"
17
#include "errno-util.h"
18
#include "fd-util.h"
19
#include "format-table.h"
20
#include "glyph-util.h"
21
#include "hashmap.h"
22
#include "help-util.h"
23
#include "hexdecoct.h"
24
#include "image-policy.h"
25
#include "json-util.h"
26
#include "loop-util.h"
27
#include "main-func.h"
28
#include "mount-util.h"
29
#include "options.h"
30
#include "os-util.h"
31
#include "pager.h"
32
#include "parse-argument.h"
33
#include "parse-util.h"
34
#include "pretty-print.h"
35
#include "runtime-scope.h"
36
#include "sort-util.h"
37
#include "specifier.h"
38
#include "string-util.h"
39
#include "strv.h"
40
#include "sysupdate.h"
41
#include "sysupdate-cleanup.h"
42
#include "sysupdate-feature.h"
43
#include "sysupdate-instance.h"
44
#include "sysupdate-target.h"
45
#include "sysupdate-transfer.h"
46
#include "sysupdate-update-set.h"
47
#include "sysupdate-util.h"
48
#include "varlink-io.systemd.SysUpdate.h"
49
#include "varlink-util.h"
50
#include "verbs.h"
51

52
static char *arg_definitions = NULL;
53
static bool arg_sync = true;
54
static uint64_t arg_instances_max = UINT64_MAX;
55
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
56
static PagerFlags arg_pager_flags = 0;
57
static bool arg_legend = true;
58
static char *arg_root = NULL;
59
static char *arg_image = NULL;
60
static bool arg_reboot = false;
61
static int arg_cleanup = -1;
62
static char *arg_component = NULL;
63
static bool arg_component_all = false;
64
static int arg_verify = -1;
65
static ImagePolicy *arg_image_policy = NULL;
66
static bool arg_offline = false;
67
static char *arg_transfer_source = NULL;
68
static bool arg_varlink = false;
69

70
STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
1,324✔
71
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
1,324✔
72
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
1,324✔
73
STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
1,324✔
74
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
1,324✔
75
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
1,324✔
76

77
const Specifier specifier_table[] = {
78
        COMMON_SYSTEM_SPECIFIERS,
79
        COMMON_TMP_SPECIFIERS,
80
        {}
81
};
82

83
#define CONTEXT_NULL \
84
        (Context) { \
85
                .sync = true, \
86
                .instances_max = UINT64_MAX, \
87
                .verify = -1, \
88
                .cleanup = -1, \
89
                .installdb_fd = -EBADF, \
90
                .target_identifier.class = _TARGET_CLASS_INVALID, \
91
        }
92

93
void context_done(Context *c) {
2,414✔
94
        assert(c);
2,414✔
95

96
        c->mounted_dir = umount_and_rmdir_and_free(c->mounted_dir);
2,414✔
97
        c->loop_device = loop_device_unref(c->loop_device);
2,414✔
98

99
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers)
11,190✔
100
                transfer_free(*tr);
8,776✔
101
        c->transfers = mfree(c->transfers);
2,414✔
102
        c->n_transfers = 0;
2,414✔
103

104
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
3,546✔
105
                transfer_free(*tr);
1,132✔
106
        c->disabled_transfers = mfree(c->disabled_transfers);
2,414✔
107
        c->n_disabled_transfers = 0;
2,414✔
108

109
        c->features = hashmap_free(c->features);
2,414✔
110

111
        FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
7,093✔
112
                update_set_free(*us);
4,679✔
113
        c->update_sets = mfree(c->update_sets);
2,414✔
114
        c->n_update_sets = 0;
2,414✔
115

116
        c->web_cache = hashmap_free(c->web_cache);
2,414✔
117

118
        c->installdb_fd = safe_close(c->installdb_fd);
2,414✔
119

120
        c->definitions = mfree(c->definitions);
2,414✔
121
        c->root = mfree(c->root);
2,414✔
122
        c->image = mfree(c->image);
2,414✔
123
        c->component = mfree(c->component);
2,414✔
124
        c->image_policy = image_policy_free(c->image_policy);
2,414✔
125
        c->transfer_source = mfree(c->transfer_source);
2,414✔
126

127
        target_identifier_done(&c->target_identifier);
2,414✔
128
}
2,414✔
129

130
static int context_from_cmdline(Context *ret) {
1,107✔
131
        assert(ret);
1,107✔
132

133
        _cleanup_(context_done) Context context = CONTEXT_NULL;
1,107✔
134

135
        context.instances_max = arg_instances_max;
1,107✔
136
        context.sync = arg_sync;
1,107✔
137
        context.reboot = arg_reboot;
1,107✔
138
        context.verify = arg_verify;
1,107✔
139
        context.offline = arg_offline;
1,107✔
140
        context.cleanup = arg_cleanup;
1,107✔
141
        context.component_all = arg_component_all;
1,107✔
142

143
        if (strdup_to(&context.definitions, arg_definitions) < 0)
1,107✔
UNCOV
144
                return log_oom();
×
145

146
        if (strdup_to(&context.root, arg_root) < 0)
1,107✔
UNCOV
147
                return log_oom();
×
148

149
        if (strdup_to(&context.image, arg_image) < 0)
1,107✔
UNCOV
150
                return log_oom();
×
151

152
        if (strdup_to(&context.component, arg_component) < 0)
1,107✔
UNCOV
153
                return log_oom();
×
154

155
        if (strdup_to(&context.transfer_source, arg_transfer_source) < 0)
1,107✔
UNCOV
156
                return log_oom();
×
157

158
        if (arg_image_policy) {
1,107✔
UNCOV
159
                context.image_policy = image_policy_copy(arg_image_policy);
×
UNCOV
160
                if (!context.image_policy)
×
UNCOV
161
                        return log_oom();
×
162
        }
163

164
        *ret = TAKE_GENERIC(context, Context, CONTEXT_NULL);
1,107✔
165
        return 0;
1,107✔
166
}
167

168
/* Stores any long-running server state which needs to persist between varlink calls, such as state for
169
 * pending polkit requests */
170
typedef struct Server {
171
        sd_bus *system_bus;
172
        Hashmap *polkit_registry;
173
} Server;
174

175
#define SERVER_NULL \
176
        (Server) { \
177
                /* all fields fine with being initialised to NULL */ \
178
        }
179

180
static void server_done(Server *s) {
212✔
181
        assert(s);
212✔
182

183
        s->polkit_registry = hashmap_free(s->polkit_registry);
212✔
184
        s->system_bus = sd_bus_flush_close_unref(s->system_bus);
212✔
185
}
212✔
186

UNCOV
187
static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free);
×
188

189
static int read_definitions(
1,294✔
190
                Context *c,
191
                const char **dirs,
192
                const char *suffix,
193
                const char *node) {
194

195
        ConfFile **files = NULL;
1,294✔
196
        Transfer **transfers = NULL, **disabled = NULL;
1,294✔
197
        size_t n_files = 0, n_transfers = 0, n_disabled = 0;
1,294✔
198
        int r;
1,294✔
199

200
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
1,294✔
201
        CLEANUP_ARRAY(transfers, n_transfers, transfer_free_array);
1,294✔
202
        CLEANUP_ARRAY(disabled, n_disabled, transfer_free_array);
1,294✔
203

204
        assert(c);
1,294✔
205
        assert(dirs);
1,294✔
206
        assert(suffix);
1,294✔
207

208
        r = conf_files_list_strv_full(suffix, c->root,
1,294✔
209
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
210
                                      dirs, &files, &n_files);
211
        if (r < 0)
1,294✔
UNCOV
212
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
×
213

214
        FOREACH_ARRAY(i, files, n_files) {
11,202✔
UNCOV
215
                _cleanup_(transfer_freep) Transfer *t = NULL;
×
216
                Transfer **appended;
9,908✔
217
                ConfFile *e = *i;
9,908✔
218

219
                t = transfer_new(c);
9,908✔
220
                if (!t)
9,908✔
UNCOV
221
                        return log_oom();
×
222

223
                r = transfer_read_definition(t, e->result, dirs, c->features);
9,908✔
224
                if (r < 0)
9,908✔
225
                        return r;
226

227
                r = transfer_resolve_paths(t, c->root, node);
9,908✔
228
                if (r < 0)
9,908✔
229
                        return r;
230

231
                if (t->enabled)
9,908✔
232
                        appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
8,776✔
233
                else
234
                        appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
1,132✔
235
                if (!appended)
9,908✔
UNCOV
236
                        return log_oom();
×
237
                TAKE_PTR(t);
9,908✔
238
        }
239

240
        c->transfers = TAKE_PTR(transfers);
1,294✔
241
        c->n_transfers = n_transfers;
1,294✔
242
        c->disabled_transfers = TAKE_PTR(disabled);
1,294✔
243
        c->n_disabled_transfers = n_disabled;
1,294✔
244
        return 0;
1,294✔
245
}
246

247
typedef enum ReadDefinitionsFlags {
248
        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0,
249
        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS     = 1 << 1,
250
} ReadDefinitionsFlags;
251

252
static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) {
1,287✔
253
        _cleanup_strv_free_ char **dirs = NULL;
1,287✔
254
        int r;
1,287✔
255

256
        assert(c);
1,287✔
257

258
        if (c->definitions)
1,287✔
259
                dirs = strv_new(c->definitions);
1✔
260
        else if (c->component) {
1,286✔
261
                char **l = CONF_PATHS_STRV("");
38✔
262
                size_t i = 0;
38✔
263

264
                dirs = new0(char*, strv_length(l) + 1);
38✔
265
                if (!dirs)
38✔
UNCOV
266
                        return log_oom();
×
267

268
                STRV_FOREACH(dir, l) {
190✔
269
                        char *j;
152✔
270

271
                        j = strjoin(*dir, "sysupdate.", c->component, ".d");
152✔
272
                        if (!j)
152✔
UNCOV
273
                                return log_oom();
×
274

275
                        dirs[i++] = j;
152✔
276
                }
277
        } else
278
                dirs = strv_new(CONF_PATHS("sysupdate.d"));
1,248✔
279
        if (!dirs)
1,287✔
UNCOV
280
                return log_oom();
×
281

282
        ConfFile **files = NULL;
1,287✔
283
        size_t n_files = 0;
1,287✔
284

285
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
1,287✔
286

287
        r = conf_files_list_strv_full(".feature", c->root,
1,287✔
288
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
289
                                      (const char**) dirs, &files, &n_files);
290
        if (r < 0)
1,287✔
UNCOV
291
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
×
292

293
        FOREACH_ARRAY(i, files, n_files) {
2,519✔
UNCOV
294
                _cleanup_(feature_unrefp) Feature *f = NULL;
×
295
                ConfFile *e = *i;
1,232✔
296

297
                f = feature_new();
1,232✔
298
                if (!f)
1,232✔
UNCOV
299
                        return log_oom();
×
300

301
                r = feature_read_definition(f, c->root, e->result, (const char**) dirs);
1,232✔
302
                if (r < 0)
1,232✔
303
                        return r;
304

305
                r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
1,232✔
306
                if (r < 0)
1,232✔
UNCOV
307
                        return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
×
308
                TAKE_PTR(f);
1,232✔
309
        }
310

311
        r = read_definitions(c, (const char**) dirs, ".transfer", node);
1,287✔
312
        if (r < 0)
1,287✔
313
                return r;
314

315
        if (c->n_transfers + c->n_disabled_transfers == 0) {
1,287✔
316
                /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
317
                r = read_definitions(c, (const char**) dirs, ".conf", node);
7✔
318
                if (r < 0)
7✔
319
                        return r;
320

321
                if (c->n_transfers + c->n_disabled_transfers > 0)
7✔
UNCOV
322
                        log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
×
323
        }
324

325
        if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS) &&
1,287✔
326
            c->n_transfers + (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS) ? 0 : c->n_disabled_transfers) == 0) {
1,103✔
UNCOV
327
                if (c->component)
×
UNCOV
328
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
329
                                               "No transfer definitions for component '%s' found.",
330
                                               c->component);
331

UNCOV
332
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
333
                                       "No transfer definitions found.");
334
        }
335

336
        return 0;
337
}
338

339
static int context_load_installed_instances(Context *c) {
1,287✔
340
        int r;
1,287✔
341

342
        assert(c);
1,287✔
343

344
        log_info("Discovering installed instances%s", glyph(GLYPH_ELLIPSIS));
2,574✔
345

346
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
10,063✔
347
                Transfer *t = *tr;
8,776✔
348

349
                r = resource_load_instances(
8,776✔
350
                                &t->target,
351
                                c->verify >= 0 ? c->verify : t->verify,
8,776✔
352
                                &c->web_cache);
353
                if (r < 0)
8,776✔
354
                        return r;
355
        }
356

357
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
2,419✔
358
                Transfer *t = *tr;
1,132✔
359

360
                r = resource_load_instances(
1,132✔
361
                                &t->target,
362
                                c->verify >= 0 ? c->verify : t->verify,
1,132✔
363
                                &c->web_cache);
364
                if (r < 0)
1,132✔
365
                        return r;
366
        }
367

368
        return 0;
369
}
370

371
static int context_load_available_instances(Context *c) {
827✔
372
        int r;
827✔
373

374
        assert(c);
827✔
375

376
        log_info("Discovering available instances%s", glyph(GLYPH_ELLIPSIS));
1,654✔
377

378
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
6,412✔
379
                Transfer *t = *tr;
5,602✔
380

381
                r = resource_load_instances(
5,602✔
382
                                &t->source,
383
                                c->verify >= 0 ? c->verify : t->verify,
5,602✔
384
                                &c->web_cache);
385
                if (r < 0)
5,602✔
386
                        return r;
387
        }
388

389
        return 0;
390
}
391

392
static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
1,832✔
393
        _cleanup_free_ char *boundary = NULL;
1,832✔
394
        bool newest_found = false;
1,832✔
395
        int r;
1,832✔
396

397
        assert(c);
1,832✔
398
        assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
1,832✔
399

400
        for (;;) {
9,806✔
UNCOV
401
                _cleanup_free_ Instance **cursor_instances = NULL;
×
402
                bool skip = false;
9,806✔
403
                UpdateSetFlags extra_flags = 0;
9,806✔
404
                _cleanup_free_ char *cursor = NULL;
7,974✔
405
                UpdateSet *us = NULL;
9,806✔
406

407
                /* First, let's find the newest version that's older than the boundary. */
408
                FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
72,210✔
409
                        Resource *rr;
63,214✔
410

411
                        assert(*tr);
63,214✔
412

413
                        if (flags == UPDATE_AVAILABLE)
63,214✔
414
                                rr = &(*tr)->source;
32,205✔
415
                        else {
416
                                assert(flags == UPDATE_INSTALLED);
31,009✔
417
                                rr = &(*tr)->target;
31,009✔
418
                        }
419

420
                        FOREACH_ARRAY(inst, rr->instances, rr->n_instances) {
198,576✔
421
                                Instance *i = *inst; /* Sorted newest-to-oldest */
183,373✔
422

423
                                assert(i);
183,373✔
424

425
                                if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
183,373✔
426
                                        continue; /* Not older than the boundary */
135,362✔
427

428
                                if (cursor && strverscmp(i->metadata.version, cursor) <= 0)
48,011✔
429
                                        break; /* Not newer than the cursor. The same will be true for all
430
                                                * subsequent instances (due to sorting) so let's skip to the
431
                                                * next transfer. */
432

433
                                if (free_and_strdup(&cursor, i->metadata.version) < 0)
7,974✔
UNCOV
434
                                        return log_oom();
×
435

436
                                break; /* All subsequent instances will be older than this one */
437
                        }
438

439
                        if (flags == UPDATE_AVAILABLE && !cursor)
63,214✔
440
                                break; /* This transfer didn't have a version older than the boundary,
441
                                        * so any older-than-boundary version that might exist in a different
442
                                        * transfer must always be incomplete. For reasons described below,
443
                                        * we don't include incomplete versions for AVAILABLE updates. So we
444
                                        * are completely done looking. */
445
                }
446

447
                if (!cursor) /* We didn't find anything older than the boundary, so we're done. */
9,806✔
448
                        break;
449

450
                cursor_instances = new0(Instance*, c->n_transfers);
7,974✔
451
                if (!cursor_instances)
7,974✔
UNCOV
452
                        return log_oom();
×
453

454
                /* Now let's find all the instances that match the version of the cursor, if we have them */
455
                for (size_t k = 0; k < c->n_transfers; k++) {
61,445✔
456
                        Transfer *t = c->transfers[k];
54,099✔
457
                        Instance *match = NULL;
54,099✔
458

459
                        assert(t);
54,099✔
460

461
                        if (flags == UPDATE_AVAILABLE) {
54,099✔
462
                                match = resource_find_instance(&t->source, cursor);
30,087✔
463
                                if (!match) {
30,087✔
464
                                        /* When we're looking for updates to download, we don't offer
465
                                         * incomplete versions at all. The server wants to send us an update
466
                                         * with parts of the OS missing. For robustness sake, let's not do
467
                                         * that. */
468
                                        skip = true;
469
                                        break;
470
                                }
471
                        } else {
472
                                assert(flags == UPDATE_INSTALLED);
24,012✔
473

474
                                match = resource_find_instance(&t->target, cursor);
24,012✔
475
                                if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING)))
24,012✔
476
                                        /* When we're looking for installed versions, let's be robust and treat
477
                                         * an incomplete installation as an installation. Otherwise, there are
478
                                         * situations that can lead to sysupdate wiping the currently booted OS.
479
                                         * See https://github.com/systemd/systemd/issues/33339 */
480
                                        extra_flags |= UPDATE_INCOMPLETE;
7,444✔
481
                        }
482

483
                        cursor_instances[k] = match;
53,471✔
484

485
                        if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
53,471✔
UNCOV
486
                                extra_flags |= UPDATE_OBSOLETE;
×
487

488
                        if (strv_contains(t->protected_versions, cursor))
53,471✔
UNCOV
489
                                extra_flags |= UPDATE_PROTECTED;
×
490

491
                        /* Partial or pending updates by definition are not incomplete, they’re
492
                         * partial/pending instead. While an individual Instance cannot be both partial and
493
                         * pending, an UpdateSet as a whole can contain both partial and pending instances. */
494
                        assert(!match || !(match->is_partial && match->is_pending));
53,471✔
495

496
                        if (match && match->is_partial)
45,847✔
497
                                extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
36✔
498

499
                        if (match && match->is_pending)
45,847✔
500
                                extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
748✔
501
                }
502

503
                r = free_and_strdup_warn(&boundary, cursor);
7,974✔
504
                if (r < 0)
7,974✔
505
                        return r;
506

507
                if (skip)
7,974✔
508
                        continue;
628✔
509

510
                /* See if we already have this update set in our table */
511
                FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
21,140✔
512
                        UpdateSet *u = *update_set;
16,461✔
513

514
                        if (strverscmp_improved(u->version, cursor) != 0)
16,461✔
515
                                continue;
13,794✔
516

517
                        /* Merge in what we've learned and continue onto the next version */
518

519
                        if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
2,667✔
520
                            FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
1,323✔
521
                            FLAGS_SET(u->flags, UPDATE_PENDING)) {
1,307✔
522
                                assert(u->n_instances == c->n_transfers);
1,392✔
523

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

527
                                for (size_t j = 0; j < u->n_instances; j++) {
11,328✔
528
                                        if (!u->instances[j])
9,936✔
529
                                                u->instances[j] = cursor_instances[j];
5,792✔
530

531
                                        /* Make sure that the list is full if the update is AVAILABLE */
532
                                        assert(flags != UPDATE_AVAILABLE || u->instances[j]);
9,936✔
533
                                }
534
                        }
535

536
                        u->flags |= flags | extra_flags;
2,667✔
537

538
                        /* If this is the newest installed version, that is incomplete and just became marked
539
                         * as available, and if there is no other candidate available, we promote this to be
540
                         * the candidate. Ignore partial or pending status on the update set. */
541
                        if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) &&
2,667✔
542
                            !c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE))
64✔
543
                                c->candidate = u;
64✔
544

545
                        skip = true;
546
                        newest_found = true;
547
                        break;
548
                }
549

550
                if (skip)
64✔
551
                        continue;
2,667✔
552

553
                /* Doesn't exist yet, let's add it */
554
                if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
4,679✔
UNCOV
555
                        return log_oom();
×
556

557
                us = new(UpdateSet, 1);
4,679✔
558
                if (!us)
4,679✔
UNCOV
559
                        return log_oom();
×
560

561
                *us = (UpdateSet) {
4,679✔
562
                        .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
4,679✔
563
                        .version = TAKE_PTR(cursor),
4,679✔
564
                        .instances = TAKE_PTR(cursor_instances),
4,679✔
565
                        .n_instances = c->n_transfers,
4,679✔
566
                };
567

568
                c->update_sets[c->n_update_sets++] = us;
4,679✔
569

570
                newest_found = true;
4,679✔
571

572
                /* Remember which one is the newest installed */
573
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
4,679✔
574
                        c->newest_installed = us;
983✔
575

576
                /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
577
                 * It may be partial or pending. */
578
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
4,679✔
579
                        c->candidate = us;
312✔
580
        }
581

582
        /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
583
        if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) &&
1,832✔
584
            c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
1,594✔
585
                c->candidate = NULL;
48✔
586

587
        /* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */
588
        if (c->newest_installed &&
1,832✔
589
            (c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) &&
1,754✔
590
            !c->candidate)
196✔
591
                c->candidate = c->newest_installed;
196✔
592

593
        return 0;
594
}
595

596
static int context_discover_update_sets(Context *c) {
1,022✔
597
        int r;
1,022✔
598

599
        assert(c);
1,022✔
600

601
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
2,044✔
602

603
        r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
1,022✔
604
        if (r < 0)
1,022✔
605
                return r;
606

607
        if (!c->offline) {
1,022✔
608
                log_info("Determining available update sets%s", glyph(GLYPH_ELLIPSIS));
1,620✔
609

610
                r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
810✔
611
                if (r < 0)
810✔
612
                        return r;
613
        }
614

615
        typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
1,022✔
616
        return 0;
1,022✔
617
}
618

UNCOV
619
static int context_show_table(Context *c) {
×
UNCOV
620
        _cleanup_(table_unrefp) Table *t = NULL;
×
UNCOV
621
        int r;
×
622

623
        assert(c);
×
624

UNCOV
625
        t = table_new("", "version", "installed", "available", "assessment");
×
UNCOV
626
        if (!t)
×
UNCOV
627
                return log_oom();
×
628

UNCOV
629
        (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
×
UNCOV
630
        (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
×
UNCOV
631
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
×
632

UNCOV
633
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
×
UNCOV
634
                UpdateSet *us = *update_set;
×
UNCOV
635
                const char *color;
×
636

UNCOV
637
                color = update_set_flags_to_color(us->flags);
×
638

UNCOV
639
                r = table_add_many(t,
×
640
                                   TABLE_STRING,    update_set_flags_to_glyph(us->flags),
641
                                   TABLE_SET_COLOR, color,
642
                                   TABLE_STRING,    us->version,
643
                                   TABLE_SET_COLOR, color,
644
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
645
                                   TABLE_SET_COLOR, color,
646
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
647
                                   TABLE_SET_COLOR, color,
648
                                   TABLE_STRING,    update_set_flags_to_string(us->flags),
649
                                   TABLE_SET_COLOR, color);
650
                if (r < 0)
×
651
                        return table_log_add_error(r);
×
652
        }
653

UNCOV
654
        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
×
655
}
656

657
static UpdateSet* context_update_set_by_version(Context *c, const char *version) {
208✔
658
        assert(c);
208✔
659
        assert(version);
208✔
660

661
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets)
368✔
662
                if (streq((*update_set)->version, version))
368✔
663
                        return *update_set;
664

665
        return NULL;
666
}
667

668
static int context_show_version(Context *c, const char *version) {
208✔
669
        bool show_fs_columns = false, show_partition_columns = false,
208✔
670
                have_fs_attributes = false, have_partition_attributes = false,
208✔
671
                have_size = false, have_tries = false, have_no_auto = false,
208✔
672
                have_read_only = false, have_growfs = false, have_sha256 = false;
208✔
673
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
208✔
674
        _cleanup_(table_unrefp) Table *t = NULL;
208✔
675
        _cleanup_strv_free_ char **changelog_urls = NULL;
208✔
676
        UpdateSet *us;
208✔
677
        int r;
208✔
678

679
        assert(c);
208✔
680
        assert(version);
208✔
681

682
        us = context_update_set_by_version(c, version);
208✔
683
        if (!us)
208✔
UNCOV
684
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
685

686
        if (arg_json_format_flags & (SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO))
208✔
687
                pager_open(arg_pager_flags);
112✔
688

689
        if (!sd_json_format_enabled(arg_json_format_flags))
208✔
690
                printf("%s%s%s Version: %s\n"
896✔
691
                       "    State: %s%s%s\n"
692
                       "Installed: %s%s%s%s\n"
693
                       "Available: %s%s\n"
694
                       "Protected: %s%s%s\n"
695
                       " Obsolete: %s%s%s\n\n",
696
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
112✔
697
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
112✔
698
                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
128✔
699
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
208✔
700
                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
208✔
701
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
112✔
702
                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
112✔
703

704
        t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
208✔
705
        if (!t)
208✔
UNCOV
706
                return log_oom();
×
707

708
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
208✔
709
        (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
208✔
710
        (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
208✔
711
        (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
208✔
712
        (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
208✔
713
        (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
208✔
714
        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
208✔
715

716
        /* Starting in v257, these fields would be automatically formatted with underscores. This would have
717
         * been a breaking change, so to avoid that let's hard-code their original names. */
718
        (void) table_set_json_field_name(t, 7, "tries-done");
208✔
719
        (void) table_set_json_field_name(t, 8, "tries-left");
208✔
720

721
        /* Determine if the target will make use of partition/fs attributes for any of the transfers */
722
        FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
1,696✔
723
                Transfer *tr = *transfer;
1,488✔
724

725
                if (tr->target.type == RESOURCE_PARTITION)
1,488✔
726
                        show_partition_columns = true;
416✔
727
                if (RESOURCE_IS_FILESYSTEM(tr->target.type))
1,488✔
728
                        show_fs_columns = true;
1,072✔
729

730
                STRV_FOREACH(changelog, tr->changelog) {
1,488✔
UNCOV
731
                        assert(*changelog);
×
732

733
                        _cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
×
UNCOV
734
                        if (!changelog_url)
×
UNCOV
735
                                return log_oom();
×
736

737
                        /* Avoid duplicates */
738
                        if (strv_contains(changelog_urls, changelog_url))
×
739
                                continue;
×
740

741
                        /* changelog_urls takes ownership of expanded changelog_url */
UNCOV
742
                        r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
×
UNCOV
743
                        if (r < 0)
×
744
                                return log_oom();
×
745
                }
746
        }
747

748
        FOREACH_ARRAY(inst, us->instances, us->n_instances) {
1,696✔
749
                Instance *i = *inst;
1,488✔
750

751
                if (!i) {
1,488✔
752
                        assert(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING));
112✔
753
                        continue;
112✔
754
                }
755

756
                r = table_add_many(t,
1,376✔
757
                                   TABLE_STRING, resource_type_to_string(i->resource->type),
758
                                   TABLE_PATH, i->path);
759
                if (r < 0)
1,376✔
760
                        return table_log_add_error(r);
×
761

762
                if (i->metadata.partition_uuid_set) {
1,376✔
763
                        have_partition_attributes = true;
288✔
764
                        r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
288✔
765
                } else
766
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,088✔
767
                if (r < 0)
1,376✔
UNCOV
768
                        return table_log_add_error(r);
×
769

770
                if (i->metadata.partition_flags_set) {
1,376✔
771
                        have_partition_attributes = true;
288✔
772
                        r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
288✔
773
                } else
774
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,088✔
775
                if (r < 0)
1,376✔
UNCOV
776
                        return table_log_add_error(r);
×
777

778
                if (i->metadata.mtime != USEC_INFINITY) {
1,376✔
779
                        have_fs_attributes = true;
1,056✔
780
                        r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
1,056✔
781
                } else
782
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
320✔
783
                if (r < 0)
1,376✔
UNCOV
784
                        return table_log_add_error(r);
×
785

786
                if (i->metadata.mode != MODE_INVALID) {
1,376✔
787
                        have_fs_attributes = true;
1,056✔
788
                        r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
1,056✔
789
                } else
790
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
320✔
791
                if (r < 0)
1,376✔
UNCOV
792
                        return table_log_add_error(r);
×
793

794
                if (i->metadata.size != UINT64_MAX) {
1,376✔
UNCOV
795
                        have_size = true;
×
UNCOV
796
                        r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
×
797
                } else
798
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,376✔
799
                if (r < 0)
1,376✔
UNCOV
800
                        return table_log_add_error(r);
×
801

802
                if (i->metadata.tries_done != UINT64_MAX) {
1,376✔
803
                        have_tries = true;
128✔
804
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
128✔
805
                } else
806
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,248✔
807
                if (r < 0)
1,376✔
UNCOV
808
                        return table_log_add_error(r);
×
809

810
                if (i->metadata.tries_left != UINT64_MAX) {
1,376✔
811
                        have_tries = true;
128✔
812
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
128✔
813
                } else
814
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,248✔
815
                if (r < 0)
1,376✔
UNCOV
816
                        return table_log_add_error(r);
×
817

818
                if (i->metadata.no_auto >= 0) {
1,376✔
UNCOV
819
                        bool b;
×
820

UNCOV
821
                        have_no_auto = true;
×
UNCOV
822
                        b = i->metadata.no_auto;
×
823
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
824
                } else
825
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,376✔
826
                if (r < 0)
1,376✔
827
                        return table_log_add_error(r);
×
828
                if (i->metadata.read_only >= 0) {
1,376✔
829
                        bool b;
288✔
830

831
                        have_read_only = true;
288✔
832
                        b = i->metadata.read_only;
288✔
833
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
288✔
834
                } else
835
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,088✔
836
                if (r < 0)
1,376✔
837
                        return table_log_add_error(r);
×
838

839
                if (i->metadata.growfs >= 0) {
1,376✔
UNCOV
840
                        bool b;
×
841

UNCOV
842
                        have_growfs = true;
×
UNCOV
843
                        b = i->metadata.growfs;
×
UNCOV
844
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
845
                } else
846
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,376✔
847
                if (r < 0)
1,376✔
UNCOV
848
                        return table_log_add_error(r);
×
849

850
                if (i->metadata.sha256sum_set) {
1,376✔
851
                        _cleanup_free_ char *formatted = NULL;
32✔
852

853
                        have_sha256 = true;
32✔
854

855
                        formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
32✔
856
                        if (!formatted)
32✔
UNCOV
857
                                return log_oom();
×
858

859
                        r = table_add_cell(t, NULL, TABLE_STRING, formatted);
32✔
860
                } else
861
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,344✔
862
                if (r < 0)
1,376✔
UNCOV
863
                        return table_log_add_error(r);
×
864
        }
865

866
        /* Hide the fs/partition columns if we don't have any data to show there */
867
        if (!have_fs_attributes)
208✔
868
                show_fs_columns = false;
16✔
869
        if (!have_partition_attributes)
208✔
870
                show_partition_columns = false;
871

872
        if (!show_partition_columns)
144✔
873
                (void) table_hide_column_from_display(t, 2, 3);
64✔
874
        if (!show_fs_columns)
208✔
875
                (void) table_hide_column_from_display(t, 4, 5);
16✔
876
        if (!have_size)
208✔
877
                (void) table_hide_column_from_display(t, 6);
208✔
878
        if (!have_tries)
208✔
879
                (void) table_hide_column_from_display(t, 7, 8);
80✔
880
        if (!have_no_auto)
208✔
881
                (void) table_hide_column_from_display(t, 9);
208✔
882
        if (!have_read_only)
208✔
883
                (void) table_hide_column_from_display(t, 10);
64✔
884
        if (!have_growfs)
208✔
885
                (void) table_hide_column_from_display(t, 11);
208✔
886
        if (!have_sha256)
208✔
887
                (void) table_hide_column_from_display(t, 12);
192✔
888

889
        if (!sd_json_format_enabled(arg_json_format_flags)) {
208✔
890
                printf("%s%s%s Version: %s\n"
1,008✔
891
                       "    State: %s%s%s\n"
892
                       "Installed: %s%s%s%s%s%s%s\n"
893
                       "Available: %s%s\n"
894
                       "Protected: %s%s%s\n"
895
                       " Obsolete: %s%s%s\n",
896
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
112✔
897
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
112✔
898
                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
128✔
899
                       FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
144✔
900
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
208✔
901
                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
208✔
902
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
112✔
903
                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
112✔
904

905
                STRV_FOREACH(url, changelog_urls) {
112✔
UNCOV
906
                        _cleanup_free_ char *changelog_link = NULL;
×
UNCOV
907
                        r = terminal_urlify(*url, NULL, &changelog_link);
×
UNCOV
908
                        if (r < 0)
×
UNCOV
909
                                return log_oom();
×
UNCOV
910
                        printf("ChangeLog: %s\n", changelog_link);
×
911
                }
912
                printf("\n");
112✔
913

914
                return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
112✔
915
        } else {
916
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
96✔
917

918
                r = table_to_json(t, &t_json);
96✔
919
                if (r < 0)
96✔
UNCOV
920
                        return log_error_errno(r, "failed to convert table to JSON: %m");
×
921

922
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("version", us->version),
96✔
923
                                          SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)),
924
                                          SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
925
                                          SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
926
                                          SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)),
927
                                          SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)),
928
                                          SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
929
                                          SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
930
                                          SD_JSON_BUILD_PAIR_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)),
931
                                          SD_JSON_BUILD_PAIR_STRV("changelogUrls", changelog_urls),
932
                                          SD_JSON_BUILD_PAIR_VARIANT("contents", t_json));
933
                if (r < 0)
96✔
UNCOV
934
                        return log_error_errno(r, "Failed to create JSON: %m");
×
935

936
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
96✔
937
                if (r < 0)
96✔
UNCOV
938
                        return log_error_errno(r, "Failed to print JSON: %m");
×
939

940
                return 0;
941
        }
942
}
943

944
static int context_vacuum(
216✔
945
                Context *c,
946
                uint64_t space,
947
                const char *extra_protected_version) {
948

949
        size_t disabled_count = 0;
216✔
950
        int r, count = 0;
216✔
951

952
        assert(c);
216✔
953

954
        if (space == 0)
216✔
955
                log_info("Making room%s", glyph(GLYPH_ELLIPSIS));
64✔
956
        else
957
                log_info("Making room for %" PRIu64 " updates%s", space, glyph(GLYPH_ELLIPSIS));
368✔
958

959
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
1,699✔
960
                Transfer *t = *tr;
1,483✔
961

962
                /* Don't bother clearing out space if we're not going to be downloading anything */
963
                if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version))
1,483✔
964
                        continue;
208✔
965

966
                r = transfer_vacuum(t, space, extra_protected_version);
1,275✔
967
                if (r < 0)
1,275✔
968
                        return r;
969

970
                count = MAX(count, r);
1,275✔
971
        }
972

973
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
408✔
974
                r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
192✔
975
                if (r < 0)
192✔
976
                        return r;
977
                if (r > 0)
192✔
978
                        disabled_count++;
16✔
979
        }
980

981
        if (!sd_json_format_enabled(arg_json_format_flags)) {
216✔
982
                if (count > 0 && disabled_count > 0)
164✔
UNCOV
983
                        log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
×
984
                else if (count > 0)
164✔
985
                        log_info("Removed %i instances.", count);
76✔
986
                else if (disabled_count > 0)
88✔
987
                        log_info("Removed %zu disabled transfers.", disabled_count);
16✔
988
                else
989
                        log_info("Found nothing to remove.");
72✔
990
        } else {
991
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
52✔
992

993
                r = sd_json_buildo(&json,
52✔
994
                                   SD_JSON_BUILD_PAIR_INTEGER("removed", count),
995
                                   SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
996
                if (r < 0)
52✔
UNCOV
997
                        return log_error_errno(r, "Failed to create JSON: %m");
×
998

999
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
52✔
1000
                if (r < 0)
52✔
UNCOV
1001
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1002
        }
1003

1004
        return 0;
1005
}
1006

1007
typedef enum ProcessImageFlags {
1008
        PROCESS_IMAGE_READ_ONLY = 1 << 0,
1009
} ProcessImageFlags;
1010

1011
static int context_process_image(
1,287✔
1012
                Context *c,
1013
                ProcessImageFlags flags) {
1014

1015
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
1,287✔
1016
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
1,287✔
1017
        int r;
1,287✔
1018

1019
        assert(c);
1,287✔
1020

1021
        if (!c->image)
1,287✔
1022
                return 0;
1023

UNCOV
1024
        assert(!c->root);
×
UNCOV
1025
        assert(!c->mounted_dir);
×
UNCOV
1026
        assert(!c->loop_device);
×
1027

1028
        r = mount_image_privately_interactively(
×
1029
                        c->image,
UNCOV
1030
                        c->image_policy,
×
UNCOV
1031
                        (FLAGS_SET(flags, PROCESS_IMAGE_READ_ONLY) ? DISSECT_IMAGE_READ_ONLY : 0) |
×
1032
                        DISSECT_IMAGE_FSCK |
1033
                        DISSECT_IMAGE_MKDIR |
1034
                        DISSECT_IMAGE_GROWFS |
1035
                        DISSECT_IMAGE_RELAX_VAR_CHECK |
1036
                        DISSECT_IMAGE_USR_NO_ROOT |
1037
                        DISSECT_IMAGE_GENERIC_ROOT |
1038
                        DISSECT_IMAGE_REQUIRE_ROOT |
1039
                        DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
1040
                        &mounted_dir,
1041
                        /* ret_dir_fd= */ NULL,
1042
                        &loop_device);
UNCOV
1043
        if (r < 0)
×
1044
                return r;
1045

UNCOV
1046
        c->root = strdup(mounted_dir);
×
UNCOV
1047
        if (!c->root)
×
UNCOV
1048
                return log_oom();
×
1049

1050
        c->mounted_dir = TAKE_PTR(mounted_dir);
×
UNCOV
1051
        c->loop_device = TAKE_PTR(loop_device);
×
1052

UNCOV
1053
        return 0;
×
1054
}
1055

1056
static int context_list_components(Context *context, char ***ret_component_names, bool *ret_has_default_component);
1057

1058
static int context_load_offline(
1,287✔
1059
                Context *context,
1060
                ProcessImageFlags process_image_flags,
1061
                ReadDefinitionsFlags read_definitions_flags) {
1062
        int r;
1,287✔
1063

1064
        assert(context);
1,287✔
1065

1066
        /* Sets up a context object and initializes everything we can initialize offline, i.e. without
1067
         * checking on the update source (i.e. the Internet) what versions are available */
1068

1069
        r = context_process_image(context, process_image_flags);
1,287✔
1070
        if (r < 0)
1,287✔
1071
                return r;
1072

1073
        r = context_read_definitions(context, context->loop_device ? context->loop_device->node : NULL, read_definitions_flags);
1,287✔
1074
        if (r < 0)
1,287✔
1075
                return r;
1076

1077
        r = context_load_installed_instances(context);
1,287✔
1078
        if (r < 0)
1,287✔
UNCOV
1079
                return r;
×
1080

1081
        return 0;
1082
}
1083

1084
static int context_load_online(
1,039✔
1085
                Context *context,
1086
                ProcessImageFlags process_image_flags) {
1087
        int r;
1,039✔
1088

1089
        assert(context);
1,039✔
1090

1091
        /* Like context_load_offline(), but also communicates with the update source looking for new
1092
         * versions (as long as --offline is not specified on the command line). */
1093

1094
        r = context_load_offline(
1,039✔
1095
                        context,
1096
                        process_image_flags,
1097
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
1098
        if (r < 0)
1,039✔
1099
                return r;
1100

1101
        if (!context->offline) {
1,039✔
1102
                r = context_load_available_instances(context);
827✔
1103
                if (r < 0)
827✔
1104
                        return r;
1105
        }
1106

1107
        r = context_discover_update_sets(context);
1,022✔
1108
        if (r < 0)
1,022✔
UNCOV
1109
                return r;
×
1110

1111
        return 0;
1112
}
1113

UNCOV
1114
static bool image_type_can_sysupdate(ImageType image_type) {
×
1115
        /* systemd-sysupdate doesn't support mstack images yet */
NEW
1116
        return IN_SET(image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME, IMAGE_RAW, IMAGE_BLOCK);
×
1117
}
1118

UNCOV
1119
static int context_load_paths_from_image(Context *context, Image *image) {
×
UNCOV
1120
        assert(context);
×
UNCOV
1121
        assert(image);
×
1122

UNCOV
1123
        assert(!context->root);
×
UNCOV
1124
        assert(!context->image);
×
1125

UNCOV
1126
        switch (image->type) {
×
UNCOV
1127
        case IMAGE_DIRECTORY:
×
1128
        case IMAGE_SUBVOLUME:
UNCOV
1129
                context->root = strdup(image->path);
×
UNCOV
1130
                if (!context->root)
×
1131
                        return log_oom();
×
1132
                return 0;
1133
        case IMAGE_RAW:
×
1134
        case IMAGE_BLOCK:
UNCOV
1135
                context->image = strdup(image->path);
×
1136
                if (!context->image)
×
UNCOV
1137
                        return log_oom();
×
1138
                return 0;
UNCOV
1139
        default:
×
UNCOV
1140
                assert_not_reached();
×
1141
        }
1142
}
1143

1144
/* Load a Context to point to the target given by the TargetIdentifier. The TargetIdentifier will have been
1145
 * syntactically validated by dispatch_target_identifier(), but might still point to components which don’t
1146
 * exist, images which the user isn’t privileged to access, etc. This function validates the TargetIdentifier
1147
 * against an enumerated list of known targets, which are safe to update without additional permissions. */
1148
static int context_load_online_from_target(Context *context, ProcessImageFlags process_image_flags) {
192✔
1149
        int r;
192✔
1150

1151
        assert(context);
192✔
1152
        assert(context->target_identifier.class != _TARGET_CLASS_INVALID);
192✔
1153

1154
        /* These shouldn’t have been set up some other way first */
1155
        assert(!context->component);
192✔
1156
        assert(!context->root);
192✔
1157
        assert(!context->image);
192✔
1158

1159
        switch (context->target_identifier.class) {
192✔
UNCOV
1160
        case TARGET_MACHINE:
×
1161
        case TARGET_PORTABLE:
1162
        case TARGET_SYSEXT:
1163
        case TARGET_CONFEXT: {
UNCOV
1164
                _cleanup_hashmap_free_ Hashmap *images = NULL;
×
UNCOV
1165
                Image *image, *selected_image = NULL;
×
1166

1167
                /* These are all image-based target classes, so first find the corresponding image. */
UNCOV
1168
                r = image_discover(RUNTIME_SCOPE_SYSTEM, (ImageClass) context->target_identifier.class, NULL, &images);
×
UNCOV
1169
                if (r < 0)
×
1170
                        return r;
1171

UNCOV
1172
                HASHMAP_FOREACH(image, images) {
×
UNCOV
1173
                        bool have = false;
×
UNCOV
1174
                        _cleanup_(context_done) Context image_context = CONTEXT_NULL;
×
1175

UNCOV
1176
                        if (image_is_host(image))
×
UNCOV
1177
                                continue; /* We already enroll the host ourselves */
×
1178

UNCOV
1179
                        if (!image_type_can_sysupdate(image->type))
×
UNCOV
1180
                                continue;
×
1181

UNCOV
1182
                        if (!streq(image->name, context->target_identifier.name))
×
UNCOV
1183
                                continue;
×
1184

UNCOV
1185
                        r = context_load_paths_from_image(&image_context, image);
×
UNCOV
1186
                        if (r < 0)
×
1187
                                return r;
1188

1189
                        /* Load the components in a separate Context specific to the given Image before
1190
                         * committing to loading that state to the main Context. */
UNCOV
1191
                        r = context_load_offline(&image_context, 0, 0);
×
UNCOV
1192
                        if (r < 0)
×
1193
                                return r;
1194

NEW
1195
                        r = context_list_components(&image_context, /* ret_component_names= */ NULL, &have);
×
1196
                        if (r < 0)
×
1197
                                return r;
UNCOV
1198
                        if (!have) {
×
UNCOV
1199
                                log_debug("Skipping %s because it has no default component", image->path);
×
1200
                                continue;
×
1201
                        }
1202

1203
                        /* This is the match we were looking for */
UNCOV
1204
                        selected_image = image;
×
UNCOV
1205
                        break;
×
1206
                }
1207

UNCOV
1208
                if (!selected_image)
×
1209
                        return -ENOENT;
1210

UNCOV
1211
                r = context_load_paths_from_image(context, selected_image);
×
UNCOV
1212
                if (r < 0)
×
1213
                        return r;
1214

UNCOV
1215
                break;
×
1216
        }
1217
        case TARGET_HOST:
1218
                /* No additional setup needed */
1219
                break;
1220
        case TARGET_COMPONENT: {
8✔
1221
                _cleanup_strv_free_ char **component_names = NULL;
8✔
1222

1223
                r = context_list_components(context, &component_names, /* ret_has_default_component= */ NULL);
8✔
1224
                if (r < 0)
8✔
1225
                        return r;
1226

1227
                if (!strv_contains(component_names, context->target_identifier.name))
8✔
1228
                        return -ENOENT;
1229

UNCOV
1230
                context->component = strdup(context->target_identifier.name);
×
UNCOV
1231
                if (!context->component)
×
UNCOV
1232
                        return log_oom();
×
UNCOV
1233
                break;
×
1234
        }
UNCOV
1235
        default:
×
UNCOV
1236
                assert_not_reached();
×
1237
        }
1238

1239
        return context_load_online(context, process_image_flags);
184✔
1240
}
1241

1242
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
825✔
1243
        const Context *c = ASSERT_PTR(t->context);
825✔
1244
        size_t i, n = c->n_transfers;
825✔
1245
        uint64_t base, scaled;
825✔
1246
        unsigned overall;
825✔
1247

1248
        for (i = 0; i < n; i++)
2,915✔
1249
                if (c->transfers[i] == t)
2,915✔
1250
                        break;
1251
        assert(i < n); /* We should have found the index */
825✔
1252

1253
        base = (100 * 100 * i) / n;
825✔
1254
        scaled = (100 * percentage) / n;
825✔
1255
        overall = (unsigned) ((base + scaled) / 100);
825✔
1256
        assert(overall <= 100);
825✔
1257

1258
        log_debug("Transfer %zu/%zu is %u%% complete (%u%% overall).", i+1, n, percentage, overall);
825✔
1259
        return sd_notifyf(/* unset_environment= */ false, "X_SYSUPDATE_PROGRESS=%u\n"
1,650✔
1260
                                              "X_SYSUPDATE_TRANSFERS_LEFT=%zu\n"
1261
                                              "X_SYSUPDATE_TRANSFERS_DONE=%zu\n"
1262
                                              "STATUS=Updating to '%s' (%u%% complete).",
1263
                                              overall, n - i, i, inst->metadata.version, overall);
825✔
1264
}
1265

1266
static int context_process_partial_and_pending(Context *c, const char *version);
1267

1268
static int context_acquire(
282✔
1269
                Context *c,
1270
                const char *version) {
1271

1272
        UpdateSet *us = NULL;
282✔
1273
        int r;
282✔
1274

1275
        assert(c);
282✔
1276

1277
        if (version) {
282✔
UNCOV
1278
                us = context_update_set_by_version(c, version);
×
1279
                if (!us)
×
1280
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
282✔
1281
        } else {
1282
                if (!c->candidate) {
282✔
1283
                        log_info("No update needed.");
50✔
1284

1285
                        return 0;
1286
                }
1287

1288
                us = c->candidate;
1289
        }
1290

1291
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
232✔
1292
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
32✔
1293
        else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
200✔
1294
                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
16✔
1295
        } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
184✔
1296
                log_info("Selected update '%s' is already acquired and pending installation.", us->version);
32✔
1297

1298
                return context_process_partial_and_pending(c, version);
32✔
1299
        } else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
152✔
UNCOV
1300
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1301

1302
                return 0;
1303
        }
1304

1305
        if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
184✔
UNCOV
1306
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
×
1307
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
184✔
UNCOV
1308
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1309

1310
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
184✔
UNCOV
1311
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1312
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
184✔
UNCOV
1313
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1314

1315
        log_info("Selected update '%s' for install.", us->version);
184✔
1316

1317
        _cleanup_free_ InstanceMetadata *metadata = new0(InstanceMetadata, c->n_transfers);
368✔
1318
        if (!metadata)
184✔
UNCOV
1319
                return log_oom();
×
1320

1321
        /* Compute up the temporary paths before vacuuming so we don't vacuum anything if we fail to compute
1322
         * any paths because of failed validations (e.g. exceeding the gpt partition label size). */
1323
        for (size_t i = 0; i < c->n_transfers; i++) {
1,443✔
1324
                Instance *inst = us->instances[i];
1,259✔
1325
                Transfer *t = c->transfers[i];
1,259✔
1326

1327
                assert(inst);
1,259✔
1328

1329
                r = transfer_compute_temporary_paths(t, inst, metadata + i);
1,259✔
1330
                if (r < 0)
1,259✔
1331
                        return r;
1332
        }
1333

1334
        (void) sd_notifyf(/* unset_environment= */ false,
184✔
1335
                          "READY=1\n"
1336
                          "X_SYSUPDATE_VERSION=%s\n"
1337
                          "STATUS=Making room for '%s'.", us->version, us->version);
1338

1339
        /* Let's make some room. We make sure for each transfer we have one free space to fill. While
1340
         * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
1341
         * download succeeded already, in which case we shouldn't remove it just to acquire it again */
1342
        r = context_vacuum(
368✔
1343
                        c,
1344
                        /* space= */ 1,
1345
                        /* extra_protected_version= */ us->version);
184✔
1346
        if (r < 0)
184✔
1347
                return r;
1348

1349
        if (c->sync)
184✔
1350
                sync();
184✔
1351

1352
        (void) sd_notifyf(/* unset_environment= */ false,
184✔
1353
                          "STATUS=Updating to '%s'.", us->version);
1354

1355
        /* There should now be one instance picked for each transfer, and the order is the same */
1356
        assert(us->n_instances == c->n_transfers);
184✔
1357

1358
        for (size_t i = 0; i < c->n_transfers; i++) {
1,347✔
1359
                Instance *inst = us->instances[i];
1,179✔
1360
                Transfer *t = c->transfers[i];
1,179✔
1361

1362
                assert(inst); /* ditto */
1,179✔
1363

1364
                if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */
1,179✔
1365
                        assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
208✔
1366
                        continue;
208✔
1367
                }
1368

1369
                r = transfer_acquire_instance(t, inst, metadata + i, context_on_acquire_progress, c);
971✔
1370
                if (r < 0)
971✔
1371
                        return r;
1372
        }
1373

1374
        if (c->sync)
168✔
1375
                sync();
168✔
1376

1377
        return 1;
1378
}
1379

1380
/* Check to see if we have an update set acquired and pending installation. */
1381
static int context_process_partial_and_pending(
116✔
1382
                Context *c,
1383
                const char *version) {
1384

1385
        UpdateSet *us = NULL;
116✔
1386
        int r;
116✔
1387

1388
        assert(c);
116✔
1389

1390
        if (version) {
116✔
UNCOV
1391
                us = context_update_set_by_version(c, version);
×
1392
                if (!us)
×
UNCOV
1393
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1394
        } else {
1395
                if (!c->candidate) {
116✔
UNCOV
1396
                        log_info("No update needed.");
×
1397

1398
                        return 0;
1399
                }
1400

1401
                us = c->candidate;
1402
        }
1403

1404
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
116✔
UNCOV
1405
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
×
1406
        else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
116✔
1407
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1408

1409
                return 0;
1410
        }
1411

1412
        if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
116✔
1413
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
4✔
1414
        if (!FLAGS_SET(us->flags, UPDATE_PENDING))
112✔
1415
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version);
×
1416

1417
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
112✔
UNCOV
1418
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1419

1420
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
112✔
UNCOV
1421
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1422
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
112✔
UNCOV
1423
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1424

1425
        log_info("Selected update '%s' for install.", us->version);
112✔
1426

1427
        /* There should now be one instance picked for each transfer, and the order is the same */
1428
        assert(us->n_instances == c->n_transfers);
112✔
1429

1430
        for (size_t i = 0; i < c->n_transfers; i++) {
908✔
1431
                Instance *inst = us->instances[i];
796✔
1432
                Transfer *t = c->transfers[i];
796✔
1433

1434
                assert(inst);
796✔
1435

1436
                r = transfer_process_partial_and_pending_instance(t, inst);
796✔
1437
                if (r < 0)
796✔
1438
                        return r;
1439
        }
1440

1441
        return 1;
1442
}
1443

1444
static int notify_subscribers_reply(
169✔
1445
                sd_varlink *link,
1446
                sd_json_variant *reply,
1447
                const char *error_id,
1448
                sd_varlink_reply_flags_t flags,
1449
                void *userdata) {
1450

1451
        assert(link);
169✔
1452

1453
        if (error_id)
169✔
UNCOV
1454
                log_warning("Notification subscriber '%s' returned error, ignoring: %s",
×
1455
                            strna(sd_varlink_get_description(link)), error_id);
1456

1457
        return 0;
169✔
1458
}
1459

1460
static int context_notify_subscribers(Context *c, UpdateSet *us) {
168✔
1461
        int r;
168✔
1462

1463
        assert(c);
168✔
1464

1465
        /* 'us' is NULL when we are forced to notify even though no update was applied (via
1466
         * SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1). In that case we send neither a version nor a resource list. */
1467

1468
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *resources = NULL;
168✔
1469
        if (us)
168✔
1470
                for (size_t i = 0; i < c->n_transfers; i++) {
1,315✔
1471
                        Instance *inst = us->instances[i];
1,147✔
1472
                        Transfer *t = c->transfers[i];
1,147✔
1473

1474
                        if (inst->resource == &t->target &&
1,147✔
1475
                            !inst->is_pending)
920✔
1476
                                continue;
208✔
1477

1478
                        /* Report where the resource was installed *to* (not the source it came from): the
1479
                         * final on-disk path for filesystem targets, the partition device node for partition
1480
                         * targets. */
1481
                        const char *target_path =
256✔
1482
                                RESOURCE_IS_FILESYSTEM(t->target.type) ? t->final_path :
939✔
1483
                                t->target.type == RESOURCE_PARTITION ? t->partition_info.device :
1484
                                NULL;
1485

1486
                        r = sd_json_variant_append_arraybo(
939✔
1487
                                        &resources,
1488
                                        SD_JSON_BUILD_PAIR_STRING("transfer", t->id),
1489
                                        SD_JSON_BUILD_PAIR_CONDITION(!!target_path, "path", SD_JSON_BUILD_STRING(target_path)));
1490
                        if (r < 0)
939✔
1491
                                return log_warning_errno(r, "Failed to build sysupdate notify resources list, skipping notification: %m");
×
1492
                }
1493

1494
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
168✔
1495
        r = sd_json_buildo(
168✔
1496
                        &params,
1497
                        SD_JSON_BUILD_PAIR_CONDITION(!!c->component, "component", SD_JSON_BUILD_STRING(c->component)),
1498
                        SD_JSON_BUILD_PAIR_CONDITION(!!us, "version", SD_JSON_BUILD_STRING(us ? us->version : NULL)),
1499
                        SD_JSON_BUILD_PAIR_CONDITION(!!resources, "resources", SD_JSON_BUILD_VARIANT(resources)));
1500
        if (r < 0)
168✔
UNCOV
1501
                return log_warning_errno(r, "Failed to build sysupdate notify parameters, skipping notification: %m");
×
1502

1503
        ssize_t n = varlink_execute_directory(
168✔
1504
                        VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK,
1505
                        "io.systemd.SysUpdate.Notify.OnCompletedUpdate",
1506
                        params,
1507
                        /* more= */ false,
1508
                        /* timeout_usec= */ 5 * USEC_PER_MINUTE,
1509
                        notify_subscribers_reply,
1510
                        /* userdata= */ NULL);
1511
        if (n < 0)
168✔
1512
                log_debug_errno(n, "Failed to dispatch sysupdate notification to %s, ignoring: %m",
168✔
1513
                                VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
1514
        else if (n > 0)
168✔
1515
                log_debug("Dispatched sysupdate notification to %zi subscribers in %s.", n, VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
168✔
1516

1517
        return 0;
1518
}
1519

1520
static int context_install(
168✔
1521
                Context *c,
1522
                const char *version,
1523
                UpdateSet **ret_applied) {
1524

1525
        UpdateSet *us = NULL;
168✔
1526
        int r;
168✔
1527

1528
        assert(c);
168✔
1529

1530
        if (version) {
168✔
UNCOV
1531
                us = context_update_set_by_version(c, version);
×
UNCOV
1532
                if (!us)
×
UNCOV
1533
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1534
        } else {
1535
                if (!c->candidate) {
168✔
1536
                        log_info("No update needed.");
×
1537

1538
                        return 0;
1539
                }
1540

1541
                us = c->candidate;
1542
        }
1543

1544
        (void) sd_notifyf(/* unset_environment=*/ false,
168✔
1545
                          "READY=1\n"
1546
                          "X_SYSUPDATE_VERSION=%s\n"
1547
                          "STATUS=Installing '%s'.", us->version, us->version);
1548

1549
        for (size_t i = 0; i < c->n_transfers; i++) {
1,483✔
1550
                Instance *inst = us->instances[i];
1,147✔
1551
                Transfer *t = c->transfers[i];
1,147✔
1552

1553
                if (inst->resource == &t->target &&
1,147✔
1554
                    !inst->is_pending)
920✔
1555
                        continue;
208✔
1556

1557
                r = transfer_install_instance(t, inst, c->root);
939✔
1558
                if (r < 0)
939✔
1559
                        return r;
1560
        }
1561

1562
        log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
168✔
1563

1564
        if (!c->root)
168✔
1565
                (void) context_notify_subscribers(c, us);
168✔
1566

1567
        (void) sd_notifyf(/* unset_environment= */ false,
168✔
1568
                          "STATUS=Installed '%s'.", us->version);
1569

1570
        if (ret_applied)
168✔
1571
                *ret_applied = us;
168✔
1572

1573
        return 1;
1574
}
1575

1576
static JSON_DISPATCH_ENUM_DEFINE(dispatch_target_class, TargetClass, target_class_from_string);
200✔
1577

1578
static int dispatch_target_identifier(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
200✔
1579
        TargetIdentifier *t = ASSERT_PTR(userdata);
200✔
1580
        static const sd_json_dispatch_field dispatch[] = {
200✔
1581
                { "class", SD_JSON_VARIANT_STRING, dispatch_target_class,   voffsetof(*t, class), SD_JSON_MANDATORY },
1582
                { "name",  SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(*t, name),  SD_JSON_NULLABLE  },
1583
                {}
1584
        };
1585
        int r;
200✔
1586

1587
        r = sd_json_dispatch(variant, dispatch, flags, t);
200✔
1588
        if (r < 0)
200✔
1589
                return r;
1590

1591
        /* Name is mandatory unless class is `host` */
1592
        if ((t->class == TARGET_HOST) != (!t->name))
200✔
UNCOV
1593
                return json_log(variant, flags, SYNTHETIC_ERRNO(ENXIO), "Target name does not match class.");
×
1594

1595
        if (t->class == TARGET_COMPONENT && !component_name_valid(t->name))
200✔
1596
                        return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", t->name);
8✔
1597

1598
        return 0;
1599
}
1600

1601
static int verify_polkit(Context *context, sd_varlink *link, const char *action, const char **details) {
192✔
1602
        int r;
192✔
1603
        Server *s = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link)));
192✔
1604

1605
        assert(context);
192✔
1606

1607
        if (!s->system_bus) {
192✔
1608
                r = sd_bus_open_system_with_description(&s->system_bus, "sysupdate-system");
192✔
1609
                if (r < 0)
192✔
UNCOV
1610
                        return log_error_errno(r, "Failed to get system bus connection: %m");
×
1611

1612
                r = sd_bus_attach_event(s->system_bus, sd_varlink_get_event(link), SD_EVENT_PRIORITY_NORMAL);
192✔
1613
                if (r < 0)
192✔
UNCOV
1614
                        return log_error_errno(r, "Failed to attach system bus to event loop: %m");
×
1615
        }
1616

1617
        return varlink_verify_polkit_async(link,
192✔
1618
                        s->system_bus,
1619
                        action,
1620
                        details,
1621
                        &s->polkit_registry);
1622
}
1623

1624
VERB(verb_list, "list", "[VERSION]", VERB_ANY, 2, VERB_DEFAULT,
1625
     "Show installed and available versions");
1626
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
256✔
1627
        _cleanup_(context_done) Context context = CONTEXT_NULL;
256✔
1628
        _cleanup_strv_free_ char **appstream_urls = NULL;
256✔
1629
        const char *version;
256✔
1630
        int r;
256✔
1631

1632
        assert(argc <= 2);
256✔
1633
        version = argc >= 2 ? argv[1] : NULL;
256✔
1634

1635
        r = context_from_cmdline(&context);
256✔
1636
        if (r < 0)
256✔
1637
                return r;
1638

1639
        if (context.component_all)
256✔
UNCOV
1640
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1641

1642
        r = context_load_online(&context, PROCESS_IMAGE_READ_ONLY);
256✔
1643
        if (r < 0)
256✔
1644
                return r;
1645

1646
        if (version)
256✔
1647
                return context_show_version(&context, version);
208✔
1648
        else if (!sd_json_format_enabled(arg_json_format_flags))
48✔
UNCOV
1649
                return context_show_table(&context);
×
1650
        else {
1651
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
48✔
1652
                _cleanup_strv_free_ char **versions = NULL;
48✔
1653
                const char *current = NULL;
48✔
1654
                bool current_is_pending = false;
48✔
1655

1656
                FOREACH_ARRAY(update_set, context.update_sets, context.n_update_sets) {
224✔
1657
                        UpdateSet *us = *update_set;
176✔
1658

1659
                        if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
176✔
1660
                            FLAGS_SET(us->flags, UPDATE_NEWEST)) {
160✔
1661
                                current = us->version;
48✔
1662
                                current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
48✔
1663
                        }
1664

1665
                        r = strv_extend(&versions, us->version);
176✔
1666
                        if (r < 0)
176✔
UNCOV
1667
                                return log_oom();
×
1668
                }
1669

1670
                FOREACH_ARRAY(tr, context.transfers, context.n_transfers)
288✔
1671
                        STRV_FOREACH(appstream_url, (*tr)->appstream) {
240✔
1672
                                /* Avoid duplicates */
UNCOV
1673
                                if (strv_contains(appstream_urls, *appstream_url))
×
UNCOV
1674
                                        continue;
×
1675

UNCOV
1676
                                r = strv_extend(&appstream_urls, *appstream_url);
×
UNCOV
1677
                                if (r < 0)
×
UNCOV
1678
                                        return log_oom();
×
1679
                        }
1680

1681
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
96✔
1682
                                          SD_JSON_BUILD_PAIR_STRV("all", versions),
1683
                                          SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
1684
                if (r < 0)
48✔
UNCOV
1685
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1686

1687
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
48✔
1688
                if (r < 0)
48✔
1689
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1690

1691
                return 0;
1692
        }
1693
}
1694

1695
VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, 0,
1696
     "Show optional features");
1697
static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) {
32✔
1698
        _cleanup_(context_done) Context context = CONTEXT_NULL;
32✔
1699
        _cleanup_(table_unrefp) Table *table = NULL;
32✔
1700
        const char *feature_id;
32✔
1701
        Feature *f;
32✔
1702
        int r;
32✔
1703

1704
        assert(argc <= 2);
32✔
1705
        feature_id = argc >= 2 ? argv[1] : NULL;
32✔
1706

1707
        r = context_from_cmdline(&context);
32✔
1708
        if (r < 0)
32✔
1709
                return r;
1710

1711
        if (context.component_all)
32✔
1712
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1713

1714
        r = context_load_offline(
32✔
1715
                        &context,
1716
                        PROCESS_IMAGE_READ_ONLY,
1717
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
1718
        if (r < 0)
32✔
1719
                return r;
1720

1721
        if (feature_id) {
32✔
1722
                _cleanup_strv_free_ char **transfers = NULL;
16✔
1723

1724
                f = hashmap_get(context.features, feature_id);
16✔
1725
                if (!f)
16✔
1726
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
1727
                                               "Optional feature not found: %s",
1728
                                               feature_id);
1729

1730
                table = table_new_vertical();
16✔
1731
                if (!table)
16✔
UNCOV
1732
                        return log_oom();
×
1733

1734
                FOREACH_ARRAY(tr, context.transfers, context.n_transfers) {
128✔
1735
                        Transfer *t = *tr;
112✔
1736

1737
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
112✔
1738
                                continue;
112✔
1739

UNCOV
1740
                        r = strv_extend(&transfers, t->id);
×
UNCOV
1741
                        if (r < 0)
×
UNCOV
1742
                                return log_oom();
×
1743
                }
1744

1745
                FOREACH_ARRAY(tr, context.disabled_transfers, context.n_disabled_transfers) {
32✔
1746
                        Transfer *t = *tr;
16✔
1747

1748
                        if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
16✔
UNCOV
1749
                                continue;
×
1750

1751
                        r = strv_extend(&transfers, t->id);
16✔
1752
                        if (r < 0)
16✔
UNCOV
1753
                                return log_oom();
×
1754
                }
1755

1756
                r = table_add_many(table,
16✔
1757
                                   TABLE_FIELD, "Name",
1758
                                   TABLE_STRING, f->id,
1759
                                   TABLE_FIELD, "Enabled",
1760
                                   TABLE_BOOLEAN, f->enabled);
1761
                if (r < 0)
16✔
UNCOV
1762
                        return table_log_add_error(r);
×
1763

1764
                if (f->description) {
16✔
1765
                        r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
16✔
1766
                        if (r < 0)
16✔
UNCOV
1767
                                return table_log_add_error(r);
×
1768
                }
1769

1770
                if (f->documentation) {
16✔
1771
                        r = table_add_many(table,
×
1772
                                           TABLE_FIELD, "Documentation",
1773
                                           TABLE_STRING, f->documentation,
1774
                                           TABLE_SET_URL, f->documentation);
UNCOV
1775
                        if (r < 0)
×
UNCOV
1776
                                return table_log_add_error(r);
×
1777
                }
1778

1779
                if (f->appstream) {
16✔
UNCOV
1780
                        r = table_add_many(table,
×
1781
                                           TABLE_FIELD, "AppStream",
1782
                                           TABLE_STRING, f->appstream,
1783
                                           TABLE_SET_URL, f->appstream);
UNCOV
1784
                        if (r < 0)
×
UNCOV
1785
                                return table_log_add_error(r);
×
1786
                }
1787

1788
                if (!strv_isempty(transfers)) {
16✔
1789
                        r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
16✔
1790
                        if (r < 0)
16✔
1791
                                return table_log_add_error(r);
×
1792
                }
1793

1794
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
16✔
1795
        } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
16✔
1796
                table = table_new("", "feature", "description", "documentation");
16✔
1797
                if (!table)
16✔
UNCOV
1798
                        return log_oom();
×
1799

1800
                HASHMAP_FOREACH(f, context.features) {
32✔
1801
                        r = table_add_many(table,
16✔
1802
                                           TABLE_BOOLEAN_CHECKMARK, f->enabled,
1803
                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
1804
                                           TABLE_STRING, f->id,
1805
                                           TABLE_STRING, f->description,
1806
                                           TABLE_STRING, f->documentation,
1807
                                           TABLE_SET_URL, f->documentation);
1808
                        if (r < 0)
16✔
UNCOV
1809
                                return table_log_add_error(r);
×
1810
                }
1811

1812
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
16✔
1813
        } else {
UNCOV
1814
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
×
UNCOV
1815
                _cleanup_strv_free_ char **features = NULL;
×
1816

UNCOV
1817
                HASHMAP_FOREACH(f, context.features) {
×
UNCOV
1818
                        r = strv_extend(&features, f->id);
×
UNCOV
1819
                        if (r < 0)
×
UNCOV
1820
                                return log_oom();
×
1821
                }
1822

UNCOV
1823
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
×
UNCOV
1824
                if (r < 0)
×
UNCOV
1825
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1826

UNCOV
1827
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
×
UNCOV
1828
                if (r < 0)
×
UNCOV
1829
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1830
        }
1831

UNCOV
1832
        return 0;
×
1833
}
1834

1835
VERB_NOARG(verb_check_new, "check-new",
1836
           "Check if there's a new version available");
1837
static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) {
217✔
1838
        _cleanup_(context_done) Context context = CONTEXT_NULL;
217✔
1839
        int r;
217✔
1840

1841
        assert(argc <= 1);
217✔
1842

1843
        r = context_from_cmdline(&context);
217✔
1844
        if (r < 0)
217✔
1845
                return r;
1846

1847
        if (context.component_all)
217✔
1848
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
1849

1850
        r = context_load_online(
217✔
1851
                        &context,
1852
                        PROCESS_IMAGE_READ_ONLY);
1853
        if (r < 0)
217✔
1854
                return r;
1855

1856
        if (!sd_json_format_enabled(arg_json_format_flags)) {
216✔
1857
                if (!context.candidate) {
184✔
1858
                        log_debug("No candidate found.");
96✔
1859
                        return EXIT_FAILURE;
1860
                }
1861

1862
                puts(context.candidate->version);
88✔
1863
        } else {
1864
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
32✔
1865

1866
                if (context.candidate)
32✔
UNCOV
1867
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version));
×
1868
                else
1869
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_NULL("available"));
32✔
1870
                if (r < 0)
32✔
1871
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1872

1873
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
32✔
1874
                if (r < 0)
32✔
UNCOV
1875
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1876
        }
1877

1878
        return EXIT_SUCCESS;
1879
}
1880

1881
static int vl_method_check_new(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
200✔
1882
        _cleanup_(context_done) Context context = CONTEXT_NULL;
200✔
1883
        int r;
200✔
1884

1885
        assert(link);
200✔
1886

1887
        static const sd_json_dispatch_field dispatch_table[] = {
200✔
1888
                { "target", SD_JSON_VARIANT_OBJECT, dispatch_target_identifier, voffsetof(context, target_identifier), SD_JSON_MANDATORY },
1889
                VARLINK_DISPATCH_POLKIT_FIELD,
1890
                {},
1891
        };
1892

1893
        r = sd_varlink_dispatch(link, parameters, dispatch_table, &context);
200✔
1894
        if (r != 0)
200✔
1895
                return r;
1896

1897
        r = verify_polkit(&context, link, "org.freedesktop.sysupdate1.check",
576✔
1898
                        (const char**) STRV_MAKE(
376✔
1899
                                        "class", target_class_to_string(context.target_identifier.class),
1900
                                        "offline", "0",
1901
                                        context.target_identifier.name ? "name" : NULL, context.target_identifier.name));
1902
        if (r <= 0)
192✔
1903
                return r;
1904

1905
        if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
192✔
1906
                context.verify = 0;
192✔
1907

1908
        /* CheckNew is always online */
1909
        context.offline = false;
192✔
1910

1911
        r = context_load_online_from_target(&context, PROCESS_IMAGE_READ_ONLY);
192✔
1912
        if (r == -ENOENT)
192✔
1913
                return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL);
8✔
1914
        if (r < 0)
184✔
1915
                return r;
1916

1917
        if (context.candidate)
184✔
1918
                r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version));
88✔
1919
        else
1920
                r = sd_varlink_error(link, "io.systemd.SysUpdate.NoUpdateNeeded", NULL);
96✔
1921
        if (r < 0)
184✔
1922
                return r;
96✔
1923

1924
        return 0;
1925
}
1926

1927
typedef enum {
1928
        UPDATE_ACTION_ACQUIRE = 1 << 0,
1929
        UPDATE_ACTION_INSTALL = 1 << 1,
1930
} UpdateActionFlags;
1931

1932
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
383✔
1933
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
1934
        _cleanup_free_ char *booted_version = NULL;
383✔
1935
        UpdateSet *applied = NULL;
383✔
1936
        const char *version;
383✔
1937
        int r;
383✔
1938

1939
        assert(argc <= 2);
383✔
1940
        version = argc >= 2 ? argv[1] : NULL;
383✔
1941

1942
        r = context_from_cmdline(&context);
383✔
1943
        if (r < 0)
383✔
1944
                return r;
1945

1946
        if (context.component_all)
383✔
1947
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
1✔
1948

1949
        if (context.instances_max < 2)
382✔
UNCOV
1950
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1951
                                      "The --instances-max argument must be >= 2 while updating");
1952

1953
        if (context.reboot) {
382✔
1954
                /* If automatic reboot on completion is requested, let's first determine the currently booted image */
1955

UNCOV
1956
                r = parse_os_release(context.root, "IMAGE_VERSION", &booted_version);
×
UNCOV
1957
                if (r < 0)
×
UNCOV
1958
                        return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
UNCOV
1959
                if (!booted_version)
×
1960
                        return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
×
1961
        }
1962

1963
        bool installed = false;
382✔
1964
        int ret = 0;
382✔
1965

1966
        r = context_load_online(
382✔
1967
                        &context,
1968
                        /* process_image_flags= */ 0);
1969
        if (r < 0) {
382✔
1970
                if (r != -ENOENT)
16✔
1971
                        return r;
1972

1973
                /* No transfer files found. In that case, still do the installdb cleanup below */
1974
                RET_GATHER(ret, r);
1975
        } else {
1976
                if (action_flags & UPDATE_ACTION_ACQUIRE)
366✔
1977
                        r = context_acquire(&context, version);
282✔
1978
                else
1979
                        r = context_process_partial_and_pending(&context, version);
84✔
1980
                if (r < 0)
366✔
1981
                        return r;
1982

1983
                if (FLAGS_SET(action_flags, UPDATE_ACTION_INSTALL) && r > 0) { /* installation of update indicated */
330✔
1984
                        r = context_install(&context, version, &applied);
168✔
1985
                        if (r < 0)
168✔
1986
                                return r;
1987

1988
                        installed = r > 0;
168✔
1989
                }
1990

1991
                /* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If
1992
                 * nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a
1993
                 * resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */
1994
                if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) {
218✔
1995
                        int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY");
50✔
1996
                        if (f < 0 && f != -ENXIO)
50✔
1997
                                log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m");
×
1998
                        if (f > 0)
50✔
1999
                                (void) context_notify_subscribers(&context, /* us= */ NULL);
×
2000
                }
2001
        }
2002

2003
        if (context.cleanup > 0)
330✔
2004
                RET_GATHER(ret, installdb_cleanup_component(&context));
2✔
2005

2006
        if (installed) {
330✔
2007
                /* We installed something, yay */
2008

2009
                if (context.reboot) {
168✔
UNCOV
2010
                        assert(applied);
×
UNCOV
2011
                        assert(booted_version);
×
2012

UNCOV
2013
                        if (strverscmp_improved(applied->version, booted_version) > 0) {
×
2014
                                log_notice("Newly installed version is newer than booted version, rebooting.");
×
UNCOV
2015
                                RET_GATHER(ret, reboot_now());
×
UNCOV
2016
                        } else if (strverscmp_improved(applied->version, booted_version) == 0 &&
×
2017
                                   FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) {
×
UNCOV
2018
                                log_notice("Currently booted version was incomplete and has been repaired, rebooting.");
×
UNCOV
2019
                                RET_GATHER(ret, reboot_now());
×
2020
                        } else
UNCOV
2021
                                log_info("Booted version is newer or identical to newly installed version, not rebooting.");
×
2022
                }
2023
        }
2024

2025
        return ret;
2026
}
2027

2028
VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0,
2029
     "Install new version now");
2030
static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) {
247✔
2031
        UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
247✔
2032

2033
        if (!arg_offline)
247✔
2034
                flags |= UPDATE_ACTION_ACQUIRE;
163✔
2035

2036
        return verb_update_impl(argc, argv, flags);
247✔
2037
}
2038

2039
VERB(verb_acquire, "acquire", "[VERSION]", VERB_ANY, 2, 0,
2040
     "Acquire (download) new version now");
2041
static int verb_acquire(int argc, char *argv[], uintptr_t _data, void *userdata) {
136✔
2042
        return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
136✔
2043
}
2044

2045
VERB_NOARG(verb_vacuum, "vacuum",
2046
           "Make room, by deleting old versions");
2047
static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) {
32✔
2048
        _cleanup_(context_done) Context context = CONTEXT_NULL;
32✔
2049
        int r;
32✔
2050

2051
        assert(argc <= 1);
32✔
2052

2053
        r = context_from_cmdline(&context);
32✔
2054
        if (r < 0)
32✔
2055
                return r;
2056

2057
        if (context.component_all)
32✔
UNCOV
2058
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
2059

2060
        if (context.instances_max < 1)
32✔
UNCOV
2061
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2062
                                      "The --instances-max argument must be >= 1 while vacuuming");
2063

2064
        r = context_load_offline(
32✔
2065
                        &context,
2066
                        /* process_image_flags= */ 0,
2067
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2068
        if (r < 0)
32✔
2069
                return r;
2070

2071
        return context_vacuum(&context, 0, NULL);
32✔
2072
}
2073

2074
VERB(verb_pending_or_reboot, "pending", NULL, 1, 1, 0,
2075
     "Report whether a newer version is installed than currently booted");
2076
VERB(verb_pending_or_reboot, "reboot", NULL, 1, 1, 0,
2077
     "Reboot if a newer version is installed than booted");
2078
static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
2✔
UNCOV
2079
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
2080
        _cleanup_free_ char *booted_version = NULL;
2✔
2081
        int r;
2✔
2082

2083
        assert(argc == 1);
2✔
2084

2085
        r = context_from_cmdline(&context);
2✔
2086
        if (r < 0)
2✔
2087
                return r;
2088

2089
        if (context.image || context.root)
2✔
UNCOV
2090
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2091
                                       "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]);
2092

2093
        if (context.component || context.component_all)
2✔
2094
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
2✔
2095
                                       "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]);
2096

UNCOV
2097
        r = context_load_offline(
×
2098
                        &context,
2099
                        /* process_image_flags= */ 0,
2100
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
UNCOV
2101
        if (r < 0)
×
2102
                return r;
2103

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

UNCOV
2106
        r = context_discover_update_sets_by_flag(&context, UPDATE_INSTALLED);
×
UNCOV
2107
        if (r < 0)
×
2108
                return r;
UNCOV
2109
        if (!context.newest_installed)
×
UNCOV
2110
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
×
2111

UNCOV
2112
        r = parse_os_release(context.root, "IMAGE_VERSION", &booted_version);
×
UNCOV
2113
        if (r < 0) /* yes, context.root is NULL here, but we have to pass something, and it's a lot more readable
×
2114
                    * if we see what the first argument is about */
UNCOV
2115
                return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
UNCOV
2116
        if (!booted_version)
×
UNCOV
2117
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
×
2118

UNCOV
2119
        r = strverscmp_improved(context.newest_installed->version, booted_version);
×
UNCOV
2120
        if (r > 0) {
×
UNCOV
2121
                log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
×
2122
                           context.newest_installed->version, booted_version,
2123
                           streq(argv[0], "pending") ? " Reboot recommended." : "");
2124

UNCOV
2125
                if (streq(argv[0], "reboot"))
×
UNCOV
2126
                        return reboot_now();
×
2127

2128
                return EXIT_SUCCESS;
UNCOV
2129
        } else if (r == 0)
×
UNCOV
2130
                log_info("Newest installed version '%s' matches booted version '%s'.",
×
2131
                         context.newest_installed->version, booted_version);
2132
        else
UNCOV
2133
                log_warning("Newest installed version '%s' is older than booted version '%s'.",
×
2134
                            context.newest_installed->version, booted_version);
2135

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

2139
        return EXIT_SUCCESS;
2140
}
2141

2142
static int context_list_components(Context *context, char ***ret_component_names, bool *ret_has_default_component) {
182✔
2143
        int r;
182✔
2144

2145
        assert(context);
182✔
2146

2147
        _cleanup_strv_free_ char **z = NULL;
182✔
2148
        r = get_component_list(context->root, &z);
182✔
2149
        if (r < 0)
182✔
2150
                return r;
2151

2152
        if (ret_component_names)
182✔
2153
                *ret_component_names = TAKE_PTR(z);
182✔
2154

2155
        /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a
2156
         * TARGET_HOST? See target_get_argument() in sysupdated.c */
2157
        if (ret_has_default_component)
182✔
2158
                *ret_has_default_component = (!context->definitions &&
522✔
2159
                                          !context->component &&
174✔
2160
                                          !context->root &&
174✔
2161
                                          !context->image &&
348✔
2162
                                          context->n_transfers > 0);
176✔
2163

2164
        return 0;
2165
}
2166

2167
VERB_NOARG(verb_components, "components",
2168
           "Show list of components");
2169
static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
174✔
2170
        _cleanup_(context_done) Context context = CONTEXT_NULL;
174✔
2171
        _cleanup_strv_free_ char **component_names = NULL;
174✔
2172
        bool has_default_component = false;
174✔
2173
        int r;
174✔
2174

2175
        assert(argc <= 1);
174✔
2176

2177
        r = context_from_cmdline(&context);
174✔
2178
        if (r < 0)
174✔
2179
                return r;
2180

2181
        if (context.component_all)
174✔
UNCOV
2182
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]);
×
2183

2184
        r = context_load_offline(
174✔
2185
                        &context,
2186
                        /* process_image_flags= */ 0,
2187
                        /* read_definitions_flags= */ 0);
2188
        if (r < 0)
174✔
2189
                return r;
2190

2191
        r = context_list_components(&context, &component_names, &has_default_component);
174✔
2192
        if (r < 0)
174✔
UNCOV
2193
                return log_error_errno(r, "Failed to enumerate components: %m");
×
2194

2195
        if (!sd_json_format_enabled(arg_json_format_flags)) {
174✔
UNCOV
2196
                if (!has_default_component && strv_isempty(component_names)) {
×
UNCOV
2197
                        log_info("No components defined.");
×
2198
                        return 0;
2199
                }
2200

UNCOV
2201
                if (has_default_component)
×
UNCOV
2202
                        printf("%s<default>%s\n",
×
2203
                               ansi_highlight(), ansi_normal());
2204

UNCOV
2205
                STRV_FOREACH(i, component_names)
×
UNCOV
2206
                        puts(*i);
×
2207
        } else {
2208
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
174✔
2209

2210
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component),
174✔
2211
                                          SD_JSON_BUILD_PAIR_STRV("components", component_names));
2212
                if (r < 0)
174✔
UNCOV
2213
                        return log_error_errno(r, "Failed to create JSON: %m");
×
2214

2215
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
174✔
2216
                if (r < 0)
174✔
UNCOV
2217
                        return log_error_errno(r, "Failed to print JSON: %m");
×
2218
        }
2219

2220
        return 0;
2221
}
2222

2223
VERB_NOARG(verb_cleanup, "cleanup", "Clean up orphaned files");
2224
static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) {
7✔
2225
        _cleanup_(context_done) Context context = CONTEXT_NULL;
7✔
2226
        int r;
7✔
2227

2228
        assert(argc <= 1);
7✔
2229

2230
        r = context_from_cmdline(&context);
7✔
2231
        if (r < 0)
7✔
2232
                return r;
2233

2234
        if (context.cleanup == 0)
7✔
2235
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invocation of 'cleanup' with --cleanup=no is contradictory, refusing.");
1✔
2236

2237
        r = context_load_offline(
6✔
2238
                        &context,
2239
                        /* process_image_flags= */ 0,
2240
                        /* read_definitions_flags= */ 0);
2241
        if (r < 0)
6✔
2242
                return r;
2243

2244
        int ret = 0;
6✔
2245
        RET_GATHER(ret, installdb_cleanup_component(&context));
6✔
2246

2247
        if (context.component_all) {
6✔
2248
                _cleanup_strv_free_ char **z = NULL;
2✔
2249
                r = installdb_list_components(&context, &z);
2✔
2250
                if (r < 0)
2✔
UNCOV
2251
                        return log_error_errno(r, "Failed to enumerate components: %m");
×
2252

2253
                STRV_FOREACH(i, z) {
6✔
UNCOV
2254
                        _cleanup_(context_done) Context component_context = CONTEXT_NULL;
×
2255

2256
                        r = context_from_cmdline(&component_context);
4✔
2257
                        if (r < 0)
4✔
2258
                                return r;
2259

2260
                        /* Override the component with our iter. This needs to be done in a fresh Context
2261
                         * as the installdb_fd and other state are specific to the component. */
2262
                        r = free_and_strdup_warn(&component_context.component, *i);
4✔
2263
                        if (r < 0)
4✔
2264
                                return r;
2265

2266
                        r = context_load_offline(
4✔
2267
                                        &component_context,
2268
                                        /* process_image_flags= */ 0,
2269
                                        /* read_definitions_flags= */ 0);
2270
                        if (r < 0)
4✔
2271
                                return r;
2272

2273
                        RET_GATHER(ret, installdb_cleanup_component(&component_context));
4✔
2274
                }
2275
        }
2276

2277
        return ret;
2278
}
2279

UNCOV
2280
static int help(void) {
×
UNCOV
2281
        _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL;
×
UNCOV
2282
        int r;
×
2283

UNCOV
2284
        r = verbs_get_help_table(&verbs);
×
UNCOV
2285
        if (r < 0)
×
2286
                return r;
2287

UNCOV
2288
        r = option_parser_get_help_table(&common_options);
×
UNCOV
2289
        if (r < 0)
×
2290
                return r;
2291

UNCOV
2292
        r = option_parser_get_help_table_group("Options", &options);
×
UNCOV
2293
        if (r < 0)
×
2294
                return r;
2295

UNCOV
2296
        (void) table_sync_column_widths(0, verbs, common_options, options);
×
2297

UNCOV
2298
        help_cmdline("[OPTIONS…] [VERSION]");
×
UNCOV
2299
        help_abstract("Update OS images.");
×
2300

UNCOV
2301
        help_section("Commands");
×
UNCOV
2302
        r = table_print_or_warn(verbs);
×
UNCOV
2303
        if (r < 0)
×
2304
                return r;
2305

UNCOV
2306
        r = table_print_or_warn(common_options);
×
UNCOV
2307
        if (r < 0)
×
2308
                return r;
2309

UNCOV
2310
        help_section("Options");
×
UNCOV
2311
        r = table_print_or_warn(options);
×
UNCOV
2312
        if (r < 0)
×
2313
                return r;
2314

UNCOV
2315
        help_man_page_reference("systemd-sysupdate", "8");
×
2316
        return 0;
2317
}
2318

UNCOV
2319
VERB_COMMON_HELP_HIDDEN(help);
×
2320

2321
static int parse_argv(int argc, char *argv[], char ***remaining_args) {
1,324✔
2322
        assert(argc >= 0);
1,324✔
2323
        assert(argv);
1,324✔
2324
        assert(remaining_args);
1,324✔
2325

2326
        OptionParser opts = { argc, argv };
1,324✔
2327
        int r;
1,324✔
2328

2329
        FOREACH_OPTION_OR_RETURN(c, &opts)
4,116✔
2330
                switch (c) {
1,476✔
2331

UNCOV
2332
                OPTION_COMMON_HELP:
×
UNCOV
2333
                        return help();
×
2334

UNCOV
2335
                OPTION_COMMON_VERSION:
×
UNCOV
2336
                        return version();
×
2337

2338
                OPTION_GROUP("Options"):
2339
                        break;
2340

2341
                OPTION('C', "component", "NAME",
45✔
2342
                       "Select component to update"):
2343
                        if (isempty(opts.arg)) {
45✔
UNCOV
2344
                                arg_component = mfree(arg_component);
×
UNCOV
2345
                                arg_component_all = false;
×
UNCOV
2346
                                break;
×
2347
                        }
2348

2349
                        if (!component_name_valid(opts.arg))
45✔
2350
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", opts.arg);
8✔
2351

2352
                        r = free_and_strdup_warn(&arg_component, opts.arg);
37✔
2353
                        if (r < 0)
37✔
2354
                                return r;
2355

2356
                        arg_component_all = false;
37✔
2357
                        break;
37✔
2358

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

2361
                        arg_component = mfree(arg_component);
3✔
2362
                        arg_component_all = true;
3✔
2363
                        break;
3✔
2364

2365
                OPTION_LONG("definitions", "DIR",
1✔
2366
                            "Find transfer definitions in specified directory"):
2367
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions);
1✔
2368
                        if (r < 0)
1✔
2369
                                return r;
2370
                        break;
2371

UNCOV
2372
                OPTION_LONG("root", "PATH",
×
2373
                            "Operate on an alternate filesystem root"):
UNCOV
2374
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
×
UNCOV
2375
                        if (r < 0)
×
2376
                                return r;
2377
                        break;
2378

UNCOV
2379
                OPTION_LONG("image", "PATH",
×
2380
                            "Operate on disk image as filesystem root"):
UNCOV
2381
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
UNCOV
2382
                        if (r < 0)
×
2383
                                return r;
2384
                        break;
2385

UNCOV
2386
                OPTION_LONG("image-policy", "POLICY",
×
2387
                            "Specify disk image dissection policy"):
UNCOV
2388
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
UNCOV
2389
                        if (r < 0)
×
2390
                                return r;
2391
                        break;
2392

UNCOV
2393
                OPTION_LONG("transfer-source", "PATH",
×
2394
                            "Specify the directory to transfer sources from"):
UNCOV
2395
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_transfer_source);
×
UNCOV
2396
                        if (r < 0)
×
2397
                                return r;
2398

2399
                        break;
2400

UNCOV
2401
                OPTION('m', "instances-max", "INT",
×
2402
                       "How many instances to maintain"):
UNCOV
2403
                        r = safe_atou64(opts.arg, &arg_instances_max);
×
UNCOV
2404
                        if (r < 0)
×
UNCOV
2405
                                return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", opts.arg);
×
2406

2407
                        break;
2408

UNCOV
2409
                OPTION_LONG("sync", "BOOL",
×
2410
                            "Controls whether to sync data to disk"):
UNCOV
2411
                        r = parse_boolean_argument("--sync=", opts.arg, &arg_sync);
×
UNCOV
2412
                        if (r < 0)
×
2413
                                return r;
2414
                        break;
2415

2416
                OPTION_LONG("verify", "BOOL",
736✔
2417
                            "Force signature verification on or off"): {
2418
                        bool b;
736✔
2419

2420
                        r = parse_boolean_argument("--verify=", opts.arg, &b);
736✔
2421
                        if (r < 0)
736✔
UNCOV
2422
                                return r;
×
2423

2424
                        arg_verify = b;
736✔
2425
                        break;
736✔
2426
                }
2427

2428
                OPTION_LONG("reboot", NULL,
1✔
2429
                            "Reboot after updating to newer version"):
2430
                        arg_reboot = true;
1✔
2431
                        break;
1✔
2432

2433
                OPTION_LONG("offline", NULL,
228✔
2434
                            "Do not fetch metadata from the network"):
2435
                        arg_offline = true;
228✔
2436
                        break;
228✔
2437

2438
                OPTION_LONG("cleanup", "BOOL", "Clean up orphaned files after completing update"): {
4✔
2439
                        bool b;
4✔
2440

2441
                        r = parse_boolean_argument("--cleanup=", opts.arg, &b);
4✔
2442
                        if (r < 0)
4✔
UNCOV
2443
                                return r;
×
2444

2445
                        arg_cleanup = b;
4✔
2446
                        break;
4✔
2447
                }
2448

UNCOV
2449
                OPTION_COMMON_NO_PAGER:
×
UNCOV
2450
                        arg_pager_flags |= PAGER_DISABLE;
×
UNCOV
2451
                        break;
×
2452

UNCOV
2453
                OPTION_COMMON_NO_LEGEND:
×
UNCOV
2454
                        arg_legend = false;
×
UNCOV
2455
                        break;
×
2456

2457
                OPTION_COMMON_JSON:
458✔
2458
                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
458✔
2459
                        if (r <= 0)
458✔
2460
                                return r;
2461

2462
                        break;
2463
                }
2464

2465
        if (arg_image && arg_root)
1,316✔
UNCOV
2466
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
×
2467

2468
        if ((arg_image || arg_root) && arg_reboot)
1,316✔
UNCOV
2469
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
×
2470

2471
        if (arg_reboot && arg_component)
1,316✔
2472
                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✔
2473

2474
        if (arg_definitions && arg_component)
1,315✔
UNCOV
2475
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
×
2476

2477
        r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
1,315✔
2478
        if (r < 0)
1,315✔
UNCOV
2479
                return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
×
2480
        if (r > 0)
1,315✔
2481
                arg_varlink = true;
212✔
2482

2483
        *remaining_args = option_parser_get_args(&opts);
1,315✔
2484
        return 1;
1,315✔
2485
}
2486

2487
static int vl_server(void) {
212✔
2488
        _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
212✔
2489
        _cleanup_(server_done) Server server = SERVER_NULL;
212✔
2490
        int r;
212✔
2491

2492
        r = varlink_server_new(&varlink_server,
212✔
2493
                               SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA,
2494
                               &server);
2495
        if (r < 0)
212✔
UNCOV
2496
                return log_error_errno(r, "Failed to allocate Varlink server: %m");
×
2497

2498
        r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_SysUpdate);
212✔
2499
        if (r < 0)
212✔
UNCOV
2500
                return log_error_errno(r, "Failed to add Varlink interface: %m");
×
2501

2502
        r = sd_varlink_server_bind_method_many(
212✔
2503
                        varlink_server,
2504
                        "io.systemd.SysUpdate.CheckNew", vl_method_check_new);
2505
        if (r < 0)
212✔
UNCOV
2506
                return log_error_errno(r, "Failed to bind Varlink method: %m");
×
2507

2508
        r = sd_varlink_server_loop_auto(varlink_server);
212✔
2509
        if (r < 0)
212✔
UNCOV
2510
                return log_error_errno(r, "Failed to run Varlink event loop: %m");
×
2511

2512
        return 0;
2513
}
2514

2515
static int run(int argc, char *argv[]) {
1,324✔
2516
        int r;
1,324✔
2517

2518
        log_setup();
1,324✔
2519

2520
        char **args = NULL;
1,324✔
2521
        r = parse_argv(argc, argv, &args);
1,324✔
2522
        if (r <= 0)
1,324✔
2523
                return r;
1,324✔
2524

2525
        if (arg_varlink)
1,315✔
2526
                return vl_server(); /* Invocation as Varlink service */
212✔
2527

2528
        return dispatch_verb(args, NULL);
1,103✔
2529
}
2530

2531
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
1,324✔
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