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

systemd / systemd / 29378720657

14 Jul 2026 11:44PM UTC coverage: 72.936% (-0.004%) from 72.94%
29378720657

push

github

yuwata
sd-dhcp-relay: fix off-by-one when discarding BOOTREQUEST messages by hops count

According to RFC specifications
```
RFC 1542 section 4.1.1 states:
The relay agent MUST silently discard BOOTREQUEST messages whose 'hops'
   field exceeds the value 16."
```

"Exceeds the value 16" means hops > 16, i.e. a message that arrives with
hops == 16 is still valid and must be relayed (after which its hops field
becomes 17). The code used ">= 16", which silently dropped a valid message
that had legitimately traversed exactly 16 relay agents, one hop too early.

This matches the wording of the adjacent comment, which already says
"exceeds the value 16".

2 of 2 new or added lines in 2 files covered. (100.0%)

4253 existing lines in 80 files now uncovered.

345834 of 474164 relevant lines covered (72.94%)

1318659.74 hits per line

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

62.99
/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 "condition.h"
13
#include "conf-files.h"
14
#include "conf-parser.h"
15
#include "constants.h"
16
#include "discover-image.h"
17
#include "dissect-image.h"
18
#include "dlopen-note.h"
19
#include "dropin.h"
20
#include "env-util.h"
21
#include "errno-util.h"
22
#include "fd-util.h"
23
#include "format-table.h"
24
#include "glyph-util.h"
25
#include "hashmap.h"
26
#include "help-util.h"
27
#include "hexdecoct.h"
28
#include "image-policy.h"
29
#include "json-util.h"
30
#include "loop-util.h"
31
#include "main-func.h"
32
#include "mount-util.h"
33
#include "options.h"
34
#include "os-util.h"
35
#include "pager.h"
36
#include "parse-argument.h"
37
#include "parse-util.h"
38
#include "path-util.h"
39
#include "pretty-print.h"
40
#include "runtime-scope.h"
41
#include "set.h"
42
#include "siphash24.h"
43
#include "sort-util.h"
44
#include "stdio-util.h"
45
#include "string-util.h"
46
#include "strv.h"
47
#include "sysupdate.h"
48
#include "sysupdate-cleanup.h"
49
#include "sysupdate-config.h"
50
#include "sysupdate-feature.h"
51
#include "sysupdate-instance.h"
52
#include "sysupdate-target.h"
53
#include "sysupdate-transfer.h"
54
#include "sysupdate-update-set.h"
55
#include "sysupdate-util.h"
56
#include "varlink-io.systemd.SysUpdate.h"
57
#include "varlink-util.h"
58
#include "verbs.h"
59

60
static char *arg_definitions = NULL;
61
static bool arg_sync = true;
62
static uint64_t arg_instances_max = UINT64_MAX;
63
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
64
static PagerFlags arg_pager_flags = 0;
65
static bool arg_legend = true;
66
static char *arg_root = NULL;
67
static char *arg_image = NULL;
68
static bool arg_reboot = false;
69
static int arg_cleanup = -1;
70
static SelectMode arg_feature_select = SELECT_EXPLICIT;
71
static char *arg_component = NULL;
72
static SelectMode arg_component_select = SELECT_EXPLICIT;
73
static int arg_verify = -1;
74
static ImagePolicy *arg_image_policy = NULL;
75
static bool arg_offline = false;
76
static char *arg_transfer_source = NULL;
77
static bool arg_varlink = false;
78

79
STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
1,344✔
80
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
1,344✔
81
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
1,344✔
82
STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
1,344✔
83
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
1,344✔
84
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
1,344✔
85

86
#define CONTEXT_NULL                                              \
87
        (Context) {                                               \
88
                .component_enabled = true,                        \
89
                .sync = true,                                     \
90
                .instances_max = UINT64_MAX,                      \
91
                .verify = -1,                                     \
92
                .cleanup = -1,                                    \
93
                .installdb_fd = -EBADF,                           \
94
                .target_identifier.class = _TARGET_CLASS_INVALID, \
95
                .component_suggest = -1,                          \
96
        }
97

98
void context_done(Context *c) {
2,437✔
99
        assert(c);
2,437✔
100

101
        c->mounted_dir = umount_and_rmdir_and_free(c->mounted_dir);
2,437✔
102
        c->loop_device = loop_device_unref(c->loop_device);
2,437✔
103

104
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers)
11,231✔
105
                transfer_free(*tr);
8,794✔
106
        c->transfers = mfree(c->transfers);
2,437✔
107
        c->n_transfers = 0;
2,437✔
108

109
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
3,569✔
110
                transfer_free(*tr);
1,132✔
111
        c->disabled_transfers = mfree(c->disabled_transfers);
2,437✔
112
        c->n_disabled_transfers = 0;
2,437✔
113

114
        c->features = hashmap_free(c->features);
2,437✔
115

116
        FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
7,132✔
117
                update_set_free(*us);
4,695✔
118
        c->update_sets = mfree(c->update_sets);
2,437✔
119
        c->n_update_sets = 0;
2,437✔
120

121
        c->web_cache = hashmap_free(c->web_cache);
2,437✔
122

123
        c->installdb_fd = safe_close(c->installdb_fd);
2,437✔
124

125
        c->definitions = mfree(c->definitions);
2,437✔
126
        c->root = mfree(c->root);
2,437✔
127
        c->image = mfree(c->image);
2,437✔
128
        c->component = mfree(c->component);
2,437✔
129
        c->component_description = mfree(c->component_description);
2,437✔
130
        c->component_documentation = strv_free(c->component_documentation);
2,437✔
131
        c->image_policy = image_policy_free(c->image_policy);
2,437✔
132
        c->transfer_source = mfree(c->transfer_source);
2,437✔
133

134
        target_identifier_done(&c->target_identifier);
2,437✔
135
        condition_free_list(c->component_suggest_on);
2,437✔
136
}
2,437✔
137

138
static int context_from_cmdline(Context *ret) {
1,106✔
139
        assert(ret);
1,106✔
140

141
        _cleanup_(context_done) Context context = CONTEXT_NULL;
1,106✔
142

143
        context.instances_max = arg_instances_max;
1,106✔
144
        context.sync = arg_sync;
1,106✔
145
        context.reboot = arg_reboot;
1,106✔
146
        context.verify = arg_verify;
1,106✔
147
        context.offline = arg_offline;
1,106✔
148
        context.cleanup = arg_cleanup;
1,106✔
149
        context.component_select = arg_component_select;
1,106✔
150
        context.feature_select = arg_feature_select;
1,106✔
151

152
        if (strdup_to(&context.definitions, arg_definitions) < 0)
1,106✔
UNCOV
153
                return log_oom();
×
154

155
        if (strdup_to(&context.root, arg_root) < 0)
1,106✔
UNCOV
156
                return log_oom();
×
157

158
        if (strdup_to(&context.image, arg_image) < 0)
1,106✔
UNCOV
159
                return log_oom();
×
160

161
        if (strdup_to(&context.component, arg_component) < 0)
1,106✔
UNCOV
162
                return log_oom();
×
163

164
        if (strdup_to(&context.transfer_source, arg_transfer_source) < 0)
1,106✔
UNCOV
165
                return log_oom();
×
166

167
        if (arg_image_policy) {
1,106✔
168
                context.image_policy = image_policy_copy(arg_image_policy);
×
UNCOV
169
                if (!context.image_policy)
×
UNCOV
170
                        return log_oom();
×
171
        }
172

173
        *ret = TAKE_GENERIC(context, Context, CONTEXT_NULL);
1,106✔
174
        return 0;
1,106✔
175
}
176

177
static int context_from_base_with_component(const Context *base, const char *component, Context *ret) {
×
178
        assert(base);
×
UNCOV
179
        assert(component);
×
UNCOV
180
        assert(ret);
×
181

182
        /* Copies the specified context, but changes the component to the specified one */
183

184
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
185

186
        context.instances_max = base->instances_max;
×
187
        context.sync = base->sync;
×
188
        context.reboot = base->reboot;
×
189
        context.verify = base->verify;
×
190
        context.offline = base->offline;
×
UNCOV
191
        context.cleanup = base->cleanup;
×
192
        context.feature_select = base->feature_select;
×
193

UNCOV
194
        if (strdup_to(&context.root, base->root) < 0)
×
195
                return log_oom();
×
196

UNCOV
197
        if (strdup_to(&context.image, base->image) < 0)
×
198
                return log_oom();
×
199

UNCOV
200
        if (strdup_to(&context.transfer_source, base->transfer_source) < 0)
×
201
                return log_oom();
×
202

203
        if (base->image_policy) {
×
204
                context.image_policy = image_policy_copy(base->image_policy);
×
UNCOV
205
                if (!context.image_policy)
×
UNCOV
206
                        return log_oom();
×
207
        }
208

UNCOV
209
        if (strdup_to(&context.component, component) < 0)
×
UNCOV
210
                return log_oom();
×
211

212
        /* NB: we do not copy .loop_device/.mounted_dir here, since that's for lifetime tracking only, and
213
         * not needed for access (we only need to copy .root/.image) for that. Under the assumption that the
214
         * contexts initialized via context_from_base_with_component() have a shorter lifetime than the
215
         * contexts they are copied from we don't bother with lifetime tracking of the loopback device/mount
216
         * point here. */
217

UNCOV
218
        *ret = TAKE_GENERIC(context, Context, CONTEXT_NULL);
×
UNCOV
219
        return 0;
×
220
}
221

222
/* Stores any long-running server state which needs to persist between varlink calls, such as state for
223
 * pending polkit requests */
224
typedef struct Server {
225
        sd_bus *system_bus;
226
        Hashmap *polkit_registry;
227
} Server;
228

229
#define SERVER_NULL \
230
        (Server) { \
231
                /* all fields fine with being initialised to NULL */ \
232
        }
233

234
static void server_done(Server *s) {
233✔
235
        assert(s);
233✔
236

237
        s->polkit_registry = hashmap_free(s->polkit_registry);
233✔
238
        s->system_bus = sd_bus_flush_close_unref(s->system_bus);
233✔
239
}
233✔
240

UNCOV
241
static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free);
×
242

243
static int read_features(
1,307✔
244
                Context *c,
245
                const char **dirs) {
246

247
        int r;
1,307✔
248

249
        assert(c);
1,307✔
250

251
        ConfFile **files = NULL;
1,307✔
252
        size_t n_files = 0;
1,307✔
253
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
1,307✔
254

255
        r = conf_files_list_strv_full(
2,614✔
256
                        ".feature",
257
                        c->root,
1,307✔
258
                        CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
259
                        dirs,
260
                        &files,
261
                        &n_files);
262
        if (r < 0)
1,307✔
UNCOV
263
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
×
264

265
        FOREACH_ARRAY(i, files, n_files) {
2,539✔
266
                ConfFile *e = *i;
1,232✔
267

268
                _cleanup_(feature_unrefp) Feature *f = feature_new();
1,232✔
269
                if (!f)
1,232✔
UNCOV
270
                        return log_oom();
×
271

272
                r = feature_read_definition(f, c->root, e->result, dirs);
1,232✔
273
                if (r < 0)
1,232✔
274
                        return r;
275

276
                r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
1,232✔
277
                if (r < 0)
1,232✔
UNCOV
278
                        return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
×
279

280
                TAKE_PTR(f);
1,232✔
281
        }
282

283
        return 0;
284
}
285

286
static int read_transfers(
1,319✔
287
                Context *c,
288
                const char **dirs,
289
                const char *suffix,
290
                const char *node) {
291

292
        ConfFile **files = NULL;
1,319✔
293
        Transfer **transfers = NULL, **disabled = NULL;
1,319✔
294
        size_t n_files = 0, n_transfers = 0, n_disabled = 0;
1,319✔
295
        int r;
1,319✔
296

297
        CLEANUP_ARRAY(files, n_files, conf_file_free_array);
1,319✔
298
        CLEANUP_ARRAY(transfers, n_transfers, transfer_free_array);
1,319✔
299
        CLEANUP_ARRAY(disabled, n_disabled, transfer_free_array);
1,319✔
300

301
        assert(c);
1,319✔
302
        assert(dirs);
1,319✔
303
        assert(suffix);
1,319✔
304

305
        r = conf_files_list_strv_full(suffix, c->root,
1,319✔
306
                                      CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
307
                                      dirs, &files, &n_files);
308
        if (r < 0)
1,319✔
UNCOV
309
                return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
×
310

311
        FOREACH_ARRAY(i, files, n_files) {
11,245✔
UNCOV
312
                _cleanup_(transfer_freep) Transfer *t = NULL;
×
313
                Transfer **appended;
9,926✔
314
                ConfFile *e = *i;
9,926✔
315

316
                t = transfer_new(c);
9,926✔
317
                if (!t)
9,926✔
UNCOV
318
                        return log_oom();
×
319

320
                r = transfer_read_definition(t, e->result, dirs, c->features);
9,926✔
321
                if (r < 0)
9,926✔
322
                        return r;
323

324
                r = transfer_resolve_paths(t, c->root, node);
9,926✔
325
                if (r < 0)
9,926✔
326
                        return r;
327

328
                if (t->enabled)
9,926✔
329
                        appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
8,794✔
330
                else
331
                        appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
1,132✔
332
                if (!appended)
9,926✔
UNCOV
333
                        return log_oom();
×
334
                TAKE_PTR(t);
9,926✔
335
        }
336

337
        c->transfers = TAKE_PTR(transfers);
1,319✔
338
        c->n_transfers = n_transfers;
1,319✔
339
        c->disabled_transfers = TAKE_PTR(disabled);
1,319✔
340
        c->n_disabled_transfers = n_disabled;
1,319✔
341
        return 0;
1,319✔
342
}
343

344
static int read_component(Context *c) {
1,307✔
345
        int r;
1,307✔
346

347
        assert(c);
1,307✔
348

349
        /* Read a component description file, but only if we actually operate on a component */
350
        if (c->definitions || !c->component)
1,307✔
351
                return 0;
1,307✔
352

353
        _cleanup_free_ char *j = strjoin("sysupdate.", c->component, ".component");
76✔
354
        if (!j)
38✔
UNCOV
355
                return log_oom();
×
356

357
        ConfigTableItem table[] = {
38✔
358
                { "Component", "Description",                config_parse_string,              0,                             &c->component_description   },
38✔
359
                { "Component", "Documentation",              config_parse_url_specifiers_many, 0,                             &c->component_documentation },
38✔
360
                { "Component", "Enabled",                    config_parse_bool,                0,                             &c->component_enabled       },
38✔
361
                { "Component", "Suggest",                    config_parse_tristate,            0,                             &c->component_suggest       },
38✔
362
                { "Component", "SuggestOnArchitecture",      config_parse_condition,           CONDITION_ARCHITECTURE,        &c->component_suggest_on    },
38✔
363
                { "Component", "SuggestOnFirmware",          config_parse_condition,           CONDITION_FIRMWARE,            &c->component_suggest_on    },
364
                { "Component", "SuggestOnVirtualization",    config_parse_condition,           CONDITION_VIRTUALIZATION,      &c->component_suggest_on    },
365
                { "Component", "SuggestOnHost",              config_parse_condition,           CONDITION_HOST,                &c->component_suggest_on    },
366
                { "Component", "SuggestOnFraction",          config_parse_condition,           CONDITION_FRACTION,            &c->component_suggest_on    },
367
                { "Component", "SuggestOnKernelCommandLine", config_parse_condition,           CONDITION_KERNEL_COMMAND_LINE, &c->component_suggest_on    },
368
                { "Component", "SuggestOnVersion",           config_parse_condition,           CONDITION_VERSION,             &c->component_suggest_on    },
369
                { "Component", "SuggestOnCredential",        config_parse_condition,           CONDITION_CREDENTIAL,          &c->component_suggest_on    },
370
                { "Component", "SuggestOnSecurity",          config_parse_condition,           CONDITION_SECURITY,            &c->component_suggest_on    },
371
                { "Component", "SuggestOnOSRelease",         config_parse_condition,           CONDITION_OS_RELEASE,          &c->component_suggest_on    },
372
                { "Component", "SuggestOnMachineTag",        config_parse_condition,           CONDITION_MACHINE_TAG,         &c->component_suggest_on    },
373
                {}
374
        };
375

376
        r = config_parse_standard_file_with_dropins_full(
76✔
377
                        c->root,
378
                        /* root_fd= */ -EBADF,
379
                        j,
380
                        "Component\0",
381
                        config_item_table_lookup, table,
382
                        CONFIG_PARSE_WARN,
383
                        /* userdata= */ (void *) c->root,
38✔
384
                        /* ret_stats_by_path= */ NULL,
385
                        /* ret_dropin_files= */ NULL);
386
        if (r < 0)
38✔
UNCOV
387
                return r;
×
388

389
        return 0;
390
}
391

392
typedef enum ReadDefinitionsFlags {
393
        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, /* fail unless there's at least one enabled transfer */
394
        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS     = 1 << 1, /* fail unless there's at least one transfer */
395
        READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT = 1 << 2, /* fail if component is disabled */
396
} ReadDefinitionsFlags;
397

398
static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) {
1,307✔
399
        _cleanup_strv_free_ char **dirs = NULL;
1,307✔
400
        int r;
1,307✔
401

402
        assert(c);
1,307✔
403

404
        if (c->definitions)
1,307✔
405
                dirs = strv_new(c->definitions);
5✔
406
        else if (c->component) {
1,302✔
407
                char **l = CONF_PATHS_STRV("");
38✔
408
                size_t i = 0;
38✔
409

410
                dirs = new0(char*, strv_length(l) + 1);
38✔
411
                if (!dirs)
38✔
UNCOV
412
                        return log_oom();
×
413

414
                STRV_FOREACH(dir, l) {
190✔
415
                        char *j;
152✔
416

417
                        j = strjoin(*dir, "sysupdate.", c->component, ".d");
152✔
418
                        if (!j)
152✔
UNCOV
419
                                return log_oom();
×
420

421
                        dirs[i++] = j;
152✔
422
                }
423
        } else
424
                dirs = strv_new(CONF_PATHS("sysupdate.d"));
1,264✔
425
        if (!dirs)
1,307✔
UNCOV
426
                return log_oom();
×
427

428
        r = read_component(c);
1,307✔
429
        if (r < 0)
1,307✔
430
                return r;
431

432
        r = read_features(c, (const char**) dirs);
1,307✔
433
        if (r < 0)
1,307✔
434
                return r;
435

436
        r = read_transfers(c, (const char**) dirs, ".transfer", node);
1,307✔
437
        if (r < 0)
1,307✔
438
                return r;
439

440
        if (c->n_transfers + c->n_disabled_transfers == 0) {
1,307✔
441
                /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
442
                r = read_transfers(c, (const char**) dirs, ".conf", node);
12✔
443
                if (r < 0)
12✔
444
                        return r;
445

446
                if (c->n_transfers + c->n_disabled_transfers > 0)
12✔
UNCOV
447
                        log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
×
448
        }
449

450
        if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS) &&
1,307✔
451
            c->n_transfers + (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS) ? 0 : c->n_disabled_transfers) == 0) {
1,118✔
UNCOV
452
                if (c->component)
×
UNCOV
453
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
454
                                               "No transfer definitions for component '%s' found.",
455
                                               c->component);
456

UNCOV
457
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
458
                                       "No transfer definitions found.");
459
        }
460

461
        if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT) && !c->component_enabled)
1,307✔
UNCOV
462
                return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Component is disabled.");
×
463

464
        return 0;
465
}
466

467
static int context_load_installed_instances(Context *c) {
1,307✔
468
        int r;
1,307✔
469

470
        assert(c);
1,307✔
471

472
        log_debug("Discovering installed instances%s", glyph(GLYPH_ELLIPSIS));
2,410✔
473

474
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
10,101✔
475
                Transfer *t = *tr;
8,794✔
476

477
                r = resource_load_instances(
8,794✔
478
                                &t->target,
479
                                c->verify >= 0 ? c->verify : t->verify,
8,794✔
480
                                &c->web_cache);
481
                if (r < 0)
8,794✔
482
                        return r;
483
        }
484

485
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
2,439✔
486
                Transfer *t = *tr;
1,132✔
487

488
                r = resource_load_instances(
1,132✔
489
                                &t->target,
490
                                c->verify >= 0 ? c->verify : t->verify,
1,132✔
491
                                &c->web_cache);
492
                if (r < 0)
1,132✔
493
                        return r;
494
        }
495

496
        return 0;
497
}
498

499
static int context_load_available_instances(Context *c) {
840✔
500
        int r;
840✔
501

502
        assert(c);
840✔
503

504
        log_info("Discovering available instances%s", glyph(GLYPH_ELLIPSIS));
1,680✔
505

506
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
6,438✔
507
                Transfer *t = *tr;
5,618✔
508

509
                r = resource_load_instances(
5,618✔
510
                                &t->source,
511
                                c->verify >= 0 ? c->verify : t->verify,
5,618✔
512
                                &c->web_cache);
513
                if (r < 0)
5,618✔
514
                        return r;
515
        }
516

517
        return 0;
518
}
519

520
static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
1,852✔
521
        _cleanup_free_ char *boundary = NULL;
1,852✔
522
        bool newest_found = false;
1,852✔
523
        int r;
1,852✔
524

525
        assert(c);
1,852✔
526
        assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
1,852✔
527

528
        for (;;) {
9,845✔
UNCOV
529
                _cleanup_free_ Instance **cursor_instances = NULL;
×
530
                bool skip = false;
9,845✔
531
                UpdateSetFlags extra_flags = 0;
9,845✔
532
                _cleanup_free_ char *cursor = NULL;
7,993✔
533
                UpdateSet *us = NULL;
9,845✔
534

535
                /* First, let's find the newest version that's older than the boundary. */
536
                FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
72,287✔
537
                        Resource *rr;
63,262✔
538

539
                        assert(*tr);
63,262✔
540

541
                        if (flags == UPDATE_AVAILABLE)
63,262✔
542
                                rr = &(*tr)->source;
32,238✔
543
                        else {
544
                                assert(flags == UPDATE_INSTALLED);
31,024✔
545
                                rr = &(*tr)->target;
31,024✔
546
                        }
547

548
                        FOREACH_ARRAY(inst, rr->instances, rr->n_instances) {
198,653✔
549
                                Instance *i = *inst; /* Sorted newest-to-oldest */
183,426✔
550

551
                                assert(i);
183,426✔
552

553
                                if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
183,426✔
554
                                        continue; /* Not older than the boundary */
135,391✔
555

556
                                if (cursor && strverscmp_improved(i->metadata.version, cursor) <= 0)
48,035✔
557
                                        break; /* Not newer than the cursor. The same will be true for all
558
                                                * subsequent instances (due to sorting) so let's skip to the
559
                                                * next transfer. */
560

561
                                if (free_and_strdup(&cursor, i->metadata.version) < 0)
7,993✔
UNCOV
562
                                        return log_oom();
×
563

564
                                break; /* All subsequent instances will be older than this one */
565
                        }
566

567
                        if (flags == UPDATE_AVAILABLE && !cursor)
63,262✔
568
                                break; /* This transfer didn't have a version older than the boundary,
569
                                        * so any older-than-boundary version that might exist in a different
570
                                        * transfer must always be incomplete. For reasons described below,
571
                                        * we don't include incomplete versions for AVAILABLE updates. So we
572
                                        * are completely done looking. */
573
                }
574

575
                if (!cursor) /* We didn't find anything older than the boundary, so we're done. */
9,845✔
576
                        break;
577

578
                cursor_instances = new0(Instance*, c->n_transfers);
7,993✔
579
                if (!cursor_instances)
7,993✔
UNCOV
580
                        return log_oom();
×
581

582
                /* Now let's find all the instances that match the version of the cursor, if we have them */
583
                for (size_t k = 0; k < c->n_transfers; k++) {
61,486✔
584
                        Transfer *t = c->transfers[k];
54,122✔
585
                        Instance *match = NULL;
54,122✔
586

587
                        assert(t);
54,122✔
588

589
                        if (flags == UPDATE_AVAILABLE) {
54,122✔
590
                                match = resource_find_instance(&t->source, cursor);
30,108✔
591
                                if (!match) {
30,108✔
592
                                        /* When we're looking for updates to download, we don't offer
593
                                         * incomplete versions at all. The server wants to send us an update
594
                                         * with parts of the OS missing. For robustness sake, let's not do
595
                                         * that. */
596
                                        skip = true;
597
                                        break;
598
                                }
599
                        } else {
600
                                assert(flags == UPDATE_INSTALLED);
24,014✔
601

602
                                match = resource_find_instance(&t->target, cursor);
24,014✔
603
                                if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING)))
24,014✔
604
                                        /* When we're looking for installed versions, let's be robust and treat
605
                                         * an incomplete installation as an installation. Otherwise, there are
606
                                         * situations that can lead to sysupdate wiping the currently booted OS.
607
                                         * See https://github.com/systemd/systemd/issues/33339 */
608
                                        extra_flags |= UPDATE_INCOMPLETE;
7,444✔
609
                        }
610

611
                        cursor_instances[k] = match;
53,493✔
612

613
                        if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
53,493✔
UNCOV
614
                                extra_flags |= UPDATE_OBSOLETE;
×
615

616
                        if (strv_contains(t->protected_versions, cursor))
53,493✔
UNCOV
617
                                extra_flags |= UPDATE_PROTECTED;
×
618

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

624
                        if (match && match->is_partial)
45,869✔
625
                                extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
36✔
626

627
                        if (match && match->is_pending)
45,869✔
628
                                extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
748✔
629
                }
630

631
                r = free_and_strdup_warn(&boundary, cursor);
7,993✔
632
                if (r < 0)
7,993✔
633
                        return r;
634

635
                if (skip)
7,993✔
636
                        continue;
629✔
637

638
                /* See if we already have this update set in our table */
639
                FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
21,164✔
640
                        UpdateSet *u = *update_set;
16,469✔
641

642
                        if (strverscmp_improved(u->version, cursor) != 0)
16,469✔
643
                                continue;
13,800✔
644

645
                        /* Merge in what we've learned and continue onto the next version */
646

647
                        if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
2,669✔
648
                            FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
1,325✔
649
                            FLAGS_SET(u->flags, UPDATE_PENDING)) {
1,309✔
650
                                assert(u->n_instances == c->n_transfers);
1,392✔
651

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

655
                                for (size_t j = 0; j < u->n_instances; j++) {
11,328✔
656
                                        if (!u->instances[j])
9,936✔
657
                                                u->instances[j] = cursor_instances[j];
5,792✔
658

659
                                        /* Make sure that the list is full if the update is AVAILABLE */
660
                                        assert(flags != UPDATE_AVAILABLE || u->instances[j]);
9,936✔
661
                                }
662
                        }
663

664
                        u->flags |= flags | extra_flags;
2,669✔
665

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

673
                        skip = true;
674
                        newest_found = true;
675
                        break;
676
                }
677

678
                if (skip)
64✔
679
                        continue;
2,669✔
680

681
                /* Doesn't exist yet, let's add it */
682
                if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
4,695✔
UNCOV
683
                        return log_oom();
×
684

685
                us = new(UpdateSet, 1);
4,695✔
686
                if (!us)
4,695✔
UNCOV
687
                        return log_oom();
×
688

689
                *us = (UpdateSet) {
4,695✔
690
                        .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
4,695✔
691
                        .version = TAKE_PTR(cursor),
4,695✔
692
                        .instances = TAKE_PTR(cursor_instances),
4,695✔
693
                        .n_instances = c->n_transfers,
4,695✔
694
                };
695

696
                c->update_sets[c->n_update_sets++] = us;
4,695✔
697

698
                newest_found = true;
4,695✔
699

700
                /* Remember which one is the newest installed */
701
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
4,695✔
702
                        c->newest_installed = us;
985✔
703

704
                /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
705
                 * It may be partial or pending. */
706
                if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
4,695✔
707
                        c->candidate = us;
321✔
708
        }
709

710
        /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
711
        if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) &&
1,852✔
712
            c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
1,598✔
713
                c->candidate = NULL;
48✔
714

715
        /* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */
716
        if (c->newest_installed &&
1,852✔
717
            (c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) &&
1,758✔
718
            !c->candidate)
196✔
719
                c->candidate = c->newest_installed;
196✔
720

721
        return 0;
722
}
723

724
static int context_discover_update_sets(Context *c) {
1,032✔
725
        int r;
1,032✔
726

727
        assert(c);
1,032✔
728

729
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
2,064✔
730

731
        r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
1,032✔
732
        if (r < 0)
1,032✔
733
                return r;
734

735
        if (!c->offline) {
1,032✔
736
                log_info("Determining available update sets%s", glyph(GLYPH_ELLIPSIS));
1,640✔
737

738
                r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
820✔
739
                if (r < 0)
820✔
740
                        return r;
741
        }
742

743
        typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
1,032✔
744
        return 0;
1,032✔
745
}
746

747
static int context_show_table(Context *c) {
×
UNCOV
748
        _cleanup_(table_unrefp) Table *t = NULL;
×
749
        int r;
×
750

751
        assert(c);
×
752

753
        t = table_new("", "version", "installed", "available", "assessment");
×
UNCOV
754
        if (!t)
×
755
                return log_oom();
×
756

757
        (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
×
UNCOV
758
        (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
×
759
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
×
760

761
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
×
UNCOV
762
                UpdateSet *us = *update_set;
×
763
                const char *color;
×
764

765
                color = update_set_flags_to_color(us->flags);
×
766

UNCOV
767
                r = table_add_many(t,
×
768
                                   TABLE_STRING,    update_set_flags_to_glyph(us->flags),
769
                                   TABLE_SET_COLOR, color,
770
                                   TABLE_STRING,    us->version,
771
                                   TABLE_SET_COLOR, color,
772
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
773
                                   TABLE_SET_COLOR, color,
774
                                   TABLE_STRING,    glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
775
                                   TABLE_SET_COLOR, color,
776
                                   TABLE_STRING,    update_set_flags_to_string(us->flags),
777
                                   TABLE_SET_COLOR, color);
UNCOV
778
                if (r < 0)
×
UNCOV
779
                        return table_log_add_error(r);
×
780
        }
781

UNCOV
782
        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
×
783
}
784

785
static UpdateSet* context_update_set_by_version(Context *c, const char *version) {
209✔
786
        assert(c);
209✔
787
        assert(version);
209✔
788

789
        FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets)
369✔
790
                if (streq((*update_set)->version, version))
369✔
791
                        return *update_set;
792

793
        return NULL;
794
}
795

796
static int context_show_version(Context *c, const char *version) {
209✔
797
        bool show_fs_columns = false, show_partition_columns = false,
209✔
798
                have_fs_attributes = false, have_partition_attributes = false,
209✔
799
                have_size = false, have_tries = false, have_no_auto = false,
209✔
800
                have_read_only = false, have_growfs = false, have_sha256 = false;
209✔
801
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
209✔
802
        _cleanup_(table_unrefp) Table *t = NULL;
209✔
803
        _cleanup_strv_free_ char **changelog_urls = NULL;
209✔
804
        UpdateSet *us;
209✔
805
        int r;
209✔
806

807
        assert(c);
209✔
808
        assert(version);
209✔
809

810
        us = context_update_set_by_version(c, version);
209✔
811
        if (!us)
209✔
UNCOV
812
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
813

814
        if (arg_json_format_flags & (SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO))
209✔
815
                pager_open(arg_pager_flags);
113✔
816

817
        t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
209✔
818
        if (!t)
209✔
UNCOV
819
                return log_oom();
×
820

821
        (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
209✔
822
        (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
209✔
823
        (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
209✔
824
        (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
209✔
825
        (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
209✔
826
        (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
209✔
827
        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
209✔
828

829
        /* Starting in v257, these fields would be automatically formatted with underscores. This would have
830
         * been a breaking change, so to avoid that let's hard-code their original names. */
831
        (void) table_set_json_field_name(t, 7, "tries-done");
209✔
832
        (void) table_set_json_field_name(t, 8, "tries-left");
209✔
833

834
        /* Determine if the target will make use of partition/fs attributes for any of the transfers */
835
        FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
1,698✔
836
                Transfer *tr = *transfer;
1,489✔
837

838
                if (tr->target.type == RESOURCE_PARTITION)
1,489✔
839
                        show_partition_columns = true;
416✔
840
                if (RESOURCE_IS_FILESYSTEM(tr->target.type))
1,489✔
841
                        show_fs_columns = true;
1,073✔
842

843
                STRV_FOREACH(changelog, tr->changelog) {
1,489✔
UNCOV
844
                        assert(*changelog);
×
845

UNCOV
846
                        _cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
×
UNCOV
847
                        if (!changelog_url)
×
UNCOV
848
                                return log_oom();
×
849

850
                        /* Avoid duplicates */
UNCOV
851
                        if (strv_contains(changelog_urls, changelog_url))
×
UNCOV
852
                                continue;
×
853

854
                        /* changelog_urls takes ownership of expanded changelog_url */
UNCOV
855
                        r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
×
UNCOV
856
                        if (r < 0)
×
857
                                return log_oom();
×
858
                }
859
        }
860

861
        FOREACH_ARRAY(inst, us->instances, us->n_instances) {
1,698✔
862
                Instance *i = *inst;
1,489✔
863

864
                if (!i) {
1,489✔
865
                        assert(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING));
112✔
866
                        continue;
112✔
867
                }
868

869
                r = table_add_many(t,
1,377✔
870
                                   TABLE_STRING, resource_type_to_string(i->resource->type),
871
                                   TABLE_PATH, i->path);
872
                if (r < 0)
1,377✔
UNCOV
873
                        return table_log_add_error(r);
×
874

875
                if (i->metadata.partition_uuid_set) {
1,377✔
876
                        have_partition_attributes = true;
288✔
877
                        r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
288✔
878
                } else
879
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,089✔
880
                if (r < 0)
1,377✔
UNCOV
881
                        return table_log_add_error(r);
×
882

883
                if (i->metadata.partition_flags_set) {
1,377✔
884
                        have_partition_attributes = true;
288✔
885
                        r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
288✔
886
                } else
887
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,089✔
888
                if (r < 0)
1,377✔
UNCOV
889
                        return table_log_add_error(r);
×
890

891
                if (i->metadata.mtime != USEC_INFINITY) {
1,377✔
892
                        have_fs_attributes = true;
1,057✔
893
                        r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
1,057✔
894
                } else
895
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
320✔
896
                if (r < 0)
1,377✔
UNCOV
897
                        return table_log_add_error(r);
×
898

899
                if (i->metadata.mode != MODE_INVALID) {
1,377✔
900
                        have_fs_attributes = true;
1,057✔
901
                        r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
1,057✔
902
                } else
903
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
320✔
904
                if (r < 0)
1,377✔
UNCOV
905
                        return table_log_add_error(r);
×
906

907
                if (i->metadata.size != UINT64_MAX) {
1,377✔
UNCOV
908
                        have_size = true;
×
UNCOV
909
                        r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
×
910
                } else
911
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,377✔
912
                if (r < 0)
1,377✔
UNCOV
913
                        return table_log_add_error(r);
×
914

915
                if (i->metadata.tries_done != UINT64_MAX) {
1,377✔
916
                        have_tries = true;
128✔
917
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
128✔
918
                } else
919
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,249✔
920
                if (r < 0)
1,377✔
921
                        return table_log_add_error(r);
×
922

923
                if (i->metadata.tries_left != UINT64_MAX) {
1,377✔
924
                        have_tries = true;
128✔
925
                        r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
128✔
926
                } else
927
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,249✔
928
                if (r < 0)
1,377✔
UNCOV
929
                        return table_log_add_error(r);
×
930

931
                if (i->metadata.no_auto >= 0) {
1,377✔
UNCOV
932
                        bool b;
×
933

934
                        have_no_auto = true;
×
UNCOV
935
                        b = i->metadata.no_auto;
×
UNCOV
936
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
937
                } else
938
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,377✔
939
                if (r < 0)
1,377✔
UNCOV
940
                        return table_log_add_error(r);
×
941
                if (i->metadata.read_only >= 0) {
1,377✔
942
                        bool b;
288✔
943

944
                        have_read_only = true;
288✔
945
                        b = i->metadata.read_only;
288✔
946
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
288✔
947
                } else
948
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,089✔
949
                if (r < 0)
1,377✔
UNCOV
950
                        return table_log_add_error(r);
×
951

952
                if (i->metadata.growfs >= 0) {
1,377✔
953
                        bool b;
×
954

UNCOV
955
                        have_growfs = true;
×
UNCOV
956
                        b = i->metadata.growfs;
×
UNCOV
957
                        r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
×
958
                } else
959
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,377✔
960
                if (r < 0)
1,377✔
UNCOV
961
                        return table_log_add_error(r);
×
962

963
                if (i->metadata.sha256sum_set) {
1,377✔
964
                        _cleanup_free_ char *formatted = NULL;
32✔
965

966
                        have_sha256 = true;
32✔
967

968
                        formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
32✔
969
                        if (!formatted)
32✔
970
                                return log_oom();
×
971

972
                        r = table_add_cell(t, NULL, TABLE_STRING, formatted);
32✔
973
                } else
974
                        r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
1,345✔
975
                if (r < 0)
1,377✔
UNCOV
976
                        return table_log_add_error(r);
×
977
        }
978

979
        /* Hide the fs/partition columns if we don't have any data to show there */
980
        if (!have_fs_attributes)
209✔
981
                show_fs_columns = false;
16✔
982
        if (!have_partition_attributes)
209✔
983
                show_partition_columns = false;
984

985
        if (!show_partition_columns)
144✔
986
                (void) table_hide_column_from_display(t, 2, 3);
65✔
987
        if (!show_fs_columns)
209✔
988
                (void) table_hide_column_from_display(t, 4, 5);
16✔
989
        if (!have_size)
209✔
990
                (void) table_hide_column_from_display(t, 6);
209✔
991
        if (!have_tries)
209✔
992
                (void) table_hide_column_from_display(t, 7, 8);
81✔
993
        if (!have_no_auto)
209✔
994
                (void) table_hide_column_from_display(t, 9);
209✔
995
        if (!have_read_only)
209✔
996
                (void) table_hide_column_from_display(t, 10);
65✔
997
        if (!have_growfs)
209✔
998
                (void) table_hide_column_from_display(t, 11);
209✔
999
        if (!have_sha256)
209✔
1000
                (void) table_hide_column_from_display(t, 12);
193✔
1001

1002
        if (!sd_json_format_enabled(arg_json_format_flags)) {
209✔
1003
                printf("%s%s%s Version: %s\n"
1,017✔
1004
                       "    State: %s%s%s\n"
1005
                       "Installed: %s%s%s%s%s%s%s\n"
1006
                       "Available: %s%s\n"
1007
                       "Protected: %s%s%s\n"
1008
                       " Obsolete: %s%s%s\n",
1009
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
113✔
1010
                       strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
113✔
1011
                       yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
129✔
1012
                       FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
145✔
1013
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
210✔
1014
                       yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
210✔
1015
                       FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
113✔
1016
                       us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
113✔
1017

1018
                STRV_FOREACH(url, changelog_urls) {
113✔
UNCOV
1019
                        _cleanup_free_ char *changelog_link = NULL;
×
UNCOV
1020
                        r = terminal_urlify(*url, NULL, &changelog_link);
×
UNCOV
1021
                        if (r < 0)
×
UNCOV
1022
                                return log_oom();
×
UNCOV
1023
                        printf("ChangeLog: %s\n", changelog_link);
×
1024
                }
1025
                printf("\n");
113✔
1026

1027
                return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
113✔
1028
        } else {
1029
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
96✔
1030

1031
                r = table_to_json(t, &t_json);
96✔
1032
                if (r < 0)
96✔
1033
                        return log_error_errno(r, "failed to convert table to JSON: %m");
×
1034

1035
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("version", us->version),
96✔
1036
                                          SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)),
1037
                                          SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
1038
                                          SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
1039
                                          SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)),
1040
                                          SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)),
1041
                                          SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
1042
                                          SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
1043
                                          SD_JSON_BUILD_PAIR_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)),
1044
                                          SD_JSON_BUILD_PAIR_STRV("changelogUrls", changelog_urls),
1045
                                          SD_JSON_BUILD_PAIR_VARIANT("contents", t_json));
1046
                if (r < 0)
96✔
UNCOV
1047
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1048

1049
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
96✔
1050
                if (r < 0)
96✔
UNCOV
1051
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1052

1053
                return 0;
1054
        }
1055
}
1056

1057
static int context_vacuum(
224✔
1058
                Context *c,
1059
                uint64_t space,
1060
                const char *extra_protected_version) {
1061

1062
        size_t disabled_count = 0;
224✔
1063
        int r, count = 0;
224✔
1064

1065
        assert(c);
224✔
1066

1067
        if (space == 0)
224✔
1068
                log_info("Making room%s", glyph(GLYPH_ELLIPSIS));
64✔
1069
        else
1070
                log_info("Making room for %" PRIu64 " updates%s", space, glyph(GLYPH_ELLIPSIS));
384✔
1071

1072
        FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
1,718✔
1073
                Transfer *t = *tr;
1,494✔
1074

1075
                /* Don't bother clearing out space if we're not going to be downloading anything */
1076
                if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version))
1,494✔
1077
                        continue;
208✔
1078

1079
                r = transfer_vacuum(t, space, extra_protected_version);
1,286✔
1080
                if (r < 0)
1,286✔
1081
                        return r;
1082

1083
                count = MAX(count, r);
1,286✔
1084
        }
1085

1086
        FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
416✔
1087
                r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
192✔
1088
                if (r < 0)
192✔
1089
                        return r;
1090
                if (r > 0)
192✔
1091
                        disabled_count++;
16✔
1092
        }
1093

1094
        if (!sd_json_format_enabled(arg_json_format_flags)) {
224✔
1095
                if (count > 0 && disabled_count > 0)
172✔
UNCOV
1096
                        log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
×
1097
                else if (count > 0)
172✔
1098
                        log_info("Removed %i instances.", count);
76✔
1099
                else if (disabled_count > 0)
96✔
1100
                        log_info("Removed %zu disabled transfers.", disabled_count);
16✔
1101
                else
1102
                        log_info("Found nothing to remove.");
80✔
1103
        } else {
1104
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
52✔
1105

1106
                r = sd_json_buildo(&json,
52✔
1107
                                   SD_JSON_BUILD_PAIR_INTEGER("removed", count),
1108
                                   SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
1109
                if (r < 0)
52✔
UNCOV
1110
                        return log_error_errno(r, "Failed to create JSON: %m");
×
1111

1112
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
52✔
1113
                if (r < 0)
52✔
UNCOV
1114
                        return log_error_errno(r, "Failed to print JSON: %m");
×
1115
        }
1116

1117
        return 0;
1118
}
1119

1120
typedef enum ProcessImageFlags {
1121
        PROCESS_IMAGE_READ_ONLY = 1 << 0,
1122
} ProcessImageFlags;
1123

1124
static int context_process_image(
1,307✔
1125
                Context *c,
1126
                ProcessImageFlags flags) {
1127

1128
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
1,307✔
1129
        _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
1,307✔
1130
        int r;
1,307✔
1131

1132
        assert(c);
1,307✔
1133

1134
        if (c->root || !c->image) /* Idempotent or nothing to do */
1,307✔
1135
                return 0;
1136

UNCOV
1137
        assert(!c->mounted_dir);
×
UNCOV
1138
        assert(!c->loop_device);
×
1139

UNCOV
1140
        r = mount_image_privately_interactively(
×
1141
                        c->image,
UNCOV
1142
                        c->image_policy,
×
UNCOV
1143
                        (FLAGS_SET(flags, PROCESS_IMAGE_READ_ONLY) ? DISSECT_IMAGE_READ_ONLY : 0) |
×
1144
                        DISSECT_IMAGE_FSCK |
1145
                        DISSECT_IMAGE_MKDIR |
1146
                        DISSECT_IMAGE_GROWFS |
1147
                        DISSECT_IMAGE_RELAX_VAR_CHECK |
1148
                        DISSECT_IMAGE_USR_NO_ROOT |
1149
                        DISSECT_IMAGE_GENERIC_ROOT |
1150
                        DISSECT_IMAGE_REQUIRE_ROOT |
1151
                        DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
1152
                        &mounted_dir,
1153
                        /* ret_dir_fd= */ NULL,
1154
                        &loop_device);
1155
        if (r < 0)
×
1156
                return r;
1157

UNCOV
1158
        c->root = strdup(mounted_dir);
×
UNCOV
1159
        if (!c->root)
×
UNCOV
1160
                return log_oom();
×
1161

UNCOV
1162
        c->mounted_dir = TAKE_PTR(mounted_dir);
×
UNCOV
1163
        c->loop_device = TAKE_PTR(loop_device);
×
1164

UNCOV
1165
        return 0;
×
1166
}
1167

1168
static int context_list_components(Context *context, char ***ret_component_names, bool *ret_has_default_component);
1169

1170
static int context_load_offline(
1,307✔
1171
                Context *context,
1172
                ProcessImageFlags process_image_flags,
1173
                ReadDefinitionsFlags read_definitions_flags) {
1174
        int r;
1,307✔
1175

1176
        assert(context);
1,307✔
1177

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

1181
        r = context_process_image(context, process_image_flags);
1,307✔
1182
        if (r < 0)
1,307✔
1183
                return r;
1184

1185
        r = context_read_definitions(context, context->loop_device ? context->loop_device->node : NULL, read_definitions_flags);
1,307✔
1186
        if (r < 0)
1,307✔
1187
                return r;
1188

1189
        r = context_load_installed_instances(context);
1,307✔
1190
        if (r < 0)
1,307✔
UNCOV
1191
                return r;
×
1192

1193
        return 0;
1194
}
1195

1196
static int context_load_online(
1,052✔
1197
                Context *context,
1198
                ProcessImageFlags process_image_flags,
1199
                ReadDefinitionsFlags read_definitions_flags) {
1200

1201
        int r;
1,052✔
1202

1203
        assert(context);
1,052✔
1204

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

1208
        r = context_load_offline(
1,052✔
1209
                        context,
1210
                        process_image_flags,
1211
                        read_definitions_flags);
1212
        if (r < 0)
1,052✔
1213
                return r;
1214

1215
        if (!context->offline) {
1,052✔
1216
                r = context_load_available_instances(context);
840✔
1217
                if (r < 0)
840✔
1218
                        return r;
1219
        }
1220

1221
        r = context_discover_update_sets(context);
1,032✔
1222
        if (r < 0)
1,032✔
UNCOV
1223
                return r;
×
1224

1225
        return 0;
1226
}
1227

UNCOV
1228
static bool image_type_can_sysupdate(ImageType image_type) {
×
1229
        /* systemd-sysupdate doesn't support mstack images yet */
UNCOV
1230
        return IN_SET(image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME, IMAGE_RAW, IMAGE_BLOCK);
×
1231
}
1232

UNCOV
1233
static int context_load_paths_from_image(Context *context, Image *image) {
×
UNCOV
1234
        assert(context);
×
UNCOV
1235
        assert(image);
×
1236

UNCOV
1237
        assert(!context->root);
×
UNCOV
1238
        assert(!context->image);
×
1239

UNCOV
1240
        switch (image->type) {
×
1241
        case IMAGE_DIRECTORY:
×
1242
        case IMAGE_SUBVOLUME:
1243
                context->root = strdup(image->path);
×
UNCOV
1244
                if (!context->root)
×
UNCOV
1245
                        return log_oom();
×
1246
                return 0;
1247
        case IMAGE_RAW:
×
1248
        case IMAGE_BLOCK:
UNCOV
1249
                context->image = strdup(image->path);
×
1250
                if (!context->image)
×
1251
                        return log_oom();
×
1252
                return 0;
1253
        default:
×
1254
                assert_not_reached();
×
1255
        }
1256
}
1257

1258
static void target_identifier_hash_func(const TargetIdentifier *t, struct siphash *state) {
3✔
1259
        assert(t);
3✔
1260

1261
        siphash24_compress_typesafe(t->class, state);
3✔
1262
        siphash24_compress_string(t->name, state);
3✔
1263
}
3✔
1264

UNCOV
1265
static int target_identifier_compare_func(const TargetIdentifier *x, const TargetIdentifier *y) {
×
1266
        int r;
×
1267

UNCOV
1268
        assert(x);
×
UNCOV
1269
        assert(y);
×
1270

UNCOV
1271
        r = CMP(x->class, y->class);
×
UNCOV
1272
        if (r != 0)
×
1273
                return r;
1274

UNCOV
1275
        return strcmp(x->name, y->name);
×
1276
}
1277

1278
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(target_identifier_hash_ops,
3✔
1279
                                              TargetIdentifier, target_identifier_hash_func, target_identifier_compare_func,
1280
                                              TargetIdentifier, target_identifier_free);
1281

1282
static int enumerate_image_class(RuntimeScope runtime_scope, TargetClass class, Set **targets) {
16✔
1283
        _cleanup_hashmap_free_ Hashmap *images = NULL;
16✔
1284
        Image *image;
16✔
1285
        int r;
16✔
1286

1287
        r = image_discover(runtime_scope, (ImageClass) class, NULL, &images);
16✔
1288
        if (r < 0)
16✔
1289
                return r;
1290

1291
        HASHMAP_FOREACH(image, images) {
20✔
1292
                _cleanup_(target_identifier_freep) TargetIdentifier *t = NULL;
4✔
1293
                bool have = false;
4✔
1294
                _cleanup_(context_done) Context image_context = CONTEXT_NULL;
4✔
1295

1296
                if (image_is_host(image))
4✔
1297
                        continue; /* We already enroll the host ourselves */
4✔
1298

1299
                if (!image_type_can_sysupdate(image->type))
×
UNCOV
1300
                        continue;
×
1301

1302
                r = context_load_paths_from_image(&image_context, image);
×
1303
                if (r < 0)
×
1304
                        return r;
1305

1306
                /* Load the components in a separate Context specific to the given Image before
1307
                 * committing to loading that state to the main Context. */
UNCOV
1308
                r = context_load_offline(
×
1309
                                &image_context,
1310
                                PROCESS_IMAGE_READ_ONLY,
1311
                                /* read_definitions_flags= */ 0);
1312
                if (r < 0)
×
1313
                        return r;
1314

1315
                r = context_list_components(&image_context, /* ret_component_names= */ NULL, &have);
×
1316
                if (r < 0)
×
1317
                        return r;
UNCOV
1318
                if (!have) {
×
UNCOV
1319
                        log_debug("Skipping %s because it has no default component", image->path);
×
UNCOV
1320
                        continue;
×
1321
                }
1322

UNCOV
1323
                r = target_identifier_new(class, image->name, &t);
×
UNCOV
1324
                if (r < 0)
×
1325
                        return r;
1326

UNCOV
1327
                r = set_ensure_consume(targets, &target_identifier_hash_ops, TAKE_PTR(t));
×
1328
                if (r < 0)
×
1329
                        return r;
1330
        }
1331

1332
        return 0;
16✔
1333
}
1334

1335
static int context_enumerate_components(Context *context, Set **targets) {
4✔
1336
        _cleanup_strv_free_ char **component_names = NULL;
4✔
1337
        bool have_default_component;
4✔
1338
        int r;
4✔
1339

1340
        assert(context);
4✔
1341

1342
        r = context_list_components(context, &component_names, &have_default_component);
4✔
1343
        if (r < 0)
4✔
1344
                return r;
1345

1346
        if (have_default_component) {
4✔
UNCOV
1347
                _cleanup_(target_identifier_freep) TargetIdentifier *t = NULL;
×
1348

UNCOV
1349
                r = target_identifier_new(TARGET_HOST, "host", &t);
×
UNCOV
1350
                if (r < 0)
×
1351
                        return r;
1352

UNCOV
1353
                r = set_ensure_consume(targets, &target_identifier_hash_ops, TAKE_PTR(t));
×
UNCOV
1354
                if (r < 0)
×
1355
                        return r;
1356
        }
1357

1358
        STRV_FOREACH(component, component_names) {
7✔
1359
                _cleanup_(target_identifier_freep) TargetIdentifier *t = NULL;
3✔
1360

1361
                r = target_identifier_new(TARGET_COMPONENT, *component, &t);
3✔
1362
                if (r < 0)
3✔
1363
                        return r;
1364

1365
                r = set_ensure_consume(targets, &target_identifier_hash_ops, TAKE_PTR(t));
3✔
1366
                if (r < 0)
3✔
1367
                        return r;
1368
        }
1369

1370
        return 0;
1371
}
1372

1373
static int context_enumerate_targets(Context *context, Set **targets) {
4✔
1374
        static const TargetClass discoverable_classes[] = {
4✔
1375
                TARGET_MACHINE,
1376
                TARGET_PORTABLE,
1377
                TARGET_SYSEXT,
1378
                TARGET_CONFEXT,
1379
        };
1380
        int r;
4✔
1381

1382
        assert(context);
4✔
1383

1384
        FOREACH_ARRAY(class, discoverable_classes, ELEMENTSOF(discoverable_classes)) {
20✔
1385
                r = enumerate_image_class(RUNTIME_SCOPE_SYSTEM, *class, targets);
16✔
1386
                if (r < 0)
16✔
1387
                        return r;
1388
        }
1389

1390
        r = context_enumerate_components(context, targets);
4✔
1391
        if (r < 0)
4✔
UNCOV
1392
                return r;
×
1393

1394
        return 0;
1395
}
1396

1397
static int context_load_paths_from_target(Context *context) {
209✔
1398
        int r;
209✔
1399

1400
        assert(context);
209✔
1401
        assert(context->target_identifier.class != _TARGET_CLASS_INVALID);
209✔
1402

1403
        /* These shouldn’t have been set up some other way first */
1404
        assert(!context->component);
209✔
1405
        assert(!context->root);
209✔
1406
        assert(!context->image);
209✔
1407

1408
        switch (context->target_identifier.class) {
209✔
1409
        case TARGET_MACHINE:
×
1410
        case TARGET_PORTABLE:
1411
        case TARGET_SYSEXT:
1412
        case TARGET_CONFEXT: {
UNCOV
1413
                _cleanup_hashmap_free_ Hashmap *images = NULL;
×
UNCOV
1414
                Image *image, *selected_image = NULL;
×
1415

1416
                /* These are all image-based target classes, so first find the corresponding image. */
UNCOV
1417
                r = image_discover(RUNTIME_SCOPE_SYSTEM, (ImageClass) context->target_identifier.class, NULL, &images);
×
UNCOV
1418
                if (r < 0)
×
1419
                        return r;
1420

UNCOV
1421
                HASHMAP_FOREACH(image, images) {
×
UNCOV
1422
                        bool have = false;
×
UNCOV
1423
                        _cleanup_(context_done) Context image_context = CONTEXT_NULL;
×
1424

UNCOV
1425
                        if (image_is_host(image))
×
UNCOV
1426
                                continue; /* We already enroll the host ourselves */
×
1427

UNCOV
1428
                        if (!image_type_can_sysupdate(image->type))
×
UNCOV
1429
                                continue;
×
1430

UNCOV
1431
                        if (!streq(image->name, context->target_identifier.name))
×
UNCOV
1432
                                continue;
×
1433

UNCOV
1434
                        r = context_load_paths_from_image(&image_context, image);
×
UNCOV
1435
                        if (r < 0)
×
1436
                                return r;
1437

1438
                        /* Load the components in a separate Context specific to the given Image before
1439
                         * committing to loading that state to the main Context. */
UNCOV
1440
                        r = context_load_offline(&image_context, 0, 0);
×
1441
                        if (r < 0)
×
1442
                                return r;
1443

UNCOV
1444
                        r = context_list_components(&image_context, /* ret_component_names= */ NULL, &have);
×
UNCOV
1445
                        if (r < 0)
×
1446
                                return r;
UNCOV
1447
                        if (!have) {
×
UNCOV
1448
                                log_debug("Skipping %s because it has no default component", image->path);
×
1449
                                continue;
×
1450
                        }
1451

1452
                        /* This is the match we were looking for */
UNCOV
1453
                        selected_image = image;
×
UNCOV
1454
                        break;
×
1455
                }
1456

UNCOV
1457
                if (!selected_image)
×
1458
                        return -ENOENT;
1459

UNCOV
1460
                r = context_load_paths_from_image(context, selected_image);
×
UNCOV
1461
                if (r < 0)
×
1462
                        return r;
1463

UNCOV
1464
                break;
×
1465
        }
1466
        case TARGET_HOST:
1467
                /* No additional setup needed */
1468
                break;
1469
        case TARGET_COMPONENT: {
8✔
1470
                _cleanup_strv_free_ char **component_names = NULL;
8✔
1471

1472
                r = context_list_components(context, &component_names, /* ret_has_default_component= */ NULL);
8✔
1473
                if (r < 0)
8✔
1474
                        return r;
1475

1476
                if (!strv_contains(component_names, context->target_identifier.name))
8✔
1477
                        return -ENOENT;
1478

UNCOV
1479
                context->component = strdup(context->target_identifier.name);
×
UNCOV
1480
                if (!context->component)
×
UNCOV
1481
                        return log_oom();
×
UNCOV
1482
                break;
×
1483
        }
UNCOV
1484
        default:
×
UNCOV
1485
                assert_not_reached();
×
1486
        }
1487

1488
        return 0;
1489
}
1490

1491
/* Load a Context to point to the target given by the TargetIdentifier. The TargetIdentifier will have been
1492
 * syntactically validated by dispatch_target_identifier(), but might still point to components which don’t
1493
 * exist, images which the user isn’t privileged to access, etc. This function validates the TargetIdentifier
1494
 * against an enumerated list of known targets, which are safe to update without additional permissions. */
1495
static int context_load_online_from_target(
192✔
1496
                Context *context,
1497
                ProcessImageFlags process_image_flags,
1498
                ReadDefinitionsFlags read_definitions_flags) {
1499
        int r;
192✔
1500

1501
        assert(context);
192✔
1502
        assert(context->target_identifier.class != _TARGET_CLASS_INVALID);
192✔
1503

1504
        r = context_load_paths_from_target(context);
192✔
1505
        if (r < 0)
192✔
1506
                return r;
1507

1508
        return context_load_online(context, process_image_flags, read_definitions_flags);
184✔
1509
}
1510

1511
static int context_load_offline_from_target(
17✔
1512
                Context *context,
1513
                ProcessImageFlags process_image_flags,
1514
                ReadDefinitionsFlags read_definitions_flags) {
1515
        int r;
17✔
1516

1517
        assert(context);
17✔
1518
        assert(context->target_identifier.class != _TARGET_CLASS_INVALID);
17✔
1519

1520
        r = context_load_paths_from_target(context);
17✔
1521
        if (r < 0)
17✔
1522
                return r;
1523

1524
        return context_load_offline(context, process_image_flags, read_definitions_flags);
17✔
1525
}
1526

1527
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
843✔
1528
        const Context *c = ASSERT_PTR(t->context);
843✔
1529
        size_t i, n = c->n_transfers;
843✔
1530
        uint64_t base, scaled;
843✔
1531
        unsigned overall;
843✔
1532

1533
        for (i = 0; i < n; i++)
2,945✔
1534
                if (c->transfers[i] == t)
2,945✔
1535
                        break;
1536
        assert(i < n); /* We should have found the index */
843✔
1537

1538
        base = (100 * 100 * i) / n;
843✔
1539
        scaled = (100 * percentage) / n;
843✔
1540
        overall = (unsigned) ((base + scaled) / 100);
843✔
1541
        assert(overall <= 100);
843✔
1542

1543
        log_debug("Transfer %zu/%zu is %u%% complete (%u%% overall).", i+1, n, percentage, overall);
843✔
1544
        return sd_notifyf(/* unset_environment= */ false, "X_SYSUPDATE_PROGRESS=%u\n"
1,686✔
1545
                                              "X_SYSUPDATE_TRANSFERS_LEFT=%zu\n"
1546
                                              "X_SYSUPDATE_TRANSFERS_DONE=%zu\n"
1547
                                              "STATUS=Updating to '%s' (%u%% complete).",
1548
                                              overall, n - i, i, inst->metadata.version, overall);
843✔
1549
}
1550

1551
static int context_process_partial_and_pending(Context *c, const char *version);
1552

1553
static int context_acquire(
290✔
1554
                Context *c,
1555
                const char *version) {
1556

1557
        UpdateSet *us = NULL;
290✔
1558
        int r;
290✔
1559

1560
        assert(c);
290✔
1561

1562
        if (version) {
290✔
UNCOV
1563
                us = context_update_set_by_version(c, version);
×
UNCOV
1564
                if (!us)
×
1565
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
290✔
1566
        } else {
1567
                if (!c->candidate) {
290✔
1568
                        log_info("No update needed.");
50✔
1569

1570
                        return 0;
1571
                }
1572

1573
                us = c->candidate;
1574
        }
1575

1576
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
240✔
1577
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
32✔
1578
        else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
208✔
1579
                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✔
1580
        } else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
192✔
1581
                log_info("Selected update '%s' is already acquired and pending installation.", us->version);
32✔
1582

1583
                return context_process_partial_and_pending(c, version);
32✔
1584
        } else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
160✔
UNCOV
1585
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1586

1587
                return 0;
1588
        }
1589

1590
        if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
192✔
UNCOV
1591
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
×
1592
        if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
192✔
UNCOV
1593
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
×
1594

1595
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
192✔
UNCOV
1596
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1597
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
192✔
UNCOV
1598
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1599

1600
        log_info("Selected update '%s' for install.", us->version);
192✔
1601

1602
        _cleanup_free_ InstanceMetadata *metadata = new0(InstanceMetadata, c->n_transfers);
384✔
1603
        if (!metadata)
192✔
UNCOV
1604
                return log_oom();
×
1605

1606
        /* Compute up the temporary paths before vacuuming so we don't vacuum anything if we fail to compute
1607
         * any paths because of failed validations (e.g. exceeding the gpt partition label size). */
1608
        for (size_t i = 0; i < c->n_transfers; i++) {
1,462✔
1609
                Instance *inst = us->instances[i];
1,270✔
1610
                Transfer *t = c->transfers[i];
1,270✔
1611

1612
                assert(inst);
1,270✔
1613

1614
                r = transfer_compute_temporary_paths(t, inst, metadata + i);
1,270✔
1615
                if (r < 0)
1,270✔
1616
                        return r;
1617
        }
1618

1619
        (void) sd_notifyf(/* unset_environment= */ false,
192✔
1620
                          "READY=1\n"
1621
                          "X_SYSUPDATE_VERSION=%s\n"
1622
                          "STATUS=Making room for '%s'.", us->version, us->version);
1623

1624
        /* Let's make some room. We make sure for each transfer we have one free space to fill. While
1625
         * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
1626
         * download succeeded already, in which case we shouldn't remove it just to acquire it again */
1627
        r = context_vacuum(
384✔
1628
                        c,
1629
                        /* space= */ 1,
1630
                        /* extra_protected_version= */ us->version);
192✔
1631
        if (r < 0)
192✔
1632
                return r;
1633

1634
        if (c->sync)
192✔
1635
                sync();
192✔
1636

1637
        (void) sd_notifyf(/* unset_environment= */ false,
192✔
1638
                          "STATUS=Updating to '%s'.", us->version);
1639

1640
        /* There should now be one instance picked for each transfer, and the order is the same */
1641
        assert(us->n_instances == c->n_transfers);
192✔
1642

1643
        for (size_t i = 0; i < c->n_transfers; i++) {
1,366✔
1644
                Instance *inst = us->instances[i];
1,190✔
1645
                Transfer *t = c->transfers[i];
1,190✔
1646

1647
                assert(inst); /* ditto */
1,190✔
1648

1649
                if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */
1,190✔
1650
                        assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
208✔
1651
                        continue;
208✔
1652
                }
1653

1654
                r = transfer_acquire_instance(t, inst, metadata + i, context_on_acquire_progress, c);
982✔
1655
                if (r < 0)
982✔
1656
                        return r;
1657
        }
1658

1659
        if (c->sync)
176✔
1660
                sync();
176✔
1661

1662
        return 1;
1663
}
1664

1665
/* Check to see if we have an update set acquired and pending installation. */
1666
static int context_process_partial_and_pending(
116✔
1667
                Context *c,
1668
                const char *version) {
1669

1670
        UpdateSet *us = NULL;
116✔
1671
        int r;
116✔
1672

1673
        assert(c);
116✔
1674

1675
        if (version) {
116✔
UNCOV
1676
                us = context_update_set_by_version(c, version);
×
UNCOV
1677
                if (!us)
×
UNCOV
1678
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1679
        } else {
1680
                if (!c->candidate) {
116✔
UNCOV
1681
                        log_info("No update needed.");
×
1682

1683
                        return 0;
1684
                }
1685

1686
                us = c->candidate;
1687
        }
1688

1689
        if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
116✔
UNCOV
1690
                log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
×
1691
        else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
116✔
UNCOV
1692
                log_info("Selected update '%s' is already installed. Skipping update.", us->version);
×
1693

1694
                return 0;
1695
        }
1696

1697
        if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
116✔
1698
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
4✔
1699
        if (!FLAGS_SET(us->flags, UPDATE_PENDING))
112✔
UNCOV
1700
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version);
×
1701

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

1705
        if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
112✔
UNCOV
1706
                log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
×
1707
        if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
112✔
UNCOV
1708
                log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
×
1709

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

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

1715
        for (size_t i = 0; i < c->n_transfers; i++) {
908✔
1716
                Instance *inst = us->instances[i];
796✔
1717
                Transfer *t = c->transfers[i];
796✔
1718

1719
                assert(inst);
796✔
1720

1721
                r = transfer_process_partial_and_pending_instance(t, inst);
796✔
1722
                if (r < 0)
796✔
1723
                        return r;
1724
        }
1725

1726
        return 1;
1727
}
1728

1729
static int notify_subscribers_reply(
177✔
1730
                sd_varlink *link,
1731
                sd_json_variant *reply,
1732
                const char *error_id,
1733
                sd_varlink_reply_flags_t flags,
1734
                void *userdata) {
1735

1736
        assert(link);
177✔
1737

1738
        if (error_id)
177✔
UNCOV
1739
                log_warning("Notification subscriber '%s' returned error, ignoring: %s",
×
1740
                            strna(sd_varlink_get_description(link)), error_id);
1741

1742
        return 0;
177✔
1743
}
1744

1745
static int context_notify_subscribers(Context *c, UpdateSet *us) {
176✔
1746
        int r;
176✔
1747

1748
        assert(c);
176✔
1749

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

1753
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *resources = NULL;
176✔
1754
        if (us)
176✔
1755
                for (size_t i = 0; i < c->n_transfers; i++) {
1,334✔
1756
                        Instance *inst = us->instances[i];
1,158✔
1757
                        Transfer *t = c->transfers[i];
1,158✔
1758

1759
                        if (inst->resource == &t->target &&
1,158✔
1760
                            !inst->is_pending)
920✔
1761
                                continue;
208✔
1762

1763
                        /* Report where the resource was installed *to* (not the source it came from): the
1764
                         * final on-disk path for filesystem targets, the partition device node for partition
1765
                         * targets. */
1766
                        const char *target_path =
256✔
1767
                                RESOURCE_IS_FILESYSTEM(t->target.type) ? t->final_path :
950✔
1768
                                t->target.type == RESOURCE_PARTITION ? t->partition_info.device :
1769
                                NULL;
1770

1771
                        r = sd_json_variant_append_arraybo(
950✔
1772
                                        &resources,
1773
                                        SD_JSON_BUILD_PAIR_STRING("transfer", t->id),
1774
                                        SD_JSON_BUILD_PAIR_CONDITION(!!target_path, "path", SD_JSON_BUILD_STRING(target_path)));
1775
                        if (r < 0)
950✔
UNCOV
1776
                                return log_warning_errno(r, "Failed to build sysupdate notify resources list, skipping notification: %m");
×
1777
                }
1778

1779
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
176✔
1780
        r = sd_json_buildo(
176✔
1781
                        &params,
1782
                        SD_JSON_BUILD_PAIR_CONDITION(!!c->component, "component", SD_JSON_BUILD_STRING(c->component)),
1783
                        SD_JSON_BUILD_PAIR_CONDITION(!!us, "version", SD_JSON_BUILD_STRING(us ? us->version : NULL)),
1784
                        SD_JSON_BUILD_PAIR_CONDITION(!!resources, "resources", SD_JSON_BUILD_VARIANT(resources)));
1785
        if (r < 0)
176✔
UNCOV
1786
                return log_warning_errno(r, "Failed to build sysupdate notify parameters, skipping notification: %m");
×
1787

1788
        ssize_t n = varlink_execute_directory(
176✔
1789
                        VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK,
1790
                        "io.systemd.SysUpdate.Notify.OnCompletedUpdate",
1791
                        params,
1792
                        /* more= */ false,
1793
                        /* timeout_usec= */ 5 * USEC_PER_MINUTE,
1794
                        notify_subscribers_reply,
1795
                        /* userdata= */ NULL);
1796
        if (n < 0)
176✔
1797
                log_debug_errno(n, "Failed to dispatch sysupdate notification to %s, ignoring: %m",
176✔
1798
                                VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
1799
        else if (n > 0)
176✔
1800
                log_debug("Dispatched sysupdate notification to %zi subscribers in %s.", n, VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
176✔
1801

1802
        return 0;
1803
}
1804

1805
static int context_install(
176✔
1806
                Context *c,
1807
                const char *version,
1808
                UpdateSet **ret_applied) {
1809

1810
        UpdateSet *us = NULL;
176✔
1811
        int r;
176✔
1812

1813
        assert(c);
176✔
1814

1815
        if (version) {
176✔
UNCOV
1816
                us = context_update_set_by_version(c, version);
×
UNCOV
1817
                if (!us)
×
UNCOV
1818
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
×
1819
        } else {
1820
                if (!c->candidate) {
176✔
1821
                        log_info("No update needed.");
×
1822

1823
                        return 0;
1824
                }
1825

1826
                us = c->candidate;
1827
        }
1828

1829
        (void) sd_notifyf(/* unset_environment=*/ false,
176✔
1830
                          "READY=1\n"
1831
                          "X_SYSUPDATE_VERSION=%s\n"
1832
                          "STATUS=Installing '%s'.", us->version, us->version);
1833

1834
        for (size_t i = 0; i < c->n_transfers; i++) {
1,510✔
1835
                Instance *inst = us->instances[i];
1,158✔
1836
                Transfer *t = c->transfers[i];
1,158✔
1837

1838
                if (inst->resource == &t->target &&
1,158✔
1839
                    !inst->is_pending)
920✔
1840
                        continue;
208✔
1841

1842
                r = transfer_install_instance(t, inst, c->root);
950✔
1843
                if (r < 0)
950✔
1844
                        return r;
1845
        }
1846

1847
        log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
176✔
1848

1849
        if (!c->root)
176✔
1850
                (void) context_notify_subscribers(c, us);
176✔
1851

1852
        (void) sd_notifyf(/* unset_environment= */ false,
176✔
1853
                          "STATUS=Installed '%s'.", us->version);
1854

1855
        if (ret_applied)
176✔
1856
                *ret_applied = us;
176✔
1857

1858
        return 1;
1859
}
1860

1861
static int context_get_transfers_for_feature(Context *context, const char *feature_id, char ***ret_transfers) {
24✔
1862
        int r;
24✔
1863

1864
        assert(context);
24✔
1865
        assert(feature_id);
24✔
1866

1867
        FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
192✔
1868
                Transfer *t = *tr;
168✔
1869

1870
                if (!strv_contains(t->features, feature_id) && !strv_contains(t->requisite_features, feature_id))
168✔
1871
                        continue;
168✔
1872

UNCOV
1873
                r = strv_extend(ret_transfers, t->id);
×
UNCOV
1874
                if (r < 0)
×
UNCOV
1875
                        return log_oom();
×
1876
        }
1877

1878
        FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) {
48✔
1879
                Transfer *t = *tr;
24✔
1880

1881
                if (!strv_contains(t->features, feature_id) && !strv_contains(t->requisite_features, feature_id))
24✔
UNCOV
1882
                        continue;
×
1883

1884
                r = strv_extend(ret_transfers, t->id);
24✔
1885
                if (r < 0)
24✔
UNCOV
1886
                        return log_oom();
×
1887
        }
1888

1889
        return 0;
1890
}
1891

1892
static JSON_DISPATCH_ENUM_DEFINE(dispatch_target_class, TargetClass, target_class_from_string);
217✔
1893

1894
static int dispatch_target_identifier(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
217✔
1895
        TargetIdentifier *t = ASSERT_PTR(userdata);
217✔
1896
        static const sd_json_dispatch_field dispatch[] = {
217✔
1897
                { "class", SD_JSON_VARIANT_STRING, dispatch_target_class,   voffsetof(*t, class), SD_JSON_MANDATORY },
1898
                { "name",  SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(*t, name),  SD_JSON_NULLABLE  },
1899
                {}
1900
        };
1901
        int r;
217✔
1902

1903
        r = sd_json_dispatch(variant, dispatch, flags, t);
217✔
1904
        if (r < 0)
217✔
1905
                return r;
1906

1907
        /* Name is mandatory unless class is `host` */
1908
        if ((t->class == TARGET_HOST) != (!t->name))
217✔
UNCOV
1909
                return json_log(variant, flags, SYNTHETIC_ERRNO(ENXIO), "Target name does not match class.");
×
1910

1911
        if (t->class == TARGET_COMPONENT && !component_name_valid(t->name))
217✔
1912
                        return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", t->name);
8✔
1913

1914
        return 0;
1915
}
1916

1917
static int verify_polkit(Context *context, sd_varlink *link, const char *action, const char **details) {
192✔
1918
        int r;
192✔
1919
        Server *s = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link)));
192✔
1920

1921
        assert(context);
192✔
1922

1923
        if (!s->system_bus) {
192✔
1924
                r = sd_bus_open_system_with_description(&s->system_bus, "sysupdate-system");
192✔
1925
                if (r < 0)
192✔
UNCOV
1926
                        return log_error_errno(r, "Failed to get system bus connection: %m");
×
1927

1928
                r = sd_bus_attach_event(s->system_bus, sd_varlink_get_event(link), SD_EVENT_PRIORITY_NORMAL);
192✔
1929
                if (r < 0)
192✔
UNCOV
1930
                        return log_error_errno(r, "Failed to attach system bus to event loop: %m");
×
1931
        }
1932

1933
        return varlink_verify_polkit_async(link,
192✔
1934
                        s->system_bus,
1935
                        action,
1936
                        details,
1937
                        &s->polkit_registry);
1938
}
1939

1940
VERB(verb_list, "list", "[VERSION]", VERB_ANY, 2, VERB_DEFAULT,
1941
     "Show installed and available versions");
1942
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
257✔
1943
        _cleanup_(context_done) Context context = CONTEXT_NULL;
257✔
1944
        _cleanup_strv_free_ char **appstream_urls = NULL;
257✔
1945
        const char *version;
257✔
1946
        int r;
257✔
1947

1948
        assert(argc <= 2);
257✔
1949
        version = argc >= 2 ? argv[1] : NULL;
257✔
1950

1951
        r = context_from_cmdline(&context);
257✔
1952
        if (r < 0)
257✔
1953
                return r;
1954

1955
        if (context.feature_select != SELECT_EXPLICIT)
257✔
UNCOV
1956
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
1957
        if (context.component_select != SELECT_EXPLICIT)
257✔
UNCOV
1958
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
×
1959

1960
        r = context_load_online(
257✔
1961
                        &context,
1962
                        PROCESS_IMAGE_READ_ONLY,
1963
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
1964
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
1965
        if (r < 0)
257✔
1966
                return r;
1967

1968
        if (version)
257✔
1969
                return context_show_version(&context, version);
209✔
1970
        else if (!sd_json_format_enabled(arg_json_format_flags))
48✔
UNCOV
1971
                return context_show_table(&context);
×
1972
        else {
1973
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
48✔
1974
                _cleanup_strv_free_ char **versions = NULL;
48✔
1975
                const char *current = NULL;
48✔
1976
                bool current_is_pending = false;
48✔
1977

1978
                FOREACH_ARRAY(update_set, context.update_sets, context.n_update_sets) {
224✔
1979
                        UpdateSet *us = *update_set;
176✔
1980

1981
                        if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
176✔
1982
                            FLAGS_SET(us->flags, UPDATE_NEWEST)) {
160✔
1983
                                current = us->version;
48✔
1984
                                current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
48✔
1985
                        }
1986

1987
                        r = strv_extend(&versions, us->version);
176✔
1988
                        if (r < 0)
176✔
1989
                                return log_oom();
×
1990
                }
1991

1992
                FOREACH_ARRAY(tr, context.transfers, context.n_transfers)
288✔
1993
                        STRV_FOREACH(appstream_url, (*tr)->appstream) {
240✔
1994
                                /* Avoid duplicates */
1995
                                if (strv_contains(appstream_urls, *appstream_url))
×
UNCOV
1996
                                        continue;
×
1997

1998
                                r = strv_extend(&appstream_urls, *appstream_url);
×
UNCOV
1999
                                if (r < 0)
×
UNCOV
2000
                                        return log_oom();
×
2001
                        }
2002

2003
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
96✔
2004
                                          SD_JSON_BUILD_PAIR_STRV("all", versions),
2005
                                          SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
2006
                if (r < 0)
48✔
UNCOV
2007
                        return log_error_errno(r, "Failed to create JSON: %m");
×
2008

2009
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
48✔
2010
                if (r < 0)
48✔
2011
                        return log_error_errno(r, "Failed to print JSON: %m");
×
2012

2013
                return 0;
2014
        }
2015
}
2016

2017
VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, 0,
2018
     "Show optional features");
2019
static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) {
17✔
2020
        _cleanup_(context_done) Context context = CONTEXT_NULL;
17✔
2021
        _cleanup_(table_unrefp) Table *table = NULL;
17✔
2022
        const char *feature_id;
17✔
2023
        Feature *f;
17✔
2024
        int r;
17✔
2025

2026
        assert(argc <= 2);
17✔
2027
        feature_id = argc >= 2 ? argv[1] : NULL;
17✔
2028

2029
        r = context_from_cmdline(&context);
17✔
2030
        if (r < 0)
17✔
2031
                return r;
2032

2033
        if (context.feature_select != SELECT_EXPLICIT)
17✔
2034
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2035
        if (context.component_select != SELECT_EXPLICIT)
17✔
UNCOV
2036
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
×
2037

2038
        r = context_load_offline(
17✔
2039
                        &context,
2040
                        PROCESS_IMAGE_READ_ONLY,
2041
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2042
        if (r < 0)
17✔
2043
                return r;
2044

2045
        if (feature_id) {
17✔
2046
                _cleanup_strv_free_ char **transfers = NULL;
8✔
2047

2048
                f = hashmap_get(context.features, feature_id);
8✔
2049
                if (!f)
8✔
UNCOV
2050
                        return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
×
2051
                                               "Optional feature not found: %s",
2052
                                               feature_id);
2053

2054
                table = table_new_vertical();
8✔
2055
                if (!table)
8✔
2056
                        return log_oom();
×
2057

2058
                r = context_get_transfers_for_feature(&context, f->id, &transfers);
8✔
2059
                if (r < 0)
8✔
2060
                        return r;
2061

2062
                r = table_add_many(table,
8✔
2063
                                   TABLE_FIELD, "Name",
2064
                                   TABLE_STRING, f->id,
2065
                                   TABLE_FIELD, "Enabled",
2066
                                   TABLE_BOOLEAN, f->enabled);
2067
                if (r < 0)
8✔
2068
                        return table_log_add_error(r);
×
2069

2070
                r = feature_is_suggested(f);
8✔
2071
                if (r < 0) {
8✔
2072
                        errno = -r; /* Let's make %m below show this error */
×
2073
                        _cleanup_free_ char *k = asprintf_safe("error (%m)");
×
UNCOV
2074
                        r = table_add_many(table,
×
2075
                                           TABLE_FIELD, "Suggested",
2076
                                           TABLE_STRING, k);
2077
                } else
2078
                        r = table_add_many(table,
8✔
2079
                                           TABLE_FIELD, "Suggested",
2080
                                           TABLE_BOOLEAN, r);
2081
                if (r < 0)
8✔
2082
                        return table_log_add_error(r);
×
2083

2084
                if (f->description) {
8✔
2085
                        r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
8✔
2086
                        if (r < 0)
8✔
2087
                                return table_log_add_error(r);
×
2088
                }
2089

2090
                if (f->documentation) {
8✔
2091
                        r = table_add_many(table,
×
2092
                                           TABLE_FIELD, "Documentation",
2093
                                           TABLE_STRING, f->documentation,
2094
                                           TABLE_SET_URL, f->documentation);
2095
                        if (r < 0)
×
2096
                                return table_log_add_error(r);
×
2097
                }
2098

2099
                if (f->appstream) {
8✔
UNCOV
2100
                        r = table_add_many(table,
×
2101
                                           TABLE_FIELD, "AppStream",
2102
                                           TABLE_STRING, f->appstream,
2103
                                           TABLE_SET_URL, f->appstream);
2104
                        if (r < 0)
×
UNCOV
2105
                                return table_log_add_error(r);
×
2106
                }
2107

2108
                if (!strv_isempty(transfers)) {
8✔
2109
                        r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
8✔
2110
                        if (r < 0)
8✔
UNCOV
2111
                                return table_log_add_error(r);
×
2112
                }
2113

2114
                return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
8✔
2115
        } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
9✔
2116
                table = table_new("enabled", "suggested", "feature", "description", "documentation");
9✔
2117
                if (!table)
9✔
UNCOV
2118
                        return log_oom();
×
2119

2120
                table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
9✔
2121

2122
                HASHMAP_FOREACH(f, context.features) {
17✔
2123
                        r = table_add_many(table,
8✔
2124
                                           TABLE_BOOLEAN_CHECKMARK, f->enabled,
2125
                                           TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled));
2126
                        if (r < 0)
8✔
2127
                                return table_log_add_error(r);
×
2128

2129
                        r = feature_is_suggested(f);
8✔
2130
                        if (r < 0)
8✔
UNCOV
2131
                                r = table_add_many(table, TABLE_EMPTY);
×
2132
                        else
2133
                                r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, r);
8✔
2134
                        if (r < 0)
8✔
UNCOV
2135
                                return table_log_add_error(r);
×
2136

2137
                        r = table_add_many(table,
8✔
2138
                                           TABLE_STRING, f->id,
2139
                                           TABLE_STRING, f->description,
2140
                                           TABLE_STRING, f->documentation,
2141
                                           TABLE_SET_URL, f->documentation);
2142
                        if (r < 0)
8✔
2143
                                return table_log_add_error(r);
×
2144
                }
2145

2146
                r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
9✔
2147
                if (r < 0)
9✔
2148
                        return r;
2149

2150
                if (arg_legend) {
9✔
2151
                        if (table_isempty(table))
18✔
2152
                                log_info("No features.");
1✔
2153
                        else
2154
                                printf("\n%zu features listed.\n", table_get_rows(table) - 1);
8✔
2155
                }
2156
        } else {
2157
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
×
2158
                _cleanup_strv_free_ char **features = NULL;
×
2159

2160
                HASHMAP_FOREACH(f, context.features) {
×
UNCOV
2161
                        r = strv_extend(&features, f->id);
×
UNCOV
2162
                        if (r < 0)
×
2163
                                return log_oom();
×
2164
                }
2165

UNCOV
2166
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
×
UNCOV
2167
                if (r < 0)
×
UNCOV
2168
                        return log_error_errno(r, "Failed to create JSON: %m");
×
2169

UNCOV
2170
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
×
UNCOV
2171
                if (r < 0)
×
2172
                        return log_error_errno(r, "Failed to print JSON: %m");
×
2173
        }
2174

2175
        return 0;
2176
}
2177

2178
static int make_dropin_dir(Context *c, char **ret) {
×
2179
        _cleanup_free_ char *dir = NULL;
×
2180

2181
        assert(c);
×
UNCOV
2182
        assert(ret);
×
2183

2184
        /* Returns the (writable) directory where feature definitions live, so that we can drop our
2185
         * 'Enabled=' override right next to them. This mirrors the directory logic in
2186
         * context_read_definitions(), but settles on a single writable location below /etc. */
2187

UNCOV
2188
        if (c->definitions)
×
2189
                dir = strdup(c->definitions); /* --root= is not supported for this for now */
×
2190
        else if (c->component) {
×
2191
                _cleanup_free_ char *n = strjoin("sysupdate.", c->component, ".d");
×
UNCOV
2192
                if (!n)
×
2193
                        return log_oom();
×
2194

UNCOV
2195
                dir = path_join(c->root, SYSCONF_DIR, n);
×
2196
        } else
2197
                dir = path_join(c->root, SYSCONF_DIR "/sysupdate.d");
×
2198
        if (!dir)
×
2199
                return log_oom();
×
2200

UNCOV
2201
        *ret = TAKE_PTR(dir);
×
2202
        return 0;
×
2203
}
2204

UNCOV
2205
static const char *context_component_display(const Context *c) {
×
2206
        assert(c);
×
2207

2208
        return c->component ?: "<default>";
×
2209
}
2210

2211
static int context_enable_feature(
×
2212
                Context *c,
2213
                const char *argv0,
2214
                bool enable,
2215
                char **features) {
2216

2217
        int r;
×
2218

UNCOV
2219
        assert(c);
×
UNCOV
2220
        assert(argv0);
×
2221

UNCOV
2222
        _cleanup_free_ char **l = NULL;
×
UNCOV
2223
        switch (c->feature_select) {
×
2224

2225
        case SELECT_EXPLICIT:
2226
                break;
2227

UNCOV
2228
        case SELECT_ALL: {
×
UNCOV
2229
                assert(!features);
×
2230

UNCOV
2231
                Feature *f;
×
UNCOV
2232
                HASHMAP_FOREACH(f, c->features)
×
UNCOV
2233
                        if (strv_push(&l, f->id) < 0)
×
UNCOV
2234
                                return log_oom();
×
2235

UNCOV
2236
                features = l;
×
2237
                break;
×
2238
        }
2239

UNCOV
2240
        case SELECT_SUGGESTED: {
×
UNCOV
2241
                assert(!features);
×
2242

UNCOV
2243
                Feature *f;
×
UNCOV
2244
                HASHMAP_FOREACH(f, c->features) {
×
UNCOV
2245
                        r = feature_is_suggested(f);
×
UNCOV
2246
                        if (r < 0)
×
UNCOV
2247
                                return log_error_errno(r, "Failed to determine if feature '%s' of component '%s' shall be enabled: %m", f->id, context_component_display(c));
×
UNCOV
2248
                        if (!!r != !!enable) {
×
UNCOV
2249
                                log_debug("Skipping feature '%s' of component '%s'.", f->id, context_component_display(c));
×
UNCOV
2250
                                continue;
×
2251
                        }
2252

UNCOV
2253
                        log_info("%s feature '%s' of component '%s'.", enable ? "Enabling" : "Disabling", f->id, context_component_display(c));
×
2254

UNCOV
2255
                        if (strv_push(&l, f->id) < 0)
×
UNCOV
2256
                                return log_oom();
×
2257
                }
2258

UNCOV
2259
                features = l;
×
UNCOV
2260
                break;
×
2261
        }
2262

UNCOV
2263
        default:
×
UNCOV
2264
                assert_not_reached();
×
2265
        }
2266

UNCOV
2267
        if (strv_isempty(features)) {
×
UNCOV
2268
                log_debug("No features selected.");
×
2269
                return 0;
2270
        }
2271

UNCOV
2272
        _cleanup_free_ char *dropin_dir = NULL;
×
UNCOV
2273
        r = make_dropin_dir(c, &dropin_dir);
×
UNCOV
2274
        if (r < 0)
×
2275
                return r;
2276

2277
        int ret = 0;
UNCOV
2278
        STRV_FOREACH(name, features) {
×
UNCOV
2279
                if (!hashmap_contains(c->features, *name)) {
×
UNCOV
2280
                        RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Optional feature not found in component '%s': %s", context_component_display(c), *name));
×
UNCOV
2281
                        continue;
×
2282
                }
2283

UNCOV
2284
                _cleanup_free_ char *fname = strjoin(*name, ".feature");
×
UNCOV
2285
                if (!fname)
×
UNCOV
2286
                        return log_oom();
×
2287

2288
                /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
UNCOV
2289
                r = write_drop_in_format(
×
2290
                                dropin_dir,
2291
                                fname,
2292
                                50,
2293
                                "systemd-sysupdate-enabled",
2294
                                "# Generated via 'systemd-sysupdate %s'\n\n"
2295
                                "[Feature]\n"
2296
                                "Enabled=%s\n",
2297
                                argv0,
2298
                                yes_no(enable));
UNCOV
2299
                if (r < 0) {
×
UNCOV
2300
                        RET_GATHER(ret, log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name));
×
UNCOV
2301
                        continue;
×
2302
                }
2303

UNCOV
2304
                log_info("Feature '%s' %s.", *name, enabled_disabled(enable));
×
2305
        }
2306

2307
        return ret;
2308
}
2309

2310
VERB(verb_enable_feature, "enable-feature", "FEATURE…", 1, VERB_ANY, 0,
2311
     "Enable optional feature");
2312
VERB(verb_enable_feature, "disable-feature", "FEATURE…", 1, VERB_ANY, 0,
2313
     "Disable optional feature");
UNCOV
2314
static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *userdata) {
×
UNCOV
2315
        bool enable = streq(argv[0], "enable-feature");
×
UNCOV
2316
        int r;
×
2317

UNCOV
2318
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
UNCOV
2319
        r = context_from_cmdline(&context);
×
UNCOV
2320
        if (r < 0)
×
2321
                return r;
2322

UNCOV
2323
        if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL))
×
UNCOV
2324
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--component-suggested is not supported for '%s'.", argv[0]);
×
UNCOV
2325
        if (context.definitions)
×
UNCOV
2326
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2327
                                       "The --definitions= switch may not be combined with '%s'.", argv[0]);
2328

UNCOV
2329
        char **features;
×
UNCOV
2330
        if (argc > 1 && context.feature_select == SELECT_EXPLICIT) {
×
UNCOV
2331
                features = strv_skip(argv, 1);
×
2332

UNCOV
2333
                STRV_FOREACH(name, features)
×
UNCOV
2334
                        if (!feature_name_valid(*name))
×
UNCOV
2335
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name);
×
2336

UNCOV
2337
        } else if (argc <= 1 && context.feature_select != SELECT_EXPLICIT)
×
2338
                features = NULL;
2339
        else
UNCOV
2340
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify features as positional parameter or via --feature-all/--feature-suggested, not both.");
×
2341

2342
        r = context_load_offline(
×
2343
                        &context,
2344
                        /* process_image_flags= */ 0,
2345
                        /* read_definitions_flags= */ 0);
UNCOV
2346
        if (r < 0)
×
2347
                return r;
2348

UNCOV
2349
        switch (context.component_select) {
×
2350

UNCOV
2351
        case SELECT_EXPLICIT:
×
UNCOV
2352
                return context_enable_feature(&context, argv[0], enable, features);
×
2353

2354
        case SELECT_ALL: {
×
2355
                _cleanup_strv_free_ char **component_names = NULL;
×
2356
                bool has_default_component;
×
2357
                r = context_list_components(&context, &component_names, &has_default_component);
×
UNCOV
2358
                if (r < 0)
×
2359
                        return r;
2360

UNCOV
2361
                if (strv_isempty(component_names) && !has_default_component) {
×
UNCOV
2362
                        log_debug("No components selected.");
×
2363
                        return 0;
2364
                }
2365

UNCOV
2366
                int ret = 0;
×
UNCOV
2367
                if (has_default_component)
×
UNCOV
2368
                        RET_GATHER(ret, context_enable_feature(&context, argv[0], enable, features));
×
2369

UNCOV
2370
                STRV_FOREACH(name, component_names) {
×
UNCOV
2371
                        _cleanup_(context_done) Context cc = CONTEXT_NULL;
×
2372

UNCOV
2373
                        r = context_from_base_with_component(&context, *name, &cc);
×
UNCOV
2374
                        if (r < 0) {
×
UNCOV
2375
                                RET_GATHER(ret, r);
×
UNCOV
2376
                                continue;
×
2377
                        }
2378

UNCOV
2379
                        r = context_load_offline(
×
2380
                                        &cc,
2381
                                        /* process_image_flags= */ 0,
2382
                                        /* read_definitions_flags= */ 0);
UNCOV
2383
                        if (r < 0) {
×
UNCOV
2384
                                RET_GATHER(ret, r);
×
UNCOV
2385
                                continue;
×
2386
                        }
2387

UNCOV
2388
                        RET_GATHER(ret, context_enable_feature(&cc, argv[0], enable, features));
×
2389
                }
2390

2391
                return ret;
2392
        }
2393

UNCOV
2394
        default:
×
UNCOV
2395
                assert_not_reached();
×
2396
        }
2397
}
2398

2399
static int feature_to_json(Context *context, const Feature *f, sd_json_variant **ret) {
16✔
2400
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
16✔
2401
        _cleanup_strv_free_ char **transfers = NULL;
16✔
2402
        const char *documentation_strv[2] = { NULL, };
16✔
2403
        int r;
16✔
2404

2405
        assert(context);
16✔
2406
        assert(f);
16✔
2407
        assert(ret);
16✔
2408

2409
        r = context_get_transfers_for_feature(context, f->id, &transfers);
16✔
2410
        if (r < 0)
16✔
2411
                return r;
2412

2413
        /* FIXME: Long term we’d like to support an array of documentation, but currently the D-Bus interface
2414
         * doesn’t support that and neither do the internals of sysupdate. So just expose 0 or 1 URLs for now. */
2415
        documentation_strv[0] = f->documentation;
16✔
2416

2417
        r = sd_json_variant_merge_objectbo(
16✔
2418
                        &v,
2419
                        SD_JSON_BUILD_PAIR_STRING("id", f->id),
2420
                        JSON_BUILD_PAIR_STRING_NON_EMPTY("description", f->description),
2421
                        JSON_BUILD_PAIR_STRV_NON_EMPTY("documentation", (char **) documentation_strv),
2422
                        JSON_BUILD_PAIR_STRING_NON_EMPTY("appstream", f->appstream),
2423
                        SD_JSON_BUILD_PAIR_BOOLEAN("isEnabled", f->enabled),
2424
                        JSON_BUILD_PAIR_STRV_NON_EMPTY("transfers", transfers));
2425
        if (r < 0)
16✔
UNCOV
2426
                return log_oom();
×
2427

2428
        *ret = TAKE_PTR(v);
16✔
2429
        return 1;
16✔
2430
}
2431

2432
static int vl_method_list_features(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
17✔
UNCOV
2433
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
2434
        Feature *f;
17✔
2435
        int r;
17✔
2436

2437
        assert(link);
17✔
2438

2439
        static const sd_json_dispatch_field dispatch_table[] = {
17✔
2440
                { "target", SD_JSON_VARIANT_OBJECT, dispatch_target_identifier, voffsetof(context, target_identifier), SD_JSON_MANDATORY },
2441
                {},
2442
        };
2443

2444
        r = sd_varlink_dispatch(link, parameters, dispatch_table, &context);
17✔
2445
        if (r != 0)
17✔
2446
                return r;
2447

2448
        /* Listing features doesn’t require a polkit check */
2449

2450
        if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
17✔
2451
                context.verify = 0;
17✔
2452

2453
        /* ListFeatures is always offline */
2454
        context.offline = true;
17✔
2455

2456
        r = context_load_offline_from_target(
17✔
2457
                        &context,
2458
                        PROCESS_IMAGE_READ_ONLY,
2459
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2460
        if (r == -ENOENT)
17✔
UNCOV
2461
                return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL);
×
2462
        if (r < 0)
17✔
2463
                return r;
2464

2465
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *l = NULL;
17✔
2466

2467
        HASHMAP_FOREACH(f, context.features) {
33✔
2468
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
16✔
2469

2470
                r = feature_to_json(&context, f, &v);
16✔
2471
                if (r < 0)
16✔
2472
                        return r;
2473

2474
                r = sd_json_variant_append_array(&l, v);
16✔
2475
                if (r < 0)
16✔
2476
                        return r;
2477
        }
2478

2479
        if (!l) {
17✔
2480
                r = sd_json_variant_new_array(&l, NULL, 0);
1✔
2481
                if (r < 0)
1✔
2482
                        return r;
2483
        }
2484

2485
        return sd_varlink_replybo(link,
17✔
2486
                        SD_JSON_BUILD_PAIR_VARIANT("features", l));
2487
}
2488

2489
VERB_NOARG(verb_check_new, "check-new",
2490
           "Check if there's a new version available");
2491
static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) {
218✔
2492
        int r;
218✔
2493

2494
        assert(argc <= 1);
218✔
2495

2496
        _cleanup_(context_done) Context context = CONTEXT_NULL;
218✔
2497
        r = context_from_cmdline(&context);
218✔
2498
        if (r < 0)
218✔
2499
                return r;
2500

2501
        if (context.feature_select != SELECT_EXPLICIT)
218✔
UNCOV
2502
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2503
        if (context.component_select != SELECT_EXPLICIT)
218✔
UNCOV
2504
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
×
2505

2506
        r = context_load_online(
218✔
2507
                        &context,
2508
                        PROCESS_IMAGE_READ_ONLY,
2509
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
2510
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2511
        if (r < 0)
218✔
2512
                return r;
2513

2514
        if (!sd_json_format_enabled(arg_json_format_flags)) {
217✔
2515
                if (!context.candidate) {
185✔
2516
                        log_debug("No candidate found.");
96✔
2517
                        return EXIT_FAILURE;
2518
                }
2519

2520
                puts(context.candidate->version);
89✔
2521
        } else {
2522
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
32✔
2523

2524
                if (context.candidate)
32✔
UNCOV
2525
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version));
×
2526
                else
2527
                        r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_NULL("available"));
32✔
2528
                if (r < 0)
32✔
UNCOV
2529
                        return log_error_errno(r, "Failed to create JSON: %m");
×
2530

2531
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
32✔
2532
                if (r < 0)
32✔
UNCOV
2533
                        return log_error_errno(r, "Failed to print JSON: %m");
×
2534
        }
2535

2536
        return EXIT_SUCCESS;
2537
}
2538

2539
static int vl_method_check_new(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
200✔
2540
        _cleanup_(context_done) Context context = CONTEXT_NULL;
200✔
2541
        int r;
200✔
2542

2543
        assert(link);
200✔
2544

2545
        static const sd_json_dispatch_field dispatch_table[] = {
200✔
2546
                { "target", SD_JSON_VARIANT_OBJECT, dispatch_target_identifier, voffsetof(context, target_identifier), SD_JSON_MANDATORY },
2547
                VARLINK_DISPATCH_POLKIT_FIELD,
2548
                {},
2549
        };
2550

2551
        r = sd_varlink_dispatch(link, parameters, dispatch_table, &context);
200✔
2552
        if (r != 0)
200✔
2553
                return r;
2554

2555
        r = verify_polkit(&context, link, "org.freedesktop.sysupdate1.check",
576✔
2556
                        (const char**) STRV_MAKE(
376✔
2557
                                        "class", target_class_to_string(context.target_identifier.class),
2558
                                        "offline", "0",
2559
                                        context.target_identifier.name ? "name" : NULL, context.target_identifier.name));
2560
        if (r <= 0)
192✔
2561
                return r;
2562

2563
        if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
192✔
2564
                context.verify = 0;
192✔
2565

2566
        /* CheckNew is always online */
2567
        context.offline = false;
192✔
2568

2569
        r = context_load_online_from_target(
192✔
2570
                        &context,
2571
                        PROCESS_IMAGE_READ_ONLY,
2572
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
2573
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2574
        if (r == -ENOENT)
192✔
2575
                return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL);
8✔
2576
        if (r < 0)
184✔
2577
                return r;
2578

2579
        if (context.candidate)
184✔
2580
                r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version));
88✔
2581
        else
2582
                r = sd_varlink_error(link, "io.systemd.SysUpdate.NoUpdateNeeded", NULL);
96✔
2583
        if (r < 0)
184✔
2584
                return r;
96✔
2585

2586
        return 0;
2587
}
2588

2589
typedef enum {
2590
        UPDATE_ACTION_ACQUIRE = 1 << 0,
2591
        UPDATE_ACTION_INSTALL = 1 << 1,
2592
} UpdateActionFlags;
2593

2594
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
394✔
2595
        _cleanup_free_ char *booted_version = NULL;
394✔
2596
        UpdateSet *applied = NULL;
394✔
2597
        const char *version;
394✔
2598
        int r;
394✔
2599

2600
        assert(argc <= 2);
394✔
2601
        version = argc >= 2 ? argv[1] : NULL;
394✔
2602

2603
        _cleanup_(context_done) Context context = CONTEXT_NULL;
394✔
2604
        r = context_from_cmdline(&context);
394✔
2605
        if (r < 0)
394✔
2606
                return r;
2607

2608
        if (context.feature_select != SELECT_EXPLICIT)
394✔
UNCOV
2609
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2610
        if (context.component_select != SELECT_EXPLICIT)
394✔
2611
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
1✔
2612

2613
        if (context.instances_max < 2)
393✔
UNCOV
2614
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2615
                                      "The --instances-max= argument must be >= 2 while updating");
2616

2617
        if (context.reboot) {
393✔
2618
                /* If automatic reboot on completion is requested, let's first determine the currently booted image */
2619

UNCOV
2620
                r = parse_os_release(context.root, "IMAGE_VERSION", &booted_version);
×
UNCOV
2621
                if (r < 0)
×
UNCOV
2622
                        return log_error_errno(r, "Failed to parse /etc/os-release: %m");
×
UNCOV
2623
                if (!booted_version)
×
UNCOV
2624
                        return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
×
2625
        }
2626

2627
        bool installed = false;
393✔
2628
        int ret = 0;
393✔
2629

2630
        r = context_load_online(
393✔
2631
                        &context,
2632
                        /* process_image_flags= */ 0,
2633
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
2634
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS|
2635
                        READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
2636
        if (r < 0) {
393✔
2637
                if (r != -ENOENT)
19✔
2638
                        return r;
2639

2640
                /* No transfer files found. In that case, still do the installdb cleanup below */
2641
                RET_GATHER(ret, r);
2642
        } else {
2643
                if (action_flags & UPDATE_ACTION_ACQUIRE)
374✔
2644
                        r = context_acquire(&context, version);
290✔
2645
                else
2646
                        r = context_process_partial_and_pending(&context, version);
84✔
2647
                if (r < 0)
374✔
2648
                        return r;
2649

2650
                if (FLAGS_SET(action_flags, UPDATE_ACTION_INSTALL) && r > 0) { /* installation of update indicated */
338✔
2651
                        r = context_install(&context, version, &applied);
176✔
2652
                        if (r < 0)
176✔
2653
                                return r;
2654

2655
                        installed = r > 0;
176✔
2656
                }
2657

2658
                /* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If
2659
                 * nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a
2660
                 * resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */
2661
                if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) {
226✔
2662
                        int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY");
50✔
2663
                        if (f < 0 && f != -ENXIO)
50✔
UNCOV
2664
                                log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m");
×
2665
                        if (f > 0)
50✔
UNCOV
2666
                                (void) context_notify_subscribers(&context, /* us= */ NULL);
×
2667
                }
2668
        }
2669

2670
        if (context.cleanup > 0)
338✔
2671
                RET_GATHER(ret, installdb_cleanup_component(&context));
2✔
2672

2673
        if (installed) {
338✔
2674
                /* We installed something, yay */
2675

2676
                if (context.reboot) {
176✔
UNCOV
2677
                        assert(applied);
×
UNCOV
2678
                        assert(booted_version);
×
2679

UNCOV
2680
                        if (strverscmp_improved(applied->version, booted_version) > 0) {
×
UNCOV
2681
                                log_notice("Newly installed version is newer than booted version, rebooting.");
×
UNCOV
2682
                                RET_GATHER(ret, reboot_now());
×
2683
                        } else if (strverscmp_improved(applied->version, booted_version) == 0 &&
×
UNCOV
2684
                                   FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) {
×
UNCOV
2685
                                log_notice("Currently booted version was incomplete and has been repaired, rebooting.");
×
2686
                                RET_GATHER(ret, reboot_now());
×
2687
                        } else
2688
                                log_info("Booted version is newer or identical to newly installed version, not rebooting.");
×
2689
                }
2690
        }
2691

2692
        return ret;
2693
}
2694

2695
VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0,
2696
     "Install new version now");
2697
static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) {
258✔
2698
        UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
258✔
2699

2700
        if (!arg_offline)
258✔
2701
                flags |= UPDATE_ACTION_ACQUIRE;
174✔
2702

2703
        return verb_update_impl(argc, argv, flags);
258✔
2704
}
2705

2706
VERB(verb_acquire, "acquire", "[VERSION]", VERB_ANY, 2, 0,
2707
     "Acquire (download) new version now");
2708
static int verb_acquire(int argc, char *argv[], uintptr_t _data, void *userdata) {
136✔
2709
        return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
136✔
2710
}
2711

2712
VERB_NOARG(verb_vacuum, "vacuum",
2713
           "Make room, by deleting old versions");
2714
static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) {
32✔
2715
        _cleanup_(context_done) Context context = CONTEXT_NULL;
32✔
2716
        int r;
32✔
2717

2718
        assert(argc <= 1);
32✔
2719

2720
        r = context_from_cmdline(&context);
32✔
2721
        if (r < 0)
32✔
2722
                return r;
2723

2724
        if (context.feature_select != SELECT_EXPLICIT)
32✔
UNCOV
2725
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2726
        if (context.component_select != SELECT_EXPLICIT)
32✔
2727
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
×
2728

2729
        if (context.instances_max < 1)
32✔
UNCOV
2730
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2731
                                      "The --instances-max= argument must be >= 1 while vacuuming");
2732

2733
        r = context_load_offline(
32✔
2734
                        &context,
2735
                        /* process_image_flags= */ 0,
2736
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
2737
        if (r < 0)
32✔
2738
                return r;
2739

2740
        return context_vacuum(&context, 0, NULL);
32✔
2741
}
2742

2743
VERB_NOARG(verb_cleanup, "cleanup", "Clean up orphaned files");
2744
static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) {
7✔
2745
        _cleanup_(context_done) Context context = CONTEXT_NULL;
7✔
2746
        int r;
7✔
2747

2748
        assert(argc <= 1);
7✔
2749

2750
        r = context_from_cmdline(&context);
7✔
2751
        if (r < 0)
7✔
2752
                return r;
2753

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

2757
        if (context.feature_select != SELECT_EXPLICIT)
6✔
2758
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2759
        if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL))
6✔
UNCOV
2760
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-suggested currently not supported for '%s'.", argv[0]);
×
2761

2762
        r = context_load_offline(
6✔
2763
                        &context,
2764
                        /* process_image_flags= */ 0,
2765
                        /* read_definitions_flags= */ 0);
2766
        if (r < 0)
6✔
2767
                return r;
2768

2769
        int ret = 0;
6✔
2770
        RET_GATHER(ret, installdb_cleanup_component(&context));
6✔
2771

2772
        if (context.component_select == SELECT_ALL) {
6✔
2773
                _cleanup_strv_free_ char **z = NULL;
2✔
2774
                r = installdb_list_components(&context, &z);
2✔
2775
                if (r < 0)
2✔
UNCOV
2776
                        return log_error_errno(r, "Failed to enumerate components: %m");
×
2777

2778
                STRV_FOREACH(i, z) {
6✔
2779
                        _cleanup_(context_done) Context component_context = CONTEXT_NULL;
×
2780

2781
                        r = context_from_cmdline(&component_context);
4✔
2782
                        if (r < 0)
4✔
2783
                                return r;
2784

2785
                        /* Override the component with our iter. This needs to be done in a fresh Context
2786
                         * as the installdb_fd and other state are specific to the component. */
2787
                        r = free_and_strdup_warn(&component_context.component, *i);
4✔
2788
                        if (r < 0)
4✔
2789
                                return r;
2790

2791
                        r = context_load_offline(
4✔
2792
                                        &component_context,
2793
                                        /* process_image_flags= */ 0,
2794
                                        /* read_definitions_flags= */ 0);
2795
                        if (r < 0)
4✔
2796
                                return r;
2797

2798
                        RET_GATHER(ret, installdb_cleanup_component(&component_context));
4✔
2799
                }
2800
        }
2801

2802
        return ret;
2803
}
2804

2805
VERB(verb_pending_or_reboot, "pending", NULL, 1, 1, 0,
2806
     "Report whether a newer version is installed than currently booted");
2807
VERB(verb_pending_or_reboot, "reboot", NULL, 1, 1, 0,
2808
     "Reboot if a newer version is installed than booted");
2809
static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
2✔
UNCOV
2810
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
2811
        _cleanup_free_ char *booted_version = NULL;
2✔
2812
        int r;
2✔
2813

2814
        assert(argc == 1);
2✔
2815

2816
        r = context_from_cmdline(&context);
2✔
2817
        if (r < 0)
2✔
2818
                return r;
2819

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

2824
        if (context.feature_select != SELECT_EXPLICIT)
2✔
UNCOV
2825
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2826
        if (context.component || context.component_select != SELECT_EXPLICIT)
2✔
2827
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
2✔
2828
                                       "The --component=, --component-all and --component-suggested switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]);
2829

UNCOV
2830
        r = context_load_offline(
×
2831
                        &context,
2832
                        /* process_image_flags= */ 0,
2833
                        READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
2834
                        READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS|
2835
                        READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
UNCOV
2836
        if (r < 0)
×
2837
                return r;
2838

2839
        log_info("Determining installed update sets%s", glyph(GLYPH_ELLIPSIS));
×
2840

UNCOV
2841
        r = context_discover_update_sets_by_flag(&context, UPDATE_INSTALLED);
×
2842
        if (r < 0)
×
2843
                return r;
2844
        if (!context.newest_installed)
×
2845
                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
×
2846

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

UNCOV
2854
        r = strverscmp_improved(context.newest_installed->version, booted_version);
×
2855
        if (r > 0) {
×
2856
                log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
×
2857
                           context.newest_installed->version, booted_version,
2858
                           streq(argv[0], "pending") ? " Reboot recommended." : "");
2859

2860
                if (streq(argv[0], "reboot"))
×
2861
                        return reboot_now();
×
2862

2863
                return EXIT_SUCCESS;
UNCOV
2864
        } else if (r == 0)
×
UNCOV
2865
                log_info("Newest installed version '%s' matches booted version '%s'.",
×
2866
                         context.newest_installed->version, booted_version);
2867
        else
2868
                log_warning("Newest installed version '%s' is older than booted version '%s'.",
×
2869
                            context.newest_installed->version, booted_version);
2870

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

2874
        return EXIT_SUCCESS;
2875
}
2876

2877
static int context_list_components(Context *context, char ***ret_component_names, bool *ret_has_default_component) {
187✔
2878
        int r;
187✔
2879

2880
        assert(context);
187✔
2881

2882
        _cleanup_strv_free_ char **z = NULL;
187✔
2883
        r = get_component_list(context->root, &z);
187✔
2884
        if (r < 0)
187✔
2885
                return r;
2886

2887
        if (ret_component_names)
187✔
2888
                *ret_component_names = TAKE_PTR(z);
187✔
2889

2890
        /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a
2891
         * TARGET_HOST? See target_get_argument() in sysupdated.c */
2892
        if (ret_has_default_component)
187✔
2893
                *ret_has_default_component =
358✔
2894
                        !context->definitions &&
358✔
2895
                        !context->component &&
179✔
2896
                        !context->root &&
179✔
2897
                        !context->image &&
358✔
2898
                        context->n_transfers > 0;
186✔
2899

2900
        return 0;
2901
}
2902

UNCOV
2903
static int context_component_is_suggested(Context *c) {
×
UNCOV
2904
        assert(c);
×
2905

2906
        /* Only applies to components, not to the main system */
UNCOV
2907
        if (!c->component)
×
2908
                return -ENOTTY;
2909

UNCOV
2910
        if (c->component_suggest >= 0)
×
2911
                return c->component_suggest;
2912

UNCOV
2913
        if (!c->component_suggest_on) /* no condition → false */
×
2914
                return false;
2915

UNCOV
2916
        return condition_test_list(c->component_suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL);
×
2917
}
2918

2919
VERB_NOARG(verb_components, "components",
2920
           "Show list of components");
2921
static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
175✔
2922
        int r;
175✔
2923

2924
        assert(argc <= 1);
175✔
2925

2926
        _cleanup_(context_done) Context context = CONTEXT_NULL;
175✔
2927
        r = context_from_cmdline(&context);
175✔
2928
        if (r < 0)
175✔
2929
                return r;
2930

2931
        if (context.feature_select != SELECT_EXPLICIT)
175✔
2932
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
2933
        if (context.component_select != SELECT_EXPLICIT)
175✔
UNCOV
2934
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
×
2935
        if (context.definitions)
175✔
2936
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2937
                                       "The --definitions= switch may not be combined with '%s'.", argv[0]);
2938

2939
        r = context_load_offline(
175✔
2940
                        &context,
2941
                        /* process_image_flags= */ 0,
2942
                        /* read_definitions_flags= */ 0);
2943
        if (r < 0)
175✔
2944
                return r;
2945

2946
        _cleanup_strv_free_ char **component_names = NULL;
175✔
2947
        bool has_default_component = false;
175✔
2948
        r = context_list_components(&context, &component_names, &has_default_component);
175✔
2949
        if (r < 0)
175✔
2950
                return log_error_errno(r, "Failed to enumerate components: %m");
×
2951

2952
        if (!sd_json_format_enabled(arg_json_format_flags)) {
175✔
2953
                if (!has_default_component && strv_isempty(component_names)) {
1✔
2954
                        log_info("No components defined.");
1✔
2955
                        return 0;
1✔
2956
                }
2957

UNCOV
2958
                _cleanup_(table_unrefp) Table *t = table_new("enabled", "suggested", "component", "description", "documentation");
×
2959
                if (!t)
×
UNCOV
2960
                        return log_oom();
×
2961

UNCOV
2962
                table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
×
2963

UNCOV
2964
                if (has_default_component) {
×
UNCOV
2965
                        r = table_add_many(
×
2966
                                        t,
2967
                                        TABLE_EMPTY,
2968
                                        TABLE_EMPTY,
2969
                                        TABLE_STRING, "<default>",
2970
                                        TABLE_SET_COLOR, ansi_highlight(),
2971
                                        TABLE_STRING, "Default Component",
2972
                                        TABLE_EMPTY);
2973
                        if (r < 0)
×
UNCOV
2974
                                return table_log_add_error(r);
×
2975
                }
2976

UNCOV
2977
                STRV_FOREACH(i, component_names) {
×
UNCOV
2978
                        _cleanup_(context_done) Context cc = CONTEXT_NULL;
×
2979

UNCOV
2980
                        r = context_from_base_with_component(
×
2981
                                        &context,
2982
                                        *i,
2983
                                        &cc);
2984
                        if (r < 0)
×
2985
                                return r;
2986

UNCOV
2987
                        r = context_load_offline(
×
2988
                                        &cc,
2989
                                        /* process_image_flags= */ 0,
2990
                                        /* read_definitions_flags= */ 0);
UNCOV
2991
                        if (r == -ENOMEM)
×
2992
                                return r;
UNCOV
2993
                        if (r < 0)
×
UNCOV
2994
                                continue;
×
2995

UNCOV
2996
                        r = table_add_many(
×
2997
                                        t,
2998
                                        TABLE_BOOLEAN_CHECKMARK, cc.component_enabled,
2999
                                        TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled));
UNCOV
3000
                        if (r < 0)
×
UNCOV
3001
                                return table_log_add_error(r);
×
3002

UNCOV
3003
                        r = context_component_is_suggested(&cc);
×
3004
                        if (r < 0)
×
3005
                                r = table_add_many(t, TABLE_EMPTY);
×
3006
                        else
3007
                                r = table_add_many(t, TABLE_BOOLEAN_CHECKMARK, r);
×
UNCOV
3008
                        if (r < 0)
×
3009
                                return table_log_add_error(r);
×
3010

3011
                        const char *doc = cc.component_documentation ? cc.component_documentation[0] : NULL;
×
3012

3013
                        r = table_add_many(
×
3014
                                        t,
3015
                                        TABLE_STRING, *i,
3016
                                        TABLE_STRING, cc.component_description,
3017
                                        TABLE_STRING, doc,
3018
                                        TABLE_SET_URL, doc);
UNCOV
3019
                        if (r < 0)
×
UNCOV
3020
                                return table_log_add_error(r);
×
3021
                }
3022

UNCOV
3023
                return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
×
3024
        } else {
UNCOV
3025
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
×
3026

3027
                r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component),
174✔
3028
                                          SD_JSON_BUILD_PAIR_STRV("components", component_names));
3029
                if (r < 0)
174✔
UNCOV
3030
                        return log_error_errno(r, "Failed to create JSON: %m");
×
3031

3032
                r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
174✔
3033
                if (r < 0)
174✔
3034
                        return log_error_errno(r, "Failed to print JSON: %m");
×
3035
        }
3036

3037
        return 0;
174✔
3038
}
3039

3040
static int vl_method_list_targets(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
4✔
3041
        _cleanup_(context_done) Context context = CONTEXT_NULL;
4✔
UNCOV
3042
        _cleanup_set_free_ Set *targets = NULL;
×
3043
        int r;
4✔
3044

3045
        assert(link);
4✔
3046

3047
        r = sd_varlink_dispatch(link, parameters, NULL, NULL);
4✔
3048
        if (r != 0)
4✔
3049
                return r;
3050

3051
        /* Listing targets doesn’t require a polkit check */
3052

3053
        if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
4✔
3054
                context.verify = 0;
4✔
3055

3056
        /* ListTargets is always offline */
3057
        context.offline = true;
4✔
3058

3059
        r = context_load_offline(
4✔
3060
                        &context,
3061
                        PROCESS_IMAGE_READ_ONLY,
3062
                        /* read_definitions_flags= */ 0);
3063
        if (r < 0)
4✔
3064
                return r;
3065

3066
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *l = NULL;
4✔
3067

3068
        r = context_enumerate_targets(&context, &targets);
4✔
3069
        if (r < 0)
4✔
3070
                return r;
3071

3072
        /* Sort to ensure consistent ordering */
3073
        size_t n;
4✔
3074
        _cleanup_free_ TargetIdentifier **sorted = NULL;
4✔
3075
        r = set_dump_sorted(targets, (void***) &sorted, &n);
4✔
3076
        if (r < 0)
4✔
UNCOV
3077
                return log_oom();
×
3078

3079
        FOREACH_ARRAY(p, sorted, n) {
7✔
3080
                TargetIdentifier *target_identifier = *p;
3✔
3081
                const char *name = (target_identifier->class != TARGET_HOST) ? target_identifier->name : NULL;
3✔
3082

3083
                r = sd_json_variant_append_arraybo(&l,
3✔
3084
                                SD_JSON_BUILD_PAIR_OBJECT("id",
3085
                                                JSON_BUILD_PAIR_ENUM("class", target_class_to_string(target_identifier->class)),
3086
                                                SD_JSON_BUILD_PAIR_STRING("name", name)));
3087
                if (r < 0)
3✔
3088
                        return r;
3089
        }
3090

3091
        if (!l) {
4✔
3092
                r = sd_json_variant_new_array(&l, NULL, 0);
1✔
3093
                if (r < 0)
1✔
3094
                        return r;
3095
        }
3096

3097
        return sd_varlink_replybo(link,
4✔
3098
                        SD_JSON_BUILD_PAIR_VARIANT("targets", l));
3099
}
3100

3101
VERB(verb_enable_component, "enable-component", "COMPONENT…", 1, VERB_ANY, 0,
3102
     "Enable component");
3103
VERB(verb_enable_component, "disable-component", "COMPONENT…", 1, VERB_ANY, 0,
3104
     "Disable component");
3105
static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void *userdata) {
×
3106
        bool enable = streq(argv[0], "enable-component");
×
3107
        int r;
×
3108

UNCOV
3109
        _cleanup_(context_done) Context context = CONTEXT_NULL;
×
UNCOV
3110
        r = context_from_cmdline(&context);
×
UNCOV
3111
        if (r < 0)
×
3112
                return r;
3113

UNCOV
3114
        if (context.feature_select != SELECT_EXPLICIT)
×
UNCOV
3115
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
×
UNCOV
3116
        if (context.definitions)
×
UNCOV
3117
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
3118
                                       "The --definitions= switch may not be combined with '%s'.", argv[0]);
3119

UNCOV
3120
        char **arguments, *array[2];
×
3121
        if (argc > 1) {
×
3122
                if (context.component || context.component_select != SELECT_EXPLICIT)
×
UNCOV
3123
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify component names as positional parameter or via --component=/--component-all/--component-suggested, not both.");
×
3124

3125
                arguments = strv_skip(argv, 1);
×
3126

UNCOV
3127
        } else if (context.component_select == SELECT_EXPLICIT) {
×
UNCOV
3128
                if (!context.component)
×
UNCOV
3129
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No component specified.");
×
3130

UNCOV
3131
                array[0] = context.component;
×
UNCOV
3132
                array[1] = NULL;
×
3133
                arguments = array;
×
3134
        } else {
UNCOV
3135
                assert(!context.component);
×
3136
                arguments = NULL;
3137
        }
3138

UNCOV
3139
        STRV_FOREACH(name, arguments)
×
UNCOV
3140
                if (!component_name_valid(*name))
×
UNCOV
3141
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name);
×
3142

UNCOV
3143
        r = context_load_offline(
×
3144
                        &context,
3145
                        /* process_image_flags= */ 0,
3146
                        /* read_definitions_flags= */ 0);
UNCOV
3147
        if (r < 0)
×
3148
                return r;
3149

UNCOV
3150
        _cleanup_strv_free_ char **component_names = NULL;
×
UNCOV
3151
        r = context_list_components(&context, &component_names, /* ret_has_default_component= */ NULL);
×
UNCOV
3152
        if (r < 0)
×
3153
                return r;
3154

3155
        _cleanup_strv_free_ char **suggested = NULL;
×
3156

UNCOV
3157
        switch (context.component_select) {
×
3158

3159
        case SELECT_EXPLICIT:
UNCOV
3160
                STRV_FOREACH(name, arguments)
×
UNCOV
3161
                        if (!strv_contains(component_names, *name))
×
UNCOV
3162
                                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name);
×
3163
                break;
3164

3165
        case SELECT_ALL:
×
UNCOV
3166
                assert(!arguments);
×
UNCOV
3167
                arguments = component_names;
×
UNCOV
3168
                break;
×
3169

UNCOV
3170
        case SELECT_SUGGESTED:
×
UNCOV
3171
                assert(!arguments);
×
UNCOV
3172
                STRV_FOREACH(name, component_names) {
×
UNCOV
3173
                        _cleanup_(context_done) Context cc = CONTEXT_NULL;
×
3174

UNCOV
3175
                        r = context_from_base_with_component(&context, *name, &cc);
×
UNCOV
3176
                        if (r < 0)
×
3177
                                return r;
3178

UNCOV
3179
                        r = context_load_offline(
×
3180
                                        &cc,
3181
                                        /* process_image_flags= */ 0,
3182
                                        /* read_definitions_flags= */ 0);
UNCOV
3183
                        if (r < 0)
×
UNCOV
3184
                                continue;
×
3185

UNCOV
3186
                        r = context_component_is_suggested(&cc);
×
UNCOV
3187
                        if (r < 0) {
×
UNCOV
3188
                                log_warning_errno(r, "Failed to determine whether '%s' shall be enabled, skipping: %m", *name);
×
UNCOV
3189
                                continue;
×
3190
                        }
3191

3192
                        /* This reconciles the system with the suggestions: on 'enable-component' we act on
3193
                         * the components that are suggested, on 'disable-component' we act on the ones that
3194
                         * are not. Hence pick the components whose suggestion state matches the operation. */
UNCOV
3195
                        if (!!r != !!enable) {
×
UNCOV
3196
                                log_debug("Skipping '%s'.", *name);
×
UNCOV
3197
                                continue;
×
3198
                        }
3199

UNCOV
3200
                        log_info("%s '%s'.", enable ? "Enabling" : "Disabling", *name);
×
3201

UNCOV
3202
                        if (strv_extend(&suggested, *name) < 0)
×
UNCOV
3203
                                return log_oom();
×
3204
                }
3205

UNCOV
3206
                arguments = suggested;
×
UNCOV
3207
                break;
×
3208

UNCOV
3209
        default:
×
UNCOV
3210
                assert_not_reached();
×
3211
        }
3212

UNCOV
3213
        if (strv_isempty(arguments)) {
×
UNCOV
3214
                log_info("No components selected.");
×
3215
                return 0;
3216
        }
3217

3218
        /* Component definition files live directly below the configuration directories, hence the drop-in
3219
         * goes right next to them below /etc. */
UNCOV
3220
        _cleanup_free_ char *dropin_dir = path_join(context.root, SYSCONF_DIR);
×
UNCOV
3221
        if (!dropin_dir)
×
UNCOV
3222
                return log_oom();
×
3223

UNCOV
3224
        STRV_FOREACH(name, arguments) {
×
UNCOV
3225
                _cleanup_free_ char *fname = strjoin("sysupdate.", *name, ".component");
×
UNCOV
3226
                if (!fname)
×
UNCOV
3227
                        return log_oom();
×
3228

3229
                /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
UNCOV
3230
                r = write_drop_in_format(
×
3231
                                dropin_dir,
3232
                                fname,
3233
                                50, "systemd-sysupdate-enabled",
3234
                                "# Generated via 'systemd-sysupdate %s'\n\n"
3235
                                "[Component]\n"
3236
                                "Enabled=%s\n",
3237
                                argv[0],
3238
                                yes_no(enable));
UNCOV
3239
                if (r < 0)
×
UNCOV
3240
                        return log_error_errno(r, "Failed to write drop-in for component '%s': %m", *name);
×
3241

UNCOV
3242
                log_info("Component '%s' %s.", *name, enable ? "enabled" : "disabled");
×
3243
        }
3244

3245
        return 0;
3246
}
3247

UNCOV
3248
static int help(void) {
×
UNCOV
3249
        _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL;
×
UNCOV
3250
        int r;
×
3251

UNCOV
3252
        r = verbs_get_help_table(&verbs);
×
UNCOV
3253
        if (r < 0)
×
3254
                return r;
3255

UNCOV
3256
        r = option_parser_get_help_table(&common_options);
×
UNCOV
3257
        if (r < 0)
×
3258
                return r;
3259

UNCOV
3260
        r = option_parser_get_help_table_group("Options", &options);
×
UNCOV
3261
        if (r < 0)
×
3262
                return r;
3263

UNCOV
3264
        (void) table_sync_column_widths(0, verbs, common_options, options);
×
3265

UNCOV
3266
        help_cmdline("[OPTIONS…] [VERSION]");
×
UNCOV
3267
        help_abstract("Update OS images.");
×
3268

UNCOV
3269
        help_section("Commands");
×
UNCOV
3270
        r = table_print_or_warn(verbs);
×
UNCOV
3271
        if (r < 0)
×
3272
                return r;
3273

UNCOV
3274
        r = table_print_or_warn(common_options);
×
UNCOV
3275
        if (r < 0)
×
3276
                return r;
3277

UNCOV
3278
        help_section("Options");
×
UNCOV
3279
        r = table_print_or_warn(options);
×
UNCOV
3280
        if (r < 0)
×
3281
                return r;
3282

UNCOV
3283
        help_man_page_reference("systemd-sysupdate", "8");
×
3284
        return 0;
3285
}
3286

UNCOV
3287
VERB_COMMON_HELP_HIDDEN(help);
×
3288

3289
static int parse_argv(int argc, char *argv[], char ***remaining_args) {
1,344✔
3290
        assert(argc >= 0);
1,344✔
3291
        assert(argv);
1,344✔
3292
        assert(remaining_args);
1,344✔
3293

3294
        OptionParser opts = { argc, argv };
1,344✔
3295
        int r;
1,344✔
3296

3297
        FOREACH_OPTION_OR_RETURN(c, &opts)
4,169✔
3298
                switch (c) {
1,489✔
3299

UNCOV
3300
                OPTION_COMMON_HELP:
×
UNCOV
3301
                        return help();
×
3302

UNCOV
3303
                OPTION_COMMON_VERSION:
×
UNCOV
3304
                        return version();
×
3305

3306
                OPTION_GROUP("Options"):
3307
                        break;
3308

3309
                OPTION('C', "component", "NAME",
45✔
3310
                       "Select component to update"):
3311
                        if (isempty(opts.arg)) {
45✔
UNCOV
3312
                                arg_component = mfree(arg_component);
×
UNCOV
3313
                                arg_component_select = SELECT_EXPLICIT;
×
UNCOV
3314
                                break;
×
3315
                        }
3316

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

3320
                        r = free_and_strdup_warn(&arg_component, opts.arg);
37✔
3321
                        if (r < 0)
37✔
3322
                                return r;
3323

3324
                        arg_component_select = SELECT_EXPLICIT;
37✔
3325
                        break;
37✔
3326

3327
                OPTION('A', "component-all", NULL, "Select all components"):
3✔
3328
                        arg_component = mfree(arg_component);
3✔
3329
                        arg_component_select = SELECT_ALL;
3✔
3330
                        break;
3✔
3331

UNCOV
3332
                OPTION('S', "component-suggested", NULL, "Select all suggested components"):
×
UNCOV
3333
                        arg_component = mfree(arg_component);
×
UNCOV
3334
                        arg_component_select = SELECT_SUGGESTED;
×
UNCOV
3335
                        break;
×
3336

UNCOV
3337
                OPTION('a', "feature-all", NULL, "Select all features"):
×
UNCOV
3338
                        arg_feature_select = SELECT_ALL;
×
UNCOV
3339
                        break;
×
3340

UNCOV
3341
                OPTION('s', "feature-suggested", NULL, "Select all suggested features"):
×
UNCOV
3342
                        arg_feature_select = SELECT_SUGGESTED;
×
UNCOV
3343
                        break;
×
3344

3345
                OPTION_LONG("definitions", "DIR",
5✔
3346
                            "Find transfer definitions in specified directory"):
3347
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions);
5✔
3348
                        if (r < 0)
5✔
3349
                                return r;
3350
                        break;
3351

UNCOV
3352
                OPTION_LONG("root", "PATH",
×
3353
                            "Operate on an alternate filesystem root"):
UNCOV
3354
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
×
UNCOV
3355
                        if (r < 0)
×
3356
                                return r;
3357
                        break;
3358

UNCOV
3359
                OPTION_LONG("image", "PATH",
×
3360
                            "Operate on disk image as filesystem root"):
UNCOV
3361
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
UNCOV
3362
                        if (r < 0)
×
3363
                                return r;
3364
                        break;
3365

UNCOV
3366
                OPTION_LONG("image-policy", "POLICY",
×
3367
                            "Specify disk image dissection policy"):
UNCOV
3368
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
UNCOV
3369
                        if (r < 0)
×
3370
                                return r;
3371
                        break;
3372

UNCOV
3373
                OPTION_LONG("transfer-source", "PATH",
×
3374
                            "Specify the directory to transfer sources from"):
UNCOV
3375
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_transfer_source);
×
UNCOV
3376
                        if (r < 0)
×
3377
                                return r;
3378

3379
                        break;
3380

UNCOV
3381
                OPTION('m', "instances-max", "INT",
×
3382
                       "How many instances to maintain"):
UNCOV
3383
                        r = safe_atou64(opts.arg, &arg_instances_max);
×
UNCOV
3384
                        if (r < 0)
×
UNCOV
3385
                                return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", opts.arg);
×
3386

3387
                        break;
3388

UNCOV
3389
                OPTION_LONG("sync", "BOOL",
×
3390
                            "Controls whether to sync data to disk"):
UNCOV
3391
                        r = parse_boolean_argument("--sync=", opts.arg, &arg_sync);
×
UNCOV
3392
                        if (r < 0)
×
3393
                                return r;
3394
                        break;
3395

3396
                OPTION_LONG("verify", "BOOL",
745✔
3397
                            "Force signature verification on or off"): {
3398
                        bool b;
745✔
3399

3400
                        r = parse_boolean_argument("--verify=", opts.arg, &b);
745✔
3401
                        if (r < 0)
745✔
UNCOV
3402
                                return r;
×
3403

3404
                        arg_verify = b;
745✔
3405
                        break;
745✔
3406
                }
3407

3408
                OPTION_LONG("reboot", NULL,
1✔
3409
                            "Reboot after updating to newer version"):
3410
                        arg_reboot = true;
1✔
3411
                        break;
1✔
3412

3413
                OPTION_LONG("offline", NULL,
228✔
3414
                            "Do not fetch metadata from the network"):
3415
                        arg_offline = true;
228✔
3416
                        break;
228✔
3417

3418
                OPTION_LONG("cleanup", "BOOL", "Clean up orphaned files after completing update"): {
4✔
3419
                        bool b;
4✔
3420

3421
                        r = parse_boolean_argument("--cleanup=", opts.arg, &b);
4✔
3422
                        if (r < 0)
4✔
UNCOV
3423
                                return r;
×
3424

3425
                        arg_cleanup = b;
4✔
3426
                        break;
4✔
3427
                }
3428

UNCOV
3429
                OPTION_COMMON_NO_PAGER:
×
UNCOV
3430
                        arg_pager_flags |= PAGER_DISABLE;
×
UNCOV
3431
                        break;
×
3432

UNCOV
3433
                OPTION_COMMON_NO_LEGEND:
×
UNCOV
3434
                        arg_legend = false;
×
UNCOV
3435
                        break;
×
3436

3437
                OPTION_COMMON_JSON:
458✔
3438
                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
458✔
3439
                        if (r <= 0)
458✔
3440
                                return r;
3441

3442
                        break;
3443
                }
3444

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

3448
        if (arg_image || arg_root) {
1,336✔
UNCOV
3449
                if (arg_reboot)
×
UNCOV
3450
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
×
3451

UNCOV
3452
                if (arg_definitions)
×
UNCOV
3453
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with --root= or --image=.");
×
3454
        }
3455

3456
        if ((arg_component || arg_component_select != SELECT_EXPLICIT)) {
1,336✔
3457
                if (arg_reboot)
40✔
3458
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=/--component-all/--component-suggested, as automatic reboots only apply to the booted OS version.");
1✔
3459

3460
                if (arg_definitions)
39✔
UNCOV
3461
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component=/--component-all/--component-suggested switches may not be combined.");
×
3462
        }
3463

3464
        r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
1,335✔
3465
        if (r < 0)
1,335✔
UNCOV
3466
                return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
×
3467
        if (r > 0)
1,335✔
3468
                arg_varlink = true;
233✔
3469

3470
        *remaining_args = option_parser_get_args(&opts);
1,335✔
3471
        return 1;
1,335✔
3472
}
3473

3474
static int vl_server(void) {
233✔
3475
        _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
233✔
3476
        _cleanup_(server_done) Server server = SERVER_NULL;
233✔
3477
        int r;
233✔
3478

3479
        r = varlink_server_new(&varlink_server,
233✔
3480
                               SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA,
3481
                               &server);
3482
        if (r < 0)
233✔
UNCOV
3483
                return log_error_errno(r, "Failed to allocate Varlink server: %m");
×
3484

3485
        r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_SysUpdate);
233✔
3486
        if (r < 0)
233✔
UNCOV
3487
                return log_error_errno(r, "Failed to add Varlink interface: %m");
×
3488

3489
        r = sd_varlink_server_bind_method_many(
233✔
3490
                        varlink_server,
3491
                        "io.systemd.SysUpdate.CheckNew",     vl_method_check_new,
3492
                        "io.systemd.SysUpdate.ListFeatures", vl_method_list_features,
3493
                        "io.systemd.SysUpdate.ListTargets",  vl_method_list_targets);
3494
        if (r < 0)
233✔
UNCOV
3495
                return log_error_errno(r, "Failed to bind Varlink method: %m");
×
3496

3497
        r = sd_varlink_server_loop_auto(varlink_server);
233✔
3498
        if (r < 0)
233✔
UNCOV
3499
                return log_error_errno(r, "Failed to run Varlink event loop: %m");
×
3500

3501
        return 0;
3502
}
3503

3504
static int run(int argc, char *argv[]) {
1,344✔
3505
        int r;
1,344✔
3506

3507
        LIBAPPARMOR_NOTE(recommended);
1,344✔
3508
        LIBAUDIT_NOTE(recommended);
1,344✔
3509
        LIBBLKID_NOTE(recommended);
1,344✔
3510
        LIBCRYPTO_NOTE(suggested);
1,344✔
3511
        LIBCRYPTSETUP_NOTE(suggested);
1,344✔
3512
        LIBMOUNT_NOTE(recommended);
1,344✔
3513
        LIBSELINUX_NOTE(recommended);
1,344✔
3514
        LIBTSS2_ESYS_NOTE(suggested);
1,344✔
3515
        LIBTSS2_MU_NOTE(suggested);
1,344✔
3516
        LIBTSS2_RC_NOTE(suggested);
1,344✔
3517

3518
        log_setup();
1,344✔
3519

3520
        char **args = NULL;
1,344✔
3521
        r = parse_argv(argc, argv, &args);
1,344✔
3522
        if (r <= 0)
1,344✔
3523
                return r;
1,344✔
3524

3525
        if (arg_varlink)
1,335✔
3526
                return vl_server(); /* Invocation as Varlink service */
233✔
3527

3528
        return dispatch_verb(args, NULL);
1,102✔
3529
}
3530

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

© 2026 Coveralls, Inc