Index: lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py =================================================================== --- lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py +++ lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py @@ -588,6 +588,15 @@ # Caller must still call wait_for_stopped. return response + def request_restart(self): + command_dict = { + 'command': 'restart', + 'type': 'request', + } + response = self.send_recv(command_dict) + # Caller must still call wait_for_stopped. + return response + def request_disconnect(self, terminateDebuggee=None): args_dict = {} if terminateDebuggee is not None: Index: lldb/source/API/SBProcess.cpp =================================================================== --- lldb/source/API/SBProcess.cpp +++ lldb/source/API/SBProcess.cpp @@ -769,8 +769,8 @@ bool SBProcess::EventIsProcessEvent(const SBEvent &event) { LLDB_INSTRUMENT_VA(event); - return (event.GetBroadcasterClass() == SBProcess::GetBroadcasterClass()) && - !EventIsStructuredDataEvent(event); + return Process::ProcessEventData::GetEventDataFromEvent(event.get()) != + nullptr; } bool SBProcess::EventIsStructuredDataEvent(const lldb::SBEvent &event) { Index: lldb/test/API/tools/lldb-vscode/restart/Makefile =================================================================== --- /dev/null +++ lldb/test/API/tools/lldb-vscode/restart/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules Index: lldb/test/API/tools/lldb-vscode/restart/TestVSCode_restart.py =================================================================== --- /dev/null +++ lldb/test/API/tools/lldb-vscode/restart/TestVSCode_restart.py @@ -0,0 +1,82 @@ +""" +Test lldb-vscode RestartRequest. +""" + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import line_number +import lldbvscode_testcase + + +class TestVSCode_restart(lldbvscode_testcase.VSCodeTestCaseBase): + + @skipIfWindows + @skipIfRemote + def test_basic_functionality(self): + ''' + Tests the basic restarting functionality: set two breakpoints in + sequence, restart at the second, check that we hit the first one. + ''' + line_A = line_number('main.c', '// breakpoint A') + line_B = line_number('main.c', '// breakpoint B') + + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + [bp_A, bp_B] = self.set_source_breakpoints('main.c', [line_A, line_B]) + + # Verify we hit A, then B. + self.vscode.request_configurationDone() + self.verify_breakpoint_hit([bp_A]) + self.vscode.request_continue() + self.verify_breakpoint_hit([bp_B]) + + # Make sure i has been modified from its initial value of 0. + self.assertEquals(int(self.vscode.get_local_variable_value('i')), + 1234, 'i != 1234 after hitting breakpoint B') + + # Restart then check we stop back at A and program state has been reset. + self.vscode.request_restart() + self.verify_breakpoint_hit([bp_A]) + self.assertEquals(int(self.vscode.get_local_variable_value('i')), + 0, 'i != 0 after hitting breakpoint A on restart') + + + @skipIfWindows + @skipIfRemote + def test_stopOnEntry(self): + ''' + Check that the stopOnEntry setting is still honored after a restart. + ''' + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, stopOnEntry=True) + [bp_main] = self.set_function_breakpoints(['main']) + self.vscode.request_configurationDone() + + # Once the "configuration done" event is sent, we should get a stopped + # event immediately because of stopOnEntry. + stopped_events = self.vscode.wait_for_stopped() + for stopped_event in stopped_events: + if 'body' in stopped_event: + body = stopped_event['body'] + if 'reason' in body: + reason = body['reason'] + self.assertNotEqual( + reason, 'breakpoint', + 'verify stop isn\'t "main" breakpoint') + + # Then, if we continue, we should hit the breakpoint at main. + self.vscode.request_continue() + self.verify_breakpoint_hit([bp_main]) + + # Restart and check that we still get a stopped event before reaching + # main. + self.vscode.request_restart() + stopped_events = self.vscode.wait_for_stopped() + for stopped_event in stopped_events: + if 'body' in stopped_event: + body = stopped_event['body'] + if 'reason' in body: + reason = body['reason'] + self.assertNotEqual( + reason, 'breakpoint', + 'verify stop after restart isn\'t "main" breakpoint') + Index: lldb/test/API/tools/lldb-vscode/restart/TestVSCode_restart_runInTerminal.py =================================================================== --- /dev/null +++ lldb/test/API/tools/lldb-vscode/restart/TestVSCode_restart_runInTerminal.py @@ -0,0 +1,104 @@ +""" +Test lldb-vscode RestartRequest. +""" + +import os +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import line_number +import lldbvscode_testcase + + +class TestVSCode_restart_runInTerminal(lldbvscode_testcase.VSCodeTestCaseBase): + + def isTestSupported(self): + try: + # We skip this test for debug builds because it takes too long + # parsing lldb's own debug info. Release builds are fine. + # Checking the size of the lldb-vscode binary seems to be a decent + # proxy for a quick detection. It should be far less than 1 MB in + # Release builds. + return os.path.getsize(os.environ["LLDBVSCODE_EXEC"]) < 1000000 + except: + return False + + + @skipIfWindows + @skipIfRemote + def test_basic_functionality(self): + ''' + Test basic restarting functionality when the process is running in + a terminal. + ''' + if not self.isTestSupported(): + return + line_A = line_number('main.c', '// breakpoint A') + line_B = line_number('main.c', '// breakpoint B') + + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, runInTerminal=True) + [bp_A, bp_B] = self.set_source_breakpoints('main.c', [line_A, line_B]) + + # Verify we hit A, then B. + self.vscode.request_configurationDone() + self.verify_breakpoint_hit([bp_A]) + self.vscode.request_continue() + self.verify_breakpoint_hit([bp_B]) + + # Make sure i has been modified from its initial value of 0. + self.assertEquals(int(self.vscode.get_local_variable_value('i')), + 1234, 'i != 1234 after hitting breakpoint B') + + # Restart. + self.vscode.request_restart() + + # Finally, check we stop back at A and program state has been reset. + self.verify_breakpoint_hit([bp_A]) + self.assertEquals(int(self.vscode.get_local_variable_value('i')), + 0, 'i != 0 after hitting breakpoint A on restart') + + @skipIfWindows + @skipIfRemote + def test_stopOnEntry(self): + ''' + Check that stopOnEntry works correctly when using runInTerminal. + ''' + if not self.isTestSupported(): + return + line_A = line_number('main.c', '// breakpoint A') + line_B = line_number('main.c', '// breakpoint B') + + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, runInTerminal=True, stopOnEntry=True) + [bp_main] = self.set_function_breakpoints(['main']) + self.vscode.request_configurationDone() + + # When using stopOnEntry, configurationDone doesn't result in a running + # process, we should immediately get a stopped event instead. + stopped_events = self.vscode.wait_for_stopped() + # We should be stopped at the entry point. + for stopped_event in stopped_events: + if 'body' in stopped_event: + body = stopped_event['body'] + if 'reason' in body: + reason = body['reason'] + self.assertNotEqual( + reason, 'breakpoint', + 'verify stop isn\'t a breakpoint') + + # Then, if we continue, we should hit the breakpoint at main. + self.vscode.request_continue() + self.verify_breakpoint_hit([bp_main]) + + # Restart and check that we still get a stopped event before reaching + # main. + self.vscode.request_restart() + stopped_events = self.vscode.wait_for_stopped() + for stopped_event in stopped_events: + if 'body' in stopped_event: + body = stopped_event['body'] + if 'reason' in body: + reason = body['reason'] + self.assertNotEqual( + reason, 'breakpoint', + 'verify stop after restart isn\'t "main" breakpoint') + Index: lldb/test/API/tools/lldb-vscode/restart/main.c =================================================================== --- /dev/null +++ lldb/test/API/tools/lldb-vscode/restart/main.c @@ -0,0 +1,9 @@ +#include + +int main(int argc, char const *argv[], char const *envp[]) { + int i = 0; + printf("Do something\n"); // breakpoint A + printf("Do something else\n"); + i = 1234; + return 0; // breakpoint B +} Index: lldb/tools/lldb-vscode/VSCode.h =================================================================== --- lldb/tools/lldb-vscode/VSCode.h +++ lldb/tools/lldb-vscode/VSCode.h @@ -141,10 +141,17 @@ std::vector exit_commands; std::vector stop_commands; std::vector terminate_commands; + // A copy of the last LaunchRequest or AttachRequest so we can reuse its + // arguments if we get a RestartRequest. + llvm::json::Object last_launch_or_attach_request; lldb::tid_t focus_tid; bool sent_terminated_event; bool stop_at_entry; bool is_attach; + // The process event thread normally responds to process exited events by + // shutting down the entire adapter. When we're restarting, we keep the id of + // the old process here so we can detect this case and keep running. + lldb::pid_t restarting_process_id; bool configuration_done_sent; uint32_t reverse_request_seq; std::map request_handlers; Index: lldb/tools/lldb-vscode/VSCode.cpp =================================================================== --- lldb/tools/lldb-vscode/VSCode.cpp +++ lldb/tools/lldb-vscode/VSCode.cpp @@ -39,8 +39,10 @@ {"swift_catch", "Swift Catch", lldb::eLanguageTypeSwift}, {"swift_throw", "Swift Throw", lldb::eLanguageTypeSwift}}), focus_tid(LLDB_INVALID_THREAD_ID), sent_terminated_event(false), - stop_at_entry(false), is_attach(false), configuration_done_sent(false), - reverse_request_seq(0), waiting_for_run_in_terminal(false), + stop_at_entry(false), is_attach(false), + restarting_process_id(LLDB_INVALID_PROCESS_ID), + configuration_done_sent(false), reverse_request_seq(0), + waiting_for_run_in_terminal(false), progress_event_reporter( [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }) { const char *log_file_path = getenv("LLDBVSCODE_LOG"); Index: lldb/tools/lldb-vscode/lldb-vscode.cpp =================================================================== --- lldb/tools/lldb-vscode/lldb-vscode.cpp +++ lldb/tools/lldb-vscode/lldb-vscode.cpp @@ -458,7 +458,8 @@ // manually send a stopped event in request_configurationDone(...) // so don't send any before then. if (g_vsc.configuration_done_sent) { - // Only report a stopped event if the process was not restarted. + // Only report a stopped event if the process was not + // automatically restarted. if (!lldb::SBProcess::GetRestartedFromEvent(event)) { SendStdOutStdErr(process); SendThreadStoppedEvent(); @@ -468,14 +469,22 @@ case lldb::eStateRunning: g_vsc.WillContinue(); break; - case lldb::eStateExited: { - // Run any exit LLDB commands the user specified in the - // launch.json - g_vsc.RunExitCommands(); - SendProcessExitedEvent(process); - SendTerminatedEvent(); - done = true; - } break; + case lldb::eStateExited: + // When restarting, we can get an "exited" event for the process we + // just killed with the old PID, or even with no PID. In that case + // we don't have to terminate the session. + if (process.GetProcessID() == LLDB_INVALID_PROCESS_ID || + process.GetProcessID() == g_vsc.restarting_process_id) { + g_vsc.restarting_process_id = LLDB_INVALID_PROCESS_ID; + } else { + // Run any exit LLDB commands the user specified in the + // launch.json + g_vsc.RunExitCommands(); + SendProcessExitedEvent(process); + SendTerminatedEvent(); + done = true; + } + break; } } else if ((event_mask & lldb::SBProcess::eBroadcastBitSTDOUT) || (event_mask & lldb::SBProcess::eBroadcastBitSTDERR)) { @@ -1527,7 +1536,7 @@ // The debug adapter supports the RestartRequest. In this case a client // should not implement 'restart' by terminating and relaunching the adapter // but by calling the RestartRequest. - body.try_emplace("supportsRestartRequest", false); + body.try_emplace("supportsRestartRequest", true); // The debug adapter supports 'exceptionOptions' on the // setExceptionBreakpoints request. body.try_emplace("supportsExceptionOptions", true); @@ -1622,6 +1631,71 @@ error.GetCString()); } +// Takes a LaunchRequest object and launches the process, also handling +// runInTerminal if applicable. It doesn't do any of the additional +// initialization and bookkeeping stuff that is needed for `request_launch`. +// This way we can reuse the process launching logic for RestartRequest too. +lldb::SBError LaunchProcess(const llvm::json::Object &request) { + lldb::SBError error; + auto arguments = request.getObject("arguments"); + auto launchCommands = GetStrings(arguments, "launchCommands"); + + // Instantiate a launch info instance for the target. + auto launch_info = g_vsc.target.GetLaunchInfo(); + + // Grab the current working directory if there is one and set it in the + // launch info. + const auto cwd = GetString(arguments, "cwd"); + if (!cwd.empty()) + launch_info.SetWorkingDirectory(cwd.data()); + + // Extract any extra arguments and append them to our program arguments for + // when we launch + auto args = GetStrings(arguments, "args"); + if (!args.empty()) + launch_info.SetArguments(MakeArgv(args).data(), true); + + // Pass any environment variables along that the user specified. + auto envs = GetStrings(arguments, "env"); + if (!envs.empty()) + launch_info.SetEnvironmentEntries(MakeArgv(envs).data(), true); + + auto flags = launch_info.GetLaunchFlags(); + + if (GetBoolean(arguments, "disableASLR", true)) + flags |= lldb::eLaunchFlagDisableASLR; + if (GetBoolean(arguments, "disableSTDIO", false)) + flags |= lldb::eLaunchFlagDisableSTDIO; + if (GetBoolean(arguments, "shellExpandArguments", false)) + flags |= lldb::eLaunchFlagShellExpandArguments; + const bool detachOnError = GetBoolean(arguments, "detachOnError", false); + launch_info.SetDetachOnError(detachOnError); + launch_info.SetLaunchFlags(flags | lldb::eLaunchFlagDebug | + lldb::eLaunchFlagStopAtEntry); + const uint64_t timeout_seconds = GetUnsigned(arguments, "timeout", 30); + + if (GetBoolean(arguments, "runInTerminal", false)) { + if (llvm::Error err = request_runInTerminal(request)) + error.SetErrorString(llvm::toString(std::move(err)).c_str()); + } else if (launchCommands.empty()) { + // Disable async events so the launch will be successful when we return from + // the launch call and the launch will happen synchronously + g_vsc.debugger.SetAsync(false); + g_vsc.target.Launch(launch_info, error); + g_vsc.debugger.SetAsync(true); + } else { + g_vsc.RunLLDBCommands("Running launchCommands:", launchCommands); + // The custom commands might have created a new target so we should use the + // selected target after these commands are run. + g_vsc.target = g_vsc.debugger.GetSelectedTarget(); + // Make sure the process is launched and stopped at the entry point before + // proceeding as the the launch commands are not run using the synchronous + // mode. + error = g_vsc.WaitForProcessToStop(timeout_seconds); + } + return error; +} + // "LaunchRequest": { // "allOf": [ { "$ref": "#/definitions/Request" }, { // "type": "object", @@ -1658,8 +1732,8 @@ // } void request_launch(const llvm::json::Object &request) { g_vsc.is_attach = false; + g_vsc.last_launch_or_attach_request = request; llvm::json::Object response; - lldb::SBError error; FillResponse(request, response); auto arguments = request.getObject("arguments"); g_vsc.init_commands = GetStrings(arguments, "initCommands"); @@ -1667,12 +1741,10 @@ g_vsc.stop_commands = GetStrings(arguments, "stopCommands"); g_vsc.exit_commands = GetStrings(arguments, "exitCommands"); g_vsc.terminate_commands = GetStrings(arguments, "terminateCommands"); - auto launchCommands = GetStrings(arguments, "launchCommands"); std::vector postRunCommands = GetStrings(arguments, "postRunCommands"); g_vsc.stop_at_entry = GetBoolean(arguments, "stopOnEntry", false); const llvm::StringRef debuggerRoot = GetString(arguments, "debuggerRoot"); - const uint64_t timeout_seconds = GetUnsigned(arguments, "timeout", 30); // This is a hack for loading DWARF in .o files on Mac where the .o files // in the debug map of the main executable have relative paths which require @@ -1697,76 +1769,27 @@ return; } - // Instantiate a launch info instance for the target. - auto launch_info = g_vsc.target.GetLaunchInfo(); - - // Grab the current working directory if there is one and set it in the - // launch info. - const auto cwd = GetString(arguments, "cwd"); - if (!cwd.empty()) - launch_info.SetWorkingDirectory(cwd.data()); - - // Extract any extra arguments and append them to our program arguments for - // when we launch - auto args = GetStrings(arguments, "args"); - if (!args.empty()) - launch_info.SetArguments(MakeArgv(args).data(), true); - - // Pass any environment variables along that the user specified. - auto envs = GetStrings(arguments, "env"); - if (!envs.empty()) - launch_info.SetEnvironmentEntries(MakeArgv(envs).data(), true); - - auto flags = launch_info.GetLaunchFlags(); - - if (GetBoolean(arguments, "disableASLR", true)) - flags |= lldb::eLaunchFlagDisableASLR; - if (GetBoolean(arguments, "disableSTDIO", false)) - flags |= lldb::eLaunchFlagDisableSTDIO; - if (GetBoolean(arguments, "shellExpandArguments", false)) - flags |= lldb::eLaunchFlagShellExpandArguments; - const bool detatchOnError = GetBoolean(arguments, "detachOnError", false); - launch_info.SetDetachOnError(detatchOnError); - launch_info.SetLaunchFlags(flags | lldb::eLaunchFlagDebug | - lldb::eLaunchFlagStopAtEntry); - // Run any pre run LLDB commands the user specified in the launch.json g_vsc.RunPreRunCommands(); - if (GetBoolean(arguments, "runInTerminal", false)) { - if (llvm::Error err = request_runInTerminal(request)) - error.SetErrorString(llvm::toString(std::move(err)).c_str()); - } else if (launchCommands.empty()) { - // Disable async events so the launch will be successful when we return from - // the launch call and the launch will happen synchronously - g_vsc.debugger.SetAsync(false); - g_vsc.target.Launch(launch_info, error); - g_vsc.debugger.SetAsync(true); - } else { - g_vsc.RunLLDBCommands("Running launchCommands:", launchCommands); - // The custom commands might have created a new target so we should use the - // selected target after these commands are run. - g_vsc.target = g_vsc.debugger.GetSelectedTarget(); - // Make sure the process is launched and stopped at the entry point before - // proceeding as the the launch commands are not run using the synchronous - // mode. - error = g_vsc.WaitForProcessToStop(timeout_seconds); - } + status = LaunchProcess(request); - if (error.Fail()) { + if (status.Fail()) { response["success"] = llvm::json::Value(false); - EmplaceSafeString(response, "message", std::string(error.GetCString())); + EmplaceSafeString(response, "message", std::string(status.GetCString())); } else { g_vsc.RunLLDBCommands("Running postRunCommands:", postRunCommands); } g_vsc.SendJSON(llvm::json::Value(std::move(response))); - if (g_vsc.is_attach) - SendProcessEvent(Attach); // this happens when doing runInTerminal - else - SendProcessEvent(Launch); - g_vsc.SendJSON(llvm::json::Value(CreateEventObject("initialized"))); + if (!status.Fail()) { + if (g_vsc.is_attach) + SendProcessEvent(Attach); // this happens when doing runInTerminal + else + SendProcessEvent(Launch); + } + g_vsc.SendJSON(CreateEventObject("initialized")); } // "NextRequest": { @@ -1867,6 +1890,102 @@ g_vsc.SendJSON(llvm::json::Value(std::move(response))); } + +// "RestartRequest": { +// "allOf": [ { "$ref": "#/definitions/Request" }, { +// "type": "object", +// "description": "Restarts a debug session. Clients should only call this +// request if the corresponding capability `supportsRestartRequest` is +// true.\nIf the capability is missing or has the value false, a typical +// client emulates `restart` by terminating the debug adapter first and then +// launching it anew.", +// "properties": { +// "command": { +// "type": "string", +// "enum": [ "restart" ] +// }, +// "arguments": { +// "$ref": "#/definitions/RestartArguments" +// } +// }, +// "required": [ "command" ] +// }] +// }, +// "RestartArguments": { +// "type": "object", +// "description": "Arguments for `restart` request.", +// "properties": { +// "arguments": { +// "oneOf": [ +// { "$ref": "#/definitions/LaunchRequestArguments" }, +// { "$ref": "#/definitions/AttachRequestArguments" } +// ], +// "description": "The latest version of the `launch` or `attach` +// configuration." +// } +// } +// }, +// "RestartResponse": { +// "allOf": [ { "$ref": "#/definitions/Response" }, { +// "type": "object", +// "description": "Response to `restart` request. This is just an +// acknowledgement, so no body field is required." +// }] +// }, +void request_restart(const llvm::json::Object &request) { + llvm::json::Object response; + FillResponse(request, response); + // Check if we were in a "launch" session or an "attach" session. + // + // Restarting is not well defined when we started the session by attaching to + // an existing process, because we don't know how the process was started, so + // we don't support it. + // + // Note that when using runInTerminal we're technically attached, but it's an + // implementation detail. The adapter *did* launch the process in response to + // a "launch" command, so we can still stop it and re-run it. This is why we + // don't just check `g_vsc.is_attach`. + if (GetString(g_vsc.last_launch_or_attach_request, "command") == "attach") { + response["success"] = llvm::json::Value(false); + EmplaceSafeString(response, "message", + "Restarting an \"attach\" session is not supported."); + g_vsc.SendJSON(llvm::json::Value(std::move(response))); + return; + } + + // Keep track of the old PID so when we get a "process exited" event from the + // killed process we can detect it and not shut down the whole session. + lldb::SBProcess process = g_vsc.target.GetProcess(); + g_vsc.restarting_process_id = process.GetProcessID(); + + // Stop the current process if necessary. The logic here is similar to + // CommandObjectProcessLaunchOrAttach::StopProcessIfNecessary, except that + // we don't ask the user for confirmation. + g_vsc.debugger.SetAsync(false); + if (process.IsValid()) { + lldb::StateType state = process.GetState(); + if (state != lldb::eStateConnected) { + process.Kill(); + } + // Clear the list of thread ids to avoid sending "thread exited" events + // for threads of the process we are terminating. + g_vsc.thread_ids.clear(); + } + g_vsc.debugger.SetAsync(true); + LaunchProcess(g_vsc.last_launch_or_attach_request); + + // This is normally done after receiving a "configuration done" request. + // Because we're restarting, configuration has already happened so we can + // continue the process right away. + if (g_vsc.stop_at_entry) { + SendThreadStoppedEvent(); + } else { + g_vsc.target.GetProcess().Continue(); + } + + g_vsc.SendJSON(llvm::json::Value(std::move(response))); +} + // "ScopesRequest": { // "allOf": [ { "$ref": "#/definitions/Request" }, { // "type": "object", @@ -3084,6 +3203,7 @@ g_vsc.RegisterRequestCallback("launch", request_launch); g_vsc.RegisterRequestCallback("next", request_next); g_vsc.RegisterRequestCallback("pause", request_pause); + g_vsc.RegisterRequestCallback("restart", request_restart); g_vsc.RegisterRequestCallback("scopes", request_scopes); g_vsc.RegisterRequestCallback("setBreakpoints", request_setBreakpoints); g_vsc.RegisterRequestCallback("setExceptionBreakpoints",