From c8a86397dcb6f921a016bf45300d97ac52005de5 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Mon, 27 May 2019 11:47:37 +0800 Subject: [PATCH] (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. --- lib/puppet-languageserver/json_rpc_handler.rb | 2 +- lib/puppet-languageserver/message_router.rb | 64 +++++++++++++++++++ .../server_capabilities.rb | 6 ++ lib/puppet_languageserver.rb | 41 ++++++++++-- 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/lib/puppet-languageserver/json_rpc_handler.rb b/lib/puppet-languageserver/json_rpc_handler.rb index 821c5029..a475e022 100644 --- a/lib/puppet-languageserver/json_rpc_handler.rb +++ b/lib/puppet-languageserver/json_rpc_handler.rb @@ -278,7 +278,7 @@ def reply_result(result) end def reply_internal_error(message = nil) - return nil if @json_rpc_handler.error? + return nil if @json_rpc_handler.connection_error? @json_rpc_handler.reply_error(@id, CODE_INTERNAL_ERROR, message || MSG_INTERNAL_ERROR) end diff --git a/lib/puppet-languageserver/message_router.rb b/lib/puppet-languageserver/message_router.rb index 1a467e90..2b469075 100644 --- a/lib/puppet-languageserver/message_router.rb +++ b/lib/puppet-languageserver/message_router.rb @@ -247,4 +247,68 @@ def receive_notification(method, params) raise end end + + class DisabledMessageRouter + attr_accessor :json_rpc_handler + + def initialize(_options) + end + + def receive_request(request) + case request.rpc_method + when 'initialize' + PuppetLanguageServer.log_message(:debug, 'Received initialize method') + # If the Language Server is not active then we can not respond to any capability. We also + # send a warning to the user telling them this + request.reply_result('capabilities' => PuppetLanguageServer::ServerCapabilites.no_capabilities) + # Add a minor delay before sending the notification to give the client some processing time + sleep(0.5) + @json_rpc_handler.send_show_message_notification( + LSP::MessageType::WARNING, + 'An error occured while the Language Server was starting. The server has been disabled.' + ) + + when 'shutdown' + PuppetLanguageServer.log_message(:debug, 'Received shutdown method') + request.reply_result(nil) + + when 'puppet/getVersion' + # Clients may use the getVersion request to figure out when the server has "finished" loading. In this + # case just fake the response that we are fully loaded with unknown gem versions + request.reply_result(LSP::PuppetVersion.new( + 'puppetVersion' => 'Unknown', + 'facterVersion' => 'Unknown', + 'factsLoaded' => true, + 'functionsLoaded' => true, + 'typesLoaded' => true, + 'classesLoaded' => true + )) + + else + # For any request return an internal error. + request.reply_internal_error('Puppet Language Server is not active') + PuppetLanguageServer.log_message(:error, "Unknown RPC method #{request.rpc_method}") + end + rescue StandardError => e + PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'request' => request.rpc_method, 'params' => request.params) + raise + end + + def receive_notification(method, params) + case method + when 'initialized' + PuppetLanguageServer.log_message(:info, 'Client has received initialization') + + when 'exit' + PuppetLanguageServer.log_message(:info, 'Received exit notification. Closing connection to client...') + @json_rpc_handler.close_connection + + else + PuppetLanguageServer.log_message(:error, "Unknown RPC notification #{method}") + end + rescue StandardError => e + PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'notification' => method, 'params' => params) + raise + end + end end diff --git a/lib/puppet-languageserver/server_capabilities.rb b/lib/puppet-languageserver/server_capabilities.rb index a880667c..d418d168 100644 --- a/lib/puppet-languageserver/server_capabilities.rb +++ b/lib/puppet-languageserver/server_capabilities.rb @@ -17,5 +17,11 @@ def self.capabilities 'workspaceSymbolProvider' => true } end + + def self.no_capabilities + # Any empty hash denotes no capabilities at all + { + } + end end end diff --git a/lib/puppet_languageserver.rb b/lib/puppet_languageserver.rb index 283f29b3..a11be38c 100644 --- a/lib/puppet_languageserver.rb +++ b/lib/puppet_languageserver.rb @@ -16,9 +16,18 @@ def self.version PuppetEditorServices.version end + # Whether the language server is actually in a state that can be used. + # Typically this is false when a catastrophic error occurs during startup e.g. Puppet is missing. + # + # @return [Bool] Whether the language server is actually in a state that can be used + def self.active? + @server_is_active + end + def self.require_gems(options) original_verbose = $VERBOSE $VERBOSE = nil + @server_is_active = false # Use specific Puppet Gem version if possible unless options[:puppet_version].nil? @@ -32,16 +41,33 @@ def self.require_gems(options) end end - require 'lsp/lsp' - require 'puppet' - + # These libraries do not require the puppet gem and required for the + # server to respond to clients. %w[ json_rpc_handler document_store crash_dump message_router - validation_queue server_capabilities + ].each do |lib| + begin + require "puppet-languageserver/#{lib}" + rescue LoadError + require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', lib)) + end + end + + begin + require 'lsp/lsp' + require 'puppet' + rescue LoadError => e + log_message(:error, "Error while loading a critical gem: #{e} #{e.backtrace}") + return + end + + # These libraries require the puppet and LSP gems. + %w[ + validation_queue sidecar_protocol sidecar_queue puppet_parser_helper @@ -57,6 +83,7 @@ def self.require_gems(options) require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', lib)) end end + @server_is_active = true ensure $VERBOSE = original_verbose end @@ -165,6 +192,7 @@ def self.init_puppet(options) log_message(:info, "Language Server is v#{PuppetEditorServices.version}") log_message(:debug, 'Loading gems...') require_gems(options) + return unless active? log_message(:info, "Using Puppet v#{Puppet.version}") log_message(:debug, "Detected additional puppet settings #{options[:puppet_settings]}") @@ -220,6 +248,11 @@ def self.rpc_server(options) log_message(:info, 'Starting RPC Server...') options[:servicename] = 'LANGUAGE SERVER' + unless active? + options[:message_router] = @message_router = PuppetLanguageServer::DisabledMessageRouter.new(options) + log_message(:info, 'Configured the Language Server to use the Disabled Message Router') + end + if options[:stdio] log_message(:debug, 'Using STDIO') server = PuppetEditorServices::SimpleSTDIOServer.new