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 24, 2024
1 parent 664b8cc commit 365c237
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 11 deletions.
7 changes: 7 additions & 0 deletions exe/ruby-lsp-launcher
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,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 @@ -92,6 +98,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
49 changes: 49 additions & 0 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

require "sorbet-runtime"
require "bundler"
require "bundler/cli/install"
require "bundler/cli/update"
require "fileutils"
require "pathname"
require "digest"
Expand Down Expand Up @@ -48,6 +50,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 @@ -187,6 +190,52 @@ 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 @launcher

# This same check happens conditionally when running through the command. For invoking the CLI directly, it's
# important that we ensure the Bundler version is set to avoid restarts
if @bundler_version
env["BUNDLER_VERSION"] = @bundler_version.to_s
install_bundler_if_needed
end

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)
T.unsafe(ENV).merge!(env)

unless should_bundle_update?
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"]

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)
base_bundle = base_bundle_command(env)

# If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
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
26 changes: 23 additions & 3 deletions sorbet/rbi/shims/bundler.rbi
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
# typed: true

class Bundler::Settings
sig { params(name: String).returns(String) }
def self.key_for(name); end
module Bundler
class Settings
sig { params(name: String).returns(String) }
def self.key_for(name); end
end

module CLI
class Install
sig { params(options: T::Hash[String, T.untyped]).void }
def initialize(options); end

sig { void }
def run; end
end

class Update
sig { params(options: T::Hash[String, T.untyped], gems: T::Array[String]).void }
def initialize(options, gems); end

sig { void }
def run; end
end
end
end
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, launcher: 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, launcher: true).setup!
end
end
end
end
end

private

def with_default_external_encoding(encoding, &block)
Expand Down

0 comments on commit 365c237

Please sign in to comment.