JavaScript Debugging

As you probably already know GNOME Shell is a mix of C/C++ and JavaScript code. In case of C/C++ language there are a plenty of different choices when it comes to native debuggers, but as has been said in the following article Projects/GnomeShell/Debugging, the most common used is definitely GDB, at last if you are stuck with GNU toolchain. It's also a console based solution what can be really helpful when pausing the whole GNOME Shell, because you can use CTRL+ALT+F1-F12 just to switch directly to a console and debug GNOME Shell using GDB. Therefore, everything is absolutely fine in case of C/C++ code, it can be easily debugged and the process is relatively simple as long as you are familiar with C/C++ development.

Situation becomes a bit more complicated when you are about to debug the JavaScript based parts. There are a lot of JavaScript engines on the market. They share the same specification when it comes to the language implementation, but there is no standard which would describe a kind of common debugging API or anything like that. Most frequently, the engine merely provides a set of very low level facilities more or less similar to those exposed directly by processors, that can be then used to implement higher level debuggers like FireBug in case of Firfox Browser. This is why most of applications embedding SpiderMonkey engine don't provide any debuggers. Just because they would need to implement the whole logic from scratch basing on the mentioned low level debugging API.

GNOME Shell and GJS don't do so as well. It's why it is not possible to debug GNOME Shell internals as well as standalone applications executed directly by GJS.

JavaScript Remote Debugger

There is JSRDbg project which provides high level debugger implementation for SpiderMonkey engine. It can be used to integrate a JavaScript debugger with GNOME Shell and GJS by exposing high level debugging API through a TCP/IP connection. There is also a console based client similar to GDB, which can be used to connect to the API. Given the fact that you will need to switch to a native console in order to debug GNOME Shell it has great advantages over standard GUI based solutions.

It's not a part of GNOME 3 environment, so as long as you use distribution other than Gentoo GNU/Linux, you have to do it on your own. Anyway the process is relative easy and shouldn't be problematic especially for developers who are working with the GNOME code on a daily basis.

The following page describes how to install and configure JSRDbg on Gentoo: JSRDbg Gentoo Portage Overlay

If you use different GNU/Linux distribution and you would like to integrate the library by yourself, you will also find useful links to the necessary patches there.

Head over to the official JSRDbg documentation for more details: JSRDbg - Official Site

How to use it in practice

If you would like to dig deep into the project, head over to the official manual. This chapter shows how to start using the debugger in practice, with as less effort as possible.

GJS

First of all download the last code sample for the following article and make sure it works correctly: GJS Tutorial

Then run GJS in a debugger mode:

$ gjs -D example.js

You should see the following text on the console and the application should start with a window which gives you cookies :-).

$ gjs -D example.js
Debugger is listening on port: 8089

If you see it, it means that your GJS started a debugger instance at port 8089, which is the default one for JSRDbg. We haven't used '-S' option to start the application suspended, so it started as soon as GJS started evaluating the code. OK, let's connect to the port with the standard console client and play a bit with the application. First of all just type "jrdb". It doesn't need any special arguments as long as you use the default 8089 port.

$ jrdb 
JavaScript Remote Debugger Client connected to a remote debugger.
Waiting for a list of JavaScript contexts being debugged.
Type "help context" for more information.
Available JSContext instances: 
0) gjs-console (RUNNING)
There is only one JSContext managed by the debugger, so it has been chosen as 
the current one.
You can change it using the "context" command.
jrdb> 

The client has informed us that it managed to connect to a debugger instance at given port and found one running context, which has been chosen as the current one then. Type "help context" for more details about contexts. It's crucial to understand them well.

Let's try to type "source" command which should display list of scripts managed by the debugger.

jrdb> source
jrdb> 

What the hell, nothing happened...

The whole communication is completely asynchronous and it can be a source of many confusions if you do not understand how everything work here, so let's start with a bit of theory first.

The debugger needs to communicate a working SpiderMonkey engine to complete almost every command exposed by the protocol. Every SpiderMonkey JSContext works on its own dedicated thread. The same thread is also used to execute debugger's code when needed, but there is nothing like context switching well known from preemptive multitasking for instance. Debugger has to wait patiently for its turn. It's so called cooperative multitasking. Everything works fluently as expected as long as the SpiderMonkey engine executes its byte code continuously, because it can switch to the debugger code periodically in order to complete queued commands. The problem shows itself when a native function called from the JavaScript code hangs on a system call like "sleep" or a whole application is event based one like GTK applications for instance and the event loop is handled natively. In such a case queued commands cannot be handled immediately, but need to wait their turn. This behavior can be only observed when you play with a running application. When an application is paused you have the mentioned thread all for you. Even if such a model has a lot of advantages especially concerning the ease of maintenance and the overall simplicity of solutions based on it, it can be real painful to implement multitasking algorithms based on fairness. Despite these remarks, it's how JavaScript engines work, so we have to get used to it and be aware of all consequences.

Given the above explanation, let's try to pause the application then:

jrdb> source
jrdb> pause
jrdb> 

As you can see still nothing happened there, but for now you should be able to explain why. The command you have just sent to the debugger has been queued and waits for the SpiderMonkey engine time. The application is running GTK+ event loop, so we have to do something to get back to the SpiderMonkey engine.

So let's play with the application a bit.

I order to do so, type "cookie" in the text book and click "Get a cookie". It should execute some JavaScript code to produce a cookie for us and in fact execute the debugger code by the way.

jrdb> source
jrdb> pause
example.js
resource:///org/gnome/gjs/modules/overrides/GLib.js
resource:///org/gnome/gjs/modules/overrides/Gtk.js
resource:///org/gnome/gjs/modules/overrides/GObject.js
resource:///org/gnome/gjs/modules/lang.js
resource:///org/gnome/gjs/modules/overrides/Gio.js
resource:///org/gnome/gjs/modules/signals.js
example.js
87             this._cookieLabel.set_label ("Number of cookies: " + cookies);
jrdb> 

The client is also fully asynchronous, so it has got responses for the two pending commands that have just been executed by the debugger. The list of scripts used by the JavaScript context being debugged and a response to the "pause" command - script, line number and line of the source code from the script where we are currently paused.

OK, it seems that we are paused at line 87, therefore let's take a look at the source code by typing "source example.js" in the command prompt. This command prints source code of the script given in arguments.

80     _getACookie: function() {
81 
82         // Did you spell "cookie" correctly?
83         if ((this._spellCookie.get_text()).toLowerCase() == "cookie") {
84 
85             // Increase the number of cookies by 1 and update the label
86             cookies++;
87             this._cookieLabel.set_label ("Number of cookies: " + cookies);
88 
89         }
90 
91     }

Seeing a bigger part of the source code a perceptive reader may ask: "Why the hell have we paused at the line 87, but not 83 which in fact is the fist line in our callback function?". It's not a bug or anything like that. It's just how SpiderMonkey works with its cooperative multitasking. We do not have any control about when SpiderMonkey executes our queued commands. It does it just as soon as possible, so it might take few instructions before they get called. Bear in mind that this behavior exists only in case of RUNNING code. When application is paused we have the whole engine for the debugger, so all commands are handled on the fly.

OK, we are paused, that's great...but we would like to debug this function from the beginning. The best way to do it, is just to register a new breakpoint at line 83.

jrdb> break example.js 83
Breakpoint 0: script example.js, 83.
jrdb> 

We are still paused here, so the command has been executed immediately. Breakpoint has been registered; therefore we can go further and allow the application to continue in order to get the possibility to hit the button again.

jrdb> c
jrdb> 

Now, go to the application and click on the "Gimme a cookie" button again. Just after releasing the button you should see an information that the application has been paused again, but this time at our line 83.

83         if ((this._spellCookie.get_text()).toLowerCase() == "cookie") {
jrdb> 

Let's turn on a source code animation which shows a bigger piece of the source code when we are stepping around and let's step through the function again. It will show us one another strange behavior you have to be definitely aware of in order to keep your sanity :-).

picture_1.png

Angle bracket at line 87 shows us where we are paused.

Everything seems to work as expected until we notice that the condition in the 'if' statement is definitely not met! In this case it seems to be a bug in a line mapping mechanism on the level of SpiderMonkey debugger. The SpiderMonkey low level debugger just informs us that the next instruction which will be executed just after the 'if' statement is placed at line 87. In fact there are two instructions here, but they should be rather mapped to the line 89 or 91. Anyway it's not a big thing. In practice in 99% of cases everything is mapped correctly and works exactly as we would expect.

Let's back to the code. Type "step" command again, press enter and...we are still in the same line. It's because the debugger gives us an opportunity to see a result of the last instruction executed before leaving a function.

OK, you have already known how to step through source code, how to add breakpoints, how to pause an application and why debugger may seem not to respond for your commands.

The next thing we are going to do is to execute "_buildUI" function from the example script. It's a bit more complex use case, so it should be a good exercise.

This function is a specific one, because it's executed before the application window is even displayed. There is no way to execute it by pressing a button or something like that. In order to to so, we have to run the application in 'suspended' mode and add breakpoint before any application code is executed.

First of all, type "continue" command just to resume the application and close it by clicking the "close" button or something. It should result in disconnecting the debugger client.

jrdb> c
Remote connection closed.

Then start the application again using an additional "-S" argument:

$  gjs -D -S example.js
Debugger is listening on port: 8089
Application is suspended.

In this case application is suspended and debugger waits for commands. Let's connect to the debugger and see what's going on.

$ jrdb
JavaScript Remote Debugger Client connected to a remote debugger.
Waiting for a list of JavaScript contexts being debugged.
Type "help context" for more information.
Available JSContext instances: 
0) gjs-console (PAUSED)
There is only one JSContext managed by the debugger, so it has been chosen as the current one.
You can change it using the "context" command.
jrdb> 

Notice that application is already paused. Type "list" command to see where we are paused then.

picture_2.png

So we are stuck at line 3 which is in fact the first executable line in our case. It looks promising, but we are interested in debugging "_buildUI" function, therefore set a breakpoint at the line. Remember about 'source' command you can use to display a list of managed scripts and their content. This command is not so important in this case, where we have access to the source code through an editor, but it will be priceless in case of debugging GNOME Shell directly, where all core scripts are embedded into libgnome-shell.so shared library. OK, so let's add breakpoint at line 37, the first executable line in the "_buildUI" function, turn the animations on and resume the application just to hit the breakpoint.

picture_3.png

OK, everything works like a harm (at last in my case ;-)), so let's step through the function. Keep steeping until you reach line 53, because we have something to do there.

picture_4.png

Great, we are paused at line 53 as we always wanted, so let's get to work.

The next very important command allows us to evaluate code in the scope of an application being debugged. It's "print" command which can be used to do it. So let's try to change the label of the button created at line 49 to "Gimme a cookie".

jrdb> p this._cookieButton = new Gtk.Button ({ label: "Gimme a cookie" });
{}
jrdb> c
jrdb>

After resuming the application, you should see a window like the one below:

picture_5.png

As you can see this is really powerful tool. With it in hand you can change almost everything in already running code.

OK, that's all at least concerning GJS. It should be enough for you to start using the debugger in practice. If you would like to learn more just read over the official documentation here: JSRDbg.

GNOME Shell

I assume that you got through the installation and configuration process, you have your GNOME Shell integrated with JSRDbg correctly and your debugger up and running at port 8090. I changed the default one just not to get in conflict with GJS.

OK, let's connect to the remote debugger using "jrdb" command.

Remember, do not try to add any breakpoints nor pause the GNOME Shell yet.

$ jrdb --port=8090
JavaScript Remote Debugger Client connected to a remote debugger.
Waiting for a list of JavaScript contexts being debugged.
Type "help context" for more information.
Available JSContext instances: 
0) gnome-shell-global (RUNNING)
There is only one JSContext managed by the debugger, so it has been chosen as the current one.
You can change it using the "context" command.
jrdb> 

Great, we are in. In the contrary to GJS, the process of debugging GNOME Shell can be called pure pleasure when we are dealing with its RUNNING state. Mostly because it runs SpiderMonkey almost all the time, so you do not need any awkward tricks to execute pending commands like in case of GJS.

First of all, let's check the list of scripts used by GNOME Shell. It should give you a general overview how big the application being debugged might be.

jrdb> source
/usr/share/gnome-shell/extensions/window-list@gnome-shell-extensions.gcampax.github.com/extension.js
resource:///org/gnome/shell/ui/components/autorunManager.js
resource:///org/gnome/shell/ui/components/telepathyClient.js
resource:///org/gnome/shell/ui/status/volume.js
......
resource:///org/gnome/shell/ui/components/automountManager.js
/home/tas/.local/share/gnome-shell/extensions/suspend-button@laserb/extension.js
/home/tas/.local/share/gnome-shell/extensions/suspend-button@laserb/lib.js
/usr/share/gnome-shell/extensions/window-list@gnome-shell-extensions.gcampax.github.com/convenience.js
jrdb>

Now you should appreciate the existence of "source" command which can be used to display every of the listed scrips. For instance:

jrdb> source resource:///org/gnome/shell/ui/status/power.js
  1 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2 
  3 const Gio = imports.gi.Gio;
  4 const Lang = imports.lang;
  5 const UPower = imports.gi.UPowerGlib;
  6 

OK, it's time to debug some real things. For instance there is a calendar widget implemented in: resource:///org/gnome/shell/ui/calendar.js.

Display its source code and find the following function: "_onNextMonthButtonClicked". The function is placed at line 559 in my case. It's called when on a small button inside the GNOME calendar gets clicked. The calendar is displayed after clicking on date-time widget.

picture_6.png

OK, add a breakpoint inside the function, but do not click on the button yet.

jrdb> break resource:///org/gnome/shell/ui/calendar.js 560
Breakpoint 0: script resource:///org/gnome/shell/ui/calendar.js, 560.
jrdb>

Now, we are prepared to hit the breakpoint, but be aware that after this operation GNOME Shell will freeze immediately and completely. You will have to switch to a native console using CTRL+ALT+F1 for instance. Being in a new console you have to connect to the debugger using a new instance of the debugger client (yes, more than one client can be connected at the same time in order to control the application). When client is connected you can play with the GNOME Shell however you like. Do not forget to resume the application when you finish by using "continue" command. Go ahead - click the big red button and let the fun begin! ;-).

Some screen shots from the other side:

I have started just by connecting to the debugger, turning on steeping animations and checking where we are currently paused:

picture_7.png

Then, steeping through the code I have reached the line 578:

picture_8.png

And displayed local variables:

picture_9.png

Just after resuming the execution using "c" command I have returned to the GNOME Shell by pressing CTRL+ALT+F7 in my case.

The next exercise we are going to do is to debug GNOME Shell from the first instruction, by starting it in suspended mode like we did in case of GJS.

The first thing you have to do is to change the appropriate environment variable in order to enable suspension. I assume that you configured the debugger as it's described in the installation instruction for Gentoo GNU/Linux. So everything you have to do is to change GNOME_SHELL_DBG_SUSPEND variable to 1 in the file /etc/profile.d/gnome-shell-dbg.sh and restart the system, or just exit the GNOME Shell, export the new variable and call "startx" to run the GNOME Shell again. However you do it, after starting GNOME Shell you should see black screen and nothing more. Then switch to the console using CTRL+ALT+F1 and you will be able to connect to the debugger using "jrdb".

Two screenshots from the battlefield:

picture_10.png

picture_11.png

Bear in mind that when you are stepping through the code you are also pausing on the function and variable declarations not only when they are executed. It's why it might look a bit unnatural for the first time when you are debugging a script from the first instruction.

That's it.

Remember that the project is really young. Please report all problems you faced using GitHub. Contributions are also highly welcome! I really hope that using it will be as enjoyable as working on it - at least with time it should be :-).

Projects/GnomeShell/DebuggingJavaScript (last edited 2015-05-13 19:25:08 by swojtasiak)