From 2ce87216bc6266cacac868e0c0d844b7063091ad Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Fri, 10 Feb 2017 21:15:45 +0100 Subject: [PATCH] os: input_filter implementation and test The input_filter is the successor of the input_merger. In addition to merging input streams, the component applies several forms of input transformations such as the application of keyboard layouts. Issue #2264 --- repos/os/run/input_filter.run | 355 ++++++++++++ repos/os/src/server/input_filter/README | 110 ++++ .../src/server/input_filter/chargen_source.h | 524 ++++++++++++++++++ repos/os/src/server/input_filter/connection.h | 95 ++++ repos/os/src/server/input_filter/de.chargen | 164 ++++++ .../os/src/server/input_filter/en_us.chargen | 148 +++++ .../server/input_filter/include_accessor.h | 67 +++ .../os/src/server/input_filter/input_source.h | 47 ++ .../server/input_filter/key_code_by_name.h | 43 ++ repos/os/src/server/input_filter/main.cc | 436 +++++++++++++++ .../os/src/server/input_filter/merge_source.h | 49 ++ .../os/src/server/input_filter/remap_source.h | 132 +++++ repos/os/src/server/input_filter/source.h | 96 ++++ repos/os/src/server/input_filter/target.mk | 4 + .../src/server/input_filter/timer_accessor.h | 24 + repos/os/src/server/input_filter/types.h | 23 + repos/os/src/test/input_filter/main.cc | 425 ++++++++++++++ repos/os/src/test/input_filter/target.mk | 3 + tool/autopilot.list | 1 + 19 files changed, 2746 insertions(+) create mode 100644 repos/os/run/input_filter.run create mode 100644 repos/os/src/server/input_filter/README create mode 100644 repos/os/src/server/input_filter/chargen_source.h create mode 100644 repos/os/src/server/input_filter/connection.h create mode 100644 repos/os/src/server/input_filter/de.chargen create mode 100644 repos/os/src/server/input_filter/en_us.chargen create mode 100644 repos/os/src/server/input_filter/include_accessor.h create mode 100644 repos/os/src/server/input_filter/input_source.h create mode 100644 repos/os/src/server/input_filter/key_code_by_name.h create mode 100644 repos/os/src/server/input_filter/main.cc create mode 100644 repos/os/src/server/input_filter/merge_source.h create mode 100644 repos/os/src/server/input_filter/remap_source.h create mode 100644 repos/os/src/server/input_filter/source.h create mode 100644 repos/os/src/server/input_filter/target.mk create mode 100644 repos/os/src/server/input_filter/timer_accessor.h create mode 100644 repos/os/src/server/input_filter/types.h create mode 100644 repos/os/src/test/input_filter/main.cc create mode 100644 repos/os/src/test/input_filter/target.mk diff --git a/repos/os/run/input_filter.run b/repos/os/run/input_filter.run new file mode 100644 index 000000000..a13a6ee37 --- /dev/null +++ b/repos/os/run/input_filter.run @@ -0,0 +1,355 @@ +# +# Build +# + +set build_components { + core init drivers/timer + server/report_rom server/input_filter test/input_filter +} + +build $build_components + +create_boot_directory + +# +# Generate config +# + +append config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +install_config $config + +# +# Boot modules +# + +set boot_modules { core ld.lib.so init timer report_rom + input_filter test-input_filter } + +build_boot_image $boot_modules + +append qemu_args " -nographic " + +run_genode_until {.*child "test-input_filter" exited with exit value 0.*} 60 + diff --git a/repos/os/src/server/input_filter/README b/repos/os/src/server/input_filter/README new file mode 100644 index 000000000..fbf07aec6 --- /dev/null +++ b/repos/os/src/server/input_filter/README @@ -0,0 +1,110 @@ +This component transforms input events originating from multiple sources. + + +Configuration +------------- + +An input-filter configuration consists of two parts, a declaration of +input sources ("Input" connections) that the component should request, +and the definition of a filter chain. Each input source is defined via +an '' node with the name of the input source as 'name' attribute and +the session label as 'label' attribute. The latter can be used to route +several input sources to different components, i.e, input device drivers. + +The filter chain is defined via one '' node. It contains exactly +one of the following filters: + +:: + + Refers to the input source with the matching 'name'. + +:: + + Applies low-level key remapping to the events produced by another filter + that is embedded as a child node. + + It may contain any number of '' nodes. Each of those key nodes has + the key name as 'name' attribute, may feature an optional 'to' attribute + with the name of the key that should be reported instead of 'name', and + an optional 'sticky' attribute. If the latter is set to "yes", the key + behaves like a sticky key. That means, only press events are evaluated + and every second press event is reported as a release event. This is + useful for special keys like capslock. + +:: + + Merges the results of any number of filters that appear as child nodes. + +:: + + Supplements the input-event stream of another filter with artificial + 'CHARACTER' events by applying character mapping rules. The originating + filter is defined as a child node. + + +Character generator rules +------------------------- + +The character-generator ('') rules are defined via the following +sub nodes: + +:///: + + Defines which physical keys are interpreted as modifier keys. Usually, + '' corresponds to shift, '' to control, and '' to altgr + (on German keyboards). Each modifier node may host any number of '' + nodes with their corresponding 'name' attribute. For example: + + ! + ! + ! + +:: + + A '' node contains a list of keys that emit a specified character when + pressed. Any number of '' nodes can be present. For each map node, the + attributes 'mod1' to 'mod4' denote the condition, under which it is + considered. Each 'mod' attribute has three possible values. If the attribute + is not present, the state of the modifier does not matter. If set to 'yes', + the modifier must be active. If set to 'no', the modifier must not be active. + + Each '' may contain any number of '' subnodes. Each '' + must have the key name as 'name' attribute. The to-be-emitted character + is defined by the attributes 'ascii', 'char', or 'b0/b1/b2/b3'. The + 'ascii' attribute accepts an integer value between 0 and 127, the + 'char' attribute accepts a single ASCII character, the 'b0/b1/b2/b3' + attributes define the individual bytes of an UTF-8 character. + +:: + + The '' node defines the character-repeat delay and rate that + triggers the periodic emission of the last produced character while + the corresponding key is held. + +:: + + The '' node includes further content into the '' node + and thereby allows the easy reuse of common rules. The included ROM must + have an '' top-level node. + + +Additional features +------------------- + +The input filter is able to respond to configuration updates as well as updates +of included ROM modules. However, a new configuration is applied only if the +input sources are in their idle state - that is, no key is pressed. This +ensures the consistency of the generated key events (for each press event there +must be a corresponding release event), on which clients of the input filter +may depend. However, this deferred reconfiguration can be overridden by setting +the 'force' attribute of the '' node to 'yes'. If forced, the new +configuration is applied immediately. + + +Examples +-------- + +An automated test that exercises various corner cases of the input filter +can be found at _os/run/input_filter.run_. For a practical example of how +to use the input filter with the terminal, please refer to the +_gems/run/terminal_echo.run_ script. diff --git a/repos/os/src/server/input_filter/chargen_source.h b/repos/os/src/server/input_filter/chargen_source.h new file mode 100644 index 000000000..8219ee9aa --- /dev/null +++ b/repos/os/src/server/input_filter/chargen_source.h @@ -0,0 +1,524 @@ +/* + * \brief Input-event source that generates character events + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__CHARGEN_SOURCE_H_ +#define _INPUT_FILTER__CHARGEN_SOURCE_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include +#include +#include + +namespace Input_filter { class Chargen_source; } + + +class Input_filter::Chargen_source : public Source, Source::Sink +{ + private: + + Allocator &_alloc; + Timer_accessor &_timer_accessor; + Include_accessor &_include_accessor; + + /* + * Modifier definitions + */ + + struct Modifier + { + enum Id { MOD1 = 0, MOD2 = 1, MOD3 = 2, MOD4 = 3, UNDEFINED }; + + typedef String<8> Name; + + Registry::Element _element; + + Id const _id; + + Input::Keycode const _code; + + static Id id(Xml_node mod_node) + { + if (mod_node.type() == "mod1") return MOD1; + if (mod_node.type() == "mod2") return MOD2; + if (mod_node.type() == "mod3") return MOD3; + if (mod_node.type() == "mod4") return MOD4; + + return UNDEFINED; + } + + Modifier(Registry ®istry, Id id, Input::Keycode code) + : + _element(registry, *this), _id(id), _code(code) + { } + + Input::Keycode code() const { return _code; } + + Id id() const { return _id; } + }; + + Registry _modifiers; + + /* + * Key rules for generating characters + */ + + enum { NUM_MODIFIERS = 4 }; + + /** + * Cached state of modifiers, updated when a modifier key event occurs + */ + struct Modifier_map + { + struct State { bool enabled = false; } states[NUM_MODIFIERS]; + + } _mod_map; + + /** + * State tracked per physical key + */ + struct Key + { + enum Type { DEFAULT, MODIFIER } type = DEFAULT; + enum State { RELEASED, PRESSED } state = RELEASED; + + struct Rule + { + Registry::Element _reg_elem; + + /* + * Conditions that must be satisfied to let the rule take effect + */ + struct Conditions + { + struct Modifier + { + enum Constraint { PRESSED, RELEASED, DONT_CARE }; + + Constraint constraint = DONT_CARE; + + bool match(Modifier_map::State state) const + { + if ((constraint == RELEASED && state.enabled) || + (constraint == PRESSED && !state.enabled)) + return false; + + return true; + } + }; + + Modifier modifiers[NUM_MODIFIERS]; + + /** + * Return true if current modifier state fulfils conditions + */ + bool match(Modifier_map const &mod_map) const + { + for (unsigned i = 0; i < NUM_MODIFIERS; i++) + if (!modifiers[i].match(mod_map.states[i])) + return false; + + return true; + } + + unsigned num_modifier_constraints() const + { + unsigned cnt = 0; + for (unsigned i = 0; i < NUM_MODIFIERS; i++) + if (modifiers[i].constraint != Modifier::DONT_CARE) + cnt++; + + return cnt; + } + }; + + Conditions const _conditions; + + Input::Event::Utf8 const _character; + + Rule(Registry ®istry, + Conditions conditions, + Input::Event::Utf8 character) + : + _reg_elem(registry, *this), + _conditions(conditions), + _character(character) + { } + + /** + * Return match score for the given modifier state + * + * \return 0 if rule mismatches, + * 1 if rule matches, + * 1+N if rule with N modifier constraints matches + */ + unsigned match_score(Modifier_map const &mod_map) const + { + if (!_conditions.match(mod_map)) + return 0; + + return 1 + _conditions.num_modifier_constraints(); + } + + Input::Event::Utf8 character() const { return _character; } + }; + + Registry rules; + + /** + * Call functor 'fn' with the 'Input::Event::Utf8' character + * defined for the best matching rule + */ + template + void apply_best_matching_rule(Modifier_map const &mod_map, FN const &fn) const + { + Input::Event::Utf8 best_match { 0 }; + + unsigned max_score = 0; + + rules.for_each([&] (Rule const &rule) { + + unsigned score = rule.match_score(mod_map); + if (score <= max_score) + return; + + max_score = score; + best_match = rule.character(); + }); + + if (max_score > 0) + fn(best_match); + } + }; + + /** + * Map of the states of the physical keys + */ + class Key_map + { + private: + + Allocator &_alloc; + + Key _keys[Input::KEY_MAX]; + + public: + + Key_map(Allocator &alloc) : _alloc(alloc) { } + + ~Key_map() + { + for (unsigned i = 0; i < Input::KEY_MAX; i++) + _keys[i].rules.for_each([&] (Key::Rule &rule) { + destroy(_alloc, &rule); }); + } + + /** + * Return key object that belongs to the specified key code + */ + Key &key(Input::Keycode code) + { + if ((unsigned)code >= (unsigned)Input::KEY_MAX) + return _keys[Input::KEY_UNKNOWN]; + + return _keys[code]; + }; + + /** + * Obtain modifier condition from map XML node + */ + static Key::Rule::Conditions::Modifier::Constraint + _map_mod_cond(Xml_node map, Modifier::Name const &mod_name) + { + if (!map.has_attribute(mod_name.string())) + return Key::Rule::Conditions::Modifier::DONT_CARE; + + bool const pressed = map.attribute_value(mod_name.string(), false); + + return pressed ? Key::Rule::Conditions::Modifier::PRESSED + : Key::Rule::Conditions::Modifier::RELEASED; + } + + struct Missing_character_definition { }; + + /** + * Return UTF8 character defined in XML node attributes + * + * \throw Missing_character_definition + */ + static Input::Event::Utf8 _utf8_from_xml_node(Xml_node node) + { + if (node.has_attribute("ascii")) + return Input::Event::Utf8(node.attribute_value("ascii", 0UL)); + + if (node.has_attribute("char")) { + + typedef String<2> Value; + Value value = node.attribute_value("char", Value()); + + unsigned char const ascii = value.string()[0]; + + if (ascii < 128) + return Input::Event::Utf8(ascii); + + warning("char attribute with non-ascii character " + "'", value, "'"); + throw Missing_character_definition(); + } + + if (node.has_attribute("b0")) { + unsigned char const b0 = node.attribute_value("b0", 0UL), + b1 = node.attribute_value("b1", 0UL), + b2 = node.attribute_value("b2", 0UL), + b3 = node.attribute_value("b3", 0UL); + + return Input::Event::Utf8(b0, b1, b2, b3); + } + + throw Missing_character_definition(); + } + + void import_map(Xml_node map) + { + /* obtain modifier conditions from map attributes */ + Key::Rule::Conditions cond; + cond.modifiers[Modifier::MOD1].constraint = _map_mod_cond(map, "mod1"); + cond.modifiers[Modifier::MOD2].constraint = _map_mod_cond(map, "mod2"); + cond.modifiers[Modifier::MOD3].constraint = _map_mod_cond(map, "mod3"); + cond.modifiers[Modifier::MOD4].constraint = _map_mod_cond(map, "mod4"); + + /* add a rule for each sub node */ + map.for_each_sub_node("key", [&] (Xml_node key_node) { + + Key_name const name = key_node.attribute_value("name", Key_name()); + + Input::Keycode const code = key_code_by_name(name); + + new (_alloc) Key::Rule(key(code).rules, cond, + _utf8_from_xml_node(key_node)); + }); + } + + } _key_map; + + void _update_modifier_state() + { + /* reset */ + _mod_map = Modifier_map(); + + /* apply state of all modifier keys to modifier map */ + _modifiers.for_each([&] (Modifier const &mod) { + _mod_map.states[mod.id()].enabled |= + _key_map.key(mod.code()).state; }); + } + + Owner _owner; + + Source::Sink &_destination; + + /** + * Mechanism for periodically repeating the last character + */ + struct Char_repeater + { + Source::Sink &_destination; + Genode::Timer &_timer; + + Time_source::Microseconds const _delay; + Time_source::Microseconds const _rate; + + Input::Event::Utf8 _curr_character { 0 }; + + enum State { IDLE, REPEAT } _state; + + void _handle_timeout(Time_source::Microseconds) + { + if (_state == REPEAT) { + _destination.submit_event(Input::Event(_curr_character)); + _timeout.start(_rate); + } + } + + One_shot_timeout _timeout { + _timer, *this, &Char_repeater::_handle_timeout }; + + Char_repeater(Source::Sink &destination, Genode::Timer &timer, + Xml_node node) + : + _destination(destination), _timer(timer), + _delay(node.attribute_value("delay_ms", 0UL)*1000), + _rate (node.attribute_value("rate_ms", 0UL)*1000) + { } + + void schedule_repeat(Input::Event::Utf8 character) + { + _curr_character = character; + _state = REPEAT; + + _timeout.start(_delay); + } + + void cancel() + { + _curr_character = Input::Event::Utf8(0); + _state = IDLE; + } + }; + + Constructible _char_repeater; + + /** + * Sink interface (called from our child node) + */ + void submit_event(Input::Event const &event) override + { + using Input::Event; + + /* forward event as is */ + _destination.submit_event(event); + + /* don't do anything for non-press/release events */ + if (event.type() != Event::PRESS && event.type() != Event::RELEASE) + return; + + Key &key = _key_map.key(event.keycode()); + + /* track key state */ + if (event.type() == Event::PRESS) key.state = Key::PRESSED; + if (event.type() == Event::RELEASE) key.state = Key::RELEASED; + + if (key.type == Key::MODIFIER) { + _update_modifier_state(); + + /* never emit a character when pressing a modifier key */ + return; + } + + if (event.type() == Event::PRESS) { + key.apply_best_matching_rule(_mod_map, [&] (Event::Utf8 utf8) { + + _destination.submit_event(Event(utf8)); + + if (_char_repeater.constructed()) + _char_repeater->schedule_repeat(utf8); + }); + } + + if (event.type() == Event::RELEASE) + if (_char_repeater.constructed()) + _char_repeater->cancel(); + } + + Source &_source; + + void _apply_config(Xml_node const config, unsigned const max_recursion = 4) + { + config.for_each_sub_node([&] (Xml_node node) { + _apply_sub_node(node, max_recursion); }); + } + + void _apply_sub_node(Xml_node const node, unsigned const max_recursion) + { + if (max_recursion == 0) { + error("too deeply nested includes"); + throw Invalid_config(); + } + + /* + * Handle includes + */ + if (node.type() == "include") { + try { + Include_accessor::Name const rom = + node.attribute_value("rom", Include_accessor::Name()); + + _include_accessor.apply_include(rom, name(), [&] (Xml_node inc) { + _apply_config(inc, max_recursion - 1); }); + return; + } + catch (Include_accessor::Include_unavailable) { + throw Invalid_config(); } + } + + /* + * Handle map nodes + */ + if (node.type() == "map") { + _key_map.import_map(node); + return; + } + + /* + * Instantiate character repeater on demand + */ + if (node.type() == "repeat") { + _char_repeater.construct(_destination, + _timer_accessor.timer(), node); + return; + } + + /* + * Handle modifier-definition nodes + */ + Modifier::Id const id = Modifier::id(node); + if (id == Modifier::UNDEFINED) + return; + + node.for_each_sub_node("key", [&] (Xml_node key_node) { + + Key_name const name = key_node.attribute_value("name", Key_name()); + Input::Keycode const key = key_code_by_name(name); + + new (_alloc) Modifier(_modifiers, id, key); + }); + } + + public: + + static char const *name() { return "chargen"; } + + Chargen_source(Owner &owner, + Xml_node config, + Source::Sink &destination, + Source::Factory &factory, + Allocator &alloc, + Timer_accessor &timer_accessor, + Include_accessor &include_accessor) + : + Source(owner), + _alloc(alloc), + _timer_accessor(timer_accessor), + _include_accessor(include_accessor), + _key_map(_alloc), + _owner(factory), + _destination(destination), + _source(factory.create_source(_owner, input_sub_node(config), *this)) + { + _apply_config(config); + + /* assign key types in key map */ + _modifiers.for_each([&] (Modifier const &mod) { + _key_map.key(mod.code()).type = Key::MODIFIER; }); + } + + ~Chargen_source() + { + _modifiers.for_each([&] (Modifier &mod) { destroy(_alloc, &mod); }); + } + + void generate() override { _source.generate(); } +}; + +#endif /* _INPUT_FILTER__CHARGEN_SOURCE_H_ */ diff --git a/repos/os/src/server/input_filter/connection.h b/repos/os/src/server/input_filter/connection.h new file mode 100644 index 000000000..72d06015d --- /dev/null +++ b/repos/os/src/server/input_filter/connection.h @@ -0,0 +1,95 @@ +/* + * \brief Connection for incoming input events + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__CONNECTION_H_ +#define _INPUT_FILTER__CONNECTION_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include + +namespace Input_filter { struct Input_connection; } + + +class Input_filter::Input_connection +{ + public: + + struct Avail_handler { virtual void handle_input_avail() = 0; }; + + private: + + Session_label const _label; + Input::Connection _connection; + Attached_dataspace _events_ds; + Avail_handler &_avail_handler; + + unsigned _key_cnt = 0; + + Signal_handler _input_handler; + + void _handle_input() { _avail_handler.handle_input_avail(); } + + size_t _num_ev = 0; + + size_t const _max_events = _events_ds.size() / sizeof(Input::Event); + + public: + + static char const *name() { return "input"; } + + Input_connection(Env &env, Session_label const &label, + Avail_handler &avail_handler, Allocator &alloc) + : + _label(label), + _connection(env, label.string()), + _events_ds(env.rm(), _connection.dataspace()), + _avail_handler(avail_handler), + _input_handler(env.ep(), *this, &Input_connection::_handle_input) + { + _connection.sigh(_input_handler); + } + + Session_label label() const { return _label; } + + template + void for_each_event(FUNC const &func) const + { + Input::Event const *event_ptr = _events_ds.local_addr(); + + for (size_t i = 0; i < _num_ev; i++) + func(*event_ptr++); + } + + void flush() + { + _num_ev = min(_max_events, (size_t)_connection.flush()); + + auto update_key_cnt = [&] (Input::Event const &event) + { + if (event.type() == Input::Event::PRESS) _key_cnt++; + if (event.type() == Input::Event::RELEASE) _key_cnt--; + }; + + for_each_event(update_key_cnt); + } + + bool idle() const { return _key_cnt == 0; } + + bool pending() const { return _num_ev > 0; } +}; + +#endif /* _INPUT_FILTER__CONNECTION_H_ */ diff --git a/repos/os/src/server/input_filter/de.chargen b/repos/os/src/server/input_filter/de.chargen new file mode 100644 index 000000000..37e507e7e --- /dev/null +++ b/repos/os/src/server/input_filter/de.chargen @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/os/src/server/input_filter/en_us.chargen b/repos/os/src/server/input_filter/en_us.chargen new file mode 100644 index 000000000..c5caf5d1b --- /dev/null +++ b/repos/os/src/server/input_filter/en_us.chargen @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/os/src/server/input_filter/include_accessor.h b/repos/os/src/server/input_filter/include_accessor.h new file mode 100644 index 000000000..dddaba5b3 --- /dev/null +++ b/repos/os/src/server/input_filter/include_accessor.h @@ -0,0 +1,67 @@ +/* + * \brief Interface for accessing a configuration snippets + * \author Norman Feske + * \date 2017-02-10 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__INCLUDE_ACCESSOR_H_ +#define _INPUT_FILTER__INCLUDE_ACCESSOR_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include + +namespace Input_filter { struct Include_accessor; } + + +class Input_filter::Include_accessor +{ + public: + + typedef String<64> Name; + typedef String<32> Type; + + struct Include_unavailable : Exception { }; + + protected: + + struct Functor { virtual void apply(Xml_node node) const = 0; }; + + /* + * \throw Include_unavailable + */ + virtual void _apply_include(Name const &name, Type const &type, Functor const &) = 0; + + public: + + /** + * Call functor 'fn' with the 'Xml_node' of the named include + * + * \throw Include_unavailable + */ + template + void apply_include(Name const &name, Type const &type, FN const &fn) + { + struct Functor : Include_accessor::Functor + { + FN const &fn; + + Functor(FN const &fn) : fn(fn) { } + + void apply(Xml_node node) const override { fn(node); } + } _functor(fn); + + _apply_include(name, type, _functor); + } +}; + +#endif /* _INPUT_FILTER__INCLUDE_ACCESSOR_H_ */ diff --git a/repos/os/src/server/input_filter/input_source.h b/repos/os/src/server/input_filter/input_source.h new file mode 100644 index 000000000..294179d24 --- /dev/null +++ b/repos/os/src/server/input_filter/input_source.h @@ -0,0 +1,47 @@ +/* + * \brief Input-event source that obtains events from input connection + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__INPUT_SOURCE_H_ +#define _INPUT_FILTER__INPUT_SOURCE_H_ + +/* local includes */ +#include +#include + +namespace Input_filter { class Input_source; } + + +class Input_filter::Input_source : public Source +{ + private: + + Input_connection &_connection; + Sink &_destination; + + public: + + static char const *name() { return "input"; } + + Input_source(Owner &owner, Input_connection &connection, Sink &destination) + : + Source(owner), _connection(connection), _destination(destination) + { } + + void generate() override + { + _connection.for_each_event([&] (Input::Event const &event) { + _destination.submit_event(event); }); + } +}; + +#endif /* _INPUT_FILTER__INPUT_SOURCE_H_ */ diff --git a/repos/os/src/server/input_filter/key_code_by_name.h b/repos/os/src/server/input_filter/key_code_by_name.h new file mode 100644 index 000000000..4dbd64f85 --- /dev/null +++ b/repos/os/src/server/input_filter/key_code_by_name.h @@ -0,0 +1,43 @@ +/* + * \brief Utility to convert key names into their corresponding key codes + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__KEY_CODE_BY_NAME_H_ +#define _INPUT_FILTER__KEY_CODE_BY_NAME_H_ + +/* Genode includes */ +#include +#include + +namespace Input_filter { + + struct Unknown_key : Genode::Exception { }; + + typedef Genode::String<20> Key_name; + + /* + * \throw Unknown_key + */ + Input::Keycode key_code_by_name(Key_name const &name) + { + for (unsigned i = 0; i < Input::KEY_MAX; i++) { + Input::Keycode const code = Input::Keycode(i); + if (name == Input::key_name(code)) + return code; + } + + error("unknown key: ", name); + throw Unknown_key(); + } +} + +#endif /* _INPUT_FILTER__KEY_CODE_BY_NAME_H_ */ diff --git a/repos/os/src/server/input_filter/main.cc b/repos/os/src/server/input_filter/main.cc new file mode 100644 index 000000000..54ff00e21 --- /dev/null +++ b/repos/os/src/server/input_filter/main.cc @@ -0,0 +1,436 @@ +/* + * \brief Input-event filter + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include + +/* local includes */ +#include +#include +#include +#include + +namespace Input_filter { struct Main; } + + +struct Input_filter::Main : Input_connection::Avail_handler, + Source::Factory +{ + Env &_env; + + Attached_rom_dataspace _config { _env, "config" }; + + Heap _heap { _env.ram(), _env.rm() }; + + Registry > _input_connections; + + typedef String Label; + + /* + * Mechanism to construct a 'Timer' on demand + * + * By lazily constructing the timer, the input-filter does not depend + * on a timer service unless its configuration defines time-related + * filtering operations like key repeat. + */ + struct Timer_accessor : Input_filter::Timer_accessor + { + struct Lazy + { + Timer::Connection connection; + Genode::Timer timer; + + Lazy(Env &env) : connection(env), timer(connection, env.ep()) { } + }; + + Env &_env; + + Constructible lazy; + + Timer_accessor(Env &env) : _env(env) { } + + /** + * Timer_accessor interface + */ + Genode::Timer &timer() override + { + if (!lazy.constructed()) + lazy.construct(_env); + + return lazy->timer; + } + } _timer_accessor { _env }; + + /** + * Pool of configuration include snippets, obtained as ROM modules + */ + struct Include_accessor : Input_filter::Include_accessor + { + struct Rom + { + typedef Include_accessor::Name Name; + + Registry::Element _reg_elem; + Name const _name; + Attached_rom_dataspace _dataspace; + Signal_context_capability _reconfig_sigh; + + void _handle_rom_update() + { + _dataspace.update(); + + /* trigger reconfiguration */ + Signal_transmitter(_reconfig_sigh).submit(); + } + + Signal_handler _rom_update_handler; + + Rom(Registry ®istry, Env &env, + Name const &name, Type const &type, + Signal_context_capability reconfig_sigh) + : + _reg_elem(registry, *this), _name(name), + _dataspace(env, name.string()), _reconfig_sigh(reconfig_sigh), + _rom_update_handler(env.ep(), *this, &Rom::_handle_rom_update) + { + /* validate top-level node type */ + xml(type); + + /* respond to ROM updates */ + _dataspace.sigh(_rom_update_handler); + } + + bool has_name(Name const &name) const { return _name == name; } + + /** + * Return ROM content as XML + * + * \throw Include_unavailable + */ + Xml_node xml(Include_accessor::Type const &type) const + { + Xml_node const node = _dataspace.xml(); + if (node.type() == type) + return node; + + error("unexpected <", node.type(), "> node " "in included " + "ROM \"", _name, "\", expected, <", type, "> node"); + throw Include_unavailable(); + } + }; + + Env &_env; + Allocator &_alloc; + Signal_context_capability _sigh; + Registry _registry; + + /** + * Return true if registry contains an include with the given name + */ + bool _exists(Rom::Name const &name) + { + bool exists = false; + _registry.for_each([&] (Rom const &rom) { + if (rom.has_name(name)) + exists = true; }); + + return exists; + } + + /** + * Constructor + * + * \param sigh signal handler that responds to new ROM versions + */ + Include_accessor(Env &env, Allocator &alloc, Signal_context_capability sigh) + : + _env(env), _alloc(alloc), _sigh(sigh) + { } + + ~Include_accessor() + { + _registry.for_each([&] (Rom &rom) { destroy(_alloc, &rom); }); + } + + void _apply_include(Name const &name, Type const &type, Functor const &fn) override + { + /* populate registry on demand */ + if (!_exists(name)) { + try { new (_alloc) Rom(_registry, _env, name, type, _sigh); } + catch (...) { + error("include \"", name, "\" unavailable"); + throw Include_unavailable(); + } + } + + /* call 'fn' with the XML content of the named include */ + Rom const *matching_rom = nullptr; + _registry.for_each([&] (Rom const &rom) { + if (rom.has_name(name)) + matching_rom = &rom; }); + + /* this condition should never occur */ + if (!matching_rom) + throw Include_unavailable(); + + fn.apply(matching_rom->xml(type)); + } + }; + + /** + * Maximum nesting depth of input sources, for limiting the stack usage + */ + unsigned _create_source_max_nesting_level = 16; + + /** + * Source::Factory interface + * + * \throw Source::Invalid_config + */ + Source &create_source(Source::Owner &owner, Xml_node node, Source::Sink &sink) override + { + /* + * Guard for the protection against too deep recursions while + * processing the configuration. + */ + struct Nesting_level_guard + { + unsigned &level; + + Nesting_level_guard(unsigned &level) : level(level) + { + if (level == 0) { + error("too many nested input sources"); + throw Source::Invalid_config(); + } + level--; + } + + ~Nesting_level_guard() { level++; } + + } nesting_level_guard { _create_source_max_nesting_level }; + + /* return input source with the matching name */ + if (node.type() == Input_source::name()) { + Label const label = node.attribute_value("name", Label()); + Input_connection *match = nullptr; + + _input_connections.for_each([&] (Input_connection &connection) { + if (connection.label() == label) + match = &connection; }); + + if (match) + return *new (_heap) Input_source(owner, *match, sink); + + error("input named '", label, "' does not exist"); + throw Source::Invalid_config(); + } + + /* create regular filter */ + if (node.type() == Remap_source::name()) + return *new (_heap) Remap_source(owner, node, sink, *this); + + if (node.type() == Merge_source::name()) + return *new (_heap) Merge_source(owner, node, sink, *this); + + if (node.type() == Chargen_source::name()) + return *new (_heap) Chargen_source(owner, node, sink, *this, _heap, + _timer_accessor, _include_accessor); + + error("unknown <", node.type(), "> input-source node type"); + throw Source::Invalid_config(); + } + + /** + * Source::Factory interface + */ + void destroy_source(Source &source) override { destroy(_heap, &source); } + + /* + * Flag used to defer configuration updates until all input sources are + * in their default state. + */ + bool _config_update_pending = false; + + /** + * Return true if all input sources are in their default state + */ + bool _input_connections_idle() const + { + bool idle = true; + + _input_connections.for_each([&] (Input_connection const &connection) { + if (!connection.idle()) + idle = false; }); + + return idle; + } + + struct Output + { + Source::Owner _owner; + Source &_top_level; + + /** + * Constructor + * + * \throw Source::Invalid_config + * \throw Genode::Out_of_memory + */ + Output(Xml_node output, Source::Sink &sink, Source::Factory &factory) + : + _owner(factory), + _top_level(factory.create_source(_owner, Source::input_sub_node(output), sink)) + { } + + void generate() { _top_level.generate(); } + }; + + Constructible _output; + + /* + * Input session provided to our client + */ + Input::Session_component _input_session { _env, _env.ram() }; + + /* process events */ + struct Final_sink : Source::Sink + { + Input::Session_component &_input_session; + + Final_sink(Input::Session_component &input_session) + : _input_session(input_session) { } + + void submit_event(Input::Event const &event) override { + _input_session.submit(event); } + + } _final_sink { _input_session }; + + /* + * Input_connection::Avail_handler + */ + void handle_input_avail() override + { + for (;;) { + + /* fetch events in input sources */ + _input_connections.for_each([&] (Input_connection &connection) { + connection.flush(); }); + + bool pending = false; + + _input_connections.for_each([&] (Input_connection &connection) { + pending |= connection.pending(); }); + + if (pending && _output.constructed()) + _output->generate(); + + if (_config_update_pending && _input_connections_idle()) + Signal_transmitter(_config_handler).submit(); + + /* stop if no events are pending */ + if (!pending) + break; + } + } + + Static_root _input_root { _env.ep().manage(_input_session) }; + + void _handle_config() + { + _config.update(); + + bool const force = _config.xml().attribute_value("force", false); + bool const idle = _input_connections_idle(); + + /* defer reconfiguration until all sources are idle */ + if (!idle && !force) { + _config_update_pending = true; + return; + } + + if (!idle) + warning("force reconfiguration while input state is not idle"); + + _apply_config(); + } + + void _apply_config() + { + _input_connections.for_each([&] (Registered &conn) { + destroy(_heap, &conn); }); + + _config.xml().for_each_sub_node("input", [&] (Xml_node input_node) { + + try { + Label const label = + input_node.attribute_value("label", Label()); + + try { + Input_connection &conn = *new (_heap) + Registered(_input_connections, _env, + label, *this, _heap); + + } catch (Genode::Parent::Service_denied) { + error("parent denied input source '", label, "'"); + } + } catch (Xml_node::Nonexistent_attribute) { + error("ignoring invalid input node '", input_node); + } + }); + + try { + if (_config.xml().has_sub_node("output")) + _output.construct(_config.xml().sub_node("output"), + _final_sink, *this); + + } catch (Source::Invalid_config) { + error("invalid configuration"); + } catch (Allocator::Out_of_memory) { + error("out of memory while constructing filter chain"); } + + _config_update_pending = false; + } + + Signal_handler
_config_handler + { _env.ep(), *this, &Main::_handle_config }; + + Include_accessor _include_accessor { _env, _heap, _config_handler }; + + /** + * Constructor + */ + Main(Genode::Env &env) : _env(env) + { + _input_session.event_queue().enabled(true); + _config.sigh(_config_handler); + + /* + * Apply initial configuration + */ + _apply_config(); + + /* + * Announce service + */ + _env.parent().announce(_env.ep().manage(_input_root)); + } +}; + + +void Component::construct(Genode::Env &env) { static Input_filter::Main inst(env); } diff --git a/repos/os/src/server/input_filter/merge_source.h b/repos/os/src/server/input_filter/merge_source.h new file mode 100644 index 000000000..dd2161f7d --- /dev/null +++ b/repos/os/src/server/input_filter/merge_source.h @@ -0,0 +1,49 @@ +/* + * \brief Input-event source that merges other sources + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__MERGE_SOURCE_H_ +#define _INPUT_FILTER__MERGE_SOURCE_H_ + +/* local includes */ +#include + +namespace Input_filter { class Merge_source; } + + +class Input_filter::Merge_source : public Source +{ + private: + + Owner _owner; + + public: + + static char const *name() { return "merge"; } + + Merge_source(Owner &owner, Xml_node config, Sink &destination, + Source::Factory &factory) + : + Source(owner), _owner(factory) + { + config.for_each_sub_node([&] (Xml_node node) { + if (input_node(node)) + factory.create_source(_owner, node, destination); }); + } + + void generate() override + { + _owner.for_each([&] (Source &source) { source.generate(); }); + } +}; + +#endif /* _INPUT_FILTER__REMAP_SOURCE_H_ */ diff --git a/repos/os/src/server/input_filter/remap_source.h b/repos/os/src/server/input_filter/remap_source.h new file mode 100644 index 000000000..c10dcb3da --- /dev/null +++ b/repos/os/src/server/input_filter/remap_source.h @@ -0,0 +1,132 @@ +/* + * \brief Input-event source that remaps keys from another source + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__REMAP_SOURCE_H_ +#define _INPUT_FILTER__REMAP_SOURCE_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include +#include + +namespace Input_filter { class Remap_source; } + + +class Input_filter::Remap_source : public Source, Source::Sink +{ + private: + + struct Key + { + Input::Keycode code = Input::KEY_UNKNOWN; + bool sticky = false; + + enum State { RELEASED, PRESSED } state = RELEASED; + + void toggle() + { + state = (state == PRESSED) ? RELEASED : PRESSED; + } + }; + + Key _keys[Input::KEY_MAX]; + + Owner _owner; + + Source &_source; + + Source::Sink &_destination; + + /** + * Sink interface + */ + void submit_event(Input::Event const &event) override + { + using Input::Event; + + bool const key_event = + event.type() == Event::PRESS || event.type() == Event::RELEASE; + + bool const code_valid = + event.keycode() >= 0 && event.keycode() < Input::KEY_MAX; + + /* forward events that are unrelated to the remapper */ + if (!key_event || !code_valid) { + _destination.submit_event(event); + return; + } + + Key &key = _keys[event.keycode()]; + + Key::State const old_state = key.state; + + /* update key state, depending on the stickyness of the key */ + if (key.sticky) { + if (event.type() == Event::PRESS) + key.toggle(); + } else { + key.state = (event.type() == Event::PRESS) ? Key::PRESSED + : Key::RELEASED; + } + + /* drop release events of sticky keys */ + if (key.state == old_state) + return; + + Event::Type const type = + key.state == Key::PRESSED ? Event::PRESS : Event::RELEASE; + + _destination.submit_event(Event(type, key.code, 0, 0, 0, 0)); + } + + public: + + static char const *name() { return "remap"; } + + Remap_source(Owner &owner, Xml_node config, Source::Sink &destination, + Source::Factory &factory) + : + Source(owner), + _owner(factory), + _source(factory.create_source(_owner, input_sub_node(config), *this)), + _destination(destination) + { + for (unsigned i = 0; i < Input::KEY_MAX; i++) + _keys[i].code = Input::Keycode(i); + + config.for_each_sub_node("key", [&] (Xml_node node) { + + Key_name const key_name = node.attribute_value("name", Key_name()); + + try { + Input::Keycode const code = key_code_by_name(key_name); + + if (node.has_attribute("to")) { + Key_name const to = node.attribute_value("to", Key_name()); + try { _keys[code].code = key_code_by_name(to); } + catch (Unknown_key) { warning("ignoring remap rule ", node); } + } + + _keys[code].sticky = node.attribute_value("sticky", false); + } + catch (Unknown_key) { + warning("invalid key name ", key_name); } + }); + } + + void generate() override { _source.generate(); } +}; + +#endif /* _INPUT_FILTER__REMAP_SOURCE_H_ */ diff --git a/repos/os/src/server/input_filter/source.h b/repos/os/src/server/input_filter/source.h new file mode 100644 index 000000000..51715ab23 --- /dev/null +++ b/repos/os/src/server/input_filter/source.h @@ -0,0 +1,96 @@ +/* + * \brief Input-event source interface + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__SOURCE_H_ +#define _INPUT_FILTER__SOURCE_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include + +namespace Input_filter { struct Source; } + + +class Input_filter::Source +{ + private: + + Registry::Element _owner_elem; + + public: + + struct Invalid_config : Exception { }; + + Source(Registry &owner) : _owner_elem(owner, *this) { } + + static bool input_node(Xml_node node) + { + return node.type() == "input" + || node.type() == "remap" + || node.type() == "chargen" + || node.type() == "merge"; + + return false; + } + + static Xml_node input_sub_node(Xml_node node) + { + Xml_node result(""); + + node.for_each_sub_node([&] (Xml_node sub_node) { + if (input_node(sub_node)) + result = sub_node; }); + + if (result.type() != "none") + return result; + + error("missing // sub node in ", node); + throw Invalid_config { }; + } + + virtual void generate() = 0; + + struct Owner; + + struct Sink + { + virtual void submit_event(Input::Event const &) = 0; + }; + + struct Factory + { + /* + * \throw Invalid_config + */ + virtual Source &create_source(Owner &, Xml_node, Sink &) = 0; + + virtual void destroy_source(Source &) = 0; + }; + + struct Owner : Registry + { + Factory &_factory; + + Owner(Factory &factory) : _factory(factory) { } + + ~Owner() + { + for_each([&] (Source &s) { _factory.destroy_source(s); }); + } + }; +}; + +#endif /* _INPUT_FILTER__SOURCE_H_ */ diff --git a/repos/os/src/server/input_filter/target.mk b/repos/os/src/server/input_filter/target.mk new file mode 100644 index 000000000..af45ee015 --- /dev/null +++ b/repos/os/src/server/input_filter/target.mk @@ -0,0 +1,4 @@ +TARGET = input_filter +SRC_CC = main.cc +LIBS = base timeout +INC_DIR += $(PRG_DIR) diff --git a/repos/os/src/server/input_filter/timer_accessor.h b/repos/os/src/server/input_filter/timer_accessor.h new file mode 100644 index 000000000..56d698ddf --- /dev/null +++ b/repos/os/src/server/input_filter/timer_accessor.h @@ -0,0 +1,24 @@ +/* + * \brief Interface for accessing a timer + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__TIMER_ACCESSOR_H_ +#define _INPUT_FILTER__TIMER_ACCESSOR_H_ + +/* Genode includes */ +#include + +namespace Input_filter { struct Timer_accessor; } + +struct Input_filter::Timer_accessor { virtual Genode::Timer &timer() = 0; }; + +#endif /* _INPUT_FILTER__TIMER_ACCESSOR_H_ */ diff --git a/repos/os/src/server/input_filter/types.h b/repos/os/src/server/input_filter/types.h new file mode 100644 index 000000000..9b7e40107 --- /dev/null +++ b/repos/os/src/server/input_filter/types.h @@ -0,0 +1,23 @@ +/* + * \brief Types used by the input filter + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _INPUT_FILTER__TYPES_H_ +#define _INPUT_FILTER__TYPES_H_ + +/* Genode includes */ +#include +#include + +namespace Input_filter { using namespace Genode; } + +#endif /* _INPUT_FILTER__TYPES_H_ */ diff --git a/repos/os/src/test/input_filter/main.cc b/repos/os/src/test/input_filter/main.cc new file mode 100644 index 000000000..ff0048355 --- /dev/null +++ b/repos/os/src/test/input_filter/main.cc @@ -0,0 +1,425 @@ +/* + * \brief Test for input filter + * \author Norman Feske + * \date 2017-02-01 + */ + +/* + * Copyright (C) 2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Test { + class Input_from_filter; + class Input_to_filter; + class Input_root; + class Main; + using namespace Genode; +} + + +namespace Genode { + + static inline void print(Output &output, Input::Event const &ev) + { + switch (ev.type()) { + case Input::Event::INVALID: print(output, "INVALID"); break; + case Input::Event::MOTION: print(output, "MOTION"); break; + case Input::Event::PRESS: print(output, "PRESS"); break; + case Input::Event::RELEASE: print(output, "RELEASE"); break; + case Input::Event::WHEEL: print(output, "WHEEL"); break; + case Input::Event::FOCUS: print(output, "FOCUS"); break; + case Input::Event::LEAVE: print(output, "LEAVE"); break; + case Input::Event::TOUCH: print(output, "TOUCH"); break; + case Input::Event::CHARACTER: print(output, "CHARACTER"); break; + }; + + if (ev.type() == Input::Event::PRESS || ev.type() == Input::Event::RELEASE) + print(output, " (", Input::key_name(ev.keycode()), ")"); + } +} + + +class Test::Input_from_filter +{ + public: + + struct Event_handler + { + virtual void handle_event_from_filter(Input::Event const &) = 0; + }; + + private: + + Env &_env; + + Event_handler &_event_handler; + + Input::Connection _connection; + + bool _input_expected = false; + + void _handle_input() + { + if (_input_expected) + _connection.for_each_event([&] (Input::Event const &event) { + _event_handler.handle_event_from_filter(event); }); + } + + Signal_handler _input_handler { + _env.ep(), *this, &Input_from_filter::_handle_input }; + + public: + + Input_from_filter(Env &env, Event_handler &event_handler) + : + _env(env), _event_handler(event_handler), _connection(env) + { + _connection.sigh(_input_handler); + } + + void input_expected(bool expected) + { + _input_expected = expected; + _handle_input(); + } +}; + + +class Test::Input_root : public Root_component +{ + private: + + Input::Session_component &_usb_input; + Input::Session_component &_ps2_input; + + public: + + Input_root(Entrypoint &ep, Allocator &md_alloc, + Input::Session_component &usb_input, + Input::Session_component &ps2_input) + : + Root_component(ep, md_alloc), + _usb_input(usb_input), _ps2_input(ps2_input) + { } + + Input::Session_component *_create_session(const char *args, + Affinity const &) + { + Session_label const label = label_from_args(args); + + if (label.last_element() == "usb") return &_usb_input; + if (label.last_element() == "ps2") return &_ps2_input; + + error("no matching policy for session label ", label); + throw Root::Invalid_args(); + } + + /* + * Prevent the default 'Root_component' implementation from attempting + * to free the session objects. + */ + void _destroy_session(Input::Session_component *) override { } +}; + + +class Test::Input_to_filter +{ + private: + + Env &_env; + + Sliced_heap _sliced_heap { _env.ram(), _env.rm() }; + + /* + * Provide the input service via an independent entrypoint to avoid a + * possible deadlock between the input_filter and the test when + * both try to invoke 'Input::Session::flush' from each other. + */ + enum { STACK_SIZE = 4*1024*sizeof(long) }; + + Entrypoint _ep { _env, STACK_SIZE, "input_server_ep" }; + + /* + * Input supplied to the input_filter + */ + Input::Session_component _usb { _env, _env.ram() }; + Input::Session_component _ps2 { _env, _env.ram() }; + + Input_root _root { _ep, _sliced_heap, _usb, _ps2}; + + typedef String<20> Key_name; + + Input::Keycode _code(Key_name const &key_name) + { + for (unsigned i = 0; i < Input::KEY_MAX - 1; i++) { + Input::Keycode const code = Input::Keycode(i); + if (key_name == Input::key_name(code)) + return code; + } + + error("unknown key name: ", key_name); + throw Exception(); + }; + + public: + + Input_to_filter(Env &env) : _env(env) + { + _env.parent().announce(_ep.manage(_root)); + + _usb.event_queue().enabled(true); + _ps2.event_queue().enabled(true); + } + + void submit_events(Xml_node step) + { + if (step.type() != "usb" && step.type() != "ps2") { + error("unexpected argument to Input_to_filter::submit"); + throw Exception(); + } + + Input::Session_component &dst = step.type() == "usb" ? _usb : _ps2; + + step.for_each_sub_node([&] (Xml_node node) { + + Input::Event::Type const type = + node.type() == "press" ? Input::Event::PRESS : + node.type() == "release" ? Input::Event::RELEASE : + Input::Event::INVALID; + + if (type == Input::Event::PRESS || type == Input::Event::RELEASE) { + Key_name const key_name = node.attribute_value("code", Key_name()); + dst.submit(Input::Event(type, _code(key_name), 0, 0, 0, 0)); + } + }); + } +}; + + +struct Test::Main : Input_from_filter::Event_handler +{ + Env &_env; + + Timer::Connection _timer { _env }; + + Input_from_filter _input_from_filter { _env, *this }; + + Input_to_filter _input_to_filter { _env }; + + Reporter _input_filter_config_reporter { _env, "config", "input_filter.config" }; + Reporter _chargen_include_reporter { _env, "chargen", "chargen_include" }; + Reporter _remap_include_reporter { _env, "remap", "remap_include" }; + + Attached_rom_dataspace _config { _env, "config" }; + + void _publish_report(Reporter &reporter, Xml_node node) + { + Reporter::Xml_generator xml(reporter, [&] () { + xml.append(node.content_base(), node.content_size()); }); + } + + void _gen_chargen_rec(Xml_generator &xml, unsigned depth) + { + if (depth > 0) { + xml.node("chargen", [&] () { _gen_chargen_rec(xml, depth - 1); }); + } else { + xml.node("input", [&] () { xml.attribute("name", "usb"); }); + } + } + + void _deep_filter_config(Reporter &reporter, Xml_node node) + { + unsigned const depth = node.attribute_value("depth", 0UL); + + Reporter::Xml_generator xml(_input_filter_config_reporter, [&] () { + xml.node("input", [&] () { xml.attribute("label", "usb"); }); + xml.node("output", [&] () { _gen_chargen_rec(xml, depth); }); + }); + } + + unsigned const _num_steps = _config.xml().num_sub_nodes(); + unsigned _curr_step = 0; + + unsigned long _went_to_sleep_time = 0; + + Xml_node _curr_step_xml() const { return _config.xml().sub_node(_curr_step); } + + void _advance_step() + { + _curr_step++; + + /* exit when reaching the end of the sequence */ + if (_curr_step == _num_steps) { + _env.parent().exit(0); + sleep_forever(); + } + }; + + void _execute_curr_step() + { + for (;;) { + Xml_node const step = _curr_step_xml(); + + log("step ", _curr_step, " (", step.type(), ")"); + + _input_from_filter.input_expected(step.type() == "expect_press" || + step.type() == "expect_release" || + step.type() == "expect_char"); + + if (step.type() == "filter_config") { + _publish_report(_input_filter_config_reporter, step); + _advance_step(); + continue; + } + + if (step.type() == "deep_filter_config") { + _deep_filter_config(_input_filter_config_reporter, step); + _advance_step(); + continue; + } + + if (step.type() == "chargen_include") { + _publish_report(_chargen_include_reporter, step); + _advance_step(); + continue; + } + + if (step.type() == "remap_include") { + _publish_report(_remap_include_reporter, step); + _advance_step(); + continue; + } + + if (step.type() == "usb" || step.type() == "ps2") { + _input_to_filter.submit_events(step); + _advance_step(); + continue; + } + + if (step.type() == "message") { + typedef String<80> Message; + Message const message = step.attribute_value("string", Message()); + log("\n--- ", message, " ---"); + _advance_step(); + continue; + } + + if (step.type() == "nop") { + _advance_step(); + continue; + } + + if (step.type() == "expect_press" || step.type() == "expect_release" + || step.type() == "expect_char") + return; + + if (step.type() == "sleep") { + if (_went_to_sleep_time == 0) { + unsigned long const timeout_ms = step.attribute_value("ms", 250UL); + _went_to_sleep_time = _timer.elapsed_ms(); + _timer.trigger_once(timeout_ms*1000); + } + return; + } + + error("unexpected step: ", step); + throw Exception(); + } + } + + /** + * Input_to_filter::Event_handler interface + */ + void handle_event_from_filter(Input::Event const &ev) override + { + typedef Genode::String<20> Value; + + Xml_node const step = _curr_step_xml(); + + switch (ev.type()) { + case Input::Event::PRESS: + if (step.type() == "expect_press" + && step.attribute_value("code", Value()) == Input::key_name(ev.keycode())) + break; + + case Input::Event::RELEASE: + if (step.type() == "expect_release" + && step.attribute_value("code", Value()) == Input::key_name(ev.keycode())) + break; + + case Input::Event::CHARACTER: + if (step.type() == "expect_char" + && step.attribute_value("char", Value()) == Value(Char(ev.utf8().b0))) + break; + + case Input::Event::INVALID: + case Input::Event::MOTION: + case Input::Event::WHEEL: + case Input::Event::FOCUS: + case Input::Event::LEAVE: + case Input::Event::TOUCH: + error("unexpected event: ", ev); + throw Exception(); + }; + + _advance_step(); + _execute_curr_step(); + } + + void _handle_timer() + { + if (_curr_step_xml().type() != "sleep") { + error("got spurious timeout signal"); + throw Exception(); + } + + unsigned long const duration = _curr_step_xml().attribute_value("ms", 0UL); + unsigned long const now = _timer.elapsed_ms(); + unsigned long const slept = now - _went_to_sleep_time; + + if (slept < duration) { + warning("spurious wakeup from sleep"); + _timer.trigger_once(1000*(duration - slept)); + return; + } + + /* skip */ + _advance_step(); + + _went_to_sleep_time = 0; + _execute_curr_step(); + } + + Signal_handler
_timer_handler { + _env.ep(), *this, &Main::_handle_timer }; + + Main(Env &env) : _env(env) + { + _timer.sigh(_timer_handler); + _input_filter_config_reporter.enabled(true); + _chargen_include_reporter.enabled(true); + _remap_include_reporter.enabled(true); + _execute_curr_step(); + } +}; + + +void Component::construct(Genode::Env &env) { static Test::Main main(env); } + diff --git a/repos/os/src/test/input_filter/target.mk b/repos/os/src/test/input_filter/target.mk new file mode 100644 index 000000000..f0ef5cd51 --- /dev/null +++ b/repos/os/src/test/input_filter/target.mk @@ -0,0 +1,3 @@ +TARGET = test-input_filter +SRC_CC = main.cc +LIBS += base diff --git a/tool/autopilot.list b/tool/autopilot.list index abef880e1..1f0c8daa7 100644 --- a/tool/autopilot.list +++ b/tool/autopilot.list @@ -85,3 +85,4 @@ reconstructible synced_interface timer_accuracy trace +input_filter