1 - Partial Statements The interpreter loop runs every line it receives, so partial Lua statements are not being handled properly. This is a problem for multiline breakpoint scripts since the interpreter loop, for this particular case, is just an abstraction to a partially parsed function body declaration. This patch addresses this issue and as a side effect improves the general Lua interpreter loop as well. It's now possible to write partial statements in the 'script' command. Example: (lldb) script >>> do ..> local a = 123 ..> print(a) ..> end 123 The technique implemented is the same as the one employed by Lua's own REPL implementation. Partial statements always errors out with the '<eof>' tag in the error message. 2 - CheckSyntax in Lua.h In order to support (1), we need an API for just checking the syntax of string buffers. 3 - Multiline scripted breakpoints Finally, with all the base features implemented this feature is straightforward. The interpreter loop behaves exactly the same, the difference is that it will aggregate all Lua statements into the body of the breakpoint function. An explicit 'quit' statement is needed to exit the interpreter loop. Example: (lldb) breakpoint command add -s lua Enter your Lua command(s). Type 'quit' to end. The commands are compiled as the body of the following Lua function function (frame, bp_loc, ...) end ..> print(456) ..> a = 123 ..> quit Differential Revision: https://reviews.llvm.org/D93481
321 lines
11 KiB
C++
321 lines
11 KiB
C++
//===-- ScriptInterpreterLua.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 "ScriptInterpreterLua.h"
|
|
#include "Lua.h"
|
|
#include "lldb/Breakpoint/StoppointCallbackContext.h"
|
|
#include "lldb/Core/Debugger.h"
|
|
#include "lldb/Core/PluginManager.h"
|
|
#include "lldb/Core/StreamFile.h"
|
|
#include "lldb/Interpreter/CommandReturnObject.h"
|
|
#include "lldb/Target/ExecutionContext.h"
|
|
#include "lldb/Utility/Stream.h"
|
|
#include "lldb/Utility/StringList.h"
|
|
#include "lldb/Utility/Timer.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/FormatAdapters.h"
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
|
|
LLDB_PLUGIN_DEFINE(ScriptInterpreterLua)
|
|
|
|
enum ActiveIOHandler {
|
|
eIOHandlerNone,
|
|
eIOHandlerBreakpoint,
|
|
eIOHandlerWatchpoint
|
|
};
|
|
|
|
class IOHandlerLuaInterpreter : public IOHandlerDelegate,
|
|
public IOHandlerEditline {
|
|
public:
|
|
IOHandlerLuaInterpreter(Debugger &debugger,
|
|
ScriptInterpreterLua &script_interpreter,
|
|
ActiveIOHandler active_io_handler = eIOHandlerNone)
|
|
: IOHandlerEditline(debugger, IOHandler::Type::LuaInterpreter, "lua",
|
|
">>> ", "..> ", true, debugger.GetUseColor(), 0,
|
|
*this, nullptr),
|
|
m_script_interpreter(script_interpreter),
|
|
m_active_io_handler(active_io_handler) {
|
|
llvm::cantFail(m_script_interpreter.GetLua().ChangeIO(
|
|
debugger.GetOutputFile().GetStream(),
|
|
debugger.GetErrorFile().GetStream()));
|
|
llvm::cantFail(m_script_interpreter.EnterSession(debugger.GetID()));
|
|
}
|
|
|
|
~IOHandlerLuaInterpreter() override {
|
|
llvm::cantFail(m_script_interpreter.LeaveSession());
|
|
}
|
|
|
|
void IOHandlerActivated(IOHandler &io_handler, bool interactive) override {
|
|
const char *instructions = nullptr;
|
|
switch (m_active_io_handler) {
|
|
case eIOHandlerNone:
|
|
case eIOHandlerWatchpoint:
|
|
break;
|
|
case eIOHandlerBreakpoint:
|
|
instructions = "Enter your Lua command(s). Type 'quit' to end.\n"
|
|
"The commands are compiled as the body of the following "
|
|
"Lua function\n"
|
|
"function (frame, bp_loc, ...) end\n";
|
|
SetPrompt(llvm::StringRef("..> "));
|
|
break;
|
|
}
|
|
if (instructions == nullptr)
|
|
return;
|
|
if (interactive)
|
|
*io_handler.GetOutputStreamFileSP() << instructions;
|
|
}
|
|
|
|
bool IOHandlerIsInputComplete(IOHandler &io_handler,
|
|
StringList &lines) override {
|
|
size_t last = lines.GetSize() - 1;
|
|
if (IsQuitCommand(lines.GetStringAtIndex(last))) {
|
|
if (m_active_io_handler == eIOHandlerBreakpoint)
|
|
lines.DeleteStringAtIndex(last);
|
|
return true;
|
|
}
|
|
StreamString str;
|
|
lines.Join("\n", str);
|
|
if (llvm::Error E =
|
|
m_script_interpreter.GetLua().CheckSyntax(str.GetString())) {
|
|
std::string error_str = toString(std::move(E));
|
|
// Lua always errors out to incomplete code with '<eof>'
|
|
return error_str.find("<eof>") == std::string::npos;
|
|
}
|
|
// The breakpoint handler only exits with a explicit 'quit'
|
|
return m_active_io_handler != eIOHandlerBreakpoint;
|
|
}
|
|
|
|
void IOHandlerInputComplete(IOHandler &io_handler,
|
|
std::string &data) override {
|
|
switch (m_active_io_handler) {
|
|
case eIOHandlerBreakpoint: {
|
|
auto *bp_options_vec = static_cast<std::vector<BreakpointOptions *> *>(
|
|
io_handler.GetUserData());
|
|
for (auto *bp_options : *bp_options_vec) {
|
|
Status error = m_script_interpreter.SetBreakpointCommandCallback(
|
|
bp_options, data.c_str());
|
|
if (error.Fail())
|
|
*io_handler.GetErrorStreamFileSP() << error.AsCString() << '\n';
|
|
}
|
|
io_handler.SetIsDone(true);
|
|
} break;
|
|
case eIOHandlerWatchpoint:
|
|
io_handler.SetIsDone(true);
|
|
break;
|
|
case eIOHandlerNone:
|
|
if (IsQuitCommand(data)) {
|
|
io_handler.SetIsDone(true);
|
|
return;
|
|
}
|
|
if (llvm::Error error = m_script_interpreter.GetLua().Run(data))
|
|
*io_handler.GetErrorStreamFileSP() << toString(std::move(error));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
ScriptInterpreterLua &m_script_interpreter;
|
|
ActiveIOHandler m_active_io_handler;
|
|
|
|
bool IsQuitCommand(llvm::StringRef cmd) { return cmd.rtrim() == "quit"; }
|
|
};
|
|
|
|
ScriptInterpreterLua::ScriptInterpreterLua(Debugger &debugger)
|
|
: ScriptInterpreter(debugger, eScriptLanguageLua),
|
|
m_lua(std::make_unique<Lua>()) {}
|
|
|
|
ScriptInterpreterLua::~ScriptInterpreterLua() {}
|
|
|
|
bool ScriptInterpreterLua::ExecuteOneLine(llvm::StringRef command,
|
|
CommandReturnObject *result,
|
|
const ExecuteScriptOptions &options) {
|
|
if (command.empty()) {
|
|
if (result)
|
|
result->AppendError("empty command passed to lua\n");
|
|
return false;
|
|
}
|
|
|
|
llvm::Expected<std::unique_ptr<ScriptInterpreterIORedirect>>
|
|
io_redirect_or_error = ScriptInterpreterIORedirect::Create(
|
|
options.GetEnableIO(), m_debugger, result);
|
|
if (!io_redirect_or_error) {
|
|
if (result)
|
|
result->AppendErrorWithFormatv(
|
|
"failed to redirect I/O: {0}\n",
|
|
llvm::fmt_consume(io_redirect_or_error.takeError()));
|
|
else
|
|
llvm::consumeError(io_redirect_or_error.takeError());
|
|
return false;
|
|
}
|
|
|
|
ScriptInterpreterIORedirect &io_redirect = **io_redirect_or_error;
|
|
|
|
if (llvm::Error e =
|
|
m_lua->ChangeIO(io_redirect.GetOutputFile()->GetStream(),
|
|
io_redirect.GetErrorFile()->GetStream())) {
|
|
result->AppendErrorWithFormatv("lua failed to redirect I/O: {0}\n",
|
|
llvm::toString(std::move(e)));
|
|
return false;
|
|
}
|
|
|
|
if (llvm::Error e = m_lua->Run(command)) {
|
|
result->AppendErrorWithFormatv(
|
|
"lua failed attempting to evaluate '{0}': {1}\n", command,
|
|
llvm::toString(std::move(e)));
|
|
return false;
|
|
}
|
|
|
|
io_redirect.Flush();
|
|
return true;
|
|
}
|
|
|
|
void ScriptInterpreterLua::ExecuteInterpreterLoop() {
|
|
LLDB_SCOPED_TIMER();
|
|
|
|
// At the moment, the only time the debugger does not have an input file
|
|
// handle is when this is called directly from lua, in which case it is
|
|
// both dangerous and unnecessary (not to mention confusing) to try to embed
|
|
// a running interpreter loop inside the already running lua interpreter
|
|
// loop, so we won't do it.
|
|
if (!m_debugger.GetInputFile().IsValid())
|
|
return;
|
|
|
|
IOHandlerSP io_handler_sp(new IOHandlerLuaInterpreter(m_debugger, *this));
|
|
m_debugger.RunIOHandlerAsync(io_handler_sp);
|
|
}
|
|
|
|
bool ScriptInterpreterLua::LoadScriptingModule(
|
|
const char *filename, bool init_session, lldb_private::Status &error,
|
|
StructuredData::ObjectSP *module_sp, FileSpec extra_search_dir) {
|
|
|
|
FileSystem::Instance().Collect(filename);
|
|
if (llvm::Error e = m_lua->LoadModule(filename)) {
|
|
error.SetErrorStringWithFormatv("lua failed to import '{0}': {1}\n",
|
|
filename, llvm::toString(std::move(e)));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ScriptInterpreterLua::Initialize() {
|
|
static llvm::once_flag g_once_flag;
|
|
|
|
llvm::call_once(g_once_flag, []() {
|
|
PluginManager::RegisterPlugin(GetPluginNameStatic(),
|
|
GetPluginDescriptionStatic(),
|
|
lldb::eScriptLanguageLua, CreateInstance);
|
|
});
|
|
}
|
|
|
|
void ScriptInterpreterLua::Terminate() {}
|
|
|
|
llvm::Error ScriptInterpreterLua::EnterSession(user_id_t debugger_id) {
|
|
if (m_session_is_active)
|
|
return llvm::Error::success();
|
|
|
|
const char *fmt_str =
|
|
"lldb.debugger = lldb.SBDebugger.FindDebuggerWithID({0}); "
|
|
"lldb.target = lldb.debugger:GetSelectedTarget(); "
|
|
"lldb.process = lldb.target:GetProcess(); "
|
|
"lldb.thread = lldb.process:GetSelectedThread(); "
|
|
"lldb.frame = lldb.thread:GetSelectedFrame()";
|
|
return m_lua->Run(llvm::formatv(fmt_str, debugger_id).str());
|
|
}
|
|
|
|
llvm::Error ScriptInterpreterLua::LeaveSession() {
|
|
if (!m_session_is_active)
|
|
return llvm::Error::success();
|
|
|
|
m_session_is_active = false;
|
|
|
|
llvm::StringRef str = "lldb.debugger = nil; "
|
|
"lldb.target = nil; "
|
|
"lldb.process = nil; "
|
|
"lldb.thread = nil; "
|
|
"lldb.frame = nil";
|
|
return m_lua->Run(str);
|
|
}
|
|
|
|
bool ScriptInterpreterLua::BreakpointCallbackFunction(
|
|
void *baton, StoppointCallbackContext *context, user_id_t break_id,
|
|
user_id_t break_loc_id) {
|
|
assert(context);
|
|
|
|
ExecutionContext exe_ctx(context->exe_ctx_ref);
|
|
Target *target = exe_ctx.GetTargetPtr();
|
|
if (target == nullptr)
|
|
return true;
|
|
|
|
StackFrameSP stop_frame_sp(exe_ctx.GetFrameSP());
|
|
BreakpointSP breakpoint_sp = target->GetBreakpointByID(break_id);
|
|
BreakpointLocationSP bp_loc_sp(breakpoint_sp->FindLocationByID(break_loc_id));
|
|
|
|
Debugger &debugger = target->GetDebugger();
|
|
ScriptInterpreterLua *lua_interpreter = static_cast<ScriptInterpreterLua *>(
|
|
debugger.GetScriptInterpreter(true, eScriptLanguageLua));
|
|
Lua &lua = lua_interpreter->GetLua();
|
|
|
|
llvm::Expected<bool> BoolOrErr =
|
|
lua.CallBreakpointCallback(baton, stop_frame_sp, bp_loc_sp);
|
|
if (llvm::Error E = BoolOrErr.takeError()) {
|
|
debugger.GetErrorStream() << toString(std::move(E));
|
|
return true;
|
|
}
|
|
|
|
return *BoolOrErr;
|
|
}
|
|
|
|
void ScriptInterpreterLua::CollectDataForBreakpointCommandCallback(
|
|
std::vector<BreakpointOptions *> &bp_options_vec,
|
|
CommandReturnObject &result) {
|
|
IOHandlerSP io_handler_sp(
|
|
new IOHandlerLuaInterpreter(m_debugger, *this, eIOHandlerBreakpoint));
|
|
io_handler_sp->SetUserData(&bp_options_vec);
|
|
m_debugger.RunIOHandlerAsync(io_handler_sp);
|
|
}
|
|
|
|
Status ScriptInterpreterLua::SetBreakpointCommandCallback(
|
|
BreakpointOptions *bp_options, const char *command_body_text) {
|
|
Status error;
|
|
auto data_up = std::make_unique<CommandDataLua>();
|
|
error = m_lua->RegisterBreakpointCallback(data_up.get(), command_body_text);
|
|
if (error.Fail())
|
|
return error;
|
|
auto baton_sp =
|
|
std::make_shared<BreakpointOptions::CommandBaton>(std::move(data_up));
|
|
bp_options->SetCallback(ScriptInterpreterLua::BreakpointCallbackFunction,
|
|
baton_sp);
|
|
return error;
|
|
}
|
|
|
|
lldb::ScriptInterpreterSP
|
|
ScriptInterpreterLua::CreateInstance(Debugger &debugger) {
|
|
return std::make_shared<ScriptInterpreterLua>(debugger);
|
|
}
|
|
|
|
lldb_private::ConstString ScriptInterpreterLua::GetPluginNameStatic() {
|
|
static ConstString g_name("script-lua");
|
|
return g_name;
|
|
}
|
|
|
|
const char *ScriptInterpreterLua::GetPluginDescriptionStatic() {
|
|
return "Lua script interpreter";
|
|
}
|
|
|
|
lldb_private::ConstString ScriptInterpreterLua::GetPluginName() {
|
|
return GetPluginNameStatic();
|
|
}
|
|
|
|
uint32_t ScriptInterpreterLua::GetPluginVersion() { return 1; }
|
|
|
|
Lua &ScriptInterpreterLua::GetLua() { return *m_lua; }
|