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