Add readline support to cli
This PR adds readline support to the daemon and monero-wallet-cli. Only GNU readline is supported (e.g. not libedit) and there are cmake checks to ensure this. There is a cmake variable, Readline_ROOT_DIR that can specify a directory to find readline, otherwise some default paths are searched. There is also a cmake option, USE_READLINE, that defaults to ON. If set to ON, if readline is not found, the build continues but without readline support. One negative side effect of using readline is that the color prompt in the wallet-cli now has no color and just uses terminal default. I know how to fix this but it's quite a big change so will tackle another time.
This commit is contained in:
parent
421a6d0340
commit
e1f3dfccc8
8 changed files with 376 additions and 1 deletions
|
@ -655,6 +655,19 @@ endif()
|
||||||
|
|
||||||
list(APPEND EXTRA_LIBRARIES ${CMAKE_DL_LIBS})
|
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)
|
if(ANDROID)
|
||||||
set(ATOMIC libatomic.a)
|
set(ATOMIC libatomic.a)
|
||||||
endif()
|
endif()
|
||||||
|
|
66
cmake/FindReadline.cmake
Normal file
66
cmake/FindReadline.cmake
Normal file
|
@ -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 <stdio.h>
|
||||||
|
#include <readline/readline.h>
|
||||||
|
int
|
||||||
|
main()
|
||||||
|
{
|
||||||
|
char * s = rl_copy_text(0, 0);
|
||||||
|
}
|
||||||
|
" GNU_READLINE_FOUND)
|
|
@ -38,6 +38,10 @@
|
||||||
#endif
|
#endif
|
||||||
#include <boost/thread.hpp>
|
#include <boost/thread.hpp>
|
||||||
|
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
#include "readline_buffer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace epee
|
namespace epee
|
||||||
{
|
{
|
||||||
class async_stdin_reader
|
class async_stdin_reader
|
||||||
|
@ -49,6 +53,10 @@ namespace epee
|
||||||
, m_read_status(state_init)
|
, m_read_status(state_init)
|
||||||
{
|
{
|
||||||
m_reader_thread = boost::thread(std::bind(&async_stdin_reader::reader_thread_func, this));
|
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()
|
~async_stdin_reader()
|
||||||
|
@ -56,6 +64,13 @@ namespace epee
|
||||||
stop();
|
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.
|
// Not thread safe. Only one thread can call this method at once.
|
||||||
bool get_line(std::string& line)
|
bool get_line(std::string& line)
|
||||||
{
|
{
|
||||||
|
@ -98,6 +113,10 @@ namespace epee
|
||||||
|
|
||||||
m_request_cv.notify_one();
|
m_request_cv.notify_one();
|
||||||
m_reader_thread.join();
|
m_reader_thread.join();
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
m_readline_buffer.stop();
|
||||||
|
m_readline_thread.join();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +193,16 @@ namespace epee
|
||||||
return true;
|
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()
|
void reader_thread_func()
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
|
@ -187,7 +216,11 @@ namespace epee
|
||||||
{
|
{
|
||||||
if (m_run.load(std::memory_order_relaxed))
|
if (m_run.load(std::memory_order_relaxed))
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
m_readline_buffer.get_line(line);
|
||||||
|
#else
|
||||||
std::getline(std::cin, line);
|
std::getline(std::cin, line);
|
||||||
|
#endif
|
||||||
read_ok = !std::cin.eof() && !std::cin.fail();
|
read_ok = !std::cin.eof() && !std::cin.fail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,6 +262,10 @@ namespace epee
|
||||||
private:
|
private:
|
||||||
boost::thread m_reader_thread;
|
boost::thread m_reader_thread;
|
||||||
std::atomic<bool> m_run;
|
std::atomic<bool> m_run;
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
boost::thread m_readline_thread;
|
||||||
|
rdln::readline_buffer m_readline_buffer;
|
||||||
|
#endif
|
||||||
|
|
||||||
std::string m_line;
|
std::string m_line;
|
||||||
bool m_has_read_request;
|
bool m_has_read_request;
|
||||||
|
@ -277,12 +314,16 @@ namespace epee
|
||||||
{
|
{
|
||||||
if (!m_prompt.empty())
|
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);
|
epee::set_console_color(epee::console_color_yellow, true);
|
||||||
std::cout << m_prompt;
|
std::cout << m_prompt;
|
||||||
if (' ' != m_prompt.back())
|
if (' ' != m_prompt.back())
|
||||||
std::cout << ' ';
|
std::cout << ' ';
|
||||||
epee::reset_console_color();
|
epee::reset_console_color();
|
||||||
std::cout.flush();
|
std::cout.flush();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
contrib/epee/include/readline_buffer.h
Normal file
40
contrib/epee/include/readline_buffer.h
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <streambuf>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,12 @@
|
||||||
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
# 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.
|
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
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)
|
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
|
# Build and install libepee if we're building for GUI
|
||||||
if (BUILD_GUI_DEPS)
|
if (BUILD_GUI_DEPS)
|
||||||
if(IOS)
|
if(IOS)
|
||||||
|
|
196
contrib/epee/src/readline_buffer.cpp
Normal file
196
contrib/epee/src/readline_buffer.cpp
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
#include "readline_buffer.h"
|
||||||
|
#include <readline/readline.h>
|
||||||
|
#include <readline/history.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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();
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@
|
||||||
#include "cryptonote_config.h"
|
#include "cryptonote_config.h"
|
||||||
#include "string_tools.h"
|
#include "string_tools.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
#include "readline_buffer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace command_line
|
namespace command_line
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
@ -49,6 +53,9 @@ namespace command_line
|
||||||
|
|
||||||
std::string input_line(const std::string& prompt)
|
std::string input_line(const std::string& prompt)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
rdln::suspend_readline pause_readline;
|
||||||
|
#endif
|
||||||
std::cout << prompt;
|
std::cout << prompt;
|
||||||
|
|
||||||
std::string buf;
|
std::string buf;
|
||||||
|
|
|
@ -42,6 +42,10 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
#include "readline_buffer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
@ -238,6 +242,9 @@ namespace tools
|
||||||
|
|
||||||
boost::optional<password_container> password_container::prompt(const bool verify, const char *message)
|
boost::optional<password_container> password_container::prompt(const bool verify, const char *message)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_READLINE
|
||||||
|
rdln::suspend_readline pause_readline;
|
||||||
|
#endif
|
||||||
password_container pass1{};
|
password_container pass1{};
|
||||||
password_container pass2{};
|
password_container pass2{};
|
||||||
if (is_cin_tty() ? read_from_tty(verify, message, pass1.m_password, pass2.m_password) : read_from_file(pass1.m_password))
|
if (is_cin_tty() ? read_from_tty(verify, message, pass1.m_password, pass2.m_password) : read_from_file(pass1.m_password))
|
||||||
|
|
Loading…
Reference in a new issue