Saturday, May 12, 2018

Howto launch and debug in VSCode using the debug adapter protocol (part 2)

Ok, after the basic infracstructure, the next thing to do is actually launch some program without worrying about the debugger, so, we'll just run a program without being in debug mode to completion, show its output and terminate it when requested.

To launch a program, our debug adapter must treat the 'LaunchRequest' message and actually run the program (bear in mind that we'll just launch it without doing any debugging at this point).

The first point then is how to actually launch it. We provided options for the debugger to be launched with different console arguments specifying where to launch it (either just showing output to the debug console, using the integrated terminal or using an external terminal).

So, let's start with just showing output in the debug console.

Launching it should be simple: just generate a command line while treating the 'LaunchRequest' message, but then, based on the console specified some things may be different...

Let's start handling just showing the output on the debug console.

To do that we have launch the command properly redirecting the output to pipes (for python it's something as subprocess.Popen([sys.executable, '-u', file_to_run], stdout=subprocess.PIPE, stderr=subprocess.PIPE) and then create threads which will read that output to provide it back to vscode (so, when output is obtained, an OutputEvent must be given).

Also, create another thread so that when the process finishes, a TerminatedEvent is given (in python, just do a popen.wait() in a thread and when complete send the TerminatedEvent -- you may want to synchronize to make sure other threads related to output have finished before doing that).

At this point, we can run something and should be able to see anything printed both to stdout and stderr and when the process finishes, VSCode itself acknowledges that and closes the related controls.

Great! On to launching in the integrated terminal!

So, to launch in the terminal we have to first actually check if the client does support running in the terminal... in the InitializeRequest, if it is supported, we should've received in the arguments "supportsRunInTerminalRequest": True (if it doesn't, in my case I just fall back to the debug console).

This also becomes a little bit trickier because at this point we're the ones doing the request (RunInTerminalRequest) and the client should send a response (RunInTerminalResponse). So, on to it: when the client launches, create a RunInTerminalRequest with the proper kind ('internal' or 'external') and wait for the response.

At this point, the processId may not actually be available after launching in that mode (the RunInTerminalResponse processId is optional), which means that if we didn't really create a debugger (just a simple run), we're blind... we could do another program to launch it and return the pid to be able to notify that it was stopped and to kill it when needed, but this seems a bit overkill for me and I couldn't find any info on the proper behavior here, so, I decided that when the user chooses that mode with 'noDebug' I'll simply notify that the debug session is finished for the adapter with a TerminatedEvent (and the user can see the output and Ctrl+C it in the actual terminal).

As a note the 'noDebug' option is added behind the scenes by VSCode depending on whether the user has chosen to do a debug or run for the selected launch (so, it shouldn't be a part of the declared configuration in the extension).

Now, thinking a bit more about it, there's a caveat: when launching with the redirection to the debug console, we should treat sending to stdin too (we don't want to create a process he can't do any communication with later on).

To do that in 'noDebug' should be simple... when we receive an 'EvaluateRequest', we'll send it to stdin (when actually in debug mode we probably have to check the current debugger state to determine if we want to do an evaluation or send to stdin -- i.e.: if we are stopped in a breakpoint we may want to evaluate and not send to stdin).

As a note, after playing with it more I renamed the "console" option to "terminal" with options "none", "internal", "external" as I think that's a better representation of what's expected.

So, that's it for part 2: we're launching a program and redirecting the output as requested by the user (albeit without actually debugging it for now).

No comments: