Class: PuppetDebugServer::DebugSession::HookHandlers

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet-debugserver/debug_session/hook_handlers.rb

Overview

TODO:

The following hooks are not implemented

Implements the hooks within the debug session.

:hook_after_compile

Fires after a catalog compilation is succesfully completed
Arguments:
  Puppet::Resource::Catalog - Resultant compiled catalog

:hook_before_parser_function_reset

Fires before the Puppet::Parser::Functions is reset, destroying the existing list of loaded functions
Arguments:
  Puppet::Parser::Functions - Instance of Puppet::Parser::Functions

Constant Summary collapse

EXCLUDED_CLASSES =

List of Puppet POPS classes that the Source Breakpoints will NOT trigger on

%w[BlockExpression HostClassDefinition].freeze

Instance Method Summary collapse

Constructor Details

#initialize(debug_session) ⇒ HookHandlers

Returns a new instance of HookHandlers.

Parameters:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 23

def initialize(debug_session)
  @debug_session = debug_session

  @debug_session.hook_manager.add_hook(:hook_after_parser_function_reset, :debug_session) { |args| on_hook_after_parser_function_reset(args) }
  @debug_session.hook_manager.add_hook(:hook_after_pops_evaluate, :debug_session)         { |args| on_hook_after_pops_evaluate(args) }
  @debug_session.hook_manager.add_hook(:hook_before_apply_exit, :debug_session)           { |args| on_hook_before_apply_exit(args) }
  @debug_session.hook_manager.add_hook(:hook_before_compile, :debug_session)              { |args| on_hook_before_compile(args) }
  @debug_session.hook_manager.add_hook(:hook_before_pops_evaluate, :debug_session)        { |args| on_hook_before_pops_evaluate(args) }
  @debug_session.hook_manager.add_hook(:hook_breakpoint, :debug_session)                  { |args| on_hook_breakpoint(args) }
  @debug_session.hook_manager.add_hook(:hook_exception, :debug_session)                   { |args| on_hook_exception(args) }
  @debug_session.hook_manager.add_hook(:hook_function_breakpoint, :debug_session)         { |args| on_hook_function_breakpoint(args) }
  @debug_session.hook_manager.add_hook(:hook_log_message, :debug_session)                 { |args| on_hook_log_message(args) }
  @debug_session.hook_manager.add_hook(:hook_step_breakpoint, :debug_session)             { |args| on_hook_step_breakpoint(args) }
end

Instance Method Details

#on_hook_after_parser_function_reset(args) ⇒ Object

Fires after the Puppet::Parser::Functions class is reset

Arguments:
  Puppet::Parser::Functions - Instance of Puppet::Parser::Functions


41
42
43
44
45
46
47
48
49
50
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 41

def on_hook_after_parser_function_reset(args)
  func_object = args[0]

  # This mimics the break function from puppet-debugger
  # https://github.com/nwops/puppet-debug#usage
  func_object.newfunction(:'debug::break', type: :rvalue, arity: -1, doc: 'Breakpoint Function') do |arguments|
    # This function is just a place holder. It gets interpretted at the
    # pops_evaluate hooks but the function itself still needs to exist though.
  end
end

#on_hook_after_pops_evaluate(_args) ⇒ Object

Fires after an item in the AST is evaluated Arguments:

The Pops object about to be evaluated
The scope of the Pops object


56
57
58
59
60
61
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 56

def on_hook_after_pops_evaluate(_args)
  # If the debug session is paused no need to process
  return if @debug_session.flow_control.session_paused?

  @debug_session.puppet_session_state.actual.decrement_pops_depth
end

#on_hook_before_apply_exit(args) ⇒ Object

Fires before the Puppet::Apply application tries to call Kernel#exit. Arguments:

Integer - Exit Code


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 66

def on_hook_before_apply_exit(args)
  option = args[0]

  @debug_session.send_exited_event(option)
  @debug_session.send_output_event(
    'category' => 'console',
    'output' => "puppet exited with #{option}"
  )

  @debug_session.flow_control.unassert_flag(:puppet_started)
  @debug_session.close

  # Wait up to 30 seconds for the client to disconnect and stop the debug session
  # Anymore than that and we force the debug session to stop.
  sleep(30)
  @debug_session.force_terminate
end

#on_hook_before_compile(args) ⇒ Object

Fires before a catalog compilation is attempted Arguments:

Puppet::Parser::Compiler - Current compiler in use


87
88
89
90
91
92
93
94
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 87

def on_hook_before_compile(args)
  @debug_session.puppet_session_state.actual.update_compiler(args[0])

  # Spin-wait for the configurationDone message from the client before we continue compilation
  return if @debug_session.flow_control.flag?(:client_completed_configuration)

  sleep(0.5) until @debug_session.flow_control.flag?(:client_completed_configuration)
end

#on_hook_before_pops_evaluate(args) ⇒ Object

Fires before an item in the AST is evaluated during a catalog compilation

Arguments:
  The Pops object about to be evaluated
  The scope of the Pops object


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 100

def on_hook_before_pops_evaluate(args)
  # If the debug session is paused no need to process
  return if @debug_session.flow_control.session_paused?

  @debug_session.puppet_session_state.actual.increment_pops_depth

  target = args[1]
  # Ignore this if there is no positioning information available
  return unless target.is_a?(Puppet::Pops::Model::Positioned)

  target_loc = @debug_session.get_location_from_pops_object(target)

  # Even if it's positioned, it can still contain invalid information.  Ignore it if
  # it's missing required information.  This can happen when evaluting strings (e.g. watches from VSCode)
  # i.e. not a file on disk
  return if target_loc.file.nil? || target_loc.file.empty?

  target_classname = @debug_session.get_puppet_class_name(target)
  ast_classname = get_ast_class_name(target)

  # Break if we hit a specific puppet function
  if target_classname == 'CallNamedFunctionExpression' && @debug_session.breakpoints.function_breakpoint_names.include?(target.functor_expr.value)
    # Re-raise the hook as a breakpoint
    @debug_session.execute_hook(:hook_function_breakpoint, [target.functor_expr.value, ast_classname] + args)
    return
  end

  # Check for Source based breakpoints
  unless target_loc.length.zero? || EXCLUDED_CLASSES.include?(target_classname)
    line_breakpoints = @debug_session.breakpoints.line_breakpoints(target_loc.file)

    # Calculate the start and end lines of the target
    target_start_line = target_loc.line
    target_end_line   = @debug_session.line_for_offset(target, target_loc.offset + target_loc.length)

    # TODO: What about Hit and Conditional BreakPoints?
    bp = line_breakpoints.find_index { |bp_line| bp_line >= target_start_line && bp_line <= target_end_line }
    unless bp.nil?
      # Re-raise the hook as a breakpoint
      @debug_session.execute_hook(:hook_breakpoint, [ast_classname, ''] + args)
      return
    end
  end

  # Break if we are stepping
  case @debug_session.flow_control.run_mode.mode
  when :stepin
    # Stepping-in is basically break on everything
    # Re-raise the hook as a step breakpoint
    @debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
  when :next
    # Next will break on anything at this Pop depth or shallower than this Pop depth. Re-raise the hook as a step breakpoint
    depth = @debug_session.flow_control.run_mode.options[:pops_depth_level] || -1
    if @debug_session.puppet_session_state.actual.pops_depth_level <= depth # rubocop:disable Style/IfUnlessModifier
      @debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
    end
  when :stepout
    # Stepping-Out will break on anything shallower than this Pop depth. Re-raise the hook as a step breakpoint
    depth = @debug_session.flow_control.run_mode.options[:pops_depth_level] || -1
    if @debug_session.puppet_session_state.actual.pops_depth_level < depth # rubocop:disable Style/IfUnlessModifier
      @debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
    end
  end
  nil
end

#on_hook_breakpoint(args) ⇒ Object

Fires when a source/line breakpoint is hit Arguments:

String - Breakpoint display text
String - Breakpoint full text
Object - self where the breakpoint was hit
Object[] - optional objects


172
173
174
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 172

def on_hook_breakpoint(args)
  process_breakpoint_hook('breakpoint', args)
end

#on_hook_exception(args) ⇒ Object

Fires when an unhandled exception is hit during puppet apply Arguments:

Error - The exception information


179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 179

def on_hook_exception(args)
  # If the debug session is paused, can't raise a new exception
  return if @debug_session.flow_control.session_paused?

  error_detail = args[0]

  @debug_session.flow_control.raise_stopped_event_and_wait(
    'exception',
    'Compilation Exception',
    error_detail.basic_message,
    session_exception: error_detail,
    puppet_stacktrace: Puppet::Pops::PuppetStack.stacktrace_from_backtrace(error_detail)
  )
end

#on_hook_function_breakpoint(args) ⇒ Object

Fires when a function breakpoint is hit Arguments:

String - Breakpoint display text
String - Breakpoint full text
Object - self where the function breakpoint was hit
Object[] - optional objects


200
201
202
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 200

def on_hook_function_breakpoint(args)
  process_breakpoint_hook('function breakpoint', args)
end

#on_hook_log_message(args) ⇒ Object

Fires when a message is sent to the puppet logger Arguments:

Message - The message being sent to the log


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 207

def on_hook_log_message(args)
  return if @debug_session.flow_control.flag?(:suppress_log_messages)

  msg = args[0]
  str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
  str = "#{msg.source}: #{str}" unless msg.source == 'Puppet'

  level = msg.level.to_s.capitalize

  category = 'stderr'
  category = 'stdout' if msg.level == :notice || msg.level == :info || msg.level == :debug

  @debug_session.send_output_event(
    'category' => category,
    'output' => "#{level}: #{str}\n"
  )
end

#on_hook_step_breakpoint(args) ⇒ Object

Fires when a function breakpoint is hit Arguments:

String - Breakpoint display text
String - Breakpoint full text
Object - self where the step breakpoint was hit
Object[] - optional objects


231
232
233
# File 'lib/puppet-debugserver/debug_session/hook_handlers.rb', line 231

def on_hook_step_breakpoint(args)
  process_breakpoint_hook('step', args)
end