From c0b93190d0f141244b83a762506e3c63543738b4 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Sun, 24 Feb 2019 04:14:31 +0100 Subject: [PATCH] sculpt: browse depot index files in '+' menu This commit turns the '+' menu into a tool for the following tasks: - Selecting and downloading of depot index files - Browsing of the hierarchical depot index files - Installation of packages found in the index files - Interactive routing configuration of a selected package - Deployment of configured component --- repos/gems/run/sculpt/leitzentrale.config | 1 + repos/gems/src/app/depot_deploy/child.h | 16 +- repos/gems/src/app/depot_deploy/children.h | 8 + .../src/app/depot_download_manager/import.h | 17 + .../src/app/depot_download_manager/main.cc | 18 +- repos/gems/src/app/sculpt_manager/deploy.cc | 25 +- repos/gems/src/app/sculpt_manager/deploy.h | 59 ++- .../gems/src/app/sculpt_manager/depot_query.h | 74 +++ repos/gems/src/app/sculpt_manager/main.cc | 171 ++++++- .../src/app/sculpt_manager/model/component.h | 83 ++++ .../app/sculpt_manager/model/download_queue.h | 125 +++++ .../gems/src/app/sculpt_manager/model/route.h | 162 ++++++ .../app/sculpt_manager/model/runtime_config.h | 132 ++++- .../app/sculpt_manager/model/runtime_state.h | 122 ++++- .../app/sculpt_manager/model/sculpt_version.h | 28 ++ .../src/app/sculpt_manager/model/service.h | 93 ++++ .../app/sculpt_manager/view/popup_dialog.cc | 438 +++++++++++++++++ .../app/sculpt_manager/view/popup_dialog.h | 460 ++++++++++++++++-- repos/gems/src/app/sculpt_manager/xml.h | 2 +- 19 files changed, 1895 insertions(+), 139 deletions(-) create mode 100644 repos/gems/src/app/sculpt_manager/depot_query.h create mode 100644 repos/gems/src/app/sculpt_manager/model/component.h create mode 100644 repos/gems/src/app/sculpt_manager/model/download_queue.h create mode 100644 repos/gems/src/app/sculpt_manager/model/route.h create mode 100644 repos/gems/src/app/sculpt_manager/model/sculpt_version.h create mode 100644 repos/gems/src/app/sculpt_manager/model/service.h create mode 100644 repos/gems/src/app/sculpt_manager/view/popup_dialog.cc diff --git a/repos/gems/run/sculpt/leitzentrale.config b/repos/gems/run/sculpt/leitzentrale.config index a56f75269..4b67a6ad0 100644 --- a/repos/gems/run/sculpt/leitzentrale.config +++ b/repos/gems/run/sculpt/leitzentrale.config @@ -140,6 +140,7 @@ + diff --git a/repos/gems/src/app/depot_deploy/child.h b/repos/gems/src/app/depot_deploy/child.h index 796f56ab4..e35839483 100644 --- a/repos/gems/src/app/depot_deploy/child.h +++ b/repos/gems/src/app/depot_deploy/child.h @@ -261,16 +261,22 @@ class Depot_deploy::Child : public List_model::Element } } - void gen_query(Xml_generator &xml) const + bool blueprint_needed() const { if (_configured() || _pkg_incomplete) - return; + return false; if (_defined_by_launcher() && !_launcher_xml.constructed()) - return; + return false; - xml.node("blueprint", [&] () { - xml.attribute("pkg", _blueprint_pkg_path); }); + return true; + } + + void gen_query(Xml_generator &xml) const + { + if (blueprint_needed()) + xml.node("blueprint", [&] () { + xml.attribute("pkg", _blueprint_pkg_path); }); } /** diff --git a/repos/gems/src/app/depot_deploy/children.h b/repos/gems/src/app/depot_deploy/children.h index d438e2586..558e6795a 100644 --- a/repos/gems/src/app/depot_deploy/children.h +++ b/repos/gems/src/app/depot_deploy/children.h @@ -141,6 +141,14 @@ class Depot_deploy::Children return result; } + bool any_blueprint_needed() const + { + bool result = false; + _children.for_each([&] (Child const &child) { + result |= child.blueprint_needed(); }); + return result; + } + bool exists(Child::Name const &name) const { bool result = false; diff --git a/repos/gems/src/app/depot_download_manager/import.h b/repos/gems/src/app/depot_download_manager/import.h index 0852aa5d4..d5c41f85c 100644 --- a/repos/gems/src/app/depot_download_manager/import.h +++ b/repos/gems/src/app/depot_download_manager/import.h @@ -73,6 +73,14 @@ class Depot_download_manager::Import State state = DOWNLOAD_IN_PROGRESS; + bool in_progress() const + { + return state == DOWNLOAD_IN_PROGRESS + || state == DOWNLOAD_COMPLETE + || state == VERIFICATION_IN_PROGRESS + || state == VERIFIED; + } + Item(Registry ®istry, Archive::Path const &path) : _element(registry, *this), path(path) @@ -281,6 +289,15 @@ class Depot_download_manager::Import }); }); } + + bool in_progress() const + { + bool result = false; + _items.for_each([&] (Item const &item) { + result |= item.in_progress(); }); + + return result; + } }; #endif /* _IMPORT_H_ */ diff --git a/repos/gems/src/app/depot_download_manager/main.cc b/repos/gems/src/app/depot_download_manager/main.cc index 40c8629dd..bac3e4505 100644 --- a/repos/gems/src/app/depot_download_manager/main.cc +++ b/repos/gems/src/app/depot_download_manager/main.cc @@ -174,6 +174,8 @@ struct Depot_download_manager::Main : Import::Download_progress Job::Update_policy policy(_heap); _jobs.update_from_xml(policy, _installation.xml()); + _depot_query_count.value++; + _generate_init_config(); } @@ -378,7 +380,6 @@ void Depot_download_manager::Main::_handle_init_state() _verified.update(); bool reconfigure_init = false; - bool import_finished = false; if (!_import.constructed()) return; @@ -450,14 +451,8 @@ void Depot_download_manager::Main::_handle_init_state() if (extract_state.exited && extract_state.code != 0) error("extract failed with exit code ", extract_state.code); - if (extract_state.exited && extract_state.code == 0) { + if (extract_state.exited && extract_state.code == 0) import.all_verified_archives_extracted(); - import_finished = true; - - /* kill extract, re-issue new depot query to start next iteration */ - _depot_query_count.value++; - reconfigure_init = true; - } } /* flag failed jobs to prevent re-attempts in subsequent import iterations */ @@ -470,9 +465,14 @@ void Depot_download_manager::Main::_handle_init_state() if (reconfigure_init) _update_state_report(); - if (import_finished) + if (!import.in_progress()) { _import.destruct(); + /* re-issue new depot query to start next iteration */ + _depot_query_count.value++; + reconfigure_init = true; + } + if (reconfigure_init) _generate_init_config(); } diff --git a/repos/gems/src/app/sculpt_manager/deploy.cc b/repos/gems/src/app/sculpt_manager/deploy.cc index f1ba5a5fc..5c8eeb703 100644 --- a/repos/gems/src/app/sculpt_manager/deploy.cc +++ b/repos/gems/src/app/sculpt_manager/deploy.cc @@ -98,37 +98,24 @@ void Sculpt::Deploy::handle_deploy() }); }); - /* update query for blueprints of all unconfigured start nodes */ - if (_arch.valid()) { - _depot_query_reporter.generate([&] (Xml_generator &xml) { - xml.attribute("arch", _arch); - xml.attribute("version", _query_version.value); - _children.gen_queries(xml); - }); - } - - /* - * Apply blueprint after 'gen_queries'. Otherwise the application of a - * stale blueprint may flag children whose pkgs have been installed in the - * meanwhile as incomplete, suppressing their respective queries. - */ try { Xml_node const blueprint = _blueprint_rom.xml(); /* apply blueprint, except when stale */ typedef String<32> Version; Version const version = blueprint.attribute_value("version", Version()); - if (version == Version(_query_version.value)) + if (version == Version(_depot_query.depot_query_version().value)) _children.apply_blueprint(_blueprint_rom.xml()); } catch (...) { error("spurious exception during deploy update (apply_blueprint)"); } + /* update query for blueprints of all unconfigured start nodes */ + if (_children.any_blueprint_needed()) + _depot_query.trigger_depot_query(); + /* feed missing packages to installation queue */ - if (!_installation.try_generate_manually_managed()) - _installation.generate([&] (Xml_generator &xml) { - xml.attribute("arch", _arch); - _children.gen_installation_entries(xml); }); + update_installation(); /* apply runtime condition checks */ update_child_conditions(); diff --git a/repos/gems/src/app/sculpt_manager/deploy.h b/repos/gems/src/app/sculpt_manager/deploy.h index 9e4a6ba00..e0083cb5e 100644 --- a/repos/gems/src/app/sculpt_manager/deploy.h +++ b/repos/gems/src/app/sculpt_manager/deploy.h @@ -23,10 +23,12 @@ /* local includes */ #include +#include #include #include #include #include +#include namespace Sculpt { struct Deploy; } @@ -43,23 +45,22 @@ struct Sculpt::Deploy Runtime_config_generator &_runtime_config_generator; + Depot_query &_depot_query; + Attached_rom_dataspace const &_launcher_listing_rom; + Attached_rom_dataspace const &_blueprint_rom; + + Download_queue const &_download_queue; typedef String<16> Arch; Arch _arch { }; - struct Query_version { unsigned value; } _query_version { 0 }; - Child_state cached_depot_rom_state { "depot_rom", Ram_quota{24*1024*1024}, Cap_quota{200} }; Child_state uncached_depot_rom_state { "dynamic_depot_rom", Ram_quota{8*1024*1024}, Cap_quota{200} }; - Attached_rom_dataspace _blueprint_rom { _env, "report -> runtime/depot_query/blueprint" }; - - Expanding_reporter _depot_query_reporter { _env, "query", "depot_query"}; - /* * Report written to '/config/managed/deploy' * @@ -122,7 +123,9 @@ struct Sculpt::Deploy bool update_needed() const { - return _manual_installation_scheduled || _children.any_incomplete(); + return _manual_installation_scheduled + || _children.any_incomplete() + || _download_queue.any_active_download(); } void handle_deploy(); @@ -130,14 +133,8 @@ struct Sculpt::Deploy void _handle_managed_deploy() { _managed_deploy_rom.update(); - _query_version.value++; - handle_deploy(); - } - - void _handle_blueprint() - { - _blueprint_rom.update(); handle_deploy(); + _depot_query.trigger_depot_query(); } /** @@ -191,13 +188,10 @@ struct Sculpt::Deploy Signal_handler _managed_deploy_handler { _env.ep(), *this, &Deploy::_handle_managed_deploy }; - Signal_handler _blueprint_handler { - _env.ep(), *this, &Deploy::_handle_blueprint }; - void restart() { /* ignore stale query results */ - _query_version.value++; + _depot_query.trigger_depot_query(); _children.apply_config(Xml_node("")); } @@ -208,18 +202,41 @@ struct Sculpt::Deploy handle_deploy(); } + void gen_depot_query(Xml_generator &xml) const + { + _children.gen_queries(xml); + } + + void update_installation() + { + /* feed missing packages to installation queue */ + if (_installation.try_generate_manually_managed()) + return; + + _installation.generate([&] (Xml_generator &xml) { + xml.attribute("arch", _arch); + _children.gen_installation_entries(xml); + _download_queue.gen_installation_entries(xml); + }); + } + Deploy(Env &env, Allocator &alloc, Runtime_info const &runtime_info, Dialog::Generator &dialog_generator, Runtime_config_generator &runtime_config_generator, - Attached_rom_dataspace const &launcher_listing_rom) + Depot_query &depot_query, + Attached_rom_dataspace const &launcher_listing_rom, + Attached_rom_dataspace const &blueprint_rom, + Download_queue const &download_queue) : _env(env), _alloc(alloc), _runtime_info(runtime_info), _dialog_generator(dialog_generator), _runtime_config_generator(runtime_config_generator), - _launcher_listing_rom(launcher_listing_rom) + _depot_query(depot_query), + _launcher_listing_rom(launcher_listing_rom), + _blueprint_rom(blueprint_rom), + _download_queue(download_queue) { _managed_deploy_rom.sigh(_managed_deploy_handler); - _blueprint_rom.sigh(_blueprint_handler); } }; diff --git a/repos/gems/src/app/sculpt_manager/depot_query.h b/repos/gems/src/app/sculpt_manager/depot_query.h new file mode 100644 index 000000000..d85fa61ba --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/depot_query.h @@ -0,0 +1,74 @@ +/* + * \brief Interface for querying information about the depot + * \author Norman Feske + * \date 2019-02-22 + */ + +/* + * 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 _DEPOT_QUERY_H_ +#define _DEPOT_QUERY_H_ + +#include "types.h" + +namespace Sculpt { + + struct Depot_query; + + static inline bool blueprint_missing (Xml_node, Path const &); + static inline bool blueprint_any_missing (Xml_node); + static inline bool blueprint_any_rom_missing(Xml_node); +} + + +struct Sculpt::Depot_query : Interface +{ + struct Version { unsigned value; }; + + virtual Version depot_query_version() const = 0; + + virtual void trigger_depot_query() = 0; +}; + + +static inline bool Sculpt::blueprint_missing(Xml_node blueprint, Path const &path) +{ + bool result = false; + blueprint.for_each_sub_node("missing", [&] (Xml_node missing) { + if (missing.attribute_value("path", Path()) == path) + result = true; }); + + return result; +} + + +static inline bool Sculpt::blueprint_any_missing(Xml_node blueprint) +{ + return blueprint.has_sub_node("missing"); +} + + +static inline bool Sculpt::blueprint_any_rom_missing(Xml_node blueprint) +{ + bool result = false; + blueprint.for_each_sub_node("pkg", [&] (Xml_node pkg) { + pkg.for_each_sub_node("missing_rom", [&] (Xml_node missing_rom) { + + /* ld.lib.so is always taken from the base system */ + Label const label = missing_rom.attribute_value("label", Label()); + if (label == "ld.lib.so") + return; + + /* some ingredient is not extracted yet, or actually missing */ + result = true; + }); + }); + return result; +} + +#endif /* _DEPOT_QUERY_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc index 5c6220aa7..63e0ee062 100644 --- a/repos/gems/src/app/sculpt_manager/main.cc +++ b/repos/gems/src/app/sculpt_manager/main.cc @@ -42,7 +42,9 @@ struct Sculpt::Main : Input_event_handler, Runtime_config_generator, Storage::Target_user, Graph::Action, - Popup_dialog::Action + Popup_dialog::Action, + Popup_dialog::Construction_info, + Depot_query { Env &_env; @@ -184,6 +186,67 @@ struct Sculpt::Main : Input_event_handler, && _deploy.update_needed(); } + Download_queue _download_queue { _heap }; + + + /***************** + ** Depot query ** + *****************/ + + Depot_query::Version _query_version { 0 }; + + Expanding_reporter _depot_query_reporter { _env, "query", "depot_query"}; + + /** + * Depot_query interface + */ + Depot_query::Version depot_query_version() const override + { + return _query_version; + } + + /** + * Depot_query interface + */ + void trigger_depot_query() override + { + _query_version.value++; + + if (_deploy._arch.valid()) { + _depot_query_reporter.generate([&] (Xml_generator &xml) { + xml.attribute("arch", _deploy._arch); + xml.attribute("version", _query_version.value); + + _popup_dialog.gen_depot_query(xml); + + /* update query for blueprints of all unconfigured start nodes */ + _deploy.gen_depot_query(xml); + }); + } + } + + + /********************* + ** Blueprint query ** + *********************/ + + Attached_rom_dataspace _blueprint_rom { _env, "report -> runtime/depot_query/blueprint" }; + + Signal_handler
_blueprint_handler { + _env.ep(), *this, &Main::_handle_blueprint }; + + void _handle_blueprint() + { + _blueprint_rom.update(); + + Xml_node const blueprint = _blueprint_rom.xml(); + + _runtime_state.apply_to_construction([&] (Component &component) { + _popup_dialog.apply_blueprint(component, blueprint); }); + + _deploy.handle_deploy(); + } + /************ ** Deploy ** @@ -214,7 +277,8 @@ struct Sculpt::Main : Input_event_handler, } - Deploy _deploy { _env, _heap, _runtime_state, *this, *this, _launcher_listing_rom }; + Deploy _deploy { _env, _heap, _runtime_state, *this, *this, *this, + _launcher_listing_rom, _blueprint_rom, _download_queue }; Attached_rom_dataspace _manual_deploy_rom { _env, "config -> deploy" }; @@ -418,7 +482,8 @@ struct Sculpt::Main : Input_event_handler, && !_graph.add_button_hovered()) { _popup.state = Popup::OFF; - _popup_dialog.reset_hover(); + _popup_dialog.reset(); + discard_construction(); /* de-select '+' button */ _graph._gen_graph_dialog(); @@ -427,15 +492,15 @@ struct Sculpt::Main : Input_event_handler, _handle_window_layout(); } - if (_graph.hovered()) _graph.click(*this); - + if (_graph.hovered()) _graph.click(*this); if (_popup_dialog.hovered()) _popup_dialog.click(*this); } if (ev.key_release(Input::BTN_LEFT)) { if (_hovered_dialog == Hovered::STORAGE) _storage.dialog.clack(_storage); - if (_graph.hovered()) _graph.clack(*this); + if (_graph.hovered()) _graph.clack(*this); + if (_popup_dialog.hovered()) _popup_dialog.clack(*this); } if (_keyboard_focus.target == Keyboard_focus::WPA_PASSPHRASE) @@ -469,6 +534,17 @@ struct Sculpt::Main : Input_event_handler, _handle_window_layout(); } + void _close_popup_dialog() + { + /* close popup menu */ + _popup.state = Popup::OFF; + _popup_dialog.reset(); + _handle_window_layout(); + + /* reset state of the '+' button */ + _graph._gen_graph_dialog(); + } + /* * Popup_dialog::Action interface */ @@ -476,19 +552,57 @@ struct Sculpt::Main : Input_event_handler, { _runtime_state.launch(launcher, launcher); - /* close popup menu */ - _popup.state = Popup::OFF; - _popup_dialog.reset_hover(); - _handle_window_layout(); - - /* reset state of the '+' button */ - _graph._gen_graph_dialog(); + _close_popup_dialog(); /* trigger change of the deployment */ _deploy.update_managed_deploy_config(_manual_deploy_rom.xml()); } - Popup_dialog _popup_dialog { _env, _launchers, _runtime_state }; + Start_name new_construction(Component::Path const &pkg, + Component::Info const &info) override + { + return _runtime_state.new_construction(pkg, info); + } + + void _apply_to_construction(Popup_dialog::Action::Apply_to &fn) override + { + _runtime_state.apply_to_construction([&] (Component &c) { fn.apply_to(c); }); + } + + void discard_construction() override { _runtime_state.discard_construction(); } + + void launch_construction() override + { + _runtime_state.launch_construction(); + + _close_popup_dialog(); + + /* trigger change of the deployment */ + _deploy.update_managed_deploy_config(_manual_deploy_rom.xml()); + } + + void trigger_download(Path const &path) override + { + _download_queue.add(path); + + /* incorporate new download-queue content into update */ + _deploy.update_installation(); + + generate_runtime_config(); + } + + /** + * Popup_dialog::Construction_info interface + */ + void _with_construction(Popup_dialog::Construction_info::With const &fn) const override + { + _runtime_state.with_construction([&] (Component const &c) { fn.with(c); }); + } + + Popup_dialog _popup_dialog { _env, _heap, _launchers, + _network._nic_state, _network._nic_target, + _runtime_state, _cached_runtime_config, + _download_queue, *this, *this }; Managed_config
_fb_drv_config { _env, "config", "fb_drv", *this, &Main::_handle_fb_drv_config }; @@ -591,6 +705,7 @@ struct Sculpt::Main : Input_event_handler, _window_list .sigh(_window_list_handler); _decorator_margins .sigh(_decorator_margins_handler); _launcher_listing_rom.sigh(_launcher_listing_handler); + _blueprint_rom .sigh(_blueprint_handler); /* * Generate initial configurations @@ -886,11 +1001,33 @@ void Sculpt::Main::_handle_update_state() _update_state_rom.update(); generate_dialog(); - bool const installation_complete = - !_update_state_rom.xml().attribute_value("progress", false); + Xml_node const update_state = _update_state_rom.xml(); - if (installation_complete) + if (update_state.num_sub_nodes() == 0) + return; + + bool const popup_watches_downloads = + _popup_dialog.interested_in_download(); + + _download_queue.apply_update_state(update_state); + _download_queue.remove_inactive_downloads(); + + Xml_node const blueprint = _blueprint_rom.xml(); + bool const new_depot_query_needed = popup_watches_downloads + || blueprint_any_missing(blueprint) + || blueprint_any_rom_missing(blueprint); + if (new_depot_query_needed) + trigger_depot_query(); + + if (popup_watches_downloads) + _deploy.update_installation(); + + bool const installation_complete = + !update_state.attribute_value("progress", false); + + if (installation_complete) { _deploy.reattempt_after_installation(); + } } diff --git a/repos/gems/src/app/sculpt_manager/model/component.h b/repos/gems/src/app/sculpt_manager/model/component.h new file mode 100644 index 000000000..f9d3249b8 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/component.h @@ -0,0 +1,83 @@ +/* + * \brief Representation of a component + * \author Norman Feske + * \date 2019-02-25 + */ + +/* + * 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 _MODEL__COMPONENT_H_ +#define _MODEL__COMPONENT_H_ + +#include +#include + +namespace Sculpt { struct Component; } + + +struct Sculpt::Component : Noncopyable +{ + Route::Update_policy _route_update_policy; + + typedef Depot::Archive::Path Path; + typedef Depot::Archive::Name Name; + typedef String<100> Info; + typedef Start_name Service; + + /* defined at construction time */ + Path const path; + Info const info; + + /* defined when blueprint arrives */ + uint64_t ram { }; + size_t caps { }; + + bool blueprint_known = false; + + List_model routes { }; + + Component(Allocator &alloc, Path const &path, Info const &info) + : _route_update_policy(alloc), path(path), info(info) { } + + ~Component() + { + routes.update_from_xml(_route_update_policy, Xml_node("")); + } + + void try_apply_blueprint(Xml_node blueprint) + { + blueprint.for_each_sub_node("pkg", [&] (Xml_node pkg) { + + if (path != pkg.attribute_value("path", Path())) + return; + + pkg.with_sub_node("runtime", [&] (Xml_node runtime) { + + ram = runtime.attribute_value("ram", Number_of_bytes()); + caps = runtime.attribute_value("caps", 0UL); + + runtime.with_sub_node("requires", [&] (Xml_node requires) { + routes.update_from_xml(_route_update_policy, requires); }); + }); + + blueprint_known = true; + }); + } + + bool all_routes_defined() const + { + bool result = true; + routes.for_each([&] (Route const &route) { + if (!route.selected_service.constructed()) + result = false; }); + + return result; + } +}; + +#endif /* _MODEL__COMPONENT_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/download_queue.h b/repos/gems/src/app/sculpt_manager/model/download_queue.h new file mode 100644 index 000000000..81f373b64 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/download_queue.h @@ -0,0 +1,125 @@ +/* + * \brief List of depot downloads that are currently in flight + * \author Norman Feske + * \date 2019-02-25 + */ + +/* + * 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 _MODEL__DOWNLOAD_QUEUE_H_ +#define _MODEL__DOWNLOAD_QUEUE_H_ + +#include +#include + +namespace Sculpt { struct Download_queue; } + + +struct Sculpt::Download_queue : Noncopyable +{ + struct Download : Interface + { + Path const path; + + enum class State { DOWNLOADING, FAILED, DONE } state; + + Download(Path const &path) : path(path), state(State::DOWNLOADING) { } + + void gen_installation_entry(Xml_generator &xml) const + { + if (state != State::DOWNLOADING) + return; + + if (Depot::Archive::index(path)) + xml.node("index", [&] () { + xml.attribute("path", path); }); + else + xml.node("archive", [&] () { + xml.attribute("path", path); + xml.attribute("source", "no"); }); + } + }; + + Allocator &_alloc; + + Registry > _downloads { }; + + Download_queue(Allocator &alloc) : _alloc(alloc) { } + + void add(Path const &path) + { + log("add to download queue: ", path); + bool already_exists = false; + _downloads.for_each([&] (Download const &download) { + if (download.path == path) + already_exists = true; }); + + if (already_exists) + return; + + new (_alloc) Registered(_downloads, path); + } + + bool in_progress(Path const &path) const + { + bool result = false; + _downloads.for_each([&] (Download const &download) { + if (download.path == path && download.state == Download::State::DOWNLOADING) + result = true; }); + + return result; + } + + void apply_update_state(Xml_node state) + { + /* 'elem' may be of type 'index' or 'archive' */ + state.for_each_sub_node([&] (Xml_node elem) { + + Path const path = elem.attribute_value("path", Path()); + + _downloads.for_each([&] (Download &download) { + + if (download.path != path) + return; + + typedef String<16> State; + State const state = elem.attribute_value("state", State()); + + if (state == "done") download.state = Download::State::DONE; + if (state == "failed") download.state = Download::State::FAILED; + if (state == "unavailable") download.state = Download::State::FAILED; + if (state == "corrupted") download.state = Download::State::FAILED; + }); + }); + } + + void remove_inactive_downloads() + { + _downloads.for_each([&] (Download &download) { + if (download.state != Download::State::DOWNLOADING) + destroy(_alloc, &download); }); + } + + void gen_installation_entries(Xml_generator &xml) const + { + _downloads.for_each([&] (Download const &download) { + download.gen_installation_entry(xml); }); + } + + bool any_active_download() const + { + bool result = false; + _downloads.for_each([&] (Download const &download) { + if (!result && download.state == Download::State::DOWNLOADING) + result = true; }); + + return result; + } +}; + +#endif /* _MODEL__ROUTE_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/route.h b/repos/gems/src/app/sculpt_manager/model/route.h new file mode 100644 index 000000000..ac5a28d76 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/route.h @@ -0,0 +1,162 @@ +/* + * \brief Representation of a route to a service + * \author Norman Feske + * \date 2019-02-25 + */ + +/* + * 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 _MODEL__ROUTE_H_ +#define _MODEL__ROUTE_H_ + +#include +#include + +namespace Sculpt { struct Route; } + + +struct Sculpt::Route : List_model::Element +{ + typedef String<32> Id; + typedef String<80> Info; + + static char const *xml_type(Service::Type type) + { + switch (type) { + case Service::Type::AUDIO_IN: return "audio_in"; + case Service::Type::AUDIO_OUT: return "audio_out"; + case Service::Type::BLOCK: return "block"; + case Service::Type::FILE_SYSTEM: return "file_system"; + case Service::Type::NIC: return "nic"; + case Service::Type::NITPICKER: return "nitpicker"; + case Service::Type::RM: return "rm"; + case Service::Type::IO_MEM: return "io_mem"; + case Service::Type::IO_PORT: return "io_port"; + case Service::Type::IRQ: return "irq"; + case Service::Type::REPORT: return "report"; + case Service::Type::ROM: return "rom"; + case Service::Type::TERMINAL: return "terminal"; + case Service::Type::TRACE: return "trace"; + case Service::Type::USB: return "usb"; + case Service::Type::RTC: return "rtc"; + case Service::Type::PLATFORM: return "platform"; + case Service::Type::VM: return "vm"; + case Service::Type::UNDEFINED: break; + } + return "undefined"; + } + + static char const *_pretty_name(Service::Type type) + { + switch (type) { + case Service::Type::AUDIO_IN: return "Audio input"; + case Service::Type::AUDIO_OUT: return "Audio output"; + case Service::Type::BLOCK: return "Block device"; + case Service::Type::FILE_SYSTEM: return "File system"; + case Service::Type::NIC: return "Network"; + case Service::Type::NITPICKER: return "GUI"; + case Service::Type::RM: return "Region maps"; + case Service::Type::IO_MEM: return "Direct memory-mapped I/O"; + case Service::Type::IO_PORT: return "Direct port I/O"; + case Service::Type::IRQ: return "Direct device interrupts"; + case Service::Type::REPORT: return "Report"; + case Service::Type::ROM: return "ROM"; + case Service::Type::TERMINAL: return "Terminal"; + case Service::Type::TRACE: return "Tracing"; + case Service::Type::USB: return "USB"; + case Service::Type::RTC: return "Real-time clock"; + case Service::Type::PLATFORM: return "Device access"; + case Service::Type::VM: return "Hardware-based virtualization"; + case Service::Type::UNDEFINED: break; + } + return ""; + } + + static Service::Type _required(Xml_node node) + { + for (unsigned i = 0; i < (unsigned)Service::Type::UNDEFINED; i++) { + Service::Type const s = (Service::Type)i; + if (node.has_type(xml_type(s))) + return s; + } + + return Service::Type::UNDEFINED; + } + + Service::Type const required; + Label const required_label; + + Constructible selected_service { }; + + Id selected_service_id { }; + + /** + * Constructor + * + * \param required sub node of a runtime's node + */ + Route(Xml_node node) + : + required(_required(node)), + required_label(node.attribute_value("label", Label())) + { } + + void print(Output &out) const + { + Genode::print(out, _pretty_name(required)); + if (required_label.valid()) + Genode::print(out, " (", required_label, ") "); + } + + void gen_xml(Xml_generator &xml) const + { + if (!selected_service.constructed()) { + warning("no service assigned to route ", *this); + return; + } + + gen_named_node(xml, "service", Service::name_attr(required), [&] () { + + if (required_label.valid()) + xml.attribute("label", required_label); + + selected_service->gen_xml(xml); + }); + } + + struct Update_policy + { + typedef Route Element; + + Allocator &_alloc; + + Update_policy(Allocator &alloc) : _alloc(alloc) { } + + void destroy_element(Route &elem) { destroy(_alloc, &elem); } + + Route &create_element(Xml_node node) + { + return *new (_alloc) Route(node); + } + + void update_element(Route &, Xml_node) { } + + static bool element_matches_xml_node(Route const &elem, Xml_node node) + { + return elem.required == _required(node) + && elem.required_label == node.attribute_value("label", Label()); + } + + static bool node_is_element(Xml_node node) + { + return _required(node) != Service::Type::UNDEFINED; + } + }; +}; + +#endif /* _MODEL__ROUTE_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/runtime_config.h b/repos/gems/src/app/sculpt_manager/model/runtime_config.h index 7cf618803..99619115f 100644 --- a/repos/gems/src/app/sculpt_manager/model/runtime_config.h +++ b/repos/gems/src/app/sculpt_manager/model/runtime_config.h @@ -17,12 +17,15 @@ /* Genode includes */ #include #include +#include /* local includes */ #include +#include namespace Sculpt { class Runtime_config; } + class Sculpt::Runtime_config { private: @@ -52,9 +55,8 @@ class Sculpt::Runtime_config node.with_sub_node("parent", [&] (Xml_node parent) { - typedef String<16> Service; - Service const service = - node.attribute_value("name", Service()); + Service::Type_name const service = + node.attribute_value("name", Service::Type_name()); Label const dst_label = parent.attribute_value("label", Label()); @@ -123,6 +125,54 @@ class Sculpt::Runtime_config return result; } + struct Child_service : Service, List_model::Element + { + static Service::Type type_from_xml(Xml_node service) + { + auto const name = service.attribute_value("name", Service::Type_name()); + for (unsigned i = 0; i < (unsigned)Type::UNDEFINED; i++) { + Type const t = (Type)i; + if (name == Service::name_attr(t)) + return t; + } + + return Type::UNDEFINED; + } + + Child_service(Start_name server, Xml_node provides) + : Service(server, type_from_xml(provides), Label()) { } + + struct Update_policy + { + typedef Child_service Element; + + Start_name _server; + Allocator &_alloc; + + Update_policy(Start_name const &server, Allocator &alloc) + : _server(server), _alloc(alloc) { } + + void destroy_element(Element &elem) { destroy(_alloc, &elem); } + + Element &create_element(Xml_node node) + { + return *new (_alloc) Child_service(_server, node); + } + + void update_element(Element &, Xml_node) { } + + static bool element_matches_xml_node(Element const &elem, Xml_node node) + { + return type_from_xml(node) == elem.type; + } + + static bool node_is_element(Xml_node node) + { + return type_from_xml(node) != Service::Type::UNDEFINED; + } + }; + }; + public: struct Component : List_model::Element @@ -177,21 +227,29 @@ class Sculpt::Runtime_config fn(dep.to_name); }); } + List_model _child_services { }; + Component(Start_name const &name) : name(name) { } + template + void for_each_service(FN const &fn) const + { + _child_services.for_each(fn); + } + struct Update_policy { typedef Component Element; Allocator &_alloc; - Dep::Update_policy _dep_update_policy { _alloc }; - Update_policy(Allocator &alloc) : _alloc(alloc) { } void destroy_element(Component &elem) { - elem.deps.update_from_xml(_dep_update_policy, Xml_node("")); + /* flush list models */ + update_element(elem, Xml_node(" ")); + destroy(_alloc, &elem); } @@ -205,8 +263,20 @@ class Sculpt::Runtime_config { elem.primary_dependency = _primary_dependency(node); - node.with_sub_node("route", [&] (Xml_node route) { - elem.deps.update_from_xml(_dep_update_policy, route); }); + { + Dep::Update_policy policy { _alloc }; + + node.with_sub_node("route", [&] (Xml_node route) { + elem.deps.update_from_xml(policy, route); }); + } + + { + Child_service::Update_policy policy { elem.name, _alloc }; + + node.with_sub_node("provides", [&] (Xml_node provides) { + elem._child_services.update_from_xml(policy, + provides); }); + } } static bool element_matches_xml_node(Component const &elem, Xml_node node) @@ -222,6 +292,43 @@ class Sculpt::Runtime_config List_model _components { }; + struct Parent_services + { + typedef Registered_no_delete Parent_service; + typedef Service::Type Type; + + Registry _r { }; + + Parent_service const + _focus { _r, Type::NITPICKER, "keyboard focus", "focus" }, + _backdrop { _r, Type::NITPICKER, "desktop background", "backdrop" }, + _nitpicker { _r, Type::NITPICKER, "system GUI server" }, + _config_fs { _r, Type::FILE_SYSTEM, "writeable system configuration", "config" }, + _report_fs { _r, Type::FILE_SYSTEM, "read-only system reports", "report" }, + _capslock { _r, Type::ROM, "global capslock state", "capslock" }, + _vimrc { _r, Type::ROM, "default vim configuration", "config -> vimrc" }, + _fonts { _r, Type::ROM, "system font configuration", "config -> managed/fonts" }, + _pf_info { _r, Type::ROM, "platform information", "platform_info" }, + _report { _r, Type::REPORT, "system reports" }, + _rm { _r, Type::RM, "custom virtual memory objects" }, + _io_mem { _r, Type::IO_MEM, "raw hardware access" }, + _io_port { _r, Type::IO_PORT, "raw hardware access" }, + _irq { _r, Type::IRQ, "raw hardware access" }, + _rtc { _r, Type::RTC, "system clock" }, + _block { _r, Type::BLOCK, "direct block-device access" }, + _usb { _r, Type::USB, "direct USB-device access" }, + _pci_wifi { _r, Type::PLATFORM, "wifi hardware", "wifi" }, + _pci_net { _r, Type::PLATFORM, "network hardware", "nic" }, + _pci_audio { _r, Type::PLATFORM, "audio hardware", "audio" }, + _pci_acpi { _r, Type::PLATFORM, "ACPI", "acpica" }, + _trace { _r, Type::TRACE, "system-global tracing" }, + _vm { _r, Type::VM, "virtualization hardware" }; + + template + void for_each(FN const &fn) const { _r.for_each(fn); } + + } _parent_services { }; + public: Runtime_config(Allocator &alloc) : _alloc(alloc) { } @@ -246,6 +353,15 @@ class Sculpt::Runtime_config component.deps.for_each([&] (Component::Dep const &dep) { fn(dep.to_name); }); } }); } + + template + void for_each_service(FN const &fn) const + { + _components.for_each([&] (Component const &component) { + component.for_each_service(fn); }); + + _parent_services.for_each(fn); + } }; #endif /* _MODEL__RUNTIME_CONFIG_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/runtime_state.h b/repos/gems/src/app/sculpt_manager/model/runtime_state.h index 4b0626234..042a75cf9 100644 --- a/repos/gems/src/app/sculpt_manager/model/runtime_state.h +++ b/repos/gems/src/app/sculpt_manager/model/runtime_state.h @@ -22,6 +22,7 @@ #include #include #include +#include namespace Sculpt { class Runtime_state; } @@ -85,12 +86,64 @@ class Sculpt::Runtime_state : public Runtime_info { Start_name const name; Path const launcher; + + Constructible construction { }; + + bool launched; + + /** + * Constructor used for child started via launcher + */ Launched_child(Start_name const &name, Path const &launcher) - : name(name), launcher(launcher) { }; + : name(name), launcher(launcher), launched(true) { }; + + /** + * Constructor used for interactively configured child + */ + Launched_child(Allocator &alloc, Start_name const &name, + Component::Path const &pkg_path, + Component::Info const &info) + : + name(name), launcher(), launched(false) + { + construction.construct(alloc, pkg_path, info); + } + + void gen_deploy_start_node(Xml_generator &xml) const + { + if (!launched) + return; + + gen_named_node(xml, "start", name, [&] () { + + /* interactively constructed */ + if (construction.constructed()) { + xml.attribute("pkg", construction->path); + + xml.node("route", [&] () { + construction->routes.for_each([&] (Route const &route) { + route.gen_xml(xml); }); }); + } + + /* created via launcher */ + else { + if (name != launcher) + xml.attribute("launcher", launcher); + } + }); + } }; Registry > _launched_children { }; + Registered *_currently_constructed = nullptr; + + bool _construction_in_progress() const + { + return _currently_constructed + && _currently_constructed->construction.constructed(); + } + struct Update_policy : List_model::Update_policy { Allocator &_alloc; @@ -133,6 +186,12 @@ class Sculpt::Runtime_state : public Runtime_info static bool node_is_element(Xml_node node) { return node.has_type("child"); } }; + /* + * Noncopyable + */ + Runtime_state(Runtime_state const &); + Runtime_state &operator = (Runtime_state const &); + public: Runtime_state(Allocator &alloc) : _alloc(alloc) { } @@ -151,9 +210,15 @@ class Sculpt::Runtime_state : public Runtime_info bool present_in_runtime(Start_name const &name) const override { bool result = false; + _children.for_each([&] (Child const &child) { if (!result && child.name == name) result = true; }); + + _launched_children.for_each([&] (Launched_child const &child) { + if (!result && child.name == name) + result = true; }); + return result; } @@ -175,9 +240,7 @@ class Sculpt::Runtime_state : public Runtime_info void gen_launched_deploy_start_nodes(Xml_generator &xml) const override { _launched_children.for_each([&] (Launched_child const &child) { - gen_named_node(xml, "start", child.name, [&] () { - if (child.name != child.launcher) - xml.attribute("launcher", child.launcher); }); }); + child.gen_deploy_start_node(xml); }); } Info info(Start_name const &name) const @@ -243,7 +306,7 @@ class Sculpt::Runtime_state : public Runtime_info * entry from '_launched_children'. */ bool was_interactively_launched = false; - _launched_children.for_each([&] (Launched_child &child) { + _launched_children.for_each([&] (Registered &child) { if (child.name == name) { was_interactively_launched = true; destroy(_alloc, &child); @@ -264,6 +327,55 @@ class Sculpt::Runtime_state : public Runtime_info new (_alloc) Registered(_launched_children, name, launcher); } + Start_name new_construction(Component::Path const pkg, + Component::Info const &info) + { + /* allow only one construction at a time */ + discard_construction(); + + /* determine unique name for new child */ + Depot::Archive::Name const archive_name = Depot::Archive::name(pkg); + Start_name unique_name = archive_name; + unsigned cnt = 1; + while (present_in_runtime(unique_name)) + unique_name = Start_name(archive_name, ".", ++cnt); + + _currently_constructed = new (_alloc) + Registered(_launched_children, _alloc, + unique_name, pkg, info); + return unique_name; + } + + void discard_construction() + { + if (_currently_constructed) { + destroy(_alloc, _currently_constructed); + _currently_constructed = nullptr; + } + } + + template + void apply_to_construction(FN const &fn) + { + if (_construction_in_progress()) + fn(*_currently_constructed->construction); + } + + template + void with_construction(FN const &fn) const + { + if (_construction_in_progress()) + fn(*_currently_constructed->construction); + } + + void launch_construction() + { + if (_currently_constructed) + _currently_constructed->launched = true; + + _currently_constructed = nullptr; + } + void reset_abandoned_and_launched_children() { _abandoned_children.for_each([&] (Abandoned_child &child) { diff --git a/repos/gems/src/app/sculpt_manager/model/sculpt_version.h b/repos/gems/src/app/sculpt_manager/model/sculpt_version.h new file mode 100644 index 000000000..4d02a2198 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/sculpt_version.h @@ -0,0 +1,28 @@ +/* + * \brief Sculpt version + * \author Norman Feske + * \date 2019-02-24 + */ + +/* + * 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 _MODEL__SCULPT_VERSION_H_ +#define _MODEL__SCULPT_VERSION_H_ + +#include + +namespace Sculpt { struct Sculpt_version; } + +struct Sculpt::Sculpt_version : String<6> +{ + Sculpt_version(Env &env) + : String<6>(Attached_rom_dataspace(env, "VERSION").local_addr()) + { } +}; + +#endif /* _MODEL__SCULPT_VERSION_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/service.h b/repos/gems/src/app/sculpt_manager/model/service.h new file mode 100644 index 000000000..241181c5b --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/service.h @@ -0,0 +1,93 @@ +/* + * \brief Representation of service that can be targeted by a route + * \author Norman Feske + * \date 2019-02-25 + */ + +/* + * 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 _MODEL__SERVICE_H_ +#define _MODEL__SERVICE_H_ + +#include "types.h" + +namespace Sculpt { struct Service; } + + +struct Sculpt::Service +{ + typedef String<16> Type_name; + typedef String<32> Info; + + enum class Type { + AUDIO_IN, AUDIO_OUT, BLOCK, FILE_SYSTEM, NIC, NITPICKER, + RM, IO_MEM, IO_PORT, IRQ, REPORT, ROM, TERMINAL, TRACE, + USB, RTC, PLATFORM, VM, UNDEFINED }; + + Start_name server { }; /* invalid for parent service */ + Type type; + Label label; + Info info; + + /** + * Return name attribute value of node + */ + static char const *name_attr(Type type) + { + switch (type) { + case Type::AUDIO_IN: return "Audio_in"; + case Type::AUDIO_OUT: return "Audio_out"; + case Type::BLOCK: return "Block"; + case Type::FILE_SYSTEM: return "File_system"; + case Type::NIC: return "Nic"; + case Type::NITPICKER: return "Nitpicker"; + case Type::RM: return "RM"; + case Type::IO_MEM: return "IO_MEM"; + case Type::IO_PORT: return "IO_PORT"; + case Type::IRQ: return "IRQ"; + case Type::REPORT: return "Report"; + case Type::ROM: return "ROM"; + case Type::TERMINAL: return "Terminal"; + case Type::TRACE: return "TRACE"; + case Type::USB: return "Usb"; + case Type::RTC: return "Rtc"; + case Type::PLATFORM: return "Platform"; + case Type::VM: return "VM"; + case Type::UNDEFINED: break; + } + return "undefined"; + } + + /** + * Constructor for child service + */ + Service(Start_name const &server, Type type, Label const &label) + : server(server), type(type), label(label), info(server) { } + + /** + * Constructor for parent service + */ + Service(Type type, Info const &info, Label const &label = Label()) + : type(type), label(label), info(info) { } + + void gen_xml(Xml_generator &xml) const + { + bool const parent = !server.valid(); + + xml.node(parent ? "parent" : "child", [&] () { + + if (!parent) + xml.attribute("name", server); + + if (label.valid()) + xml.attribute("label", label); + }); + } +}; + +#endif /* _MODEL__SERVICE_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/popup_dialog.cc b/repos/gems/src/app/sculpt_manager/view/popup_dialog.cc new file mode 100644 index 000000000..6cac254d5 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/popup_dialog.cc @@ -0,0 +1,438 @@ +/* + * \brief Popup dialog + * \author Norman Feske + * \date 2018-09-12 + */ + +/* + * Copyright (C) 2018 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. + */ + +#include + +using namespace Sculpt; + + +void Popup_dialog::_gen_pkg_info(Xml_generator &xml, + Component const &component) const +{ + gen_named_node(xml, "label", "info", [&] () { + xml.attribute("text", Component::Info(" ", component.info, " ")); }); + + _gen_info_label(xml, "pad1", ""); + _gen_info_label(xml, "path", component.path); +} + + +void Popup_dialog::_gen_pkg_elements(Xml_generator &xml, + Component const &component) const +{ + typedef Component::Info Info; + + _gen_sub_menu_title(xml, "back", Menu::Name("Add ", _construction_name)); + + _gen_pkg_info(xml, component); + + _gen_info_label(xml, "resources", Info(Capacity{component.ram}, " ", + component.caps, " caps")); + _gen_info_label(xml, "pad2", ""); + + unsigned cnt = 0; + component.routes.for_each([&] (Route const &route) { + + Route::Id const id(cnt++); + + gen_named_node(xml, "frame", id, [&] () { + + xml.node("vbox", [&] () { + + bool const selected = _route_selected(id); + bool const defined = route.selected_service.constructed(); + + if (!selected) { + _gen_route_entry(xml, id, + defined ? Info(route.selected_service->info) + : Info(route), + defined); + } + + /* + * List of routing options + */ + if (selected) { + _gen_route_entry(xml, "back", Info(route), true, "back"); + + unsigned cnt = 0; + _runtime_config.for_each_service([&] (Service const &service) { + + Hoverable_item::Id const id("service.", cnt++); + + bool const service_selected = + route.selected_service.constructed() && + id == route.selected_service_id; + + if (service.type == route.required) + _gen_route_entry(xml, id, service.info, service_selected); + }); + } + }); + }); + }); + + /* + * Display "Add component" button once all routes are defined + */ + if (component.all_routes_defined()) { + gen_named_node(xml, "button", "launch", [&] () { + _action_item.gen_button_attr(xml, "launch"); + xml.node("label", [&] () { + xml.attribute("text", "Add component"); }); + }); + } +} + + +void Popup_dialog::_gen_menu_elements(Xml_generator &xml) const +{ + /* + * Lauchers + */ + if (_state == TOP_LEVEL || _state < DEPOT_SHOWN) { + _launchers.for_each([&] (Launchers::Info const &info) { + + /* allow each launcher to be used only once */ + if (_runtime_info.present_in_runtime(info.path)) + return; + + _gen_menu_entry(xml, info.path, info.path, false); + }); + + _gen_menu_entry(xml, "depot", "Depot ...", false); + } + + /* + * Depot users with an available index + */ + if (_state == DEPOT_SHOWN || _state == INDEX_REQUESTED) { + _gen_sub_menu_title(xml, "back", "Depot"); + + _scan_rom.xml().for_each_sub_node("user", [&] (Xml_node user) { + + User const name = user.attribute_value("name", User()); + bool const selected = (_selected_user == name); + + if (_index_avail(name)) + _gen_menu_entry(xml, name, User(name, " ..."), selected); + }); + + /* + * Depot selection menu item + */ + if (_state == DEPOT_SHOWN || _state == INDEX_REQUESTED) { + + if (_nic_ready()) + _gen_menu_entry(xml, "selection", "Selection ...", false); + } + } + + /* + * List of depot users for removing/adding indices + */ + if (_state == DEPOT_SELECTION) { + _gen_sub_menu_title(xml, "back", "Selection"); + + _scan_rom.xml().for_each_sub_node("user", [&] (Xml_node user) { + + User const name = user.attribute_value("name", User()); + bool const selected = _index_avail(name); + + String<32> const suffix = _download_queue.in_progress(_index_path(name)) + ? " fetch... " : " "; + + _gen_menu_entry(xml, name, User(name, suffix), selected, "checkbox"); + }); + } + + /* + * Title of index + */ + if (_state >= INDEX_SHOWN && _state < PKG_SHOWN) { + Menu::Name title("Depot ", _selected_user); + if (_menu._level) + title = Menu::Name(title, " ", _menu, " "); + + _gen_sub_menu_title(xml, "back", title); + } + + /* + * Index menu + */ + if (_state >= INDEX_SHOWN && _state < PKG_SHOWN) { + + unsigned cnt = 0; + _for_each_menu_item([&] (Xml_node item) { + + Hoverable_item::Id const id(cnt); + + if (item.has_type("index")) { + auto const name = item.attribute_value("name", Menu::Name()); + _gen_menu_entry(xml, id, Menu::Name(name, " ..."), false); + } + + if (item.has_type("pkg")) { + auto const path = item.attribute_value("path", Depot::Archive::Path()); + auto const name = Depot::Archive::name(path); + auto const version = Depot::Archive::version(path); + + bool selected = false; + + bool const installing = _download_queue.in_progress(path); + + _construction_info.with_construction([&] (Component const &component) { + + if (component.path == path) + selected = true; + }); + + String<100> const text(name, " " "(", version, ")", + installing ? " installing... " : "..."); + + _gen_menu_entry(xml, id, text, selected); + + if (selected && _pkg_missing && !installing) { + + _construction_info.with_construction([&] (Component const &component) { + + gen_named_node(xml, "float", "install", [&] () { + + gen_named_node(xml, "vbox", "vbox", [&] () { + + if (_nic_ready()) { + + _gen_pkg_info(xml, component); + _gen_info_label(xml, "pad2", ""); + + gen_named_node(xml, "float", "install", [&] () { + xml.node("button", [&] () { + _install_item.gen_button_attr(xml, "install"); + xml.node("label", [&] () { + xml.attribute("text", "Install"); + }); + }); + }); + + } else { + _gen_info_label(xml, "pad2", ""); + _gen_info_label(xml, "path", component.path); + _gen_info_label(xml, "pad3", ""); + xml.node("label", [&] () { + xml.attribute("text", "not installed"); }); + } + + _gen_info_label(xml, "pad4", ""); + }); + }); + }); + } + } + cnt++; + }); + } + + /* + * Pkg configuration + */ + if (_state >= PKG_SHOWN) + _construction_info.with_construction([&] (Component const &component) { + _gen_pkg_elements(xml, component); }); +} + + +void Popup_dialog::click(Action &action) +{ + Hoverable_item::Id const clicked = _item._hovered; + _item._hovered = Hoverable_item::Id(); + + _action_item .propose_activation_on_click(); + _install_item.propose_activation_on_click(); + + Route::Id const clicked_route = _route_item._hovered; + + auto back_to_index = [&] () + { + _state = INDEX_SHOWN; + action.discard_construction(); + _selected_route.destruct(); + }; + + if (_state == TOP_LEVEL) { + + if (clicked == "depot") { + _state = DEPOT_REQUESTED; + _depot_query.trigger_depot_query(); + } else { + action.launch_global(clicked); + } + } + + else if (_state == DEPOT_SHOWN) { + + /* back to top-level menu */ + if (clicked == "back") { + _state = TOP_LEVEL; + + } else if (clicked == "selection") { + _state = DEPOT_SELECTION; + + } else { + + /* enter depot users menu */ + _selected_user = clicked; + _state = INDEX_REQUESTED; + _depot_query.trigger_depot_query(); + } + } + + else if (_state == DEPOT_SELECTION) { + + /* back to depot users */ + if (clicked == "back") { + _state = DEPOT_SHOWN; + } else { + + if (!_index_avail(clicked)) + action.trigger_download(_index_path(clicked)); + } + } + + else if (_state >= INDEX_SHOWN && _state < PKG_SHOWN) { + + /* back to depot users */ + if (_menu._level == 0 && clicked == "back") { + _state = DEPOT_SHOWN; + _selected_user = User(); + } else { + + /* go one menu up */ + if (clicked == "back") { + _menu._selected[_menu._level] = Menu::Name(); + _menu._level--; + action.discard_construction(); + } else { + + /* enter sub menu of index */ + if (_menu._level < Menu::MAX_LEVELS - 1) { + + unsigned cnt = 0; + _for_each_menu_item([&] (Xml_node item) { + + if (clicked == Hoverable_item::Id(cnt)) { + + if (item.has_type("index")) { + + Menu::Name const name = + item.attribute_value("name", Menu::Name()); + + _menu._selected[_menu._level] = name; + _menu._level++; + + } else if (item.has_type("pkg")) { + + auto path = item.attribute_value("path", Component::Path()); + auto info = item.attribute_value("info", Component::Info()); + + _construction_name = action.new_construction(path, info); + + _state = PKG_REQUESTED; + _pkg_missing = false; + _depot_query.trigger_depot_query(); + } + } + cnt++; + }); + } + } + } + } + + else if (_state == PKG_SHOWN) { + + /* back to index */ + if (clicked == "back") { + back_to_index(); + + } else { + + /* select route to present routing options */ + if (clicked_route.valid()) { + _state = ROUTE_SELECTED; + _selected_route.construct(clicked_route); + } + } + } + + else if (_state == ROUTE_SELECTED) { + + /* back to index */ + if (clicked == "back") { + back_to_index(); + + } else { + + /* close selected route */ + if (clicked_route == "back") { + _state = PKG_SHOWN; + _selected_route.destruct(); + + } else { + + bool clicked_on_selected_route = false; + + _apply_to_selected_route(action, [&] (Route &route) { + + unsigned cnt = 0; + _runtime_config.for_each_service([&] (Service const &service) { + + Hoverable_item::Id const id("service.", cnt++); + + if (clicked_route == id) { + + bool const clicked_service_already_selected = + route.selected_service.constructed() && + id == route.selected_service_id; + + if (clicked_service_already_selected) { + + /* clear selection */ + route.selected_service.destruct(); + route.selected_service_id = Hoverable_item::Id(); + + } else { + + /* select different service */ + route.selected_service.construct(service); + route.selected_service_id = id; + } + + _state = PKG_SHOWN; + _selected_route.destruct(); + + clicked_on_selected_route = true; + } + }); + }); + + /* select different route */ + if (!clicked_on_selected_route && clicked_route.valid()) { + _state = ROUTE_SELECTED; + _selected_route.construct(clicked_route); + } + } + } + } + + generate(); +} diff --git a/repos/gems/src/app/sculpt_manager/view/popup_dialog.h b/repos/gems/src/app/sculpt_manager/view/popup_dialog.h index a3089fe4e..bbcd02495 100644 --- a/repos/gems/src/app/sculpt_manager/view/popup_dialog.h +++ b/repos/gems/src/app/sculpt_manager/view/popup_dialog.h @@ -14,10 +14,21 @@ #ifndef _VIEW__POPUP_DIALOG_H_ #define _VIEW__POPUP_DIALOG_H_ +/* Genode includes */ +#include +#include + +/* local includes */ #include #include +#include +#include +#include +#include +#include #include -#include +#include +#include namespace Sculpt { struct Popup_dialog; } @@ -26,9 +37,70 @@ struct Sculpt::Popup_dialog { Env &_env; - Launchers const &_launchers; + Allocator &_alloc; - Runtime_info const &_runtime_info; + Sculpt_version const _sculpt_version { _env }; + + Launchers const &_launchers; + Nic_state const &_nic_state; + Nic_target const &_nic_target; + + bool _nic_ready() const { return _nic_target.ready() && _nic_state.ready(); } + + Runtime_info const &_runtime_info; + Runtime_config const &_runtime_config; + Download_queue const &_download_queue; + + Depot_query &_depot_query; + + struct Construction_info : Interface + { + struct With : Interface { virtual void with(Component const &) const = 0; }; + + virtual void _with_construction(With const &) const = 0; + + template + void with_construction(FN const &fn) const + { + struct _With : With { + FN const &_fn; + _With(FN const &fn) : _fn(fn) { } + void with(Component const &c) const override { _fn(c); } + }; + _with_construction(_With(fn)); + } + }; + + struct Action : Interface + { + virtual void launch_global(Path const &launcher) = 0; + + virtual Start_name new_construction(Component::Path const &pkg, + Component::Info const &info) = 0; + + struct Apply_to : Interface { virtual void apply_to(Component &) = 0; }; + + virtual void _apply_to_construction(Apply_to &) = 0; + + template + void apply_to_construction(FN const &fn) + { + struct _Apply_to : Apply_to { + FN const &_fn; + _Apply_to(FN const &fn) : _fn(fn) { } + void apply_to(Component &c) override { _fn(c); } + } apply_fn(fn); + + _apply_to_construction(apply_fn); + } + + virtual void discard_construction() = 0; + virtual void launch_construction() = 0; + + virtual void trigger_download(Path const &) = 0; + }; + + Construction_info const &_construction_info; Expanding_reporter _dialog_reporter { _env, "dialog", "popup_dialog" }; @@ -37,10 +109,85 @@ struct Sculpt::Popup_dialog Signal_handler _hover_handler { _env.ep(), *this, &Popup_dialog::_handle_hover }; - Hoverable_item _item { }; + Hoverable_item _item { }; + Activatable_item _action_item { }; + Activatable_item _install_item { }; + Hoverable_item _route_item { }; bool _hovered = false; + enum State { TOP_LEVEL, DEPOT_REQUESTED, DEPOT_SHOWN, DEPOT_SELECTION, + INDEX_REQUESTED, INDEX_SHOWN, + PKG_REQUESTED, PKG_SHOWN, ROUTE_SELECTED }; + + State _state { TOP_LEVEL }; + + typedef Depot::Archive::User User; + User _selected_user { }; + + bool _pkg_missing = false; + + Component::Name _construction_name { }; + + Constructible _selected_route { }; + + bool _route_selected(Route::Id const &id) const + { + return _selected_route.constructed() && id == _selected_route->string(); + } + + template + void _apply_to_selected_route(Action &action, FN const &fn) + { + unsigned cnt = 0; + action.apply_to_construction([&] (Component &component) { + component.routes.for_each([&] (Route &route) { + if (_route_selected(Route::Id(cnt++))) + fn(route); }); }); + } + + struct Menu + { + enum { MAX_LEVELS = 5 }; + + unsigned _level = 0; + + typedef String<64> Name; + + Name _selected[MAX_LEVELS] { }; + + void print(Output &out) const + { + using Genode::print; + for (unsigned i = 0; i < _level; i++) { + print(out, _selected[i]); + if (i + 1 < _level) + print(out, " "); + } + } + + template + void _for_each_item(Xml_node index, FN const &fn, unsigned level) const + { + if (level == _level) { + index.for_each_sub_node(fn); + return; + } + + index.for_each_sub_node("index", [&] (Xml_node index) { + if (index.attribute_value("name", Name()) == _selected[level]) + _for_each_item(index, fn, level + 1); }); + } + + template + void for_each_item(Xml_node index, FN const &fn) const + { + _for_each_item(index, fn, 0); + } + }; + + Menu _menu { }; + void _handle_hover() { _hover_rom.update(); @@ -49,7 +196,11 @@ struct Sculpt::Popup_dialog _hovered = hover.has_sub_node("dialog"); - bool const changed = _item.match(hover, "dialog", "frame", "vbox", "hbox", "name"); + bool const changed = + _item .match(hover, "dialog", "frame", "vbox", "hbox", "name") | + _action_item .match(hover, "dialog", "frame", "vbox", "button", "name") | + _install_item.match(hover, "dialog", "frame", "vbox", "float", "vbox", "float", "button", "name") | + _route_item .match(hover, "dialog", "frame", "vbox", "frame", "vbox", "hbox", "name"); if (changed) generate(); @@ -57,78 +208,279 @@ struct Sculpt::Popup_dialog bool hovered() const { return _hovered; }; + Attached_rom_dataspace _scan_rom { _env, "report -> runtime/depot_query/scan" }; + + Signal_handler _scan_handler { + _env.ep(), *this, &Popup_dialog::_handle_scan }; + + void _handle_scan() + { + _scan_rom.update(); + + if (_state == DEPOT_REQUESTED) + _state = DEPOT_SHOWN; + + if (_state != TOP_LEVEL) + generate(); + } + + Attached_rom_dataspace _index_rom { _env, "report -> runtime/depot_query/index" }; + + Signal_handler _index_handler { + _env.ep(), *this, &Popup_dialog::_handle_index }; + + void _handle_index() + { + /* prevent modifications of index while browing it */ + if (_state >= INDEX_SHOWN) + return; + + _index_rom.update(); + + if (_state == INDEX_REQUESTED) + _state = INDEX_SHOWN; + + generate(); + } + + bool _index_avail(User const &user) const + { + bool result = false; + _index_rom.xml().for_each_sub_node("index", [&] (Xml_node index) { + if (index.attribute_value("user", User()) == user) + result = true; }); + return result; + }; + + Path _index_path(User const &user) const + { + return Path(user, "/index/", _sculpt_version); + } + + void _gen_sub_menu_title(Xml_generator &xml, + Start_name const &name, + Start_name const &text) const + { + gen_named_node(xml, "hbox", name, [&] () { + gen_named_node(xml, "float", "left", [&] () { + xml.attribute("west", "yes"); + xml.node("hbox", [&] () { + gen_named_node(xml, "button", "back", [&] () { + xml.attribute("selected", "yes"); + xml.attribute("style", "back"); + _item.gen_button_attr(xml, name); + xml.node("hbox", [&] () { }); + }); + gen_named_node(xml, "label", "label", [&] () { + xml.attribute("font", "title/regular"); + xml.attribute("text", Path(" ", text)); + }); + }); + }); + gen_named_node(xml, "hbox", "right", [&] () { }); + }); + } + + void _gen_menu_entry(Xml_generator &xml, Start_name const &name, + Component::Info const &text, bool selected, + char const *style = "radio") const + { + gen_named_node(xml, "hbox", name, [&] () { + + gen_named_node(xml, "float", "left", [&] () { + xml.attribute("west", "yes"); + + xml.node("hbox", [&] () { + gen_named_node(xml, "button", "button", [&] () { + + if (selected) + xml.attribute("selected", "yes"); + + xml.attribute("style", style); + _item.gen_button_attr(xml, name); + xml.node("hbox", [&] () { }); + }); + gen_named_node(xml, "label", "name", [&] () { + xml.attribute("text", Path(" ", text)); }); + }); + }); + + gen_named_node(xml, "hbox", "right", [&] () { }); + }); + } + + void _gen_route_entry(Xml_generator &xml, + Start_name const &name, + Start_name const &text, + bool selected, char const *style = "radio") const + { + gen_named_node(xml, "hbox", name, [&] () { + + gen_named_node(xml, "float", "left", [&] () { + xml.attribute("west", "yes"); + + xml.node("hbox", [&] () { + gen_named_node(xml, "button", "button", [&] () { + + if (selected) + xml.attribute("selected", "yes"); + + xml.attribute("style", style); + _route_item.gen_button_attr(xml, name); + xml.node("hbox", [&] () { }); + }); + gen_named_node(xml, "label", "name", [&] () { + xml.attribute("text", Path(" ", text)); }); + }); + }); + + gen_named_node(xml, "hbox", "right", [&] () { }); + }); + } + + template + void _for_each_menu_item(FN const &fn) const + { + Xml_node index = _index_rom.xml(); + + /* + * The index may contain duplicates, evaluate only the first match. + */ + bool first = true; + index.for_each_sub_node("index", [&] (Xml_node index) { + + if (index.attribute_value("user", User()) != _selected_user) + return; + + if (first) + _menu.for_each_item(index, fn); + + first = false; + }); + } + + static void _gen_info_label(Xml_generator &xml, char const *name, + Component::Info const &info) + { + gen_named_node(xml, "label", name, [&] () { + xml.attribute("font", "annotation/regular"); + xml.attribute("text", Component::Info(" ", info, " ")); }); + } + + void _gen_pkg_info (Xml_generator &, Component const &) const; + void _gen_pkg_elements (Xml_generator &, Component const &) const; + void _gen_menu_elements(Xml_generator &) const; + void generate() { _dialog_reporter.generate([&] (Xml_generator &xml) { xml.node("frame", [&] () { xml.node("vbox", [&] () { + _gen_menu_elements(xml); }); }); }); + } - _launchers.for_each([&] (Launchers::Info const &info) { + void click(Action &action); - /* allow each launcher to be used only once */ - if (_runtime_info.present_in_runtime(info.path)) - return; + void clack(Action &action) + { + _action_item.confirm_activation_on_clack(); + _install_item.confirm_activation_on_clack(); - gen_named_node(xml, "hbox", info.path, [&] () { + if (_action_item.activated("launch")) { + action.launch_construction(); + reset(); + } - gen_named_node(xml, "float", "left", [&] () { - xml.attribute("west", "yes"); - - xml.node("hbox", [&] () { - gen_named_node(xml, "button", "button", [&] () { - xml.attribute("style", "radio"); - _item.gen_button_attr(xml, info.path); - xml.node("hbox", [&] () { }); - }); - gen_named_node(xml, "label", "name", [&] () { - xml.attribute("text", Path(" ", info.path)); }); - }); - }); - - gen_named_node(xml, "hbox", "right", [&] () { }); - }); - }); - }); + if (_pkg_missing && _install_item.activated("install")) { + _construction_info.with_construction([&] (Component const &component) { + action.trigger_download(component.path); + _install_item.reset(); + generate(); }); - }); + } } - struct Action : Interface - { - virtual void launch_global(Path const &launcher) = 0; - }; - - void click(Action &action) - { - action.launch_global(_item._hovered); - } - - /* - * XXX - * - * This method should not be needed. However, in some situations, - * the popup menu_view does not receive a leave event when the popup - * is closed, to the effect of maintaining stale hover information - * instead of generating an empty hover report. So from the sculpt- - * manager's point of view, the popup is still hovered even if it is - * actually closed. The 'reset_hover' shortcuts the regular hover - * update in this situation. - */ - void reset_hover() + void reset() { _item._hovered = Hoverable_item::Id(); + _route_item._hovered = Hoverable_item::Id(); + _action_item.reset(); + _install_item.reset(); _hovered = false; + _state = TOP_LEVEL; + _selected_user = User(); + _selected_route.destruct(); + _menu._level = 0; } - Popup_dialog(Env &env, Launchers const &launchers, - Runtime_info const &runtime_info) + Popup_dialog(Env &env, Allocator &alloc, + Launchers const &launchers, + Nic_state const &nic_state, + Nic_target const &nic_target, + Runtime_info const &runtime_info, + Runtime_config const &runtime_config, + Download_queue const &download_queue, + Depot_query &depot_query, + Construction_info const &construction_info) : - _env(env), _launchers(launchers), _runtime_info(runtime_info) + _env(env), _alloc(alloc), _launchers(launchers), + _nic_state(nic_state), _nic_target(nic_target), + _runtime_info(runtime_info), _runtime_config(runtime_config), + _download_queue(download_queue), _depot_query(depot_query), + _construction_info(construction_info) { _hover_rom.sigh(_hover_handler); + _scan_rom.sigh(_scan_handler); + _index_rom.sigh(_index_handler); generate(); } + + void gen_depot_query(Xml_generator &xml) const + { + if (_state >= TOP_LEVEL) + xml.node("scan", [&] () { + xml.attribute("users", "yes"); }); + + if (_state >= TOP_LEVEL) + _scan_rom.xml().for_each_sub_node("user", [&] (Xml_node user) { + xml.node("index", [&] () { + User const name = user.attribute_value("name", User()); + xml.attribute("user", name); + xml.attribute("version", _sculpt_version); + if (_state >= INDEX_REQUESTED && _selected_user == name) + xml.attribute("content", "yes"); + }); + }); + + if (_state >= PKG_REQUESTED) + _construction_info.with_construction([&] (Component const &component) { + xml.node("blueprint", [&] () { + xml.attribute("pkg", component.path); }); }); + } + + void apply_blueprint(Component &construction, Xml_node blueprint) + { + if (_state < PKG_REQUESTED) + return; + + _pkg_missing = blueprint_missing(blueprint, construction.path) + || blueprint_any_rom_missing(blueprint); + + construction.try_apply_blueprint(blueprint); + if (construction.blueprint_known) + _state = PKG_SHOWN; + + generate(); + } + + bool interested_in_download() const + { + if (_state == DEPOT_SELECTION) + return true; + + return _state >= PKG_REQUESTED && _pkg_missing; + } }; #endif /* _VIEW__POPUP_DIALOG_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/xml.h b/repos/gems/src/app/sculpt_manager/xml.h index c680191ae..04b981144 100644 --- a/repos/gems/src/app/sculpt_manager/xml.h +++ b/repos/gems/src/app/sculpt_manager/xml.h @@ -121,7 +121,7 @@ namespace Sculpt { } /** - * Query attribute value from XML sub nodd + * Query attribute value from XML sub node * * The list of arguments except for the last one refer to XML path into the * XML structure. The last argument denotes the queried attribute name.