diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0001-refactor-RaucBundleAccessArgs-http_info_headers-from.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0001-refactor-RaucBundleAccessArgs-http_info_headers-from.patch new file mode 100644 index 00000000..b9cdbf30 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0001-refactor-RaucBundleAccessArgs-http_info_headers-from.patch @@ -0,0 +1,122 @@ +From 7a68ed8f8865cd5ad8562ee3f5aa8189c5908baa Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Feb 2025 16:14:09 +0100 +Subject: [PATCH 01/23] refactor RaucBundleAccessArgs->http_info_headers from + GStrv to GPtrArray + +Keeping it as a GPtrArray makes it it easy to modify after the initial +creation and also avoids the need to keep a trailing NULL pointer. In a +follow-up commit, we'll make assemble_info_headers usable for bundle +inspection as well. + +Signed-off-by: Jan Luebbe +--- + include/bundle.h | 2 +- + include/nbd.h | 2 +- + src/bundle.c | 5 +++-- + src/install.c | 7 +++---- + src/nbd.c | 5 +++-- + 5 files changed, 11 insertions(+), 10 deletions(-) + +diff --git a/include/bundle.h b/include/bundle.h +index eb64944d..6e14bdfb 100644 +--- a/include/bundle.h ++++ b/include/bundle.h +@@ -29,7 +29,7 @@ typedef struct { + gchar *tls_ca; + gboolean tls_no_verify; + GStrv http_headers; +- GStrv http_info_headers; ++ GPtrArray *http_info_headers; + } RaucBundleAccessArgs; + + typedef struct { +diff --git a/include/nbd.h b/include/nbd.h +index fd0b2428..8fe2846d 100644 +--- a/include/nbd.h ++++ b/include/nbd.h +@@ -39,7 +39,7 @@ typedef struct { + gchar *tls_ca; /* local file */ + gboolean tls_no_verify; + GStrv headers; /* array of strings such as 'Foo: bar' */ +- GStrv info_headers; /* array of strings such as 'Foo: bar' */ ++ GPtrArray *info_headers; /* array of strings such as 'Foo: bar' */ + + /* discovered information */ + guint64 data_size; /* bundle size */ +diff --git a/src/bundle.c b/src/bundle.c +index 59cd036d..96d31cdc 100644 +--- a/src/bundle.c ++++ b/src/bundle.c +@@ -2343,7 +2343,8 @@ gboolean check_bundle(const gchar *bundlename, RaucBundle **bundle, CheckBundleP + ibundle->nbd_srv->tls_ca = g_strdup(access_args->tls_ca); + ibundle->nbd_srv->tls_no_verify = access_args->tls_no_verify; + ibundle->nbd_srv->headers = g_strdupv(access_args->http_headers); +- ibundle->nbd_srv->info_headers = g_strdupv(access_args->http_info_headers); ++ if (access_args->http_info_headers) ++ ibundle->nbd_srv->info_headers = g_ptr_array_ref(access_args->http_info_headers); + } + if (!ibundle->nbd_srv->tls_cert) + ibundle->nbd_srv->tls_cert = g_strdup(r_context()->config->streaming_tls_cert); +@@ -3283,7 +3284,7 @@ void clear_bundle_access_args(RaucBundleAccessArgs *access_args) + g_free(access_args->tls_key); + g_free(access_args->tls_ca); + g_strfreev(access_args->http_headers); +- g_strfreev(access_args->http_info_headers); ++ g_clear_pointer(&access_args->http_info_headers, g_ptr_array_unref); + } + + memset(access_args, 0, sizeof(*access_args)); +diff --git a/src/install.c b/src/install.c +index 3b292fe8..49852a04 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -1526,9 +1526,9 @@ static gchar *system_info_to_header(const gchar *key, const gchar *value) + return g_strdup_printf("RAUC-%s: %s", header_key, value); + } + +-static gchar **assemble_info_headers(RaucInstallArgs *args) ++static GPtrArray *assemble_info_headers(RaucInstallArgs *args) + { +- GPtrArray *headers = g_ptr_array_new_with_free_func(g_free); ++ g_autoptr(GPtrArray) headers = g_ptr_array_new_with_free_func(g_free); + + g_return_val_if_fail(args, NULL); + +@@ -1569,9 +1569,8 @@ no_std_headers: + g_ptr_array_add(headers, header); + } + } +- g_ptr_array_add(headers, NULL); + +- return (gchar**) g_ptr_array_free(headers, FALSE); ++ return g_steal_pointer(&headers); + } + + gboolean do_install_bundle(RaucInstallArgs *args, GError **error) +diff --git a/src/nbd.c b/src/nbd.c +index d9677bb3..518bbc60 100644 +--- a/src/nbd.c ++++ b/src/nbd.c +@@ -86,7 +86,7 @@ void r_nbd_free_server(RaucNBDServer *nbd_srv) + g_free(nbd_srv->tls_key); + g_free(nbd_srv->tls_ca); + g_strfreev(nbd_srv->headers); +- g_strfreev(nbd_srv->info_headers); ++ g_clear_pointer(&nbd_srv->info_headers, g_ptr_array_unref); + g_free(nbd_srv->effective_url); + g_free(nbd_srv); + } +@@ -1032,7 +1032,8 @@ static gboolean nbd_configure(RaucNBDServer *nbd_srv, GError **error) + if (nbd_srv->headers) + g_variant_dict_insert(&dict, "headers", "^as", nbd_srv->headers); + if (nbd_srv->info_headers) +- g_variant_dict_insert(&dict, "info-headers", "^as", nbd_srv->info_headers); ++ g_variant_dict_insert(&dict, "info-headers", "@as", ++ g_variant_new_strv((const gchar **)nbd_srv->info_headers->pdata, nbd_srv->info_headers->len)); + v = g_variant_dict_end(&dict); + { + g_autofree gchar *tmp = g_variant_print(v, TRUE); +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0002-src-install-refactor-assemble_info_headers-to-avoid-.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0002-src-install-refactor-assemble_info_headers-to-avoid-.patch new file mode 100644 index 00000000..06f9d0e3 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0002-src-install-refactor-assemble_info_headers-to-avoid-.patch @@ -0,0 +1,55 @@ +From ac528c5b8c20de71497439e297d5655551aa4555 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Tue, 25 Feb 2025 10:31:16 +0100 +Subject: [PATCH 02/23] src/install: refactor assemble_info_headers to avoid + access to install args + +We want to use this function for all streaming bundle accesses, not just +installations. + +Signed-off-by: Jan Luebbe +--- + src/install.c | 10 ++++------ + 1 file changed, 4 insertions(+), 6 deletions(-) + +diff --git a/src/install.c b/src/install.c +index 49852a04..ac4c39fd 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -1526,12 +1526,10 @@ static gchar *system_info_to_header(const gchar *key, const gchar *value) + return g_strdup_printf("RAUC-%s: %s", header_key, value); + } + +-static GPtrArray *assemble_info_headers(RaucInstallArgs *args) ++static GPtrArray *assemble_info_headers(const gchar *transaction) + { + g_autoptr(GPtrArray) headers = g_ptr_array_new_with_free_func(g_free); + +- g_return_val_if_fail(args, NULL); +- + if (!r_context()->config->enabled_headers) + goto no_std_headers; + +@@ -1546,8 +1544,8 @@ static GPtrArray *assemble_info_headers(RaucInstallArgs *args) + if (g_strcmp0(*header, "variant") == 0) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Variant: %s", r_context()->config->system_variant)); + /* Add per-installation information */ +- if (g_strcmp0(*header, "transaction-id") == 0) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Transaction-ID: %s", args->transaction)); ++ if (g_strcmp0(*header, "transaction-id") == 0 && transaction != NULL) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Transaction-ID: %s", transaction)); + /* Add live information */ + if (g_strcmp0(*header, "uptime") == 0) { + g_autofree gchar *uptime = get_uptime(); +@@ -1604,7 +1602,7 @@ gboolean do_install_bundle(RaucInstallArgs *args, GError **error) + // TODO: mount info in context ? + install_args_update(args, "Checking and mounting bundle..."); + +- args->access_args.http_info_headers = assemble_info_headers(args); ++ args->access_args.http_info_headers = assemble_info_headers(args->transaction); + + res = check_bundle(bundlefile, &bundle, CHECK_BUNDLE_DEFAULT, &args->access_args, &ierror); + if (!res) { +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0003-move-assemble_info_headers-from-install.c-to-bundle..patch b/meta-lxatac-bsp/recipes-core/rauc/files/0003-move-assemble_info_headers-from-install.c-to-bundle..patch new file mode 100644 index 00000000..e32d83c1 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0003-move-assemble_info_headers-from-install.c-to-bundle..patch @@ -0,0 +1,232 @@ +From 6bb4dbe94d67fffe1fb0e18d5e4a6161a1cc4a21 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Tue, 25 Feb 2025 10:47:40 +0100 +Subject: [PATCH 03/23] move assemble_info_headers from install.c to bundle.c + +The collected info comes mainly from the context and will be used for +all streaming bundle accesses. + +Signed-off-by: Jan Luebbe +--- + include/bundle.h | 12 +++++++ + src/bundle.c | 84 +++++++++++++++++++++++++++++++++++++++++++++++ + src/install.c | 85 ------------------------------------------------ + 3 files changed, 96 insertions(+), 85 deletions(-) + +diff --git a/include/bundle.h b/include/bundle.h +index 6e14bdfb..0ef55f06 100644 +--- a/include/bundle.h ++++ b/include/bundle.h +@@ -276,6 +276,18 @@ void free_bundle(RaucBundle *bundle); + + G_DEFINE_AUTOPTR_CLEANUP_FUNC(RaucBundle, free_bundle); + ++/** ++ * Assembles HTTP Headers for use in the initial streaming request, based on the ++ * selection in the system config. Additional headers can be added to the ++ * GPtrArray later. ++ * ++ * @param transaction currently running installation transaction or NULL ++ * ++ * @return newly allocated GPtrArray with HTTP Header strings ++ */ ++GPtrArray *assemble_info_headers(const gchar *transaction) ++G_GNUC_WARN_UNUSED_RESULT; ++ + /** + * Frees the memory pointed to by the RaucBundleAccessArgs, but not the + * structure itself. +diff --git a/src/bundle.c b/src/bundle.c +index 96d31cdc..e174eac0 100644 +--- a/src/bundle.c ++++ b/src/bundle.c +@@ -3277,6 +3277,90 @@ void free_bundle(RaucBundle *bundle) + g_free(bundle); + } + ++static gchar* get_uptime(void) ++{ ++ g_autofree gchar *contents = NULL; ++ g_autoptr(GError) ierror = NULL; ++ g_auto(GStrv) uptime = NULL; ++ ++ if (!g_file_get_contents("/proc/uptime", &contents, NULL, &ierror)) { ++ g_warning("Failed to get uptime: %s", ierror->message); ++ return NULL; ++ } ++ ++ /* file contains two values and a newline, 'chomp' in-place and split then */ ++ uptime = g_strsplit(g_strchomp(contents), " ", 2); ++ ++ return g_strdup(uptime[0]); ++} ++ ++/* If the input key starts with RAUC_HTTP_, it returns a valid HTTP header ++ * string with 'RAUC_HTTP_' replaced by 'RAUC-'. ++ * If the input string does not start with RAUC_HTTP_, NULL is returned. ++ */ ++static gchar *system_info_to_header(const gchar *key, const gchar *value) ++{ ++ g_autofree gchar *header_key = NULL; ++ ++ g_return_val_if_fail(key, NULL); ++ g_return_val_if_fail(value, NULL); ++ ++ if (!g_str_has_prefix(key, "RAUC_HTTP_")) ++ return NULL; ++ ++ header_key = g_strdup(key + strlen("RAUC_HTTP_")); ++ for (size_t i = 0; i < strlen(header_key); i++) { ++ if (header_key[i] == '_') ++ header_key[i] = '-'; ++ } ++ ++ return g_strdup_printf("RAUC-%s: %s", header_key, value); ++} ++ ++GPtrArray *assemble_info_headers(const gchar *transaction) ++{ ++ g_autoptr(GPtrArray) headers = g_ptr_array_new_with_free_func(g_free); ++ ++ if (!r_context()->config->enabled_headers) ++ goto no_std_headers; ++ ++ for (gchar **header = r_context()->config->enabled_headers; *header; header++) { ++ /* Add static system information */ ++ if (g_strcmp0(*header, "boot-id") == 0) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Boot-ID: %s", r_context()->boot_id)); ++ if (g_strcmp0(*header, "machine-id") == 0) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Machine-ID: %s", r_context()->machine_id)); ++ if (g_strcmp0(*header, "serial") == 0) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Serial: %s", r_context()->system_serial)); ++ if (g_strcmp0(*header, "variant") == 0) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Variant: %s", r_context()->config->system_variant)); ++ /* Add per-installation information */ ++ if (g_strcmp0(*header, "transaction-id") == 0 && transaction != NULL) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Transaction-ID: %s", transaction)); ++ /* Add live information */ ++ if (g_strcmp0(*header, "uptime") == 0) { ++ g_autofree gchar *uptime = get_uptime(); ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-Uptime: %s", uptime)); ++ } ++ } ++ ++no_std_headers: ++ ++ if (r_context()->system_info) { ++ GHashTableIter iter; ++ gchar *key = NULL; ++ gchar *value = NULL; ++ ++ g_hash_table_iter_init(&iter, r_context()->system_info); ++ while (g_hash_table_iter_next(&iter, (gpointer*) &key, (gpointer*) &value)) { ++ gchar *header = system_info_to_header(key, value); ++ if (header) ++ g_ptr_array_add(headers, header); ++ } ++ } ++ ++ return g_steal_pointer(&headers); ++} + void clear_bundle_access_args(RaucBundleAccessArgs *access_args) + { + if (ENABLE_STREAMING) { +diff --git a/src/install.c b/src/install.c +index ac4c39fd..cecd6054 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -1486,91 +1486,6 @@ static gboolean launch_and_wait_default_handler(RaucInstallArgs *args, gchar* bu + return TRUE; + } + +-static gchar* get_uptime(void) +-{ +- g_autofree gchar *contents = NULL; +- g_autoptr(GError) ierror = NULL; +- g_auto(GStrv) uptime = NULL; +- +- if (!g_file_get_contents("/proc/uptime", &contents, NULL, &ierror)) { +- g_warning("Failed to get uptime: %s", ierror->message); +- return NULL; +- } +- +- /* file contains two values and a newline, 'chomp' in-place and split then */ +- uptime = g_strsplit(g_strchomp(contents), " ", 2); +- +- return g_strdup(uptime[0]); +-} +- +-/* If the input key starts with RAUC_HTTP_, it returns a valid HTTP header +- * string with 'RAUC_HTTP_' replaced by 'RAUC-'. +- * If the input string does not start with RAUC_HTTP_, NULL is returned. +- */ +-static gchar *system_info_to_header(const gchar *key, const gchar *value) +-{ +- g_autofree gchar *header_key = NULL; +- +- g_return_val_if_fail(key, NULL); +- g_return_val_if_fail(value, NULL); +- +- if (!g_str_has_prefix(key, "RAUC_HTTP_")) +- return NULL; +- +- header_key = g_strdup(key + strlen("RAUC_HTTP_")); +- for (size_t i = 0; i < strlen(header_key); i++) { +- if (header_key[i] == '_') +- header_key[i] = '-'; +- } +- +- return g_strdup_printf("RAUC-%s: %s", header_key, value); +-} +- +-static GPtrArray *assemble_info_headers(const gchar *transaction) +-{ +- g_autoptr(GPtrArray) headers = g_ptr_array_new_with_free_func(g_free); +- +- if (!r_context()->config->enabled_headers) +- goto no_std_headers; +- +- for (gchar **header = r_context()->config->enabled_headers; *header; header++) { +- /* Add static system information */ +- if (g_strcmp0(*header, "boot-id") == 0) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Boot-ID: %s", r_context()->boot_id)); +- if (g_strcmp0(*header, "machine-id") == 0) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Machine-ID: %s", r_context()->machine_id)); +- if (g_strcmp0(*header, "serial") == 0) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Serial: %s", r_context()->system_serial)); +- if (g_strcmp0(*header, "variant") == 0) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Variant: %s", r_context()->config->system_variant)); +- /* Add per-installation information */ +- if (g_strcmp0(*header, "transaction-id") == 0 && transaction != NULL) +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Transaction-ID: %s", transaction)); +- /* Add live information */ +- if (g_strcmp0(*header, "uptime") == 0) { +- g_autofree gchar *uptime = get_uptime(); +- g_ptr_array_add(headers, g_strdup_printf("RAUC-Uptime: %s", uptime)); +- } +- } +- +-no_std_headers: +- +- if (r_context()->system_info) { +- GHashTableIter iter; +- gchar *key = NULL; +- gchar *value = NULL; +- +- g_hash_table_iter_init(&iter, r_context()->system_info); +- while (g_hash_table_iter_next(&iter, (gpointer*) &key, (gpointer*) &value)) { +- gchar *header = system_info_to_header(key, value); +- if (header) +- g_ptr_array_add(headers, header); +- } +- } +- +- return g_steal_pointer(&headers); +-} +- + gboolean do_install_bundle(RaucInstallArgs *args, GError **error) + { + const gchar* bundlefile = args->name; +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0004-send-custom-headers-with-the-first-HTTP-streaming-re.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0004-send-custom-headers-with-the-first-HTTP-streaming-re.patch new file mode 100644 index 00000000..9d491b77 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0004-send-custom-headers-with-the-first-HTTP-streaming-re.patch @@ -0,0 +1,149 @@ +From 38c9d68df1646b3dd5dc74e003e120f63ee413bf Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Tue, 25 Feb 2025 11:36:22 +0100 +Subject: [PATCH 04/23] send custom headers with the first HTTP streaming + request + +Previously, they were sent only during installation. + +Signed-off-by: Jan Luebbe +--- + src/main.c | 3 +++ + src/service.c | 3 +++ + test/nginx_backend.py | 3 +++ + test/test_http.py | 52 ++++++++++++++++++++++++++++++++++++++----- + 4 files changed, 56 insertions(+), 5 deletions(-) + +diff --git a/src/main.c b/src/main.c +index 3e085047..f7cc0152 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -1307,6 +1307,9 @@ static gboolean info_start(int argc, char **argv) + if (no_check_time) + check_bundle_params |= CHECK_BUNDLE_NO_CHECK_TIME; + ++ g_assert(access_args.http_info_headers == NULL); ++ access_args.http_info_headers = assemble_info_headers(NULL); ++ + res = check_bundle(bundlelocation, &bundle, check_bundle_params, &access_args, &error); + if (!res) { + g_printerr("%s\n", error->message); +diff --git a/src/service.c b/src/service.c +index 98208815..1567fff3 100644 +--- a/src/service.c ++++ b/src/service.c +@@ -197,6 +197,9 @@ static gboolean r_on_handle_inspect_bundle(RInstaller *interface, + goto out; + } + ++ g_assert(access_args.http_info_headers == NULL); ++ access_args.http_info_headers = assemble_info_headers(NULL); ++ + res = check_bundle(arg_bundle, &bundle, CHECK_BUNDLE_DEFAULT, &access_args, &error); + if (!res) { + message = g_strdup(error->message); +diff --git a/test/nginx_backend.py b/test/nginx_backend.py +index a154f8aa..688d1c43 100755 +--- a/test/nginx_backend.py ++++ b/test/nginx_backend.py +@@ -62,6 +62,7 @@ async def token_get(request): + def reset_summary(request): + request.app["rauc"]["summary"] = { + "first_request_headers": {}, ++ "second_request_headers": {}, + "requests": 0, + "range_requests": [], + } +@@ -93,6 +94,8 @@ async def get_handler(request): + + if not summary["first_request_headers"]: + summary["first_request_headers"] = dict(request.headers) ++ elif not summary["second_request_headers"]: ++ summary["second_request_headers"] = dict(request.headers) + + if request.http_range: + start = str(request.http_range.start) or "" +diff --git a/test/test_http.py b/test/test_http.py +index a4b5ef32..1fb76847 100644 +--- a/test/test_http.py ++++ b/test/test_http.py +@@ -1,4 +1,5 @@ + import json ++import uuid + + from conftest import have_json + from helper import run +@@ -35,10 +36,40 @@ def test_backend_headers(http_server): + assert summary["first_request_headers"].get("RAUC-Test") == "value" + + ++def prune_standard_headers(headers): ++ for k in ["Host", "X-Forwarded-For", "Connection", "Accept", "User-Agent"]: ++ try: ++ del headers[k] ++ except KeyError: ++ pass ++ ++ ++def is_uuid(value): ++ try: ++ uuid.UUID(value) ++ except ValueError: ++ return False ++ return True ++ ++ ++def is_uptime(value): ++ try: ++ float(value) ++ except ValueError: ++ return False ++ return True ++ ++ + @have_json + def test_info_headers(create_system_files, system, http_server): + """Test if the info command sends custom headers correctly.""" + system.prepare_minimal_config() ++ system.config["handlers"] = { ++ "system-info": "bin/systeminfo.sh", ++ } ++ system.config["streaming"] = { ++ "send-headers": "boot-id;machine-id;serial;variant;transaction-id;uptime", ++ } + system.write_config() + http_server.setup( + file_path="test/good-verity-bundle.raucb", +@@ -54,13 +85,24 @@ def test_info_headers(create_system_files, system, http_server): + summary = http_server.get_summary() + assert summary["requests"] == 3 + +- headers = summary["first_request_headers"] +- assert headers["User-Agent"].startswith("rauc/") +- for k in ["Host", "X-Forwarded-For", "Connection", "Accept", "User-Agent"]: +- del headers[k] +- assert headers == { ++ first_headers = summary["first_request_headers"] ++ assert first_headers.pop("User-Agent").startswith("rauc/") ++ assert is_uuid(first_headers.pop("RAUC-Boot-ID")) ++ assert is_uuid(first_headers.pop("RAUC-Machine-ID")) ++ assert is_uptime(first_headers.pop("RAUC-Uptime")) ++ prune_standard_headers(first_headers) ++ assert first_headers == { + "Range": "bytes=0-3", + "Test-Header": "Test-Value", ++ "RAUC-Serial": "1234", ++ "RAUC-Variant": "test-variant-x", ++ } ++ ++ second_headers = summary["second_request_headers"] ++ prune_standard_headers(second_headers) ++ assert second_headers == { ++ "Range": "bytes=26498-26505", ++ "Test-Header": "Test-Value", + } + + assert summary["range_requests"] == [ +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0005-test-test_http-also-test-HTTP-info-headers-when-usin.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0005-test-test_http-also-test-HTTP-info-headers-when-usin.patch new file mode 100644 index 00000000..54e1da2d --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0005-test-test_http-also-test-HTTP-info-headers-when-usin.patch @@ -0,0 +1,75 @@ +From 59c3ad761553f829c222ce19a1181f275cbe3bbc Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Tue, 25 Feb 2025 16:26:02 +0100 +Subject: [PATCH 05/23] test/test_http: also test HTTP info headers when using + InspectBundle via D-Bus + +Signed-off-by: Jan Luebbe +--- + test/test_http.py | 34 ++++++++++++++++++++++++++-------- + 1 file changed, 26 insertions(+), 8 deletions(-) + +diff --git a/test/test_http.py b/test/test_http.py +index 1fb76847..033dc860 100644 +--- a/test/test_http.py ++++ b/test/test_http.py +@@ -1,7 +1,10 @@ + import json + import uuid + +-from conftest import have_json ++import pytest ++from dasbus.typing import get_native, get_variant ++ ++from conftest import have_json, have_service + from helper import run + + +@@ -60,9 +63,13 @@ def is_uptime(value): + return True + + ++@pytest.mark.parametrize("api", ["cli", "dbus"]) + @have_json +-def test_info_headers(create_system_files, system, http_server): ++def test_info_headers(create_system_files, system, http_server, api): + """Test if the info command sends custom headers correctly.""" ++ if api == "dbus" and not have_service(): ++ pytest.skip("Missing service") ++ + system.prepare_minimal_config() + system.config["handlers"] = { + "system-info": "bin/systeminfo.sh", +@@ -75,12 +82,23 @@ def test_info_headers(create_system_files, system, http_server): + file_path="test/good-verity-bundle.raucb", + ) + +- out, err, exitcode = run( +- f"{system.prefix} info {http_server.url} --output-format=json -H 'Test-Header: Test-Value'" +- ) +- assert exitcode == 0 +- info = json.loads(out) +- assert info["compatible"] == "Test Config" ++ if api == "cli": ++ out, err, exitcode = run( ++ f"{system.prefix} info {http_server.url} --output-format=json -H 'Test-Header: Test-Value'" ++ ) ++ assert exitcode == 0 ++ info = json.loads(out) ++ assert info["compatible"] == "Test Config" ++ elif api == "dbus": ++ with system.running_service("A"): ++ info = system.proxy.InspectBundle( ++ http_server.url, ++ { ++ "http-headers": get_variant("as", ["Test-Header: Test-Value"]), ++ }, ++ ) ++ info = get_native(info) ++ assert info["update"]["compatible"] == "Test Config" + + summary = http_server.get_summary() + assert summary["requests"] == 3 +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0006-src-bundle-avoid-sending-headers-for-missing-values.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0006-src-bundle-avoid-sending-headers-for-missing-values.patch new file mode 100644 index 00000000..39b2871d --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0006-src-bundle-avoid-sending-headers-for-missing-values.patch @@ -0,0 +1,42 @@ +From 53018483a950dd1ef8fd9a3cf02e51fa0f7787a9 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Fri, 28 Feb 2025 11:42:17 +0100 +Subject: [PATCH 06/23] src/bundle: avoid sending headers for missing values + +Even if the header is enabled in the config, the value might be missing +at runtime. + +Signed-off-by: Jan Luebbe +--- + src/bundle.c | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/src/bundle.c b/src/bundle.c +index e174eac0..0340f155 100644 +--- a/src/bundle.c ++++ b/src/bundle.c +@@ -3326,16 +3326,16 @@ GPtrArray *assemble_info_headers(const gchar *transaction) + + for (gchar **header = r_context()->config->enabled_headers; *header; header++) { + /* Add static system information */ +- if (g_strcmp0(*header, "boot-id") == 0) ++ if (g_strcmp0(*header, "boot-id") == 0 && r_context()->boot_id) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Boot-ID: %s", r_context()->boot_id)); +- if (g_strcmp0(*header, "machine-id") == 0) ++ if (g_strcmp0(*header, "machine-id") == 0 && r_context()->machine_id) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Machine-ID: %s", r_context()->machine_id)); +- if (g_strcmp0(*header, "serial") == 0) ++ if (g_strcmp0(*header, "serial") == 0 && r_context()->system_serial) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Serial: %s", r_context()->system_serial)); +- if (g_strcmp0(*header, "variant") == 0) ++ if (g_strcmp0(*header, "variant") == 0 && r_context()->config->system_variant) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Variant: %s", r_context()->config->system_variant)); + /* Add per-installation information */ +- if (g_strcmp0(*header, "transaction-id") == 0 && transaction != NULL) ++ if (g_strcmp0(*header, "transaction-id") == 0 && transaction) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Transaction-ID: %s", transaction)); + /* Add live information */ + if (g_strcmp0(*header, "uptime") == 0) { +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0007-support-sending-a-RAUC-System-Version-header.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0007-support-sending-a-RAUC-System-Version-header.patch new file mode 100644 index 00000000..280a8ff3 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0007-support-sending-a-RAUC-System-Version-header.patch @@ -0,0 +1,86 @@ +From 9a53f4dedbb270e41752ac5e12a725afb2a1b526 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Fri, 28 Feb 2025 11:57:57 +0100 +Subject: [PATCH 07/23] support sending a RAUC-System-Version header + +Use a System- prefix to disabiguate it from the RAUC or bundle version. + +Signed-off-by: Jan Luebbe +--- + docs/reference.rst | 1 + + src/bundle.c | 2 ++ + src/install.c | 11 ++++++++++- + test/test_http.py | 3 ++- + 4 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/docs/reference.rst b/docs/reference.rst +index 21f53167..c90ad12c 100644 +--- a/docs/reference.rst ++++ b/docs/reference.rst +@@ -363,6 +363,7 @@ For more information about using the streaming support of RAUC, refer to + + .. note:: The machine ID should be considered "confidential" and thus not + be used over unauthenticated connections or with untrusted servers! ++ * ``system-version``: Enables sending the *system version* as ``RAUC-System-Version`` header field. + * ``serial``: Enables sending the *system serial* as ``RAUC-Serial`` header field. + * ``variant``: Enables sending the *variant* as ``RAUC-Variant`` header field. + * ``transaction-id``: Enables sending the *transaction UUID* as ``RAUC-Transaction-ID`` header field. +diff --git a/src/bundle.c b/src/bundle.c +index 0340f155..b2c27756 100644 +--- a/src/bundle.c ++++ b/src/bundle.c +@@ -3330,6 +3330,8 @@ GPtrArray *assemble_info_headers(const gchar *transaction) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Boot-ID: %s", r_context()->boot_id)); + if (g_strcmp0(*header, "machine-id") == 0 && r_context()->machine_id) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Machine-ID: %s", r_context()->machine_id)); ++ if (g_strcmp0(*header, "system-version") == 0 && r_context()->system_version) ++ g_ptr_array_add(headers, g_strdup_printf("RAUC-System-Version: %s", r_context()->system_version)); + if (g_strcmp0(*header, "serial") == 0 && r_context()->system_serial) + g_ptr_array_add(headers, g_strdup_printf("RAUC-Serial: %s", r_context()->system_serial)); + if (g_strcmp0(*header, "variant") == 0 && r_context()->config->system_variant) +diff --git a/src/install.c b/src/install.c +index cecd6054..5c596de3 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -1722,7 +1722,16 @@ gboolean install_run(RaucInstallArgs *args) + return TRUE; + } + +-static const gchar *supported_http_headers[] = {"boot-id", "transaction-id", "machine-id", "serial", "variant", "uptime", NULL}; ++static const gchar *supported_http_headers[] = { ++ "boot-id", ++ "transaction-id", ++ "machine-id", ++ "system-version", ++ "serial", ++ "variant", ++ "uptime", ++ NULL ++}; + + gboolean r_install_is_supported_http_header(const gchar *header) + { +diff --git a/test/test_http.py b/test/test_http.py +index 033dc860..d043b066 100644 +--- a/test/test_http.py ++++ b/test/test_http.py +@@ -75,7 +75,7 @@ def test_info_headers(create_system_files, system, http_server, api): + "system-info": "bin/systeminfo.sh", + } + system.config["streaming"] = { +- "send-headers": "boot-id;machine-id;serial;variant;transaction-id;uptime", ++ "send-headers": "boot-id;machine-id;system-version;serial;variant;transaction-id;uptime", + } + system.write_config() + http_server.setup( +@@ -113,6 +113,7 @@ def test_info_headers(create_system_files, system, http_server, api): + "Range": "bytes=0-3", + "Test-Header": "Test-Value", + "RAUC-Serial": "1234", ++ "RAUC-System-Version": "1.0.0", + "RAUC-Variant": "test-variant-x", + } + +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0008-nbd-add-support-for-ETag-based-caching.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0008-nbd-add-support-for-ETag-based-caching.patch new file mode 100644 index 00000000..8afeff14 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0008-nbd-add-support-for-ETag-based-caching.patch @@ -0,0 +1,162 @@ +From aa63317478db7b8580bf66a6dc701d6ce79876d6 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 6 Mar 2025 11:09:02 +0100 +Subject: [PATCH 08/23] nbd: add support for ETag based caching + +This is useful to avoid fetching the manifest unless it has changed. For +now, we don't expose the received ETag over D-Bus. + +Signed-off-by: Jan Luebbe +--- + include/nbd.h | 1 + + src/nbd.c | 23 ++++++++++++++++++++--- + test/nbd.c | 32 ++++++++++++++++++++++++++++++++ + 3 files changed, 53 insertions(+), 3 deletions(-) + +diff --git a/include/nbd.h b/include/nbd.h +index 8fe2846d..bd1fa364 100644 +--- a/include/nbd.h ++++ b/include/nbd.h +@@ -46,6 +46,7 @@ typedef struct { + gchar *effective_url; /* url after redirects */ + guint64 current_time; /* date header from server */ + guint64 modified_time; /* last-modified header from server */ ++ gchar *etag; /* etag received from the server */ + } RaucNBDServer; + + RaucNBDDevice *r_nbd_new_device(void); +diff --git a/src/nbd.c b/src/nbd.c +index 518bbc60..6ab98689 100644 +--- a/src/nbd.c ++++ b/src/nbd.c +@@ -88,6 +88,7 @@ void r_nbd_free_server(RaucNBDServer *nbd_srv) + g_strfreev(nbd_srv->headers); + g_clear_pointer(&nbd_srv->info_headers, g_ptr_array_unref); + g_free(nbd_srv->effective_url); ++ g_free(nbd_srv->etag); + g_free(nbd_srv); + } + +@@ -318,8 +319,17 @@ struct RaucNBDTransfer { + guint64 content_size; + guint64 current_time; /* date header from server */ + guint64 modified_time; /* last-modified header from server */ ++ gchar *etag; + }; + ++static void free_transfer(struct RaucNBDTransfer *xfer) ++{ ++ g_clear_pointer(&xfer->buffer, g_free); ++ g_clear_pointer(&xfer->etag, g_free); ++ ++ g_free(xfer); ++} ++ + static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) + { + struct RaucNBDTransfer *xfer = userdata; +@@ -398,6 +408,10 @@ static size_t header_cb(char *buffer, size_t size, size_t nitems, void *userdata + xfer->modified_time = date; + g_message("nbd server received HTTP file date %"G_GUINT64_FORMAT, xfer->modified_time); + } ++ } else if (g_str_equal(h_name, "etag")) { ++ r_replace_strdup(&xfer->etag, h_pair[1]); ++ g_autofree gchar *escaped = g_strescape(h_pair[1], NULL); ++ g_message("nbd server received HTTP ETag: \"%s\"", escaped); + } + + return nitems; +@@ -637,7 +651,7 @@ static void start_request(struct RaucNBDContext *ctx, struct RaucNBDTransfer *xf + case NBD_CMD_DISC: { + g_message("nbd server received disconnect request"); + ctx->done = TRUE; +- g_free(xfer); /* not queued via curl_multi_add_handle */ ++ free_transfer(xfer); /* not queued via curl_multi_add_handle */ + break; + } + case RAUC_NBD_CMD_CONFIGURE: { +@@ -787,6 +801,8 @@ reply: + g_variant_dict_insert(&dict, "current-time", "t", xfer->current_time); + if (xfer->modified_time) + g_variant_dict_insert(&dict, "modified-time", "t", xfer->modified_time); ++ if (xfer->etag) ++ g_variant_dict_insert(&dict, "etag", "s", xfer->etag); + + v = g_variant_dict_end(&dict); + reply_size = g_variant_get_size(v); +@@ -951,12 +967,12 @@ gboolean r_nbd_run_server(gint sock, GError **error) + error, + R_NBD_ERROR, R_NBD_ERROR_SHUTDOWN, + "finish_request failed, shutting down"); +- g_free(xfer); ++ free_transfer(xfer); + goto out; + } + + if (xfer->done) { +- g_free(xfer); ++ free_transfer(xfer); + } else { + /* retry */ + sleep(1); +@@ -1132,6 +1148,7 @@ static gboolean nbd_configure(RaucNBDServer *nbd_srv, GError **error) + g_autofree gchar *formatted_date = g_date_time_format(datetime, "%Y-%m-%d %H:%M:%S"); + g_message("received HTTP server info: modified time %s (%"G_GUINT64_FORMAT ")", formatted_date, nbd_srv->modified_time); + } ++ g_variant_dict_lookup(&dict, "etag", "s", &nbd_srv->etag); + + return TRUE; + } +diff --git a/test/nbd.c b/test/nbd.c +index 9502cf9a..bcb1ad9d 100644 +--- a/test/nbd.c ++++ b/test/nbd.c +@@ -171,6 +171,32 @@ static void test_extract(NBDFixture *fixture, gconstpointer user_data) + g_assert_false(res); + } + ++static void test_cache_etag(NBDFixture *fixture, gconstpointer user_data) ++{ ++ g_autoptr(RaucBundle) bundle = NULL; ++ g_autoptr(GError) ierror = NULL; ++ gboolean res = FALSE; ++ ++ if (!have_http_server()) ++ return; ++ ++ res = check_bundle("http://127.0.0.1/test/good-verity-bundle.raucb", &bundle, CHECK_BUNDLE_DEFAULT, NULL, &ierror); ++ g_assert_no_error(ierror); ++ g_assert_true(res); ++ g_assert_nonnull(bundle); ++ g_assert_nonnull(bundle->nbd_srv->etag); ++ g_autofree gchar *etag = g_strdup(bundle->nbd_srv->etag); ++ g_clear_pointer(&bundle, free_bundle); ++ ++ g_auto(RaucBundleAccessArgs) access_args = {0}; ++ access_args.http_info_headers = g_ptr_array_new_with_free_func(g_free); ++ g_ptr_array_add(access_args.http_info_headers, g_strdup_printf("If-None-Match: %s", etag)); ++ res = check_bundle("http://127.0.0.1/test/good-verity-bundle.raucb", &bundle, CHECK_BUNDLE_DEFAULT, &access_args, &ierror); ++ g_assert_error(ierror, R_NBD_ERROR, R_NBD_ERROR_NOT_MODIFIED); ++ g_assert_false(res); ++ g_assert_null(bundle); ++} ++ + static void test_nbd_mount(NBDFixture *fixture, gconstpointer user_data) + { + NBDData *data = (NBDData*)user_data; +@@ -325,6 +351,12 @@ int main(int argc, char *argv[]) + nbd_fixture_set_up, test_extract, + nbd_fixture_tear_down); + ++ /* bundle caching */ ++ g_test_add("/nbd/cache/etag", ++ NBDFixture, NULL, ++ nbd_fixture_set_up, test_cache_etag, ++ nbd_fixture_tear_down); ++ + /* mount via HTTP */ + nbd_data = dup_test_data(ptrs, (&(NBDData) { + .bundle_url = "http://127.0.0.1/test/good-verity-bundle.raucb", +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0009-qemu-test-rauc-config-set-TLS-CA-for-streaming.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0009-qemu-test-rauc-config-set-TLS-CA-for-streaming.patch new file mode 100644 index 00000000..c312bb1c --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0009-qemu-test-rauc-config-set-TLS-CA-for-streaming.patch @@ -0,0 +1,27 @@ +From 82ab9c41ffcade3bb2a845c2329b9ec1275a54b4 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Wed, 5 Mar 2025 15:27:54 +0100 +Subject: [PATCH 09/23] qemu-test-rauc-config: set TLS CA for streaming + +Signed-off-by: Jan Luebbe +--- + qemu-test-rauc-config | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/qemu-test-rauc-config b/qemu-test-rauc-config +index c062bc00..6dcb7739 100644 +--- a/qemu-test-rauc-config ++++ b/qemu-test-rauc-config +@@ -13,6 +13,9 @@ events=all + [keyring] + path=ca.cert.pem + ++[streaming] ++tls-ca=/etc/rauc/ca.cert.pem ++ + [slot.rootfs.0] + device=/dev/root + bootname=A +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0010-test-install-simplify-install_cleanup.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0010-test-install-simplify-install_cleanup.patch new file mode 100644 index 00000000..8c95e1ef --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0010-test-install-simplify-install_cleanup.patch @@ -0,0 +1,44 @@ +From e5e1a3825d65b34366d71e50fe69e08da8bf49e6 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Tue, 25 Feb 2025 17:43:26 +0100 +Subject: [PATCH 10/23] test/install: simplify install_cleanup + +There is no other caller of r_quit, so just fold it into +install_cleanup. + +Signed-off-by: Jan Luebbe +--- + test/install.c | 10 +--------- + 1 file changed, 1 insertion(+), 9 deletions(-) + +diff --git a/test/install.c b/test/install.c +index ed6bc726..427cd3b9 100644 +--- a/test/install.c ++++ b/test/install.c +@@ -1041,14 +1041,6 @@ device=/dev/null\n\ + g_assert_error(error, R_INSTALL_ERROR, R_INSTALL_ERROR_FAILED); + } + +-static gboolean r_quit(gpointer data) +-{ +- g_assert_nonnull(r_loop); +- g_main_loop_quit(r_loop); +- +- return G_SOURCE_REMOVE; +-} +- + static gboolean install_notify(gpointer data) + { + RaucInstallArgs *args = data; +@@ -1073,7 +1065,7 @@ static gboolean install_cleanup(gpointer data) + install_args_free(args); + + if (r_loop) +- r_quit(data); ++ g_main_loop_quit(r_loop); + + return G_SOURCE_REMOVE; + } +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0011-utils-add-helper-to-get-a-string-list-from-a-GKeyFil.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0011-utils-add-helper-to-get-a-string-list-from-a-GKeyFil.patch new file mode 100644 index 00000000..7486b959 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0011-utils-add-helper-to-get-a-string-list-from-a-GKeyFil.patch @@ -0,0 +1,98 @@ +From 71c65602bfc88d9a86a0524b82046aead0562715 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Mon, 3 Mar 2025 18:56:21 +0100 +Subject: [PATCH 11/23] utils: add helper to get a string list from a GKeyFile + +The helper also optionally checks the strings against an allow-list. + +Signed-off-by: Jan Luebbe +--- + include/utils.h | 21 +++++++++++++++++++++ + src/utils.c | 39 +++++++++++++++++++++++++++++++++++++++ + 2 files changed, 60 insertions(+) + +diff --git a/include/utils.h b/include/utils.h +index bb27e0e1..48d21c64 100644 +--- a/include/utils.h ++++ b/include/utils.h +@@ -294,6 +294,27 @@ guint64 key_file_consume_binary_suffixed_string(GKeyFile *key_file, + GError **error) + G_GNUC_WARN_UNUSED_RESULT; + ++/** ++ * Get list of string arguments from key and remove key from key_file. ++ * ++ * Optionally filter ++ * ++ * @param key_file g GKeyFile ++ * @param group_name the group name ++ * @param key the key name ++ * @param allowed a list of allowed strings, or NULL ++ * @param error return location for a GError, or NULL ++ * ++ * @return a GStrv or NULL if the key was not found or an error occurred ++ */ ++gchar **key_file_consume_string_list( ++ GKeyFile *key_file, ++ const gchar *group_name, ++ const gchar *key, ++ const gchar **allowed, ++ GError **error) ++G_GNUC_WARN_UNUSED_RESULT; ++ + gchar * r_realpath(const gchar *path) + G_GNUC_WARN_UNUSED_RESULT; + +diff --git a/src/utils.c b/src/utils.c +index dcb947a5..e5f41026 100644 +--- a/src/utils.c ++++ b/src/utils.c +@@ -485,6 +485,45 @@ guint64 key_file_consume_binary_suffixed_string(GKeyFile *key_file, + return result << scale_shift; + } + ++gchar **key_file_consume_string_list( ++ GKeyFile *key_file, ++ const gchar *group_name, ++ const gchar *key, ++ const gchar **allowed, ++ GError **error) ++{ ++ GError *ierror = NULL; ++ gsize length = 0; ++ ++ g_auto(GStrv) result = g_key_file_get_string_list(key_file, group_name, key, &length, &ierror); ++ if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { ++ /* handle missing key the same as an empty list */ ++ g_clear_error(&ierror); ++ return NULL; ++ } else if (ierror) { ++ g_propagate_error(error, ierror); ++ return NULL; ++ } ++ ++ g_key_file_remove_key(key_file, group_name, key, NULL); ++ ++ if (!length) ++ return NULL; ++ ++ /* check against allow-list */ ++ if (allowed) { ++ for (gsize i = 0; i < length; i++) { ++ if (!g_strv_contains(allowed, result[i])) { ++ g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, ++ "Unsupported value '%s' for key '%s' in [%s]", result[i], key, group_name); ++ return NULL; ++ } ++ } ++ } ++ ++ return g_steal_pointer(&result); ++} ++ + gchar * r_realpath(const gchar *path) + { + gchar buf[PATH_MAX + 1]; +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0012-conftest-implement-D-Bus-message-monitoring.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0012-conftest-implement-D-Bus-message-monitoring.patch new file mode 100644 index 00000000..47a3403b --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0012-conftest-implement-D-Bus-message-monitoring.patch @@ -0,0 +1,78 @@ +From 52494d255dfa383a29795ace256ff24f8a5ecb69 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Wed, 5 Mar 2025 15:12:44 +0100 +Subject: [PATCH 12/23] conftest: implement D-Bus message monitoring + +Signed-off-by: Jan Luebbe +--- + test/conftest.py | 40 ++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) + +diff --git a/test/conftest.py b/test/conftest.py +index 7aa7bbd6..cedcc31f 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -1,6 +1,9 @@ + import json ++import fcntl + import os ++import shlex + import shutil ++import signal + import subprocess + import time + from functools import cache +@@ -497,6 +500,9 @@ class System: + self.service = None + self.proxy = None + ++ self.dbus_monitor = None ++ self.dbus_rest = b"" ++ + def prepare_minimal_config(self): + self.config["system"] = { + "compatible": "Test Config", +@@ -585,6 +591,40 @@ class System: + with open(self.output, "w") as f: + self.config.write(f, space_around_delimiters=False) + ++ def start_dbus_monitor(self): ++ assert self.dbus_monitor is None ++ ++ addr = os.environ["DBUS_SESSION_BUS_ADDRESS"].split(',')[0] ++ ++ self.dbus_monitor = subprocess.Popen( ++ ["busctl", "--json=short", f"--address={addr}", "monitor", "de.pengutronix.rauc"], ++ stdout=subprocess.PIPE, ++ ) ++ fcntl.fcntl(self.dbus_monitor.stdout, fcntl.F_SETFL, os.O_NONBLOCK) ++ fcntl.fcntl(self.dbus_monitor.stdout, fcntl.F_SETPIPE_SZ, 10*1024*1024) ++ ++ def get_dbus_events(self): ++ lines = [] ++ while True: ++ new_bytes = self.dbus_monitor.stdout.read() ++ if new_bytes is None: ++ break ++ # TODO wait until idle? ++ #with open("buslog", "ab") as f: ++ # f.write(new_bytes) ++ # f.write(b"\nMARKER\n") ++ #print(f"before: {self.dbus_rest} | {new_bytes}") ++ [*new_lines, self.dbus_rest] = (self.dbus_rest + new_bytes).split(b'\n') ++ #print(f"after: {new_lines} | {self.dbus_rest}") ++ lines += new_lines ++ events = [] ++ for line in lines: ++ try: ++ events.append(json.loads(line)) ++ except json.decoder.JSONDecodeError: ++ print(f"failed to decode {repr(line)}") ++ return events ++ + @contextmanager + def running_service(self, bootslot): + if not have_service(): +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0013-add-a-Poller-D-Bus-interface.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0013-add-a-Poller-D-Bus-interface.patch new file mode 100644 index 00000000..05209c42 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0013-add-a-Poller-D-Bus-interface.patch @@ -0,0 +1,54 @@ +From b2e08e3350a2cf000ba46e13c161657407f81a6f Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Mon, 3 Mar 2025 18:57:55 +0100 +Subject: [PATCH 13/23] add a Poller D-Bus interface + +Signed-off-by: Jan Luebbe +--- + meson.build | 5 ++++- + src/de.pengutronix.rauc.Poller.xml | 17 +++++++++++++++++ + 2 files changed, 21 insertions(+), 1 deletion(-) + create mode 100644 src/de.pengutronix.rauc.Poller.xml + +diff --git a/meson.build b/meson.build +index 91414267..ba6d29d2 100644 +--- a/meson.build ++++ b/meson.build +@@ -173,7 +173,10 @@ if composefsdep.found() + endif + + gnome = import('gnome') +-dbus_ifaces = files('src/de.pengutronix.rauc.Installer.xml') ++dbus_ifaces = files([ ++ 'src/de.pengutronix.rauc.Installer.xml', ++ 'src/de.pengutronix.rauc.Poller.xml', ++]) + dbus_sources = gnome.gdbus_codegen( + 'rauc-installer-generated', + sources : dbus_ifaces, +diff --git a/src/de.pengutronix.rauc.Poller.xml b/src/de.pengutronix.rauc.Poller.xml +new file mode 100644 +index 00000000..7a66c008 +--- /dev/null ++++ b/src/de.pengutronix.rauc.Poller.xml +@@ -0,0 +1,17 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0014-test-conftest-allow-running-the-service-with-a-polli.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0014-test-conftest-allow-running-the-service-with-a-polli.patch new file mode 100644 index 00000000..abaf87a3 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0014-test-conftest-allow-running-the-service-with-a-polli.patch @@ -0,0 +1,36 @@ +From 4e098bb40f868435bf14ca4a5952ebf4885889b7 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Wed, 5 Mar 2025 15:29:53 +0100 +Subject: [PATCH 14/23] test/conftest: allow running the service with a polling + speedup factor + +Signed-off-by: Jan Luebbe +--- + test/conftest.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/test/conftest.py b/test/conftest.py +index cedcc31f..41d79777 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -626,7 +626,7 @@ class System: + return events + + @contextmanager +- def running_service(self, bootslot): ++ def running_service(self, bootslot, *, poll_speedup=None): + if not have_service(): + # TODO avoid unnescesary setup by moving using a pytest mark for all service/noservice cases + pytest.skip("No service") +@@ -636,6 +636,8 @@ class System: + + env = os.environ.copy() + env["RAUC_PYTEST_TMP"] = str(self.tmp_path) ++ if poll_speedup: ++ env["RAUC_TEST_POLL_SPEEDUP"] = f"{poll_speedup}" + + self.service = subprocess.Popen( + f"rauc service --conf={self.output} --mount={self.tmp_path}/mnt --override-boot-slot={bootslot}".split(), +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0015-context-implement-a-polling-speedup-factor-for-testi.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0015-context-implement-a-polling-speedup-factor-for-testi.patch new file mode 100644 index 00000000..55c9c773 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0015-context-implement-a-polling-speedup-factor-for-testi.patch @@ -0,0 +1,48 @@ +From 2600b9ab34a29062fdd45724c5e8902f3d5d6a7f Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Wed, 5 Mar 2025 15:33:48 +0100 +Subject: [PATCH 15/23] context: implement a polling speedup factor for testing + +Signed-off-by: Jan Luebbe +--- + include/context.h | 1 + + src/context.c | 11 +++++++++++ + 2 files changed, 12 insertions(+) + +diff --git a/include/context.h b/include/context.h +index 67d44179..6cb1174f 100644 +--- a/include/context.h ++++ b/include/context.h +@@ -76,6 +76,7 @@ typedef struct { + struct { + /* mock contents of /proc/cmdline */ + const gchar *proc_cmdline; ++ gint64 poll_speedup; + } mock; + } RaucContext; + +diff --git a/src/context.c b/src/context.c +index 1fb65e63..415cf5fe 100644 +--- a/src/context.c ++++ b/src/context.c +@@ -501,6 +501,17 @@ gboolean r_context_configure(GError **error) + return FALSE; + } + ++ /* configure mocks */ ++ const gchar *poll_speedup = g_getenv("RAUC_TEST_POLL_SPEEDUP"); ++ if (poll_speedup) { ++ gint64 result = g_ascii_strtoll(poll_speedup, NULL, 10); ++ if ((result < 1) || (result > 1000)) { ++ g_error("Invalid RAUC_TEST_POLL_SPEEDUP value '%s'", poll_speedup); ++ return FALSE; ++ } ++ context->mock.poll_speedup = result; ++ } ++ + context->pending = FALSE; + + return TRUE; +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0016-install-remember-if-a-slot-or-artifact-was-updated.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0016-install-remember-if-a-slot-or-artifact-was-updated.patch new file mode 100644 index 00000000..a2474046 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0016-install-remember-if-a-slot-or-artifact-was-updated.patch @@ -0,0 +1,51 @@ +From 964738aae0d0251d4b77c46b367bad2fb8214347 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 10:58:41 +0100 +Subject: [PATCH 16/23] install: remember if a slot or artifact was updated + +This will allow us to determine if a reboot should be performed after an +automatic installation. + +Signed-off-by: Jan Luebbe +--- + include/install.h | 4 ++++ + src/install.c | 2 ++ + 2 files changed, 6 insertions(+) + +diff --git a/include/install.h b/include/install.h +index b5232775..6dac9161 100644 +--- a/include/install.h ++++ b/include/install.h +@@ -36,6 +36,10 @@ typedef struct { + gchar *require_manifest_hash; + gchar *transaction; + RaucBundleAccessArgs access_args; ++ ++ /* install result flags */ ++ gboolean updated_slots; ++ gboolean updated_artifacts; + } RaucInstallArgs; + + /** +diff --git a/src/install.c b/src/install.c +index 5c596de3..61cf5962 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -1451,12 +1451,14 @@ static gboolean launch_and_wait_default_handler(RaucInstallArgs *args, gchar* bu + r_context_end_step("update_slots", FALSE); + return FALSE; + } ++ args->updated_slots = TRUE; + } else if (plan->target_repo) { + if (!handle_artifact_install_plan(manifest, plan, args, hook_name, &ierror)) { + g_propagate_error(error, ierror); + r_context_end_step("update_slots", FALSE); + return FALSE; + } ++ args->updated_artifacts = TRUE; + } + } + +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0017-include-utils-ensure-that-r_subprocess_-args-are-ter.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0017-include-utils-ensure-that-r_subprocess_-args-are-ter.patch new file mode 100644 index 00000000..a1651630 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0017-include-utils-ensure-that-r_subprocess_-args-are-ter.patch @@ -0,0 +1,36 @@ +From 1d223956cb304c735c649b38d284d3b1422a9e4a Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 12:37:40 +0100 +Subject: [PATCH 17/23] include/utils: ensure that r_subprocess_* args are + terminated + +Signed-off-by: Jan Luebbe +--- + include/utils.h | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/include/utils.h b/include/utils.h +index 48d21c64..7491ed63 100644 +--- a/include/utils.h ++++ b/include/utils.h +@@ -45,6 +45,8 @@ G_DEFINE_AUTO_CLEANUP_FREE_FUNC(filedesc, close_preserve_errno, -1) + + static inline GSubprocess* r_subprocess_newv(GPtrArray *args, GSubprocessFlags flags, GError **error) + { ++ g_assert(args->len); ++ g_assert(args->pdata[args->len-1] == NULL); + g_autofree gchar *call = g_strjoinv(" ", (gchar**) args->pdata); + g_log(R_LOG_DOMAIN_SUBPROCESS, G_LOG_LEVEL_DEBUG, "launching subprocess: %s", call); + +@@ -53,6 +55,8 @@ static inline GSubprocess* r_subprocess_newv(GPtrArray *args, GSubprocessFlags f + + static inline GSubprocess * r_subprocess_launcher_spawnv(GSubprocessLauncher *launcher, GPtrArray *args, GError **error) + { ++ g_assert(args->len); ++ g_assert(args->pdata[args->len-1] == NULL); + g_autofree gchar *call = g_strjoinv(" ", (gchar**) args->pdata); + g_log(R_LOG_DOMAIN_SUBPROCESS, G_LOG_LEVEL_DEBUG, "launching subprocess: %s", call); + +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0018-HACK-gdb-in-pytest-for-service.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0018-HACK-gdb-in-pytest-for-service.patch new file mode 100644 index 00000000..477cfb5d --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0018-HACK-gdb-in-pytest-for-service.patch @@ -0,0 +1,54 @@ +From 581b44a609f1e466d892a7a86cfb69331aa91286 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Wed, 5 Mar 2025 15:29:29 +0100 +Subject: [PATCH 18/23] HACK: gdb in pytest for service + +--- + test/conftest.py | 21 ++++++++++++++++++--- + 1 file changed, 18 insertions(+), 3 deletions(-) + +diff --git a/test/conftest.py b/test/conftest.py +index 41d79777..e864d0e1 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -639,8 +639,14 @@ class System: + if poll_speedup: + env["RAUC_TEST_POLL_SPEEDUP"] = f"{poll_speedup}" + ++ command = "" ++ if "SERVICE_BACKTRACE" in env: ++ command += 'gdb --return-child-result --batch --ex "run" --ex "thread apply all bt" --args ' ++ ++ command += f"rauc service --conf={self.output} --mount={self.tmp_path}/mnt --override-boot-slot={bootslot}" ++ + self.service = subprocess.Popen( +- f"rauc service --conf={self.output} --mount={self.tmp_path}/mnt --override-boot-slot={bootslot}".split(), ++ shlex.split(command), + env=env, + ) + +@@ -660,10 +666,19 @@ class System: + + yield + +- self.service.terminate() ++ rauc_dbus_pid = None ++ try: ++ dbus_daemon = bus.get_proxy("org.freedesktop.DBus", "/") ++ rauc_dbus_pid = dbus_daemon.GetConnectionUnixProcessID("de.pengutronix.rauc") ++ print(f"rauc PID via D-Bus {rauc_dbus_pid}") ++ os.kill(rauc_dbus_pid, signal.SIGTERM) ++ except DBusError: ++ self.service.terminate() ++ + try: + self.service.wait(timeout=10) +- assert self.service.returncode == 0 ++ print(f"rauc returncode is {self.service.returncode}") ++ assert rauc_dbus_pid or self.service.returncode == 0 + except subprocess.TimeoutExpired: + self.service.kill() + self.service.wait() +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0019-WIP-implement-polling-in-the-service.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0019-WIP-implement-polling-in-the-service.patch new file mode 100644 index 00000000..2d594483 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0019-WIP-implement-polling-in-the-service.patch @@ -0,0 +1,999 @@ +From b67b3ab612cc6dda1253ebf15ec78e27785d4c75 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 17:04:24 +0100 +Subject: [PATCH 19/23] WIP: implement polling in the service + +--- + include/config_file.h | 11 + + include/install.h | 1 + + include/poll.h | 19 ++ + include/service.h | 5 + + meson.build | 1 + + qemu-test-init | 16 +- + src/config_file.c | 150 +++++++++++ + src/install.c | 4 + + src/poll.c | 575 ++++++++++++++++++++++++++++++++++++++++++ + src/service.c | 18 +- + 10 files changed, 794 insertions(+), 6 deletions(-) + create mode 100644 include/poll.h + create mode 100644 src/poll.c + +diff --git a/include/config_file.h b/include/config_file.h +index 186bd7c1..bf821715 100644 +--- a/include/config_file.h ++++ b/include/config_file.h +@@ -26,6 +26,7 @@ typedef enum { + R_CONFIG_ERROR_DATA_DIRECTORY, + R_CONFIG_ERROR_ARTIFACT_REPO_TYPE, + R_CONFIG_ERROR_EMPTY_FILE, ++ R_CONFIG_ERROR_POLL, + } RConfigError; + + #define R_CONFIG_ERROR r_config_error_quark() +@@ -99,6 +100,16 @@ typedef struct { + /* logging */ + GList *loggers; + ++ /* polling */ ++ gchar *poll_source; ++ gchar **poll_inhibit_files; ++ gchar **poll_candidate_criteria; ++ gchar **poll_install_criteria; ++ gchar **poll_reboot_criteria; ++ gint64 poll_interval_ms; ++ gint64 poll_max_interval_ms; ++ gchar *poll_reboot_cmd; ++ + GHashTable *slots; + /* flag to ensure slot states were determined */ + gboolean slot_states_determined; +diff --git a/include/install.h b/include/install.h +index 6dac9161..303efd7e 100644 +--- a/include/install.h ++++ b/include/install.h +@@ -26,6 +26,7 @@ typedef struct { + gchar *name; + GSourceFunc notify; + GSourceFunc cleanup; ++ gpointer data; /* private pointer for notify and cleanup callbacks */ + GMutex status_mutex; + GQueue status_messages; + gint status_result; +diff --git a/include/poll.h b/include/poll.h +new file mode 100644 +index 00000000..20103bc8 +--- /dev/null ++++ b/include/poll.h +@@ -0,0 +1,19 @@ ++#pragma once ++ ++#include ++#include ++ ++/** ++ * Register Poller interface. ++ * ++ * @param connection the connection on which the name was acquired ++ */ ++void r_poll_on_bus_acquired(GDBusConnection *connection); ++ ++/** ++ * Set up the polling GSource and D-Bus interface. ++ * ++ * @return new GSource or NULL if polling is disabled ++ */ ++GSource *r_poll_setup(void) ++G_GNUC_WARN_UNUSED_RESULT; +diff --git a/include/service.h b/include/service.h +index a33a723d..e5c0b684 100644 +--- a/include/service.h ++++ b/include/service.h +@@ -2,6 +2,11 @@ + + #include + ++#include "rauc-installer-generated.h" ++ + gboolean r_service_run(void) + G_GNUC_WARN_UNUSED_RESULT; + void set_last_error(const gchar *message); ++ ++/* used by poll.c */ ++extern RInstaller *r_installer; +diff --git a/meson.build b/meson.build +index ba6d29d2..483f8a85 100644 +--- a/meson.build ++++ b/meson.build +@@ -132,6 +132,7 @@ sources_rauc = files([ + 'src/mark.c', + 'src/mbr.c', + 'src/mount.c', ++ 'src/poll.c', + 'src/service.c', + 'src/shell.c', + 'src/signature.c', +diff --git a/qemu-test-init b/qemu-test-init +index b2a881bd..fea4cd6a 100755 +--- a/qemu-test-init ++++ b/qemu-test-init +@@ -40,7 +40,9 @@ for x in "$@"; do + export LSAN_OPTIONS=suppressions="$(pwd)/test/asan.supp" + export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 + elif [ "$x" = "service-backtrace" ]; then +- SERVICE_BACKTRACE=1 ++ export SERVICE_BACKTRACE=1 ++ elif [ "$x" = "service-poll" ]; then ++ export SERVICE_POLL=1 + fi + done + +@@ -179,6 +181,17 @@ if [ -n "$SERVICE" ]; then + ln -s ../.artifact-empty-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tmp/file-artifacts-dual/rootfs.0/empty + mkdir /tmp/tree-artifacts + ++ if [ -n "$SERVICE_POLL" ]; then ++ export RAUC_TEST_POLL_SPEEDUP=10 ++ echo "" >> /etc/rauc/system.conf ++ echo "[poll]" >> /etc/rauc/system.conf ++ echo "source=https://127.0.0.2/test/good-adaptive-meta-bundle.raucb" >> /etc/rauc/system.conf ++ echo "inhibit-files=/run/rauc/inhibit-poll" >> /etc/rauc/system.conf ++ echo "interval-sec=600" >> /etc/rauc/system.conf ++ echo "max-interval-sec=6000" >> /etc/rauc/system.conf ++ echo "reboot-cmd=touch /run/rauc-poll-reboot-pending" >> /etc/rauc/system.conf ++ fi ++ + if grep -q "ENABLE_COMPOSEFS 1" $BUILD_DIR/config.h; then + echo "" >> /etc/rauc/system.conf + echo "[artifacts.composefs]" >> /etc/rauc/system.conf +@@ -222,6 +235,7 @@ echo "use ctrl-a x to exit and ctrl-a c to access the qemu monitor" + echo "system ready" + + if [ -n "$SERVICE" ]; then ++ sleep 1 + rauc status mark-good + fi + +diff --git a/src/config_file.c b/src/config_file.c +index c289abd9..07498403 100644 +--- a/src/config_file.c ++++ b/src/config_file.c +@@ -668,6 +668,144 @@ static GHashTable *parse_artifact_repos(const char *filename, const char *data_d + return g_steal_pointer(&repos); + } + ++static const gchar *supported_poll_candidate_criteria[] = { ++ "higher-semver", ++ "different-version", ++ NULL ++}; ++static const gchar *default_poll_candidate_criteria[] = { ++ "higher-semver", ++ NULL ++}; ++static const gchar *supported_poll_install_criteria[] = { ++ "always", ++ "higher-semver", ++ "different-version", ++ NULL ++}; ++static const gchar *supported_poll_reboot_criteria[] = { ++ "updated-slots", ++ "updated-artifacts", ++ "failed-update", ++ NULL ++}; ++ ++static gboolean parse_poll_config(RaucConfig *config, GKeyFile *key_file, GError **error) ++{ ++ GError *ierror = NULL; ++ ++ if (!g_key_file_has_group(key_file, "poll")) ++ return TRUE; ++ ++ if (!ENABLE_STREAMING) { ++ g_set_error( ++ error, ++ R_CONFIG_ERROR, ++ R_CONFIG_ERROR_POLL, ++ "Polling not supported, recompile with -Dstreaming=true" ++ ); ++ return FALSE; ++ } ++ ++ config->poll_inhibit_files = key_file_consume_string_list(key_file, "poll", "inhibit-files", NULL, &ierror); ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ ++ config->poll_candidate_criteria = key_file_consume_string_list(key_file, "poll", "candidate-criteria", supported_poll_candidate_criteria, &ierror); ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } else if (!config->poll_candidate_criteria) { ++ config->poll_candidate_criteria = g_strdupv((GStrv)default_poll_candidate_criteria); ++ } ++ ++ config->poll_install_criteria = key_file_consume_string_list(key_file, "poll", "install-criteria", supported_poll_install_criteria, &ierror); ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ ++ config->poll_reboot_criteria = key_file_consume_string_list(key_file, "poll", "reboot-criteria", supported_poll_reboot_criteria, &ierror); ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ ++ config->poll_source = key_file_consume_string(key_file, "poll", "source", NULL); ++ if (!config->poll_source) { ++ g_set_error( ++ error, ++ R_CONFIG_ERROR, ++ R_CONFIG_ERROR_POLL, ++ "Polling source must be set if [poll] section exists" ++ ); ++ return FALSE; ++ } else if (!g_str_has_prefix(config->poll_source, "http")) { ++ g_set_error( ++ error, ++ R_CONFIG_ERROR, ++ R_CONFIG_ERROR_POLL, ++ "Polling source (%s) must be a HTTP(S) URL", ++ config->poll_source ++ ); ++ return FALSE; ++ } ++ ++ gint interval_sec = key_file_consume_integer(key_file, "poll", "interval-sec", &ierror); ++ if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { ++ interval_sec = 24*60*60; /* one day */ ++ g_clear_error(&ierror); ++ } else if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ if (interval_sec < 60) { ++ g_set_error( ++ error, ++ R_CONFIG_ERROR, ++ R_CONFIG_ERROR_POLL, ++ "Polling interval (%d s) must not be smaller than one minute", ++ interval_sec ++ ); ++ return FALSE; ++ } ++ ++ gint max_interval_sec = key_file_consume_integer(key_file, "poll", "max-interval-sec", &ierror); ++ if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { ++ max_interval_sec = interval_sec * 4; ++ g_clear_error(&ierror); ++ } else if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ if (max_interval_sec <= interval_sec) { ++ g_set_error( ++ error, ++ R_CONFIG_ERROR, ++ R_CONFIG_ERROR_POLL, ++ "Maximum polling interval (%d s) must be larger than the normal polling interval (%d s)", ++ max_interval_sec, ++ interval_sec ++ ); ++ return FALSE; ++ } ++ ++ config->poll_interval_ms = interval_sec * 1000; ++ config->poll_max_interval_ms = max_interval_sec * 1000; ++ ++ config->poll_reboot_cmd = key_file_consume_string(key_file, "poll", "reboot-cmd", NULL); ++ ++ if (!check_remaining_keys(key_file, "poll", &ierror)) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ g_key_file_remove_group(key_file, "poll", NULL); ++ ++ return TRUE; ++} ++ + static gboolean check_unique_slotclasses(RaucConfig *config, GError **error) + { + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); +@@ -1262,6 +1400,12 @@ gboolean load_config(const gchar *filename, RaucConfig **config, GError **error) + return FALSE; + } + ++ /* parse [poll] section */ ++ if (!parse_poll_config(c, key_file, &ierror)) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ + if (!check_unique_slotclasses(c, &ierror)) { + g_propagate_error(error, ierror); + return FALSE; +@@ -1327,6 +1471,12 @@ void free_config(RaucConfig *config) + g_free(config->encryption_key); + g_free(config->encryption_cert); + g_list_free_full(config->loggers, (GDestroyNotify)r_event_log_free_logger); ++ g_free(config->poll_source); ++ g_strfreev(config->poll_inhibit_files); ++ g_strfreev(config->poll_candidate_criteria); ++ g_strfreev(config->poll_install_criteria); ++ g_strfreev(config->poll_reboot_criteria); ++ g_free(config->poll_reboot_cmd); + g_clear_pointer(&config->slots, g_hash_table_destroy); + g_free(config->custom_bootloader_backend); + g_free(config->file_checksum); +diff --git a/src/install.c b/src/install.c +index 61cf5962..11ec5505 100644 +--- a/src/install.c ++++ b/src/install.c +@@ -48,6 +48,10 @@ static void install_args_update(RaucInstallArgs *args, const gchar *msg, ...) + g_return_if_fail(args); + g_return_if_fail(msg); + ++ /* without a notify function, there is nothing to do */ ++ if (!args->notify) ++ return; ++ + va_start(list, msg); + formatted = g_strdup_vprintf(msg, list); + va_end(list); +diff --git a/src/poll.c b/src/poll.c +new file mode 100644 +index 00000000..26b1ffce +--- /dev/null ++++ b/src/poll.c +@@ -0,0 +1,575 @@ ++#include ++#include ++ ++#include "poll.h" ++#include "bundle.h" ++#include "config_file.h" ++#include "context.h" ++#include "install.h" ++#include "manifest.h" ++#include "service.h" ++#include "utils.h" ++ ++RPoller *r_poller = NULL; ++ ++typedef struct { ++ GSource source; ++ ++ gboolean installation_running; ++ ++ guint64 attempt_count; ++ guint64 recent_error_count; /* since last success */ ++ gint64 last_attempt_time; /* monotonic */ ++ gint64 last_success_time; /* monotonic */ ++ gchar *last_error_message; ++ gboolean update_available; ++ gchar *summary; ++ gchar *attempted_hash; /* manifest hash of the attempted update */ ++ ++ /* from the last successful attempt */ ++ RaucManifest *manifest; ++ guint64 bundle_size; ++ gchar *bundle_effective_url; ++ guint64 bundle_modified_time; ++ gchar *bundle_etag; ++ ++ /* from the last installation */ ++ gboolean must_reboot; ++} RPollSource; ++ ++typedef enum { ++ POLL_DELAY_NORMAL = 0, ++ POLL_DELAY_SHORT, ++ POLL_DELAY_NOW, ++ POLL_DELAY_INITIAL, ++} RPollDelay; ++ ++static void poll_reschedule(RPollSource *poll_source, RPollDelay delay) ++{ ++ g_return_if_fail(poll_source); ++ ++ gint64 delay_ms = 0; ++ ++ switch (delay) { ++ case POLL_DELAY_NORMAL: ++ delay_ms = r_context()->config->poll_interval_ms * (poll_source->recent_error_count+1); ++ delay_ms = MIN(delay_ms, r_context()->config->poll_max_interval_ms); ++ break; ++ case POLL_DELAY_SHORT: ++ delay_ms = 15 * 1000; ++ break; ++ case POLL_DELAY_NOW: ++ delay_ms = 2 * 1000; ++ break; ++ case POLL_DELAY_INITIAL: ++ delay_ms = r_context()->config->poll_interval_ms * g_random_double_range(0.1, 0.9); ++ break; ++ default: ++ g_assert_not_reached(); ++ } ++ ++ //g_message("delay_ms=%"G_GINT64_FORMAT, delay_ms); ++ if (r_context()->mock.poll_speedup) ++ delay_ms = delay_ms / r_context()->mock.poll_speedup; ++ //g_message("speedup delay_ms=%"G_GINT64_FORMAT, delay_ms); ++ ++ gint64 next = g_get_monotonic_time() + delay_ms * 1000; ++ r_poller_set_next_poll(r_poller, next); ++ //g_message("now=%"G_GINT64_FORMAT" next poll=%"G_GINT64_FORMAT, g_get_monotonic_time(), next); ++ g_source_set_ready_time(&poll_source->source, next); ++ ++ g_autofree gchar *duration_str = r_format_duration(delay_ms / 1000); ++ g_message("scheduled next poll in: %s", duration_str); ++} ++ ++static gboolean poll_fetch(RPollSource *poll_source, GError **error) ++{ ++ GError *ierror = NULL; ++ ++ g_return_val_if_fail(poll_source, FALSE); ++ ++ /* fetch manifest */ ++ g_auto(RaucBundleAccessArgs) access_args = {0}; ++ access_args.http_info_headers = assemble_info_headers(NULL); ++ if (poll_source->bundle_etag) { ++ g_ptr_array_add(access_args.http_info_headers, g_strdup_printf("If-None-Match: %s", poll_source->bundle_etag)); ++ } ++ ++ g_autoptr(RaucBundle) bundle = NULL; ++ if (!check_bundle(r_context()->config->poll_source, &bundle, CHECK_BUNDLE_DEFAULT, &access_args, &ierror)) { ++ if (g_error_matches(ierror, R_NBD_ERROR, R_NBD_ERROR_NO_CONTENT)) { ++ g_message("polling: no bundle available"); ++ /* TODO update summary? */ ++ return TRUE; /* FIXME should this be an error? */ ++ } else if (g_error_matches(ierror, R_NBD_ERROR, R_NBD_ERROR_NOT_MODIFIED)) { ++ g_message("polling: bundle not modified"); ++ /* TODO update summary? */ ++ return TRUE; ++ } else { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ } ++ ++ if (!bundle->manifest) { ++ g_message("polling failed: no manifest found"); ++ return FALSE; ++ } ++ ++ g_clear_pointer(&poll_source->manifest, free_manifest); ++ poll_source->manifest = g_steal_pointer(&bundle->manifest); ++ ++ g_assert(bundle->nbd_srv); ++ poll_source->bundle_size = bundle->nbd_srv->data_size; ++ poll_source->bundle_modified_time = bundle->nbd_srv->modified_time; ++ r_replace_strdup(&poll_source->bundle_effective_url, bundle->nbd_srv->effective_url); ++ r_replace_strdup(&poll_source->bundle_etag, bundle->nbd_srv->etag); ++ ++ return TRUE; ++} ++ ++static gboolean poll_check_candidate_criteria(RPollSource *poll_source, GError **error) ++{ ++ GError *ierror = NULL; ++ ++ g_return_val_if_fail(poll_source, FALSE); ++ g_return_val_if_fail(poll_source->manifest, FALSE); ++ g_return_val_if_fail(error == NULL || *error == NULL, FALSE); ++ ++ const gchar *const *criteria = (const gchar *const *)r_context()->config->poll_candidate_criteria; ++ g_assert(criteria); ++ const gchar *system_ver = r_context()->system_version; ++ g_assert(system_ver); ++ const gchar *update_ver = poll_source->manifest->update_version; ++ g_assert(update_ver); ++ ++ if (g_strv_contains(criteria, "higher-semver")) { ++ if (!r_semver_less_equal(update_ver, system_ver, &ierror)) { ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ r_replace_strdup(&poll_source->summary, "update candidate found: higher semantic version"); ++ return TRUE; ++ } ++ } ++ ++ if (g_strv_contains(criteria, "different-version")) { ++ if (g_strcmp0(r_context()->system_version, poll_source->manifest->update_version) != 0) { ++ r_replace_strdup(&poll_source->summary, "update candidate found: different version"); ++ return TRUE; ++ } ++ } ++ ++ r_replace_strdup(&poll_source->summary, "no update candidate available"); ++ return FALSE; ++} ++ ++static gboolean poll_check_install_criteria(RPollSource *poll_source, GError **error) ++{ ++ GError *ierror = NULL; ++ ++ g_return_val_if_fail(poll_source, FALSE); ++ g_return_val_if_fail(poll_source->manifest, FALSE); ++ g_return_val_if_fail(error == NULL || *error == NULL, FALSE); ++ ++ const gchar *const *criteria = (const gchar *const *)r_context()->config->poll_install_criteria; ++ if (!criteria) { ++ g_debug("polling: no installation criteria"); ++ return FALSE; ++ } ++ ++ const gchar *system_ver = r_context()->system_version; ++ g_assert(system_ver); ++ const gchar *update_ver = poll_source->manifest->update_version; ++ g_assert(update_ver); ++ ++ if (g_strv_contains(criteria, "higher-semver")) { ++ if (!r_semver_less_equal(update_ver, system_ver, &ierror)) { ++ if (ierror) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ g_message("polling: automatic installation: higher semantic version"); ++ return TRUE; ++ } ++ } ++ ++ if (g_strv_contains(criteria, "different-version")) { ++ if (g_strcmp0(r_context()->system_version, poll_source->manifest->update_version) != 0) { ++ g_message("polling: automatic installation: different version"); ++ return TRUE; ++ } ++ } ++ ++ if (g_strv_contains(criteria, "always")) { ++ g_message("polling: automatic installation"); ++ return TRUE; ++ } ++ ++ return FALSE; ++} ++ ++static gboolean poll_check_reboot_criteria(const RaucInstallArgs *install_args) ++{ ++ g_return_val_if_fail(install_args, FALSE); ++ ++ const gchar *const *criteria = (const gchar *const *)r_context()->config->poll_reboot_criteria; ++ if (!criteria) ++ return FALSE; ++ ++ if (g_strv_contains(criteria, "failed-update")) { ++ if (install_args->status_result != 0) { ++ g_message("polling: installation failed, triggering reboot"); ++ return TRUE; ++ } ++ } else { ++ if (install_args->status_result != 0) { ++ g_message("polling: installation failed, suppressing reboot"); ++ return FALSE; ++ } ++ } ++ ++ if (g_strv_contains(criteria, "updated-slots")) { ++ if (install_args->updated_slots) ++ return TRUE; ++ } ++ ++ if (g_strv_contains(criteria, "updated-artifacts")) { ++ if (install_args->updated_artifacts) ++ return TRUE; ++ } ++ ++ return FALSE; ++} ++ ++static gboolean poll_install_cleanup(gpointer data) ++{ ++ g_return_val_if_fail(data, G_SOURCE_REMOVE); ++ ++ RaucInstallArgs *args = data; ++ RPollSource *poll_source = args->data; ++ ++ g_return_val_if_fail(poll_source, G_SOURCE_REMOVE); ++ ++ poll_source->installation_running = FALSE; ++ ++ g_mutex_lock(&args->status_mutex); ++ if (args->status_result == 0) { ++ g_message("polling: installation of `%s` succeeded", args->name); ++ } else { ++ g_message("polling: installation of `%s` failed: %d", args->name, args->status_result); ++ } ++ /* TODO expose error? */ ++ r_installer_emit_completed(r_installer, args->status_result); ++ r_installer_set_operation(r_installer, "idle"); ++ g_dbus_interface_skeleton_flush(G_DBUS_INTERFACE_SKELETON(r_installer)); ++ if (poll_check_reboot_criteria(args)) { ++ poll_source->must_reboot = TRUE; ++ } ++ g_mutex_unlock(&args->status_mutex); ++ ++ install_args_free(args); ++ ++ poll_reschedule(poll_source, POLL_DELAY_SHORT); ++ g_dbus_interface_skeleton_flush(G_DBUS_INTERFACE_SKELETON(r_poller)); ++ ++ return G_SOURCE_REMOVE; ++} ++ ++static gboolean poll_install(RPollSource *poll_source, GError **error) ++{ ++ RaucInstallArgs *args = install_args_new(); ++ g_autofree gchar *message = NULL; ++ gboolean res = FALSE; ++ ++ g_return_val_if_fail(poll_source, FALSE); ++ g_return_val_if_fail(poll_source->manifest, FALSE); ++ g_return_val_if_fail(poll_source->manifest->hash, FALSE); ++ g_return_val_if_fail(error == NULL || *error == NULL, FALSE); ++ ++ args->name = g_strdup(r_context()->config->poll_source); ++ args->cleanup = poll_install_cleanup; ++ args->data = poll_source; ++ /* lock bundle via manifest hash */ ++ args->require_manifest_hash = g_strdup(poll_source->manifest->hash); ++ ++ r_installer_set_operation(r_installer, "installing"); ++ res = install_run(args); ++ if (!res) { ++ args->status_result = 1; ++ goto out; ++ } ++ args = NULL; ++ poll_source->installation_running = TRUE; ++ ++out: ++ g_clear_pointer(&args, install_args_free); ++ if (!res) { ++ r_installer_set_operation(r_installer, "idle"); ++ } ++ ++ return TRUE; ++} ++ ++static gboolean poll_reboot(GError **error) ++{ ++ GError *ierror = NULL; ++ ++ g_return_val_if_fail(error == NULL || *error == NULL, FALSE); ++ ++ const gchar *cmd = r_context()->config->poll_reboot_cmd; ++ g_assert(cmd); ++ ++ g_auto(GStrv) argvp = NULL; ++ if (!g_shell_parse_argv(cmd, NULL, &argvp, &ierror)) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ ++ g_autoptr(GPtrArray) args = g_ptr_array_new_full(10, g_free); ++ r_ptr_array_addv(args, argvp, TRUE); ++ g_ptr_array_add(args, NULL); ++ ++ if (!r_subprocess_runv(args, G_SUBPROCESS_FLAGS_NONE, &ierror)) { ++ g_propagate_prefixed_error( ++ error, ++ ierror, ++ "failed to run reboot command ('%s'): ", ++ cmd ++ ); ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ ++static gboolean poll_step(RPollSource *poll_source, GError **error) ++{ ++ GError *ierror = NULL; ++ ++ g_return_val_if_fail(poll_source, FALSE); ++ g_return_val_if_fail(error == NULL || *error == NULL, FALSE); ++ ++ if (!poll_fetch(poll_source, &ierror)) { ++ g_propagate_error(error, ierror); ++ return FALSE; ++ } ++ ++ g_clear_pointer(&poll_source->summary, g_free); ++ poll_source->update_available = FALSE; ++ gboolean candidate = poll_check_candidate_criteria(poll_source, &ierror); ++ if (ierror) { ++ g_propagate_prefixed_error(error, ierror, "candidate criteria check failed: "); ++ return FALSE; ++ } else if (!candidate) { ++ return TRUE; ++ } ++ poll_source->update_available = TRUE; ++ ++ gboolean install = poll_check_install_criteria(poll_source, &ierror); ++ if (ierror) { ++ g_propagate_prefixed_error(error, ierror, "installation criteria check failed: "); ++ return FALSE; ++ } else if (!install) { ++ g_message("polling: candidate needs to be explicitly confirmed"); ++ return TRUE; ++ } ++ ++ /* skip attempt if manifest hash is the same */ ++ if (g_strcmp0(poll_source->manifest->hash, poll_source->attempted_hash) == 0) { ++ g_message("polling: manifest is unchanged, skipping installation"); ++ return TRUE; ++ } ++ ++ g_message("polling: starting installation of version '%s'", poll_source->manifest->update_version); ++ r_replace_strdup(&poll_source->attempted_hash, poll_source->manifest->hash); ++ if (!poll_install(poll_source, &ierror)) { ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ ++static void poll_update_status(RPollSource *poll_source) ++{ ++ g_return_if_fail(poll_source); ++ ++ g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{sv}")); ++ ++ g_variant_builder_add(&builder, "{sv}", "attempt-count", g_variant_new_uint64(poll_source->attempt_count)); ++ g_variant_builder_add(&builder, "{sv}", "recent-error-count", g_variant_new_uint64(poll_source->recent_error_count)); ++ ++ g_variant_builder_add(&builder, "{sv}", "last-attempt-time", g_variant_new_uint64(poll_source->last_attempt_time)); ++ g_variant_builder_add(&builder, "{sv}", "last-success-time", g_variant_new_uint64(poll_source->last_success_time)); ++ if (poll_source->last_error_message) ++ g_variant_builder_add(&builder, "{sv}", "last-error-message", g_variant_new_string(poll_source->last_error_message)); ++ g_variant_builder_add(&builder, "{sv}", "update-available", g_variant_new_boolean(poll_source->update_available)); ++ if (poll_source->summary) ++ g_variant_builder_add(&builder, "{sv}", "summary", g_variant_new_string(poll_source->summary)); ++ if (poll_source->attempted_hash) ++ g_variant_builder_add(&builder, "{sv}", "attempted-hash", g_variant_new_string(poll_source->attempted_hash)); ++ ++ if (poll_source->manifest) { ++ /* manifest dict */ ++ g_variant_builder_add(&builder, "{sv}", "manifest", r_manifest_to_dict(poll_source->manifest)); ++ ++ /* bundle dict */ ++ g_variant_builder_open(&builder, G_VARIANT_TYPE("{sv}")); ++ g_variant_builder_add(&builder, "s", "bundle"); ++ g_variant_builder_open(&builder, G_VARIANT_TYPE("v")); ++ g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); ++ if (poll_source->bundle_size) ++ g_variant_builder_add(&builder, "{sv}", "size", g_variant_new_uint64(poll_source->bundle_size)); ++ if (poll_source->bundle_effective_url) ++ g_variant_builder_add(&builder, "{sv}", "effective-url", g_variant_new_string(poll_source->bundle_effective_url)); ++ if (poll_source->bundle_modified_time) ++ g_variant_builder_add(&builder, "{sv}", "modified-time", g_variant_new_uint64(poll_source->bundle_modified_time)); ++ if (poll_source->bundle_etag) ++ g_variant_builder_add(&builder, "{sv}", "etag", g_variant_new_string(poll_source->bundle_etag)); ++ g_variant_builder_close(&builder); /* inner a{sv} */ ++ g_variant_builder_close(&builder); /* inner v */ ++ g_variant_builder_close(&builder); /* outer {sv} */ ++ } ++ ++ r_poller_set_status(r_poller, g_variant_builder_end(&builder)); ++} ++ ++static gboolean on_handle_poll(RPoller *interface, GDBusMethodInvocation *invocation) ++{ ++ RPollSource *poll_source = (RPollSource *)g_object_get_data(G_OBJECT(interface), "r-poll"); ++ g_assert(poll_source); ++ ++ poll_reschedule(poll_source, POLL_DELAY_NOW); ++ g_dbus_interface_skeleton_flush(G_DBUS_INTERFACE_SKELETON(r_poller)); ++ r_poller_complete_poll(interface, invocation); ++ ++ return TRUE; ++} ++ ++static gboolean poll_source_dispatch(GSource *source, GSourceFunc _callback, gpointer _user_data) ++{ ++ g_return_val_if_fail(source, G_SOURCE_REMOVE); ++ ++ RPollSource *poll_source = (void *)source; ++ g_autoptr(GError) ierror = NULL; ++ ++ /* check busy state */ ++ if (r_context_get_busy()) { ++ g_debug("context busy, will try again later"); ++ poll_reschedule(poll_source, POLL_DELAY_SHORT); ++ return G_SOURCE_CONTINUE; ++ } ++ ++ /* check inhibit */ ++ for (gchar **p = r_context()->config->poll_inhibit_files; p && *p; p++) { ++ if (g_file_test(*p, G_FILE_TEST_EXISTS)) { ++ g_debug("inhibited by %s", *p); ++ poll_reschedule(poll_source, POLL_DELAY_SHORT); ++ return G_SOURCE_CONTINUE; ++ } ++ } ++ ++ /* check if we need to reboot */ ++ if (poll_source->must_reboot) { ++ if (!poll_reboot(&ierror)) { ++ g_message("reboot failed: %s", ierror->message); ++ poll_reschedule(poll_source, POLL_DELAY_SHORT); ++ return G_SOURCE_CONTINUE; ++ } ++ ++ /* after triggering a reboot, we stop polling */ ++ return G_SOURCE_REMOVE; ++ } ++ ++ /* poll once */ ++ poll_source->last_attempt_time = g_get_monotonic_time(); ++ poll_source->attempt_count += 1; ++ /* TODO add some headers? recent errors? */ ++ if (!poll_step(poll_source, &ierror)) { ++ g_message("polling failed: %s", ierror->message); ++ r_replace_strdup(&poll_source->last_error_message, ierror->message); ++ poll_source->recent_error_count += 1; ++ } else { ++ g_clear_pointer(&poll_source->last_error_message, g_free); ++ poll_source->last_success_time = g_get_monotonic_time(); ++ poll_source->recent_error_count = 0; ++ } ++ ++ poll_update_status(poll_source); ++ ++ if (poll_source->installation_running) { ++ /* wait until the installation has completed */ ++ g_source_set_ready_time(&poll_source->source, -1); ++ } else { ++ /* schedule next poll */ ++ poll_reschedule(poll_source, POLL_DELAY_NORMAL); ++ } ++ ++ return G_SOURCE_CONTINUE; ++} ++ ++static void poll_source_finalize(GSource *source) ++{ ++ g_return_if_fail(source); ++ ++ g_clear_pointer(&r_poller, g_object_unref); ++ ++ RPollSource *poll_source = (void *)source; ++ g_clear_pointer(&poll_source->last_error_message, g_free); ++ g_clear_pointer(&poll_source->manifest, free_manifest); ++ g_clear_pointer(&poll_source->summary, g_free); ++ g_clear_pointer(&poll_source->attempted_hash, g_free); ++ g_clear_pointer(&poll_source->bundle_effective_url, g_free); ++ g_clear_pointer(&poll_source->bundle_etag, g_free); ++} ++ ++static GSourceFuncs source_funcs = { ++ .dispatch = poll_source_dispatch, ++ .finalize = poll_source_finalize, ++}; ++ ++void r_poll_on_bus_acquired(GDBusConnection *connection) ++{ ++ GError *ierror = NULL; ++ ++ if (!r_context()->config->poll_source) { ++ return; ++ } ++ ++ g_assert(r_poller); ++ ++ g_signal_connect(r_poller, "handle-poll", ++ G_CALLBACK(on_handle_poll), ++ NULL); ++ ++ if (!g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(r_poller), ++ connection, ++ "/", ++ &ierror)) { ++ g_error("Failed to export interface: %s", ierror->message); ++ g_error_free(ierror); ++ } ++ g_message("poller skeleton set up"); ++} ++ ++GSource *r_poll_setup(void) ++{ ++ if (!r_context()->config->poll_source) { ++ return NULL; ++ } ++ ++ r_poller = r_poller_skeleton_new(); ++ ++ GSource *source = g_source_new(&source_funcs, sizeof(RPollSource)); ++ RPollSource *poll_source = (void *)source; ++ g_object_set_data(G_OBJECT(r_poller), "r-poll", poll_source); ++ ++ poll_update_status(poll_source); ++ poll_reschedule(poll_source, POLL_DELAY_INITIAL); ++ g_source_attach(source, NULL); ++ ++ g_message("polling enabled"); ++ ++ return source; ++} +diff --git a/src/service.c b/src/service.c +index 1567fff3..6899648e 100644 +--- a/src/service.c ++++ b/src/service.c +@@ -1,6 +1,5 @@ +-#include +-#include + #include ++#include + #include + + #include "artifacts.h" +@@ -9,11 +8,13 @@ + #include "config_file.h" + #include "context.h" + #include "install.h" ++#include "manifest.h" + #include "mark.h" +-#include "rauc-installer-generated.h" ++#include "nbd.h" + #include "service.h" + #include "status_file.h" + #include "utils.h" ++#include "poll.h" + + GMainLoop *service_loop = NULL; + RInstaller *r_installer = NULL; +@@ -554,8 +555,6 @@ static void r_on_bus_acquired(GDBusConnection *connection, + { + GError *ierror = NULL; + +- r_installer = r_installer_skeleton_new(); +- + g_signal_connect(r_installer, "handle-install", + G_CALLBACK(r_on_handle_install), + NULL); +@@ -605,6 +604,8 @@ static void r_on_bus_acquired(GDBusConnection *connection, + r_installer_set_variant(r_installer, r_context()->config->system_variant); + r_installer_set_boot_slot(r_installer, r_context()->bootslot); + ++ r_poll_on_bus_acquired(connection); ++ + return; + } + +@@ -661,10 +662,15 @@ gboolean r_service_run(void) + gboolean service_return = TRUE; + GBusType bus_type = (!g_strcmp0(g_getenv("DBUS_STARTER_BUS_TYPE"), "session")) + ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM; ++ GSource *poll_source = NULL; + + service_loop = g_main_loop_new(NULL, FALSE); + g_unix_signal_add(SIGTERM, r_on_signal, NULL); + ++ r_installer = r_installer_skeleton_new(); ++ ++ poll_source = r_poll_setup(); ++ + r_bus_name_id = g_bus_own_name(bus_type, + "de.pengutronix.rauc", + G_BUS_NAME_OWNER_FLAGS_NONE, +@@ -682,6 +688,8 @@ gboolean r_service_run(void) + service_loop = NULL; + + g_clear_pointer(&r_installer, g_object_unref); ++ if (poll_source) ++ g_source_unref(poll_source); + + return service_return; + } +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0020-WIP-docs-document-polling.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0020-WIP-docs-document-polling.patch new file mode 100644 index 00000000..0bfab1a8 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0020-WIP-docs-document-polling.patch @@ -0,0 +1,366 @@ +From dc1be15013c9f07afeffb8578cf4bf97c2019d67 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 17:04:47 +0100 +Subject: [PATCH 20/23] WIP: docs: document polling + +--- + docs/advanced.rst | 137 ++++++++++++++++++++++++++++++++++ + docs/reference.rst | 179 ++++++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 315 insertions(+), 1 deletion(-) + +diff --git a/docs/advanced.rst b/docs/advanced.rst +index c47dd671..5f47e4ea 100644 +--- a/docs/advanced.rst ++++ b/docs/advanced.rst +@@ -508,6 +508,143 @@ Beside some standard information, like the *boot ID*, the system's *uptime* or + the *installation transaction ID*, one can also expose custom information + provided by the ``system-info`` :ref:`handler `. + ++.. _sec-polling: ++ ++Update Polling ++-------------- ++ ++The polling functionality in RAUC allows a device to periodically check for ++updates from a specified source. ++It fetches bundle manifests, checks for available updates, and initiates ++installations if necessary. ++This functionality is intended to automate the update process and ensure that ++the system remains up-to-date with minimal manual intervention. ++ ++The polling functionality is designed so that it can be used and extended for ++different scenarios: ++ ++simple HTTP(S) server with fixed URL ++ New versions are deployed by simply replacing the bundle on the server with ++ the new one. ++ ++simple server-side script with HTTP redirect ++ The script evaluates the HTTP headers sent by RAUC and information about the ++ available update bundles to select one for installation. ++ It replies with a HTTP 3xx redirect to the actual bundle URL, which might be ++ on a different server or CDN. ++ If no update should be installed, a HTTP 204 (no content) code can be sent. ++ ++device management server ++ This is similar to the previous case, but uses a database of devices indexed ++ by the device identity (e.g. system serial or TLS client certificate). ++ This way, updates can be targeted at the level of individual devices. ++ By collecting the information sent in the HTTP request headers, the database ++ can keep track of which version is running on which device and if the devices ++ are polling regularly. ++ ++While the server-side software is (currently) out-of-scope for the RAUC project, ++we're open to linking to compatible implementations from this documentation. ++More complex scenarios can be supported in the future by using the ++:ref:`[rollout] manifest section ` (for example: update time ++windows, phased rollouts, freshness checks, ...). ++Please contact us if this is relevant to your use-case. ++ ++To use polling, bundles in the ``verity`` or ``crypt`` :ref:`formats ++` must be used. ++The configuration is done via the :ref:`[poll] section ` of ++``system.conf``. ++The :ref:`Poller D-Bus interface ` ++can be used to trigger a poll and also exposes the results. ++ ++If there are times where RAUC should *not* poll for updates, other parts of the ++system can signal this by creating inhibit files. ++As long as any of the file listed in ``inhibit-files`` exist, no polling or ++installation is started. ++Note that a running poll or installation is not aborted. ++ ++To support different use-cases, the polling functionality can be used with or ++without automatic installation. ++ ++Automatic Installation ++~~~~~~~~~~~~~~~~~~~~~~ ++ ++The criteria which a bundle needs to fulfill to be considered a valid update can ++be configured in via ``candidate-criteria`` in the ``[poll]`` section. ++For any valid candidate, the ``install-criteria`` option can be used to trigger ++an automatic installation (see the :ref:`reference ` ++for supported criteria). ++If no automatic installation is triggered, it can be confirmed explicitly (see ++below). ++ ++After an automatic installation completes, the ``reboot-criteria`` are checked ++to determine if the system should be rebooted. ++If the installation of a bundle fails, it is not attempted again until the RAUC ++service is restarted or a new manifest is found. ++ ++Example usage: ++ ++.. code-block:: cfg ++ :emphasize-lines: 3-5 ++ ++ [poll] ++ source=https://example.com/stable/my_product.raucb ++ candidate-criteria=different-version ++ install-criteria=higher-semver ++ reboot-criteria=updated-slots;updated-artifacts ++ ++In this example: ++ ++1. A new bundle is considered a valid update candidate if it's declared version ++ is different from the current system version. ++2. It is automatically installed only if its semantic version is higher. ++3. A reboot occurs automatically after installation **only** if a slot or ++ artifact was actually installed. ++ ++For valid candidates which don't have a higher version, the update is only ++offered for confirmation. ++ ++Confirmed Installation ++~~~~~~~~~~~~~~~~~~~~~~ ++ ++For more complex cases, RAUC can handle the polling and inform a separate ++application or the user if an update is available. ++If and when an update should be installed, the :ref:`InstallBundle D-Bus method ++` is used to trigger ++the actual installation (see below). ++ ++To ensure that the correct bundle is installed after evaluating the manifest ++information, RAUC uses the "manifest hash". ++This hash is computed over the signed manifest and fully identifies the contents ++of the bundle. ++Note that the signature itself is *not* included in the manifest hash. ++ ++Backoff ++~~~~~~~ ++ ++If RAUC is unable to fetch a valid bundle manifest from the URL, it will ++increase the polling interval to avoid an overload scenario. ++Each consecutive failure extends the base interval (``interval-sec``) by the ++length of the base interval. ++The interval is not extended beyond the maximum set by ``max-interval-sec``, ++which defaults to four times the base interval. ++ ++Example usage: ++ ++.. code-block:: cfg ++ :emphasize-lines: 3-4 ++ ++ [poll] ++ source=https://example.com/stable/my_product.raucb ++ interval-sec=300 ++ max-interval-sec=3600 ++ ++In this example: ++ ++* RAUC polls the source every 5 minutes as long as no error occurs. ++* Every time a poll attempt fails, the interval is increased by 5 minutes, until ++ it reaches 1 hour. ++* If an attempt is successful again, the interval is reduced to 5 minutes. ++ + .. _sec-encryption: + + Bundle Encryption +diff --git a/docs/reference.rst b/docs/reference.rst +index c90ad12c..7dc6c0ea 100644 +--- a/docs/reference.rst ++++ b/docs/reference.rst +@@ -369,6 +369,95 @@ For more information about using the streaming support of RAUC, refer to + * ``transaction-id``: Enables sending the *transaction UUID* as ``RAUC-Transaction-ID`` header field. + * ``uptime``: Enables sending the system's current uptime as ``RAUC-Uptime`` header field. + ++.. _poll-section: ++ ++.. FIXME s/poll/polling? ++ ++``[poll]`` Section ++~~~~~~~~~~~~~~~~~~ ++ ++``source`` (required if the section exists) ++ The URL from which to fetch the update manifest. This must be an HTTP(S) URL. ++ ++``interval-sec`` (optional, default is one day/86400 seconds) ++ The interval, in seconds, between polling attempts. Default is one day (86400 seconds). ++ ++``max-interval-sec`` (optional, default is four times ``interval-sec``) ++ The maximum interval, in seconds, between polling attempts. ++ This should be larger than interval-sec. ++ ++``inhibit-files`` (optional) ++ A list of files that, if present, inhibit polling. ++ ++.. _poll-candidate-criteria: ++ ++``candidate-criteria`` (optional, default is ``higher-semver``) ++ Specifies the conditions under which a new bundle is considered a **candidate** ++ for updating. ++ These conditions do **not** automatically trigger the installation of the ++ update. ++ ++ Supported values are: ++ ++ * ``higher-semver``: The new bundle's version (interpreted as a Semantic ++ Version) is higher than the current system version. ++ * ``different-version``: The new bundle's version string differs from the ++ current system version (regardless of ordering). ++ ++ .. or ``version-is-different`` ++ ++ Multiple conditions can be specified as a ``;``-separated list. ++ A new bundle is considered a candidate if it meets **at least one** of the ++ listed conditions. ++ ++ .. note:: The current system version is the ``RAUC_SYSTEM_VERSION`` as ++ reported by the ``system-info`` handler. ++ ++``install-criteria`` (optional, default is empty) ++ Specifies the conditions under which a candidate is **automatically ++ installed**. ++ Only bundles already deemed valid candidates (by ``candidate-criteria``) are ++ considered for installation. ++ ++ Supported values are: ++ ++ * any of the values supported for ``candidate-criteria`` ++ * ``always``: Any candidate should be installed automatically. ++ ++ .. we could later add ``urgent`` as an option or match on meta-data ++ ++ Multiple conditions can be specified as a ``;``-separated list. ++ A candidate is automatically installed if it meets **at least one** of the ++ listed conditions. ++ ++ If the installation of a bundle fails, it is not attempted again until the ++ RAUC service is restarted or a new manifest is found. ++ ++ .. note:: If you do **not** configure ``install-criteria``, or if no ++ conditions are met, a new bundle recognized as a candidate will **not** be ++ installed automatically and will require explicit confirmation. ++ ++``reboot-criteria`` (optional, default is empty) ++ Specifies the conditions under which a reboot is triggered **after** an ++ automatic installation. ++ Only applies if ``install-criteria`` have been met and an update was actually ++ installed. ++ ++ Supported values are: ++ ++ * ``updated-slots``: The bundle contained new images for slots. ++ * ``updated-artifacts``: The bundle contained new or changed artifacts. ++ * ``failed-update``: The update could not be installed due to a runtime error. ++ ++ Multiple conditions can be listed; if **any** condition is met, a reboot is ++ triggered by executing the ``reboot-cmd``. ++ ++ .. note:: If you do **not** configure ``reboot-criteria``, the default ++ behavior is to never automatically reboot after installation. ++ ++``reboot-cmd`` (optional, defaults to ``reboot``) ++ Command to execute for rebooting the system after an update. ++ + ``[encryption]`` Section + ~~~~~~~~~~~~~~~~~~~~~~~~ + +@@ -1743,8 +1832,10 @@ IN *args* ``a{sv}``: + *args.tls-no-verify* variant ``b`` : + Ignore verification errors for the server certificate + ++.. _gdbus-method-de-pengutronix-rauc-Installer.InspectBundle.info: ++ + OUT *info* ``a{sv}``: +- Bundle info ++ Information from the bundle's manifest. + + *info.manifest-hash* variant ``s`` : + A SHA256 hash sum over the manifest content +@@ -1995,6 +2086,92 @@ This can either be the slot name (e.g. ``rauc.slot=rootfs.0``) or the root devic + path (e.g. ``root=PARTUUID=0815``). If the ``root=`` kernel command line option is + used, the symlink is resolved to the block device (e.g. ``/dev/mmcblk0p1``). + ++.. _gdbus-interface-de-pengutronix-rauc-Poller: ++ ++Poller Interface ++~~~~~~~~~~~~~~~~ ++ ++.. literalinclude:: ../src/de.pengutronix.rauc.Poller.xml ++ :caption: ``src/de.pengutronix.rauc.Poller.xml`` ++ :language: xml ++ :lineno-match: ++ :end-at: ++ :end-at: : ++ The number of polling attempts ++ ++*recent-error-count* variant ``t`` : ++ The number of failed polling attempts since the last success (or service startup) ++ ++*last-attempt-time* variant ``t`` : ++ The time of the last polling attempt (in FIXME) ++ ++*last-success-time* variant ``t`` : ++ The time of the last successful poll (in FIXME) ++ ++*last-error-message* variant ``s`` : ++ The failure cause, if the most last attempt failed ++ ++*update-available* variant ``b`` : ++ True if the bundle is considered a valid update according to the configuration ++ ++*summary* variant ``b`` : ++ Summary of whether the bundle is a valid update. ++ ++*attempted-hash* variant ``s`` : ++ The manifest hash of the most recent installation attempt ++ ++*manifest* variant ``a{sv}`` : ++ The contents of the bundle's manifest, :ref:`as documented in the ++ InspectBundle() method ++ ` ++ ++ Check the ``recent-error-count`` and ``last-success-time`` to know if this ++ may be outdated. ++ ++*bundle* variant ``a{sv}`` : ++ Details of the polled bundle ++ ++ *bundle.size* variant ``t`` : ++ The size of the bundle file in bytes ++ ++ *bundle.effective-url* variant ``s`` : ++ The actual URL used after following any potential redirects ++ ++ *bundle.modified-time* variant ``t`` : ++ The modification time of the bundle as reported by the server via the ++ ``Last-Modified`` HTTP header in Unix time ++ ++ *bundle.etag* variant ``s`` : ++ The ``ETag`` HTTP header value as reported by the server + + RAUC's Basic Update Procedure + ----------------------------- +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0021-WIP-add-tests-for-polling.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0021-WIP-add-tests-for-polling.patch new file mode 100644 index 00000000..5f0e6cae --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0021-WIP-add-tests-for-polling.patch @@ -0,0 +1,327 @@ +From 03a7e0779a0c81f1f681ba05d71a62f0049c55a8 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 17:05:01 +0100 +Subject: [PATCH 21/23] WIP: add tests for polling + +--- + test/bin/systeminfo.sh | 6 +- + test/conftest.py | 7 +- + test/meson.build | 1 + + test/test_polling.py | 245 +++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 257 insertions(+), 2 deletions(-) + create mode 100644 test/test_polling.py + +diff --git a/test/bin/systeminfo.sh b/test/bin/systeminfo.sh +index c6cf9215..7310a354 100755 +--- a/test/bin/systeminfo.sh ++++ b/test/bin/systeminfo.sh +@@ -2,6 +2,10 @@ + + echo RAUC_SYSTEM_SERIAL=1234 + echo RAUC_SYSTEM_VARIANT=test-variant-x +-echo RAUC_SYSTEM_VERSION=1.0.0 ++if [ -n "$RAUC_TEST_SYSTEM_VERSION" ]; then ++ echo RAUC_SYSTEM_VERSION="$RAUC_TEST_SYSTEM_VERSION" ++else ++ echo RAUC_SYSTEM_VERSION=1.0.0 ++fi + echo RAUC_CUSTOM_VARIABLE=test-value + echo RAUC_TEST_VAR +diff --git a/test/conftest.py b/test/conftest.py +index e864d0e1..977c9a1d 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -162,6 +162,9 @@ needs_emmc = pytest.mark.skipif("RAUC_TEST_EMMC" not in os.environ, reason="Miss + needs_composefs = pytest.mark.skipif(not string_in_config_h("ENABLE_COMPOSEFS 1"), reason="Missing composefs support") + + ++needs_nbd = pytest.mark.skipif("RAUC_TEST_NBD_SERVER" not in os.environ, reason="Missing NBD") ++ ++ + def softhsm2_load_key_pair(cert, privkey, label, id_, softhsm2_mod): + proc = subprocess.run( + f"openssl x509 -in {cert} -inform pem -outform der " +@@ -626,7 +629,7 @@ class System: + return events + + @contextmanager +- def running_service(self, bootslot, *, poll_speedup=None): ++ def running_service(self, bootslot, *, poll_speedup=None, extra_env=None): + if not have_service(): + # TODO avoid unnescesary setup by moving using a pytest mark for all service/noservice cases + pytest.skip("No service") +@@ -638,6 +641,8 @@ class System: + env["RAUC_PYTEST_TMP"] = str(self.tmp_path) + if poll_speedup: + env["RAUC_TEST_POLL_SPEEDUP"] = f"{poll_speedup}" ++ if extra_env: ++ env.update(extra_env) + + command = "" + if "SERVICE_BACKTRACE" in env: +diff --git a/test/meson.build b/test/meson.build +index 25d0666d..05b5c952 100644 +--- a/test/meson.build ++++ b/test/meson.build +@@ -90,6 +90,7 @@ if pytest.found() + 'mount', + 'replace_signature', + 'resign', ++ 'polling', + 'service', + 'sign', + 'status', +diff --git a/test/test_polling.py b/test/test_polling.py +new file mode 100644 +index 00000000..72724095 +--- /dev/null ++++ b/test/test_polling.py +@@ -0,0 +1,245 @@ ++import time ++ ++import pytest ++from dasbus.typing import get_native ++ ++from conftest import needs_nbd ++ ++ ++def wait_one_poll(system, *, timeout=15.0): ++ start = time.monotonic() ++ old = system.proxy.NextPoll ++ while old == system.proxy.NextPoll: ++ time.sleep(0.1) ++ assert time.monotonic() < (start + timeout) ++ return time.monotonic() - start ++ ++ ++def test_poll_only(create_system_files, system, http_server): ++ """Test if the info command sends custom headers correctly.""" ++ http_server.setup( ++ file_path="test/good-verity-bundle.raucb", ++ ) ++ ++ system.prepare_minimal_config() ++ system.config["handlers"] = { ++ "system-info": "bin/systeminfo.sh", ++ } ++ system.config["streaming"] = { ++ "send-headers": "system-version;transaction-id", ++ } ++ system.config["poll"] = { ++ "source": http_server.url, ++ "interval-sec": "60", ++ } ++ system.write_config() ++ ++ with system.running_service("A", poll_speedup=10): ++ slots_initial = get_native(system.proxy.GetSlotStatus()) ++ status_1 = get_native(system.proxy.Status) ++ ++ wait_time_1 = wait_one_poll(system) ++ status_2 = get_native(system.proxy.Status) ++ ++ wait_time_2 = wait_one_poll(system) ++ ++ system.proxy.Poll() ++ wait_time_3 = wait_one_poll(system) ++ status_3 = get_native(system.proxy.Status) ++ ++ slots_final = get_native(system.proxy.GetSlotStatus()) ++ ++ assert "manifest" not in status_1 ++ assert wait_time_1 < 60 / 10 # initial delay ++ assert status_2["manifest"]["update"]["version"] == "2011.03-2" ++ assert status_2["recent-error-count"] == 0 ++ assert status_2["attempt-count"] == 1 ++ assert wait_time_2 == pytest.approx(60 / 10, abs=0.5) # normal poll ++ assert wait_time_3 == pytest.approx(2 / 10, abs=0.5) # poll now ++ assert status_3["manifest"] == status_2["manifest"] ++ assert status_3["recent-error-count"] == 0 ++ assert status_3["attempt-count"] == 3 ++ assert status_3["summary"] == "update candidate found: higher semantic version" ++ ++ assert slots_initial == slots_final ++ ++ ++@pytest.mark.parametrize( ++ "sys_ver,criteria,result", ++ [ ++ pytest.param("0.1", "different-version", "update candidate found: different version", id="version different"), ++ pytest.param("2011.03-2", "different-version", "no update candidate available", id="version unchanged"), ++ pytest.param("0.1", "higher-semver", "update candidate found: higher semantic version", id="semver newer"), ++ pytest.param("2011.03-2", "higher-semver", "no update candidate available", id="semver unchanged"), ++ pytest.param("9999.1", "higher-semver", "no update candidate available", id="semver older"), ++ ], ++) ++def test_poll_candidate_criteria(create_system_files, system, http_server, sys_ver, criteria, result): ++ """Test if the info command sends custom headers correctly.""" ++ http_server.setup( ++ file_path="test/good-verity-bundle.raucb", ++ ) ++ ++ system.prepare_minimal_config() ++ system.config["handlers"] = { ++ "system-info": "bin/systeminfo.sh", ++ } ++ system.config["streaming"] = { ++ "send-headers": "system-version;transaction-id", ++ } ++ system.config["poll"] = { ++ "source": http_server.url, ++ "interval-sec": "60", ++ "candidate-criteria": criteria, ++ } ++ system.write_config() ++ ++ env = {"RAUC_TEST_SYSTEM_VERSION": sys_ver} ++ ++ with system.running_service("A", poll_speedup=10, extra_env=env): ++ slots_initial = get_native(system.proxy.GetSlotStatus()) ++ wait_one_poll(system) ++ status = get_native(system.proxy.Status) ++ slots_final = get_native(system.proxy.GetSlotStatus()) ++ ++ assert status["manifest"]["update"]["version"] == "2011.03-2" ++ assert status["recent-error-count"] == 0 ++ assert status["attempt-count"] == 1 ++ assert status["summary"] == result ++ ++ assert slots_initial == slots_final ++ ++ ++@pytest.mark.parametrize( ++ "sys_ver,criteria,result", ++ [ ++ pytest.param( ++ "2010.01-1", ++ ("different-version", "higher-semver"), ++ ("update candidate found: different version", True), ++ id="semver newer", ++ ), ++ pytest.param( ++ "2022.12-3", ++ ("different-version", "different-version"), ++ ("update candidate found: different version", True), ++ id="version different", ++ ), ++ pytest.param( ++ "2022.12-3", ++ ("different-version", "higher-semver"), ++ ("update candidate found: different version", False), ++ id="semver older", ++ ), ++ pytest.param( ++ "2022.12-3", ++ ("different-version", "always"), ++ ("update candidate found: different version", True), ++ id="always", ++ ), ++ ], ++) ++@needs_nbd ++def test_poll_install_criteria(create_system_files, system, http_server, sys_ver, criteria, result): ++ """Test if the info command sends custom headers correctly.""" ++ http_server.setup( ++ file_path="test/good-verity-bundle.raucb", ++ ) ++ ++ system.prepare_minimal_config() ++ system.config["handlers"] = { ++ "system-info": "bin/systeminfo.sh", ++ } ++ system.config["streaming"] = { ++ "send-headers": "system-version;transaction-id", ++ } ++ system.config["poll"] = { ++ "source": http_server.url, ++ "interval-sec": "60", ++ "candidate-criteria": criteria[0], ++ "install-criteria": criteria[1], ++ } ++ system.write_config() ++ ++ env = {"RAUC_TEST_SYSTEM_VERSION": sys_ver} ++ ++ with system.running_service("A", poll_speedup=10, extra_env=env): ++ slots_initial = get_native(system.proxy.GetSlotStatus()) ++ wait_one_poll(system) ++ status = get_native(system.proxy.Status) ++ slots_final = get_native(system.proxy.GetSlotStatus()) ++ ++ assert status["manifest"]["update"]["version"] == "2011.03-2" ++ assert status["recent-error-count"] == 0 ++ assert status["attempt-count"] == 1 ++ assert status["summary"] == result[0] ++ ++ slots_initial = dict(slots_initial) ++ slots_final = dict(slots_final) ++ assert slots_initial["rootfs.1"].get("installed.count", 0) == 0 ++ if result[1]: # installation should have happened ++ assert slots_final["rootfs.1"].get("installed.count", 0) == 1 ++ else: ++ assert slots_initial == slots_final ++ ++ ++@pytest.mark.parametrize( ++ "remove_appfs,criteria,result", ++ [ ++ pytest.param(False, "updated-slots", (True, True), id="updated slots"), ++ pytest.param(True, "updated-slots", (False, False), id="updated slots with error"), ++ pytest.param(False, "updated-artifacts", (True, False), id="no updated artifacts"), ++ pytest.param(True, "failed-update", (False, True), id="bad config"), ++ ], ++) ++@needs_nbd ++def test_poll_reboot_criteria(create_system_files, system, http_server, tmp_path, remove_appfs, criteria, result): ++ """Test if the info command sends custom headers correctly.""" ++ reboot_flag = tmp_path / "reboot-flag" ++ assert not reboot_flag.exists() ++ ++ http_server.setup( ++ file_path="test/good-verity-bundle.raucb", ++ ) ++ ++ system.prepare_minimal_config() ++ system.config["handlers"] = { ++ "system-info": "bin/systeminfo.sh", ++ } ++ system.config["streaming"] = { ++ "send-headers": "system-version;transaction-id", ++ } ++ system.config["poll"] = { ++ "source": http_server.url, ++ "interval-sec": "60", ++ "install-criteria": "always", ++ "reboot-criteria": criteria, ++ "reboot-cmd": f"touch {reboot_flag}", ++ } ++ if remove_appfs: ++ del system.config["slot.appfs.0"] ++ del system.config["slot.appfs.1"] ++ system.write_config() ++ ++ env = {"RAUC_TEST_SYSTEM_VERSION": "2010.01-1"} ++ ++ with system.running_service("A", poll_speedup=10, extra_env=env): ++ slots_initial = get_native(system.proxy.GetSlotStatus()) ++ wait_one_poll(system) ++ status = get_native(system.proxy.Status) ++ slots_final = get_native(system.proxy.GetSlotStatus()) ++ time.sleep(5) ++ ++ assert status["manifest"]["update"]["version"] == "2011.03-2" ++ assert status["recent-error-count"] == 0 ++ assert status["attempt-count"] == 1 ++ ++ slots_initial = dict(slots_initial) ++ slots_final = dict(slots_final) ++ assert slots_initial["rootfs.1"].get("installed.count", 0) == 0 ++ if result[0]: # installation should have happened ++ assert slots_final["rootfs.1"].get("installed.count", 0) == 1 ++ else: ++ assert slots_initial == slots_final ++ ++ assert reboot_flag.exists() == result[1] +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0022-test-conftest-ruff-fixes.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0022-test-conftest-ruff-fixes.patch new file mode 100644 index 00000000..62533a0c --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0022-test-conftest-ruff-fixes.patch @@ -0,0 +1,50 @@ +From 228a8600b665caa63c1efcc17f484d7a8ec1f707 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Thu, 20 Mar 2025 17:06:45 +0100 +Subject: [PATCH 22/23] test/conftest: ruff fixes + +--- + test/conftest.py | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/test/conftest.py b/test/conftest.py +index 977c9a1d..bdff9e05 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -597,14 +597,14 @@ class System: + def start_dbus_monitor(self): + assert self.dbus_monitor is None + +- addr = os.environ["DBUS_SESSION_BUS_ADDRESS"].split(',')[0] ++ addr = os.environ["DBUS_SESSION_BUS_ADDRESS"].split(",")[0] + + self.dbus_monitor = subprocess.Popen( + ["busctl", "--json=short", f"--address={addr}", "monitor", "de.pengutronix.rauc"], + stdout=subprocess.PIPE, + ) + fcntl.fcntl(self.dbus_monitor.stdout, fcntl.F_SETFL, os.O_NONBLOCK) +- fcntl.fcntl(self.dbus_monitor.stdout, fcntl.F_SETPIPE_SZ, 10*1024*1024) ++ fcntl.fcntl(self.dbus_monitor.stdout, fcntl.F_SETPIPE_SZ, 10 * 1024 * 1024) + + def get_dbus_events(self): + lines = [] +@@ -613,12 +613,12 @@ class System: + if new_bytes is None: + break + # TODO wait until idle? +- #with open("buslog", "ab") as f: ++ # with open("buslog", "ab") as f: + # f.write(new_bytes) + # f.write(b"\nMARKER\n") +- #print(f"before: {self.dbus_rest} | {new_bytes}") +- [*new_lines, self.dbus_rest] = (self.dbus_rest + new_bytes).split(b'\n') +- #print(f"after: {new_lines} | {self.dbus_rest}") ++ # print(f"before: {self.dbus_rest} | {new_bytes}") ++ [*new_lines, self.dbus_rest] = (self.dbus_rest + new_bytes).split(b"\n") ++ # print(f"after: {new_lines} | {self.dbus_rest}") + lines += new_lines + events = [] + for line in lines: +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/0023-test-support-running-http-backend-without-qemu.patch b/meta-lxatac-bsp/recipes-core/rauc/files/0023-test-support-running-http-backend-without-qemu.patch new file mode 100644 index 00000000..cfd28214 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/0023-test-support-running-http-backend-without-qemu.patch @@ -0,0 +1,227 @@ +From 4b4093490fa7b39ddcb824a28ab26a2d7f66a2f8 Mon Sep 17 00:00:00 2001 +From: Jan Luebbe +Date: Fri, 21 Mar 2025 09:19:05 +0100 +Subject: [PATCH 23/23] test: support running http backend without qemu + +Signed-off-by: Jan Luebbe +--- + qemu-test-init | 2 +- + test/conftest.py | 54 +++++++++++++++++++++++++++++++++++-------- + test/nginx_backend.py | 31 ++++++++++++++++++------- + test/test_http.py | 6 ++--- + test/test_polling.py | 8 +++---- + 5 files changed, 76 insertions(+), 25 deletions(-) + +diff --git a/qemu-test-init b/qemu-test-init +index fea4cd6a..6c093c5e 100755 +--- a/qemu-test-init ++++ b/qemu-test-init +@@ -226,7 +226,7 @@ if type nginx; then + export RAUC_TEST_HTTP_SERVER=1 + fi + +-if python3 test/nginx_backend.py; then ++if python3 test/nginx_backend.py -s /tmp/backend.sock -d; then + export RAUC_TEST_HTTP_BACKEND=1 + fi + +diff --git a/test/conftest.py b/test/conftest.py +index bdff9e05..29baba27 100644 +--- a/test/conftest.py ++++ b/test/conftest.py +@@ -697,17 +697,51 @@ def system(tmp_path, dbus_session_bus): + + + class HTTPServer: +- BASE = "http://127.0.0.1/backend" +- + def __init__(self): +- self.url = f"{self.BASE}/get" ++ self.server = None ++ # in the qemu test environment, the server is already running ++ if "RAUC_TEST_HTTP_BACKEND" in os.environ: ++ self.base = "http://127.0.0.1/backend" ++ else: ++ self.base = "http://127.0.0.1:8080" ++ self.start() ++ self.url = f"{self.base}/get" ++ ++ def start(self): ++ if "RAUC_TEST_HTTP_BACKEND" in os.environ: ++ return ++ assert self.server is None ++ ++ self.server = subprocess.Popen(["python3", "nginx_backend.py"]) ++ timeout = time.monotonic() + 5.0 ++ while True: ++ time.sleep(0.1) ++ try: ++ resp = requests.get(f"{self.base}/") ++ resp.raise_for_status() ++ break ++ except requests.exceptions.ConnectionError: ++ if time.monotonic() > timeout: ++ raise ++ ++ def stop(self): ++ if "RAUC_TEST_HTTP_SERVER" in os.environ: ++ return ++ assert self.server is not None ++ ++ self.server.terminate() ++ try: ++ self.server.wait(timeout=10) ++ except subprocess.TimeoutExpired: ++ self.server.kill() ++ self.server.wait() + + def setup(self, *, file_path): + resp = requests.post( +- f"{self.BASE}/setup", ++ f"{self.base}/setup", + timeout=5, + json={ +- "file_path": file_path, ++ "file_path": os.path.abspath(file_path), + }, + ) + resp.raise_for_status() +@@ -721,11 +755,13 @@ class HTTPServer: + return requests.head(self.url, **kwargs) + + def get_summary(self): +- resp = requests.get(f"{self.BASE}/summary", timeout=15) ++ resp = requests.get(f"{self.base}/summary", timeout=15) + resp.raise_for_status() + return resp.json() + + +-@pytest.fixture +-def http_server(): +- return HTTPServer() ++@pytest.fixture(scope="session") ++def http_server(env_setup): ++ server = HTTPServer() ++ yield server ++ server.stop() +diff --git a/test/nginx_backend.py b/test/nginx_backend.py +index 688d1c43..bdc4944d 100755 +--- a/test/nginx_backend.py ++++ b/test/nginx_backend.py +@@ -1,5 +1,6 @@ + #!/usr/bin/python3 + ++import argparse + import json + import os + import socket +@@ -112,13 +113,27 @@ async def summary_handler(request): + return web.json_response(summary) + + +-s = open_socket("/tmp/backend.sock") ++if __name__ == "__main__": ++ parser = argparse.ArgumentParser(description="Run aiohttp server with optional socket and daemon mode.") ++ parser.add_argument("-s", "--socket", help="Path to Unix domain socket") ++ parser.add_argument("-d", "--daemon", action="store_true", help="Run as daemon") + +-daemonize() ++ args = parser.parse_args() + +-app = web.Application() +-app["rauc"] = { +- "sporadic_counter": -1, +-} +-app.add_routes(routes) +-web.run_app(app, sock=s) ++ app_args = {} ++ ++ if args.socket: ++ app_args["sock"] = open_socket(args.socket) ++ else: ++ app_args["host"] = "127.0.0.1" ++ app_args["port"] = 8080 ++ ++ if args.daemon: ++ daemonize() ++ ++ app = web.Application() ++ app["rauc"] = { ++ "sporadic_counter": -1, ++ } ++ app.add_routes(routes) ++ web.run_app(app, **app_args) +diff --git a/test/test_http.py b/test/test_http.py +index d043b066..aac35b53 100644 +--- a/test/test_http.py ++++ b/test/test_http.py +@@ -11,7 +11,7 @@ from helper import run + def test_backend_range(http_server): + """Test if the backend returns the range parameters correctly.""" + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + resp = http_server.get(headers={"Range": "bytes=0-3"}) +@@ -27,7 +27,7 @@ def test_backend_range(http_server): + def test_backend_headers(http_server): + """Test if the backend returns the request headers correctly.""" + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + resp = http_server.head(headers={"RAUC-Test": "value"}) +@@ -79,7 +79,7 @@ def test_info_headers(create_system_files, system, http_server, api): + } + system.write_config() + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + if api == "cli": +diff --git a/test/test_polling.py b/test/test_polling.py +index 72724095..d5c341e6 100644 +--- a/test/test_polling.py ++++ b/test/test_polling.py +@@ -18,7 +18,7 @@ def wait_one_poll(system, *, timeout=15.0): + def test_poll_only(create_system_files, system, http_server): + """Test if the info command sends custom headers correctly.""" + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + system.prepare_minimal_config() +@@ -77,7 +77,7 @@ def test_poll_only(create_system_files, system, http_server): + def test_poll_candidate_criteria(create_system_files, system, http_server, sys_ver, criteria, result): + """Test if the info command sends custom headers correctly.""" + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + system.prepare_minimal_config() +@@ -143,7 +143,7 @@ def test_poll_candidate_criteria(create_system_files, system, http_server, sys_v + def test_poll_install_criteria(create_system_files, system, http_server, sys_ver, criteria, result): + """Test if the info command sends custom headers correctly.""" + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + system.prepare_minimal_config() +@@ -199,7 +199,7 @@ def test_poll_reboot_criteria(create_system_files, system, http_server, tmp_path + assert not reboot_flag.exists() + + http_server.setup( +- file_path="test/good-verity-bundle.raucb", ++ file_path="good-verity-bundle.raucb", + ) + + system.prepare_minimal_config() +-- +2.39.5 + diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system-info-handler.sh b/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system-info-handler.sh new file mode 100644 index 00000000..b4bc7ac7 --- /dev/null +++ b/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system-info-handler.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# The device serial number (00009.12345) is read from EEPROM by the bootloader +# and passed to the kernel via the devicetree. +SERIAL_FILE=/sys/firmware/devicetree/base/chosen/baseboard-factory-data/serial-number + +# Strip the trailing null byte that is inherent to devicetree strings +SERIAL=$(tr -d '\000' < "${SERIAL_FILE}") + +echo "RAUC_SYSTEM_SERIAL=${SERIAL}" + +# The os-release contains a line with the os version (VERSION_ID=24.09-dev) +# grep it ... +VERSION_ID="$(grep "^VERSION_ID=" /etc/os-release)" + +# ... and remove the VERSION_ID= prefix. +VERSION_ID="${VERSION_ID#VERSION_ID=}" + +# The RAUC install hook has a better idea of the installed bundle version. +# Hence why it overrides the VERSION_ID here: +# + +echo "RAUC_SYSTEM_VERSION=${VERSION_ID}" diff --git a/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system.conf b/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system.conf index 9af94d70..37ed2f92 100644 --- a/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system.conf +++ b/meta-lxatac-bsp/recipes-core/rauc/files/lxatac/system.conf @@ -1,3 +1,10 @@ +# WARNING: this file is read by the tacd and used as a template to generate a +# version with additional update polling configuration. +# The original file is placed in `/usr/lib/rauc/system.conf`, while the runtime +# version is placed in `/run/rauc/system.conf`. +# The latter may not exist if polling is not enabled and thus no special +# runtime config is required. + [system] compatible=Linux Automation GmbH - LXA TAC bootloader=barebox @@ -5,7 +12,15 @@ bundle-formats=-plain data-directory=/srv/rauc [keyring] -directory=certificates-enabled +directory=/etc/rauc/certificates-enabled + +[handlers] +system-info=/usr/lib/rauc/system-info-handler.sh + +[streaming] +send-headers=boot-id;system-version;serial;transaction-id;uptime + +# [slot.rootfs.0] device=/dev/disk/by-partuuid/e82e6873-62cc-46fb-90f0-3e936743fa62 diff --git a/meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bbappend b/meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bb similarity index 63% rename from meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bbappend rename to meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bb index da83f234..6df439ef 100644 --- a/meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bbappend +++ b/meta-lxatac-bsp/recipes-core/rauc/rauc-conf.bb @@ -1,9 +1,21 @@ -FILESEXTRAPATHS:prepend := "${THISDIR}/files:" +SUMMARY = "RAUC system configuration & verification keyring" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420" + +RAUC_KEYRING_URI ??= "file://${RAUC_KEYRING_FILE}" + +RPROVIDES:${PN} += "virtual-rauc-conf" + +INHIBIT_DEFAULT_DEPS = "1" +do_compile[noexec] = "1" DEPENDS += "openssl-native" RDEPENDS:${PN} += "bash" -SRC_URI += " \ +SRC_URI = " \ + file://system.conf \ + file://system-info-handler.sh \ + ${RAUC_KEYRING_URI} \ file://rauc-disable-cert.sh \ file://rauc-enable-cert.sh \ file://devel.cert.pem \ @@ -11,13 +23,23 @@ SRC_URI += " \ file://testing.cert.pem \ " -do_install:append() { +do_install () { + # Install the distro-provided system.conf to /usr/lib/rauc + # so it can be overridden by a user-provided one (/etc/rauc) + # or one generated at runtime by the tacd (/run/rauc). + install -d ${D}${libdir}/rauc + install -m 0644 ${WORKDIR}/system.conf ${D}${libdir}/rauc/ + install -m 0755 ${WORKDIR}/system-info-handler.sh ${D}${libdir}/rauc/ + install -D -m 0755 ${WORKDIR}/rauc-disable-cert.sh \ ${D}${bindir}/rauc-disable-cert install -D -m 0755 ${WORKDIR}/rauc-enable-cert.sh \ ${D}${bindir}/rauc-enable-cert + # Install the available certificates to /etc, as they are + # subject to changes by the user. + install -d ${D}${sysconfdir}/rauc install -d ${D}${sysconfdir}/rauc/certificates-available install -d ${D}${sysconfdir}/rauc/certificates-enabled @@ -38,7 +60,7 @@ do_install:append() { # installed above is overwritten by this mv. # If RAUC_KEYRING_FILE is overridden the extra cert will be installed # along with the other ones. - mv ${D}${sysconfdir}/rauc/${KEYRING_FILE_NAME} \ + install -m 0644 ${WORKDIR}/${RAUC_KEYRING_FILE} \ ${D}${sysconfdir}/rauc/certificates-available/${KEYRING_FILE_NAME} # Due to the certificate enable/disable logic in the RAUC hook the @@ -50,3 +72,5 @@ do_install:append() { openssl rehash ${D}${sysconfdir}/rauc/certificates-enabled } + +FILES:${PN} += "${libdir}/rauc" diff --git a/meta-lxatac-bsp/recipes-core/rauc/rauc_%.bbappend b/meta-lxatac-bsp/recipes-core/rauc/rauc_%.bbappend index 87dae8a1..a731b367 100644 --- a/meta-lxatac-bsp/recipes-core/rauc/rauc_%.bbappend +++ b/meta-lxatac-bsp/recipes-core/rauc/rauc_%.bbappend @@ -4,8 +4,33 @@ RDEPENDS:${PN} += "e2fsprogs-resize2fs" SRC_URI += " \ file://require-mount-srv.conf \ + file://0001-refactor-RaucBundleAccessArgs-http_info_headers-from.patch \ + file://0002-src-install-refactor-assemble_info_headers-to-avoid-.patch \ + file://0003-move-assemble_info_headers-from-install.c-to-bundle..patch \ + file://0004-send-custom-headers-with-the-first-HTTP-streaming-re.patch \ + file://0005-test-test_http-also-test-HTTP-info-headers-when-usin.patch \ + file://0006-src-bundle-avoid-sending-headers-for-missing-values.patch \ + file://0007-support-sending-a-RAUC-System-Version-header.patch \ + file://0008-nbd-add-support-for-ETag-based-caching.patch \ + file://0009-qemu-test-rauc-config-set-TLS-CA-for-streaming.patch \ + file://0010-test-install-simplify-install_cleanup.patch \ + file://0011-utils-add-helper-to-get-a-string-list-from-a-GKeyFil.patch \ + file://0012-conftest-implement-D-Bus-message-monitoring.patch \ + file://0013-add-a-Poller-D-Bus-interface.patch \ + file://0014-test-conftest-allow-running-the-service-with-a-polli.patch \ + file://0015-context-implement-a-polling-speedup-factor-for-testi.patch \ + file://0016-install-remember-if-a-slot-or-artifact-was-updated.patch \ + file://0017-include-utils-ensure-that-r_subprocess_-args-are-ter.patch \ + file://0018-HACK-gdb-in-pytest-for-service.patch \ + file://0019-WIP-implement-polling-in-the-service.patch \ + file://0020-WIP-docs-document-polling.patch \ + file://0021-WIP-add-tests-for-polling.patch \ + file://0022-test-conftest-ruff-fixes.patch \ + file://0023-test-support-running-http-backend-without-qemu.patch \ " +SRCREV = "2c455f87b96400779b77a3f87ac3d655a4c80bcf" + do_install:append() { install -D -m 0644 ${WORKDIR}/require-mount-srv.conf \ ${D}${systemd_system_unitdir}/rauc.service.d/require-mount-srv.conf diff --git a/meta-lxatac-software/recipes-core/bundles/files/hook.sh b/meta-lxatac-software/recipes-core/bundles/files/hook.sh index baed95ee..96d0d871 100644 --- a/meta-lxatac-software/recipes-core/bundles/files/hook.sh +++ b/meta-lxatac-software/recipes-core/bundles/files/hook.sh @@ -62,6 +62,17 @@ function process_migrate_lists () { done } +function insert_bundle_version () { + # The running system will not have a good idea about its current RAUC + # bundle version, which it does however need to check for updates. + # Provide the version by extracting it from the current manifest file and + # placing an override in the system-info handler. + VERSION_ID=$(grep '^version=' "${RAUC_BUNDLE_MOUNT_POINT:?}/manifest.raucm" | cut -d'=' -f2) + + sed -i "s/^# /VERSION_ID=${VERSION_ID}/" \ + "${RAUC_SLOT_MOUNT_POINT}/usr/lib/rauc/system-info-handler.sh" +} + case "$1" in slot-post-install) enable_certificates @@ -91,6 +102,8 @@ case "$1" in # The files should contain one file per line that should be # migrated to the new slot. process_migrate_lists + + insert_bundle_version ;; *) exit 1 diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0001-tacd-dbus-rauc-re-introspect-services.patch b/meta-lxatac-software/recipes-rust/tacd/files/0001-tacd-dbus-rauc-re-introspect-services.patch new file mode 100644 index 00000000..ed73ba02 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0001-tacd-dbus-rauc-re-introspect-services.patch @@ -0,0 +1,82 @@ +From ac2e0c416223552d9a604bab7e33e514e1d518bd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Thu, 27 Mar 2025 08:59:42 +0100 +Subject: [PATCH 01/17] tacd: dbus: rauc: re-introspect services +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RAUC is in the process of adding native update polling support. +Re-introspect the dbus service to add the new APIs. + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 3 +++ + src/dbus/rauc/installer.rs | 5 +++++ + src/dbus/rauc/poller.rs | 24 ++++++++++++++++++++++++ + 3 files changed, 32 insertions(+) + create mode 100644 src/dbus/rauc/poller.rs + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 415f771..6d69084 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -43,6 +43,9 @@ mod installer; + #[cfg(not(feature = "demo_mode"))] + use installer::InstallerProxy; + ++#[cfg(not(feature = "demo_mode"))] ++mod poller; ++ + #[cfg(feature = "demo_mode")] + mod imports { + use std::collections::HashMap; +diff --git a/src/dbus/rauc/installer.rs b/src/dbus/rauc/installer.rs +index d8c1836..f030161 100644 +--- a/src/dbus/rauc/installer.rs ++++ b/src/dbus/rauc/installer.rs +@@ -10,6 +10,11 @@ use zbus::proxy; + default_path = "/" + )] + trait Installer { ++ /// GetArtifactStatus method ++ fn get_artifact_status( ++ &self, ++ ) -> zbus::Result>>; ++ + /// GetPrimary method + fn get_primary(&self) -> zbus::Result; + +diff --git a/src/dbus/rauc/poller.rs b/src/dbus/rauc/poller.rs +new file mode 100644 +index 0000000..70fdb3b +--- /dev/null ++++ b/src/dbus/rauc/poller.rs +@@ -0,0 +1,24 @@ ++//! This code was generated by `zbus-xmlgen` `4.1.0` from DBus introspection data. ++//! ++//! By running `zbus-xmlgen system de.pengutronix.rauc /` on the LXA TAC. ++ ++use zbus::proxy; ++ ++#[proxy( ++ interface = "de.pengutronix.rauc.Poller", ++ default_service = "de.pengutronix.rauc", ++ default_path = "/" ++)] ++trait Poller { ++ /// Poll method ++ fn poll(&self) -> zbus::Result<()>; ++ ++ /// NextPoll property ++ #[zbus(property)] ++ fn next_poll(&self) -> zbus::Result; ++ ++ /// Status property ++ #[zbus(property)] ++ fn status(&self) ++ -> zbus::Result>; ++} +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0002-dbus-systemd-also-monitor-rauc.service.patch b/meta-lxatac-software/recipes-rust/tacd/files/0002-dbus-systemd-also-monitor-rauc.service.patch new file mode 100644 index 00000000..b98847ed --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0002-dbus-systemd-also-monitor-rauc.service.patch @@ -0,0 +1,78 @@ +From 77aafa2af22bb812df0effec2d3a37f808af21ac Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 14:45:10 +0200 +Subject: [PATCH 02/17] dbus: systemd: also monitor rauc.service +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This exposes the status of the RAUC daemon to users inside and outside of +the tacd. As of now neither of them exist, but in the next few commits +support for re-starting the RAUC deamon to reload its configuration will +be added. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 2 ++ + src/dbus/systemd.rs | 5 +++++ + 2 files changed, 7 insertions(+) + +diff --git a/openapi.yaml b/openapi.yaml +index 7451efc..46589cc 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -697,6 +697,7 @@ paths: + - network-manager + - labgrid-exporter + - lxa-iobus ++ - rauc + put: + summary: Perform an action on a systemd service + tags: [System] +@@ -722,6 +723,7 @@ paths: + - network-manager + - labgrid-exporter + - lxa-iobus ++ - rauc + get: + summary: Get the status of a systemd service + tags: [System] +diff --git a/src/dbus/systemd.rs b/src/dbus/systemd.rs +index e06f2dd..ff78eb3 100644 +--- a/src/dbus/systemd.rs ++++ b/src/dbus/systemd.rs +@@ -66,6 +66,8 @@ pub struct Systemd { + pub labgrid: Service, + #[allow(dead_code)] + pub iobus: Service, ++ #[allow(dead_code)] ++ pub rauc: Service, + } + + impl ServiceStatus { +@@ -238,6 +240,7 @@ impl Systemd { + let networkmanager = Service::new(bb, "network-manager"); + let labgrid = Service::new(bb, "labgrid-exporter"); + let iobus = Service::new(bb, "lxa-iobus"); ++ let rauc = Service::new(bb, "rauc"); + + networkmanager + .connect(wtb, conn.clone(), "NetworkManager.service") +@@ -248,12 +251,14 @@ impl Systemd { + iobus + .connect(wtb, conn.clone(), "lxa-iobus.service") + .await?; ++ rauc.connect(wtb, conn.clone(), "rauc.service").await?; + + Ok(Self { + reboot, + networkmanager, + labgrid, + iobus, ++ rauc, + }) + } + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0003-dbus-rauc-add-Channels-type-to-use-instead-of-Vec-Ch.patch b/meta-lxatac-software/recipes-rust/tacd/files/0003-dbus-rauc-add-Channels-type-to-use-instead-of-Vec-Ch.patch new file mode 100644 index 00000000..115b0b4f --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0003-dbus-rauc-add-Channels-type-to-use-instead-of-Vec-Ch.patch @@ -0,0 +1,247 @@ +From a164327eb4853ecedce2aa9b11032ae687b3f11e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 09:59:19 +0200 +Subject: [PATCH 03/17] dbus: rauc: add Channels type to use instead of + Vec +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This allows us to add methods like "get the primary channel", +that operates on all update channels at once, to this `Channels` +type later on. + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 12 ++--- + src/dbus/rauc/update_channels.rs | 81 ++++++++++++++++++------------ + src/motd.rs | 1 + + src/ui/screens/diagnostics.rs | 2 +- + src/ui/screens/update_available.rs | 7 +-- + 5 files changed, 61 insertions(+), 42 deletions(-) + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 6d69084..db57da1 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -32,7 +32,7 @@ use crate::broker::{BrokerBuilder, Topic}; + use crate::watched_tasks::WatchedTasksBuilder; + + mod update_channels; +-pub use update_channels::Channel; ++pub use update_channels::{Channel, Channels}; + + #[cfg(feature = "demo_mode")] + mod demo_mode; +@@ -124,7 +124,7 @@ pub struct Rauc { + pub primary: Arc>, + pub last_error: Arc>, + pub install: Arc>, +- pub channels: Arc>>, ++ pub channels: Arc>, + pub reload: Arc>, + pub should_reboot: Arc>, + pub enable_polling: Arc>, +@@ -203,7 +203,7 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, + enable_polling: Arc>, +- channels: Arc>>, ++ channels: Arc>, + slot_status: Arc>>, + name: String, + ) { +@@ -213,7 +213,7 @@ async fn channel_polling_task( + + while let Some(mut channel) = channels + .try_get() +- .and_then(|chs| chs.into_iter().find(|ch| ch.name == name)) ++ .and_then(|chs| chs.into_vec().into_iter().find(|ch| ch.name == name)) + { + // Make sure update polling is enabled before doing anything, + // as contacting the update server requires user consent. +@@ -271,7 +271,7 @@ async fn channel_list_update_task( + conn: Arc, + mut reload_stream: Receiver, + enable_polling: Arc>, +- channels: Arc>>, ++ channels: Arc>, + slot_status: Arc>>, + ) -> Result<()> { + let mut previous: Option = None; +@@ -292,7 +292,7 @@ async fn channel_list_update_task( + } + + // Read the list of available update channels +- let new_channels = match Channel::from_directory(CHANNELS_DIR) { ++ let new_channels = match Channels::from_directory(CHANNELS_DIR) { + Ok(chs) => chs, + Err(e) => { + warn!("Failed to get list of update channels: {e}"); +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index 1a5fedd..bb40d51 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -54,6 +54,9 @@ pub struct Channel { + pub bundle: Option, + } + ++#[derive(Serialize, Deserialize, Clone, PartialEq)] ++pub struct Channels(Vec); ++ + #[derive(Deserialize)] + pub struct ChannelFile { + pub name: String, +@@ -140,38 +143,6 @@ impl Channel { + Ok(ch) + } + +- pub(super) fn from_directory(dir: &str) -> Result> { +- // Find all .yaml files in CHANNELS_DIR +- let mut dir_entries: Vec = read_dir(dir)? +- .filter_map(|dir_entry| dir_entry.ok()) +- .filter(|dir_entry| { +- dir_entry +- .file_name() +- .as_os_str() +- .as_bytes() +- .ends_with(b".yaml") +- }) +- .collect(); +- +- // And sort them alphabetically, so that 01_stable.yaml takes precedence over +- // 05_testing.yaml. +- dir_entries.sort_by_key(|dir_entry| dir_entry.file_name()); +- +- let mut channels: Vec = Vec::new(); +- +- for dir_entry in dir_entries { +- let channel = Self::from_file(&dir_entry.path())?; +- +- if channels.iter().any(|ch| ch.name == channel.name) { +- bail!("Encountered duplicate channel name \"{}\"", channel.name); +- } +- +- channels.push(channel); +- } +- +- Ok(channels) +- } +- + fn update_enabled(&mut self) { + // Which channels are enabled is decided based on which RAUC certificates are enabled. + let cert_file = self.name.clone() + ".cert.pem"; +@@ -206,6 +177,52 @@ impl Channel { + } + } + ++impl Channels { ++ pub(super) fn from_directory(dir: &str) -> Result { ++ // Find all .yaml files in CHANNELS_DIR ++ let mut dir_entries: Vec = read_dir(dir)? ++ .filter_map(|dir_entry| dir_entry.ok()) ++ .filter(|dir_entry| { ++ dir_entry ++ .file_name() ++ .as_os_str() ++ .as_bytes() ++ .ends_with(b".yaml") ++ }) ++ .collect(); ++ ++ // And sort them alphabetically, so that 01_stable.yaml takes precedence over ++ // 05_testing.yaml. ++ dir_entries.sort_by_key(|dir_entry| dir_entry.file_name()); ++ ++ let mut channels: Vec = Vec::new(); ++ ++ for dir_entry in dir_entries { ++ let channel = Channel::from_file(&dir_entry.path())?; ++ ++ if channels.iter().any(|ch| ch.name == channel.name) { ++ bail!("Encountered duplicate channel name \"{}\"", channel.name); ++ } ++ ++ channels.push(channel); ++ } ++ ++ Ok(Self(channels)) ++ } ++ ++ pub fn into_vec(self) -> Vec { ++ self.0 ++ } ++ ++ pub fn iter(&self) -> std::slice::Iter { ++ self.0.iter() ++ } ++ ++ pub fn iter_mut(&mut self) -> std::slice::IterMut { ++ self.0.iter_mut() ++ } ++} ++ + impl UpstreamBundle { + fn new(compatible: String, version: String, slot_status: Option<&SlotStatus>) -> Self { + let mut ub = Self { +diff --git a/src/motd.rs b/src/motd.rs +index c2990d3..7220229 100644 +--- a/src/motd.rs ++++ b/src/motd.rs +@@ -207,6 +207,7 @@ pub fn run( + }, + update = channels_events.recv().fuse() => { + motd.rauc_update_urls = update? ++ .into_vec() + .into_iter() + .filter_map(|ch| { + ch.bundle +diff --git a/src/ui/screens/diagnostics.rs b/src/ui/screens/diagnostics.rs +index 3fd7160..2ab8f63 100644 +--- a/src/ui/screens/diagnostics.rs ++++ b/src/ui/screens/diagnostics.rs +@@ -130,7 +130,7 @@ fn diagnostic_text(ui: &Ui) -> Result { + if let Some(channels) = ui.res.rauc.channels.try_get() { + write!(&mut text, "chs: ")?; + +- for ch in channels { ++ for ch in channels.into_vec() { + let en = if ch.enabled { "[x]" } else { "[ ]" }; + let name = ch.name; + +diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs +index 4d5e50f..887a55c 100644 +--- a/src/ui/screens/update_available.rs ++++ b/src/ui/screens/update_available.rs +@@ -30,7 +30,7 @@ use super::{ + InputEvent, Screen, Ui, + }; + use crate::broker::Topic; +-use crate::dbus::rauc::Channel; ++use crate::dbus::rauc::{Channel, Channels}; + use crate::watched_tasks::WatchedTasksBuilder; + + const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateAvailable; +@@ -73,8 +73,9 @@ impl Selection { + !self.channels.is_empty() + } + +- fn update_channels(&self, channels: Vec) -> Option { ++ fn update_channels(&self, channels: Channels) -> Option { + let channels: Vec = channels ++ .into_vec() + .into_iter() + .filter(|ch| { + ch.bundle +@@ -143,7 +144,7 @@ impl UpdateAvailableScreen { + pub fn new( + wtb: &mut WatchedTasksBuilder, + alerts: &Arc>, +- channels: &Arc>>, ++ channels: &Arc>, + ) -> Result { + let (mut channels_events, _) = channels.clone().subscribe_unbounded(); + let alerts = alerts.clone(); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0004-dbus-rauc-use-a-UpdateRequest-object-instead-of-a-si.patch b/meta-lxatac-software/recipes-rust/tacd/files/0004-dbus-rauc-use-a-UpdateRequest-object-instead-of-a-si.patch new file mode 100644 index 00000000..7ae572cc --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0004-dbus-rauc-use-a-UpdateRequest-object-instead-of-a-si.patch @@ -0,0 +1,169 @@ +From 0833495804d26c8d6a80ae6ebaecd2f828e28249 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 14:14:30 +0200 +Subject: [PATCH 04/17] dbus: rauc: use a UpdateRequest object instead of a + simple URL for install +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Right now the UpdateRequest object also only contains the URL, +just like before, but in the future we will at least add a +`manifest_hash` field that ensures that the user gets exactly +the update bundle they agreed to install. + +The change is backwards-compatible. Incoming requests to the +`/v1/tac/update/install` endpoint are first parsed as JSON object +(the new UpdateRequest object) and if that fails as simple string +(the old URL string) and then transformed transparently. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 12 +++++++--- + src/dbus/rauc.rs | 35 +++++++++++++++++++++++++++--- + src/ui/screens/update_available.rs | 14 ++++++++---- + 3 files changed, 51 insertions(+), 10 deletions(-) + +diff --git a/openapi.yaml b/openapi.yaml +index 46589cc..97c5cf2 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -827,12 +827,12 @@ paths: + content: + application/json: + schema: +- type: string ++ $ref: '#/components/schemas/UpdateRequest' + responses: + '204': +- description: The value was parsed as string and will be tried ++ description: The value was parsed successfully and will be tried + '400': +- description: The value could not be parsed as string ++ description: The value could not be parsed + + /v1/tac/update/channels: + get: +@@ -1102,6 +1102,12 @@ components: + nesting_depth: + type: number + ++ UpdateRequest: ++ type: object ++ properties: ++ url: ++ type: string ++ + UpdateChannels: + type: array + items: +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index db57da1..ebe5dfd 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -114,6 +114,30 @@ impl From<(i32, String, i32)> for Progress { + } + } + ++#[derive(Serialize, Deserialize, Clone)] ++#[serde(from = "UpdateRequestDe")] ++pub struct UpdateRequest { ++ pub url: Option, ++} ++ ++#[derive(Deserialize)] ++#[serde(untagged)] ++enum UpdateRequestDe { ++ UrlObject { url: Option }, ++ UrlOnly(String), ++} ++ ++impl From for UpdateRequest { ++ fn from(de: UpdateRequestDe) -> Self { ++ // Provide API backward compatibility by allowing either just a String ++ // as argument or a map with url and manifest hash inside. ++ match de { ++ UpdateRequestDe::UrlObject { url } => Self { url }, ++ UpdateRequestDe::UrlOnly(url) => Self { url: Some(url) }, ++ } ++ } ++} ++ + type SlotStatus = HashMap>; + + pub struct Rauc { +@@ -123,7 +147,7 @@ pub struct Rauc { + #[cfg_attr(feature = "demo_mode", allow(dead_code))] + pub primary: Arc>, + pub last_error: Arc>, +- pub install: Arc>, ++ pub install: Arc>, + pub channels: Arc>, + pub reload: Arc>, + pub should_reboot: Arc>, +@@ -336,7 +360,7 @@ impl Rauc { + slot_status: bb.topic_ro("/v1/tac/update/slots", None), + primary: bb.topic_ro("/v1/tac/update/primary", None), + last_error: bb.topic_ro("/v1/tac/update/last_error", None), +- install: bb.topic_wo("/v1/tac/update/install", Some("".to_string())), ++ install: bb.topic_wo("/v1/tac/update/install", None), + channels: bb.topic_ro("/v1/tac/update/channels", None), + reload: bb.topic_wo("/v1/tac/update/channels/reload", Some(true)), + should_reboot: bb.topic_ro("/v1/tac/update/should_reboot", Some(false)), +@@ -546,7 +570,12 @@ impl Rauc { + wtb.spawn_task("rauc-forward-install", async move { + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); + +- while let Some(url) = install_stream.next().await { ++ while let Some(update_request) = install_stream.next().await { ++ let url = match update_request.url { ++ Some(url) => url, ++ None => continue, ++ }; ++ + // Poor-mans validation. It feels wrong to let someone point to any + // file on the TAC from the web interface. + if url.starts_with("http://") || url.starts_with("https://") { +diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs +index 887a55c..6f125d0 100644 +--- a/src/ui/screens/update_available.rs ++++ b/src/ui/screens/update_available.rs +@@ -30,7 +30,7 @@ use super::{ + InputEvent, Screen, Ui, + }; + use crate::broker::Topic; +-use crate::dbus::rauc::{Channel, Channels}; ++use crate::dbus::rauc::{Channel, Channels, UpdateRequest}; + use crate::watched_tasks::WatchedTasksBuilder; + + const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateAvailable; +@@ -121,9 +121,15 @@ impl Selection { + } + } + +- fn perform(&self, alerts: &Arc>, install: &Arc>) { ++ fn perform(&self, alerts: &Arc>, install: &Arc>) { + match self.highlight { +- Highlight::Channel(ch) => install.set(self.channels[ch].url.clone()), ++ Highlight::Channel(ch) => { ++ let req = UpdateRequest { ++ url: Some(self.channels[ch].url.clone()), ++ }; ++ ++ install.set(req); ++ } + Highlight::Dismiss => alerts.deassert(SCREEN_TYPE), + } + } +@@ -136,7 +142,7 @@ pub struct UpdateAvailableScreen { + struct Active { + widgets: WidgetContainer, + alerts: Arc>, +- install: Arc>, ++ install: Arc>, + selection: Arc>, + } + +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0005-dbus-rauc-allow-restricting-installation-to-a-specif.patch b/meta-lxatac-software/recipes-rust/tacd/files/0005-dbus-rauc-allow-restricting-installation-to-a-specif.patch new file mode 100644 index 00000000..06615b45 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0005-dbus-rauc-allow-restricting-installation-to-a-specif.patch @@ -0,0 +1,101 @@ +From b1d8baa0f5a6a7e9660480709a04e5f681781f04 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 15:22:32 +0200 +Subject: [PATCH 05/17] dbus: rauc: allow restricting installation to a + specific manifest_hash +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This makes sure that an user gets exact the bundle they intended to +install, e.g. the bundle was not replaced by a newer one on the server +or otherwise tampered with. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 2 ++ + src/dbus/rauc.rs | 22 ++++++++++++++++++---- + src/ui/screens/update_available.rs | 1 + + 3 files changed, 21 insertions(+), 4 deletions(-) + +diff --git a/openapi.yaml b/openapi.yaml +index 97c5cf2..d7b6b0d 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -1107,6 +1107,8 @@ components: + properties: + url: + type: string ++ manifest_hash: ++ type: string + + UpdateChannels: + type: array +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index ebe5dfd..3f55291 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -117,13 +117,17 @@ impl From<(i32, String, i32)> for Progress { + #[derive(Serialize, Deserialize, Clone)] + #[serde(from = "UpdateRequestDe")] + pub struct UpdateRequest { ++ pub manifest_hash: Option, + pub url: Option, + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum UpdateRequestDe { +- UrlObject { url: Option }, ++ UrlAndHash { ++ manifest_hash: Option, ++ url: Option, ++ }, + UrlOnly(String), + } + +@@ -132,8 +136,11 @@ impl From for UpdateRequest { + // Provide API backward compatibility by allowing either just a String + // as argument or a map with url and manifest hash inside. + match de { +- UpdateRequestDe::UrlObject { url } => Self { url }, +- UpdateRequestDe::UrlOnly(url) => Self { url: Some(url) }, ++ UpdateRequestDe::UrlAndHash { manifest_hash, url } => Self { manifest_hash, url }, ++ UpdateRequestDe::UrlOnly(url) => Self { ++ manifest_hash: None, ++ url: Some(url), ++ }, + } + } + } +@@ -579,7 +586,14 @@ impl Rauc { + // Poor-mans validation. It feels wrong to let someone point to any + // file on the TAC from the web interface. + if url.starts_with("http://") || url.starts_with("https://") { +- let args = HashMap::new(); ++ let manifest_hash: Option = ++ update_request.manifest_hash.map(|mh| mh.into()); ++ ++ let mut args = HashMap::new(); ++ ++ if let Some(manifest_hash) = &manifest_hash { ++ args.insert("require-manifest-hash", manifest_hash); ++ } + + if let Err(e) = proxy.install_bundle(&url, args).await { + error!("Failed to install bundle: {}", e); +diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs +index 6f125d0..637da48 100644 +--- a/src/ui/screens/update_available.rs ++++ b/src/ui/screens/update_available.rs +@@ -126,6 +126,7 @@ impl Selection { + Highlight::Channel(ch) => { + let req = UpdateRequest { + url: Some(self.channels[ch].url.clone()), ++ manifest_hash: None, + }; + + install.set(req); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0006-dbus-rauc-remove-tacd-based-update-polling.patch b/meta-lxatac-software/recipes-rust/tacd/files/0006-dbus-rauc-remove-tacd-based-update-polling.patch new file mode 100644 index 00000000..2f0552d1 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0006-dbus-rauc-remove-tacd-based-update-polling.patch @@ -0,0 +1,458 @@ +From 46cb8753c96c4cd21165562ea16eb1dc87db1691 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 16:26:15 +0200 +Subject: [PATCH 06/17] dbus: rauc: remove tacd-based update polling +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RAUC is in the process of adding native polling support, which we want to +integrate into the tacd. + +To do the switch in a reviewable way first remove the tacd-based polling +and then add the native support in separate commits. + +Signed-off-by: Leonard Göhrs +--- + src/broker/topic.rs | 12 -- + src/dbus/rauc.rs | 189 +------------------------------ + src/dbus/rauc/update_channels.rs | 92 --------------- + 3 files changed, 3 insertions(+), 290 deletions(-) + +diff --git a/src/broker/topic.rs b/src/broker/topic.rs +index 3a0c2c9..ef32e98 100644 +--- a/src/broker/topic.rs ++++ b/src/broker/topic.rs +@@ -336,18 +336,6 @@ impl Topic { + + self.modify(|prev| if prev != msg { msg } else { None }); + } +- +- /// Wait until the topic is set to the specified value +- pub async fn wait_for(self: &Arc, val: E) { +- let (mut stream, sub) = self.clone().subscribe_unbounded(); +- +- // Unwrap here to keep the interface simple. The stream could only yield +- // None if the sender side is dropped, which will not happen as we hold +- // an Arc to self which contains the senders vec. +- while stream.next().await.unwrap() != val {} +- +- sub.unsubscribe() +- } + } + + impl> Topic { +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 3f55291..3e09281 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -15,15 +15,12 @@ + // with this program; if not, write to the Free Software Foundation, Inc., + // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-use std::cmp::Ordering; + use std::collections::HashMap; +-use std::time::{Duration, Instant}; + + use anyhow::Result; + use async_std::channel::Receiver; + use async_std::stream::StreamExt; + use async_std::sync::Arc; +-use async_std::task::{sleep, spawn, JoinHandle}; + use log::warn; + use serde::{Deserialize, Serialize}; + +@@ -48,38 +45,6 @@ mod poller; + + #[cfg(feature = "demo_mode")] + mod imports { +- use std::collections::HashMap; +- +- pub(super) struct InstallerProxy<'a> { +- _dummy: &'a (), +- } +- +- impl<'a> InstallerProxy<'a> { +- pub async fn new(_conn: C) -> Option> { +- Some(Self { _dummy: &() }) +- } +- +- pub async fn inspect_bundle( +- &self, +- _source: &str, +- _args: HashMap<&str, zbus::zvariant::Value<'_>>, +- ) -> zbus::Result> { +- let update: HashMap = [ +- ( +- "compatible".into(), +- "Linux Automation GmbH - LXA TAC".into(), +- ), +- ("version".into(), "24.04-20240415070800".into()), +- ] +- .into(); +- +- let info: HashMap = +- [("update".into(), update.into())].into(); +- +- Ok(info) +- } +- } +- + pub(super) const CHANNELS_DIR: &str = "demo_files/usr/share/tacd/update_channels"; + } + +@@ -91,10 +56,6 @@ mod imports { + pub(super) const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels"; + } + +-const RELOAD_RATE_LIMIT: Duration = Duration::from_secs(10 * 60); +-const RETRY_INTERVAL_MIN: Duration = Duration::from_secs(60); +-const RETRY_INTERVAL_MAX: Duration = Duration::from_secs(60 * 60); +- + use imports::*; + + #[derive(Serialize, Deserialize, Clone)] +@@ -158,20 +119,10 @@ pub struct Rauc { + pub channels: Arc>, + pub reload: Arc>, + pub should_reboot: Arc>, ++ #[allow(dead_code)] + pub enable_polling: Arc>, + } + +-fn compare_versions(v1: &str, v2: &str) -> Option { +- // Version strings look something like this: "4.0-0-20230428214619" +- // Use string sorting on the date part to determine which bundle is newer. +- let date_1 = v1.rsplit_once('-').map(|(_, d)| d); +- let date_2 = v2.rsplit_once('-').map(|(_, d)| d); +- +- // Return Sone if either version could not be split or a Some with the +- // ordering between the dates. +- date_1.zip(date_2).map(|(d1, d2)| d1.cmp(d2)) +-} +- + #[cfg(not(feature = "demo_mode"))] + fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option) -> Result { + let rootfs_0 = slot_status.get("rootfs_0"); +@@ -231,97 +182,15 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, +- enable_polling: Arc>, +- channels: Arc>, +- slot_status: Arc>>, +- name: String, +-) { +- let proxy = InstallerProxy::new(&conn).await.unwrap(); +- +- let mut retry_interval = RETRY_INTERVAL_MIN; +- +- while let Some(mut channel) = channels +- .try_get() +- .and_then(|chs| chs.into_vec().into_iter().find(|ch| ch.name == name)) +- { +- // Make sure update polling is enabled before doing anything, +- // as contacting the update server requires user consent. +- enable_polling.wait_for(true).await; +- +- let polling_interval = channel.polling_interval; +- let slot_status = slot_status.try_get(); +- +- if let Err(e) = channel.poll(&proxy, slot_status.as_deref()).await { +- warn!( +- "Failed to fetch update for update channel \"{}\": {}. Retrying in {}s.", +- channel.name, +- e, +- retry_interval.as_secs() +- ); +- +- if retry_interval < RETRY_INTERVAL_MAX { +- sleep(retry_interval).await; +- +- // Perform a (limited) exponential backoff on the retry interval to recover +- // fast from short-term issues while also preventing the update server from +- // being DDOSed by excessive retries. +- retry_interval *= 2; +- +- continue; +- } +- } +- +- retry_interval = RETRY_INTERVAL_MIN; +- +- channels.modify(|chs| { +- let mut chs = chs?; +- let channel_prev = chs.iter_mut().find(|ch| ch.name == name)?; +- +- // Check if the bundle we polled is the same as before and we don't need +- // to send a message to the subscribers. +- if *channel_prev == channel { +- return None; +- } +- +- // Update the channel description with the newly polled bundle info +- *channel_prev = channel; +- +- Some(chs) +- }); +- +- match polling_interval { +- Some(pi) => sleep(pi).await, +- None => break, +- } +- } +-} +- + async fn channel_list_update_task( +- conn: Arc, + mut reload_stream: Receiver, +- enable_polling: Arc>, + channels: Arc>, +- slot_status: Arc>>, + ) -> Result<()> { +- let mut previous: Option = None; +- let mut polling_tasks: Vec> = Vec::new(); +- + while let Some(reload) = reload_stream.next().await { + if !reload { + continue; + } + +- // Polling for updates is a somewhat expensive operation. +- // Make sure it can not be abused to DOS the tacd. +- if previous +- .map(|p| p.elapsed() < RELOAD_RATE_LIMIT) +- .unwrap_or(false) +- { +- continue; +- } +- + // Read the list of available update channels + let new_channels = match Channels::from_directory(CHANNELS_DIR) { + Ok(chs) => chs, +@@ -331,29 +200,7 @@ async fn channel_list_update_task( + } + }; + +- // Stop the currently running polling tasks +- for task in polling_tasks.drain(..) { +- task.cancel().await; +- } +- +- let names: Vec = new_channels.iter().map(|c| c.name.clone()).collect(); +- + channels.set(new_channels); +- +- // Spawn new polling tasks. They will poll once immediately. +- for name in names.into_iter() { +- let polling_task = spawn(channel_polling_task( +- conn.clone(), +- enable_polling.clone(), +- channels.clone(), +- slot_status.clone(), +- name, +- )); +- +- polling_tasks.push(polling_task); +- } +- +- previous = Some(Instant::now()); + } + + Ok(()) +@@ -398,13 +245,7 @@ impl Rauc { + let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + wtb.spawn_task( + "rauc-channel-list-update", +- channel_list_update_task( +- Arc::new(Connection), +- reload_stream, +- inst.enable_polling.clone(), +- inst.channels.clone(), +- inst.slot_status.clone(), +- ), ++ channel_list_update_task(reload_stream, inst.channels.clone()), + )?; + + Ok(inst) +@@ -422,7 +263,6 @@ impl Rauc { + let operation = inst.operation.clone(); + let slot_status = inst.slot_status.clone(); + let primary = inst.primary.clone(); +- let channels = inst.channels.clone(); + let should_reboot = inst.should_reboot.clone(); + + wtb.spawn_task("rauc-slot-status-update", async move { +@@ -485,23 +325,6 @@ impl Rauc { + }) + .collect(); + +- // Update the `newer_than_installed` field for the upstream bundles inside +- // of the update channels. +- channels.modify(|prev| { +- let prev = prev?; +- +- let mut new = prev.clone(); +- +- for ch in new.iter_mut() { +- if let Some(bundle) = ch.bundle.as_mut() { +- bundle.update_install(&slots); +- } +- } +- +- // Only send out messages if anything changed +- (new != prev).then_some(new) +- }); +- + // Provide a simple yes/no "should reboot into other slot?" information + // based on the bundle versions in the booted slot and the other slot. + match would_reboot_into_other_slot(&slots, new_primary) { +@@ -608,13 +431,7 @@ impl Rauc { + let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + wtb.spawn_task( + "rauc-channel-list-update", +- channel_list_update_task( +- conn.clone(), +- reload_stream, +- inst.enable_polling.clone(), +- inst.channels.clone(), +- inst.slot_status.clone(), +- ), ++ channel_list_update_task(reload_stream, inst.channels.clone()), + )?; + + Ok(inst) +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index bb40d51..fcdb693 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -15,7 +15,6 @@ + // with this program; if not, write to the Free Software Foundation, Inc., + // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-use std::collections::HashMap; + use std::fs::{read_dir, read_to_string, DirEntry}; + use std::os::unix::ffi::OsStrExt; + use std::path::Path; +@@ -24,8 +23,6 @@ use std::time::Duration; + use anyhow::{anyhow, bail, Result}; + use serde::{Deserialize, Serialize}; + +-use super::{compare_versions, InstallerProxy, SlotStatus}; +- + #[cfg(feature = "demo_mode")] + const ENABLE_DIR: &str = "demo_files/etc/rauc/certificates-enabled"; + +@@ -66,28 +63,6 @@ pub struct ChannelFile { + pub polling_interval: Option, + } + +-fn zvariant_walk_nested_dicts(map: &zvariant::Dict, path: &[&str]) -> Result { +- let (&key, rem) = path +- .split_first() +- .ok_or_else(|| anyhow!("Got an empty path to walk"))?; +- +- let value: &zvariant::Value = map +- .get(&key)? +- .ok_or_else(|| anyhow!("Could not find key \"{key}\" in dict"))?; +- +- if rem.is_empty() { +- value.downcast_ref().map_err(|e| { +- anyhow!("Failed to convert value in dictionary for key \"{key}\" to a string: {e}") +- }) +- } else { +- let value = value.downcast_ref().map_err(|e| { +- anyhow!("Failed to convert value in dictionary for key \"{key}\" to a dict: {e}") +- })?; +- +- zvariant_walk_nested_dicts(value, rem) +- } +-} +- + impl Channel { + fn from_file(path: &Path) -> Result { + let file_name = || { +@@ -150,31 +125,6 @@ impl Channel { + + self.enabled = cert_path.exists(); + } +- +- /// Ask RAUC to determine the version of the bundle on the server +- pub(super) async fn poll( +- &mut self, +- proxy: &InstallerProxy<'_>, +- slot_status: Option<&SlotStatus>, +- ) -> Result<()> { +- self.update_enabled(); +- +- self.bundle = None; +- +- if self.enabled { +- let args = HashMap::new(); +- let bundle = proxy.inspect_bundle(&self.url, args).await?; +- let bundle: zvariant::Dict = bundle.into(); +- +- let compatible = +- zvariant_walk_nested_dicts(&bundle, &["update", "compatible"])?.to_owned(); +- let version = zvariant_walk_nested_dicts(&bundle, &["update", "version"])?.to_owned(); +- +- self.bundle = Some(UpstreamBundle::new(compatible, version, slot_status)); +- } +- +- Ok(()) +- } + } + + impl Channels { +@@ -213,46 +163,4 @@ impl Channels { + pub fn into_vec(self) -> Vec { + self.0 + } +- +- pub fn iter(&self) -> std::slice::Iter { +- self.0.iter() +- } +- +- pub fn iter_mut(&mut self) -> std::slice::IterMut { +- self.0.iter_mut() +- } +-} +- +-impl UpstreamBundle { +- fn new(compatible: String, version: String, slot_status: Option<&SlotStatus>) -> Self { +- let mut ub = Self { +- compatible, +- version, +- newer_than_installed: false, +- }; +- +- if let Some(slot_status) = slot_status { +- ub.update_install(slot_status); +- } +- +- ub +- } +- +- pub(super) fn update_install(&mut self, slot_status: &SlotStatus) { +- let slot_0_is_older = slot_status +- .get("rootfs_0") +- .filter(|r| r.get("boot_status").is_some_and(|b| b == "good")) +- .and_then(|r| r.get("bundle_version")) +- .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) +- .unwrap_or(true); +- +- let slot_1_is_older = slot_status +- .get("rootfs_1") +- .filter(|r| r.get("boot_status").is_some_and(|b| b == "good")) +- .and_then(|r| r.get("bundle_version")) +- .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) +- .unwrap_or(true); +- +- self.newer_than_installed = slot_0_is_older && slot_1_is_older; +- } + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0007-dbus-rauc-update_channels-add-a-concept-of-a-single-.patch b/meta-lxatac-software/recipes-rust/tacd/files/0007-dbus-rauc-update_channels-add-a-concept-of-a-single-.patch new file mode 100644 index 00000000..107774a0 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0007-dbus-rauc-update_channels-add-a-concept-of-a-single-.patch @@ -0,0 +1,96 @@ +From 6a433336d8d13441f794b8349bd9f76d6559a532 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 14:31:58 +0200 +Subject: [PATCH 07/17] dbus: rauc: update_channels: add a concept of a single + primary channel +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RAUC native update polling only supports a single update channel, +while our native update polling did support multiple +(all channels which RAUC would have accepted updates from, +based on the enabled signing certificates, were polled for updates and the +user was asked if they wanted to install updates from them). + +Prepare for the change by adding a concept of a single primary update +channel. The primary channel is the first enabled one. Based on the +channel definition file name. + +E.g. on production TACs these channel files are available: + + root@lxatac-00011:~# ls /usr/share/tacd/update_channels/ + 01_stable.yaml 05_testing.yaml + +They are sorted by name when they are read from disk, so if both +`stable.cert.pem` and `testing.cert.pem` are found +in `/etc/rauc/certificates-enabled/`, then the stable channel will be +the primary channel, but bundles from the testing channel may still be +installed via the command line interface (e.g. to facilitate a channel +switch). + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 2 ++ + src/dbus/rauc/update_channels.rs | 12 +++++++++++- + 2 files changed, 13 insertions(+), 1 deletion(-) + +diff --git a/openapi.yaml b/openapi.yaml +index d7b6b0d..eab9af0 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -1132,6 +1132,8 @@ components: + type: integer + enabled: + type: boolean ++ primary: ++ type: boolean + bundle: + type: object + properties: +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index fcdb693..65cebba 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -48,6 +48,7 @@ pub struct Channel { + pub url: String, + pub polling_interval: Option, + pub enabled: bool, ++ pub primary: bool, + pub bundle: Option, + } + +@@ -110,6 +111,7 @@ impl Channel { + url: channel_file.url.trim().to_string(), + polling_interval, + enabled: false, ++ primary: false, + bundle: None, + }; + +@@ -147,13 +149,21 @@ impl Channels { + + let mut channels: Vec = Vec::new(); + ++ let mut have_primary = false; ++ + for dir_entry in dir_entries { +- let channel = Channel::from_file(&dir_entry.path())?; ++ let mut channel = Channel::from_file(&dir_entry.path())?; + + if channels.iter().any(|ch| ch.name == channel.name) { + bail!("Encountered duplicate channel name \"{}\"", channel.name); + } + ++ // There can only be one primary channel. ++ // If multiple channels are enabled the primary one is the one with ++ // the highest precedence. ++ channel.primary = channel.enabled && !have_primary; ++ have_primary |= channel.primary; ++ + channels.push(channel); + } + +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0008-dbus-rauc-only-install-bundles-from-the-primary-chan.patch b/meta-lxatac-software/recipes-rust/tacd/files/0008-dbus-rauc-only-install-bundles-from-the-primary-chan.patch new file mode 100644 index 00000000..67de2aa4 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0008-dbus-rauc-only-install-bundles-from-the-primary-chan.patch @@ -0,0 +1,108 @@ +From 42b959fb71b165058fd69a40ca31aaea7dadae4f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 10:36:00 +0200 +Subject: [PATCH 08/17] dbus: rauc: only install bundles from the primary + channel +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This restricts the sources that the `/v1/tac/update/install` will accept +update requests from to only the primary channel. +The web interface has not exposed the feature to install arbitrary URLs +for some time now and users that want to do so are better served by using +the command line interface instead. + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 47 ++++++++++++++++++++++---------- + src/dbus/rauc/update_channels.rs | 5 ++++ + 2 files changed, 37 insertions(+), 15 deletions(-) + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 3e09281..f3c281c 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -394,6 +394,7 @@ impl Rauc { + })?; + + let conn_task = conn.clone(); ++ let channels = inst.channels.clone(); + let (mut install_stream, _) = inst.install.clone().subscribe_unbounded(); + + // Forward the "install" topic from the broker framework to RAUC +@@ -401,26 +402,42 @@ impl Rauc { + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); + + while let Some(update_request) = install_stream.next().await { +- let url = match update_request.url { +- Some(url) => url, +- None => continue, ++ let channels = match channels.try_get() { ++ Some(chs) => chs, ++ None => { ++ warn!("Got install request with no channels available yet"); ++ continue; ++ } + }; + +- // Poor-mans validation. It feels wrong to let someone point to any +- // file on the TAC from the web interface. +- if url.starts_with("http://") || url.starts_with("https://") { +- let manifest_hash: Option = +- update_request.manifest_hash.map(|mh| mh.into()); +- +- let mut args = HashMap::new(); +- +- if let Some(manifest_hash) = &manifest_hash { +- args.insert("require-manifest-hash", manifest_hash); ++ let primary = match channels.primary() { ++ Some(primary) => primary, ++ None => { ++ warn!("Got install request with no primary channel configured"); ++ continue; + } ++ }; + +- if let Err(e) = proxy.install_bundle(&url, args).await { +- error!("Failed to install bundle: {}", e); ++ let url = match &update_request.url { ++ None => &primary.url, ++ Some(url) if url == &primary.url => &primary.url, ++ Some(_) => { ++ warn!("Got install request with URL not matching primary channel URL"); ++ continue; + } ++ }; ++ ++ let manifest_hash: Option = ++ update_request.manifest_hash.map(|mh| mh.into()); ++ ++ let mut args = HashMap::new(); ++ ++ if let Some(manifest_hash) = &manifest_hash { ++ args.insert("require-manifest-hash", manifest_hash); ++ } ++ ++ if let Err(e) = proxy.install_bundle(url, args).await { ++ error!("Failed to install bundle: {}", e); + } + } + +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index 65cebba..6b4c961 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -173,4 +173,9 @@ impl Channels { + pub fn into_vec(self) -> Vec { + self.0 + } ++ ++ #[cfg(not(feature = "demo_mode"))] ++ pub(super) fn primary(&self) -> Option<&Channel> { ++ self.0.iter().find(|ch| ch.primary) ++ } + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0009-dbus-rauc-system_conf-write-runtime-RAUC-config-with.patch b/meta-lxatac-software/recipes-rust/tacd/files/0009-dbus-rauc-system_conf-write-runtime-RAUC-config-with.patch new file mode 100644 index 00000000..af00dde4 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0009-dbus-rauc-system_conf-write-runtime-RAUC-config-with.patch @@ -0,0 +1,276 @@ +From 2b84818540ee844643e764f7be96061e6be33001 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 10:34:36 +0200 +Subject: [PATCH 09/17] dbus: rauc: system_conf: write runtime RAUC config with + poll section +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This configures RAUC to poll for updates on our behalf. +We do not use the information yet or enable automatic installation but +those are next steps. +We also need to trigger RAUC to re-read the file for this to be useful. +All of these features are added in follow-up commits. + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 49 +++++++++---- + src/dbus/rauc/system_conf.rs | 120 +++++++++++++++++++++++++++++++ + src/dbus/rauc/update_channels.rs | 1 - + 3 files changed, 155 insertions(+), 15 deletions(-) + create mode 100644 src/dbus/rauc/system_conf.rs + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index f3c281c..f32a34c 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -18,9 +18,9 @@ + use std::collections::HashMap; + + use anyhow::Result; +-use async_std::channel::Receiver; + use async_std::stream::StreamExt; + use async_std::sync::Arc; ++use futures_util::FutureExt; + use log::warn; + use serde::{Deserialize, Serialize}; + +@@ -31,6 +31,9 @@ use crate::watched_tasks::WatchedTasksBuilder; + mod update_channels; + pub use update_channels::{Channel, Channels}; + ++mod system_conf; ++use system_conf::update_system_conf; ++ + #[cfg(feature = "demo_mode")] + mod demo_mode; + +@@ -119,7 +122,6 @@ pub struct Rauc { + pub channels: Arc>, + pub reload: Arc>, + pub should_reboot: Arc>, +- #[allow(dead_code)] + pub enable_polling: Arc>, + } + +@@ -183,13 +185,26 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, ++ reload: Arc>, ++ enable_polling: Arc>, + channels: Arc>, + ) -> Result<()> { +- while let Some(reload) = reload_stream.next().await { +- if !reload { +- continue; +- } ++ let (reload_stream, _) = reload.subscribe_unbounded(); ++ let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); ++ ++ let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); ++ ++ 'reload_loop: loop { ++ futures::select! { ++ reload = reload_stream.recv().fuse() => { ++ if !(reload?) { ++ continue 'reload_loop ++ } ++ } ++ enable_polling_new = enable_polling_stream.recv().fuse() => { ++ enable_polling = enable_polling_new?; ++ } ++ }; + + // Read the list of available update channels + let new_channels = match Channels::from_directory(CHANNELS_DIR) { +@@ -200,10 +215,10 @@ async fn channel_list_update_task( + } + }; + ++ update_system_conf(new_channels.primary(), enable_polling)?; ++ + channels.set(new_channels); + } +- +- Ok(()) + } + + impl Rauc { +@@ -242,10 +257,13 @@ impl Rauc { + inst.last_error.set("".to_string()); + + // Reload the channel list on request +- let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + wtb.spawn_task( + "rauc-channel-list-update", +- channel_list_update_task(reload_stream, inst.channels.clone()), ++ channel_list_update_task( ++ inst.reload.clone(), ++ inst.enable_polling.clone(), ++ inst.channels.clone(), ++ ), + )?; + + Ok(inst) +@@ -444,11 +462,14 @@ impl Rauc { + Ok(()) + })?; + +- // Reload the channel list on request +- let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); ++ // Reload the channel list when required + wtb.spawn_task( + "rauc-channel-list-update", +- channel_list_update_task(reload_stream, inst.channels.clone()), ++ channel_list_update_task( ++ inst.reload.clone(), ++ inst.enable_polling.clone(), ++ inst.channels.clone(), ++ ), + )?; + + Ok(inst) +diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs +new file mode 100644 +index 0000000..627df25 +--- /dev/null ++++ b/src/dbus/rauc/system_conf.rs +@@ -0,0 +1,120 @@ ++use std::fmt::Write; ++use std::fs::{create_dir_all, read_to_string, remove_file, rename, write}; ++use std::io::{Error, ErrorKind}; ++use std::path::Path; ++ ++use super::Channel; ++ ++use log::info; ++ ++#[cfg(feature = "demo_mode")] ++mod imports { ++ pub(super) const STATIC_CONF_PATH: &str = "demo_files/usr/lib/rauc/system.conf"; ++ pub(super) const DYNAMIC_CONF_PATH: &str = "demo_files/run/rauc/system.conf"; ++} ++ ++#[cfg(not(feature = "demo_mode"))] ++mod imports { ++ pub(super) const STATIC_CONF_PATH: &str = "/usr/lib/rauc/system.conf"; ++ pub(super) const DYNAMIC_CONF_PATH: &str = "/run/rauc/system.conf"; ++} ++ ++use imports::*; ++ ++const MAGIC_LINE: &str = "\n# \n"; ++ ++fn poll_section( ++ primary_channel: Option<&Channel>, ++ polling: bool, ++) -> Result, std::fmt::Error> { ++ // If no primary channel is configured or if polling is not enabled, ++ // then we do not need a `[poll]` section at all. ++ let primary_channel = match (primary_channel, polling) { ++ (Some(pc), true) => pc, ++ _ => return Ok(None), ++ }; ++ ++ let mut section = String::new(); ++ ++ writeln!(&mut section)?; ++ writeln!(&mut section, "[poll]")?; ++ writeln!(&mut section, "source={}", primary_channel.url)?; ++ ++ if let Some(interval) = primary_channel.polling_interval { ++ writeln!(&mut section, "interval-sec={}", interval.as_secs())?; ++ } ++ ++ writeln!(&mut section, "candidate-criteria=different-version")?; ++ ++ Ok(Some(section)) ++} ++ ++pub fn update_system_conf( ++ primary_channel: Option<&Channel>, ++ enable_polling: bool, ++) -> std::io::Result { ++ let dynamic_conf = { ++ match poll_section(primary_channel, enable_polling) { ++ Ok(Some(ps)) => { ++ // We use the config in /etc as a template ... ++ let static_conf = read_to_string(STATIC_CONF_PATH)?; ++ ++ // ... and replace the line `# ` with our ++ // generated `[poll]` section. ++ let dc = static_conf.replacen(MAGIC_LINE, &ps, 1); ++ ++ // The user may have decided not to include a `# ` ++ // line. In that case we do not need a dynamic config at all. ++ if dc == static_conf { ++ info!( ++ "Rauc config {} did not contain magic line '{}'. Will not generate poll section.", ++ STATIC_CONF_PATH, MAGIC_LINE ++ ); ++ ++ None ++ } else { ++ Some(dc) ++ } ++ } ++ _ => None, ++ } ++ }; ++ ++ /* Do we need a dynamic config in /run/rauc? ++ * ++ * If so, then is it actually different from what we already have? ++ * If not, then there is no need to restart the daemon. ++ * If it is, we write the new one and signal the need for a daemon ++ * restart. ++ * ++ * If we do not need dynamic config, then try to delete the previous one. ++ * If there was none, then the daemon does not have to be restarted. ++ * If there was a dynamic config before, then we need to restart the ++ * daemon. ++ */ ++ match dynamic_conf { ++ Some(new) => match read_to_string(DYNAMIC_CONF_PATH) { ++ Ok(old) if old == new => Ok(false), ++ Err(err) if err.kind() != ErrorKind::NotFound => Err(err), ++ Ok(_) | Err(_) => { ++ let dynamic_conf_dir = Path::new(DYNAMIC_CONF_PATH) ++ .parent() ++ .ok_or_else(|| Error::other("Invalid dynamic config path"))?; ++ ++ let tmp_path = dynamic_conf_dir.join("system.conf.tacd-tmp"); ++ ++ create_dir_all(dynamic_conf_dir)?; ++ ++ write(&tmp_path, &new)?; ++ rename(&tmp_path, DYNAMIC_CONF_PATH)?; ++ ++ Ok(true) ++ } ++ }, ++ None => match remove_file(DYNAMIC_CONF_PATH) { ++ Ok(_) => Ok(true), ++ Err(err) if err.kind() == ErrorKind::NotFound => Ok(false), ++ Err(err) => Err(err), ++ }, ++ } ++} +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index 6b4c961..e5da5c1 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -174,7 +174,6 @@ impl Channels { + self.0 + } + +- #[cfg(not(feature = "demo_mode"))] + pub(super) fn primary(&self) -> Option<&Channel> { + self.0.iter().find(|ch| ch.primary) + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0010-dbus-rauc-reload-rauc-daemon-when-required.patch b/meta-lxatac-software/recipes-rust/tacd/files/0010-dbus-rauc-reload-rauc-daemon-when-required.patch new file mode 100644 index 00000000..ab3082ce --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0010-dbus-rauc-reload-rauc-daemon-when-required.patch @@ -0,0 +1,155 @@ +From 008b8839b5d7b6afd2b359f5c5d7186da8ce5e5b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 09:42:54 +0200 +Subject: [PATCH 10/17] dbus: rauc: reload rauc daemon when required +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This uses the existing systemd DBus API integration to trigger a daemon +restart (there is no support to just reload the config as of now) and +keep track of the following state changes. + +Signed-off-by: Leonard Göhrs +--- + src/dbus.rs | 6 ++++-- + src/dbus/rauc.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- + src/dbus/systemd.rs | 1 - + 3 files changed, 46 insertions(+), 5 deletions(-) + +diff --git a/src/dbus.rs b/src/dbus.rs +index aa8e333..326c02a 100644 +--- a/src/dbus.rs ++++ b/src/dbus.rs +@@ -89,11 +89,13 @@ impl DbusSession { + + let conn = Arc::new(tacd.serve(conn_builder).build().await?); + ++ let systemd = Systemd::new(bb, wtb, &conn).await?; ++ + Ok(Self { + hostname: Hostname::new(bb, wtb, &conn)?, + network: Network::new(bb, wtb, &conn, led_dut, led_uplink)?, +- rauc: Rauc::new(bb, wtb, &conn)?, +- systemd: Systemd::new(bb, wtb, &conn).await?, ++ rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone())?, ++ systemd, + }) + } + } +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index f32a34c..28e9b26 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -21,9 +21,10 @@ use anyhow::Result; + use async_std::stream::StreamExt; + use async_std::sync::Arc; + use futures_util::FutureExt; +-use log::warn; ++use log::{info, warn}; + use serde::{Deserialize, Serialize}; + ++use super::systemd::{Service, ServiceAction}; + use super::Connection; + use crate::broker::{BrokerBuilder, Topic}; + use crate::watched_tasks::WatchedTasksBuilder; +@@ -188,6 +189,7 @@ async fn channel_list_update_task( + reload: Arc>, + enable_polling: Arc>, + channels: Arc>, ++ rauc_service: Service, + ) -> Result<()> { + let (reload_stream, _) = reload.subscribe_unbounded(); + let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); +@@ -215,9 +217,43 @@ async fn channel_list_update_task( + } + }; + +- update_system_conf(new_channels.primary(), enable_polling)?; ++ let should_reload = update_system_conf(new_channels.primary(), enable_polling)?; + + channels.set(new_channels); ++ ++ if should_reload { ++ info!("New RAUC config written. Triggering daemon restart."); ++ ++ let (mut status, status_subscription) = ++ rauc_service.status.clone().subscribe_unbounded(); ++ rauc_service.action.set(ServiceAction::Restart); ++ ++ info!("Waiting for daemon to go down"); ++ ++ while let Some(ev) = status.next().await { ++ info!("Current status: {} ({})", ev.active_state, ev.sub_state); ++ ++ if ev.active_state != "active" { ++ break; ++ } ++ } ++ ++ info!("Waiting for daemon to come up again"); ++ ++ while let Some(ev) = status.next().await { ++ info!("Current status: {} ({})", ev.active_state, ev.sub_state); ++ ++ if ev.active_state == "active" { ++ break; ++ } ++ } ++ ++ info!("Done"); ++ ++ status_subscription.unsubscribe(); ++ } else { ++ info!("Config is up to date. Will not reload."); ++ } + } + } + +@@ -249,6 +285,7 @@ impl Rauc { + bb: &mut BrokerBuilder, + wtb: &mut WatchedTasksBuilder, + _conn: &Arc, ++ rauc_service: Service, + ) -> Result { + let inst = Self::setup_topics(bb); + +@@ -263,6 +300,7 @@ impl Rauc { + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), ++ rauc_service, + ), + )?; + +@@ -274,6 +312,7 @@ impl Rauc { + bb: &mut BrokerBuilder, + wtb: &mut WatchedTasksBuilder, + conn: &Arc, ++ rauc_service: Service, + ) -> Result { + let inst = Self::setup_topics(bb); + +@@ -469,6 +508,7 @@ impl Rauc { + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), ++ rauc_service, + ), + )?; + +diff --git a/src/dbus/systemd.rs b/src/dbus/systemd.rs +index ff78eb3..2f5f72e 100644 +--- a/src/dbus/systemd.rs ++++ b/src/dbus/systemd.rs +@@ -66,7 +66,6 @@ pub struct Systemd { + pub labgrid: Service, + #[allow(dead_code)] + pub iobus: Service, +- #[allow(dead_code)] + pub rauc: Service, + } + +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0011-dbus-rauc-trigger-a-single-poll-for-updates-after-re.patch b/meta-lxatac-software/recipes-rust/tacd/files/0011-dbus-rauc-trigger-a-single-poll-for-updates-after-re.patch new file mode 100644 index 00000000..23644901 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0011-dbus-rauc-trigger-a-single-poll-for-updates-after-re.patch @@ -0,0 +1,109 @@ +From 632685f71cc76c4fba4a5acf9fbf0ac25476212d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Thu, 3 Apr 2025 08:59:22 +0200 +Subject: [PATCH 11/17] dbus: rauc: trigger a single poll for updates after + reloading the daemon +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 33 +++++++++++++++++++++++++++++++-- + 1 file changed, 31 insertions(+), 2 deletions(-) + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 28e9b26..080a532 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -21,7 +21,7 @@ use anyhow::Result; + use async_std::stream::StreamExt; + use async_std::sync::Arc; + use futures_util::FutureExt; +-use log::{info, warn}; ++use log::{error, info, warn}; + use serde::{Deserialize, Serialize}; + + use super::systemd::{Service, ServiceAction}; +@@ -47,15 +47,31 @@ use installer::InstallerProxy; + #[cfg(not(feature = "demo_mode"))] + mod poller; + ++#[cfg(not(feature = "demo_mode"))] ++use poller::PollerProxy; ++ + #[cfg(feature = "demo_mode")] + mod imports { + pub(super) const CHANNELS_DIR: &str = "demo_files/usr/share/tacd/update_channels"; ++ ++ pub(super) struct PollerProxy<'a> { ++ _dummy: &'a (), ++ } ++ ++ impl PollerProxy<'_> { ++ pub async fn new(_conn: C) -> Option { ++ Some(Self { _dummy: &() }) ++ } ++ ++ pub async fn poll(&self) -> zbus::Result<()> { ++ Ok(()) ++ } ++ } + } + + #[cfg(not(feature = "demo_mode"))] + mod imports { + pub(super) use anyhow::bail; +- pub(super) use log::error; + + pub(super) const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels"; + } +@@ -186,11 +202,14 @@ fn would_reboot_into_other_slot(slot_status: &SlotStatus, primary: Option, + reload: Arc>, + enable_polling: Arc>, + channels: Arc>, + rauc_service: Service, + ) -> Result<()> { ++ let poller = PollerProxy::new(&conn).await.unwrap(); ++ + let (reload_stream, _) = reload.subscribe_unbounded(); + let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); + +@@ -254,6 +273,14 @@ async fn channel_list_update_task( + } else { + info!("Config is up to date. Will not reload."); + } ++ ++ if enable_polling { ++ info!("Trigger a poll"); ++ ++ if let Err(err) = poller.poll().await { ++ error!("Failed to poll for updates: {err}"); ++ } ++ } + } + } + +@@ -297,6 +324,7 @@ impl Rauc { + wtb.spawn_task( + "rauc-channel-list-update", + channel_list_update_task( ++ Arc::new(Connection), + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), +@@ -505,6 +533,7 @@ impl Rauc { + wtb.spawn_task( + "rauc-channel-list-update", + channel_list_update_task( ++ conn.clone(), + inst.reload.clone(), + inst.enable_polling.clone(), + inst.channels.clone(), +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0012-dbus-rauc-forward-poller-status-to-broker.patch b/meta-lxatac-software/recipes-rust/tacd/files/0012-dbus-rauc-forward-poller-status-to-broker.patch new file mode 100644 index 00000000..a65df3f0 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0012-dbus-rauc-forward-poller-status-to-broker.patch @@ -0,0 +1,175 @@ +From 0ab21a3c075fb0f1a9c2e97ebf78ea3c281d2866 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 10:32:28 +0200 +Subject: [PATCH 12/17] dbus: rauc: forward poller status to broker +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RAUC native polling provides us with information about the recent poll +attempts. This includes information about the bundle version and wether +it is an update over what is currently running on the device. + +In other words: it gives us everything we need to show update +notifications again. Forward this information to the same places we used +with the tacd-based update polling. + +Signed-off-by: Leonard Göhrs +--- + src/dbus/rauc.rs | 50 ++++++++++++++++++++++++ + src/dbus/rauc/update_channels.rs | 66 ++++++++++++++++++++++++++++++++ + 2 files changed, 116 insertions(+) + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 080a532..8c66c3f 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -478,6 +478,56 @@ impl Rauc { + Ok(()) + })?; + ++ let conn_task = conn.clone(); ++ let channels = inst.channels.clone(); ++ ++ // Forward the "Poller::status" property to the broker framework ++ wtb.spawn_task("rauc-forward-poller-status", async move { ++ let proxy = PollerProxy::new(&conn_task).await.unwrap(); ++ ++ let mut stream = proxy.receive_status_changed().await; ++ ++ if let Ok(status) = proxy.status().await { ++ channels.modify(|chs| { ++ let mut chs = chs?; ++ ++ match chs.update_from_poll_status(status.into()) { ++ Ok(true) => Some(chs), ++ Ok(false) => None, ++ Err(e) => { ++ warn!("Could not update channel list from poll status: {e}"); ++ None ++ } ++ } ++ }); ++ } ++ ++ while let Some(status) = stream.next().await { ++ let status = match status.get().await { ++ Ok(status) => status, ++ Err(e) => { ++ warn!("Could not get poll status: {e}"); ++ continue; ++ } ++ }; ++ ++ channels.modify(|chs| { ++ let mut chs = chs?; ++ ++ match chs.update_from_poll_status(status.into()) { ++ Ok(true) => Some(chs), ++ Ok(false) => None, ++ Err(e) => { ++ warn!("Could not update channel list from poll status: {e}"); ++ None ++ } ++ } ++ }); ++ } ++ ++ Ok(()) ++ })?; ++ + let conn_task = conn.clone(); + let channels = inst.channels.clone(); + let (mut install_stream, _) = inst.install.clone().subscribe_unbounded(); +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index e5da5c1..460fc36 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -15,6 +15,8 @@ + // with this program; if not, write to the Free Software Foundation, Inc., + // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + ++#[cfg(not(feature = "demo_mode"))] ++use std::convert::TryFrom; + use std::fs::{read_dir, read_to_string, DirEntry}; + use std::os::unix::ffi::OsStrExt; + use std::path::Path; +@@ -64,6 +66,34 @@ pub struct ChannelFile { + pub polling_interval: Option, + } + ++#[cfg(not(feature = "demo_mode"))] ++fn zvariant_walk_nested_dicts<'a, T>(map: &'a zvariant::Dict, path: &'a [&'a str]) -> Result<&'a T> ++where ++ &'a T: TryFrom<&'a zvariant::Value<'a>>, ++ <&'a T as TryFrom<&'a zvariant::Value<'a>>>::Error: Into, ++{ ++ let (key, rem) = path ++ .split_first() ++ .ok_or_else(|| anyhow!("Got an empty path to walk"))?; ++ ++ let value: &zvariant::Value = map ++ .get(key)? ++ .ok_or_else(|| anyhow!("Could not find key \"{key}\" in dict"))?; ++ ++ if rem.is_empty() { ++ value.downcast_ref().map_err(|e| { ++ let type_name = std::any::type_name::(); ++ anyhow!("Failed to convert value in dictionary for key \"{key}\" to {type_name}: {e}") ++ }) ++ } else { ++ let value = value.downcast_ref().map_err(|e| { ++ anyhow!("Failed to convert value in dictionary for key \"{key}\" to a dict: {e}") ++ })?; ++ ++ zvariant_walk_nested_dicts(value, rem) ++ } ++} ++ + impl Channel { + fn from_file(path: &Path) -> Result { + let file_name = || { +@@ -177,4 +207,40 @@ impl Channels { + pub(super) fn primary(&self) -> Option<&Channel> { + self.0.iter().find(|ch| ch.primary) + } ++ ++ #[cfg(not(feature = "demo_mode"))] ++ fn primary_mut(&mut self) -> Option<&mut Channel> { ++ self.0.iter_mut().find(|ch| ch.primary) ++ } ++ ++ #[cfg(not(feature = "demo_mode"))] ++ pub(super) fn update_from_poll_status(&mut self, poll_status: zvariant::Dict) -> Result { ++ let compatible: &zvariant::Str = ++ zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "compatible"])?; ++ let version: &zvariant::Str = ++ zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "version"])?; ++ let newer_than_installed: &bool = ++ zvariant_walk_nested_dicts(&poll_status, &["update-available"])?; ++ ++ if let Some(pb) = self.0.iter().find_map(|ch| ch.bundle.as_ref()) { ++ if compatible == pb.compatible.as_str() ++ && version == pb.version.as_str() ++ && *newer_than_installed == pb.newer_than_installed ++ { ++ return Ok(false); ++ } ++ } ++ ++ self.0.iter_mut().for_each(|ch| ch.bundle = None); ++ ++ if let Some(primary) = self.primary_mut() { ++ primary.bundle = Some(UpstreamBundle { ++ compatible: compatible.as_str().into(), ++ version: version.as_str().into(), ++ newer_than_installed: *newer_than_installed, ++ }); ++ } ++ ++ Ok(true) ++ } + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0013-dbus-rauc-add-manifest_hash-und-effective_url-to-Ups.patch b/meta-lxatac-software/recipes-rust/tacd/files/0013-dbus-rauc-add-manifest_hash-und-effective_url-to-Ups.patch new file mode 100644 index 00000000..19bb993e --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0013-dbus-rauc-add-manifest_hash-und-effective_url-to-Ups.patch @@ -0,0 +1,163 @@ +From 911e4895dff86b2889dded82f039aaca55eb038c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 14:35:33 +0200 +Subject: [PATCH 13/17] dbus: rauc: add manifest_hash und effective_url to + UpstreamBundle +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The RAUC native polling interface provides more information than +just the basic `compatible` and `version` fields. +Among these extra informations are the following: + + - `manifest_hash` + + By using the `manifest_hash` in the `InstallBundle` call we can + (cryptographically) ensure that the exact bundle (content) that the + user agreed to install is actually being installed and that no switch + has happened in between. + + - `effective_url` + + This is the bundle URL after all HTTP redirects have been followed. + This is e.g. relevant when a "clever" update server is used that + redirects poll requests to specific bundles to e.g. implement staged + rollouts or prevents updates from incompatible bundle versions. + + By using this one can ensure that the bundle URL used matches + the `manifest_hash` provided and that the redirects did not change + (e.g. because the next step in a staged update was reached) + between the last poll and the user accepting an update. + +The update dialog on the LCD is updated to use this mechanism now, +while the web interface will be updated later. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 4 ++++ + src/dbus/rauc.rs | 13 +++++++++++-- + src/dbus/rauc/update_channels.rs | 10 ++++++++++ + src/ui/screens/update_available.rs | 15 ++++++++++----- + 4 files changed, 35 insertions(+), 7 deletions(-) + +diff --git a/openapi.yaml b/openapi.yaml +index eab9af0..da1e47b 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -1141,6 +1141,10 @@ components: + type: string + version: + type: string, ++ manifest_hash: ++ type: string, ++ effective_url: ++ type: string, + newer_than_installed: + type: boolean + +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 8c66c3f..9219043 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -553,11 +553,20 @@ impl Rauc { + } + }; + ++ let upstream_bundle = match primary.bundle.as_ref() { ++ Some(us) => us, ++ None => { ++ warn!("Got install request with no upstream bundle info available yet"); ++ continue ++ } ++ }; ++ + let url = match &update_request.url { +- None => &primary.url, ++ None => &upstream_bundle.effective_url, ++ Some(url) if url == &upstream_bundle.effective_url => &upstream_bundle.effective_url, + Some(url) if url == &primary.url => &primary.url, + Some(_) => { +- warn!("Got install request with URL not matching primary channel URL"); ++ warn!("Got install request with URL matching neither channel URL nor effective bundle URL"); + continue; + } + }; +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index 460fc36..9790a3c 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -39,6 +39,8 @@ const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60); + pub struct UpstreamBundle { + pub compatible: String, + pub version: String, ++ pub manifest_hash: String, ++ pub effective_url: String, + pub newer_than_installed: bool, + } + +@@ -219,12 +221,18 @@ impl Channels { + zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "compatible"])?; + let version: &zvariant::Str = + zvariant_walk_nested_dicts(&poll_status, &["manifest", "update", "version"])?; ++ let manifest_hash: &zvariant::Str = ++ zvariant_walk_nested_dicts(&poll_status, &["manifest", "manifest-hash"])?; ++ let effective_url: &zvariant::Str = ++ zvariant_walk_nested_dicts(&poll_status, &["bundle", "effective-url"])?; + let newer_than_installed: &bool = + zvariant_walk_nested_dicts(&poll_status, &["update-available"])?; + + if let Some(pb) = self.0.iter().find_map(|ch| ch.bundle.as_ref()) { + if compatible == pb.compatible.as_str() + && version == pb.version.as_str() ++ && manifest_hash == pb.manifest_hash.as_str() ++ && effective_url == pb.effective_url.as_str() + && *newer_than_installed == pb.newer_than_installed + { + return Ok(false); +@@ -237,6 +245,8 @@ impl Channels { + primary.bundle = Some(UpstreamBundle { + compatible: compatible.as_str().into(), + version: version.as_str().into(), ++ manifest_hash: manifest_hash.as_str().into(), ++ effective_url: effective_url.as_str().into(), + newer_than_installed: *newer_than_installed, + }); + } +diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs +index 637da48..dddd34e 100644 +--- a/src/ui/screens/update_available.rs ++++ b/src/ui/screens/update_available.rs +@@ -22,6 +22,7 @@ use async_trait::async_trait; + use embedded_graphics::{ + mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, + }; ++use log::error; + use serde::{Deserialize, Serialize}; + + use super::widgets::*; +@@ -124,12 +125,16 @@ impl Selection { + fn perform(&self, alerts: &Arc>, install: &Arc>) { + match self.highlight { + Highlight::Channel(ch) => { +- let req = UpdateRequest { +- url: Some(self.channels[ch].url.clone()), +- manifest_hash: None, +- }; ++ if let Some(bundle) = &self.channels[ch].bundle { ++ let req = UpdateRequest { ++ url: Some(bundle.effective_url.clone()), ++ manifest_hash: Some(bundle.manifest_hash.clone()), ++ }; + +- install.set(req); ++ install.set(req); ++ } else { ++ error!("Update channel is missing upstream bundle information."); ++ }; + } + Highlight::Dismiss => alerts.deassert(SCREEN_TYPE), + } +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0014-dbus-rauc-add-support-for-enabling-the-auto-install-.patch b/meta-lxatac-software/recipes-rust/tacd/files/0014-dbus-rauc-add-support-for-enabling-the-auto-install-.patch new file mode 100644 index 00000000..f596660b --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0014-dbus-rauc-add-support-for-enabling-the-auto-install-.patch @@ -0,0 +1,178 @@ +From f6d6b462acb2b9df2a54bc22d2cc159567e6a052 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 09:52:55 +0200 +Subject: [PATCH 14/17] dbus: rauc: add support for enabling the auto install + feature +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Automatic installation and boot of updates can be useful when managing a +fleet of devices. This is however a feature that requires strict user +consent, hence why it is off by default. + +Add backend-support for enabling this feature. Frontent support in the +web interface will be added later. + +We always enable auto-reboot together with auto-install, +since the migration scripts only run once at the end of the installation. +A system that is updated, but not rebooted, would thus accumulate changes +that are not migrated to the other slot. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 15 +++++++++++++++ + src/dbus/rauc.rs | 20 +++++++++++++++++++- + src/dbus/rauc/system_conf.rs | 13 ++++++++++++- + 3 files changed, 46 insertions(+), 2 deletions(-) + +diff --git a/openapi.yaml b/openapi.yaml +index da1e47b..d8f247e 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -775,6 +775,21 @@ paths: + '400': + description: The value could not be parsed as boolean + ++ /v1/tac/update/enable_auto_install: ++ put: ++ summary: Enable automatic installation of operating system updates ++ tags: [Updating] ++ requestBody: ++ content: ++ application/json: ++ schema: ++ type: boolean ++ responses: ++ '204': ++ description: Automatic installation of updates was enabled/disabled ++ '400': ++ description: The value could not be parsed as boolean ++ + /v1/tac/update/operation: + get: + summary: Get the currently running system update operation +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index 9219043..c8ca2ab 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -140,6 +140,7 @@ pub struct Rauc { + pub reload: Arc>, + pub should_reboot: Arc>, + pub enable_polling: Arc>, ++ pub enable_auto_install: Arc>, + } + + #[cfg(not(feature = "demo_mode"))] +@@ -205,6 +206,7 @@ async fn channel_list_update_task( + conn: Arc, + reload: Arc>, + enable_polling: Arc>, ++ enable_auto_install: Arc>, + channels: Arc>, + rauc_service: Service, + ) -> Result<()> { +@@ -212,8 +214,10 @@ async fn channel_list_update_task( + + let (reload_stream, _) = reload.subscribe_unbounded(); + let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); ++ let (mut enable_auto_install_stream, _) = enable_auto_install.subscribe_unbounded(); + + let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); ++ let mut enable_auto_install = enable_auto_install_stream.next().await.unwrap_or(false); + + 'reload_loop: loop { + futures::select! { +@@ -225,6 +229,9 @@ async fn channel_list_update_task( + enable_polling_new = enable_polling_stream.recv().fuse() => { + enable_polling = enable_polling_new?; + } ++ enable_auto_install_new = enable_auto_install_stream.recv().fuse() => { ++ enable_auto_install = enable_auto_install_new?; ++ } + }; + + // Read the list of available update channels +@@ -236,7 +243,8 @@ async fn channel_list_update_task( + } + }; + +- let should_reload = update_system_conf(new_channels.primary(), enable_polling)?; ++ let should_reload = ++ update_system_conf(new_channels.primary(), enable_polling, enable_auto_install)?; + + channels.set(new_channels); + +@@ -304,6 +312,14 @@ impl Rauc { + Some(false), + 1, + ), ++ enable_auto_install: bb.topic( ++ "/v1/tac/update/enable_auto_install", ++ true, ++ true, ++ true, ++ Some(false), ++ 1, ++ ), + } + } + +@@ -327,6 +343,7 @@ impl Rauc { + Arc::new(Connection), + inst.reload.clone(), + inst.enable_polling.clone(), ++ inst.enable_auto_install.clone(), + inst.channels.clone(), + rauc_service, + ), +@@ -595,6 +612,7 @@ impl Rauc { + conn.clone(), + inst.reload.clone(), + inst.enable_polling.clone(), ++ inst.enable_auto_install.clone(), + inst.channels.clone(), + rauc_service, + ), +diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs +index 627df25..91f62ac 100644 +--- a/src/dbus/rauc/system_conf.rs ++++ b/src/dbus/rauc/system_conf.rs +@@ -26,6 +26,7 @@ const MAGIC_LINE: &str = "\n# \n"; + fn poll_section( + primary_channel: Option<&Channel>, + polling: bool, ++ auto_install: bool, + ) -> Result, std::fmt::Error> { + // If no primary channel is configured or if polling is not enabled, + // then we do not need a `[poll]` section at all. +@@ -46,15 +47,25 @@ fn poll_section( + + writeln!(&mut section, "candidate-criteria=different-version")?; + ++ if auto_install { ++ writeln!(&mut section, "install-criteria=different-version")?; ++ writeln!( ++ &mut section, ++ "reboot-criteria=updated-slots;updated-artifacts" ++ )?; ++ writeln!(&mut section, "reboot-cmd=systemctl reboot")?; ++ } ++ + Ok(Some(section)) + } + + pub fn update_system_conf( + primary_channel: Option<&Channel>, + enable_polling: bool, ++ enable_auto_install: bool, + ) -> std::io::Result { + let dynamic_conf = { +- match poll_section(primary_channel, enable_polling) { ++ match poll_section(primary_channel, enable_polling, enable_auto_install) { + Ok(Some(ps)) => { + // We use the config in /etc as a template ... + let static_conf = read_to_string(STATIC_CONF_PATH)?; +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0015-dbus-rauc-implement-forced-polling-via-update-channe.patch b/meta-lxatac-software/recipes-rust/tacd/files/0015-dbus-rauc-implement-forced-polling-via-update-channe.patch new file mode 100644 index 00000000..0a08d017 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0015-dbus-rauc-implement-forced-polling-via-update-channe.patch @@ -0,0 +1,100 @@ +From 79f970f23e792eb57ec2bd0d29336e86b74dff7c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Mon, 31 Mar 2025 15:17:59 +0200 +Subject: [PATCH 15/17] dbus: rauc: implement forced polling via update channel + files +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Users managing a fleet of devices with custom-built update bundles and +update channel may want to automatically enable update polling and +automatic installation of updates without having to do so explicitly via +the web ui. (At least we at Pengutronix do). + +Enable this usecase by adding optional `force_polling` and +`force_auto_install` config options to the update channel definition +files. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 4 ++++ + src/dbus/rauc/system_conf.rs | 15 ++++++++++++++- + src/dbus/rauc/update_channels.rs | 6 ++++++ + 3 files changed, 24 insertions(+), 1 deletion(-) + +diff --git a/openapi.yaml b/openapi.yaml +index d8f247e..c5efb2b 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -1162,6 +1162,10 @@ components: + type: string, + newer_than_installed: + type: boolean ++ force_polling: ++ type: boolean ++ force_auto_install: ++ type: boolean + + ServiceStatus: + type: object +diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs +index 91f62ac..e387cb7 100644 +--- a/src/dbus/rauc/system_conf.rs ++++ b/src/dbus/rauc/system_conf.rs +@@ -65,7 +65,20 @@ pub fn update_system_conf( + enable_auto_install: bool, + ) -> std::io::Result { + let dynamic_conf = { +- match poll_section(primary_channel, enable_polling, enable_auto_install) { ++ // Allow force-enabling update polling and automatic installations ++ // via the update channel config file to implement company wide ++ // auto-update policies. ++ let force_polling = primary_channel ++ .and_then(|pc| pc.force_polling) ++ .unwrap_or(false); ++ let force_auto_install = primary_channel ++ .and_then(|pc| pc.force_auto_install) ++ .unwrap_or(false); ++ ++ let polling = enable_polling || force_polling; ++ let auto_install = enable_auto_install || force_auto_install; ++ ++ match poll_section(primary_channel, polling, auto_install) { + Ok(Some(ps)) => { + // We use the config in /etc as a template ... + let static_conf = read_to_string(STATIC_CONF_PATH)?; +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index 9790a3c..c067d2b 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -54,6 +54,8 @@ pub struct Channel { + pub enabled: bool, + pub primary: bool, + pub bundle: Option, ++ pub force_polling: Option, ++ pub force_auto_install: Option, + } + + #[derive(Serialize, Deserialize, Clone, PartialEq)] +@@ -66,6 +68,8 @@ pub struct ChannelFile { + pub description: String, + pub url: String, + pub polling_interval: Option, ++ pub force_polling: Option, ++ pub force_auto_install: Option, + } + + #[cfg(not(feature = "demo_mode"))] +@@ -145,6 +149,8 @@ impl Channel { + enabled: false, + primary: false, + bundle: None, ++ force_polling: channel_file.force_polling, ++ force_auto_install: channel_file.force_auto_install, + }; + + ch.update_enabled(); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0016-dbus-rauc-allow-configuring-the-_criteria-in-channel.patch b/meta-lxatac-software/recipes-rust/tacd/files/0016-dbus-rauc-allow-configuring-the-_criteria-in-channel.patch new file mode 100644 index 00000000..c6ef99cf --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0016-dbus-rauc-allow-configuring-the-_criteria-in-channel.patch @@ -0,0 +1,116 @@ +From e5c3a3cc1c3e012e080475f6a6e5c953f632ce19 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Mon, 31 Mar 2025 15:45:50 +0200 +Subject: [PATCH 16/17] dbus: rauc: allow configuring the *_criteria in channel + files +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The native RAUC polling feature allows configuring when a new update +bundle is even considered as an update candidate (`candidate_criteria`), +when it is considered for automatic installation (`install_criteria`) +and under which conditions to auto-boot into another slot after +installation (`reboot_criteria`). + +The defaults we have chosen in previous commits generally make sense, +but allow users with custom update channels to customize them if they +deem it necessary. + +Signed-off-by: Leonard Göhrs +--- + openapi.yaml | 6 ++++++ + src/dbus/rauc/system_conf.rs | 23 +++++++++++++++++------ + src/dbus/rauc/update_channels.rs | 9 +++++++++ + 3 files changed, 32 insertions(+), 6 deletions(-) + +diff --git a/openapi.yaml b/openapi.yaml +index c5efb2b..1d6fae6 100644 +--- a/openapi.yaml ++++ b/openapi.yaml +@@ -1166,6 +1166,12 @@ components: + type: boolean + force_auto_install: + type: boolean ++ candidate_criteria: ++ type: string ++ install_criteria: ++ type: string ++ reboot_criteria: ++ type: string + + ServiceStatus: + type: object +diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs +index e387cb7..32dabcb 100644 +--- a/src/dbus/rauc/system_conf.rs ++++ b/src/dbus/rauc/system_conf.rs +@@ -45,14 +45,25 @@ fn poll_section( + writeln!(&mut section, "interval-sec={}", interval.as_secs())?; + } + +- writeln!(&mut section, "candidate-criteria=different-version")?; ++ let candidate_criteria = primary_channel ++ .candidate_criteria ++ .as_deref() ++ .unwrap_or("different-version"); ++ ++ writeln!(&mut section, "candidate-criteria={candidate_criteria}")?; + + if auto_install { +- writeln!(&mut section, "install-criteria=different-version")?; +- writeln!( +- &mut section, +- "reboot-criteria=updated-slots;updated-artifacts" +- )?; ++ let install_criteria = primary_channel ++ .install_criteria ++ .as_deref() ++ .unwrap_or("different-version"); ++ let reboot_criteria = primary_channel ++ .reboot_criteria ++ .as_deref() ++ .unwrap_or("updated-slots;updated-artifacts"); ++ ++ writeln!(&mut section, "install-criteria={install_criteria}")?; ++ writeln!(&mut section, "reboot-criteria={reboot_criteria}")?; + writeln!(&mut section, "reboot-cmd=systemctl reboot")?; + } + +diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs +index c067d2b..d147f6c 100644 +--- a/src/dbus/rauc/update_channels.rs ++++ b/src/dbus/rauc/update_channels.rs +@@ -56,6 +56,9 @@ pub struct Channel { + pub bundle: Option, + pub force_polling: Option, + pub force_auto_install: Option, ++ pub candidate_criteria: Option, ++ pub install_criteria: Option, ++ pub reboot_criteria: Option, + } + + #[derive(Serialize, Deserialize, Clone, PartialEq)] +@@ -70,6 +73,9 @@ pub struct ChannelFile { + pub polling_interval: Option, + pub force_polling: Option, + pub force_auto_install: Option, ++ pub candidate_criteria: Option, ++ pub install_criteria: Option, ++ pub reboot_criteria: Option, + } + + #[cfg(not(feature = "demo_mode"))] +@@ -151,6 +157,9 @@ impl Channel { + bundle: None, + force_polling: channel_file.force_polling, + force_auto_install: channel_file.force_auto_install, ++ candidate_criteria: channel_file.candidate_criteria, ++ install_criteria: channel_file.install_criteria, ++ reboot_criteria: channel_file.reboot_criteria, + }; + + ch.update_enabled(); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/files/0017-dbus-rauc-prevent-auto-updates-while-in-setup-mode.patch b/meta-lxatac-software/recipes-rust/tacd/files/0017-dbus-rauc-prevent-auto-updates-while-in-setup-mode.patch new file mode 100644 index 00000000..09274b79 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/files/0017-dbus-rauc-prevent-auto-updates-while-in-setup-mode.patch @@ -0,0 +1,210 @@ +From 31dd18980a67d838adb1c86507ea14fcd8a39c44 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 10:56:32 +0200 +Subject: [PATCH 17/17] dbus: rauc: prevent auto updates while in setup mode +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is to prevent a situation where a user enables auto-install in the +setup mode and an update starts immediately, does a migration of whatever +the user has configured so far and triggers a reboot while the user is +still configuring things in the web interface. + +This should make the user experience a bit better: + + - Unbox TAC. + - Enter setup mode. + - Enable update polling and auto install. + (A poll for updates will trigger immediately, but no auto install) + - Configure the rest of the system. + - Leave the setup mode. + - Another poll for updates may be triggered and if available an + installation will start. + - The user is greeted by the normal web interface showing a notification + about the ongoing installation. + +Signed-off-by: Leonard Göhrs +--- + src/dbus.rs | 3 ++- + src/dbus/rauc.rs | 18 ++++++++++++++++-- + src/dbus/rauc/system_conf.rs | 8 +++++++- + src/main.rs | 25 ++++++++++++++++--------- + 4 files changed, 41 insertions(+), 13 deletions(-) + +diff --git a/src/dbus.rs b/src/dbus.rs +index 326c02a..db9c4b5 100644 +--- a/src/dbus.rs ++++ b/src/dbus.rs +@@ -82,6 +82,7 @@ impl DbusSession { + wtb: &mut WatchedTasksBuilder, + led_dut: Arc>, + led_uplink: Arc>, ++ setup_mode: Arc>, + ) -> anyhow::Result { + let tacd = Tacd::new(); + +@@ -94,7 +95,7 @@ impl DbusSession { + Ok(Self { + hostname: Hostname::new(bb, wtb, &conn)?, + network: Network::new(bb, wtb, &conn, led_dut, led_uplink)?, +- rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone())?, ++ rauc: Rauc::new(bb, wtb, &conn, systemd.rauc.clone(), setup_mode)?, + systemd, + }) + } +diff --git a/src/dbus/rauc.rs b/src/dbus/rauc.rs +index c8ca2ab..11bf60f 100644 +--- a/src/dbus/rauc.rs ++++ b/src/dbus/rauc.rs +@@ -207,6 +207,7 @@ async fn channel_list_update_task( + reload: Arc>, + enable_polling: Arc>, + enable_auto_install: Arc>, ++ setup_mode: Arc>, + channels: Arc>, + rauc_service: Service, + ) -> Result<()> { +@@ -215,9 +216,11 @@ async fn channel_list_update_task( + let (reload_stream, _) = reload.subscribe_unbounded(); + let (mut enable_polling_stream, _) = enable_polling.subscribe_unbounded(); + let (mut enable_auto_install_stream, _) = enable_auto_install.subscribe_unbounded(); ++ let (mut setup_mode_stream, _) = setup_mode.subscribe_unbounded(); + + let mut enable_polling = enable_polling_stream.next().await.unwrap_or(false); + let mut enable_auto_install = enable_auto_install_stream.next().await.unwrap_or(false); ++ let mut setup_mode = setup_mode_stream.next().await.unwrap_or(true); + + 'reload_loop: loop { + futures::select! { +@@ -232,6 +235,9 @@ async fn channel_list_update_task( + enable_auto_install_new = enable_auto_install_stream.recv().fuse() => { + enable_auto_install = enable_auto_install_new?; + } ++ setup_mode_new = setup_mode_stream.recv().fuse() => { ++ setup_mode = setup_mode_new?; ++ } + }; + + // Read the list of available update channels +@@ -243,8 +249,12 @@ async fn channel_list_update_task( + } + }; + +- let should_reload = +- update_system_conf(new_channels.primary(), enable_polling, enable_auto_install)?; ++ let should_reload = update_system_conf( ++ new_channels.primary(), ++ enable_polling, ++ enable_auto_install, ++ setup_mode, ++ )?; + + channels.set(new_channels); + +@@ -329,6 +339,7 @@ impl Rauc { + wtb: &mut WatchedTasksBuilder, + _conn: &Arc, + rauc_service: Service, ++ setup_mode: Arc>, + ) -> Result { + let inst = Self::setup_topics(bb); + +@@ -344,6 +355,7 @@ impl Rauc { + inst.reload.clone(), + inst.enable_polling.clone(), + inst.enable_auto_install.clone(), ++ setup_mode, + inst.channels.clone(), + rauc_service, + ), +@@ -358,6 +370,7 @@ impl Rauc { + wtb: &mut WatchedTasksBuilder, + conn: &Arc, + rauc_service: Service, ++ setup_mode: Arc>, + ) -> Result { + let inst = Self::setup_topics(bb); + +@@ -613,6 +626,7 @@ impl Rauc { + inst.reload.clone(), + inst.enable_polling.clone(), + inst.enable_auto_install.clone(), ++ setup_mode, + inst.channels.clone(), + rauc_service, + ), +diff --git a/src/dbus/rauc/system_conf.rs b/src/dbus/rauc/system_conf.rs +index 32dabcb..fce3227 100644 +--- a/src/dbus/rauc/system_conf.rs ++++ b/src/dbus/rauc/system_conf.rs +@@ -74,6 +74,7 @@ pub fn update_system_conf( + primary_channel: Option<&Channel>, + enable_polling: bool, + enable_auto_install: bool, ++ setup_mode: bool, + ) -> std::io::Result { + let dynamic_conf = { + // Allow force-enabling update polling and automatic installations +@@ -86,8 +87,13 @@ pub fn update_system_conf( + .and_then(|pc| pc.force_auto_install) + .unwrap_or(false); + ++ // Allow polling for updates, but not automatically installing them ++ // while the user is still in setup mode. ++ // Otherwise they may unbox a TAC, click through the setup process, ++ // activate auto installation, and then an installation starts in the ++ // background without them even noticing. + let polling = enable_polling || force_polling; +- let auto_install = enable_auto_install || force_auto_install; ++ let auto_install = (enable_auto_install || force_auto_install) && !setup_mode; + + match poll_section(primary_channel, polling, auto_install) { + Ok(Some(ps)) => { +diff --git a/src/main.rs b/src/main.rs +index 203b74b..29ab557 100644 +--- a/src/main.rs ++++ b/src/main.rs +@@ -107,9 +107,23 @@ async fn init(screenshooter: ScreenShooter) -> Result<(Ui, WatchedTasksBuilder)> + adc.iobus_curr.fast.clone(), + adc.iobus_volt.fast.clone(), + )?; ++ ++ // Set up a http server and provide some static files like the web ++ // interface and config files that may be edited inside the web ui. ++ let mut http_server = HttpServer::new(); ++ ++ // Allow editing some aspects of the TAC configuration when in "setup mode". ++ let setup_mode = SetupMode::new(&mut bb, &mut wtb, &mut http_server.server)?; ++ + let (hostname, network, rauc, systemd) = { +- let dbus = +- DbusSession::new(&mut bb, &mut wtb, led.eth_dut.clone(), led.eth_lab.clone()).await?; ++ let dbus = DbusSession::new( ++ &mut bb, ++ &mut wtb, ++ led.eth_dut.clone(), ++ led.eth_lab.clone(), ++ setup_mode.setup_mode.clone(), ++ ) ++ .await?; + + (dbus.hostname, dbus.network, dbus.rauc, dbus.systemd) + }; +@@ -123,13 +137,6 @@ async fn init(screenshooter: ScreenShooter) -> Result<(Ui, WatchedTasksBuilder)> + // (if requested on start). + let watchdog = Watchdog::new(dut_pwr.tick()); + +- // Set up a http server and provide some static files like the web +- // interface and config files that may be edited inside the web ui. +- let mut http_server = HttpServer::new(); +- +- // Allow editing some aspects of the TAC configuration when in "setup mode". +- let setup_mode = SetupMode::new(&mut bb, &mut wtb, &mut http_server.server)?; +- + // Expose a live log of the TAC's systemd journal so it can be viewed + // in the web interface. + journal::serve(&mut http_server.server); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-rust/tacd/tacd-crates.inc b/meta-lxatac-software/recipes-rust/tacd/tacd-crates.inc new file mode 100644 index 00000000..a5a0dcf1 --- /dev/null +++ b/meta-lxatac-software/recipes-rust/tacd/tacd-crates.inc @@ -0,0 +1,758 @@ +# Autogenerated with 'bitbake -c update_crates tacd' + +# from Cargo.lock +SRC_URI += " \ + crate://crates.io/addr2line/0.24.2 \ + crate://crates.io/adler2/2.0.0 \ + crate://crates.io/aead/0.3.2 \ + crate://crates.io/aes/0.6.0 \ + crate://crates.io/aes-gcm/0.8.0 \ + crate://crates.io/aes-soft/0.6.4 \ + crate://crates.io/aesni/0.10.0 \ + crate://crates.io/aho-corasick/1.1.3 \ + crate://crates.io/android-tzdata/0.1.1 \ + crate://crates.io/android_system_properties/0.1.5 \ + crate://crates.io/anstream/0.6.18 \ + crate://crates.io/anstyle/1.0.10 \ + crate://crates.io/anstyle-parse/0.2.6 \ + crate://crates.io/anstyle-query/1.1.2 \ + crate://crates.io/anstyle-wincon/3.0.6 \ + crate://crates.io/anyhow/1.0.93 \ + crate://crates.io/arrayref/0.3.9 \ + crate://crates.io/arrayvec/0.5.2 \ + crate://crates.io/async-attributes/1.1.2 \ + crate://crates.io/async-broadcast/0.7.1 \ + crate://crates.io/async-channel/1.9.0 \ + crate://crates.io/async-channel/2.3.1 \ + crate://crates.io/async-dup/1.2.4 \ + crate://crates.io/async-executor/1.13.1 \ + crate://crates.io/async-fs/2.1.2 \ + crate://crates.io/async-global-executor/2.4.1 \ + crate://crates.io/async-h1/2.3.4 \ + crate://crates.io/async-io/1.13.0 \ + crate://crates.io/async-io/2.4.0 \ + crate://crates.io/async-lock/2.8.0 \ + crate://crates.io/async-lock/3.4.0 \ + crate://crates.io/async-process/2.3.0 \ + crate://crates.io/async-recursion/1.1.1 \ + crate://crates.io/async-session/2.0.1 \ + crate://crates.io/async-signal/0.2.10 \ + crate://crates.io/async-sse/4.1.0 \ + crate://crates.io/async-sse/5.1.0 \ + crate://crates.io/async-std/1.13.0 \ + crate://crates.io/async-task/4.7.1 \ + crate://crates.io/async-trait/0.1.83 \ + crate://crates.io/async-tungstenite/0.28.0 \ + crate://crates.io/atomic-waker/1.1.2 \ + crate://crates.io/autocfg/1.4.0 \ + crate://crates.io/az/1.2.1 \ + crate://crates.io/backtrace/0.3.74 \ + crate://crates.io/base-x/0.2.11 \ + crate://crates.io/base64/0.12.3 \ + crate://crates.io/base64/0.13.1 \ + crate://crates.io/base64/0.22.1 \ + crate://crates.io/bincode/1.3.3 \ + crate://crates.io/bitflags/1.3.2 \ + crate://crates.io/bitflags/2.6.0 \ + crate://crates.io/bitvec/1.0.1 \ + crate://crates.io/blake3/0.3.8 \ + crate://crates.io/block-buffer/0.9.0 \ + crate://crates.io/block-buffer/0.10.4 \ + crate://crates.io/blocking/1.6.1 \ + crate://crates.io/build-env/0.3.1 \ + crate://crates.io/bumpalo/3.16.0 \ + crate://crates.io/byteorder/1.5.0 \ + crate://crates.io/bytes/1.8.0 \ + crate://crates.io/cc/1.2.1 \ + crate://crates.io/cfg-if/0.1.10 \ + crate://crates.io/cfg-if/1.0.0 \ + crate://crates.io/cfg_aliases/0.2.1 \ + crate://crates.io/chrono/0.4.38 \ + crate://crates.io/cipher/0.2.5 \ + crate://crates.io/colorchoice/1.0.3 \ + crate://crates.io/concurrent-queue/2.5.0 \ + crate://crates.io/config/0.10.1 \ + crate://crates.io/const_fn/0.4.10 \ + crate://crates.io/constant_time_eq/0.1.5 \ + crate://crates.io/cookie/0.14.4 \ + crate://crates.io/core-foundation-sys/0.8.7 \ + crate://crates.io/cpufeatures/0.2.15 \ + crate://crates.io/cpuid-bool/0.2.0 \ + crate://crates.io/crc32fast/1.4.2 \ + crate://crates.io/crossbeam-queue/0.3.11 \ + crate://crates.io/crossbeam-utils/0.8.20 \ + crate://crates.io/crypto-common/0.1.6 \ + crate://crates.io/crypto-mac/0.8.0 \ + crate://crates.io/crypto-mac/0.10.1 \ + crate://crates.io/cstr-argument/0.1.2 \ + crate://crates.io/ctr/0.6.0 \ + crate://crates.io/dashmap/5.5.3 \ + crate://crates.io/data-encoding/2.6.0 \ + crate://crates.io/deadpool/0.7.0 \ + crate://crates.io/digest/0.9.0 \ + crate://crates.io/digest/0.10.7 \ + crate://crates.io/discard/1.0.4 \ + crate://crates.io/displaydoc/0.2.5 \ + crate://crates.io/embedded-graphics/0.8.1 \ + crate://crates.io/embedded-graphics-core/0.4.0 \ + crate://crates.io/endi/1.1.0 \ + crate://crates.io/enumflags2/0.7.10 \ + crate://crates.io/enumflags2_derive/0.7.10 \ + crate://crates.io/env_filter/0.1.2 \ + crate://crates.io/env_logger/0.11.5 \ + crate://crates.io/equivalent/1.0.1 \ + crate://crates.io/erased-serde/0.4.5 \ + crate://crates.io/errno/0.2.8 \ + crate://crates.io/errno/0.3.9 \ + crate://crates.io/errno-dragonfly/0.1.2 \ + crate://crates.io/evdev/0.12.2 \ + crate://crates.io/event-listener/2.5.3 \ + crate://crates.io/event-listener/5.3.1 \ + crate://crates.io/event-listener-strategy/0.5.2 \ + crate://crates.io/fastrand/1.9.0 \ + crate://crates.io/fastrand/2.2.0 \ + crate://crates.io/fdeflate/0.3.6 \ + crate://crates.io/femme/2.2.1 \ + crate://crates.io/flate2/1.0.35 \ + crate://crates.io/float-cmp/0.9.0 \ + crate://crates.io/fnv/1.0.7 \ + crate://crates.io/foreign-types/0.5.0 \ + crate://crates.io/foreign-types-macros/0.2.3 \ + crate://crates.io/foreign-types-shared/0.3.1 \ + crate://crates.io/form_urlencoded/1.2.1 \ + crate://crates.io/framebuffer/0.3.1 \ + crate://crates.io/funty/2.0.0 \ + crate://crates.io/futures/0.3.31 \ + crate://crates.io/futures-channel/0.3.31 \ + crate://crates.io/futures-core/0.3.31 \ + crate://crates.io/futures-executor/0.3.31 \ + crate://crates.io/futures-io/0.3.31 \ + crate://crates.io/futures-lite/1.13.0 \ + crate://crates.io/futures-lite/2.5.0 \ + crate://crates.io/futures-macro/0.3.31 \ + crate://crates.io/futures-sink/0.3.31 \ + crate://crates.io/futures-task/0.3.31 \ + crate://crates.io/futures-util/0.3.31 \ + crate://crates.io/generic-array/0.14.7 \ + crate://crates.io/getrandom/0.1.16 \ + crate://crates.io/getrandom/0.2.15 \ + crate://crates.io/ghash/0.3.1 \ + crate://crates.io/gimli/0.31.1 \ + crate://crates.io/gloo-timers/0.3.0 \ + crate://crates.io/gpio-cdev/0.6.0 \ + crate://crates.io/hashbrown/0.14.5 \ + crate://crates.io/hashbrown/0.15.1 \ + crate://crates.io/hermit-abi/0.3.9 \ + crate://crates.io/hermit-abi/0.4.0 \ + crate://crates.io/hex/0.4.3 \ + crate://crates.io/hkdf/0.10.0 \ + crate://crates.io/hmac/0.8.1 \ + crate://crates.io/hmac/0.10.1 \ + crate://crates.io/html-escape/0.2.13 \ + crate://crates.io/http/1.1.0 \ + crate://crates.io/http-client/6.5.3 \ + crate://crates.io/http-types/2.12.0 \ + crate://crates.io/httparse/1.9.5 \ + crate://crates.io/humantime/2.1.0 \ + crate://crates.io/iana-time-zone/0.1.61 \ + crate://crates.io/iana-time-zone-haiku/0.1.2 \ + crate://crates.io/icu_collections/1.5.0 \ + crate://crates.io/icu_locid/1.5.0 \ + crate://crates.io/icu_locid_transform/1.5.0 \ + crate://crates.io/icu_locid_transform_data/1.5.0 \ + crate://crates.io/icu_normalizer/1.5.0 \ + crate://crates.io/icu_normalizer_data/1.5.0 \ + crate://crates.io/icu_properties/1.5.1 \ + crate://crates.io/icu_properties_data/1.5.0 \ + crate://crates.io/icu_provider/1.5.0 \ + crate://crates.io/icu_provider_macros/1.5.0 \ + crate://crates.io/idna/1.0.3 \ + crate://crates.io/idna_adapter/1.2.0 \ + crate://crates.io/indexmap/2.6.0 \ + crate://crates.io/industrial-io/0.5.2 \ + crate://crates.io/infer/0.2.3 \ + crate://crates.io/instant/0.1.13 \ + crate://crates.io/io-lifetimes/1.0.11 \ + crate://crates.io/is_terminal_polyfill/1.70.1 \ + crate://crates.io/itoa/1.0.11 \ + crate://crates.io/js-sys/0.3.72 \ + crate://crates.io/kv-log-macro/1.0.7 \ + crate://crates.io/lazy_static/1.5.0 \ + crate://crates.io/lexical-core/0.7.6 \ + crate://crates.io/libc/0.2.164 \ + crate://crates.io/libiio-sys/0.3.1 \ + crate://crates.io/libsystemd-sys/0.9.3 \ + crate://crates.io/linux-raw-sys/0.3.8 \ + crate://crates.io/linux-raw-sys/0.4.14 \ + crate://crates.io/litemap/0.7.3 \ + crate://crates.io/lock_api/0.4.12 \ + crate://crates.io/log/0.4.22 \ + crate://crates.io/memchr/2.7.4 \ + crate://crates.io/memmap/0.7.0 \ + crate://crates.io/memoffset/0.6.5 \ + crate://crates.io/memoffset/0.9.1 \ + crate://crates.io/micromath/2.1.0 \ + crate://crates.io/mime/0.3.17 \ + crate://crates.io/mime_guess/2.0.5 \ + crate://crates.io/miniz_oxide/0.8.0 \ + crate://crates.io/mqtt-protocol/0.12.0 \ + crate://crates.io/nix/0.23.2 \ + crate://crates.io/nix/0.27.1 \ + crate://crates.io/nix/0.29.0 \ + crate://crates.io/nom/5.1.3 \ + crate://crates.io/num-traits/0.2.19 \ + crate://crates.io/num_cpus/1.16.0 \ + crate://crates.io/numtoa/0.2.4 \ + crate://crates.io/object/0.36.5 \ + crate://crates.io/once_cell/1.20.2 \ + crate://crates.io/opaque-debug/0.3.1 \ + crate://crates.io/ordered-stream/0.2.0 \ + crate://crates.io/parking/2.2.1 \ + crate://crates.io/parking_lot_core/0.9.10 \ + crate://crates.io/percent-encoding/2.3.1 \ + crate://crates.io/pin-project/1.1.7 \ + crate://crates.io/pin-project-internal/1.1.7 \ + crate://crates.io/pin-project-lite/0.1.12 \ + crate://crates.io/pin-project-lite/0.2.15 \ + crate://crates.io/pin-utils/0.1.0 \ + crate://crates.io/piper/0.2.4 \ + crate://crates.io/pkg-config/0.3.31 \ + crate://crates.io/png/0.17.14 \ + crate://crates.io/polling/2.8.0 \ + crate://crates.io/polling/3.7.4 \ + crate://crates.io/polyval/0.4.5 \ + crate://crates.io/ppv-lite86/0.2.20 \ + crate://crates.io/proc-macro-crate/3.2.0 \ + crate://crates.io/proc-macro-hack/0.5.20+deprecated \ + crate://crates.io/proc-macro2/1.0.89 \ + crate://crates.io/quote/1.0.37 \ + crate://crates.io/radium/0.7.0 \ + crate://crates.io/rand/0.7.3 \ + crate://crates.io/rand/0.8.5 \ + crate://crates.io/rand_chacha/0.2.2 \ + crate://crates.io/rand_chacha/0.3.1 \ + crate://crates.io/rand_core/0.5.1 \ + crate://crates.io/rand_core/0.6.4 \ + crate://crates.io/rand_hc/0.2.0 \ + crate://crates.io/redox_syscall/0.5.7 \ + crate://crates.io/regex/1.11.1 \ + crate://crates.io/regex-automata/0.4.9 \ + crate://crates.io/regex-syntax/0.8.5 \ + crate://crates.io/route-recognizer/0.2.0 \ + crate://crates.io/rustc-demangle/0.1.24 \ + crate://crates.io/rustc_version/0.2.3 \ + crate://crates.io/rustix/0.37.27 \ + crate://crates.io/rustix/0.38.40 \ + crate://crates.io/rustversion/1.0.18 \ + crate://crates.io/ryu/1.0.18 \ + crate://crates.io/scopeguard/1.2.0 \ + crate://crates.io/semver/0.9.0 \ + crate://crates.io/semver-parser/0.7.0 \ + crate://crates.io/serde/1.0.215 \ + crate://crates.io/serde_derive/1.0.215 \ + crate://crates.io/serde_fmt/1.0.3 \ + crate://crates.io/serde_json/1.0.133 \ + crate://crates.io/serde_qs/0.8.5 \ + crate://crates.io/serde_repr/0.1.19 \ + crate://crates.io/serde_urlencoded/0.7.1 \ + crate://crates.io/serde_yaml/0.9.34+deprecated \ + crate://crates.io/sha-1/0.10.1 \ + crate://crates.io/sha1/0.6.1 \ + crate://crates.io/sha1/0.10.6 \ + crate://crates.io/sha1_smol/1.0.1 \ + crate://crates.io/sha2/0.9.9 \ + crate://crates.io/shlex/1.3.0 \ + crate://crates.io/signal-hook-registry/1.4.2 \ + crate://crates.io/simd-adler32/0.3.7 \ + crate://crates.io/slab/0.4.9 \ + crate://crates.io/smallvec/1.13.2 \ + crate://crates.io/socket2/0.4.10 \ + crate://crates.io/stable_deref_trait/1.2.0 \ + crate://crates.io/standback/0.2.17 \ + crate://crates.io/static_assertions/1.1.0 \ + crate://crates.io/stdweb/0.4.20 \ + crate://crates.io/stdweb-derive/0.5.3 \ + crate://crates.io/stdweb-internal-macros/0.2.9 \ + crate://crates.io/stdweb-internal-runtime/0.1.5 \ + crate://crates.io/subtle/2.4.1 \ + crate://crates.io/surf/2.3.2 \ + crate://crates.io/sval/2.13.2 \ + crate://crates.io/sval_buffer/2.13.2 \ + crate://crates.io/sval_dynamic/2.13.2 \ + crate://crates.io/sval_fmt/2.13.2 \ + crate://crates.io/sval_json/2.13.2 \ + crate://crates.io/sval_nested/2.13.2 \ + crate://crates.io/sval_ref/2.13.2 \ + crate://crates.io/sval_serde/2.13.2 \ + crate://crates.io/syn/1.0.109 \ + crate://crates.io/syn/2.0.87 \ + crate://crates.io/synstructure/0.13.1 \ + crate://crates.io/sysfs-class/0.1.3 \ + crate://crates.io/systemd/0.10.0 \ + crate://crates.io/tap/1.0.1 \ + crate://crates.io/tempfile/3.14.0 \ + crate://crates.io/thiserror/1.0.69 \ + crate://crates.io/thiserror-impl/1.0.69 \ + crate://crates.io/thread-priority/1.2.0 \ + crate://crates.io/tide/0.16.0 \ + crate://crates.io/time/0.2.27 \ + crate://crates.io/time-macros/0.1.1 \ + crate://crates.io/time-macros-impl/0.1.2 \ + crate://crates.io/tinystr/0.7.6 \ + crate://crates.io/tokio/1.41.1 \ + crate://crates.io/toml_datetime/0.6.8 \ + crate://crates.io/toml_edit/0.22.22 \ + crate://crates.io/tracing/0.1.40 \ + crate://crates.io/tracing-attributes/0.1.27 \ + crate://crates.io/tracing-core/0.1.32 \ + crate://crates.io/tungstenite/0.24.0 \ + crate://crates.io/typeid/1.0.2 \ + crate://crates.io/typenum/1.17.0 \ + crate://crates.io/uds_windows/1.1.0 \ + crate://crates.io/unicase/2.8.0 \ + crate://crates.io/unicode-ident/1.0.13 \ + crate://crates.io/unique-token/0.2.0 \ + crate://crates.io/universal-hash/0.4.1 \ + crate://crates.io/unsafe-libyaml/0.2.11 \ + crate://crates.io/url/2.5.3 \ + crate://crates.io/utf-8/0.7.6 \ + crate://crates.io/utf16_iter/1.0.5 \ + crate://crates.io/utf8-cstr/0.1.6 \ + crate://crates.io/utf8-width/0.1.7 \ + crate://crates.io/utf8_iter/1.0.4 \ + crate://crates.io/utf8parse/0.2.2 \ + crate://crates.io/value-bag/1.10.0 \ + crate://crates.io/value-bag-serde1/1.10.0 \ + crate://crates.io/value-bag-sval2/1.10.0 \ + crate://crates.io/version_check/0.9.5 \ + crate://crates.io/waker-fn/1.2.0 \ + crate://crates.io/wasi/0.9.0+wasi-snapshot-preview1 \ + crate://crates.io/wasi/0.11.0+wasi-snapshot-preview1 \ + crate://crates.io/wasm-bindgen/0.2.95 \ + crate://crates.io/wasm-bindgen-backend/0.2.95 \ + crate://crates.io/wasm-bindgen-futures/0.4.45 \ + crate://crates.io/wasm-bindgen-macro/0.2.95 \ + crate://crates.io/wasm-bindgen-macro-support/0.2.95 \ + crate://crates.io/wasm-bindgen-shared/0.2.95 \ + crate://crates.io/web-sys/0.3.72 \ + crate://crates.io/winapi/0.3.9 \ + crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \ + crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \ + crate://crates.io/windows-core/0.52.0 \ + crate://crates.io/windows-sys/0.48.0 \ + crate://crates.io/windows-sys/0.52.0 \ + crate://crates.io/windows-sys/0.59.0 \ + crate://crates.io/windows-targets/0.48.5 \ + crate://crates.io/windows-targets/0.52.6 \ + crate://crates.io/windows_aarch64_gnullvm/0.48.5 \ + crate://crates.io/windows_aarch64_gnullvm/0.52.6 \ + crate://crates.io/windows_aarch64_msvc/0.48.5 \ + crate://crates.io/windows_aarch64_msvc/0.52.6 \ + crate://crates.io/windows_i686_gnu/0.48.5 \ + crate://crates.io/windows_i686_gnu/0.52.6 \ + crate://crates.io/windows_i686_gnullvm/0.52.6 \ + crate://crates.io/windows_i686_msvc/0.48.5 \ + crate://crates.io/windows_i686_msvc/0.52.6 \ + crate://crates.io/windows_x86_64_gnu/0.48.5 \ + crate://crates.io/windows_x86_64_gnu/0.52.6 \ + crate://crates.io/windows_x86_64_gnullvm/0.48.5 \ + crate://crates.io/windows_x86_64_gnullvm/0.52.6 \ + crate://crates.io/windows_x86_64_msvc/0.48.5 \ + crate://crates.io/windows_x86_64_msvc/0.52.6 \ + crate://crates.io/winnow/0.6.20 \ + crate://crates.io/write16/1.0.0 \ + crate://crates.io/writeable/0.5.5 \ + crate://crates.io/wyz/0.5.1 \ + crate://crates.io/xdg-home/1.3.0 \ + crate://crates.io/yoke/0.7.4 \ + crate://crates.io/yoke-derive/0.7.4 \ + crate://crates.io/zbus/4.4.0 \ + crate://crates.io/zbus_macros/4.4.0 \ + crate://crates.io/zbus_names/3.0.0 \ + crate://crates.io/zerocopy/0.7.35 \ + crate://crates.io/zerocopy-derive/0.7.35 \ + crate://crates.io/zerofrom/0.1.4 \ + crate://crates.io/zerofrom-derive/0.1.4 \ + crate://crates.io/zerovec/0.10.4 \ + crate://crates.io/zerovec-derive/0.10.3 \ + crate://crates.io/zvariant/4.2.0 \ + crate://crates.io/zvariant_derive/4.2.0 \ + crate://crates.io/zvariant_utils/2.1.0 \ +" + +SRC_URI[addr2line-0.24.2.sha256sum] = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +SRC_URI[adler2-2.0.0.sha256sum] = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +SRC_URI[aead-0.3.2.sha256sum] = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +SRC_URI[aes-0.6.0.sha256sum] = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +SRC_URI[aes-gcm-0.8.0.sha256sum] = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +SRC_URI[aes-soft-0.6.4.sha256sum] = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +SRC_URI[aesni-0.10.0.sha256sum] = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +SRC_URI[aho-corasick-1.1.3.sha256sum] = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +SRC_URI[android-tzdata-0.1.1.sha256sum] = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +SRC_URI[android_system_properties-0.1.5.sha256sum] = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +SRC_URI[anstream-0.6.18.sha256sum] = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +SRC_URI[anstyle-1.0.10.sha256sum] = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +SRC_URI[anstyle-parse-0.2.6.sha256sum] = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +SRC_URI[anstyle-query-1.1.2.sha256sum] = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +SRC_URI[anstyle-wincon-3.0.6.sha256sum] = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +SRC_URI[anyhow-1.0.93.sha256sum] = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +SRC_URI[arrayref-0.3.9.sha256sum] = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +SRC_URI[arrayvec-0.5.2.sha256sum] = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +SRC_URI[async-attributes-1.1.2.sha256sum] = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +SRC_URI[async-broadcast-0.7.1.sha256sum] = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +SRC_URI[async-channel-1.9.0.sha256sum] = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +SRC_URI[async-channel-2.3.1.sha256sum] = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +SRC_URI[async-dup-1.2.4.sha256sum] = "7c2886ab563af5038f79ec016dd7b87947ed138b794e8dd64992962c9cca0411" +SRC_URI[async-executor-1.13.1.sha256sum] = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +SRC_URI[async-fs-2.1.2.sha256sum] = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +SRC_URI[async-global-executor-2.4.1.sha256sum] = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +SRC_URI[async-h1-2.3.4.sha256sum] = "5d1d1dae8cb2c4258a79d6ed088b7fb9b4763bf4e9b22d040779761e046a2971" +SRC_URI[async-io-1.13.0.sha256sum] = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +SRC_URI[async-io-2.4.0.sha256sum] = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +SRC_URI[async-lock-2.8.0.sha256sum] = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +SRC_URI[async-lock-3.4.0.sha256sum] = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +SRC_URI[async-process-2.3.0.sha256sum] = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +SRC_URI[async-recursion-1.1.1.sha256sum] = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +SRC_URI[async-session-2.0.1.sha256sum] = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" +SRC_URI[async-signal-0.2.10.sha256sum] = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +SRC_URI[async-sse-4.1.0.sha256sum] = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" +SRC_URI[async-sse-5.1.0.sha256sum] = "2e6fa871e4334a622afd6bb2f611635e8083a6f5e2936c0f90f37c7ef9856298" +SRC_URI[async-std-1.13.0.sha256sum] = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +SRC_URI[async-task-4.7.1.sha256sum] = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +SRC_URI[async-trait-0.1.83.sha256sum] = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +SRC_URI[async-tungstenite-0.28.0.sha256sum] = "90e661b6cb0a6eb34d02c520b052daa3aa9ac0cc02495c9d066bbce13ead132b" +SRC_URI[atomic-waker-1.1.2.sha256sum] = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +SRC_URI[autocfg-1.4.0.sha256sum] = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +SRC_URI[az-1.2.1.sha256sum] = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +SRC_URI[backtrace-0.3.74.sha256sum] = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +SRC_URI[base-x-0.2.11.sha256sum] = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +SRC_URI[base64-0.12.3.sha256sum] = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +SRC_URI[base64-0.13.1.sha256sum] = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +SRC_URI[base64-0.22.1.sha256sum] = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +SRC_URI[bincode-1.3.3.sha256sum] = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +SRC_URI[bitflags-1.3.2.sha256sum] = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +SRC_URI[bitflags-2.6.0.sha256sum] = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +SRC_URI[bitvec-1.0.1.sha256sum] = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +SRC_URI[blake3-0.3.8.sha256sum] = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" +SRC_URI[block-buffer-0.9.0.sha256sum] = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +SRC_URI[block-buffer-0.10.4.sha256sum] = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +SRC_URI[blocking-1.6.1.sha256sum] = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +SRC_URI[build-env-0.3.1.sha256sum] = "1522ac6ee801a11bf9ef3f80403f4ede6eb41291fac3dde3de09989679305f25" +SRC_URI[bumpalo-3.16.0.sha256sum] = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +SRC_URI[byteorder-1.5.0.sha256sum] = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +SRC_URI[bytes-1.8.0.sha256sum] = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +SRC_URI[cc-1.2.1.sha256sum] = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +SRC_URI[cfg-if-0.1.10.sha256sum] = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +SRC_URI[cfg-if-1.0.0.sha256sum] = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +SRC_URI[cfg_aliases-0.2.1.sha256sum] = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +SRC_URI[chrono-0.4.38.sha256sum] = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +SRC_URI[cipher-0.2.5.sha256sum] = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +SRC_URI[colorchoice-1.0.3.sha256sum] = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +SRC_URI[concurrent-queue-2.5.0.sha256sum] = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +SRC_URI[config-0.10.1.sha256sum] = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +SRC_URI[const_fn-0.4.10.sha256sum] = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" +SRC_URI[constant_time_eq-0.1.5.sha256sum] = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +SRC_URI[cookie-0.14.4.sha256sum] = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +SRC_URI[core-foundation-sys-0.8.7.sha256sum] = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +SRC_URI[cpufeatures-0.2.15.sha256sum] = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +SRC_URI[cpuid-bool-0.2.0.sha256sum] = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +SRC_URI[crc32fast-1.4.2.sha256sum] = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +SRC_URI[crossbeam-queue-0.3.11.sha256sum] = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +SRC_URI[crossbeam-utils-0.8.20.sha256sum] = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +SRC_URI[crypto-common-0.1.6.sha256sum] = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +SRC_URI[crypto-mac-0.8.0.sha256sum] = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +SRC_URI[crypto-mac-0.10.1.sha256sum] = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +SRC_URI[cstr-argument-0.1.2.sha256sum] = "b6bd9c8e659a473bce955ae5c35b116af38af11a7acb0b480e01f3ed348aeb40" +SRC_URI[ctr-0.6.0.sha256sum] = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +SRC_URI[dashmap-5.5.3.sha256sum] = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +SRC_URI[data-encoding-2.6.0.sha256sum] = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +SRC_URI[deadpool-0.7.0.sha256sum] = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d" +SRC_URI[digest-0.9.0.sha256sum] = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +SRC_URI[digest-0.10.7.sha256sum] = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +SRC_URI[discard-1.0.4.sha256sum] = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +SRC_URI[displaydoc-0.2.5.sha256sum] = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +SRC_URI[embedded-graphics-0.8.1.sha256sum] = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" +SRC_URI[embedded-graphics-core-0.4.0.sha256sum] = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +SRC_URI[endi-1.1.0.sha256sum] = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +SRC_URI[enumflags2-0.7.10.sha256sum] = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +SRC_URI[enumflags2_derive-0.7.10.sha256sum] = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +SRC_URI[env_filter-0.1.2.sha256sum] = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +SRC_URI[env_logger-0.11.5.sha256sum] = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +SRC_URI[equivalent-1.0.1.sha256sum] = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +SRC_URI[erased-serde-0.4.5.sha256sum] = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +SRC_URI[errno-0.2.8.sha256sum] = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +SRC_URI[errno-0.3.9.sha256sum] = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +SRC_URI[errno-dragonfly-0.1.2.sha256sum] = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +SRC_URI[evdev-0.12.2.sha256sum] = "ab6055a93a963297befb0f4f6e18f314aec9767a4bbe88b151126df2433610a7" +SRC_URI[event-listener-2.5.3.sha256sum] = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +SRC_URI[event-listener-5.3.1.sha256sum] = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +SRC_URI[event-listener-strategy-0.5.2.sha256sum] = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +SRC_URI[fastrand-1.9.0.sha256sum] = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +SRC_URI[fastrand-2.2.0.sha256sum] = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +SRC_URI[fdeflate-0.3.6.sha256sum] = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +SRC_URI[femme-2.2.1.sha256sum] = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" +SRC_URI[flate2-1.0.35.sha256sum] = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +SRC_URI[float-cmp-0.9.0.sha256sum] = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +SRC_URI[fnv-1.0.7.sha256sum] = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +SRC_URI[foreign-types-0.5.0.sha256sum] = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +SRC_URI[foreign-types-macros-0.2.3.sha256sum] = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +SRC_URI[foreign-types-shared-0.3.1.sha256sum] = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +SRC_URI[form_urlencoded-1.2.1.sha256sum] = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +SRC_URI[framebuffer-0.3.1.sha256sum] = "878caaaf1bb92c9f707dc6eef90933e07e913dac4bb8e11e145eaabaa94ef149" +SRC_URI[funty-2.0.0.sha256sum] = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +SRC_URI[futures-0.3.31.sha256sum] = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +SRC_URI[futures-channel-0.3.31.sha256sum] = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +SRC_URI[futures-core-0.3.31.sha256sum] = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +SRC_URI[futures-executor-0.3.31.sha256sum] = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +SRC_URI[futures-io-0.3.31.sha256sum] = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +SRC_URI[futures-lite-1.13.0.sha256sum] = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +SRC_URI[futures-lite-2.5.0.sha256sum] = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +SRC_URI[futures-macro-0.3.31.sha256sum] = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +SRC_URI[futures-sink-0.3.31.sha256sum] = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +SRC_URI[futures-task-0.3.31.sha256sum] = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +SRC_URI[futures-util-0.3.31.sha256sum] = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +SRC_URI[generic-array-0.14.7.sha256sum] = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +SRC_URI[getrandom-0.1.16.sha256sum] = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +SRC_URI[getrandom-0.2.15.sha256sum] = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +SRC_URI[ghash-0.3.1.sha256sum] = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +SRC_URI[gimli-0.31.1.sha256sum] = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +SRC_URI[gloo-timers-0.3.0.sha256sum] = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +SRC_URI[gpio-cdev-0.6.0.sha256sum] = "09831ec59b80be69e75d29cf36e16afbbe5fd1af9c1bf4689ad91c77db5aa6a6" +SRC_URI[hashbrown-0.14.5.sha256sum] = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +SRC_URI[hashbrown-0.15.1.sha256sum] = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +SRC_URI[hermit-abi-0.3.9.sha256sum] = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +SRC_URI[hermit-abi-0.4.0.sha256sum] = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +SRC_URI[hex-0.4.3.sha256sum] = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +SRC_URI[hkdf-0.10.0.sha256sum] = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +SRC_URI[hmac-0.8.1.sha256sum] = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +SRC_URI[hmac-0.10.1.sha256sum] = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +SRC_URI[html-escape-0.2.13.sha256sum] = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +SRC_URI[http-1.1.0.sha256sum] = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +SRC_URI[http-client-6.5.3.sha256sum] = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" +SRC_URI[http-types-2.12.0.sha256sum] = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +SRC_URI[httparse-1.9.5.sha256sum] = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +SRC_URI[humantime-2.1.0.sha256sum] = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +SRC_URI[iana-time-zone-0.1.61.sha256sum] = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +SRC_URI[iana-time-zone-haiku-0.1.2.sha256sum] = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +SRC_URI[icu_collections-1.5.0.sha256sum] = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +SRC_URI[icu_locid-1.5.0.sha256sum] = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +SRC_URI[icu_locid_transform-1.5.0.sha256sum] = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +SRC_URI[icu_locid_transform_data-1.5.0.sha256sum] = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +SRC_URI[icu_normalizer-1.5.0.sha256sum] = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +SRC_URI[icu_normalizer_data-1.5.0.sha256sum] = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +SRC_URI[icu_properties-1.5.1.sha256sum] = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +SRC_URI[icu_properties_data-1.5.0.sha256sum] = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +SRC_URI[icu_provider-1.5.0.sha256sum] = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +SRC_URI[icu_provider_macros-1.5.0.sha256sum] = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +SRC_URI[idna-1.0.3.sha256sum] = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +SRC_URI[idna_adapter-1.2.0.sha256sum] = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +SRC_URI[indexmap-2.6.0.sha256sum] = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +SRC_URI[industrial-io-0.5.2.sha256sum] = "3948a8818efcd4e0e189df60d79b58a9f4ae20b87437b2fde4dc4ca5b520c4ed" +SRC_URI[infer-0.2.3.sha256sum] = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" +SRC_URI[instant-0.1.13.sha256sum] = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +SRC_URI[io-lifetimes-1.0.11.sha256sum] = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +SRC_URI[is_terminal_polyfill-1.70.1.sha256sum] = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +SRC_URI[itoa-1.0.11.sha256sum] = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +SRC_URI[js-sys-0.3.72.sha256sum] = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +SRC_URI[kv-log-macro-1.0.7.sha256sum] = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +SRC_URI[lazy_static-1.5.0.sha256sum] = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +SRC_URI[lexical-core-0.7.6.sha256sum] = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +SRC_URI[libc-0.2.164.sha256sum] = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +SRC_URI[libiio-sys-0.3.1.sha256sum] = "6d6c255d73b8f345bb10550b44e93b2b5c910a31ce72ee2528c5dbc028cf24ec" +SRC_URI[libsystemd-sys-0.9.3.sha256sum] = "ed080163caa59cc29b34bce2209b737149a4bac148cd9a8b04e4c12822798119" +SRC_URI[linux-raw-sys-0.3.8.sha256sum] = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +SRC_URI[linux-raw-sys-0.4.14.sha256sum] = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +SRC_URI[litemap-0.7.3.sha256sum] = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +SRC_URI[lock_api-0.4.12.sha256sum] = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +SRC_URI[log-0.4.22.sha256sum] = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +SRC_URI[memchr-2.7.4.sha256sum] = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +SRC_URI[memmap-0.7.0.sha256sum] = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +SRC_URI[memoffset-0.6.5.sha256sum] = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +SRC_URI[memoffset-0.9.1.sha256sum] = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +SRC_URI[micromath-2.1.0.sha256sum] = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" +SRC_URI[mime-0.3.17.sha256sum] = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +SRC_URI[mime_guess-2.0.5.sha256sum] = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +SRC_URI[miniz_oxide-0.8.0.sha256sum] = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +SRC_URI[mqtt-protocol-0.12.0.sha256sum] = "6055e1b086ad64bb20b7ca0af2c8cd09a0506e6120eda29267509ca00e68747b" +SRC_URI[nix-0.23.2.sha256sum] = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +SRC_URI[nix-0.27.1.sha256sum] = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +SRC_URI[nix-0.29.0.sha256sum] = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +SRC_URI[nom-5.1.3.sha256sum] = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +SRC_URI[num-traits-0.2.19.sha256sum] = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +SRC_URI[num_cpus-1.16.0.sha256sum] = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +SRC_URI[numtoa-0.2.4.sha256sum] = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" +SRC_URI[object-0.36.5.sha256sum] = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +SRC_URI[once_cell-1.20.2.sha256sum] = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +SRC_URI[opaque-debug-0.3.1.sha256sum] = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +SRC_URI[ordered-stream-0.2.0.sha256sum] = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +SRC_URI[parking-2.2.1.sha256sum] = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +SRC_URI[parking_lot_core-0.9.10.sha256sum] = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +SRC_URI[percent-encoding-2.3.1.sha256sum] = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +SRC_URI[pin-project-1.1.7.sha256sum] = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +SRC_URI[pin-project-internal-1.1.7.sha256sum] = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +SRC_URI[pin-project-lite-0.1.12.sha256sum] = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" +SRC_URI[pin-project-lite-0.2.15.sha256sum] = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +SRC_URI[pin-utils-0.1.0.sha256sum] = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +SRC_URI[piper-0.2.4.sha256sum] = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +SRC_URI[pkg-config-0.3.31.sha256sum] = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +SRC_URI[png-0.17.14.sha256sum] = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +SRC_URI[polling-2.8.0.sha256sum] = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +SRC_URI[polling-3.7.4.sha256sum] = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +SRC_URI[polyval-0.4.5.sha256sum] = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +SRC_URI[ppv-lite86-0.2.20.sha256sum] = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +SRC_URI[proc-macro-crate-3.2.0.sha256sum] = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +SRC_URI[proc-macro-hack-0.5.20+deprecated.sha256sum] = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +SRC_URI[proc-macro2-1.0.89.sha256sum] = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +SRC_URI[quote-1.0.37.sha256sum] = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +SRC_URI[radium-0.7.0.sha256sum] = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +SRC_URI[rand-0.7.3.sha256sum] = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +SRC_URI[rand-0.8.5.sha256sum] = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +SRC_URI[rand_chacha-0.2.2.sha256sum] = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +SRC_URI[rand_chacha-0.3.1.sha256sum] = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +SRC_URI[rand_core-0.5.1.sha256sum] = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +SRC_URI[rand_core-0.6.4.sha256sum] = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +SRC_URI[rand_hc-0.2.0.sha256sum] = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +SRC_URI[redox_syscall-0.5.7.sha256sum] = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +SRC_URI[regex-1.11.1.sha256sum] = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +SRC_URI[regex-automata-0.4.9.sha256sum] = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +SRC_URI[regex-syntax-0.8.5.sha256sum] = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +SRC_URI[route-recognizer-0.2.0.sha256sum] = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" +SRC_URI[rustc-demangle-0.1.24.sha256sum] = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +SRC_URI[rustc_version-0.2.3.sha256sum] = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +SRC_URI[rustix-0.37.27.sha256sum] = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +SRC_URI[rustix-0.38.40.sha256sum] = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +SRC_URI[rustversion-1.0.18.sha256sum] = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +SRC_URI[ryu-1.0.18.sha256sum] = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +SRC_URI[scopeguard-1.2.0.sha256sum] = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +SRC_URI[semver-0.9.0.sha256sum] = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +SRC_URI[semver-parser-0.7.0.sha256sum] = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +SRC_URI[serde-1.0.215.sha256sum] = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +SRC_URI[serde_derive-1.0.215.sha256sum] = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +SRC_URI[serde_fmt-1.0.3.sha256sum] = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +SRC_URI[serde_json-1.0.133.sha256sum] = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +SRC_URI[serde_qs-0.8.5.sha256sum] = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +SRC_URI[serde_repr-0.1.19.sha256sum] = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +SRC_URI[serde_urlencoded-0.7.1.sha256sum] = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +SRC_URI[serde_yaml-0.9.34+deprecated.sha256sum] = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +SRC_URI[sha-1-0.10.1.sha256sum] = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +SRC_URI[sha1-0.6.1.sha256sum] = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +SRC_URI[sha1-0.10.6.sha256sum] = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +SRC_URI[sha1_smol-1.0.1.sha256sum] = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +SRC_URI[sha2-0.9.9.sha256sum] = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +SRC_URI[shlex-1.3.0.sha256sum] = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +SRC_URI[signal-hook-registry-1.4.2.sha256sum] = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +SRC_URI[simd-adler32-0.3.7.sha256sum] = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +SRC_URI[slab-0.4.9.sha256sum] = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +SRC_URI[smallvec-1.13.2.sha256sum] = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +SRC_URI[socket2-0.4.10.sha256sum] = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +SRC_URI[stable_deref_trait-1.2.0.sha256sum] = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +SRC_URI[standback-0.2.17.sha256sum] = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +SRC_URI[static_assertions-1.1.0.sha256sum] = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +SRC_URI[stdweb-0.4.20.sha256sum] = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +SRC_URI[stdweb-derive-0.5.3.sha256sum] = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +SRC_URI[stdweb-internal-macros-0.2.9.sha256sum] = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +SRC_URI[stdweb-internal-runtime-0.1.5.sha256sum] = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +SRC_URI[subtle-2.4.1.sha256sum] = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +SRC_URI[surf-2.3.2.sha256sum] = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" +SRC_URI[sval-2.13.2.sha256sum] = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" +SRC_URI[sval_buffer-2.13.2.sha256sum] = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" +SRC_URI[sval_dynamic-2.13.2.sha256sum] = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" +SRC_URI[sval_fmt-2.13.2.sha256sum] = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" +SRC_URI[sval_json-2.13.2.sha256sum] = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" +SRC_URI[sval_nested-2.13.2.sha256sum] = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" +SRC_URI[sval_ref-2.13.2.sha256sum] = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" +SRC_URI[sval_serde-2.13.2.sha256sum] = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" +SRC_URI[syn-1.0.109.sha256sum] = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +SRC_URI[syn-2.0.87.sha256sum] = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +SRC_URI[synstructure-0.13.1.sha256sum] = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +SRC_URI[sysfs-class-0.1.3.sha256sum] = "5e1bbcf869732c45a77898f7f61ed6d411dfc37613517e444842f58d428856d1" +SRC_URI[systemd-0.10.0.sha256sum] = "afec0101d9ae8ab26aedf0840109df689938ea7e538aa03df4369f1854f11562" +SRC_URI[tap-1.0.1.sha256sum] = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +SRC_URI[tempfile-3.14.0.sha256sum] = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +SRC_URI[thiserror-1.0.69.sha256sum] = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +SRC_URI[thiserror-impl-1.0.69.sha256sum] = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +SRC_URI[thread-priority-1.2.0.sha256sum] = "cfe075d7053dae61ac5413a34ea7d4913b6e6207844fd726bdd858b37ff72bf5" +SRC_URI[tide-0.16.0.sha256sum] = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" +SRC_URI[time-0.2.27.sha256sum] = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +SRC_URI[time-macros-0.1.1.sha256sum] = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +SRC_URI[time-macros-impl-0.1.2.sha256sum] = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +SRC_URI[tinystr-0.7.6.sha256sum] = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +SRC_URI[tokio-1.41.1.sha256sum] = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +SRC_URI[toml_datetime-0.6.8.sha256sum] = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +SRC_URI[toml_edit-0.22.22.sha256sum] = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +SRC_URI[tracing-0.1.40.sha256sum] = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +SRC_URI[tracing-attributes-0.1.27.sha256sum] = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +SRC_URI[tracing-core-0.1.32.sha256sum] = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +SRC_URI[tungstenite-0.24.0.sha256sum] = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +SRC_URI[typeid-1.0.2.sha256sum] = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" +SRC_URI[typenum-1.17.0.sha256sum] = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +SRC_URI[uds_windows-1.1.0.sha256sum] = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +SRC_URI[unicase-2.8.0.sha256sum] = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +SRC_URI[unicode-ident-1.0.13.sha256sum] = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +SRC_URI[unique-token-0.2.0.sha256sum] = "d3be9e39e944fa35b07f5eb280902bf4d2dc29dfbc26175230a0d0ea124a7b66" +SRC_URI[universal-hash-0.4.1.sha256sum] = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +SRC_URI[unsafe-libyaml-0.2.11.sha256sum] = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +SRC_URI[url-2.5.3.sha256sum] = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +SRC_URI[utf-8-0.7.6.sha256sum] = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +SRC_URI[utf16_iter-1.0.5.sha256sum] = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +SRC_URI[utf8-cstr-0.1.6.sha256sum] = "55bcbb425141152b10d5693095950b51c3745d019363fc2929ffd8f61449b628" +SRC_URI[utf8-width-0.1.7.sha256sum] = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +SRC_URI[utf8_iter-1.0.4.sha256sum] = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +SRC_URI[utf8parse-0.2.2.sha256sum] = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +SRC_URI[value-bag-1.10.0.sha256sum] = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +SRC_URI[value-bag-serde1-1.10.0.sha256sum] = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" +SRC_URI[value-bag-sval2-1.10.0.sha256sum] = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" +SRC_URI[version_check-0.9.5.sha256sum] = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +SRC_URI[waker-fn-1.2.0.sha256sum] = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +SRC_URI[wasi-0.9.0+wasi-snapshot-preview1.sha256sum] = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +SRC_URI[wasi-0.11.0+wasi-snapshot-preview1.sha256sum] = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +SRC_URI[wasm-bindgen-0.2.95.sha256sum] = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +SRC_URI[wasm-bindgen-backend-0.2.95.sha256sum] = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +SRC_URI[wasm-bindgen-futures-0.4.45.sha256sum] = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +SRC_URI[wasm-bindgen-macro-0.2.95.sha256sum] = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +SRC_URI[wasm-bindgen-macro-support-0.2.95.sha256sum] = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +SRC_URI[wasm-bindgen-shared-0.2.95.sha256sum] = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +SRC_URI[web-sys-0.3.72.sha256sum] = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +SRC_URI[winapi-0.3.9.sha256sum] = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +SRC_URI[winapi-i686-pc-windows-gnu-0.4.0.sha256sum] = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +SRC_URI[winapi-x86_64-pc-windows-gnu-0.4.0.sha256sum] = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +SRC_URI[windows-core-0.52.0.sha256sum] = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +SRC_URI[windows-sys-0.48.0.sha256sum] = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +SRC_URI[windows-sys-0.52.0.sha256sum] = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +SRC_URI[windows-sys-0.59.0.sha256sum] = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +SRC_URI[windows-targets-0.48.5.sha256sum] = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +SRC_URI[windows-targets-0.52.6.sha256sum] = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +SRC_URI[windows_aarch64_gnullvm-0.48.5.sha256sum] = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +SRC_URI[windows_aarch64_gnullvm-0.52.6.sha256sum] = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +SRC_URI[windows_aarch64_msvc-0.48.5.sha256sum] = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +SRC_URI[windows_aarch64_msvc-0.52.6.sha256sum] = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +SRC_URI[windows_i686_gnu-0.48.5.sha256sum] = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +SRC_URI[windows_i686_gnu-0.52.6.sha256sum] = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +SRC_URI[windows_i686_gnullvm-0.52.6.sha256sum] = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +SRC_URI[windows_i686_msvc-0.48.5.sha256sum] = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +SRC_URI[windows_i686_msvc-0.52.6.sha256sum] = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +SRC_URI[windows_x86_64_gnu-0.48.5.sha256sum] = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +SRC_URI[windows_x86_64_gnu-0.52.6.sha256sum] = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +SRC_URI[windows_x86_64_gnullvm-0.48.5.sha256sum] = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +SRC_URI[windows_x86_64_gnullvm-0.52.6.sha256sum] = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +SRC_URI[windows_x86_64_msvc-0.48.5.sha256sum] = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +SRC_URI[windows_x86_64_msvc-0.52.6.sha256sum] = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +SRC_URI[winnow-0.6.20.sha256sum] = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +SRC_URI[write16-1.0.0.sha256sum] = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +SRC_URI[writeable-0.5.5.sha256sum] = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +SRC_URI[wyz-0.5.1.sha256sum] = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +SRC_URI[xdg-home-1.3.0.sha256sum] = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +SRC_URI[yoke-0.7.4.sha256sum] = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +SRC_URI[yoke-derive-0.7.4.sha256sum] = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +SRC_URI[zbus-4.4.0.sha256sum] = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +SRC_URI[zbus_macros-4.4.0.sha256sum] = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +SRC_URI[zbus_names-3.0.0.sha256sum] = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +SRC_URI[zerocopy-0.7.35.sha256sum] = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +SRC_URI[zerocopy-derive-0.7.35.sha256sum] = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +SRC_URI[zerofrom-0.1.4.sha256sum] = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +SRC_URI[zerofrom-derive-0.1.4.sha256sum] = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +SRC_URI[zerovec-0.10.4.sha256sum] = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +SRC_URI[zerovec-derive-0.10.3.sha256sum] = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +SRC_URI[zvariant-4.2.0.sha256sum] = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +SRC_URI[zvariant_derive-4.2.0.sha256sum] = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +SRC_URI[zvariant_utils-2.1.0.sha256sum] = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" diff --git a/meta-lxatac-software/recipes-rust/tacd/tacd_git.bb b/meta-lxatac-software/recipes-rust/tacd/tacd_git.bb index df74db57..8680422e 100644 --- a/meta-lxatac-software/recipes-rust/tacd/tacd_git.bb +++ b/meta-lxatac-software/recipes-rust/tacd/tacd_git.bb @@ -1,707 +1,33 @@ inherit cargo +inherit cargo-update-recipe-crates DEFAULT_PREFERENCE = "-1" -SRC_URI += "git://github.com/linux-automation/tacd.git;protocol=https;branch=main" -SRCREV = "e79b017da65f4a084a8b24f1118e15b0c3f25ae8" +SRC_URI += "\ + git://github.com/linux-automation/tacd.git;protocol=https;branch=main \ + file://0001-tacd-dbus-rauc-re-introspect-services.patch \ + file://0002-dbus-systemd-also-monitor-rauc.service.patch \ + file://0003-dbus-rauc-add-Channels-type-to-use-instead-of-Vec-Ch.patch \ + file://0004-dbus-rauc-use-a-UpdateRequest-object-instead-of-a-si.patch \ + file://0005-dbus-rauc-allow-restricting-installation-to-a-specif.patch \ + file://0006-dbus-rauc-remove-tacd-based-update-polling.patch \ + file://0007-dbus-rauc-update_channels-add-a-concept-of-a-single-.patch \ + file://0008-dbus-rauc-only-install-bundles-from-the-primary-chan.patch \ + file://0009-dbus-rauc-system_conf-write-runtime-RAUC-config-with.patch \ + file://0010-dbus-rauc-reload-rauc-daemon-when-required.patch \ + file://0011-dbus-rauc-trigger-a-single-poll-for-updates-after-re.patch \ + file://0012-dbus-rauc-forward-poller-status-to-broker.patch \ + file://0013-dbus-rauc-add-manifest_hash-und-effective_url-to-Ups.patch \ + file://0014-dbus-rauc-add-support-for-enabling-the-auto-install-.patch \ + file://0015-dbus-rauc-implement-forced-polling-via-update-channe.patch \ + file://0016-dbus-rauc-allow-configuring-the-_criteria-in-channel.patch \ + file://0017-dbus-rauc-prevent-auto-updates-while-in-setup-mode.patch \ +" +SRCREV = "965fdd0f7dbf27dc0daddd3a23f5f7aea0d7e8dd" S = "${WORKDIR}/git" CARGO_SRC_DIR = "" PV = "0.1.0+git${SRCPV}" -SRC_URI += " \ - crate://crates.io/addr2line/0.22.0 \ - crate://crates.io/adler/1.0.2 \ - crate://crates.io/aead/0.3.2 \ - crate://crates.io/aes-gcm/0.8.0 \ - crate://crates.io/aes-soft/0.6.4 \ - crate://crates.io/aes/0.6.0 \ - crate://crates.io/aesni/0.10.0 \ - crate://crates.io/aho-corasick/1.1.3 \ - crate://crates.io/android-tzdata/0.1.1 \ - crate://crates.io/android_system_properties/0.1.5 \ - crate://crates.io/anyhow/1.0.86 \ - crate://crates.io/arrayref/0.3.8 \ - crate://crates.io/arrayvec/0.5.2 \ - crate://crates.io/async-attributes/1.1.2 \ - crate://crates.io/async-broadcast/0.5.1 \ - crate://crates.io/async-channel/1.9.0 \ - crate://crates.io/async-channel/2.3.1 \ - crate://crates.io/async-dup/1.2.4 \ - crate://crates.io/async-executor/1.13.0 \ - crate://crates.io/async-fs/1.6.0 \ - crate://crates.io/async-global-executor/2.4.1 \ - crate://crates.io/async-h1/2.3.4 \ - crate://crates.io/async-io/1.13.0 \ - crate://crates.io/async-io/2.3.3 \ - crate://crates.io/async-lock/2.8.0 \ - crate://crates.io/async-lock/3.4.0 \ - crate://crates.io/async-process/1.8.1 \ - crate://crates.io/async-recursion/1.1.1 \ - crate://crates.io/async-session/2.0.1 \ - crate://crates.io/async-signal/0.2.9 \ - crate://crates.io/async-sse/4.1.0 \ - crate://crates.io/async-sse/5.1.0 \ - crate://crates.io/async-std/1.12.0 \ - crate://crates.io/async-task/4.7.1 \ - crate://crates.io/async-trait/0.1.81 \ - crate://crates.io/async-tungstenite/0.22.2 \ - crate://crates.io/atomic-waker/1.1.2 \ - crate://crates.io/autocfg/1.3.0 \ - crate://crates.io/az/1.2.1 \ - crate://crates.io/backtrace/0.3.73 \ - crate://crates.io/base-x/0.2.11 \ - crate://crates.io/base64/0.12.3 \ - crate://crates.io/base64/0.13.1 \ - crate://crates.io/base64/0.21.7 \ - crate://crates.io/bincode/1.3.3 \ - crate://crates.io/bitflags/1.3.2 \ - crate://crates.io/bitflags/2.6.0 \ - crate://crates.io/bitvec/1.0.1 \ - crate://crates.io/blake3/0.3.8 \ - crate://crates.io/block-buffer/0.10.4 \ - crate://crates.io/block-buffer/0.9.0 \ - crate://crates.io/blocking/1.6.1 \ - crate://crates.io/build-env/0.3.1 \ - crate://crates.io/bumpalo/3.16.0 \ - crate://crates.io/byteorder/1.5.0 \ - crate://crates.io/bytes/1.7.1 \ - crate://crates.io/cc/1.1.7 \ - crate://crates.io/cfg-if/0.1.10 \ - crate://crates.io/cfg-if/1.0.0 \ - crate://crates.io/chrono/0.4.38 \ - crate://crates.io/cipher/0.2.5 \ - crate://crates.io/concurrent-queue/2.5.0 \ - crate://crates.io/config/0.10.1 \ - crate://crates.io/const_fn/0.4.10 \ - crate://crates.io/constant_time_eq/0.1.5 \ - crate://crates.io/cookie/0.14.4 \ - crate://crates.io/core-foundation-sys/0.8.6 \ - crate://crates.io/cpufeatures/0.2.12 \ - crate://crates.io/cpuid-bool/0.2.0 \ - crate://crates.io/crc32fast/1.4.2 \ - crate://crates.io/crossbeam-queue/0.3.11 \ - crate://crates.io/crossbeam-utils/0.8.20 \ - crate://crates.io/crypto-common/0.1.6 \ - crate://crates.io/crypto-mac/0.10.1 \ - crate://crates.io/crypto-mac/0.8.0 \ - crate://crates.io/cstr-argument/0.1.2 \ - crate://crates.io/ctr/0.6.0 \ - crate://crates.io/dashmap/5.5.3 \ - crate://crates.io/data-encoding/2.6.0 \ - crate://crates.io/deadpool/0.7.0 \ - crate://crates.io/derivative/2.2.0 \ - crate://crates.io/digest/0.10.7 \ - crate://crates.io/digest/0.9.0 \ - crate://crates.io/discard/1.0.4 \ - crate://crates.io/embedded-graphics-core/0.3.3 \ - crate://crates.io/embedded-graphics/0.7.1 \ - crate://crates.io/enumflags2/0.7.10 \ - crate://crates.io/enumflags2_derive/0.7.10 \ - crate://crates.io/env_logger/0.10.2 \ - crate://crates.io/equivalent/1.0.1 \ - crate://crates.io/erased-serde/0.4.5 \ - crate://crates.io/errno-dragonfly/0.1.2 \ - crate://crates.io/errno/0.2.8 \ - crate://crates.io/errno/0.3.9 \ - crate://crates.io/evdev/0.12.2 \ - crate://crates.io/event-listener-strategy/0.5.2 \ - crate://crates.io/event-listener/2.5.3 \ - crate://crates.io/event-listener/3.1.0 \ - crate://crates.io/event-listener/5.3.1 \ - crate://crates.io/fastrand/1.9.0 \ - crate://crates.io/fastrand/2.1.0 \ - crate://crates.io/fdeflate/0.3.4 \ - crate://crates.io/femme/2.2.1 \ - crate://crates.io/flate2/1.0.30 \ - crate://crates.io/float-cmp/0.8.0 \ - crate://crates.io/fnv/1.0.7 \ - crate://crates.io/foreign-types-macros/0.2.3 \ - crate://crates.io/foreign-types-shared/0.3.1 \ - crate://crates.io/foreign-types/0.5.0 \ - crate://crates.io/form_urlencoded/1.2.1 \ - crate://crates.io/framebuffer/0.3.1 \ - crate://crates.io/funty/2.0.0 \ - crate://crates.io/futures-channel/0.3.30 \ - crate://crates.io/futures-core/0.3.30 \ - crate://crates.io/futures-executor/0.3.30 \ - crate://crates.io/futures-io/0.3.30 \ - crate://crates.io/futures-lite/1.13.0 \ - crate://crates.io/futures-lite/2.3.0 \ - crate://crates.io/futures-macro/0.3.30 \ - crate://crates.io/futures-sink/0.3.30 \ - crate://crates.io/futures-task/0.3.30 \ - crate://crates.io/futures-util/0.3.30 \ - crate://crates.io/futures/0.3.30 \ - crate://crates.io/generic-array/0.14.7 \ - crate://crates.io/getrandom/0.1.16 \ - crate://crates.io/getrandom/0.2.15 \ - crate://crates.io/ghash/0.3.1 \ - crate://crates.io/gimli/0.29.0 \ - crate://crates.io/gloo-timers/0.2.6 \ - crate://crates.io/gpio-cdev/0.5.1 \ - crate://crates.io/hashbrown/0.14.5 \ - crate://crates.io/hermit-abi/0.3.9 \ - crate://crates.io/hermit-abi/0.4.0 \ - crate://crates.io/hex/0.4.3 \ - crate://crates.io/hkdf/0.10.0 \ - crate://crates.io/hmac/0.10.1 \ - crate://crates.io/hmac/0.8.1 \ - crate://crates.io/html-escape/0.2.13 \ - crate://crates.io/http-client/6.5.3 \ - crate://crates.io/http-types/2.12.0 \ - crate://crates.io/http/0.2.12 \ - crate://crates.io/httparse/1.9.4 \ - crate://crates.io/humantime/2.1.0 \ - crate://crates.io/iana-time-zone-haiku/0.1.2 \ - crate://crates.io/iana-time-zone/0.1.60 \ - crate://crates.io/idna/0.5.0 \ - crate://crates.io/indexmap/2.3.0 \ - crate://crates.io/industrial-io/0.5.2 \ - crate://crates.io/infer/0.2.3 \ - crate://crates.io/instant/0.1.13 \ - crate://crates.io/io-lifetimes/1.0.11 \ - crate://crates.io/is-terminal/0.4.12 \ - crate://crates.io/itoa/1.0.11 \ - crate://crates.io/js-sys/0.3.69 \ - crate://crates.io/kv-log-macro/1.0.7 \ - crate://crates.io/lazy_static/1.5.0 \ - crate://crates.io/lexical-core/0.7.6 \ - crate://crates.io/libc/0.2.155 \ - crate://crates.io/libiio-sys/0.3.1 \ - crate://crates.io/libsystemd-sys/0.9.3 \ - crate://crates.io/linux-raw-sys/0.3.8 \ - crate://crates.io/linux-raw-sys/0.4.14 \ - crate://crates.io/lock_api/0.4.12 \ - crate://crates.io/log/0.4.22 \ - crate://crates.io/memchr/2.7.4 \ - crate://crates.io/memmap/0.7.0 \ - crate://crates.io/memoffset/0.6.5 \ - crate://crates.io/memoffset/0.7.1 \ - crate://crates.io/memoffset/0.9.1 \ - crate://crates.io/micromath/1.1.1 \ - crate://crates.io/mime/0.3.17 \ - crate://crates.io/mime_guess/2.0.5 \ - crate://crates.io/miniz_oxide/0.7.4 \ - crate://crates.io/mqtt-protocol/0.11.2 \ - crate://crates.io/nix/0.23.2 \ - crate://crates.io/nix/0.26.4 \ - crate://crates.io/nom/5.1.3 \ - crate://crates.io/num-traits/0.2.19 \ - crate://crates.io/num_cpus/1.16.0 \ - crate://crates.io/numtoa/0.2.4 \ - crate://crates.io/object/0.36.2 \ - crate://crates.io/once_cell/1.19.0 \ - crate://crates.io/opaque-debug/0.3.1 \ - crate://crates.io/ordered-stream/0.2.0 \ - crate://crates.io/parking/2.2.0 \ - crate://crates.io/parking_lot_core/0.9.10 \ - crate://crates.io/percent-encoding/2.3.1 \ - crate://crates.io/pin-project-internal/1.1.5 \ - crate://crates.io/pin-project-lite/0.1.12 \ - crate://crates.io/pin-project-lite/0.2.14 \ - crate://crates.io/pin-project/1.1.5 \ - crate://crates.io/pin-utils/0.1.0 \ - crate://crates.io/piper/0.2.3 \ - crate://crates.io/pkg-config/0.3.30 \ - crate://crates.io/png/0.17.13 \ - crate://crates.io/polling/2.8.0 \ - crate://crates.io/polling/3.7.2 \ - crate://crates.io/polyval/0.4.5 \ - crate://crates.io/ppv-lite86/0.2.18 \ - crate://crates.io/proc-macro-crate/1.3.1 \ - crate://crates.io/proc-macro-hack/0.5.20+deprecated \ - crate://crates.io/proc-macro2/1.0.86 \ - crate://crates.io/quote/1.0.36 \ - crate://crates.io/radium/0.7.0 \ - crate://crates.io/rand/0.7.3 \ - crate://crates.io/rand/0.8.5 \ - crate://crates.io/rand_chacha/0.2.2 \ - crate://crates.io/rand_chacha/0.3.1 \ - crate://crates.io/rand_core/0.5.1 \ - crate://crates.io/rand_core/0.6.4 \ - crate://crates.io/rand_hc/0.2.0 \ - crate://crates.io/redox_syscall/0.5.3 \ - crate://crates.io/regex-automata/0.4.7 \ - crate://crates.io/regex-syntax/0.8.4 \ - crate://crates.io/regex/1.10.5 \ - crate://crates.io/route-recognizer/0.2.0 \ - crate://crates.io/rustc-demangle/0.1.24 \ - crate://crates.io/rustc_version/0.2.3 \ - crate://crates.io/rustix/0.37.27 \ - crate://crates.io/rustix/0.38.34 \ - crate://crates.io/rustversion/1.0.17 \ - crate://crates.io/ryu/1.0.18 \ - crate://crates.io/scopeguard/1.2.0 \ - crate://crates.io/semver-parser/0.7.0 \ - crate://crates.io/semver/0.9.0 \ - crate://crates.io/serde/1.0.204 \ - crate://crates.io/serde_derive/1.0.204 \ - crate://crates.io/serde_fmt/1.0.3 \ - crate://crates.io/serde_json/1.0.122 \ - crate://crates.io/serde_qs/0.8.5 \ - crate://crates.io/serde_repr/0.1.19 \ - crate://crates.io/serde_urlencoded/0.7.1 \ - crate://crates.io/serde_yaml/0.9.34+deprecated \ - crate://crates.io/sha-1/0.10.1 \ - crate://crates.io/sha1/0.10.6 \ - crate://crates.io/sha1/0.6.1 \ - crate://crates.io/sha1_smol/1.0.1 \ - crate://crates.io/sha2/0.9.9 \ - crate://crates.io/signal-hook-registry/1.4.2 \ - crate://crates.io/simd-adler32/0.3.7 \ - crate://crates.io/slab/0.4.9 \ - crate://crates.io/smallvec/1.13.2 \ - crate://crates.io/socket2/0.4.10 \ - crate://crates.io/standback/0.2.17 \ - crate://crates.io/static_assertions/1.1.0 \ - crate://crates.io/stdweb-derive/0.5.3 \ - crate://crates.io/stdweb-internal-macros/0.2.9 \ - crate://crates.io/stdweb-internal-runtime/0.1.5 \ - crate://crates.io/stdweb/0.4.20 \ - crate://crates.io/subtle/2.4.1 \ - crate://crates.io/surf/2.3.2 \ - crate://crates.io/sval/2.13.0 \ - crate://crates.io/sval_buffer/2.13.0 \ - crate://crates.io/sval_dynamic/2.13.0 \ - crate://crates.io/sval_fmt/2.13.0 \ - crate://crates.io/sval_json/2.13.0 \ - crate://crates.io/sval_nested/2.13.0 \ - crate://crates.io/sval_ref/2.13.0 \ - crate://crates.io/sval_serde/2.13.0 \ - crate://crates.io/syn/1.0.109 \ - crate://crates.io/syn/2.0.72 \ - crate://crates.io/sysfs-class/0.1.3 \ - crate://crates.io/systemd/0.10.0 \ - crate://crates.io/tap/1.0.1 \ - crate://crates.io/tempfile/3.10.1 \ - crate://crates.io/termcolor/1.4.1 \ - crate://crates.io/thiserror-impl/1.0.63 \ - crate://crates.io/thiserror/1.0.63 \ - crate://crates.io/thread-priority/0.13.1 \ - crate://crates.io/tide/0.16.0 \ - crate://crates.io/time-macros-impl/0.1.2 \ - crate://crates.io/time-macros/0.1.1 \ - crate://crates.io/time/0.2.27 \ - crate://crates.io/tinyvec/1.8.0 \ - crate://crates.io/tinyvec_macros/0.1.1 \ - crate://crates.io/tokio/1.39.2 \ - crate://crates.io/toml_datetime/0.6.8 \ - crate://crates.io/toml_edit/0.19.15 \ - crate://crates.io/tracing-attributes/0.1.27 \ - crate://crates.io/tracing-core/0.1.32 \ - crate://crates.io/tracing/0.1.40 \ - crate://crates.io/tungstenite/0.19.0 \ - crate://crates.io/typeid/1.0.0 \ - crate://crates.io/typenum/1.17.0 \ - crate://crates.io/uds_windows/1.1.0 \ - crate://crates.io/unicase/2.7.0 \ - crate://crates.io/unicode-bidi/0.3.15 \ - crate://crates.io/unicode-ident/1.0.12 \ - crate://crates.io/unicode-normalization/0.1.23 \ - crate://crates.io/unique-token/0.2.0 \ - crate://crates.io/universal-hash/0.4.1 \ - crate://crates.io/unsafe-libyaml/0.2.11 \ - crate://crates.io/url/2.5.2 \ - crate://crates.io/utf-8/0.7.6 \ - crate://crates.io/utf8-cstr/0.1.6 \ - crate://crates.io/utf8-width/0.1.7 \ - crate://crates.io/value-bag-serde1/1.9.0 \ - crate://crates.io/value-bag-sval2/1.9.0 \ - crate://crates.io/value-bag/1.9.0 \ - crate://crates.io/version_check/0.9.5 \ - crate://crates.io/waker-fn/1.2.0 \ - crate://crates.io/wasi/0.11.0+wasi-snapshot-preview1 \ - crate://crates.io/wasi/0.9.0+wasi-snapshot-preview1 \ - crate://crates.io/wasm-bindgen-backend/0.2.92 \ - crate://crates.io/wasm-bindgen-futures/0.4.42 \ - crate://crates.io/wasm-bindgen-macro-support/0.2.92 \ - crate://crates.io/wasm-bindgen-macro/0.2.92 \ - crate://crates.io/wasm-bindgen-shared/0.2.92 \ - crate://crates.io/wasm-bindgen/0.2.92 \ - crate://crates.io/web-sys/0.3.69 \ - crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \ - crate://crates.io/winapi-util/0.1.8 \ - crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \ - crate://crates.io/winapi/0.3.9 \ - crate://crates.io/windows-core/0.52.0 \ - crate://crates.io/windows-sys/0.48.0 \ - crate://crates.io/windows-sys/0.52.0 \ - crate://crates.io/windows-targets/0.48.5 \ - crate://crates.io/windows-targets/0.52.6 \ - crate://crates.io/windows_aarch64_gnullvm/0.48.5 \ - crate://crates.io/windows_aarch64_gnullvm/0.52.6 \ - crate://crates.io/windows_aarch64_msvc/0.48.5 \ - crate://crates.io/windows_aarch64_msvc/0.52.6 \ - crate://crates.io/windows_i686_gnu/0.48.5 \ - crate://crates.io/windows_i686_gnu/0.52.6 \ - crate://crates.io/windows_i686_gnullvm/0.52.6 \ - crate://crates.io/windows_i686_msvc/0.48.5 \ - crate://crates.io/windows_i686_msvc/0.52.6 \ - crate://crates.io/windows_x86_64_gnu/0.48.5 \ - crate://crates.io/windows_x86_64_gnu/0.52.6 \ - crate://crates.io/windows_x86_64_gnullvm/0.48.5 \ - crate://crates.io/windows_x86_64_gnullvm/0.52.6 \ - crate://crates.io/windows_x86_64_msvc/0.48.5 \ - crate://crates.io/windows_x86_64_msvc/0.52.6 \ - crate://crates.io/winnow/0.5.40 \ - crate://crates.io/wyz/0.5.1 \ - crate://crates.io/xdg-home/1.2.0 \ - crate://crates.io/zbus/3.15.2 \ - crate://crates.io/zbus_macros/3.15.2 \ - crate://crates.io/zbus_names/2.6.1 \ - crate://crates.io/zerocopy-derive/0.6.6 \ - crate://crates.io/zerocopy/0.6.6 \ - crate://crates.io/zvariant/3.15.2 \ - crate://crates.io/zvariant_derive/3.15.2 \ - crate://crates.io/zvariant_utils/1.0.1 \ -" - -SRC_URI[addr2line-0.22.0.sha256sum] = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -SRC_URI[adler-1.0.2.sha256sum] = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -SRC_URI[aead-0.3.2.sha256sum] = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" -SRC_URI[aes-gcm-0.8.0.sha256sum] = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" -SRC_URI[aes-soft-0.6.4.sha256sum] = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -SRC_URI[aes-0.6.0.sha256sum] = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -SRC_URI[aesni-0.10.0.sha256sum] = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -SRC_URI[aho-corasick-1.1.3.sha256sum] = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -SRC_URI[android-tzdata-0.1.1.sha256sum] = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" -SRC_URI[android_system_properties-0.1.5.sha256sum] = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -SRC_URI[anyhow-1.0.86.sha256sum] = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -SRC_URI[arrayref-0.3.8.sha256sum] = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" -SRC_URI[arrayvec-0.5.2.sha256sum] = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" -SRC_URI[async-attributes-1.1.2.sha256sum] = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -SRC_URI[async-broadcast-0.5.1.sha256sum] = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" -SRC_URI[async-channel-1.9.0.sha256sum] = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -SRC_URI[async-channel-2.3.1.sha256sum] = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -SRC_URI[async-dup-1.2.4.sha256sum] = "7c2886ab563af5038f79ec016dd7b87947ed138b794e8dd64992962c9cca0411" -SRC_URI[async-executor-1.13.0.sha256sum] = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" -SRC_URI[async-fs-1.6.0.sha256sum] = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -SRC_URI[async-global-executor-2.4.1.sha256sum] = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -SRC_URI[async-h1-2.3.4.sha256sum] = "5d1d1dae8cb2c4258a79d6ed088b7fb9b4763bf4e9b22d040779761e046a2971" -SRC_URI[async-io-1.13.0.sha256sum] = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -SRC_URI[async-io-2.3.3.sha256sum] = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" -SRC_URI[async-lock-2.8.0.sha256sum] = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -SRC_URI[async-lock-3.4.0.sha256sum] = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -SRC_URI[async-process-1.8.1.sha256sum] = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -SRC_URI[async-recursion-1.1.1.sha256sum] = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -SRC_URI[async-session-2.0.1.sha256sum] = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" -SRC_URI[async-signal-0.2.9.sha256sum] = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" -SRC_URI[async-sse-4.1.0.sha256sum] = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" -SRC_URI[async-sse-5.1.0.sha256sum] = "2e6fa871e4334a622afd6bb2f611635e8083a6f5e2936c0f90f37c7ef9856298" -SRC_URI[async-std-1.12.0.sha256sum] = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -SRC_URI[async-task-4.7.1.sha256sum] = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -SRC_URI[async-trait-0.1.81.sha256sum] = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -SRC_URI[async-tungstenite-0.22.2.sha256sum] = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58" -SRC_URI[atomic-waker-1.1.2.sha256sum] = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -SRC_URI[autocfg-1.3.0.sha256sum] = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -SRC_URI[az-1.2.1.sha256sum] = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" -SRC_URI[backtrace-0.3.73.sha256sum] = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -SRC_URI[base-x-0.2.11.sha256sum] = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" -SRC_URI[base64-0.12.3.sha256sum] = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" -SRC_URI[base64-0.13.1.sha256sum] = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -SRC_URI[base64-0.21.7.sha256sum] = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -SRC_URI[bincode-1.3.3.sha256sum] = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -SRC_URI[bitflags-1.3.2.sha256sum] = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -SRC_URI[bitflags-2.6.0.sha256sum] = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -SRC_URI[bitvec-1.0.1.sha256sum] = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -SRC_URI[blake3-0.3.8.sha256sum] = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -SRC_URI[block-buffer-0.10.4.sha256sum] = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -SRC_URI[block-buffer-0.9.0.sha256sum] = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -SRC_URI[blocking-1.6.1.sha256sum] = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -SRC_URI[build-env-0.3.1.sha256sum] = "1522ac6ee801a11bf9ef3f80403f4ede6eb41291fac3dde3de09989679305f25" -SRC_URI[bumpalo-3.16.0.sha256sum] = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -SRC_URI[byteorder-1.5.0.sha256sum] = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -SRC_URI[bytes-1.7.1.sha256sum] = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" -SRC_URI[cc-1.1.7.sha256sum] = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" -SRC_URI[cfg-if-0.1.10.sha256sum] = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -SRC_URI[cfg-if-1.0.0.sha256sum] = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -SRC_URI[chrono-0.4.38.sha256sum] = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -SRC_URI[cipher-0.2.5.sha256sum] = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" -SRC_URI[concurrent-queue-2.5.0.sha256sum] = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -SRC_URI[config-0.10.1.sha256sum] = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" -SRC_URI[const_fn-0.4.10.sha256sum] = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" -SRC_URI[constant_time_eq-0.1.5.sha256sum] = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -SRC_URI[cookie-0.14.4.sha256sum] = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" -SRC_URI[core-foundation-sys-0.8.6.sha256sum] = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -SRC_URI[cpufeatures-0.2.12.sha256sum] = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -SRC_URI[cpuid-bool-0.2.0.sha256sum] = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" -SRC_URI[crc32fast-1.4.2.sha256sum] = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -SRC_URI[crossbeam-queue-0.3.11.sha256sum] = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -SRC_URI[crossbeam-utils-0.8.20.sha256sum] = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" -SRC_URI[crypto-common-0.1.6.sha256sum] = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -SRC_URI[crypto-mac-0.10.1.sha256sum] = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" -SRC_URI[crypto-mac-0.8.0.sha256sum] = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -SRC_URI[cstr-argument-0.1.2.sha256sum] = "b6bd9c8e659a473bce955ae5c35b116af38af11a7acb0b480e01f3ed348aeb40" -SRC_URI[ctr-0.6.0.sha256sum] = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" -SRC_URI[dashmap-5.5.3.sha256sum] = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -SRC_URI[data-encoding-2.6.0.sha256sum] = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" -SRC_URI[deadpool-0.7.0.sha256sum] = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d" -SRC_URI[derivative-2.2.0.sha256sum] = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -SRC_URI[digest-0.10.7.sha256sum] = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -SRC_URI[digest-0.9.0.sha256sum] = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -SRC_URI[discard-1.0.4.sha256sum] = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" -SRC_URI[embedded-graphics-core-0.3.3.sha256sum] = "b8b1239db5f3eeb7e33e35bd10bd014e7b2537b17e071f726a09351431337cfa" -SRC_URI[embedded-graphics-0.7.1.sha256sum] = "750082c65094fbcc4baf9ba31583ce9a8bb7f52cadfb96f6164b1bc7f922f32b" -SRC_URI[enumflags2-0.7.10.sha256sum] = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" -SRC_URI[enumflags2_derive-0.7.10.sha256sum] = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" -SRC_URI[env_logger-0.10.2.sha256sum] = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -SRC_URI[equivalent-1.0.1.sha256sum] = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -SRC_URI[erased-serde-0.4.5.sha256sum] = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" -SRC_URI[errno-dragonfly-0.1.2.sha256sum] = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -SRC_URI[errno-0.2.8.sha256sum] = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -SRC_URI[errno-0.3.9.sha256sum] = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -SRC_URI[evdev-0.12.2.sha256sum] = "ab6055a93a963297befb0f4f6e18f314aec9767a4bbe88b151126df2433610a7" -SRC_URI[event-listener-strategy-0.5.2.sha256sum] = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -SRC_URI[event-listener-2.5.3.sha256sum] = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -SRC_URI[event-listener-3.1.0.sha256sum] = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -SRC_URI[event-listener-5.3.1.sha256sum] = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -SRC_URI[fastrand-1.9.0.sha256sum] = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -SRC_URI[fastrand-2.1.0.sha256sum] = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -SRC_URI[fdeflate-0.3.4.sha256sum] = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" -SRC_URI[femme-2.2.1.sha256sum] = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" -SRC_URI[flate2-1.0.30.sha256sum] = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -SRC_URI[float-cmp-0.8.0.sha256sum] = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" -SRC_URI[fnv-1.0.7.sha256sum] = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -SRC_URI[foreign-types-macros-0.2.3.sha256sum] = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -SRC_URI[foreign-types-shared-0.3.1.sha256sum] = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" -SRC_URI[foreign-types-0.5.0.sha256sum] = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -SRC_URI[form_urlencoded-1.2.1.sha256sum] = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -SRC_URI[framebuffer-0.3.1.sha256sum] = "878caaaf1bb92c9f707dc6eef90933e07e913dac4bb8e11e145eaabaa94ef149" -SRC_URI[funty-2.0.0.sha256sum] = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -SRC_URI[futures-channel-0.3.30.sha256sum] = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -SRC_URI[futures-core-0.3.30.sha256sum] = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -SRC_URI[futures-executor-0.3.30.sha256sum] = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -SRC_URI[futures-io-0.3.30.sha256sum] = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -SRC_URI[futures-lite-1.13.0.sha256sum] = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -SRC_URI[futures-lite-2.3.0.sha256sum] = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -SRC_URI[futures-macro-0.3.30.sha256sum] = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -SRC_URI[futures-sink-0.3.30.sha256sum] = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" -SRC_URI[futures-task-0.3.30.sha256sum] = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" -SRC_URI[futures-util-0.3.30.sha256sum] = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -SRC_URI[futures-0.3.30.sha256sum] = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -SRC_URI[generic-array-0.14.7.sha256sum] = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -SRC_URI[getrandom-0.1.16.sha256sum] = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -SRC_URI[getrandom-0.2.15.sha256sum] = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -SRC_URI[ghash-0.3.1.sha256sum] = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" -SRC_URI[gimli-0.29.0.sha256sum] = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" -SRC_URI[gloo-timers-0.2.6.sha256sum] = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -SRC_URI[gpio-cdev-0.5.1.sha256sum] = "409296415b8abc7b47e5b77096faae14595c53724972da227434fc8f4b05ec8b" -SRC_URI[hashbrown-0.14.5.sha256sum] = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -SRC_URI[hermit-abi-0.3.9.sha256sum] = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -SRC_URI[hermit-abi-0.4.0.sha256sum] = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" -SRC_URI[hex-0.4.3.sha256sum] = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -SRC_URI[hkdf-0.10.0.sha256sum] = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" -SRC_URI[hmac-0.10.1.sha256sum] = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" -SRC_URI[hmac-0.8.1.sha256sum] = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -SRC_URI[html-escape-0.2.13.sha256sum] = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -SRC_URI[http-client-6.5.3.sha256sum] = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" -SRC_URI[http-types-2.12.0.sha256sum] = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" -SRC_URI[http-0.2.12.sha256sum] = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -SRC_URI[httparse-1.9.4.sha256sum] = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" -SRC_URI[humantime-2.1.0.sha256sum] = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -SRC_URI[iana-time-zone-haiku-0.1.2.sha256sum] = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -SRC_URI[iana-time-zone-0.1.60.sha256sum] = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -SRC_URI[idna-0.5.0.sha256sum] = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -SRC_URI[indexmap-2.3.0.sha256sum] = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" -SRC_URI[industrial-io-0.5.2.sha256sum] = "3948a8818efcd4e0e189df60d79b58a9f4ae20b87437b2fde4dc4ca5b520c4ed" -SRC_URI[infer-0.2.3.sha256sum] = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" -SRC_URI[instant-0.1.13.sha256sum] = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -SRC_URI[io-lifetimes-1.0.11.sha256sum] = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -SRC_URI[is-terminal-0.4.12.sha256sum] = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" -SRC_URI[itoa-1.0.11.sha256sum] = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -SRC_URI[js-sys-0.3.69.sha256sum] = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -SRC_URI[kv-log-macro-1.0.7.sha256sum] = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -SRC_URI[lazy_static-1.5.0.sha256sum] = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -SRC_URI[lexical-core-0.7.6.sha256sum] = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -SRC_URI[libc-0.2.155.sha256sum] = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -SRC_URI[libiio-sys-0.3.1.sha256sum] = "6d6c255d73b8f345bb10550b44e93b2b5c910a31ce72ee2528c5dbc028cf24ec" -SRC_URI[libsystemd-sys-0.9.3.sha256sum] = "ed080163caa59cc29b34bce2209b737149a4bac148cd9a8b04e4c12822798119" -SRC_URI[linux-raw-sys-0.3.8.sha256sum] = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" -SRC_URI[linux-raw-sys-0.4.14.sha256sum] = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -SRC_URI[lock_api-0.4.12.sha256sum] = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -SRC_URI[log-0.4.22.sha256sum] = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -SRC_URI[memchr-2.7.4.sha256sum] = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -SRC_URI[memmap-0.7.0.sha256sum] = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" -SRC_URI[memoffset-0.6.5.sha256sum] = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -SRC_URI[memoffset-0.7.1.sha256sum] = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -SRC_URI[memoffset-0.9.1.sha256sum] = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -SRC_URI[micromath-1.1.1.sha256sum] = "bc4010833aea396656c2f91ee704d51a6f1329ec2ab56ffd00bfd56f7481ea94" -SRC_URI[mime-0.3.17.sha256sum] = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -SRC_URI[mime_guess-2.0.5.sha256sum] = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -SRC_URI[miniz_oxide-0.7.4.sha256sum] = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -SRC_URI[mqtt-protocol-0.11.2.sha256sum] = "ca0b17380dc69fbcf5f967828cfd10e55028ba83a57da1f580c5b0792ab807ac" -SRC_URI[nix-0.23.2.sha256sum] = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -SRC_URI[nix-0.26.4.sha256sum] = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -SRC_URI[nom-5.1.3.sha256sum] = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -SRC_URI[num-traits-0.2.19.sha256sum] = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -SRC_URI[num_cpus-1.16.0.sha256sum] = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -SRC_URI[numtoa-0.2.4.sha256sum] = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" -SRC_URI[object-0.36.2.sha256sum] = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" -SRC_URI[once_cell-1.19.0.sha256sum] = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -SRC_URI[opaque-debug-0.3.1.sha256sum] = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -SRC_URI[ordered-stream-0.2.0.sha256sum] = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -SRC_URI[parking-2.2.0.sha256sum] = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" -SRC_URI[parking_lot_core-0.9.10.sha256sum] = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -SRC_URI[percent-encoding-2.3.1.sha256sum] = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -SRC_URI[pin-project-internal-1.1.5.sha256sum] = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -SRC_URI[pin-project-lite-0.1.12.sha256sum] = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" -SRC_URI[pin-project-lite-0.2.14.sha256sum] = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -SRC_URI[pin-project-1.1.5.sha256sum] = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -SRC_URI[pin-utils-0.1.0.sha256sum] = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -SRC_URI[piper-0.2.3.sha256sum] = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" -SRC_URI[pkg-config-0.3.30.sha256sum] = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -SRC_URI[png-0.17.13.sha256sum] = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" -SRC_URI[polling-2.8.0.sha256sum] = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -SRC_URI[polling-3.7.2.sha256sum] = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" -SRC_URI[polyval-0.4.5.sha256sum] = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" -SRC_URI[ppv-lite86-0.2.18.sha256sum] = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" -SRC_URI[proc-macro-crate-1.3.1.sha256sum] = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -SRC_URI[proc-macro-hack-0.5.20+deprecated.sha256sum] = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" -SRC_URI[proc-macro2-1.0.86.sha256sum] = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -SRC_URI[quote-1.0.36.sha256sum] = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -SRC_URI[radium-0.7.0.sha256sum] = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -SRC_URI[rand-0.7.3.sha256sum] = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -SRC_URI[rand-0.8.5.sha256sum] = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -SRC_URI[rand_chacha-0.2.2.sha256sum] = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -SRC_URI[rand_chacha-0.3.1.sha256sum] = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -SRC_URI[rand_core-0.5.1.sha256sum] = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -SRC_URI[rand_core-0.6.4.sha256sum] = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -SRC_URI[rand_hc-0.2.0.sha256sum] = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -SRC_URI[redox_syscall-0.5.3.sha256sum] = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -SRC_URI[regex-automata-0.4.7.sha256sum] = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -SRC_URI[regex-syntax-0.8.4.sha256sum] = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -SRC_URI[regex-1.10.5.sha256sum] = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" -SRC_URI[route-recognizer-0.2.0.sha256sum] = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" -SRC_URI[rustc-demangle-0.1.24.sha256sum] = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -SRC_URI[rustc_version-0.2.3.sha256sum] = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -SRC_URI[rustix-0.37.27.sha256sum] = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -SRC_URI[rustix-0.38.34.sha256sum] = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -SRC_URI[rustversion-1.0.17.sha256sum] = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -SRC_URI[ryu-1.0.18.sha256sum] = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -SRC_URI[scopeguard-1.2.0.sha256sum] = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -SRC_URI[semver-parser-0.7.0.sha256sum] = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -SRC_URI[semver-0.9.0.sha256sum] = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -SRC_URI[serde-1.0.204.sha256sum] = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" -SRC_URI[serde_derive-1.0.204.sha256sum] = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" -SRC_URI[serde_fmt-1.0.3.sha256sum] = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -SRC_URI[serde_json-1.0.122.sha256sum] = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" -SRC_URI[serde_qs-0.8.5.sha256sum] = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -SRC_URI[serde_repr-0.1.19.sha256sum] = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -SRC_URI[serde_urlencoded-0.7.1.sha256sum] = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -SRC_URI[serde_yaml-0.9.34+deprecated.sha256sum] = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -SRC_URI[sha-1-0.10.1.sha256sum] = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -SRC_URI[sha1-0.10.6.sha256sum] = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -SRC_URI[sha1-0.6.1.sha256sum] = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -SRC_URI[sha1_smol-1.0.1.sha256sum] = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" -SRC_URI[sha2-0.9.9.sha256sum] = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -SRC_URI[signal-hook-registry-1.4.2.sha256sum] = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -SRC_URI[simd-adler32-0.3.7.sha256sum] = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -SRC_URI[slab-0.4.9.sha256sum] = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -SRC_URI[smallvec-1.13.2.sha256sum] = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -SRC_URI[socket2-0.4.10.sha256sum] = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -SRC_URI[standback-0.2.17.sha256sum] = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -SRC_URI[static_assertions-1.1.0.sha256sum] = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -SRC_URI[stdweb-derive-0.5.3.sha256sum] = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -SRC_URI[stdweb-internal-macros-0.2.9.sha256sum] = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -SRC_URI[stdweb-internal-runtime-0.1.5.sha256sum] = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -SRC_URI[stdweb-0.4.20.sha256sum] = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -SRC_URI[subtle-2.4.1.sha256sum] = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -SRC_URI[surf-2.3.2.sha256sum] = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" -SRC_URI[sval-2.13.0.sha256sum] = "53eb957fbc79a55306d5d25d87daf3627bc3800681491cda0709eef36c748bfe" -SRC_URI[sval_buffer-2.13.0.sha256sum] = "96e860aef60e9cbf37888d4953a13445abf523c534640d1f6174d310917c410d" -SRC_URI[sval_dynamic-2.13.0.sha256sum] = "ea3f2b07929a1127d204ed7cb3905049381708245727680e9139dac317ed556f" -SRC_URI[sval_fmt-2.13.0.sha256sum] = "c4e188677497de274a1367c4bda15bd2296de4070d91729aac8f0a09c1abf64d" -SRC_URI[sval_json-2.13.0.sha256sum] = "32f456c07dae652744781f2245d5e3b78e6a9ebad70790ac11eb15dbdbce5282" -SRC_URI[sval_nested-2.13.0.sha256sum] = "886feb24709f0476baaebbf9ac10671a50163caa7e439d7a7beb7f6d81d0a6fb" -SRC_URI[sval_ref-2.13.0.sha256sum] = "be2e7fc517d778f44f8cb64140afa36010999565528d48985f55e64d45f369ce" -SRC_URI[sval_serde-2.13.0.sha256sum] = "79bf66549a997ff35cd2114a27ac4b0c2843280f2cfa84b240d169ecaa0add46" -SRC_URI[syn-1.0.109.sha256sum] = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -SRC_URI[syn-2.0.72.sha256sum] = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" -SRC_URI[sysfs-class-0.1.3.sha256sum] = "5e1bbcf869732c45a77898f7f61ed6d411dfc37613517e444842f58d428856d1" -SRC_URI[systemd-0.10.0.sha256sum] = "afec0101d9ae8ab26aedf0840109df689938ea7e538aa03df4369f1854f11562" -SRC_URI[tap-1.0.1.sha256sum] = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -SRC_URI[tempfile-3.10.1.sha256sum] = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -SRC_URI[termcolor-1.4.1.sha256sum] = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -SRC_URI[thiserror-impl-1.0.63.sha256sum] = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -SRC_URI[thiserror-1.0.63.sha256sum] = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -SRC_URI[thread-priority-0.13.1.sha256sum] = "0c56ce92f1285eaaa11fc1a3201e25de97898c50e87caa4c2aee836fe05288de" -SRC_URI[tide-0.16.0.sha256sum] = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" -SRC_URI[time-macros-impl-0.1.2.sha256sum] = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -SRC_URI[time-macros-0.1.1.sha256sum] = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -SRC_URI[time-0.2.27.sha256sum] = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" -SRC_URI[tinyvec-1.8.0.sha256sum] = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" -SRC_URI[tinyvec_macros-0.1.1.sha256sum] = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -SRC_URI[tokio-1.39.2.sha256sum] = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" -SRC_URI[toml_datetime-0.6.8.sha256sum] = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -SRC_URI[toml_edit-0.19.15.sha256sum] = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -SRC_URI[tracing-attributes-0.1.27.sha256sum] = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -SRC_URI[tracing-core-0.1.32.sha256sum] = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -SRC_URI[tracing-0.1.40.sha256sum] = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -SRC_URI[tungstenite-0.19.0.sha256sum] = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" -SRC_URI[typeid-1.0.0.sha256sum] = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" -SRC_URI[typenum-1.17.0.sha256sum] = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -SRC_URI[uds_windows-1.1.0.sha256sum] = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -SRC_URI[unicase-2.7.0.sha256sum] = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -SRC_URI[unicode-bidi-0.3.15.sha256sum] = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -SRC_URI[unicode-ident-1.0.12.sha256sum] = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -SRC_URI[unicode-normalization-0.1.23.sha256sum] = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -SRC_URI[unique-token-0.2.0.sha256sum] = "d3be9e39e944fa35b07f5eb280902bf4d2dc29dfbc26175230a0d0ea124a7b66" -SRC_URI[universal-hash-0.4.1.sha256sum] = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -SRC_URI[unsafe-libyaml-0.2.11.sha256sum] = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -SRC_URI[url-2.5.2.sha256sum] = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -SRC_URI[utf-8-0.7.6.sha256sum] = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -SRC_URI[utf8-cstr-0.1.6.sha256sum] = "55bcbb425141152b10d5693095950b51c3745d019363fc2929ffd8f61449b628" -SRC_URI[utf8-width-0.1.7.sha256sum] = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" -SRC_URI[value-bag-serde1-1.9.0.sha256sum] = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" -SRC_URI[value-bag-sval2-1.9.0.sha256sum] = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" -SRC_URI[value-bag-1.9.0.sha256sum] = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" -SRC_URI[version_check-0.9.5.sha256sum] = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -SRC_URI[waker-fn-1.2.0.sha256sum] = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" -SRC_URI[wasi-0.11.0+wasi-snapshot-preview1.sha256sum] = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -SRC_URI[wasi-0.9.0+wasi-snapshot-preview1.sha256sum] = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -SRC_URI[wasm-bindgen-backend-0.2.92.sha256sum] = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -SRC_URI[wasm-bindgen-futures-0.4.42.sha256sum] = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -SRC_URI[wasm-bindgen-macro-support-0.2.92.sha256sum] = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -SRC_URI[wasm-bindgen-macro-0.2.92.sha256sum] = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -SRC_URI[wasm-bindgen-shared-0.2.92.sha256sum] = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -SRC_URI[wasm-bindgen-0.2.92.sha256sum] = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -SRC_URI[web-sys-0.3.69.sha256sum] = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -SRC_URI[winapi-i686-pc-windows-gnu-0.4.0.sha256sum] = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -SRC_URI[winapi-util-0.1.8.sha256sum] = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -SRC_URI[winapi-x86_64-pc-windows-gnu-0.4.0.sha256sum] = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -SRC_URI[winapi-0.3.9.sha256sum] = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -SRC_URI[windows-core-0.52.0.sha256sum] = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -SRC_URI[windows-sys-0.48.0.sha256sum] = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -SRC_URI[windows-sys-0.52.0.sha256sum] = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -SRC_URI[windows-targets-0.48.5.sha256sum] = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -SRC_URI[windows-targets-0.52.6.sha256sum] = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -SRC_URI[windows_aarch64_gnullvm-0.48.5.sha256sum] = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -SRC_URI[windows_aarch64_gnullvm-0.52.6.sha256sum] = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -SRC_URI[windows_aarch64_msvc-0.48.5.sha256sum] = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -SRC_URI[windows_aarch64_msvc-0.52.6.sha256sum] = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -SRC_URI[windows_i686_gnu-0.48.5.sha256sum] = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -SRC_URI[windows_i686_gnu-0.52.6.sha256sum] = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -SRC_URI[windows_i686_gnullvm-0.52.6.sha256sum] = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -SRC_URI[windows_i686_msvc-0.48.5.sha256sum] = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -SRC_URI[windows_i686_msvc-0.52.6.sha256sum] = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -SRC_URI[windows_x86_64_gnu-0.48.5.sha256sum] = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -SRC_URI[windows_x86_64_gnu-0.52.6.sha256sum] = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -SRC_URI[windows_x86_64_gnullvm-0.48.5.sha256sum] = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -SRC_URI[windows_x86_64_gnullvm-0.52.6.sha256sum] = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -SRC_URI[windows_x86_64_msvc-0.48.5.sha256sum] = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -SRC_URI[windows_x86_64_msvc-0.52.6.sha256sum] = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -SRC_URI[winnow-0.5.40.sha256sum] = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -SRC_URI[wyz-0.5.1.sha256sum] = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -SRC_URI[xdg-home-1.2.0.sha256sum] = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" -SRC_URI[zbus-3.15.2.sha256sum] = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" -SRC_URI[zbus_macros-3.15.2.sha256sum] = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" -SRC_URI[zbus_names-2.6.1.sha256sum] = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" -SRC_URI[zerocopy-derive-0.6.6.sha256sum] = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" -SRC_URI[zerocopy-0.6.6.sha256sum] = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -SRC_URI[zvariant-3.15.2.sha256sum] = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" -SRC_URI[zvariant_derive-3.15.2.sha256sum] = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" -SRC_URI[zvariant_utils-1.0.1.sha256sum] = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" - LIC_FILES_CHKSUM = " \ file://LICENSE;md5=b234ee4d69f5fce4486a80fdaf4a4263 \ " @@ -714,4 +40,5 @@ LICENSE = "GPL-2.0-or-later" # this is useful for anything you may want to override from # what cargo-bitbake generates. include tacd-${PV}.inc +include tacd-crates.inc include tacd.inc diff --git a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0001-web-setup-inform-the-user-about-additional-headers-t.patch b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0001-web-setup-inform-the-user-about-additional-headers-t.patch new file mode 100644 index 00000000..1678522f --- /dev/null +++ b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0001-web-setup-inform-the-user-about-additional-headers-t.patch @@ -0,0 +1,37 @@ +From cb89591d94e03f056b89d4eda6d83095560c40ec Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 18:32:50 +0200 +Subject: [PATCH 1/4] web: setup: inform the user about additional headers that + are sent now +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +We include the `boot-id` and `uptime` options in the RAUC `send-headers` +config in the hopes of detecting boot-loops during update roll-out. +This was not anticipated when writing the setup page, so we add it now. + +Signed-off-by: Leonard Göhrs +--- + web/src/Setup.tsx | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/web/src/Setup.tsx b/web/src/Setup.tsx +index 816acc5..3ff0c82 100644 +--- a/web/src/Setup.tsx ++++ b/web/src/Setup.tsx +@@ -158,8 +158,9 @@ export default function Setup() { + + When polling for updates the LXA TAC will transmit the + following information to our server: The IP address the +- request is coming from, the serial number of the device +- and information on the currently installed software. ++ request is coming from, the serial number of the device, ++ the device uptime and boot id and information on the ++ currently installed software. + + + +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0002-web-indicate-to-the-user-when-a-channel-is-enabled-b.patch b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0002-web-indicate-to-the-user-when-a-channel-is-enabled-b.patch new file mode 100644 index 00000000..5df65d27 --- /dev/null +++ b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0002-web-indicate-to-the-user-when-a-channel-is-enabled-b.patch @@ -0,0 +1,53 @@ +From 9f5d2562ac4037112dfe1bc1e213b1e30bfd2400 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 18:35:36 +0200 +Subject: [PATCH 2/4] web: indicate to the user when a channel is enabled but + not primary +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The channel list in the web interface now contains a "Upgrade" column +with one of the following: + + - "Not enabled" for channels which are not enabled, which means bundles + from it can not be installed for it. + - "Not primary" (this one is new) for channels which are enabled, + but are not the primary one and are thus not polled by the native RAUC + polling feature. + - "Polling disabled" if the polling feature is not enabled. + - A spinner if we do not know the status yet. + - "Up to date" if the TAC is in sync with this update channel. + - "Upgrade" (a button) if an update is available. + +Signed-off-by: Leonard Göhrs +--- + web/src/TacComponents.tsx | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx +index e1c9967..8ef5056 100644 +--- a/web/src/TacComponents.tsx ++++ b/web/src/TacComponents.tsx +@@ -125,6 +125,7 @@ type Channel = { + url: string; + polling_interval?: Duration; + enabled: boolean; ++ primary: boolean; + bundle?: UpstreamBundle; + }; + +@@ -379,6 +380,10 @@ export function UpdateChannels(props: UpdateChannelsProps) { + return "Not enabled"; + } + ++ if (!e.primary) { ++ return "Not primary"; ++ } ++ + if (!e.bundle) { + if (enable_polling) { + return ; +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0003-web-add-toggle-switches-to-enable-automatic-installa.patch b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0003-web-add-toggle-switches-to-enable-automatic-installa.patch new file mode 100644 index 00000000..a64f8436 --- /dev/null +++ b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0003-web-add-toggle-switches-to-enable-automatic-installa.patch @@ -0,0 +1,49 @@ +From 84c6b9ecd41dfb0b79f12584c873b11f2351dc7c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 18:33:39 +0200 +Subject: [PATCH 3/4] web: add toggle switches to enable automatic installation + of updates +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Leonard Göhrs +--- + web/src/Setup.tsx | 3 +++ + web/src/TacComponents.tsx | 6 ++++++ + 2 files changed, 9 insertions(+) + +diff --git a/web/src/Setup.tsx b/web/src/Setup.tsx +index 3ff0c82..979b30b 100644 +--- a/web/src/Setup.tsx ++++ b/web/src/Setup.tsx +@@ -166,6 +166,9 @@ export default function Setup() { + + Periodically check for updates + ++ ++ Automatically install and boot updates ++ + + + ), +diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx +index 8ef5056..57249f7 100644 +--- a/web/src/TacComponents.tsx ++++ b/web/src/TacComponents.tsx +@@ -254,6 +254,12 @@ export function UpdateConfig() { + Periodically check for updates + + ++ ++ Auto Install ++ ++ Automatically install and boot updates ++ ++ + + + ); +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0004-web-use-manifest_hash-and-effective_url-when-trigger.patch b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0004-web-use-manifest_hash-and-effective_url-when-trigger.patch new file mode 100644 index 00000000..7798fac4 --- /dev/null +++ b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface/0004-web-use-manifest_hash-and-effective_url-when-trigger.patch @@ -0,0 +1,90 @@ +From 4e711b3da94e16eb94b97b4b3c7b0708d2b53b27 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= +Date: Wed, 2 Apr 2025 18:35:01 +0200 +Subject: [PATCH 4/4] web: use manifest_hash and effective_url when triggering + an install +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This ensures that the exact bundle (content) that the user agreed to +install is actually installed. + +Signed-off-by: Leonard Göhrs +--- + web/src/TacComponents.tsx | 27 ++++++++++++++++++++++++--- + 1 file changed, 24 insertions(+), 3 deletions(-) + +diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx +index 57249f7..9f18d5f 100644 +--- a/web/src/TacComponents.tsx ++++ b/web/src/TacComponents.tsx +@@ -115,6 +115,8 @@ type Duration = { + type UpstreamBundle = { + compatible: string; + version: string; ++ manifest_hash: string; ++ effective_url: string; + newer_than_installed: boolean; + }; + +@@ -129,6 +131,11 @@ type Channel = { + bundle?: UpstreamBundle; + }; + ++type UpdateRequest = { ++ manifest_hash: string; ++ url: string; ++}; ++ + interface SlotStatusProps { + setCmdHint: (hint: React.ReactNode | null) => void; + } +@@ -402,11 +409,16 @@ export function UpdateChannels(props: UpdateChannelsProps) { + return "Up to date"; + } + ++ const request: UpdateRequest = { ++ manifest_hash: e.bundle.manifest_hash, ++ url: e.bundle.effective_url, ++ }; ++ + return ( + + Upgrade + +@@ -539,7 +551,16 @@ export function UpdateNotification() { + if (channels !== undefined) { + for (let ch of channels) { + if (ch.enabled && ch.bundle && ch.bundle.newer_than_installed) { +- updates.push(ch); ++ const request: UpdateRequest = { ++ manifest_hash: ch.bundle.manifest_hash, ++ url: ch.bundle.effective_url, ++ }; ++ ++ updates.push({ ++ name: ch.name, ++ display_name: ch.display_name, ++ request: request, ++ }); + } + } + } +@@ -549,7 +570,7 @@ export function UpdateNotification() { + key={u.name} + iconName="download" + topic="/v1/tac/update/install" +- send={u.url} ++ send={u.request} + > + Install new {u.display_name} bundle + +-- +2.39.5 + diff --git a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface_git.bb b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface_git.bb index c136599e..dd0ab6a6 100644 --- a/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface_git.bb +++ b/meta-lxatac-software/recipes-webadmin/tacd-webinterface/tacd-webinterface_git.bb @@ -1,15 +1,18 @@ SUMMARY = "The LXA TAC System Daemon - Web Interface" -SRC_URI = " \ - git://github.com/linux-automation/tacd.git;protocol=https;branch=main \ - npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json \ - " +SRC_URI = "git://github.com/linux-automation/tacd.git;protocol=https;branch=main \ + npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json \ + file://0001-web-setup-inform-the-user-about-additional-headers-t.patch;patchdir=.. \ + file://0002-web-indicate-to-the-user-when-a-channel-is-enabled-b.patch;patchdir=.. \ + file://0003-web-add-toggle-switches-to-enable-automatic-installa.patch;patchdir=.. \ + file://0004-web-use-manifest_hash-and-effective_url-when-trigger.patch;patchdir=.. \ + " LICENSE = "GPL-2.0-or-later" LIC_FILES_CHKSUM = " \ file://../LICENSE;md5=b234ee4d69f5fce4486a80fdaf4a4263 \ " PV = "0.1.0+git${SRCPV}" -SRCREV = "e79b017da65f4a084a8b24f1118e15b0c3f25ae8" +SRCREV = "965fdd0f7dbf27dc0daddd3a23f5f7aea0d7e8dd" S = "${WORKDIR}/git/web"