From 1212a302b500c37420613086a1b85f0659f09d57 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Tue, 22 Sep 2026 17:54:25 +0200 Subject: lib/io: refactor I/O module --- CMakeLists.txt | 7 ++- ttwhy/io.cppm | 136 ---------------------------------------- ttwhy/io/mod.cppm | 5 ++ ttwhy/io/readers.cppm | 93 +++++++++++++++++++++++++++ ttwhy/io/scoped_attributes.cppm | 135 +++++++++++++++++++++++++++++++++++++++ ttwhy/io/signal_handling.cppm | 58 +++++++++++++++++ ttwhy/lib.cppm | 5 +- ttwhy/main.cpp | 4 +- ttwhy/routers/echo.cppm | 1 - ttwhy/scoped_attributes.cppm | 135 --------------------------------------- 10 files changed, 301 insertions(+), 278 deletions(-) delete mode 100644 ttwhy/io.cppm create mode 100644 ttwhy/io/mod.cppm create mode 100644 ttwhy/io/readers.cppm create mode 100644 ttwhy/io/scoped_attributes.cppm create mode 100644 ttwhy/io/signal_handling.cppm delete mode 100644 ttwhy/scoped_attributes.cppm diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a76144..138a84a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,9 +63,12 @@ target_sources("ttwhy-core" PUBLIC FILE_SET CXX_MODULES FILES - "ttwhy/io.cppm" "ttwhy/lib.cppm" - "ttwhy/scoped_attributes.cppm" + + "ttwhy/io/mod.cppm" + "ttwhy/io/readers.cppm" + "ttwhy/io/scoped_attributes.cppm" + "ttwhy/io/signal_handling.cppm" "ttwhy/routers/mod.cppm" "ttwhy/routers/concepts.cppm" diff --git a/ttwhy/io.cppm b/ttwhy/io.cppm deleted file mode 100644 index fe973ed..0000000 --- a/ttwhy/io.cppm +++ /dev/null @@ -1,136 +0,0 @@ -module; - -#include -#include - -#include // IWYU pragma: keep -#include -#include -#include - -export module ttwhy:io; - -import ttwhy.scanners; - -namespace ttwhy::io -{ - - export template - auto handle_signals(Stream & stream, TerminalAttributes & attributes) -> asio::awaitable - { - auto executor = co_await asio::this_coro::executor; - auto signals = asio::signal_set{executor, SIGINT, SIGTERM}; - signals.add(SIGTSTP); - signals.add(SIGCONT); - - while (true) - { - auto [error, signal] = co_await signals.async_wait(asio::as_tuple(asio::use_awaitable)); - if (error) - { - co_return error; - } - - switch (signal) - { - case SIGINT: - case SIGTERM: - { - auto message = std::format("Received signal {}, exiting ...\n", signal); - co_await asio::async_write(stream, asio::buffer(message), asio::use_awaitable); - co_return asio::error_code{}; - } - case SIGTSTP: - { - attributes.canonical_mode(true).echo(true); - ::raise(SIGSTOP); - break; - } - case SIGCONT: - { - attributes.canonical_mode(false).echo(false); - break; - } - } - - if (!error) - { - } - } - } - - export template - auto read_events(InputStream & stream, AppRouter & router) -> asio::awaitable - { - using namespace asio::experimental::awaitable_operators; - using namespace std::chrono_literals; - - auto executor = co_await asio::this_coro::executor; - auto timer = asio::steady_timer{executor}; - - auto queue = std::vector{}; - queue.reserve(16); - - auto sink = [&queue](auto const & event) { - queue.push_back(event); - }; - - using terminal_policy = ttwhy::scanners::associated_terminal_policy_t; - auto scanner = scanners::terminal_scanner{sink}; - - auto raw_buffer = std::array{}; - - while (true) - { - auto error = asio::error_code{}; - auto bytes_read = 0uz; - - if (scanner.is_pending()) - { - timer.expires_after(50ms); - - auto result = co_await (stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable)) || - timer.async_wait(asio::as_tuple(asio::use_awaitable))); - - if (result.index() == 0) - { - std::tie(error, bytes_read) = std::get<0>(result); - } - else - { - scanner.timeout(); - for (auto const & event : queue) - { - co_await router.process(event); - } - queue.clear(); - continue; - } - } - else - { - std::tie(error, bytes_read) = - co_await stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable)); - } - - if (error) - { - if (error == asio::error::interrupted) - { - continue; - } - co_return; - } - - auto const byte_span = std::span{raw_buffer.data(), bytes_read}; - scanner.process(byte_span); - - for (auto const & event : queue) - { - co_await router.process(event); - } - queue.clear(); - } - } - -} // namespace ttwhy::io diff --git a/ttwhy/io/mod.cppm b/ttwhy/io/mod.cppm new file mode 100644 index 0000000..8deff38 --- /dev/null +++ b/ttwhy/io/mod.cppm @@ -0,0 +1,5 @@ +export module ttwhy.io; + +export import :readers; +export import :scoped_attributes; +export import :signal_handling; diff --git a/ttwhy/io/readers.cppm b/ttwhy/io/readers.cppm new file mode 100644 index 0000000..d15cc7a --- /dev/null +++ b/ttwhy/io/readers.cppm @@ -0,0 +1,93 @@ +module; + +#include +#include + +#include +#include +#include +#include + +export module ttwhy.io:readers; + +import ttwhy.routers; +import ttwhy.scanners; + +namespace ttwhy::io +{ + + export template + auto read_events(InputStream & stream, AppRouter & router) -> asio::awaitable + { + using namespace asio::experimental::awaitable_operators; + using namespace std::chrono_literals; + + auto executor = co_await asio::this_coro::executor; + auto timer = asio::steady_timer{executor}; + + auto queue = std::vector{}; + queue.reserve(16); + + auto sink = [&queue](auto const & event) { + queue.push_back(event); + }; + + using terminal_policy = ttwhy::scanners::associated_terminal_policy_t; + auto scanner = scanners::terminal_scanner{sink}; + + auto raw_buffer = std::array{}; + + while (true) + { + auto error = asio::error_code{}; + auto bytes_read = 0uz; + + if (scanner.is_pending()) + { + timer.expires_after(50ms); + + auto result = co_await (stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable)) || + timer.async_wait(asio::as_tuple(asio::use_awaitable))); + + if (result.index() == 0) + { + std::tie(error, bytes_read) = std::get<0>(result); + } + else + { + scanner.timeout(); + for (auto const & event : queue) + { + co_await router.process(event); + } + queue.clear(); + continue; + } + } + else + { + std::tie(error, bytes_read) = + co_await stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable)); + } + + if (error) + { + if (error == asio::error::interrupted) + { + continue; + } + co_return; + } + + auto const byte_span = std::span{raw_buffer.data(), bytes_read}; + scanner.process(byte_span); + + for (auto const & event : queue) + { + co_await router.process(event); + } + queue.clear(); + } + } + +} // namespace ttwhy::io diff --git a/ttwhy/io/scoped_attributes.cppm b/ttwhy/io/scoped_attributes.cppm new file mode 100644 index 0000000..5e42e39 --- /dev/null +++ b/ttwhy/io/scoped_attributes.cppm @@ -0,0 +1,135 @@ +module; + +#include +#include + +#include +#include +#include + +export module ttwhy.io:scoped_attributes; + +namespace ttwhy::io +{ + + auto static read_attributes(int file_descriptor) -> termios + { + auto active_attributes = termios{}; + if (::tcgetattr(file_descriptor, &active_attributes)) + { + throw std::system_error(errno, std::system_category(), "failed to read termios attributes"); + } + return active_attributes; + } + + auto static get_line_discipline_flag(int file_descriptor, int flag) -> bool + { + auto active_attributes = read_attributes(file_descriptor); + return active_attributes.c_lflag & flag; + } + + auto static set_line_discipline_flag(int file_descriptor, int flag, bool enabled) -> void + { + auto active_attributes = read_attributes(file_descriptor); + + if ((active_attributes.c_lflag & flag) == enabled) + { + return; + } + + if (enabled) + { + active_attributes.c_lflag = active_attributes.c_lflag | flag; + } + else + { + active_attributes.c_lflag = active_attributes.c_lflag & ~flag; + } + + if (::tcsetattr(file_descriptor, TCSANOW, &active_attributes)) + { + throw std::system_error(errno, std::system_category(), "failed to write termios attributes"); + } + } + + //! Store the current TC attributes and restore them on destruction. + export struct scoped_attributes + { + explicit scoped_attributes(int file_descriptor) + : m_file_descriptor{-1} + , m_attributes{} + { + if (isatty(file_descriptor)) + { + m_file_descriptor = dup(file_descriptor); + m_attributes = read_attributes(m_file_descriptor); + } + } + + scoped_attributes(scoped_attributes const &) = delete; + + scoped_attributes(scoped_attributes && other) + : m_file_descriptor{std::exchange(other.m_file_descriptor, -1)} + , m_attributes{std::move(other.m_attributes)} + {} + + ~scoped_attributes() + { + if (m_file_descriptor > 0) + { + ::tcsetattr(m_file_descriptor, TCSANOW, &m_attributes); + ::close(m_file_descriptor); + } + } + + auto operator=(scoped_attributes const &) -> scoped_attributes & = delete; + + auto operator=(scoped_attributes && other) -> scoped_attributes & + { + std::ranges::swap(m_file_descriptor, other.m_file_descriptor); + std::ranges::swap(m_attributes, other.m_attributes); + return *this; + } + + //! Check if canonical mode is currently active + [[nodiscard]] auto canonical_mode() const noexcept -> bool + { + return get_line_discipline_flag(m_file_descriptor, ICANON); + } + + //! Set whether canonical mode is active or not. + auto canonical_mode(bool enabled) & -> scoped_attributes & + { + set_line_discipline_flag(m_file_descriptor, ICANON, enabled); + return *this; + } + + auto canonical_mode(bool enabled) && -> scoped_attributes + { + set_line_discipline_flag(m_file_descriptor, ICANON, enabled); + return std::move(*this); + } + + [[nodiscard]] auto echo() const noexcept -> bool + { + return get_line_discipline_flag(m_file_descriptor, ECHO); + } + + auto echo(bool enabled) & -> scoped_attributes & + { + set_line_discipline_flag(m_file_descriptor, ECHO, enabled); + return *this; + } + + auto echo(bool enabled) && -> scoped_attributes + { + set_line_discipline_flag(m_file_descriptor, ECHO, enabled); + return std::move(*this); + } + + private: + int m_file_descriptor; + termios m_attributes; + }; + +} // namespace ttwhy::io diff --git a/ttwhy/io/signal_handling.cppm b/ttwhy/io/signal_handling.cppm new file mode 100644 index 0000000..1f3fecc --- /dev/null +++ b/ttwhy/io/signal_handling.cppm @@ -0,0 +1,58 @@ +module; + +#include + +#include +#include +#include + +export module ttwhy.io:signal_handling; + +namespace ttwhy::io +{ + + export template + auto handle_signals(Stream & stream, TerminalAttributes & attributes) -> asio::awaitable + { + auto executor = co_await asio::this_coro::executor; + auto signals = asio::signal_set{executor, SIGINT, SIGTERM}; + signals.add(SIGTSTP); + signals.add(SIGCONT); + + while (true) + { + auto [error, signal] = co_await signals.async_wait(asio::as_tuple(asio::use_awaitable)); + if (error) + { + co_return error; + } + + switch (signal) + { + case SIGINT: + case SIGTERM: + { + auto message = std::format("Received signal {}, exiting ...\n", signal); + co_await asio::async_write(stream, asio::buffer(message), asio::use_awaitable); + co_return asio::error_code{}; + } + case SIGTSTP: + { + attributes.canonical_mode(true).echo(true); + ::raise(SIGSTOP); + break; + } + case SIGCONT: + { + attributes.canonical_mode(false).echo(false); + break; + } + } + + if (!error) + { + } + } + } + +} // namespace ttwhy::io diff --git a/ttwhy/lib.cppm b/ttwhy/lib.cppm index 14e0d5c..474d6b4 100644 --- a/ttwhy/lib.cppm +++ b/ttwhy/lib.cppm @@ -1,4 +1,5 @@ export module ttwhy; -export import :io; -export import :scoped_attributes; +export import ttwhy.io; +export import ttwhy.routers; +export import ttwhy.scanners; diff --git a/ttwhy/main.cpp b/ttwhy/main.cpp index 1cd2ac8..ab635a7 100644 --- a/ttwhy/main.cpp +++ b/ttwhy/main.cpp @@ -3,14 +3,14 @@ #include -import ttwhy; +import ttwhy.io; import ttwhy.routers; auto app(int in, int out, int error) -> asio::awaitable { using namespace asio::experimental::awaitable_operators; - auto terminal_attributes = ttwhy::scoped_attributes{fileno(stdin)} // + auto terminal_attributes = ttwhy::io::scoped_attributes{fileno(stdin)} // .canonical_mode(false) .echo(false); diff --git a/ttwhy/routers/echo.cppm b/ttwhy/routers/echo.cppm index 6fe60cb..ff309c8 100644 --- a/ttwhy/routers/echo.cppm +++ b/ttwhy/routers/echo.cppm @@ -10,7 +10,6 @@ module; export module ttwhy.routers:echo; -import ttwhy; import ttwhy.scanners; namespace ttwhy::routers diff --git a/ttwhy/scoped_attributes.cppm b/ttwhy/scoped_attributes.cppm deleted file mode 100644 index 080bca7..0000000 --- a/ttwhy/scoped_attributes.cppm +++ /dev/null @@ -1,135 +0,0 @@ -module; - -#include -#include - -#include -#include -#include - -export module ttwhy:scoped_attributes; - -namespace ttwhy -{ - - auto static read_attributes(int file_descriptor) -> termios - { - auto active_attributes = termios{}; - if (::tcgetattr(file_descriptor, &active_attributes)) - { - throw std::system_error(errno, std::system_category(), "failed to read termios attributes"); - } - return active_attributes; - } - - auto static get_line_discipline_flag(int file_descriptor, int flag) -> bool - { - auto active_attributes = read_attributes(file_descriptor); - return active_attributes.c_lflag & flag; - } - - auto static set_line_discipline_flag(int file_descriptor, int flag, bool enabled) -> void - { - auto active_attributes = read_attributes(file_descriptor); - - if ((active_attributes.c_lflag & flag) == enabled) - { - return; - } - - if (enabled) - { - active_attributes.c_lflag = active_attributes.c_lflag | flag; - } - else - { - active_attributes.c_lflag = active_attributes.c_lflag & ~flag; - } - - if (::tcsetattr(file_descriptor, TCSANOW, &active_attributes)) - { - throw std::system_error(errno, std::system_category(), "failed to write termios attributes"); - } - } - - //! Store the current TC attributes and restore them on destruction. - export struct scoped_attributes - { - explicit scoped_attributes(int file_descriptor) - : m_file_descriptor{-1} - , m_attributes{} - { - if (isatty(file_descriptor)) - { - m_file_descriptor = dup(file_descriptor); - m_attributes = read_attributes(m_file_descriptor); - } - } - - scoped_attributes(scoped_attributes const &) = delete; - - scoped_attributes(scoped_attributes && other) - : m_file_descriptor{std::exchange(other.m_file_descriptor, -1)} - , m_attributes{std::move(other.m_attributes)} - {} - - ~scoped_attributes() - { - if (m_file_descriptor > 0) - { - ::tcsetattr(m_file_descriptor, TCSANOW, &m_attributes); - ::close(m_file_descriptor); - } - } - - auto operator=(scoped_attributes const &) -> scoped_attributes & = delete; - - auto operator=(scoped_attributes && other) -> scoped_attributes & - { - std::ranges::swap(m_file_descriptor, other.m_file_descriptor); - std::ranges::swap(m_attributes, other.m_attributes); - return *this; - } - - //! Check if canonical mode is currently active - [[nodiscard]] auto canonical_mode() const noexcept -> bool - { - return get_line_discipline_flag(m_file_descriptor, ICANON); - } - - //! Set whether canonical mode is active or not. - auto canonical_mode(bool enabled) & -> scoped_attributes & - { - set_line_discipline_flag(m_file_descriptor, ICANON, enabled); - return *this; - } - - auto canonical_mode(bool enabled) && -> scoped_attributes - { - set_line_discipline_flag(m_file_descriptor, ICANON, enabled); - return std::move(*this); - } - - [[nodiscard]] auto echo() const noexcept -> bool - { - return get_line_discipline_flag(m_file_descriptor, ECHO); - } - - auto echo(bool enabled) & -> scoped_attributes & - { - set_line_discipline_flag(m_file_descriptor, ECHO, enabled); - return *this; - } - - auto echo(bool enabled) && -> scoped_attributes - { - set_line_discipline_flag(m_file_descriptor, ECHO, enabled); - return std::move(*this); - } - - private: - int m_file_descriptor; - termios m_attributes; - }; - -} // namespace ttwhy -- cgit v1.2.3