From e0a499d194daf23e48a18f878b567ee779f6a3ae Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Sat, 1 Feb 2020 22:08:09 +0800 Subject: [PATCH 01/34] (maint) Allow travis to build 1.0 branch --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 00d741ec..bc73821b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,12 @@ os: # OSX is REALLY slow to test # - osx +# safelist +branches: + only: + - master + - "1.0" + matrix: fast_finish: true From 7a99d98f2e40f77007d5f10e37522973b6497636 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 28 Nov 2019 14:30:29 +0800 Subject: [PATCH 02/34] (GH-209) Create a session_state namespace This commit creates a session_state namespace. This will hold the document store, language client and the object cache. This just moves the files to preserve git history. --- lib/puppet-languageserver/{ => session_state}/document_store.rb | 0 lib/puppet-languageserver/{ => session_state}/language_client.rb | 0 .../{puppet_helper/cache.rb => session_state/object_cache.rb} | 0 .../{ => session_state}/document_store_spec.rb | 0 .../{ => session_state}/document_store_spec.rb | 0 .../{ => session_state}/language_client_spec.rb | 0 .../cache_spec.rb => session_state/object_cache_spec.rb} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename lib/puppet-languageserver/{ => session_state}/document_store.rb (100%) rename lib/puppet-languageserver/{ => session_state}/language_client.rb (100%) rename lib/puppet-languageserver/{puppet_helper/cache.rb => session_state/object_cache.rb} (100%) rename spec/languageserver/integration/puppet-languageserver/{ => session_state}/document_store_spec.rb (100%) rename spec/languageserver/unit/puppet-languageserver/{ => session_state}/document_store_spec.rb (100%) rename spec/languageserver/unit/puppet-languageserver/{ => session_state}/language_client_spec.rb (100%) rename spec/languageserver/unit/puppet-languageserver/{puppet_helper/cache_spec.rb => session_state/object_cache_spec.rb} (100%) diff --git a/lib/puppet-languageserver/document_store.rb b/lib/puppet-languageserver/session_state/document_store.rb similarity index 100% rename from lib/puppet-languageserver/document_store.rb rename to lib/puppet-languageserver/session_state/document_store.rb diff --git a/lib/puppet-languageserver/language_client.rb b/lib/puppet-languageserver/session_state/language_client.rb similarity index 100% rename from lib/puppet-languageserver/language_client.rb rename to lib/puppet-languageserver/session_state/language_client.rb diff --git a/lib/puppet-languageserver/puppet_helper/cache.rb b/lib/puppet-languageserver/session_state/object_cache.rb similarity index 100% rename from lib/puppet-languageserver/puppet_helper/cache.rb rename to lib/puppet-languageserver/session_state/object_cache.rb diff --git a/spec/languageserver/integration/puppet-languageserver/document_store_spec.rb b/spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb similarity index 100% rename from spec/languageserver/integration/puppet-languageserver/document_store_spec.rb rename to spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb diff --git a/spec/languageserver/unit/puppet-languageserver/document_store_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb similarity index 100% rename from spec/languageserver/unit/puppet-languageserver/document_store_spec.rb rename to spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb diff --git a/spec/languageserver/unit/puppet-languageserver/language_client_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb similarity index 100% rename from spec/languageserver/unit/puppet-languageserver/language_client_spec.rb rename to spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb diff --git a/spec/languageserver/unit/puppet-languageserver/puppet_helper/cache_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb similarity index 100% rename from spec/languageserver/unit/puppet-languageserver/puppet_helper/cache_spec.rb rename to spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb From 051a28b0a25d761e7c9741e96845a8265342fd66 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 28 Nov 2019 16:50:57 +0800 Subject: [PATCH 03/34] (GH-209) Create session_state and refactor DocumentStore This commit: * Creates a ClientSessionState object which holds all of the state information for a single client. * Changes all of the object references to the new SessionState namespace. * Adds a ducktype of the old PuppetLanguageServer::DocumentStore module and uses the ClientSessionState behind the scenes. Later commits will remove this module. --- .../client_session_state.rb | 21 ++ lib/puppet-languageserver/message_handler.rb | 106 +++++- lib/puppet-languageserver/puppet_helper.rb | 11 +- .../session_state/document_store.rb | 305 +++++++++--------- .../session_state/language_client.rb | 294 ++++++++--------- .../session_state/object_cache.rb | 4 +- lib/puppet-languageserver/uri_helper.rb | 10 +- lib/puppet_languageserver.rb | 14 +- .../session_state/document_store_spec.rb | 76 ++--- .../sidecar_queue_spec.rb | 2 +- .../manifest/completion_provider_spec.rb | 2 +- .../manifest/document_symbol_provider_spec.rb | 2 +- .../message_handler_spec.rb | 8 +- .../puppet_helper_spec.rb | 2 +- .../session_state/document_store_spec.rb | 5 +- .../session_state/language_client_spec.rb | 4 +- .../session_state/object_cache_spec.rb | 4 +- .../sidecar_protocol_spec.rb | 1 - 18 files changed, 482 insertions(+), 389 deletions(-) create mode 100644 lib/puppet-languageserver/client_session_state.rb diff --git a/lib/puppet-languageserver/client_session_state.rb b/lib/puppet-languageserver/client_session_state.rb new file mode 100644 index 00000000..43e257d6 --- /dev/null +++ b/lib/puppet-languageserver/client_session_state.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'puppet-languageserver/session_state/document_store' +require 'puppet-languageserver/session_state/language_client' +require 'puppet-languageserver/session_state/object_cache' + +module PuppetLanguageServer + class ClientSessionState + attr_reader :documents + + attr_reader :language_client + + attr_reader :object_cache + + def initialize(message_handler, options = {}) + @documents = options[:documents].nil? ? PuppetLanguageServer::SessionState::DocumentStore.new : options[:documents] + @language_client = options[:language_client].nil? ? PuppetLanguageServer::SessionState::LanguageClient.new(message_handler) : options[:language_client] + @object_cache = options[:object_cache].nil? ? PuppetLanguageServer::SessionState::ObjectCache.new : options[:object_cache] + end + end +end diff --git a/lib/puppet-languageserver/message_handler.rb b/lib/puppet-languageserver/message_handler.rb index e1725e6d..74cafa0f 100644 --- a/lib/puppet-languageserver/message_handler.rb +++ b/lib/puppet-languageserver/message_handler.rb @@ -3,18 +3,81 @@ require 'puppet_editor_services/handler/json_rpc' require 'puppet_editor_services/protocol/json_rpc_messages' require 'puppet-languageserver/server_capabilities' +require 'puppet-languageserver/client_session_state' module PuppetLanguageServer - class MessageHandler < PuppetEditorServices::Handler::JsonRPC - attr_reader :language_client + # This module is just duck-typing the old PuppetLanguageServer::DocumentStore Module. + # This will eventually be refactored out. But for now, this exists for backwards compatibility + module DocumentStore + def self.instance + @instance ||= PuppetLanguageServer::SessionState::DocumentStore.new + end + + def self.set_document(uri, content, doc_version) + instance.set_document(uri, content, doc_version) + end + + def self.remove_document(uri) + instance.remove_document(uri) + end + + def self.clear + instance.clear + end + + def self.document(uri, doc_version = nil) + instance.document(uri, doc_version) + end + + def self.document_version(uri) + instance.document_version(uri) + end + + def self.document_uris + instance.document_uris + end + + def self.document_type(uri) + instance.document_type(uri) + end + + def self.plan_file?(uri) + instance.plan_file?(uri) + end + + def self.initialize_store(options = {}) + instance.initialize_store(options) + end + def self.expire_store_information + instance.expire_store_information + end + + def self.store_root_path + instance.store_root_path + end + + def self.store_has_module_metadata? + instance.store_has_module_metadata? + end + + def self.store_has_environmentconf? + instance.store_has_environmentconf? + end + end + + class MessageHandler < PuppetEditorServices::Handler::JsonRPC def initialize(*_) super - @language_client = LanguageClient.new(self) + @session_state = ClientSessionState.new(self, :documents => DocumentStore.instance) + end + + def language_client + @session_state.language_client end def documents - PuppetLanguageServer::DocumentStore + @session_state.documents end def request_initialize(_, json_rpc_message) @@ -25,6 +88,17 @@ def request_initialize(_, json_rpc_message) :documentOnTypeFormattingProvider => !language_client.client_capability('textDocument', 'onTypeFormatting', 'dynamicRegistration') } + # Configure the document store + documents.initialize_store( + :workspace => workspace_root_from_initialize_params(json_rpc_message.params) + ) + + # Initiate loading of the workspace if needed + if documents.store_has_module_metadata? || documents.store_has_environmentconf? + PuppetLanguageServer.log_message(:info, 'Loading Workspace (Async)...') + PuppetLanguageServer::PuppetHelper.load_workspace_async + end + { 'capabilities' => PuppetLanguageServer::ServerCapabilites.capabilities(info) } end @@ -110,7 +184,7 @@ def request_textdocument_completion(_, json_rpc_message) case documents.document_type(file_uri) when :manifest - PuppetLanguageServer::Manifest::CompletionProvider.complete(content, line_num, char_num, :context => context, :tasks_mode => PuppetLanguageServer::DocumentStore.plan_file?(file_uri)) + PuppetLanguageServer::Manifest::CompletionProvider.complete(content, line_num, char_num, :context => context, :tasks_mode => documents.plan_file?(file_uri)) else raise "Unable to provide completion on #{file_uri}" end @@ -134,7 +208,7 @@ def request_textdocument_hover(_, json_rpc_message) content = documents.document(file_uri) case documents.document_type(file_uri) when :manifest - PuppetLanguageServer::Manifest::HoverProvider.resolve(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.plan_file?(file_uri)) + PuppetLanguageServer::Manifest::HoverProvider.resolve(content, line_num, char_num, :tasks_mode => documents.plan_file?(file_uri)) else raise "Unable to provide hover on #{file_uri}" end @@ -151,7 +225,7 @@ def request_textdocument_definition(_, json_rpc_message) case documents.document_type(file_uri) when :manifest - PuppetLanguageServer::Manifest::DefinitionProvider.find_definition(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.plan_file?(file_uri)) + PuppetLanguageServer::Manifest::DefinitionProvider.find_definition(content, line_num, char_num, :tasks_mode => documents.plan_file?(file_uri)) else raise "Unable to provide definition on #{file_uri}" end @@ -166,7 +240,7 @@ def request_textdocument_documentsymbol(_, json_rpc_message) case documents.document_type(file_uri) when :manifest - PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content, :tasks_mode => PuppetLanguageServer::DocumentStore.plan_file?(file_uri)) + PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content, :tasks_mode => documents.plan_file?(file_uri)) else raise "Unable to provide definition on #{file_uri}" end @@ -211,7 +285,7 @@ def request_textdocument_signaturehelp(_, json_rpc_message) content, line_num, char_num, - :tasks_mode => PuppetLanguageServer::DocumentStore.plan_file?(file_uri) + :tasks_mode => documents.plan_file?(file_uri) ) else raise "Unable to provide signatures on #{file_uri}" @@ -280,8 +354,8 @@ def notification_textdocument_didchange(client_handler_id, json_rpc_message) def notification_textdocument_didsave(_, _json_rpc_message) PuppetLanguageServer.log_message(:info, 'Received textDocument/didSave notification.') # Expire the store cache so that the store information can re-evaluated - PuppetLanguageServer::DocumentStore.expire_store_information - if PuppetLanguageServer::DocumentStore.store_has_module_metadata? || PuppetLanguageServer::DocumentStore.store_has_environmentconf? + documents.expire_store_information + if documents.store_has_module_metadata? || documents.store_has_environmentconf? # Load the workspace information PuppetLanguageServer::PuppetHelper.load_workspace_async else @@ -332,6 +406,16 @@ def enqueue_validation(file_uri, doc_version, client_handler_id) end PuppetLanguageServer::ValidationQueue.enqueue(file_uri, doc_version, client_handler_id, options) end + + def workspace_root_from_initialize_params(params) + if params.key?('workspaceFolders') + return nil if params['workspaceFolders'].nil? || params['workspaceFolders'].empty? + # We don't support multiple workspace folders yet, so just select the first one + return UriHelper.uri_path(params['workspaceFolders'][0]['uri']) + end + return UriHelper.uri_path(params['rootUri']) if params.key?('rootUri') + params['rootPath'] + end end class DisabledMessageHandler < PuppetEditorServices::Handler::JsonRPC diff --git a/lib/puppet-languageserver/puppet_helper.rb b/lib/puppet-languageserver/puppet_helper.rb index c01b63cf..e19896f0 100644 --- a/lib/puppet-languageserver/puppet_helper.rb +++ b/lib/puppet-languageserver/puppet_helper.rb @@ -2,14 +2,7 @@ require 'pathname' require 'tempfile' - -%w[puppet_helper/cache].each do |lib| - begin - require "puppet-languageserver/#{lib}" - rescue LoadError - require File.expand_path(File.join(File.dirname(__FILE__), lib)) - end -end +require 'puppet-languageserver/session_state/object_cache' module PuppetLanguageServer module PuppetHelper @@ -24,7 +17,7 @@ module PuppetHelper def self.initialize_helper(options = {}) @helper_options = options - @inmemory_cache = PuppetLanguageServer::PuppetHelper::Cache.new + @inmemory_cache = PuppetLanguageServer::SessionState::ObjectCache.new sidecar_queue.cache = @inmemory_cache end diff --git a/lib/puppet-languageserver/session_state/document_store.rb b/lib/puppet-languageserver/session_state/document_store.rb index e95a687e..b890e916 100644 --- a/lib/puppet-languageserver/session_state/document_store.rb +++ b/lib/puppet-languageserver/session_state/document_store.rb @@ -1,184 +1,191 @@ # frozen_string_literal: true module PuppetLanguageServer - module DocumentStore - @documents = {} - @doc_mutex = Mutex.new - - def self.set_document(uri, content, doc_version) - @doc_mutex.synchronize do - @documents[uri] = { - :content => content, - :version => doc_version - } + module SessionState + class DocumentStore + # @param options :workspace Path to the workspace + def initialize(_options = {}) + @documents = {} + @doc_mutex = Mutex.new + + initialize_store end - end - def self.remove_document(uri) - @doc_mutex.synchronize { @documents[uri] = nil } - end + def set_document(uri, content, doc_version) + @doc_mutex.synchronize do + @documents[uri] = { + :content => content, + :version => doc_version + } + end + end - def self.clear - @doc_mutex.synchronize { @documents.clear } - end + def remove_document(uri) + @doc_mutex.synchronize { @documents[uri] = nil } + end - def self.document(uri, doc_version = nil) - @doc_mutex.synchronize do - return nil if @documents[uri].nil? - return nil unless doc_version.nil? || @documents[uri][:version] == doc_version - @documents[uri][:content].clone + def clear + @doc_mutex.synchronize { @documents.clear } end - end - def self.document_version(uri) - @doc_mutex.synchronize do - return nil if @documents[uri].nil? - @documents[uri][:version] + def document(uri, doc_version = nil) + @doc_mutex.synchronize do + return nil if @documents[uri].nil? + return nil unless doc_version.nil? || @documents[uri][:version] == doc_version + @documents[uri][:content].clone + end end - end - def self.document_uris - @doc_mutex.synchronize { @documents.keys.dup } - end + def document_version(uri) + @doc_mutex.synchronize do + return nil if @documents[uri].nil? + @documents[uri][:version] + end + end - def self.document_type(uri) - case uri - when /\/Puppetfile$/i - :puppetfile - when /\.pp$/i - :manifest - when /\.epp$/i - :epp - else - :unknown + def document_uris + @doc_mutex.synchronize { @documents.keys.dup } end - end - # Plan files https://puppet.com/docs/bolt/1.x/writing_plans.html#concept-4485 can exist in many places - # The current best detection method is as follows: - # "Given the full path to the .pp file, if it contains a directory called plans, AND that plans is not a sub-directory of manifests, then it is a plan file" - # - # See https://github.com/lingua-pupuli/puppet-editor-services/issues/129 for the full discussion - def self.plan_file?(uri) - uri_path = PuppetLanguageServer::UriHelper.uri_path(uri) - return false if uri_path.nil? - if windows? - plans_index = uri_path.upcase.index('/PLANS/') - manifests_index = uri_path.upcase.index('/MANIFESTS/') - else - plans_index = uri_path.index('/plans/') - manifests_index = uri_path.index('/manifests/') - end - return false if plans_index.nil? - return true if manifests_index.nil? - - plans_index < manifests_index - end + def document_type(uri) + case uri + when /\/Puppetfile$/i + :puppetfile + when /\.pp$/i + :manifest + when /\.epp$/i + :epp + else + :unknown + end + end - # Workspace management - WORKSPACE_CACHE_TTL_SECONDS = 60 - def self.initialize_store(options = {}) - @workspace_path = options[:workspace] - @workspace_info_cache = { - :expires => Time.new - 120 - } - end + # Plan files https://puppet.com/docs/bolt/1.x/writing_plans.html#concept-4485 can exist in many places + # The current best detection method is as follows: + # "Given the full path to the .pp file, if it contains a directory called plans, AND that plans is not a sub-directory of manifests, then it is a plan file" + # + # See https://github.com/lingua-pupuli/puppet-editor-services/issues/129 for the full discussion + def plan_file?(uri) + uri_path = PuppetLanguageServer::UriHelper.uri_path(uri) + return false if uri_path.nil? + # For the text searching below we need a leading slash. That way + # we don't need to use regexes which is slower + uri_path = '/' + uri_path unless uri_path.start_with?('/') + if windows? + plans_index = uri_path.upcase.index('/PLANS/') + manifests_index = uri_path.upcase.index('/MANIFESTS/') + else + plans_index = uri_path.index('/plans/') + manifests_index = uri_path.index('/manifests/') + end + return false if plans_index.nil? + return true if manifests_index.nil? - def self.expire_store_information - @doc_mutex.synchronize do - @workspace_info_cache[:expires] = Time.new - 120 + plans_index < manifests_index end - end - def self.store_root_path - store_details[:root_path] - end + # Workspace management + WORKSPACE_CACHE_TTL_SECONDS ||= 60 + def initialize_store(options = {}) + @workspace_path = options[:workspace] + @workspace_info_cache = { + :expires => Time.new - 120 + } + end - def self.store_has_module_metadata? - store_details[:has_metadatajson] - end + def expire_store_information + @doc_mutex.synchronize do + @workspace_info_cache[:expires] = Time.new - 120 + end + end - def self.store_has_environmentconf? - store_details[:has_environmentconf] - end + def store_root_path + store_details[:root_path] + end + + def store_has_module_metadata? + store_details[:has_metadatajson] + end - # Given a path, locate a metadata.json or environment.conf file to determine where the - # root of the module/control repo actually is - def self.find_root_path(path) - return nil if path.nil? - filepath = File.expand_path(path) - - if dir_exist?(filepath) - directory = filepath - elsif file_exist?(filepath) - directory = File.dirname(filepath) - else - return nil - end - - until directory.nil? - break if file_exist?(File.join(directory, 'metadata.json')) || file_exist?(File.join(directory, 'environment.conf')) - parent = File.dirname(directory) - # If the parent is the same as the original, then we've reached the end of the path chain - if parent == directory - directory = nil + def store_has_environmentconf? + store_details[:has_environmentconf] + end + + private + + # Given a path, locate a metadata.json or environment.conf file to determine where the + # root of the module/control repo actually is + def find_root_path(path) + return nil if path.nil? + filepath = File.expand_path(path) + + if dir_exist?(filepath) + directory = filepath + elsif file_exist?(filepath) + directory = File.dirname(filepath) else - directory = parent + return nil + end + + until directory.nil? + break if file_exist?(File.join(directory, 'metadata.json')) || file_exist?(File.join(directory, 'environment.conf')) + parent = File.dirname(directory) + # If the parent is the same as the original, then we've reached the end of the path chain + if parent == directory + directory = nil + else + directory = parent + end end + + directory end - directory - end - private_class_method :find_root_path - - def self.store_details - return @workspace_info_cache unless @workspace_info_cache[:never_expires] || @workspace_info_cache[:expires] < Time.new - # TTL has expired, time to calculate the document store details - - new_cache = { - :root_path => nil, - :has_environmentconf => false, - :has_metadatajson => false - } - if @workspace_path.nil? - # If we have never been given a local workspace path on the command line then there is really no - # way to know where the module file system path is. Therefore the root_path is nil and assume that - # environment.conf and metadata.json does not exist. And don't bother trying to re-evaluate - new_cache[:never_expires] = true - else - root_path = find_root_path(@workspace_path) - if root_path.nil? - new_cache[:root_path] = @workspace_path + def store_details + return @workspace_info_cache unless @workspace_info_cache[:never_expires] || @workspace_info_cache[:expires] < Time.new + # TTL has expired, time to calculate the document store details + + new_cache = { + :root_path => nil, + :has_environmentconf => false, + :has_metadatajson => false + } + if @workspace_path.nil? + # If we have never been given a local workspace path on the command line then there is really no + # way to know where the module file system path is. Therefore the root_path is nil and assume that + # environment.conf and metadata.json does not exist. And don't bother trying to re-evaluate + new_cache[:never_expires] = true else - new_cache[:root_path] = root_path - new_cache[:has_metadatajson] = file_exist?(File.join(root_path, 'metadata.json')) - new_cache[:has_environmentconf] = file_exist?(File.join(root_path, 'environment.conf')) + root_path = find_root_path(@workspace_path) + if root_path.nil? + new_cache[:root_path] = @workspace_path + else + new_cache[:root_path] = root_path + new_cache[:has_metadatajson] = file_exist?(File.join(root_path, 'metadata.json')) + new_cache[:has_environmentconf] = file_exist?(File.join(root_path, 'environment.conf')) + end end - end - new_cache[:expires] = Time.new + WORKSPACE_CACHE_TTL_SECONDS + new_cache[:expires] = Time.new + WORKSPACE_CACHE_TTL_SECONDS - @doc_mutex.synchronize do - @workspace_info_cache = new_cache + @doc_mutex.synchronize do + @workspace_info_cache = new_cache + end + @workspace_info_cache end - @workspace_info_cache - end - private_class_method :store_details - def self.file_exist?(path) - File.exist?(path) && !File.directory?(path) - end - private_class_method :file_exist? + def file_exist?(path) + File.exist?(path) && !File.directory?(path) + end - def self.dir_exist?(path) - Dir.exist?(path) - end - private_class_method :dir_exist? + def dir_exist?(path) + Dir.exist?(path) + end - def self.windows? - # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard - # library uses that to test what platform it's on. - !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation + def windows? + # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard + # library uses that to test what platform it's on. + !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation + end end - private_class_method :windows? end end diff --git a/lib/puppet-languageserver/session_state/language_client.rb b/lib/puppet-languageserver/session_state/language_client.rb index 5261ae7b..c8d4fbea 100644 --- a/lib/puppet-languageserver/session_state/language_client.rb +++ b/lib/puppet-languageserver/session_state/language_client.rb @@ -1,192 +1,194 @@ # frozen_string_literal: true module PuppetLanguageServer - class LanguageClient - attr_reader :message_handler - - # Client settings - attr_reader :format_on_type - attr_reader :use_puppetfile_resolver - - def initialize(message_handler) - @message_handler = message_handler - @client_capabilites = {} - - # Internal registry of dynamic registrations and their current state - # @registrations[ <[String] method_name>] = [ - # { - # :id => [String] Request ID. Used for de-registration - # :registered => [Boolean] true | false - # :state => [Enum] :pending | :complete - # } - # ] - @registrations = {} - - # Default settings - @format_on_type = false - @use_puppetfile_resolver = true - end + module SessionState + class LanguageClient + attr_reader :message_handler + + # Client settings + attr_reader :format_on_type + attr_reader :use_puppetfile_resolver + + def initialize(message_handler) + @message_handler = message_handler + @client_capabilites = {} + + # Internal registry of dynamic registrations and their current state + # @registrations[ <[String] method_name>] = [ + # { + # :id => [String] Request ID. Used for de-registration + # :registered => [Boolean] true | false + # :state => [Enum] :pending | :complete + # } + # ] + @registrations = {} + + # Default settings + @format_on_type = false + @use_puppetfile_resolver = true + end - def client_capability(*names) - safe_hash_traverse(@client_capabilites, *names) - end + def client_capability(*names) + safe_hash_traverse(@client_capabilites, *names) + end - def send_configuration_request - params = LSP::ConfigurationParams.new.from_h!('items' => []) - params.items << LSP::ConfigurationItem.new.from_h!('section' => 'puppet') + def send_configuration_request + params = LSP::ConfigurationParams.new.from_h!('items' => []) + params.items << LSP::ConfigurationItem.new.from_h!('section' => 'puppet') - message_handler.protocol.send_client_request('workspace/configuration', params) - true - end + message_handler.protocol.send_client_request('workspace/configuration', params) + true + end - def parse_lsp_initialize!(initialize_params = {}) - @client_capabilites = initialize_params['capabilities'] - end + def parse_lsp_initialize!(initialize_params = {}) + @client_capabilites = initialize_params['capabilities'] + end - def parse_lsp_configuration_settings!(settings = {}) - # format on type - value = safe_hash_traverse(settings, 'puppet', 'editorService', 'formatOnType', 'enable') - unless value.nil? || to_boolean(value) == @format_on_type # rubocop:disable Style/GuardClause Ummm no. - # Is dynamic registration available? - if client_capability('textDocument', 'onTypeFormatting', 'dynamicRegistration') == true - if value - register_capability('textDocument/onTypeFormatting', PuppetLanguageServer::ServerCapabilites.document_on_type_formatting_options) - else - unregister_capability('textDocument/onTypeFormatting') + def parse_lsp_configuration_settings!(settings = {}) + # format on type + value = safe_hash_traverse(settings, 'puppet', 'editorService', 'formatOnType', 'enable') + unless value.nil? || to_boolean(value) == @format_on_type # rubocop:disable Style/GuardClause Ummm no. + # Is dynamic registration available? + if client_capability('textDocument', 'onTypeFormatting', 'dynamicRegistration') == true + if value + register_capability('textDocument/onTypeFormatting', PuppetLanguageServer::ServerCapabilites.document_on_type_formatting_options) + else + unregister_capability('textDocument/onTypeFormatting') + end end + @format_on_type = value end - @format_on_type = value + # use puppetfile resolver + value = safe_hash_traverse(settings, 'puppet', 'validate', 'resolvePuppetfiles') + @use_puppetfile_resolver = to_boolean(value) end - # use puppetfile resolver - value = safe_hash_traverse(settings, 'puppet', 'validate', 'resolvePuppetfiles') - @use_puppetfile_resolver = to_boolean(value) - end - - def capability_registrations(method) - return [{ :registered => false, :state => :complete }] if @registrations[method].nil? || @registrations[method].empty? - @registrations[method].dup - end - def register_capability(method, options = {}) - id = new_request_id + def capability_registrations(method) + return [{ :registered => false, :state => :complete }] if @registrations[method].nil? || @registrations[method].empty? + @registrations[method].dup + end - PuppetLanguageServer.log_message(:info, "Attempting to dynamically register the #{method} method with id #{id}") + def register_capability(method, options = {}) + id = new_request_id - if @registrations[method] && @registrations[method].select { |i| i[:state] == :pending }.count > 0 - # The protocol doesn't specify whether this is allowed and is probably per client specific. For the moment we will allow - # the registration to be sent but log a message that something may be wrong. - PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method is already in progress") - end + PuppetLanguageServer.log_message(:info, "Attempting to dynamically register the #{method} method with id #{id}") - params = LSP::RegistrationParams.new.from_h!('registrations' => []) - params.registrations << LSP::Registration.new.from_h!('id' => id, 'method' => method, 'registerOptions' => options) - # Note - Don't put more than one method per request even though you can. It makes decoding errors much harder! + if @registrations[method] && @registrations[method].select { |i| i[:state] == :pending }.count > 0 + # The protocol doesn't specify whether this is allowed and is probably per client specific. For the moment we will allow + # the registration to be sent but log a message that something may be wrong. + PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method is already in progress") + end - @registrations[method] = [] if @registrations[method].nil? - @registrations[method] << { :registered => false, :state => :pending, :id => id } + params = LSP::RegistrationParams.new.from_h!('registrations' => []) + params.registrations << LSP::Registration.new.from_h!('id' => id, 'method' => method, 'registerOptions' => options) + # Note - Don't put more than one method per request even though you can. It makes decoding errors much harder! - message_handler.protocol.send_client_request('client/registerCapability', params) - true - end + @registrations[method] = [] if @registrations[method].nil? + @registrations[method] << { :registered => false, :state => :pending, :id => id } - def unregister_capability(method) - if @registrations[method].nil? - PuppetLanguageServer.log_message(:debug, "No registrations to deregister for the #{method}") - return true + message_handler.protocol.send_client_request('client/registerCapability', params) + true end - params = LSP::UnregistrationParams.new.from_h!('unregisterations' => []) - @registrations[method].each do |reg| - next if reg[:id].nil? - PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method, with id #{reg[:id]} is already in progress") if reg[:state] == :pending - # Ignore registrations that don't need to be unregistered - next if reg[:state] == :complete && !reg[:registered] - params.unregisterations << LSP::Unregistration.new.from_h!('id' => reg[:id], 'method' => method) - reg[:state] = :pending - end + def unregister_capability(method) + if @registrations[method].nil? + PuppetLanguageServer.log_message(:debug, "No registrations to deregister for the #{method}") + return true + end + + params = LSP::UnregistrationParams.new.from_h!('unregisterations' => []) + @registrations[method].each do |reg| + next if reg[:id].nil? + PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method, with id #{reg[:id]} is already in progress") if reg[:state] == :pending + # Ignore registrations that don't need to be unregistered + next if reg[:state] == :complete && !reg[:registered] + params.unregisterations << LSP::Unregistration.new.from_h!('id' => reg[:id], 'method' => method) + reg[:state] = :pending + end + + if params.unregisterations.count.zero? + PuppetLanguageServer.log_message(:debug, "Nothing to deregister for the #{method} method") + return true + end - if params.unregisterations.count.zero? - PuppetLanguageServer.log_message(:debug, "Nothing to deregister for the #{method} method") - return true + message_handler.protocol.send_client_request('client/unregisterCapability', params) + true end - message_handler.protocol.send_client_request('client/unregisterCapability', params) - true - end + def parse_register_capability_response!(response, original_request) + raise 'Response is not from client/registerCapability request' unless original_request.rpc_method == 'client/registerCapability' - def parse_register_capability_response!(response, original_request) - raise 'Response is not from client/registerCapability request' unless original_request.rpc_method == 'client/registerCapability' + unless response.is_successful + original_request.params.registrations.each do |reg| + # Mark the registration as completed and failed + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = false; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine + end + return true + end - unless response.is_successful original_request.params.registrations.each do |reg| - # Mark the registration as completed and failed - @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? - @registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = false; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine - end - return true - end + PuppetLanguageServer.log_message(:info, "Succesfully dynamically registered the #{reg.method__lsp} method") - original_request.params.registrations.each do |reg| - PuppetLanguageServer.log_message(:info, "Succesfully dynamically registered the #{reg.method__lsp} method") + # Mark the registration as completed and succesful + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = true; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine - # Mark the registration as completed and succesful - @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? - @registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = true; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine + # If we just registered the workspace/didChangeConfiguration method then + # also trigger a configuration request to get the initial state + send_configuration_request if reg.method__lsp == 'workspace/didChangeConfiguration' + end - # If we just registered the workspace/didChangeConfiguration method then - # also trigger a configuration request to get the initial state - send_configuration_request if reg.method__lsp == 'workspace/didChangeConfiguration' + true end - true - end + def parse_unregister_capability_response!(response, original_request) + raise 'Response is not from client/unregisterCapability request' unless original_request.rpc_method == 'client/unregisterCapability' - def parse_unregister_capability_response!(response, original_request) - raise 'Response is not from client/unregisterCapability request' unless original_request.rpc_method == 'client/unregisterCapability' + unless response.is_successful + original_request.params.unregisterations.each do |reg| + # Mark the registration as completed and failed + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @registrations[reg.method__lsp].select { |i| i[:id] == reg.id && i[:registered] }.each { |i| i[:state] = :complete } + @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id && !i[:registered] } + end + return true + end - unless response.is_successful original_request.params.unregisterations.each do |reg| - # Mark the registration as completed and failed + PuppetLanguageServer.log_message(:info, "Succesfully dynamically unregistered the #{reg.method__lsp} method") + + # Remove registrations @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? - @registrations[reg.method__lsp].select { |i| i[:id] == reg.id && i[:registered] }.each { |i| i[:state] = :complete } - @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id && !i[:registered] } + @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id } end - return true - end - - original_request.params.unregisterations.each do |reg| - PuppetLanguageServer.log_message(:info, "Succesfully dynamically unregistered the #{reg.method__lsp} method") - # Remove registrations - @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? - @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id } + true end - true - end - - private + private - def to_boolean(value) - return false if value.nil? || value == false - return true if value == true - value.to_s =~ %r{^(true|t|yes|y|1)$/i} - end + def to_boolean(value) + return false if value.nil? || value == false + return true if value == true + value.to_s =~ %r{^(true|t|yes|y|1)$/i} + end - def new_request_id - SecureRandom.uuid - end + def new_request_id + SecureRandom.uuid + end - def safe_hash_traverse(hash, *names) - return nil if names.empty? || hash.nil? || hash.empty? - item = nil - loop do - name = names.shift - item = item.nil? ? hash[name] : item[name] - return nil if item.nil? - return item if names.empty? + def safe_hash_traverse(hash, *names) + return nil if names.empty? || hash.nil? || hash.empty? + item = nil + loop do + name = names.shift + item = item.nil? ? hash[name] : item[name] + return nil if item.nil? + return item if names.empty? + end + nil end - nil end end end diff --git a/lib/puppet-languageserver/session_state/object_cache.rb b/lib/puppet-languageserver/session_state/object_cache.rb index e68eda31..2270ec0e 100644 --- a/lib/puppet-languageserver/session_state/object_cache.rb +++ b/lib/puppet-languageserver/session_state/object_cache.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module PuppetLanguageServer - module PuppetHelper - class Cache + module SessionState + class ObjectCache SECTIONS = %i[class type function datatype fact].freeze ORIGINS = %i[default workspace bolt].freeze diff --git a/lib/puppet-languageserver/uri_helper.rb b/lib/puppet-languageserver/uri_helper.rb index 74399675..ad081a18 100644 --- a/lib/puppet-languageserver/uri_helper.rb +++ b/lib/puppet-languageserver/uri_helper.rb @@ -9,13 +9,9 @@ def self.build_file_uri(path) 'file://' + Puppet::Util.uri_encode(path.start_with?('/') ? path : '/' + path) end - def self.uri_path(uri) - actual_uri = URI(uri) - - # CGI.unescape doesn't handle space rules properly in uri paths - # URI.unescape does, but returns strings in their original encoding - # Mostly safe here as we're only worried about file based URIs - URI.unescape(actual_uri.path) # rubocop:disable Lint/UriEscapeUnescape + def self.uri_path(uri_string) + return nil if uri_string.nil? + Puppet::Util.uri_to_path(URI(uri_string)) end # Compares two URIs and returns the relative path diff --git a/lib/puppet_languageserver.rb b/lib/puppet_languageserver.rb index d7995ff3..4ca4a8cd 100644 --- a/lib/puppet_languageserver.rb +++ b/lib/puppet_languageserver.rb @@ -57,9 +57,8 @@ def self.require_gems(options) # These libraries do not require the puppet gem and required for the # server to respond to clients. %w[ - document_store + client_session_state crash_dump - language_client message_handler server_capabilities ].each do |lib| @@ -185,8 +184,7 @@ def self.parse(options) args[:puppet_version] = text end - opts.on('--local-workspace=PATH', 'The workspace or file path that will be used to provide module-specific functionality. Default is no workspace path.') do |path| - args[:workspace] = path + opts.on('--local-workspace=PATH', '** DEPRECATED ** The workspace or file path that will be used to provide module-specific functionality. Default is no workspace path.') do |_path| end opts.on('-h', '--help', 'Prints this help') do @@ -224,9 +222,6 @@ def self.init_puppet(options) log_message(:info, 'Initializing Puppet Helper...') PuppetLanguageServer::PuppetHelper.initialize_helper(options) - log_message(:debug, 'Initializing Document Store...') - PuppetLanguageServer::DocumentStore.initialize_store(options) - log_message(:info, 'Initializing settings...') if options[:fast_start_langserver] Thread.new do @@ -269,11 +264,6 @@ def self.init_puppet_worker(options) PuppetLanguageServer::PuppetHelper.load_default_datatypes_async end - if PuppetLanguageServer::DocumentStore.store_has_module_metadata? || PuppetLanguageServer::DocumentStore.store_has_environmentconf? - log_message(:info, 'Preloading Workspace (Async)...') - PuppetLanguageServer::PuppetHelper.load_workspace_async - end - log_message(:info, 'Preloading static data (Async)...') PuppetLanguageServer::PuppetHelper.load_static_data_async else diff --git a/spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb b/spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb index a0368a52..c1fc6300 100644 --- a/spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb +++ b/spec/languageserver/integration/puppet-languageserver/session_state/document_store_spec.rb @@ -1,72 +1,72 @@ require 'spec_helper' -describe 'PuppetLanguageServer::DocumentStore' do - let(:subject) { PuppetLanguageServer::DocumentStore } +describe 'PuppetLanguageServer::SessionState::DocumentStore' do + let(:subject) { PuppetLanguageServer::SessionState::DocumentStore.new } RSpec.shared_examples 'an empty workspace' do |expected_root_path| it 'should return the workspace directory for the root_path' do - expect(PuppetLanguageServer::DocumentStore.store_root_path).to eq(expected_root_path) + expect(subject.store_root_path).to eq(expected_root_path) end it 'should not find module metadata' do - expect(PuppetLanguageServer::DocumentStore.store_has_module_metadata?).to be false + expect(subject.store_has_module_metadata?).to be false end it 'should not find environmentconf' do - expect(PuppetLanguageServer::DocumentStore.store_has_environmentconf?).to be false + expect(subject.store_has_environmentconf?).to be false end end RSpec.shared_examples 'a environmentconf workspace' do |expected_root_path| it 'should return the control repo root for the root_path' do - expect(PuppetLanguageServer::DocumentStore.store_root_path).to eq(expected_root_path) + expect(subject.store_root_path).to eq(expected_root_path) end it 'should not find module metadata' do - expect(PuppetLanguageServer::DocumentStore.store_has_module_metadata?).to be false + expect(subject.store_has_module_metadata?).to be false end it 'should find environmentconf' do - expect(PuppetLanguageServer::DocumentStore.store_has_environmentconf?).to be true + expect(subject.store_has_environmentconf?).to be true end end RSpec.shared_examples 'a metadata.json workspace' do |expected_root_path| it 'should return the control repo root for the root_path' do - expect(PuppetLanguageServer::DocumentStore.store_root_path).to eq(expected_root_path) + expect(subject.store_root_path).to eq(expected_root_path) end it 'should find module metadata' do - expect(PuppetLanguageServer::DocumentStore.store_has_module_metadata?).to be true + expect(subject.store_has_module_metadata?).to be true end it 'should not find environmentconf' do - expect(PuppetLanguageServer::DocumentStore.store_has_environmentconf?).to be false + expect(subject.store_has_environmentconf?).to be false end end RSpec.shared_examples 'a cached workspace' do it 'should cache the information' do expect(subject).to receive(:file_exist?).at_least(:once).and_call_original - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path # Subsequent calls should be cached expect(subject).to receive(:file_exist?).exactly(0).times - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path end it 'should recache the information when the cache expires' do - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path # Expire the cache - PuppetLanguageServer::DocumentStore.expire_store_information + subject.expire_store_information expect(subject).to receive(:file_exist?).at_least(:once).and_call_original - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path # Subsequent calls should be cached expect(subject).to receive(:file_exist?).exactly(0).times - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path end end @@ -75,7 +75,7 @@ # TODO: This test is a little fragile but can't think of a better way to prove it expect(subject).to receive(:file_exist?).exactly(expected_file_calls).times.and_call_original expect(subject).to receive(:dir_exist?).exactly(expected_dir_calls).times.and_call_original - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path end end @@ -84,27 +84,27 @@ let(:server_options) { {} } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'an empty workspace', nil it 'should cache the information' do expect(subject).to receive(:file_exist?).exactly(0).times - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path end it 'should not recache the information when the cache expires' do expect(subject).to receive(:file_exist?).exactly(0).times - result = PuppetLanguageServer::DocumentStore.store_root_path - PuppetLanguageServer::DocumentStore.expire_store_information - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path - result = PuppetLanguageServer::DocumentStore.store_root_path + result = subject.store_root_path + subject.expire_store_information + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path + result = subject.store_root_path end end @@ -112,7 +112,7 @@ let(:server_options) { { :workspace => '/a/directory/which/does/not/exist' } } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'an empty workspace', '/a/directory/which/does/not/exist' @@ -125,7 +125,7 @@ let(:server_options) { { :workspace => expected_root } } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'a environmentconf workspace', expected_root @@ -140,7 +140,7 @@ let(:server_options) { { :workspace => deep_path } } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'a environmentconf workspace', expected_root @@ -155,7 +155,7 @@ let(:server_options) { { :workspace => expected_root } } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'a metadata.json workspace', expected_root @@ -200,7 +200,7 @@ let(:server_options) { { :workspace => deep_path } } before(:each) do - PuppetLanguageServer::DocumentStore.initialize_store(server_options) + subject.initialize_store(server_options) end it_should_behave_like 'a metadata.json workspace', expected_root diff --git a/spec/languageserver/integration/puppet-languageserver/sidecar_queue_spec.rb b/spec/languageserver/integration/puppet-languageserver/sidecar_queue_spec.rb index 21e0d1e0..bab7507f 100644 --- a/spec/languageserver/integration/puppet-languageserver/sidecar_queue_spec.rb +++ b/spec/languageserver/integration/puppet-languageserver/sidecar_queue_spec.rb @@ -7,7 +7,7 @@ def exitstatus end describe 'sidecar_queue' do - let(:cache) { PuppetLanguageServer::PuppetHelper::Cache.new } + let(:cache) { PuppetLanguageServer::SessionState::ObjectCache.new } let(:subject) { subject = PuppetLanguageServer::SidecarQueue.new subject.cache = cache diff --git a/spec/languageserver/unit/puppet-languageserver/manifest/completion_provider_spec.rb b/spec/languageserver/unit/puppet-languageserver/manifest/completion_provider_spec.rb index 438b6f05..803adcc0 100644 --- a/spec/languageserver/unit/puppet-languageserver/manifest/completion_provider_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/manifest/completion_provider_spec.rb @@ -53,7 +53,7 @@ def create_mock_type(parameters = [], properties = []) after(:each) do # Clear out the Object Cache of workspace objects - PuppetLanguageServer::PuppetHelper::Cache::SECTIONS.each do |section| + PuppetLanguageServer::SessionState::ObjectCache::SECTIONS.each do |section| PuppetLanguageServer::PuppetHelper.cache.import_sidecar_list!([], section, :workspace) end end diff --git a/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb b/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb index d6734b28..cfa0bfca 100644 --- a/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/manifest/document_symbol_provider_spec.rb @@ -42,7 +42,7 @@ let(:subject) { PuppetLanguageServer::Manifest::DocumentSymbolProvider } describe '#workspace_symbols' do - let(:cache) { PuppetLanguageServer::PuppetHelper::Cache.new } + let(:cache) { PuppetLanguageServer::SessionState::ObjectCache.new } before(:each) do # Add test objects diff --git a/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb b/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb index 6242e3c5..fef45230 100644 --- a/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb @@ -496,7 +496,7 @@ it 'should set tasks_mode option if the file is Puppet plan file' do expect(PuppetLanguageServer::Manifest::CompletionProvider).to receive(:complete).with(Object, line_num, char_num, { :tasks_mode => true, :context => nil }).and_return('something') - allow(PuppetLanguageServer::DocumentStore).to receive(:plan_file?).and_return true + allow(subject.documents).to receive(:plan_file?).and_return true subject.request_textdocument_completion(connection_id, request_message) end @@ -605,7 +605,7 @@ it 'should set tasks_mode option if the file is Puppet plan file' do expect(PuppetLanguageServer::Manifest::HoverProvider).to receive(:resolve).with(Object,line_num,char_num,{:tasks_mode=>true}).and_return('something') - allow(PuppetLanguageServer::DocumentStore).to receive(:plan_file?).and_return true + allow(subject.documents).to receive(:plan_file?).and_return true subject.request_textdocument_hover(connection_id, request_message) end @@ -666,7 +666,7 @@ it 'should set tasks_mode option if the file is Puppet plan file' do expect(PuppetLanguageServer::Manifest::DefinitionProvider).to receive(:find_definition) .with(Object,line_num,char_num,{:tasks_mode=>true}).and_return('something') - allow(PuppetLanguageServer::DocumentStore).to receive(:plan_file?).and_return true + allow(subject.documents).to receive(:plan_file?).and_return true subject.request_textdocument_definition(connection_id, request_message) end @@ -721,7 +721,7 @@ it 'should set tasks_mode option if the file is Puppet plan file' do expect(PuppetLanguageServer::Manifest::DocumentSymbolProvider).to receive(:extract_document_symbols) .with(Object,{:tasks_mode=>true}).and_return('something') - allow(PuppetLanguageServer::DocumentStore).to receive(:plan_file?).and_return true + allow(subject.documents).to receive(:plan_file?).and_return true subject.request_textdocument_documentsymbol(connection_id, request_message) end diff --git a/spec/languageserver/unit/puppet-languageserver/puppet_helper_spec.rb b/spec/languageserver/unit/puppet-languageserver/puppet_helper_spec.rb index a1b2779e..a0c8e372 100644 --- a/spec/languageserver/unit/puppet-languageserver/puppet_helper_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/puppet_helper_spec.rb @@ -9,7 +9,7 @@ def contains_bolt_objects?(cache) before(:each) do # Purge the static data - PuppetLanguageServer::PuppetHelper::Cache::SECTIONS.each do |section| + PuppetLanguageServer::SessionState::ObjectCache::SECTIONS.each do |section| PuppetLanguageServer::PuppetHelper.cache.remove_section!(section, :bolt) end expect(contains_bolt_objects?(PuppetLanguageServer::PuppetHelper.cache)).to be(false) diff --git a/spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb index ecde5f5b..dfd159a4 100644 --- a/spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/session_state/document_store_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe 'PuppetLanguageServer::DocumentStore' do - let(:subject) { PuppetLanguageServer::DocumentStore } +describe 'PuppetLanguageServer::SessionState::DocumentStore' do + let(:subject) { PuppetLanguageServer::SessionState::DocumentStore.new } describe '#plan_file?' do before(:each) do @@ -32,6 +32,7 @@ prefixes.each do |prefix| it "should detect '#{prefix}#{testcase}' as a plan file" do file_uri = PuppetLanguageServer::UriHelper.build_file_uri(prefix + testcase) + expect(subject.plan_file?(file_uri)).to be(true) end end diff --git a/spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb index 0bf90e04..358962c7 100644 --- a/spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/session_state/language_client_spec.rb @@ -98,11 +98,11 @@ def pretty_value(value) end end -describe 'PuppetLanguageServer::LanguageClient' do +describe 'PuppetLanguageServer::SessionState::LanguageClient' do let(:server) do MockServer.new({}, {}, { :class => PuppetEditorServices::Protocol::JsonRPC }, {}) end - let(:subject) { PuppetLanguageServer::LanguageClient.new(server.handler_object) } + let(:subject) { PuppetLanguageServer::SessionState::LanguageClient.new(server.handler_object) } let(:protocol) { server.protocol_object } let(:mock_connection) { server.connection_object } diff --git a/spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb b/spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb index c54a7275..0e86afbc 100644 --- a/spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/session_state/object_cache_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe 'PuppetLanguageServer::PuppetHelper::Cache' do +describe 'PuppetLanguageServer::SessionState::ObjectCache' do let(:section_function) { :function } let(:origin_default) { :default } let(:origin_workspace) { :workspace } - let(:subject) { PuppetLanguageServer::PuppetHelper::Cache.new() } + let(:subject) { PuppetLanguageServer::SessionState::ObjectCache.new() } describe "#import_sidecar_list!" do # Note that this method is used a lot in the test fixtures below so it diff --git a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb index c69f3518..a53969b1 100644 --- a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb @@ -194,7 +194,6 @@ describe '#from_json!' do puppet_node_graph_properties.each do |testcase| it "should deserialize a serialized #{testcase} value" do - #require 'pry'; binding.pry serial = subject.to_json deserial = subject_klass.new.from_json!(serial) From 1c41cf144ea32d93f25b647cdd9163a8bac09da7 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Mon, 2 Dec 2019 13:53:15 +0800 Subject: [PATCH 04/34] (maint) Return nil if the connection does not exist Previously the TCP Server would return a hash instead of nil if the connection ID did not exist. This commit forces the output to nil if the connectino does not exist. --- lib/puppet_editor_services/server/tcp.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/puppet_editor_services/server/tcp.rb b/lib/puppet_editor_services/server/tcp.rb index 4d95c6d5..dee4d0e4 100644 --- a/lib/puppet_editor_services/server/tcp.rb +++ b/lib/puppet_editor_services/server/tcp.rb @@ -311,6 +311,7 @@ def connection(connection_id) return v[:handler] unless v[:handler].nil? || v[:handler].id != connection_id end end + nil end # @api private From 1884907768901dec84df2bae09041aee820ce7ab Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Tue, 31 Mar 2020 14:03:06 +0800 Subject: [PATCH 05/34] (maint) Remove unused protocol object Previously the NodeGraph protocol object was changed to PuppetNodeGraph however the original object was never deleted. This commit deletes this unused object. --- lib/puppet-languageserver/sidecar_protocol.rb | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/puppet-languageserver/sidecar_protocol.rb b/lib/puppet-languageserver/sidecar_protocol.rb index 3ced1afd..9a6a640a 100644 --- a/lib/puppet-languageserver/sidecar_protocol.rb +++ b/lib/puppet-languageserver/sidecar_protocol.rb @@ -163,25 +163,6 @@ def from_json!(json_string) end end - class NodeGraph < BaseClass - attr_accessor :dot_content - attr_accessor :error_content - - def to_json(*options) - { - 'dot_content' => dot_content, - 'error_content' => error_content - }.to_json(options) - end - - def from_json!(json_string) - obj = ::JSON.parse(json_string) - self.dot_content = obj['dot_content'] - self.error_content = obj['error_content'] - self - end - end - class PuppetClass < BasePuppetObject attr_accessor :parameters attr_accessor :doc From f1c9fa4d664a52bbf14f431de169220ae4fa7030 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Sat, 30 Nov 2019 21:40:02 +0800 Subject: [PATCH 06/34] (GH-209) Refactor ValidationQueue into a class This commit: * Creates a new base queue called SingleInstanceQueue. This queue process jobs in one or more threads. But only allows one job key to exist in the queue at once. When you add an additional job, it gets added to the end of the queue and any other jobs are removed. * Creates a ValidationQueue which inherits from the SingleInstanceQueue but only has the extra things it needs to do validation. * Creats a global module called GlobalQueues, which holds an instance of the validation queue. Validation is a global queue (for any connection). * Updates the spec tests for the new queue name * Cleans up some of the logic in the queue. It was pre-optimising but made it harder to follow through. * The Validation Queue now uses the session_state for a given connection to make decisions instead of the global DocumentStore modulre. --- lib/puppet-languageserver/global_queues.rb | 11 ++ .../global_queues/single_instance_queue.rb | 127 +++++++++++++++ .../global_queues/validation_queue.rb | 98 +++++++++++ lib/puppet-languageserver/message_handler.rb | 3 +- lib/puppet-languageserver/validation_queue.rb | 152 ------------------ lib/puppet_languageserver.rb | 2 +- .../validation_queue_spec.rb | 130 ++++++++------- .../message_handler_spec.rb | 4 +- 8 files changed, 305 insertions(+), 222 deletions(-) create mode 100644 lib/puppet-languageserver/global_queues.rb create mode 100644 lib/puppet-languageserver/global_queues/single_instance_queue.rb create mode 100644 lib/puppet-languageserver/global_queues/validation_queue.rb delete mode 100644 lib/puppet-languageserver/validation_queue.rb rename spec/languageserver/unit/puppet-languageserver/{ => global_queues}/validation_queue_spec.rb (52%) diff --git a/lib/puppet-languageserver/global_queues.rb b/lib/puppet-languageserver/global_queues.rb new file mode 100644 index 00000000..650d6f94 --- /dev/null +++ b/lib/puppet-languageserver/global_queues.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'puppet-languageserver/global_queues/validation_queue' + +module PuppetLanguageServer + module GlobalQueues + def self.validate_queue + @validate_queue ||= ValidationQueue.new + end + end +end diff --git a/lib/puppet-languageserver/global_queues/single_instance_queue.rb b/lib/puppet-languageserver/global_queues/single_instance_queue.rb new file mode 100644 index 00000000..85c1b4c2 --- /dev/null +++ b/lib/puppet-languageserver/global_queues/single_instance_queue.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +module PuppetLanguageServer + module GlobalQueues + class SingleInstanceQueueJob + def initialize(*_); end + + # Unique key for the job. The SingleInstanceQueue uses the key + # to ensure that only a single instance is in the queue + # @api asbtract + def key + raise ArgumentError, "key should be implenmented for #{self.class}" + end + end + + # Base class for enqueing and running queued jobs asynchronously + # When adding a job, it will remove any other for the same + # key in the queue, so that only the latest job needs to be processed. + class SingleInstanceQueue + def initialize + @queue = [] + @queue_mutex = Mutex.new + @queue_threads_mutex = Mutex.new + @queue_threads = [] + end + + # Default is one thread to process the queue + def max_queue_threads + 1 + end + + # The ruby Job class that this queue operates on + # @api asbtract + def job_class + raise ArgumentError, "job_class should be implemented for #{self.class}" + end + + def new_job(*args) + job_class.new(*args) + end + + # Helpful method to create, then enqueue a job + def enqueue(*args) + enqueue_job(new_job(*args)) + end + + # Enqueue a job + def enqueue_job(job_object) + raise "Invalid job object for #{self.class}. Got #{job_object.class} but expected #{job_class}" unless job_object.is_a?(job_class) + + @queue_mutex.synchronize do + @queue.reject! { |queue_item| queue_item.key == job_object.key } + @queue << job_object + end + + @queue_threads_mutex.synchronize do + # Clear up any done threads + @queue_threads.reject! { |thr| thr.nil? || !thr.alive? } + # Append a new thread if we have space + if @queue_threads.count < max_queue_threads + @queue_threads << Thread.new do + begin + thread_worker + rescue => e # rubocop:disable Style/RescueStandardError + PuppetLanguageServer.log_message(:error, "Error in #{self.class} Thread: #{e}") + raise + end + end + end + end + nil + end + + # Helpful method to create, then enqueue a job + def execute(*args) + execute_job(new_job(*args)) + end + + # Synchronously executes the same work as an enqueued item. Does not consume a queue thread + # The thread worker calls this method when processing enqueued items + # @abstract + def execute_job(job_object) + raise "Invalid job object for #{self.class}. Got #{job_object.class} but expected #{job_class}" unless job_object.is_a?(job_class) + end + + # Wait for the queue to become empty + def drain_queue + @queue_threads.each do |item| + item.join unless item.nil? || !item.alive? + end + nil + end + + # Testing helper resets the queue and prepopulates it with + # a known arbitrary configuration. + # ONLY USE THIS FOR TESTING! + def reset_queue(initial_state = []) + @queue_mutex.synchronize do + @queue = initial_state + end + end + + private + + # Thread worker which processes all jobs in the queue and calls the sidecar for each action + def thread_worker + work_item = nil + loop do + @queue_mutex.synchronize do + return if @queue.empty? + work_item = @queue.shift + end + return if work_item.nil? + + # Perform action + begin + # When running async (i.e. from a thread swallow any output) + _result = execute_job(work_item) + rescue StandardError => e + PuppetLanguageServer.log_message(:error, "#{self.class} Thread: Error running job #{work_item.key}. #{e}") + nil + end + end + end + end + end +end diff --git a/lib/puppet-languageserver/global_queues/validation_queue.rb b/lib/puppet-languageserver/global_queues/validation_queue.rb new file mode 100644 index 00000000..7946be80 --- /dev/null +++ b/lib/puppet-languageserver/global_queues/validation_queue.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'puppet-languageserver/global_queues/single_instance_queue' +require 'puppet_editor_services/server' + +module PuppetLanguageServer + module GlobalQueues + class ValidationQueueJob < SingleInstanceQueueJob + attr_accessor :file_uri + attr_accessor :doc_version + attr_accessor :connection_id + attr_accessor :options + + def initialize(file_uri, doc_version, connection_id, options = {}) + super + @file_uri = file_uri + @doc_version = doc_version + @connection_id = connection_id + @options = options + end + + def key + @file_uri + end + end + + # Class for enqueing and running document level validation asynchronously + # + # Uses a single instance queue so only the latest document needs to be processed. + # It will also ignore sending back validation results to the client if the document is updated during the validation process + class ValidationQueue < SingleInstanceQueue + def max_queue_threads + 1 + end + + def job_class + ValidationQueueJob + end + + def execute_job(job_object) + super(job_object) + document_store = document_store_from_connection_id(job_object.connection_id) + raise "Document store is not available for connection id #{job_object.connection_id}" if document_store.nil? + + # Check if the document is the latest version + content = document_store.document(job_object.file_uri, job_object.doc_version) + if content.nil? + PuppetLanguageServer.log_message(:debug, "#{self.class.name}: Ignoring #{job_object.file_uri} as it is not the latest version or has been removed") + return + end + + # Perform validation + options = job_object.options.dup + results = case document_store.document_type(job_object.file_uri) + when :manifest + options[:tasks_mode] = document_store.plan_file?(job_object.file_uri) + PuppetLanguageServer:: Manifest::ValidationProvider.validate(content, options) + when :epp + PuppetLanguageServer::Epp::ValidationProvider.validate(content) + when :puppetfile + options[:document_uri] = job_object.file_uri + PuppetLanguageServer::Puppetfile::ValidationProvider.validate(content, options) + else + [] + end + + # Because this may be asynchronous it's possible the user has edited the document while we're performing validation. + # Check if the document is still latest version and ignore the results if it's no longer the latest + current_version = document_store.document_version(job_object.file_uri) + if current_version != job_object.doc_version + PuppetLanguageServer.log_message(:debug, "ValidationQueue Thread: Ignoring #{job_object.file_uri} as has changed version from #{job_object.doc_version} to #{current_version}") + return + end + + # Send the response + send_diagnostics(job_object.connection_id, job_object.file_uri, results) + end + + private + + def document_store_from_connection_id(connection_id) + connection = PuppetEditorServices::Server.current_server.connection(connection_id) + return if connection.nil? + handler = connection.protocol.handler + handler.respond_to?(:documents) ? handler.documents : nil + end + + def send_diagnostics(connection_id, file_uri, diagnostics) + connection = PuppetEditorServices::Server.current_server.connection(connection_id) + return if connection.nil? + + connection.protocol.encode_and_send( + ::PuppetEditorServices::Protocol::JsonRPCMessages.new_notification('textDocument/publishDiagnostics', 'uri' => file_uri, 'diagnostics' => diagnostics) + ) + end + end + end +end diff --git a/lib/puppet-languageserver/message_handler.rb b/lib/puppet-languageserver/message_handler.rb index 74cafa0f..0e5a0e35 100644 --- a/lib/puppet-languageserver/message_handler.rb +++ b/lib/puppet-languageserver/message_handler.rb @@ -4,6 +4,7 @@ require 'puppet_editor_services/protocol/json_rpc_messages' require 'puppet-languageserver/server_capabilities' require 'puppet-languageserver/client_session_state' +require 'puppet-languageserver/global_queues' module PuppetLanguageServer # This module is just duck-typing the old PuppetLanguageServer::DocumentStore Module. @@ -404,7 +405,7 @@ def enqueue_validation(file_uri, doc_version, client_handler_id) options[:puppet_version] = Puppet.version options[:module_path] = PuppetLanguageServer::PuppetHelper.module_path end - PuppetLanguageServer::ValidationQueue.enqueue(file_uri, doc_version, client_handler_id, options) + GlobalQueues.validate_queue.enqueue(file_uri, doc_version, client_handler_id, options) end def workspace_root_from_initialize_params(params) diff --git a/lib/puppet-languageserver/validation_queue.rb b/lib/puppet-languageserver/validation_queue.rb deleted file mode 100644 index 3d68c73d..00000000 --- a/lib/puppet-languageserver/validation_queue.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -module PuppetLanguageServer - # Module for enqueing and running document level validation asynchronously - # When adding a document to be validation, it will remove any validation requests for the same - # document in the queue so that only the latest document needs to be processed. - # - # It will also ignore sending back validation results to the client if the document is - # updated during the validation process - module ValidationQueue - @queue = [] - @queue_mutex = Mutex.new - @queue_thread = nil - - # Enqueue a file to be validated - def self.enqueue(file_uri, doc_version, connection_id, options = {}) - document_type = PuppetLanguageServer::DocumentStore.document_type(file_uri) - - unless %i[manifest epp puppetfile].include?(document_type) - # Can't validate these types so just emit an empty validation result - send_diagnostics(connection_id, file_uri, []) - return - end - - @queue_mutex.synchronize do - @queue.reject! { |item| item['file_uri'] == file_uri } - - @queue << { - 'file_uri' => file_uri, - 'doc_version' => doc_version, - 'document_type' => document_type, - 'connection_id' => connection_id, - 'options' => options - } - end - - if @queue_thread.nil? || !@queue_thread.alive? - @queue_thread = Thread.new do - begin - worker - rescue => e # rubocop:disable Style/RescueStandardError - PuppetLanguageServer.log_message(:error, "Error in ValidationQueue Thread: #{e}") - raise - end - end - end - - nil - end - - # Synchronously validate a file - def self.validate_sync(file_uri, doc_version, connection_id, options = {}) - document_type = PuppetLanguageServer::DocumentStore.document_type(file_uri) - content = documents.document(file_uri, doc_version) - return nil if content.nil? - result = validate(file_uri, document_type, content, options) - - # Send the response - send_diagnostics(connection_id, file_uri, result) - end - - # Helper method to the Document Store - def self.documents - PuppetLanguageServer::DocumentStore - end - - # Wait for the queue to become empty - def self.drain_queue - return if @queue_thread.nil? || !@queue_thread.alive? - @queue_thread.join - nil - end - - # Testing helper resets the queue and prepopulates it with - # a known arbitrary configuration. - # ONLY USE THIS FOR TESTING! - def self.reset_queue(initial_state = []) - @queue_mutex.synchronize do - @queue = initial_state - end - end - - def self.send_diagnostics(connection_id, file_uri, diagnostics) - connection = PuppetEditorServices::Server.current_server.connection(connection_id) - return if connection.nil? - - connection.protocol.encode_and_send( - ::PuppetEditorServices::Protocol::JsonRPCMessages.new_notification('textDocument/publishDiagnostics', 'uri' => file_uri, 'diagnostics' => diagnostics) - ) - end - private_class_method :send_diagnostics - - # Validate a document - def self.validate(document_uri, document_type, content, options = {}) - options = {} if options.nil? - # Perform validation - case document_type - when :manifest - options[:tasks_mode] = PuppetLanguageServer::DocumentStore.plan_file?(document_uri) - PuppetLanguageServer::Manifest::ValidationProvider.validate(content, options) - when :epp - PuppetLanguageServer::Epp::ValidationProvider.validate(content) - when :puppetfile - options[:document_uri] = document_uri - PuppetLanguageServer::Puppetfile::ValidationProvider.validate(content, options) - else - [] - end - end - private_class_method :validate - - # Thread worker which processes all jobs in the queue and validates each document - # serially - def self.worker - work_item = nil - loop do - @queue_mutex.synchronize do - return if @queue.empty? - work_item = @queue.shift - end - return if work_item.nil? - - file_uri = work_item['file_uri'] - doc_version = work_item['doc_version'] - connection_id = work_item['connection_id'] - document_type = work_item['document_type'] - validation_options = work_item['options'] - - # Check if the document is the latest version - content = documents.document(file_uri, doc_version) - if content.nil? - PuppetLanguageServer.log_message(:debug, "ValidationQueue Thread: Ignoring #{work_item['file_uri']} as it is not the latest version or has been removed") - return - end - - # Perform validation - result = validate(file_uri, document_type, content, validation_options) - - # Check if the document is still latest version - current_version = documents.document_version(file_uri) - if current_version != doc_version - PuppetLanguageServer.log_message(:debug, "ValidationQueue Thread: Ignoring #{work_item['file_uri']} as has changed version from #{doc_version} to #{current_version}") - return - end - - # Send the response - send_diagnostics(connection_id, file_uri, result) - end - end - private_class_method :worker - end -end diff --git a/lib/puppet_languageserver.rb b/lib/puppet_languageserver.rb index 4ca4a8cd..95c6ad14 100644 --- a/lib/puppet_languageserver.rb +++ b/lib/puppet_languageserver.rb @@ -79,7 +79,7 @@ def self.require_gems(options) # These libraries require the puppet and LSP gems. %w[ - validation_queue + global_queues sidecar_protocol sidecar_queue puppet_parser_helper diff --git a/spec/languageserver/unit/puppet-languageserver/validation_queue_spec.rb b/spec/languageserver/unit/puppet-languageserver/global_queues/validation_queue_spec.rb similarity index 52% rename from spec/languageserver/unit/puppet-languageserver/validation_queue_spec.rb rename to spec/languageserver/unit/puppet-languageserver/global_queues/validation_queue_spec.rb index 6d5cc083..3f78a12f 100644 --- a/spec/languageserver/unit/puppet-languageserver/validation_queue_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/global_queues/validation_queue_spec.rb @@ -1,22 +1,33 @@ require 'spec_helper' +require 'puppet-languageserver/session_state/document_store' describe 'validation_queue' do - MANIFEST_FILENAME = 'file:///something.pp' - PUPPETFILE_FILENAME = 'file:///Puppetfile' - EPP_FILENAME = 'file:///something.epp' - UNKNOWN_FILENAME = 'file:///I_do_not_work.exe' - MISSING_FILENAME = 'file:///I_do_not_exist.jpg' - FILE_CONTENT = "file_content which causes errros\n <%- Wee!\n class 'foo' {'" - - let(:subject) { PuppetLanguageServer::ValidationQueue } + VALIDATE_MANIFEST_FILENAME = 'file:///something.pp' + VALIDATE_PUPPETFILE_FILENAME = 'file:///Puppetfile' + VALIDATE_EPP_FILENAME = 'file:///something.epp' + VALIDATE_UNKNOWN_FILENAME = 'file:///I_do_not_work.exe' + VALIDATE_MISSING_FILENAME = 'file:///I_do_not_exist.jpg' + VALIDATE_FILE_CONTENT = "file_content which causes errros\n <%- Wee!\n class 'foo' {'" + + let(:subject) { PuppetLanguageServer::GlobalQueues::ValidationQueue.new } let(:connection_id) { 'abc123' } let(:document_version) { 10 } + let(:document_store) { PuppetLanguageServer::SessionState::DocumentStore.new } + + before(:each) do + document_store.clear + allow(subject).to receive(:document_store_from_connection_id).with(connection_id).and_return(document_store) + end + + def job(file_uri, document_version, connection_id, job_options = {}) + PuppetLanguageServer::GlobalQueues::ValidationQueueJob.new(file_uri, document_version, connection_id, job_options) + end describe '#enqueue' do shared_examples_for "single document which sends validation results" do |file_uri, file_content, validation_result| it 'should send validation results' do - subject.documents.set_document(file_uri, file_content, document_version) - expect(PuppetLanguageServer::ValidationQueue).to receive(:send_diagnostics).with(connection_id, file_uri, validation_result) + document_store.set_document(file_uri, file_content, document_version) + expect(subject).to receive(:send_diagnostics).with(connection_id, file_uri, validation_result) subject.enqueue(file_uri, document_version, connection_id) # Wait for the thread to complete @@ -25,8 +36,6 @@ end before(:each) do - subject.documents.clear - allow(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Manifest::ValidationProvider.validate mock should not be called") allow(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Epp::ValidationProvider.validate mock should not be called") allow(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Puppetfile::ValidationProvider.validate mock should not be called") @@ -34,56 +43,53 @@ context 'for an invalid or missing documents' do it 'should not return validation results' do - subject.documents.set_document(MANIFEST_FILENAME, FILE_CONTENT, document_version) + document_store.set_document(VALIDATE_MANIFEST_FILENAME, VALIDATE_FILE_CONTENT, document_version) - expect(PuppetLanguageServer::ValidationQueue).to_not receive(:send_diagnostics) + expect(subject).to_not receive(:send_diagnostics) - subject.enqueue(MANIFEST_FILENAME, document_version + 1, connection_id) + subject.enqueue(VALIDATE_MANIFEST_FILENAME, document_version + 1, connection_id) # Wait for the thread to complete subject.drain_queue end end context 'for a multiple items in the queue' do - let(:file_content0) { FILE_CONTENT + "_0" } - let(:file_content1) { FILE_CONTENT + "_1" } - let(:file_content2) { FILE_CONTENT + "_2" } - let(:file_content3) { FILE_CONTENT + "_3" } + let(:file_content0) { VALIDATE_FILE_CONTENT + "_0" } + let(:file_content1) { VALIDATE_FILE_CONTENT + "_1" } + let(:file_content2) { VALIDATE_FILE_CONTENT + "_2" } + let(:file_content3) { VALIDATE_FILE_CONTENT + "_3" } let(:validation_result) { [{ 'result' => 'MockResult' }] } let(:validation_options) { { :resolve_puppetfile => false } } - before(:each) do - end - it 'should only return the most recent validation results' do # Configure the document store - subject.documents.set_document(MANIFEST_FILENAME, file_content0, document_version + 0) - subject.documents.set_document(MANIFEST_FILENAME, file_content1, document_version + 1) - subject.documents.set_document(MANIFEST_FILENAME, file_content3, document_version + 3) - subject.documents.set_document(EPP_FILENAME, file_content1, document_version + 1) - subject.documents.set_document(PUPPETFILE_FILENAME, file_content1, document_version + 1) + document_store.set_document(VALIDATE_MANIFEST_FILENAME, file_content0, document_version + 0) + document_store.set_document(VALIDATE_MANIFEST_FILENAME, file_content1, document_version + 1) + document_store.set_document(VALIDATE_MANIFEST_FILENAME, file_content3, document_version + 3) + document_store.set_document(VALIDATE_EPP_FILENAME, file_content1, document_version + 1) + document_store.set_document(VALIDATE_PUPPETFILE_FILENAME, file_content1, document_version + 1) # Preconfigure the validation queue subject.reset_queue([ - { 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 0, 'document_type' => :manifest, 'connection_id' => connection_id }, - { 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :manifest, 'connection_id' => connection_id }, - { 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 3, 'document_type' => :manifest, 'connection_id' => connection_id }, - { 'file_uri' => EPP_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :epp, 'connection_id' => connection_id }, - { 'file_uri' => PUPPETFILE_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :puppetfile, 'connection_id' => connection_id }, + job(VALIDATE_MANIFEST_FILENAME, document_version + 0, connection_id), + job(VALIDATE_MANIFEST_FILENAME, document_version + 1, connection_id), + job(VALIDATE_MANIFEST_FILENAME, document_version + 3, connection_id), + job(VALIDATE_EPP_FILENAME, document_version + 1, connection_id), + job(VALIDATE_PUPPETFILE_FILENAME, document_version + 1, connection_id), ]) # We only expect the following results to be returned expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(file_content2, Hash).and_return(validation_result) expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(file_content1).and_return(validation_result) expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(file_content1, Hash).and_return(validation_result) - expect(PuppetLanguageServer::ValidationQueue).to receive(:send_diagnostics).with(connection_id, MANIFEST_FILENAME, validation_result) - expect(PuppetLanguageServer::ValidationQueue).to receive(:send_diagnostics).with(connection_id, EPP_FILENAME, validation_result) - expect(PuppetLanguageServer::ValidationQueue).to receive(:send_diagnostics).with(connection_id, PUPPETFILE_FILENAME, validation_result) + expect(subject).to receive(:send_diagnostics).with(connection_id, VALIDATE_MANIFEST_FILENAME, validation_result) + expect(subject).to receive(:send_diagnostics).with(connection_id, VALIDATE_EPP_FILENAME, validation_result) + expect(subject).to receive(:send_diagnostics).with(connection_id, VALIDATE_PUPPETFILE_FILENAME, validation_result) - # Simulate a new document begin added by adding it to the document store and + # Simulate a new document being added, by adding it to the document store and # enqueue validation for a version that it's in the middle of the versions in the queue - subject.documents.set_document(MANIFEST_FILENAME, file_content2, document_version + 2) - subject.enqueue(MANIFEST_FILENAME, document_version + 2, connection_id) + document_store.set_document(VALIDATE_MANIFEST_FILENAME, file_content2, document_version + 2) + subject.enqueue(VALIDATE_MANIFEST_FILENAME, document_version + 2, connection_id) # Wait for the thread to complete subject.drain_queue end @@ -94,104 +100,96 @@ validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(FILE_CONTENT, Hash).and_return(validation_result) + expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT, Hash).and_return(validation_result) end - it_should_behave_like "single document which sends validation results", MANIFEST_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "single document which sends validation results", VALIDATE_MANIFEST_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'of a Puppetfile file' do validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(FILE_CONTENT, Hash).and_return(validation_result) + expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT, Hash).and_return(validation_result) end - it_should_behave_like "single document which sends validation results", PUPPETFILE_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "single document which sends validation results", VALIDATE_PUPPETFILE_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'of a EPP template file' do validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(FILE_CONTENT).and_return(validation_result) + expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT).and_return(validation_result) end - it_should_behave_like "single document which sends validation results", EPP_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "single document which sends validation results", VALIDATE_EPP_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'of a unknown file' do validation_result = [] - it_should_behave_like "single document which sends validation results", UNKNOWN_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "single document which sends validation results", VALIDATE_UNKNOWN_FILENAME, VALIDATE_FILE_CONTENT, validation_result end end end - describe '#validate_sync' do + describe '#execute' do shared_examples_for "document which sends validation results" do |file_uri, file_content, validation_result| it 'should send validation results' do - subject.documents.set_document(file_uri, file_content, document_version) - expect(PuppetLanguageServer::ValidationQueue).to receive(:send_diagnostics).with(connection_id, file_uri, validation_result) + document_store.set_document(file_uri, file_content, document_version) + expect(subject).to receive(:send_diagnostics).with(connection_id, file_uri, validation_result) - subject.validate_sync(file_uri, document_version, connection_id) + subject.execute(file_uri, document_version, connection_id) end end before(:each) do - subject.documents.clear - allow(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Manifest::ValidationProvider.validate mock should not be called") allow(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Epp::ValidationProvider.validate mock should not be called") allow(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Puppetfile::ValidationProvider.validate mock should not be called") end it 'should not send validation results for documents that do not exist' do - expect(PuppetLanguageServer::ValidationQueue).to_not receive(:send_diagnostics) + expect(subject).to_not receive(:send_diagnostics) - subject.validate_sync(MISSING_FILENAME, 1, connection_id) + subject.execute(VALIDATE_MISSING_FILENAME, 1, connection_id) end context 'for a puppet manifest file' do validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(FILE_CONTENT, Hash).and_return(validation_result) + expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT, Hash).and_return(validation_result) end - it_should_behave_like "document which sends validation results", MANIFEST_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "document which sends validation results", VALIDATE_MANIFEST_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'for a Puppetfile file' do validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(FILE_CONTENT, Hash).and_return(validation_result) + expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT, Hash).and_return(validation_result) end - it_should_behave_like "document which sends validation results", PUPPETFILE_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "document which sends validation results", VALIDATE_PUPPETFILE_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'for an EPP template file' do validation_result = [{ 'result' => 'MockResult' }] before(:each) do - expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(FILE_CONTENT).and_return(validation_result) + expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(VALIDATE_FILE_CONTENT).and_return(validation_result) end - it_should_behave_like "document which sends validation results", EPP_FILENAME, FILE_CONTENT, validation_result + it_should_behave_like "document which sends validation results", VALIDATE_EPP_FILENAME, VALIDATE_FILE_CONTENT, validation_result end context 'for an unknown file' do validation_result = [] - it_should_behave_like "document which sends validation results", UNKNOWN_FILENAME, FILE_CONTENT, validation_result - end - end - - describe '#documents' do - it 'should respond to documents method' do - expect(subject).to respond_to(:documents) + it_should_behave_like "document which sends validation results", VALIDATE_UNKNOWN_FILENAME, VALIDATE_FILE_CONTENT, validation_result end end end diff --git a/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb b/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb index fef45230..277857e8 100644 --- a/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/message_handler_spec.rb @@ -912,7 +912,7 @@ end it 'should enqueue the file for validation' do - expect(PuppetLanguageServer::ValidationQueue).to receive(:enqueue).with(file_uri, 1, Object, Hash) + expect(PuppetLanguageServer::GlobalQueues::validate_queue).to receive(:enqueue).with(file_uri, 1, Object, Hash) subject.notification_textdocument_didopen(connection_id, notification_message) end end @@ -984,7 +984,7 @@ end it 'should enqueue the file for validation' do - expect(PuppetLanguageServer::ValidationQueue).to receive(:enqueue).with(file_uri, 2, Object, Hash) + expect(PuppetLanguageServer::GlobalQueues::validate_queue).to receive(:enqueue).with(file_uri, 2, Object, Hash) subject.notification_textdocument_didchange(connection_id, notification_message) end end From 33ed34581d53d934935fdb6f4b83cd37f55444b1 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Mon, 2 Dec 2019 09:19:08 +0800 Subject: [PATCH 07/34] (GH-209) Refactor Crash Dump to use session state This commit updates the Crash Dump to use the session_state instead of the global DocumentStore module. --- lib/puppet-languageserver/crash_dump.rb | 6 +++--- lib/puppet-languageserver/message_handler.rb | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/puppet-languageserver/crash_dump.rb b/lib/puppet-languageserver/crash_dump.rb index f66b6de4..66f01671 100644 --- a/lib/puppet-languageserver/crash_dump.rb +++ b/lib/puppet-languageserver/crash_dump.rb @@ -6,7 +6,7 @@ def self.default_crash_file File.join(Dir.tmpdir, 'puppet_language_server_crash.txt') end - def self.write_crash_file(err, filename = nil, additional = {}) + def self.write_crash_file(err, session_state, filename = nil, additional = {}) # Create the crash text puppet_version = Puppet.version rescue 'Unknown' # rubocop:disable Lint/RescueWithoutErrorClass, Style/RescueModifier @@ -33,8 +33,8 @@ def self.write_crash_file(err, filename = nil, additional = {}) # rubocop:enable Layout/IndentHeredoc, Layout/ClosingHeredocIndentation, Style/FormatStringToken # Append the documents in the cache - PuppetLanguageServer::DocumentStore.document_uris.each do |uri| - crashtext += "Document - #{uri}\n---\n#{PuppetLanguageServer::DocumentStore.document(uri)}\n\n" + session_state.documents.document_uris.each do |uri| + crashtext += "Document - #{uri}\n---\n#{session_state.documents.document(uri)}\n\n" end # Append additional objects from the crash additional.each do |k, v| diff --git a/lib/puppet-languageserver/message_handler.rb b/lib/puppet-languageserver/message_handler.rb index 0e5a0e35..63c2bf40 100644 --- a/lib/puppet-languageserver/message_handler.rb +++ b/lib/puppet-languageserver/message_handler.rb @@ -73,12 +73,16 @@ def initialize(*_) @session_state = ClientSessionState.new(self, :documents => DocumentStore.instance) end + def session_state # rubocop:disable Style/TrivialAccessors During the refactor, this is fine. + @session_state + end + def language_client - @session_state.language_client + session_state.language_client end def documents - @session_state.documents + session_state.documents end def request_initialize(_, json_rpc_message) @@ -393,7 +397,7 @@ def response_workspace_configuration(_, json_rpc_message, original_request) def unhandled_exception(error, options) super(error, options) - PuppetLanguageServer::CrashDump.write_crash_file(error, nil, options) + PuppetLanguageServer::CrashDump.write_crash_file(error, session_state, nil, options) end private From d3a424272ff6929d52c4d5c10fab58d2a48b6abf Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Mon, 2 Dec 2019 10:19:53 +0800 Subject: [PATCH 08/34] (GH-209) Refactor Sidecar queue This commit: * Uses the single_instance_queue for sidecar queue * The Sidecar queue gets session_state via the connection_id property. Removing the calls to the global PuppetHelper module. * Adds the connection_id getter and setter methods to the PuppetHelper to help transition the refactor. These methods will be removed in later commits once the refactor is complete * Because the Sidecar Queue now requires a connection_id and session_state it is no longer possible to preload the Puppet information without an actual (or mocked connection). Therefore the spec_helper is updated to inject a JSON fixture file into the object cache instead of doing actual sidecar calls. This also speeds up tests run time as it no longer has to _actually_ do real queries, but just consume a test fixture. # Please enter the commit message for your changes. Lines starting --- lib/puppet-languageserver/facter_helper.rb | 16 +- lib/puppet-languageserver/global_queues.rb | 5 + .../global_queues/sidecar_queue.rb | 198 ++++++++++++++++ lib/puppet-languageserver/message_handler.rb | 35 ++- lib/puppet-languageserver/puppet_helper.rb | 51 ++-- lib/puppet-languageserver/sidecar_queue.rb | 224 ------------------ lib/puppet_languageserver.rb | 48 +--- .../fixtures/fact_object_cache.json | 1 + .../fixtures/puppet_object_cache.json | 1 + .../{ => global_queues}/sidecar_queue_spec.rb | 83 ++++--- .../manifest/completion_provider_spec.rb | 2 +- .../manifest/definition_provider_spec.rb | 15 +- spec/languageserver/spec_helper.rb | 52 ++-- .../manifest/completion_provider_spec.rb | 6 - tools/generate_puppet_fixtures.rb | 124 ++++++++++ 15 files changed, 497 insertions(+), 364 deletions(-) create mode 100644 lib/puppet-languageserver/global_queues/sidecar_queue.rb delete mode 100644 lib/puppet-languageserver/sidecar_queue.rb create mode 100644 spec/languageserver/fixtures/fact_object_cache.json create mode 100644 spec/languageserver/fixtures/puppet_object_cache.json rename spec/languageserver/integration/puppet-languageserver/{ => global_queues}/sidecar_queue_spec.rb (62%) create mode 100644 tools/generate_puppet_fixtures.rb diff --git a/lib/puppet-languageserver/facter_helper.rb b/lib/puppet-languageserver/facter_helper.rb index 40cc0007..b0876f95 100644 --- a/lib/puppet-languageserver/facter_helper.rb +++ b/lib/puppet-languageserver/facter_helper.rb @@ -23,12 +23,12 @@ def self.assert_facts_loaded def self.load_facts @facts_loaded = false - sidecar_queue.execute_sync('facts', []) + sidecar_queue.execute('facts', [], false, connection_id) end def self.load_facts_async @facts_loaded = false - sidecar_queue.enqueue('facts', []) + sidecar_queue.enqueue('facts', [], false, connection_id) end def self.fact(name) @@ -46,5 +46,17 @@ def self.fact_names return [] if @facts_loaded == false cache.object_names_by_section(:fact).map(&:to_s) end + + # This is a temporary module level variable. It will be removed once FacterHelper + # is refactored into a session_state style class + def self.connection_id + @connection_id + end + + # This is a temporary module level variable. It will be removed once FacterHelper + # is refactored into a session_state style class + def self.connection_id=(value) + @connection_id = value + end end end diff --git a/lib/puppet-languageserver/global_queues.rb b/lib/puppet-languageserver/global_queues.rb index 650d6f94..f3a59fa4 100644 --- a/lib/puppet-languageserver/global_queues.rb +++ b/lib/puppet-languageserver/global_queues.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true require 'puppet-languageserver/global_queues/validation_queue' +require 'puppet-languageserver/global_queues/sidecar_queue' module PuppetLanguageServer module GlobalQueues def self.validate_queue @validate_queue ||= ValidationQueue.new end + + def self.sidecar_queue + @sidecar_queue ||= SidecarQueue.new + end end end diff --git a/lib/puppet-languageserver/global_queues/sidecar_queue.rb b/lib/puppet-languageserver/global_queues/sidecar_queue.rb new file mode 100644 index 00000000..a01dddac --- /dev/null +++ b/lib/puppet-languageserver/global_queues/sidecar_queue.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'puppet-languageserver/global_queues/single_instance_queue' +require 'puppet_editor_services/server' +require 'open3' + +module PuppetLanguageServer + module GlobalQueues + class SidecarQueueJob < SingleInstanceQueueJob + attr_accessor :action + attr_accessor :additional_args + attr_accessor :handle_errors + attr_accessor :connection_id + + def initialize(action, additional_args, handle_errors, connection_id) + @action = action + @additional_args = additional_args + @handle_errors = handle_errors + @connection_id = connection_id + end + + def key + "#{action}-#{connection_id}" + end + end + + # Module for enqueing and running sidecar jobs asynchronously + # When adding a job, it will remove any other for the same + # job in the queue, so that only the latest job needs to be processed. + class SidecarQueue < SingleInstanceQueue + def max_queue_threads + 2 + end + + def job_class + SidecarQueueJob + end + + def execute_job(job_object) + super(job_object) + connection = connection_from_connection_id(job_object.connection_id) + raise "Connection is not available for connection id #{job_object.connection_id}" if connection.nil? + sidecar_path = File.expand_path(File.join(__dir__, '..', '..', '..', 'puppet-languageserver-sidecar')) + args = ['--action', job_object.action].concat(job_object.additional_args).concat(sidecar_args_from_connection(connection)) + cmd = ['ruby', sidecar_path].concat(args) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: Running sidecar #{cmd}") + stdout, stderr, status = run_sidecar(cmd) + PuppetLanguageServer.log_message(:warning, "SidecarQueue Thread: Calling sidecar with #{args.join(' ')} returned exitcode #{status.exitstatus}, #{stderr}") + return nil unless status.exitstatus.zero? + + # It's possible server has closed the connection while the sidecar is running. + # So raise if the connection is no longer available + raise "Connection is no longer available for connection id #{job_object.connection_id}" if connection_from_connection_id(job_object.connection_id).nil? + session_state = session_state_from_connection(connection) + raise "Session state is not available for connection id #{job_object.connection_id}" if session_state.nil? + cache = session_state.object_cache + + # Correctly encode the result as UTF8 + result = stdout.bytes.pack('U*') + + case job_object.action.downcase + when 'default_aggregate' + lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result) + cache.import_sidecar_list!(lists.classes, :class, :default) + cache.import_sidecar_list!(lists.datatypes, :datatype, :default) + cache.import_sidecar_list!(lists.functions, :function, :default) + cache.import_sidecar_list!(lists.types, :type, :default) + + PuppetLanguageServer::PuppetHelper.assert_default_classes_loaded + PuppetLanguageServer::PuppetHelper.assert_default_functions_loaded + PuppetLanguageServer::PuppetHelper.assert_default_types_loaded + PuppetLanguageServer::PuppetHelper.assert_default_datatypes_loaded + + lists.each_list do |k, v| + if v.nil? + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned no #{k}") + else + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned #{v.count} #{k}") + end + end + + when 'default_classes' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result) + cache.import_sidecar_list!(list, :class, :default) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_classes returned #{list.count} items") + + PuppetLanguageServer::PuppetHelper.assert_default_classes_loaded + + when 'default_datatypes' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result) + cache.import_sidecar_list!(list, :datatype, :default) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_datatypes returned #{list.count} items") + + PuppetLanguageServer::PuppetHelper.assert_default_datatypes_loaded + + when 'default_functions' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result) + cache.import_sidecar_list!(list, :function, :default) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_functions returned #{list.count} items") + + PuppetLanguageServer::PuppetHelper.assert_default_functions_loaded + + when 'default_types' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result) + cache.import_sidecar_list!(list, :type, :default) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_types returned #{list.count} items") + + PuppetLanguageServer::PuppetHelper.assert_default_types_loaded + + when 'facts' + list = PuppetLanguageServer::Sidecar::Protocol::FactList.new.from_json!(result) + cache.import_sidecar_list!(list, :fact, :default) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: facts returned #{list.count} items") + + PuppetLanguageServer::FacterHelper.assert_facts_loaded + + when 'node_graph' + return PuppetLanguageServer::Sidecar::Protocol::PuppetNodeGraph.new.from_json!(result) + + when 'resource_list' + return PuppetLanguageServer::Sidecar::Protocol::ResourceList.new.from_json!(result) + + when 'workspace_aggregate' + lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result) + cache.import_sidecar_list!(lists.classes, :class, :workspace) + cache.import_sidecar_list!(lists.datatypes, :datatype, :workspace) + cache.import_sidecar_list!(lists.functions, :function, :workspace) + cache.import_sidecar_list!(lists.types, :type, :workspace) + + lists.each_list do |k, v| + if v.nil? + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned no #{k}") + else + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned #{v.count} #{k}") + end + end + + when 'workspace_classes' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result) + cache.import_sidecar_list!(list, :class, :workspace) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_classes returned #{list.count} items") + + when 'workspace_datatypes' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result) + cache.import_sidecar_list!(list, :datatype, :workspace) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_datatypes returned #{list.count} items") + + when 'workspace_functions' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result) + cache.import_sidecar_list!(list, :function, :workspace) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_functions returned #{list.count} items") + + when 'workspace_types' + list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result) + cache.import_sidecar_list!(list, :type, :workspace) + PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_types returned #{list.count} items") + + else + PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Unknown action #{job_object.action}") + end + + true + rescue StandardError => e + raise unless job_object.handle_errors + PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Error running action #{job_object.action}. #{e}") + nil + end + + private + + def connection_from_connection_id(connection_id) + PuppetEditorServices::Server.current_server.connection(connection_id) + end + + def session_state_from_connection(connection) + return if connection.nil? + handler = connection.protocol.handler + handler.respond_to?(:session_state) ? handler.session_state : nil + end + + def run_sidecar(cmd) + Open3.capture3(*cmd) + end + + def sidecar_args_from_connection(connection) + return nil if connection.nil? + options = connection.server.handler_options + return [] if options.nil? + result = [] + result << '--no-cache' if options[:disable_sidecar_cache] + result << "--puppet-version=#{Puppet.version}" + result << "--feature-flags=#{options[:flags].join(',')}" if options[:flags] && !options[:flags].empty? + result << "--puppet-settings=#{options[:puppet_settings].join(',')}" if options[:puppet_settings] && !options[:puppet_settings].empty? + result + end + end + end +end diff --git a/lib/puppet-languageserver/message_handler.rb b/lib/puppet-languageserver/message_handler.rb index 63c2bf40..1fb509a3 100644 --- a/lib/puppet-languageserver/message_handler.rb +++ b/lib/puppet-languageserver/message_handler.rb @@ -70,7 +70,7 @@ def self.store_has_environmentconf? class MessageHandler < PuppetEditorServices::Handler::JsonRPC def initialize(*_) super - @session_state = ClientSessionState.new(self, :documents => DocumentStore.instance) + @session_state = ClientSessionState.new(self, :documents => DocumentStore.instance, :object_cache => PuppetLanguageServer::PuppetHelper.cache) end def session_state # rubocop:disable Style/TrivialAccessors During the refactor, this is fine. @@ -85,8 +85,13 @@ def documents session_state.documents end - def request_initialize(_, json_rpc_message) + def request_initialize(connection_id, json_rpc_message) PuppetLanguageServer.log_message(:debug, 'Received initialize method') + + # This is a temporary module level variable. It will be removed once refactored into a session_state style class + PuppetLanguageServer::PuppetHelper.connection_id = connection_id + PuppetLanguageServer::FacterHelper.connection_id = connection_id + language_client.parse_lsp_initialize!(json_rpc_message.params) # Setup static registrations if dynamic registration is not available info = { @@ -98,6 +103,32 @@ def request_initialize(_, json_rpc_message) :workspace => workspace_root_from_initialize_params(json_rpc_message.params) ) + # Initiate loading the object_cache + if PuppetLanguageServer.featureflag?('puppetstrings') + PuppetLanguageServer.log_message(:info, 'Loading Default metadata (Async)...') + PuppetLanguageServer::PuppetHelper.load_default_aggregate_async + + PuppetLanguageServer.log_message(:info, 'Loading Facter (Async)...') + PuppetLanguageServer::FacterHelper.load_facts_async + else + PuppetLanguageServer.log_message(:info, 'Loading Puppet Types (Async)...') + PuppetLanguageServer::PuppetHelper.load_default_types_async + + PuppetLanguageServer.log_message(:info, 'Loading Facter (Async)...') + PuppetLanguageServer::FacterHelper.load_facts_async + + PuppetLanguageServer.log_message(:info, 'Loading Functions (Async)...') + PuppetLanguageServer::PuppetHelper.load_default_functions_async + + PuppetLanguageServer.log_message(:info, 'Loading Classes (Async)...') + PuppetLanguageServer::PuppetHelper.load_default_classes_async + + PuppetLanguageServer.log_message(:info, 'Loading DataTypes (Async)...') + PuppetLanguageServer::PuppetHelper.load_default_datatypes_async + end + PuppetLanguageServer.log_message(:info, 'Loading static data (Async)...') + PuppetLanguageServer::PuppetHelper.load_static_data_async + # Initiate loading of the workspace if needed if documents.store_has_module_metadata? || documents.store_has_environmentconf? PuppetLanguageServer.log_message(:info, 'Loading Workspace (Async)...') diff --git a/lib/puppet-languageserver/puppet_helper.rb b/lib/puppet-languageserver/puppet_helper.rb index e19896f0..e732e84e 100644 --- a/lib/puppet-languageserver/puppet_helper.rb +++ b/lib/puppet-languageserver/puppet_helper.rb @@ -3,6 +3,7 @@ require 'pathname' require 'tempfile' require 'puppet-languageserver/session_state/object_cache' +require 'puppet-languageserver/global_queues' module PuppetLanguageServer module PuppetHelper @@ -12,13 +13,9 @@ module PuppetHelper @default_functions_loaded = nil @default_classes_loaded = nil @inmemory_cache = nil - @sidecar_queue_obj = nil - @helper_options = nil - def self.initialize_helper(options = {}) - @helper_options = options + def self.initialize_helper(_options) @inmemory_cache = PuppetLanguageServer::SessionState::ObjectCache.new - sidecar_queue.cache = @inmemory_cache end def self.module_path @@ -43,7 +40,7 @@ def self.get_node_graph(content, local_workspace) args = ['--action-parameters=' + ap.to_json] args << "--local-workspace=#{local_workspace}" unless local_workspace.nil? - sidecar_queue.execute_sync('node_graph', args, false) + sidecar_queue.execute('node_graph', args, false, connection_id) end end @@ -55,7 +52,7 @@ def self.get_puppet_resource(typename, title, local_workspace) args = ['--action-parameters=' + ap.to_json] args << "--local-workspace=#{local_workspace}" unless local_workspace.nil? - sidecar_queue.execute_sync('resource_list', args) + sidecar_queue.execute('resource_list', args, false, connection_id) end # Static data @@ -112,13 +109,13 @@ def self.assert_default_types_loaded def self.load_default_types raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_types_loaded = false - sidecar_queue.execute_sync('default_types', []) + sidecar_queue.execute('default_types', [], false, connection_id) end def self.load_default_types_async raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_types_loaded = false - sidecar_queue.enqueue('default_types', []) + sidecar_queue.enqueue('default_types', [], false, connection_id) end def self.get_type(name) @@ -145,13 +142,13 @@ def self.assert_default_functions_loaded def self.load_default_functions raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_functions_loaded = false - sidecar_queue.execute_sync('default_functions', []) + sidecar_queue.execute('default_functions', [], false, connection_id) end def self.load_default_functions_async raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_functions_loaded = false - sidecar_queue.enqueue('default_functions', []) + sidecar_queue.enqueue('default_functions', [], false, connection_id) end def self.filtered_function_names(&block) @@ -199,13 +196,13 @@ def self.assert_default_classes_loaded def self.load_default_classes raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_classes_loaded = false - sidecar_queue.execute_sync('default_classes', []) + sidecar_queue.execute('default_classes', [], false, connection_id) end def self.load_default_classes_async raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_classes_loaded = false - sidecar_queue.enqueue('default_classes', []) + sidecar_queue.enqueue('default_classes', [], false, connection_id) end def self.get_class(name) @@ -233,13 +230,13 @@ def self.assert_default_datatypes_loaded def self.load_default_datatypes raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_datatypes_loaded = false - sidecar_queue.execute_sync('default_datatypes', []) + sidecar_queue.execute('default_datatypes', [], false, connection_id) end def self.load_default_datatypes_async raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil? @default_datatypes_loaded = false - sidecar_queue.enqueue('default_datatypes', []) + sidecar_queue.enqueue('default_datatypes', [], false, connection_id) end def self.datatype(name, tasks_mode = false) @@ -261,11 +258,23 @@ def self.cache @inmemory_cache end + # This is a temporary module level variable. It will be removed once PuppetHelper + # is refactored into a session_state style class + def self.connection_id + @connection_id + end + + # This is a temporary module level variable. It will be removed once PuppetHelper + # is refactored into a session_state style class + def self.connection_id=(value) + @connection_id = value + end + # Workspace Loading def self.load_workspace_async if PuppetLanguageServer.featureflag?('puppetstrings') return true if PuppetLanguageServer::DocumentStore.store_root_path.nil? - sidecar_queue.enqueue('workspace_aggregate', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path]) + sidecar_queue.enqueue('workspace_aggregate', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path], false, connection_id) return true end load_workspace_classes_async @@ -276,17 +285,17 @@ def self.load_workspace_async def self.load_workspace_classes_async return if PuppetLanguageServer::DocumentStore.store_root_path.nil? - sidecar_queue.enqueue('workspace_classes', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path]) + sidecar_queue.enqueue('workspace_classes', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path], false, connection_id) end def self.load_workspace_functions_async return if PuppetLanguageServer::DocumentStore.store_root_path.nil? - sidecar_queue.enqueue('workspace_functions', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path]) + sidecar_queue.enqueue('workspace_functions', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path], false, connection_id) end def self.load_workspace_types_async return if PuppetLanguageServer::DocumentStore.store_root_path.nil? - sidecar_queue.enqueue('workspace_types', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path]) + sidecar_queue.enqueue('workspace_types', ['--local-workspace', PuppetLanguageServer::DocumentStore.store_root_path], false, connection_id) end def self.purge_workspace @@ -296,7 +305,7 @@ def self.purge_workspace end def self.sidecar_queue - @sidecar_queue_obj ||= PuppetLanguageServer::SidecarQueue.new(@helper_options) + PuppetLanguageServer::GlobalQueues.sidecar_queue end def self.with_temporary_file(content) @@ -317,7 +326,7 @@ def self.load_default_aggregate_async @default_classes_loaded = false if @default_classes_loaded.nil? @default_functions_loaded = false if @default_functions_loaded.nil? @default_types_loaded = false if @default_types_loaded.nil? - sidecar_queue.enqueue('default_aggregate', []) + sidecar_queue.enqueue('default_aggregate', [], false, connection_id) end end end diff --git a/lib/puppet-languageserver/sidecar_queue.rb b/lib/puppet-languageserver/sidecar_queue.rb deleted file mode 100644 index 9dbb5d61..00000000 --- a/lib/puppet-languageserver/sidecar_queue.rb +++ /dev/null @@ -1,224 +0,0 @@ -# frozen_string_literal: true - -require 'open3' - -module PuppetLanguageServer - # Module for enqueing and running sidecar jobs asynchronously - # When adding a job, it will remove any other for the same - # job in the queue, so that only the latest job needs to be processed. - class SidecarQueue - attr_writer :cache - - def initialize(options = {}) - @queue = [] - @queue_mutex = Mutex.new - @queue_threads_mutex = Mutex.new - @queue_threads = [] - @cache = nil - @queue_options = options - end - - def queue_size - 2 - end - - # Enqueue a sidecar action - def enqueue(action, additional_args) - @queue_mutex.synchronize do - @queue.reject! { |item| item[:action] == action } - @queue << { action: action, additional_args: additional_args } - end - - @queue_threads_mutex.synchronize do - # Clear up any done threads - @queue_threads.reject! { |item| item.nil? || !item.alive? } - # Append a new thread if we have space - if @queue_threads.count < queue_size - @queue_threads << Thread.new do - begin - worker - rescue => e # rubocop:disable Style/RescueStandardError - PuppetLanguageServer.log_message(:error, "Error in SidecarQueue Thread: #{e}") - raise - end - end - end - end - nil - end - - # Synchronously call the sidecar - # Returns nil if an error occurs, otherwise returns an object - def execute_sync(action, additional_args, handle_errors = false) - return nil if @cache.nil? - sidecar_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'puppet-languageserver-sidecar')) - args = ['--action', action].concat(additional_args).concat(sidecar_args_from_options) - - cmd = ['ruby', sidecar_path].concat(args) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: Running sidecar #{cmd}") - stdout, stderr, status = run_sidecar(cmd) - PuppetLanguageServer.log_message(:warning, "SidecarQueue Thread: Calling sidecar with #{args.join(' ')} returned exitcode #{status.exitstatus}, #{stderr}") - return nil unless status.exitstatus.zero? - # Correctly encode the result as UTF8 - result = stdout.bytes.pack('U*') - - case action.downcase - when 'default_aggregate' - lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result) - @cache.import_sidecar_list!(lists.classes, :class, :default) - @cache.import_sidecar_list!(lists.datatypes, :datatype, :default) - @cache.import_sidecar_list!(lists.functions, :function, :default) - @cache.import_sidecar_list!(lists.types, :type, :default) - - PuppetLanguageServer::PuppetHelper.assert_default_classes_loaded - PuppetLanguageServer::PuppetHelper.assert_default_functions_loaded - PuppetLanguageServer::PuppetHelper.assert_default_types_loaded - PuppetLanguageServer::PuppetHelper.assert_default_datatypes_loaded - - lists.each_list do |k, v| - if v.nil? - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned no #{k}") - else - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned #{v.count} #{k}") - end - end - - when 'default_classes' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result) - @cache.import_sidecar_list!(list, :class, :default) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_classes returned #{list.count} items") - - PuppetLanguageServer::PuppetHelper.assert_default_classes_loaded - - when 'default_datatypes' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result) - @cache.import_sidecar_list!(list, :datatype, :default) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_datatypes returned #{list.count} items") - - PuppetLanguageServer::PuppetHelper.assert_default_datatypes_loaded - - when 'default_functions' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result) - @cache.import_sidecar_list!(list, :function, :default) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_functions returned #{list.count} items") - - PuppetLanguageServer::PuppetHelper.assert_default_functions_loaded - - when 'default_types' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result) - @cache.import_sidecar_list!(list, :type, :default) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_types returned #{list.count} items") - - PuppetLanguageServer::PuppetHelper.assert_default_types_loaded - - when 'facts' - list = PuppetLanguageServer::Sidecar::Protocol::FactList.new.from_json!(result) - @cache.import_sidecar_list!(list, :fact, :default) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: facts returned #{list.count} items") - - PuppetLanguageServer::FacterHelper.assert_facts_loaded - - when 'node_graph' - return PuppetLanguageServer::Sidecar::Protocol::PuppetNodeGraph.new.from_json!(result) - - when 'resource_list' - return PuppetLanguageServer::Sidecar::Protocol::ResourceList.new.from_json!(result) - - when 'workspace_aggregate' - lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result) - @cache.import_sidecar_list!(lists.classes, :class, :workspace) - @cache.import_sidecar_list!(lists.datatypes, :datatype, :workspace) - @cache.import_sidecar_list!(lists.functions, :function, :workspace) - @cache.import_sidecar_list!(lists.types, :type, :workspace) - - lists.each_list do |k, v| - if v.nil? - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned no #{k}") - else - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned #{v.count} #{k}") - end - end - - when 'workspace_classes' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result) - @cache.import_sidecar_list!(list, :class, :workspace) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_classes returned #{list.count} items") - - when 'workspace_datatypes' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result) - @cache.import_sidecar_list!(list, :datatype, :workspace) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_datatypes returned #{list.count} items") - - when 'workspace_functions' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result) - @cache.import_sidecar_list!(list, :function, :workspace) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_functions returned #{list.count} items") - - when 'workspace_types' - list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result) - @cache.import_sidecar_list!(list, :type, :workspace) - PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_types returned #{list.count} items") - - else - PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Unknown action action #{action}") - end - - true - rescue StandardError => e - raise unless handle_errors - PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Error running action #{action}. #{e}") - nil - end - - # Wait for the queue to become empty - def drain_queue - @queue_threads.each do |item| - item.join unless item.nil? || !item.alive? - end - nil - end - - # Testing helper resets the queue and prepopulates it with - # a known arbitrary configuration. - # ONLY USE THIS FOR TESTING! - def reset_queue(initial_state = []) - @queue_mutex.synchronize do - @queue = initial_state - end - end - - private - - # Thread worker which processes all jobs in the queue and calls the sidecar for each action - def worker - work_item = nil - loop do - @queue_mutex.synchronize do - return if @queue.empty? - work_item = @queue.shift - end - return if work_item.nil? - - action = work_item[:action] - additional_args = work_item[:additional_args] - - # Perform action - _result = execute_sync(action, additional_args) - end - end - - def run_sidecar(cmd) - Open3.capture3(*cmd) - end - - def sidecar_args_from_options - return [] if @queue_options.nil? - result = [] - result << '--no-cache' if @queue_options[:disable_sidecar_cache] - result << "--puppet-version=#{Puppet.version}" - result << "--feature-flags=#{@queue_options[:flags].join(',')}" if @queue_options[:flags] && !@queue_options[:flags].empty? - result << "--puppet-settings=#{@queue_options[:puppet_settings].join(',')}" if @queue_options[:puppet_settings] && !@queue_options[:puppet_settings].empty? - result - end - end -end diff --git a/lib/puppet_languageserver.rb b/lib/puppet_languageserver.rb index 95c6ad14..0ce1c566 100644 --- a/lib/puppet_languageserver.rb +++ b/lib/puppet_languageserver.rb @@ -79,9 +79,8 @@ def self.require_gems(options) # These libraries require the puppet and LSP gems. %w[ - global_queues sidecar_protocol - sidecar_queue + global_queues puppet_parser_helper puppet_helper facter_helper @@ -156,7 +155,7 @@ def self.parse(options) args[:debug] = debug end - opts.on('-s', '--slow-start', 'Delay starting the Language Server until Puppet initialisation has completed. Default is to start fast') do |_misc| + opts.on('-s', '--slow-start', '** DEPRECATED ** Delay starting the Language Server until Puppet initialisation has completed. Default is to start fast') do |_misc| args[:fast_start_langserver] = false end @@ -215,6 +214,7 @@ def self.init_puppet(options) return unless active? log_message(:info, "Using Puppet v#{Puppet.version}") + log_message(:info, "Using Facter v#{Facter.version}") log_message(:debug, "Detected additional puppet settings #{options[:puppet_settings]}") options[:puppet_settings].nil? ? Puppet.initialize_settings : Puppet.initialize_settings(options[:puppet_settings]) @@ -223,52 +223,12 @@ def self.init_puppet(options) PuppetLanguageServer::PuppetHelper.initialize_helper(options) log_message(:info, 'Initializing settings...') - if options[:fast_start_langserver] - Thread.new do - init_puppet_worker(options) - end - else - init_puppet_worker(options) - end - true - end - - def self.init_puppet_worker(options) # Remove all other logging destinations except for ours Puppet::Util::Log.destinations.clear Puppet::Util::Log.newdestination('null_logger') - log_message(:info, "Using Facter v#{Facter.version}") - if options[:preload_puppet] - if featureflag?('puppetstrings') - log_message(:info, 'Preloading Default metadata (Async)...') - PuppetLanguageServer::PuppetHelper.load_default_aggregate_async - - log_message(:info, 'Preloading Facter (Async)...') - PuppetLanguageServer::FacterHelper.load_facts_async - else - log_message(:info, 'Preloading Puppet Types (Async)...') - PuppetLanguageServer::PuppetHelper.load_default_types_async - - log_message(:info, 'Preloading Facter (Async)...') - PuppetLanguageServer::FacterHelper.load_facts_async - - log_message(:info, 'Preloading Functions (Async)...') - PuppetLanguageServer::PuppetHelper.load_default_functions_async - - log_message(:info, 'Preloading Classes (Async)...') - PuppetLanguageServer::PuppetHelper.load_default_classes_async - - log_message(:info, 'Preloading DataTypes (Async)...') - PuppetLanguageServer::PuppetHelper.load_default_datatypes_async - end - - log_message(:info, 'Preloading static data (Async)...') - PuppetLanguageServer::PuppetHelper.load_static_data_async - else - log_message(:info, 'Skipping preloading Puppet') - end + true end def self.rpc_server(options) diff --git a/spec/languageserver/fixtures/fact_object_cache.json b/spec/languageserver/fixtures/fact_object_cache.json new file mode 100644 index 00000000..229f665c --- /dev/null +++ b/spec/languageserver/fixtures/fact_object_cache.json @@ -0,0 +1 @@ +[{"key":"aio_agent_version","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"6.6.0"},{"key":"architecture","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"x64"},{"key":"dhcp_servers","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"Ethernet0":"10.32.22.9","system":"10.32.22.9"}},{"key":"dmi","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"manufacturer":"VMware, Inc.","product":{"name":"VMware7,1","serial_number":"VMware-42 1a c5 9e 0d 1e 09 0f-a1 de 63 7a 5c 08 55 45","uuid":"9EC51A42-1E0D-0F09-A1DE-637A5C085545"}}},{"key":"domain","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"example.com"},{"key":"env_windows_installdir","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"C:\\Program Files\\Puppet Labs\\Puppet"},{"key":"facterversion","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"3.14.1"},{"key":"fqdn","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"foo.example.com"},{"key":"hardwareisa","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"x64"},{"key":"hardwaremodel","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"x86_64"},{"key":"hostname","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"foo"},{"key":"hypervisors","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"vmware":{}}},{"key":"id","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"DWSDU58COLV5CB3\\Administrator"},{"key":"identity","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"privileged":true,"user":"DWSDU58COLV5CB3\\Administrator"}},{"key":"interfaces","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Ethernet0"},{"key":"ipaddress","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"10.16.119.74"},{"key":"ipaddress6","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"fe80::b184:711f:f4a:414f%12"},{"key":"ipaddress6_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"fe80::b184:711f:f4a:414f%12"},{"key":"ipaddress_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"10.16.119.74"},{"key":"is_virtual","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":true},{"key":"kernel","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"windows"},{"key":"kernelmajversion","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"6.3"},{"key":"kernelrelease","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"6.3.9600"},{"key":"kernelversion","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"6.3.9600"},{"key":"macaddress","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"00:50:56:9A:73:0D"},{"key":"macaddress_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"00:50:56:9A:73:0D"},{"key":"manufacturer","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"VMware, Inc."},{"key":"memory","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"system":{"available":"3.16 GiB","available_bytes":3388596224,"capacity":"21.08%","total":"4.00 GiB","total_bytes":4293898240,"used":"863.36 MiB","used_bytes":905302016}}},{"key":"memoryfree","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"3.16 GiB"},{"key":"memoryfree_mb","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":3231.6171875},{"key":"memorysize","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"4.00 GiB"},{"key":"memorysize_mb","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":4094.98046875},{"key":"mtu_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":1500},{"key":"netmask","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"255.255.240.0"},{"key":"netmask6","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"ffff:ffff:ffff:ffff::"},{"key":"netmask6_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"ffff:ffff:ffff:ffff::"},{"key":"netmask_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"255.255.240.0"},{"key":"network","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"10.16.112.0"},{"key":"network6","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"fe80::%12"},{"key":"network6_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"fe80::%12"},{"key":"network_Ethernet0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"10.16.112.0"},{"key":"networking","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"dhcp":"10.32.22.9","domain":"example.com","fqdn":"foo.example.com","hostname":"foo","interfaces":{"Ethernet0":{"bindings":[{"address":"10.16.119.74","netmask":"255.255.240.0","network":"10.16.112.0"}],"bindings6":[{"address":"fe80::b184:711f:f4a:414f%12","netmask":"ffff:ffff:ffff:ffff::","network":"fe80::%12"}],"dhcp":"10.32.22.9","ip":"10.16.119.74","ip6":"fe80::b184:711f:f4a:414f%12","mac":"00:50:56:9A:73:0D","mtu":1500,"netmask":"255.255.240.0","netmask6":"ffff:ffff:ffff:ffff::","network":"10.16.112.0","network6":"fe80::%12"}},"ip":"10.16.119.74","ip6":"fe80::b184:711f:f4a:414f%12","mac":"00:50:56:9A:73:0D","mtu":1500,"netmask":"255.255.240.0","netmask6":"ffff:ffff:ffff:ffff::","network":"10.16.112.0","network6":"fe80::%12","primary":"Ethernet0"}},{"key":"operatingsystem","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"windows"},{"key":"operatingsystemmajrelease","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"2012 R2"},{"key":"operatingsystemrelease","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"2012 R2"},{"key":"os","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"architecture":"x64","family":"windows","hardware":"x86_64","name":"windows","release":{"full":"2012 R2","major":"2012 R2"},"windows":{"edition_id":"ServerStandard","installation_type":"Server","product_name":"Windows Server 2012 R2 Standard","system32":"C:\\Windows\\system32"}}},{"key":"osfamily","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"windows"},{"key":"path","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"C:\\Program Files\\Puppet Labs\\Puppet\\puppet\\bin;C:\\Program Files\\Puppet Labs\\Puppet\\bin;C:\\cygwin64\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Packer\\SysInternals;C:\\Program Files\\Git\\cmd;C:\\Program Files\\PowerShell\\6"},{"key":"physicalprocessorcount","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":2},{"key":"processor0","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz"},{"key":"processor1","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz"},{"key":"processorcount","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":2},{"key":"processors","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"count":2,"isa":"x64","models":["Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz","Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz"],"physicalcount":2}},{"key":"productname","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"VMware7,1"},{"key":"puppetversion","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"6.6.0"},{"key":"ruby","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"platform":"x64-mingw32","sitedir":"C:/Program Files/Puppet Labs/Puppet/puppet/lib/ruby/site_ruby/2.5.0","version":"2.5.3"}},{"key":"rubyplatform","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"x64-mingw32"},{"key":"rubysitedir","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"C:/Program Files/Puppet Labs/Puppet/puppet/lib/ruby/site_ruby/2.5.0"},{"key":"rubyversion","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"2.5.3"},{"key":"serialnumber","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"VMware-42 1a c5 9e 0d 1e 09 0f-a1 de 63 7a 5c 08 55 45"},{"key":"system32","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"C:\\Windows\\system32"},{"key":"system_uptime","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":{"days":0,"hours":0,"seconds":987,"uptime":"0:16 hours"}},{"key":"timezone","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Coordinated Universal Time"},{"key":"uptime","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"0:16 hours"},{"key":"uptime_days","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":0},{"key":"uptime_hours","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":0},{"key":"uptime_seconds","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":987},{"key":"uuid","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"9EC51A42-1E0D-0F09-A1DE-637A5C085545"},{"key":"virtual","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"vmware"},{"key":"windows_edition_id","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"ServerStandard"},{"key":"windows_installation_type","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Server"},{"key":"windows_product_name","calling_source":null,"source":null,"line":null,"char":null,"length":null,"value":"Windows Server 2012 R2 Standard"}] \ No newline at end of file diff --git a/spec/languageserver/fixtures/puppet_object_cache.json b/spec/languageserver/fixtures/puppet_object_cache.json new file mode 100644 index 00000000..c0c35ded --- /dev/null +++ b/spec/languageserver/fixtures/puppet_object_cache.json @@ -0,0 +1 @@ +{"functions":[{"key":"abs","calling_source":"/lib/puppet/functions/abs.rb","source":"/lib/puppet/functions/abs.rb","line":33,"char":34,"length":4,"doc":"Returns the absolute value of a Numeric value, for example -34.56 becomes\n34.56. Takes a single `Integer` or `Float` value as an argument.\n\n*Deprecated behavior*\n\nFor backwards compatibility reasons this function also works when given a\nnumber in `String` format such that it first attempts to covert it to either a `Float` or\nan `Integer` and then taking the absolute value of the result. Only strings representing\na number in decimal format is supported - an error is raised if\nvalue is not decimal (using base 10). Leading 0 chars in the string\nare ignored. A floating point value in string form can use some forms of\nscientific notation but not all.\n\nCallers should convert strings to `Numeric` before calling\nthis function to have full control over the conversion.\n\n```puppet\nabs(Numeric($str_val))\n```\n\nIt is worth noting that `Numeric` can convert to absolute value\ndirectly as in the following examples:\n\n```puppet\nNumeric($strval, true) # Converts to absolute Integer or Float\nInteger($strval, 10, true) # Converts to absolute Integer using base 10 (decimal)\nInteger($strval, 16, true) # Converts to absolute Integer using base 16 (hex)\nFloat($strval, true) # Converts to absolute Float\n```","function_version":4,"signatures":[{"key":"abs(Numeric $val)","doc":"","return_types":["Any"],"parameters":[{"name":"val","doc":"","types":["Numeric"],"signature_key_offset":12,"signature_key_length":4}]},{"key":"abs(String $val)","doc":"","return_types":["Any"],"parameters":[{"name":"val","doc":"","types":["String"],"signature_key_offset":11,"signature_key_length":4}]}]},{"key":"alert","calling_source":"/lib/puppet/functions/alert.rb","source":"/lib/puppet/functions/alert.rb","line":2,"char":34,"length":6,"doc":"Logs a message on the server at level `alert`.","function_version":4,"signatures":[{"key":"alert(Any *$values)","doc":"Logs a message on the server at level `alert`.","return_types":["Undef"],"parameters":[{"name":"*values","doc":"The values to log.","types":["Any"],"signature_key_offset":10,"signature_key_length":8}]}]},{"key":"all","calling_source":"/lib/puppet/functions/all.rb","source":"/lib/puppet/functions/all.rb","line":62,"char":34,"length":4,"doc":"Since 5.2.0\n\nRuns a [lambda](https://puppet.com/docs/puppet/latest/lang_lambdas.html)\nrepeatedly using each value in a data structure until the lambda returns a non \"truthy\" value which\nmakes the function return `false`, or if the end of the iteration is reached, `true` is returned.\n\nThis function takes two mandatory arguments, in this order:\n\n1. An array, hash, or other iterable object that the function will iterate over.\n2. A lambda, which the function calls for each element in the first argument. It can\nrequest one or two parameters.\n\n`$data.all |$parameter| { }`\n\nor\n\n`all($data) |$parameter| { }`\n\n```puppet\n# For the array $data, run a lambda that checks that all values are multiples of 10\n$data = [10, 20, 30]\nnotice $data.all |$item| { $item % 10 == 0 }\n```\n\nWould notice `true`.\n\nWhen the first argument is a `Hash`, Puppet passes each key and value pair to the lambda\nas an array in the form `[key, value]`.\n\n```puppet\n# For the hash $data, run a lambda using each item as a key-value array\n$data = { 'a_0'=> 10, 'b_1' => 20 }\nnotice $data.all |$item| { $item[1] % 10 == 0 }\n```\n\nWould notice `true` if all values in the hash are multiples of 10.\n\nWhen the lambda accepts two arguments, the first argument gets the index in an array\nor the key from a hash, and the second argument the value.\n\n\n```puppet\n# Check that all values are a multiple of 10 and keys start with 'abc'\n$data = {abc_123 => 10, abc_42 => 20, abc_blue => 30}\nnotice $data.all |$key, $value| { $value % 10 == 0 and $key =~ /^abc/ }\n```\n\nWould notice true.\n\nFor an general examples that demonstrates iteration, see the Puppet\n[iteration](https://puppet.com/docs/puppet/latest/lang_iteration.html)\ndocumentation.","function_version":4,"signatures":[{"key":"all(Hash[Any, Any] $hash, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":19,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"all(Hash[Any, Any] $hash, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":19,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"all(Iterable $enumerable, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":13,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"all(Iterable $enumerable, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":13,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":40,"signature_key_length":7}]}]},{"key":"annotate","calling_source":"/lib/puppet/functions/annotate.rb","source":"/lib/puppet/functions/annotate.rb","line":66,"char":34,"length":9,"doc":"Since 5.0.0\n\nHandles annotations on objects. The function can be used in four different ways.\n\nWith two arguments, an `Annotation` type and an object, the function returns the annotation\nfor the object of the given type, or `undef` if no such annotation exists.\n\n```puppet\n$annotation = Mod::NickNameAdapter.annotate(o)\n\n$annotation = annotate(Mod::NickNameAdapter.annotate, o)\n```\n\nWith three arguments, an `Annotation` type, an object, and a block, the function returns the\nannotation for the object of the given type, or annotates it with a new annotation initialized\nfrom the hash returned by the given block when no such annotation exists. The block will not\nbe called when an annotation of the given type is already present.\n\n```puppet\n$annotation = Mod::NickNameAdapter.annotate(o) || { { 'nick_name' => 'Buddy' } }\n\n$annotation = annotate(Mod::NickNameAdapter.annotate, o) || { { 'nick_name' => 'Buddy' } }\n```\n\nWith three arguments, an `Annotation` type, an object, and an `Hash`, the function will annotate\nthe given object with a new annotation of the given type that is initialized from the given hash.\nAn existing annotation of the given type is discarded.\n\n```puppet\n$annotation = Mod::NickNameAdapter.annotate(o, { 'nick_name' => 'Buddy' })\n\n$annotation = annotate(Mod::NickNameAdapter.annotate, o, { 'nick_name' => 'Buddy' })\n```\n\nWith three arguments, an `Annotation` type, an object, and an the string `clear`, the function will\nclear the annotation of the given type in the given object. The old annotation is returned if\nit existed.\n\n```puppet\n$annotation = Mod::NickNameAdapter.annotate(o, clear)\n\n$annotation = annotate(Mod::NickNameAdapter.annotate, o, clear)\n```\n\nWith three arguments, the type `Pcore`, an object, and a Hash of hashes keyed by `Annotation` types,\nthe function will annotate the given object with all types used as keys in the given hash. Each annotation\nis initialized with the nested hash for the respective type. The annotated object is returned.\n\n```puppet\n $person = Pcore.annotate(Mod::Person({'name' => 'William'}), {\n Mod::NickNameAdapter >= { 'nick_name' => 'Bill' },\n Mod::HobbiesAdapter => { 'hobbies' => ['Ham Radio', 'Philatelist'] }\n })\n```","function_version":4,"signatures":[{"key":"annotate(Type[Annotation] $type, Any $value, Optional[Callable[0, 0]] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"type","doc":"","types":["Type[Annotation]"],"signature_key_offset":26,"signature_key_length":5},{"name":"value","doc":"","types":["Any"],"signature_key_offset":37,"signature_key_length":6},{"name":"&block","doc":"","types":["Optional[Callable[0, 0]]"],"signature_key_offset":70,"signature_key_length":7}]},{"key":"annotate(Type[Annotation] $type, Any $value, Variant[Enum[clear],Hash[Pcore::MemberName,Any]] $annotation_hash)","doc":"","return_types":["Any"],"parameters":[{"name":"type","doc":"","types":["Type[Annotation]"],"signature_key_offset":26,"signature_key_length":5},{"name":"value","doc":"","types":["Any"],"signature_key_offset":37,"signature_key_length":6},{"name":"annotation_hash","doc":"","types":["Variant[Enum[clear],Hash[Pcore::MemberName,Any]]"],"signature_key_offset":94,"signature_key_length":16}]},{"key":"annotate(Type[Pcore] $type, Any $value, Hash[Type[Annotation], Hash[Pcore::MemberName,Any]] $annotations)","doc":"","return_types":["Any"],"parameters":[{"name":"type","doc":"","types":["Type[Pcore]"],"signature_key_offset":21,"signature_key_length":5},{"name":"value","doc":"","types":["Any"],"signature_key_offset":32,"signature_key_length":6},{"name":"annotations","doc":"","types":["Hash[Type[Annotation], Hash[Pcore::MemberName,Any]]"],"signature_key_offset":92,"signature_key_length":12}]}]},{"key":"any","calling_source":"/lib/puppet/functions/any.rb","source":"/lib/puppet/functions/any.rb","line":67,"char":34,"length":4,"doc":"Since 5.2.0\n\nRuns a [lambda](https://puppet.com/docs/puppet/latest/lang_lambdas.html)\nrepeatedly using each value in a data structure until the lambda returns a \"truthy\" value which\nmakes the function return `true`, or if the end of the iteration is reached, false is returned.\n\nThis function takes two mandatory arguments, in this order:\n\n1. An array, hash, or other iterable object that the function will iterate over.\n2. A lambda, which the function calls for each element in the first argument. It can\nrequest one or two parameters.\n\n`$data.any |$parameter| { }`\n\nor\n\n`any($data) |$parameter| { }`\n\n```puppet\n# For the array $data, run a lambda that checks if an unknown hash contains those keys\n$data = [\"routers\", \"servers\", \"workstations\"]\n$looked_up = lookup('somekey', Hash)\nnotice $data.any |$item| { $looked_up[$item] }\n```\n\nWould notice `true` if the looked up hash had a value that is neither `false` nor `undef` for at least\none of the keys. That is, it is equivalent to the expression\n`$looked_up[routers] || $looked_up[servers] || $looked_up[workstations]`.\n\nWhen the first argument is a `Hash`, Puppet passes each key and value pair to the lambda\nas an array in the form `[key, value]`.\n\n```puppet\n# For the hash $data, run a lambda using each item as a key-value array.\n$data = {\"rtr\" => \"Router\", \"svr\" => \"Server\", \"wks\" => \"Workstation\"}\n$looked_up = lookup('somekey', Hash)\nnotice $data.any |$item| { $looked_up[$item[0]] }\n```\n\nWould notice `true` if the looked up hash had a value for one of the wanted key that is\nneither `false` nor `undef`.\n\nWhen the lambda accepts two arguments, the first argument gets the index in an array\nor the key from a hash, and the second argument the value.\n\n\n```puppet\n# Check if there is an even numbered index that has a non String value\n$data = [key1, 1, 2, 2]\nnotice $data.any |$index, $value| { $index % 2 == 0 and $value !~ String }\n```\n\nWould notice true as the index `2` is even and not a `String`\n\nFor an general examples that demonstrates iteration, see the Puppet\n[iteration](https://puppet.com/docs/puppet/latest/lang_iteration.html)\ndocumentation.","function_version":4,"signatures":[{"key":"any(Hash[Any, Any] $hash, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":19,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"any(Hash[Any, Any] $hash, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":19,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"any(Iterable $enumerable, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":13,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":40,"signature_key_length":7}]},{"key":"any(Iterable $enumerable, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":13,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":40,"signature_key_length":7}]}]},{"key":"assert_type","calling_source":"/lib/puppet/functions/assert_type.rb","source":"/lib/puppet/functions/assert_type.rb","line":53,"char":34,"length":12,"doc":"Since 4.0.0\n\nReturns the given value if it is of the given\n[data type](https://puppet.com/docs/puppet/latest/lang_data.html), or\notherwise either raises an error or executes an optional two-parameter\n[lambda](https://puppet.com/docs/puppet/latest/lang_lambdas.html).\n\nThe function takes two mandatory arguments, in this order:\n\n1. The expected data type.\n2. A value to compare against the expected data type.\n\n```puppet\n$raw_username = 'Amy Berry'\n\n# Assert that $raw_username is a non-empty string and assign it to $valid_username.\n$valid_username = assert_type(String[1], $raw_username)\n\n# $valid_username contains \"Amy Berry\".\n# If $raw_username was an empty string or a different data type, the Puppet run would\n# fail with an \"Expected type does not match actual\" error.\n```\n\nYou can use an optional lambda to provide enhanced feedback. The lambda takes two\nmandatory parameters, in this order:\n\n1. The expected data type as described in the function's first argument.\n2. The actual data type of the value.\n\n```puppet\n$raw_username = 'Amy Berry'\n\n# Assert that $raw_username is a non-empty string and assign it to $valid_username.\n# If it isn't, output a warning describing the problem and use a default value.\n$valid_username = assert_type(String[1], $raw_username) |$expected, $actual| {\n warning( \"The username should be \\'${expected}\\', not \\'${actual}\\'. Using 'anonymous'.\" )\n 'anonymous'\n}\n\n# $valid_username contains \"Amy Berry\".\n# If $raw_username was an empty string, the Puppet run would set $valid_username to\n# \"anonymous\" and output a warning: \"The username should be 'String[1, default]', not\n# 'String[0, 0]'. Using 'anonymous'.\"\n```\n\nFor more information about data types, see the\n[documentation](https://puppet.com/docs/puppet/latest/lang_data.html).","function_version":4,"signatures":[{"key":"assert_type(Type $type, Any $value, Optional[Callable[Type, Type]] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"type","doc":"","types":["Type"],"signature_key_offset":17,"signature_key_length":5},{"name":"value","doc":"","types":["Any"],"signature_key_offset":28,"signature_key_length":6},{"name":"&block","doc":"","types":["Optional[Callable[Type, Type]]"],"signature_key_offset":67,"signature_key_length":7}]},{"key":"assert_type(String $type_string, Any $value, Optional[Callable[Type, Type]] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"type_string","doc":"","types":["String"],"signature_key_offset":19,"signature_key_length":12},{"name":"value","doc":"","types":["Any"],"signature_key_offset":37,"signature_key_length":6},{"name":"&block","doc":"","types":["Optional[Callable[Type, Type]]"],"signature_key_offset":76,"signature_key_length":7}]}]},{"key":"binary_file","calling_source":"/lib/puppet/functions/binary_file.rb","source":"/lib/puppet/functions/binary_file.rb","line":18,"char":34,"length":12,"doc":"Since 4.8.0\n\nLoads a binary file from a module or file system and returns its contents as a `Binary`.\nThe argument to this function should be a `/`\nreference, which will load `` from a module's `files`\ndirectory. (For example, the reference `mysql/mysqltuner.pl` will load the\nfile `/mysql/files/mysqltuner.pl`.)\n\nThis function also accepts an absolute file path that allows reading\nbinary file content from anywhere on disk.\n\nAn error is raised if the given file does not exists.\n\nTo search for the existence of files, use the `find_file()` function.\n\n- since 4.8.0","function_version":4,"signatures":[{"key":"binary_file(String $path)","doc":"Loads a binary file from a module or file system and returns its contents as a `Binary`.\nThe argument to this function should be a `/`\nreference, which will load `` from a module's `files`\ndirectory. (For example, the reference `mysql/mysqltuner.pl` will load the\nfile `/mysql/files/mysqltuner.pl`.)\n\nThis function also accepts an absolute file path that allows reading\nbinary file content from anywhere on disk.\n\nAn error is raised if the given file does not exists.\n\nTo search for the existence of files, use the `find_file()` function.\n\n- since 4.8.0","return_types":["Any"],"parameters":[{"name":"path","doc":"","types":["String"],"signature_key_offset":19,"signature_key_length":5}]}]},{"key":"break","calling_source":"/lib/puppet/functions/break.rb","source":"/lib/puppet/functions/break.rb","line":34,"char":34,"length":6,"doc":"Since 4.8.0\n\nBreaks an innermost iteration as if it encountered an end of input.\nThis function does not return to the caller.\n\nThe signal produced to stop the iteration bubbles up through\nthe call stack until either terminating the innermost iteration or\nraising an error if the end of the call stack is reached.\n\nThe break() function does not accept an argument.\n\n```puppet\n$data = [1,2,3]\nnotice $data.map |$x| { if $x == 3 { break() } $x*10 }\n```\n\nWould notice the value `[10, 20]`\n\n```puppet\nfunction break_if_even($x) {\n if $x % 2 == 0 { break() }\n}\n$data = [1,2,3]\nnotice $data.map |$x| { break_if_even($x); $x*10 }\n```\nWould notice the value `[10]`\n\n* Also see functions `next` and `return`","function_version":4,"signatures":[{"key":"break()","doc":"Breaks an innermost iteration as if it encountered an end of input.\nThis function does not return to the caller.\n\nThe signal produced to stop the iteration bubbles up through\nthe call stack until either terminating the innermost iteration or\nraising an error if the end of the call stack is reached.\n\nThe break() function does not accept an argument.\n\n```puppet\n$data = [1,2,3]\nnotice $data.map |$x| { if $x == 3 { break() } $x*10 }\n```\n\nWould notice the value `[10, 20]`\n\n```puppet\nfunction break_if_even($x) {\n if $x % 2 == 0 { break() }\n}\n$data = [1,2,3]\nnotice $data.map |$x| { break_if_even($x); $x*10 }\n```\nWould notice the value `[10]`\n\n* Also see functions `next` and `return`","return_types":["Any"],"parameters":[]}]},{"key":"call","calling_source":"/lib/puppet/functions/call.rb","source":"/lib/puppet/functions/call.rb","line":58,"char":34,"length":5,"doc":"Since 5.0.0\n\nCalls an arbitrary Puppet function by name.\n\nThis function takes one mandatory argument and one or more optional arguments:\n\n1. A string corresponding to a function name.\n2. Any number of arguments to be passed to the called function.\n3. An optional lambda, if the function being called supports it.\n\nThis function can also be used to resolve a `Deferred` given as\nthe only argument to the function (does not accept arguments nor\na block).\n\n```puppet\n$a = 'notice'\ncall($a, 'message')\n```\n\n```puppet\n$a = 'each'\n$b = [1,2,3]\ncall($a, $b) |$item| {\n notify { $item: }\n}\n```\n\nThe `call` function can be used to call either Ruby functions or Puppet language\nfunctions.\n\nWhen used with `Deferred` values, the deferred value can either describe\na function call, or a dig into a variable.\n\n```puppet\n$d = Deferred('join', [[1,2,3], ':']) # A future call to join that joins the arguments 1,2,3 with ':'\nnotice($d.call())\n```\n\nWould notice the string \"1:2:3\".\n\n```puppet\n$d = Deferred('$facts', ['processors', 'count'])\nnotice($d.call())\n```\n\nWould notice the value of `$facts['processors']['count']` at the time when the `call` is made.\n\n* Deferred values supported since Puppet 5.6.0","function_version":4,"signatures":[{"key":"call(String $function_name, Any *$arguments, Optional[Callable] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"function_name","doc":"","types":["String"],"signature_key_offset":12,"signature_key_length":14},{"name":"*arguments","doc":"","types":["Any"],"signature_key_offset":32,"signature_key_length":11},{"name":"&block","doc":"","types":["Optional[Callable]"],"signature_key_offset":64,"signature_key_length":7}]},{"key":"call(Deferred $deferred)","doc":"","return_types":["Any"],"parameters":[{"name":"deferred","doc":"","types":["Deferred"],"signature_key_offset":14,"signature_key_length":9}]}]},{"key":"camelcase","calling_source":"/lib/puppet/functions/camelcase.rb","source":"/lib/puppet/functions/camelcase.rb","line":31,"char":34,"length":10,"doc":"Creates a Camel Case version of a String\n\nThis function is compatible with the stdlib function with the same name.\n\nThe function does the following:\n* For a `String` the conversion replaces all combinations of *_* with an upcased version of the\n character following the _. This is done using Ruby system locale which handles some, but not all\n special international up-casing rules (for example German double-s ß is upcased to \"Ss\").\n* For an `Iterable[Variant[String, Numeric]]` (for example an `Array`) each value is capitalized and the conversion is not recursive.\n* If the value is `Numeric` it is simply returned (this is for backwards compatibility).\n* An error is raised for all other data types.\n* The result will not contain any underscore characters.\n\nPlease note: This function relies directly on Ruby's String implementation and as such may not be entirely UTF8 compatible.\nTo ensure best compatibility please use this function with Ruby 2.4.0 or greater - https://bugs.ruby-lang.org/issues/10085.\n\n```puppet\n'hello_friend'.camelcase()\ncamelcase('hello_friend')\n```\nWould both result in `\"HelloFriend\"`\n\n```puppet\n['abc_def', 'bcd_xyz'].camelcase()\ncamelcase(['abc_def', 'bcd_xyz'])\n```\nWould both result in `['AbcDef', 'BcdXyz']`","function_version":4,"signatures":[{"key":"camelcase(Numeric $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Numeric"],"signature_key_offset":18,"signature_key_length":4}]},{"key":"camelcase(String $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["String"],"signature_key_offset":17,"signature_key_length":4}]},{"key":"camelcase(Iterable[Variant[String, Numeric]] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Iterable[Variant[String, Numeric]]"],"signature_key_offset":45,"signature_key_length":4}]}]},{"key":"capitalize","calling_source":"/lib/puppet/functions/capitalize.rb","source":"/lib/puppet/functions/capitalize.rb","line":30,"char":34,"length":11,"doc":"Capitalizes the first character of a String, or the first character of every String in an Iterable value (such as an Array).\n\nThis function is compatible with the stdlib function with the same name.\n\nThe function does the following:\n* For a `String`, a string with its first character in upper case version is returned. \n This is done using Ruby system locale which handles some, but not all\n special international up-casing rules (for example German double-s ß is capitalized to \"Ss\").\n* For an `Iterable[Variant[String, Numeric]]` (for example an `Array`) each value is capitalized and the conversion is not recursive.\n* If the value is `Numeric` it is simply returned (this is for backwards compatibility).\n* An error is raised for all other data types.\n\nPlease note: This function relies directly on Ruby's String implementation and as such may not be entirely UTF8 compatible.\nTo ensure best compatibility please use this function with Ruby 2.4.0 or greater - https://bugs.ruby-lang.org/issues/10085.\n\n```puppet\n'hello'.capitalize()\nupcase('hello')\n```\nWould both result in \"Hello\"\n\n```puppet\n['abc', 'bcd'].capitalize()\ncapitalize(['abc', 'bcd'])\n```\nWould both result in ['Abc', 'Bcd']","function_version":4,"signatures":[{"key":"capitalize(Numeric $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Numeric"],"signature_key_offset":19,"signature_key_length":4}]},{"key":"capitalize(String $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["String"],"signature_key_offset":18,"signature_key_length":4}]},{"key":"capitalize(Iterable[Variant[String, Numeric]] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Iterable[Variant[String, Numeric]]"],"signature_key_offset":46,"signature_key_length":4}]}]},{"key":"ceiling","calling_source":"/lib/puppet/functions/ceiling.rb","source":"/lib/puppet/functions/ceiling.rb","line":12,"char":34,"length":8,"doc":"Returns the smallest `Integer` greater or equal to the argument.\nTakes a single numeric value as an argument.\n\nThis function is backwards compatible with the same function in stdlib\nand accepts a `Numeric` value. A `String` that can be converted\nto a floating point number can also be used in this version - but this\nis deprecated.\n\nIn general convert string input to `Numeric` before calling this function\nto have full control over how the conversion is done.","function_version":4,"signatures":[{"key":"ceiling(Numeric $val)","doc":"","return_types":["Any"],"parameters":[{"name":"val","doc":"","types":["Numeric"],"signature_key_offset":16,"signature_key_length":4}]},{"key":"ceiling(String $val)","doc":"","return_types":["Any"],"parameters":[{"name":"val","doc":"","types":["String"],"signature_key_offset":15,"signature_key_length":4}]}]},{"key":"chomp","calling_source":"/lib/puppet/functions/chomp.rb","source":"/lib/puppet/functions/chomp.rb","line":26,"char":34,"length":6,"doc":"Returns a new string with the record separator character(s) removed.\nThe record separator is the line ending characters `\\r` and `\\n`.\n\nThis function is compatible with the stdlib function with the same name.\n\nThe function does the following:\n* For a `String` the conversion removes `\\r\\n`, `\\n` or `\\r` from the end of a string.\n* For an `Iterable[Variant[String, Numeric]]` (for example an `Array`) each value is processed and the conversion is not recursive.\n* If the value is `Numeric` it is simply returned (this is for backwards compatibility).\n* An error is raised for all other data types.\n\n```puppet\n\"hello\\r\\n\".chomp()\nchomp(\"hello\\r\\n\")\n```\nWould both result in `\"hello\"`\n\n```puppet\n[\"hello\\r\\n\", \"hi\\r\\n\"].chomp()\nchomp([\"hello\\r\\n\", \"hi\\r\\n\"])\n```\nWould both result in `['hello', 'hi']`","function_version":4,"signatures":[{"key":"chomp(Numeric $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Numeric"],"signature_key_offset":14,"signature_key_length":4}]},{"key":"chomp(String $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["String"],"signature_key_offset":13,"signature_key_length":4}]},{"key":"chomp(Iterable[Variant[String, Numeric]] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Iterable[Variant[String, Numeric]]"],"signature_key_offset":41,"signature_key_length":4}]}]},{"key":"chop","calling_source":"/lib/puppet/functions/chop.rb","source":"/lib/puppet/functions/chop.rb","line":36,"char":34,"length":5,"doc":"Returns a new string with the last character removed.\nIf the string ends with `\\r\\n`, both characters are removed. Applying chop to an empty\nstring returns an empty string. If you wish to merely remove record\nseparators then you should use the `chomp` function.\n\nThis function is compatible with the stdlib function with the same name.\n\nThe function does the following:\n* For a `String` the conversion removes the last character, or if it ends with \\r\\n` it removes both. If String is empty\n an empty string is returned.\n* For an `Iterable[Variant[String, Numeric]]` (for example an `Array`) each value is processed and the conversion is not recursive.\n* If the value is `Numeric` it is simply returned (this is for backwards compatibility).\n* An error is raised for all other data types.\n\n```puppet\n\"hello\\r\\n\".chop()\nchop(\"hello\\r\\n\")\n```\nWould both result in `\"hello\"`\n\n```puppet\n\"hello\".chop()\nchop(\"hello\")\n```\nWould both result in `\"hell\"`\n\n```puppet\n[\"hello\\r\\n\", \"hi\\r\\n\"].chop()\nchop([\"hello\\r\\n\", \"hi\\r\\n\"])\n```\nWould both result in `['hello', 'hi']`","function_version":4,"signatures":[{"key":"chop(Numeric $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Numeric"],"signature_key_offset":13,"signature_key_length":4}]},{"key":"chop(String $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["String"],"signature_key_offset":12,"signature_key_length":4}]},{"key":"chop(Iterable[Variant[String, Numeric]] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Iterable[Variant[String, Numeric]]"],"signature_key_offset":40,"signature_key_length":4}]}]},{"key":"compare","calling_source":"/lib/puppet/functions/compare.rb","source":"/lib/puppet/functions/compare.rb","line":12,"char":34,"length":8,"doc":"Compares two values and returns -1, 0 or 1 if first value is smaller, equal or larger than the second value.\nThe compare function accepts arguments of the data types `String`, `Numeric`, `Timespan`, `Timestamp`, and `Semver`,\nsuch that:\n\n* two of the same data type can be compared\n* `Timespan` and `Timestamp` can be compared with each other and with `Numeric`\n\nWhen comparing two `String` values the comparison can be made to consider case by passing a third (optional)\nboolean `false` value - the default is `true` which ignores case as the comparison operators\nin the Puppet Language.","function_version":4,"signatures":[{"key":"compare(Numeric $a, Numeric $b)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["Numeric"],"signature_key_offset":16,"signature_key_length":2},{"name":"b","doc":"","types":["Numeric"],"signature_key_offset":28,"signature_key_length":2}]},{"key":"compare(String $a, String $b, Optional[Boolean] $ignore_case)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["String"],"signature_key_offset":15,"signature_key_length":2},{"name":"b","doc":"","types":["String"],"signature_key_offset":26,"signature_key_length":2},{"name":"ignore_case","doc":"","types":["Optional[Boolean]"],"signature_key_offset":48,"signature_key_length":12}]},{"key":"compare(Semver $a, Semver $b)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["Semver"],"signature_key_offset":15,"signature_key_length":2},{"name":"b","doc":"","types":["Semver"],"signature_key_offset":26,"signature_key_length":2}]},{"key":"compare(Numeric $a, Variant[Timespan, Timestamp] $b)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["Numeric"],"signature_key_offset":16,"signature_key_length":2},{"name":"b","doc":"","types":["Variant[Timespan, Timestamp]"],"signature_key_offset":49,"signature_key_length":2}]},{"key":"compare(Timestamp $a, Variant[Timestamp, Numeric] $b)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["Timestamp"],"signature_key_offset":18,"signature_key_length":2},{"name":"b","doc":"","types":["Variant[Timestamp, Numeric]"],"signature_key_offset":50,"signature_key_length":2}]},{"key":"compare(Timespan $a, Variant[Timespan, Numeric] $b)","doc":"","return_types":["Any"],"parameters":[{"name":"a","doc":"","types":["Timespan"],"signature_key_offset":17,"signature_key_length":2},{"name":"b","doc":"","types":["Variant[Timespan, Numeric]"],"signature_key_offset":48,"signature_key_length":2}]}]},{"key":"contain","calling_source":"/lib/puppet/functions/contain.rb","source":"/lib/puppet/functions/contain.rb","line":21,"char":34,"length":8,"doc":"Makes one or more classes be contained inside the current class.\nIf any of these classes are undeclared, they will be declared as if\nthere were declared with the `include` function.\nAccepts a class name, an array of class names, or a comma-separated\nlist of class names.\n\nA contained class will not be applied before the containing class is\nbegun, and will be finished before the containing class is finished.\n\nYou must use the class's full name;\nrelative names are not allowed. In addition to names in string form,\nyou may also directly use `Class` and `Resource` `Type`-values that are produced by\nevaluating resource and relationship expressions.\n\nThe function returns an array of references to the classes that were contained thus\nallowing the function call to `contain` to directly continue.\n\n- Since 4.0.0 support for `Class` and `Resource` `Type`-values, absolute names\n- Since 4.7.0 a value of type `Array[Type[Class[n]]]` is returned with all the contained classes","function_version":4,"signatures":[{"key":"contain(Any *$names)","doc":"Makes one or more classes be contained inside the current class.\nIf any of these classes are undeclared, they will be declared as if\nthere were declared with the `include` function.\nAccepts a class name, an array of class names, or a comma-separated\nlist of class names.\n\nA contained class will not be applied before the containing class is\nbegun, and will be finished before the containing class is finished.\n\nYou must use the class's full name;\nrelative names are not allowed. In addition to names in string form,\nyou may also directly use `Class` and `Resource` `Type`-values that are produced by\nevaluating resource and relationship expressions.\n\nThe function returns an array of references to the classes that were contained thus\nallowing the function call to `contain` to directly continue.\n\n- Since 4.0.0 support for `Class` and `Resource` `Type`-values, absolute names\n- Since 4.7.0 a value of type `Array[Type[Class[n]]]` is returned with all the contained classes","return_types":["Any"],"parameters":[{"name":"*names","doc":"","types":["Any"],"signature_key_offset":12,"signature_key_length":7}]}]},{"key":"convert_to","calling_source":"/lib/puppet/functions/convert_to.rb","source":"/lib/puppet/functions/convert_to.rb","line":22,"char":34,"length":11,"doc":"Since 5.4.0\n\nThe `convert_to(value, type)` is a convenience function that does the same as `new(type, value)`.\nThe difference in the argument ordering allows it to be used in chained style for\nimproved readability \"left to right\".\n\nWhen the function is given a lambda, it is called with the converted value, and the function\nreturns what the lambda returns, otherwise the converted value.\n\n```puppet\n # The harder to read variant:\n # Using new operator - that is \"calling the type\" with operator ()\n Hash(Array(\"abc\").map |$i,$v| { [$i, $v] })\n\n # The easier to read variant:\n # using 'convert_to'\n \"abc\".convert_to(Array).map |$i,$v| { [$i, $v] }.convert_to(Hash)\n```","function_version":4,"signatures":[{"key":"convert_to(Any $value, Type $type, Optional[Any] *$args, Optional[Callable[1,1]] &$block)","doc":"The `convert_to(value, type)` is a convenience function that does the same as `new(type, value)`.\nThe difference in the argument ordering allows it to be used in chained style for\nimproved readability \"left to right\".\n\nWhen the function is given a lambda, it is called with the converted value, and the function\nreturns what the lambda returns, otherwise the converted value.\n\n```puppet\n # The harder to read variant:\n # Using new operator - that is \"calling the type\" with operator ()\n Hash(Array(\"abc\").map |$i,$v| { [$i, $v] })\n\n # The easier to read variant:\n # using 'convert_to'\n \"abc\".convert_to(Array).map |$i,$v| { [$i, $v] }.convert_to(Hash)\n```","return_types":["Any"],"parameters":[{"name":"value","doc":"","types":["Any"],"signature_key_offset":15,"signature_key_length":6},{"name":"type","doc":"","types":["Type"],"signature_key_offset":28,"signature_key_length":5},{"name":"*args","doc":"","types":["Optional[Any]"],"signature_key_offset":49,"signature_key_length":6},{"name":"&block","doc":"","types":["Optional[Callable[1,1]]"],"signature_key_offset":81,"signature_key_length":7}]}]},{"key":"crit","calling_source":"/lib/puppet/functions/crit.rb","source":"/lib/puppet/functions/crit.rb","line":2,"char":34,"length":5,"doc":"Logs a message on the server at level `crit`.","function_version":4,"signatures":[{"key":"crit(Any *$values)","doc":"Logs a message on the server at level `crit`.","return_types":["Undef"],"parameters":[{"name":"*values","doc":"The values to log.","types":["Any"],"signature_key_offset":9,"signature_key_length":8}]}]},{"key":"debug","calling_source":"/lib/puppet/functions/debug.rb","source":"/lib/puppet/functions/debug.rb","line":2,"char":34,"length":6,"doc":"Logs a message on the server at level `debug`.","function_version":4,"signatures":[{"key":"debug(Any *$values)","doc":"Logs a message on the server at level `debug`.","return_types":["Undef"],"parameters":[{"name":"*values","doc":"The values to log.","types":["Any"],"signature_key_offset":10,"signature_key_length":8}]}]},{"key":"defined","calling_source":"/lib/puppet/functions/defined.rb","source":"/lib/puppet/functions/defined.rb","line":102,"char":null,"length":null,"doc":"Since 2.7.0\n\nDetermines whether a given class or resource type is defined and returns a Boolean\nvalue. You can also use `defined` to determine whether a specific resource is defined,\nor whether a variable has a value (including `undef`, as opposed to the variable never\nbeing declared or assigned).\n\nThis function takes at least one string argument, which can be a class name, type name,\nresource reference, or variable reference of the form `'$name'`. (Note that the `$` sign\nis included in the string which must be in single quotes to prevent the `$` character\nto be interpreted as interpolation.\n\nThe `defined` function checks both native and defined types, including types\nprovided by modules. Types and classes are matched by their names. The function matches\nresource declarations by using resource references.\n\n```puppet\n# Matching resource types\ndefined(\"file\")\ndefined(\"customtype\")\n\n# Matching defines and classes\ndefined(\"foo\")\ndefined(\"foo::bar\")\n\n# Matching variables (note the single quotes)\ndefined('$name')\n\n# Matching declared resources\ndefined(File['/tmp/file'])\n```\n\nPuppet depends on the configuration's evaluation order when checking whether a resource\nis declared.\n\n```puppet\n# Assign values to $is_defined_before and $is_defined_after using identical `defined`\n# functions.\n\n$is_defined_before = defined(File['/tmp/file'])\n\nfile { \"/tmp/file\":\n ensure => present,\n}\n\n$is_defined_after = defined(File['/tmp/file'])\n\n# $is_defined_before returns false, but $is_defined_after returns true.\n```\n\nThis order requirement only refers to evaluation order. The order of resources in the\nconfiguration graph (e.g. with `before` or `require`) does not affect the `defined`\nfunction's behavior.\n\n> **Warning:** Avoid relying on the result of the `defined` function in modules, as you\n> might not be able to guarantee the evaluation order well enough to produce consistent\n> results. This can cause other code that relies on the function's result to behave\n> inconsistently or fail.\n\nIf you pass more than one argument to `defined`, the function returns `true` if _any_\nof the arguments are defined. You can also match resources by type, allowing you to\nmatch conditions of different levels of specificity, such as whether a specific resource\nis of a specific data type.\n\n```puppet\nfile { \"/tmp/file1\":\n ensure => file,\n}\n\n$tmp_file = file { \"/tmp/file2\":\n ensure => file,\n}\n\n# Each of these statements return `true` ...\ndefined(File['/tmp/file1'])\ndefined(File['/tmp/file1'],File['/tmp/file2'])\ndefined(File['/tmp/file1'],File['/tmp/file2'],File['/tmp/file3'])\n# ... but this returns `false`.\ndefined(File['/tmp/file3'])\n\n# Each of these statements returns `true` ...\ndefined(Type[Resource['file','/tmp/file2']])\ndefined(Resource['file','/tmp/file2'])\ndefined(File['/tmp/file2'])\ndefined('$tmp_file')\n# ... but each of these returns `false`.\ndefined(Type[Resource['exec','/tmp/file2']])\ndefined(Resource['exec','/tmp/file2'])\ndefined(File['/tmp/file3'])\ndefined('$tmp_file2')\n```","function_version":4,"signatures":[{"key":"defined(Variant[String, Type[CatalogEntry], Type[Type[CatalogEntry]]] *$vals)","doc":"Determines whether a given class or resource type is defined and returns a Boolean\nvalue. You can also use `defined` to determine whether a specific resource is defined,\nor whether a variable has a value (including `undef`, as opposed to the variable never\nbeing declared or assigned).\n\nThis function takes at least one string argument, which can be a class name, type name,\nresource reference, or variable reference of the form `'$name'`. (Note that the `$` sign\nis included in the string which must be in single quotes to prevent the `$` character\nto be interpreted as interpolation.\n\nThe `defined` function checks both native and defined types, including types\nprovided by modules. Types and classes are matched by their names. The function matches\nresource declarations by using resource references.\n\n```puppet\n# Matching resource types\ndefined(\"file\")\ndefined(\"customtype\")\n\n# Matching defines and classes\ndefined(\"foo\")\ndefined(\"foo::bar\")\n\n# Matching variables (note the single quotes)\ndefined('$name')\n\n# Matching declared resources\ndefined(File['/tmp/file'])\n```\n\nPuppet depends on the configuration's evaluation order when checking whether a resource\nis declared.\n\n```puppet\n# Assign values to $is_defined_before and $is_defined_after using identical `defined`\n# functions.\n\n$is_defined_before = defined(File['/tmp/file'])\n\nfile { \"/tmp/file\":\n ensure => present,\n}\n\n$is_defined_after = defined(File['/tmp/file'])\n\n# $is_defined_before returns false, but $is_defined_after returns true.\n```\n\nThis order requirement only refers to evaluation order. The order of resources in the\nconfiguration graph (e.g. with `before` or `require`) does not affect the `defined`\nfunction's behavior.\n\n> **Warning:** Avoid relying on the result of the `defined` function in modules, as you\n> might not be able to guarantee the evaluation order well enough to produce consistent\n> results. This can cause other code that relies on the function's result to behave\n> inconsistently or fail.\n\nIf you pass more than one argument to `defined`, the function returns `true` if _any_\nof the arguments are defined. You can also match resources by type, allowing you to\nmatch conditions of different levels of specificity, such as whether a specific resource\nis of a specific data type.\n\n```puppet\nfile { \"/tmp/file1\":\n ensure => file,\n}\n\n$tmp_file = file { \"/tmp/file2\":\n ensure => file,\n}\n\n# Each of these statements return `true` ...\ndefined(File['/tmp/file1'])\ndefined(File['/tmp/file1'],File['/tmp/file2'])\ndefined(File['/tmp/file1'],File['/tmp/file2'],File['/tmp/file3'])\n# ... but this returns `false`.\ndefined(File['/tmp/file3'])\n\n# Each of these statements returns `true` ...\ndefined(Type[Resource['file','/tmp/file2']])\ndefined(Resource['file','/tmp/file2'])\ndefined(File['/tmp/file2'])\ndefined('$tmp_file')\n# ... but each of these returns `false`.\ndefined(Type[Resource['exec','/tmp/file2']])\ndefined(Resource['exec','/tmp/file2'])\ndefined(File['/tmp/file3'])\ndefined('$tmp_file2')\n```","return_types":["Any"],"parameters":[{"name":"*vals","doc":"","types":["Variant[String, Type[CatalogEntry], Type[Type[CatalogEntry]]]"],"signature_key_offset":70,"signature_key_length":6}]}]},{"key":"dig","calling_source":"/lib/puppet/functions/dig.rb","source":"/lib/puppet/functions/dig.rb","line":30,"char":34,"length":4,"doc":"Since 4.5.0\n\nReturns a value for a sequence of given keys/indexes into a structure, such as\nan array or hash.\n\nThis function is used to \"dig into\" a complex data structure by\nusing a sequence of keys / indexes to access a value from which\nthe next key/index is accessed recursively.\n\nThe first encountered `undef` value or key stops the \"dig\" and `undef` is returned.\n\nAn error is raised if an attempt is made to \"dig\" into\nsomething other than an `undef` (which immediately returns `undef`), an `Array` or a `Hash`.\n\n```puppet\n$data = {a => { b => [{x => 10, y => 20}, {x => 100, y => 200}]}}\nnotice $data.dig('a', 'b', 1, 'x')\n```\n\nWould notice the value 100.\n\nThis is roughly equivalent to `$data['a']['b'][1]['x']`. However, a standard\nindex will return an error and cause catalog compilation failure if any parent\nof the final key (`'x'`) is `undef`. The `dig` function will return `undef`,\nrather than failing catalog compilation. This allows you to check if data\nexists in a structure without mandating that it always exists.","function_version":4,"signatures":[{"key":"dig(Optional[Collection] $data, Any *$arg)","doc":"Returns a value for a sequence of given keys/indexes into a structure, such as\nan array or hash.\n\nThis function is used to \"dig into\" a complex data structure by\nusing a sequence of keys / indexes to access a value from which\nthe next key/index is accessed recursively.\n\nThe first encountered `undef` value or key stops the \"dig\" and `undef` is returned.\n\nAn error is raised if an attempt is made to \"dig\" into\nsomething other than an `undef` (which immediately returns `undef`), an `Array` or a `Hash`.\n\n```puppet\n$data = {a => { b => [{x => 10, y => 20}, {x => 100, y => 200}]}}\nnotice $data.dig('a', 'b', 1, 'x')\n```\n\nWould notice the value 100.\n\nThis is roughly equivalent to `$data['a']['b'][1]['x']`. However, a standard\nindex will return an error and cause catalog compilation failure if any parent\nof the final key (`'x'`) is `undef`. The `dig` function will return `undef`,\nrather than failing catalog compilation. This allows you to check if data\nexists in a structure without mandating that it always exists.","return_types":["Any"],"parameters":[{"name":"data","doc":"","types":["Optional[Collection]"],"signature_key_offset":25,"signature_key_length":5},{"name":"*arg","doc":"","types":["Any"],"signature_key_offset":36,"signature_key_length":5}]}]},{"key":"downcase","calling_source":"/lib/puppet/functions/downcase.rb","source":"/lib/puppet/functions/downcase.rb","line":46,"char":34,"length":9,"doc":"Converts a String, Array or Hash (recursively) into lower case.\n\nThis function is compatible with the stdlib function with the same name.\n\nThe function does the following:\n* For a `String`, its lower case version is returned. This is done using Ruby system locale which handles some, but not all\n special international up-casing rules (for example German double-s ß is upcased to \"SS\", whereas upper case double-s\n is downcased to ß).\n* For `Array` and `Hash` the conversion to lower case is recursive and each key and value must be convertible by\n this function.\n* When a `Hash` is converted, some keys could result in the same key - in those cases, the\n latest key-value wins. For example if keys \"aBC\", and \"abC\" where both present, after downcase there would only be one\n key \"abc\".\n* If the value is `Numeric` it is simply returned (this is for backwards compatibility).\n* An error is raised for all other data types.\n\nPlease note: This function relies directly on Ruby's String implementation and as such may not be entirely UTF8 compatible.\nTo ensure best compatibility please use this function with Ruby 2.4.0 or greater - https://bugs.ruby-lang.org/issues/10085.\n\n```puppet\n'HELLO'.downcase()\ndowncase('HEllO')\n```\nWould both result in \"hello\"\n\n```puppet\n['A', 'B'].downcase()\ndowncase(['A', 'B'])\n```\nWould both result in ['a', 'b']\n\n```puppet\n{'A' => 'HEllO', 'B' => 'GOODBYE'}.downcase()\n```\nWould result in `{'a' => 'hello', 'b' => 'goodbye'}`\n\n```puppet\n['A', 'B', ['C', ['D']], {'X' => 'Y'}].downcase\n```\nWould result in `['a', 'b', ['c', ['d']], {'x' => 'y'}]`","function_version":4,"signatures":[{"key":"downcase(Numeric $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Numeric"],"signature_key_offset":17,"signature_key_length":4}]},{"key":"downcase(String $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["String"],"signature_key_offset":16,"signature_key_length":4}]},{"key":"downcase(Array[StringData] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Array[StringData]"],"signature_key_offset":27,"signature_key_length":4}]},{"key":"downcase(Hash[StringData, StringData] $arg)","doc":"","return_types":["Any"],"parameters":[{"name":"arg","doc":"","types":["Hash[StringData, StringData]"],"signature_key_offset":38,"signature_key_length":4}]}]},{"key":"each","calling_source":"/lib/puppet/functions/each.rb","source":"/lib/puppet/functions/each.rb","line":97,"char":34,"length":5,"doc":"Since 4.0.0\n\nRuns a [lambda](https://puppet.com/docs/puppet/latest/lang_lambdas.html)\nrepeatedly using each value in a data structure, then returns the values unchanged.\n\nThis function takes two mandatory arguments, in this order:\n\n1. An array, hash, or other iterable object that the function will iterate over.\n2. A lambda, which the function calls for each element in the first argument. It can\nrequest one or two parameters.\n\n`$data.each |$parameter| { }`\n\nor\n\n`each($data) |$parameter| { }`\n\nWhen the first argument (`$data` in the above example) is an array, Puppet passes each\nvalue in turn to the lambda, then returns the original values.\n\n```puppet\n# For the array $data, run a lambda that creates a resource for each item.\n$data = [\"routers\", \"servers\", \"workstations\"]\n$data.each |$item| {\n notify { $item:\n message => $item\n }\n}\n# Puppet creates one resource for each of the three items in $data. Each resource is\n# named after the item's value and uses the item's value in a parameter.\n```\n\nWhen the first argument is a hash, Puppet passes each key and value pair to the lambda\nas an array in the form `[key, value]` and returns the original hash.\n\n```puppet\n# For the hash $data, run a lambda using each item as a key-value array that creates a\n# resource for each item.\n$data = {\"rtr\" => \"Router\", \"svr\" => \"Server\", \"wks\" => \"Workstation\"}\n$data.each |$items| {\n notify { $items[0]:\n message => $items[1]\n }\n}\n# Puppet creates one resource for each of the three items in $data, each named after the\n# item's key and containing a parameter using the item's value.\n```\n\nWhen the first argument is an array and the lambda has two parameters, Puppet passes the\narray's indexes (enumerated from 0) in the first parameter and its values in the second\nparameter.\n\n```puppet\n# For the array $data, run a lambda using each item's index and value that creates a\n# resource for each item.\n$data = [\"routers\", \"servers\", \"workstations\"]\n$data.each |$index, $value| {\n notify { $value:\n message => $index\n }\n}\n# Puppet creates one resource for each of the three items in $data, each named after the\n# item's value and containing a parameter using the item's index.\n```\n\nWhen the first argument is a hash, Puppet passes its keys to the first parameter and its\nvalues to the second parameter.\n\n```puppet\n# For the hash $data, run a lambda using each item's key and value to create a resource\n# for each item.\n$data = {\"rtr\" => \"Router\", \"svr\" => \"Server\", \"wks\" => \"Workstation\"}\n$data.each |$key, $value| {\n notify { $key:\n message => $value\n }\n}\n# Puppet creates one resource for each of the three items in $data, each named after the\n# item's key and containing a parameter using the item's value.\n```\n\nFor an example that demonstrates how to create multiple `file` resources using `each`,\nsee the Puppet\n[iteration](https://puppet.com/docs/puppet/latest/lang_iteration.html)\ndocumentation.","function_version":4,"signatures":[{"key":"each(Hash[Any, Any] $hash, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":20,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":41,"signature_key_length":7}]},{"key":"each(Hash[Any, Any] $hash, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"hash","doc":"","types":["Hash[Any, Any]"],"signature_key_offset":20,"signature_key_length":5},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":41,"signature_key_length":7}]},{"key":"each(Iterable $enumerable, Callable[2,2] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":14,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[2,2]"],"signature_key_offset":41,"signature_key_length":7}]},{"key":"each(Iterable $enumerable, Callable[1,1] &$block)","doc":"","return_types":["Any"],"parameters":[{"name":"enumerable","doc":"","types":["Iterable"],"signature_key_offset":14,"signature_key_length":11},{"name":"&block","doc":"","types":["Callable[1,1]"],"signature_key_offset":41,"signature_key_length":7}]}]},{"key":"emerg","calling_source":"/lib/puppet/functions/emerg.rb","source":"/lib/puppet/functions/emerg.rb","line":2,"char":34,"length":6,"doc":"Logs a message on the server at level `emerg`.","function_version":4,"signatures":[{"key":"emerg(Any *$values)","doc":"Logs a message on the server at level `emerg`.","return_types":["Undef"],"parameters":[{"name":"*values","doc":"The values to log.","types":["Any"],"signature_key_offset":10,"signature_key_length":8}]}]},{"key":"empty","calling_source":"/lib/puppet/functions/empty.rb","source":"/lib/puppet/functions/empty.rb","line":24,"char":34,"length":6,"doc":"Since Puppet 5.5.0 - support for Binary\n\nReturns `true` if the given argument is an empty collection of values.\n\nThis function can answer if one of the following is empty:\n* `Array`, `Hash` - having zero entries\n* `String`, `Binary` - having zero length\n\nFor backwards compatibility with the stdlib function with the same name the\nfollowing data types are also accepted by the function instead of raising an error.\nUsing these is deprecated and will raise a warning:\n\n* `Numeric` - `false` is returned for all `Numeric` values.\n* `Undef` - `true` is returned for all `Undef` values.\n\n```puppet\nnotice([].empty)\nnotice(empty([]))\n# would both notice 'true'\n```","function_version":4,"signatures":[{"key":"empty(Collection $coll)","doc":"","return_types":["Any"],"parameters":[{"name":"coll","doc":"","types":["Collection"],"signature_key_offset":17,"signature_key_length":5}]},{"key":"empty(String $str)","doc":"","return_types":["Any"],"parameters":[{"name":"str","doc":"","types":["String"],"signature_key_offset":13,"signature_key_length":4}]},{"key":"empty(Numeric $num)","doc":"","return_types":["Any"],"parameters":[{"name":"num","doc":"","types":["Numeric"],"signature_key_offset":14,"signature_key_length":4}]},{"key":"empty(Binary $bin)","doc":"","return_types":["Any"],"parameters":[{"name":"bin","doc":"","types":["Binary"],"signature_key_offset":13,"signature_key_length":4}]},{"key":"empty(Undef $x)","doc":"","return_types":["Any"],"parameters":[{"name":"x","doc":"","types":["Undef"],"signature_key_offset":12,"signature_key_length":2}]}]},{"key":"epp","calling_source":"/lib/puppet/functions/epp.rb","source":"/lib/puppet/functions/epp.rb","line":37,"char":34,"length":4,"doc":"Since 4.0.0\n\nEvaluates an Embedded Puppet (EPP) template file and returns the rendered text\nresult as a String.\n\n`epp('/