depot_download: support downloading index files

With this commit, the 'installation' input of the depot-download
subsystem accepts <index> nodes in addition to <archive> nodes. Each
index node refers to one index file specified via the 'path' attribute.

This commit also improves the tracking of failure states. Once an
installation job failed (due to a download of verification error),
it won't get re-scheduled. In the past, such failure states were not kept
across subsequent import iterations, which could result in infinite
re-attempts when an installation contained archives from multiple users.
The the progress of the download process is now reflected by the
"progress" attribute on the download manager's state report, which
allows the final report to contain the list of installed/failed archives
along with the overall progress/completed state. The detection of the
latter is important for the sculpt manager for reattempting the
deployment of the completed packages.

The patch enhances the depot_download.run script to stress the new
abilities. In particular, the scenario downloads a mix of index files
(one present, one missing) and archives, from two different depot users
(genodelabs and nfeske).

Issue #3172
This commit is contained in:
Norman Feske 2019-02-20 09:51:55 +01:00 committed by Christian Helmuth
parent 1b518965cc
commit ac68073ffc
12 changed files with 357 additions and 66 deletions

View File

@ -88,8 +88,30 @@ struct Depot::Archive
throw Unknown_archive_type();
}
static Name name (Path const &path) { return _path_element<Name>(path, 2); }
static Version version(Path const &path) { return _path_element<Name>(path, 3); }
/**
* Return true if 'path' refers to an index file
*/
static bool index(Path const &path)
{
return _path_element<Name>(path, 1) == "index";
}
static Name name (Path const &path) { return _path_element<Name>(path, 2); }
static Version version (Path const &path) { return _path_element<Version>(path, 3); }
static Version index_version(Path const &path) { return _path_element<Version>(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_ */

View File

@ -26,6 +26,8 @@
report="manager -> init_config"/>
<policy label="manager -> dependencies"
report="dynamic -> depot_query -> dependencies"/>
<policy label="manager -> index"
report="dynamic -> depot_query -> index"/>
<policy label="manager -> user"
report="dynamic -> depot_query -> user"/>
<policy label="manager -> init_state"
@ -82,6 +84,7 @@
<service name="Report" label="state"> <parent label="state"/> </service>
<service name="Report"> <child name="report_rom"/> </service>
<service name="ROM" label="dependencies"> <child name="report_rom"/> </service>
<service name="ROM" label="index"> <child name="report_rom"/> </service>
<service name="ROM" label="user"> <child name="report_rom"/> </service>
<service name="ROM" label="init_state"> <child name="report_rom"/> </service>
<service name="ROM" label="verified"> <child name="report_rom"/> </service>

View File

@ -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 {
<start name="timer">
@ -69,10 +69,15 @@ append config {
<config>
<vfs>
<dir name="depot">
<dir name="} [depot_user] {">
<dir name="nfeske">
<ram/>
<inline name="download">} [depot_user_download] {</inline>
<inline name="pubkey">} [depot_user_pubkey] {</inline>
<inline name="download">} [depot_user_download nfeske] {</inline>
<inline name="pubkey">} [depot_user_pubkey nfeske] {</inline>
</dir>
<dir name="genodelabs">
<ram/>
<inline name="download">} [depot_user_download genodelabs] {</inline>
<inline name="pubkey">} [depot_user_pubkey genodelabs] {</inline>
</dir>
</dir>
<dir name="public"> <ram/> </dir>
@ -109,10 +114,12 @@ append config {
set fd [open [run_dir]/genode/installation w]
puts $fd "
<installation arch=\"x86_64\">
<archive path=\"[depot_user]/pkg/wm/2018-02-26\"/>
</installation>"
puts $fd {
<installation arch="x86_64">
<archive path="genodelabs/pkg/wm/2018-02-26"/>
<index path="nfeske/index/19.02"/>
<index path="nfeske/index/19.03"/>
</installation>}
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

View File

@ -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<Job> 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<Archive::Version>(path, 2));
});
});
if (next_user.valid())
xml.node("user", [&] () { xml.attribute("name", next_user); });
});

View File

@ -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));
});
});
});

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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 '<missing>' sub nodes
* \param user depot origin to use for the import
* \param dependencies information about '<missing>' archives
* \param index information about '<missing>' index files
*
* The import constructor considers only those '<missing>' 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 <typename FN>
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 <typename FN>
@ -173,6 +202,13 @@ class Depot_download_manager::Import
_for_each_item(Item::UNPACKED, fn);
}
template <typename FN>
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; });
}

View File

@ -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 <util/list_model.h>
#include <base/allocator.h>
#include "types.h"
namespace Depot_download_manager {
using namespace Depot;
struct Job;
}
struct Depot_download_manager::Job : List_model<Job>::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_ */

View File

@ -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<Job> _jobs { };
Constructible<Import> _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();

View File

@ -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<Job> const &);
void gen_fetchurl_start_content(Xml_generator &, Import const &, Url const &,
Fetchurl_version);

View File

@ -173,10 +173,16 @@ struct Sculpt::Main : Input_event_handler,
Signal_handler<Main> _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();