spring 4.0.0 → 4.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f13e2a4d08faffc1b15c2f1dfe801a3b0b6a12b80d7a66a39601e74ccf3feff
4
- data.tar.gz: 60c63476055ff6050668abfc79489f7c6fbbfc9a7b3a11ea998cb5b2ab86ef9a
3
+ metadata.gz: a37d78748e084596d151cde2918d30799c8dfe9e10e029920ece9e9eee8118ad
4
+ data.tar.gz: c47e0897d3938b7350245854672812436505379cc498638c3eca9e2d52537fe6
5
5
  SHA512:
6
- metadata.gz: 9c467ea15cea498f05ec7e5211bb014163e37a75557d9694e03cc4d6d6e94d4b62c6edcede752e61c4ff51accf472939150c7fc01901aab94cd60d4d06c8a8b1
7
- data.tar.gz: 6d363498591c949b3a49885c38d8694db924d9a2b24c2cc8499236d67739f4cb863eff5db114385bba8cb02bf996af48acd892cbf5e2edebe495bf3bdfffca93
6
+ metadata.gz: cda176e1dd4d22cabb680cb85c4fd562c256f8fb3dc04f9064fc61f910cfb384f044f63bee74a424501505b62f80e0746f6c05bddf327c6be0150f12cc678a3f
7
+ data.tar.gz: e8b1675241b3fc42c52eabb9e2f501d4abae16c35ae3e3d19080de332f3d28b258b7245c127264326871f09c35c333a3b71fcfb8fbbfecdc87648a72e1328c14
data/README.md CHANGED
@@ -16,8 +16,9 @@ boot it every time you run a test, rake task or migration.
16
16
 
17
17
  ## Compatibility
18
18
 
19
- * Ruby versions: MRI 2.7, MRI 3.0, MRI 3.1
20
- * Rails versions: 6.0, 6.1, 7.0
19
+ * Ruby versions: MRI 3.1+
20
+ * Rails versions: 7.1+
21
+ * Bundler v2.1+
21
22
 
22
23
  Spring makes extensive use of `Process.fork`, so won't be able to
23
24
  provide a speed up on platforms which don't support forking (Windows, JRuby).
@@ -70,11 +71,39 @@ Rails directory.
70
71
  Spring reloads application code, and therefore needs the application to have
71
72
  reloading enabled.
72
73
 
73
- Please, make sure `config.cache_classes` is `false` in the environments that
74
+ Ensure that `config.enable_reloading` is `true` in the environments that
74
75
  Spring manages. That setting is typically configured in
75
- `config/environments/*.rb`. In particular, make sure it is `false` for the
76
+ `config/environments/*.rb`. In particular, make sure it is `true` for the
76
77
  `test` environment.
77
78
 
79
+ Note: in versions of Rails before 7.1, the setting is called `cache_classes`,
80
+ and it needs to be `false` for Spring to work.
81
+
82
+ #### Running Spring with reloading disabled (advanced, risky)
83
+
84
+ If you know what you are doing, and if every reloadable resource is configured
85
+ to materialize *lazily in the forked child*, not in the Spring server process,
86
+ then you can decide to run Spring without Rail's in-process reloaders.
87
+
88
+ To opt into this, set in `config/spring.rb`:
89
+
90
+ ```ruby
91
+ Spring.dangerously_allow_disabling_reloading = true
92
+ ```
93
+
94
+ If you set this, you should make sure that you are NOT pre-loading reloadable
95
+ resource in the Spring server. One approach is to add boot-time assertions at
96
+ the end of `Spring.after_environment_load`, e.g.:
97
+
98
+ ```ruby
99
+ Spring.after_environment_load do
100
+ raise "routes were drawn at boot" if Rails.application.routes_reloader.loaded
101
+ raise "i18n locales where loaded at boot" unless I18n.backend.instance_variable_get(:@translations).nil?
102
+ end
103
+ ```
104
+
105
+ When unsure, leave this disabled.
106
+
78
107
  ### Usage
79
108
 
80
109
  For this walkthrough I've generated a new Rails application, and run
@@ -330,7 +359,8 @@ server process is started, it can be used to add new top-level commands.
330
359
  Spring must know how to find your Rails application. If you have a
331
360
  normal app everything works out of the box. If you are working on a
332
361
  project with a special setup (an engine for example), you must tell
333
- Spring where your app is located:
362
+ Spring where your app is located by specifying the below code in a
363
+ `config/spring.rb` file relative to your engines root directory :
334
364
 
335
365
  ```ruby
336
366
  Spring.application_root = './test/dummy'
@@ -388,6 +418,12 @@ a command runs:
388
418
  Spring.quiet = true
389
419
  ```
390
420
 
421
+ You can also set the initial state of the `quiet` configuration option to true
422
+ by setting the `SPRING_QUIET` environment variable before executing Spring.
423
+ This is useful if you want to set quiet mode when invoking the Spring executable
424
+ in a subprocess, and cannot or prefer not to set it programmatically
425
+ via the `Spring.quiet` option in `~/.spring.rb` or the app's `config/spring.rb`.
426
+
391
427
  ### Environment variables
392
428
 
393
429
  The following environment variables are used by Spring:
@@ -412,6 +448,8 @@ The following environment variables are used by Spring:
412
448
  the long-running Spring server process. By default, this is related to
413
449
  the socket path; if the socket path is `/foo/bar/spring.sock` the
414
450
  pidfile will be `/foo/bar/spring.pid`.
451
+ * `SPRING_QUIET` - If set, the initial state of the `Spring.quiet`
452
+ configuration option will default to `true`.
415
453
  * `SPRING_SERVER_COMMAND` - The command to run to start up the Spring
416
454
  server when it is not already running. Defaults to `spring _[version]_
417
455
  server --background`.
@@ -11,9 +11,15 @@ app = Spring::Application.new(
11
11
 
12
12
  Signal.trap("TERM") { app.terminate }
13
13
 
14
- Spring::ProcessTitleUpdater.run { |distance|
15
- "spring app | #{app.app_name} | started #{distance} ago | #{app.app_env} mode"
16
- }
14
+ Spring::ProcessTitleUpdater.run do |distance|
15
+ attributes = [
16
+ app.app_name,
17
+ "started #{distance} ago",
18
+ "#{app.app_env} mode",
19
+ app.spawn_env,
20
+ ].compact
21
+ "spring app | #{attributes.join(" | ")}"
22
+ end
17
23
 
18
24
  app.eager_preload if ENV.delete("SPRING_PRELOAD") == "1"
19
25
  app.run
@@ -1,5 +1,4 @@
1
1
  require "spring/boot"
2
- require "set"
3
2
  require "pty"
4
3
 
5
4
  module Spring
@@ -29,6 +28,11 @@ module Spring
29
28
  @interrupt.last.write "."
30
29
  end
31
30
 
31
+ def spawn_env
32
+ env = JSON.load(ENV["SPRING_SPAWN_ENV"].dup).map { |key, value| "#{key}=#{value}" }
33
+ env.join(", ") if env.any?
34
+ end
35
+
32
36
  def app_env
33
37
  ENV['RAILS_ENV']
34
38
  end
@@ -97,16 +101,28 @@ module Spring
97
101
  end
98
102
 
99
103
  Rails::Application.initializer :ensure_reloading_is_enabled, group: :all do
104
+ next if Spring.dangerously_allow_disabling_reloading
105
+
100
106
  if Rails.application.config.cache_classes
107
+ config_name, set_to = if Rails.application.config.respond_to?(:enable_reloading=)
108
+ ["enable_reloading", "true"]
109
+ else
110
+ ["cache_classes", "false"]
111
+ end
101
112
  raise <<-MSG.strip_heredoc
102
113
  Spring reloads, and therefore needs the application to have reloading enabled.
103
- Please, set config.cache_classes to false in config/environments/#{Rails.env}.rb.
114
+ Please, set config.#{config_name} to #{set_to} in config/environments/#{Rails.env}.rb.
115
+ (If you understand the trade-offs and want to disable Rails' reloader anyway,
116
+ set `Spring.dangerously_allow_disabling_reloading = true` in config/spring.rb.)
104
117
  MSG
105
118
  end
106
119
  end
107
120
 
108
121
  require Spring.application_root_path.join("config", "environment")
109
122
 
123
+ invoke_after_environment_load_callbacks
124
+ preload_framework_base_classes
125
+
110
126
  disconnect_database
111
127
 
112
128
  @preloaded = :success
@@ -120,6 +136,12 @@ module Spring
120
136
 
121
137
  if defined?(Rails) && Rails.application
122
138
  watcher.add Rails.application.paths["config/initializers"]
139
+ rails_root = Rails.root.to_s
140
+ Rails::Engine.subclasses.each do |engine|
141
+ if engine.root.to_s.start_with?(rails_root)
142
+ watcher.add engine.paths["config/initializers"].expanded
143
+ end
144
+ end
123
145
  watcher.add Rails.application.paths["config/database"]
124
146
  if secrets_path = Rails.application.paths["config/secrets"]
125
147
  watcher.add secrets_path
@@ -127,8 +149,26 @@ module Spring
127
149
  end
128
150
  end
129
151
 
152
+ # Eagerly autoload framework base classes
153
+ FRAMEWORK_BASE_CLASSES = %w[
154
+ ActionMailer::Base
155
+ ActionController::Base
156
+ ActionController::API
157
+ ].freeze
158
+
159
+ def preload_framework_base_classes
160
+ FRAMEWORK_BASE_CLASSES.each do |const|
161
+ Object.const_get(const) if Object.const_defined?(const)
162
+ end
163
+ end
164
+
130
165
  def eager_preload
131
- with_pty { preload }
166
+ with_pty do
167
+ # we can't see stderr and there could be issues when it's overflown
168
+ # see https://github.com/rails/spring/issues/396
169
+ STDERR.reopen("/dev/null")
170
+ preload
171
+ end
132
172
  end
133
173
 
134
174
  def run
@@ -163,7 +203,7 @@ module Spring
163
203
  client.puts(0) # preload success
164
204
  rescue Exception
165
205
  log "preload failed"
166
- client.puts(1) # preload failure
206
+ ignore_client_disconnect { client.puts(1) } # preload failure
167
207
  raise
168
208
  end
169
209
  end
@@ -224,15 +264,19 @@ module Spring
224
264
 
225
265
  wait pid, streams, client
226
266
  rescue Exception => e
227
- log "exception: #{e}"
267
+ if e.is_a?(Errno::EPIPE)
268
+ log "client disconnected (#{e.message}), ignoring command"
269
+ else
270
+ log "exception: #{e}"
271
+ end
228
272
  manager.puts unless pid
229
273
 
230
274
  if streams && !e.is_a?(SystemExit)
231
- print_exception(stderr, e)
232
- streams.each(&:close)
275
+ ignore_client_disconnect { print_exception(stderr, e) }
276
+ streams.each { |stream| ignore_client_disconnect { stream.close } }
233
277
  end
234
278
 
235
- client.puts(1) if pid
279
+ ignore_client_disconnect { client.puts(1) if pid }
236
280
  client.close
237
281
  ensure
238
282
  # Redirect STDOUT and STDERR to prevent from keeping the original FDs
@@ -280,6 +324,12 @@ module Spring
280
324
  end
281
325
  end
282
326
 
327
+ def invoke_after_environment_load_callbacks
328
+ Spring.after_environment_load_callbacks.each do |callback|
329
+ callback.call
330
+ end
331
+ end
332
+
283
333
  def loaded_application_features
284
334
  root = Spring.application_root_path.to_s
285
335
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
@@ -298,13 +348,28 @@ module Spring
298
348
  Kernel.module_eval do
299
349
  old_raise = Kernel.method(:raise)
300
350
  remove_method :raise
301
- define_method :raise do |*args|
302
- begin
303
- old_raise.call(*args)
304
- ensure
305
- if $!
306
- lib = File.expand_path("..", __FILE__)
307
- $!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
351
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0')
352
+ define_method :raise do |*args, **kwargs|
353
+ begin
354
+ old_raise.call(*args, **kwargs)
355
+ ensure
356
+ if $!
357
+ lib = File.expand_path("..", __FILE__)
358
+ $!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
359
+ $!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
360
+ end
361
+ end
362
+ end
363
+ else
364
+ define_method :raise do |*args|
365
+ begin
366
+ old_raise.call(*args)
367
+ ensure
368
+ if $!
369
+ lib = File.expand_path("..", __FILE__)
370
+ $!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
371
+ $!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
372
+ end
308
373
  end
309
374
  end
310
375
  end
@@ -343,10 +408,10 @@ module Spring
343
408
  Spring.failsafe_thread {
344
409
  begin
345
410
  _, status = Process.wait2 pid
346
- log "#{pid} exited with #{status.exitstatus}"
411
+ log "#{pid} exited with #{status.exitstatus || status.inspect}"
347
412
 
348
413
  streams.each(&:close)
349
- client.puts(status.exitstatus)
414
+ client.puts(status.exitstatus || status.to_i)
350
415
  client.close
351
416
  ensure
352
417
  @mutex.synchronize { @waiting.delete pid }
@@ -368,6 +433,15 @@ module Spring
368
433
 
369
434
  private
370
435
 
436
+ # Tolerate Errno::EPIPE on writes to the client socket. Once the client
437
+ # disconnects, every subsequent write to it raises — that's expected
438
+ # during `serve`'s status reporting; we'd be talking to a process
439
+ # that's gone.
440
+ def ignore_client_disconnect
441
+ yield
442
+ rescue Errno::EPIPE
443
+ end
444
+
371
445
  def active_record_configured?
372
446
  defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
373
447
  end
@@ -1,9 +1,10 @@
1
1
  module Spring
2
2
  class ApplicationManager
3
- attr_reader :pid, :child, :app_env, :spring_env, :status
3
+ attr_reader :pid, :child, :app_env, :spawn_env, :spring_env, :status
4
4
 
5
- def initialize(app_env, spring_env)
5
+ def initialize(app_env, spawn_env, spring_env)
6
6
  @app_env = app_env
7
+ @spawn_env = spawn_env
7
8
  @spring_env = spring_env
8
9
  @mutex = Mutex.new
9
10
  @state = :running
@@ -41,7 +42,7 @@ module Spring
41
42
  if alive?
42
43
  begin
43
44
  yield
44
- rescue Errno::ECONNRESET, Errno::EPIPE
45
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
45
46
  # The child has died but has not been collected by the wait thread yet,
46
47
  # so start a new child and try again.
47
48
  log "child dead; starting"
@@ -93,14 +94,16 @@ module Spring
93
94
  def start_child(preload = false)
94
95
  @child, child_socket = UNIXSocket.pair
95
96
 
96
- Bundler.with_unbundled_env do
97
+ Bundler.with_original_env do
97
98
  bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
98
99
  @pid = Process.spawn(
99
100
  {
100
101
  "RAILS_ENV" => app_env,
101
102
  "RACK_ENV" => app_env,
102
103
  "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
103
- "SPRING_PRELOAD" => preload ? "1" : "0"
104
+ "SPRING_PRELOAD" => preload ? "1" : "0",
105
+ "SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact),
106
+ **spawn_env,
104
107
  },
105
108
  "ruby",
106
109
  *(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
@@ -123,7 +126,8 @@ module Spring
123
126
  # as if it does we're no longer interested in the child
124
127
  loop do
125
128
  IO.select([child])
126
- break if child.recv(1, Socket::MSG_PEEK).empty?
129
+ peek = child.recv(1, Socket::MSG_PEEK)
130
+ break if peek.nil? || peek.empty?
127
131
  sleep 0.01
128
132
  end
129
133
 
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module Spring
4
2
  module Client
5
3
  class Binstub < Command
@@ -22,13 +20,13 @@ module Spring
22
20
  SPRING = <<~CODE
23
21
  #!/usr/bin/env ruby
24
22
 
25
- # This file loads Spring without using loading other gems in the Gemfile, in order to be fast.
23
+ # This file loads Spring without loading other gems in the Gemfile in order to be fast.
26
24
  # It gets overwritten when you run the `spring binstub` command.
27
25
 
28
26
  if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
29
27
  require "bundler"
30
28
 
31
- Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring|
29
+ Bundler.definition.requested_specs.find { |spec| spec.name == "spring" }&.tap do |spring|
32
30
  Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
33
31
  gem "spring", spring.version
34
32
  require "spring/binstub"
@@ -1,5 +1,3 @@
1
- require "set"
2
-
3
1
  module Spring
4
2
  module Client
5
3
  class Rails < Command
@@ -22,7 +20,7 @@ module Spring
22
20
 
23
21
  if COMMANDS.include?(command_name)
24
22
  Run.call(["rails_#{command_name}", *args.drop(2)])
25
- elsif command_name.start_with?("db:")
23
+ elsif command_name&.start_with?("db:") && !command_name.start_with?("db:system")
26
24
  Run.call(["rake", *args.drop(1)])
27
25
  else
28
26
  require "spring/configuration"
@@ -6,8 +6,7 @@ module Spring
6
6
  module Client
7
7
  class Run < Command
8
8
  FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
9
- CONNECT_TIMEOUT = 1
10
- BOOT_TIMEOUT = 20
9
+ ServerReadTimeout = Class.new(StandardError)
11
10
 
12
11
  attr_reader :server
13
12
 
@@ -73,8 +72,8 @@ module Spring
73
72
  def boot_server
74
73
  env.socket_path.unlink if env.socket_path.exist?
75
74
 
76
- pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
77
- timeout = Time.now + BOOT_TIMEOUT
75
+ pid = Process.spawn(server_process_env, env.server_command, out: File::NULL)
76
+ timeout = Time.now + Spring.boot_timeout
78
77
 
79
78
  @server_booted = true
80
79
 
@@ -85,7 +84,7 @@ module Spring
85
84
  exit status.exitstatus
86
85
  elsif Time.now > timeout
87
86
  $stderr.puts "Starting Spring server with `#{env.server_command}` " \
88
- "timed out after #{BOOT_TIMEOUT} seconds"
87
+ "timed out after #{Spring.boot_timeout} seconds"
89
88
  exit 1
90
89
  end
91
90
 
@@ -107,6 +106,14 @@ module Spring
107
106
  }
108
107
  end
109
108
 
109
+ def reset_env
110
+ ENV.slice(*Spring.reset_on_env)
111
+ end
112
+
113
+ def server_process_env
114
+ reset_env.merge(gem_env)
115
+ end
116
+
110
117
  def stop_server
111
118
  server.close
112
119
  @server = nil
@@ -114,7 +121,30 @@ module Spring
114
121
  end
115
122
 
116
123
  def verify_server_version
117
- server_version = server.gets.chomp
124
+ begin
125
+ line = read_server_line
126
+ rescue ServerReadTimeout
127
+ if waiting_for_server_boot?
128
+ begin
129
+ # Try again, but with same timeout as booting, as server might still be booting
130
+ # from another client starting it.
131
+ line = read_server_line(Spring.boot_timeout)
132
+ rescue ServerReadTimeout
133
+ reboot_or_raise_connection_error
134
+ return
135
+ end
136
+ else
137
+ reboot_or_raise_connection_error
138
+ return
139
+ end
140
+ end
141
+
142
+ if line.nil?
143
+ reboot_or_raise_connection_error
144
+ return
145
+ end
146
+
147
+ server_version = line.chomp
118
148
  if server_version != env.version
119
149
  $stderr.puts "There is a version mismatch between the Spring client " \
120
150
  "(#{env.version}) and the server (#{server_version})."
@@ -130,11 +160,30 @@ module Spring
130
160
  end
131
161
  end
132
162
 
163
+ def read_server_line(timeout = Spring.connect_timeout)
164
+ raise ServerReadTimeout if IO.select([server], [], [], timeout).nil?
165
+
166
+ server.gets
167
+ end
168
+
169
+ def waiting_for_server_boot?
170
+ !server_booted? && env.server_running?
171
+ end
172
+
173
+ def reboot_or_raise_connection_error
174
+ if server_booted?
175
+ raise "Error connecting to Spring server"
176
+ else
177
+ stop_server
178
+ cold_run
179
+ end
180
+ end
181
+
133
182
  def connect_to_application(client)
134
183
  server.send_io client
135
- send_json server, "args" => args, "default_rails_env" => default_rails_env
184
+ send_json server, "args" => args, "default_rails_env" => default_rails_env, "spawn_env" => spawn_env, "reset_env" => reset_env
136
185
 
137
- if IO.select([server], [], [], CONNECT_TIMEOUT)
186
+ if IO.select([server], [], [], Spring.connect_timeout)
138
187
  server.gets or raise CommandNotFound
139
188
  else
140
189
  raise "Error connecting to Spring server"
@@ -169,11 +218,16 @@ module Spring
169
218
  suspend_resume_on_tstp_cont(pid)
170
219
 
171
220
  forward_signals(application)
172
- status = application.read.to_i
221
+ status = application.read
222
+ log "got exit status #{status.inspect}"
173
223
 
174
- log "got exit status #{status}"
224
+ # Status should always be an integer. If it is empty, something unexpected must have happened to the server.
225
+ if status.to_s.strip.empty?
226
+ log "unexpected empty exit status, app crashed?"
227
+ exit 1
228
+ end
175
229
 
176
- exit status
230
+ exit status.to_i
177
231
  else
178
232
  log "got no pid"
179
233
  exit 1
@@ -232,6 +286,12 @@ module Spring
232
286
  def default_rails_env
233
287
  ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
234
288
  end
289
+
290
+ def spawn_env
291
+ Spring.spawn_on_env.to_h do |key|
292
+ [key, ENV[key]]
293
+ end
294
+ end
235
295
  end
236
296
  end
237
297
  end
@@ -28,9 +28,16 @@ module Spring
28
28
  config = File.expand_path("~/.spring.rb")
29
29
  require config if File.exist?(config)
30
30
 
31
- # If the config/spring.rb contains requires for commands from other gems,
32
- # then we need to be under bundler.
33
- require "bundler/setup"
31
+ # We force the TTY so bundler doesn't show a backtrace in case gems are missing.
32
+ old_env = ENV["BUNDLER_FORCE_TTY"]
33
+ ENV["BUNDLER_FORCE_TTY"] = "true"
34
+ begin
35
+ # If the config/spring.rb contains requires for commands from other gems,
36
+ # then we need to be under bundler.
37
+ require "bundler/setup"
38
+ ensure
39
+ ENV["BUNDLER_FORCE_TTY"] = old_env
40
+ end
34
41
 
35
42
  # Auto-require any Spring extensions which are in the Gemfile
36
43
  Gem::Specification.map(&:name).grep(/^spring-/).each do |command|
@@ -1,8 +1,18 @@
1
1
  require "spring/errors"
2
2
 
3
3
  module Spring
4
+ @connect_timeout = 5
5
+ @boot_timeout = 20
6
+ @dangerously_allow_disabling_reloading = false
7
+
4
8
  class << self
5
- attr_accessor :application_root, :quiet
9
+ attr_accessor :application_root, :connect_timeout, :boot_timeout
10
+ attr_writer :quiet
11
+
12
+ # Opt-in: skip the `:ensure_reloading_is_enabled` initializer so Spring
13
+ # boots with `config.enable_reloading = false`. Default `false`. See
14
+ # README "Running Spring with reloading disabled" for what this gives up.
15
+ attr_accessor :dangerously_allow_disabling_reloading
6
16
 
7
17
  def gemfile
8
18
  require "bundler"
@@ -31,6 +41,22 @@ module Spring
31
41
  after_fork_callbacks << block
32
42
  end
33
43
 
44
+ def after_environment_load_callbacks
45
+ @after_environment_load_callbacks ||= []
46
+ end
47
+
48
+ def after_environment_load(&block)
49
+ after_environment_load_callbacks << block
50
+ end
51
+
52
+ def spawn_on_env
53
+ @spawn_on_env ||= []
54
+ end
55
+
56
+ def reset_on_env
57
+ @reset_on_env ||= []
58
+ end
59
+
34
60
  def verify_environment
35
61
  application_root_path
36
62
  end
@@ -52,6 +78,10 @@ module Spring
52
78
  @project_root_path ||= find_project_root(Pathname.new(File.expand_path(Dir.pwd)))
53
79
  end
54
80
 
81
+ def quiet
82
+ @quiet || ENV.key?('SPRING_QUIET')
83
+ end
84
+
55
85
  private
56
86
 
57
87
  def find_project_root(current_dir)
data/lib/spring/env.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "pathname"
2
- require "digest/md5"
3
2
 
4
3
  require "spring/version"
5
4
  require "spring/configuration"
@@ -41,6 +40,7 @@ module Spring
41
40
  end
42
41
 
43
42
  def application_id
43
+ require "digest/md5"
44
44
  ENV["SPRING_APPLICATION_ID"] || Digest::MD5.hexdigest(RUBY_VERSION + project_root.to_s)
45
45
  end
46
46
 
data/lib/spring/json.rb CHANGED
@@ -1,4 +1,4 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  # ### WHY SPRING VENDORS A JSON LIBRARY ###
4
4
  #
@@ -13,7 +13,7 @@
13
13
  module Spring
14
14
  module JSON
15
15
  def self.load(string)
16
- string.force_encoding("utf-8")
16
+ string = string.dup.force_encoding("utf-8") unless string.encoding == Encoding::UTF_8
17
17
  OkJson.decode(string)
18
18
  end
19
19
 
@@ -364,7 +364,7 @@ private
364
364
  end
365
365
  end
366
366
  if rubydoesenc?
367
- a[w] = '' << uchar
367
+ a[w] = +'' << uchar
368
368
  w += 1
369
369
  else
370
370
  w += ucharenc(a, w, uchar)
data/lib/spring/server.rb CHANGED
@@ -19,7 +19,9 @@ module Spring
19
19
  def initialize(options = {})
20
20
  @foreground = options.fetch(:foreground, false)
21
21
  @env = options[:env] || default_env
22
- @applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k, env) }
22
+ @applications = Hash.new do |hash, key|
23
+ hash[key] = ApplicationManager.new(*key, env)
24
+ end
23
25
  @pidfile = env.pidfile_path.open('a')
24
26
  @mutex = Mutex.new
25
27
  end
@@ -57,24 +59,29 @@ module Spring
57
59
  app_client = client.recv_io
58
60
  command = JSON.load(client.read(client.gets.to_i))
59
61
 
60
- args, default_rails_env = command.values_at('args', 'default_rails_env')
62
+ args, default_rails_env, spawn_env, reset_env = command.values_at('args', 'default_rails_env', 'spawn_env', 'reset_env')
61
63
 
62
64
  if Spring.command?(args.first)
65
+ application = @applications[rails_env_for(args, default_rails_env, spawn_env)]
66
+ reset_if_env_changed(application, reset_env)
67
+
63
68
  log "running command #{args.first}"
64
69
  client.puts
65
- client.puts @applications[rails_env_for(args, default_rails_env)].run(app_client)
70
+ client.puts application.run(app_client)
66
71
  else
67
72
  log "command not found #{args.first}"
68
73
  client.close
69
74
  end
70
75
  rescue SocketError => e
71
76
  raise e unless client.eof?
77
+ rescue Errno::EPIPE => e
78
+ log "client disconnected with error #{e.message}, ignoring command"
72
79
  ensure
73
80
  redirect_output
74
81
  end
75
82
 
76
- def rails_env_for(args, default_rails_env)
77
- Spring.command(args.first).env(args.drop(1)) || default_rails_env
83
+ def rails_env_for(args, default_rails_env, spawn_env)
84
+ [Spring.command(args.first).env(args.drop(1)) || default_rails_env, spawn_env]
78
85
  end
79
86
 
80
87
  # Boot the server into the process group of the current session.
@@ -136,6 +143,10 @@ module Spring
136
143
 
137
144
  private
138
145
 
146
+ def reset_if_env_changed(application, reset_env)
147
+ application.stop if ENV.slice(*reset_env.keys) != reset_env
148
+ end
149
+
139
150
  def default_env
140
151
  Env.new(log_file: default_log_file)
141
152
  end
@@ -1,3 +1,3 @@
1
1
  module Spring
2
- VERSION = "4.0.0"
2
+ VERSION = "4.7.0"
3
3
  end
@@ -1,6 +1,4 @@
1
- require "set"
2
1
  require "pathname"
3
- require "mutex_m"
4
2
 
5
3
  module Spring
6
4
  module Watcher
@@ -10,13 +8,10 @@ module Spring
10
8
  # IO.select([watcher]) # watcher is running in background
11
9
  # watcher.stale? # => true
12
10
  class Abstract
13
- include Mutex_m
14
-
15
11
  attr_reader :files, :directories, :root, :latency
16
12
 
17
13
  def initialize(root, latency)
18
- super()
19
-
14
+ @mutex = Mutex.new
20
15
  @root = File.realpath(root)
21
16
  @latency = latency
22
17
  @files = {}
@@ -27,6 +22,11 @@ module Spring
27
22
  @on_debug = nil
28
23
  end
29
24
 
25
+ def synchronize(&block)
26
+ # Used by some gems.
27
+ @mutex.synchronize(&block)
28
+ end
29
+
30
30
  def on_debug(&block)
31
31
  @on_debug = block
32
32
  end
@@ -60,7 +60,7 @@ module Spring
60
60
  end
61
61
  end
62
62
 
63
- synchronize {
63
+ @mutex.synchronize do
64
64
  items.each do |item|
65
65
  if item.directory?
66
66
  directories[item.realpath.to_s] = true
@@ -76,7 +76,7 @@ module Spring
76
76
  end
77
77
 
78
78
  subjects_changed
79
- }
79
+ end
80
80
  end
81
81
 
82
82
  def stale?
@@ -12,7 +12,7 @@ module Spring
12
12
  end
13
13
 
14
14
  def check_stale
15
- synchronize do
15
+ @mutex.synchronize do
16
16
  computed = compute_mtime
17
17
  if mtime < computed
18
18
  debug { "check_stale: mtime=#{mtime.inspect} < computed=#{computed.inspect}" }
metadata CHANGED
@@ -1,57 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spring
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Leighton
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-12-10 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: rake
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: bump
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: activesupport
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
55
12
  description: Preloads your application so things like console, rake and tests run
56
13
  faster
57
14
  email:
@@ -99,7 +56,7 @@ licenses:
99
56
  - MIT
100
57
  metadata:
101
58
  rubygems_mfa_required: 'true'
102
- post_install_message:
59
+ changelog_uri: https://github.com/rails/spring/blob/main/CHANGELOG.md
103
60
  rdoc_options: []
104
61
  require_paths:
105
62
  - lib
@@ -107,15 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
64
  requirements:
108
65
  - - ">="
109
66
  - !ruby/object:Gem::Version
110
- version: 2.7.0
67
+ version: 3.1.0
111
68
  required_rubygems_version: !ruby/object:Gem::Requirement
112
69
  requirements:
113
70
  - - ">="
114
71
  - !ruby/object:Gem::Version
115
72
  version: '0'
116
73
  requirements: []
117
- rubygems_version: 3.2.32
118
- signing_key:
74
+ rubygems_version: 3.6.7
119
75
  specification_version: 4
120
76
  summary: Rails application preloader
121
77
  test_files: []