llvm-project/lldb/source/Expression/REPL.cpp

614 lines
22 KiB
C++

//===-- REPL.cpp ----------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "lldb/Expression/REPL.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/StreamFile.h"
#include "lldb/Expression/ExpressionVariable.h"
#include "lldb/Expression/UserExpression.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandReturnObject.h"
#include "lldb/Target/Thread.h"
#include "lldb/Utility/AnsiTerminal.h"
#include <memory>
using namespace lldb_private;
REPL::REPL(LLVMCastKind kind, Target &target) : m_target(target), m_kind(kind) {
// Make sure all option values have sane defaults
Debugger &debugger = m_target.GetDebugger();
auto exe_ctx = debugger.GetCommandInterpreter().GetExecutionContext();
m_format_options.OptionParsingStarting(&exe_ctx);
m_varobj_options.OptionParsingStarting(&exe_ctx);
}
REPL::~REPL() = default;
lldb::REPLSP REPL::Create(Status &err, lldb::LanguageType language,
Debugger *debugger, Target *target,
const char *repl_options) {
uint32_t idx = 0;
lldb::REPLSP ret;
while (REPLCreateInstance create_instance =
PluginManager::GetREPLCreateCallbackAtIndex(idx++)) {
ret = (*create_instance)(err, language, debugger, target, repl_options);
if (ret) {
break;
}
}
return ret;
}
std::string REPL::GetSourcePath() {
ConstString file_basename = GetSourceFileBasename();
FileSpec tmpdir_file_spec = HostInfo::GetProcessTempDir();
if (tmpdir_file_spec) {
tmpdir_file_spec.GetFilename() = file_basename;
m_repl_source_path = tmpdir_file_spec.GetPath();
} else {
tmpdir_file_spec = FileSpec("/tmp");
tmpdir_file_spec.AppendPathComponent(file_basename.GetStringRef());
}
return tmpdir_file_spec.GetPath();
}
lldb::IOHandlerSP REPL::GetIOHandler() {
if (!m_io_handler_sp) {
Debugger &debugger = m_target.GetDebugger();
m_io_handler_sp = std::make_shared<IOHandlerEditline>(
debugger, IOHandler::Type::REPL,
"lldb-repl", // Name of input reader for history
llvm::StringRef("> "), // prompt
llvm::StringRef(". "), // Continuation prompt
true, // Multi-line
true, // The REPL prompt is always colored
1, // Line number
*this, nullptr);
// Don't exit if CTRL+C is pressed
static_cast<IOHandlerEditline *>(m_io_handler_sp.get())
->SetInterruptExits(false);
if (m_io_handler_sp->GetIsInteractive() &&
m_io_handler_sp->GetIsRealTerminal()) {
m_indent_str.assign(debugger.GetTabSize(), ' ');
m_enable_auto_indent = debugger.GetAutoIndent();
} else {
m_indent_str.clear();
m_enable_auto_indent = false;
}
}
return m_io_handler_sp;
}
void REPL::IOHandlerActivated(IOHandler &io_handler, bool interactive) {
lldb::ProcessSP process_sp = m_target.GetProcessSP();
if (process_sp && process_sp->IsAlive())
return;
lldb::StreamFileSP error_sp(io_handler.GetErrorStreamFileSP());
error_sp->Printf("REPL requires a running target process.\n");
io_handler.SetIsDone(true);
}
bool REPL::IOHandlerInterrupt(IOHandler &io_handler) { return false; }
void REPL::IOHandlerInputInterrupted(IOHandler &io_handler, std::string &line) {
}
const char *REPL::IOHandlerGetFixIndentationCharacters() {
return (m_enable_auto_indent ? GetAutoIndentCharacters() : nullptr);
}
ConstString REPL::IOHandlerGetControlSequence(char ch) {
if (ch == 'd')
return ConstString(":quit\n");
return ConstString();
}
const char *REPL::IOHandlerGetCommandPrefix() { return ":"; }
const char *REPL::IOHandlerGetHelpPrologue() {
return "\nThe REPL (Read-Eval-Print-Loop) acts like an interpreter. "
"Valid statements, expressions, and declarations are immediately "
"compiled and executed.\n\n"
"The complete set of LLDB debugging commands are also available as "
"described below.\n\nCommands "
"must be prefixed with a colon at the REPL prompt (:quit for "
"example.) Typing just a colon "
"followed by return will switch to the LLDB prompt.\n\n"
"Type “< path” to read in code from a text file “path”.\n\n";
}
bool REPL::IOHandlerIsInputComplete(IOHandler &io_handler, StringList &lines) {
// Check for meta command
const size_t num_lines = lines.GetSize();
if (num_lines == 1) {
const char *first_line = lines.GetStringAtIndex(0);
if (first_line[0] == ':')
return true; // Meta command is a single line where that starts with ':'
}
// Check if REPL input is done
std::string source_string(lines.CopyList());
return SourceIsComplete(source_string);
}
int REPL::CalculateActualIndentation(const StringList &lines) {
std::string last_line = lines[lines.GetSize() - 1];
int actual_indent = 0;
for (char &ch : last_line) {
if (ch != ' ')
break;
++actual_indent;
}
return actual_indent;
}
int REPL::IOHandlerFixIndentation(IOHandler &io_handler,
const StringList &lines,
int cursor_position) {
if (!m_enable_auto_indent)
return 0;
if (!lines.GetSize()) {
return 0;
}
int tab_size = io_handler.GetDebugger().GetTabSize();
lldb::offset_t desired_indent =
GetDesiredIndentation(lines, cursor_position, tab_size);
int actual_indent = REPL::CalculateActualIndentation(lines);
if (desired_indent == LLDB_INVALID_OFFSET)
return 0;
return (int)desired_indent - actual_indent;
}
static bool ReadCode(const std::string &path, std::string &code,
lldb::StreamFileSP &error_sp) {
auto &fs = FileSystem::Instance();
llvm::Twine pathTwine(path);
if (!fs.Exists(pathTwine)) {
error_sp->Printf("no such file at path '%s'\n", path.c_str());
return false;
}
if (!fs.Readable(pathTwine)) {
error_sp->Printf("could not read file at path '%s'\n", path.c_str());
return false;
}
const size_t file_size = fs.GetByteSize(pathTwine);
const size_t max_size = code.max_size();
if (file_size > max_size) {
error_sp->Printf("file at path '%s' too large: "
"file_size = %zu, max_size = %zu\n",
path.c_str(), file_size, max_size);
return false;
}
auto data_sp = fs.CreateDataBuffer(pathTwine);
if (data_sp == nullptr) {
error_sp->Printf("could not create buffer for file at path '%s'\n",
path.c_str());
return false;
}
code.assign((const char *)data_sp->GetBytes(), data_sp->GetByteSize());
return true;
}
void REPL::IOHandlerInputComplete(IOHandler &io_handler, std::string &code) {
lldb::StreamFileSP output_sp(io_handler.GetOutputStreamFileSP());
lldb::StreamFileSP error_sp(io_handler.GetErrorStreamFileSP());
bool extra_line = false;
bool did_quit = false;
if (code.empty()) {
m_code.AppendString("");
static_cast<IOHandlerEditline &>(io_handler)
.SetBaseLineNumber(m_code.GetSize() + 1);
} else {
Debugger &debugger = m_target.GetDebugger();
CommandInterpreter &ci = debugger.GetCommandInterpreter();
extra_line = ci.GetSpaceReplPrompts();
ExecutionContext exe_ctx(m_target.GetProcessSP()
->GetThreadList()
.GetSelectedThread()
->GetSelectedFrame()
.get());
lldb::ProcessSP process_sp(exe_ctx.GetProcessSP());
if (code[0] == ':') {
// Meta command
// Strip the ':'
code.erase(0, 1);
if (!llvm::StringRef(code).trim().empty()) {
// "lldb" was followed by arguments, so just execute the command dump
// the results
// Turn off prompt on quit in case the user types ":quit"
const bool saved_prompt_on_quit = ci.GetPromptOnQuit();
if (saved_prompt_on_quit)
ci.SetPromptOnQuit(false);
// Execute the command
CommandReturnObject result(debugger.GetUseColor());
result.SetImmediateOutputStream(output_sp);
result.SetImmediateErrorStream(error_sp);
ci.HandleCommand(code.c_str(), eLazyBoolNo, result);
if (saved_prompt_on_quit)
ci.SetPromptOnQuit(true);
if (result.GetStatus() == lldb::eReturnStatusQuit) {
did_quit = true;
io_handler.SetIsDone(true);
if (debugger.CheckTopIOHandlerTypes(
IOHandler::Type::REPL, IOHandler::Type::CommandInterpreter)) {
// We typed "quit" or an alias to quit so we need to check if the
// command interpreter is above us and tell it that it is done as
// well so we don't drop back into the command interpreter if we
// have already quit
lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
if (io_handler_sp)
io_handler_sp->SetIsDone(true);
}
}
} else {
// ":" was followed by no arguments, so push the LLDB command prompt
if (debugger.CheckTopIOHandlerTypes(
IOHandler::Type::REPL, IOHandler::Type::CommandInterpreter)) {
// If the user wants to get back to the command interpreter and the
// command interpreter is what launched the REPL, then just let the
// REPL exit and fall back to the command interpreter.
io_handler.SetIsDone(true);
} else {
// The REPL wasn't launched the by the command interpreter, it is the
// base IOHandler, so we need to get the command interpreter and
lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
if (io_handler_sp) {
io_handler_sp->SetIsDone(false);
debugger.RunIOHandlerAsync(ci.GetIOHandler());
}
}
}
} else {
if (code[0] == '<') {
// User wants to read code from a file.
// Interpret rest of line as a literal path.
auto path = llvm::StringRef(code.substr(1)).trim().str();
if (!ReadCode(path, code, error_sp)) {
return;
}
}
// Unwind any expression we might have been running in case our REPL
// expression crashed and the user was looking around
if (m_dedicated_repl_mode) {
Thread *thread = exe_ctx.GetThreadPtr();
if (thread && thread->UnwindInnermostExpression().Success()) {
thread->SetSelectedFrameByIndex(0, false);
exe_ctx.SetFrameSP(thread->GetSelectedFrame());
}
}
const bool colorize_err = error_sp->GetFile().GetIsTerminalWithColors();
EvaluateExpressionOptions expr_options = m_expr_options;
expr_options.SetCoerceToId(m_varobj_options.use_objc);
expr_options.SetKeepInMemory(true);
expr_options.SetUseDynamic(m_varobj_options.use_dynamic);
expr_options.SetGenerateDebugInfo(true);
expr_options.SetREPLEnabled(true);
expr_options.SetColorizeErrors(colorize_err);
expr_options.SetPoundLine(m_repl_source_path.c_str(),
m_code.GetSize() + 1);
expr_options.SetLanguage(GetLanguage());
PersistentExpressionState *persistent_state =
m_target.GetPersistentExpressionStateForLanguage(GetLanguage());
if (!persistent_state)
return;
const size_t var_count_before = persistent_state->GetSize();
const char *expr_prefix = nullptr;
lldb::ValueObjectSP result_valobj_sp;
Status error;
lldb::ExpressionResults execution_results =
UserExpression::Evaluate(exe_ctx, expr_options, code.c_str(),
expr_prefix, result_valobj_sp, error,
nullptr); // fixed expression
// CommandInterpreter &ci = debugger.GetCommandInterpreter();
if (process_sp && process_sp->IsAlive()) {
bool add_to_code = true;
bool handled = false;
if (result_valobj_sp) {
lldb::Format format = m_format_options.GetFormat();
if (result_valobj_sp->GetError().Success()) {
handled |= PrintOneVariable(debugger, output_sp, result_valobj_sp);
} else if (result_valobj_sp->GetError().GetError() ==
UserExpression::kNoResult) {
if (format != lldb::eFormatVoid && debugger.GetNotifyVoid()) {
error_sp->PutCString("(void)\n");
handled = true;
}
}
}
if (debugger.GetPrintDecls()) {
for (size_t vi = var_count_before, ve = persistent_state->GetSize();
vi != ve; ++vi) {
lldb::ExpressionVariableSP persistent_var_sp =
persistent_state->GetVariableAtIndex(vi);
lldb::ValueObjectSP valobj_sp = persistent_var_sp->GetValueObject();
PrintOneVariable(debugger, output_sp, valobj_sp,
persistent_var_sp.get());
}
}
if (!handled) {
bool useColors = error_sp->GetFile().GetIsTerminalWithColors();
switch (execution_results) {
case lldb::eExpressionSetupError:
case lldb::eExpressionParseError:
add_to_code = false;
LLVM_FALLTHROUGH;
case lldb::eExpressionDiscarded:
error_sp->Printf("%s\n", error.AsCString());
break;
case lldb::eExpressionCompleted:
break;
case lldb::eExpressionInterrupted:
if (useColors) {
error_sp->Printf(ANSI_ESCAPE1(ANSI_FG_COLOR_RED));
error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_BOLD));
}
error_sp->Printf("Execution interrupted. ");
if (useColors)
error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_NORMAL));
error_sp->Printf("Enter code to recover and continue.\nEnter LLDB "
"commands to investigate (type :help for "
"assistance.)\n");
break;
case lldb::eExpressionHitBreakpoint:
// Breakpoint was hit, drop into LLDB command interpreter
if (useColors) {
error_sp->Printf(ANSI_ESCAPE1(ANSI_FG_COLOR_RED));
error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_BOLD));
}
output_sp->Printf("Execution stopped at breakpoint. ");
if (useColors)
error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_NORMAL));
output_sp->Printf("Enter LLDB commands to investigate (type help "
"for assistance.)\n");
{
lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
if (io_handler_sp) {
io_handler_sp->SetIsDone(false);
debugger.RunIOHandlerAsync(ci.GetIOHandler());
}
}
break;
case lldb::eExpressionTimedOut:
error_sp->Printf("error: timeout\n");
if (error.AsCString())
error_sp->Printf("error: %s\n", error.AsCString());
break;
case lldb::eExpressionResultUnavailable:
// Shoulnd't happen???
error_sp->Printf("error: could not fetch result -- %s\n",
error.AsCString());
break;
case lldb::eExpressionStoppedForDebug:
// Shoulnd't happen???
error_sp->Printf("error: stopped for debug -- %s\n",
error.AsCString());
break;
case lldb::eExpressionThreadVanished:
// Shoulnd't happen???
error_sp->Printf("error: expression thread vanished -- %s\n",
error.AsCString());
break;
}
}
if (add_to_code) {
const uint32_t new_default_line = m_code.GetSize() + 1;
m_code.SplitIntoLines(code);
// Update our code on disk
if (!m_repl_source_path.empty()) {
auto file = FileSystem::Instance().Open(
FileSpec(m_repl_source_path),
File::eOpenOptionWrite | File::eOpenOptionTruncate |
File::eOpenOptionCanCreate,
lldb::eFilePermissionsFileDefault);
if (file) {
std::string code(m_code.CopyList());
code.append(1, '\n');
size_t bytes_written = code.size();
file.get()->Write(code.c_str(), bytes_written);
file.get()->Close();
} else {
std::string message = llvm::toString(file.takeError());
error_sp->Printf("error: couldn't open %s: %s\n",
m_repl_source_path.c_str(), message.c_str());
}
// Now set the default file and line to the REPL source file
m_target.GetSourceManager().SetDefaultFileAndLine(
FileSpec(m_repl_source_path), new_default_line);
}
static_cast<IOHandlerEditline &>(io_handler)
.SetBaseLineNumber(m_code.GetSize() + 1);
}
if (extra_line) {
output_sp->Printf("\n");
}
}
}
// Don't complain about the REPL process going away if we are in the
// process of quitting.
if (!did_quit && (!process_sp || !process_sp->IsAlive())) {
error_sp->Printf(
"error: REPL process is no longer alive, exiting REPL\n");
io_handler.SetIsDone(true);
}
}
}
void REPL::IOHandlerComplete(IOHandler &io_handler,
CompletionRequest &request) {
// Complete an LLDB command if the first character is a colon...
if (request.GetRawLine().startswith(":")) {
Debugger &debugger = m_target.GetDebugger();
// auto complete LLDB commands
llvm::StringRef new_line = request.GetRawLine().drop_front();
CompletionResult sub_result;
CompletionRequest sub_request(new_line, request.GetRawCursorPos() - 1,
sub_result);
debugger.GetCommandInterpreter().HandleCompletion(sub_request);
StringList matches, descriptions;
sub_result.GetMatches(matches);
// Prepend command prefix that was excluded in the completion request.
if (request.GetCursorIndex() == 0)
for (auto &match : matches)
match.insert(0, 1, ':');
sub_result.GetDescriptions(descriptions);
request.AddCompletions(matches, descriptions);
return;
}
// Strip spaces from the line and see if we had only spaces
if (request.GetRawLine().trim().empty()) {
// Only spaces on this line, so just indent
request.AddCompletion(m_indent_str);
return;
}
std::string current_code;
current_code.append(m_code.CopyList());
IOHandlerEditline &editline = static_cast<IOHandlerEditline &>(io_handler);
const StringList *current_lines = editline.GetCurrentLines();
if (current_lines) {
const uint32_t current_line_idx = editline.GetCurrentLineIndex();
if (current_line_idx < current_lines->GetSize()) {
for (uint32_t i = 0; i < current_line_idx; ++i) {
const char *line_cstr = current_lines->GetStringAtIndex(i);
if (line_cstr) {
current_code.append("\n");
current_code.append(line_cstr);
}
}
}
}
current_code.append("\n");
current_code += request.GetRawLine();
CompleteCode(current_code, request);
}
bool QuitCommandOverrideCallback(void *baton, const char **argv) {
Target *target = (Target *)baton;
lldb::ProcessSP process_sp(target->GetProcessSP());
if (process_sp) {
process_sp->Destroy(false);
process_sp->GetTarget().GetDebugger().ClearIOHandlers();
}
return false;
}
Status REPL::RunLoop() {
Status error;
error = DoInitialization();
m_repl_source_path = GetSourcePath();
if (!error.Success())
return error;
Debugger &debugger = m_target.GetDebugger();
lldb::IOHandlerSP io_handler_sp(GetIOHandler());
FileSpec save_default_file;
uint32_t save_default_line = 0;
if (!m_repl_source_path.empty()) {
// Save the current default file and line
m_target.GetSourceManager().GetDefaultFileAndLine(save_default_file,
save_default_line);
}
debugger.RunIOHandlerAsync(io_handler_sp);
// Check if we are in dedicated REPL mode where LLDB was start with the "--
// repl" option from the command line. Currently we know this by checking if
// the debugger already has a IOHandler thread.
if (!debugger.HasIOHandlerThread()) {
// The debugger doesn't have an existing IOHandler thread, so this must be
// dedicated REPL mode...
m_dedicated_repl_mode = true;
debugger.StartIOHandlerThread();
llvm::StringRef command_name_str("quit");
CommandObject *cmd_obj =
debugger.GetCommandInterpreter().GetCommandObjectForCommand(
command_name_str);
if (cmd_obj) {
assert(command_name_str.empty());
cmd_obj->SetOverrideCallback(QuitCommandOverrideCallback, &m_target);
}
}
// Wait for the REPL command interpreter to get popped
io_handler_sp->WaitForPop();
if (m_dedicated_repl_mode) {
// If we were in dedicated REPL mode we would have started the IOHandler
// thread, and we should kill our process
lldb::ProcessSP process_sp = m_target.GetProcessSP();
if (process_sp && process_sp->IsAlive())
process_sp->Destroy(false);
// Wait for the IO handler thread to exit (TODO: don't do this if the IO
// handler thread already exists...)
debugger.JoinIOHandlerThread();
}
// Restore the default file and line
if (save_default_file && save_default_line != 0)
m_target.GetSourceManager().SetDefaultFileAndLine(save_default_file,
save_default_line);
return error;
}