Redo of Add terminateCommands to lldb-vscode protocol

Summary:
This redoes https://reviews.llvm.org/D79726 and fixes two things.
- The logic that determines whether to automatically disconnect during the tear down is not very dumb compared to the original implementation. Each test will determine whether to do that or not.
- The terminate commands and terminate event were being sent after the disconnect response was sent to the IDE. That was not good, as VSCode stops the debug session as soon as it receives a disconnect response. Now, the terminate event and terminateEvents are being executed before the disconnect response is sent. This ensures that any connection between the IDE and lldb-vscode is alive while the terminate commands are executed. Besides, it also allows displaying the output of the terminate commands on the debug console, as it's still alive.

Reviewers: clayborg, aadsm, kusmour, labath

Subscribers: lldb-commits

Tags: #lldb

Differential Revision: https://reviews.llvm.org/D81978
This commit is contained in:
Walter Erquinigo 2020-06-15 14:08:52 -07:00
parent 33ece57241
commit 74ab1da028
8 changed files with 131 additions and 26 deletions

View File

@ -179,6 +179,9 @@ class VSCodeTestCaseBase(TestBase):
def get_console(self, timeout=0.0):
return self.vscode.get_output('console', timeout=timeout)
def collect_console(self, duration):
return self.vscode.collect_output('console', duration=duration)
def get_local_as_int(self, name, threadId=None):
value = self.vscode.get_local_variable_value(name, threadId=threadId)
if value.startswith('0x'):
@ -239,7 +242,8 @@ class VSCodeTestCaseBase(TestBase):
def attach(self, program=None, pid=None, waitFor=None, trace=None,
initCommands=None, preRunCommands=None, stopCommands=None,
exitCommands=None, attachCommands=None, coreFile=None, disconnectAutomatically=True):
exitCommands=None, attachCommands=None, coreFile=None,
disconnectAutomatically=True, terminateCommands=None):
'''Build the default Makefile target, create the VSCode debug adaptor,
and attach to the process.
'''
@ -258,7 +262,8 @@ class VSCodeTestCaseBase(TestBase):
program=program, pid=pid, waitFor=waitFor, trace=trace,
initCommands=initCommands, preRunCommands=preRunCommands,
stopCommands=stopCommands, exitCommands=exitCommands,
attachCommands=attachCommands, coreFile=coreFile)
attachCommands=attachCommands, terminateCommands=terminateCommands,
coreFile=coreFile)
if not (response and response['success']):
self.assertTrue(response['success'],
'attach failed (%s)' % (response['message']))
@ -267,15 +272,17 @@ class VSCodeTestCaseBase(TestBase):
stopOnEntry=False, disableASLR=True,
disableSTDIO=False, shellExpandArguments=False,
trace=False, initCommands=None, preRunCommands=None,
stopCommands=None, exitCommands=None,sourcePath=None,
debuggerRoot=None, launchCommands=None, sourceMap=None):
stopCommands=None, exitCommands=None, terminateCommands=None,
sourcePath=None, debuggerRoot=None, launchCommands=None,
sourceMap=None, disconnectAutomatically=True):
'''Sending launch request to vscode
'''
# Make sure we disconnect and terminate the VSCode debug adapter,
# if we throw an exception during the test case
def cleanup():
self.vscode.request_disconnect(terminateDebuggee=True)
if disconnectAutomatically:
self.vscode.request_disconnect(terminateDebuggee=True)
self.vscode.terminate()
# Execute the cleanup function during test case tear down.
@ -297,6 +304,7 @@ class VSCodeTestCaseBase(TestBase):
preRunCommands=preRunCommands,
stopCommands=stopCommands,
exitCommands=exitCommands,
terminateCommands=terminateCommands,
sourcePath=sourcePath,
debuggerRoot=debuggerRoot,
launchCommands=launchCommands,
@ -310,7 +318,8 @@ class VSCodeTestCaseBase(TestBase):
disableSTDIO=False, shellExpandArguments=False,
trace=False, initCommands=None, preRunCommands=None,
stopCommands=None, exitCommands=None,
sourcePath=None, debuggerRoot=None):
terminateCommands=None, sourcePath=None,
debuggerRoot=None):
'''Build the default Makefile target, create the VSCode debug adaptor,
and launch the process.
'''
@ -320,4 +329,4 @@ class VSCodeTestCaseBase(TestBase):
self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
disableSTDIO, shellExpandArguments, trace,
initCommands, preRunCommands, stopCommands, exitCommands,
sourcePath, debuggerRoot)
terminateCommands, sourcePath, debuggerRoot)

View File

@ -10,6 +10,7 @@ import string
import subprocess
import sys
import threading
import time
def dump_memory(base_addr, data, num_per_line, outfile):
@ -148,6 +149,15 @@ class DebugCommunication(object):
self.output_condition.release()
return output
def collect_output(self, category, duration, clear=True):
end_time = time.time() + duration
collected_output = ""
while end_time > time.time():
output = self.get_output(category, timeout=0.25, clear=clear)
if output:
collected_output += output
return collected_output if collected_output else None
def enqueue_recv_packet(self, packet):
self.recv_condition.acquire()
self.recv_packets.append(packet)
@ -450,7 +460,8 @@ class DebugCommunication(object):
def request_attach(self, program=None, pid=None, waitFor=None, trace=None,
initCommands=None, preRunCommands=None,
stopCommands=None, exitCommands=None,
attachCommands=None, coreFile=None):
attachCommands=None, terminateCommands=None,
coreFile=None):
args_dict = {}
if pid is not None:
args_dict['pid'] = pid
@ -469,6 +480,8 @@ class DebugCommunication(object):
args_dict['stopCommands'] = stopCommands
if exitCommands:
args_dict['exitCommands'] = exitCommands
if terminateCommands:
args_dict['terminateCommands'] = terminateCommands
if attachCommands:
args_dict['attachCommands'] = attachCommands
if coreFile:
@ -571,7 +584,8 @@ class DebugCommunication(object):
stopOnEntry=False, disableASLR=True,
disableSTDIO=False, shellExpandArguments=False,
trace=False, initCommands=None, preRunCommands=None,
stopCommands=None, exitCommands=None, sourcePath=None,
stopCommands=None, exitCommands=None,
terminateCommands=None ,sourcePath=None,
debuggerRoot=None, launchCommands=None, sourceMap=None):
args_dict = {
'program': program
@ -601,6 +615,8 @@ class DebugCommunication(object):
args_dict['stopCommands'] = stopCommands
if exitCommands:
args_dict['exitCommands'] = exitCommands
if terminateCommands:
args_dict['terminateCommands'] = terminateCommands
if sourcePath:
args_dict['sourcePath'] = sourcePath
if debuggerRoot:
@ -905,7 +921,8 @@ def run_vscode(dbg, args, options):
initCommands=options.initCmds,
preRunCommands=options.preRunCmds,
stopCommands=options.stopCmds,
exitCommands=options.exitCmds)
exitCommands=options.exitCmds,
terminateCommands=options.terminateCmds)
else:
response = dbg.request_launch(options.program,
args=args,
@ -916,7 +933,8 @@ def run_vscode(dbg, args, options):
initCommands=options.initCmds,
preRunCommands=options.preRunCmds,
stopCommands=options.stopCmds,
exitCommands=options.exitCmds)
exitCommands=options.exitCmds,
terminateCommands=options.terminateCmds)
if response['success']:
if options.sourceBreakpoints:
@ -1089,6 +1107,15 @@ def main():
help=('Specify a LLDB command that will be executed when the process '
'exits. Can be specified more than once.'))
parser.add_option(
'--terminateCommand',
type='string',
action='append',
dest='terminateCmds',
default=[],
help=('Specify a LLDB command that will be executed when the debugging '
'session is terminated. Can be specified more than once.'))
parser.add_option(
'--env',
type='string',

View File

@ -43,7 +43,6 @@ class TestVSCode_attach(lldbvscode_testcase.VSCodeTestCaseBase):
if continueToExit:
self.continue_to_exit()
@skipIfWindows
@skipIfNetBSD # Hangs on NetBSD as well
@skipIfRemote
@ -121,8 +120,8 @@ class TestVSCode_attach(lldbvscode_testcase.VSCodeTestCaseBase):
def test_commands(self):
'''
Tests the "initCommands", "preRunCommands", "stopCommands",
"exitCommands", and "attachCommands" that can be passed during
attach.
"exitCommands", "terminateCommands" and "attachCommands"
that can be passed during attach.
"initCommands" are a list of LLDB commands that get executed
before the targt is created.
@ -136,6 +135,8 @@ class TestVSCode_attach(lldbvscode_testcase.VSCodeTestCaseBase):
must have a valid process in the selected target in LLDB after
they are done executing. This allows custom commands to create any
kind of debug session.
"terminateCommands" are a list of LLDB commands that get executed when
the debugger session terminates.
'''
self.build_and_create_debug_adaptor()
program = self.getBuildArtifact("a.out")
@ -150,13 +151,14 @@ class TestVSCode_attach(lldbvscode_testcase.VSCodeTestCaseBase):
preRunCommands = ['image list a.out', 'image dump sections a.out']
stopCommands = ['frame variable', 'bt']
exitCommands = ['expr 2+3', 'expr 3+4']
terminateCommands = ['expr 4+2']
self.attach(program=program,
attachCommands=attachCommands,
initCommands=initCommands,
preRunCommands=preRunCommands,
stopCommands=stopCommands,
exitCommands=exitCommands)
exitCommands=exitCommands,
terminateCommands=terminateCommands)
# Get output from the console. This should contain both the
# "initCommands" and the "preRunCommands".
output = self.get_console()
@ -187,5 +189,35 @@ class TestVSCode_attach(lldbvscode_testcase.VSCodeTestCaseBase):
self.continue_to_exit()
# Get output from the console. This should contain both the
# "exitCommands" that were run after the second breakpoint was hit
output = self.get_console(timeout=1.0)
# and the "terminateCommands" due to the debugging session ending
output = self.collect_console(duration=1.0)
self.verify_commands('exitCommands', output, exitCommands)
self.verify_commands('terminateCommands', output, terminateCommands)
@skipIfWindows
@skipIfNetBSD # Hangs on NetBSD as well
def test_terminate_commands(self):
'''
Tests that the "terminateCommands", that can be passed during
attach, are run when the debugger is disconnected.
'''
self.build_and_create_debug_adaptor()
program = self.getBuildArtifact("a.out")
# Here we just create a target and launch the process as a way to test
# if we are able to use attach commands to create any kind of a target
# and use it for debugging
attachCommands = [
'target create -d "%s"' % (program),
'process launch'
]
terminateCommands = ['expr 4+2']
self.attach(program=program,
attachCommands=attachCommands,
terminateCommands=terminateCommands,
disconnectAutomatically=False)
self.get_console()
# Once it's disconnected the console should contain the
# "terminateCommands"
self.vscode.request_disconnect(terminateDebuggee=True)
output = self.collect_console(duration=1.0)
self.verify_commands('terminateCommands', output, terminateCommands)

View File

@ -294,8 +294,9 @@ class TestVSCode_launch(lldbvscode_testcase.VSCodeTestCaseBase):
@skipIfRemote
def test_commands(self):
'''
Tests the "initCommands", "preRunCommands", "stopCommands" and
"exitCommands" that can be passed during launch.
Tests the "initCommands", "preRunCommands", "stopCommands",
"terminateCommands" and "exitCommands" that can be passed during
launch.
"initCommands" are a list of LLDB commands that get executed
before the targt is created.
@ -305,17 +306,21 @@ class TestVSCode_launch(lldbvscode_testcase.VSCodeTestCaseBase):
time the program stops.
"exitCommands" are a list of LLDB commands that get executed when
the process exits
"terminateCommands" are a list of LLDB commands that get executed when
the debugger session terminates.
'''
program = self.getBuildArtifact("a.out")
initCommands = ['target list', 'platform list']
preRunCommands = ['image list a.out', 'image dump sections a.out']
stopCommands = ['frame variable', 'bt']
exitCommands = ['expr 2+3', 'expr 3+4']
terminateCommands = ['expr 4+2']
self.build_and_launch(program,
initCommands=initCommands,
preRunCommands=preRunCommands,
stopCommands=stopCommands,
exitCommands=exitCommands)
exitCommands=exitCommands,
terminateCommands=terminateCommands)
# Get output from the console. This should contain both the
# "initCommands" and the "preRunCommands".
@ -354,8 +359,10 @@ class TestVSCode_launch(lldbvscode_testcase.VSCodeTestCaseBase):
self.continue_to_exit()
# Get output from the console. This should contain both the
# "exitCommands" that were run after the second breakpoint was hit
output = self.get_console(timeout=1.0)
# and the "terminateCommands" due to the debugging session ending
output = self.collect_console(duration=1.0)
self.verify_commands('exitCommands', output, exitCommands)
self.verify_commands('terminateCommands', output, terminateCommands)
@skipIfWindows
@skipIfRemote
@ -420,3 +427,23 @@ class TestVSCode_launch(lldbvscode_testcase.VSCodeTestCaseBase):
# "exitCommands" that were run after the second breakpoint was hit
output = self.get_console(timeout=1.0)
self.verify_commands('exitCommands', output, exitCommands)
@skipIfWindows
@skipIfNetBSD # Hangs on NetBSD as well
def test_terminate_commands(self):
'''
Tests that the "terminateCommands", that can be passed during
launch, are run when the debugger is disconnected.
'''
self.build_and_create_debug_adaptor()
program = self.getBuildArtifact("a.out")
terminateCommands = ['expr 4+2']
self.launch(program=program,
terminateCommands=terminateCommands)
self.get_console()
# Once it's disconnected the console should contain the
# "terminateCommands"
self.vscode.request_disconnect(terminateDebuggee=True)
output = self.collect_console(duration=1.0)
self.verify_commands('terminateCommands', output, terminateCommands)

View File

@ -16,14 +16,14 @@
The `lldb-vscode` tool creates a command line tool that implements the [Visual
Studio Code Debug API](https://code.visualstudio.com/docs/extensionAPI/api-debugging).
It can be installed as an extension for the Visual Studio Code and Nuclide IDE.
It can be installed as an extension for the Visual Studio Code and Nuclide IDE.
The protocol is easy to run remotely and also can allow other tools and IDEs to
get a full featured debugger with a well defined protocol.
get a full featured debugger with a well defined protocol.
# Installation for Visual Studio Code
Installing the plug-in involves creating a directory in the `~/.vscode/extensions` folder and copying the package.json file that is in the same directory as this
documentation into it, and copying to symlinking a lldb-vscode binary into
documentation into it, and copying to symlinking a lldb-vscode binary into
the `bin` directory inside the plug-in directory.
If you want to make a stand alone plug-in that you can send to others on unix systems:
@ -86,6 +86,7 @@ file that defines how your program will be run. The JSON configuration file can
|**preRunCommands** |[string]| | LLDB commands executed just before launching after the LLDB target has been created. Commands and command output will be sent to the debugger console when they are executed.
|**stopCommands** |[string]| | LLDB commands executed just after each stop. Commands and command output will be sent to the debugger console when they are executed.
|**exitCommands** |[string]| | LLDB commands executed when the program exits. Commands and command output will be sent to the debugger console when they are executed.
|**terminateCommands** |[string]| | LLDB commands executed when the debugging session ends. Commands and command output will be sent to the debugger console when they are executed.
|**sourceMap** |[string[2]]| | Specify an array of path re-mappings. Each element in the array must be a two element array containing a source and destination pathname.
|**debuggerRoot** | string| |Specify a working directory to use when launching lldb-vscode. If the debug information in your executable contains relative paths, this option can be used so that `lldb-vscode` can find source files and object files that have relative paths.
@ -112,6 +113,7 @@ The JSON configuration file can contain the following `lldb-vscode` specific lau
|**preRunCommands** |[string]| | LLDB commands executed just before launching after the LLDB target has been created. Commands and command output will be sent to the debugger console when they are executed.
|**stopCommands** |[string]| | LLDB commands executed just after each stop. Commands and command output will be sent to the debugger console when they are executed.
|**exitCommands** |[string]| | LLDB commands executed when the program exits. Commands and command output will be sent to the debugger console when they are executed.
|**terminateCommands** |[string]| | LLDB commands executed when the debugging session ends. Commands and command output will be sent to the debugger console when they are executed.
|**attachCommands** |[string]| | LLDB commands that will be executed after **preRunCommands** which take place of the code that normally does the attach. The commands can create a new target and attach or launch it however desired. This allows custom launch and attach configurations. Core files can use `target create --core /path/to/core` to attach to core files.

View File

@ -309,6 +309,10 @@ void VSCode::RunExitCommands() {
RunLLDBCommands("Running exitCommands:", exit_commands);
}
void VSCode::RunTerminateCommands() {
RunLLDBCommands("Running terminateCommands:", terminate_commands);
}
lldb::SBTarget VSCode::CreateTargetFromArguments(
const llvm::json::Object &arguments,
lldb::SBError &error) {

View File

@ -86,6 +86,7 @@ struct VSCode {
std::vector<std::string> pre_run_commands;
std::vector<std::string> exit_commands;
std::vector<std::string> stop_commands;
std::vector<std::string> terminate_commands;
lldb::tid_t focus_tid;
bool sent_terminated_event;
bool stop_at_entry;
@ -133,6 +134,7 @@ struct VSCode {
void RunPreRunCommands();
void RunStopCommands();
void RunExitCommands();
void RunTerminateCommands();
/// Create a new SBTarget object from the given request arguments.
/// \param[in] arguments

View File

@ -174,6 +174,7 @@ void SendThreadExitedEvent(lldb::tid_t tid) {
void SendTerminatedEvent() {
if (!g_vsc.sent_terminated_event) {
g_vsc.sent_terminated_event = true;
g_vsc.RunTerminateCommands();
// Send a "terminated" event
llvm::json::Object event(CreateEventObject("terminated"));
g_vsc.SendJSON(llvm::json::Value(std::move(event)));
@ -530,6 +531,7 @@ void request_attach(const llvm::json::Object &request) {
g_vsc.pre_run_commands = GetStrings(arguments, "preRunCommands");
g_vsc.stop_commands = GetStrings(arguments, "stopCommands");
g_vsc.exit_commands = GetStrings(arguments, "exitCommands");
g_vsc.terminate_commands = GetStrings(arguments, "terminateCommands");
auto attachCommands = GetStrings(arguments, "attachCommands");
llvm::StringRef core_file = GetString(arguments, "coreFile");
g_vsc.stop_at_entry =
@ -775,7 +777,6 @@ void request_disconnect(const llvm::json::Object &request) {
GetBoolean(arguments, "terminateDebuggee", defaultTerminateDebuggee);
lldb::SBProcess process = g_vsc.target.GetProcess();
auto state = process.GetState();
switch (state) {
case lldb::eStateInvalid:
case lldb::eStateUnloaded:
@ -797,8 +798,8 @@ void request_disconnect(const llvm::json::Object &request) {
g_vsc.debugger.SetAsync(true);
break;
}
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
SendTerminatedEvent();
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
if (g_vsc.event_thread.joinable()) {
g_vsc.broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread);
g_vsc.event_thread.join();
@ -1368,6 +1369,7 @@ void request_launch(const llvm::json::Object &request) {
g_vsc.pre_run_commands = GetStrings(arguments, "preRunCommands");
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");
g_vsc.stop_at_entry = GetBoolean(arguments, "stopOnEntry", false);
const llvm::StringRef debuggerRoot = GetString(arguments, "debuggerRoot");