svaante / dape

Debug Adapter Protocol for Emacs
GNU General Public License v3.0
455 stars 25 forks source link

Dape and Ruby's rdbg over docker-compose. Almost there! #36

Closed johnhamelink closed 7 months ago

johnhamelink commented 9 months ago

Hi there,

I've been playing around with Dape today. I, like many, have been wanting something like Dape to exist for many years.

I am currently working with Ruby code - particularly Ruby 2.7 code which is being upgraded to 3.x. I wanted to try running with a DAP server enabled in my docker container. I think its fair to say I got pretty far!

How to do it

Add a ruby-rdbg config to dape-configs. You can do this with customize:

'(ruby-rdbg
  modes (ruby-ts-mode ruby-mode)
  host "127.0.0.1"
  port 9876)

Run dape with the ruby-rdbg adapter. Stuff happens! I can type c to continue execution. Cool!

Screenshot 2023-12-13 at 15 18 09

What doesn't work

The main issue I seem to have come across initially is that the stacktrace files aren't aligned with the filesystem of the host that they're mounted from. You can see the outcome of this when you try and set a breakpoint using dape: rdbg drops No such file or directory @ rb_check_realpath_internal - <host project path>/<project file path> into the REPL output. The path being sent back to rdbg should be <container project path>/<project file path>, so the error makes sense.

I found dape-cwd-fn, and tried to hardcode its output to /usr/app, like in my container, but that didn't seem to help. If there was a way to map the directory root of the output to a local root on a per-project basis in .dir-locals, it would make the more awkward docker-centric configurations a bit easier to work with.

svaante commented 9 months ago

Hello! Have some updates.

  1. First off I added rdbg as an built in adapter in aa8fa46
  2. I also added tramp support for tcp adapters in d4bba35

Now both docker and ssh will work for a set of the included adapters, rdbg included. Navigate to you project inside docker with emacs and run dape with rdbg port 5678 (port number needs to be exposed by docker). Then it will prompt you with "Invoke command" which will be the argument for rdbg -c.

This will then expand to something like this rdbg command-cwd "/docker:my-container:/my-remote-project" port 5678 -c "bundle exec ruby main.rb"

If you would like to use you local project as your source you need to add the following in addition.

If you want to start rdbg by hand its ok too. But then you need to add an additional configuration.

Let me know what you think about the default rdbg config. It's a bit special as rdbg does not seam to support the normal initialization flow.

johnhamelink commented 9 months ago

@svaante fantastic, thank you, those commits sound like exactly what I need to get a working setup with Ruby. I do intend to try Dape out with Elixir as well - something I've wanted to do since perhaps 2017!

I am moving home right now, so I won't be able to provide quick feedback for the next few days, but when I'm back to work next week I'll definitely be making time to try this out with our production Ruby codebase. If I find any issues I'll do my best to send PRs wherever possible!

(On a side-note, I've been using Emacs 30's support for docker & Kubernetes within TRAMP a lot - locally and from cafes using a tailscale node on my LAN as a multi-hop bastion server. The latest version is a big improvement after a bit of tweaking).

1925381584 commented 8 months ago

Hello! Have some updates.

  1. First off I added rdbg as an built in adapter in aa8fa46
  2. I also added tramp support for tcp adapters in d4bba35

Now both docker and ssh will work for a set of the included adapters, rdbg included. Navigate to you project inside docker with emacs and run dape with rdbg port 5678 (port number needs to be exposed by docker). Then it will prompt you with "Invoke command" which will be the argument for rdbg -c.

This will then expand to something like this rdbg command-cwd "/docker:my-container:/my-remote-project" port 5678 -c "bundle exec ruby main.rb"

If you would like to use you local project as your source you need to add the following in addition.

  • prefix-remote: abs path to project within docker (without tramp stuff)
  • prefix-local: abs path to local project.

If you want to start rdbg by hand its ok too. But then you need to add an additional configuration.

Let me know what you think about the default rdbg config. It's a bit special as rdbg does not seam to support the normal initialization flow.

Hello, I just want to use Ruby to do some automated scripts, I don't need a doctor. I hope that the default configuration for Ruby is not necessary, just like in VSCode, to start debugging directly in the current file.

johnhamelink commented 8 months ago

Hi @svaante,

I gave this a quick try using the instructions provided. The rdbg command invocation I ended up with looks like this (with path names changed for privacy):

rdbg command-cwd "/docker:container-1:/usr/app/" port 5678 -c "rspec /usr/app/spec/api/example/v1/tags_spec.rb" prefix-remote "/usr/app" prefix-local "/Users/johnhamelink/code/example/services/project"

I updated the corresponding port configuration in the docker-compose.yml file:

container:
   image: container:latest
   ports:
     - "4000:3000" # HTTP
     - "5678:5678" # For remote debugging with debug.rb

When I invoke the command, the Dape UI opens up as expected, but a timeout is reached:

[info] Starting new session with config:
(modes (ruby-mode ruby-ts-mode) ensure dape-ensure-command command "rdbg" command-args ("-O" "--host" "0.0.0.0" "--port" "5678" "-c" "--" "rspec /usr/app/spec/api/example/v1/tags_spec.rb") command-cwd "/docker:container-1:/usr/app/" fn ((lambda (config) (plist-put config 'command-args (mapcar (lambda (arg) (if (eq arg :-c) (plist-get config '-c) arg)) (plist-get config 'command-args)))) dape-config-autoport dape-config-tramp) port 5678 :type "Ruby" -c "rspec /usr/app/spec/api/example/v1/tags_spec.rb" prefix-remote "/usr/app" prefix-local "/Users/johnhamelink/code/example/services/project")
[info] Server process started ("/bin/sh" "-i")
[std-server] Server stdout:
env: can't execute 'rdbg': No such file or directory

[io] Flushing io buffer:

[info] 
Process ("/bin/sh" "-i") exited with 127
[io] Flushing io buffer:

[info] 
Process nil exited with 256
[info] Connection to server established localhost:5678
[io] Sending:
(:arguments (:clientID "dape" :adapterID "Ruby" :pathFormat "path" :linesStartAt1 t :columnsStartAt1 t :supportsRunInTerminalRequest t :supportsProgressReporting t :supportsStartDebuggingRequest t) :type "request" :command "initialize" :seq 1)
[error] Timeout for reached for seq 1

The project I'm working on is incredibly slow to boot (I'm talking 90-120 second boot time).

Using the docker package, If I pop a "vterm with env" shell from docker, I can run rspec directly and I can see the rdbg command in the path:


/docker:container-1:/usr/app #$ which rdbg
/usr/local/bundle/bin/rdbg
svaante commented 8 months ago

Hmm this is strange, just to double check you are using dape 0.3.0? Does eshell find rdbg as well?

johnhamelink commented 8 months ago

Hey @svaante :)

0.3.0?

Yep, I'm running 0.3.0.

Does eshell find rdbg as well?

No it doesn't. There's something fishy going on: if I run C-x C-f /docker:container-1:/usr/app/, then M-x eshell, the shell opens within the correct directory, and if I add /usr/local/bundle/bin to the PATH, rdbg runs (yay). If I cd /usr/local/bundle/bin (or ls), I get no such file or directory, and if I ls /usr/local/bin, I can see the binaries which exist on the host, not in the container!

I'm not sure yet what's causing this weirdness.

EDIT: Some more things I've found:

mohkale commented 8 months ago

rdbg port 5678 (port number needs to be exposed by docker)

Hi, just to double check what was the motivation for exposing the port number like this instead of running and accessing the debugger completely from within Emacs without external configurations. I'm sort of accustomed to the eglot approach which spawns lsp in the container and doesn't require extra compose and language-server configurations for support.

svaante commented 8 months ago

Hi and sorry for the delay @johnhamelink have you gotten any further or still stuck? One issue could be that tramps bad defaults when it comes to finding paths. Does (add-to-list 'tramp-remote-path 'tramp-own-remote-path) make any diffrence.

@mohkale we are doing the same thing as eglot is doing and failing at the same things. For example you will only be available to run "solargraph" which is one of the few lsps using tcpip for communication in a non docker context. If you don't get lucky that its created with an :autoport which happens to be open to the host machine.

Difference is that most lsps are using stdio where most dap adapters are using tcp for communicating.

we are spawning the adapter inside of the docker instance with tramp so no differences there from eglot, rdbg does not communicate with std io which would have been fine to read from a tramp process, but rdbg opens a port. There is no way to open a network-process in tramp context which would be great as then we would not need the port to be exposed to the machine running emacs. But instead the host machine needs to connect to that port. We could circumvent this issue by installing some binary on the remote machine which pipes tcp to stdio and start that program as well in tramp context but that is surly a hassle.

johnhamelink commented 7 months ago

Hi @svaante apologies, I've been rather busy recently, but I will hopefully get a chance to revisit this in the coming days. I'm still very interested in this project!

johnhamelink commented 7 months ago

Hi @svaante apologies for the significant delay!

So I tried adding tramp-remote-path to tramp-own-remote-path with no luck, and similarly if I add the /usr/local/bundle/bin directory explicitly. The docker image is compiled with PATH set, but as you say TRAMP does not seem to pick it up. If I run eshell from the container's directory and add the directory to the PATH, then I can run rdbg.

/docker:globacap-web-platform-1:/usr/app $ rdbg
rdbg: command not found
/docker:globacap-web-platform-1:/usr/app [1] $ echo $PATH
/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/docker:globacap-web-platform-1:/usr/app $ export PATH=$PATH:/usr/local/bundle/bin
/docker:globacap-web-platform-1:/usr/app $ rdbg
Traceback (most recent call last):
/usr/local/bin/ruby: Interrupt
interrupt: 2
/docker:globacap-web-platform-1:/usr/app [2] $ 

However, I've built a minimal example project with which dape seems to run rdbg just fine!

https://git.sr.ht/~johnhamelink/dape-ruby-example

docker build . -t jjh/dape:latest
docker run -it jjh/dape:latest

I will try and narrow down what it is about my real-life scenario which is causing this issue.


EDIT: So I found I was able to force the path to be seen by TRAMP by exporting PATH in /etc/profile. I'm not sure why this is needed but at least now I can see the rdbg binary.

# HACK: For some reason the PATH isn't picked up by Emacs.
# This ensures the PATH set in the dockerfile is persisted across
# all shells.
USER root
RUN echo "export PATH=$PATH" >> /etc/profile
USER ruby

The next step for me is to figure out how to use rdbg's attach functionality to be able to attach to the existing rails server process that's running on the container (https://github.com/ruby/vscode-rdbg#attach-to-the-running-ruby-process). Once I've figured this out, I think I'll have a fully working setup! How exciting :D


EDIT 2: I was able to get the ruby process in the container to recognise dape's connection request:

cd /Users/johnhamelink/code/ruby-app && docker-compose logs --tail=100 -f ruby-app
ruby-app-1  | DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | => Booting Puma
ruby-app-1  | => Rails 5.2.8.1 application starting in development 
ruby-app-1  | => Run `rails server -h` for more startup options
ruby-app-1  | [1] Puma starting in cluster mode...
ruby-app-1  | [1] * Puma version: 6.2.2 (ruby 2.7.2-p137) ("Speaking of Now")
ruby-app-1  | [1] *  Min threads: 5
ruby-app-1  | [1] *  Max threads: 5
ruby-app-1  | [1] *  Environment: development
ruby-app-1  | [1] *   Master PID: 1
ruby-app-1  | [1] *      Workers: 4
ruby-app-1  | [1] *     Restarts: (✔) hot (✔) phased
ruby-app-1  | [1] * Listening on http://0.0.0.0:3000
ruby-app-1  | [1] Use Ctrl-C to stop
ruby-app-1  | DEBUGGER[bin/rails#24]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#28]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#39]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#49]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 0 (PID: 24) booted in 0.01s, phase: 0
ruby-app-1  | [1] - Worker 1 (PID: 28) booted in 0.01s, phase: 0
ruby-app-1  | [1] - Worker 2 (PID: 39) booted in 0.01s, phase: 0
ruby-app-1  | [1] - Worker 3 (PID: 49) booted in 0.0s, phase: 0
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 24
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 28
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 49
ruby-app-1  | DEBUGGER[bin/rails#197]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#201]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 3 (PID: 197) booted in 0.01s, phase: 0
ruby-app-1  | [1] - Worker 0 (PID: 201) booted in 0.0s, phase: 0
ruby-app-1  | DEBUGGER[bin/rails#212]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 1 (PID: 212) booted in 0.0s, phase: 0
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 39
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 197
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 201
ruby-app-1  | DEBUGGER[bin/rails#356]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#360]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 0 (PID: 356) booted in 0.01s, phase: 0
ruby-app-1  | [1] - Worker 2 (PID: 360) booted in 0.0s, phase: 0
ruby-app-1  | DEBUGGER[bin/rails#368]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 3 (PID: 368) booted in 0.0s, phase: 0
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#368]: Connected.
ruby-app-1  | [1] ! Terminating timed out worker (worker failed to check in within 60 seconds): 368
ruby-app-1  | DEBUGGER[bin/rails#435]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | [1] - Worker 3 (PID: 435) booted in 0.02s, phase: 0
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#435]: Connected.]

However as you can see, after a while dape produces a timeout error:

* Command initialize timeout *
Initialize failed due to: timeout
* Command disconnect timeout *
Adapter connection shutdown without successfully initializing
Configuration:
  modes (ruby-mode ruby-ts-mode)
  ensure dape-ensure-command
  fn (dape-config-tramp)
  command "rdbg"
  command-cwd "/docker:ruby-app-1:/usr/app/"
  :type "Ruby"
  :request "attach"
  command-args ["-A"]
  prefix-local "/docker:ruby-app-1:"]

I used M-x customize to figure out the right syntax for the dape config in question:

(rdbg-attach modes (ruby-mode ruby-ts-mode) ensure
                  dape-ensure-command fn (dape-config-tramp) command
                  "rdbg" command-cwd dape-command-cwd :type "Ruby"
                  :request "attach" command-args ["-A"])

Ok, I think this is my last update for today - If I start the service with the following environment variables:

 - RUBY_DEBUG_OPEN=true
 - RUBY_DEBUG_LOG_LEVEL=DEBUG

Then I can see the debugger loading files on boot. If I then run bundle exec rake spec from the container, it pauses at the breakpoint. However, when I then run dape with the above configuration, it fails to initialise:

cd /Users/johnhamelink/code/ruby-app && docker-compose logs --tail=100 -f ruby-app
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/lib/ruby/2.7.0/forwardable.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/lib/ruby/2.7.0/forwardable/impl.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/lib/ruby/2.7.0/forwardable.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/lib/ruby/2.7.0/forwardable/impl.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/lib/ruby/2.7.0/forwardable.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/bundle/gems/puma-6.2.2/lib/puma/cluster/worker_handle.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/bundle/gems/puma-6.2.2/lib/puma/cluster/worker.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/bundle/gems/puma-6.2.2/lib/puma/single.rb
ruby-app-1  | DEBUGGER (INFO): Load config/puma.rb
ruby-app-1  | DEBUGGER (INFO): Load /usr/local/bundle/gems/puma-6.2.2/lib/puma/plugin/tmp_restart.rb
ruby-app-1  | [1] Puma starting in cluster mode...
ruby-app-1  | [1] * Puma version: 6.2.2 (ruby 2.7.2-p137) ("Speaking of Now")
ruby-app-1  | [1] *  Min threads: 5
ruby-app-1  | [1] *  Max threads: 5
ruby-app-1  | [1] *  Environment: development
ruby-app-1  | [1] *   Master PID: 1
ruby-app-1  | [1] *      Workers: 4
ruby-app-1  | [1] *     Restarts: (✔) hot (✔) phased
ruby-app-1  | [1] * Listening on http://0.0.0.0:3000
ruby-app-1  | [1] Use Ctrl-C to stop
ruby-app-1  | DEBUGGER (INFO): Thread #14 is created.
ruby-app-1  | DEBUGGER (INFO): Attaching after process 1 fork to child process 41
ruby-app-1  | DEBUGGER[bin/rails#41]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[bin/rails#41] (INFO): Thread #15 is created.
ruby-app-1  | DEBUGGER[bin/rails#41] (INFO): Thread #16 is created.
ruby-app-1  | DEBUGGER[bin/rails#45] (INFO): Attaching after process 1 fork to child process 45
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #17 is created.
ruby-app-1  | DEBUGGER[bin/rails#45] (INFO): Thread #15 is created.
ruby-app-1  | DEBUGGER[bin/rails#45] (INFO): Thread #16 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #18 is created.
ruby-app-1  | DEBUGGER[bin/rails#45]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #19 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #20 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #21 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #22 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #23 is created.
ruby-app-1  | DEBUGGER[bin/rails#55] (INFO): Attaching after process 1 fork to child process 55
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #17 is created.
ruby-app-1  | DEBUGGER[bin/rails#55]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #18 is created.
ruby-app-1  | DEBUGGER[bin/rails#55] (INFO): Thread #15 is created.
ruby-app-1  | DEBUGGER[bin/rails#55] (INFO): Thread #16 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #19 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #20 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #21 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #22 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #23 is created.
ruby-app-1  | [1] - Worker 0 (PID: 41) booted in 0.01s, phase: 0
ruby-app-1  | DEBUGGER[bin/rails#65] (INFO): Attaching after process 1 fork to child process 65
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #17 is created.
ruby-app-1  | [1] - Worker 1 (PID: 45) booted in 0.01s, phase: 0
ruby-app-1  | DEBUGGER[bin/rails#65]: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #18 is created.
ruby-app-1  | DEBUGGER[bin/rails#65] (INFO): Thread #15 is created.
ruby-app-1  | DEBUGGER[bin/rails#65] (INFO): Thread #16 is created.
ruby-app-1  | [1] - Worker 2 (PID: 55) booted in 0.0s, phase: 0
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #19 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #20 is created.
ruby-app-1  | [1] - Worker 3 (PID: 65) booted in 0.0s, phase: 0
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #21 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #22 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #17 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #18 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #23 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #19 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #20 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #21 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #22 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #23 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio/version.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio/version.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio/version.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Load /usr/local/bundle/gems/nio4r-2.5.9/lib/nio/version.rb
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #24 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #25 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #26 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #27 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #24 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 1: 1 [app]#45] (INFO): Thread #28 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #25 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #26 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #27 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 0: 1 [app]#41] (INFO): Thread #28 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #24 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #25 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #26 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #24 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #27 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 2: 1 [app]#55] (INFO): Thread #28 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #25 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #26 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #27 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65] (INFO): Thread #28 is created.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65]: Connected.
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65]: GreetingError: HEAD request
ruby-app-1  | DEBUGGER[puma: cluster worker 3: 1 [app]#65]: Disconnected.
-*- mode: rspec-compilation; default-directory: "~/code/ruby-app/services/ruby-app/" -*-
RSpec Compilation started at Thu Feb  8 16:24:17

docker-compose -f ../../docker-compose.yml exec ruby-app sh -c "bundle exec rake spec SPEC_OPTS='--options /usr/app/.rspec' SPEC='/usr/app/spec/api/ruby-app/v1/example/tags_spec.rb:73'"
DEBUGGER (INFO): Session start (pid: 14)
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-14)
DEBUGGER (INFO): Thread #1 is created.
DEBUGGER (INFO): Thread #2 is created.
DEBUGGER (INFO): Thread #3 is created.
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.10.0/lib:/usr/local/bundle/gems/rspec-support-3.10.0/lib /usr/local/bundle/gems/rspec-core-3.10.0/exe/rspec /usr/app/spec/api/ruby-app/v1/example/tags_spec.rb:73
DEBUGGER (INFO): Session start (pid: 26)
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-26)
DEBUGGER (INFO): Thread #1 is created.
DEBUGGER (INFO): Thread #2 is created.
DEBUGGER (INFO): Thread #3 is created.
DEBUGGER (INFO): Thread #4 is created.
Run options:
  include {:locations=>{"./spec/api/ruby-app/v1/example/tags_spec.rb"=>[73]}}
  exclude {:slow=>true, :flaky=>true}

Randomized with seed 54321

Ruby-App::V1::Example::Tags
  when listing
DEBUGGER: wait for debugger connection...
DEBUGGER: Connected.
DEBUGGER: GreetingError: HEAD request
DEBUGGER: Disconnected.
DEBUGGER: Connected.
DEBUGGER: GreetingError: HEAD request
DEBUGGER: Disconnected.

(there's a bunch of Load log entries which I've cut out of the last debug output for brevity). I've attached the full log with the debug level set to DEBUG here: https://gist.github.com/johnhamelink/90ef99281d6d70d0e662a4adb70a04e4

With dape-debug set to t, the *dape-connection-events* buffer returns the following:

----------b---y---e---b---y---e----------
[jsonrpc] e[16:56:23.711] --> initialize[1] {"type":"request","seq":1,"command":"initialize","arguments":{"clientID":"dape","adapterID":"Ruby","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsRunInTerminalRequest":true,"supportsProgressReporting":true,"supportsStartDebuggingRequest":true}}
[jsonrpc] D[16:56:23.871] Connection state change: `finished
'

----------b---y---e---b---y---e----------

*dape-connection stderr* remains empty throughout.

I'm unsure where to go from here - it seems from the connection events that the response from rdbg back to dape is either not received?

I hope this is useful, please do let me know if you have any idea how I can debug from here.

svaante commented 7 months ago

As you see in the logs

  ruby-app-1  | DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/rdbg-1000/rdbg-1)

rdbg running in the container is setup to communicate via unix domain socket which dape is unable to communicate with.

Start the containerized application inside of the container with something like the following rdbg -O --host "0.0.0.0" --port <PORT_REACHABLE_OUTSIDE_OF_DOCKER> -c -- ruby my_app.rb

Then you are able to attach with the following configurations:

     ;; using source in local dir
     (add-to-list 'dape-configs
                  `(rdbg-attach-local-source
                    prefix-local "/Users/dpettersson/Workspace/ruby-test-app/"
                    prefix-remote "/app/"
                    port 5678
                    :request "attach"
                    :localfs t))

     ;; if you want to open source inside of the docker container
     (add-to-list 'dape-configs
                  (rdbg-attach-docker-source
                   prefix-local "/docker:container_name:"
                   port 5678
                   :request "attach"
                   :localfs t))

I hope I am understanding your issue correctly, as I am writing this I am not 100% sure :)

johnhamelink commented 7 months ago

Ok, so I think we've got it now!

I needed to do a few more things to get it working.

Firstly, on my docker-compose service, I added the following configuration:

   command: rdbg --port 5678 -c -- rails server -b 0.0.0.0
   environment:
     - RUBY_DEBUG_OPEN=true
     - RUBY_DEBUG_HOST=0.0.0.0
     # Allows rails server to finish initialising without a debug client connection
     - RUBY_DEBUG_NONSTOP=true
     # Needed to stop puma web server from making multiple workers - each would try 
     # to bind to port 5678, which causes `Address in use` when not using UNIX sockets.
     - WEB_CONCURRENCY=0 

I then used the first of your two suggested dape configs:

(add-to-list 'dape-configs
             `(rdbg-attach-local-source
               prefix-local "/Users/johnhamelink/code/example/services/ruby-app/"
               prefix-remote "/usr/app/"
               port 5678
               :request "attach"
               :localfs t))

And yes, I hit a breakpoint nicely now when I hit an endpoint!

My issue now is that I can't run a separate rspec process without hitting Address in use. Ideally the process would attach to the open debugging port. So what I'm doing now is running a separate config for rails and for rspec, so that rspec gets its own dedicated port. I'm using rspec-mode to run my tests through a docker container. The magic sauce for this is to configure rspec-mode is this:

docker-compose -f ../../docker-compose.yml exec ruby-app sh -c "bundle exec rdbg --port 5679 -c -- rake spec SPEC_OPTS='--options /usr/app/.rspec' SPEC='/usr/app/spec/api/ruby-app/v1/example/tags_spec.rb:73'"

However, I think rspec is forking somehow and the spec fails with Address in use - bind(2) for 0.0.0.0:5679. Running docker-compose exec -it web-platform netstat -tunpl does not show port 5679 in use. This seems to me like an rspec/debug.rb issue and not one for this thread. Thank you for all your help, I hope this issue comes in useful to others as well!

svaante commented 7 months ago

I am happy you got 90 ish percent there :)

johnhamelink commented 7 months ago

I figured out how to get rspec working for me as well now!

To get rspec-mode to run the docker-compose command perfectly:

(defun jjh/rspec--compose-default-wrapper (_compose compose-service command)
  "Function for wrapping a command for execution inside a compose
environment. By adding the port manually here, we keep it out of the
rails service - keeping it free for just rspec. We also name it so
it's easy to find."
  (format "docker-compose -f %s run -it --rm --name ruby-app-rspec -e 'RUBY_DEBUG_PORT=5680' -p 5680:5680 %s sh -c \"%s\""
          rspec-docker-file-name compose-service command))

(setq rspec-docker-wrapper-fn #'jjh/rspec--compose-default-wrapper)

When I ask rspec-mode to test one specific line in a file, it produces the following command:

docker-compose -f ../../docker-compose.yml run -it --rm --name ruby-app-spec -e 'RUBY_DEBUG_PORT=5680' -p 5680:5680 ruby-app sh -c "bundle exec rake spec SPEC_OPTS='--options /usr/app/.rspec' SPEC='/usr/app/spec/api/ruby-app/v1/example/tags_spec.rb:73'"

I specifically left port 5680 out of the docker-compose.yml file, and I don't set the rails rdbg port as an environment variable either, so that it isn't picked up by subsequent docker-compose run commands for rspec.

  ruby-app:
   image: ${OCI_REGISTRY_DOMAIN}/ruby-app:latest
   build:
     context: ./services/ruby-app
     dockerfile: Dockerfile
     target: command
   depends_on:
     - db
     - redis
     - traefik
   command: rdbg --port 5678 -c -- rails server -b 0.0.0.0
   environment:
     - RUBY_DEBUG_OPEN=true
     - RUBY_DEBUG_HOST=0.0.0.0
     # Allows rails to initialise and only stop when it hits a breakpoint
     - RUBY_DEBUG_NONSTOP=true      
     - WEB_CONCURRENCY=0
   ports:
     - "3000:3000" # HTTP
     - "5678:5678" # For remote debugging RAILS with the debug gem
   tty: true
   stdin_open: true

Make sure that in your Gemfile, debug is not required by default (so that in rspec we can explicitly require it after rake has called the rspec command.

gem "debug", require: false   # Provides DAP mode integration for IDE-style debugging

# Local Variables:
# mode: ruby
# mode: ruby-ts
# End:

And then finally in the spec_helper.rb, we can require debug:

# For debugging
require "debug/open_nonstop"

EDIT: Instead of manually setting the port in the spec_helper, I can just set the environment variable in the docker-run command which simplifies things down a bit.


I've updated the wiki with this info: https://github.com/svaante/dape/wiki#ruby---rdbg

mohkale commented 5 months ago

Hi, just wanted to say I gave a dape gdb over docker another go yesterday and it works flawlessly. I'm guessing that debug adaptors that support a stdio interface work fine over tramp, it's just tcp only ones that would be problematic. Regardless. Thanks so much for this excellent package :sunglasses:.

reynard93 commented 1 week ago

what if i am not using docker ? how do i run the rspec?