Skip to content

Commit 817fb03

Browse files
authored
Merge pull request #205 from glennsarti/gh-204-fix-debug-resource
(GH-204) Fix debug server for Puppet 4.x
2 parents 1bce130 + 6740faf commit 817fb03

File tree

33 files changed

+317
-46
lines changed

33 files changed

+317
-46
lines changed

appveyor.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,33 @@ environment:
99
# LTS Agent
1010
- PUPPET_GEM_VERSION: "~> 4.7.0"
1111
RUBY_VER: 21-x64
12-
RAKE_TASK: test
12+
RAKE_TASK: test_languageserver
1313
# Latest 4.x branch
1414
- PUPPET_GEM_VERSION: "~> 4.0"
1515
RUBY_VER: 21-x64
16-
RAKE_TASK: test
16+
RAKE_TASK: test_languageserver
1717
# Latest Puppet
1818
- PUPPET_GEM_VERSION: "~> 5.0"
1919
RUBY_VER: 24-x64
20-
RAKE_TASK: test
20+
RAKE_TASK: test_languageserver
2121
# Specific Puppet version testing
2222
- PUPPET_GEM_VERSION: "5.1.0"
2323
RUBY_VER: 24-x64
24-
RAKE_TASK: test
24+
RAKE_TASK: test_languageserver
25+
26+
# Debug Server testing
27+
# LTS Agent
28+
- PUPPET_GEM_VERSION: "~> 4.7.0"
29+
RUBY_VER: 21-x64
30+
RAKE_TASK: test_debugserver
31+
# Latest 4.x branch
32+
- PUPPET_GEM_VERSION: "~> 4.0"
33+
RUBY_VER: 21-x64
34+
RAKE_TASK: test_debugserver
35+
# Latest Puppet
36+
- PUPPET_GEM_VERSION: "~> 5.0"
37+
RUBY_VER: 24-x64
38+
RAKE_TASK: test_debugserver
2539

2640
# Ruby style
2741
- PUPPET_GEM_VERSION: "~> 4.0"

server/Rakefile

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ require 'rspec/core/rake_task'
33
rubocop_available = Gem::Specification::find_all_by_name('rubocop').any?
44
require 'rubocop/rake_task' if rubocop_available
55

6-
desc 'Run rspec tests with coloring.'
7-
RSpec::Core::RakeTask.new(:test) do |t|
8-
t.rspec_opts = %w[--color --format documentation]
9-
t.pattern = 'spec/'
6+
desc 'Run rspec tests for the Language Server with coloring.'
7+
RSpec::Core::RakeTask.new(:test_languageserver) do |t|
8+
t.rspec_opts = %w[--color --format documentation --default-path spec/languageserver]
9+
t.pattern = 'spec/languageserver'
1010
end
1111

12-
desc 'Run rspec tests and save JUnit output to results.xml.'
13-
RSpec::Core::RakeTask.new(:junit) do |t|
14-
t.rspec_opts = %w[-r yarjuf -f JUnit -o results.xml]
15-
t.pattern = 'spec/'
12+
desc 'Run rspec tests for the Debug Server with coloring.'
13+
RSpec::Core::RakeTask.new(:test_debugserver) do |t|
14+
t.rspec_opts = %w[--color --format documentation --default-path spec/debugserver]
15+
t.pattern = 'spec/debugserver'
1616
end
1717

1818
if rubocop_available
@@ -54,7 +54,7 @@ task :gem_revendor do
5454
gem_list.each do |vendor|
5555
puts "Vendoring #{vendor[:directory]}..."
5656
gem_dir = File.join(vendor_dir,vendor[:directory])
57-
57+
5858
sh "git clone #{vendor[:github_repo]} #{gem_dir}"
5959
Dir.chdir(gem_dir) do
6060
sh 'git fetch origin'

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/hooks.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module PuppetDebugServer
44
# This code was inspired from Puppet-Debugger (https://raw.githubusercontent.com/nwops/puppet-debugger/master/lib/puppet-debugger/hooks.rb) which was borrowed from Pry hooks file
5-
5+
66
# Both puppet-debugger and pry are licensed with MIT
77
# https://raw.githubusercontent.com/nwops/puppet-debugger/master/LICENSE.txt
88
# https://raw.githubusercontent.com/pry/pry/master/LICENSE
@@ -28,7 +28,7 @@ module PuppetDebugServer
2828
# String - Breakpoint full text
2929
# Object - self where the function breakpoint was hit
3030
# Object[] - optional objects
31-
#
31+
#
3232
# :hook_exception
3333
# Fires when a exception is hit
3434
# Arguments:
@@ -58,6 +58,18 @@ module PuppetDebugServer
5858
# Fires after the Puppet::Parser::Functions is reset
5959
# Arguments:
6060
# Puppet::Parser::Functions - Instance of Puppet::Parser::Functions
61+
#
62+
# :hook_before_pops_evaluate
63+
# Fires before an item in the AST is evaluated
64+
# Arguments:
65+
# The Pops object about to be evaluated
66+
# The scope of the Pops object
67+
#
68+
# :hook_after_pops_evaluate
69+
# Fires after an item in the AST is evaluated
70+
# Arguments:
71+
# The Pops object about to be evaluated
72+
# The scope of the Pops object
6173

6274
class Hooks
6375
def initialize
@@ -114,6 +126,7 @@ def add_hook(event_name, hook_name, callable=nil, &block)
114126
# @param [Array] args The arguments to pass to each hook function.
115127
# @return [Object] The return value of the last executed hook.
116128
def exec_hook(event_name, *args, &block)
129+
PuppetDebugServer.log_message(:debug, "Starting to executing hook #{event_name}") unless event_name == :hook_log_message
117130
@hooks[event_name.to_s].map do |hook_name, callable|
118131
begin
119132
callable.call(*args, &block)
@@ -122,6 +135,7 @@ def exec_hook(event_name, *args, &block)
122135
e
123136
end
124137
end.last
138+
PuppetDebugServer.log_message(:debug, "Finished to executing hook #{event_name}") unless event_name == :hook_log_message
125139
end
126140

127141
# @param [Symbol] event_name The name of the event.

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

0 commit comments

Comments
 (0)