File: ruby-lsp-launcher

package info (click to toggle)
ruby-ruby-lsp 0.26.7-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,676 kB
  • sloc: ruby: 35,294; javascript: 29; sh: 7; makefile: 4
file content (178 lines) | stat: -rwxr-xr-x 7,253 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env ruby
# frozen_string_literal: true

# !!!!!!!
# No gems can be required in this file until we invoke bundler setup except inside the forked process that sets up the
# composed bundle
# !!!!!!!

$stdin.sync = true
$stdout.sync = true
$stderr.sync = true
$stdin.binmode
$stdout.binmode
$stderr.binmode

setup_error = nil
install_error = nil
reboot = false

workspace_uri = ARGV.first
raw_initialize_path = File.join(".ruby-lsp", "raw_initialize")

raw_initialize = if workspace_uri && !workspace_uri.start_with?("--")
  # If there's an argument without `--`, then it's the server asking to compose the bundle and passing to this
  # executable the workspace URI. We can't require gems at this point, so we built a fake initialize request manually
  reboot = true
  "{\"params\":{\"workspaceFolders\":[{\"uri\":\"#{workspace_uri}\"}]}}"
elsif ARGV.include?("--retry")
  # If we're trying to re-boot automatically, we can't try to read the same initialize request again from the pipe. We
  # need to ensure that the retry mechanism always writes the request to a file, so that we can reuse it
  content = File.read(raw_initialize_path)
  File.delete(raw_initialize_path)
  content
else
  # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
  # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
  # to ensure that we're setting up the bundle in the right place
  headers = $stdin.gets("\r\n\r\n")
  content_length = headers[/Content-Length: (\d+)/i, 1].to_i
  $stdin.read(content_length)
end

# Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
# `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
pid = if Gem.win_platform?
  # Since we can't fork on Windows and spawn won't carry over the existing load paths, we need to explicitly pass that
  # down to the child process or else requiring gems during composing the bundle will fail
  load_path = $LOAD_PATH.flat_map do |path|
    ["-I", File.expand_path(path)]
  end

  Process.spawn(
    Gem.ruby,
    *load_path,
    File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__),
    raw_initialize,
  )
else
  fork do
    require_relative "../lib/ruby_lsp/scripts/compose_bundle"
    compose(raw_initialize)
  end
end

begin
  # Wait until the composed Bundle is finished
  _, status = Process.wait2(pid)
rescue Errno::ECHILD
  # In theory, the child process can finish before we even get to the wait call, but that is not an error
end

begin
  bundle_env_path = File.join(".ruby-lsp", "bundle_env")
  # We can't require `bundler/setup` because that file prematurely exits the process if setup fails. However, we can't
  # simply require bundler either because the version required might conflict with the one locked in the composed
  # bundle. We need the composed bundle sub-process to inform us of the locked Bundler version, so that we can then
  # activate the right spec and require the exact Bundler version required by the app
  if File.exist?(bundle_env_path)
    env = File.readlines(bundle_env_path).to_h { |line| line.chomp.split("=", 2) }
    ENV.merge!(env)

    if env["BUNDLER_VERSION"]
      Gem::Specification.find_by_name("bundler", env["BUNDLER_VERSION"]).activate
    end

    require "bundler"
    Bundler.ui.level = :silent

    # This Marshal load can only happen after requiring Bundler because it will load a custom error class from Bundler
    # itself. If we try to load before requiring, the class will not be defined and loading will fail
    error_path = File.join(".ruby-lsp", "install_error")
    install_error = begin
      Marshal.load(File.read(error_path)) if File.exist?(error_path)
    rescue ArgumentError
      # The class we tried to load is not defined. This might happen when the user upgrades Bundler and new error
      # classes are introduced or removed
      File.delete(error_path)
      nil
    end

    Bundler.setup
    $stderr.puts("Composed Bundle set up successfully")
  end
rescue Bundler::GemNotFound, Bundler::GitError => e
  # Sometimes, we successfully set up the bundle, but users either change their Gemfile or uninstall gems from an
  # external process. If there's no install error, but the gem is still not found, then we need to attempt to start from
  # scratch
  unless install_error || ARGV.include?("--retry")
    $stderr.puts("Initial bundle compose succeeded, but Bundler.setup failed. Trying to restart from scratch...")
    File.write(raw_initialize_path, raw_initialize)

    Bundler.with_unbundled_env do
      exec(Gem.ruby, __FILE__, *ARGV, "--retry")
    end
  end

  setup_error = e
  $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
rescue StandardError => e
  setup_error = e
  $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")

  # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
  # in degraded mode
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
end

# When performing a lockfile re-boot, this executable is invoked to set up the composed bundle ahead of time. In this
# flow, we are not booting the LSP yet, just checking if the bundle is valid before rebooting
if reboot
  # Use the exit status to signal to the server if composing the bundle succeeded
  exit(install_error || setup_error ? 1 : status&.exitstatus || 0)
end

# Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
# configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
# paths into the load path manually or we may end up requiring the wrong version of the gem
require "ruby_lsp/internal"

if ARGV.include?("--debug")
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
    $stderr.puts "Debugging is not supported on Windows"
  else
    begin
      ENV.delete("RUBY_DEBUG_IRB_CONSOLE")
      require "debug/open_nonstop"
    rescue LoadError
      $stderr.puts("You need to install the debug gem to use the --debug flag")
    end
  end
end

initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize

begin
  server = RubyLsp::Server.new(
    install_error: install_error,
    setup_error: setup_error,
    initialize_request: initialize_request,
  )

  # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
  $> = $stderr

  server.start
rescue ArgumentError
  # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
  # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
  # request manually and then start the main loop
  server = RubyLsp::Server.new

  # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
  $> = $stderr

  server.process_message(initialize_request)
  server.start
end