audition 0.2.4 → 0.4.0
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/README.md +175 -39
- data/lib/audition/bundle_sweep.rb +32 -15
- data/lib/audition/cli.rb +181 -100
- data/lib/audition/config.rb +33 -7
- data/lib/audition/dynamic/harness.rb +323 -31
- data/lib/audition/dynamic/prober.rb +190 -43
- data/lib/audition/finding.rb +10 -2
- data/lib/audition/progress.rb +418 -0
- data/lib/audition/report/github.rb +69 -0
- data/lib/audition/report/json.rb +68 -0
- data/lib/audition/report/style.rb +74 -0
- data/lib/audition/report/sweep.rb +182 -0
- data/lib/audition/report/text.rb +160 -0
- data/lib/audition/report.rb +16 -295
- data/lib/audition/rewriters.rb +8 -5
- data/lib/audition/static/analyzer.rb +86 -17
- data/lib/audition/static/checks/dependency_class_state.rb +160 -0
- data/lib/audition/static/checks/mutable_constants.rb +162 -28
- data/lib/audition/static/checks/runtime_require.rb +4 -1
- data/lib/audition/static/checks/unsafe_calls.rb +7 -6
- data/lib/audition/static/checks/unshareable_reads.rb +259 -0
- data/lib/audition/static/checks.rb +5 -2
- data/lib/audition/static/gem_calls.rb +1770 -0
- data/lib/audition/static/graph_audit.rb +1053 -12
- data/lib/audition/static/literal_classifier.rb +191 -20
- data/lib/audition/static/native_extensions.rb +175 -0
- data/lib/audition/static/source_file.rb +70 -0
- data/lib/audition/static/work_split.rb +54 -0
- data/lib/audition/target.rb +147 -14
- data/lib/audition/version.rb +1 -1
- data/lib/audition.rb +3 -0
- metadata +15 -4
|
@@ -12,9 +12,26 @@ Warning[:experimental] = false
|
|
|
12
12
|
Thread.report_on_exception = false
|
|
13
13
|
|
|
14
14
|
require "json"
|
|
15
|
+
require "rbconfig"
|
|
15
16
|
|
|
16
17
|
module AuditionHarness
|
|
17
|
-
|
|
18
|
+
# High enough for the largest targets: the sweep is the backstop
|
|
19
|
+
# for everything static analysis cannot prove, so it must reach
|
|
20
|
+
# every constant the boot defined. A hit is reported as
|
|
21
|
+
# truncated, never silent.
|
|
22
|
+
MAX_CONSTS = 200_000
|
|
23
|
+
|
|
24
|
+
# Directories under the target root that are not the target's
|
|
25
|
+
# own surface. Bundler's deployment mode (and bundler-cache in
|
|
26
|
+
# GitHub Actions) vendors every gem into <root>/vendor/bundle,
|
|
27
|
+
# and attributing those constants to the target would flip its
|
|
28
|
+
# verdict from blocked to not_ready. Must mirror
|
|
29
|
+
# Audition::Target::EXCLUDED_DIRS (this file is a standalone
|
|
30
|
+
# subprocess script and cannot require the gem); a spec keeps
|
|
31
|
+
# the two lists in sync.
|
|
32
|
+
EXCLUDED_DIRS = %w[
|
|
33
|
+
vendor node_modules tmp log coverage pkg .git .bundle
|
|
34
|
+
].freeze
|
|
18
35
|
|
|
19
36
|
# Fixtures for capability probes.
|
|
20
37
|
CAP_CONST = [1, 2] # audition:disable mutable-constants
|
|
@@ -69,7 +86,18 @@ module AuditionHarness
|
|
|
69
86
|
def describe_error(error)
|
|
70
87
|
root = unwrap(error)
|
|
71
88
|
{"class" => scrub(root.class.name.to_s),
|
|
72
|
-
"message" => scrub(root.message.to_s)[0, 500]
|
|
89
|
+
"message" => scrub(root.message.to_s)[0, 500],
|
|
90
|
+
"backtrace" => backtrace_for(root)}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Enough frames to reach the target's code under framework
|
|
94
|
+
# wrappers; frames carry paths, which can carry arbitrary bytes.
|
|
95
|
+
def backtrace_for(error)
|
|
96
|
+
Array(error.backtrace).first(30).map do |frame|
|
|
97
|
+
scrub(frame.to_s)[0, 300]
|
|
98
|
+
end
|
|
99
|
+
rescue Exception
|
|
100
|
+
[]
|
|
73
101
|
end
|
|
74
102
|
|
|
75
103
|
def scrub(text)
|
|
@@ -124,19 +152,53 @@ module AuditionHarness
|
|
|
124
152
|
Array(payload["load_paths"]).each do |lp|
|
|
125
153
|
$LOAD_PATH.unshift(lp) # audition:disable global-variables
|
|
126
154
|
end
|
|
155
|
+
baseline = module_state_snapshot
|
|
127
156
|
before = Object.constants
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
features = $LOADED_FEATURES.dup # audition:disable global-variables
|
|
158
|
+
require_target(payload.fetch("feature"),
|
|
159
|
+
Array(payload["load_paths"]))
|
|
160
|
+
merge_reopened(
|
|
161
|
+
scan(Object.constants - before, root: payload["root"],
|
|
162
|
+
limit: payload["max_constants"]), baseline
|
|
163
|
+
).merge(
|
|
164
|
+
"native_extensions" => native_extensions(
|
|
165
|
+
features, payload["root"], payload["known_compiled"]
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Gem names and entry files diverge in two conventional ways:
|
|
171
|
+
# dashed names ship slashed files (rspec-mocks provides
|
|
172
|
+
# rspec/mocks), and squashed names ship snake_case files
|
|
173
|
+
# (activesupport provides active_support). The second has no
|
|
174
|
+
# rule to invert, so when the target ships exactly one top-level
|
|
175
|
+
# file on its load paths, that file is the entry, required by
|
|
176
|
+
# absolute path. An absolute require keeps the path as given, and scan
|
|
177
|
+
# compares constant origins against the realpathed root, so the
|
|
178
|
+
# candidate is built from the realpath too (a symlinked tmpdir,
|
|
179
|
+
# macOS /var, otherwise turns own findings into dependency ones).
|
|
180
|
+
# The error reported is the last one seen: a candidate that loads
|
|
181
|
+
# but fails inside says more than "cannot load such file".
|
|
182
|
+
def require_target(feature, load_paths)
|
|
183
|
+
require feature # audition:disable runtime-require
|
|
184
|
+
rescue LoadError => error
|
|
185
|
+
entry_candidates(feature, load_paths).each do |candidate|
|
|
186
|
+
return require candidate # audition:disable runtime-require
|
|
187
|
+
rescue LoadError => e
|
|
188
|
+
error = e
|
|
189
|
+
end
|
|
190
|
+
raise error
|
|
191
|
+
end
|
|
136
192
|
|
|
137
|
-
|
|
193
|
+
def entry_candidates(feature, load_paths)
|
|
194
|
+
candidates = []
|
|
195
|
+
slashed = feature.tr("-", "/")
|
|
196
|
+
candidates << slashed if slashed != feature
|
|
197
|
+
files = load_paths.flat_map do |path|
|
|
198
|
+
Dir[File.join(realpath(path), "*.rb")]
|
|
138
199
|
end
|
|
139
|
-
|
|
200
|
+
candidates << files.first.delete_suffix(".rb") if files.size == 1
|
|
201
|
+
candidates
|
|
140
202
|
end
|
|
141
203
|
|
|
142
204
|
# Breadth-first walk of every constant the require introduced:
|
|
@@ -144,29 +206,28 @@ module AuditionHarness
|
|
|
144
206
|
# are inspected for class-level ivars and class variables, then
|
|
145
207
|
# descended into. const_get can raise (autoload failures) and
|
|
146
208
|
# anything can lie; every step is rescued and counted.
|
|
147
|
-
def scan(root_names, root: nil)
|
|
209
|
+
def scan(root_names, root: nil, limit: nil)
|
|
148
210
|
# Loaded features are realpathed by require; the target root
|
|
149
211
|
# must be too, or symlinked paths (macOS /var vs /private/var)
|
|
150
212
|
# break the own-vs-dependency comparison.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
File.realpath(root)
|
|
154
|
-
rescue
|
|
155
|
-
root
|
|
156
|
-
end
|
|
157
|
-
end
|
|
213
|
+
root = realpath(root)
|
|
214
|
+
limit = (limit || MAX_CONSTS).to_i
|
|
158
215
|
unshareable = []
|
|
159
216
|
class_state = []
|
|
160
217
|
class_vars = []
|
|
161
218
|
errors = 0
|
|
219
|
+
truncated = false
|
|
162
220
|
seen = {}
|
|
163
221
|
queue = root_names.map { |name| [Object, name.to_s] }
|
|
164
222
|
visited = 0
|
|
165
223
|
|
|
166
224
|
until queue.empty?
|
|
225
|
+
if visited >= limit
|
|
226
|
+
truncated = true
|
|
227
|
+
break
|
|
228
|
+
end
|
|
167
229
|
owner, name = queue.shift
|
|
168
230
|
visited += 1
|
|
169
|
-
break if visited > MAX_CONSTS
|
|
170
231
|
|
|
171
232
|
begin
|
|
172
233
|
value = owner.const_get(name, false)
|
|
@@ -189,8 +250,11 @@ module AuditionHarness
|
|
|
189
250
|
else
|
|
190
251
|
begin
|
|
191
252
|
unless Ractor.shareable?(value)
|
|
253
|
+
blocker, blocker_depth = blocker_for(value)
|
|
192
254
|
unshareable << origin.merge(
|
|
193
|
-
"const" => full, "class" => value.class.name
|
|
255
|
+
"const" => full, "class" => value.class.name,
|
|
256
|
+
"blocker" => blocker,
|
|
257
|
+
"blocker_nested" => blocker_depth.positive?
|
|
194
258
|
)
|
|
195
259
|
end
|
|
196
260
|
rescue Exception
|
|
@@ -203,6 +267,8 @@ module AuditionHarness
|
|
|
203
267
|
"class_state" => class_state,
|
|
204
268
|
"class_variables" => class_vars,
|
|
205
269
|
"scanned" => visited,
|
|
270
|
+
"truncated" => truncated,
|
|
271
|
+
"limit" => limit,
|
|
206
272
|
"errors" => errors}
|
|
207
273
|
end
|
|
208
274
|
|
|
@@ -218,10 +284,64 @@ module AuditionHarness
|
|
|
218
284
|
end
|
|
219
285
|
# The separator matters: /x/app must not claim /x/app-helpers.
|
|
220
286
|
own = root.nil? || path.nil? || path == root ||
|
|
221
|
-
path.start_with?(root + File::SEPARATOR)
|
|
287
|
+
(path.start_with?(root + File::SEPARATOR) &&
|
|
288
|
+
!excluded?(path, root))
|
|
222
289
|
{"path" => path, "line" => line, "own" => own}
|
|
223
290
|
end
|
|
224
291
|
|
|
292
|
+
# Matches the static scanner's exclusion rule: any excluded or
|
|
293
|
+
# dot-prefixed component in the root-relative path means the
|
|
294
|
+
# file is not the target's own code.
|
|
295
|
+
def excluded?(path, root)
|
|
296
|
+
relative = path.delete_prefix(root + File::SEPARATOR)
|
|
297
|
+
relative.split(File::SEPARATOR).any? do |part|
|
|
298
|
+
EXCLUDED_DIRS.include?(part) || part.start_with?(".")
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
NATIVE = /\.(bundle|so)\z/
|
|
303
|
+
DECLARATION = "rb_ext_ractor_safe"
|
|
304
|
+
|
|
305
|
+
# Compiled extensions the require pulled in, with the one fact
|
|
306
|
+
# that decides their Ractor behavior: whether the file imports
|
|
307
|
+
# rb_ext_ractor_safe. Ruby's own extensions (archdir) are flagged
|
|
308
|
+
# so the prober can leave them to Ruby, and files the static check
|
|
309
|
+
# already covers are flagged as known.
|
|
310
|
+
def native_extensions(before, root, known)
|
|
311
|
+
root = realpath(root)
|
|
312
|
+
known = Array(known).map { |path| realpath(path) }
|
|
313
|
+
archdir = RbConfig::CONFIG["archdir"] + File::SEPARATOR
|
|
314
|
+
loaded = $LOADED_FEATURES - before # audition:disable global-variables
|
|
315
|
+
loaded.grep(NATIVE).map do |path|
|
|
316
|
+
{"path" => path,
|
|
317
|
+
"declares" => declares?(path),
|
|
318
|
+
"ruby" => path.start_with?(archdir),
|
|
319
|
+
"known" => known.include?(path),
|
|
320
|
+
"own" => known.include?(path) || own_path?(path, root)}
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def declares?(path)
|
|
325
|
+
File.binread(path).include?(DECLARATION)
|
|
326
|
+
rescue SystemCallError
|
|
327
|
+
false
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def own_path?(path, root)
|
|
331
|
+
return false unless root
|
|
332
|
+
|
|
333
|
+
path == root ||
|
|
334
|
+
(path.start_with?(root + File::SEPARATOR) && !excluded?(path, root))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def realpath(path)
|
|
338
|
+
return path if path.nil?
|
|
339
|
+
|
|
340
|
+
File.realpath(path)
|
|
341
|
+
rescue SystemCallError
|
|
342
|
+
path
|
|
343
|
+
end
|
|
344
|
+
|
|
225
345
|
def inspect_module(full, mod, origin, class_state, class_vars)
|
|
226
346
|
ivars = mod.instance_variables
|
|
227
347
|
if ivars.any?
|
|
@@ -252,6 +372,125 @@ module AuditionHarness
|
|
|
252
372
|
false
|
|
253
373
|
end
|
|
254
374
|
|
|
375
|
+
# Ivar/cvar state of every module defined before the boot. The
|
|
376
|
+
# new-constants sweep never revisits these, so state the boot
|
|
377
|
+
# plants on reopened core and stdlib classes is diffed against
|
|
378
|
+
# this snapshot instead. Autoload stubs are left untouched:
|
|
379
|
+
# forcing them here would load code behind the back of the
|
|
380
|
+
# before/after constant accounting.
|
|
381
|
+
def module_state_snapshot(limit = 50_000)
|
|
382
|
+
snap = {}
|
|
383
|
+
seen = {}
|
|
384
|
+
queue = Object.constants.map { |name| [Object, name.to_s] }
|
|
385
|
+
until queue.empty? || snap.size >= limit
|
|
386
|
+
owner, name = queue.shift
|
|
387
|
+
begin
|
|
388
|
+
next if owner.autoload?(name, false)
|
|
389
|
+
|
|
390
|
+
value = owner.const_get(name, false)
|
|
391
|
+
rescue Exception
|
|
392
|
+
next
|
|
393
|
+
end
|
|
394
|
+
next unless value.is_a?(Module)
|
|
395
|
+
next if seen[value.object_id]
|
|
396
|
+
|
|
397
|
+
seen[value.object_id] = true
|
|
398
|
+
begin
|
|
399
|
+
snap[value] = [value.instance_variables,
|
|
400
|
+
value.class_variables(false)]
|
|
401
|
+
rescue Exception
|
|
402
|
+
next
|
|
403
|
+
end
|
|
404
|
+
value.constants(false).each do |child|
|
|
405
|
+
queue << [value, child.to_s]
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
snap
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# RubyGems, Bundler, and the VM mutate their own module state on
|
|
412
|
+
# every require; that is probe machinery, not the target's doing.
|
|
413
|
+
MACHINERY = /\A(?:Gem|Bundler|RubyVM)(?:::|\z)/
|
|
414
|
+
|
|
415
|
+
# State the boot added to pre-existing modules, reported through
|
|
416
|
+
# the same channels as freshly defined class state. No source
|
|
417
|
+
# location exists for a reopen, and unknown origins count as own.
|
|
418
|
+
def merge_reopened(result, snapshot)
|
|
419
|
+
origin = {"path" => nil, "line" => nil, "own" => true}
|
|
420
|
+
snapshot.each do |mod, (ivars, cvars)|
|
|
421
|
+
new_ivars = mod.instance_variables - ivars
|
|
422
|
+
new_cvars = mod.class_variables(false) - cvars
|
|
423
|
+
next if new_ivars.empty? && new_cvars.empty?
|
|
424
|
+
|
|
425
|
+
full = mod.name || mod.inspect
|
|
426
|
+
next if full.match?(MACHINERY)
|
|
427
|
+
if new_ivars.any?
|
|
428
|
+
unshareable = new_ivars.reject do |ivar|
|
|
429
|
+
safe_shareable?(mod.instance_variable_get(ivar))
|
|
430
|
+
end
|
|
431
|
+
result["class_state"] << origin.merge(
|
|
432
|
+
"const" => full,
|
|
433
|
+
"ivars" => new_ivars.map(&:to_s),
|
|
434
|
+
"unshareable" => unshareable.map(&:to_s)
|
|
435
|
+
)
|
|
436
|
+
end
|
|
437
|
+
if new_cvars.any?
|
|
438
|
+
result["class_variables"] << origin.merge(
|
|
439
|
+
"const" => full, "cvars" => new_cvars.map(&:to_s)
|
|
440
|
+
)
|
|
441
|
+
end
|
|
442
|
+
rescue Exception
|
|
443
|
+
next
|
|
444
|
+
end
|
|
445
|
+
result
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The innermost unshareable node of an unshareable value, with
|
|
449
|
+
# its depth: an unfrozen node with shareable contents just needs
|
|
450
|
+
# freezing, an inherently unshareable one needs replacing.
|
|
451
|
+
# Bounded, because shareability of each child is itself a
|
|
452
|
+
# recursive check.
|
|
453
|
+
def blocker_for(value, depth = 0)
|
|
454
|
+
return [describe_blocker(value), depth] if depth > 5
|
|
455
|
+
|
|
456
|
+
child = children_of(value).find { |c| !safe_shareable?(c) }
|
|
457
|
+
if child
|
|
458
|
+
blocker_for(child, depth + 1)
|
|
459
|
+
else
|
|
460
|
+
[describe_blocker(value), depth]
|
|
461
|
+
end
|
|
462
|
+
rescue Exception
|
|
463
|
+
[describe_blocker(value), depth]
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def children_of(value)
|
|
467
|
+
children =
|
|
468
|
+
case value
|
|
469
|
+
when Hash then value.keys + value.values
|
|
470
|
+
when Array then value
|
|
471
|
+
when Struct, Data then value.deconstruct
|
|
472
|
+
else
|
|
473
|
+
value.instance_variables.map do |ivar|
|
|
474
|
+
value.instance_variable_get(ivar)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
children.first(1000)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# "unfrozen X" only when freezing would actually flip the
|
|
481
|
+
# verdict; a Proc or IO stays unshareable frozen, and naming it
|
|
482
|
+
# unfrozen would send the reader to a fix that cannot work.
|
|
483
|
+
def describe_blocker(value)
|
|
484
|
+
return value.class.to_s if value.frozen?
|
|
485
|
+
|
|
486
|
+
frozen_helps = begin
|
|
487
|
+
Ractor.shareable?(value.dup.freeze)
|
|
488
|
+
rescue Exception
|
|
489
|
+
false
|
|
490
|
+
end
|
|
491
|
+
frozen_helps ? "unfrozen #{value.class}" : value.class.to_s
|
|
492
|
+
end
|
|
493
|
+
|
|
255
494
|
# -- rack --------------------------------------------------------
|
|
256
495
|
|
|
257
496
|
# App objects built in config.ru are almost never shareable (the
|
|
@@ -261,6 +500,8 @@ module AuditionHarness
|
|
|
261
500
|
# and serve one request entirely inside a Ractor.
|
|
262
501
|
def rack(payload)
|
|
263
502
|
config_ru = payload.fetch("config_ru")
|
|
503
|
+
root = payload["root"] || File.dirname(config_ru)
|
|
504
|
+
bundler_setup
|
|
264
505
|
begin
|
|
265
506
|
require "rack" # audition:disable runtime-require
|
|
266
507
|
rescue LoadError
|
|
@@ -268,6 +509,9 @@ module AuditionHarness
|
|
|
268
509
|
end
|
|
269
510
|
|
|
270
511
|
out = {"rack_available" => true}
|
|
512
|
+
baseline = module_state_snapshot
|
|
513
|
+
before = Object.constants
|
|
514
|
+
features = $LOADED_FEATURES.dup # audition:disable global-variables
|
|
271
515
|
begin
|
|
272
516
|
app = Rack::Builder.parse_file(config_ru)
|
|
273
517
|
app = app.first if app.is_a?(Array)
|
|
@@ -276,6 +520,16 @@ module AuditionHarness
|
|
|
276
520
|
rescue Exception => e
|
|
277
521
|
out["main_boot_error"] = describe_error(e)
|
|
278
522
|
end
|
|
523
|
+
# The main-process boot defines the app's constant graph;
|
|
524
|
+
# sweeping it gives rack targets the same backstop as require
|
|
525
|
+
# and rails targets. A failed boot still sweeps what loaded.
|
|
526
|
+
out.merge!(merge_reopened(
|
|
527
|
+
scan(Object.constants - before, root: root,
|
|
528
|
+
limit: payload["max_constants"]), baseline
|
|
529
|
+
))
|
|
530
|
+
out["native_extensions"] = native_extensions(
|
|
531
|
+
features, root, payload["known_compiled"]
|
|
532
|
+
)
|
|
279
533
|
|
|
280
534
|
out["ractor_boot_call"] = rack_in_ractor(config_ru)
|
|
281
535
|
if out["ractor_boot_call"]["ok"]
|
|
@@ -357,20 +611,58 @@ module AuditionHarness
|
|
|
357
611
|
|
|
358
612
|
# -- rails -------------------------------------------------------
|
|
359
613
|
|
|
614
|
+
# The prober sets BUNDLE_GEMFILE to the target's Gemfile; the
|
|
615
|
+
# target's gems must resolve before its boot files load. A
|
|
616
|
+
# setup failure propagates: it is the true boot failure, and
|
|
617
|
+
# the standard reporting captures it. Bundler.setup is called
|
|
618
|
+
# directly because bundler/setup exits on failure, swallowing
|
|
619
|
+
# the message.
|
|
620
|
+
def bundler_setup
|
|
621
|
+
return unless ENV["BUNDLE_GEMFILE"]
|
|
622
|
+
|
|
623
|
+
require "bundler" # audition:disable runtime-require
|
|
624
|
+
Bundler.ui.silence { Bundler.setup }
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# A boot failure does not abandon the sweep: everything defined
|
|
628
|
+
# before the failure is still scanned, so the probe reports what
|
|
629
|
+
# it could reach alongside the boot error.
|
|
360
630
|
def rails(payload)
|
|
361
631
|
environment = payload.fetch("environment")
|
|
632
|
+
bundler_setup
|
|
633
|
+
baseline = module_state_snapshot
|
|
362
634
|
before = Object.constants
|
|
635
|
+
features = $LOADED_FEATURES.dup # audition:disable global-variables
|
|
363
636
|
started = Time.now
|
|
364
|
-
|
|
637
|
+
boot_error = nil
|
|
365
638
|
begin
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
639
|
+
# Absolute requires keep the path as given; realpathing it
|
|
640
|
+
# keeps own-vs-dependency attribution honest under symlinked
|
|
641
|
+
# roots (macOS /var).
|
|
642
|
+
require realpath(environment) # audition:disable runtime-require
|
|
643
|
+
begin
|
|
644
|
+
Rails.application.eager_load!
|
|
645
|
+
rescue Exception
|
|
646
|
+
nil
|
|
647
|
+
end
|
|
648
|
+
rescue Exception => e
|
|
649
|
+
boot_error = describe_error(e)
|
|
369
650
|
end
|
|
370
|
-
boot =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
651
|
+
boot =
|
|
652
|
+
if boot_error
|
|
653
|
+
{"ok" => false, "error" => boot_error}
|
|
654
|
+
else
|
|
655
|
+
{"ok" => true, "seconds" => (Time.now - started).round(1)}
|
|
656
|
+
end
|
|
657
|
+
merge_reopened(
|
|
658
|
+
scan(Object.constants - before, root: payload["root"],
|
|
659
|
+
limit: payload["max_constants"]), baseline
|
|
660
|
+
).merge(
|
|
661
|
+
"boot" => boot,
|
|
662
|
+
"native_extensions" => native_extensions(
|
|
663
|
+
features, payload["root"], payload["known_compiled"]
|
|
664
|
+
)
|
|
665
|
+
)
|
|
374
666
|
rescue Exception => e
|
|
375
667
|
{"boot" => {"ok" => false, "error" => describe_error(e)}}
|
|
376
668
|
end
|