puppet-editor-services 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +510 -0
- data/CODEOWNERS +2 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +54 -0
- data/Gemfile +53 -0
- data/LICENSE +201 -0
- data/README.md +308 -0
- data/Rakefile +185 -0
- data/bin/puppet-debugserver +8 -0
- data/bin/puppet-languageserver +7 -0
- data/bin/puppet-languageserver-sidecar +7 -0
- data/lib/dsp/dsp.rb +7 -0
- data/lib/dsp/dsp_base.rb +62 -0
- data/lib/dsp/dsp_protocol.rb +4619 -0
- data/lib/lsp/lsp.rb +10 -0
- data/lib/lsp/lsp_base.rb +63 -0
- data/lib/lsp/lsp_custom.rb +170 -0
- data/lib/lsp/lsp_enums.rb +143 -0
- data/lib/lsp/lsp_protocol.rb +2785 -0
- data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
- data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
- data/lib/lsp/lsp_protocol_configuration.rb +82 -0
- data/lib/lsp/lsp_protocol_declaration.rb +73 -0
- data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
- data/lib/lsp/lsp_protocol_implementation.rb +75 -0
- data/lib/lsp/lsp_protocol_progress.rb +200 -0
- data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
- data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
- data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
- data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
- data/lib/lsp/lsp_types.rb +1534 -0
- data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
- data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
- data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
- data/lib/puppet-debugserver/hooks.rb +132 -0
- data/lib/puppet-debugserver/message_handler.rb +277 -0
- data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
- data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
- data/lib/puppet-languageserver/client_session_state.rb +119 -0
- data/lib/puppet-languageserver/crash_dump.rb +50 -0
- data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
- data/lib/puppet-languageserver/facter_helper.rb +25 -0
- data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
- data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
- data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
- data/lib/puppet-languageserver/global_queues.rb +16 -0
- data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
- data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
- data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
- data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
- data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
- data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
- data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
- data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
- data/lib/puppet-languageserver/message_handler.rb +462 -0
- data/lib/puppet-languageserver/providers.rb +18 -0
- data/lib/puppet-languageserver/puppet_helper.rb +108 -0
- data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
- data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
- data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
- data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
- data/lib/puppet-languageserver/server_capabilities.rb +48 -0
- data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
- data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
- data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
- data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
- data/lib/puppet-languageserver/uri_helper.rb +46 -0
- data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
- data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
- data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
- data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
- data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
- data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
- data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
- data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
- data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
- data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
- data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
- data/lib/puppet_debugserver.rb +164 -0
- data/lib/puppet_editor_services/connection/base.rb +62 -0
- data/lib/puppet_editor_services/connection/stdio.rb +25 -0
- data/lib/puppet_editor_services/connection/tcp.rb +34 -0
- data/lib/puppet_editor_services/handler/base.rb +16 -0
- data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
- data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
- data/lib/puppet_editor_services/logging.rb +45 -0
- data/lib/puppet_editor_services/protocol/base.rb +27 -0
- data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
- data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
- data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
- data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
- data/lib/puppet_editor_services/server/base.rb +42 -0
- data/lib/puppet_editor_services/server/stdio.rb +85 -0
- data/lib/puppet_editor_services/server/tcp.rb +349 -0
- data/lib/puppet_editor_services/server.rb +15 -0
- data/lib/puppet_editor_services/version.rb +36 -0
- data/lib/puppet_editor_services.rb +8 -0
- data/lib/puppet_languageserver.rb +263 -0
- data/lib/puppet_languageserver_sidecar.rb +361 -0
- data/puppet-debugserver +11 -0
- data/puppet-editor-services.gemspec +29 -0
- data/puppet-languageserver +15 -0
- data/puppet-languageserver-sidecar +14 -0
- metadata +240 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PuppetDebugServer
|
|
4
|
+
module DebugSession
|
|
5
|
+
# Manages storing and validating breakpoints for the Debug Session.
|
|
6
|
+
class BreakPoints
|
|
7
|
+
# @param debug_session [PuppetDebugServer::PuppetDebugSession] The debug session to manage the flow for.
|
|
8
|
+
def initialize(debug_session)
|
|
9
|
+
@debug_session = debug_session
|
|
10
|
+
|
|
11
|
+
@breakpoint_mutex = Mutex.new
|
|
12
|
+
@source_breakpoints = {}
|
|
13
|
+
@function_breakpoints = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Takes the arguments for the setBreakpoints request and then validates that the breakpoints requested
|
|
17
|
+
# are valid and exist.
|
|
18
|
+
#
|
|
19
|
+
# @todo Do we care about Breakpoint.Id? That seems to be only required for activating/deactivating breakpoints dynamically.
|
|
20
|
+
# @param arguments [DSP::SetBreakpointsArguments]
|
|
21
|
+
# @return [Array<DSP::Breakpoint>] All of the breakpoints, in the same order as arguments, with validation set correctly.
|
|
22
|
+
def process_set_breakpoints_request!(arguments)
|
|
23
|
+
file_path = File.expand_path(arguments.source.path) # Rub-ify the filepath. Important on Windows platforms.
|
|
24
|
+
file_contents = {}
|
|
25
|
+
|
|
26
|
+
if File.exist?(file_path)
|
|
27
|
+
# Open the file and extact the lines we need
|
|
28
|
+
line_list = arguments.breakpoints.map(&:line) # These are 1-based line numbers
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
# TODO: This could be slow on big files....
|
|
32
|
+
IO.foreach(file_path, mode: 'rb', encoding: 'UTF-8').each_with_index do |item, index|
|
|
33
|
+
# index here zero-based whereas we want one-based indexing
|
|
34
|
+
file_contents[index + 1] = item if line_list.include?(index + 1)
|
|
35
|
+
end
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
PuppetDebugServer.log_message(:error, "Error reading file #{arguments.source.path} for source breakpoints: #{e}")
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
PuppetDebugServer.log_message(:debug, "Unable to set source breakpoints for non-existant file #{arguments.source.path}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Create the initial list of breakpoint responses
|
|
44
|
+
break_points = arguments.breakpoints.map do
|
|
45
|
+
DSP::Breakpoint.new.from_h!(
|
|
46
|
+
'verified' => false,
|
|
47
|
+
'source' => arguments.source.to_h
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The internal list of break points only cares about valid breakpoints
|
|
52
|
+
@breakpoint_mutex.synchronize { @source_breakpoints[canonical_file_path(file_path)] = [] }
|
|
53
|
+
# Verify that each breakpoints is valid
|
|
54
|
+
arguments.breakpoints.each_with_index do |sbp, bp_index|
|
|
55
|
+
line_text = file_contents[sbp.line]
|
|
56
|
+
bp = break_points[bp_index]
|
|
57
|
+
|
|
58
|
+
if line_text.nil?
|
|
59
|
+
bp.message = 'Line does not exist'
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
bp.line = sbp.line
|
|
64
|
+
|
|
65
|
+
# Strip whitespace
|
|
66
|
+
line_text.strip!
|
|
67
|
+
# Strip block comments i.e. ` # something`
|
|
68
|
+
line_text = line_text.partition('#')[0]
|
|
69
|
+
|
|
70
|
+
if line_text.empty?
|
|
71
|
+
bp.message = 'Line is blank'
|
|
72
|
+
else
|
|
73
|
+
bp.verified = true
|
|
74
|
+
@breakpoint_mutex.synchronize { @source_breakpoints[canonical_file_path(file_path)] << bp }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
break_points
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Takes the arguments for the setFunctionBreakpoints request and then validates that the breakpoints requested are valid.
|
|
82
|
+
#
|
|
83
|
+
# @todo Do we care about Breakpoint.Id? That seems to be only required for activating/deactivating breakpoints dynamically.
|
|
84
|
+
# @param arguments [DSP::SetFunctionBreakpointsArguments]
|
|
85
|
+
# @return [Array<DSP::Breakpoint>] All of the breakpoints, in the same order as arguments, with validation set correctly.
|
|
86
|
+
def process_set_function_breakpoints_request!(arguments)
|
|
87
|
+
# Update this internal list of active breakpoints
|
|
88
|
+
@breakpoint_mutex.synchronize do
|
|
89
|
+
@function_breakpoints = arguments.breakpoints
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# All Function breakpoints are considered valid
|
|
93
|
+
arguments.breakpoints.map do
|
|
94
|
+
DSP::Breakpoint.new.from_h!(
|
|
95
|
+
'verified' => true
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns all of the line breakpoints for a given file.
|
|
101
|
+
#
|
|
102
|
+
# @param file_path [String] Absolute path to the file.
|
|
103
|
+
# @return [Array<Integer>] All of the line breakpoints. Returns empty array if there no breakpoints.
|
|
104
|
+
def line_breakpoints(file_path)
|
|
105
|
+
return [] if @source_breakpoints[canonical_file_path(file_path)].nil?
|
|
106
|
+
|
|
107
|
+
@source_breakpoints[canonical_file_path(file_path)].map(&:line)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns all of the function names that should break on.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<String>] All of the function names that the debugger should break on
|
|
113
|
+
def function_breakpoint_names
|
|
114
|
+
result = @function_breakpoints.map(&:name)
|
|
115
|
+
# Also add the debug::break function which mimics puppet-debug behaviour
|
|
116
|
+
# https://github.com/nwops/puppet-debug#usage
|
|
117
|
+
result << 'debug::break'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Returns unique, canonical name for a file path, regardless of Operating System.
|
|
123
|
+
#
|
|
124
|
+
# @param file_path [String] The path to canonicalise.
|
|
125
|
+
# @return [String] All of the function names that the debugger should break on.
|
|
126
|
+
# @private
|
|
127
|
+
def canonical_file_path(file_path)
|
|
128
|
+
# This could be a little dangerous. The paths that come from the editor are URIs, and may or may not always
|
|
129
|
+
# represent their actual filename on disk e.g. case-insensitive file systems. So a quick and dirty way to
|
|
130
|
+
# reconcile this is just to always use lowercase file paths. While this works ok on Windows (NTFS or FAT)
|
|
131
|
+
# other operating systems, could, in theory have two manifests being debugged that only differ by case. This
|
|
132
|
+
# is not recommend as it breaks cross platform editing, but it's still possible
|
|
133
|
+
file_path.downcase
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PuppetDebugServer
|
|
4
|
+
module DebugSession
|
|
5
|
+
# Manages the flags used to control the flow of puppet and Debugger during a debug session.
|
|
6
|
+
class FlowControl
|
|
7
|
+
# What mode the debug session is running in
|
|
8
|
+
# @see PuppetDebugServer::DebugSession::PuppetSessionRunMode
|
|
9
|
+
# @return [PuppetDebugServer::DebugSession::PuppetSessionRunMode]
|
|
10
|
+
attr_reader :run_mode
|
|
11
|
+
|
|
12
|
+
# @param debug_session [PuppetDebugServer::PuppetDebugSession] The debug session to manage the flow for.
|
|
13
|
+
def initialize(debug_session)
|
|
14
|
+
@debug_session = debug_session
|
|
15
|
+
|
|
16
|
+
@flag_mutex = Mutex.new
|
|
17
|
+
@flags = {
|
|
18
|
+
start_puppet: false,
|
|
19
|
+
puppet_started: false,
|
|
20
|
+
session_paused: false,
|
|
21
|
+
client_completed_configuration: false,
|
|
22
|
+
session_setup: false,
|
|
23
|
+
terminate: false,
|
|
24
|
+
suppress_log_messages: false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@run_mode = PuppetDebugServer::DebugSession::PuppetSessionRunMode.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns which flags are set.
|
|
31
|
+
#
|
|
32
|
+
# Available flags
|
|
33
|
+
# :start_puppet Indicates the main thread can start running Puppet and begin the debug session
|
|
34
|
+
# :puppet_started Indicates the main thread has started running puppet and debug session is now active
|
|
35
|
+
# :session_paused Indicates that the debug session has hit a breakpoint and is currently paused
|
|
36
|
+
# :client_completed_configuration The debug client has completed it's configuration
|
|
37
|
+
# :session_setup This debug session has been setup ready to start
|
|
38
|
+
# :terminate Indicates that all threads and wait processes should terminate
|
|
39
|
+
# :suppress_log_messages Indicates that Puppet log messages should not be sent to the client
|
|
40
|
+
#
|
|
41
|
+
# @param flag_name [Symbol] The name of the flag
|
|
42
|
+
# @return [Boolean] Whether the flag is set
|
|
43
|
+
def flag?(flag_name)
|
|
44
|
+
result = false
|
|
45
|
+
@flag_mutex.synchronize do
|
|
46
|
+
result = @flags[flag_name]
|
|
47
|
+
end
|
|
48
|
+
result.nil? ? false : result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Asserts a flag is set
|
|
52
|
+
#
|
|
53
|
+
# @param flag_name [Symbol] The name of the flag
|
|
54
|
+
def assert_flag(flag_name)
|
|
55
|
+
@flag_mutex.synchronize do
|
|
56
|
+
@flags[flag_name] = true
|
|
57
|
+
PuppetDebugServer.log_message(:debug, "Asserting flag #{flag_name} is true")
|
|
58
|
+
# Any custom logic for when flags are asserted
|
|
59
|
+
# rubocop:disable Style/MultipleComparison, Style/SoleNestedConditional This is faster and doesn't require creation of an array
|
|
60
|
+
if flag_name == :client_completed_configuration || flag_name == :session_setup
|
|
61
|
+
# If the client_completed_configuration and session_setup flag are asserted but the session isn't active yet
|
|
62
|
+
# assert the flag start_puppet so puppet can start in the main thread.
|
|
63
|
+
if !@flags[:puppet_started] && @flags[:client_completed_configuration] && @flags[:session_setup]
|
|
64
|
+
PuppetDebugServer.log_message(:debug, 'Asserting flag start_puppet is true')
|
|
65
|
+
@flags[:start_puppet] = true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
@terminate_flag = true if flag_name == :terminate
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
# rubocop:enable Style/MultipleComparison, Style/SoleNestedConditional
|
|
72
|
+
|
|
73
|
+
# Removes/unasserts a flag
|
|
74
|
+
#
|
|
75
|
+
# @param flag_name [Symbol] The name of the flag
|
|
76
|
+
def unassert_flag(flag_name)
|
|
77
|
+
return if flag_name == :terminate # Can never unset the terminate flag
|
|
78
|
+
|
|
79
|
+
@flag_mutex.synchronize do
|
|
80
|
+
@flags[flag_name] = false
|
|
81
|
+
PuppetDebugServer.log_message(:debug, "Unasserting flag #{flag_name} is true")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The terminate flag will be queried quite often during spin-wait cycles and it's basically immutable (i.e. Once set it can not be unset).
|
|
86
|
+
# So to help speed up access just treat it as a normal boolean. This can also stop any deadlocks.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] Whether the debug session should be terminating.
|
|
89
|
+
def terminate?
|
|
90
|
+
@terminate_flag
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether the debug session has started.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] Returns true if the debug session has started.
|
|
96
|
+
def session_active?
|
|
97
|
+
flag?(:puppet_started)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Whether the debug session is paused due to breakpoints.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] Returns true if the debug session is paused.
|
|
103
|
+
def session_paused?
|
|
104
|
+
flag?(:session_paused)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Raises a stopped event to the Debug Client and waits for the debug session to continue.
|
|
108
|
+
#
|
|
109
|
+
# @param reason [String] The reason for the event. Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', 'function breakpoint', 'data breakpoint'.
|
|
110
|
+
# @param description [String] The full reason for the event, e.g. 'Paused on exception'. This string is shown in the UI as is and must be translated.
|
|
111
|
+
# @param text [String] Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI.
|
|
112
|
+
# @param session_state [Hash] Additional information about the puppet state when the event is raised. See PuppetSessionState::Saved.
|
|
113
|
+
def raise_stopped_event_and_wait(reason, description, text, session_state)
|
|
114
|
+
# Signal a stop event
|
|
115
|
+
assert_flag(:session_paused)
|
|
116
|
+
|
|
117
|
+
# Save the state so when the client queries us, we can respond.
|
|
118
|
+
@debug_session.puppet_session_state.saved.update!(session_state)
|
|
119
|
+
|
|
120
|
+
@debug_session.send_stopped_event(
|
|
121
|
+
reason,
|
|
122
|
+
'description' => description,
|
|
123
|
+
'text' => text,
|
|
124
|
+
'threadId' => @debug_session.puppet_thread_id
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Spin-wait for the session to be unpaused...
|
|
128
|
+
# TODO - Could be better. Semaphore maybe?
|
|
129
|
+
sleep(0.5) while flag?(:session_paused) && !terminate?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Continues a paused debug session
|
|
133
|
+
def continue!
|
|
134
|
+
run_mode.run!
|
|
135
|
+
@debug_session.puppet_session_state.saved.clear!
|
|
136
|
+
unassert_flag(:session_paused)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Next steps through a paused debug session
|
|
140
|
+
def next!
|
|
141
|
+
run_mode.next!(@debug_session.puppet_session_state.saved.pops_depth_level)
|
|
142
|
+
@debug_session.puppet_session_state.saved.clear!
|
|
143
|
+
unassert_flag(:session_paused)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Steps into a paused debug session
|
|
147
|
+
def step_in!
|
|
148
|
+
run_mode.step_in!
|
|
149
|
+
@debug_session.puppet_session_state.saved.clear!
|
|
150
|
+
unassert_flag(:session_paused)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Steps out of a paused debug session
|
|
154
|
+
def step_out!
|
|
155
|
+
run_mode.step_out!(@debug_session.puppet_session_state.saved.pops_depth_level)
|
|
156
|
+
@debug_session.puppet_session_state.saved.clear!
|
|
157
|
+
unassert_flag(:session_paused)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PuppetDebugServer
|
|
4
|
+
module DebugSession
|
|
5
|
+
# Implements the hooks within the debug session.
|
|
6
|
+
#
|
|
7
|
+
# @todo The following hooks are not implemented
|
|
8
|
+
# :hook_after_compile
|
|
9
|
+
# Fires after a catalog compilation is succesfully completed
|
|
10
|
+
# Arguments:
|
|
11
|
+
# Puppet::Resource::Catalog - Resultant compiled catalog
|
|
12
|
+
#
|
|
13
|
+
# :hook_before_parser_function_reset
|
|
14
|
+
# Fires before the Puppet::Parser::Functions is reset, destroying the existing list of loaded functions
|
|
15
|
+
# Arguments:
|
|
16
|
+
# Puppet::Parser::Functions - Instance of Puppet::Parser::Functions
|
|
17
|
+
#
|
|
18
|
+
class HookHandlers
|
|
19
|
+
# List of Puppet POPS classes that the Source Breakpoints will NOT trigger on
|
|
20
|
+
EXCLUDED_CLASSES = %w[BlockExpression HostClassDefinition].freeze
|
|
21
|
+
|
|
22
|
+
# @param debug_session [PuppetDebugServer::PuppetDebugSession] The debug session to manage the hooks for.
|
|
23
|
+
def initialize(debug_session)
|
|
24
|
+
@debug_session = debug_session
|
|
25
|
+
|
|
26
|
+
@debug_session.hook_manager.add_hook(:hook_after_parser_function_reset, :debug_session) { |args| on_hook_after_parser_function_reset(args) }
|
|
27
|
+
@debug_session.hook_manager.add_hook(:hook_after_pops_evaluate, :debug_session) { |args| on_hook_after_pops_evaluate(args) }
|
|
28
|
+
@debug_session.hook_manager.add_hook(:hook_before_apply_exit, :debug_session) { |args| on_hook_before_apply_exit(args) }
|
|
29
|
+
@debug_session.hook_manager.add_hook(:hook_before_compile, :debug_session) { |args| on_hook_before_compile(args) }
|
|
30
|
+
@debug_session.hook_manager.add_hook(:hook_before_pops_evaluate, :debug_session) { |args| on_hook_before_pops_evaluate(args) }
|
|
31
|
+
@debug_session.hook_manager.add_hook(:hook_breakpoint, :debug_session) { |args| on_hook_breakpoint(args) }
|
|
32
|
+
@debug_session.hook_manager.add_hook(:hook_exception, :debug_session) { |args| on_hook_exception(args) }
|
|
33
|
+
@debug_session.hook_manager.add_hook(:hook_function_breakpoint, :debug_session) { |args| on_hook_function_breakpoint(args) }
|
|
34
|
+
@debug_session.hook_manager.add_hook(:hook_log_message, :debug_session) { |args| on_hook_log_message(args) }
|
|
35
|
+
@debug_session.hook_manager.add_hook(:hook_step_breakpoint, :debug_session) { |args| on_hook_step_breakpoint(args) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Fires after the Puppet::Parser::Functions class is reset
|
|
39
|
+
# Arguments:
|
|
40
|
+
# Puppet::Parser::Functions - Instance of Puppet::Parser::Functions
|
|
41
|
+
def on_hook_after_parser_function_reset(args)
|
|
42
|
+
func_object = args[0]
|
|
43
|
+
|
|
44
|
+
# This mimics the break function from puppet-debugger
|
|
45
|
+
# https://github.com/nwops/puppet-debug#usage
|
|
46
|
+
func_object.newfunction(:'debug::break', type: :rvalue, arity: -1, doc: 'Breakpoint Function') do |arguments|
|
|
47
|
+
# This function is just a place holder. It gets interpretted at the
|
|
48
|
+
# pops_evaluate hooks but the function itself still needs to exist though.
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fires after an item in the AST is evaluated
|
|
53
|
+
# Arguments:
|
|
54
|
+
# The Pops object about to be evaluated
|
|
55
|
+
# The scope of the Pops object
|
|
56
|
+
def on_hook_after_pops_evaluate(_args)
|
|
57
|
+
# If the debug session is paused no need to process
|
|
58
|
+
return if @debug_session.flow_control.session_paused?
|
|
59
|
+
|
|
60
|
+
@debug_session.puppet_session_state.actual.decrement_pops_depth
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fires before the Puppet::Apply application tries to call Kernel#exit.
|
|
64
|
+
# Arguments:
|
|
65
|
+
# Integer - Exit Code
|
|
66
|
+
def on_hook_before_apply_exit(args)
|
|
67
|
+
option = args[0]
|
|
68
|
+
|
|
69
|
+
@debug_session.send_exited_event(option)
|
|
70
|
+
@debug_session.send_output_event(
|
|
71
|
+
'category' => 'console',
|
|
72
|
+
'output' => "puppet exited with #{option}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@debug_session.flow_control.unassert_flag(:puppet_started)
|
|
76
|
+
@debug_session.close
|
|
77
|
+
|
|
78
|
+
# Wait up to 30 seconds for the client to disconnect and stop the debug session
|
|
79
|
+
# Anymore than that and we force the debug session to stop.
|
|
80
|
+
sleep(30)
|
|
81
|
+
@debug_session.force_terminate
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Fires before a catalog compilation is attempted
|
|
85
|
+
# Arguments:
|
|
86
|
+
# Puppet::Parser::Compiler - Current compiler in use
|
|
87
|
+
def on_hook_before_compile(args)
|
|
88
|
+
@debug_session.puppet_session_state.actual.update_compiler(args[0])
|
|
89
|
+
|
|
90
|
+
# Spin-wait for the configurationDone message from the client before we continue compilation
|
|
91
|
+
return if @debug_session.flow_control.flag?(:client_completed_configuration)
|
|
92
|
+
|
|
93
|
+
sleep(0.5) until @debug_session.flow_control.flag?(:client_completed_configuration)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Fires before an item in the AST is evaluated during a catalog compilation
|
|
97
|
+
# Arguments:
|
|
98
|
+
# The Pops object about to be evaluated
|
|
99
|
+
# The scope of the Pops object
|
|
100
|
+
def on_hook_before_pops_evaluate(args)
|
|
101
|
+
# If the debug session is paused no need to process
|
|
102
|
+
return if @debug_session.flow_control.session_paused?
|
|
103
|
+
|
|
104
|
+
@debug_session.puppet_session_state.actual.increment_pops_depth
|
|
105
|
+
|
|
106
|
+
target = args[1]
|
|
107
|
+
# Ignore this if there is no positioning information available
|
|
108
|
+
return unless target.is_a?(Puppet::Pops::Model::Positioned)
|
|
109
|
+
|
|
110
|
+
target_loc = @debug_session.get_location_from_pops_object(target)
|
|
111
|
+
|
|
112
|
+
# Even if it's positioned, it can still contain invalid information. Ignore it if
|
|
113
|
+
# it's missing required information. This can happen when evaluting strings (e.g. watches from VSCode)
|
|
114
|
+
# i.e. not a file on disk
|
|
115
|
+
return if target_loc.file.nil? || target_loc.file.empty?
|
|
116
|
+
|
|
117
|
+
target_classname = @debug_session.get_puppet_class_name(target)
|
|
118
|
+
ast_classname = get_ast_class_name(target)
|
|
119
|
+
|
|
120
|
+
# Break if we hit a specific puppet function
|
|
121
|
+
if target_classname == 'CallNamedFunctionExpression' && @debug_session.breakpoints.function_breakpoint_names.include?(target.functor_expr.value)
|
|
122
|
+
# Re-raise the hook as a breakpoint
|
|
123
|
+
@debug_session.execute_hook(:hook_function_breakpoint, [target.functor_expr.value, ast_classname] + args)
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check for Source based breakpoints
|
|
128
|
+
unless target_loc.length.zero? || EXCLUDED_CLASSES.include?(target_classname)
|
|
129
|
+
line_breakpoints = @debug_session.breakpoints.line_breakpoints(target_loc.file)
|
|
130
|
+
|
|
131
|
+
# Calculate the start and end lines of the target
|
|
132
|
+
target_start_line = target_loc.line
|
|
133
|
+
target_end_line = @debug_session.line_for_offset(target, target_loc.offset + target_loc.length)
|
|
134
|
+
|
|
135
|
+
# TODO: What about Hit and Conditional BreakPoints?
|
|
136
|
+
bp = line_breakpoints.find_index { |bp_line| bp_line >= target_start_line && bp_line <= target_end_line }
|
|
137
|
+
unless bp.nil?
|
|
138
|
+
# Re-raise the hook as a breakpoint
|
|
139
|
+
@debug_session.execute_hook(:hook_breakpoint, [ast_classname, ''] + args)
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Break if we are stepping
|
|
145
|
+
case @debug_session.flow_control.run_mode.mode
|
|
146
|
+
when :stepin
|
|
147
|
+
# Stepping-in is basically break on everything
|
|
148
|
+
# Re-raise the hook as a step breakpoint
|
|
149
|
+
@debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
|
|
150
|
+
when :next
|
|
151
|
+
# Next will break on anything at this Pop depth or shallower than this Pop depth. Re-raise the hook as a step breakpoint
|
|
152
|
+
depth = @debug_session.flow_control.run_mode.options[:pops_depth_level] || -1
|
|
153
|
+
if @debug_session.puppet_session_state.actual.pops_depth_level <= depth # rubocop:disable Style/IfUnlessModifier
|
|
154
|
+
@debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
|
|
155
|
+
end
|
|
156
|
+
when :stepout
|
|
157
|
+
# Stepping-Out will break on anything shallower than this Pop depth. Re-raise the hook as a step breakpoint
|
|
158
|
+
depth = @debug_session.flow_control.run_mode.options[:pops_depth_level] || -1
|
|
159
|
+
if @debug_session.puppet_session_state.actual.pops_depth_level < depth # rubocop:disable Style/IfUnlessModifier
|
|
160
|
+
@debug_session.execute_hook(:hook_step_breakpoint, [ast_classname, ''] + args)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Fires when a source/line breakpoint is hit
|
|
167
|
+
# Arguments:
|
|
168
|
+
# String - Breakpoint display text
|
|
169
|
+
# String - Breakpoint full text
|
|
170
|
+
# Object - self where the breakpoint was hit
|
|
171
|
+
# Object[] - optional objects
|
|
172
|
+
def on_hook_breakpoint(args)
|
|
173
|
+
process_breakpoint_hook('breakpoint', args)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Fires when an unhandled exception is hit during puppet apply
|
|
177
|
+
# Arguments:
|
|
178
|
+
# Error - The exception information
|
|
179
|
+
def on_hook_exception(args)
|
|
180
|
+
# If the debug session is paused, can't raise a new exception
|
|
181
|
+
return if @debug_session.flow_control.session_paused?
|
|
182
|
+
|
|
183
|
+
error_detail = args[0]
|
|
184
|
+
|
|
185
|
+
@debug_session.flow_control.raise_stopped_event_and_wait(
|
|
186
|
+
'exception',
|
|
187
|
+
'Compilation Exception',
|
|
188
|
+
error_detail.basic_message,
|
|
189
|
+
session_exception: error_detail,
|
|
190
|
+
puppet_stacktrace: Puppet::Pops::PuppetStack.stacktrace_from_backtrace(error_detail)
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Fires when a function breakpoint is hit
|
|
195
|
+
# Arguments:
|
|
196
|
+
# String - Breakpoint display text
|
|
197
|
+
# String - Breakpoint full text
|
|
198
|
+
# Object - self where the function breakpoint was hit
|
|
199
|
+
# Object[] - optional objects
|
|
200
|
+
def on_hook_function_breakpoint(args)
|
|
201
|
+
process_breakpoint_hook('function breakpoint', args)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Fires when a message is sent to the puppet logger
|
|
205
|
+
# Arguments:
|
|
206
|
+
# Message - The message being sent to the log
|
|
207
|
+
def on_hook_log_message(args)
|
|
208
|
+
return if @debug_session.flow_control.flag?(:suppress_log_messages)
|
|
209
|
+
|
|
210
|
+
msg = args[0]
|
|
211
|
+
str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
|
|
212
|
+
str = "#{msg.source}: #{str}" unless msg.source == 'Puppet'
|
|
213
|
+
|
|
214
|
+
level = msg.level.to_s.capitalize
|
|
215
|
+
|
|
216
|
+
category = 'stderr'
|
|
217
|
+
category = 'stdout' if msg.level == :notice || msg.level == :info || msg.level == :debug
|
|
218
|
+
|
|
219
|
+
@debug_session.send_output_event(
|
|
220
|
+
'category' => category,
|
|
221
|
+
'output' => "#{level}: #{str}\n"
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Fires when a function breakpoint is hit
|
|
226
|
+
# Arguments:
|
|
227
|
+
# String - Breakpoint display text
|
|
228
|
+
# String - Breakpoint full text
|
|
229
|
+
# Object - self where the step breakpoint was hit
|
|
230
|
+
# Object[] - optional objects
|
|
231
|
+
def on_hook_step_breakpoint(args)
|
|
232
|
+
process_breakpoint_hook('step', args)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
# Raises a stop event and waits for the debug session to continue for a given breakpoint type.
|
|
238
|
+
#
|
|
239
|
+
# @param reason [String] The type of breakpoint that was hit
|
|
240
|
+
# @param args [Array<Object>] An array of arguments for the breakpoint
|
|
241
|
+
# @private
|
|
242
|
+
def process_breakpoint_hook(reason, args)
|
|
243
|
+
# If the debug session is paused no need to process
|
|
244
|
+
return if @debug_session.flow_control.session_paused?
|
|
245
|
+
|
|
246
|
+
break_display_text = args[0] # TODO: REALLY don't like all this magic array stuff. Real Object? Hash?
|
|
247
|
+
break_description = args[1]
|
|
248
|
+
|
|
249
|
+
scope_object = nil
|
|
250
|
+
pops_target_object = nil
|
|
251
|
+
pops_depth_level = nil
|
|
252
|
+
|
|
253
|
+
# Check if the breakpoint came from the Pops::Evaluator
|
|
254
|
+
if args[2].is_a?(Puppet::Pops::Evaluator::EvaluatorImpl)
|
|
255
|
+
pops_target_object = args[3]
|
|
256
|
+
scope_object = args[4]
|
|
257
|
+
pops_depth_level = @debug_session.puppet_session_state.actual.pops_depth_level
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
break_description = break_display_text if break_description.empty?
|
|
261
|
+
# Due to a modification to the way stack traces are treated in Puppet 6.11.0, the stack
|
|
262
|
+
# now includes entries for files in Line 0, which doesn't exist. These indicate that a file
|
|
263
|
+
# has started to be processed/parsed/compiled. So we just ignore them
|
|
264
|
+
# See https://tickets.puppetlabs.com/browse/PUP-10150 for more infomation
|
|
265
|
+
stack_trace = Puppet::Pops::PuppetStack.stacktrace.reject { |item| item[1].zero? }
|
|
266
|
+
# Due to https://github.com/puppetlabs/puppet/commit/0f96dd918b6184261bc2219e5e68e246ffbeac10
|
|
267
|
+
# Prior to Puppet 4.8.0, stacktrace is in reverse order
|
|
268
|
+
stack_trace.reverse! if Gem::Version.new(Puppet.version) < Gem::Version.new('4.8.0')
|
|
269
|
+
|
|
270
|
+
@debug_session.flow_control.raise_stopped_event_and_wait(
|
|
271
|
+
reason,
|
|
272
|
+
break_display_text,
|
|
273
|
+
break_description,
|
|
274
|
+
pops_target: pops_target_object,
|
|
275
|
+
scope: scope_object,
|
|
276
|
+
pops_depth_level: pops_depth_level,
|
|
277
|
+
puppet_stacktrace: stack_trace
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Retrieves the name of a Puppet AST object for Puppet 5+ and Puppet 4.x.
|
|
282
|
+
#
|
|
283
|
+
# @param obj [Object] The Puppet POPS object.
|
|
284
|
+
# @return [String] Then class name of the object
|
|
285
|
+
# @private
|
|
286
|
+
def get_ast_class_name(obj)
|
|
287
|
+
# Puppet 5 has PCore Types
|
|
288
|
+
return obj._pcore_type.name if obj.respond_to?(:_pcore_type)
|
|
289
|
+
|
|
290
|
+
# .. otherwise revert to Pops classname
|
|
291
|
+
obj.class.to_s
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PuppetDebugServer
|
|
4
|
+
module DebugSession
|
|
5
|
+
# The run mode and configuration for a Debug Session.
|
|
6
|
+
class PuppetSessionRunMode
|
|
7
|
+
# The run mode fo the debug session. Either run, stepin, next, or stepout
|
|
8
|
+
# @return [Symbol]
|
|
9
|
+
attr_accessor :mode
|
|
10
|
+
|
|
11
|
+
# Any options associated with the current mode.
|
|
12
|
+
# @option options [Integer] :pops_depth_level The depth of the AST object where the mode was initiated from.
|
|
13
|
+
# @return [Hash]
|
|
14
|
+
attr_accessor :options
|
|
15
|
+
|
|
16
|
+
# @param mode See mode. Default is run
|
|
17
|
+
# @param options See options
|
|
18
|
+
def initialize(mode = :run, options = {})
|
|
19
|
+
raise "Invalid mode #{mode}" unless %i[run stepin next stepout].include?(mode)
|
|
20
|
+
|
|
21
|
+
@mode = mode
|
|
22
|
+
@options = options
|
|
23
|
+
|
|
24
|
+
@run_mode_mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Configures the run_mode for "continue until a breakpoint is hit.
|
|
28
|
+
def run!
|
|
29
|
+
@run_mode_mutex.synchronize do
|
|
30
|
+
@mode = :run
|
|
31
|
+
@options = {}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Configures the run_mode for "next"-ing through a debug session.
|
|
36
|
+
# @param pops_depth_level [Integer] The depth of the AST object where the next command was initiated from.
|
|
37
|
+
def next!(pops_depth_level)
|
|
38
|
+
@run_mode_mutex.synchronize do
|
|
39
|
+
@mode = :next
|
|
40
|
+
@options = {
|
|
41
|
+
pops_depth_level: pops_depth_level
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Configures the run_mode for "stepping in" a debug session.
|
|
47
|
+
def step_in!
|
|
48
|
+
@run_mode_mutex.synchronize do
|
|
49
|
+
@mode = :stepin
|
|
50
|
+
@options = {}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Configures the run_mode for "stepping out" a debug session.
|
|
55
|
+
# @param pops_depth_level [Integer] The depth of the AST object where the step put command was initiated from.
|
|
56
|
+
def step_out!(pops_depth_level)
|
|
57
|
+
@run_mode_mutex.synchronize do
|
|
58
|
+
@mode = :stepout
|
|
59
|
+
@options = {
|
|
60
|
+
pops_depth_level: pops_depth_level
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|