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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +510 -0
  3. data/CODEOWNERS +2 -0
  4. data/CODE_OF_CONDUCT.md +46 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/Gemfile +53 -0
  7. data/LICENSE +201 -0
  8. data/README.md +308 -0
  9. data/Rakefile +185 -0
  10. data/bin/puppet-debugserver +8 -0
  11. data/bin/puppet-languageserver +7 -0
  12. data/bin/puppet-languageserver-sidecar +7 -0
  13. data/lib/dsp/dsp.rb +7 -0
  14. data/lib/dsp/dsp_base.rb +62 -0
  15. data/lib/dsp/dsp_protocol.rb +4619 -0
  16. data/lib/lsp/lsp.rb +10 -0
  17. data/lib/lsp/lsp_base.rb +63 -0
  18. data/lib/lsp/lsp_custom.rb +170 -0
  19. data/lib/lsp/lsp_enums.rb +143 -0
  20. data/lib/lsp/lsp_protocol.rb +2785 -0
  21. data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
  22. data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
  23. data/lib/lsp/lsp_protocol_configuration.rb +82 -0
  24. data/lib/lsp/lsp_protocol_declaration.rb +73 -0
  25. data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
  26. data/lib/lsp/lsp_protocol_implementation.rb +75 -0
  27. data/lib/lsp/lsp_protocol_progress.rb +200 -0
  28. data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
  29. data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
  30. data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
  31. data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
  32. data/lib/lsp/lsp_types.rb +1534 -0
  33. data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
  34. data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
  35. data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
  36. data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
  37. data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
  38. data/lib/puppet-debugserver/hooks.rb +132 -0
  39. data/lib/puppet-debugserver/message_handler.rb +277 -0
  40. data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
  41. data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
  42. data/lib/puppet-languageserver/client_session_state.rb +119 -0
  43. data/lib/puppet-languageserver/crash_dump.rb +50 -0
  44. data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
  45. data/lib/puppet-languageserver/facter_helper.rb +25 -0
  46. data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
  47. data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
  48. data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
  49. data/lib/puppet-languageserver/global_queues.rb +16 -0
  50. data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
  51. data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
  52. data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
  53. data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
  54. data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
  55. data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
  56. data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
  57. data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
  58. data/lib/puppet-languageserver/message_handler.rb +462 -0
  59. data/lib/puppet-languageserver/providers.rb +18 -0
  60. data/lib/puppet-languageserver/puppet_helper.rb +108 -0
  61. data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
  62. data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
  63. data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
  64. data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
  65. data/lib/puppet-languageserver/server_capabilities.rb +48 -0
  66. data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
  67. data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
  68. data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
  69. data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
  70. data/lib/puppet-languageserver/uri_helper.rb +46 -0
  71. data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
  72. data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
  73. data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
  74. data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
  75. data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
  76. data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
  77. data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
  78. data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
  79. data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
  80. data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
  81. data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
  82. data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
  83. data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
  84. data/lib/puppet_debugserver.rb +164 -0
  85. data/lib/puppet_editor_services/connection/base.rb +62 -0
  86. data/lib/puppet_editor_services/connection/stdio.rb +25 -0
  87. data/lib/puppet_editor_services/connection/tcp.rb +34 -0
  88. data/lib/puppet_editor_services/handler/base.rb +16 -0
  89. data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
  90. data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
  91. data/lib/puppet_editor_services/logging.rb +45 -0
  92. data/lib/puppet_editor_services/protocol/base.rb +27 -0
  93. data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
  94. data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
  95. data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
  96. data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
  97. data/lib/puppet_editor_services/server/base.rb +42 -0
  98. data/lib/puppet_editor_services/server/stdio.rb +85 -0
  99. data/lib/puppet_editor_services/server/tcp.rb +349 -0
  100. data/lib/puppet_editor_services/server.rb +15 -0
  101. data/lib/puppet_editor_services/version.rb +36 -0
  102. data/lib/puppet_editor_services.rb +8 -0
  103. data/lib/puppet_languageserver.rb +263 -0
  104. data/lib/puppet_languageserver_sidecar.rb +361 -0
  105. data/puppet-debugserver +11 -0
  106. data/puppet-editor-services.gemspec +29 -0
  107. data/puppet-languageserver +15 -0
  108. data/puppet-languageserver-sidecar +14 -0
  109. 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