|
| 1 | +module PuppetDebugServer |
| 2 | + module PuppetDebugSession |
| 3 | + @hook_handler = nil |
| 4 | + |
| 5 | + def self.hooks |
| 6 | + if @hook_handler.nil? |
| 7 | + @hook_handler = PuppetDebugServer::Hooks.new |
| 8 | + |
| 9 | + @hook_handler.add_hook(:hook_before_apply_exit, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::on_hook_before_apply_exit(args) } |
| 10 | + @hook_handler.add_hook(:hook_breakpoint, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_breakpoint(args) } |
| 11 | + @hook_handler.add_hook(:hook_step_breakpoint, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_step_breakpoint(args) } |
| 12 | + @hook_handler.add_hook(:hook_function_breakpoint, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_function_breakpoint(args) } |
| 13 | + @hook_handler.add_hook(:hook_before_compile, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_before_compile(args) } |
| 14 | + @hook_handler.add_hook(:hook_exception, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_exception(args) } |
| 15 | + @hook_handler.add_hook(:hook_log_message, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_log_message(args) } |
| 16 | + @hook_handler.add_hook(:hook_after_parser_function_reset, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_after_parser_function_reset(args) } |
| 17 | + @hook_handler.add_hook(:hook_before_pops_evaluate, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_before_pops_evaluate(args) } |
| 18 | + @hook_handler.add_hook(:hook_after_pops_evaluate, :debug_session) { |args| PuppetDebugServer::PuppetDebugSession::hook_after_pops_evaluate(args) } |
| 19 | + end |
| 20 | + @hook_handler |
| 21 | + end |
| 22 | + |
| 23 | + def self.hook_before_pops_evaluate(args) |
| 24 | + @session_pops_eval_depth = @session_pops_eval_depth + 1 |
| 25 | + target = args[1] |
| 26 | + # Ignore this if there is no positioning information available |
| 27 | + return unless target.is_a?(Puppet::Pops::Model::Positioned) |
| 28 | + # Even if it's positioned, it can still contain invalid information. Ignore it if |
| 29 | + # it's missing required information. This can happen when evaluting strings (e.g. watches from VSCode) |
| 30 | + # i.e. not a file on disk |
| 31 | + return if target.file.nil? || target.file.empty? |
| 32 | + |
| 33 | + # Break if we hit a specific puppet function |
| 34 | + if target._pcore_type.simple_name == 'CallNamedFunctionExpression' |
| 35 | + # TODO Do we really need to break on a function called breakpoint? |
| 36 | + if target.functor_expr.value == 'breakpoint' |
| 37 | + # Re-raise the hook as a breakpoint |
| 38 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_function_breakpoint, [target.functor_expr.value, target._pcore_type.name] +args) |
| 39 | + return |
| 40 | + else |
| 41 | + func_names = PuppetDebugServer::PuppetDebugSession.function_breakpoints |
| 42 | + func_names.each do |func| |
| 43 | + next unless func['name'] == target.functor_expr.value |
| 44 | + # Re-raise the hook as a breakpoint |
| 45 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_function_breakpoint, [target.functor_expr.value, target._pcore_type.name] + args) |
| 46 | + return |
| 47 | + end |
| 48 | + end |
| 49 | + end |
| 50 | + |
| 51 | + unless target.length == 0 |
| 52 | + excluded_classes = ['BlockExpression','HostClassDefinition'] |
| 53 | + file_path = target.file |
| 54 | + breakpoints = PuppetDebugServer::PuppetDebugSession.source_breakpoints(file_path) |
| 55 | + |
| 56 | + #target._pcore_type.simple_name |
| 57 | + # TODO should check if it's an object we don't care aount |
| 58 | + |
| 59 | + unless excluded_classes.include?(target._pcore_type.simple_name) || breakpoints.nil? || breakpoints.empty? |
| 60 | + # Calculate the start and end lines of the target |
| 61 | + target_start_line = target.line |
| 62 | + target_end_line = target.locator.line_for_offset(target.offset + target.length) |
| 63 | + |
| 64 | + breakpoints.each do |bp| |
| 65 | + bp_line = bp['line'] |
| 66 | + # TODO Hit and conditional BreakPoints? |
| 67 | + if bp_line >= target_start_line && bp_line <= target_end_line |
| 68 | + # Re-raise the hook as a breakpoint |
| 69 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_breakpoint, [target._pcore_type.name, ''] + args) |
| 70 | + #require 'pry'; binding.pry |
| 71 | + return |
| 72 | + end |
| 73 | + end |
| 74 | + end |
| 75 | + end |
| 76 | + |
| 77 | + # Break if we are stepping |
| 78 | + case PuppetDebugServer::PuppetDebugSession.run_mode |
| 79 | + when :stepin |
| 80 | + # Stepping-in is basically break on everything |
| 81 | + # Re-raise the hook as a step breakpoint |
| 82 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [target._pcore_type.name, ''] + args) |
| 83 | + return |
| 84 | + when :next |
| 85 | + # Next will break on anything at this Pop depth or shallower |
| 86 | + # Re-raise the hook as a step breakpoint |
| 87 | + run_options = PuppetDebugServer::PuppetDebugSession.run_mode_options |
| 88 | + if !run_options[:pops_depth_level].nil? && @session_pops_eval_depth <= run_options[:pops_depth_level] |
| 89 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [target._pcore_type.name, ''] + args) |
| 90 | + return |
| 91 | + end |
| 92 | + when :stepout |
| 93 | + # Stepping-Out will break on anything shallower than this Pop depth |
| 94 | + # Re-raise the hook as a step breakpoint |
| 95 | + run_options = PuppetDebugServer::PuppetDebugSession.run_mode_options |
| 96 | + if !run_options[:pops_depth_level].nil? && @session_pops_eval_depth < run_options[:pops_depth_level] |
| 97 | + PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [target._pcore_type.name, ''] + args) |
| 98 | + return |
| 99 | + end |
| 100 | + end |
| 101 | + end |
| 102 | + def self.hook_after_pops_evaluate(args) |
| 103 | + @session_pops_eval_depth = @session_pops_eval_depth - 1 |
| 104 | + target = args[1] |
| 105 | + return unless target.is_a?(Puppet::Pops::Model::Positioned) |
| 106 | + end |
| 107 | + |
| 108 | + def self.hook_after_parser_function_reset(args) |
| 109 | + func_object = args[0] |
| 110 | + |
| 111 | + # TODO Do we really need to break on a function called breakpoint? |
| 112 | + func_object.newfunction(:breakpoint, :type => :rvalue, :arity => -1, :doc => "Breakpoint Function") do |arguments| |
| 113 | + # This function is just a place holder. It gets interpretted at the pops_evaluate hooks but the function |
| 114 | + # itself still needs to exist though. |
| 115 | + end |
| 116 | + end |
| 117 | + |
| 118 | + def self.on_hook_before_apply_exit(args) |
| 119 | + option = args[0] |
| 120 | + |
| 121 | + PuppetDebugServer::PuppetDebugSession.connection.send_exited_event(option) |
| 122 | + PuppetDebugServer::PuppetDebugSession.connection.send_output_event({ |
| 123 | + 'category' => 'console', |
| 124 | + 'output' => "puppet exited with #{option}", |
| 125 | + }) |
| 126 | + end |
| 127 | + |
| 128 | + def self.hook_breakpoint(args) |
| 129 | + process_breakpoint_hook('breakpoint', args) |
| 130 | + end |
| 131 | + |
| 132 | + def self.hook_function_breakpoint(args) |
| 133 | + process_breakpoint_hook('function breakpoint', args) |
| 134 | + end |
| 135 | + |
| 136 | + def self.hook_step_breakpoint(args) |
| 137 | + process_breakpoint_hook('step', args) |
| 138 | + end |
| 139 | + |
| 140 | + def self.process_breakpoint_hook(reason, args) |
| 141 | + # If the debug session is paused, can't raise a new breakpoint |
| 142 | + return if PuppetDebugServer::PuppetDebugSession.session_paused? |
| 143 | + break_display_text = args[0] |
| 144 | + break_description = args[1] |
| 145 | + |
| 146 | + scope_object = nil |
| 147 | + pops_target_object = nil |
| 148 | + pops_depth_level = nil |
| 149 | + |
| 150 | + # Check if the breakpoint came from the Pops::Evaluator |
| 151 | + if args[2].is_a?(Puppet::Pops::Evaluator::EvaluatorImpl) |
| 152 | + pops_target_object = args[3] |
| 153 | + scope_object = args[4] |
| 154 | + pops_depth_level = @session_pops_eval_depth |
| 155 | + end |
| 156 | + |
| 157 | + break_description = break_display_text if break_description.empty? |
| 158 | + PuppetDebugServer::PuppetDebugSession.raise_and_wait_stopped_event(reason, break_display_text, break_description, { |
| 159 | + :pops_target => pops_target_object, |
| 160 | + :scope => scope_object, |
| 161 | + :pops_depth_level => pops_depth_level, |
| 162 | + :puppet_stacktrace => Puppet::Pops::PuppetStack.stacktrace |
| 163 | + }) |
| 164 | + end |
| 165 | + |
| 166 | + def self.hook_before_compile(args) |
| 167 | + PuppetDebugServer::PuppetDebugSession.session_compiler = args[0] |
| 168 | + |
| 169 | + # Spin-wait for the configurationDone message from the client before we continue compilation |
| 170 | + begin |
| 171 | + sleep(0.5) |
| 172 | + end while !PuppetDebugServer::PuppetDebugSession.client_completed_configuration? |
| 173 | + end |
| 174 | + |
| 175 | + def self.hook_exception(args) |
| 176 | + # If the debug session is paused, can't raise a new exception |
| 177 | + return if PuppetDebugServer::PuppetDebugSession.session_paused? |
| 178 | + |
| 179 | + error_detail = args[0] |
| 180 | + |
| 181 | + PuppetDebugServer::PuppetDebugSession.raise_and_wait_stopped_event( |
| 182 | + 'exception', 'Compilation Exception', error_detail.basic_message, { |
| 183 | + :session_exception => error_detail, |
| 184 | + :puppet_stacktrace => Puppet::Pops::PuppetStack.stacktrace_from_backtrace(error_detail) |
| 185 | + }) |
| 186 | + end |
| 187 | + |
| 188 | + def self.hook_log_message(args) |
| 189 | + return if self.suppress_log_messages |
| 190 | + msg = args[0] |
| 191 | + str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s |
| 192 | + str = msg.source == 'Puppet' ? str : "#{msg.source}: #{str}" |
| 193 | + |
| 194 | + level = msg.level.to_s.capitalize |
| 195 | + |
| 196 | + category = 'stderr' |
| 197 | + category = 'stdout' if msg.level == :notice || msg.level == :info || msg.level == :debug |
| 198 | + |
| 199 | + PuppetDebugServer::PuppetDebugSession.connection.send_output_event({ |
| 200 | + 'category' => category, |
| 201 | + 'output' => "#{level}: #{str}\n" |
| 202 | + }) |
| 203 | + end |
| 204 | + end |
| 205 | +end |
0 commit comments