| 1 | //===-- ScriptInterpreterLua.cpp ------------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "ScriptInterpreterLua.h" |
| 10 | #include "Lua.h" |
| 11 | #include "lldb/Breakpoint/StoppointCallbackContext.h" |
| 12 | #include "lldb/Core/Debugger.h" |
| 13 | #include "lldb/Core/PluginManager.h" |
| 14 | #include "lldb/Host/StreamFile.h" |
| 15 | #include "lldb/Interpreter/CommandReturnObject.h" |
| 16 | #include "lldb/Target/ExecutionContext.h" |
| 17 | #include "lldb/Utility/Stream.h" |
| 18 | #include "lldb/Utility/StringList.h" |
| 19 | #include "lldb/Utility/Timer.h" |
| 20 | #include "lldb/lldb-forward.h" |
| 21 | #include "llvm/ADT/StringRef.h" |
| 22 | #include "llvm/Support/FormatAdapters.h" |
| 23 | #include <memory> |
| 24 | #include <vector> |
| 25 | |
| 26 | using namespace lldb; |
| 27 | using namespace lldb_private; |
| 28 | |
| 29 | LLDB_PLUGIN_DEFINE(ScriptInterpreterLua) |
| 30 | |
| 31 | enum ActiveIOHandler { |
| 32 | eIOHandlerNone, |
| 33 | eIOHandlerBreakpoint, |
| 34 | eIOHandlerWatchpoint |
| 35 | }; |
| 36 | |
| 37 | class IOHandlerLuaInterpreter : public IOHandlerDelegate, |
| 38 | public IOHandlerEditline { |
| 39 | public: |
| 40 | IOHandlerLuaInterpreter(Debugger &debugger, |
| 41 | ScriptInterpreterLua &script_interpreter, |
| 42 | ActiveIOHandler active_io_handler = eIOHandlerNone) |
| 43 | : IOHandlerEditline(debugger, IOHandler::Type::LuaInterpreter, "lua" , |
| 44 | llvm::StringRef(">>> " ), llvm::StringRef("..> " ), |
| 45 | true, debugger.GetUseColor(), 0, *this), |
| 46 | m_script_interpreter(script_interpreter), |
| 47 | m_active_io_handler(active_io_handler) { |
| 48 | llvm::cantFail(Err: m_script_interpreter.GetLua().ChangeIO( |
| 49 | out: debugger.GetOutputFileSP()->GetStream(), |
| 50 | err: debugger.GetErrorFileSP()->GetStream())); |
| 51 | llvm::cantFail(Err: m_script_interpreter.EnterSession(debugger_id: debugger.GetID())); |
| 52 | } |
| 53 | |
| 54 | ~IOHandlerLuaInterpreter() override { |
| 55 | llvm::cantFail(Err: m_script_interpreter.LeaveSession()); |
| 56 | } |
| 57 | |
| 58 | void IOHandlerActivated(IOHandler &io_handler, bool interactive) override { |
| 59 | const char *instructions = nullptr; |
| 60 | switch (m_active_io_handler) { |
| 61 | case eIOHandlerNone: |
| 62 | break; |
| 63 | case eIOHandlerWatchpoint: |
| 64 | instructions = "Enter your Lua command(s). Type 'quit' to end.\n" |
| 65 | "The commands are compiled as the body of the following " |
| 66 | "Lua function\n" |
| 67 | "function (frame, wp) end\n" ; |
| 68 | SetPrompt(llvm::StringRef("..> " )); |
| 69 | break; |
| 70 | case eIOHandlerBreakpoint: |
| 71 | instructions = "Enter your Lua command(s). Type 'quit' to end.\n" |
| 72 | "The commands are compiled as the body of the following " |
| 73 | "Lua function\n" |
| 74 | "function (frame, bp_loc, ...) end\n" ; |
| 75 | SetPrompt(llvm::StringRef("..> " )); |
| 76 | break; |
| 77 | } |
| 78 | if (instructions == nullptr) |
| 79 | return; |
| 80 | if (interactive) { |
| 81 | if (lldb::LockableStreamFileSP output_sp = |
| 82 | io_handler.GetOutputStreamFileSP()) { |
| 83 | LockedStreamFile locked_stream = output_sp->Lock(); |
| 84 | locked_stream << instructions; |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | bool IOHandlerIsInputComplete(IOHandler &io_handler, |
| 90 | StringList &lines) override { |
| 91 | size_t last = lines.GetSize() - 1; |
| 92 | if (IsQuitCommand(cmd: lines.GetStringAtIndex(idx: last))) { |
| 93 | if (m_active_io_handler == eIOHandlerBreakpoint || |
| 94 | m_active_io_handler == eIOHandlerWatchpoint) |
| 95 | lines.DeleteStringAtIndex(id: last); |
| 96 | return true; |
| 97 | } |
| 98 | StreamString str; |
| 99 | lines.Join(separator: "\n" , strm&: str); |
| 100 | if (llvm::Error E = |
| 101 | m_script_interpreter.GetLua().CheckSyntax(buffer: str.GetString())) { |
| 102 | std::string error_str = toString(E: std::move(E)); |
| 103 | // Lua always errors out to incomplete code with '<eof>' |
| 104 | return error_str.find(s: "<eof>" ) == std::string::npos; |
| 105 | } |
| 106 | // The breakpoint and watchpoint handler only exits with a explicit 'quit' |
| 107 | return m_active_io_handler != eIOHandlerBreakpoint && |
| 108 | m_active_io_handler != eIOHandlerWatchpoint; |
| 109 | } |
| 110 | |
| 111 | void IOHandlerInputComplete(IOHandler &io_handler, |
| 112 | std::string &data) override { |
| 113 | switch (m_active_io_handler) { |
| 114 | case eIOHandlerBreakpoint: { |
| 115 | auto *bp_options_vec = |
| 116 | static_cast<std::vector<std::reference_wrapper<BreakpointOptions>> *>( |
| 117 | io_handler.GetUserData()); |
| 118 | for (BreakpointOptions &bp_options : *bp_options_vec) { |
| 119 | Status error = m_script_interpreter.SetBreakpointCommandCallback( |
| 120 | bp_options, command_body_text: data.c_str(), /*is_callback=*/false); |
| 121 | if (error.Fail()) { |
| 122 | LockedStreamFile locked_stream = |
| 123 | io_handler.GetErrorStreamFileSP()->Lock(); |
| 124 | locked_stream << error.AsCString() << '\n'; |
| 125 | } |
| 126 | } |
| 127 | io_handler.SetIsDone(true); |
| 128 | } break; |
| 129 | case eIOHandlerWatchpoint: { |
| 130 | auto *wp_options = |
| 131 | static_cast<WatchpointOptions *>(io_handler.GetUserData()); |
| 132 | m_script_interpreter.SetWatchpointCommandCallback(wp_options, |
| 133 | command_body_text: data.c_str(), |
| 134 | /*is_callback=*/false); |
| 135 | io_handler.SetIsDone(true); |
| 136 | } break; |
| 137 | case eIOHandlerNone: |
| 138 | if (IsQuitCommand(cmd: data)) { |
| 139 | io_handler.SetIsDone(true); |
| 140 | return; |
| 141 | } |
| 142 | if (llvm::Error error = m_script_interpreter.GetLua().Run(buffer: data)) { |
| 143 | LockedStreamFile locked_stream = |
| 144 | io_handler.GetErrorStreamFileSP()->Lock(); |
| 145 | locked_stream << toString(E: std::move(error)); |
| 146 | } |
| 147 | break; |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | private: |
| 152 | ScriptInterpreterLua &m_script_interpreter; |
| 153 | ActiveIOHandler m_active_io_handler; |
| 154 | |
| 155 | bool IsQuitCommand(llvm::StringRef cmd) { return cmd.rtrim() == "quit" ; } |
| 156 | }; |
| 157 | |
| 158 | ScriptInterpreterLua::ScriptInterpreterLua(Debugger &debugger) |
| 159 | : ScriptInterpreter(debugger, eScriptLanguageLua), |
| 160 | m_lua(std::make_unique<Lua>()) {} |
| 161 | |
| 162 | ScriptInterpreterLua::~ScriptInterpreterLua() = default; |
| 163 | |
| 164 | StructuredData::DictionarySP ScriptInterpreterLua::GetInterpreterInfo() { |
| 165 | auto info = std::make_shared<StructuredData::Dictionary>(); |
| 166 | info->AddStringItem(key: "language" , value: "lua" ); |
| 167 | return info; |
| 168 | } |
| 169 | |
| 170 | bool ScriptInterpreterLua::ExecuteOneLine(llvm::StringRef command, |
| 171 | CommandReturnObject *result, |
| 172 | const ExecuteScriptOptions &options) { |
| 173 | if (command.empty()) { |
| 174 | if (result) |
| 175 | result->AppendError(in_string: "empty command passed to lua\n" ); |
| 176 | return false; |
| 177 | } |
| 178 | |
| 179 | llvm::Expected<std::unique_ptr<ScriptInterpreterIORedirect>> |
| 180 | io_redirect_or_error = ScriptInterpreterIORedirect::Create( |
| 181 | enable_io: options.GetEnableIO(), debugger&: m_debugger, result); |
| 182 | if (!io_redirect_or_error) { |
| 183 | if (result) |
| 184 | result->AppendErrorWithFormatv( |
| 185 | format: "failed to redirect I/O: {0}\n" , |
| 186 | args: llvm::fmt_consume(Item: io_redirect_or_error.takeError())); |
| 187 | else |
| 188 | llvm::consumeError(Err: io_redirect_or_error.takeError()); |
| 189 | return false; |
| 190 | } |
| 191 | |
| 192 | ScriptInterpreterIORedirect &io_redirect = **io_redirect_or_error; |
| 193 | |
| 194 | if (llvm::Error e = |
| 195 | m_lua->ChangeIO(out: io_redirect.GetOutputFile()->GetStream(), |
| 196 | err: io_redirect.GetErrorFile()->GetStream())) { |
| 197 | result->AppendErrorWithFormatv(format: "lua failed to redirect I/O: {0}\n" , |
| 198 | args: llvm::toString(E: std::move(e))); |
| 199 | return false; |
| 200 | } |
| 201 | |
| 202 | if (llvm::Error e = m_lua->Run(buffer: command)) { |
| 203 | result->AppendErrorWithFormatv( |
| 204 | format: "lua failed attempting to evaluate '{0}': {1}\n" , args&: command, |
| 205 | args: llvm::toString(E: std::move(e))); |
| 206 | return false; |
| 207 | } |
| 208 | |
| 209 | io_redirect.Flush(); |
| 210 | return true; |
| 211 | } |
| 212 | |
| 213 | void ScriptInterpreterLua::ExecuteInterpreterLoop() { |
| 214 | LLDB_SCOPED_TIMER(); |
| 215 | |
| 216 | // At the moment, the only time the debugger does not have an input file |
| 217 | // handle is when this is called directly from lua, in which case it is |
| 218 | // both dangerous and unnecessary (not to mention confusing) to try to embed |
| 219 | // a running interpreter loop inside the already running lua interpreter |
| 220 | // loop, so we won't do it. |
| 221 | if (!m_debugger.GetInputFile().IsValid()) |
| 222 | return; |
| 223 | |
| 224 | IOHandlerSP io_handler_sp(new IOHandlerLuaInterpreter(m_debugger, *this)); |
| 225 | m_debugger.RunIOHandlerAsync(reader_sp: io_handler_sp); |
| 226 | } |
| 227 | |
| 228 | bool ScriptInterpreterLua::LoadScriptingModule( |
| 229 | const char *filename, const LoadScriptOptions &options, |
| 230 | lldb_private::Status &error, StructuredData::ObjectSP *module_sp, |
| 231 | FileSpec , lldb::TargetSP loaded_into_target_sp) { |
| 232 | |
| 233 | if (llvm::Error e = m_lua->LoadModule(filename)) { |
| 234 | error = Status::FromErrorStringWithFormatv( |
| 235 | format: "lua failed to import '{0}': {1}\n" , args&: filename, |
| 236 | args: llvm::toString(E: std::move(e))); |
| 237 | return false; |
| 238 | } |
| 239 | return true; |
| 240 | } |
| 241 | |
| 242 | void ScriptInterpreterLua::Initialize() { |
| 243 | static llvm::once_flag g_once_flag; |
| 244 | |
| 245 | llvm::call_once(flag&: g_once_flag, F: []() { |
| 246 | PluginManager::RegisterPlugin(name: GetPluginNameStatic(), |
| 247 | description: GetPluginDescriptionStatic(), |
| 248 | script_lang: lldb::eScriptLanguageLua, create_callback: CreateInstance); |
| 249 | }); |
| 250 | } |
| 251 | |
| 252 | void ScriptInterpreterLua::Terminate() {} |
| 253 | |
| 254 | llvm::Error ScriptInterpreterLua::EnterSession(user_id_t debugger_id) { |
| 255 | if (m_session_is_active) |
| 256 | return llvm::Error::success(); |
| 257 | |
| 258 | const char *fmt_str = |
| 259 | "lldb.debugger = lldb.SBDebugger.FindDebuggerWithID({0}); " |
| 260 | "lldb.target = lldb.debugger:GetSelectedTarget(); " |
| 261 | "lldb.process = lldb.target:GetProcess(); " |
| 262 | "lldb.thread = lldb.process:GetSelectedThread(); " |
| 263 | "lldb.frame = lldb.thread:GetSelectedFrame()" ; |
| 264 | return m_lua->Run(buffer: llvm::formatv(Fmt: fmt_str, Vals&: debugger_id).str()); |
| 265 | } |
| 266 | |
| 267 | llvm::Error ScriptInterpreterLua::LeaveSession() { |
| 268 | if (!m_session_is_active) |
| 269 | return llvm::Error::success(); |
| 270 | |
| 271 | m_session_is_active = false; |
| 272 | |
| 273 | llvm::StringRef str = "lldb.debugger = nil; " |
| 274 | "lldb.target = nil; " |
| 275 | "lldb.process = nil; " |
| 276 | "lldb.thread = nil; " |
| 277 | "lldb.frame = nil" ; |
| 278 | return m_lua->Run(buffer: str); |
| 279 | } |
| 280 | |
| 281 | bool ScriptInterpreterLua::BreakpointCallbackFunction( |
| 282 | void *baton, StoppointCallbackContext *context, user_id_t break_id, |
| 283 | user_id_t break_loc_id) { |
| 284 | assert(context); |
| 285 | |
| 286 | ExecutionContext exe_ctx(context->exe_ctx_ref); |
| 287 | Target *target = exe_ctx.GetTargetPtr(); |
| 288 | if (target == nullptr) |
| 289 | return true; |
| 290 | |
| 291 | StackFrameSP stop_frame_sp(exe_ctx.GetFrameSP()); |
| 292 | BreakpointSP breakpoint_sp = target->GetBreakpointByID(break_id); |
| 293 | BreakpointLocationSP bp_loc_sp(breakpoint_sp->FindLocationByID(bp_loc_id: break_loc_id)); |
| 294 | |
| 295 | Debugger &debugger = target->GetDebugger(); |
| 296 | ScriptInterpreterLua *lua_interpreter = static_cast<ScriptInterpreterLua *>( |
| 297 | debugger.GetScriptInterpreter(can_create: true, language: eScriptLanguageLua)); |
| 298 | Lua &lua = lua_interpreter->GetLua(); |
| 299 | |
| 300 | CommandDataLua *bp_option_data = static_cast<CommandDataLua *>(baton); |
| 301 | llvm::Expected<bool> BoolOrErr = lua.CallBreakpointCallback( |
| 302 | baton, stop_frame_sp, bp_loc_sp, extra_args_sp: bp_option_data->m_extra_args_sp); |
| 303 | if (llvm::Error E = BoolOrErr.takeError()) { |
| 304 | *debugger.GetAsyncErrorStream() << toString(E: std::move(E)); |
| 305 | return true; |
| 306 | } |
| 307 | |
| 308 | return *BoolOrErr; |
| 309 | } |
| 310 | |
| 311 | bool ScriptInterpreterLua::WatchpointCallbackFunction( |
| 312 | void *baton, StoppointCallbackContext *context, user_id_t watch_id) { |
| 313 | assert(context); |
| 314 | |
| 315 | ExecutionContext exe_ctx(context->exe_ctx_ref); |
| 316 | Target *target = exe_ctx.GetTargetPtr(); |
| 317 | if (target == nullptr) |
| 318 | return true; |
| 319 | |
| 320 | StackFrameSP stop_frame_sp(exe_ctx.GetFrameSP()); |
| 321 | WatchpointSP wp_sp = target->GetWatchpointList().FindByID(watchID: watch_id); |
| 322 | |
| 323 | Debugger &debugger = target->GetDebugger(); |
| 324 | ScriptInterpreterLua *lua_interpreter = static_cast<ScriptInterpreterLua *>( |
| 325 | debugger.GetScriptInterpreter(can_create: true, language: eScriptLanguageLua)); |
| 326 | Lua &lua = lua_interpreter->GetLua(); |
| 327 | |
| 328 | llvm::Expected<bool> BoolOrErr = |
| 329 | lua.CallWatchpointCallback(baton, stop_frame_sp, wp_sp); |
| 330 | if (llvm::Error E = BoolOrErr.takeError()) { |
| 331 | *debugger.GetAsyncErrorStream() << toString(E: std::move(E)); |
| 332 | return true; |
| 333 | } |
| 334 | |
| 335 | return *BoolOrErr; |
| 336 | } |
| 337 | |
| 338 | void ScriptInterpreterLua::CollectDataForBreakpointCommandCallback( |
| 339 | std::vector<std::reference_wrapper<BreakpointOptions>> &bp_options_vec, |
| 340 | CommandReturnObject &result) { |
| 341 | IOHandlerSP io_handler_sp( |
| 342 | new IOHandlerLuaInterpreter(m_debugger, *this, eIOHandlerBreakpoint)); |
| 343 | io_handler_sp->SetUserData(&bp_options_vec); |
| 344 | m_debugger.RunIOHandlerAsync(reader_sp: io_handler_sp); |
| 345 | } |
| 346 | |
| 347 | void ScriptInterpreterLua::CollectDataForWatchpointCommandCallback( |
| 348 | WatchpointOptions *wp_options, CommandReturnObject &result) { |
| 349 | IOHandlerSP io_handler_sp( |
| 350 | new IOHandlerLuaInterpreter(m_debugger, *this, eIOHandlerWatchpoint)); |
| 351 | io_handler_sp->SetUserData(wp_options); |
| 352 | m_debugger.RunIOHandlerAsync(reader_sp: io_handler_sp); |
| 353 | } |
| 354 | |
| 355 | Status ScriptInterpreterLua::SetBreakpointCommandCallbackFunction( |
| 356 | BreakpointOptions &bp_options, const char *function_name, |
| 357 | StructuredData::ObjectSP ) { |
| 358 | const char *fmt_str = "return {0}(frame, bp_loc, ...)" ; |
| 359 | std::string oneliner = llvm::formatv(Fmt: fmt_str, Vals&: function_name).str(); |
| 360 | return RegisterBreakpointCallback(bp_options, command_body_text: oneliner.c_str(), |
| 361 | extra_args_sp); |
| 362 | } |
| 363 | |
| 364 | Status ScriptInterpreterLua::SetBreakpointCommandCallback( |
| 365 | BreakpointOptions &bp_options, const char *command_body_text, |
| 366 | bool is_callback) { |
| 367 | return RegisterBreakpointCallback(bp_options, command_body_text, extra_args_sp: {}); |
| 368 | } |
| 369 | |
| 370 | Status ScriptInterpreterLua::RegisterBreakpointCallback( |
| 371 | BreakpointOptions &bp_options, const char *command_body_text, |
| 372 | StructuredData::ObjectSP ) { |
| 373 | auto data_up = std::make_unique<CommandDataLua>(args&: extra_args_sp); |
| 374 | llvm::Error err = |
| 375 | m_lua->RegisterBreakpointCallback(baton: data_up.get(), body: command_body_text); |
| 376 | if (err) |
| 377 | return Status::FromError(error: std::move(err)); |
| 378 | auto baton_sp = |
| 379 | std::make_shared<BreakpointOptions::CommandBaton>(args: std::move(data_up)); |
| 380 | bp_options.SetCallback(callback: ScriptInterpreterLua::BreakpointCallbackFunction, |
| 381 | command_baton_sp: baton_sp); |
| 382 | return {}; |
| 383 | } |
| 384 | |
| 385 | void ScriptInterpreterLua::SetWatchpointCommandCallback( |
| 386 | WatchpointOptions *wp_options, const char *command_body_text, |
| 387 | bool is_callback) { |
| 388 | RegisterWatchpointCallback(wp_options, command_body_text, extra_args_sp: {}); |
| 389 | } |
| 390 | |
| 391 | Status ScriptInterpreterLua::RegisterWatchpointCallback( |
| 392 | WatchpointOptions *wp_options, const char *command_body_text, |
| 393 | StructuredData::ObjectSP ) { |
| 394 | auto data_up = std::make_unique<WatchpointOptions::CommandData>(); |
| 395 | llvm::Error err = |
| 396 | m_lua->RegisterWatchpointCallback(baton: data_up.get(), body: command_body_text); |
| 397 | if (err) |
| 398 | return Status::FromError(error: std::move(err)); |
| 399 | auto baton_sp = |
| 400 | std::make_shared<WatchpointOptions::CommandBaton>(args: std::move(data_up)); |
| 401 | wp_options->SetCallback(callback: ScriptInterpreterLua::WatchpointCallbackFunction, |
| 402 | baton_sp); |
| 403 | return {}; |
| 404 | } |
| 405 | |
| 406 | lldb::ScriptInterpreterSP |
| 407 | ScriptInterpreterLua::CreateInstance(Debugger &debugger) { |
| 408 | return std::make_shared<ScriptInterpreterLua>(args&: debugger); |
| 409 | } |
| 410 | |
| 411 | llvm::StringRef ScriptInterpreterLua::GetPluginDescriptionStatic() { |
| 412 | return "Lua script interpreter" ; |
| 413 | } |
| 414 | |
| 415 | Lua &ScriptInterpreterLua::GetLua() { return *m_lua; } |
| 416 | |