Skip to content

Commit 6740faf

Browse files
committed
(GH-204) Add support for Puppet 4.x to the Debug Server
Previously the Debug Server only support Puppet 5 agents. However a large number of users still run Puppet 4.x. This commit: * Refactors function calls which can differ between Puppet 4 and 5 into helper methods. Namely positioning information and class name shortcuts. * Identifies other potential issues which use Puppet 5 only concepts
1 parent 8577b48 commit 6740faf

File tree

12 files changed

+232
-30
lines changed

12 files changed

+232
-30
lines changed

server/lib/puppet-debugserver/debug_hook_handlers.rb

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module PuppetDebugServer
22
module PuppetDebugSession
33
@hook_handler = nil
4-
4+
55
def self.hooks
66
if @hook_handler.nil?
77
@hook_handler = PuppetDebugServer::Hooks.new
@@ -25,49 +25,49 @@ def self.hook_before_pops_evaluate(args)
2525
target = args[1]
2626
# Ignore this if there is no positioning information available
2727
return unless target.is_a?(Puppet::Pops::Model::Positioned)
28+
target_loc = get_location_from_pops_object(target)
29+
2830
# Even if it's positioned, it can still contain invalid information. Ignore it if
2931
# it's missing required information. This can happen when evaluting strings (e.g. watches from VSCode)
3032
# i.e. not a file on disk
31-
return if target.file.nil? || target.file.empty?
33+
return if target_loc.file.nil? || target_loc.file.empty?
34+
target_classname = get_puppet_class_name(target)
35+
ast_classname = get_ast_class_name(target)
3236

3337
# Break if we hit a specific puppet function
34-
if target._pcore_type.simple_name == 'CallNamedFunctionExpression'
38+
if target_classname == 'CallNamedFunctionExpression'
3539
# TODO Do we really need to break on a function called breakpoint?
3640
if target.functor_expr.value == 'breakpoint'
3741
# 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)
42+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_function_breakpoint, [target.functor_expr.value, ast_classname] +args)
3943
return
4044
else
4145
func_names = PuppetDebugServer::PuppetDebugSession.function_breakpoints
4246
func_names.each do |func|
4347
next unless func['name'] == target.functor_expr.value
4448
# 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)
49+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_function_breakpoint, [target.functor_expr.value, ast_classname] + args)
4650
return
4751
end
4852
end
4953
end
5054

51-
unless target.length == 0
55+
unless target_loc.length == 0
5256
excluded_classes = ['BlockExpression','HostClassDefinition']
53-
file_path = target.file
57+
file_path = target_loc.file
5458
breakpoints = PuppetDebugServer::PuppetDebugSession.source_breakpoints(file_path)
55-
56-
#target._pcore_type.simple_name
5759
# 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+
unless excluded_classes.include?(target_classname) || breakpoints.nil? || breakpoints.empty?
6061
# 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)
62+
target_start_line = target_loc.line
63+
target_end_line = line_for_offset(target, target_loc.offset + target_loc.length)
6364

6465
breakpoints.each do |bp|
6566
bp_line = bp['line']
6667
# TODO Hit and conditional BreakPoints?
6768
if bp_line >= target_start_line && bp_line <= target_end_line
6869
# 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
70+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_breakpoint, [ast_classname, ''] + args)
7171
return
7272
end
7373
end
@@ -79,26 +79,27 @@ def self.hook_before_pops_evaluate(args)
7979
when :stepin
8080
# Stepping-in is basically break on everything
8181
# Re-raise the hook as a step breakpoint
82-
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [target._pcore_type.name, ''] + args)
82+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
8383
return
8484
when :next
8585
# Next will break on anything at this Pop depth or shallower
8686
# Re-raise the hook as a step breakpoint
8787
run_options = PuppetDebugServer::PuppetDebugSession.run_mode_options
8888
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)
89+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
9090
return
9191
end
9292
when :stepout
9393
# Stepping-Out will break on anything shallower than this Pop depth
9494
# Re-raise the hook as a step breakpoint
9595
run_options = PuppetDebugServer::PuppetDebugSession.run_mode_options
9696
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)
97+
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
9898
return
9999
end
100100
end
101101
end
102+
102103
def self.hook_after_pops_evaluate(args)
103104
@session_pops_eval_depth = @session_pops_eval_depth - 1
104105
target = args[1]
@@ -191,7 +192,7 @@ def self.hook_log_message(args)
191192
str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
192193
str = msg.source == 'Puppet' ? str : "#{msg.source}: #{str}"
193194

194-
level = msg.level.to_s.capitalize
195+
level = msg.level.to_s.capitalize
195196

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

server/lib/puppet-debugserver/puppet_debug_session.rb

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module PuppetDebugSession
1515
@session_compiler = nil # TODO Not sure we need this
1616
@session_paused_state = {}
1717
@session_variables_cache = {}
18-
18+
1919
def self.puppet_thread_id
2020
@puppet_thread.object_id.to_i unless @puppet_thread.nil?
2121
end
@@ -78,7 +78,7 @@ def self.continue_stepout_session
7878
clear_paused_state
7979
}
8080
end
81-
81+
8282
def self.continue_next_session
8383
@session_mutex.synchronize {
8484
@session_paused = false
@@ -159,7 +159,7 @@ def self.raise_and_wait_stopped_event(reason, description, text, options = {})
159159
@session_paused_state[:pops_target] = options[:pops_target] unless options[:pops_target].nil?
160160
@session_paused_state[:scope] = options[:scope] unless options[:scope].nil?
161161
@session_paused_state[:pops_depth_level] = options[:pops_depth_level] unless options[:pops_depth_level].nil?
162-
162+
163163
PuppetDebugServer::PuppetDebugSession.connection.send_stopped_event(reason, {
164164
'description' => description,
165165
'text' => text,
@@ -323,13 +323,14 @@ def self.generate_stackframe_list(options = {})
323323

324324
frame = {
325325
'id' => stack_frames.count,
326-
'name' => target._pcore_type.simple_name.to_s,
326+
'name' => get_puppet_class_name(target),
327327
'line' => 0,
328328
'column' => 0,
329329
}
330330

331331
# TODO need to check on the client capabilities of zero or one based indexes
332332
if target.is_a?(Puppet::Pops::Model::Positioned)
333+
# TODO - Potential issue here with 4.10.x not implementing .file on the Positioned class
333334
frame['source'] = PuppetDebugServer::Protocol::Source.create({
334335
'path' => target.file,
335336
})
@@ -346,7 +347,7 @@ def self.generate_stackframe_list(options = {})
346347

347348
stack_frames << frame
348349
end
349-
350+
350351
# Generate StackFrame for an error
351352
unless @session_paused_state[:exception].nil?
352353
err = @session_paused_state[:exception]
@@ -358,6 +359,7 @@ def self.generate_stackframe_list(options = {})
358359
}
359360

360361
# TODO need to check on the client capabilities of zero or one based indexes
362+
# TODO - Potential issue here with 4.10.x not implementing .file on the Positioned class
361363
unless err.file.nil? || err.line.nil?
362364
frame['source'] = PuppetDebugServer::Protocol::Source.create({
363365
'path' => err.file,
@@ -388,11 +390,63 @@ def self.generate_stackframe_list(options = {})
388390
stack_frames << frame
389391
end
390392
end
391-
393+
392394
stack_frames
393395
end
394396

397+
# Public method but only called publicly for
398+
# for testing
399+
def self.reset_pops_eval_depth
400+
@session_pops_eval_depth = 0
401+
end
402+
395403
# Private methods
404+
def self.get_location_from_pops_object(obj)
405+
pos = SourcePosition.new()
406+
return pos unless obj.is_a?(Puppet::Pops::Model::Positioned)
407+
408+
if obj.respond_to?(:file) && obj.respond_to?(:line)
409+
# These methods were added to the Puppet::Pops::Model::Positioned in Puppet 5.x
410+
pos.file = obj.file
411+
pos.line = obj.line
412+
pos.offset = obj.offset
413+
pos.length = obj.length
414+
else
415+
# Revert to Puppet 4.x location information. A little more expensive to call
416+
obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj)
417+
pos.file = obj_loc.locator.file
418+
pos.line = obj_loc.line
419+
pos.offset = obj_loc.offset
420+
pos.length = obj_loc.length
421+
end
422+
423+
pos
424+
end
425+
426+
def self.get_puppet_class_name(obj)
427+
# Puppet 5 has PCore Types
428+
return obj._pcore_type.simple_name if obj.respond_to?(:_pcore_type)
429+
# .. otherwise revert to simple naive text splitting
430+
# e.g. Puppet::Pops::Model::CallNamedFunctionExpression becomes CallNamedFunctionExpression
431+
obj.class.to_s.split('::').last
432+
end
433+
434+
def self.get_ast_class_name(obj)
435+
# Puppet 5 has PCore Types
436+
return obj._pcore_type.name if obj.respond_to?(:_pcore_type)
437+
# .. otherwise revert to Pops classname
438+
obj.class.to_s
439+
end
440+
441+
def self.line_for_offset(obj, offset)
442+
# Puppet 5 exposes the source locator on the Pops object
443+
return obj.locator.line_for_offset(offset) if obj.respond_to?(:locator)
444+
445+
# Revert to Puppet 4.x location information. A little more expensive to call
446+
obj_loc = Puppet::Pops::Utils.find_closest_positioned(obj)
447+
obj_loc.locator.line_for_offset(offset)
448+
end
449+
396450
def self.debug_session_watcher
397451
loop do
398452
sleep(1)
@@ -414,8 +468,8 @@ def self.start_puppet
414468
cmd_args << '--noop' if @session_options['noop'] == true
415469
cmd_args.push(*@session_options['args']) unless @session_options['args'].nil?
416470

417-
@session_pops_eval_depth = 0
418-
471+
reset_pops_eval_depth
472+
419473
# Send experimental warning
420474
PuppetDebugServer::PuppetDebugSession.connection.send_output_event({
421475
'category' => 'console',
@@ -428,5 +482,12 @@ def self.start_puppet
428482
})
429483
Puppet::Util::CommandLine.new('puppet.rb',cmd_args).execute
430484
end
485+
486+
class SourcePosition
487+
attr_accessor :file
488+
attr_accessor :line
489+
attr_accessor :offset
490+
attr_accessor :length
491+
end
431492
end
432493
end

server/lib/puppet-debugserver/puppet_monkey_patches.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def self.compile(node, code_id = nil)
2020
# Based on puppet/lib/puppet/parser/compiler.rb
2121
begin
2222
node.environment.check_for_reparse
23-
23+
2424
errors = node.environment.validation_errors
2525
if !errors.empty?
2626
errors.each { |e| Puppet.err(e) } if errors.size > 1
@@ -52,6 +52,7 @@ def self.compile(node, code_id = nil)
5252
raise Puppet::Error, message, detail.backtrace
5353
end
5454
rescue Puppet::ParseErrorWithIssue => detail
55+
# TODO - Potential issue here with 4.10.x not implementing .file on the Positioned class
5556
# Just re-raise if there is no Puppet manifest file associated with the error
5657
raise if detail.file.nil? || detail.line.nil? || detail.pos.nil?
5758
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_exception, [detail])
@@ -88,7 +89,7 @@ def evaluate(target, scope)
8889
result = original_evaluate(target, scope)
8990

9091
PuppetDebugServer::PuppetDebugSession.hooks.exec_hook(:hook_after_pops_evaluate, [self, target, scope])
91-
92+
9293
result
9394
end
9495
end

server/spec/debugserver/fixtures/cache/facts.d/.gitkeep

Whitespace-only changes.

server/spec/debugserver/fixtures/cache/lib/facter/.gitkeep

Whitespace-only changes.

server/spec/debugserver/fixtures/cache/lib/puppet/.gitkeep

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Text fixture puppet.conf which redirects all Puppet configuration into the
2+
# fixtures directory
3+
[main]
4+
environment = test-fixtures
5+
environmentpath = $codedir/environments
6+
codedir = $vardir/..
7+
logdir = $vardir/../logdir

server/spec/debugserver/fixtures/environments/.gitkeep

Whitespace-only changes.

server/spec/debugserver/fixtures/logdir/.gitkeep

Whitespace-only changes.

server/spec/debugserver/spec_debug_helper.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55

66
require 'puppet-debugserver'
77
fixtures_dir = File.join(File.dirname(__FILE__),'fixtures')
8-
8+
# Currently there is no way to re-initialize the puppet loader so for the moment
9+
# all tests must run off the single puppet config settings instead of per example setting
10+
puppet_settings = ['--vardir',File.join(fixtures_dir,'cache'),
11+
'--confdir',File.join(fixtures_dir,'confdir')]
912
PuppetDebugServer::init_puppet(PuppetDebugServer::CommandLineParser.parse([]))
13+
Puppet.initialize_settings(puppet_settings)
1014

1115
# Custom RSpec Matchers
1216

0 commit comments

Comments
 (0)