Skip to content

Commit c8a8639

Browse files
committed
(GH-118) Fail gracefully when critical gems cannot load
Previously the language server would crash/terminate early if a critical gem like puppet was unavailable. This can happen when the ruby environment is not from the Puppet Agent or PDK. This commit changes the behaviour of the Language Server to still execute but it a completely disable fashion: * Detects a failed gem load for critical gems and sets PuppetLanguageServer.active? is to false * When the Language Server is not active, a different Message Router is used which effectively warns the user that the server failed to start and that all functions are disabled. The server responds to the client with no capabilities and the custom getVersion request, is responded to with unknown data No automated tests were added as this is an edge case. Manual testing was performed by changing the call `require 'puppet'` to `require 'puppetxxxxx'` which is enough to trigger a failure.
1 parent da5d292 commit c8a8639

File tree

4 files changed

+108
-5
lines changed

4 files changed

+108
-5
lines changed

lib/puppet-languageserver/json_rpc_handler.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def reply_result(result)
278278
end
279279

280280
def reply_internal_error(message = nil)
281-
return nil if @json_rpc_handler.error?
281+
return nil if @json_rpc_handler.connection_error?
282282
@json_rpc_handler.reply_error(@id, CODE_INTERNAL_ERROR, message || MSG_INTERNAL_ERROR)
283283
end
284284

lib/puppet-languageserver/message_router.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,68 @@ def receive_notification(method, params)
247247
raise
248248
end
249249
end
250+
251+
class DisabledMessageRouter
252+
attr_accessor :json_rpc_handler
253+
254+
def initialize(_options)
255+
end
256+
257+
def receive_request(request)
258+
case request.rpc_method
259+
when 'initialize'
260+
PuppetLanguageServer.log_message(:debug, 'Received initialize method')
261+
# If the Language Server is not active then we can not respond to any capability. We also
262+
# send a warning to the user telling them this
263+
request.reply_result('capabilities' => PuppetLanguageServer::ServerCapabilites.no_capabilities)
264+
# Add a minor delay before sending the notification to give the client some processing time
265+
sleep(0.5)
266+
@json_rpc_handler.send_show_message_notification(
267+
LSP::MessageType::WARNING,
268+
'An error occured while the Language Server was starting. The server has been disabled.'
269+
)
270+
271+
when 'shutdown'
272+
PuppetLanguageServer.log_message(:debug, 'Received shutdown method')
273+
request.reply_result(nil)
274+
275+
when 'puppet/getVersion'
276+
# Clients may use the getVersion request to figure out when the server has "finished" loading. In this
277+
# case just fake the response that we are fully loaded with unknown gem versions
278+
request.reply_result(LSP::PuppetVersion.new(
279+
'puppetVersion' => 'Unknown',
280+
'facterVersion' => 'Unknown',
281+
'factsLoaded' => true,
282+
'functionsLoaded' => true,
283+
'typesLoaded' => true,
284+
'classesLoaded' => true
285+
))
286+
287+
else
288+
# For any request return an internal error.
289+
request.reply_internal_error('Puppet Language Server is not active')
290+
PuppetLanguageServer.log_message(:error, "Unknown RPC method #{request.rpc_method}")
291+
end
292+
rescue StandardError => e
293+
PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'request' => request.rpc_method, 'params' => request.params)
294+
raise
295+
end
296+
297+
def receive_notification(method, params)
298+
case method
299+
when 'initialized'
300+
PuppetLanguageServer.log_message(:info, 'Client has received initialization')
301+
302+
when 'exit'
303+
PuppetLanguageServer.log_message(:info, 'Received exit notification. Closing connection to client...')
304+
@json_rpc_handler.close_connection
305+
306+
else
307+
PuppetLanguageServer.log_message(:error, "Unknown RPC notification #{method}")
308+
end
309+
rescue StandardError => e
310+
PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'notification' => method, 'params' => params)
311+
raise
312+
end
313+
end
250314
end

lib/puppet-languageserver/server_capabilities.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,11 @@ def self.capabilities
1717
'workspaceSymbolProvider' => true
1818
}
1919
end
20+
21+
def self.no_capabilities
22+
# Any empty hash denotes no capabilities at all
23+
{
24+
}
25+
end
2026
end
2127
end

lib/puppet_languageserver.rb

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ def self.version
1616
PuppetEditorServices.version
1717
end
1818

19+
# Whether the language server is actually in a state that can be used.
20+
# Typically this is false when a catastrophic error occurs during startup e.g. Puppet is missing.
21+
#
22+
# @return [Bool] Whether the language server is actually in a state that can be used
23+
def self.active?
24+
@server_is_active
25+
end
26+
1927
def self.require_gems(options)
2028
original_verbose = $VERBOSE
2129
$VERBOSE = nil
30+
@server_is_active = false
2231

2332
# Use specific Puppet Gem version if possible
2433
unless options[:puppet_version].nil?
@@ -32,16 +41,33 @@ def self.require_gems(options)
3241
end
3342
end
3443

35-
require 'lsp/lsp'
36-
require 'puppet'
37-
44+
# These libraries do not require the puppet gem and required for the
45+
# server to respond to clients.
3846
%w[
3947
json_rpc_handler
4048
document_store
4149
crash_dump
4250
message_router
43-
validation_queue
4451
server_capabilities
52+
].each do |lib|
53+
begin
54+
require "puppet-languageserver/#{lib}"
55+
rescue LoadError
56+
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', lib))
57+
end
58+
end
59+
60+
begin
61+
require 'lsp/lsp'
62+
require 'puppet'
63+
rescue LoadError => e
64+
log_message(:error, "Error while loading a critical gem: #{e} #{e.backtrace}")
65+
return
66+
end
67+
68+
# These libraries require the puppet and LSP gems.
69+
%w[
70+
validation_queue
4571
sidecar_protocol
4672
sidecar_queue
4773
puppet_parser_helper
@@ -57,6 +83,7 @@ def self.require_gems(options)
5783
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', lib))
5884
end
5985
end
86+
@server_is_active = true
6087
ensure
6188
$VERBOSE = original_verbose
6289
end
@@ -165,6 +192,7 @@ def self.init_puppet(options)
165192
log_message(:info, "Language Server is v#{PuppetEditorServices.version}")
166193
log_message(:debug, 'Loading gems...')
167194
require_gems(options)
195+
return unless active?
168196
log_message(:info, "Using Puppet v#{Puppet.version}")
169197

170198
log_message(:debug, "Detected additional puppet settings #{options[:puppet_settings]}")
@@ -220,6 +248,11 @@ def self.rpc_server(options)
220248
log_message(:info, 'Starting RPC Server...')
221249
options[:servicename] = 'LANGUAGE SERVER'
222250

251+
unless active?
252+
options[:message_router] = @message_router = PuppetLanguageServer::DisabledMessageRouter.new(options)
253+
log_message(:info, 'Configured the Language Server to use the Disabled Message Router')
254+
end
255+
223256
if options[:stdio]
224257
log_message(:debug, 'Using STDIO')
225258
server = PuppetEditorServices::SimpleSTDIOServer.new

0 commit comments

Comments
 (0)