Skip to content

Commit 06311ef

Browse files
committed
Update server after launching
1 parent fce94d4 commit 06311ef

File tree

3 files changed

+83
-59
lines changed

3 files changed

+83
-59
lines changed

lib/ruby_lsp/server.rb

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ def run_initialized
372372

373373
perform_initial_indexing
374374
check_formatter_is_available
375+
update_server
375376
end
376377

377378
#: (Hash[Symbol, untyped] message) -> void
@@ -1417,8 +1418,39 @@ def compose_bundle(message)
14171418

14181419
# We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
14191420
# we return the response back to the editor, then the restart is triggered
1421+
launch_bundle_compose("Recomposing the bundle ahead of restart") do |stderr, status|
1422+
if status&.exitstatus == 0
1423+
# Create a signal for the restart that it can skip composing the bundle and launch directly
1424+
FileUtils.touch(already_composed_path)
1425+
send_message(Result.new(id: id, response: { success: true }))
1426+
else
1427+
# This special error code makes the extension avoid restarting in case we already know that the composed
1428+
# bundle is not valid
1429+
send_message(
1430+
Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1431+
)
1432+
end
1433+
end
1434+
end
1435+
1436+
#: -> void
1437+
def update_server
1438+
return unless @global_state.enabled_feature?(:launcher)
1439+
return unless File.exist?(File.join(@global_state.workspace_path, ".ruby-lsp", "needs_update"))
1440+
1441+
launch_bundle_compose("Trying to update server") do |stderr, status|
1442+
if status&.exitstatus == 0
1443+
send_log_message("Successfully updated the server")
1444+
else
1445+
send_log_message("Failed to update server\n#{stderr}", type: Constant::MessageType::ERROR)
1446+
end
1447+
end
1448+
end
1449+
1450+
#: (String) { (IO, Process::Status?) -> void } -> Thread
1451+
def launch_bundle_compose(log, &block)
14201452
Thread.new do
1421-
send_log_message("Recomposing the bundle ahead of restart")
1453+
send_log_message(log)
14221454

14231455
_stdout, stderr, status = Bundler.with_unbundled_env do
14241456
Open3.capture3(
@@ -1433,17 +1465,7 @@ def compose_bundle(message)
14331465
)
14341466
end
14351467

1436-
if status&.exitstatus == 0
1437-
# Create a signal for the restart that it can skip composing the bundle and launch directly
1438-
FileUtils.touch(already_composed_path)
1439-
send_message(Result.new(id: id, response: { success: true }))
1440-
else
1441-
# This special error code makes the extension avoid restarting in case we already know that the composed
1442-
# bundle is not valid
1443-
send_message(
1444-
Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1445-
)
1446-
end
1468+
block.call(stderr, status)
14471469
end
14481470
end
14491471

lib/ruby_lsp/setup_bundler.rb

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def initialize(project_path, **options)
6161
@bundler_version = bundler_version #: Gem::Version?
6262
@rails_app = rails_app? #: bool
6363
@retry = false #: bool
64+
@needs_update_path = @custom_dir + "needs_update" #: Pathname
6465
end
6566

6667
# Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
@@ -256,31 +257,60 @@ def run_bundle_install(bundle_gemfile = @gemfile)
256257
#: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
257258
def run_bundle_install_directly(env, force_install: false)
258259
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
260+
return update(env) if @needs_update_path.exist?
259261

260262
# The ENV can only be merged after checking if an update is required because we depend on the original value of
261263
# ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
262-
should_update = should_bundle_update?
263-
ENV #: as untyped
264-
.merge!(env)
264+
FileUtils.touch(@needs_update_path) if should_bundle_update?
265+
ENV.merge!(env)
265266

266-
unless should_update && !force_install
267-
Bundler::CLI::Install.new({ "no-cache" => true }).run
268-
correct_relative_remote_paths if @custom_lockfile.exist?
269-
return env
267+
$stderr.puts("Ruby LSP> Checking if the composed bundle is satisfied...")
268+
missing_gems = bundle_check
269+
270+
unless missing_gems.empty?
271+
$stderr.puts(<<~MESSAGE)
272+
Ruby LSP> Running bundle install because the following gems are not installed:
273+
#{missing_gems.map { |g| "#{g.name}: #{g.version}" }.join("\n")}
274+
MESSAGE
275+
276+
bundle_install
270277
end
271278

279+
$stderr.puts("Ruby LSP> Bundle already satisfied")
280+
env
281+
rescue => e
282+
$stderr.puts("Ruby LSP> Running bundle install because #{e.message}")
283+
bundle_install
284+
env
285+
end
286+
287+
# Essentially the same as bundle check, but simplified
288+
#: -> Array[Gem::Specification]
289+
def bundle_check
290+
definition = Bundler.definition
291+
definition.validate_runtime!
292+
definition.check!
293+
definition.missing_specs
294+
end
295+
296+
#: -> void
297+
def bundle_install
298+
Bundler::CLI::Install.new({ "no-cache" => true }).run
299+
correct_relative_remote_paths if @custom_lockfile.exist?
300+
end
301+
302+
#: (Hash[String, String]) -> Hash[String, String]
303+
def update(env)
272304
# Try to auto upgrade the gems we depend on, unless they are in the Gemfile as that would result in undesired
273305
# source control changes
274306
gems = ["ruby-lsp", "debug", "prism"].reject { |dep| @dependencies[dep] }
275307
gems << "ruby-lsp-rails" if @rails_app && !@dependencies["ruby-lsp-rails"]
276308

277309
Bundler::CLI::Update.new({ conservative: true }, gems).run
278310
correct_relative_remote_paths if @custom_lockfile.exist?
311+
@needs_update_path.delete
279312
@last_updated_path.write(Time.now.iso8601)
280313
env
281-
rescue Bundler::GemNotFound, Bundler::GitError
282-
# If a gem is not installed, skip the upgrade and try to install it with a single retry
283-
@retry ? env : run_bundle_install_directly(env, force_install: true)
284314
end
285315

286316
#: (Hash[String, String] env) -> Hash[String, String]

test/setup_bundler_test.rb

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,10 @@ def test_invoke_cli_calls_bundler_directly_for_install
694694
mock_install = mock("install")
695695
mock_install.expects(:run)
696696
Bundler::CLI::Install.expects(:new).with({ "no-cache" => true }).returns(mock_install)
697-
RubyLsp::SetupBundler.new(dir, launcher: true).setup!
697+
698+
compose = RubyLsp::SetupBundler.new(dir, launcher: true)
699+
compose.expects(:bundle_check).raises(StandardError, "missing gems")
700+
compose.setup!
698701
end
699702
end
700703
end
@@ -728,6 +731,8 @@ def test_invoke_cli_calls_bundler_directly_for_update
728731
{ conservative: true },
729732
["ruby-lsp", "debug", "prism"],
730733
).returns(mock_update)
734+
735+
FileUtils.touch(File.join(dir, ".ruby-lsp", "needs_update"))
731736
RubyLsp::SetupBundler.new(dir, launcher: true).setup!
732737
end
733738
end
@@ -750,7 +755,9 @@ def test_progress_is_printed_to_stderr
750755
end
751756

752757
stdout, stderr = capture_subprocess_io do
753-
RubyLsp::SetupBundler.new(dir, launcher: true).setup!
758+
compose = RubyLsp::SetupBundler.new(dir, launcher: true)
759+
compose.expects(:bundle_check).raises(StandardError, "missing gems")
760+
compose.setup!
754761
end
755762

756763
assert_match(/Bundle complete! [\d]+ Gemfile dependencies, [\d]+ gems now installed/, stderr)
@@ -844,41 +851,6 @@ def test_is_resilient_to_gemfile_changes_in_the_middle_of_setup
844851
end
845852
end
846853

847-
def test_update_does_not_fail_if_gems_are_uninstalled
848-
Dir.mktmpdir do |dir|
849-
Dir.chdir(dir) do
850-
File.write(File.join(dir, "Gemfile"), <<~GEMFILE)
851-
source "https://rubygems.org"
852-
gem "rdoc"
853-
GEMFILE
854-
855-
capture_subprocess_io do
856-
Bundler.with_unbundled_env do
857-
system("bundle install")
858-
run_script(dir)
859-
860-
mock_update = mock("update")
861-
mock_update.expects(:run).raises(Bundler::GemNotFound.new("rdoc"))
862-
require "bundler/cli/update"
863-
Bundler::CLI::Update.expects(:new).with(
864-
{ conservative: true },
865-
["ruby-lsp", "debug", "prism"],
866-
).returns(mock_update)
867-
868-
mock_install = mock("install")
869-
mock_install.expects(:run)
870-
require "bundler/cli/install"
871-
Bundler::CLI::Install.expects(:new).with({ "no-cache" => true }).returns(mock_install)
872-
873-
RubyLsp::SetupBundler.new(dir, launcher: true).setup!
874-
end
875-
end
876-
877-
refute_path_exists(File.join(".ruby-lsp", "install_error"))
878-
end
879-
end
880-
end
881-
882854
def test_only_returns_environment_if_bundle_was_composed_ahead_of_time
883855
Dir.mktmpdir do |dir|
884856
Dir.chdir(dir) do

0 commit comments

Comments
 (0)