Class: PuppetDebugServer::PuppetDebugSession

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializePuppetDebugSession

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
  @message_handler = 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

#breakpointsPuppetDebugServer::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_controlPuppetDebugServer::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_handlersPuppetDebugServer::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_managerPuppetDebugServer::Hooks (readonly)

The hook manager class. This is responsible for adding and calling hooks.

Returns:

See Also:



9
10
11
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 9

def hook_manager
  @hook_manager
end

#puppet_session_statePuppetDebugServer::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_idInteger (readonly)

The Ruby ID (not Operating System Thread ID) of the thread running Puppet (as opposed to RPC Server or debug session)

Returns:

  • (Integer)


24
25
26
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 24

def puppet_thread_id
  @puppet_thread_id
end

Class Method Details

.instanceObject

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

#closeObject

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
      message_aggregator = LogMessageAggregator.new(hook_manager)
      message_aggregator.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 = message_aggregator.messages.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)
        message_aggregator.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

Parameters:

  • event_name (Symbol)

    The name of the hook to execute.

  • args (Array<Object>)

    The arguments of the hook

See Also:



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_terminateObject

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

Returns:

See Also:



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_listArray<DSP::StackFrame>

Creates the list of stack frames from the saved puppet session state

Returns:

See Also:



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

Parameters:

  • obj (Object)

    The Puppet POPS object.

Returns:



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.

Parameters:

  • obj (Object)

    The Puppet POPS object.

Returns:

  • (String)

    Then class name of the object



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_sessionObject

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

Parameters:

  • obj (Object)

    The Puppet POPS object.

  • offset (Integer)

    The line number

Returns:

  • (Integer)

    The position in the line



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

Parameters:

  • obj (Object)

    The Puppet POPS object.

  • offset (Integer)

    The character offset in the manifest

Returns:

  • (Integer)

    The position in the line



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_puppetObject

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', @session_options['manifest'], '--detailed-exitcodes', '--logdest', 'debugserver']
  cmd_args << '--noop' if @session_options['noop'] == true
  cmd_args.push(*@session_options['args']) unless @session_options['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

Parameters:

  • exitcode (Integer)

    The exit code from the process. This is the puppet detailed exit code

See Also:



107
108
109
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 107

def send_exited_event(exitcode)
  @message_handler.send_exited_event(exitcode) unless @message_handler.nil?
end

#send_output_event(options) ⇒ Object

Sends an OutputEvent to the Debug Client

Parameters:

  • options (Hash)

    Options for the output

See Also:



78
79
80
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 78

def send_output_event(options)
  @message_handler.send_output_event(options) unless @message_handler.nil?
end

#send_stopped_event(reason, options = {}) ⇒ Object

Sends a StoppedEvent to the Debug Client

Parameters:

  • reason (String)

    Why the session has stopped

  • options (Hash) (defaults to: {})

    Options for the output

See Also:



86
87
88
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 86

def send_stopped_event(reason, options = {})
  @message_handler.send_stopped_event(reason, options) unless @message_handler.nil?
end

#send_termination_eventObject

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
  @message_handler.send_termination_event unless @message_handler.nil?
end

#send_thread_event(reason, thread_id) ⇒ Object

Sends a ThreadEvent to the Debug Client

Parameters:

  • reason (String)

    Why the the thread status has changed

  • thread_id (Integer)

    The ID of the thread

See Also:



94
95
96
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 94

def send_thread_event(reason, thread_id)
  @message_handler.send_thread_event(reason, thread_id) unless @message_handler.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

Parameters:

  • message_handler (PuppetDebugServer::MessageRouter)

    The message router used to communicate with the Debug Client.

  • options (Hash<String, String>) (defaults to: {})

    Hash of launch arguments from the DSP launch request



115
116
117
118
119
# File 'lib/puppet-debugserver/puppet_debug_session.rb', line 115

def setup(message_handler, options = {})
  @message_handler = message_handler
  @session_options = options
  flow_control.assert_flag(:session_setup)
end