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,541 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetDebugServer
4
+ # Manages a Puppet Debug session including features such as; breakpoints, flow control, hooks into puppet.
5
+ class PuppetDebugSession
6
+ # The hook manager class. This is responsible for adding and calling hooks.
7
+ # @see PuppetDebugServer::Hooks
8
+ # @return [PuppetDebugServer::Hooks]
9
+ attr_reader :hook_manager
10
+
11
+ # The hook handler class. This is responsible for responding to invoked hooks
12
+ # @see PuppetDebugServer::DebugSession::HookHandlers
13
+ # @return [PuppetDebugServer::DebugSession::HookHandlers]
14
+ attr_reader :hook_handlers
15
+
16
+ # The flow control class. This is responsible for controlling how the puppet agent execution flows. Including
17
+ # cross thread flags, determining if a session is paused or terminating.
18
+ # @see PuppetDebugServer::DebugSession::FlowControl
19
+ # @return [PuppetDebugServer::DebugSession::FlowControl]
20
+ attr_reader :flow_control
21
+
22
+ # The Ruby ID (not Operating System Thread ID) of the thread running Puppet (as opposed to RPC Server or debug session)
23
+ # @return [Integer]
24
+ attr_reader :puppet_thread_id
25
+
26
+ # The breakpoints class. This is responsible for storing and validation the active breakpoints during a debug session
27
+ # @see PuppetDebugServer::DebugSession::BreakPoints
28
+ # @return [PuppetDebugServer::DebugSession::BreakPoints]
29
+ attr_reader :breakpoints
30
+
31
+ # The session state class. This is responsible for determining the current and saved state of Puppet throughout the debug session
32
+ # @see PuppetDebugServer::DebugSession::PuppetSessionState
33
+ # @return [PuppetDebugServer::DebugSession::PuppetSessionState]
34
+ attr_reader :puppet_session_state
35
+
36
+ # Use to track the default instance of the debug session
37
+ @@session_instance = nil # rubocop:disable Style/ClassVars This class method (not instance) should be inherited
38
+
39
+ VARIABLES_REFERENCE_TOP_SCOPE = 1
40
+ ERROR_LOG_LEVELS = %i[warning err alert emerg crit].freeze
41
+
42
+ # Creates a debug session
43
+ def self.instance
44
+ # This can be called from any thread
45
+ return @@session_instance unless @@session_instance.nil? # This class method (not instance) should be inherited
46
+
47
+ @@session_instance = PuppetDebugSession.new # rubocop:disable Style/ClassVars This class method (not instance) should be inherited
48
+ end
49
+
50
+ def initialize
51
+ @message_handler = nil
52
+ @flow_control = PuppetDebugServer::DebugSession::FlowControl.new(self)
53
+ @hook_manager = PuppetDebugServer::Hooks.new
54
+ @hook_handlers = PuppetDebugServer::DebugSession::HookHandlers.new(self)
55
+ @breakpoints = PuppetDebugServer::DebugSession::BreakPoints.new(self)
56
+ @puppet_session_state = PuppetDebugServer::DebugSession::PuppetSessionState.new
57
+ @evaluate_string_mutex = Mutex.new
58
+ end
59
+
60
+ # Executes a hook synchronously
61
+ # @see PuppetDebugServer::DebugSession::HookHandlers
62
+ # @param event_name [Symbol] The name of the hook to execute.
63
+ # @param args [Array<Object>] The arguments of the hook
64
+ def execute_hook(event_name, args)
65
+ @hook_manager.exec_hook(event_name, args)
66
+ end
67
+
68
+ # Configures the debug session in it's initial state. Typically called as soon as the debug session is created
69
+ def initialize_session
70
+ # Save the thread incase we need to forcibly kill it
71
+ @puppet_thread = Thread.current
72
+ @puppet_thread_id = @puppet_thread.object_id.to_i
73
+ end
74
+
75
+ # Sends an OutputEvent to the Debug Client
76
+ # @see DSP::OutputEvent
77
+ # @param options [Hash] Options for the output
78
+ def send_output_event(options)
79
+ @message_handler.send_output_event(options) unless @message_handler.nil?
80
+ end
81
+
82
+ # Sends a StoppedEvent to the Debug Client
83
+ # @see DSP::StoppedEvent
84
+ # @param reason [String] Why the session has stopped
85
+ # @param options [Hash] Options for the output
86
+ def send_stopped_event(reason, options = {})
87
+ @message_handler.send_stopped_event(reason, options) unless @message_handler.nil?
88
+ end
89
+
90
+ # Sends a ThreadEvent to the Debug Client
91
+ # @see DSP::ThreadEvent
92
+ # @param reason [String] Why the the thread status has changed
93
+ # @param thread_id [Integer] The ID of the thread
94
+ def send_thread_event(reason, thread_id)
95
+ @message_handler.send_thread_event(reason, thread_id) unless @message_handler.nil?
96
+ end
97
+
98
+ # Sends an TerminatedEvent to the Debug Client to indicated the Debug Server is terminating
99
+ # @see DSP::TerminatedEvent
100
+ def send_termination_event
101
+ @message_handler.send_termination_event unless @message_handler.nil?
102
+ end
103
+
104
+ # Sends an ExitedEvent to the Debug Client
105
+ # @see DSP::ExitedEvent
106
+ # @param exitcode [Integer] The exit code from the process. This is the puppet detailed exit code
107
+ def send_exited_event(exitcode)
108
+ @message_handler.send_exited_event(exitcode) unless @message_handler.nil?
109
+ end
110
+
111
+ # Sets up the debug session ready for actual use. This is different from initialize_session in that
112
+ # it requires a running RPC server
113
+ # @param message_handler [PuppetDebugServer::MessageRouter] The message router used to communicate with the Debug Client.
114
+ # @param options [Hash<String, String>] Hash of launch arguments from the DSP launch request
115
+ def setup(message_handler, options = {})
116
+ @message_handler = message_handler
117
+ @session_options = options
118
+ flow_control.assert_flag(:session_setup)
119
+ end
120
+
121
+ # Synchronously runs Puppet in the debug session, assuming it has been configured correctly.
122
+ # Requires the session_setup and client_completed_configuration flags to be set prior.
123
+ def run_puppet
124
+ # Perform pre-run checks...
125
+ return if flow_control.terminate?
126
+ raise 'Missing session setup' unless flow_control.flag?(:session_setup)
127
+ raise 'Missing client configuration' unless flow_control.flag?(:client_completed_configuration)
128
+
129
+ # Run puppet
130
+ puppet_session_state.actual.reset!
131
+ flow_control.assert_flag(:puppet_started)
132
+ cmd_args = ['apply', @session_options['manifest'], '--detailed-exitcodes', '--logdest', 'debugserver']
133
+ cmd_args << '--noop' if @session_options['noop'] == true
134
+ cmd_args.push(*@session_options['args']) unless @session_options['args'].nil?
135
+
136
+ send_output_event(
137
+ 'category' => 'console',
138
+ 'output' => "puppet #{cmd_args.join(' ')}\n"
139
+ )
140
+ send_thread_event('started', @puppet_thread_id)
141
+
142
+ Puppet::Util::CommandLine.new('puppet.rb', cmd_args).execute
143
+ end
144
+
145
+ # Creates the list of stack frames from the saved puppet session state
146
+ # @see DSP::StackFrame
147
+ # @return [Array<DSP::StackFrame>]
148
+ def generate_stackframe_list
149
+ stack_frames = []
150
+ state = puppet_session_state.saved
151
+
152
+ # Generate StackFrame for a Pops::Evaluator object with location information
153
+ unless state.pops_target.nil?
154
+ target = state.pops_target
155
+
156
+ frame = DSP::StackFrame.new.from_h!(
157
+ 'id' => stack_frames.count,
158
+ 'name' => get_puppet_class_name(target),
159
+ 'line' => 0,
160
+ 'column' => 0
161
+ )
162
+
163
+ # TODO: Need to check on the client capabilities of zero or one based indexes
164
+ if target.is_a?(Puppet::Pops::Model::Positioned)
165
+ target_loc = get_location_from_pops_object(target)
166
+ frame.name = target_loc.file
167
+ frame.line = target_loc.line
168
+ frame.column = pos_on_line(target, target_loc.offset)
169
+ frame.source = DSP::Source.new.from_h!('path' => target_loc.file)
170
+
171
+ if target_loc.length > 0 # rubocop:disable Style/ZeroLengthPredicate
172
+ end_offset = target_loc.offset + target_loc.length
173
+ frame.endLine = line_for_offset(target, end_offset)
174
+ frame.endColumn = pos_on_line(target, end_offset)
175
+ end
176
+ end
177
+
178
+ stack_frames << frame
179
+ end
180
+
181
+ # Generate StackFrame for an error
182
+ unless state.exception.nil?
183
+ err = state.exception
184
+ frame = DSP::StackFrame.new.from_h!(
185
+ 'id' => stack_frames.count,
186
+ 'name' => err.class.to_s,
187
+ 'line' => 0,
188
+ 'column' => 0
189
+ )
190
+
191
+ # TODO: Need to check on the client capabilities of zero or one based indexes
192
+ unless err.file.nil? || err.line.nil?
193
+ frame.source = DSP::Source.new.from_h!('path' => err.file)
194
+ frame.line = err.line
195
+ frame.column = err.pos || 0
196
+ end
197
+
198
+ stack_frames << frame
199
+ end
200
+
201
+ # Generate StackFrame for each PuppetStack element
202
+ unless state.puppet_stacktrace.nil?
203
+ state.puppet_stacktrace.each do |pup_stack|
204
+ source_file = pup_stack[0]
205
+ # TODO: Need to check on the client capabilities of zero or one based indexes
206
+ source_line = pup_stack[1]
207
+
208
+ frame = DSP::StackFrame.new.from_h!(
209
+ 'id' => stack_frames.count,
210
+ 'name' => source_file.to_s,
211
+ 'source' => { 'path' => source_file },
212
+ 'line' => source_line,
213
+ 'column' => 0
214
+ )
215
+ stack_frames << frame
216
+ end
217
+ end
218
+
219
+ stack_frames
220
+ end
221
+
222
+ # Creates the list of scopes from the saved puppet session state
223
+ # @see DSP::Scope
224
+ # @return [Array<DSP::Scope>]
225
+ def generate_scopes_list(frame_id)
226
+ # Unfortunately we can only respond to Frame 0 as we don't have the variable state in other stack frames
227
+ return [] unless frame_id.zero?
228
+
229
+ result = []
230
+
231
+ this_scope = puppet_session_state.saved.scope # Go home rubocop, you're drunk.
232
+ until this_scope.nil? || this_scope.is_topscope?
233
+ result << DSP::Scope.new.from_h!(
234
+ 'name' => this_scope.to_s,
235
+ 'variablesReference' => this_scope.object_id,
236
+ 'namedVariables' => this_scope.to_hash(false).count,
237
+ 'expensive' => false
238
+ )
239
+ this_scope = this_scope.parent
240
+ end
241
+ unless puppet_session_state.actual.compiler.nil?
242
+ result << DSP::Scope.new.from_h!(
243
+ 'name' => puppet_session_state.actual.compiler.topscope.to_s,
244
+ 'variablesReference' => VARIABLES_REFERENCE_TOP_SCOPE,
245
+ 'namedVariables' => puppet_session_state.actual.compiler.topscope.to_hash(false).count,
246
+ 'expensive' => false
247
+ )
248
+ end
249
+ result
250
+ end
251
+
252
+ # Creates the list of variables from the saved puppet session state, given the arguments from a DSP::VariablesArguments object
253
+ # @see DSP::VariablesArguments
254
+ # @see DSP::Variable
255
+ # @return [Array<DSP::Variable>]
256
+ def generate_variables_list(arguments)
257
+ variables_reference = arguments.variablesReference
258
+ result = nil
259
+
260
+ # Check if this is the topscope
261
+ if variables_reference == VARIABLES_REFERENCE_TOP_SCOPE # rubocop:disable Style/IfUnlessModifier Nicer to read like this
262
+ result = variable_list_from_hash(puppet_session_state.actual.compiler.topscope.to_hash(false))
263
+ end
264
+ return result unless result.nil?
265
+
266
+ # Could be a cached variables reference
267
+ cache_list = puppet_session_state.saved.variable_cache[variables_reference]
268
+ unless cache_list.nil?
269
+ result = case cache_list
270
+ when Hash
271
+ variable_list_from_hash(cache_list)
272
+ when Array
273
+ variable_list_from_array(cache_list)
274
+ else
275
+ # Should never get here but just in case
276
+ []
277
+ end
278
+ end
279
+ return result unless result.nil?
280
+
281
+ # Could be a child scope
282
+ this_scope = puppet_session_state.saved.scope
283
+ until this_scope.nil? || this_scope.is_topscope?
284
+ if this_scope.object_id == variables_reference
285
+ result = variable_list_from_hash(this_scope.to_hash(false))
286
+ break
287
+ end
288
+ this_scope = this_scope.parent
289
+ end
290
+ return result unless result.nil?
291
+
292
+ []
293
+ end
294
+
295
+ # Evaluates or "compiles" an arbitrary puppet language string in the current scope. This comes from a DSP::EvaluateRequest
296
+ # which contains a DSP::EvaluateArguments object.
297
+ # @see DSP::EvaluateRequest
298
+ # @see DSP::EvaluateArguments
299
+ # @param arguments [DSP::EvaluateArguments]
300
+ # @returns [String, nil] Result of evaluating the string.
301
+ def evaluate_string(arguments)
302
+ raise "Unable to evaluate on Frame #{arguments.frameId}. Only the top-scope is supported" unless arguments.frameId.nil? || arguments.frameId.zero?
303
+ return nil if arguments.expression.nil? || arguments.expression.to_s.empty?
304
+ return nil if puppet_session_state.actual.compiler.nil?
305
+
306
+ # Ignore any log messages when evaluating watch expressions. They just clutter the debug console for no reason.
307
+ suppress_log = arguments.context == 'watch'
308
+
309
+ @evaluating_parser ||= ::Puppet::Pops::Parser::EvaluatingParser.new
310
+
311
+ # Unfortunately the log supression is global so we can only do one evaluation at a time.
312
+ result = nil
313
+ @evaluate_string_mutex.synchronize do
314
+ if suppress_log
315
+ flow_control.assert_flag(:suppress_log_messages) if suppress_log
316
+ # Even though we're suppressing log messages, we still need to save them to emit errors in a different format
317
+ message_aggregator = LogMessageAggregator.new(hook_manager)
318
+ message_aggregator.start!
319
+ end
320
+ begin
321
+ result = @evaluating_parser.evaluate_string(puppet_session_state.actual.compiler.topscope, arguments.expression)
322
+ if result.nil? && suppress_log
323
+ # A nil result could indicate a failure. Check the message_aggregator
324
+ msgs = message_aggregator.messages.select { |log| ERROR_LOG_LEVELS.include?(log.level) }.map(&:message)
325
+ raise msgs.join("\n") unless msgs.empty?
326
+ end
327
+ ensure
328
+ if suppress_log
329
+ flow_control.unassert_flag(:suppress_log_messages)
330
+ message_aggregator.stop!
331
+ end
332
+ end
333
+ end
334
+ # As this will be transmitted over JSON, force the output to a string
335
+ result.nil? ? nil : result.to_s
336
+ end
337
+
338
+ # Indicates that the debug session should stop gracefully
339
+ def close
340
+ send_termination_event
341
+ end
342
+
343
+ # Indicates that the debug session will be stopped in a forced manner
344
+ def force_terminate
345
+ @puppet_thread.exit unless @puppet_thread.nil?
346
+ end
347
+
348
+ # Retrieves the class name of a Puppet POPS object for Puppet 5+ and Puppet 4.x.
349
+ #
350
+ # @param obj [Object] The Puppet POPS object.
351
+ # @return [String] Then class name of the object
352
+ def get_puppet_class_name(obj)
353
+ # Puppet 5+ has PCore Types
354
+ return obj._pcore_type.simple_name if obj.respond_to?(:_pcore_type)
355
+
356
+ # .. otherwise revert to simple naive text splitting
357
+ # e.g. Puppet::Pops::Model::CallNamedFunctionExpression becomes CallNamedFunctionExpression
358
+ obj.class.to_s.split('::').last
359
+ end
360
+
361
+ # Retrieves the location of Puppet POPS object within a manifest
362
+ #
363
+ # @param obj [Object] The Puppet POPS object.
364
+ # @return [SourcePosition] The location of the object
365
+ def get_location_from_pops_object(obj)
366
+ # TODO: Should really use the SourceAdpater
367
+ # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25
368
+ pos = SourcePosition.new
369
+ return pos unless obj.is_a?(Puppet::Pops::Model::Positioned)
370
+
371
+ if obj.respond_to?(:file) && obj.respond_to?(:line)
372
+ # These methods were added to the Puppet::Pops::Model::Positioned in Puppet 5.x
373
+ pos.file = obj.file
374
+ pos.line = obj.line
375
+ pos.offset = obj.offset
376
+ pos.length = obj.length
377
+ else
378
+ # Revert to Puppet 4.x location information. A little more expensive to call
379
+ obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj)
380
+ unless obj_loc.nil?
381
+ pos.file = obj_loc.locator.file
382
+ pos.line = obj_loc.line
383
+ pos.offset = obj_loc.offset
384
+ pos.length = obj_loc.length
385
+ end
386
+ end
387
+
388
+ pos
389
+ end
390
+
391
+ # Retrieves the position on a line for a given document character offset
392
+ #
393
+ # @param obj [Object] The Puppet POPS object.
394
+ # @param offset [Integer] The character offset in the manifest
395
+ # @return [Integer] The position in the line
396
+ def pos_on_line(obj, offset)
397
+ # TODO: Should really use the SourceAdpater
398
+ # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25
399
+
400
+ # Puppet 5 exposes the source locator on the Pops object
401
+ return obj.locator.pos_on_line(offset) if obj.respond_to?(:locator)
402
+
403
+ # Revert to Puppet 4.x location information. A little more expensive to call
404
+ obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj)
405
+ obj_loc.locator.pos_on_line(offset)
406
+ end
407
+
408
+ # Retrieves line number for a given document character offset
409
+ #
410
+ # @param obj [Object] The Puppet POPS object.
411
+ # @param offset [Integer] The line number
412
+ # @return [Integer] The position in the line
413
+ def line_for_offset(obj, offset)
414
+ # TODO: Should really use the SourceAdpater
415
+ # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25
416
+
417
+ # Puppet 5 exposes the source locator on the Pops object
418
+ return obj.locator.line_for_offset(offset) if obj.respond_to?(:locator)
419
+
420
+ # Revert to Puppet 4.x location information. A little more expensive to call
421
+ obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj)
422
+ obj_loc.locator.line_for_offset(offset)
423
+ end
424
+
425
+ private
426
+
427
+ # Converts a hash of ruby objects into an array of DSP::Variable objects
428
+ #
429
+ # @see DSP::Variable
430
+ # @param obj_hash [Hash<Symbol, Object>] Hash of Puppet Objects
431
+ # @return [Array<DSP::Variable>] Array of DSP::Variable
432
+ # @private
433
+ def variable_list_from_hash(obj_hash = {})
434
+ obj_hash.sort.map do |key, value|
435
+ variable_from_ruby_object(key, value)
436
+ end
437
+ end
438
+
439
+ # Converts an array of ruby objects into an array of DSP::Variable objects
440
+ #
441
+ # @see DSP::Variable
442
+ # @param obj_hash [Array<Object>] Array of Puppet Objects
443
+ # @return [Array<DSP::Variable>] Array of DSP::Variable
444
+ # @private
445
+ def variable_list_from_array(obj_array = [])
446
+ result = []
447
+ # TODO: Could use obj_array.map.each_with_index ... ?
448
+ obj_array.each_index do |index|
449
+ result << variable_from_ruby_object(index.to_s, obj_array[index])
450
+ end
451
+ result
452
+ end
453
+
454
+ # Converts a ruby object into a DSP::Variable object
455
+ #
456
+ # @see DSP::Variable
457
+ # @param name [String] The name of the variable
458
+ # @param value [Object] The value of the variable
459
+ # @return [DSP::Variable]
460
+ # @private
461
+ def variable_from_ruby_object(name, value)
462
+ var_ref = 0
463
+ out_value = value.to_s
464
+
465
+ if value.is_a?(Array)
466
+ indexed_variables = value.count
467
+ var_ref = value.object_id
468
+ out_value = "Array [#{indexed_variables} item/s]"
469
+ puppet_session_state.saved.variable_cache[var_ref] = value
470
+ end
471
+
472
+ if value.is_a?(Hash)
473
+ named_variables = value.count
474
+ var_ref = value.object_id
475
+ out_value = "Hash [#{named_variables} item/s]"
476
+ puppet_session_state.saved.variable_cache[var_ref] = value
477
+ end
478
+
479
+ DSP::Variable.new.from_h!(
480
+ 'name' => name,
481
+ 'value' => out_value,
482
+ 'variablesReference' => var_ref
483
+ )
484
+ end
485
+ end
486
+
487
+ # A simple class which represents the position of somethin within a source document
488
+ class SourcePosition
489
+ # The path of the source file
490
+ # @return [String]
491
+ attr_accessor :file
492
+
493
+ # The line in the source file
494
+ # @return [Integer]
495
+ attr_accessor :line
496
+
497
+ # The absolute offset of the location in the source file
498
+ # @return [Integer]
499
+ attr_accessor :offset
500
+
501
+ # The numner of characters this position encompasses
502
+ # @return [Integer]
503
+ attr_accessor :length
504
+ end
505
+
506
+ # A helper class which hooks into log messages and saves them in receive order
507
+ class LogMessageAggregator
508
+ # The saved messages
509
+ # @return [Array<Puppet::Util::Log>]
510
+ attr_reader :messages
511
+
512
+ # @param hook_manager [PuppetDebugServer::Hooks] The hook manager to use
513
+ def initialize(hook_manager)
514
+ @hook_manager = hook_manager
515
+ @hook_id = :"aggregator#{object_id}"
516
+ @messages = []
517
+ @started = false
518
+ end
519
+
520
+ # Start aggregating log messages
521
+ def start!
522
+ return if @started
523
+
524
+ @hook_manager.add_hook(:hook_log_message, @hook_id) { |args| on_hook_log_message(args) }
525
+ end
526
+
527
+ # Stop aggregating log messages
528
+ def stop!
529
+ return unless @started
530
+
531
+ @hook_manager.delete_hook(:hook_log_message, @hook_id)
532
+ end
533
+
534
+ # Fires when a message is sent to the puppet logger
535
+ # Arguments:
536
+ # Message - The message being sent to the log
537
+ def on_hook_log_message(args)
538
+ @messages << args[0]
539
+ end
540
+ end
541
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet'
4
+
5
+ # Note - As much as I'd like to not monkey patch, the debug server supports many versions of the Puppet gem, and the internal
6
+ # classes etc. are not exposed easily. Ideally I'd prefer to use a custom compiler and evaluator but it's just not that easy
7
+ # to inject them. Instead we have to resort to monkey patching which is less than ideal
8
+
9
+ # Monkey patch the Apply application (puppet apply) so that we route the exit
10
+ # statement into the debugger first and then exit the puppet thread
11
+ require 'puppet/application/apply'
12
+ module Puppet
13
+ class Application
14
+ class Apply < Puppet::Application
15
+ def exit(option)
16
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_before_apply_exit, [option])
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ # Monkey patch the compiler so we can wrap our own rescue block around it
23
+ # to trap any exceptions that may be of interest to us
24
+ require 'puppet/parser/compiler'
25
+ module Puppet
26
+ module Parser
27
+ class Compiler
28
+ alias original_compile compile
29
+
30
+ def compile
31
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_before_compile, [self])
32
+ result = original_compile
33
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_after_compile, [result]) # TODO: This doesn't seem to be needed
34
+ result
35
+ rescue Puppet::ParseErrorWithIssue => e
36
+ # TODO: Potential issue here with 4.10.x not implementing .file on the Positioned class
37
+ # Just re-raise if there is no Puppet manifest file associated with the error
38
+ raise if e.file.nil? || e.line.nil? || e.pos.nil?
39
+
40
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_exception, [e])
41
+ raise
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # Add hooks to the evaluator so we can trap before and after evaluating parts of the syntax tree
48
+ require 'puppet/pops/evaluator/evaluator_impl'
49
+ module Puppet
50
+ module Pops
51
+ module Evaluator
52
+ class EvaluatorImpl
53
+ alias original_evaluate evaluate
54
+
55
+ def evaluate(target, scope)
56
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_before_pops_evaluate, [self, target, scope])
57
+ result = original_evaluate(target, scope)
58
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_after_pops_evaluate, [self, target, scope])
59
+ result
60
+ rescue => e # rubocop:disable Style/RescueStandardError Any error could be thrown here
61
+ # Emit non-Puppet related errors to the debug log. We shouldn't get any of these!
62
+ PuppetDebugServer.log_message(:debug, "Error in Puppet::Pops::Evaluator::EvaluatorImpl.evaluate #{e}: #{e.backtrace}") unless e.is_a?(Puppet::Error)
63
+ raise
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # These come from the original Puppet source
71
+ # rubocop:disable Style/PerlBackrefs, Style/EachWithObject
72
+ #
73
+ # Add a helper method to the PuppetStack object
74
+ require 'puppet/pops/puppet_stack'
75
+ module Puppet
76
+ module Pops
77
+ module PuppetStack
78
+ # This is very similar to the stacktrace function, but uses the exception
79
+ # backtrace instead of caller()
80
+ def self.stacktrace_from_backtrace(exception)
81
+ exception.backtrace.reduce([]) do |memo, loc|
82
+ if loc =~ /^(.*\.pp)?:([0-9]+):in (`stack'|`block in call_function')/
83
+ memo << [$1.nil? ? 'unknown' : $1, $2.to_i]
84
+ end
85
+ memo
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ # rubocop:enable Style/PerlBackrefs, Style/EachWithObject
92
+
93
+ # Add hooks to the functions reset so that we can add any needed functions
94
+ require 'puppet/parser/functions'
95
+ module Puppet
96
+ module Parser
97
+ module Functions
98
+ class << self
99
+ alias original_reset reset
100
+
101
+ def reset
102
+ result = original_reset
103
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_after_parser_function_reset, [self])
104
+ result
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # MUST BE LAST!!!!!!
112
+ # Add a debugserver log destination type
113
+ require 'puppet/util/log'
114
+ Puppet::Util::Log.newdesttype :debugserver do
115
+ def handle(msg)
116
+ PuppetDebugServer::PuppetDebugSession.instance.execute_hook(:hook_log_message, [msg])
117
+ end
118
+ end