diff --git a/repos/gems/include/depot/archive.h b/repos/gems/include/depot/archive.h index 8278beb5fe..f25fcd0fe9 100644 --- a/repos/gems/include/depot/archive.h +++ b/repos/gems/include/depot/archive.h @@ -88,8 +88,30 @@ struct Depot::Archive throw Unknown_archive_type(); } - static Name name (Path const &path) { return _path_element(path, 2); } - static Version version(Path const &path) { return _path_element(path, 3); } + /** + * Return true if 'path' refers to an index file + */ + static bool index(Path const &path) + { + return _path_element(path, 1) == "index"; + } + + static Name name (Path const &path) { return _path_element(path, 2); } + static Version version (Path const &path) { return _path_element(path, 3); } + static Version index_version(Path const &path) { return _path_element(path, 2); } + + /** + * Return name of compressed file to download for the given depot path + * + * Archives are shipped as tar.xz files whereas index files are shipped + * as xz-compressed files. + */ + static Archive::Path download_file_path(Archive::Path path) + { + return Archive::index(path) ? Archive::Path(path, ".xz") + : Archive::Path(path, ".tar.xz"); + } + }; #endif /* _INCLUDE__DEPOT__ARCHIVE_H_ */ diff --git a/repos/gems/recipes/raw/depot_download/depot_download.config b/repos/gems/recipes/raw/depot_download/depot_download.config index 21e2fe7a86..1d23799204 100644 --- a/repos/gems/recipes/raw/depot_download/depot_download.config +++ b/repos/gems/recipes/raw/depot_download/depot_download.config @@ -26,6 +26,8 @@ report="manager -> init_config"/> + + diff --git a/repos/gems/run/depot_download.run b/repos/gems/run/depot_download.run index cfddb8675d..e1cf90f8ca 100644 --- a/repos/gems/run/depot_download.run +++ b/repos/gems/run/depot_download.run @@ -51,11 +51,11 @@ set config { append_platform_drv_config -proc depot_user_download { } { - return [exec cat [genode_dir]/depot/[depot_user]/download] } +proc depot_user_download { user } { + return [exec cat [genode_dir]/depot/$user/download] } -proc depot_user_pubkey { } { - return [exec cat [genode_dir]/depot/[depot_user]/pubkey] } +proc depot_user_pubkey { user } { + return [exec cat [genode_dir]/depot/$user/pubkey] } append config { @@ -69,10 +69,15 @@ append config { - + - } [depot_user_download] { - } [depot_user_pubkey] { + } [depot_user_download nfeske] { + } [depot_user_pubkey nfeske] { + + + + } [depot_user_download genodelabs] { + } [depot_user_pubkey genodelabs] { @@ -109,10 +114,12 @@ append config { set fd [open [run_dir]/genode/installation w] -puts $fd " - - -" +puts $fd { + + + + +} close $fd @@ -129,5 +136,11 @@ build_boot_image $boot_modules append qemu_args " -nographic -net nic,model=e1000 -net user " -run_genode_until {.*\[init -> depot_download -> manager\] installation complete.*\n} 150 +# watch the state reports generated by the depot-download manager +set expected_pattern {} +append expected_pattern {.*path="genodelabs/pkg/wm/2018-02-26" state="done".*} +append expected_pattern {.*path="nfeske/index/19.02" state="done".*} +append expected_pattern {.*path="nfeske/index/19.03" state="failed".*} + +run_genode_until $expected_pattern 150 diff --git a/repos/gems/src/app/depot_download_manager/gen_depot_query.cc b/repos/gems/src/app/depot_download_manager/gen_depot_query.cc index dcea5474af..efe7e56b16 100644 --- a/repos/gems/src/app/depot_download_manager/gen_depot_query.cc +++ b/repos/gems/src/app/depot_download_manager/gen_depot_query.cc @@ -16,7 +16,8 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml, Xml_node installation, Archive::User const &next_user, - Depot_query_version version) + Depot_query_version version, + List_model const &jobs) { gen_common_start_content(xml, "depot_query", Cap_quota{100}, Ram_quota{2*1024*1024}); @@ -33,7 +34,28 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml, }); }); + /* + * Filter out failed parts of the installation from being re-queried. + * The inclusion of those parts may otherwise result in an infinite + * loop if the installation is downloaded from a mix of depot users. + */ + auto job_failed = [&] (Xml_node node) + { + Archive::Path const path = node.attribute_value("path", Archive::Path()); + + bool failed = false; + jobs.for_each([&] (Job const &job) { + if (job.path == path && job.failed) + failed = true; }); + + return failed; + }; + installation.for_each_sub_node("archive", [&] (Xml_node archive) { + + if (job_failed(archive)) + return; + xml.node("dependencies", [&] () { xml.attribute("path", archive.attribute_value("path", Archive::Path())); xml.attribute("source", archive.attribute_value("source", true)); @@ -41,6 +63,22 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml, }); }); + installation.for_each_sub_node("index", [&] (Xml_node index) { + + if (job_failed(index)) + return; + + xml.node("index", [&] () { + Archive::Path const path = index.attribute_value("path", Archive::Path()); + if (!Archive::index(path)) { + warning("malformed index path '", path, "'"); + return; + } + xml.attribute("user", Archive::user(path)); + xml.attribute("version", Archive::_path_element(path, 2)); + }); + }); + if (next_user.valid()) xml.node("user", [&] () { xml.attribute("name", next_user); }); }); diff --git a/repos/gems/src/app/depot_download_manager/gen_extract.cc b/repos/gems/src/app/depot_download_manager/gen_extract.cc index 82bd713666..2ea6504a87 100644 --- a/repos/gems/src/app/depot_download_manager/gen_extract.cc +++ b/repos/gems/src/app/depot_download_manager/gen_extract.cc @@ -54,15 +54,13 @@ void Depot_download_manager::gen_extract_start_content(Xml_generator &xml, import.for_each_verified_archive([&] (Archive::Path const &path) { typedef String<160> Path; - typedef String<16> Ext; - - Ext const ext (".tar.xz"); - Path const tar_path ("/public/", path, ext); - Path const dst_path ("/depot/", without_last_path_element(path)); xml.node("extract", [&] () { - xml.attribute("archive", tar_path); - xml.attribute("to", dst_path); + xml.attribute("archive", Path("/public/", Archive::download_file_path(path))); + xml.attribute("to", Path("/depot/", without_last_path_element(path))); + + if (Archive::index(path)) + xml.attribute("name", Archive::index_version(path)); }); }); }); diff --git a/repos/gems/src/app/depot_download_manager/gen_fetchurl.cc b/repos/gems/src/app/depot_download_manager/gen_fetchurl.cc index a7c4482b10..9608fddbd6 100644 --- a/repos/gems/src/app/depot_download_manager/gen_fetchurl.cc +++ b/repos/gems/src/app/depot_download_manager/gen_fetchurl.cc @@ -58,11 +58,11 @@ void Depot_download_manager::gen_fetchurl_start_content(Xml_generator &xml, import.for_each_download([&] (Archive::Path const &path) { typedef String<160> Remote; typedef String<160> Local; - typedef String<16> Ext; + typedef String<100> File_path; - Ext const ext (".tar.xz"); - Remote const remote (current_user_url, "/", path, ext); - Local const local ("/download/", path, ext); + File_path const file_path = Archive::download_file_path(path); + Remote const remote (current_user_url, "/", file_path); + Local const local ("/download/", file_path); xml.node("fetch", [&] () { xml.attribute("url", remote); diff --git a/repos/gems/src/app/depot_download_manager/gen_verify.cc b/repos/gems/src/app/depot_download_manager/gen_verify.cc index 1daefec6a5..f40c4f00b1 100644 --- a/repos/gems/src/app/depot_download_manager/gen_verify.cc +++ b/repos/gems/src/app/depot_download_manager/gen_verify.cc @@ -48,14 +48,12 @@ void Depot_download_manager::gen_verify_start_content(Xml_generator &xml, import.for_each_unverified_archive([&] (Archive::Path const &path) { typedef String<160> Path; - typedef String<16> Ext; - Ext const ext (".tar.xz"); - Path const tar_path ("/public/", path, ext); + Path const file_path ("/public/", Archive::download_file_path(path)); Path const pubkey_path (user_path, "/pubkey"); xml.node("verify", [&] () { - xml.attribute("path", tar_path); + xml.attribute("path", file_path); xml.attribute("pubkey", pubkey_path); }); }); diff --git a/repos/gems/src/app/depot_download_manager/import.h b/repos/gems/src/app/depot_download_manager/import.h index 160a7f4a25..0852aa5d42 100644 --- a/repos/gems/src/app/depot_download_manager/import.h +++ b/repos/gems/src/app/depot_download_manager/import.h @@ -41,6 +41,15 @@ class Depot_download_manager::Import { typedef String<32> Bytes; Bytes total, now; + + bool complete() const + { + /* fetchurl has not yet determined the file size */ + if (total == "0.0") + return false; + + return now == total; + } }; virtual Info download_progress(Archive::Path const &) const = 0; @@ -57,6 +66,7 @@ class Depot_download_manager::Import enum State { DOWNLOAD_IN_PROGRESS, DOWNLOAD_COMPLETE, DOWNLOAD_UNAVAILABLE, + VERIFICATION_IN_PROGRESS, VERIFIED, VERIFICATION_FAILED, UNPACKED }; @@ -71,12 +81,13 @@ class Depot_download_manager::Import char const *state_text() const { switch (state) { - case DOWNLOAD_IN_PROGRESS: return "download"; - case DOWNLOAD_COMPLETE: return "verify"; - case DOWNLOAD_UNAVAILABLE: return "unavailable"; - case VERIFIED: return "extract"; - case VERIFICATION_FAILED: return "verification failed"; - case UNPACKED: return "done"; + case DOWNLOAD_IN_PROGRESS: return "download"; + case DOWNLOAD_COMPLETE: return "fetched"; + case DOWNLOAD_UNAVAILABLE: return "unavailable"; + case VERIFICATION_IN_PROGRESS: return "verify"; + case VERIFIED: return "extract"; + case VERIFICATION_FAILED: return "corrupted"; + case UNPACKED: return "done"; }; return ""; } @@ -111,22 +122,35 @@ class Depot_download_manager::Import /** * Constructor * - * \param user depot origin to use for the import - * \param node XML node containing any number of '' sub nodes + * \param user depot origin to use for the import + * \param dependencies information about '' archives + * \param index information about '' index files * * The import constructor considers only those '' sub nodes as * items that match the 'user'. The remaining sub nodes are imported in * a future iteration. */ - Import(Allocator &alloc, Archive::User const &user, Xml_node node) + Import(Allocator &alloc, Archive::User const &user, + Xml_node dependencies, Xml_node index) : _alloc(alloc) { - node.for_each_sub_node("missing", [&] (Xml_node item) { + dependencies.for_each_sub_node("missing", [&] (Xml_node item) { Archive::Path const path = item.attribute_value("path", Archive::Path()); if (Archive::user(path) == user) new (alloc) Item(_items, path); }); + + index.for_each_sub_node("missing", [&] (Xml_node item) { + + Archive::Path const + path(item.attribute_value("user", Archive::User()), + "/index/", + item.attribute_value("version", Archive::Version())); + + if (Archive::user(path) == user) + new (alloc) Item(_items, path); + }); } ~Import() @@ -139,11 +163,16 @@ class Depot_download_manager::Import return _item_state_exists(Item::DOWNLOAD_IN_PROGRESS); } - bool unverified_archives_available() const + bool completed_downloads_available() const { return _item_state_exists(Item::DOWNLOAD_COMPLETE); } + bool unverified_archives_available() const + { + return _item_state_exists(Item::VERIFICATION_IN_PROGRESS); + } + bool verified_archives_available() const { return _item_state_exists(Item::VERIFIED); @@ -158,7 +187,7 @@ class Depot_download_manager::Import template void for_each_unverified_archive(FN const &fn) const { - _for_each_item(Item::DOWNLOAD_COMPLETE, fn); + _for_each_item(Item::VERIFICATION_IN_PROGRESS, fn); } template @@ -173,6 +202,13 @@ class Depot_download_manager::Import _for_each_item(Item::UNPACKED, fn); } + template + void for_each_failed_archive(FN const &fn) const + { + _for_each_item(Item::DOWNLOAD_UNAVAILABLE, fn); + _for_each_item(Item::VERIFICATION_FAILED, fn); + } + void all_downloads_completed() { _items.for_each([&] (Item &item) { @@ -180,7 +216,26 @@ class Depot_download_manager::Import item.state = Item::DOWNLOAD_COMPLETE; }); } - void all_downloads_unavailable() + void verify_all_downloaded_archives() + { + _items.for_each([&] (Item &item) { + if (item.state == Item::DOWNLOAD_COMPLETE) + item.state = Item::VERIFICATION_IN_PROGRESS; }); + } + + void apply_download_progress(Download_progress const &progress) + { + _items.for_each([&] (Item &item) { + + if (item.state == Item::DOWNLOAD_IN_PROGRESS + && progress.download_progress(item.path).complete()) { + + item.state = Item::DOWNLOAD_COMPLETE; + } + }); + } + + void all_remaining_downloads_unavailable() { _items.for_each([&] (Item &item) { if (item.state == Item::DOWNLOAD_IN_PROGRESS) @@ -190,7 +245,7 @@ class Depot_download_manager::Import void archive_verified(Archive::Path const &archive) { _items.for_each([&] (Item &item) { - if (item.state == Item::DOWNLOAD_COMPLETE) + if (item.state == Item::VERIFICATION_IN_PROGRESS) if (item.path == archive) item.state = Item::VERIFIED; }); } @@ -198,7 +253,7 @@ class Depot_download_manager::Import void archive_verification_failed(Archive::Path const &archive) { _items.for_each([&] (Item &item) { - if (item.state == Item::DOWNLOAD_COMPLETE) + if (item.state == Item::VERIFICATION_IN_PROGRESS) if (item.path == archive) item.state = Item::VERIFICATION_FAILED; }); } diff --git a/repos/gems/src/app/depot_download_manager/job.h b/repos/gems/src/app/depot_download_manager/job.h new file mode 100644 index 0000000000..2bda7d64e5 --- /dev/null +++ b/repos/gems/src/app/depot_download_manager/job.h @@ -0,0 +1,65 @@ +/* + * \brief Failure state of jobs submitted via the 'installation' + * \author Norman Feske + * \date 2019-02-21 + */ + +/* + * Copyright (C) 2019 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _JOB_H_ +#define _JOB_H_ + +/* Genode includes */ +#include +#include + +#include "types.h" + +namespace Depot_download_manager { + using namespace Depot; + struct Job; +} + + +struct Depot_download_manager::Job : List_model::Element +{ + bool started = false; + bool failed = false; + + Archive::Path const path; + + Job(Archive::Path const &path) : path(path) { } + + struct Update_policy + { + typedef Job Element; + + Allocator &_alloc; + + Update_policy(Allocator &alloc) : _alloc(alloc) { } + + void destroy_element(Job &elem) { destroy(_alloc, &elem); } + + Job &create_element(Xml_node elem_node) + { + return *new (_alloc) + Job(elem_node.attribute_value("path", Archive::Path())); + } + + void update_element(Job &, Xml_node) { } + + static bool element_matches_xml_node(Job const &job, Xml_node node) + { + return node.attribute_value("path", Archive::Path()) == job.path; + } + + static bool node_is_element(Xml_node) { return true; } + }; +}; + +#endif /* _JOB_H_ */ diff --git a/repos/gems/src/app/depot_download_manager/main.cc b/repos/gems/src/app/depot_download_manager/main.cc index d65a9a5697..40c8629ddb 100644 --- a/repos/gems/src/app/depot_download_manager/main.cc +++ b/repos/gems/src/app/depot_download_manager/main.cc @@ -58,6 +58,7 @@ struct Depot_download_manager::Main : Import::Download_progress Attached_rom_dataspace _installation { _env, "installation" }; Attached_rom_dataspace _dependencies { _env, "dependencies" }; + Attached_rom_dataspace _index { _env, "index" }; Attached_rom_dataspace _init_state { _env, "init_state" }; Attached_rom_dataspace _fetchurl_progress { _env, "fetchurl_progress" }; @@ -104,6 +105,8 @@ struct Depot_download_manager::Main : Import::Download_progress Archive::User _next_user { }; + List_model _jobs { }; + Constructible _import { }; /** @@ -113,7 +116,7 @@ struct Depot_download_manager::Main : Import::Download_progress { Info result { Info::Bytes(), Info::Bytes() }; try { - Url const url_path(_current_user_url(), "/", path, ".tar.xz"); + Url const url_path(_current_user_url(), "/", Archive::download_file_path(path)); /* search fetchurl progress report for matching 'url_path' */ _fetchurl_progress.xml().for_each_sub_node("fetch", [&] (Xml_node fetch) { @@ -128,8 +131,32 @@ struct Depot_download_manager::Main : Import::Download_progress void _update_state_report() { _state_reporter.generate([&] (Xml_generator &xml) { - if (_import.constructed()) - _import->report(xml, *this); }); + + /* produce detailed reports while the installation is in progress */ + if (_import.constructed()) { + xml.attribute("progress", "yes"); + _import->report(xml, *this); + } + + /* once all imports have settled, present the final results */ + else { + _jobs.for_each([&] (Job const &job) { + + if (!job.started) + return; + + /* + * If a job has been started and has not failed, it must + * have succeeded at the time when the import is finished. + */ + char const *type = Archive::index(job.path) ? "index" : "archive"; + xml.node(type, [&] () { + xml.attribute("path", job.path); + xml.attribute("state", job.failed ? "failed" : "done"); + }); + }); + } + }); } void _generate_init_config(Xml_generator &); @@ -143,6 +170,10 @@ struct Depot_download_manager::Main : Import::Download_progress void _handle_installation() { _installation.update(); + + Job::Update_policy policy(_heap); + _jobs.update_from_xml(policy, _installation.xml()); + _generate_init_config(); } @@ -165,18 +196,29 @@ struct Depot_download_manager::Main : Import::Download_progress void _handle_fetchurl_progress() { _fetchurl_progress.update(); + + if (_import.constructed()) { + _import->apply_download_progress(*this); + + /* proceed with next import step if all downloads are done or failed */ + if (!_import->downloads_in_progress()) + _generate_init_config(); + } + _update_state_report(); } Main(Env &env) : _env(env) { _dependencies .sigh(_query_result_handler); + _index .sigh(_query_result_handler); _current_user .sigh(_query_result_handler); _init_state .sigh(_init_state_handler); _verified .sigh(_init_state_handler); _installation .sigh(_installation_handler); _fetchurl_progress.sigh(_fetchurl_progress_handler); + _handle_installation(); _generate_init_config(); } }; @@ -223,7 +265,7 @@ void Depot_download_manager::Main::_generate_init_config(Xml_generator &xml) xml.node("start", [&] () { gen_depot_query_start_content(xml, _installation.xml(), - _next_user, _depot_query_count); }); + _next_user, _depot_query_count, _jobs); }); if (_import.constructed() && _import->downloads_in_progress()) { try { @@ -259,23 +301,54 @@ void Depot_download_manager::Main::_handle_query_result() return; _dependencies.update(); + _index.update(); _current_user.update(); Xml_node const dependencies = _dependencies.xml(); - if (dependencies.num_sub_nodes() == 0) + Xml_node const index = _index.xml(); + + if (dependencies.num_sub_nodes() == 0 && index.num_sub_nodes() == 0) return; - if (!dependencies.has_sub_node("missing")) { + bool const missing_dependencies = dependencies.has_sub_node("missing"); + bool const missing_index_files = index.has_sub_node("missing"); + + if (!missing_dependencies && !missing_index_files) { log("installation complete."); _update_state_report(); return; } - Archive::Path const path = - dependencies.sub_node("missing").attribute_value("path", Archive::Path()); + /** + * Select depot user for next import + * + * Prefer the downloading of index files over archives because index files + * are quick to download and important for interactivity. + */ + auto select_next_user = [&] () + { + Archive::User user { }; - if (Archive::user(path) != _current_user_name()) { - _next_user = Archive::user(path); + if (missing_index_files) + index.with_sub_node("missing", [&] (Xml_node missing) { + user = missing.attribute_value("user", Archive::User()); }); + + if (user.valid()) + return user; + + dependencies.with_sub_node("missing", [&] (Xml_node missing) { + user = Archive::user(missing.attribute_value("path", Archive::Path())); }); + + if (!user.valid()) + warning("unable to select depot user for next import"); + + return user; + }; + + Archive::User const next_user = select_next_user(); + + if (next_user != _current_user_name()) { + _next_user = next_user; /* query user info from 'depot_query' */ _generate_init_config(); @@ -283,7 +356,14 @@ void Depot_download_manager::Main::_handle_query_result() } /* start new import */ - _import.construct(_heap, _current_user_name(), _dependencies.xml()); + _import.construct(_heap, _current_user_name(), dependencies, index); + + /* mark imported jobs as started */ + _import->for_each_download([&] (Archive::Path const &path) { + _jobs.for_each([&] (Job &job) { + if (job.path == path) + job.started = true; }); }); + _fetchurl_attempt = 0; _update_state_report(); @@ -316,7 +396,7 @@ void Depot_download_manager::Main::_handle_init_state() _fetchurl_count.value++; if (_fetchurl_attempt++ >= _fetchurl_max_attempts) { - import.all_downloads_unavailable(); + import.all_remaining_downloads_unavailable(); _fetchurl_attempt = 0; } @@ -326,11 +406,16 @@ void Depot_download_manager::Main::_handle_init_state() if (fetchurl_state.exited && fetchurl_state.code == 0) { import.all_downloads_completed(); - /* kill fetchurl, start untar */ + /* kill fetchurl, start verify */ reconfigure_init = true; } } + if (!import.downloads_in_progress() && import.completed_downloads_available()) { + import.verify_all_downloaded_archives(); + reconfigure_init = true; + } + if (import.unverified_archives_available()) { _verified.xml().for_each_sub_node([&] (Xml_node node) { @@ -341,7 +426,7 @@ void Depot_download_manager::Main::_handle_init_state() /* determine matching archive path */ Path path; import.for_each_unverified_archive([&] (Archive::Path const &archive) { - if (abs_path == Path("/public/", archive, ".tar.xz")) + if (abs_path == Path("/public/", Archive::download_file_path(archive))) path = archive; }); if (path.valid()) { @@ -375,6 +460,12 @@ void Depot_download_manager::Main::_handle_init_state() } } + /* flag failed jobs to prevent re-attempts in subsequent import iterations */ + import.for_each_failed_archive([&] (Archive::Path const &path) { + _jobs.for_each([&] (Job &job) { + if (job.path == path) + job.failed = true; }); }); + /* report before destructing '_import' to avoid empty intermediate reports */ if (reconfigure_init) _update_state_report(); diff --git a/repos/gems/src/app/depot_download_manager/xml.h b/repos/gems/src/app/depot_download_manager/xml.h index 5a6e39cb18..2504f5d8e8 100644 --- a/repos/gems/src/app/depot_download_manager/xml.h +++ b/repos/gems/src/app/depot_download_manager/xml.h @@ -27,6 +27,7 @@ /* local includes */ #include "import.h" +#include "job.h" namespace Depot_download_manager { @@ -84,7 +85,8 @@ namespace Depot_download_manager { void gen_depot_query_start_content(Xml_generator &, Xml_node installation, Archive::User const &, - Depot_query_version); + Depot_query_version, + List_model const &); void gen_fetchurl_start_content(Xml_generator &, Import const &, Url const &, Fetchurl_version); diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc index d555ebfd7d..51a988df40 100644 --- a/repos/gems/src/app/sculpt_manager/main.cc +++ b/repos/gems/src/app/sculpt_manager/main.cc @@ -173,10 +173,16 @@ struct Sculpt::Main : Input_event_handler, Signal_handler
_update_state_handler { _env.ep(), *this, &Main::_handle_update_state }; - bool _update_running() const { return _storage._sculpt_partition.valid() - && !_prepare_in_progress() - && _network.ready() - && _deploy.update_needed(); }; + /** + * Condition for spawning the update subsystem + */ + bool _update_running() const + { + return _storage._sculpt_partition.valid() + && !_prepare_in_progress() + && _network.ready() + && _deploy.update_needed(); + } /************ @@ -281,7 +287,7 @@ struct Sculpt::Main : Input_event_handler, _deploy.gen_child_diagnostics(xml); Xml_node const state = _update_state_rom.xml(); - if (_update_running() && state.has_sub_node("archive")) + if (_update_running() && state.attribute_value("progress", false)) gen_download_status(xml, state); }); }); @@ -809,7 +815,7 @@ void Sculpt::Main::_handle_update_state() generate_dialog(); bool const installation_complete = - !_update_state_rom.xml().has_sub_node("archive"); + !_update_state_rom.xml().attribute_value("progress", false); if (installation_complete) _deploy.reattempt_after_installation();