spring 1.0.0 → 1.3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -1
  4. data/CHANGELOG.md +81 -0
  5. data/CONTRIBUTING.md +52 -0
  6. data/Gemfile +0 -2
  7. data/README.md +137 -54
  8. data/Rakefile +1 -1
  9. data/bin/spring +17 -0
  10. data/lib/spring/application/boot.rb +18 -0
  11. data/lib/spring/application.rb +183 -76
  12. data/lib/spring/application_manager.rb +54 -40
  13. data/lib/spring/binstub.rb +13 -0
  14. data/lib/spring/boot.rb +8 -0
  15. data/lib/spring/client/binstub.rb +150 -39
  16. data/lib/spring/client/help.rb +4 -12
  17. data/lib/spring/client/rails.rb +2 -3
  18. data/lib/spring/client/run.rb +69 -11
  19. data/lib/spring/client/status.rb +2 -2
  20. data/lib/spring/client/stop.rb +6 -21
  21. data/lib/spring/client/version.rb +11 -0
  22. data/lib/spring/client.rb +8 -5
  23. data/lib/spring/command_wrapper.rb +82 -0
  24. data/lib/spring/commands/rails.rb +44 -13
  25. data/lib/spring/commands/rake.rb +0 -4
  26. data/lib/spring/commands.rb +14 -4
  27. data/lib/spring/configuration.rb +11 -2
  28. data/lib/spring/env.rb +36 -10
  29. data/lib/spring/server.rb +9 -35
  30. data/lib/spring/sid.rb +1 -1
  31. data/lib/spring/test/acceptance_test.rb +346 -0
  32. data/lib/spring/test/application.rb +217 -0
  33. data/lib/spring/test/application_generator.rb +121 -0
  34. data/lib/spring/test/rails_version.rb +40 -0
  35. data/lib/spring/test/watcher_test.rb +167 -0
  36. data/lib/spring/test.rb +18 -0
  37. data/lib/spring/version.rb +1 -1
  38. data/lib/spring/watcher/abstract.rb +7 -8
  39. data/lib/spring/watcher/polling.rb +2 -0
  40. data/lib/spring/watcher.rb +4 -8
  41. data/spring.gemspec +2 -2
  42. data/test/acceptance_test.rb +4 -0
  43. data/test/helper.rb +2 -2
  44. data/test/unit/client/version_test.rb +14 -0
  45. data/test/unit/commands_test.rb +23 -1
  46. data/test/unit/watcher_test.rb +2 -184
  47. metadata +30 -17
  48. data/lib/spring/watcher/listen.rb +0 -52
  49. data/test/acceptance/app_test.rb +0 -511
@@ -1,41 +1,84 @@
1
+ require "spring/boot"
1
2
  require "set"
2
- require "spring/watcher"
3
- require "thread"
3
+ require "pty"
4
4
 
5
5
  module Spring
6
6
  class Application
7
- attr_reader :manager, :watcher, :spring_env
7
+ attr_reader :manager, :watcher, :spring_env, :original_env
8
+
9
+ def initialize(manager, original_env)
10
+ @manager = manager
11
+ @original_env = original_env
12
+ @spring_env = Env.new
13
+ @mutex = Mutex.new
14
+ @waiting = Set.new
15
+ @preloaded = false
16
+ @state = :initialized
17
+ @interrupt = IO.pipe
18
+ end
8
19
 
9
- def initialize(manager, watcher = Spring.watcher)
10
- @manager = manager
11
- @watcher = watcher
12
- @setup = Set.new
13
- @spring_env = Env.new
14
- @preloaded = false
15
- @mutex = Mutex.new
16
- @waiting = 0
17
- @exiting = false
20
+ def state(val)
21
+ return if exiting?
22
+ log "#{@state} -> #{val}"
23
+ @state = val
24
+ end
18
25
 
19
- # Workaround for GC bug in Ruby 2 which causes segfaults if watcher.to_io
20
- # instances get dereffed.
21
- @fds = [manager, watcher.to_io]
26
+ def state!(val)
27
+ state val
28
+ @interrupt.last.write "."
29
+ end
30
+
31
+ def app_env
32
+ ENV['RAILS_ENV']
33
+ end
34
+
35
+ def app_name
36
+ spring_env.app_name
22
37
  end
23
38
 
24
39
  def log(message)
25
- spring_env.log "[application:#{ENV['RAILS_ENV']}] #{message}"
40
+ spring_env.log "[application:#{app_env}] #{message}"
26
41
  end
27
42
 
28
43
  def preloaded?
29
44
  @preloaded
30
45
  end
31
46
 
47
+ def preload_failed?
48
+ @preloaded == :failure
49
+ end
50
+
32
51
  def exiting?
33
- @exiting
52
+ @state == :exiting
53
+ end
54
+
55
+ def terminating?
56
+ @state == :terminating
57
+ end
58
+
59
+ def watcher_stale?
60
+ @state == :watcher_stale
61
+ end
62
+
63
+ def initialized?
64
+ @state == :initialized
65
+ end
66
+
67
+ def start_watcher
68
+ @watcher = Spring.watcher
69
+ @watcher.on_stale { state! :watcher_stale }
70
+ @watcher.start
34
71
  end
35
72
 
36
73
  def preload
37
74
  log "preloading app"
38
75
 
76
+ begin
77
+ require "spring/commands"
78
+ ensure
79
+ start_watcher
80
+ end
81
+
39
82
  require Spring.application_root_path.join("config", "application")
40
83
 
41
84
  # config/environments/test.rb will have config.cache_classes = true. However
@@ -48,47 +91,54 @@ module Spring
48
91
 
49
92
  require Spring.application_root_path.join("config", "environment")
50
93
 
94
+ @original_cache_classes = Rails.application.config.cache_classes
51
95
  Rails.application.config.cache_classes = false
96
+
52
97
  disconnect_database
53
98
 
99
+ @preloaded = :success
100
+ rescue Exception => e
101
+ @preloaded = :failure
102
+ watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
103
+ raise e unless initialized?
104
+ ensure
54
105
  watcher.add loaded_application_features
55
106
  watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"
56
- watcher.add Rails.application.paths["config/initializers"]
57
107
 
58
- @preloaded = true
108
+ if defined?(Rails) && Rails.application
109
+ watcher.add Rails.application.paths["config/initializers"]
110
+ watcher.add Rails.application.paths["config/database"]
111
+ if secrets_path = Rails.application.paths["config/secrets"]
112
+ watcher.add secrets_path
113
+ end
114
+ end
115
+ end
116
+
117
+ def eager_preload
118
+ with_pty { preload }
59
119
  end
60
120
 
61
121
  def run
62
- log "running"
63
- watcher.start
122
+ state :running
123
+ manager.puts
64
124
 
65
125
  loop do
66
- IO.select(@fds)
67
-
68
- if watcher.stale?
69
- log "watcher stale; exiting"
70
- manager.close
71
- @exiting = true
72
- try_exit
73
- sleep
126
+ IO.select [manager, @interrupt.first]
127
+
128
+ if terminating? || watcher_stale? || preload_failed?
129
+ exit
74
130
  else
75
131
  serve manager.recv_io(UNIXSocket)
76
132
  end
77
133
  end
78
134
  end
79
135
 
80
- def try_exit
81
- @mutex.synchronize {
82
- exit if exiting? && @waiting == 0
83
- }
84
- end
85
-
86
136
  def serve(client)
87
137
  log "got client"
88
138
  manager.puts
89
139
 
90
- streams = 3.times.map { client.recv_io }
91
- [STDOUT, STDERR].zip(streams).each { |a, b| a.reopen(b) }
140
+ stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
141
+ [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
92
142
 
93
143
  preload unless preloaded?
94
144
 
@@ -104,74 +154,84 @@ module Spring
104
154
  end
105
155
 
106
156
  pid = fork {
107
- Process.setsid
108
- STDIN.reopen(streams.last)
109
157
  IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
158
+ trap("TERM", "DEFAULT")
110
159
 
111
160
  ARGV.replace(args)
161
+ $0 = command.exec_name
112
162
 
113
163
  # Delete all env vars which are unchanged from before spring started
114
- Spring.original_env.each { |k, v| ENV.delete k if ENV[k] == v }
164
+ original_env.each { |k, v| ENV.delete k if ENV[k] == v }
115
165
 
116
166
  # Load in the current env vars, except those which *were* changed when spring started
117
167
  env.each { |k, v| ENV[k] ||= v }
118
168
 
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
+
119
177
  connect_database
120
178
  srand
121
179
 
122
180
  invoke_after_fork_callbacks
123
181
  shush_backtraces
124
182
 
125
- if command.respond_to?(:call)
126
- command.call
127
- else
128
- exec_name = command.exec_name
129
- gem_name = command.gem_name if command.respond_to?(:gem_name)
130
-
131
- exec = Gem.bin_path(gem_name || exec_name, exec_name)
132
- $0 = exec
133
- load exec
134
- end
183
+ command.call
135
184
  }
136
185
 
137
186
  disconnect_database
138
- [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
187
+ reset_streams
139
188
 
140
189
  log "forked #{pid}"
141
190
  manager.puts pid
142
191
 
143
- # Wait in a separate thread so we can run multiple commands at once
144
- Thread.new {
145
- @mutex.synchronize { @waiting += 1 }
146
-
147
- _, status = Process.wait2 pid
148
- log "#{pid} exited with #{status.exitstatus}"
192
+ wait pid, streams, client
193
+ rescue Exception => e
194
+ log "exception: #{e}"
195
+ manager.puts unless pid
149
196
 
197
+ if streams && !e.is_a?(SystemExit)
198
+ print_exception(stderr, e)
150
199
  streams.each(&:close)
151
- client.puts(status.exitstatus)
152
- client.close
153
-
154
- @mutex.synchronize { @waiting -= 1 }
155
- try_exit
156
- }
200
+ end
157
201
 
158
- rescue => e
159
- log "exception: #{e}"
160
- streams.each(&:close) if streams
161
- client.puts(1)
202
+ client.puts(1) if pid
162
203
  client.close
163
- raise
204
+ end
205
+
206
+ def terminate
207
+ if exiting?
208
+ # Ensure that we do not ignore subsequent termination attempts
209
+ log "forced exit"
210
+ @waiting.each { |pid| Process.kill("TERM", pid) }
211
+ Kernel.exit
212
+ else
213
+ state! :terminating
214
+ end
215
+ end
216
+
217
+ def exit
218
+ state :exiting
219
+ manager.shutdown(:RDWR)
220
+ exit_if_finished
221
+ sleep
222
+ end
223
+
224
+ def exit_if_finished
225
+ @mutex.synchronize {
226
+ Kernel.exit if exiting? && @waiting.empty?
227
+ }
164
228
  end
165
229
 
166
230
  # The command might need to require some files in the
167
231
  # main process so that they are cached. For example a test command wants to
168
232
  # load the helper file once and have it cached.
169
233
  def setup(command)
170
- return if @setup.include?(command.class)
171
- @setup << command.class
172
-
173
- if command.respond_to?(:setup)
174
- command.setup
234
+ if command.setup
175
235
  watcher.add loaded_application_features # loaded features may have changed
176
236
  end
177
237
  end
@@ -183,15 +243,16 @@ module Spring
183
243
  end
184
244
 
185
245
  def loaded_application_features
186
- $LOADED_FEATURES.select { |f| f.start_with?(File.realpath(Rails.root)) }
246
+ root = Spring.application_root_path.to_s
247
+ $LOADED_FEATURES.select { |f| f.start_with?(root) }
187
248
  end
188
249
 
189
250
  def disconnect_database
190
- ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base)
251
+ ActiveRecord::Base.remove_connection if active_record_configured?
191
252
  end
192
253
 
193
254
  def connect_database
194
- ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
255
+ ActiveRecord::Base.establish_connection if active_record_configured?
195
256
  end
196
257
 
197
258
  # This feels very naughty
@@ -209,7 +270,53 @@ module Spring
209
270
  end
210
271
  end
211
272
  end
273
+ private :raise
212
274
  end
213
275
  end
276
+
277
+ def print_exception(stream, error)
278
+ first, rest = error.backtrace.first, error.backtrace.drop(1)
279
+ stream.puts("#{first}: #{error} (#{error.class})")
280
+ rest.each { |line| stream.puts("\tfrom #{line}") }
281
+ end
282
+
283
+ def with_pty
284
+ PTY.open do |master, slave|
285
+ [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
286
+ Thread.new { master.read }
287
+ yield
288
+ reset_streams
289
+ end
290
+ end
291
+
292
+ def reset_streams
293
+ [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
294
+ STDIN.reopen("/dev/null")
295
+ end
296
+
297
+ def wait(pid, streams, client)
298
+ @mutex.synchronize { @waiting << pid }
299
+
300
+ # Wait in a separate thread so we can run multiple commands at once
301
+ Thread.new {
302
+ begin
303
+ _, status = Process.wait2 pid
304
+ log "#{pid} exited with #{status.exitstatus}"
305
+
306
+ streams.each(&:close)
307
+ client.puts(status.exitstatus)
308
+ client.close
309
+ ensure
310
+ @mutex.synchronize { @waiting.delete pid }
311
+ exit_if_finished
312
+ end
313
+ }
314
+ end
315
+
316
+ private
317
+
318
+ def active_record_configured?
319
+ defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
320
+ end
214
321
  end
215
322
  end
@@ -1,20 +1,16 @@
1
- require "socket"
2
- require "thread"
3
- require "spring/application"
4
-
5
1
  module Spring
6
2
  class ApplicationManager
7
- attr_reader :pid, :child, :app_env, :spring_env, :server
3
+ attr_reader :pid, :child, :app_env, :spring_env, :status
8
4
 
9
- def initialize(server, app_env)
10
- @server = server
5
+ def initialize(app_env)
11
6
  @app_env = app_env
12
7
  @spring_env = Env.new
13
8
  @mutex = Mutex.new
9
+ @state = :running
14
10
  end
15
11
 
16
12
  def log(message)
17
- server.log(message)
13
+ spring_env.log "[application_manager:#{app_env}] #{message}"
18
14
  end
19
15
 
20
16
  # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
@@ -28,10 +24,10 @@ module Spring
28
24
 
29
25
  def start
30
26
  start_child
31
- start_wait_thread
32
27
  end
33
28
 
34
29
  def restart
30
+ return if @state == :stopping
35
31
  start_child(true)
36
32
  end
37
33
 
@@ -63,13 +59,15 @@ module Spring
63
59
  def run(client)
64
60
  with_child do
65
61
  child.send_io client
66
- child.gets
62
+ child.gets or raise Errno::EPIPE
67
63
  end
68
64
 
69
- pid = child.gets
70
- pid = pid.chomp.to_i if pid
71
- log "got worker pid #{pid}"
72
- pid
65
+ pid = child.gets.to_i
66
+
67
+ unless pid.zero?
68
+ log "got worker pid #{pid}"
69
+ pid
70
+ end
73
71
  rescue Errno::ECONNRESET, Errno::EPIPE => e
74
72
  log "#{e} while reading from child; returning no pid"
75
73
  nil
@@ -78,45 +76,61 @@ module Spring
78
76
  end
79
77
 
80
78
  def stop
81
- Process.kill('TERM', pid) if pid
79
+ log "stopping"
80
+ @state = :stopping
81
+
82
+ if pid
83
+ Process.kill('TERM', pid)
84
+ Process.wait(pid)
85
+ end
86
+ rescue Errno::ESRCH, Errno::ECHILD
87
+ # Don't care
82
88
  end
83
89
 
84
90
  private
85
91
 
86
92
  def start_child(preload = false)
87
- server.application_starting
88
-
89
93
  @child, child_socket = UNIXSocket.pair
90
- @pid = fork {
91
- (ObjectSpace.each_object(IO).to_a - [STDOUT, STDERR, STDIN, child_socket])
92
- .reject(&:closed?)
93
- .each(&:close)
94
94
 
95
- ENV['RAILS_ENV'] = ENV['RACK_ENV'] = app_env
96
-
97
- ProcessTitleUpdater.run { |distance|
98
- "spring app | #{spring_env.app_name} | started #{distance} ago | #{app_env} mode"
99
- }
95
+ Bundler.with_clean_env do
96
+ @pid = Process.spawn(
97
+ {
98
+ "RAILS_ENV" => app_env,
99
+ "RACK_ENV" => app_env,
100
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
101
+ "SPRING_PRELOAD" => preload ? "1" : "0"
102
+ },
103
+ "ruby",
104
+ "-I", File.expand_path("../..", __FILE__),
105
+ "-e", "require 'spring/application/boot'",
106
+ 3 => child_socket
107
+ )
108
+ end
100
109
 
101
- app = Application.new(child_socket)
102
- app.preload if preload
103
- app.run
104
- }
110
+ start_wait_thread(pid, child) if child.gets
105
111
  child_socket.close
106
112
  end
107
113
 
108
- def start_wait_thread
109
- @wait_thread = Thread.new {
110
- Thread.current.abort_on_exception = true
114
+ def start_wait_thread(pid, child)
115
+ Process.detach(pid)
111
116
 
112
- while alive?
113
- _, status = Process.wait2(pid)
114
- @pid = nil
115
-
116
- # In the forked child, this will block forever, so we won't
117
- # return to the next iteration of the loop.
118
- synchronize { restart if !alive? && status.success? }
117
+ Thread.new {
118
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
119
+ # as if it does we're no longer interested in the child
120
+ loop do
121
+ IO.select([child])
122
+ break if child.recv(1, Socket::MSG_PEEK).empty?
123
+ sleep 0.01
119
124
  end
125
+
126
+ log "child #{pid} shutdown"
127
+
128
+ synchronize {
129
+ if @pid == pid
130
+ @pid = nil
131
+ restart
132
+ end
133
+ }
120
134
  }
121
135
  end
122
136
  end
@@ -0,0 +1,13 @@
1
+ command = File.basename($0)
2
+ bin_path = File.expand_path("../../../bin/spring", __FILE__)
3
+
4
+ if command == "spring"
5
+ load bin_path
6
+ else
7
+ disable = ENV["DISABLE_SPRING"]
8
+
9
+ if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
10
+ ARGV.unshift(command)
11
+ load bin_path
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require "socket"
2
+ require "thread"
3
+
4
+ require "spring/configuration"
5
+ require "spring/env"
6
+ require "spring/process_title_updater"
7
+ require "spring/json"
8
+ require "spring/watcher"