ClickHouse/base/base/ReplxxLineReader.cpp

377 lines
12 KiB
C++
Raw Normal View History

2021-10-02 07:13:14 +00:00
#include <base/ReplxxLineReader.h>
#include <base/errnoToString.h>
#include <chrono>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <functional>
2020-06-04 21:31:51 +00:00
#include <sys/file.h>
2020-12-01 10:34:34 +00:00
#include <sys/types.h>
#include <sys/wait.h>
#include <csignal>
#include <dlfcn.h>
#include <fcntl.h>
#include <fstream>
#include <fmt/format.h>
namespace
{
/// Trim ending whitespace inplace
void trim(String & s)
{
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end());
}
2021-11-16 21:44:52 +00:00
std::string getEditor()
{
const char * editor = std::getenv("EDITOR");
if (!editor || !*editor)
editor = "vim";
return editor;
}
2021-07-02 01:25:07 +00:00
/// Copied from replxx::src/util.cxx::now_ms_str() under the terms of 3-clause BSD license of Replxx.
/// Copyright (c) 2017-2018, Marcin Konarski (amok at codestation.org)
/// Copyright (c) 2010, Salvatore Sanfilippo (antirez at gmail dot com)
/// Copyright (c) 2010, Pieter Noordhuis (pcnoordhuis at gmail dot com)
2021-07-02 05:51:53 +00:00
std::string replxx_now_ms_str()
{
std::chrono::milliseconds ms(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()));
time_t t = ms.count() / 1000;
tm broken;
if (!localtime_r(&t, &broken))
{
return std::string();
}
static int const BUFF_SIZE(32);
char str[BUFF_SIZE];
strftime(str, BUFF_SIZE, "%Y-%m-%d %H:%M:%S.", &broken);
snprintf(str + sizeof("YYYY-mm-dd HH:MM:SS"), 5, "%03d", static_cast<int>(ms.count() % 1000));
return str;
}
/// Convert from readline to replxx format.
///
/// replxx requires each history line to prepended with time line:
///
/// ### YYYY-MM-DD HH:MM:SS.SSS
/// select 1
///
/// And w/o those service lines it will load all lines from history file as
/// one history line for suggestion. And if there are lots of lines in file it
/// will take lots of time (getline() + tons of reallocations).
///
/// NOTE: this code uses std::ifstream/std::ofstream like original replxx code.
void convertHistoryFile(const std::string & path, replxx::Replxx & rx)
{
std::ifstream in(path);
if (!in)
{
rx.print("Cannot open %s reading (for conversion): %s\n",
path.c_str(), errnoToString(errno).c_str());
return;
}
std::string line;
if (getline(in, line).bad())
{
rx.print("Cannot read from %s (for conversion): %s\n",
path.c_str(), errnoToString(errno).c_str());
return;
}
/// This is the marker of the date, no need to convert.
static char const REPLXX_TIMESTAMP_PATTERN[] = "### dddd-dd-dd dd:dd:dd.ddd";
if (line.empty() || (line.starts_with("### ") && line.size() == strlen(REPLXX_TIMESTAMP_PATTERN)))
{
return;
}
std::vector<std::string> lines;
in.seekg(0);
while (getline(in, line).good())
{
lines.push_back(line);
}
in.close();
size_t lines_size = lines.size();
std::sort(lines.begin(), lines.end());
lines.erase(std::unique(lines.begin(), lines.end()), lines.end());
rx.print("The history file (%s) is in old format. %zu lines, %zu unique lines.\n",
path.c_str(), lines_size, lines.size());
std::ofstream out(path);
if (!out)
{
rx.print("Cannot open %s for writing (for conversion): %s\n",
path.c_str(), errnoToString(errno).c_str());
return;
}
const std::string & timestamp = replxx_now_ms_str();
for (const auto & out_line : lines)
{
out << "### " << timestamp << "\n" << out_line << std::endl;
}
out.close();
}
}
2022-01-28 15:14:54 +00:00
bool replxx_last_is_delimiter = false;
ReplxxLineReader::ReplxxLineReader(
Suggest & suggest,
2020-06-04 22:45:04 +00:00
const String & history_file_path_,
bool multiline_,
Patterns extenders_,
Patterns delimiters_,
replxx::Replxx::highlighter_callback_t highlighter_)
: LineReader(history_file_path_, multiline_, std::move(extenders_), std::move(delimiters_)), highlighter(std::move(highlighter_))
2021-11-16 21:44:52 +00:00
, editor(getEditor())
{
using namespace std::placeholders;
using Replxx = replxx::Replxx;
if (!history_file_path.empty())
2020-06-04 21:31:51 +00:00
{
history_file_fd = open(history_file_path.c_str(), O_RDWR);
2020-06-04 22:10:22 +00:00
if (history_file_fd < 0)
{
rx.print("Open of history file failed: %s\n", errnoToString(errno).c_str());
2020-06-04 22:10:22 +00:00
}
2020-06-06 09:49:15 +00:00
else
2020-06-04 22:10:22 +00:00
{
convertHistoryFile(history_file_path, rx);
2020-06-06 09:49:15 +00:00
if (flock(history_file_fd, LOCK_SH))
{
rx.print("Shared lock of history file failed: %s\n", errnoToString(errno).c_str());
2020-06-06 09:49:15 +00:00
}
else
{
if (!rx.history_load(history_file_path))
{
2020-12-17 18:08:42 +00:00
rx.print("Loading history failed: %s\n", errnoToString(errno).c_str());
}
2020-06-06 09:49:15 +00:00
if (flock(history_file_fd, LOCK_UN))
{
rx.print("Unlock of history file failed: %s\n", errnoToString(errno).c_str());
2020-06-06 09:49:15 +00:00
}
}
2020-06-04 22:10:22 +00:00
}
2020-06-04 21:31:51 +00:00
}
2020-12-11 16:34:40 +00:00
rx.install_window_change_handler();
auto callback = [&suggest] (const String & context, size_t context_size)
{
return suggest.getCompletions(context, context_size);
};
rx.set_completion_callback(callback);
rx.set_complete_on_empty(false);
rx.set_word_break_characters(word_break_characters);
rx.set_ignore_case(true);
2020-06-04 22:45:04 +00:00
if (highlighter)
rx.set_highlighter_callback(highlighter);
/// By default C-p/C-n binded to COMPLETE_NEXT/COMPLETE_PREV,
/// bind C-p/C-n to history-previous/history-next like readline.
rx.bind_key(Replxx::KEY::control('N'), [this](char32_t code) { return rx.invoke(Replxx::ACTION::HISTORY_NEXT, code); });
rx.bind_key(Replxx::KEY::control('P'), [this](char32_t code) { return rx.invoke(Replxx::ACTION::HISTORY_PREVIOUS, code); });
2021-10-14 15:56:28 +00:00
auto commit_action = [this](char32_t code)
{
/// If we allow multiline and there is already something in the input, start a newline.
2022-01-28 19:13:01 +00:00
/// NOTE: Lexer is only available if we use highlighter.
if (highlighter && multiline && !replxx_last_is_delimiter)
return rx.invoke(Replxx::ACTION::NEW_LINE, code);
2022-01-28 15:14:54 +00:00
replxx_last_is_delimiter = false;
return rx.invoke(Replxx::ACTION::COMMIT_LINE, code);
};
2021-10-14 15:56:28 +00:00
/// bind C-j to ENTER action.
rx.bind_key(Replxx::KEY::control('J'), commit_action);
rx.bind_key(Replxx::KEY::ENTER, commit_action);
2021-10-14 15:56:28 +00:00
/// By default COMPLETE_NEXT/COMPLETE_PREV was binded to C-p/C-n, re-bind
/// to M-P/M-N (that was used for HISTORY_COMMON_PREFIX_SEARCH before, but
/// it also binded to M-p/M-n).
rx.bind_key(Replxx::KEY::meta('N'), [this](char32_t code) { return rx.invoke(Replxx::ACTION::COMPLETE_NEXT, code); });
rx.bind_key(Replxx::KEY::meta('P'), [this](char32_t code) { return rx.invoke(Replxx::ACTION::COMPLETE_PREVIOUS, code); });
/// By default M-BACKSPACE is KILL_TO_WHITESPACE_ON_LEFT, while in readline it is backward-kill-word
rx.bind_key(Replxx::KEY::meta(Replxx::KEY::BACKSPACE), [this](char32_t code) { return rx.invoke(Replxx::ACTION::KILL_TO_BEGINING_OF_WORD, code); });
/// By default C-w is KILL_TO_BEGINING_OF_WORD, while in readline it is unix-word-rubout
rx.bind_key(Replxx::KEY::control('W'), [this](char32_t code) { return rx.invoke(Replxx::ACTION::KILL_TO_WHITESPACE_ON_LEFT, code); });
2020-12-01 10:34:34 +00:00
rx.bind_key(Replxx::KEY::meta('E'), [this](char32_t) { openEditor(); return Replxx::ACTION_RESULT::CONTINUE; });
}
ReplxxLineReader::~ReplxxLineReader()
{
2020-06-04 22:36:40 +00:00
if (close(history_file_fd))
2020-12-17 18:08:42 +00:00
rx.print("Close of history file failed: %s\n", errnoToString(errno).c_str());
}
LineReader::InputStatus ReplxxLineReader::readOneLine(const String & prompt)
{
input.clear();
const char* cinput = rx.input(prompt);
if (cinput == nullptr)
return (errno != EAGAIN) ? ABORT : RESET_LINE;
input = cinput;
trim(input);
return INPUT_LINE;
}
void ReplxxLineReader::addToHistory(const String & line)
{
2020-06-04 21:31:51 +00:00
// locking history file to prevent from inconsistent concurrent changes
//
// replxx::Replxx::history_save() already has lockf(),
// but replxx::Replxx::history_load() does not
// and that is why flock() is added here.
2020-06-06 09:51:42 +00:00
bool locked = false;
2020-06-04 22:10:22 +00:00
if (flock(history_file_fd, LOCK_EX))
2020-12-17 18:08:42 +00:00
rx.print("Lock of history file failed: %s\n", errnoToString(errno).c_str());
2020-06-06 09:51:42 +00:00
else
locked = true;
2020-06-04 21:31:51 +00:00
rx.history_add(line);
2020-06-04 21:31:51 +00:00
// flush changes to the disk
if (!rx.history_save(history_file_path))
2020-12-17 18:08:42 +00:00
rx.print("Saving history failed: %s\n", errnoToString(errno).c_str());
2020-06-04 21:31:51 +00:00
2020-06-06 09:51:42 +00:00
if (locked && 0 != flock(history_file_fd, LOCK_UN))
2020-12-17 18:08:42 +00:00
rx.print("Unlock of history file failed: %s\n", errnoToString(errno).c_str());
}
/// See comments in ShellCommand::executeImpl()
/// (for the vfork via dlsym())
int ReplxxLineReader::executeEditor(const std::string & path)
2020-12-01 10:34:34 +00:00
{
std::vector<char> argv0(editor.data(), editor.data() + editor.size() + 1);
std::vector<char> argv1(path.data(), path.data() + path.size() + 1);
char * const argv[] = {argv0.data(), argv1.data(), nullptr};
2020-12-01 10:34:34 +00:00
static void * real_vfork = dlsym(RTLD_DEFAULT, "vfork");
if (!real_vfork)
2020-12-01 12:48:35 +00:00
{
2020-12-05 08:52:25 +00:00
rx.print("Cannot find symbol vfork in myself: %s\n", errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
return -1;
2020-12-01 12:48:35 +00:00
}
2020-12-01 10:34:34 +00:00
pid_t pid = reinterpret_cast<pid_t (*)()>(real_vfork)();
if (-1 == pid)
2020-12-01 12:48:35 +00:00
{
rx.print("Cannot vfork: %s\n", errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
return -1;
2020-12-01 12:48:35 +00:00
}
2020-12-01 10:34:34 +00:00
/// Child
2020-12-01 10:34:34 +00:00
if (0 == pid)
{
sigset_t mask;
sigemptyset(&mask);
sigprocmask(0, nullptr, &mask);
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
execvp(editor.c_str(), argv);
rx.print("Cannot execute %s: %s\n", editor.c_str(), errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
_exit(-1);
}
int status = 0;
2021-11-16 21:44:52 +00:00
do
{
int exited_pid = waitpid(pid, &status, 0);
if (exited_pid == -1)
{
if (errno == EINTR)
continue;
rx.print("Cannot waitpid: %s\n", errnoToString(errno).c_str());
return -1;
}
else
break;
} while (true);
2020-12-01 10:34:34 +00:00
return status;
}
void ReplxxLineReader::openEditor()
{
2020-12-05 08:52:25 +00:00
char filename[] = "clickhouse_replxx_XXXXXX.sql";
int fd = ::mkstemps(filename, 4);
2020-12-01 10:34:34 +00:00
if (-1 == fd)
2020-12-01 12:48:35 +00:00
{
rx.print("Cannot create temporary file to edit query: %s\n", errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
return;
2020-12-01 12:48:35 +00:00
}
2020-12-01 13:28:55 +00:00
2020-12-01 10:34:34 +00:00
replxx::Replxx::State state(rx.get_state());
2020-12-02 02:25:22 +00:00
size_t bytes_written = 0;
const char * begin = state.text();
size_t offset = strlen(state.text());
while (bytes_written != offset)
{
ssize_t res = ::write(fd, begin + bytes_written, offset - bytes_written);
if ((-1 == res || 0 == res) && errno != EINTR)
{
rx.print("Cannot write to temporary query file %s: %s\n", filename, errnoToString(errno).c_str());
break;
2020-12-02 02:25:22 +00:00
}
bytes_written += res;
}
2020-12-01 10:34:34 +00:00
if (0 != ::close(fd))
2020-12-01 12:48:35 +00:00
{
2020-12-01 13:28:55 +00:00
rx.print("Cannot close temporary query file %s: %s\n", filename, errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
return;
2020-12-01 12:48:35 +00:00
}
2020-12-01 13:28:55 +00:00
if (0 == executeEditor(filename))
2020-12-01 10:34:34 +00:00
{
2020-12-05 08:52:25 +00:00
try
{
std::ifstream t(filename);
std::string str;
t.seekg(0, std::ios::end);
str.reserve(t.tellg());
t.seekg(0, std::ios::beg);
str.assign((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
rx.set_state(replxx::Replxx::State(str.c_str(), str.size()));
}
catch (...)
{
rx.print("Cannot read from temporary query file %s: %s\n", filename, errnoToString(errno).c_str());
return;
}
2020-12-01 10:34:34 +00:00
}
2020-12-01 13:28:55 +00:00
2020-12-01 10:34:34 +00:00
if (bracketed_paste_enabled)
enableBracketedPaste();
2020-12-01 13:28:55 +00:00
if (0 != ::unlink(filename))
rx.print("Cannot remove temporary query file %s: %s\n", filename, errnoToString(errno).c_str());
2020-12-01 10:34:34 +00:00
}
void ReplxxLineReader::enableBracketedPaste()
{
2020-12-01 10:34:34 +00:00
bracketed_paste_enabled = true;
rx.enable_bracketed_paste();
};