fiber_audit 0.1.0 → 0.2.1
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 +4 -4
- data/.fiber-audit.example.yml +19 -0
- data/ARCHITECTURE.md +726 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +201 -0
- data/README.md +66 -8
- data/lib/fiber_audit/cli.rb +87 -2
- data/lib/fiber_audit/configuration.rb +106 -6
- data/lib/fiber_audit/errors.rb +2 -0
- data/lib/fiber_audit/operation_vocabulary.rb +42 -0
- data/lib/fiber_audit/reporters/text.rb +1 -1
- data/lib/fiber_audit/runtime/active_operations.rb +146 -0
- data/lib/fiber_audit/runtime/boot.rb +83 -0
- data/lib/fiber_audit/runtime/clock.rb +35 -0
- data/lib/fiber_audit/runtime/environment.rb +289 -0
- data/lib/fiber_audit/runtime/event.rb +86 -0
- data/lib/fiber_audit/runtime/execution_context.rb +89 -0
- data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
- data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
- data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
- data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
- data/lib/fiber_audit/runtime/limits.rb +102 -0
- data/lib/fiber_audit/runtime/location.rb +44 -0
- data/lib/fiber_audit/runtime/policy.rb +121 -0
- data/lib/fiber_audit/runtime/probes/base.rb +333 -0
- data/lib/fiber_audit/runtime/probes/http.rb +80 -0
- data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
- data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
- data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
- data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
- data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
- data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
- data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
- data/lib/fiber_audit/runtime/rails_integration.rb +320 -0
- data/lib/fiber_audit/runtime/recorder.rb +333 -0
- data/lib/fiber_audit/runtime/redactor.rb +102 -0
- data/lib/fiber_audit/runtime/sampler.rb +26 -0
- data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
- data/lib/fiber_audit/runtime/session.rb +112 -0
- data/lib/fiber_audit/runtime/supervisor.rb +114 -0
- data/lib/fiber_audit/runtime/validation.rb +68 -0
- data/lib/fiber_audit/runtime/watchdog.rb +479 -0
- data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
- data/lib/fiber_audit/runtime.rb +37 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
- data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
- data/lib/fiber_audit/static/rules/io_select.rb +2 -4
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
- data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
- data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
- data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
- data/lib/fiber_audit/version.rb +1 -1
- data/lib/fiber_audit.rb +1 -0
- metadata +38 -2
|
@@ -4,13 +4,34 @@ require 'yaml'
|
|
|
4
4
|
require 'pathname'
|
|
5
5
|
require_relative 'errors'
|
|
6
6
|
require_relative 'findings/severity'
|
|
7
|
+
require_relative 'runtime/policy'
|
|
8
|
+
require_relative 'runtime/watchdog_policy'
|
|
7
9
|
|
|
8
10
|
module FiberAudit
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
12
|
class Configuration
|
|
10
|
-
KNOWN_TOP_LEVEL_KEYS = %w[static rules report].freeze
|
|
13
|
+
KNOWN_TOP_LEVEL_KEYS = %w[static rules report runtime].freeze
|
|
11
14
|
KNOWN_STATIC_KEYS = %w[include exclude suppressions_path].freeze
|
|
12
15
|
KNOWN_REPORT_KEYS = %w[formats min_severity].freeze
|
|
13
16
|
KNOWN_RULE_KEYS = %w[enabled severity].freeze
|
|
17
|
+
KNOWN_RUNTIME_KEYS = %w[redaction sampling overhead watchdog fail_open].freeze
|
|
18
|
+
KNOWN_REDACTION_KEYS = %w[mode].freeze
|
|
19
|
+
KNOWN_SAMPLING_KEYS = %w[rate].freeze
|
|
20
|
+
KNOWN_OVERHEAD_KEYS = %w[
|
|
21
|
+
max_events_per_second max_events_per_session max_record_bytes max_session_bytes
|
|
22
|
+
].freeze
|
|
23
|
+
KNOWN_WATCHDOG_KEYS = %w[
|
|
24
|
+
enabled heartbeat_interval_ms stall_threshold_ms max_frames
|
|
25
|
+
].freeze
|
|
26
|
+
RUNTIME_POLICY_PATHS = {
|
|
27
|
+
'redaction' => 'runtime.redaction.mode',
|
|
28
|
+
'sampling_rate' => 'runtime.sampling.rate',
|
|
29
|
+
'max_events_per_second' => 'runtime.overhead.max_events_per_second',
|
|
30
|
+
'max_events_per_session' => 'runtime.overhead.max_events_per_session',
|
|
31
|
+
'max_record_bytes' => 'runtime.overhead.max_record_bytes',
|
|
32
|
+
'max_session_bytes' => 'runtime.overhead.max_session_bytes',
|
|
33
|
+
'fail_open' => 'runtime.fail_open'
|
|
34
|
+
}.freeze
|
|
14
35
|
VALID_FORMATS = %w[text json].freeze
|
|
15
36
|
|
|
16
37
|
DEFAULT_STATIC_INCLUDE = %w[
|
|
@@ -28,7 +49,8 @@ module FiberAudit
|
|
|
28
49
|
].freeze
|
|
29
50
|
|
|
30
51
|
attr_reader :static_include, :static_exclude, :rules_config,
|
|
31
|
-
:report_formats, :min_severity, :suppressions_path
|
|
52
|
+
:report_formats, :min_severity, :suppressions_path,
|
|
53
|
+
:runtime_policy, :runtime_watchdog_policy
|
|
32
54
|
|
|
33
55
|
def initialize(
|
|
34
56
|
static_include: DEFAULT_STATIC_INCLUDE,
|
|
@@ -36,11 +58,14 @@ module FiberAudit
|
|
|
36
58
|
rules_config: {},
|
|
37
59
|
report_formats: %w[text],
|
|
38
60
|
min_severity: :low,
|
|
39
|
-
suppressions_path: nil
|
|
61
|
+
suppressions_path: nil,
|
|
62
|
+
runtime_policy: Runtime::Policy.new,
|
|
63
|
+
runtime_watchdog_policy: Runtime::WatchdogPolicy.new
|
|
40
64
|
)
|
|
41
65
|
validate_types!(
|
|
42
66
|
static_include, static_exclude, rules_config,
|
|
43
|
-
report_formats, min_severity, suppressions_path
|
|
67
|
+
report_formats, min_severity, suppressions_path, runtime_policy,
|
|
68
|
+
runtime_watchdog_policy
|
|
44
69
|
)
|
|
45
70
|
|
|
46
71
|
@static_include = static_include
|
|
@@ -49,6 +74,8 @@ module FiberAudit
|
|
|
49
74
|
@report_formats = report_formats
|
|
50
75
|
@min_severity = coerce_severity(min_severity, 'report.min_severity')
|
|
51
76
|
@suppressions_path = suppressions_path
|
|
77
|
+
@runtime_policy = runtime_policy
|
|
78
|
+
@runtime_watchdog_policy = runtime_watchdog_policy
|
|
52
79
|
end
|
|
53
80
|
|
|
54
81
|
def rule_enabled?(rule_id)
|
|
@@ -76,6 +103,7 @@ module FiberAudit
|
|
|
76
103
|
static = yaml['static'] || {}
|
|
77
104
|
rules = yaml['rules'] || {}
|
|
78
105
|
report = yaml['report'] || {}
|
|
106
|
+
runtime = yaml['runtime'] || {}
|
|
79
107
|
|
|
80
108
|
new(
|
|
81
109
|
static_include: static['include'] || DEFAULT_STATIC_INCLUDE,
|
|
@@ -83,7 +111,9 @@ module FiberAudit
|
|
|
83
111
|
rules_config: rules,
|
|
84
112
|
report_formats: report['formats'] || %w[text],
|
|
85
113
|
min_severity: report.fetch('min_severity', :low),
|
|
86
|
-
suppressions_path: static['suppressions_path']
|
|
114
|
+
suppressions_path: static['suppressions_path'],
|
|
115
|
+
runtime_policy: runtime_policy_from(runtime),
|
|
116
|
+
runtime_watchdog_policy: runtime_watchdog_policy_from(runtime)
|
|
87
117
|
)
|
|
88
118
|
end
|
|
89
119
|
|
|
@@ -115,6 +145,8 @@ module FiberAudit
|
|
|
115
145
|
check_unknown_keys(report, KNOWN_REPORT_KEYS, 'report')
|
|
116
146
|
end
|
|
117
147
|
|
|
148
|
+
validate_runtime_structure!(yaml['runtime']) if yaml.key?('runtime')
|
|
149
|
+
|
|
118
150
|
return unless yaml.key?('rules')
|
|
119
151
|
|
|
120
152
|
rules = yaml['rules']
|
|
@@ -124,6 +156,64 @@ module FiberAudit
|
|
|
124
156
|
"rules must be a mapping, got #{rules.class}"
|
|
125
157
|
end
|
|
126
158
|
|
|
159
|
+
def validate_runtime_structure!(runtime)
|
|
160
|
+
unless runtime.is_a?(Hash)
|
|
161
|
+
raise ConfigurationError,
|
|
162
|
+
"runtime must be a mapping, got #{runtime.class}"
|
|
163
|
+
end
|
|
164
|
+
check_unknown_keys(runtime, KNOWN_RUNTIME_KEYS, 'runtime')
|
|
165
|
+
validate_runtime_mapping!(runtime, 'redaction', KNOWN_REDACTION_KEYS)
|
|
166
|
+
validate_runtime_mapping!(runtime, 'sampling', KNOWN_SAMPLING_KEYS)
|
|
167
|
+
validate_runtime_mapping!(runtime, 'overhead', KNOWN_OVERHEAD_KEYS)
|
|
168
|
+
validate_runtime_mapping!(runtime, 'watchdog', KNOWN_WATCHDOG_KEYS)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def validate_runtime_mapping!(runtime, key, allowed)
|
|
172
|
+
return unless runtime.key?(key)
|
|
173
|
+
|
|
174
|
+
value = runtime[key]
|
|
175
|
+
unless value.is_a?(Hash)
|
|
176
|
+
raise ConfigurationError,
|
|
177
|
+
"runtime.#{key} must be a mapping, got #{value.class}"
|
|
178
|
+
end
|
|
179
|
+
check_unknown_keys(value, allowed, "runtime.#{key}")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def runtime_policy_from(runtime)
|
|
183
|
+
defaults = Runtime::Policy::DEFAULTS
|
|
184
|
+
redaction = runtime.fetch('redaction', {})
|
|
185
|
+
sampling = runtime.fetch('sampling', {})
|
|
186
|
+
overhead = runtime.fetch('overhead', {})
|
|
187
|
+
Runtime::Policy.new(
|
|
188
|
+
redaction: redaction.fetch('mode', defaults[:redaction]),
|
|
189
|
+
sampling_rate: sampling.fetch('rate', defaults[:sampling_rate]),
|
|
190
|
+
max_events_per_second: overhead.fetch('max_events_per_second', defaults[:max_events_per_second]),
|
|
191
|
+
max_events_per_session: overhead.fetch('max_events_per_session', defaults[:max_events_per_session]),
|
|
192
|
+
max_record_bytes: overhead.fetch('max_record_bytes', defaults[:max_record_bytes]),
|
|
193
|
+
max_session_bytes: overhead.fetch('max_session_bytes', defaults[:max_session_bytes]),
|
|
194
|
+
fail_open: runtime.fetch('fail_open', defaults[:fail_open])
|
|
195
|
+
)
|
|
196
|
+
rescue RuntimeContractError => e
|
|
197
|
+
field = e.message.split.first
|
|
198
|
+
path = RUNTIME_POLICY_PATHS.fetch(field, 'runtime')
|
|
199
|
+
raise ConfigurationError, "#{path} is invalid: #{e.message}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def runtime_watchdog_policy_from(runtime)
|
|
203
|
+
values = runtime.fetch('watchdog', {})
|
|
204
|
+
defaults = Runtime::WatchdogPolicy::DEFAULTS
|
|
205
|
+
Runtime::WatchdogPolicy.new(
|
|
206
|
+
enabled: values.fetch('enabled', defaults[:enabled]),
|
|
207
|
+
heartbeat_interval_ms: values.fetch('heartbeat_interval_ms', defaults[:heartbeat_interval_ms]),
|
|
208
|
+
stall_threshold_ms: values.fetch('stall_threshold_ms', defaults[:stall_threshold_ms]),
|
|
209
|
+
max_frames: values.fetch('max_frames', defaults[:max_frames])
|
|
210
|
+
)
|
|
211
|
+
rescue RuntimeContractError => e
|
|
212
|
+
field = e.message.split.first
|
|
213
|
+
path = field == 'enabled' ? 'runtime.watchdog.enabled' : "runtime.watchdog.#{field}"
|
|
214
|
+
raise ConfigurationError, "#{path} is invalid: #{e.message}"
|
|
215
|
+
end
|
|
216
|
+
|
|
127
217
|
def check_unknown_keys(hash, allowed, path)
|
|
128
218
|
unknown = hash.keys - allowed
|
|
129
219
|
return if unknown.empty?
|
|
@@ -139,7 +229,7 @@ module FiberAudit
|
|
|
139
229
|
|
|
140
230
|
def validate_types!(
|
|
141
231
|
include_patterns, exclude_patterns, rules,
|
|
142
|
-
formats, _severity, suppressions
|
|
232
|
+
formats, _severity, suppressions, runtime_policy, watchdog_policy
|
|
143
233
|
)
|
|
144
234
|
unless include_patterns.is_a?(Array) &&
|
|
145
235
|
include_patterns.all?(String)
|
|
@@ -158,6 +248,15 @@ module FiberAudit
|
|
|
158
248
|
validate_rules!(rules)
|
|
159
249
|
validate_report_formats!(formats)
|
|
160
250
|
|
|
251
|
+
unless runtime_policy.is_a?(Runtime::Policy)
|
|
252
|
+
raise ConfigurationError,
|
|
253
|
+
'runtime_policy must be a FiberAudit::Runtime::Policy'
|
|
254
|
+
end
|
|
255
|
+
unless watchdog_policy.is_a?(Runtime::WatchdogPolicy)
|
|
256
|
+
raise ConfigurationError,
|
|
257
|
+
'runtime_watchdog_policy must be a FiberAudit::Runtime::WatchdogPolicy'
|
|
258
|
+
end
|
|
259
|
+
|
|
161
260
|
return if suppressions.nil? || suppressions.is_a?(String)
|
|
162
261
|
|
|
163
262
|
raise ConfigurationError,
|
|
@@ -233,4 +332,5 @@ module FiberAudit
|
|
|
233
332
|
"#{path} is not a valid severity: #{value.inspect}"
|
|
234
333
|
end
|
|
235
334
|
end
|
|
335
|
+
# rubocop:enable Metrics/ClassLength
|
|
236
336
|
end
|
data/lib/fiber_audit/errors.rb
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
# Canonical operation names shared by static findings and runtime evidence.
|
|
5
|
+
module OperationVocabulary
|
|
6
|
+
FA1001_TARGETS = {
|
|
7
|
+
'Kernel' => %i[system exec spawn].freeze,
|
|
8
|
+
'Open3' => %i[capture2 capture2e capture3 pipeline].freeze,
|
|
9
|
+
'IO' => %i[popen].freeze,
|
|
10
|
+
'Process' => %i[waitall detach].freeze
|
|
11
|
+
}.freeze
|
|
12
|
+
FA1001_KERNEL_METHODS = %i[system exec spawn].freeze
|
|
13
|
+
|
|
14
|
+
FA1002_METHODS = %i[join value].freeze
|
|
15
|
+
FA1002_OPERATIONS = %w[Thread.join Thread.value].freeze
|
|
16
|
+
|
|
17
|
+
FA1003_TARGETS = {
|
|
18
|
+
'Mutex' => %i[lock synchronize try_lock].freeze,
|
|
19
|
+
'ConditionVariable' => %i[wait].freeze,
|
|
20
|
+
'Monitor' => %i[synchronize].freeze,
|
|
21
|
+
'MonitorMixin' => %i[synchronize].freeze
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
FA1004_THREAD_VARIABLE_METHODS = %i[thread_variable_get thread_variable_set].freeze
|
|
25
|
+
FA1004_INDEX_METHODS = %i[[] []=].freeze
|
|
26
|
+
|
|
27
|
+
FA1005_TARGETS = {
|
|
28
|
+
'IO' => :select,
|
|
29
|
+
'Kernel' => :select
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
FA1006_EXACT = %w[
|
|
33
|
+
TCPSocket TCPServer UDPSocket UNIXSocket UNIXServer Socket IPSocket
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
FA1007_NET_HTTP_METHODS = %i[get get_response start request].freeze
|
|
37
|
+
FA1007_URI_METHODS = {
|
|
38
|
+
'URI' => :open,
|
|
39
|
+
'OpenURI' => :open_uri
|
|
40
|
+
}.freeze
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -11,7 +11,7 @@ module FiberAudit
|
|
|
11
11
|
# Calls Schema.build then Schema.validate! to enforce the contract.
|
|
12
12
|
#
|
|
13
13
|
# Exact contract:
|
|
14
|
-
# - Header: `FiberAudit
|
|
14
|
+
# - Header: `FiberAudit <version> — static analysis`
|
|
15
15
|
# - Summary counts, suppressed, status
|
|
16
16
|
# - Disclaimer under status
|
|
17
17
|
# - Findings: RULE SEVERITY path:line
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'event'
|
|
4
|
+
require_relative 'location'
|
|
5
|
+
require_relative 'validation'
|
|
6
|
+
|
|
7
|
+
module FiberAudit
|
|
8
|
+
module Runtime
|
|
9
|
+
# Bounded process-local registry shared by the watchdog and future probes.
|
|
10
|
+
class ActiveOperations
|
|
11
|
+
MAX_ENTRIES = 10_000
|
|
12
|
+
MAX_SNAPSHOT = 100
|
|
13
|
+
|
|
14
|
+
Handle = Data.define(:pid, :thread_id, :fiber_id, :sequence)
|
|
15
|
+
Entry = Data.define(
|
|
16
|
+
:thread_id,
|
|
17
|
+
:fiber_id,
|
|
18
|
+
:sequence,
|
|
19
|
+
:operation,
|
|
20
|
+
:location,
|
|
21
|
+
:execution_context,
|
|
22
|
+
:started_monotonic_ns
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def initialize(pid_source: Process.method(:pid), capacity: MAX_ENTRIES, snapshot_limit: MAX_SNAPSHOT)
|
|
26
|
+
validate_source!(pid_source)
|
|
27
|
+
validate_limit!(capacity, 'capacity', MAX_ENTRIES)
|
|
28
|
+
validate_limit!(snapshot_limit, 'snapshot_limit', MAX_SNAPSHOT)
|
|
29
|
+
@pid_source = pid_source
|
|
30
|
+
@capacity = capacity
|
|
31
|
+
@snapshot_limit = snapshot_limit
|
|
32
|
+
reset_for_process!(current_pid)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def register(
|
|
36
|
+
operation:,
|
|
37
|
+
monotonic_ns:,
|
|
38
|
+
location: nil,
|
|
39
|
+
execution_context: :unknown,
|
|
40
|
+
thread: Thread.current,
|
|
41
|
+
fiber: Fiber.current
|
|
42
|
+
)
|
|
43
|
+
ensure_current_process!
|
|
44
|
+
values = normalize_entry(
|
|
45
|
+
operation: operation,
|
|
46
|
+
location: location,
|
|
47
|
+
execution_context: execution_context,
|
|
48
|
+
monotonic_ns: monotonic_ns,
|
|
49
|
+
thread: thread,
|
|
50
|
+
fiber: fiber
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
return if @entries.size >= @capacity
|
|
55
|
+
|
|
56
|
+
@sequence += 1
|
|
57
|
+
entry = Entry.new(sequence: @sequence, **values)
|
|
58
|
+
handle = Handle.new(
|
|
59
|
+
pid: @owner_pid,
|
|
60
|
+
thread_id: entry.thread_id,
|
|
61
|
+
fiber_id: entry.fiber_id,
|
|
62
|
+
sequence: entry.sequence
|
|
63
|
+
)
|
|
64
|
+
@entries[handle] = entry
|
|
65
|
+
handle
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def finish(handle)
|
|
70
|
+
ensure_current_process!
|
|
71
|
+
return unless handle.is_a?(Handle) && handle.pid == @owner_pid
|
|
72
|
+
|
|
73
|
+
@mutex.synchronize { @entries.delete(handle) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def snapshot(thread_id: nil)
|
|
77
|
+
ensure_current_process!
|
|
78
|
+
normalized_thread_id = Validation.integer(thread_id, 'thread_id', allow_nil: true)
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
entries = @entries.values
|
|
81
|
+
entries = entries.select { |entry| entry.thread_id == normalized_thread_id } if normalized_thread_id
|
|
82
|
+
entries.sort_by(&:sequence).first(@snapshot_limit).freeze
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def size
|
|
87
|
+
ensure_current_process!
|
|
88
|
+
@mutex.synchronize { @entries.size }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def normalize_entry(operation:, location:, execution_context:, monotonic_ns:, thread:, fiber:)
|
|
94
|
+
canonical_operation = Validation.operation(operation)
|
|
95
|
+
unless location.nil? || location.is_a?(Location)
|
|
96
|
+
raise RuntimeContractError, 'location must be a FiberAudit::Runtime::Location or nil'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
normalized_context = execution_context.to_sym if execution_context.is_a?(String) || execution_context.is_a?(Symbol)
|
|
100
|
+
raise RuntimeContractError, 'execution_context is invalid' unless Context::ALL.include?(normalized_context)
|
|
101
|
+
|
|
102
|
+
unless thread.respond_to?(:object_id) && fiber.respond_to?(:object_id)
|
|
103
|
+
raise RuntimeContractError, 'thread and fiber identities are invalid'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
thread_id: Validation.integer(thread.object_id, 'thread_id'),
|
|
108
|
+
fiber_id: Validation.integer(fiber.object_id, 'fiber_id'),
|
|
109
|
+
operation: canonical_operation,
|
|
110
|
+
location: location,
|
|
111
|
+
execution_context: normalized_context,
|
|
112
|
+
started_monotonic_ns: Validation.integer(monotonic_ns, 'monotonic_ns')
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ensure_current_process!
|
|
117
|
+
pid = current_pid
|
|
118
|
+
reset_for_process!(pid) unless pid == @owner_pid
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def reset_for_process!(pid)
|
|
122
|
+
@owner_pid = pid
|
|
123
|
+
@mutex = Mutex.new
|
|
124
|
+
@entries = {}
|
|
125
|
+
@sequence = 0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def current_pid
|
|
129
|
+
value = @pid_source.call
|
|
130
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
131
|
+
|
|
132
|
+
raise RuntimeContractError, 'pid source must return a positive Integer'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_source!(source)
|
|
136
|
+
raise RuntimeContractError, 'pid_source must respond to call' unless source.respond_to?(:call)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_limit!(value, name, maximum)
|
|
140
|
+
return if value.is_a?(Integer) && value.between?(1, maximum)
|
|
141
|
+
|
|
142
|
+
raise RuntimeContractError, "#{name} must be an Integer in 1..#{maximum}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require_relative 'environment'
|
|
5
|
+
require_relative 'lifecycle'
|
|
6
|
+
|
|
7
|
+
module FiberAudit
|
|
8
|
+
module Runtime
|
|
9
|
+
module Boot
|
|
10
|
+
class << self
|
|
11
|
+
def activate_from_environment!(
|
|
12
|
+
environment: ENV,
|
|
13
|
+
clock: Clock.new,
|
|
14
|
+
session_id_source: SecureRandom.method(:uuid),
|
|
15
|
+
pid_source: Process.method(:pid),
|
|
16
|
+
writer_factory: JSONL::Writer.method(:open),
|
|
17
|
+
random: Sampler::RANDOM_SOURCE
|
|
18
|
+
)
|
|
19
|
+
return current if activated_for_current_process?
|
|
20
|
+
|
|
21
|
+
mode = Environment.failure_mode(environment)
|
|
22
|
+
return unless Environment.activated?(environment)
|
|
23
|
+
|
|
24
|
+
settings = Environment.load(environment)
|
|
25
|
+
watchdog_policy = if environment.key?(Environment::WATCHDOG_SETTINGS_KEY)
|
|
26
|
+
Environment.load_watchdog_policy(environment)
|
|
27
|
+
end
|
|
28
|
+
@lifecycle = Lifecycle.start(
|
|
29
|
+
settings: settings,
|
|
30
|
+
watchdog_policy: watchdog_policy,
|
|
31
|
+
probes_enabled: Environment.probes_enabled?(environment),
|
|
32
|
+
clock: clock,
|
|
33
|
+
session_id_source: session_id_source,
|
|
34
|
+
pid_source: pid_source,
|
|
35
|
+
writer_factory: writer_factory,
|
|
36
|
+
random: random
|
|
37
|
+
)
|
|
38
|
+
@pid_source = pid_source
|
|
39
|
+
@activation_pid = pid_source.call
|
|
40
|
+
install_shutdown_hook!
|
|
41
|
+
@lifecycle
|
|
42
|
+
rescue StandardError
|
|
43
|
+
raise if defined?(mode) && mode == :closed
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def current
|
|
49
|
+
return unless @lifecycle
|
|
50
|
+
|
|
51
|
+
@lifecycle.ensure_current_process!
|
|
52
|
+
@activation_pid = @pid_source.call
|
|
53
|
+
@lifecycle
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def activated?
|
|
57
|
+
!current.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def shutdown(exception: nil)
|
|
61
|
+
current&.shutdown(exception: exception)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def activated_for_current_process?
|
|
67
|
+
return false unless @lifecycle && @pid_source && @activation_pid
|
|
68
|
+
|
|
69
|
+
@activation_pid == @pid_source.call
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def install_shutdown_hook!
|
|
73
|
+
return if @shutdown_hook_installed
|
|
74
|
+
|
|
75
|
+
at_exit { FiberAudit::Runtime::Boot.shutdown(exception: $ERROR_INFO) }
|
|
76
|
+
@shutdown_hook_installed = true
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
FiberAudit::Runtime::Boot.activate_from_environment! if ENV.key?(FiberAudit::Runtime::Environment::ACTIVATION_KEY)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'validation'
|
|
4
|
+
|
|
5
|
+
module FiberAudit
|
|
6
|
+
module Runtime
|
|
7
|
+
class Clock
|
|
8
|
+
MAX_NANOSECONDS = 9_223_372_036_854_775_807
|
|
9
|
+
WALL_SOURCE = -> { Time.now }.freeze
|
|
10
|
+
MONOTONIC_SOURCE = lambda {
|
|
11
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(wall: WALL_SOURCE, monotonic: MONOTONIC_SOURCE)
|
|
15
|
+
raise RuntimeContractError, 'wall clock source must respond to call' unless wall.respond_to?(:call)
|
|
16
|
+
raise RuntimeContractError, 'monotonic clock source must respond to call' unless monotonic.respond_to?(:call)
|
|
17
|
+
|
|
18
|
+
@wall = wall
|
|
19
|
+
@monotonic = monotonic
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def wall_time
|
|
24
|
+
Validation.utc_time(@wall.call, 'wall clock result')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def monotonic_ns
|
|
28
|
+
value = Validation.integer(@monotonic.call, 'monotonic clock result')
|
|
29
|
+
raise RuntimeSafetyError, 'monotonic clock result exceeds signed 64-bit range' if value > MAX_NANOSECONDS
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|