Skip to content
This repository has been archived by the owner on Jul 27, 2024. It is now read-only.

Prevent server from hanging on error #623

Merged
merged 1 commit into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions lib/theme_check/language_server/bridge.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "timeout"

# This class exists as a bridge (or boundary) between our handlers and the outside world.
#
# It is concerned with all the Language Server Protocol constructs. i.e.
Expand Down Expand Up @@ -78,12 +80,17 @@ def send_response(id, result = nil, error = nil)

# https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
def send_internal_error(id, e)
# For a reason I can't comprehend, sometimes
# e.full_message _hangs_ and brings your CPU to 100%.
# It's wrapped in here because it prints anyway...
# This shit is weird, yo.
Timeout.timeout(1) do
$stderr.puts e.full_message
end
ensure
send_response(id, nil, {
code: ErrorCodes::INTERNAL_ERROR,
message: <<~EOS,
#{e.class}: #{e.message}
#{e.backtrace.join("\n ")}
EOS
message: "A theme-check-language-server has occured, inspect OUTPUT logs for details.",
})
end

Expand Down
56 changes: 42 additions & 14 deletions lib/theme_check/language_server/server.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require 'json'
require 'stringio'
require 'timeout'

module ThemeCheck
module LanguageServer
Expand Down Expand Up @@ -45,10 +47,29 @@ def initialize(
def listen
start_handler_threads
start_json_rpc_thread
status_code = status_code_from_error(@error.pop)
err = @error.pop
status_code = status_code_from_error(err)

if status_code > 0
# For a reason I can't comprehend, this hangs but prints
# anyway. So it's wrapped in this ugly timeout...
Timeout.timeout(1) do
$stderr.puts err.full_message
end

# Warn user of error, otherwise server might restart
# without telling you.
@bridge.send_notification("window/showMessage", {
type: 1,
message: "A theme-check-language-server error has occurred, search OUTPUT logs for details.",
})
end

cleanup(status_code)
rescue SignalException
0
rescue StandardError
2
end

def start_json_rpc_thread
Expand All @@ -65,36 +86,43 @@ def start_json_rpc_thread
else
@queue << message
end
rescue Exception => e # rubocop:disable Lint/RescueException
break @error << e
end
rescue Exception => e # rubocop:disable Lint/RescueException
@bridge.log("rescuing #{e.class} in jsonrpc thread")
@error << e
end
end

def start_handler_threads
@number_of_threads.times do
@handlers << Thread.new do
loop do
message = @queue.pop
break if @queue.closed? && @queue.empty?
handle_message(message)
rescue Exception => e # rubocop:disable Lint/RescueException
break @error << e
end
handle_messages
end
end
end

def handle_messages
loop do
message = @queue.pop
return if @queue.closed? && @queue.empty?

handle_message(message)
end
rescue Exception => e # rubocop:disable Lint/RescueException
@bridge.log("rescuing #{e.class} in handler thread")
@error << e
end

def status_code_from_error(e)
raise e

# support ctrl+c and stuff
rescue SignalException, DoneStreaming
0

rescue Exception => e # rubocop:disable Lint/RescueException
raise e if should_raise_errors
@bridge.log("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")

@bridge.log("Fatal #{e.class}")
2
end

Expand All @@ -109,15 +137,14 @@ def handle_message(message)
if @handler.respond_to?(method_name)
@handler.send(method_name, id, params)
end

rescue DoneStreaming => e
raise e
rescue StandardError => e
is_request = id
raise e unless is_request

# Errors obtained in request handlers should be sent
# back as internal errors instead of closing the program.
@bridge.log("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
@bridge.send_internal_error(id, e)
end

Expand All @@ -132,6 +159,7 @@ def to_snake_case(method_name)
end

def cleanup(status_code)
@bridge.log("Closing server... status code = #{status_code}")
# Stop listenting to RPC calls
@messenger.close_input
# Wait for rpc loop to close
Expand Down