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 +4 -4
- data/README.md +43 -5
- data/lib/spring/application/boot.rb +9 -3
- data/lib/spring/application.rb +91 -17
- data/lib/spring/application_manager.rb +10 -6
- data/lib/spring/client/binstub.rb +2 -4
- data/lib/spring/client/rails.rb +1 -3
- data/lib/spring/client/run.rb +71 -11
- data/lib/spring/commands.rb +10 -3
- data/lib/spring/configuration.rb +31 -1
- data/lib/spring/env.rb +1 -1
- data/lib/spring/json.rb +3 -3
- data/lib/spring/server.rb +16 -5
- data/lib/spring/version.rb +1 -1
- data/lib/spring/watcher/abstract.rb +8 -8
- data/lib/spring/watcher/polling.rb +1 -1
- metadata +6 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a37d78748e084596d151cde2918d30799c8dfe9e10e029920ece9e9eee8118ad
|
|
4
|
+
data.tar.gz: c47e0897d3938b7350245854672812436505379cc498638c3eca9e2d52537fe6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
20
|
-
* Rails versions:
|
|
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
|
-
|
|
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 `
|
|
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
|
|
15
|
-
|
|
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
|
data/lib/spring/application.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|
data/lib/spring/client/rails.rb
CHANGED
|
@@ -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"
|
data/lib/spring/client/run.rb
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
77
|
-
timeout = Time.now +
|
|
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 #{
|
|
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
|
-
|
|
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], [], [],
|
|
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
|
|
221
|
+
status = application.read
|
|
222
|
+
log "got exit status #{status.inspect}"
|
|
173
223
|
|
|
174
|
-
|
|
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
|
data/lib/spring/commands.rb
CHANGED
|
@@ -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
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
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|
|
data/lib/spring/configuration.rb
CHANGED
|
@@ -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, :
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
data/lib/spring/version.rb
CHANGED
|
@@ -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
|
-
|
|
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?
|
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.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
118
|
-
signing_key:
|
|
74
|
+
rubygems_version: 3.6.7
|
|
119
75
|
specification_version: 4
|
|
120
76
|
summary: Rails application preloader
|
|
121
77
|
test_files: []
|