diff --git a/lldb/examples/python/scripted_process/my_scripted_process.py b/lldb/examples/python/scripted_process/my_scripted_process.py --- a/lldb/examples/python/scripted_process/my_scripted_process.py +++ b/lldb/examples/python/scripted_process/my_scripted_process.py @@ -29,6 +29,9 @@ def get_process_id(self) -> int: return 42 + def should_stop(self) -> bool: + return True + def is_alive(self) -> bool: return True diff --git a/lldb/examples/python/scripted_process/scripted_process.py b/lldb/examples/python/scripted_process/scripted_process.py --- a/lldb/examples/python/scripted_process/scripted_process.py +++ b/lldb/examples/python/scripted_process/scripted_process.py @@ -137,6 +137,24 @@ """ return lldb.SBError() + @abstractmethod + def should_stop(self): + """ Check if the scripted process plugin should produce the stop event. + + Returns: + bool: True if scripted process should broadcast a stop event. + False otherwise. + """ + pass + + def stop(self): + """ Trigger the scripted process stop. + + Returns: + lldb.SBError: An `lldb.SBError` with error code 0. + """ + return lldb.SBError() + @abstractmethod def is_alive(self): """ Check if the scripted process is alive. diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h --- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h +++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h @@ -144,7 +144,8 @@ ScriptInterpreter( Debugger &debugger, lldb::ScriptLanguage script_lang, - lldb::ScriptedProcessInterfaceUP scripted_process_interface_up = {}); + lldb::ScriptedProcessInterfaceUP scripted_process_interface_up = + std::make_unique()); ~ScriptInterpreter() override = default; diff --git a/lldb/include/lldb/Interpreter/ScriptedProcessInterface.h b/lldb/include/lldb/Interpreter/ScriptedProcessInterface.h --- a/lldb/include/lldb/Interpreter/ScriptedProcessInterface.h +++ b/lldb/include/lldb/Interpreter/ScriptedProcessInterface.h @@ -32,6 +32,10 @@ virtual Status Resume() { return Status("ScriptedProcess did not resume"); } + virtual bool ShouldStop() { return true; } + + virtual Status Stop() { return Status("ScriptedProcess did not stop"); } + virtual lldb::MemoryRegionInfoSP GetMemoryRegionContainingAddress(lldb::addr_t address) { return nullptr; diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h --- a/lldb/include/lldb/Target/Process.h +++ b/lldb/include/lldb/Target/Process.h @@ -2602,8 +2602,6 @@ virtual size_t DoReadMemory(lldb::addr_t vm_addr, void *buf, size_t size, Status &error) = 0; - void SetState(lldb::EventSP &event_sp); - lldb::StateType GetPrivateState(); /// The "private" side of resuming a process. This doesn't alter the state diff --git a/lldb/source/Plugins/Process/CMakeLists.txt b/lldb/source/Plugins/Process/CMakeLists.txt --- a/lldb/source/Plugins/Process/CMakeLists.txt +++ b/lldb/source/Plugins/Process/CMakeLists.txt @@ -12,6 +12,7 @@ elseif (CMAKE_SYSTEM_NAME MATCHES "Darwin") add_subdirectory(MacOSX-Kernel) endif() +add_subdirectory(scripted) add_subdirectory(gdb-remote) add_subdirectory(Utility) add_subdirectory(elf-core) diff --git a/lldb/source/Plugins/Process/scripted/CMakeLists.txt b/lldb/source/Plugins/Process/scripted/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/CMakeLists.txt @@ -0,0 +1,13 @@ +add_lldb_library(lldbPluginScriptedProcess PLUGIN + ScriptedProcess.cpp + + LINK_LIBS + lldbCore + lldbTarget + lldbUtility + lldbPluginProcessUtility + LINK_COMPONENTS + BinaryFormat + Object + Support + ) diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h @@ -0,0 +1,119 @@ +//===-- ScriptedProcess.h ------------------------------------- -*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H +#define LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H + +#include "lldb/Target/Process.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/Status.h" + +#include + +namespace lldb_private { + +class ScriptedProcess : public Process { +protected: + class ScriptedProcessInfo { + public: + ScriptedProcessInfo(const ProcessLaunchInfo &launch_info) { + m_class_name = launch_info.GetScriptedProcessClassName(); + m_dictionary_sp = launch_info.GetScriptedProcessDictionarySP(); + } + + std::string GetClassName() const { return m_class_name; } + StructuredData::DictionarySP GetDictionarySP() const { + return m_dictionary_sp; + } + + private: + std::string m_class_name; + StructuredData::DictionarySP m_dictionary_sp; + }; + +public: + static lldb::ProcessSP CreateInstance(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const FileSpec *crash_file_path, + bool can_connect); + + static void Initialize(); + + static void Terminate(); + + static ConstString GetPluginNameStatic(); + + static const char *GetPluginDescriptionStatic(); + + ScriptedProcess(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp, + const ScriptedProcess::ScriptedProcessInfo &launch_info, + Status &error); + + ~ScriptedProcess() override; + + bool CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) override; + + DynamicLoader *GetDynamicLoader() override { return nullptr; } + + ConstString GetPluginName() override; + + uint32_t GetPluginVersion() override; + + SystemRuntime *GetSystemRuntime() override { return nullptr; } + + Status DoLoadCore() override; + + Status DoLaunch(Module *exe_module, ProcessLaunchInfo &launch_info) override; + + void DidLaunch() override; + + Status DoResume() override; + + Status DoDestroy() override; + + void RefreshStateAfterStop() override{}; + + bool IsAlive() override; + + size_t DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) override; + + ArchSpec GetArchitecture(); + + Status GetMemoryRegionInfo(lldb::addr_t load_addr, + MemoryRegionInfo &range_info) override; + + Status + GetMemoryRegions(lldb_private::MemoryRegionInfos ®ion_list) override; + + bool GetProcessInfo(ProcessInstanceInfo &info) override; + +protected: + Status DoStop(); + + void Clear(); + + bool DoUpdateThreadList(ThreadList &old_thread_list, + ThreadList &new_thread_list) override; + +private: + void CheckInterpreterAndScriptObject() const; + ScriptedProcessInterface &GetInterface() const; + static bool IsScriptLanguageSupported(lldb::ScriptLanguage language); + + // Member variables. + const ScriptedProcessInfo m_scripted_process_info; + lldb_private::ScriptInterpreter *m_interpreter = nullptr; + lldb_private::StructuredData::ObjectSP m_script_object_sp = nullptr; + //@} +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp @@ -0,0 +1,313 @@ +//===-- ScriptedProcess.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 "ScriptedProcess.h" + +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/PluginManager.h" + +#include "lldb/Host/OptionParser.h" +#include "lldb/Host/ThreadLauncher.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Interpreter/OptionArgParser.h" +#include "lldb/Interpreter/OptionGroupBoolean.h" +#include "lldb/Interpreter/ScriptInterpreter.h" +#include "lldb/Target/MemoryRegionInfo.h" +#include "lldb/Target/RegisterContext.h" + +#include "lldb/Utility/Log.h" +#include "lldb/Utility/Logging.h" +#include "lldb/Utility/State.h" + +#include + +LLDB_PLUGIN_DEFINE(ScriptedProcess) + +using namespace lldb; +using namespace lldb_private; + +ConstString ScriptedProcess::GetPluginNameStatic() { + static ConstString g_name("ScriptedProcess"); + return g_name; +} + +const char *ScriptedProcess::GetPluginDescriptionStatic() { + return "Scripted Process plug-in."; +} + +static constexpr lldb::ScriptLanguage g_supported_script_languages[] = { + ScriptLanguage::eScriptLanguagePython, +}; + +bool ScriptedProcess::IsScriptLanguageSupported(lldb::ScriptLanguage language) { + llvm::ArrayRef supported_languages = + llvm::makeArrayRef(g_supported_script_languages); + + return llvm::is_contained(supported_languages, language); +} + +void ScriptedProcess::CheckInterpreterAndScriptObject() const { + lldbassert(m_interpreter && "Invalid Script Interpreter."); + lldbassert(m_script_object_sp && "Invalid Script Object."); +} + +lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const FileSpec *file, + bool can_connect) { + if (!target_sp || + !IsScriptLanguageSupported(target_sp->GetDebugger().GetScriptLanguage())) + return nullptr; + + Status error; + ScriptedProcess::ScriptedProcessInfo scripted_process_info( + target_sp->GetProcessLaunchInfo()); + + auto process_sp = std::make_shared( + target_sp, listener_sp, scripted_process_info, error); + + if (error.Fail() || !process_sp || !process_sp->m_script_object_sp || + !process_sp->m_script_object_sp->IsValid()) { + LLDB_LOGF(GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS), "%s", + error.AsCString()); + return nullptr; + } + + return process_sp; +} + +bool ScriptedProcess::CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) { + return true; +} + +ScriptedProcess::ScriptedProcess( + lldb::TargetSP target_sp, lldb::ListenerSP listener_sp, + const ScriptedProcess::ScriptedProcessInfo &scripted_process_info, + Status &error) + : Process(target_sp, listener_sp), + m_scripted_process_info(scripted_process_info) { + + if (!target_sp) { + error.SetErrorStringWithFormat("ScriptedProcess::%s () - ERROR: %s", + __FUNCTION__, "Invalid target"); + return; + } + + m_interpreter = target_sp->GetDebugger().GetScriptInterpreter(); + + if (!m_interpreter) { + error.SetErrorStringWithFormat("ScriptedProcess::%s () - ERROR: %s", + __FUNCTION__, + "Debugger has no Script Interpreter"); + return; + } + + StructuredData::ObjectSP object_sp = GetInterface().CreatePluginObject( + m_scripted_process_info.GetClassName().c_str(), target_sp, + m_scripted_process_info.GetDictionarySP()); + + if (!object_sp || !object_sp->IsValid()) { + error.SetErrorStringWithFormat("ScriptedProcess::%s () - ERROR: %s", + __FUNCTION__, + "Failed to create valid script object"); + return; + } + + m_script_object_sp = object_sp; +} + +ScriptedProcess::~ScriptedProcess() { + Clear(); + // We need to call finalize on the process before destroying ourselves to + // make sure all of the broadcaster cleanup goes as planned. If we destruct + // this class, then Process::~Process() might have problems trying to fully + // destroy the broadcaster. + Finalize(); +} + +void ScriptedProcess::Initialize() { + static llvm::once_flag g_once_flag; + + llvm::call_once(g_once_flag, []() { + PluginManager::RegisterPlugin(GetPluginNameStatic(), + GetPluginDescriptionStatic(), CreateInstance); + }); +} + +void ScriptedProcess::Terminate() { + PluginManager::UnregisterPlugin(ScriptedProcess::CreateInstance); +} + +ConstString ScriptedProcess::GetPluginName() { return GetPluginNameStatic(); } + +uint32_t ScriptedProcess::GetPluginVersion() { return 1; } + +Status ScriptedProcess::DoLoadCore() { + ProcessLaunchInfo launch_info = GetTarget().GetProcessLaunchInfo(); + + return DoLaunch(nullptr, launch_info); +} + +Status ScriptedProcess::DoLaunch(Module *exe_module, + ProcessLaunchInfo &launch_info) { + CheckInterpreterAndScriptObject(); + + /* FIXME: This doesn't reflect how lldb actually launches a process. + In reality, it attaches to debugserver, then resume the process. */ + Status error = GetInterface().Launch(); + SetPrivateState(eStateRunning); + + if (error.Fail()) + return error; + + // TODO: Fetch next state from stopped event queue then send stop event + // const StateType state = SetThreadStopInfo(response); + // if (state != eStateInvalid) { + // SetPrivateState(state); + + SetPrivateState(eStateStopped); + + UpdateThreadListIfNeeded(); + GetThreadList(); + + return {}; +} + +void ScriptedProcess::DidLaunch() { + CheckInterpreterAndScriptObject(); + m_pid = GetInterface().GetProcessID(); +} + +Status ScriptedProcess::DoResume() { + CheckInterpreterAndScriptObject(); + + Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS)); + // FIXME: Fetch data from thread. + const StateType thread_resume_state = eStateRunning; + LLDB_LOGF(log, "ScriptedProcess::%s thread_resume_state = %s", __FUNCTION__, + StateAsCString(thread_resume_state)); + + bool resume = (thread_resume_state == eStateRunning); + assert(thread_resume_state == eStateRunning && "invalid thread resume state"); + + Status error; + if (resume) { + LLDB_LOGF(log, "ScriptedProcess::%s sending resume", __FUNCTION__); + + SetPrivateState(eStateRunning); + SetPrivateState(eStateStopped); + error = GetInterface().Resume(); + } + + return error; +} + +Status ScriptedProcess::DoStop() { + CheckInterpreterAndScriptObject(); + + Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS)); + + if (GetInterface().ShouldStop()) { + SetPrivateState(eStateStopped); + LLDB_LOGF(log, "ScriptedProcess::%s Immediate stop", __FUNCTION__); + return {}; + } + + LLDB_LOGF(log, "ScriptedProcess::%s Delayed stop", __FUNCTION__); + return GetInterface().Stop(); +} + +Status ScriptedProcess::DoDestroy() { return Status(); } + +bool ScriptedProcess::IsAlive() { + if (m_interpreter && m_script_object_sp) + return GetInterface().IsAlive(); + return false; +} + +size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) { + + auto error_with_message = [&error](llvm::StringRef message) { + error.SetErrorString(message); + return 0; + }; + + if (!m_interpreter) + return error_with_message("No interpreter."); + + lldb::DataExtractorSP data_extractor_sp = + GetInterface().ReadMemoryAtAddress(addr, size, error); + + if (!data_extractor_sp || error.Fail()) + return 0; + + offset_t bytes_copied = data_extractor_sp->CopyByteOrderedData( + 0, data_extractor_sp->GetByteSize(), buf, size, GetByteOrder()); + + if (!bytes_copied || bytes_copied == LLDB_INVALID_OFFSET) + return error_with_message("Failed to copy read memory to buffer."); + + return size; +} + +ArchSpec ScriptedProcess::GetArchitecture() { + return GetTarget().GetArchitecture(); +} + +Status ScriptedProcess::GetMemoryRegionInfo(lldb::addr_t load_addr, + MemoryRegionInfo ®ion) { + // TODO: Implement + return Status(); +} + +Status ScriptedProcess::GetMemoryRegions(MemoryRegionInfos ®ion_list) { + CheckInterpreterAndScriptObject(); + + lldb::addr_t address = 0; + lldb::MemoryRegionInfoSP mem_region_sp = nullptr; + + while ((mem_region_sp = + GetInterface().GetMemoryRegionContainingAddress(address))) { + auto range = mem_region_sp->GetRange(); + address += range.GetRangeBase() + range.GetByteSize(); + region_list.push_back(*mem_region_sp.get()); + } + + return {}; +} + +void ScriptedProcess::Clear() { Process::m_thread_list.Clear(); } + +bool ScriptedProcess::DoUpdateThreadList(ThreadList &old_thread_list, + ThreadList &new_thread_list) { + // TODO: Implement + // This is supposed to get the current set of threads, if any of them are in + // old_thread_list then they get copied to new_thread_list, and then any + // actually new threads will get added to new_thread_list. + return new_thread_list.GetSize(false) > 0; +} + +bool ScriptedProcess::GetProcessInfo(ProcessInstanceInfo &info) { + info.Clear(); + info.SetProcessID(GetID()); + info.SetArchitecture(GetArchitecture()); + lldb::ModuleSP module_sp = GetTarget().GetExecutableModule(); + if (module_sp) { + const bool add_exe_file_as_first_arg = false; + info.SetExecutableFile(GetTarget().GetExecutableModule()->GetFileSpec(), + add_exe_file_as_first_arg); + } + return true; +} + +ScriptedProcessInterface &ScriptedProcess::GetInterface() const { + return m_interpreter->GetScriptedProcessInterface(); +} diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.h --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.h @@ -30,6 +30,10 @@ Status Resume() override; + bool ShouldStop() override; + + Status Stop() override; + lldb::MemoryRegionInfoSP GetMemoryRegionContainingAddress(lldb::addr_t address) override; @@ -48,7 +52,7 @@ protected: size_t GetGenericInteger(llvm::StringRef method_name); - Status LaunchOrResume(llvm::StringRef method_name); + Status GetStatusFromMethod(llvm::StringRef method_name); private: // The lifetime is managed by the ScriptInterpreter diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.cpp --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptedProcessPythonInterface.cpp @@ -55,15 +55,23 @@ } Status ScriptedProcessPythonInterface::Launch() { - return LaunchOrResume("launch"); + return GetStatusFromMethod("launch"); } Status ScriptedProcessPythonInterface::Resume() { - return LaunchOrResume("resume"); + return GetStatusFromMethod("resume"); } -Status -ScriptedProcessPythonInterface::LaunchOrResume(llvm::StringRef method_name) { +bool ScriptedProcessPythonInterface::ShouldStop() { + return GetGenericInteger("shuold_stop"); +} + +Status ScriptedProcessPythonInterface::Stop() { + return GetStatusFromMethod("stop"); +} + +Status ScriptedProcessPythonInterface::GetStatusFromMethod( + llvm::StringRef method_name) { Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock); @@ -281,7 +289,6 @@ bool ScriptedProcessPythonInterface::IsAlive() { return GetGenericInteger("is_alive"); - ; } #endif diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -2974,7 +2974,7 @@ // If we're not already connected to the process, and if we have a platform // that can launch a process for debugging, go ahead and do that here. if (state != eStateConnected && platform_sp && - platform_sp->CanDebugProcess()) { + platform_sp->CanDebugProcess() && !launch_info.IsScriptedProcess()) { LLDB_LOGF(log, "Target::%s asking the platform to debug the process", __FUNCTION__); diff --git a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py --- a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py +++ b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py @@ -11,7 +11,7 @@ from lldbsuite.test import lldbtest -class PlatformProcessCrashInfoTestCase(TestBase): +class ScriptedProcesTestCase(TestBase): mydir = TestBase.compute_mydir(__file__) @@ -43,3 +43,55 @@ self.expect('script dir(ScriptedProcess)', substrs=["launch"]) + def test_launch_scripted_process_sbapi(self): + """Test that we can launch an lldb scripted process using the SBAPI, + check its process ID and read string from memory.""" + self.build() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + + scripted_process_example_relpath = ['..','..','..','..','examples','python','scripted_process','my_scripted_process.py'] + os.environ['SKIP_SCRIPTED_PROCESS_LAUNCH'] = '1' + self.runCmd("command script import " + os.path.join(self.getSourceDir(), + *scripted_process_example_relpath)) + + launch_info = lldb.SBLaunchInfo(None) + launch_info.SetProcessPluginName("ScriptedProcess") + launch_info.SetScriptedProcessClassName("my_scripted_process.MyScriptedProcess") + + error = lldb.SBError() + process = target.Launch(launch_info, error) + self.assertTrue(process and process.IsValid(), PROCESS_IS_VALID) + self.assertEqual(process.GetProcessID(), 42) + + hello_world = "Hello, world!" + memory_read = process.ReadCStringFromMemory(0x50000000000, + len(hello_world) + 1, # NULL byte + error) + + self.assertTrue(error.Success(), "Failed to read memory from scripted process.") + self.assertEqual(hello_world, memory_read) + + def test_launch_scripted_process_cli(self): + """Test that we can launch an lldb scripted process from the command + line, check its process ID and read string from memory.""" + self.build() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + + scripted_process_example_relpath = ['..','..','..','..','examples','python','scripted_process','my_scripted_process.py'] + self.runCmd("command script import " + os.path.join(self.getSourceDir(), + *scripted_process_example_relpath)) + + process = target.GetProcess() + self.assertTrue(process, PROCESS_IS_VALID) + self.assertEqual(process.GetProcessID(), 42) + + error = lldb.SBError() + hello_world = "Hello, world!" + memory_read = process.ReadCStringFromMemory(0x50000000000, + len(hello_world) + 1, # NULL byte + error) + + self.assertTrue(error.Success(), "Failed to read memory from scripted process.") + self.assertEqual(hello_world, memory_read)