Forked to add support subprocess support in Docker containers which, as of this writing, the proprietary RubyMine debugging gems don't support. For example, debugging an application using the Passenger Docker image or any other multiprocess Rails server such as Unicorn. Will also be noticable for plain Ruby applications that spawn new processes.
If you aren't running a multiprocess app/server or are not using Docker then you can stick with the proprietary RubyMine gems.
For all the gory details on why see Additional Details section.
This quick start assumes you can run the application from RubyMine by clicking the green play button.
-
Install the gem. The below works for Ruby 2.x or greater. For older versions of Ruby see Install Gems section for the correct debase version.
group :development, :test do gem "ruby-debug-ide", git: "https://github.com/corgibytes/ruby-debug-ide", tag: "v0.7.100" gem "debase" end
-
Make sure ports 1234 and 58430-58450 are open.
#docker-compose.yml services: web: build: . <Other config settings> ports: - "3000:3000" # Rails port. - "1234:1234" # Port used for by ruby-debug-ide server. - "58430-58450:58430-58450" # Sub-process ports for multi-process servers (Unicorn, Passenger, etc).
-
Uncheck the following RubyMine registry entries. You can access RubyMine Registry settings via the Help->Find Actions menu item then enter "Registry".
- ruby.use.debase30.debugger
- ruby.use.debase23.debugger
- ruby.docker.use.proprietary.debase-debugger
-
In Rails->Development Run/Debug Configuration add the environment variable
IDE_PROCESS_DISPATCHER=host.docker.internal:26168
.
Click the debug button, the green bug, when Rails->Development is selected and you should be able to debug your multiprocess application. If everything works you should see a message about subprocess debugger listening:
docker-compose -f /home/localadmin/Desktop/Repos/website/docker-compose.yml -f /home/localadmin/.cache/JetBrains/RubyMine2023.2/tmp/docker_compose/docker-compose.override.1415.yml up --exit-code-from web --abort-on-container-exit web
Container website-db-1 Created
Container website-selenium-1 Created
Container website-web-1 Created
Attaching to website-web-1
website-web-1 | The Gemfile's dependencies are satisfied
website-web-1 | yarn install v1.22.19
website-web-1 | [1/4] Resolving packages...
website-web-1 | success Already up-to-date.
website-web-1 | Done in 4.63s.
website-web-1 | Fast Debugger (ruby-debug-ide 0.7.100, debase 0.2.4.1, file filtering is supported) listens on 0.0.0.0:1234
website-web-1 | => Booting Passenger application server
website-web-1 | => Rails 6.1.7.6 application starting in development http://0.0.0.0:3000
website-web-1 | => Run `bin/rails server --help` for more startup options
website-web-1 | Subprocess Debugger (ruby-debug-ide 0.7.100, debase 0.2.4.1, file filtering is supported) listens on 0.0.0.0:58431
website-web-1 | =============== Phusion Passenger(R) Standalone web server started ===============
website-web-1 | PID file: /home/app/website/tmp/pids/passenger.3000.pid
website-web-1 | Log file: /home/app/website/log/passenger.3000.log
website-web-1 | Environment: development
website-web-1 | Accessible via: http://0.0.0.0:3000/
< More messages...>
Depending on the used Ruby version you will need the following additional gems installed:
-
Ruby 2.x - debase
group :development, :test do gem "ruby-debug-ide", git: "https://github.com/corgibytes/ruby-debug-ide", tag: "v0.7.100" gem "debase" end
-
Ruby 1.9.x - ruby-debug-base19x
group :development, :test do gem "ruby-debug-ide", git: "https://github.com/corgibytes/ruby-debug-ide", tag: "v0.7.100" gem "ruby-debug-base19x" end
-
jRuby or Ruby 1.8.x - ruby-debug-base
group :development, :test do gem "ruby-debug-ide", git: "https://github.com/corgibytes/ruby-debug-ide", tag: "v0.7.100" gem "ruby-debug-base" end
For Windows, make sure that the Ruby DevKit is installed.
Below is how I have setup RubyMine successfully for different versions of Ruby and/or Rails. If the below does not work for you let me know by opening a issue or pull request.
In all cases the following ports need to open on the Docker container:
- 1234 - Default port that ruby-debug-ide listens on.
- 58430-58450 - Only required if your application spawns subprocesses. For example, debugging Rails applications that use Passenger or Unicorn servers.
The following RubyMine Registry settings need to be set to false:
- ruby.use.debase30.debugger
- ruby.use.debase23.debugger
- ruby.docker.use.proprietary.debase-debugger
You can access the RubyMine Registry settings via the Help->Find Actions menu item then enter "Registry".
The docker-compose.yml
file should look something like:
command: bundle exec rails s -p 3000 -b 0.0.0.0
volumes:
- .:/app
extra_hosts:
- "host.docker.internal:host-gateway" # Only required for Linux systems running Docker Desktop.
ports:
- "3000:3000" # Rails port.
- "1234:1234" # Port used for by ruby-debug-ide server.
- "58430-58450:58430-58450" # Sub-process ports for multi-process servers (Unicorn, Passenger, etc).
Create a Docker Compose Remote Interpreter as outlined here. Open up or create a Rails Run/Debug Configuration as outlined here. Make sure the Ruby SDK used is the Docker Compose one you setup .
If you are using Docker Desktop make sure you set the Environment Variable to: IDE_PROCESS_DISPATCHER=host.docker.internal:26168
. If you don't set the IDE_PROCESS_DISPATCHER
then ruby-debug-ide will try to respond to the address it receives requests from, which in the Docker world is usually 172.17.0.1. See here for more informaiton on Docker networking.
On Linux you also need to make sure you set the extra_hosts
as outlined above.
If you are using just plain old Docker on Linux then you don't need to set the IDE_PROCESS_DISPATCHER
variable.
The port 26168
is the default port RubyMine uses when spawning the docker container. For more details see the docker-compose.override
file created by RubyMine when you click the green debug button.
/usr/local/bin/docker-compose -f /home/localadmin/Repos/store/docker-compose.yml -f /home/localadmin/.cache/JetBrains/RubyMine2022.2/tmp/docker-compose.override.2581.yml up --exit-code-from web --abort-on-container-exit web
Follow the steps in Docker Compose, RubyMine, Rails 4 or greater. The only change is you can't use the default Rails Run/Debug as support for Rails 3.2 was dropped in RubyMine version 2022.1. Instead you need to create a Gem Command Run/Debug with the following settings:
- Gem name: rails
- Executable name: rails
- Arguments: s -p 3000 -h 0.0.0.0 # Other rails command line arguments as needed.
- Environment variables: IDE_PROCESS_DISPATCHER=host.docker.internal:26168 # If using Docker Desktop
The green RubyMine Run or Debug buttons should behave as they used too when using the Rails Run/Debug configuration.
Run the following command to start the debugging session for a Rails application:
rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26168 -- bin/rails s
Feel free to add additional options to the rails command at the end such as port, horst, etc.
The ruby-debug-ide gem provides the protocol to establish communication between the debugger engine (such as debase or ruby-debug-base) and IDEs (for example, RubyMine, Visual Studio Code, or Eclipse). ruby-debug-ide redirects commands from the IDE to the debugger engine. Then, it returns answers/events received from the debugger engine to the IDE. To learn more about a communication protocol, see the following document: ruby-debug-ide protocol.
The ruby-debug-ide gem is no longer published to Rubygems as outlined here. Instead it is included with RubyMine at plugins/ruby/rb/gems
. Two versions are included with RubyMine: 0.7.3 and 2.3.8. The 2.3.8 gem, as of this writing, requires Ruby 2.3 or greater the run.
RubyMine will automagically detect and install the propritory debugging gems which means you don't need add ruby-debug-ide
and debase
to your gem file.
The propritory RubyMine debug gems can be found in Linux at:
~/.local/share/JetBrains/Toolbox/apps/rubymine/plugins/ruby/rb/gems
When I wrote this the gems included with RubyMine 2023.2.4 are:
$ ls
debase-0.2.5.beta2.gem rcov-0.9.9.jb-java.gem ruby-debug-base-0.11.0-java.gem ruby-debug-ide-3.0.0.beta.12.1.gem rubymine_profiler_pid_logger.rb
debase-2.3.10.gem ruby-debug-base-0.10.5.jb2-x86-mswin32.gem ruby-debug-base19x-0.11.32.gem rubymine_debug_anything.rb
debase-3.0.0.beta.7.gem ruby-debug-base-0.10.5.rc10.gem ruby-debug-ide-0.7.3.gem rubymine_jruby_debug.rb
debug_preloader.rb ruby-debug-base-0.10.6-java.gem ruby-debug-ide-2.3.15.1.gem rubymine_passenger_debug.rb
ruby-debug-ide randomly picks a new port to communicate with the IDE when a new subprocess is created. The problem is Docker containers have all the ports closed by default so the IDE gets the new port to use but when it tries to call the server it will fail as the port is blocked.
To fix the problem this fork will only use known ports when a subprocess is spawned. That way the ports can be opened in the Docker container.
The main methods involved in the problem and fix are find_free_port and notify_dispatcher_if_needed in lib/ruby-debug-ide.rb.
Some more information and history:
- #107 - Docker debugging and sub-debugger with random port
- #186 - Error Raised when Debugging a Multi-Process app in Docker on a Mac
Since RubyMine 2023.2.4, when I noticed the issue, the following Registry settings need to be set to false for this forked debugging gem to work:
- ruby.use.debase30.debugger
- ruby.use.debase23.debugger
- ruby.docker.use.proprietary.debase-debugger
In previous verison of RubyMine the above settings didn't need to be changed. I'm actually not sure if they even existed. I think the problem started RubyMine 2023.1 as in the RubyMine 2023.1.3 fixed issue RUBY-31246 (Could not find a valid gem 'ruby-debug-ide-2.3.13gem' (>=0) in any repository).
I think the offending RubyMine code is below but if anyone knows better please let me know. Best I can tell the below code will try to load the proprietary debase (backend) gem if the Ruby version is 2.3 or greater unless the suppress proprietary is set to false. The debase gem loaded determines the ruby-debug-ide gem (frontend) to load.
# // ruby.jar!/org/jetbrains/plugins/ruby/ruby/run/configuration/RubyAbstractCommandLineState.class
private static final Pattern RDI_OSS_VERSION_PATTERN = Pattern.compile("(0\\.7(\\.[0-9A-z]+)*)");
private static final Pattern RDI_JB_VERSION_PATTERN = Pattern.compile("(2\\.3(\\.[0-9A-z]+)*)");
private static final Pattern RDI_JB_30_VERSION_PATTERN = Pattern.compile("(3\\.0(\\.[0-9A-z]+)*)");
private static final Pattern DEBASE_OSS_VERSION_PATTERN = Pattern.compile("(0\\.2(\\.[0-9A-z]+)*)");
private static final Pattern DEBASE_JB_VERSION_PATTERN = Pattern.compile("(2\\.3(\\.[0-9A-z]+)*)");
private static final Pattern DEBASE_JB_30_VERSION_PATTERN = Pattern.compile("(3\\.0(\\.[0-9A-z]+)*)");
static enum FrontendDebugGemType {
RDI_OSS("ruby-debug-ide", "0.7.3", RubyDebugIdeGemHelper.RDI_OSS_VERSION_PATTERN, RubyDebugIdeGemHelper.RDI_OSS_GEM_PATTERN, false),
RDI_JB("ruby-debug-ide", "2.3.15.1", RubyDebugIdeGemHelper.RDI_JB_VERSION_PATTERN, RubyDebugIdeGemHelper.RDI_JB_GEM_PATTERN, true),
RDI_JB_30("ruby-debug-ide", "3.0.0.beta.12.1", RubyDebugIdeGemHelper.RDI_JB_30_VERSION_PATTERN, RubyDebugIdeGemHelper.RDI_JB_30_GEM_PATTERN, true);
}
public static enum BackendDebugGemType {
DEBASE_OSS("debase", "0.2.5.beta2", RubyDebugIdeGemHelper.FrontendDebugGemType.RDI_OSS, RubyDebugIdeGemHelper.DEBASE_OSS_VERSION_PATTERN, RubyDebugIdeGemHelper.DEBASE_OSS_GEM_PATTERN, new String[0]),
DEBASE_JB("debase", "2.3.10", RubyDebugIdeGemHelper.FrontendDebugGemType.RDI_JB, RubyDebugIdeGemHelper.DEBASE_JB_VERSION_PATTERN, RubyDebugIdeGemHelper.DEBASE_JB_GEM_PATTERN, new String[0]),
DEBASE_JB_30("debase", "3.0.0.beta.7", RubyDebugIdeGemHelper.FrontendDebugGemType.RDI_JB_30, RubyDebugIdeGemHelper.DEBASE_JB_30_VERSION_PATTERN, RubyDebugIdeGemHelper.DEBASE_JB_30_GEM_PATTERN, new String[0]);
}
public static BackendDebugGemType getBaseType(@NotNull Sdk sdk, boolean suppressProprietary) {
if (sdk == null) {
$$$reportNull$$$0(12);
}
if (RubySdkUtil.isRubinius(sdk)) {
return DEBASE_OSS;
} else if (JRubySdkUtil.isJRubySDK(sdk)) {
return RubyDebugIdeGemHelper.sdkEffectiveLanguageLevelGreaterThan23(sdk) ? RDB_JRUBY_NEW : RDB_JRUBY_OLD;
} else if (!suppressProprietary && RubyDebugIdeGemHelper.isDebase30Enabled(sdk)) {
return DEBASE_JB_30;
} else if (!suppressProprietary && RubyDebugIdeGemHelper.isDebase23Enabled(sdk)) {
return DEBASE_JB;
} else if (LanguageLevel.RUBY19.isLessThan(RubyLanguageLevelPusher.getEffectiveLanguageLevel(sdk))) {
return DEBASE_OSS;
} else if (RubySdkUtil.isRuby19(sdk)) {
return RDB19;
} else {
return RubySdkSystemAccessor.notNullFrom(sdk).isWindows() && RubyDebugIdeGemHelper.shouldInstallBundledDebug(RubySdkType.getPlatform(sdk)) ? RDB18_WIN : RDB18;
}
}
public static boolean isDebase23Enabled(@NotNull Sdk sdk) {
if (sdk == null) {
$$$reportNull$$$0(5);
}
return Registry.is("ruby.use.debase23.debugger", true) && isDebase23Compatible(sdk);
}
public static boolean isDebase23Compatible(@NotNull Sdk sdk) {
if (sdk == null) {
$$$reportNull$$$0(8);
}
return RubySdkUtil.isMRISdk(sdk) && sdkEffectiveLanguageLevelGreaterThan22(sdk);
}
static MyDebuggerGemsStatus from(@Nullable Module module, @NotNull Sdk sdk) {
if (sdk == null) {
$$$reportNull$$$0(2);
}
BackendDebugGemType backEndGemType = RubyDebugIdeGemHelper.BackendDebugGemType.getBaseType(sdk, false);
GemInfo backendGem = backEndGemType.findGem(module, sdk);
boolean isBackendGemUpToDate = backEndGemType.isUpToDate(backendGem);
FrontendDebugGemType frontEndGemType = backEndGemType.getFrontendDebugGemType();
GemInfo frontEndGem = frontEndGemType.findGem(module, sdk);
boolean isFrontEndGemUpToDate = frontEndGemType.isUpToDate(frontEndGem);
return new MyDebuggerGemsStatus(backEndGemType, backendGem, isBackendGemUpToDate, frontEndGemType, frontEndGem, isFrontEndGemUpToDate);
}
The ruby.use.debase23.debugger
and ruby.use.debase30.debugger
need to be disabled otherwise RubyMine will add the --full-value-time-limit
and --full-value-memory-limit
arguments when calling the rdebug-ide
command. The error displayed is:
docker-compose -f /home/localadmin/Desktop/Repos/website/docker-compose.yml -f /home/localadmin/.cache/JetBrains/RubyMine2023.2/tmp/docker_compose/docker-compose.override.2504.yml up --exit-code-from web --abort-on-container-exit web
Attaching to website-web-1
Container website-selenium-1 Running
Container website-db-1 Running
Container website-web-1 Recreate
Container website-web-1 Recreated
website-web-1 | The Gemfile's dependencies are satisfied
website-web-1 | yarn install v1.22.19
website-web-1 | [1/4] Resolving packages...
website-web-1 | success Already up-to-date.
website-web-1 | Done in 2.03s.
website-web-1 | Using ruby-debug-base 0.2.4.1
website-web-1 | Usage: rdebug-ide is supposed to be called from RDT, NetBeans, RubyMine, or
website-web-1 | the IntelliJ IDEA Ruby plugin. The command line interface to
website-web-1 | ruby-debug is rdebug.
website-web-1 |
website-web-1 | Options:
website-web-1 | -h, --host HOST Host name used for remote debugging
<help file details cut>
website-web-1 |
website-web-1 | invalid option: --full-value-time-limit
website-web-1 exited with code 1
Aborting on container exit...
Container website-web-1 Stopping
Container website-web-1 Stopped
Process finished with exit code 1
The RubyMine generated docker compose file looks like:
version: "3.7"
services:
web:
command:
- "sh"
- "-c"
- "rm -f tmp/pids/server.pid && bash -c \"/usr/local/rvm/bin/rvm ruby-2.7.7 do\
\ /usr/local/rvm/rubies/ruby-2.7.7/bin/ruby -x /home/app/website/bin/bundle\
\ exec /usr/local/rvm/rubies/ruby-2.7.7/bin/ruby /usr/local/rvm/gems/ruby-2.7.7/bundler/gems/ruby-debug-ide-769179fb0ba6/bin/rdebug-ide\
\ --key-value --disable-int-handler --evaluation-timeout 10 --evaluation-control\
\ --time-limit 100 --memory-limit 0 --full-value-time-limit 20000 --full-value-memory-limit\
\ 0 --rubymine-protocol-extensions --port 1234 --host 0.0.0.0 --dispatcher-port\
\ 26168 -- /home/app/website/bin/rails server -b 0.0.0.0 -p 3000 -e development\""
environment:
TEAMCITY_RAKE_RUNNER_MODE: "idea"
TEAMCITY_RAKE_TU_TESTRUNNERMADIATOR_PATH: "/usr/local/rvm/rubies/ruby-2.7.7/lib/ruby/gems/2.7.0/gems/test-unit-3.3.4/lib/test/unit/ui/testrunnermediator.rb"
TEAMCITY_RAKE_RUNNER_USED_FRAMEWORKS: ":test_unit :shoulda "
RM_INFO: "RM-232.10203.15"
IDE_PROCESS_DISPATCHER: "host.docker.internal:26162"
TEAMCIY_RAKE_TU_AUTORUNNER_PATH: "/usr/local/rvm/rubies/ruby-2.7.7/lib/ruby/gems/2.7.0/gems/test-unit-3.3.4/lib/test/unit/autorunner.rb"
ANSICON: ""
RUBYLIB: "/opt/.rubymine_helpers/rb/testing/patch/common:/opt/.rubymine_helpers/rb/testing/patch/rake:/opt/.rubymine_helpers/rb/testing/patch/testunit:/usr/local/rvm/gems/ruby-2.7.7/gems/debase-0.2.4.1/lib:/usr/local/rvm/gems/ruby-2.7.7/bundler/gems/ruby-debug-ide-769179fb0ba6/lib"
ports:
- "1234:1234"
- "26166:26168"
- "3000:3000"
stdin_open: true
volumes:
- "/home/localadmin/Desktop/Repos/website:/home/app/website:rw"
- "/home/localadmin/.cache/JetBrains/RubyMine2023.2/coverage:/tmp/coverage:rw"
- "jetbrains_ruby_helpers_RM-232.10203.15:/opt/.rubymine_helpers/rb"
working_dir: "/home/app/website"
volumes:
jetbrains_ruby_helpers_RM-232.10203.15: {}
The code that causes this problem only checks if the ruby.use.debase23.debugger
or ruby.use.debase30.debugger
registry setting is true and the Ruby version is 2.3 or greater.
// ruby.jar!/org/jetbrains/plugins/ruby/ruby/run/configuration/RubyAbstractCommandLineState.class
public @NotNull T setRdebugOptions() throws ExecutionException {
this.myDebugGemHelper.enableVerboseMode(this.myArgs, this.myState.isVerboseOutput());
if (ApplicationManager.getApplication().isUnitTestMode() && (RubyDebugIdeGemHelper.isDebase23Enabled(this.mySdk) || RubyDebugIdeGemHelper.isDebase30Enabled(this.mySdk))) {
this.myArgs.add("--verbose_tests");
}
this.myDebugGemHelper.enableKeyValuePresentation(this.myArgs, this.myState.isKeyValueHashPresentation());
this.myDebugGemHelper.enableSteppingOverIntoBlocks(this.myArgs, this.myState.shouldStepOverGoIntoBlocks());
this.myDebugGemHelper.disableIntHandler(this.myArgs, RubyVMOptions.isDisableDebuggerIntHandler());
this.myDebugGemHelper.setEvaluationTimeout(this.myArgs, this.myState.getTimeout());
this.myDebugGemHelper.enableEvaluationControl(this.myArgs, this.myState.isEvaluationControl());
if (this.myState.isEvaluationControl()) {
this.myDebugGemHelper.setEvaluationTimeLimit(this.myArgs, this.myState.getTimeLimit());
this.myDebugGemHelper.setEvaluationMemoryLimit(this.myArgs, this.myState.getMemoryLimit());
}
if (RubyDebugIdeGemHelper.isDebase23Enabled(this.mySdk) || RubyDebugIdeGemHelper.isDebase30Enabled(this.mySdk)) {
this.myDebugGemHelper.setFullValueEvaluationTimeLimit(this.myArgs, Registry.intValue("ruby.debugger.fullValue.timelimit"));
this.myDebugGemHelper.setFulValueEvaluationMemoryLimit(this.myArgs, Registry.intValue("ruby.debugger.fullValue.memorylimit"));
}
this.myDebugGemHelper.addGemSpecificArgs(this.myArgs);
RubyAbstractCommandLineState.addDebuggerPorts(this.myRunner, this.myArgs, this.myData, this.myState.isMultiprocess() && this.myDebugGemHelper.isMultiProcessDebugSupported());
if (this == null) {
$$$reportNull$$$0(10);
}
return this;
}
// ruby.jar!/org/jetbrains/plugins/ruby/ruby/run/configuration/debugger/RubyDebugIdeGemHelper.class
public static boolean isDebase23Enabled(@NotNull Sdk sdk) {
if (sdk == null) {
$$$reportNull$$$0(5);
}
return Registry.is("ruby.use.debase23.debugger", true) && isDebase23Compatible(sdk);
}
public static boolean isDebase23Compatible(@NotNull Sdk sdk) {
if (sdk == null) {
$$$reportNull$$$0(8);
}
return RubySdkUtil.isMRISdk(sdk) && sdkEffectiveLanguageLevelGreaterThan22(sdk);
}
If you have any questions, notice a bug, or have a suggestion/enhancement please let me know by opening issue or pull request.
The easiest way to get you development environment setup is to use Docker. Install Docker then run build the container:
docker-compose build
Then run the container:
docker-compose run --rm app bash
Note: The gems are not installed until the container is run and the docker-entrypoint.sh is called.
If you don't want to use Docker then you can install Ruby using RVM, Rbenv, or any other method you like.
Linting is done by Standard. To check that the code if formatted correctly run:
standardrb
When working on a file remove it from the .standard_todo.yml file and correct as many linting errors as you can. Standard is configured to lint the code base for Ruby 1.8 but sometimes it makes recommendations that would break on older Ruby versions. If you spot a linting recommendation you should open an issue or pull request in Standard.
Once you have your development environment setup make sure the tests all pass:
rake
Thanks to ruby-debug team and JetBrains for creating RubyMine and the debugging gems.