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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +510 -0
- data/CODEOWNERS +2 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +54 -0
- data/Gemfile +53 -0
- data/LICENSE +201 -0
- data/README.md +308 -0
- data/Rakefile +185 -0
- data/bin/puppet-debugserver +8 -0
- data/bin/puppet-languageserver +7 -0
- data/bin/puppet-languageserver-sidecar +7 -0
- data/lib/dsp/dsp.rb +7 -0
- data/lib/dsp/dsp_base.rb +62 -0
- data/lib/dsp/dsp_protocol.rb +4619 -0
- data/lib/lsp/lsp.rb +10 -0
- data/lib/lsp/lsp_base.rb +63 -0
- data/lib/lsp/lsp_custom.rb +170 -0
- data/lib/lsp/lsp_enums.rb +143 -0
- data/lib/lsp/lsp_protocol.rb +2785 -0
- data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
- data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
- data/lib/lsp/lsp_protocol_configuration.rb +82 -0
- data/lib/lsp/lsp_protocol_declaration.rb +73 -0
- data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
- data/lib/lsp/lsp_protocol_implementation.rb +75 -0
- data/lib/lsp/lsp_protocol_progress.rb +200 -0
- data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
- data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
- data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
- data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
- data/lib/lsp/lsp_types.rb +1534 -0
- data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
- data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
- data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
- data/lib/puppet-debugserver/hooks.rb +132 -0
- data/lib/puppet-debugserver/message_handler.rb +277 -0
- data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
- data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
- data/lib/puppet-languageserver/client_session_state.rb +119 -0
- data/lib/puppet-languageserver/crash_dump.rb +50 -0
- data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
- data/lib/puppet-languageserver/facter_helper.rb +25 -0
- data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
- data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
- data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
- data/lib/puppet-languageserver/global_queues.rb +16 -0
- data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
- data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
- data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
- data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
- data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
- data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
- data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
- data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
- data/lib/puppet-languageserver/message_handler.rb +462 -0
- data/lib/puppet-languageserver/providers.rb +18 -0
- data/lib/puppet-languageserver/puppet_helper.rb +108 -0
- data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
- data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
- data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
- data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
- data/lib/puppet-languageserver/server_capabilities.rb +48 -0
- data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
- data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
- data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
- data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
- data/lib/puppet-languageserver/uri_helper.rb +46 -0
- data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
- data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
- data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
- data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
- data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
- data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
- data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
- data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
- data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
- data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
- data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
- data/lib/puppet_debugserver.rb +164 -0
- data/lib/puppet_editor_services/connection/base.rb +62 -0
- data/lib/puppet_editor_services/connection/stdio.rb +25 -0
- data/lib/puppet_editor_services/connection/tcp.rb +34 -0
- data/lib/puppet_editor_services/handler/base.rb +16 -0
- data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
- data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
- data/lib/puppet_editor_services/logging.rb +45 -0
- data/lib/puppet_editor_services/protocol/base.rb +27 -0
- data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
- data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
- data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
- data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
- data/lib/puppet_editor_services/server/base.rb +42 -0
- data/lib/puppet_editor_services/server/stdio.rb +85 -0
- data/lib/puppet_editor_services/server/tcp.rb +349 -0
- data/lib/puppet_editor_services/server.rb +15 -0
- data/lib/puppet_editor_services/version.rb +36 -0
- data/lib/puppet_editor_services.rb +8 -0
- data/lib/puppet_languageserver.rb +263 -0
- data/lib/puppet_languageserver_sidecar.rb +361 -0
- data/puppet-debugserver +11 -0
- data/puppet-editor-services.gemspec +29 -0
- data/puppet-languageserver +15 -0
- data/puppet-languageserver-sidecar +14 -0
- 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
|