puppet-editor-services 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +510 -0
  3. data/CODEOWNERS +2 -0
  4. data/CODE_OF_CONDUCT.md +46 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/Gemfile +53 -0
  7. data/LICENSE +201 -0
  8. data/README.md +308 -0
  9. data/Rakefile +185 -0
  10. data/bin/puppet-debugserver +8 -0
  11. data/bin/puppet-languageserver +7 -0
  12. data/bin/puppet-languageserver-sidecar +7 -0
  13. data/lib/dsp/dsp.rb +7 -0
  14. data/lib/dsp/dsp_base.rb +62 -0
  15. data/lib/dsp/dsp_protocol.rb +4619 -0
  16. data/lib/lsp/lsp.rb +10 -0
  17. data/lib/lsp/lsp_base.rb +63 -0
  18. data/lib/lsp/lsp_custom.rb +170 -0
  19. data/lib/lsp/lsp_enums.rb +143 -0
  20. data/lib/lsp/lsp_protocol.rb +2785 -0
  21. data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
  22. data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
  23. data/lib/lsp/lsp_protocol_configuration.rb +82 -0
  24. data/lib/lsp/lsp_protocol_declaration.rb +73 -0
  25. data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
  26. data/lib/lsp/lsp_protocol_implementation.rb +75 -0
  27. data/lib/lsp/lsp_protocol_progress.rb +200 -0
  28. data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
  29. data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
  30. data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
  31. data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
  32. data/lib/lsp/lsp_types.rb +1534 -0
  33. data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
  34. data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
  35. data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
  36. data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
  37. data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
  38. data/lib/puppet-debugserver/hooks.rb +132 -0
  39. data/lib/puppet-debugserver/message_handler.rb +277 -0
  40. data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
  41. data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
  42. data/lib/puppet-languageserver/client_session_state.rb +119 -0
  43. data/lib/puppet-languageserver/crash_dump.rb +50 -0
  44. data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
  45. data/lib/puppet-languageserver/facter_helper.rb +25 -0
  46. data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
  47. data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
  48. data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
  49. data/lib/puppet-languageserver/global_queues.rb +16 -0
  50. data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
  51. data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
  52. data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
  53. data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
  54. data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
  55. data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
  56. data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
  57. data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
  58. data/lib/puppet-languageserver/message_handler.rb +462 -0
  59. data/lib/puppet-languageserver/providers.rb +18 -0
  60. data/lib/puppet-languageserver/puppet_helper.rb +108 -0
  61. data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
  62. data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
  63. data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
  64. data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
  65. data/lib/puppet-languageserver/server_capabilities.rb +48 -0
  66. data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
  67. data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
  68. data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
  69. data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
  70. data/lib/puppet-languageserver/uri_helper.rb +46 -0
  71. data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
  72. data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
  73. data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
  74. data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
  75. data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
  76. data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
  77. data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
  78. data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
  79. data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
  80. data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
  81. data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
  82. data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
  83. data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
  84. data/lib/puppet_debugserver.rb +164 -0
  85. data/lib/puppet_editor_services/connection/base.rb +62 -0
  86. data/lib/puppet_editor_services/connection/stdio.rb +25 -0
  87. data/lib/puppet_editor_services/connection/tcp.rb +34 -0
  88. data/lib/puppet_editor_services/handler/base.rb +16 -0
  89. data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
  90. data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
  91. data/lib/puppet_editor_services/logging.rb +45 -0
  92. data/lib/puppet_editor_services/protocol/base.rb +27 -0
  93. data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
  94. data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
  95. data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
  96. data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
  97. data/lib/puppet_editor_services/server/base.rb +42 -0
  98. data/lib/puppet_editor_services/server/stdio.rb +85 -0
  99. data/lib/puppet_editor_services/server/tcp.rb +349 -0
  100. data/lib/puppet_editor_services/server.rb +15 -0
  101. data/lib/puppet_editor_services/version.rb +36 -0
  102. data/lib/puppet_editor_services.rb +8 -0
  103. data/lib/puppet_languageserver.rb +263 -0
  104. data/lib/puppet_languageserver_sidecar.rb +361 -0
  105. data/puppet-debugserver +11 -0
  106. data/puppet-editor-services.gemspec +29 -0
  107. data/puppet-languageserver +15 -0
  108. data/puppet-languageserver-sidecar +14 -0
  109. metadata +240 -0
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet-languageserver/session_state/document_store'
4
+ require 'puppet-languageserver/session_state/language_client'
5
+ require 'puppet-languageserver/session_state/object_cache'
6
+
7
+ module PuppetLanguageServer
8
+ class ClientSessionState
9
+ attr_reader :documents, :language_client, :object_cache, :connection_id
10
+
11
+ def initialize(message_handler, options = {})
12
+ @documents = options[:documents].nil? ? PuppetLanguageServer::SessionState::DocumentStore.new : options[:documents]
13
+ @language_client = options[:language_client].nil? ? PuppetLanguageServer::SessionState::LanguageClient.new(message_handler) : options[:language_client]
14
+ @object_cache = options[:object_cache].nil? ? PuppetLanguageServer::SessionState::ObjectCache.new : options[:object_cache]
15
+ @connection_id = options[:connection_id].nil? ? message_handler.protocol.connection.id : options[:connection_id]
16
+ end
17
+
18
+ # Helper methods to know the state of the object cache
19
+ def default_classes_loaded?
20
+ object_cache.section_in_origin_exist?(:class, :default)
21
+ end
22
+
23
+ def default_datatypes_loaded?
24
+ object_cache.section_in_origin_exist?(:datatype, :default)
25
+ end
26
+
27
+ def default_functions_loaded?
28
+ object_cache.section_in_origin_exist?(:function, :default)
29
+ end
30
+
31
+ def default_types_loaded?
32
+ object_cache.section_in_origin_exist?(:type, :default)
33
+ end
34
+
35
+ def facts_loaded?
36
+ object_cache.section_in_origin_exist?(:fact, :default)
37
+ end
38
+
39
+ def static_data_loaded?
40
+ object_cache.origin_exist?(:bolt)
41
+ end
42
+
43
+ # Loaders for object cache information
44
+ def load_default_data!(async = true)
45
+ PuppetLanguageServer.log_message(:info, "Loading Default Data via aggregate #{'(Async)' if async}...")
46
+ if async
47
+ sidecar_queue.enqueue('default_aggregate', [], false, connection_id)
48
+ sidecar_queue.enqueue('facts', [], false, connection_id)
49
+ else
50
+ sidecar_queue.execute('default_aggregate', [], false, connection_id)
51
+ sidecar_queue.execute('facts', [], false, connection_id)
52
+ end
53
+
54
+ true
55
+ end
56
+
57
+ def load_static_data!(async = true)
58
+ if async
59
+ Thread.new do
60
+ PuppetLanguageServer.log_message(:info, 'Loading static data (Async)...')
61
+ load_static_data_impl
62
+ end
63
+ else
64
+ PuppetLanguageServer.log_message(:info, 'Loading static data...')
65
+ load_static_data_impl
66
+ end
67
+
68
+ true
69
+ end
70
+
71
+ def load_workspace_data!(async = true)
72
+ return true if documents.store_root_path.nil?
73
+
74
+ action_args = ['--local-workspace', documents.store_root_path]
75
+ PuppetLanguageServer.log_message(:info, "Loading Workspace Data via aggregate #{'(Async)' if async}...")
76
+ if async
77
+ sidecar_queue.enqueue('workspace_aggregate', action_args, false, connection_id)
78
+ else
79
+ sidecar_queue.execute('workspace_aggregate', action_args, false, connection_id)
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ def purge_workspace_data!
86
+ object_cache.remove_origin!(:workspace)
87
+ end
88
+
89
+ private
90
+
91
+ def sidecar_queue
92
+ PuppetLanguageServer::GlobalQueues.sidecar_queue
93
+ end
94
+
95
+ def load_static_data_impl
96
+ bolt_static_data = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new
97
+ Dir.glob(File.join(PuppetLanguageServer.static_data_dir, 'bolt-*.json')) do |path|
98
+ PuppetLanguageServer.log_message(:debug, "Importing static data file #{path}...")
99
+ # No need to catch errors here. As this is static data and is tested in rspec
100
+ # Sure, we could have corrupt/missing files on disk, but then we have bigger issues
101
+ data = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(File.open(path, 'rb:UTF-8') { |f| f.read })
102
+ data.each_list { |_, list| bolt_static_data.concat!(list) }
103
+ end
104
+
105
+ object_cache.import_sidecar_list!(bolt_static_data.classes, :class, :bolt)
106
+ object_cache.import_sidecar_list!(bolt_static_data.datatypes, :datatype, :bolt)
107
+ object_cache.import_sidecar_list!(bolt_static_data.functions, :function, :bolt)
108
+ object_cache.import_sidecar_list!(bolt_static_data.types, :type, :bolt)
109
+
110
+ bolt_static_data.each_list do |k, v|
111
+ if v.nil?
112
+ PuppetLanguageServer.log_message(:debug, "Static bolt data returned no #{k}")
113
+ else
114
+ PuppetLanguageServer.log_message(:debug, "Static bolt data returned #{v.count} #{k}")
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServer
4
+ module CrashDump
5
+ def self.default_crash_file
6
+ File.join(Dir.tmpdir, 'puppet_language_server_crash.txt')
7
+ end
8
+
9
+ def self.write_crash_file(err, session_state, filename = nil, additional = {})
10
+ # Create the crash text
11
+
12
+ puppet_version = Puppet.version rescue 'Unknown' # rubocop:disable Style/RescueModifier
13
+ facter_version = Facter.version rescue 'Unknown' # rubocop:disable Style/RescueModifier
14
+ languageserver_version = PuppetLanguageServer.version rescue 'Unknown' # rubocop:disable Style/RescueModifier
15
+
16
+ crashtext = <<~TEXT
17
+ Puppet Language Server Crash File
18
+ -=--=--=--=--=--=--=--=--=--=--=-
19
+ #{DateTime.now.strftime('%a %b %e %Y %H:%M:%S %Z')}
20
+ Puppet Version #{puppet_version}
21
+ Facter Version #{facter_version}
22
+ Ruby Version #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}
23
+ Language Server Version #{languageserver_version}
24
+
25
+ Error: #{err}
26
+
27
+ Backtrace
28
+ ---------
29
+ #{err.backtrace.join("\n")}
30
+
31
+ TEXT
32
+ # Append the documents in the cache
33
+ session_state.documents.document_uris.each do |uri|
34
+ crashtext += "Document - #{uri}\n---\n#{session_state.documents.document_content(uri)}\n\n"
35
+ end
36
+ # Append additional objects from the crash
37
+ additional.each do |k, v|
38
+ crashtext += "#{k}\n---\n#{v}\n\n"
39
+ end
40
+
41
+ crash_file = filename.nil? ? default_crash_file : filename
42
+ File.binwrite(crash_file, crashtext)
43
+ rescue # rubocop:disable Style/RescueStandardError
44
+ # Swallow all errors. Errors in the error handler should not
45
+ # terminate the application
46
+ end
47
+
48
+ nil
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServer
4
+ module Epp
5
+ module ValidationProvider
6
+ def self.validate(content, _max_problems = 100)
7
+ result = []
8
+ # TODO: Need to implement max_problems
9
+ _problems = 0
10
+
11
+ begin
12
+ parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new
13
+ parser.parse_string(content, nil)
14
+ rescue StandardError => e
15
+ # Sometimes the error is in the cause not the root object itself
16
+ e = e.cause if !e.respond_to?(:line) && e.respond_to?(:cause)
17
+ ex_line = e.respond_to?(:line) && !e.line.nil? ? e.line - 1 : nil # Line numbers from puppet exceptions are base 1
18
+ ex_pos = e.respond_to?(:pos) && !e.pos.nil? ? e.pos : nil # Pos numbers from puppet are base 1
19
+
20
+ message = e.respond_to?(:message) ? e.message : nil
21
+ message = e.basic_message if message.nil? && e.respond_to?(:basic_message)
22
+
23
+ unless ex_line.nil? || ex_pos.nil? || message.nil?
24
+ result << LSP::Diagnostic.new('severity' => LSP::DiagnosticSeverity::ERROR,
25
+ 'range' => LSP.create_range(ex_line, ex_pos, ex_line, ex_pos + 1),
26
+ 'source' => 'Puppet',
27
+ 'message' => message)
28
+ end
29
+ end
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServer
4
+ module FacterHelper
5
+ # Facts
6
+ def self.fact(session_state, name)
7
+ session_state.object_cache.object_by_name(:fact, name)
8
+ end
9
+
10
+ def self.fact_value(session_state, name)
11
+ object = session_state.object_cache.object_by_name(:fact, name)
12
+ object.nil? ? nil : object.value
13
+ end
14
+
15
+ def self.fact_names(session_state)
16
+ session_state.object_cache.object_names_by_section(:fact).map(&:to_s)
17
+ end
18
+
19
+ def self.facts_to_hash(session_state)
20
+ fact_hash = {}
21
+ session_state.object_cache.objects_by_section(:fact) { |factname, fact| fact_hash[factname.to_s] = fact.value }
22
+ fact_hash
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet-languageserver/global_queues/single_instance_queue'
4
+ require 'puppet_editor_services/server'
5
+ require 'open3'
6
+
7
+ module PuppetLanguageServer
8
+ module GlobalQueues
9
+ class SidecarQueueJob < SingleInstanceQueueJob
10
+ attr_accessor :action, :additional_args, :handle_errors, :connection_id
11
+
12
+ def initialize(action, additional_args, handle_errors, connection_id)
13
+ super("#{action}-#{connection_id}")
14
+ @action = action
15
+ @additional_args = additional_args
16
+ @handle_errors = handle_errors
17
+ @connection_id = connection_id
18
+ end
19
+ end
20
+
21
+ # Module for enqueing and running sidecar jobs asynchronously
22
+ # When adding a job, it will remove any other for the same
23
+ # job in the queue, so that only the latest job needs to be processed.
24
+ class SidecarQueue < SingleInstanceQueue
25
+ def max_queue_threads
26
+ 2
27
+ end
28
+
29
+ def job_class
30
+ SidecarQueueJob
31
+ end
32
+
33
+ def execute_job(job_object)
34
+ super
35
+ connection = connection_from_connection_id(job_object.connection_id)
36
+ raise "Connection is not available for connection id #{job_object.connection_id}" if connection.nil?
37
+
38
+ sidecar_path = File.expand_path(File.join(__dir__, '..', '..', '..', 'puppet-languageserver-sidecar'))
39
+ args = ['--action', job_object.action].concat(job_object.additional_args).concat(sidecar_args_from_connection(connection))
40
+ cmd = ['ruby', sidecar_path].concat(args)
41
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: Running sidecar #{cmd}")
42
+ stdout, stderr, status = run_sidecar(cmd)
43
+ PuppetLanguageServer.log_message(:warning, "SidecarQueue Thread: Calling sidecar with #{args.join(' ')} returned exitcode #{status.exitstatus}, #{stderr}")
44
+ return nil unless status.exitstatus.zero?
45
+
46
+ # It's possible server has closed the connection while the sidecar is running.
47
+ # So raise if the connection is no longer available
48
+ raise "Connection is no longer available for connection id #{job_object.connection_id}" if connection_from_connection_id(job_object.connection_id).nil?
49
+
50
+ session_state = session_state_from_connection(connection)
51
+ raise "Session state is not available for connection id #{job_object.connection_id}" if session_state.nil?
52
+
53
+ cache = session_state.object_cache
54
+
55
+ # Correctly encode the result as UTF8
56
+ result = stdout.bytes.pack('U*')
57
+
58
+ case job_object.action.downcase
59
+ when 'default_aggregate'
60
+ lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result)
61
+ cache.import_sidecar_list!(lists.classes, :class, :default)
62
+ cache.import_sidecar_list!(lists.datatypes, :datatype, :default)
63
+ cache.import_sidecar_list!(lists.functions, :function, :default)
64
+ cache.import_sidecar_list!(lists.types, :type, :default)
65
+
66
+ lists.each_list do |k, v|
67
+ if v.nil?
68
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned no #{k}")
69
+ else
70
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_aggregate returned #{v.count} #{k}")
71
+ end
72
+ end
73
+
74
+ when 'default_classes'
75
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result)
76
+ cache.import_sidecar_list!(list, :class, :default)
77
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_classes returned #{list.count} items")
78
+
79
+ when 'default_datatypes'
80
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result)
81
+ cache.import_sidecar_list!(list, :datatype, :default)
82
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_datatypes returned #{list.count} items")
83
+
84
+ when 'default_functions'
85
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result)
86
+ cache.import_sidecar_list!(list, :function, :default)
87
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_functions returned #{list.count} items")
88
+
89
+ when 'default_types'
90
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result)
91
+ cache.import_sidecar_list!(list, :type, :default)
92
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: default_types returned #{list.count} items")
93
+
94
+ when 'facts'
95
+ list = PuppetLanguageServer::Sidecar::Protocol::FactList.new.from_json!(result)
96
+ cache.import_sidecar_list!(list, :fact, :default)
97
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: facts returned #{list.count} items")
98
+
99
+ when 'node_graph'
100
+ return PuppetLanguageServer::Sidecar::Protocol::PuppetNodeGraph.new.from_json!(result)
101
+
102
+ when 'resource_list'
103
+ return PuppetLanguageServer::Sidecar::Protocol::ResourceList.new.from_json!(result)
104
+
105
+ when 'workspace_aggregate'
106
+ lists = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new.from_json!(result)
107
+ cache.import_sidecar_list!(lists.classes, :class, :workspace)
108
+ cache.import_sidecar_list!(lists.datatypes, :datatype, :workspace)
109
+ cache.import_sidecar_list!(lists.functions, :function, :workspace)
110
+ cache.import_sidecar_list!(lists.types, :type, :workspace)
111
+
112
+ lists.each_list do |k, v|
113
+ if v.nil?
114
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned no #{k}")
115
+ else
116
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_aggregate returned #{v.count} #{k}")
117
+ end
118
+ end
119
+
120
+ when 'workspace_classes'
121
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new.from_json!(result)
122
+ cache.import_sidecar_list!(list, :class, :workspace)
123
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_classes returned #{list.count} items")
124
+
125
+ when 'workspace_datatypes'
126
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new.from_json!(result)
127
+ cache.import_sidecar_list!(list, :datatype, :workspace)
128
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_datatypes returned #{list.count} items")
129
+
130
+ when 'workspace_functions'
131
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new.from_json!(result)
132
+ cache.import_sidecar_list!(list, :function, :workspace)
133
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_functions returned #{list.count} items")
134
+
135
+ when 'workspace_types'
136
+ list = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new.from_json!(result)
137
+ cache.import_sidecar_list!(list, :type, :workspace)
138
+ PuppetLanguageServer.log_message(:debug, "SidecarQueue Thread: workspace_types returned #{list.count} items")
139
+
140
+ else
141
+ PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Unknown action #{job_object.action}")
142
+ end
143
+
144
+ true
145
+ rescue StandardError => e
146
+ raise unless job_object.handle_errors
147
+
148
+ PuppetLanguageServer.log_message(:error, "SidecarQueue Thread: Error running action #{job_object.action}. #{e}\n#{e.backtrace}")
149
+ nil
150
+ end
151
+
152
+ private
153
+
154
+ def connection_from_connection_id(connection_id)
155
+ PuppetEditorServices::Server.current_server.connection(connection_id)
156
+ end
157
+
158
+ def session_state_from_connection(connection)
159
+ return if connection.nil?
160
+
161
+ handler = connection.protocol.handler
162
+ handler.respond_to?(:session_state) ? handler.session_state : nil
163
+ end
164
+
165
+ def run_sidecar(cmd)
166
+ Open3.capture3(*cmd)
167
+
168
+ # The following code should ONLY be uncommented when debugging acceptance tests failures.
169
+ # It caches the sidecar responses so tests sidecar calls will be (VERY) quick, which speeds
170
+ # up feedback loops. It has a rubocop failure on purpose so that it fails automated CI checks
171
+ # and should never be merged in a Pull Request
172
+ #
173
+ # output_dir = File.expand_path(File.join(__dir__, '..', '..', '..', 'output'))
174
+ # FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
175
+ # require 'digest'
176
+ # cache_file = File.join(output_dir, Digest::MD5.hexdigest(cmd.join(' ')) + '.json')
177
+ # if File.exist?(cache_file)
178
+ # return [
179
+ # File.read(cache_file, mode: 'rb', encoding: 'utf-8'),
180
+ # '',
181
+ # Struct.new(:exitstatus).new(0)
182
+ # ]
183
+ # end
184
+ # # Rubocop failure is here on purpose
185
+ # stdout,stderr,status = Open3.capture3(*cmd)
186
+ # File.write(cache_file, stdout, mode: 'wb', encoding: 'utf-8')
187
+ # [stdout, stderr, status]
188
+ end
189
+
190
+ def sidecar_args_from_connection(connection)
191
+ return nil if connection.nil?
192
+
193
+ options = connection.server.handler_options
194
+ return [] if options.nil?
195
+
196
+ result = []
197
+ result << '--no-cache' if options[:disable_sidecar_cache]
198
+ result << "--puppet-version=#{Puppet.version}"
199
+ result << "--feature-flags=#{options[:flags].join(',')}" if options[:flags] && !options[:flags].empty?
200
+ result << "--puppet-settings=#{options[:puppet_settings].join(',')}" if options[:puppet_settings] && !options[:puppet_settings].empty?
201
+ result
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServer
4
+ module GlobalQueues
5
+ class SingleInstanceQueueJob
6
+ # Unique key for the job. The SingleInstanceQueue uses the key
7
+ # to ensure that only a single instance is in the queue
8
+ attr_reader :key
9
+
10
+ def initialize(key)
11
+ @key = key
12
+ end
13
+ end
14
+
15
+ # Base class for enqueing and running queued jobs asynchronously
16
+ # When adding a job, it will remove any other for the same
17
+ # key in the queue, so that only the latest job needs to be processed.
18
+ class SingleInstanceQueue
19
+ def initialize
20
+ @queue = []
21
+ @queue_mutex = Mutex.new
22
+ @queue_threads_mutex = Mutex.new
23
+ @queue_threads = []
24
+ end
25
+
26
+ # Default is one thread to process the queue
27
+ def max_queue_threads
28
+ 1
29
+ end
30
+
31
+ # The ruby Job class that this queue operates on
32
+ # Should be inherited from SingleInstanceQueueJob
33
+ # @api asbtract
34
+ def job_class
35
+ SingleInstanceQueueJob
36
+ end
37
+
38
+ def new_job(*args)
39
+ job_class.new(*args)
40
+ end
41
+
42
+ # Helpful method to create, then enqueue a job
43
+ def enqueue(*args)
44
+ enqueue_job(new_job(*args))
45
+ end
46
+
47
+ # Enqueue a job
48
+ def enqueue_job(job_object)
49
+ raise "Invalid job object for #{self.class}. Got #{job_object.class} but expected #{job_class}" unless job_object.is_a?(job_class)
50
+
51
+ @queue_mutex.synchronize do
52
+ @queue.reject! { |queue_item| queue_item.key == job_object.key }
53
+ @queue << job_object
54
+ end
55
+
56
+ @queue_threads_mutex.synchronize do
57
+ # Clear up any done threads
58
+ @queue_threads.reject! { |thr| thr.nil? || !thr.alive? }
59
+ # Append a new thread if we have space
60
+ if @queue_threads.count < max_queue_threads
61
+ @queue_threads << Thread.new do
62
+ thread_worker
63
+ rescue => e # rubocop:disable Style/RescueStandardError
64
+ PuppetLanguageServer.log_message(:error, "Error in #{self.class} Thread: #{e}")
65
+ raise
66
+ end
67
+ end
68
+ end
69
+ nil
70
+ end
71
+
72
+ # Helpful method to create, then enqueue a job
73
+ def execute(*args)
74
+ execute_job(new_job(*args))
75
+ end
76
+
77
+ # Synchronously executes the same work as an enqueued item. Does not consume a queue thread
78
+ # The thread worker calls this method when processing enqueued items
79
+ # @abstract
80
+ def execute_job(job_object)
81
+ raise "Invalid job object for #{self.class}. Got #{job_object.class} but expected #{job_class}" unless job_object.is_a?(job_class)
82
+ end
83
+
84
+ # Wait for the queue to become empty
85
+ def drain_queue
86
+ @queue_threads.each do |item|
87
+ item.join unless item.nil? || !item.alive?
88
+ end
89
+ nil
90
+ end
91
+
92
+ # Testing helper resets the queue and prepopulates it with
93
+ # a known arbitrary configuration.
94
+ # ONLY USE THIS FOR TESTING!
95
+ def reset_queue(initial_state = [])
96
+ @queue_mutex.synchronize do
97
+ @queue = initial_state
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # Thread worker which processes all jobs in the queue and calls the sidecar for each action
104
+ def thread_worker
105
+ work_item = nil
106
+ loop do
107
+ @queue_mutex.synchronize do
108
+ return if @queue.empty?
109
+
110
+ work_item = @queue.shift
111
+ end
112
+ return if work_item.nil?
113
+
114
+ # Perform action
115
+ begin
116
+ # When running async (i.e. from a thread swallow any output)
117
+ _result = execute_job(work_item)
118
+ rescue StandardError => e
119
+ PuppetLanguageServer.log_message(:error, "#{self.class} Thread: Error running job #{work_item.key}. #{e}\n#{e.backtrace}")
120
+ nil
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet-languageserver/global_queues/single_instance_queue'
4
+ require 'puppet_editor_services/server'
5
+
6
+ module PuppetLanguageServer
7
+ module GlobalQueues
8
+ class ValidationQueueJob < SingleInstanceQueueJob
9
+ attr_accessor :file_uri, :doc_version, :connection_id, :options
10
+
11
+ def initialize(file_uri, doc_version, connection_id, options = {})
12
+ super(file_uri)
13
+ @file_uri = file_uri
14
+ @doc_version = doc_version
15
+ @connection_id = connection_id
16
+ @options = options
17
+ end
18
+ end
19
+
20
+ # Class for enqueing and running document level validation asynchronously
21
+ #
22
+ # Uses a single instance queue so only the latest document needs to be processed.
23
+ # It will also ignore sending back validation results to the client if the document is updated during the validation process
24
+ class ValidationQueue < SingleInstanceQueue
25
+ def max_queue_threads
26
+ 1
27
+ end
28
+
29
+ def job_class
30
+ ValidationQueueJob
31
+ end
32
+
33
+ def execute_job(job_object)
34
+ super
35
+ session_state = session_state_from_connection_id(job_object.connection_id)
36
+ document_store = session_state.nil? ? nil : session_state.documents
37
+ raise "Document store is not available for connection id #{job_object.connection_id}" unless document_store
38
+
39
+ # Check if the document still exists
40
+ doc = document_store.get_document(job_object.file_uri)
41
+ unless doc
42
+ # Send an empty diagnostics message to clear the diagnostics
43
+ send_diagnostics(job_object.connection_id, job_object.file_uri, [])
44
+ PuppetLanguageServer.log_message(:debug, "#{self.class.name}: Ignoring #{job_object.file_uri} as it is has been removed.")
45
+ return
46
+ end
47
+
48
+ # Check if the document is the latest version
49
+ content = document_store.document_content(job_object.file_uri, job_object.doc_version)
50
+ unless content
51
+ PuppetLanguageServer.log_message(:debug, "#{self.class.name}: Ignoring #{job_object.file_uri} as it is not the latest version.")
52
+ return
53
+ end
54
+
55
+ # Perform validation
56
+ options = job_object.options.dup
57
+ results = case document_store.document_type(job_object.file_uri)
58
+ when :manifest
59
+ options[:tasks_mode] = document_store.plan_file?(job_object.file_uri)
60
+ PuppetLanguageServer::Manifest::ValidationProvider.validate(session_state, content, options)
61
+ when :epp
62
+ PuppetLanguageServer::Epp::ValidationProvider.validate(content)
63
+ when :puppetfile
64
+ options[:document_uri] = job_object.file_uri
65
+ PuppetLanguageServer::Puppetfile::ValidationProvider.validate(content, options)
66
+ else
67
+ []
68
+ end
69
+
70
+ # Because this may be asynchronous it's possible the user has edited the document while we're performing validation.
71
+ # Check if the document is still latest version and ignore the results if it's no longer the latest
72
+ current_version = document_store.document_version(job_object.file_uri)
73
+ if current_version != job_object.doc_version
74
+ PuppetLanguageServer.log_message(:debug, "ValidationQueue Thread: Ignoring #{job_object.file_uri} as has changed version from #{job_object.doc_version} to #{current_version}")
75
+ return
76
+ end
77
+
78
+ # Send the response
79
+ send_diagnostics(job_object.connection_id, job_object.file_uri, results)
80
+ end
81
+
82
+ private
83
+
84
+ def session_state_from_connection_id(connection_id)
85
+ connection = PuppetEditorServices::Server.current_server.connection(connection_id)
86
+ return if connection.nil?
87
+
88
+ handler = connection.protocol.handler
89
+ handler.respond_to?(:session_state) ? handler.session_state : nil
90
+ end
91
+
92
+ def send_diagnostics(connection_id, file_uri, diagnostics)
93
+ connection = PuppetEditorServices::Server.current_server.connection(connection_id)
94
+ return if connection.nil?
95
+
96
+ connection.protocol.encode_and_send(
97
+ ::PuppetEditorServices::Protocol::JsonRPCMessages.new_notification('textDocument/publishDiagnostics', 'uri' => file_uri, 'diagnostics' => diagnostics)
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet-languageserver/global_queues/validation_queue'
4
+ require 'puppet-languageserver/global_queues/sidecar_queue'
5
+
6
+ module PuppetLanguageServer
7
+ module GlobalQueues
8
+ def self.validate_queue
9
+ @validate_queue ||= ValidationQueue.new
10
+ end
11
+
12
+ def self.sidecar_queue
13
+ @sidecar_queue ||= SidecarQueue.new
14
+ end
15
+ end
16
+ end