spring 1.2.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.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +3 -2
  3. data/README.md +148 -42
  4. data/bin/spring +2 -1
  5. data/lib/spring/application/boot.rb +11 -4
  6. data/lib/spring/application.rb +178 -50
  7. data/lib/spring/application_manager.rb +17 -9
  8. data/lib/spring/boot.rb +2 -0
  9. data/lib/spring/client/binstub.rb +44 -45
  10. data/lib/spring/client/help.rb +1 -1
  11. data/lib/spring/client/rails.rb +6 -5
  12. data/lib/spring/client/run.rb +187 -41
  13. data/lib/spring/client/server.rb +18 -0
  14. data/lib/spring/client/status.rb +2 -2
  15. data/lib/spring/client/stop.rb +7 -22
  16. data/lib/spring/client.rb +11 -1
  17. data/lib/spring/command_wrapper.rb +0 -4
  18. data/lib/spring/commands/rails.rb +21 -0
  19. data/lib/spring/commands.rb +11 -4
  20. data/lib/spring/configuration.rb +49 -2
  21. data/lib/spring/env.rb +43 -12
  22. data/lib/spring/errors.rb +1 -1
  23. data/lib/spring/failsafe_thread.rb +14 -0
  24. data/lib/spring/json.rb +30 -33
  25. data/lib/spring/process_title_updater.rb +2 -2
  26. data/lib/spring/server.rb +45 -13
  27. data/lib/spring/version.rb +1 -1
  28. data/lib/spring/watcher/abstract.rb +44 -13
  29. data/lib/spring/watcher/polling.rb +48 -11
  30. data/lib/spring/watcher.rb +1 -1
  31. metadata +14 -71
  32. data/.gitignore +0 -19
  33. data/.travis.yml +0 -11
  34. data/CHANGELOG.md +0 -188
  35. data/CONTRIBUTING.md +0 -52
  36. data/Gemfile +0 -4
  37. data/Rakefile +0 -21
  38. data/lib/spring/sid.rb +0 -42
  39. data/lib/spring/test/acceptance_test.rb +0 -333
  40. data/lib/spring/test/application.rb +0 -217
  41. data/lib/spring/test/application_generator.rb +0 -121
  42. data/lib/spring/test/rails_version.rb +0 -40
  43. data/lib/spring/test/watcher_test.rb +0 -167
  44. data/lib/spring/test.rb +0 -15
  45. data/spring.gemspec +0 -23
  46. data/test/acceptance_test.rb +0 -4
  47. data/test/apps/.gitignore +0 -3
  48. data/test/helper.rb +0 -7
  49. data/test/unit/client/help_test.rb +0 -60
  50. data/test/unit/client/version_test.rb +0 -14
  51. data/test/unit/commands_test.rb +0 -59
  52. data/test/unit/process_title_updater_test.rb +0 -24
  53. data/test/unit/watcher_test.rb +0 -9
@@ -1,17 +1,17 @@
1
1
  require "spring/boot"
2
- require "set"
3
2
  require "pty"
4
3
 
5
4
  module Spring
6
5
  class Application
7
6
  attr_reader :manager, :watcher, :spring_env, :original_env
8
7
 
9
- def initialize(manager, original_env)
8
+ def initialize(manager, original_env, spring_env = Env.new)
10
9
  @manager = manager
11
10
  @original_env = original_env
12
- @spring_env = Env.new
11
+ @spring_env = spring_env
13
12
  @mutex = Mutex.new
14
- @waiting = Set.new
13
+ @waiting = {}
14
+ @clients = {}
15
15
  @preloaded = false
16
16
  @state = :initialized
17
17
  @interrupt = IO.pipe
@@ -28,6 +28,11 @@ module Spring
28
28
  @interrupt.last.write "."
29
29
  end
30
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
+
31
36
  def app_env
32
37
  ENV['RAILS_ENV']
33
38
  end
@@ -66,7 +71,17 @@ module Spring
66
71
 
67
72
  def start_watcher
68
73
  @watcher = Spring.watcher
69
- @watcher.on_stale { state! :watcher_stale }
74
+
75
+ @watcher.on_stale do
76
+ state! :watcher_stale
77
+ end
78
+
79
+ if @watcher.respond_to? :on_debug
80
+ @watcher.on_debug do |message|
81
+ spring_env.log "[watcher:#{app_env}] #{message}"
82
+ end
83
+ end
84
+
70
85
  @watcher.start
71
86
  end
72
87
 
@@ -81,32 +96,52 @@ module Spring
81
96
 
82
97
  require Spring.application_root_path.join("config", "application")
83
98
 
84
- # config/environments/test.rb will have config.cache_classes = true. However
85
- # we want it to be false so that we can reload files. This is a hack to
86
- # override the effect of config.cache_classes = true. We can then actually
87
- # set config.cache_classes = false after loading the environment.
88
- Rails::Application.initializer :initialize_dependency_mechanism, group: :all do
89
- ActiveSupport::Dependencies.mechanism = :load
99
+ unless Rails.respond_to?(:gem_version) && Rails.gem_version >= Gem::Version.new('6.0.0')
100
+ raise "Spring only supports Rails >= 6.0.0"
101
+ end
102
+
103
+ Rails::Application.initializer :ensure_reloading_is_enabled, group: :all do
104
+ next if Spring.dangerously_allow_disabling_reloading
105
+
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
112
+ raise <<-MSG.strip_heredoc
113
+ Spring reloads, and therefore needs the application to have reloading enabled.
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.)
117
+ MSG
118
+ end
90
119
  end
91
120
 
92
121
  require Spring.application_root_path.join("config", "environment")
93
122
 
94
- @original_cache_classes = Rails.application.config.cache_classes
95
- Rails.application.config.cache_classes = false
123
+ invoke_after_environment_load_callbacks
124
+ preload_framework_base_classes
96
125
 
97
126
  disconnect_database
98
127
 
99
128
  @preloaded = :success
100
129
  rescue Exception => e
101
130
  @preloaded = :failure
102
- watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
131
+ watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] }
103
132
  raise e unless initialized?
104
133
  ensure
105
134
  watcher.add loaded_application_features
106
- watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"
135
+ watcher.add Spring.gemfile, Spring.gemfile_lock
107
136
 
108
137
  if defined?(Rails) && Rails.application
109
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
110
145
  watcher.add Rails.application.paths["config/database"]
111
146
  if secrets_path = Rails.application.paths["config/secrets"]
112
147
  watcher.add secrets_path
@@ -114,8 +149,26 @@ module Spring
114
149
  end
115
150
  end
116
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
+
117
165
  def eager_preload
118
- 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
119
172
  end
120
173
 
121
174
  def run
@@ -137,10 +190,23 @@ module Spring
137
190
  log "got client"
138
191
  manager.puts
139
192
 
140
- stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
193
+ @clients[client] = true
194
+
195
+ _stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
141
196
  [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
142
197
 
143
- preload unless preloaded?
198
+ if preloaded?
199
+ client.puts(0) # preload success
200
+ else
201
+ begin
202
+ preload
203
+ client.puts(0) # preload success
204
+ rescue Exception
205
+ log "preload failed"
206
+ ignore_client_disconnect { client.puts(1) } # preload failure
207
+ raise
208
+ end
209
+ end
144
210
 
145
211
  args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
146
212
  command = Spring.command(args.shift)
@@ -149,31 +215,39 @@ module Spring
149
215
  setup command
150
216
 
151
217
  if Rails.application.reloaders.any?(&:updated?)
152
- ActionDispatch::Reloader.cleanup!
153
- ActionDispatch::Reloader.prepare!
218
+ Rails.application.reloader.reload!
154
219
  end
155
220
 
156
221
  pid = fork {
222
+ # Make sure to close other clients otherwise their graceful termination
223
+ # will be impossible due to reference from this fork.
224
+ @clients.each_key { |c| c.close if c != client }
225
+
226
+ Process.setsid
157
227
  IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
158
228
  trap("TERM", "DEFAULT")
159
229
 
230
+ unless Spring.quiet
231
+ STDERR.puts "Running via Spring preloader in process #{Process.pid}"
232
+
233
+ if Rails.env.production?
234
+ STDERR.puts "WARNING: Spring is running in production. To fix " \
235
+ "this make sure the spring gem is only present " \
236
+ "in `development` and `test` groups in your Gemfile " \
237
+ "and make sure you always use " \
238
+ "`bundle install --without development test` in production"
239
+ end
240
+ end
241
+
160
242
  ARGV.replace(args)
161
- $0 = command.process_title
243
+ $0 = command.exec_name
162
244
 
163
- # Delete all env vars which are unchanged from before spring started
245
+ # Delete all env vars which are unchanged from before Spring started
164
246
  original_env.each { |k, v| ENV.delete k if ENV[k] == v }
165
247
 
166
- # Load in the current env vars, except those which *were* changed when spring started
248
+ # Load in the current env vars, except those which *were* changed when Spring started
167
249
  env.each { |k, v| ENV[k] ||= v }
168
250
 
169
- # requiring is faster, so if config.cache_classes was true in
170
- # the environment's config file, then we can respect that from
171
- # here on as we no longer need constant reloading.
172
- if @original_cache_classes
173
- ActiveSupport::Dependencies.mechanism = :require
174
- Rails.application.config.cache_classes = true
175
- end
176
-
177
251
  connect_database
178
252
  srand
179
253
 
@@ -184,30 +258,38 @@ module Spring
184
258
  }
185
259
 
186
260
  disconnect_database
187
- reset_streams
188
261
 
189
262
  log "forked #{pid}"
190
263
  manager.puts pid
191
264
 
192
265
  wait pid, streams, client
193
266
  rescue Exception => e
194
- 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
195
272
  manager.puts unless pid
196
273
 
197
274
  if streams && !e.is_a?(SystemExit)
198
- print_exception(stderr, e)
199
- streams.each(&:close)
275
+ ignore_client_disconnect { print_exception(stderr, e) }
276
+ streams.each { |stream| ignore_client_disconnect { stream.close } }
200
277
  end
201
278
 
202
- client.puts(1) if pid
279
+ ignore_client_disconnect { client.puts(1) if pid }
203
280
  client.close
281
+ ensure
282
+ # Redirect STDOUT and STDERR to prevent from keeping the original FDs
283
+ # (i.e. to prevent `spring rake -T | grep db` from hanging forever),
284
+ # even when exception is raised before forking (i.e. preloading).
285
+ reset_streams
204
286
  end
205
287
 
206
288
  def terminate
207
289
  if exiting?
208
290
  # Ensure that we do not ignore subsequent termination attempts
209
291
  log "forced exit"
210
- @waiting.each { |pid| Process.kill("TERM", pid) }
292
+ @waiting.each_key { |pid| Process.kill("TERM", pid) }
211
293
  Kernel.exit
212
294
  else
213
295
  state! :terminating
@@ -242,6 +324,12 @@ module Spring
242
324
  end
243
325
  end
244
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
+
245
333
  def loaded_application_features
246
334
  root = Spring.application_root_path.to_s
247
335
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
@@ -260,13 +348,28 @@ module Spring
260
348
  Kernel.module_eval do
261
349
  old_raise = Kernel.method(:raise)
262
350
  remove_method :raise
263
- define_method :raise do |*args|
264
- begin
265
- old_raise.call(*args)
266
- ensure
267
- if $!
268
- lib = File.expand_path("..", __FILE__)
269
- $!.backtrace.reject! { |line| line.start_with?(lib) }
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
270
373
  end
271
374
  end
272
375
  end
@@ -283,8 +386,13 @@ module Spring
283
386
  def with_pty
284
387
  PTY.open do |master, slave|
285
388
  [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
286
- yield
287
- reset_streams
389
+ reader_thread = Spring.failsafe_thread { master.read }
390
+ begin
391
+ yield
392
+ ensure
393
+ reader_thread.kill
394
+ reset_streams
395
+ end
288
396
  end
289
397
  end
290
398
 
@@ -294,26 +402,46 @@ module Spring
294
402
  end
295
403
 
296
404
  def wait(pid, streams, client)
297
- @mutex.synchronize { @waiting << pid }
405
+ @mutex.synchronize { @waiting[pid] = true }
298
406
 
299
407
  # Wait in a separate thread so we can run multiple commands at once
300
- Thread.new {
408
+ Spring.failsafe_thread {
301
409
  begin
302
410
  _, status = Process.wait2 pid
303
- log "#{pid} exited with #{status.exitstatus}"
411
+ log "#{pid} exited with #{status.exitstatus || status.inspect}"
304
412
 
305
413
  streams.each(&:close)
306
- client.puts(status.exitstatus)
414
+ client.puts(status.exitstatus || status.to_i)
307
415
  client.close
308
416
  ensure
309
417
  @mutex.synchronize { @waiting.delete pid }
310
418
  exit_if_finished
311
419
  end
312
420
  }
421
+
422
+ Spring.failsafe_thread {
423
+ while signal = client.gets.chomp
424
+ begin
425
+ Process.kill(signal, -Process.getpgid(pid))
426
+ client.puts(0)
427
+ rescue Errno::ESRCH
428
+ client.puts(1)
429
+ end
430
+ end
431
+ }
313
432
  end
314
433
 
315
434
  private
316
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
+
317
445
  def active_record_configured?
318
446
  defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
319
447
  end
@@ -1,12 +1,14 @@
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)
5
+ def initialize(app_env, spawn_env, spring_env)
6
6
  @app_env = app_env
7
- @spring_env = Env.new
7
+ @spawn_env = spawn_env
8
+ @spring_env = spring_env
8
9
  @mutex = Mutex.new
9
10
  @state = :running
11
+ @pid = nil
10
12
  end
11
13
 
12
14
  def log(message)
@@ -40,7 +42,7 @@ module Spring
40
42
  if alive?
41
43
  begin
42
44
  yield
43
- rescue Errno::ECONNRESET, Errno::EPIPE
45
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
44
46
  # The child has died but has not been collected by the wait thread yet,
45
47
  # so start a new child and try again.
46
48
  log "child dead; starting"
@@ -92,18 +94,23 @@ module Spring
92
94
  def start_child(preload = false)
93
95
  @child, child_socket = UNIXSocket.pair
94
96
 
95
- Bundler.with_clean_env do
97
+ Bundler.with_original_env do
98
+ bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
96
99
  @pid = Process.spawn(
97
100
  {
98
101
  "RAILS_ENV" => app_env,
99
102
  "RACK_ENV" => app_env,
100
103
  "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
101
- "SPRING_PRELOAD" => preload ? "1" : "0"
104
+ "SPRING_PRELOAD" => preload ? "1" : "0",
105
+ "SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact),
106
+ **spawn_env,
102
107
  },
103
108
  "ruby",
109
+ *(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
104
110
  "-I", File.expand_path("../..", __FILE__),
105
111
  "-e", "require 'spring/application/boot'",
106
- 3 => child_socket
112
+ 3 => child_socket,
113
+ 4 => spring_env.log_file,
107
114
  )
108
115
  end
109
116
 
@@ -114,12 +121,13 @@ module Spring
114
121
  def start_wait_thread(pid, child)
115
122
  Process.detach(pid)
116
123
 
117
- Thread.new {
124
+ Spring.failsafe_thread {
118
125
  # The recv can raise an ECONNRESET, killing the thread, but that's ok
119
126
  # as if it does we're no longer interested in the child
120
127
  loop do
121
128
  IO.select([child])
122
- break if child.recv(1, Socket::MSG_PEEK).empty?
129
+ peek = child.recv(1, Socket::MSG_PEEK)
130
+ break if peek.nil? || peek.empty?
123
131
  sleep 0.01
124
132
  end
125
133
 
data/lib/spring/boot.rb CHANGED
@@ -6,3 +6,5 @@ require "spring/env"
6
6
  require "spring/process_title_updater"
7
7
  require "spring/json"
8
8
  require "spring/watcher"
9
+ require "spring/failsafe_thread"
10
+
@@ -1,54 +1,49 @@
1
- require 'set'
2
-
3
1
  module Spring
4
2
  module Client
5
3
  class Binstub < Command
6
- SHEBANG = /\#\!.*\n/
4
+ SHEBANG = /\#\!.*\n(\#.*\n)*/
7
5
 
8
- # If loading the bin/spring file works, it'll run spring which will
6
+ # If loading the bin/spring file works, it'll run Spring which will
9
7
  # eventually call Kernel.exit. This means that in the client process
10
- # we will never execute the lines after this block. But if the spring
8
+ # we will never execute the lines after this block. But if the Spring
11
9
  # client is not invoked for whatever reason, then the Kernel.exit won't
12
10
  # happen, and so we'll fall back to the lines after this block, which
13
11
  # should cause the "unsprung" version of the command to run.
14
- LOADER = <<CODE
15
- begin
16
- load File.expand_path("../spring", __FILE__)
17
- rescue LoadError
18
- end
19
- CODE
12
+ LOADER = <<~CODE
13
+ load File.expand_path("spring", __dir__)
14
+ CODE
20
15
 
21
16
  # The defined? check ensures these lines don't execute when we load the
22
17
  # binstub from the application process. Which means that in the application
23
18
  # process we'll execute the lines which come after the LOADER block, which
24
19
  # is what we want.
25
- #
26
- # Parsing the lockfile in this way is pretty nasty but reliable enough
27
- # The regex ensures that the match must be between a GEM line and an empty
28
- # line, so it won't go on to the next section.
29
- SPRING = <<'CODE'
30
- #!/usr/bin/env ruby
31
-
32
- # This file loads spring without using Bundler, in order to be fast
33
- # It gets overwritten when you run the `spring binstub` command
34
-
35
- unless defined?(Spring)
36
- require "rubygems"
37
- require "bundler"
38
-
39
- if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)
40
- ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR)
41
- ENV["GEM_HOME"] = ""
42
- Gem.paths = ENV
43
-
44
- gem "spring", match[1]
45
- require "spring/binstub"
46
- end
47
- end
48
- CODE
20
+ SPRING = <<~CODE
21
+ #!/usr/bin/env ruby
22
+
23
+ # This file loads Spring without loading other gems in the Gemfile in order to be fast.
24
+ # It gets overwritten when you run the `spring binstub` command.
25
+
26
+ if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
27
+ require "bundler"
28
+
29
+ Bundler.definition.requested_specs.find { |spec| spec.name == "spring" }&.tap do |spring|
30
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
31
+ gem "spring", spring.version
32
+ require "spring/binstub"
33
+ end
34
+ end
35
+ CODE
49
36
 
50
37
  OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
51
38
 
39
+ BINSTUB_VARIATIONS = Regexp.union [
40
+ %{load File.expand_path("spring", __dir__)\n},
41
+ %{begin\n load File.expand_path('../spring', __FILE__)\nrescue LoadError => e\n raise unless e.message.include?('spring')\nend\n},
42
+ %{begin\n load File.expand_path('../spring', __FILE__)\nrescue LoadError\nend\n},
43
+ %{begin\n spring_bin_path = File.expand_path('../spring', __FILE__)\n load spring_bin_path\nrescue LoadError => e\n raise unless e.message.end_with? spring_bin_path, 'spring/binstub'\nend\n},
44
+ LOADER
45
+ ].map { |binstub| /#{Regexp.escape(binstub).gsub("'", "['\"]")}/ }
46
+
52
47
  class Item
53
48
  attr_reader :command, :existing
54
49
 
@@ -75,8 +70,12 @@ CODE
75
70
  fallback = nil if fallback.include?("exec")
76
71
  generate(fallback)
77
72
  status "upgraded"
78
- elsif existing =~ /load .*spring/
79
- status "spring already present"
73
+ elsif existing.include?(LOADER)
74
+ status "Spring already present"
75
+ elsif existing =~ BINSTUB_VARIATIONS
76
+ upgraded = existing.sub(BINSTUB_VARIATIONS, LOADER)
77
+ File.write(command.binstub, upgraded)
78
+ status "upgraded"
80
79
  else
81
80
  head, shebang, tail = existing.partition(SHEBANG)
82
81
 
@@ -87,15 +86,15 @@ CODE
87
86
  end
88
87
 
89
88
  File.write(command.binstub, "#{head}#{shebang}#{LOADER}#{tail}")
90
- status "spring inserted"
89
+ status "Spring inserted"
91
90
  else
92
- status "doesn't appear to be ruby, so cannot use spring", $stderr
91
+ status "doesn't appear to be ruby, so cannot use Spring", $stderr
93
92
  exit 1
94
93
  end
95
94
  end
96
95
  else
97
96
  generate
98
- status "generated with spring"
97
+ status "generated with Spring"
99
98
  end
100
99
  end
101
100
 
@@ -111,8 +110,8 @@ CODE
111
110
 
112
111
  def remove
113
112
  if existing
114
- File.write(command.binstub, existing.sub(LOADER, ""))
115
- status "spring removed"
113
+ File.write(command.binstub, existing.sub(BINSTUB_VARIATIONS, ""))
114
+ status "Spring removed"
116
115
  end
117
116
  end
118
117
  end
@@ -120,7 +119,7 @@ CODE
120
119
  attr_reader :bindir, :items
121
120
 
122
121
  def self.description
123
- "Generate spring based binstubs. Use --all to generate a binstub for all known commands."
122
+ "Generate Spring based binstubs. Use --all to generate a binstub for all known commands. Use --remove to revert."
124
123
  end
125
124
 
126
125
  def self.rails_command
@@ -140,7 +139,7 @@ CODE
140
139
  @mode = :add
141
140
  @items = args.drop(1)
142
141
  .map { |name| find_commands name }
143
- .inject(Set.new, :|)
142
+ .flatten.uniq
144
143
  .map { |command| Item.new(command) }
145
144
  end
146
145
 
@@ -149,7 +148,7 @@ CODE
149
148
  when "--all"
150
149
  @all = true
151
150
  commands = Spring.commands.dup
152
- commands.delete_if { |name, _| name.start_with?("rails_") }
151
+ commands.delete_if { |command_name, _| command_name.start_with?("rails_") }
153
152
  commands.values + [self.class.rails_command]
154
153
  when "--remove"
155
154
  @mode = :remove
@@ -32,7 +32,7 @@ module Spring
32
32
  def formatted_help
33
33
  ["Version: #{env.version}\n",
34
34
  "Usage: spring COMMAND [ARGS]\n",
35
- *command_help("spring itself", spring_commands),
35
+ *command_help("Spring itself", spring_commands),
36
36
  '',
37
37
  *command_help("your application", application_commands)].join("\n")
38
38
  end
@@ -1,19 +1,18 @@
1
- require "set"
2
-
3
1
  module Spring
4
2
  module Client
5
3
  class Rails < Command
6
- COMMANDS = Set.new %w(console runner generate destroy)
4
+ COMMANDS = %w(console runner generate destroy test)
7
5
 
8
6
  ALIASES = {
9
7
  "c" => "console",
10
8
  "r" => "runner",
11
9
  "g" => "generate",
12
- "d" => "destroy"
10
+ "d" => "destroy",
11
+ "t" => "test"
13
12
  }
14
13
 
15
14
  def self.description
16
- "Run a rails command. The following sub commands will use spring: #{COMMANDS.to_a.join ', '}."
15
+ "Run a rails command. The following sub commands will use Spring: #{COMMANDS.to_a.join ', '}."
17
16
  end
18
17
 
19
18
  def call
@@ -21,6 +20,8 @@ module Spring
21
20
 
22
21
  if COMMANDS.include?(command_name)
23
22
  Run.call(["rails_#{command_name}", *args.drop(2)])
23
+ elsif command_name&.start_with?("db:") && !command_name.start_with?("db:system")
24
+ Run.call(["rake", *args.drop(1)])
24
25
  else
25
26
  require "spring/configuration"
26
27
  ARGV.shift