Remote debugging with VSCode, Docker and Pico

Marco Chiappetta
Hipo
Published in
4 min readJun 7, 2018

--

I’m currently part of the awesome team working on Fieldguide, here at Hipo, where we use Pico as our microframework of choice and Docker to manage our local development environment.

After finding out that VSCode supports Django/Flask debugging out of the box I was eager to find a way to make it work with our local development setup. Fortunately VSCode also supports remote debugging using ptvsd, a Python debugger developed by Microsoft for the original Visual Studio.

The final result looks great:

How remote debugging looks like on VSCode.

The setup

First of all we need to install ptvsd and integrate it into our app as described in the tutorial.

Usually this involves a pip install ptvsd and adding these lines somewhere in your application:

Note: If you’re on Docker you’ll also need to expose the port in your Dockerfile or docker-compose.yaml.

We then need to configure VSCode to attach to the debugger, we can do this by creating a new debugging configuration:

  • Click on the “Debug” tab
  • Click on the dropdown next to “DEBUG” and select “Add configuration…”
  • Select “Python: Attach” as template
  • Customize the configuration as needed. In our case the project root in the Docker container was /fieldguide.

The only thing left now is to start the application and start setting breakpoints everywhere!

If this looks too good to be true it’s because, well, it is.

Address in use

I immediately ran into an issue related with the port being used by “another” process:

As it turns out ptvsd.enable_attach() was being called twice. I wasn't entirely sure why yet and I really wanted to test the debugger on a "real" project so I decided to go with a quick hack: create a temporary file and start the debugger only if that file does not exist.

The application was finally running and the debugger was clearly accepting connections, as tested with netcat:

$ nc localhost 3000 
PTVSDBG

Time to debug!

Not so fast…

Debug adapter process has terminated unexpectedly

I tried multiple times but kept getting this nasty error:

After some Googling I found out I had to install version 3.0.0 (the latest stable at the time of writing is 3.2.1). After getting it to work once it suddenly stopped working again and it was either showing the nasty error or just plain hanging.

Note: if VSCode hangs while connecting to the debugger (i.e. the thin blue loading bar keeps looping) you don’t need to restart the editor. Saving any file will terminate the connection attempt.

I dug in the ptvsd source code until I found this snippet:

For some reason that thread was hung. Then it dawned on me: the damn reloader!

Pico is based on Werkzeug, a library that abstracts “just enough” of the WSGI specification details (we use uWSGI as the implementation).

Werkzeug allows to start an app with the “autoreload” feature. Whenever there’s a change in the source, the server will pick up the changes and reload itself. When the autoreload feature is enabled Werkzeug always keeps 2 processes alive: the watcher and the actual application. Turns out ptvsd was running as a thread of the watcher, not of the application. I'm not entirely sure why this would be a problem (since the watcher runs the application code as well) but it clearly was.

The solution to all problems

The solution to both the problem above and the double call to ptvsd.enable_attach was to simply run the debugger only within the reloaded instance of Werkzeug, not the watcher. To do so I wrote a simple function that does the following:

  • Get the parent process’ PID
  • Get the PIDs of all Python processes involved with the app
  • If the parent’s PID is within these PIDs it means it’s the Werkzeug watcher and the current process is the reloaded application process, therefore we can start the debugger (the old thread was terminated together with the old application process)

Note that ENABLE_DEBUGGER_PARAM is just some parameter you pass to your application to enable remote debugging, it might as well be the name of the file or what have you. It just makes it easier to pinpoint the processes involved with our app.

This finally made running and attaching to the debugger consistently work.

UPDATE: turns out there is an easier way of doing this. Thanks Jonathan Stiansen for letting me know!

Shortcomings

The only shortcoming I could find so far is that, since the debugger server is restarted every time Werkzeug reloads the application, VSCode needs to be re-attached every time. This isn’t a big deal but it can get annoying in the long run.

--

--