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.

Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +388 -460
  3. data/MIT-LICENSE +1 -1
  4. data/lib/active_support/array_inquirer.rb +4 -2
  5. data/lib/active_support/backtrace_cleaner.rb +3 -3
  6. data/lib/active_support/benchmarkable.rb +1 -1
  7. data/lib/active_support/cache/file_store.rb +3 -3
  8. data/lib/active_support/cache/mem_cache_store.rb +28 -18
  9. data/lib/active_support/cache/memory_store.rb +46 -26
  10. data/lib/active_support/cache/redis_cache_store.rb +25 -25
  11. data/lib/active_support/cache/strategy/local_cache.rb +20 -5
  12. data/lib/active_support/cache.rb +87 -40
  13. data/lib/active_support/callbacks.rb +65 -56
  14. data/lib/active_support/concern.rb +46 -2
  15. data/lib/active_support/configurable.rb +3 -3
  16. data/lib/active_support/configuration_file.rb +51 -0
  17. data/lib/active_support/core_ext/benchmark.rb +2 -2
  18. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  19. data/lib/active_support/core_ext/class/subclasses.rb +17 -38
  20. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  21. data/lib/active_support/core_ext/date_and_time/calculations.rb +13 -0
  22. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  23. data/lib/active_support/core_ext/enumerable.rb +76 -4
  24. data/lib/active_support/core_ext/hash/conversions.rb +2 -2
  25. data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
  26. data/lib/active_support/core_ext/hash/keys.rb +1 -1
  27. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  28. data/lib/active_support/core_ext/load_error.rb +1 -1
  29. data/lib/active_support/core_ext/marshal.rb +2 -0
  30. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  31. data/lib/active_support/core_ext/module/attribute_accessors.rb +23 -29
  32. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +8 -4
  33. data/lib/active_support/core_ext/module/concerning.rb +8 -2
  34. data/lib/active_support/core_ext/module/delegation.rb +38 -28
  35. data/lib/active_support/core_ext/module/introspection.rb +1 -25
  36. data/lib/active_support/core_ext/name_error.rb +29 -2
  37. data/lib/active_support/core_ext/numeric/conversions.rb +22 -18
  38. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  39. data/lib/active_support/core_ext/object/json.rb +12 -1
  40. data/lib/active_support/core_ext/object/try.rb +2 -2
  41. data/lib/active_support/core_ext/range/compare_range.rb +9 -3
  42. data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
  43. data/lib/active_support/core_ext/regexp.rb +8 -1
  44. data/lib/active_support/core_ext/string/access.rb +5 -24
  45. data/lib/active_support/core_ext/string/conversions.rb +1 -0
  46. data/lib/active_support/core_ext/string/inflections.rb +38 -4
  47. data/lib/active_support/core_ext/string/inquiry.rb +1 -0
  48. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  49. data/lib/active_support/core_ext/string/output_safety.rb +3 -4
  50. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  51. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
  52. data/lib/active_support/core_ext/symbol.rb +3 -0
  53. data/lib/active_support/core_ext/time/calculations.rb +17 -0
  54. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  55. data/lib/active_support/core_ext/uri.rb +5 -1
  56. data/lib/active_support/core_ext.rb +1 -1
  57. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  58. data/lib/active_support/current_attributes.rb +8 -2
  59. data/lib/active_support/dependencies.rb +37 -18
  60. data/lib/active_support/deprecation/behaviors.rb +15 -2
  61. data/lib/active_support/deprecation/disallowed.rb +56 -0
  62. data/lib/active_support/deprecation/instance_delegator.rb +0 -1
  63. data/lib/active_support/deprecation/method_wrappers.rb +3 -2
  64. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  65. data/lib/active_support/deprecation/reporting.rb +50 -7
  66. data/lib/active_support/deprecation.rb +6 -1
  67. data/lib/active_support/descendants_tracker.rb +6 -2
  68. data/lib/active_support/duration/iso8601_serializer.rb +15 -9
  69. data/lib/active_support/duration.rb +71 -22
  70. data/lib/active_support/encrypted_file.rb +19 -2
  71. data/lib/active_support/environment_inquirer.rb +20 -0
  72. data/lib/active_support/evented_file_update_checker.rb +69 -133
  73. data/lib/active_support/fork_tracker.rb +64 -0
  74. data/lib/active_support/gem_version.rb +1 -1
  75. data/lib/active_support/hash_with_indifferent_access.rb +48 -24
  76. data/lib/active_support/i18n_railtie.rb +14 -19
  77. data/lib/active_support/inflector/inflections.rb +1 -2
  78. data/lib/active_support/inflector/methods.rb +35 -31
  79. data/lib/active_support/inflector/transliterate.rb +4 -4
  80. data/lib/active_support/json/decoding.rb +4 -4
  81. data/lib/active_support/json/encoding.rb +5 -1
  82. data/lib/active_support/key_generator.rb +1 -1
  83. data/lib/active_support/locale/en.yml +7 -3
  84. data/lib/active_support/log_subscriber.rb +8 -0
  85. data/lib/active_support/logger.rb +1 -1
  86. data/lib/active_support/logger_silence.rb +2 -26
  87. data/lib/active_support/logger_thread_safe_level.rb +34 -12
  88. data/lib/active_support/message_encryptor.rb +4 -7
  89. data/lib/active_support/message_verifier.rb +5 -5
  90. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  91. data/lib/active_support/messages/rotator.rb +6 -5
  92. data/lib/active_support/multibyte/chars.rb +4 -42
  93. data/lib/active_support/multibyte/unicode.rb +9 -83
  94. data/lib/active_support/notifications/fanout.rb +23 -8
  95. data/lib/active_support/notifications/instrumenter.rb +6 -15
  96. data/lib/active_support/notifications.rb +32 -5
  97. data/lib/active_support/number_helper/number_converter.rb +1 -1
  98. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  99. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  100. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  101. data/lib/active_support/number_helper/rounding_helper.rb +12 -28
  102. data/lib/active_support/number_helper.rb +29 -14
  103. data/lib/active_support/option_merger.rb +2 -1
  104. data/lib/active_support/ordered_options.rb +8 -2
  105. data/lib/active_support/parameter_filter.rb +16 -11
  106. data/lib/active_support/per_thread_registry.rb +1 -1
  107. data/lib/active_support/rails.rb +1 -4
  108. data/lib/active_support/railtie.rb +23 -1
  109. data/lib/active_support/rescuable.rb +4 -4
  110. data/lib/active_support/secure_compare_rotator.rb +51 -0
  111. data/lib/active_support/security_utils.rb +19 -12
  112. data/lib/active_support/string_inquirer.rb +4 -2
  113. data/lib/active_support/subscriber.rb +12 -7
  114. data/lib/active_support/tagged_logging.rb +29 -4
  115. data/lib/active_support/testing/assertions.rb +18 -11
  116. data/lib/active_support/testing/parallelization/server.rb +78 -0
  117. data/lib/active_support/testing/parallelization/worker.rb +100 -0
  118. data/lib/active_support/testing/parallelization.rb +12 -95
  119. data/lib/active_support/testing/time_helpers.rb +40 -3
  120. data/lib/active_support/time_with_zone.rb +67 -43
  121. data/lib/active_support/values/time_zone.rb +20 -10
  122. data/lib/active_support/xml_mini/rexml.rb +8 -1
  123. data/lib/active_support.rb +13 -1
  124. metadata +33 -35
  125. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  126. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  127. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  128. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  129. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  130. 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, sign = normalize
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
- "#{sign}#{output}"
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
- # If all parts are negative - let's make a negative duration
44
- sign = ""
45
- if parts.values.all? { |v| v < 0 }
46
- sign = "-"
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
- [parts, sign]
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[:seconds]
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[:seconds]
55
- new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
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.map { |part, other_value| [part, value * other_value] }.to_h
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, [[:seconds, value]])
150
+ new(value, seconds: value)
151
151
  end
152
152
 
153
153
  def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
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, [[:hours, value]])
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, [[:days, value]])
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, [[:weeks, value]])
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, [[:months, value]])
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, [[:years, value]])
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.to_h
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.dup
240
- other.parts.each do |(key, value)|
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[:seconds] + other
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.map { |type, number| [type, number * other.value] })
261
+ Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
260
262
  elsif Numeric === other
261
- Duration.new(value * other, parts.map { |type, number| [type, number * other] })
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.map { |type, number| [type, number / other.value] })
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.map { |type, number| [type, number / other] })
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.map { |type, number| [type, -number] })
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, @key_path = Pathname.new(content_path), Pathname.new(key_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
- key_path.binread.strip if key_path.exist?
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
- @ph = PathHelper.new
42
- @files = files.map { |f| @ph.xpath(f) }.to_set
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
- @boot_mutex.synchronize do
73
- if @pid != Process.pid
74
- boot!
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
- private
109
- def boot!
110
- normalize_dirs!
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
- unless @dtw.empty?
113
- Listen.to(*@dtw, &method(:changed)).start
99
+ def thread_safely
100
+ @mutex.synchronize do
101
+ yield self
114
102
  end
115
103
  end
116
104
 
117
- def shutdown!
118
- Listen.stop
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 = @ph.xpath(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 = @ph.normalize_extension(file.extname)
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 == @lcsp || dir.root?
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) + @dirs.keys
157
- dtw.compact!
158
- dtw.uniq!
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
- class PathHelper
169
- def xpath(path)
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!
@@ -8,7 +8,7 @@ module ActiveSupport
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 6
11
- MINOR = 0
11
+ MINOR = 1
12
12
  TINY = 4
13
13
  PRE = nil
14
14