Class: PuppetDebugServer::PuppetDebugSession
- Inherits:
-
Object
- Object
- PuppetDebugServer::PuppetDebugSession
- Defined in:
- lib/puppet-debugserver/puppet_debug_session.rb
Overview
Manages a Puppet Debug session including features such as; breakpoints, flow control, hooks into puppet.
Constant Summary collapse
- VARIABLES_REFERENCE_TOP_SCOPE =
rubocop:disable Style/ClassVars This class method (not instance) should be inherited
1- ERROR_LOG_LEVELS =
i[warning err alert emerg crit].freeze
- @@session_instance =
Use to track the default instance of the debug session
nil
Instance Attribute Summary collapse
-
#breakpoints ⇒ PuppetDebugServer::DebugSession::BreakPoints
readonly
The breakpoints class.
-
#flow_control ⇒ PuppetDebugServer::DebugSession::FlowControl
readonly
The flow control class.
-
#hook_handlers ⇒ PuppetDebugServer::DebugSession::HookHandlers
readonly
The hook handler class.
-
#hook_manager ⇒ PuppetDebugServer::Hooks
readonly
The hook manager class.
-
#puppet_session_state ⇒ PuppetDebugServer::DebugSession::PuppetSessionState
readonly
The session state class.
-
#puppet_thread_id ⇒ Integer
readonly
The Ruby ID (not Operating System Thread ID) of the thread running Puppet (as opposed to RPC Server or debug session).
Class Method Summary collapse
-
.instance ⇒ Object
Creates a debug session.
Instance Method Summary collapse
-
#close ⇒ Object
Indicates that the debug session should stop gracefully.
-
#evaluate_string(arguments) ⇒ Object
Evaluates or “compiles” an arbitrary puppet language string in the current scope.
-
#execute_hook(event_name, args) ⇒ Object
Executes a hook synchronously.
-
#force_terminate ⇒ Object
Indicates that the debug session will be stopped in a forced manner.
-
#generate_scopes_list(frame_id) ⇒ Array<DSP::Scope>
Creates the list of scopes from the saved puppet session state.
-
#generate_stackframe_list ⇒ Array<DSP::StackFrame>
Creates the list of stack frames from the saved puppet session state.
-
#generate_variables_list(arguments) ⇒ Array<DSP::Variable>
Creates the list of variables from the saved puppet session state, given the arguments from a DSP::VariablesArguments object.
-
#get_location_from_pops_object(obj) ⇒ SourcePosition
Retrieves the location of Puppet POPS object within a manifest.
-
#get_puppet_class_name(obj) ⇒ String
Retrieves the class name of a Puppet POPS object for Puppet 5+ and Puppet 4.x.
-
#initialize ⇒ PuppetDebugSession
constructor
A new instance of PuppetDebugSession.
-
#initialize_session ⇒ Object
Configures the debug session in it’s initial state.
-
#line_for_offset(obj, offset) ⇒ Integer
Retrieves line number for a given document character offset.
-
#pos_on_line(obj, offset) ⇒ Integer
Retrieves the position on a line for a given document character offset.
-
#run_puppet ⇒ Object
Synchronously runs Puppet in the debug session, assuming it has been configured correctly.
-
#send_exited_event(exitcode) ⇒ Object
Sends an ExitedEvent to the Debug Client.
-
#send_output_event(options) ⇒ Object
Sends an OutputEvent to the Debug Client.
-
#send_stopped_event(reason, options = {}) ⇒ Object
Sends a StoppedEvent to the Debug Client.
-
#send_termination_event ⇒ Object
Sends an TerminatedEvent to the Debug Client to indicated the Debug Server is terminating.
-
#send_thread_event(reason, thread_id) ⇒ Object
Sends a ThreadEvent to the Debug Client.
-
#setup(message_handler, options = {}) ⇒ Object
Sets up the debug session ready for actual use.
Constructor Details
#initialize ⇒ PuppetDebugSession
Returns a new instance of PuppetDebugSession.
50 51 52 53 54 55 56 57 58 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 50 def initialize = nil @flow_control = PuppetDebugServer::DebugSession::FlowControl.new(self) @hook_manager = PuppetDebugServer::Hooks.new @hook_handlers = PuppetDebugServer::DebugSession::HookHandlers.new(self) @breakpoints = PuppetDebugServer::DebugSession::BreakPoints.new(self) @puppet_session_state = PuppetDebugServer::DebugSession::PuppetSessionState.new @evaluate_string_mutex = Mutex.new end |
Instance Attribute Details
#breakpoints ⇒ PuppetDebugServer::DebugSession::BreakPoints (readonly)
The breakpoints class. This is responsible for storing and validation the active breakpoints during a debug session
29 30 31 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 29 def breakpoints @breakpoints end |
#flow_control ⇒ PuppetDebugServer::DebugSession::FlowControl (readonly)
The flow control class. This is responsible for controlling how the puppet agent execution flows. Including cross thread flags, determining if a session is paused or terminating.
20 21 22 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 20 def flow_control @flow_control end |
#hook_handlers ⇒ PuppetDebugServer::DebugSession::HookHandlers (readonly)
The hook handler class. This is responsible for responding to invoked hooks
14 15 16 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 14 def hook_handlers @hook_handlers end |
#hook_manager ⇒ PuppetDebugServer::Hooks (readonly)
The hook manager class. This is responsible for adding and calling hooks.
9 10 11 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 9 def hook_manager @hook_manager end |
#puppet_session_state ⇒ PuppetDebugServer::DebugSession::PuppetSessionState (readonly)
The session state class. This is responsible for determining the current and saved state of Puppet throughout the debug session
34 35 36 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 34 def puppet_session_state @puppet_session_state end |
#puppet_thread_id ⇒ Integer (readonly)
The Ruby ID (not Operating System Thread ID) of the thread running Puppet (as opposed to RPC Server or debug session)
24 25 26 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 24 def puppet_thread_id @puppet_thread_id end |
Class Method Details
.instance ⇒ Object
Creates a debug session
43 44 45 46 47 48 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 43 def self.instance # This can be called from any thread return @@session_instance unless @@session_instance.nil? # This class method (not instance) should be inherited @@session_instance = PuppetDebugSession.new # rubocop:disable Style/ClassVars This class method (not instance) should be inherited end |
Instance Method Details
#close ⇒ Object
Indicates that the debug session should stop gracefully
339 340 341 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 339 def close send_termination_event end |
#evaluate_string(arguments) ⇒ Object
Evaluates or “compiles” an arbitrary puppet language string in the current scope. This comes from a DSP::EvaluateRequest which contains a DSP::EvaluateArguments object.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 301 def evaluate_string(arguments) raise "Unable to evaluate on Frame #{arguments.frameId}. Only the top-scope is supported" unless arguments.frameId.nil? || arguments.frameId.zero? return nil if arguments.expression.nil? || arguments.expression.to_s.empty? return nil if puppet_session_state.actual.compiler.nil? # Ignore any log messages when evaluating watch expressions. They just clutter the debug console for no reason. suppress_log = arguments.context == 'watch' @evaluating_parser ||= ::Puppet::Pops::Parser::EvaluatingParser.new # Unfortunately the log supression is global so we can only do one evaluation at a time. result = nil @evaluate_string_mutex.synchronize do if suppress_log flow_control.assert_flag(:suppress_log_messages) if suppress_log # Even though we're suppressing log messages, we still need to save them to emit errors in a different format = LogMessageAggregator.new(hook_manager) .start! end begin result = @evaluating_parser.evaluate_string(puppet_session_state.actual.compiler.topscope, arguments.expression) if result.nil? && suppress_log # A nil result could indicate a failure. Check the message_aggregator msgs = ..select { |log| ERROR_LOG_LEVELS.include?(log.level) }.map(&:message) raise msgs.join("\n") unless msgs.empty? end ensure if suppress_log flow_control.unassert_flag(:suppress_log_messages) .stop! end end end # As this will be transmitted over JSON, force the output to a string result.nil? ? nil : result.to_s end |
#execute_hook(event_name, args) ⇒ Object
Executes a hook synchronously
64 65 66 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 64 def execute_hook(event_name, args) @hook_manager.exec_hook(event_name, args) end |
#force_terminate ⇒ Object
Indicates that the debug session will be stopped in a forced manner
344 345 346 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 344 def force_terminate @puppet_thread.exit unless @puppet_thread.nil? end |
#generate_scopes_list(frame_id) ⇒ Array<DSP::Scope>
Creates the list of scopes from the saved puppet session state
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 225 def generate_scopes_list(frame_id) # Unfortunately we can only respond to Frame 0 as we don't have the variable state in other stack frames return [] unless frame_id.zero? result = [] this_scope = puppet_session_state.saved.scope # Go home rubocop, you're drunk. until this_scope.nil? || this_scope.is_topscope? result << DSP::Scope.new.from_h!( 'name' => this_scope.to_s, 'variablesReference' => this_scope.object_id, 'namedVariables' => this_scope.to_hash(false).count, 'expensive' => false ) this_scope = this_scope.parent end unless puppet_session_state.actual.compiler.nil? result << DSP::Scope.new.from_h!( 'name' => puppet_session_state.actual.compiler.topscope.to_s, 'variablesReference' => VARIABLES_REFERENCE_TOP_SCOPE, 'namedVariables' => puppet_session_state.actual.compiler.topscope.to_hash(false).count, 'expensive' => false ) end result end |
#generate_stackframe_list ⇒ Array<DSP::StackFrame>
Creates the list of stack frames from the saved puppet session state
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 148 def generate_stackframe_list stack_frames = [] state = puppet_session_state.saved # Generate StackFrame for a Pops::Evaluator object with location information unless state.pops_target.nil? target = state.pops_target frame = DSP::StackFrame.new.from_h!( 'id' => stack_frames.count, 'name' => get_puppet_class_name(target), 'line' => 0, 'column' => 0 ) # TODO: Need to check on the client capabilities of zero or one based indexes if target.is_a?(Puppet::Pops::Model::Positioned) target_loc = get_location_from_pops_object(target) frame.name = target_loc.file frame.line = target_loc.line frame.column = pos_on_line(target, target_loc.offset) frame.source = DSP::Source.new.from_h!('path' => target_loc.file) if target_loc.length > 0 # rubocop:disable Style/ZeroLengthPredicate end_offset = target_loc.offset + target_loc.length frame.endLine = line_for_offset(target, end_offset) frame.endColumn = pos_on_line(target, end_offset) end end stack_frames << frame end # Generate StackFrame for an error unless state.exception.nil? err = state.exception frame = DSP::StackFrame.new.from_h!( 'id' => stack_frames.count, 'name' => err.class.to_s, 'line' => 0, 'column' => 0 ) # TODO: Need to check on the client capabilities of zero or one based indexes unless err.file.nil? || err.line.nil? frame.source = DSP::Source.new.from_h!('path' => err.file) frame.line = err.line frame.column = err.pos || 0 end stack_frames << frame end # Generate StackFrame for each PuppetStack element unless state.puppet_stacktrace.nil? state.puppet_stacktrace.each do |pup_stack| source_file = pup_stack[0] # TODO: Need to check on the client capabilities of zero or one based indexes source_line = pup_stack[1] frame = DSP::StackFrame.new.from_h!( 'id' => stack_frames.count, 'name' => source_file.to_s, 'source' => { 'path' => source_file }, 'line' => source_line, 'column' => 0 ) stack_frames << frame end end stack_frames end |
#generate_variables_list(arguments) ⇒ Array<DSP::Variable>
Creates the list of variables from the saved puppet session state, given the arguments from a DSP::VariablesArguments object
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 256 def generate_variables_list(arguments) variables_reference = arguments.variablesReference result = nil # Check if this is the topscope if variables_reference == VARIABLES_REFERENCE_TOP_SCOPE # rubocop:disable Style/IfUnlessModifier Nicer to read like this result = variable_list_from_hash(puppet_session_state.actual.compiler.topscope.to_hash(false)) end return result unless result.nil? # Could be a cached variables reference cache_list = puppet_session_state.saved.variable_cache[variables_reference] unless cache_list.nil? result = case cache_list when Hash variable_list_from_hash(cache_list) when Array variable_list_from_array(cache_list) else # Should never get here but just in case [] end end return result unless result.nil? # Could be a child scope this_scope = puppet_session_state.saved.scope until this_scope.nil? || this_scope.is_topscope? if this_scope.object_id == variables_reference result = variable_list_from_hash(this_scope.to_hash(false)) break end this_scope = this_scope.parent end return result unless result.nil? [] end |
#get_location_from_pops_object(obj) ⇒ SourcePosition
Retrieves the location of Puppet POPS object within a manifest
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 365 def get_location_from_pops_object(obj) # TODO: Should really use the SourceAdpater # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25 pos = SourcePosition.new return pos unless obj.is_a?(Puppet::Pops::Model::Positioned) if obj.respond_to?(:file) && obj.respond_to?(:line) # These methods were added to the Puppet::Pops::Model::Positioned in Puppet 5.x pos.file = obj.file pos.line = obj.line pos.offset = obj.offset pos.length = obj.length else # Revert to Puppet 4.x location information. A little more expensive to call obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj) unless obj_loc.nil? pos.file = obj_loc.locator.file pos.line = obj_loc.line pos.offset = obj_loc.offset pos.length = obj_loc.length end end pos end |
#get_puppet_class_name(obj) ⇒ String
Retrieves the class name of a Puppet POPS object for Puppet 5+ and Puppet 4.x.
352 353 354 355 356 357 358 359 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 352 def get_puppet_class_name(obj) # Puppet 5+ has PCore Types return obj._pcore_type.simple_name if obj.respond_to?(:_pcore_type) # .. otherwise revert to simple naive text splitting # e.g. Puppet::Pops::Model::CallNamedFunctionExpression becomes CallNamedFunctionExpression obj.class.to_s.split('::').last end |
#initialize_session ⇒ Object
Configures the debug session in it’s initial state. Typically called as soon as the debug session is created
69 70 71 72 73 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 69 def initialize_session # Save the thread incase we need to forcibly kill it @puppet_thread = Thread.current @puppet_thread_id = @puppet_thread.object_id.to_i end |
#line_for_offset(obj, offset) ⇒ Integer
Retrieves line number for a given document character offset
413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 413 def line_for_offset(obj, offset) # TODO: Should really use the SourceAdpater # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25 # Puppet 5 exposes the source locator on the Pops object return obj.locator.line_for_offset(offset) if obj.respond_to?(:locator) # Revert to Puppet 4.x location information. A little more expensive to call obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj) obj_loc.locator.line_for_offset(offset) end |
#pos_on_line(obj, offset) ⇒ Integer
Retrieves the position on a line for a given document character offset
396 397 398 399 400 401 402 403 404 405 406 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 396 def pos_on_line(obj, offset) # TODO: Should really use the SourceAdpater # https://github.com/puppetlabs/puppet-strings/blob/ede2b0e76c278c98d57aa80a550971e934ba93ef/lib/puppet-strings/yard/parsers/puppet/statement.rb#L22-L25 # Puppet 5 exposes the source locator on the Pops object return obj.locator.pos_on_line(offset) if obj.respond_to?(:locator) # Revert to Puppet 4.x location information. A little more expensive to call obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj) obj_loc.locator.pos_on_line(offset) end |
#run_puppet ⇒ Object
Synchronously runs Puppet in the debug session, assuming it has been configured correctly. Requires the session_setup and client_completed_configuration flags to be set prior.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 123 def run_puppet # Perform pre-run checks... return if flow_control.terminate? raise 'Missing session setup' unless flow_control.flag?(:session_setup) raise 'Missing client configuration' unless flow_control.flag?(:client_completed_configuration) # Run puppet puppet_session_state.actual.reset! flow_control.assert_flag(:puppet_started) cmd_args = ['apply', ['manifest'], '--detailed-exitcodes', '--logdest', 'debugserver'] cmd_args << '--noop' if ['noop'] == true cmd_args.push(*['args']) unless ['args'].nil? send_output_event( 'category' => 'console', 'output' => "puppet #{cmd_args.join(' ')}\n" ) send_thread_event('started', @puppet_thread_id) Puppet::Util::CommandLine.new('puppet.rb', cmd_args).execute end |
#send_exited_event(exitcode) ⇒ Object
Sends an ExitedEvent to the Debug Client
107 108 109 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 107 def send_exited_event(exitcode) .send_exited_event(exitcode) unless .nil? end |
#send_output_event(options) ⇒ Object
Sends an OutputEvent to the Debug Client
78 79 80 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 78 def send_output_event() .send_output_event() unless .nil? end |
#send_stopped_event(reason, options = {}) ⇒ Object
Sends a StoppedEvent to the Debug Client
86 87 88 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 86 def send_stopped_event(reason, = {}) .send_stopped_event(reason, ) unless .nil? end |
#send_termination_event ⇒ Object
Sends an TerminatedEvent to the Debug Client to indicated the Debug Server is terminating
100 101 102 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 100 def send_termination_event .send_termination_event unless .nil? end |
#send_thread_event(reason, thread_id) ⇒ Object
Sends a ThreadEvent to the Debug Client
94 95 96 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 94 def send_thread_event(reason, thread_id) .send_thread_event(reason, thread_id) unless .nil? end |
#setup(message_handler, options = {}) ⇒ Object
Sets up the debug session ready for actual use. This is different from initialize_session in that it requires a running RPC server
115 116 117 118 119 |
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 115 def setup(, = {}) = = flow_control.assert_flag(:session_setup) end |