-
Notifications
You must be signed in to change notification settings - Fork 265
Expand file tree
/
Copy pathprocess_client.rb
More file actions
171 lines (137 loc) · 4.9 KB
/
Copy pathprocess_client.rb
File metadata and controls
171 lines (137 loc) · 4.9 KB
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
# typed: strict
# frozen_string_literal: true
module RubyLsp
class Addon
class ProcessClient
class InitializationError < StandardError; end
class IncompleteMessageError < StandardError; end
class EmptyMessageError < StandardError; end
MAX_RETRIES = 5
extend T::Sig
extend T::Generic
abstract!
sig { returns(Addon) }
attr_reader :addon
sig { returns(IO) }
attr_reader :stdin
sig { returns(IO) }
attr_reader :stdout
sig { returns(IO) }
attr_reader :stderr
sig { returns(Process::Waiter) }
attr_reader :wait_thread
sig { params(addon: Addon, command: String).void }
def initialize(addon, command)
@addon = T.let(addon, Addon)
@mutex = T.let(Mutex.new, Mutex)
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
# set its own session ID
begin
Process.setpgrp
Process.setsid
rescue Errno::EPERM
# If we can't set the session ID, continue
rescue NotImplementedError
# setpgrp() may be unimplemented on some platform
# https://github.com/Shopify/ruby-lsp-rails/issues/348
end
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
Open3.popen3(command)
end
@stdin = T.let(stdin, IO)
@stdout = T.let(stdout, IO)
@stderr = T.let(stderr, IO)
@wait_thread = T.let(wait_thread, Process::Waiter)
# for Windows compatibility
@stdin.binmode
@stdout.binmode
@stderr.binmode
log_output("booting server")
count = 0
begin
count += 1
handle_initialize_response(T.must(read_response))
rescue EmptyMessageError
log_output("is retrying initialize (#{count})")
retry if count < MAX_RETRIES
end
log_output("finished booting server")
register_exit_handler
rescue Errno::EPIPE, IncompleteMessageError
raise InitializationError, stderr.read
end
sig { void }
def shutdown
log_output("shutting down server")
send_message("shutdown")
sleep(0.5) # give the server a bit of time to shutdown
[stdin, stdout, stderr].each(&:close)
rescue IOError
# The server connection may have died
force_kill
end
sig { returns(T::Boolean) }
def stopped?
[stdin, stdout, stderr].all?(&:closed?) && !wait_thread.alive?
end
sig { params(message: String).void }
def log_output(message)
$stderr.puts("#{@addon.name} - #{message}")
end
# Notifications are like messages, but one-way, with no response sent back.
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
def send_notification(request, params = nil) = send_message(request, params)
private
sig do
params(
request: String,
params: T.nilable(T::Hash[Symbol, T.untyped]),
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
end
def make_request(request, params = nil)
send_message(request, params)
read_response
end
sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
def send_message(request, params = nil)
message = { method: request }
message[:params] = params if params
json = message.to_json
@mutex.synchronize do
@stdin.write("Content-Length: #{json.length}\r\n\r\n", json)
end
rescue Errno::EPIPE
# The server connection died
end
sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
def read_response
raw_response = @mutex.synchronize do
headers = @stdout.gets("\r\n\r\n")
raise IncompleteMessageError unless headers
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
raise EmptyMessageError if content_length.zero?
@stdout.read(content_length)
end
response = JSON.parse(T.must(raw_response), symbolize_names: true)
if response[:error]
log_output("error: " + response[:error])
return
end
response.fetch(:result)
rescue Errno::EPIPE
# The server connection died
nil
end
sig { void }
def force_kill
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
end
sig { abstract.void }
def register_exit_handler; end
sig { abstract.params(response: T::Hash[Symbol, T.untyped]).void }
def handle_initialize_response(response); end
end
end
end