From 4a6793608c46f05d959336ea55dd106e0b0339bd Mon Sep 17 00:00:00 2001 From: Lee Clagett Date: Tue, 8 Apr 2025 00:09:44 -0400 Subject: [PATCH] Add wallet inactivity timeout, and made wallet2 backend compile-time optional --- src/CMakeLists.txt | 5 +- src/events.cpp | 36 ++++++++++++++ src/events.h | 7 ++- src/lwcli_config.h | 2 + src/main.cpp | 107 +++++++++++++++++++++++++++++++++++++++--- src/util.h | 48 +++++++++++++++++++ src/views/manager.cpp | 27 ++++++----- 7 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 src/events.cpp create mode 100644 src/util.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 14302b6..4a361bd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,7 +30,10 @@ include_directories(.) add_subdirectory(decorate) add_subdirectory(views) -add_executable(lwcli main.cpp) +add_executable(lwcli events.cpp main.cpp) target_include_directories(lwcli PRIVATE ".") target_link_libraries(lwcli PRIVATE lwsf-api component lwcli-views) +if (DEFINED ENABLE_WALLET2) + target_compile_definitions(lwcli PRIVATE LWCLI_WALLET2_ENABLED) +endif () diff --git a/src/events.cpp b/src/events.cpp new file mode 100644 index 0000000..9762ba4 --- /dev/null +++ b/src/events.cpp @@ -0,0 +1,36 @@ +// Copyright (c) 2025, Cifro Codes LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "events.h" + +namespace lwcli { namespace event +{ + /* Keep under 15 characters so that libstdc++ and libc++ can use small + string optmization. */ + const ftxui::Event lock_wallet = ftxui::Event::Special("lwcli.lockw"); +}} diff --git a/src/events.h b/src/events.h index 5863563..755d86c 100644 --- a/src/events.h +++ b/src/events.h @@ -1,4 +1,3 @@ - // Copyright (c) 2025, Cifro Codes LLC // // All rights reserved. @@ -27,6 +26,9 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#pragma once + +#include #include namespace lwcli { namespace event @@ -41,4 +43,7 @@ namespace lwcli { namespace event virtual ~close() noexcept override = default; virtual const char* what() const noexcept override final { return "close window"; } }; + + extern const ftxui::Event lock_wallet; + }} // lwcli // event diff --git a/src/lwcli_config.h b/src/lwcli_config.h index 5c17813..a3d6096 100644 --- a/src/lwcli_config.h +++ b/src/lwcli_config.h @@ -39,6 +39,8 @@ namespace lwcli { namespace config constexpr const std::string_view default_language{"English"}; + //! Timeout interval for inactivity with open wallet + constexpr const std::chrono::minutes wallet_timeout{2}; namespace server { constexpr const std::string_view default_url{"http://127.0.0.1:8080"}; diff --git a/src/main.cpp b/src/main.cpp index 7b6836c..969977b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,17 +26,24 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include +#include +#include #include #include #include #include +#include #include #include +#include #include #include +#include #include "events.h" #include "lwcli_config.h" +#include "util.h" #include "views/manager.h" namespace @@ -45,9 +52,10 @@ namespace struct program { std::string file; + std::chrono::seconds wallet_timeout = lwcli::config::wallet_timeout; rpc backend = rpc::lws; // network is in static memory - bool failed; + bool failed = false; }; typedef const char**(*argument_handler)(program&, const char*[]); @@ -125,13 +133,34 @@ namespace return ++argv; } + const char** handle_timeout(program& prog, const char* argv[]) + { + if (!argv || !argv[0]) + { + fprintf(stderr, "Missing argument for --timeout\n"); + return nullptr; + } + + const auto value = lwcli::from_string(argv[0]); + if (!value || std::numeric_limits::max() < *value) + { + fprintf(stderr, "Invalid values for --timeout\n"); + return nullptr; + } + + prog.wallet_timeout = std::chrono::seconds{*value}; + return ++argv; + } constexpr const argument process_args[] = { {nullptr, "help", "\t\t\tList help", 'h'}, +#ifdef LWCLI_WALLET2_ENABLED {handle_backend, "backend", "\tlws | monerod\t\tlws = default , selects rpc backend", 'b'}, +#endif {handle_file, "file", "\t[file path]\t\tDefaults to home directory. Auto-fills TUI value on launch", 'f'}, {handle_network, "network", "\tmain | stage | test\tSelects wallet network type. main is default.", 'n'}, + {handle_timeout, "timeout", "\tseconds\tClose wallet after inactivity. Default 120", 't'} }; template @@ -164,11 +193,72 @@ namespace ++argv; return current->handler(prog, argv); } + + struct screen_state + { + std::atomic last_event; + ftxui::ScreenInteractive screen; + std::mutex sync; + std::condition_variable notify; + bool shutdown; + + screen_state() + : last_event(std::chrono::steady_clock::now().time_since_epoch().count()), + screen(ftxui::ScreenInteractive::Fullscreen()), + sync(), + notify(), + shutdown(false) + {} + }; + + struct watch_inactivity + { + screen_state& state; + std::thread watcher; + const std::chrono::seconds wallet_timeout; + + watch_inactivity(screen_state& st, std::chrono::seconds wt) + : state(st), watcher(), wallet_timeout(wt) + { + watcher = std::thread([this] () { + std::unique_lock lock{state.sync}; + while (!state.shutdown) + { + const std::chrono::steady_clock::time_point last_event{ + std::chrono::steady_clock::time_point::duration{state.last_event.load()} + }; + + const auto now = std::chrono::steady_clock::now(); + auto time_diff = now - last_event; + if (wallet_timeout <= time_diff) + { + state.last_event = now.time_since_epoch().count(); + state.screen.PostEvent(lwcli::event::lock_wallet); + time_diff = std::chrono::seconds{0}; + } + + state.notify.wait_for(lock, (wallet_timeout - time_diff), [this] () { + return state.shutdown; + }); + } + }); + } + + ~watch_inactivity() noexcept // abort if double exception + { + std::unique_lock lock{state.sync}; + state.shutdown = true; + state.notify.notify_one(); + lock.unlock(); + if (watcher.joinable()) + watcher.join(); + } + }; } int main(int, const char* argv[]) { - auto screen = ftxui::ScreenInteractive::Fullscreen(); + screen_state state{}; try { if (!argv) @@ -190,31 +280,36 @@ int main(int, const char* argv[]) case rpc::lws: wm.reset(lwsf::WalletManagerFactory::getWalletManager()); break; +#ifdef LWCLI_WALLET2_ENABLED case rpc::monerod: wm.reset(Monero::WalletManagerFactory::getWalletManager()); +#endif break; } auto window = ftxui::CatchEvent(lwcli::view::manager(std::move(wm), std::move(prog.file)), [&] (ftxui::Event event) { + state.last_event = std::chrono::steady_clock::now().time_since_epoch().count(); if (event == ftxui::Event::CtrlC) { - screen.ExitLoopClosure()(); + state.screen.ExitLoopClosure()(); return true; } return false; }); - screen.Loop(window); + + const watch_inactivity watch{state, prog.wallet_timeout}; + state.screen.Loop(window); } catch (const lwcli::event::close&) {} catch (const std::exception& e) { - screen.Clear(); + state.screen.Clear(); fprintf(stderr, "Fatal Error: %s\n", e.what()); return EXIT_FAILURE; } - screen.Clear(); + state.screen.Clear(); return EXIT_SUCCESS; } diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..f9b33ef --- /dev/null +++ b/src/util.h @@ -0,0 +1,48 @@ +// Copyright (c) 2025, Cifro Codes LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include + + +namespace lwcli +{ + inline std::optional from_string(const std::string_view str) noexcept + { + std::uint64_t height = 0; + const auto end = str.data() + str.size(); + auto [ptr, ec] = std::from_chars(str.data(), end, height); + if (bool(ec) || ptr != end) + return std::nullopt; + return height; + } +} diff --git a/src/views/manager.cpp b/src/views/manager.cpp index 12fdfff..6a2c81c 100644 --- a/src/views/manager.cpp +++ b/src/views/manager.cpp @@ -40,6 +40,7 @@ #include "events.h" #include "lwcli_config.h" #include "translate.h" +#include "util.h" #include "views/history.h" #include "views/keys.h" @@ -142,16 +143,6 @@ namespace lwcli { namespace view {} }; - std::optional from_string(const std::string_view str) noexcept - { - std::uint64_t height = 0; - const auto end = str.data() + str.size(); - auto [ptr, ec] = std::from_chars(str.data(), end, height); - if (bool(ec) || ptr != end) - return std::nullopt; - return height; - } - bool init_wallet(Monero::Wallet& wal, std::string* error) { auto refresh = from_string(wal.getCacheAttribute(std::string{config::server::refresh_interval})); @@ -545,7 +536,9 @@ namespace lwcli { namespace view try { - if (state_.overlay) + if (event == event::lock_wallet) + state_.error = _("Wallet Locked Due to Inactivity"); + else if (state_.overlay) return state_.overlay->OnEvent(std::move(event)); else if (event == ftxui::Event::CtrlQ) throw event::close{}; @@ -619,8 +612,16 @@ namespace lwcli { namespace view try { if (wallet_) - return wallet_->OnEvent(std::move(event)); - else if (start_->OnEvent(std::move(event)) && data_) + { + if (event == event::lock_wallet) + { + wallet_.reset(); + start_->OnEvent(std::move(event)); + } + else + return wallet_->OnEvent(std::move(event)); + } + else if (event != event::lock_wallet && start_->OnEvent(std::move(event)) && data_) wallet_ = view::wallet(std::move(data_)); data_.reset(); }