Skip to content

Commit c50572c

Browse files
authored
Move server update after launching server (#3555)
Update server after launching
1 parent 05db354 commit c50572c

File tree

3 files changed

+82
-59
lines changed

3 files changed

+82
-59
lines changed

lib/ruby_lsp/server.rb

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ def run_initialized
362362

363363
perform_initial_indexing
364364
check_formatter_is_available
365+
update_server if @global_state.enabled_feature?(:launcher)
365366
end
366367

367368
#: (Hash[Symbol, untyped] message) -> void
@@ -1406,8 +1407,38 @@ def compose_bundle(message)
14061407

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

14121443
_stdout, stderr, status = Bundler.with_unbundled_env do
14131444
Open3.capture3(
@@ -1422,17 +1453,7 @@ def compose_bundle(message)
14221453
)
14231454
end
14241455

1425-
if status&.exitstatus == 0
1426-
# Create a signal for the restart that it can skip composing the bundle and launch directly
1427-
FileUtils.touch(already_composed_path)
1428-
send_message(Result.new(id: id, response: { success: true }))
1429-
else
1430-
# This special error code makes the extension avoid restarting in case we already know that the composed
1431-
# bundle is not valid
1432-
send_message(
1433-
Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1434-
)
1435-
end
1456+
block.call(stderr, status)
14361457
end
14371458
end
14381459

lib/ruby_lsp/setup_bundler.rb

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def initialize(project_path, **options)
6565
@bundler_version = bundler_version #: Gem::Version?
6666
@rails_app = rails_app? #: bool
6767
@retry = false #: bool
68+
@needs_update_path = @custom_dir + "needs_update" #: Pathname
6869
end
6970

7071
# Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
@@ -260,31 +261,60 @@ def run_bundle_install(bundle_gemfile = @gemfile)
260261
#: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
261262
def run_bundle_install_directly(env, force_install: false)
262263
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
264+
return update(env) if @needs_update_path.exist?
263265

264266
# The ENV can only be merged after checking if an update is required because we depend on the original value of
265267
# ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
266-
should_update = should_bundle_update?
267-
ENV #: as untyped
268-
.merge!(env)
268+
FileUtils.touch(@needs_update_path) if should_bundle_update?
269+
ENV.merge!(env)
269270

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

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

281313
Bundler::CLI::Update.new({ conservative: true }, gems).run
282314
correct_relative_remote_paths if @custom_lockfile.exist?
315+
@needs_update_path.delete
283316
@last_updated_path.write(Time.now.iso8601)
284317
env
285-
rescue Bundler::GemNotFound, Bundler::GitError
286-
# If a gem is not installed, skip the upgrade and try to install it with a single retry
287-
@retry ? env : run_bundle_install_directly(env, force_install: true)
288318
end
289319

290320
#: (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)