lib/theme_check/language_server/server.rb in theme-check-1.7.0 vs lib/theme_check/language_server/server.rb in theme-check-1.7.1
- old
+ new
@@ -11,71 +11,59 @@
class Server
attr_reader :handler
attr_reader :should_raise_errors
def initialize(
- in_stream: STDIN,
- out_stream: STDOUT,
- err_stream: STDERR,
+ messenger:,
should_raise_errors: false,
number_of_threads: 2
)
- validate!([in_stream, out_stream, err_stream])
+ # This is what does the IO
+ @messenger = messenger
- @handler = Handler.new(self)
- @in = in_stream
- @out = out_stream
- @err = err_stream
+ # This is what you use to communicate with the language client
+ @bridge = Bridge.new(@messenger)
- # Because programming is fun,
- #
- # Ruby on Windows turns \n into \r\n. Which means that \r\n
- # gets turned into \r\r\n. Which means that the protocol
- # breaks on windows unless we turn STDOUT into binary mode.
- #
- # Hours wasted: 9.
- @out.binmode
+ # The handler handles messages from the language client
+ @handler = Handler.new(@bridge)
- @out.sync = true # do not buffer
- @err.sync = true # do not buffer
-
# The queue holds the JSON RPC messages
@queue = Queue.new
# The JSON RPC thread pushes messages onto the queue
@json_rpc_thread = nil
# The handler threads read messages from the queue
@number_of_threads = number_of_threads
@handlers = []
- # The messenger permits requests to be made from the handler
- # to the language client and for those messages to be resolved in place.
- @messenger = Messenger.new
-
# The error queue holds blocks the main thread. When filled, we exit the program.
@error = SizedQueue.new(1)
@should_raise_errors = should_raise_errors
end
def listen
start_handler_threads
start_json_rpc_thread
- status_code_from_error(@error.pop)
+ status_code = status_code_from_error(@error.pop)
+ cleanup(status_code)
rescue SignalException
0
- ensure
- cleanup
end
def start_json_rpc_thread
@json_rpc_thread = Thread.new do
loop do
- message = read_json_rpc_message
+ message = @bridge.read_message
if message['method'] == 'initialize'
handle_message(message)
+ elsif message.key?('result')
+ # Responses are handled on the main thread to prevent
+ # a potential deadlock caused by all handlers waiting
+ # for a responses.
+ handle_response(message)
else
@queue << message
end
rescue Exception => e # rubocop:disable Lint/RescueException
break @error << e
@@ -104,116 +92,54 @@
rescue SignalException, DoneStreaming
0
rescue Exception => e # rubocop:disable Lint/RescueException
raise e if should_raise_errors
- log(e)
- log(e.backtrace)
+ @bridge.log(e)
+ @bridge.log(e.backtrace)
2
end
- def request(&block)
- @messenger.request(&block)
- end
-
- def send_message(message)
- message_body = JSON.dump(message)
- log(JSON.pretty_generate(message)) if $DEBUG
-
- @out.write("Content-Length: #{message_body.bytesize}\r\n")
- @out.write("\r\n")
- @out.write(message_body)
- @out.flush
- end
-
- def log(message)
- @err.puts(message)
- @err.flush
- end
-
private
- def supported_io_classes
- [IO, StringIO]
- end
-
- def validate!(streams = [])
- streams.each do |stream|
- unless supported_io_classes.find { |klass| stream.is_a?(klass) }
- raise IncompatibleStream, incompatible_stream_message
- end
- end
- end
-
- def incompatible_stream_message
- 'if provided, in_stream, out_stream, and err_stream must be a kind of '\
- "one of the following: #{supported_io_classes.join(', ')}"
- end
-
- def read_json_rpc_message
- message_body = read_new_content
- message_json = JSON.parse(message_body)
- log(JSON.pretty_generate(message_json)) if $DEBUG
- message_json
- end
-
def handle_message(message)
id = message['id']
method_name = message['method']
method_name &&= "on_#{to_snake_case(method_name)}"
params = message['params']
- result = message['result']
- if message.key?('result')
- @messenger.respond(id, result)
- elsif @handler.respond_to?(method_name)
+ if @handler.respond_to?(method_name)
@handler.send(method_name, id, params)
end
end
+ def handle_response(message)
+ id = message['id']
+ result = message['result']
+ @bridge.receive_response(id, result)
+ end
+
def to_snake_case(method_name)
StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
end
- def initial_line
- # Scanning for lines that fit the protocol.
- while true
- initial_line = @in.gets
- # gets returning nil means the stream was closed.
- raise DoneStreaming if initial_line.nil?
-
- if initial_line.match(/Content-Length: (\d+)/)
- break
- end
- end
- initial_line
- end
-
- def read_new_content
- length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
- content = ''
- while content.length < length + 2
- # Why + 2? Because \r\n
- content += @in.read(length + 2)
- raise DoneStreaming if @in.closed?
- end
-
- content
- end
-
- def cleanup
+ def cleanup(status_code)
# Stop listenting to RPC calls
- @in.close unless @in.closed?
+ @messenger.close_input
# Wait for rpc loop to close
@json_rpc_thread&.join if @json_rpc_thread&.alive?
# Close the queue
@queue.close unless @queue.closed?
# Give 10 seconds for the handlers to wrap up what they were
# doing/emptying the queue. 👀 unit tests.
@handlers.each { |thread| thread.join(10) if thread.alive? }
+
+ # Hijack the status_code if an error occurred while cleaning up.
+ # 👀 unit tests.
+ return status_code_from_error(@error.pop) unless @error.empty?
+ status_code
ensure
- @err.close
- @out.close
+ @messenger.close_output
end
end
end
end