diff --git a/CMakeLists.txt b/CMakeLists.txt index a9d6be98..a8060f90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -655,6 +655,19 @@ endif() list(APPEND EXTRA_LIBRARIES ${CMAKE_DL_LIBS}) +option(USE_READLINE "Build with GNU readline support." ON) +if(USE_READLINE) + find_package(Readline) + if(READLINE_FOUND AND GNU_READLINE_FOUND) + add_definitions(-DHAVE_READLINE) + include_directories(${Readline_INCLUDE_DIR}) + list(APPEND EXTRA_LIBRARIES ${Readline_LIBRARY}) + message(STATUS "Found readline library at: ${Readline_ROOT_DIR}") + else() + message(STATUS "Could not find GNU readline library so building without readline support") + endif() +endif() + if(ANDROID) set(ATOMIC libatomic.a) endif() diff --git a/cmake/FindReadline.cmake b/cmake/FindReadline.cmake new file mode 100644 index 00000000..9ccef7ad --- /dev/null +++ b/cmake/FindReadline.cmake @@ -0,0 +1,66 @@ +# - Try to find readline include dirs and libraries +# +# Usage of this module as follows: +# +# find_package(Readline) +# +# Variables used by this module, they can change the default behaviour and need +# to be set before calling find_package: +# +# Readline_ROOT_DIR Set this variable to the root installation of +# readline if the module has problems finding the +# proper installation path. +# +# Variables defined by this module: +# +# READLINE_FOUND System has readline, include and lib dirs found +# GNU_READLINE_FOUND Version of readline found is GNU readline, not libedit! +# Readline_INCLUDE_DIR The readline include directories. +# Readline_LIBRARY The readline library. + +find_path(Readline_ROOT_DIR + NAMES include/readline/readline.h + PATHS /opt/local/ /usr/local/ /usr/ + NO_DEFAULT_PATH +) + +find_path(Readline_INCLUDE_DIR + NAMES readline/readline.h + PATHS ${Readline_ROOT_DIR}/include + NO_DEFAULT_PATH +) + +find_library(Readline_LIBRARY + NAMES readline + PATHS ${Readline_ROOT_DIR}/lib + NO_DEFAULT_PATH +) + +if(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY) + set(READLINE_FOUND TRUE) +else(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY) + FIND_LIBRARY(Readline_LIBRARY NAMES readline PATHS Readline_ROOT_DIR) + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(Readline DEFAULT_MSG Readline_INCLUDE_DIR Readline_LIBRARY ) + MARK_AS_ADVANCED(Readline_INCLUDE_DIR Readline_LIBRARY) +endif(Readline_INCLUDE_DIR AND Readline_LIBRARY AND Ncurses_LIBRARY) + +mark_as_advanced( + Readline_ROOT_DIR + Readline_INCLUDE_DIR + Readline_LIBRARY +) + +set(CMAKE_REQUIRED_INCLUDES ${Readline_INCLUDE_DIR}) +set(CMAKE_REQUIRED_LIBRARIES ${Readline_LIBRARY}) +INCLUDE(CheckCXXSourceCompiles) +CHECK_CXX_SOURCE_COMPILES( +" +#include +#include +int +main() +{ + char * s = rl_copy_text(0, 0); +} +" GNU_READLINE_FOUND) diff --git a/contrib/epee/include/console_handler.h b/contrib/epee/include/console_handler.h index 54e3e966..bb20faa6 100644 --- a/contrib/epee/include/console_handler.h +++ b/contrib/epee/include/console_handler.h @@ -38,6 +38,10 @@ #endif #include +#ifdef HAVE_READLINE + #include "readline_buffer.h" +#endif + namespace epee { class async_stdin_reader @@ -49,6 +53,10 @@ namespace epee , m_read_status(state_init) { m_reader_thread = boost::thread(std::bind(&async_stdin_reader::reader_thread_func, this)); +#ifdef HAVE_READLINE + m_readline_buffer.start(); + m_readline_thread = boost::thread(std::bind(&async_stdin_reader::readline_thread_func, this)); +#endif } ~async_stdin_reader() @@ -56,6 +64,13 @@ namespace epee stop(); } +#ifdef HAVE_READLINE + rdln::readline_buffer& get_readline_buffer() + { + return m_readline_buffer; + } +#endif + // Not thread safe. Only one thread can call this method at once. bool get_line(std::string& line) { @@ -98,6 +113,10 @@ namespace epee m_request_cv.notify_one(); m_reader_thread.join(); +#ifdef HAVE_READLINE + m_readline_buffer.stop(); + m_readline_thread.join(); +#endif } } @@ -174,6 +193,16 @@ namespace epee return true; } +#ifdef HAVE_READLINE + void readline_thread_func() + { + while (m_run.load(std::memory_order_relaxed)) + { + m_readline_buffer.process(); + } + } +#endif + void reader_thread_func() { while (true) @@ -187,7 +216,11 @@ namespace epee { if (m_run.load(std::memory_order_relaxed)) { +#ifdef HAVE_READLINE + m_readline_buffer.get_line(line); +#else std::getline(std::cin, line); +#endif read_ok = !std::cin.eof() && !std::cin.fail(); } } @@ -229,6 +262,10 @@ namespace epee private: boost::thread m_reader_thread; std::atomic m_run; +#ifdef HAVE_READLINE + boost::thread m_readline_thread; + rdln::readline_buffer m_readline_buffer; +#endif std::string m_line; bool m_has_read_request; @@ -277,12 +314,16 @@ namespace epee { if (!m_prompt.empty()) { +#ifdef HAVE_READLINE + m_stdin_reader.get_readline_buffer().set_prompt(m_prompt); +#else epee::set_console_color(epee::console_color_yellow, true); std::cout << m_prompt; if (' ' != m_prompt.back()) std::cout << ' '; epee::reset_console_color(); std::cout.flush(); +#endif } } diff --git a/contrib/epee/include/readline_buffer.h b/contrib/epee/include/readline_buffer.h new file mode 100644 index 00000000..7d929bc4 --- /dev/null +++ b/contrib/epee/include/readline_buffer.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace rdln +{ + class readline_buffer : public std::stringbuf + { + public: + readline_buffer(); + void start(); + void stop(); + int process(); + bool is_running() + { + return m_cout_buf != NULL; + } + void get_line(std::string& line); + void set_prompt(const std::string& prompt); + + protected: + virtual int sync(); + + private: + std::streambuf* m_cout_buf; + }; + + class suspend_readline + { + public: + suspend_readline(); + ~suspend_readline(); + private: + readline_buffer* m_buffer; + bool m_restart; + }; +} + diff --git a/contrib/epee/src/CMakeLists.txt b/contrib/epee/src/CMakeLists.txt index 43785183..c61a6e68 100644 --- a/contrib/epee/src/CMakeLists.txt +++ b/contrib/epee/src/CMakeLists.txt @@ -26,7 +26,12 @@ # 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. -add_library(epee STATIC hex.cpp http_auth.cpp mlog.cpp string_tools.cpp) +if (USE_READLINE AND GNU_READLINE_FOUND) + add_library(epee STATIC hex.cpp http_auth.cpp mlog.cpp string_tools.cpp readline_buffer.cpp) +else() + add_library(epee STATIC hex.cpp http_auth.cpp mlog.cpp string_tools.cpp) +endif() + # Build and install libepee if we're building for GUI if (BUILD_GUI_DEPS) if(IOS) diff --git a/contrib/epee/src/readline_buffer.cpp b/contrib/epee/src/readline_buffer.cpp new file mode 100644 index 00000000..68b739db --- /dev/null +++ b/contrib/epee/src/readline_buffer.cpp @@ -0,0 +1,196 @@ +#include "readline_buffer.h" +#include +#include +#include +#include +#include +#include + +static int process_input(); +static void install_line_handler(); +static void remove_line_handler(); + +static std::string last_line; +static std::string last_prompt; +std::mutex line_mutex, sync_mutex; +std::condition_variable have_line; + +namespace +{ + rdln::readline_buffer* current = NULL; +} + +rdln::suspend_readline::suspend_readline() +{ + m_buffer = current; + if(!m_buffer) + return; + m_restart = m_buffer->is_running(); + if(m_restart) + m_buffer->stop(); +} + +rdln::suspend_readline::~suspend_readline() +{ + if(!m_buffer) + return; + if(m_restart) + m_buffer->start(); +} + +rdln::readline_buffer::readline_buffer() +: std::stringbuf() +{ + current = this; +} + +void rdln::readline_buffer::start() +{ + if(m_cout_buf != NULL) + return; + m_cout_buf = std::cout.rdbuf(); + std::cout.rdbuf(this); + install_line_handler(); +} + +void rdln::readline_buffer::stop() +{ + if(m_cout_buf == NULL) + return; + std::cout.rdbuf(m_cout_buf); + m_cout_buf = NULL; + remove_line_handler(); +} + +void rdln::readline_buffer::get_line(std::string& line) +{ + std::unique_lock lock(line_mutex); + have_line.wait(lock); + line = last_line; +} + +void rdln::readline_buffer::set_prompt(const std::string& prompt) +{ + last_prompt = prompt; + if(m_cout_buf == NULL) + return; + rl_set_prompt(last_prompt.c_str()); + rl_redisplay(); +} + +int rdln::readline_buffer::process() +{ + if(m_cout_buf == NULL) + return 0; + return process_input(); +} + +int rdln::readline_buffer::sync() +{ + std::lock_guard lock(sync_mutex); + char* saved_line; + int saved_point; + + saved_point = rl_point; + saved_line = rl_copy_text(0, rl_end); + + rl_set_prompt(""); + rl_replace_line("", 0); + rl_redisplay(); + + do + { + char x = this->sgetc(); + m_cout_buf->sputc(x); + } + while ( this->snextc() != EOF ); + + rl_set_prompt(last_prompt.c_str()); + rl_replace_line(saved_line, 0); + rl_point = saved_point; + rl_redisplay(); + free(saved_line); + + return 0; +} + +static fd_set fds; + +static int process_input() +{ + int count; + struct timeval t; + + t.tv_sec = 0; + t.tv_usec = 0; + + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + count = select(FD_SETSIZE, &fds, NULL, NULL, &t); + if (count < 1) + { + return count; + } + rl_callback_read_char(); + return count; +} + +static void handle_line(char* line) +{ + if (line != NULL) + { + std::lock_guard lock(sync_mutex); + rl_set_prompt(last_prompt.c_str()); + rl_already_prompted = 1; + return; + } + rl_set_prompt(""); + rl_replace_line("", 0); + rl_redisplay(); + rl_set_prompt(last_prompt.c_str()); +} + +static int handle_enter(int x, int y) +{ + std::lock_guard lock(sync_mutex); + char* line = NULL; + + line = rl_copy_text(0, rl_end); + rl_set_prompt(""); + rl_replace_line("", 1); + rl_redisplay(); + + if (strcmp(line, "") != 0) + { + last_line = line; + add_history(line); + have_line.notify_one(); + } + free(line); + + rl_set_prompt(last_prompt.c_str()); + rl_redisplay(); + + rl_done = 1; + return 0; +} + +static int startup_hook() +{ + rl_bind_key(RETURN, handle_enter); + rl_bind_key(NEWLINE, handle_enter); + return 0; +} + +static void install_line_handler() +{ + rl_startup_hook = startup_hook; + rl_callback_handler_install("", handle_line); +} + +static void remove_line_handler() +{ + rl_unbind_key(RETURN); + rl_callback_handler_remove(); +} + diff --git a/src/common/command_line.cpp b/src/common/command_line.cpp index f71b3e57..8c2796bb 100644 --- a/src/common/command_line.cpp +++ b/src/common/command_line.cpp @@ -37,6 +37,10 @@ #include "cryptonote_config.h" #include "string_tools.h" +#ifdef HAVE_READLINE + #include "readline_buffer.h" +#endif + namespace command_line { namespace @@ -49,6 +53,9 @@ namespace command_line std::string input_line(const std::string& prompt) { +#ifdef HAVE_READLINE + rdln::suspend_readline pause_readline; +#endif std::cout << prompt; std::string buf; diff --git a/src/common/password.cpp b/src/common/password.cpp index bdc9c69c..5c04023f 100644 --- a/src/common/password.cpp +++ b/src/common/password.cpp @@ -42,6 +42,10 @@ #include #endif +#ifdef HAVE_READLINE + #include "readline_buffer.h" +#endif + namespace { #if defined(_WIN32) @@ -238,6 +242,9 @@ namespace tools boost::optional password_container::prompt(const bool verify, const char *message) { +#ifdef HAVE_READLINE + rdln::suspend_readline pause_readline; +#endif password_container pass1{}; password_container pass2{}; if (is_cin_tty() ? read_from_tty(verify, message, pass1.m_password, pass2.m_password) : read_from_file(pass1.m_password))