activesupport 6.0.4 → 6.1.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activesupport might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +388 -460
- data/MIT-LICENSE +1 -1
- data/lib/active_support/array_inquirer.rb +4 -2
- data/lib/active_support/backtrace_cleaner.rb +3 -3
- data/lib/active_support/benchmarkable.rb +1 -1
- data/lib/active_support/cache/file_store.rb +3 -3
- data/lib/active_support/cache/mem_cache_store.rb +28 -18
- data/lib/active_support/cache/memory_store.rb +46 -26
- data/lib/active_support/cache/redis_cache_store.rb +25 -25
- data/lib/active_support/cache/strategy/local_cache.rb +20 -5
- data/lib/active_support/cache.rb +87 -40
- data/lib/active_support/callbacks.rb +65 -56
- data/lib/active_support/concern.rb +46 -2
- data/lib/active_support/configurable.rb +3 -3
- data/lib/active_support/configuration_file.rb +51 -0
- data/lib/active_support/core_ext/benchmark.rb +2 -2
- data/lib/active_support/core_ext/class/attribute.rb +34 -44
- data/lib/active_support/core_ext/class/subclasses.rb +17 -38
- data/lib/active_support/core_ext/date/conversions.rb +2 -1
- data/lib/active_support/core_ext/date_and_time/calculations.rb +13 -0
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
- data/lib/active_support/core_ext/enumerable.rb +76 -4
- data/lib/active_support/core_ext/hash/conversions.rb +2 -2
- data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
- data/lib/active_support/core_ext/hash/keys.rb +1 -1
- data/lib/active_support/core_ext/hash/slice.rb +3 -2
- data/lib/active_support/core_ext/load_error.rb +1 -1
- data/lib/active_support/core_ext/marshal.rb +2 -0
- data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
- data/lib/active_support/core_ext/module/attribute_accessors.rb +23 -29
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +8 -4
- data/lib/active_support/core_ext/module/concerning.rb +8 -2
- data/lib/active_support/core_ext/module/delegation.rb +38 -28
- data/lib/active_support/core_ext/module/introspection.rb +1 -25
- data/lib/active_support/core_ext/name_error.rb +29 -2
- data/lib/active_support/core_ext/numeric/conversions.rb +22 -18
- data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
- data/lib/active_support/core_ext/object/json.rb +12 -1
- data/lib/active_support/core_ext/object/try.rb +2 -2
- data/lib/active_support/core_ext/range/compare_range.rb +9 -3
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
- data/lib/active_support/core_ext/regexp.rb +8 -1
- data/lib/active_support/core_ext/string/access.rb +5 -24
- data/lib/active_support/core_ext/string/conversions.rb +1 -0
- data/lib/active_support/core_ext/string/inflections.rb +38 -4
- data/lib/active_support/core_ext/string/inquiry.rb +1 -0
- data/lib/active_support/core_ext/string/multibyte.rb +2 -2
- data/lib/active_support/core_ext/string/output_safety.rb +3 -4
- data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
- data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
- data/lib/active_support/core_ext/symbol.rb +3 -0
- data/lib/active_support/core_ext/time/calculations.rb +17 -0
- data/lib/active_support/core_ext/time/conversions.rb +2 -0
- data/lib/active_support/core_ext/uri.rb +5 -1
- data/lib/active_support/core_ext.rb +1 -1
- data/lib/active_support/current_attributes/test_helper.rb +13 -0
- data/lib/active_support/current_attributes.rb +8 -2
- data/lib/active_support/dependencies.rb +37 -18
- data/lib/active_support/deprecation/behaviors.rb +15 -2
- data/lib/active_support/deprecation/disallowed.rb +56 -0
- data/lib/active_support/deprecation/instance_delegator.rb +0 -1
- data/lib/active_support/deprecation/method_wrappers.rb +3 -2
- data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
- data/lib/active_support/deprecation/reporting.rb +50 -7
- data/lib/active_support/deprecation.rb +6 -1
- data/lib/active_support/descendants_tracker.rb +6 -2
- data/lib/active_support/duration/iso8601_serializer.rb +15 -9
- data/lib/active_support/duration.rb +71 -22
- data/lib/active_support/encrypted_file.rb +19 -2
- data/lib/active_support/environment_inquirer.rb +20 -0
- data/lib/active_support/evented_file_update_checker.rb +69 -133
- data/lib/active_support/fork_tracker.rb +64 -0
- data/lib/active_support/gem_version.rb +1 -1
- data/lib/active_support/hash_with_indifferent_access.rb +48 -24
- data/lib/active_support/i18n_railtie.rb +14 -19
- data/lib/active_support/inflector/inflections.rb +1 -2
- data/lib/active_support/inflector/methods.rb +35 -31
- data/lib/active_support/inflector/transliterate.rb +4 -4
- data/lib/active_support/json/decoding.rb +4 -4
- data/lib/active_support/json/encoding.rb +5 -1
- data/lib/active_support/key_generator.rb +1 -1
- data/lib/active_support/locale/en.yml +7 -3
- data/lib/active_support/log_subscriber.rb +8 -0
- data/lib/active_support/logger.rb +1 -1
- data/lib/active_support/logger_silence.rb +2 -26
- data/lib/active_support/logger_thread_safe_level.rb +34 -12
- data/lib/active_support/message_encryptor.rb +4 -7
- data/lib/active_support/message_verifier.rb +5 -5
- data/lib/active_support/messages/rotation_configuration.rb +2 -1
- data/lib/active_support/messages/rotator.rb +6 -5
- data/lib/active_support/multibyte/chars.rb +4 -42
- data/lib/active_support/multibyte/unicode.rb +9 -83
- data/lib/active_support/notifications/fanout.rb +23 -8
- data/lib/active_support/notifications/instrumenter.rb +6 -15
- data/lib/active_support/notifications.rb +32 -5
- data/lib/active_support/number_helper/number_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
- data/lib/active_support/number_helper/rounding_helper.rb +12 -28
- data/lib/active_support/number_helper.rb +29 -14
- data/lib/active_support/option_merger.rb +2 -1
- data/lib/active_support/ordered_options.rb +8 -2
- data/lib/active_support/parameter_filter.rb +16 -11
- data/lib/active_support/per_thread_registry.rb +1 -1
- data/lib/active_support/rails.rb +1 -4
- data/lib/active_support/railtie.rb +23 -1
- data/lib/active_support/rescuable.rb +4 -4
- data/lib/active_support/secure_compare_rotator.rb +51 -0
- data/lib/active_support/security_utils.rb +19 -12
- data/lib/active_support/string_inquirer.rb +4 -2
- data/lib/active_support/subscriber.rb +12 -7
- data/lib/active_support/tagged_logging.rb +29 -4
- data/lib/active_support/testing/assertions.rb +18 -11
- data/lib/active_support/testing/parallelization/server.rb +78 -0
- data/lib/active_support/testing/parallelization/worker.rb +100 -0
- data/lib/active_support/testing/parallelization.rb +12 -95
- data/lib/active_support/testing/time_helpers.rb +40 -3
- data/lib/active_support/time_with_zone.rb +67 -43
- data/lib/active_support/values/time_zone.rb +20 -10
- data/lib/active_support/xml_mini/rexml.rb +8 -1
- data/lib/active_support.rb +13 -1
- metadata +33 -35
- data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
- data/lib/active_support/core_ext/hash/compact.rb +0 -5
- data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
- data/lib/active_support/core_ext/module/reachable.rb +0 -6
- data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
- data/lib/active_support/core_ext/range/include_range.rb +0 -9
@@ -6,6 +6,8 @@ module ActiveSupport
|
|
6
6
|
class Duration
|
7
7
|
# Serializes duration to string according to ISO 8601 Duration format.
|
8
8
|
class ISO8601Serializer # :nodoc:
|
9
|
+
DATE_COMPONENTS = %i(years months days)
|
10
|
+
|
9
11
|
def initialize(duration, precision: nil)
|
10
12
|
@duration = duration
|
11
13
|
@precision = precision
|
@@ -13,14 +15,14 @@ module ActiveSupport
|
|
13
15
|
|
14
16
|
# Builds and returns output string.
|
15
17
|
def serialize
|
16
|
-
parts
|
18
|
+
parts = normalize
|
17
19
|
return "PT0S" if parts.empty?
|
18
20
|
|
19
21
|
output = +"P"
|
20
22
|
output << "#{parts[:years]}Y" if parts.key?(:years)
|
21
23
|
output << "#{parts[:months]}M" if parts.key?(:months)
|
22
|
-
output << "#{parts[:weeks]}W" if parts.key?(:weeks)
|
23
24
|
output << "#{parts[:days]}D" if parts.key?(:days)
|
25
|
+
output << "#{parts[:weeks]}W" if parts.key?(:weeks)
|
24
26
|
time = +""
|
25
27
|
time << "#{parts[:hours]}H" if parts.key?(:hours)
|
26
28
|
time << "#{parts[:minutes]}M" if parts.key?(:minutes)
|
@@ -28,7 +30,7 @@ module ActiveSupport
|
|
28
30
|
time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
|
29
31
|
end
|
30
32
|
output << "T#{time}" unless time.empty?
|
31
|
-
|
33
|
+
output
|
32
34
|
end
|
33
35
|
|
34
36
|
private
|
@@ -40,13 +42,17 @@ module ActiveSupport
|
|
40
42
|
parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
|
41
43
|
p[k] += v unless v.zero?
|
42
44
|
end
|
43
|
-
|
44
|
-
|
45
|
-
if parts
|
46
|
-
|
47
|
-
parts.transform_values!(&:-@)
|
45
|
+
|
46
|
+
# Convert weeks to days and remove weeks if mixed with date parts
|
47
|
+
if week_mixed_with_date?(parts)
|
48
|
+
parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
|
48
49
|
end
|
49
|
-
|
50
|
+
|
51
|
+
parts
|
52
|
+
end
|
53
|
+
|
54
|
+
def week_mixed_with_date?(parts)
|
55
|
+
parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
|
50
56
|
end
|
51
57
|
end
|
52
58
|
end
|
@@ -39,7 +39,7 @@ module ActiveSupport
|
|
39
39
|
|
40
40
|
def +(other)
|
41
41
|
if Duration === other
|
42
|
-
seconds = value + other.parts
|
42
|
+
seconds = value + other.parts.fetch(:seconds, 0)
|
43
43
|
new_parts = other.parts.merge(seconds: seconds)
|
44
44
|
new_value = value + other.value
|
45
45
|
|
@@ -51,8 +51,8 @@ module ActiveSupport
|
|
51
51
|
|
52
52
|
def -(other)
|
53
53
|
if Duration === other
|
54
|
-
seconds = value - other.parts
|
55
|
-
new_parts = other.parts.
|
54
|
+
seconds = value - other.parts.fetch(:seconds, 0)
|
55
|
+
new_parts = other.parts.transform_values(&:-@)
|
56
56
|
new_parts = new_parts.merge(seconds: seconds)
|
57
57
|
new_value = value - other.value
|
58
58
|
|
@@ -64,7 +64,7 @@ module ActiveSupport
|
|
64
64
|
|
65
65
|
def *(other)
|
66
66
|
if Duration === other
|
67
|
-
new_parts = other.parts.
|
67
|
+
new_parts = other.parts.transform_values { |other_value| value * other_value }
|
68
68
|
new_value = value * other.value
|
69
69
|
|
70
70
|
Duration.new(new_value, new_parts)
|
@@ -147,31 +147,31 @@ module ActiveSupport
|
|
147
147
|
end
|
148
148
|
|
149
149
|
def seconds(value) #:nodoc:
|
150
|
-
new(value,
|
150
|
+
new(value, seconds: value)
|
151
151
|
end
|
152
152
|
|
153
153
|
def minutes(value) #:nodoc:
|
154
|
-
new(value * SECONDS_PER_MINUTE,
|
154
|
+
new(value * SECONDS_PER_MINUTE, minutes: value)
|
155
155
|
end
|
156
156
|
|
157
157
|
def hours(value) #:nodoc:
|
158
|
-
new(value * SECONDS_PER_HOUR,
|
158
|
+
new(value * SECONDS_PER_HOUR, hours: value)
|
159
159
|
end
|
160
160
|
|
161
161
|
def days(value) #:nodoc:
|
162
|
-
new(value * SECONDS_PER_DAY,
|
162
|
+
new(value * SECONDS_PER_DAY, days: value)
|
163
163
|
end
|
164
164
|
|
165
165
|
def weeks(value) #:nodoc:
|
166
|
-
new(value * SECONDS_PER_WEEK,
|
166
|
+
new(value * SECONDS_PER_WEEK, weeks: value)
|
167
167
|
end
|
168
168
|
|
169
169
|
def months(value) #:nodoc:
|
170
|
-
new(value * SECONDS_PER_MONTH,
|
170
|
+
new(value * SECONDS_PER_MONTH, months: value)
|
171
171
|
end
|
172
172
|
|
173
173
|
def years(value) #:nodoc:
|
174
|
-
new(value * SECONDS_PER_YEAR,
|
174
|
+
new(value * SECONDS_PER_YEAR, years: value)
|
175
175
|
end
|
176
176
|
|
177
177
|
# Creates a new Duration from a seconds value that is converted
|
@@ -181,6 +181,10 @@ module ActiveSupport
|
|
181
181
|
# ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
|
182
182
|
#
|
183
183
|
def build(value)
|
184
|
+
unless value.is_a?(::Numeric)
|
185
|
+
raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
|
186
|
+
end
|
187
|
+
|
184
188
|
parts = {}
|
185
189
|
remainder = value.round(9)
|
186
190
|
|
@@ -206,8 +210,7 @@ module ActiveSupport
|
|
206
210
|
end
|
207
211
|
|
208
212
|
def initialize(value, parts) #:nodoc:
|
209
|
-
@value, @parts = value, parts
|
210
|
-
@parts.default = 0
|
213
|
+
@value, @parts = value, parts
|
211
214
|
@parts.reject! { |k, v| v.zero? } unless value == 0
|
212
215
|
end
|
213
216
|
|
@@ -236,13 +239,12 @@ module ActiveSupport
|
|
236
239
|
# are treated as seconds.
|
237
240
|
def +(other)
|
238
241
|
if Duration === other
|
239
|
-
parts = @parts.
|
240
|
-
|
241
|
-
parts[key] += value
|
242
|
+
parts = @parts.merge(other.parts) do |_key, value, other_value|
|
243
|
+
value + other_value
|
242
244
|
end
|
243
245
|
Duration.new(value + other.value, parts)
|
244
246
|
else
|
245
|
-
seconds = @parts
|
247
|
+
seconds = @parts.fetch(:seconds, 0) + other
|
246
248
|
Duration.new(value + other, @parts.merge(seconds: seconds))
|
247
249
|
end
|
248
250
|
end
|
@@ -256,9 +258,9 @@ module ActiveSupport
|
|
256
258
|
# Multiplies this Duration by a Numeric and returns a new Duration.
|
257
259
|
def *(other)
|
258
260
|
if Scalar === other || Duration === other
|
259
|
-
Duration.new(value * other.value, parts.
|
261
|
+
Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
|
260
262
|
elsif Numeric === other
|
261
|
-
Duration.new(value * other, parts.
|
263
|
+
Duration.new(value * other, parts.transform_values { |number| number * other })
|
262
264
|
else
|
263
265
|
raise_type_error(other)
|
264
266
|
end
|
@@ -267,11 +269,11 @@ module ActiveSupport
|
|
267
269
|
# Divides this Duration by a Numeric and returns a new Duration.
|
268
270
|
def /(other)
|
269
271
|
if Scalar === other
|
270
|
-
Duration.new(value / other.value, parts.
|
272
|
+
Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
|
271
273
|
elsif Duration === other
|
272
274
|
value / other.value
|
273
275
|
elsif Numeric === other
|
274
|
-
Duration.new(value / other, parts.
|
276
|
+
Duration.new(value / other, parts.transform_values { |number| number / other })
|
275
277
|
else
|
276
278
|
raise_type_error(other)
|
277
279
|
end
|
@@ -290,7 +292,11 @@ module ActiveSupport
|
|
290
292
|
end
|
291
293
|
|
292
294
|
def -@ #:nodoc:
|
293
|
-
Duration.new(-value, parts.
|
295
|
+
Duration.new(-value, parts.transform_values(&:-@))
|
296
|
+
end
|
297
|
+
|
298
|
+
def +@ #:nodoc:
|
299
|
+
self
|
294
300
|
end
|
295
301
|
|
296
302
|
def is_a?(klass) #:nodoc:
|
@@ -343,6 +349,49 @@ module ActiveSupport
|
|
343
349
|
def to_i
|
344
350
|
@value.to_i
|
345
351
|
end
|
352
|
+
alias :in_seconds :to_i
|
353
|
+
|
354
|
+
# Returns the amount of minutes a duration covers as a float
|
355
|
+
#
|
356
|
+
# 1.day.in_minutes # => 1440.0
|
357
|
+
def in_minutes
|
358
|
+
in_seconds / SECONDS_PER_MINUTE.to_f
|
359
|
+
end
|
360
|
+
|
361
|
+
# Returns the amount of hours a duration covers as a float
|
362
|
+
#
|
363
|
+
# 1.day.in_hours # => 24.0
|
364
|
+
def in_hours
|
365
|
+
in_seconds / SECONDS_PER_HOUR.to_f
|
366
|
+
end
|
367
|
+
|
368
|
+
# Returns the amount of days a duration covers as a float
|
369
|
+
#
|
370
|
+
# 12.hours.in_days # => 0.5
|
371
|
+
def in_days
|
372
|
+
in_seconds / SECONDS_PER_DAY.to_f
|
373
|
+
end
|
374
|
+
|
375
|
+
# Returns the amount of weeks a duration covers as a float
|
376
|
+
#
|
377
|
+
# 2.months.in_weeks # => 8.696
|
378
|
+
def in_weeks
|
379
|
+
in_seconds / SECONDS_PER_WEEK.to_f
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns the amount of months a duration covers as a float
|
383
|
+
#
|
384
|
+
# 9.weeks.in_months # => 2.07
|
385
|
+
def in_months
|
386
|
+
in_seconds / SECONDS_PER_MONTH.to_f
|
387
|
+
end
|
388
|
+
|
389
|
+
# Returns the amount of years a duration covers as a float
|
390
|
+
#
|
391
|
+
# 30.days.in_years # => 0.082
|
392
|
+
def in_years
|
393
|
+
in_seconds / SECONDS_PER_YEAR.to_f
|
394
|
+
end
|
346
395
|
|
347
396
|
# Returns +true+ if +other+ is also a Duration instance, which has the
|
348
397
|
# same parts as this one.
|
@@ -20,17 +20,28 @@ module ActiveSupport
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
class InvalidKeyLengthError < RuntimeError
|
24
|
+
def initialize
|
25
|
+
super "Encryption key must be exactly #{EncryptedFile.expected_key_length} characters."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
23
29
|
CIPHER = "aes-128-gcm"
|
24
30
|
|
25
31
|
def self.generate_key
|
26
32
|
SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
|
27
33
|
end
|
28
34
|
|
35
|
+
def self.expected_key_length # :nodoc:
|
36
|
+
@expected_key_length ||= generate_key.length
|
37
|
+
end
|
38
|
+
|
29
39
|
|
30
40
|
attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
|
31
41
|
|
32
42
|
def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
|
33
|
-
@content_path
|
43
|
+
@content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path }
|
44
|
+
@key_path = Pathname.new(key_path)
|
34
45
|
@env_key, @raise_if_missing_key = env_key, raise_if_missing_key
|
35
46
|
end
|
36
47
|
|
@@ -73,6 +84,7 @@ module ActiveSupport
|
|
73
84
|
|
74
85
|
|
75
86
|
def encrypt(contents)
|
87
|
+
check_key_length
|
76
88
|
encryptor.encrypt_and_sign contents
|
77
89
|
end
|
78
90
|
|
@@ -90,11 +102,16 @@ module ActiveSupport
|
|
90
102
|
end
|
91
103
|
|
92
104
|
def read_key_file
|
93
|
-
|
105
|
+
return @key_file_contents if defined?(@key_file_contents)
|
106
|
+
@key_file_contents = (key_path.binread.strip if key_path.exist?)
|
94
107
|
end
|
95
108
|
|
96
109
|
def handle_missing_key
|
97
110
|
raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
|
98
111
|
end
|
112
|
+
|
113
|
+
def check_key_length
|
114
|
+
raise InvalidKeyLengthError if key&.length != self.class.expected_key_length
|
115
|
+
end
|
99
116
|
end
|
100
117
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/string_inquirer"
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
class EnvironmentInquirer < StringInquirer #:nodoc:
|
7
|
+
DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
|
8
|
+
def initialize(env)
|
9
|
+
super(env)
|
10
|
+
|
11
|
+
DEFAULT_ENVIRONMENTS.each do |default|
|
12
|
+
instance_variable_set :"@#{default}", env == default
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
DEFAULT_ENVIRONMENTS.each do |env|
|
17
|
+
class_eval "def #{env}?; @#{env}; end"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "set"
|
4
4
|
require "pathname"
|
5
5
|
require "concurrent/atomic/atomic_boolean"
|
6
|
+
require "listen"
|
7
|
+
require "active_support/fork_tracker"
|
6
8
|
|
7
9
|
module ActiveSupport
|
8
10
|
# Allows you to "listen" to changes in a file system.
|
@@ -38,62 +40,22 @@ module ActiveSupport
|
|
38
40
|
raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
|
39
41
|
end
|
40
42
|
|
41
|
-
@
|
42
|
-
@
|
43
|
-
|
44
|
-
@dirs = {}
|
45
|
-
dirs.each do |dir, exts|
|
46
|
-
@dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
|
47
|
-
end
|
48
|
-
|
49
|
-
@block = block
|
50
|
-
@updated = Concurrent::AtomicBoolean.new(false)
|
51
|
-
@lcsp = @ph.longest_common_subpath(@dirs.keys)
|
52
|
-
@pid = Process.pid
|
53
|
-
@boot_mutex = Mutex.new
|
54
|
-
|
55
|
-
dtw = directories_to_watch
|
56
|
-
@dtw, @missing = dtw.partition(&:exist?)
|
57
|
-
|
58
|
-
if @dtw.any?
|
59
|
-
# Loading listen triggers warnings. These are originated by a legit
|
60
|
-
# usage of attr_* macros for private attributes, but adds a lot of noise
|
61
|
-
# to our test suite. Thus, we lazy load it and disable warnings locally.
|
62
|
-
silence_warnings do
|
63
|
-
require "listen"
|
64
|
-
rescue LoadError => e
|
65
|
-
raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
|
66
|
-
end
|
67
|
-
end
|
68
|
-
boot!
|
43
|
+
@block = block
|
44
|
+
@core = Core.new(files, dirs)
|
45
|
+
ObjectSpace.define_finalizer(self, @core.finalizer)
|
69
46
|
end
|
70
47
|
|
71
48
|
def updated?
|
72
|
-
@
|
73
|
-
|
74
|
-
|
75
|
-
@pid = Process.pid
|
76
|
-
@updated.make_true
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
if @missing.any?(&:exist?)
|
81
|
-
@boot_mutex.synchronize do
|
82
|
-
appeared, @missing = @missing.partition(&:exist?)
|
83
|
-
shutdown!
|
84
|
-
|
85
|
-
@dtw += appeared
|
86
|
-
boot!
|
87
|
-
|
88
|
-
@updated.make_true
|
89
|
-
end
|
49
|
+
if @core.restart?
|
50
|
+
@core.thread_safely(&:restart)
|
51
|
+
@core.updated.make_true
|
90
52
|
end
|
91
53
|
|
92
|
-
@updated.true?
|
54
|
+
@core.updated.true?
|
93
55
|
end
|
94
56
|
|
95
57
|
def execute
|
96
|
-
@updated.make_false
|
58
|
+
@core.updated.make_false
|
97
59
|
@block.call
|
98
60
|
end
|
99
61
|
|
@@ -105,17 +67,59 @@ module ActiveSupport
|
|
105
67
|
end
|
106
68
|
end
|
107
69
|
|
108
|
-
|
109
|
-
|
110
|
-
|
70
|
+
class Core
|
71
|
+
attr_reader :updated
|
72
|
+
|
73
|
+
def initialize(files, dirs)
|
74
|
+
@files = files.map { |file| Pathname(file).expand_path }.to_set
|
75
|
+
|
76
|
+
@dirs = dirs.each_with_object({}) do |(dir, exts), hash|
|
77
|
+
hash[Pathname(dir).expand_path] = Array(exts).map { |ext| ext.to_s.sub(/\A\.?/, ".") }.to_set
|
78
|
+
end
|
79
|
+
|
80
|
+
@common_path = common_path(@dirs.keys)
|
81
|
+
|
82
|
+
@dtw = directories_to_watch
|
83
|
+
@missing = []
|
84
|
+
|
85
|
+
@updated = Concurrent::AtomicBoolean.new(false)
|
86
|
+
@mutex = Mutex.new
|
87
|
+
|
88
|
+
start
|
89
|
+
@after_fork = ActiveSupport::ForkTracker.after_fork { start }
|
90
|
+
end
|
91
|
+
|
92
|
+
def finalizer
|
93
|
+
proc do
|
94
|
+
stop
|
95
|
+
ActiveSupport::ForkTracker.unregister(@after_fork)
|
96
|
+
end
|
97
|
+
end
|
111
98
|
|
112
|
-
|
113
|
-
|
99
|
+
def thread_safely
|
100
|
+
@mutex.synchronize do
|
101
|
+
yield self
|
114
102
|
end
|
115
103
|
end
|
116
104
|
|
117
|
-
def
|
118
|
-
|
105
|
+
def start
|
106
|
+
normalize_dirs!
|
107
|
+
@dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
|
108
|
+
@listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
|
109
|
+
@listener&.start
|
110
|
+
end
|
111
|
+
|
112
|
+
def stop
|
113
|
+
@listener&.stop
|
114
|
+
end
|
115
|
+
|
116
|
+
def restart
|
117
|
+
stop
|
118
|
+
start
|
119
|
+
end
|
120
|
+
|
121
|
+
def restart?
|
122
|
+
@missing.any?(&:exist?)
|
119
123
|
end
|
120
124
|
|
121
125
|
def normalize_dirs!
|
@@ -125,27 +129,27 @@ module ActiveSupport
|
|
125
129
|
end
|
126
130
|
|
127
131
|
def changed(modified, added, removed)
|
128
|
-
unless updated?
|
132
|
+
unless @updated.true?
|
129
133
|
@updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
|
130
134
|
end
|
131
135
|
end
|
132
136
|
|
133
137
|
def watching?(file)
|
134
|
-
file =
|
138
|
+
file = Pathname(file)
|
135
139
|
|
136
140
|
if @files.member?(file)
|
137
141
|
true
|
138
142
|
elsif file.directory?
|
139
143
|
false
|
140
144
|
else
|
141
|
-
ext =
|
145
|
+
ext = file.extname
|
142
146
|
|
143
147
|
file.dirname.ascend do |dir|
|
144
148
|
matching = @dirs[dir]
|
145
149
|
|
146
150
|
if matching && (matching.empty? || matching.include?(ext))
|
147
151
|
break true
|
148
|
-
elsif dir == @
|
152
|
+
elsif dir == @common_path || dir.root?
|
149
153
|
break false
|
150
154
|
end
|
151
155
|
end
|
@@ -153,82 +157,14 @@ module ActiveSupport
|
|
153
157
|
end
|
154
158
|
|
155
159
|
def directories_to_watch
|
156
|
-
dtw = @files.map(&:dirname)
|
157
|
-
dtw.
|
158
|
-
dtw.
|
159
|
-
|
160
|
-
normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
|
161
|
-
dtw = dtw.reject do |path|
|
162
|
-
normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) }
|
163
|
-
end
|
164
|
-
|
165
|
-
@ph.filter_out_descendants(dtw)
|
160
|
+
dtw = @dirs.keys | @files.map(&:dirname)
|
161
|
+
accounted_for = dtw.to_set + Gem.path.map { |path| Pathname(path) }
|
162
|
+
dtw.reject { |dir| dir.ascend.drop(1).any? { |parent| accounted_for.include?(parent) } }
|
166
163
|
end
|
167
164
|
|
168
|
-
|
169
|
-
|
170
|
-
Pathname.new(path).expand_path
|
171
|
-
end
|
172
|
-
|
173
|
-
def normalize_extension(ext)
|
174
|
-
ext.to_s.sub(/\A\./, "")
|
175
|
-
end
|
176
|
-
|
177
|
-
# Given a collection of Pathname objects returns the longest subpath
|
178
|
-
# common to all of them, or +nil+ if there is none.
|
179
|
-
def longest_common_subpath(paths)
|
180
|
-
return if paths.empty?
|
181
|
-
|
182
|
-
lcsp = Pathname.new(paths[0])
|
183
|
-
|
184
|
-
paths[1..-1].each do |path|
|
185
|
-
until ascendant_of?(lcsp, path)
|
186
|
-
if lcsp.root?
|
187
|
-
# If we get here a root directory is not an ascendant of path.
|
188
|
-
# This may happen if there are paths in different drives on
|
189
|
-
# Windows.
|
190
|
-
return
|
191
|
-
else
|
192
|
-
lcsp = lcsp.parent
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
lcsp
|
198
|
-
end
|
199
|
-
|
200
|
-
# Returns the deepest existing ascendant, which could be the argument itself.
|
201
|
-
def existing_parent(dir)
|
202
|
-
dir.ascend do |ascendant|
|
203
|
-
break ascendant if ascendant.directory?
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# Filters out directories which are descendants of others in the collection (stable).
|
208
|
-
def filter_out_descendants(dirs)
|
209
|
-
return dirs if dirs.length < 2
|
210
|
-
|
211
|
-
dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
|
212
|
-
descendants = []
|
213
|
-
|
214
|
-
until dirs_sorted_by_nparts.empty?
|
215
|
-
dir = dirs_sorted_by_nparts.shift
|
216
|
-
|
217
|
-
dirs_sorted_by_nparts.reject! do |possible_descendant|
|
218
|
-
ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# Array#- preserves order.
|
223
|
-
dirs - descendants
|
224
|
-
end
|
225
|
-
|
226
|
-
private
|
227
|
-
def ascendant_of?(base, other)
|
228
|
-
base != other && other.ascend do |ascendant|
|
229
|
-
break true if base == ascendant
|
230
|
-
end
|
231
|
-
end
|
165
|
+
def common_path(paths)
|
166
|
+
paths.map { |path| path.ascend.to_a }.reduce(&:&)&.first
|
232
167
|
end
|
168
|
+
end
|
233
169
|
end
|
234
170
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
module ForkTracker # :nodoc:
|
5
|
+
module CoreExt
|
6
|
+
def fork(*)
|
7
|
+
if block_given?
|
8
|
+
super do
|
9
|
+
ForkTracker.check!
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
else
|
13
|
+
unless pid = super
|
14
|
+
ForkTracker.check!
|
15
|
+
end
|
16
|
+
pid
|
17
|
+
end
|
18
|
+
end
|
19
|
+
ruby2_keywords(:fork) if respond_to?(:ruby2_keywords, true)
|
20
|
+
end
|
21
|
+
|
22
|
+
module CoreExtPrivate
|
23
|
+
include CoreExt
|
24
|
+
|
25
|
+
private
|
26
|
+
def fork(*)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
ruby2_keywords(:fork) if respond_to?(:ruby2_keywords, true)
|
30
|
+
end
|
31
|
+
|
32
|
+
@pid = Process.pid
|
33
|
+
@callbacks = []
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def check!
|
37
|
+
if @pid != Process.pid
|
38
|
+
@callbacks.each(&:call)
|
39
|
+
@pid = Process.pid
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def hook!
|
44
|
+
if Process.respond_to?(:fork)
|
45
|
+
::Object.prepend(CoreExtPrivate)
|
46
|
+
::Kernel.prepend(CoreExtPrivate)
|
47
|
+
::Kernel.singleton_class.prepend(CoreExt)
|
48
|
+
::Process.singleton_class.prepend(CoreExt)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_fork(&block)
|
53
|
+
@callbacks << block
|
54
|
+
block
|
55
|
+
end
|
56
|
+
|
57
|
+
def unregister(callback)
|
58
|
+
@callbacks.delete(callback)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
ActiveSupport::ForkTracker.hook!
|