Lately at work I've had to deal with an application with a backend
written using nodeJS
. I wanted to understand what the application
was doing by doing interactive debugging sessions. The only
constraints I had were the following two:
-
I wanted to use
emacs
. -
The application executed within a remote server that I could login through
ssh
.
After some searches and some configuration, I managed to find the right setup. Said in another way, I'm able to debug remotely an application written in nodeJS all while using emacs, which is used both for writing and editing new code as well as a debugging interface. In this blog post I will showcase my setup, and at the end I will link to some of the references and packages that I have used.
Have fun!
#Step 1 – Lab Setup
In order to showcase the setup I will first create a situation
which simulates the scenario I was working with. In our scenario
we have a remote server running a local nodeJS app. We will be
able to connect to this remote server by using ssh
. We will
simulate the remote server with a docker container
So, as a first thing, let us start the docker
service.
systemctl start docker
To build our container we use the following Dockerfile
.
FROM ubuntu:latest
RUN apt-get update && apt-get install -y openssh-server curl nodejs passwd
RUN mkdir /var/run/sshd
RUN echo "root:password" | chpasswd
RUN sed -i 's/\#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
In this dockerfile we set a basic ssh account with login
root:password
. We also download nodejs
. We can build the docker
with docker build
by executing the following command in the same
folder where we have saved the Dockerfile
.
sudo docker build -t nodejs-debug .
At this point we can start the docker by executing docker run
docker run -d -p 1337:22 --name test-nodejs-debug nodejs-debug
Notice the syntax for exposing port 1337
. Continuing, with respect
to the code we will debug, its the following, a simple "hello
world" example.
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
To test that everything works, we can first copy the code of
interest with scp
, and then we can login with ssh
. Given that the
server will listen in the localhost interface of the remote server,
to interact with it we need to tunnel our connection through
ssh. This can be done using the -L
flag.
scp -P1337 app.js root@localhost:/root/
ssh -L 8000:localhost:3000 -p1337 root@localhost
With the ssh session we can then start the application
node /root/app.js
At this point if we go to our local browser at the URL http://localhost:8000 we should see the following
Let us see now how to setup a debuging configuration for this sort of scenarios.
#Step 2 – Debugging Configuration
In terms of emacs packages, for our purposes we need the following
two: js2-mode
, which is used to edit javascript code, and indium
which will be our main emacs interface for debugging nodeJS
code. To have a self-contained emacs configuration, one can use the
following and start emacs with emacs -q -l emacs.el
. The important
thing is to remember to download use-package
from github. Notice
also the hook we define in the indium
package configuration so that
it is executed whenever we are editing JS code.
;; Added by Package.el. This must come before configurations of
;; installed packages. Don't delete this line. If you don't want it,
;; just comment it out by adding a semicolon to the start of the line.
;; You may delete these explanatory comments.
(package-initialize)
;; NOTE: execute 'git pull https://github.com/jwiegley/use-package' from within the home directory of the user
(add-to-list 'load-path "~/use-package")
(require 'use-package)
(use-package package
:config
;; Set priorities of using melpa-stable.
;; The higher the number, the higher the priority.
(setq package-archive-priorities
'(("melpa-stable" . 2)
("MELPA" . 1)
("gnu" . 0)))
;; Add to list melpa stable
(setq package-archives
`(("melpa-stable" . "https://stable.melpa.org/packages/")
("MELPA" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")
))
)
(use-package js2-mode
:ensure t)
(use-package indium
:ensure t
:hook ((j2-mode . indium-interaction-mode))
:config
(setq indium-client-debug t))
The idea now is to use sshfs
to mount the remote folder present in
the server the app's running it with a local mount point in our
machine. This is done for various purposes. For starters, it
allows us to use emacs as we would use it to edit any other local
file in our machine. The difference is that instead of only
editing a local file, every change we make will be also pushed to
its remote counterpart. This could also be done with packages such
as tramp-term
to connect remotely, however I have felt that sshfs
is simply more efficient and easier to use.
NOTE: Given that my emacs runs as a normal user, I recommend do to
this step with a normal user. In my case I will use leo
, my
non-root user.
mkdir remote
sshfs -p1337 root@localhost:/root remote
First we create a folder called remote
, and then we connect that
folder with the /root
folder present in the remote machine. At
this point we can create a new file in ./remote/.indium.json
. This
file will be used by the indium package, and it contains
information needed to start debugging sessions. The content of the
file is as follows
{
"configurations": [
{
"name": "11ty",
"type": "node",
"host": "127.0.0.1",
"port": "8001",
"remoteRoot": "/root/"
}
]
}
We have now everything we need to have to start debugging. Before doing that we can stop the application from running if we have still it open. We can also close the tunnel we created previously.
#Step 3 – Let's debug!
For starters, we will login with ssh again, but this time we will create two tunnels, one for transfering the usual application data, and another one for transfering the debugging data.
ssh -L 8000:localhost:3000 -L 8001:localhost:9229 -p1337 root@localhost
With the ssh connection just created we can now start the
application. This time, however, we will start it with the flag
--inspect
, which will also start the debugger on the local port
9229
.
node --inspect /root/app.js
Debugger listening on ws://127.0.0.1:9229/b719ba41-eb4d-4421-9b1a-65b781be4143
For help, see: https://nodejs.org/en/docs/inspector
Server running at http://127.0.0.1:3000/
At this point we can test for basic connectivity with curl
. Note
however that this must be done in our local machine, and not in
the shell we have just opened with the server.
curl localhost:8000
We are finally read to debug the app with emacs. Indeed, we can
then open the application file app.js
with emacs (C-x C-f
), start
indium-interactive-mode
and connect to the remote server using
inidum-connect
.
Now we can freely move around our file and we can add our
breakpoints using the indium-add-breakpoint
function.
By interacting with the webapp we can trigger such breakpoint and start to navigate the dynamic nodeJS environment as we please.
After we're done with our remote session we can call indium-quit
,
we can unmount the remote path with umount remote
, and we can go
on about our day.
#Step 4 – Conlcusion and Refs
At first I did not know if it was possible to do exactly this thing in emacs. Not because it could not technically be done, of course, given that you can do anything you want with emacs. I just wondered if there were already some packages and some guides that made this possible. And yes, there were, luckily for me!
The setup just showcased was made possible also because of the following resources. I thank every author for their contribution, and for making emacs usable in every possible scenario, such as the one just described.