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 @@ -111,14 +111,18 @@ ProcessAttachInfo() : ProcessInstanceInfo(), m_listener_sp(), m_hijack_listener_sp(), m_plugin_name(), m_resume_count(0), m_wait_for_launch(false), - m_ignore_existing(true), m_continue_once_attached(false), - m_detach_on_error(true), m_async(false) {} + m_wait_for_launch_interval(UINT32_MAX), + m_wait_for_launch_duration(UINT32_MAX), m_ignore_existing(true), + m_continue_once_attached(false), m_detach_on_error(true), + m_async(false) {} ProcessAttachInfo(const ProcessLaunchInfo &launch_info) : ProcessInstanceInfo(), m_listener_sp(), m_hijack_listener_sp(), m_plugin_name(), m_resume_count(0), m_wait_for_launch(false), - m_ignore_existing(true), m_continue_once_attached(false), - m_detach_on_error(true), m_async(false) { + m_wait_for_launch_interval(UINT32_MAX), + m_wait_for_launch_duration(UINT32_MAX), m_ignore_existing(true), + m_continue_once_attached(false), m_detach_on_error(true), + m_async(false) { ProcessInfo::operator=(launch_info); SetProcessPluginName(launch_info.GetProcessPluginName()); SetResumeCount(launch_info.GetResumeCount()); @@ -131,6 +135,30 @@ void SetWaitForLaunch(bool b) { m_wait_for_launch = b; } + bool HasWaitForLaunchInterval() const { + return m_wait_for_launch_interval != UINT32_MAX; + } + + uint32_t GetWaitForLaunchInterval() const { + return m_wait_for_launch_interval; + } + + void SetWaitForLaunchInterval(uint32_t new_interval) { + m_wait_for_launch_interval = new_interval; + } + + bool HasWaitForLaunchDuration() const { + return m_wait_for_launch_duration != UINT32_MAX; + } + + uint32_t GetWaitForLaunchDuration() const { + return m_wait_for_launch_duration; + } + + void SetWaitForLaunchDuration(uint32_t new_duration) { + m_wait_for_launch_duration = new_duration; + } + bool GetAsync() const { return m_async; } void SetAsync(bool b) { m_async = b; } @@ -160,6 +188,8 @@ m_plugin_name.clear(); m_resume_count = 0; m_wait_for_launch = false; + m_wait_for_launch_interval = UINT32_MAX; + m_wait_for_launch_duration = UINT32_MAX; m_ignore_existing = true; m_continue_once_attached = false; } @@ -199,6 +229,8 @@ std::string m_plugin_name; uint32_t m_resume_count; // How many times do we resume after launching bool m_wait_for_launch; + uint32_t m_wait_for_launch_interval; + uint32_t m_wait_for_launch_duration; bool m_ignore_existing; bool m_continue_once_attached; // Supports the use-case scenario of // immediately continuing the process once diff --git a/lldb/include/lldb/Utility/StringExtractor.h b/lldb/include/lldb/Utility/StringExtractor.h --- a/lldb/include/lldb/Utility/StringExtractor.h +++ b/lldb/include/lldb/Utility/StringExtractor.h @@ -89,7 +89,7 @@ size_t GetHexBytesAvail(llvm::MutableArrayRef dest); - size_t GetHexByteString(std::string &str); + size_t GetHexByteString(std::string &str, bool set_eof_on_fail = true); size_t GetHexByteStringFixedLength(std::string &str, uint32_t nibble_length); diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h --- a/lldb/include/lldb/lldb-enumerations.h +++ b/lldb/include/lldb/lldb-enumerations.h @@ -555,6 +555,8 @@ eArgTypePermissionsNumber, eArgTypePermissionsString, eArgTypePid, + eArgTypeWaitforInterval, + eArgTypeWaitforDuration, eArgTypePlugin, eArgTypeProcessName, eArgTypePythonClass, diff --git a/lldb/source/Commands/CommandObjectProcess.cpp b/lldb/source/Commands/CommandObjectProcess.cpp --- a/lldb/source/Commands/CommandObjectProcess.cpp +++ b/lldb/source/Commands/CommandObjectProcess.cpp @@ -306,6 +306,26 @@ attach_info.SetIgnoreExisting(false); break; + case 'I': + uint32_t interval; + if (option_arg.getAsInteger(0, interval)) { + error.SetErrorStringWithFormat("invalid polling interval '%s'", + option_arg.str().c_str()); + } else { + attach_info.SetWaitForLaunchInterval(interval); + } + break; + + case 'd': + uint32_t duration; + if (option_arg.getAsInteger(0, duration)) { + error.SetErrorStringWithFormat("invalid duration '%s'", + option_arg.str().c_str()); + } else { + attach_info.SetWaitForLaunchDuration(duration); + } + break; + default: llvm_unreachable("Unimplemented option"); } diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -657,6 +657,12 @@ Group<2>, Desc<"Include existing processes when doing attach -w.">; def process_attach_waitfor : Option<"waitfor", "w">, Group<2>, Desc<"Wait for the process with to launch.">; + def process_attach_waitfor_interval : Option<"waitfor-interval", "I">, + Group<2>, Arg<"WaitforInterval">, + Desc<"The interval that waitfor uses to poll the list of processes.">; + def process_attach_waitfor_duration : Option<"waitfor-duration", "d">, + Group<2>, Arg<"WaitforDuration">, + Desc<"The interval that waitfor uses to poll the list of processes.">; } let Command = "process continue" in { diff --git a/lldb/source/Interpreter/CommandObject.cpp b/lldb/source/Interpreter/CommandObject.cpp --- a/lldb/source/Interpreter/CommandObject.cpp +++ b/lldb/source/Interpreter/CommandObject.cpp @@ -1088,6 +1088,8 @@ { eArgTypePermissionsNumber, "perms-numeric", CommandCompletions::eNoCompletion, { nullptr, false }, "Permissions given as an octal number (e.g. 755)." }, { eArgTypePermissionsString, "perms=string", CommandCompletions::eNoCompletion, { nullptr, false }, "Permissions given as a string value (e.g. rw-r-xr--)." }, { eArgTypePid, "pid", CommandCompletions::eProcessIDCompletion, { nullptr, false }, "The process ID number." }, + { eArgTypeWaitforInterval, "waitfor-interval", CommandCompletions::eProcessIDCompletion, { nullptr, false }, "The interval in milliseconds that waitfor uses to poll the list of processes. Defaults to 1." }, + { eArgTypeWaitforDuration, "waitfor-duration", CommandCompletions::eProcessIDCompletion, { nullptr, false }, "The timout that in seconds when waiting for a process with waitfor. Defaults to infinite." }, { eArgTypePlugin, "plugin", CommandCompletions::eProcessPluginCompletion, { nullptr, false }, "Help text goes here." }, { eArgTypeProcessName, "process-name", CommandCompletions::eProcessNameCompletion, { nullptr, false }, "The name of the process." }, { eArgTypePythonClass, "python-class", CommandCompletions::eNoCompletion, { nullptr, false }, "The name of a Python class." }, diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h @@ -59,6 +59,20 @@ /// attach operation. Status AttachToProcess(lldb::pid_t pid); + /// Wait to attach to a process with a given name. + /// + /// This method supports waiting for the next instance of a process + /// with a given name and attaching llgs to that via the configured + /// Platform. + /// + /// \return + /// An Status object indicating the success or failure of the + /// attach operation. + Status AttachWaitProcess(llvm::StringRef process_name, + llvm::Optional user_polling_interval, + llvm::Optional timeout, + bool include_existing); + // NativeProcessProtocol::NativeDelegate overrides void InitializeDelegate(NativeProcessProtocol *process) override; @@ -170,6 +184,12 @@ PacketResult Handle_vAttach(StringExtractorGDBRemote &packet); + PacketResult Handle_qVAttachOrWaitSupported(StringExtractorGDBRemote &packet); + + PacketResult Handle_vAttachWait(StringExtractorGDBRemote &packet); + + PacketResult Handle_vAttachOrWait(StringExtractorGDBRemote &packet); + PacketResult Handle_D(StringExtractorGDBRemote &packet); PacketResult Handle_qThreadStopInfo(StringExtractorGDBRemote &packet); @@ -227,6 +247,12 @@ void StopSTDIOForwarding(); + PacketResult + Handle_vAttachWait_or_vAttachOrWait(StringExtractorGDBRemote &packet, + bool include_existing, + const char *identifier); + + // For GDBRemoteCommunicationServerLLGS only GDBRemoteCommunicationServerLLGS(const GDBRemoteCommunicationServerLLGS &) = delete; diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp @@ -159,6 +159,15 @@ RegisterMemberFunctionHandler( StringExtractorGDBRemote::eServerPacketType_vAttach, &GDBRemoteCommunicationServerLLGS::Handle_vAttach); + RegisterMemberFunctionHandler( + StringExtractorGDBRemote::eServerPacketType_vAttachWait, + &GDBRemoteCommunicationServerLLGS::Handle_vAttachWait); + RegisterMemberFunctionHandler( + StringExtractorGDBRemote::eServerPacketType_qVAttachOrWaitSupported, + &GDBRemoteCommunicationServerLLGS::Handle_qVAttachOrWaitSupported); + RegisterMemberFunctionHandler( + StringExtractorGDBRemote::eServerPacketType_vAttachOrWait, + &GDBRemoteCommunicationServerLLGS::Handle_vAttachOrWait); RegisterMemberFunctionHandler( StringExtractorGDBRemote::eServerPacketType_vCont, &GDBRemoteCommunicationServerLLGS::Handle_vCont); @@ -334,6 +343,100 @@ return Status(); } +Status GDBRemoteCommunicationServerLLGS::AttachWaitProcess( + llvm::StringRef process_name, + llvm::Optional user_polling_interval, + llvm::Optional timeout, bool include_existing) { + Log *log(GetLogIfAnyCategoriesSet(LIBLLDB_LOG_PROCESS)); + + std::chrono::milliseconds polling_interval; + if (user_polling_interval.hasValue()) { + polling_interval = + std::chrono::milliseconds(user_polling_interval.getValue()); + LLDB_LOG(log, "using user-defined polling interval '{0}'", + polling_interval); + } else { + polling_interval = std::chrono::milliseconds(1); + } + + // Create the matcher used to search the process list. + ProcessInstanceInfoList exclusion_list; + ProcessInstanceInfoMatch match_info; + match_info.GetProcessInfo().GetExecutableFile().SetFile( + process_name, llvm::sys::path::Style::posix); + match_info.SetNameMatchType(NameMatch::EndsWith); + + if (!include_existing) { + // Create the excluded process list before polling begins. + Host::FindProcesses(match_info, exclusion_list); + LLDB_LOG(log, "placed '{0}' processes in the exclusion list.", + exclusion_list.size()); + } else { + LLDB_LOG(log, "including existing processes in search", process_name); + } + + LLDB_LOG(log, "waiting for '{0}' to appear", process_name); + + auto is_in_exclusion_list = [&exclusion_list](ProcessInstanceInfo &info) { + for (auto &excluded : exclusion_list) { + if (excluded.GetProcessID() == info.GetProcessID()) { + return true; + } + } + return false; + }; + + auto now = std::chrono::system_clock::now(); + auto target = now; + if (timeout.hasValue()) + target += std::chrono::seconds(timeout.getValue()); + + ProcessInstanceInfoList loop_process_list; + while (!timeout.hasValue() || now < target) { + loop_process_list.clear(); + if (Host::FindProcesses(match_info, loop_process_list)) { + // Remove all the elements that are in the exclusion list. + loop_process_list.erase(std::remove_if(loop_process_list.begin(), + loop_process_list.end(), + is_in_exclusion_list), + loop_process_list.end()); + + // One match! We found the desired process. + if (loop_process_list.size() == 1) { + auto matching_process_pid = loop_process_list[0].GetProcessID(); + LLDB_LOG(log, "found pid {0}", matching_process_pid); + return AttachToProcess(matching_process_pid); + } + + // Multiple matches! Return an error reporting the PIDs we found. + if (loop_process_list.size() > 1) { + StreamString error_stream; + error_stream.Printf( + "Multiple executables with name: '%s' found. Pids: ", + process_name.str().c_str()); + for (size_t i = 0; i < loop_process_list.size() - 1; ++i) { + error_stream.Printf("%lu, ", loop_process_list[i].GetProcessID()); + } + error_stream.Printf("%lu.", loop_process_list.back().GetProcessID()); + + Status error; + error.SetErrorString(error_stream.GetString()); + return error; + } + } + // No matches, we have not found the process. Sleep until next poll. + LLDB_LOG(log, "sleep {0} seconds", polling_interval); + std::this_thread::sleep_for(polling_interval); + if (timeout.hasValue()) + now = std::chrono::system_clock::now(); + } + + // Timed out + Status error; + error.SetErrorString("waitfor timed out."); + return error; +} + void GDBRemoteCommunicationServerLLGS::InitializeDelegate( NativeProcessProtocol *process) { assert(process && "process cannot be NULL"); @@ -3188,6 +3291,94 @@ return SendStopReasonForState(m_debugged_process_up->GetState()); } +GDBRemoteCommunication::PacketResult +GDBRemoteCommunicationServerLLGS::Handle_vAttachWait( + StringExtractorGDBRemote &packet) { + return Handle_vAttachWait_or_vAttachOrWait(packet, false, "vAttachWait"); +} + +GDBRemoteCommunication::PacketResult +GDBRemoteCommunicationServerLLGS::Handle_vAttachOrWait( + StringExtractorGDBRemote &packet) { + return Handle_vAttachWait_or_vAttachOrWait(packet, true, "vAttachOrWait"); +} + +GDBRemoteCommunication::PacketResult +GDBRemoteCommunicationServerLLGS::Handle_qVAttachOrWaitSupported( + StringExtractorGDBRemote &packet) { + return SendOKResponse(); +} + +GDBRemoteCommunication::PacketResult +GDBRemoteCommunicationServerLLGS::Handle_vAttachWait_or_vAttachOrWait( + StringExtractorGDBRemote &packet, bool include_existing, + const char *identifier) { + Log *log(GetLogIfAnyCategoriesSet(LIBLLDB_LOG_PROCESS)); + + // Consume the ';' after the identifier. + packet.SetFilePos(strlen(identifier)); + + if (!packet.GetBytesLeft() || packet.GetChar() != ';') + return SendIllFormedResponse( + packet, "vAttachWait/vAttachOrWait missing expected ';'"); + + // Allocate the buffer for the process name from vAttachWait. + std::string process_name; + if (!packet.GetHexByteString(process_name, false)) + return SendIllFormedResponse( + packet, "vAttachWait/vAttachOrWait failed to parse process name"); + + llvm::Optional polling_interval; + llvm::Optional waitfor_duration; + + while (packet.GetBytesLeft() && *packet.Peek() == ';') { + // Skip the semi-colon. + packet.GetChar(); + + const char action = packet.GetChar(); + switch (action) { + case 'I': { + uint32_t interval = packet.GetU32(UINT32_MAX, 16); + if (interval != UINT32_MAX) { + polling_interval = interval; + } else { + return SendIllFormedResponse(packet, + "vAttachWait/vAttachOrWait failed to " + "parse waitfor polling interval"); + } + break; + } + case 'd': { + uint32_t duration = packet.GetU32(UINT32_MAX, 16); + if (duration != UINT32_MAX) { + waitfor_duration = duration; + } else { + return SendIllFormedResponse( + packet, + "vAttachWait/vAttachOrWait failed to parse waitfor duration"); + } + break; + } + default: + return SendIllFormedResponse(packet, "Unsupported vAttachWait action"); + break; + } + } + + LLDB_LOG(log, "attempting to attach to process named '{0}'", process_name); + + Status error = AttachWaitProcess(process_name, polling_interval, + waitfor_duration, include_existing); + if (error.Fail()) { + LLDB_LOG(log, "failed to attach to process named '{0}': {1}", process_name, + error); + return SendErrorResponse(error); + } + + // Notify we attached by sending a stop packet. + return SendStopReasonForState(m_debugged_process_up->GetState()); +} + GDBRemoteCommunication::PacketResult GDBRemoteCommunicationServerLLGS::Handle_D(StringExtractorGDBRemote &packet) { Log *log(GetLogIfAnyCategoriesSet(LIBLLDB_LOG_PROCESS)); diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp @@ -1184,11 +1184,24 @@ } } else packet.PutCString("vAttachName"); + packet.PutChar(';'); packet.PutBytesAsRawHex8(process_name, strlen(process_name), endian::InlHostByteOrder(), endian::InlHostByteOrder()); + if (attach_info.HasWaitForLaunchInterval()) { + packet.PutChar(';'); + packet.PutChar('I'); + packet.PutHex32(attach_info.GetWaitForLaunchInterval()); + } + + if (attach_info.HasWaitForLaunchDuration()) { + packet.PutChar(';'); + packet.PutChar('d'); + packet.PutHex32(attach_info.GetWaitForLaunchDuration()); + } + m_async_broadcaster.BroadcastEvent( eBroadcastBitAsyncContinue, new EventDataBytes(packet.GetString().data(), packet.GetSize())); diff --git a/lldb/source/Utility/StringExtractor.cpp b/lldb/source/Utility/StringExtractor.cpp --- a/lldb/source/Utility/StringExtractor.cpp +++ b/lldb/source/Utility/StringExtractor.cpp @@ -297,11 +297,11 @@ return bytes_extracted; } -size_t StringExtractor::GetHexByteString(std::string &str) { +size_t StringExtractor::GetHexByteString(std::string &str, bool set_eof_on_fail) { str.clear(); str.reserve(GetBytesLeft() / 2); char ch; - while ((ch = GetHexU8()) != '\0') + while ((ch = GetHexU8(0, set_eof_on_fail)) != '\0') str.append(1, ch); return str.size(); } diff --git a/lldb/test/API/tools/lldb-server/TestGdbRemoteAttachWait.py b/lldb/test/API/tools/lldb-server/TestGdbRemoteAttachWait.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/tools/lldb-server/TestGdbRemoteAttachWait.py @@ -0,0 +1,69 @@ + +import os +from time import sleep + +import gdbremote_testcase +import lldbgdbserverutils +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class TestGdbRemoteAttachWait(gdbremote_testcase.GdbRemoteTestCaseBase): + + mydir = TestBase.compute_mydir(__file__) + + def test_attach_with_vAttachWait(self): + exe = '%s_%d' % (self.testMethodName, os.getpid()) + self.build(dictionary={'EXE': exe}) + self.set_inferior_startup_attach_manually() + + server = self.connect_to_debug_monitor() + self.assertIsNotNone(server) + + + self.add_no_ack_remote_stream() + self.test_sequence.add_log_lines([ + # Do the attach. + "read packet: $vAttachWait;{}#00".format(lldbgdbserverutils.gdbremote_hex_encode_string(exe)), + ], True) + # Run the stream until attachWait. + context = self.expect_gdbremote_sequence() + self.assertIsNotNone(context) + + # Sleep so we're sure that the inferior is launched after we ask for the attach. + sleep(1) + + # Launch the inferior. + inferior = self.launch_process_for_attach( + inferior_args=["sleep:60"], + exe_path=self.getBuildArtifact(exe)) + self.assertIsNotNone(inferior) + self.assertTrue(inferior.pid > 0) + self.assertTrue( + lldbgdbserverutils.process_is_running( + inferior.pid, True)) + + # Make sure the attach succeeded. + self.test_sequence.add_log_lines([ + {"direction": "send", + "regex": r"^\$T([0-9a-fA-F]{2})[^#]*#[0-9a-fA-F]{2}$", + "capture": {1: "stop_signal_hex"}}, + ], True) + self.add_process_info_collection_packets() + + + # Run the stream sending the response.. + context = self.expect_gdbremote_sequence() + self.assertIsNotNone(context) + + # Gather process info response. + process_info = self.parse_process_info_response(context) + self.assertIsNotNone(process_info) + + # Ensure the process id matches what we expected. + pid_text = process_info.get('pid', None) + self.assertIsNotNone(pid_text) + reported_pid = int(pid_text, base=16) + self.assertEqual(reported_pid, inferior.pid) +