hyperprobe-agent 1.2.27.pre.3-java

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.
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+
5
+ module HyperProbe
6
+ module Core
7
+ # Installation-time analysis only. Retain the result on the probe, not a
8
+ # binding. nil means unknown: callers must omit locals, not fall back to all.
9
+ # Tokenless lines and lines shared by different scopes are intentionally
10
+ # unknown. Source must still match the code loaded by the application.
11
+ # Parsers with incomplete lambda ASTs (including JRuby 10.1.1's arrow
12
+ # lambdas) fail closed; ordinary lambda/proc blocks remain supported.
13
+ class LexicalScope
14
+ MAX_SOURCE_BYTES = 512 * 1024
15
+ MAX_NODES = 20_000
16
+ MAX_DEPTH = 128
17
+ CLASS = Object.instance_method(:class)
18
+ Scope = Struct.new(:parent, :declared, :assigned, :locals, :visible)
19
+ private_constant :CLASS, :Scope
20
+
21
+ def self.locals_for(path, line)
22
+ return nil unless CLASS.bind(path).call.equal?(String) && CLASS.bind(line).call.equal?(Integer)
23
+ return nil unless line.positive?
24
+ return nil unless File.file?(path)
25
+
26
+ source = File.open(path, 'rb') do |file|
27
+ return nil unless file.stat.file? && file.stat.size <= MAX_SOURCE_BYTES
28
+ file.read(MAX_SOURCE_BYTES + 1)
29
+ end
30
+ return nil if source.bytesize > MAX_SOURCE_BYTES
31
+
32
+ new(source).locals_for(line)
33
+ rescue StandardError, SystemStackError
34
+ nil
35
+ end
36
+
37
+ def initialize(source)
38
+ tree = Ripper.sexp(source)
39
+ raise ArgumentError, 'Invalid Ruby source' unless tree
40
+
41
+ # Check iteratively before any recursive AST work (including positions).
42
+ stack = [[tree, 0]]
43
+ count = 0
44
+ until stack.empty?
45
+ node, depth = stack.pop
46
+ count += 1
47
+ raise ArgumentError, 'AST limit' if count > MAX_NODES || depth > MAX_DEPTH
48
+ node.each { |child| stack << [child, depth + 1] if child.is_a?(Array) }
49
+ end
50
+
51
+ @lambda_headers = lambda_headers(source)
52
+ @lambda_index = 0
53
+ @scopes = []
54
+ @lines = {}
55
+ @unknown_lines = {}
56
+ walk(tree, scope(nil))
57
+ raise ArgumentError, 'Unmatched lambda' unless @lambda_index == @lambda_headers.length
58
+
59
+ # Ancestors are complete before descendants, including assignments
60
+ # textually after a block. Such names are conservatively kept private.
61
+ @scopes.each do |current|
62
+ ancestors = current.parent ? current.parent.visible : []
63
+ current.locals = (current.declared + (current.assigned - ancestors)).uniq.freeze
64
+ current.visible = (ancestors + current.locals).uniq
65
+ end
66
+ end
67
+
68
+ def locals_for(line)
69
+ return nil if @unknown_lines[line]
70
+
71
+ owners = @lines[line]
72
+ owners && owners.length == 1 ? owners.first.locals.dup : nil
73
+ end
74
+
75
+ private
76
+
77
+ def scope(parent)
78
+ current = Scope.new(parent, [], [], nil, nil)
79
+ @scopes << current
80
+ current
81
+ end
82
+
83
+ def walk(node, current)
84
+ return unless node.is_a?(Array)
85
+
86
+ kind = node[0]
87
+ if kind.is_a?(Symbol) && kind.to_s.start_with?('@')
88
+ first = node[2][0]
89
+ last = first + node[1].count("\n")
90
+ last -= 1 if node[1].end_with?("\n")
91
+ (first..last).each do |line|
92
+ owners = (@lines[line] ||= [])
93
+ owners << current unless owners.any? { |owner| owner.equal?(current) }
94
+ end
95
+ return
96
+ end
97
+
98
+ case kind
99
+ when :def, :defs
100
+ # A line alone cannot distinguish definition from default evaluation.
101
+ header, params, body = kind == :def ? [node[1..2], node[2], node[3]] : [node[1..4], node[4], node[5]]
102
+ token_positions(header).each { |position| @unknown_lines[position[0]] = true }
103
+ method_scope = scope(nil)
104
+ parameters(params, method_scope)
105
+ walk(node[1], current) if kind == :defs
106
+ walk(params, method_scope)
107
+ walk(body, method_scope)
108
+ when :class, :module, :sclass
109
+ inner = scope(nil)
110
+ token_positions(node[1...-1]).each { |position| @unknown_lines[position[0]] = true }
111
+ walk(node[1...-1], current)
112
+ walk(node[-1], inner)
113
+ when :do_block, :brace_block, :lambda
114
+ inner = scope(current)
115
+ parameters(node[1], inner)
116
+ if kind == :lambda
117
+ raise ArgumentError, 'Incomplete lambda AST' unless node[1].is_a?(Array)
118
+
119
+ header = @lambda_headers.fetch(@lambda_index)
120
+ @lambda_index += 1
121
+ inner.declared.concat(header[:locals])
122
+ # Ripper leaves lambda semicolon locals out of sexp entirely.
123
+ # Validate the association when there are positioned parameters.
124
+ positions = token_positions(node[1])
125
+ unless positions.all? { |position| (position <=> header[:start]) >= 0 && (position <=> header[:finish]) <= 0 }
126
+ raise ArgumentError, 'Ambiguous lambda header'
127
+ end
128
+ unless token_positions(node[2]).all? { |position| (position <=> header[:finish]) >= 0 }
129
+ raise ArgumentError, 'Ambiguous lambda body'
130
+ end
131
+ end
132
+ walk(node[1], inner)
133
+ walk(node[2], inner)
134
+ when :var_field
135
+ current.assigned << node[1][1].to_sym if node[1] && node[1][0] == :@ident
136
+ node.each { |child| walk(child, current) }
137
+ when :BEGIN, :END
138
+ raise ArgumentError, 'Unsupported execution scope'
139
+ else
140
+ node.each { |child| walk(child, current) }
141
+ end
142
+ end
143
+
144
+ def parameters(node, current)
145
+ return unless node.is_a?(Array)
146
+
147
+ case node[0]
148
+ when :paren
149
+ parameters(node[1], current)
150
+ when :block_var
151
+ parameters(node[1], current)
152
+ declare(node[2], current)
153
+ when :params
154
+ declare(node[1], current)
155
+ (node[2] || []).each { |pair| declare(pair[0], current) }
156
+ declare(node[3], current)
157
+ declare(node[4], current)
158
+ (node[5] || []).each { |pair| declare(pair[0], current) }
159
+ declare(node[6], current)
160
+ declare(node[7], current)
161
+ end
162
+ end
163
+
164
+ def declare(node, current)
165
+ return unless node.is_a?(Array)
166
+
167
+ if node[0] == :@ident || node[0] == :@label
168
+ current.declared << node[1].delete_suffix(':').to_sym
169
+ else
170
+ node.each { |child| declare(child, current) }
171
+ end
172
+ end
173
+
174
+ def token_positions(node)
175
+ return [] unless node.is_a?(Array)
176
+ return [node[2]] if node[0].is_a?(Symbol) && node[0].to_s.start_with?('@')
177
+
178
+ node.flat_map { |child| token_positions(child) }
179
+ end
180
+
181
+ def lambda_headers(source)
182
+ tokens = Ripper.lex(source)
183
+ raise ArgumentError, 'Token limit' if tokens.length > MAX_NODES
184
+
185
+ headers = []
186
+ work = 0
187
+ tokens.each_with_index do |token, index|
188
+ next unless token[1] == :on_tlambda
189
+
190
+ header = { start: token[0], finish: token[0], locals: [] }
191
+ depth = 0
192
+ shadow = false
193
+ cursor = index + 1
194
+ while cursor < tokens.length
195
+ work += 1
196
+ raise ArgumentError, 'Lambda header limit' if work > MAX_NODES
197
+ position, type, text = tokens[cursor]
198
+ cursor += 1
199
+ header[:finish] = position
200
+ break if depth.zero? && (type == :on_tlambeg || (type == :on_kw && text == 'do'))
201
+
202
+ if shadow
203
+ if type == :on_rparen && depth == 1
204
+ shadow = false
205
+ elsif !%i[on_ident on_comma on_sp on_nl on_ignored_nl on_comment].include?(type)
206
+ raise ArgumentError, 'Ambiguous lambda shadow declaration'
207
+ end
208
+ header[:locals] << text.to_sym if type == :on_ident
209
+ end
210
+ depth += 1 if type == :on_lparen
211
+ depth -= 1 if type == :on_rparen
212
+ shadow = true if depth == 1 && type == :on_semicolon
213
+ end
214
+ headers << header
215
+ end
216
+ headers
217
+ end
218
+
219
+ private_class_method :new
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module HyperProbe
6
+ module Core
7
+ class Logger
8
+ COLOR_CODES = [32, 33, 34, 35, 36, 31, 92, 93, 94, 95, 96, 91].freeze # Vibrant ANSI colors
9
+ DISABLED_COLOR_VALUES = %w[0 false no off].freeze
10
+ SELECTOR_SEPARATOR = /[\s,]+/.freeze
11
+
12
+ @logger_cache = {}
13
+ @cache_mutex = Mutex.new
14
+
15
+ class << self
16
+ def get_logger(namespace = 'hyperprobe:agent')
17
+ @cache_mutex.synchronize do
18
+ @logger_cache[namespace.to_s] ||= new(namespace.to_s)
19
+ end
20
+ end
21
+
22
+ def is_namespace_enabled?(namespace, debug_value = nil)
23
+ val = debug_value.nil? ? ENV['DEBUG'] : debug_value
24
+ return false if val.nil? || val.strip.empty?
25
+
26
+ enabled_patterns, skipped_patterns = parse_selectors(val)
27
+
28
+ # Check exclusions first
29
+ return false if skipped_patterns.any? { |p| p.match?(namespace) }
30
+
31
+ # Check inclusions
32
+ enabled_patterns.any? { |p| p.match?(namespace) }
33
+ end
34
+
35
+ def parse_selectors(value)
36
+ enabled = []
37
+ skipped = []
38
+
39
+ (value || '').split(SELECTOR_SEPARATOR).each do |selector|
40
+ clean = selector.strip
41
+ next if clean.empty?
42
+
43
+ is_skip = clean.start_with?('-')
44
+ raw_pattern = is_skip ? clean[1..-1] : clean
45
+ next if raw_pattern.nil? || raw_pattern.empty?
46
+
47
+ regex = compile_selector(raw_pattern)
48
+ if is_skip
49
+ skipped << regex
50
+ else
51
+ enabled << regex
52
+ end
53
+ end
54
+
55
+ [enabled, skipped]
56
+ end
57
+
58
+ def compile_selector(pattern)
59
+ regex_str = '^' + Regexp.escape(pattern).gsub('\\*', '.*?') + '$'
60
+ Regexp.new(regex_str, Regexp::IGNORECASE)
61
+ end
62
+
63
+ def reset_cache!
64
+ @cache_mutex.synchronize do
65
+ @logger_cache.clear
66
+ end
67
+ end
68
+ end
69
+
70
+ attr_reader :namespace, :enabled, :color
71
+
72
+ def initialize(namespace = 'hyperprobe:agent')
73
+ @namespace = namespace.to_s
74
+ @enabled = self.class.is_namespace_enabled?(@namespace)
75
+ @color = select_namespace_color(@namespace)
76
+ @last_log_time_ms = monotonic_ms
77
+ @mutex = Mutex.new
78
+ end
79
+
80
+ def debug_enabled?
81
+ @enabled
82
+ end
83
+
84
+ alias is_debug_enabled debug_enabled?
85
+
86
+ def debug(message)
87
+ return unless @enabled
88
+
89
+ write_log(message, $stdout)
90
+ end
91
+
92
+ def info(message)
93
+ if @enabled
94
+ write_log(message, $stdout)
95
+ end
96
+ end
97
+
98
+ def warn(message)
99
+ timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
100
+ formatted = "#{timestamp} \e[33m[HyperProbe WARN]\e[0m [#{@namespace}] #{message}"
101
+ emit($stdout, formatted)
102
+ end
103
+
104
+ def error(message, exception = nil)
105
+ timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
106
+ formatted = "#{timestamp} \e[31m[HyperProbe ERROR]\e[0m [#{@namespace}] #{message}"
107
+ emit($stderr, formatted)
108
+ if exception
109
+ type = Kernel.instance_method(:class).bind(exception).call
110
+ emit($stderr, " #{Module.instance_method(:name).bind(type).call}")
111
+ end
112
+ rescue SignalException, SystemExit
113
+ raise
114
+ rescue Exception
115
+ nil
116
+ end
117
+
118
+ def force_info(message)
119
+ timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S.%L')
120
+ formatted = "#{timestamp} \e[34mHyperProbe\e[0m -- [#{@namespace}] #{message}"
121
+ emit($stdout, formatted)
122
+ end
123
+
124
+ alias forceInfo force_info
125
+
126
+ def force_error(message, exception = nil)
127
+ error(message, exception)
128
+ end
129
+
130
+ alias forceError force_error
131
+
132
+ private
133
+
134
+ def write_log(message, stream)
135
+ now = monotonic_ms
136
+ delta_str = ''
137
+
138
+ return unless @mutex.try_lock
139
+ begin
140
+ delta = now - @last_log_time_ms
141
+ delta_str = " \e[#{@color}m+#{delta}ms\e[0m" if delta > 0
142
+ @last_log_time_ms = now
143
+ ensure
144
+ @mutex.unlock
145
+ end
146
+
147
+ prefix = if colors_enabled?(stream)
148
+ "\e[1m\e[#{@color}m#{@namespace}\e[0m"
149
+ else
150
+ @namespace
151
+ end
152
+
153
+ emit(stream, " #{prefix} \e[90m#{message}\e[0m#{delta_str}")
154
+ rescue SignalException, SystemExit
155
+ raise
156
+ rescue Exception
157
+ nil
158
+ end
159
+
160
+ def emit(stream, message)
161
+ line = "#{message}\n".byteslice(0, 4096)
162
+ if IO === stream
163
+ IO.instance_method(:write_nonblock).bind(stream).call(line, exception: false)
164
+ elsif defined?(StringIO) && StringIO === stream
165
+ StringIO.instance_method(:write).bind(stream).call(line)
166
+ end
167
+ rescue SignalException, SystemExit
168
+ raise
169
+ rescue Exception
170
+ # Never block or call arbitrary application output adapters.
171
+ nil
172
+ end
173
+
174
+ def select_namespace_color(ns)
175
+ hash_val = 0
176
+ ns.each_char.with_index do |char, idx|
177
+ hash_val += (idx + 1) * char.ord
178
+ end
179
+ COLOR_CODES[hash_val % COLOR_CODES.length]
180
+ end
181
+
182
+ def colors_enabled?(stream)
183
+ configured = ENV['DEBUG_COLORS']
184
+ if configured
185
+ return !DISABLED_COLOR_VALUES.include?(configured.strip.downcase)
186
+ end
187
+
188
+ IO === stream && IO.instance_method(:tty?).bind(stream).call
189
+ rescue StandardError
190
+ false
191
+ end
192
+
193
+ def monotonic_ms
194
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
195
+ end
196
+ end
197
+ end
198
+ end