Skip to content

Commit

Permalink
Use Bundler CLI directly and send errors to telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Oct 23, 2024
1 parent 30423f0 commit 559c49b
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 9 deletions.
9 changes: 8 additions & 1 deletion exe/ruby-lsp-launcher
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pid = fork do
workspace_path = workspace_uri && URI(workspace_uri).to_standardized_path
workspace_path ||= Dir.pwd

env = RubyLsp::SetupBundler.new(workspace_path).setup!
env = RubyLsp::SetupBundler.new(workspace_path, invoke_cli: true).setup!
File.write(bundle_gemfile_file, env["BUNDLE_GEMFILE"])
File.write(locked_bundler_version_file, env["BUNDLER_VERSION"]) if env["BUNDLER_VERSION"]
rescue RubyLsp::SetupBundler::BundleNotLocked
Expand Down Expand Up @@ -75,6 +75,12 @@ rescue StandardError => e
Gem::Specification.find_by_name("ruby-lsp").activate
end

error_path = File.join(".ruby-lsp", "install_error")

install_error = if File.exist?(error_path)
Marshal.load(File.read(error_path))
end

# Now that the bundle is set up, we can begin actually launching the server

$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
Expand Down Expand Up @@ -103,6 +109,7 @@ $> = $stderr
initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize

RubyLsp::Server.new(
install_error: install_error,
setup_error: setup_error,
initialize_request: initialize_request,
).start
1 change: 1 addition & 0 deletions lib/ruby_lsp/base_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class BaseServer
def initialize(**options)
@test_mode = T.let(options[:test_mode], T.nilable(T::Boolean))
@setup_error = T.let(options[:setup_error], T.nilable(StandardError))
@install_error = T.let(options[:install_error], T.nilable(StandardError))
@writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
@reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
@incoming_queue = T.let(Thread::Queue.new, Thread::Queue)
Expand Down
22 changes: 14 additions & 8 deletions lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,15 +292,21 @@ def run_initialize(message)
global_state_notifications.each { |notification| send_message(notification) }

if @setup_error
message = <<~MESSAGE
An error occurred while setting up Bundler. This may be due to a failure when installing dependencies.
The Ruby LSP will continue to run, but features related to the missing dependencies will be limited.
Error:
#{@setup_error.full_message}
MESSAGE
send_message(Notification.telemetry(
type: "error",
errorMessage: @setup_error.message,
errorClass: @setup_error.class,
stack: @setup_error.backtrace&.join("\n"),
))
end

send_message(Notification.window_log_message(message, type: Constant::MessageType::ERROR))
if @install_error
send_message(Notification.telemetry(
type: "error",
errorMessage: @install_error.message,
errorClass: @install_error.class,
stack: @install_error.backtrace&.join("\n"),
))
end
end

Expand Down
43 changes: 43 additions & 0 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class BundleInstallFailure < StandardError; end
def initialize(project_path, **options)
@project_path = project_path
@branch = T.let(options[:branch], T.nilable(String))
@invoke_cli = T.let(options[:invoke_cli], T.nilable(T::Boolean))

# Regular bundle paths
@gemfile = T.let(
Expand All @@ -47,6 +48,7 @@ def initialize(project_path, **options)
@custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
@lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
@last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
@error_path = T.let(@custom_dir + "install_error", Pathname)

dependencies, bundler_version = load_dependencies
@dependencies = T.let(dependencies, T::Hash[String, T.untyped])
Expand Down Expand Up @@ -186,6 +188,47 @@ def run_bundle_install(bundle_gemfile = @gemfile)
env["BUNDLE_PATH"] = File.expand_path(env["BUNDLE_PATH"], @project_path)
end

return run_bundle_install_through_command(env) unless @invoke_cli

begin
run_bundle_install_directly(env)
rescue => e
# Write the error object to a file so that we can read it from the parent process
@error_path.write(Marshal.dump(e))
end

env
end

sig { params(env: T::Hash[String, String]).returns(T::Hash[String, String]) }
def run_bundle_install_directly(env)
ENV.merge!(env)

unless should_bundle_update?
require "bundler/cli/install"
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Bundler::CLI::Install.new({}).run

return env
end

# If any of `ruby-lsp`, `ruby-lsp-rails` or `debug` are not in the Gemfile, try to update them to the latest
# version
gems = []
gems << "ruby-lsp" unless @dependencies["ruby-lsp"]
gems << "debug" unless @dependencies["debug"]
gems << "ruby-lsp-rails" if @rails_app && !@dependencies["ruby-lsp-rails"]

require "bundler/cli/update"
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Bundler::CLI::Update.new({}, gems).run

@last_updated_path.write(Time.now.iso8601)
env
end

sig { params(env: T::Hash[String, String]).returns(T::Hash[String, String]) }
def run_bundle_install_through_command(env)
# If there's a Bundler version locked, then we need to use that one to run bundle commands, so that the composed
# lockfile is also locked to the same version. This avoids Bundler restarts on version mismatches
base_bundle = if @bundler_version
Expand Down
8 changes: 8 additions & 0 deletions lib/ruby_lsp/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ def window_log_message(message, type: Constant::MessageType::LOG)
params: Interface::LogMessageParams.new(type: type, message: message),
)
end

sig { params(data: T::Hash[Symbol, T.untyped]).returns(Notification) }
def telemetry(data)
new(
method: "telemetry/event",
params: data,
)
end
end

extend T::Sig
Expand Down
53 changes: 53 additions & 0 deletions test/setup_bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,59 @@ def test_sets_bundler_version_to_avoid_reloads
end
end

def test_invoke_cli_calls_bundler_directly_for_install
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
File.write(File.join(dir, "gems.rb"), <<~GEMFILE)
source "https://rubygems.org"
gem "irb"
GEMFILE

Bundler.with_unbundled_env do
capture_subprocess_io do
system("bundle install")

mock_object = mock("install")
mock_object.expects(:run)
Bundler::CLI::Install.expects(:new).with({}).returns(mock_object)
RubyLsp::SetupBundler.new(dir, invoke_cli: true).setup!
end
end
end
end
end

def test_invoke_cli_calls_bundler_directly_for_update
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
File.write(File.join(dir, "Gemfile"), <<~GEMFILE)
source "https://rubygems.org"
gem "rdoc"
GEMFILE

capture_subprocess_io do
Bundler.with_unbundled_env do
# Run bundle install to generate the lockfile
system("bundle install")

# Run the script once to generate a custom bundle
run_script(dir)
end
end

capture_subprocess_io do
Bundler.with_unbundled_env do
mock_object = mock("update")
mock_object.expects(:run)
require "bundler/cli/update"
Bundler::CLI::Update.expects(:new).with({}, ["ruby-lsp", "debug"]).returns(mock_object)
RubyLsp::SetupBundler.new(dir, invoke_cli: true).setup!
end
end
end
end
end

private

def with_default_external_encoding(encoding, &block)
Expand Down

0 comments on commit 559c49b

Please sign in to comment.