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:
jethro 2017-05-29 18:39:49 -04:00
parent 421a6d0340
commit e1f3dfccc8
8 changed files with 376 additions and 1 deletions

View file

@ -38,6 +38,10 @@
#endif
#include <boost/thread.hpp>
#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<bool> 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
}
}

View 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;
};
}

View file

@ -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)

View 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();
}