Skip to content

Instantly share code, notes, and snippets.

@bensheldon
Last active May 25, 2026 09:39
Show Gist options
  • Select an option

  • Save bensheldon/edbeedf620e43e501e35a69e6411c7b8 to your computer and use it in GitHub Desktop.

Select an option

Save bensheldon/edbeedf620e43e501e35a69e6411c7b8 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require_relative "../config/interactive"
if ARGV.first == "shutdown"
Interactive.shutdown
else
Interactive.command
end
#!/usr/bin/env ruby
require_relative "../config/interactive"
Interactive.start
# frozen_string_literal: true
# rubocop:disable Rails/Exit
require_relative "git_worktree"
class Interactive
PORT = GitWorktree.integer(8780..8799)
URI = "druby://localhost:#{PORT}".freeze
def self.start
require "drb/drb"
require File.expand_path("boot", __dir__)
require File.expand_path("environment", __dir__)
raise "Only available in local environments" unless Rails.env.local?
serve(binding)
end
def self.breakpoint(bnd)
require "drb/drb"
serve(bnd)
end
def self.command(argv = ARGV)
require "drb/drb"
code = argv.join(" ")
abort "Usage: bin/interact '<ruby expression>'" if code.empty?
DRb.start_service
result = connect.eval_capture(code)
if result[:error]
warn result[:error]
exit 1
end
$stdout.print result[:output] unless result[:output].empty?
$stdout.puts result[:result]
end
def self.shutdown
require "drb/drb"
DRb.start_service
connect.stop
$stdout.puts "DRb server stopped"
end
def self.connect(timeout: 300)
deadline = Time.now.utc + timeout
loop do
server = DRbObject.new_with_uri(URI)
server.eval_capture("nil")
return server
rescue DRb::DRbConnError
abort "Timed out waiting for interactive server" if Time.now.utc > deadline
$stderr.print "."
sleep 0.5
end
end
private_class_method :connect
def self.serve(bnd)
server = new(bnd)
DRb.start_service(URI, server)
$stdout.puts "Interactive server ready at #{URI}"
server.wait
DRb.stop_service
end
private_class_method :serve
def initialize(bnd)
require "stringio"
@binding = bnd
@mutex = Mutex.new
@cv = ConditionVariable.new
@done = false
end
def wait
@mutex.synchronize { @cv.wait(@mutex) until @done }
end
def stop
Thread.new do
sleep 0.1
@mutex.synchronize do
@done = true
@cv.signal
end
end
end
def eval_capture(code)
buf = StringIO.new
old = $stdout
$stdout = buf
result = @binding.eval(code)
$stdout = old
{ result: result.inspect, output: buf.string }
rescue StandardError => e
$stdout = old
{ error: "#{e.class}: #{e.message}" }
end
end
class Binding
def interactive
require File.expand_path("interactive", __dir__)
Interactive.breakpoint(self)
end
end
# rubocop:enable Rails/Exit
Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 2 column 113
---
name: interactive
description: Interactively run Ruby commands against a live Rails process using bin/interact. Supports two modes: a standalone server (bin/interactive) for general exploration, and binding.interactive breakpoints inside tests or app code to pause execution and inspect local state. Use when the user wants to explore live Rails objects, debug a test, or poke at a running process.
---

Interactively run Ruby commands against a live Rails process.

Task

$ARGUMENTS

Two modes

Mode 1: Standalone server

Start a persistent Rails process with a fresh binding:

bin/interactive > /tmp/drb_server.log 2>&1 &

Rails takes ~12 seconds to boot. bin/interact waits automatically — no need to sleep first.

Mode 2: Breakpoint inside a test (preferred for debugging)

Add binding.interactive at any point in a test to pause execution there and expose the local binding (local variables, subject, page, etc.):

RSpec.describe MyThing do
  it "does something" do
    client = create(:client, first_name: "Valentina")
    binding.interactive  # execution pauses here
    expect(client.first_name).to eq("Valentina")
  end
end

Run the test in the background:

bin/rspec spec/path/to/spec.rb > /tmp/rspec.log 2>&1 &

Then send commands immediately — bin/interact polls until the breakpoint is hit:

bin/interact 'client.first_name'

Running commands

Use bin/interact '<ruby expression>' to evaluate expressions. State persists across calls — variables set in one call are available in the next:

bin/interact 'Client.count'
bin/interact 'client = Client.first'
bin/interact 'puts client.inspect'
bin/interact 'client.snap_apps.map(&:status)'

Both the return value and any stdout output are printed.

Shutting down

When finished, shut down the server and resume execution (if in breakpoint mode):

bin/interact shutdown

Notes

  • bin/interact waits up to 5 minutes for the server to become available — no need to coordinate timing
  • bin/interact itself is fast (~250ms) since it does not reload Rails
  • The server runs on port 8787 — only one instance at a time
  • Use bin/interact shutdown rather than pkill so breakpoint mode resumes cleanly
  • If something goes wrong, check /tmp/rspec.log or /tmp/drb_server.log for errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment