spring 1.1.0.beta1 → 1.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fe796fa453e40306865cb1cf6e6541ca01571d5d
4
- data.tar.gz: 7210043ae71cdb012a21734c86984922a8127afe
3
+ metadata.gz: 9be8bb694add40714afc5a6b350c2efc4a5348d0
4
+ data.tar.gz: 5af55d085e93f225c9d3ad9cdcbf89739a8dc6e4
5
5
  SHA512:
6
- metadata.gz: 3c1bcd8097999582b66b2507029e807be714455fe95f70a52e222a3221c5e3921e89ba1131e3b16bd0d775eaae7f65c660b87e65c22526d3e2e79544a226e532
7
- data.tar.gz: a0c20068387c73fd9203e6fc408ab1789240e9b244e4e627dc233f04f9850e3c984aca138a81f59b7c553958859e02349c15659910aa789ee95b5595cf95db5c
6
+ metadata.gz: 38c97ef6b3025ed82fca2d2784ead4c978deaab7226bf8504da55e4e8e7aa9d51ac49f5379ec1d27ab2fa9dd077623b99c8685e9f71c018dbfb74457b9f66958
7
+ data.tar.gz: d9501f771bdaf0f9b9da74bc4353b6b73baeb99671384034fd8307c90c91ba212aa31e45f868ee6d92bd418da02404570722be2fe2e6a2688acb4600cace8af5
@@ -1,5 +1,4 @@
1
1
  language: ruby
2
- before_install: if [[ "$TRAVIS_RUBY_VERSION" = "1.9.3" ]]; then gem install bundler --version="1.3.5"; else gem install bundler; fi
3
2
  rvm:
4
3
  - 1.9.3
5
4
  - 2.0.0
@@ -2,7 +2,8 @@
2
2
 
3
3
  * A `bin/spring` binstub is now generated. This allows us to load spring
4
4
  correctly if you have it installed locally with a `BUNDLE_PATH`, so
5
- it's no longer necessary to install spring system-wide. Note that you
5
+ it's no longer necessary to install spring system-wide. We also
6
+ activate the correct version from your Gemfile.lock. Note that you
6
7
  still can't have spring in your Gemfile as a git repository or local
7
8
  path; it must be a proper gem.
8
9
  * Various changes to how springified binstubs are implemented. Existing
@@ -12,6 +13,17 @@
12
13
  binstubs. This won't work unless you have upgraded your binstubs to
13
14
  the new format.
14
15
  * `config/database.yml` is watched
16
+ * Better application restarts - if you introduce an error, for example
17
+ by editing `config/application.rb`, spring will now continue to watch
18
+ your files and will immediately try to restart the application when
19
+ you edit `config/application.rb` again (hopefully to correct the error).
20
+ This means that by the time you come to run a command the application
21
+ may well already be running.
22
+ * Gemfile changes are now gracefully handled. Previously they would
23
+ cause spring to simply quit, meaning that you'd incur the full startup
24
+ penalty on the next run. Now spring doesn't quit, and will try to load
25
+ up your new bundle in the background.
26
+ * Fix support for using spring with Rails engines/plugins
15
27
 
16
28
  ## 1.0.0
17
29
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Spring
2
2
 
3
- [![Build Status](https://travis-ci.org/jonleighton/spring.png?branch=master)](https://travis-ci.org/jonleighton/spring)
3
+ [![Build Status](https://travis-ci.org/rails/spring.png?branch=master)](https://travis-ci.org/rails/spring)
4
4
  [![Gem Version](https://badge.fury.io/rb/spring.png)](http://badge.fury.io/rb/spring)
5
5
 
6
6
  Spring is a Rails application preloader. It speeds up development by
@@ -222,6 +222,52 @@ To use spring like this, do a `gem install spring` and then prefix
222
222
  commands with `spring`. For example, rather than running `bin/rake -T`,
223
223
  you'd run `spring rake -T`.
224
224
 
225
+ ## Temporarily disabling Spring
226
+
227
+ If you're using Spring binstubs, but temporarily don't want commands to
228
+ run through Spring, set the `DISABLE_SPRING` environment variable.
229
+
230
+ ## Class reloading
231
+
232
+ Spring uses Rails' class reloading mechanism
233
+ (`ActiveSupport::Dependencies`) to keep your code up to date between
234
+ test runs. This is the same mechanism which allows you to see changes
235
+ during development when you refresh the page. However, you may never
236
+ have used this mechanism with your `test` environment before, and this
237
+ can cause problems.
238
+
239
+ It's important to realise that code reloading means that the constants
240
+ in your application are *different objects* after files have changed:
241
+
242
+ ```
243
+ $ bin/rails runner 'puts User.object_id'
244
+ 70127987886040
245
+ $ touch app/models/user.rb
246
+ $ bin/rails runner 'puts User.object_id'
247
+ 70127976764620
248
+ ```
249
+
250
+ Suppose you have an initializer `config/initializers/save_user_class.rb`
251
+ like so:
252
+
253
+ ``` ruby
254
+ USER_CLASS = User
255
+ ```
256
+
257
+ This saves off the *first* version of the `User` class, which will not
258
+ be the same object as `User` after the code has been reloaded:
259
+
260
+ ```
261
+ $ bin/rails runner 'puts User == USER_CLASS'
262
+ true
263
+ $ touch app/models/user.rb
264
+ $ bin/rails runner 'puts User == USER_CLASS'
265
+ false
266
+ ```
267
+
268
+ So to avoid this problem, don't save off references to application
269
+ constants in your initialization code.
270
+
225
271
  ## Configuration
226
272
 
227
273
  Spring will read `~/.spring.rb` and `config/spring.rb` for custom
@@ -250,16 +296,6 @@ which get run when your application initializers, such as
250
296
  `config/application.rb`, `config/environments/*.rb` or
251
297
  `config/initializers/*.rb`.
252
298
 
253
- For example, if loading your test helper is slow, you might like to
254
- preload it to speed up your test runs. To do this you could add the
255
- following to your `config/environments/test.rb`:
256
-
257
- ``` ruby
258
- config.after_initialize do
259
- require Rails.root.join("test/helper")
260
- end
261
- ```
262
-
263
299
  ### Running code after forking
264
300
 
265
301
  You might want to run code after Spring forked off the process but
@@ -1,40 +1,79 @@
1
+ require "spring/boot"
1
2
  require "set"
2
- require "spring/watcher"
3
- require "thread"
4
3
 
5
4
  module Spring
6
5
  class Application
7
- attr_reader :manager, :watcher, :spring_env
6
+ attr_reader :manager, :watcher, :spring_env, :original_env
7
+
8
+ def initialize(manager, original_env)
9
+ @manager = manager
10
+ @original_env = original_env
11
+ @spring_env = Env.new
12
+ @mutex = Mutex.new
13
+ @waiting = 0
14
+ @preloaded = false
15
+ @state = :initialized
16
+ @interrupt = IO.pipe
17
+ end
8
18
 
9
- def initialize(manager, watcher = Spring.watcher)
10
- @manager = manager
11
- @watcher = watcher
12
- @spring_env = Env.new
13
- @preloaded = false
14
- @mutex = Mutex.new
15
- @waiting = 0
16
- @exiting = false
19
+ def state(val)
20
+ return if exiting?
21
+ log "#{@state} -> #{val}"
22
+ @state = val
23
+ end
17
24
 
18
- # Workaround for GC bug in Ruby 2 which causes segfaults if watcher.to_io
19
- # instances get dereffed.
20
- @fds = [manager, watcher.to_io]
25
+ def state!(val)
26
+ state val
27
+ @interrupt.last.write "."
28
+ end
29
+
30
+ def app_env
31
+ ENV['RAILS_ENV']
21
32
  end
22
33
 
23
34
  def log(message)
24
- spring_env.log "[application:#{ENV['RAILS_ENV']}] #{message}"
35
+ spring_env.log "[application:#{app_env}] #{message}"
25
36
  end
26
37
 
27
38
  def preloaded?
28
39
  @preloaded
29
40
  end
30
41
 
42
+ def preload_failed?
43
+ @preloaded == :failure
44
+ end
45
+
31
46
  def exiting?
32
- @exiting
47
+ @state == :exiting
48
+ end
49
+
50
+ def terminating?
51
+ @state == :terminating
52
+ end
53
+
54
+ def watcher_stale?
55
+ @state == :watcher_stale
56
+ end
57
+
58
+ def initialized?
59
+ @state == :initialized
60
+ end
61
+
62
+ def start_watcher
63
+ @watcher = Spring.watcher
64
+ @watcher.on_stale { state! :watcher_stale }
65
+ @watcher.start
33
66
  end
34
67
 
35
68
  def preload
36
69
  log "preloading app"
37
70
 
71
+ begin
72
+ require "spring/commands"
73
+ ensure
74
+ start_watcher
75
+ end
76
+
38
77
  require Spring.application_root_path.join("config", "application")
39
78
 
40
79
  # config/environments/test.rb will have config.cache_classes = true. However
@@ -50,45 +89,43 @@ module Spring
50
89
  Rails.application.config.cache_classes = false
51
90
  disconnect_database
52
91
 
92
+ @preloaded = :success
93
+ rescue Exception => e
94
+ @preloaded = :failure
95
+ watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
96
+ raise e unless initialized?
97
+ ensure
53
98
  watcher.add loaded_application_features
54
99
  watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"
55
- watcher.add Rails.application.paths["config/initializers"]
56
- watcher.add Rails.application.paths["config/database"]
57
100
 
58
- @preloaded = true
101
+ if defined?(Rails)
102
+ watcher.add Rails.application.paths["config/initializers"]
103
+ watcher.add Rails.application.paths["config/database"]
104
+ end
59
105
  end
60
106
 
61
107
  def run
62
- log "running"
63
- watcher.start
108
+ state :running
109
+ manager.puts
110
+ update_process_title
64
111
 
65
112
  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
113
+ IO.select [manager, @interrupt.first]
114
+
115
+ if terminating? || watcher_stale? || preload_failed?
116
+ exit
74
117
  else
75
118
  serve manager.recv_io(UNIXSocket)
76
119
  end
77
120
  end
78
121
  end
79
122
 
80
- def try_exit
81
- @mutex.synchronize {
82
- exit if exiting? && @waiting == 0
83
- }
84
- end
85
-
86
123
  def serve(client)
87
124
  log "got client"
88
125
  manager.puts
89
126
 
90
- streams = 3.times.map { client.recv_io }
91
- [STDOUT, STDERR].zip(streams).each { |a, b| a.reopen(b) }
127
+ stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
128
+ [STDOUT, STDERR].zip([stdout, stderr]).each { |a, b| a.reopen(b) }
92
129
 
93
130
  preload unless preloaded?
94
131
 
@@ -105,13 +142,14 @@ module Spring
105
142
 
106
143
  pid = fork {
107
144
  Process.setsid
108
- STDIN.reopen(streams.last)
145
+ STDIN.reopen(stdin)
109
146
  IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
147
+ trap("TERM", "DEFAULT")
110
148
 
111
149
  ARGV.replace(args)
112
150
 
113
151
  # 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 }
152
+ original_env.each { |k, v| ENV.delete k if ENV[k] == v }
115
153
 
116
154
  # Load in the current env vars, except those which *were* changed when spring started
117
155
  env.each { |k, v| ENV[k] ||= v }
@@ -147,15 +185,37 @@ module Spring
147
185
  client.close
148
186
 
149
187
  @mutex.synchronize { @waiting -= 1 }
150
- try_exit
188
+ exit_if_finished
151
189
  }
152
190
 
153
- rescue => e
191
+ rescue Exception => e
154
192
  log "exception: #{e}"
155
- streams.each(&:close) if streams
156
- client.puts(1)
193
+ manager.puts unless pid
194
+
195
+ if streams && !e.is_a?(SystemExit)
196
+ print_exception(stderr, e)
197
+ streams.each(&:close)
198
+ end
199
+
200
+ client.puts(1) if pid
157
201
  client.close
158
- raise
202
+ end
203
+
204
+ def terminate
205
+ state! :terminating
206
+ end
207
+
208
+ def exit
209
+ state :exiting
210
+ manager.shutdown(:RDWR)
211
+ exit_if_finished
212
+ sleep
213
+ end
214
+
215
+ def exit_if_finished
216
+ @mutex.synchronize {
217
+ Kernel.exit if exiting? && @waiting == 0
218
+ }
159
219
  end
160
220
 
161
221
  # The command might need to require some files in the
@@ -174,7 +234,8 @@ module Spring
174
234
  end
175
235
 
176
236
  def loaded_application_features
177
- $LOADED_FEATURES.select { |f| f.start_with?(File.realpath(Rails.root)) }
237
+ root = Spring.application_root_path.to_s
238
+ $LOADED_FEATURES.select { |f| f.start_with?(root) }
178
239
  end
179
240
 
180
241
  def disconnect_database
@@ -202,5 +263,17 @@ module Spring
202
263
  end
203
264
  end
204
265
  end
266
+
267
+ def print_exception(stream, error)
268
+ first, rest = error.backtrace.first, error.backtrace.drop(1)
269
+ stream.puts("#{first}: #{error} (#{error.class})")
270
+ rest.each { |line| stream.puts("\tfrom #{line}") }
271
+ end
272
+
273
+ def update_process_title
274
+ ProcessTitleUpdater.run { |distance|
275
+ "spring app | #{spring_env.app_name} | started #{distance} ago | #{app_env} mode"
276
+ }
277
+ end
205
278
  end
206
279
  end
@@ -0,0 +1,10 @@
1
+ require "spring/application"
2
+
3
+ app = Spring::Application.new(
4
+ UNIXSocket.for_fd(3),
5
+ Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup)
6
+ )
7
+ Signal.trap("TERM") { app.terminate }
8
+
9
+ app.preload if ENV.delete("SPRING_PRELOAD") == "1"
10
+ app.run
@@ -1,20 +1,15 @@
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
14
9
  end
15
10
 
16
11
  def log(message)
17
- server.log(message)
12
+ spring_env.log "[application_manager:#{app_env}] #{message}"
18
13
  end
19
14
 
20
15
  # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
@@ -28,7 +23,6 @@ module Spring
28
23
 
29
24
  def start
30
25
  start_child
31
- start_wait_thread
32
26
  end
33
27
 
34
28
  def restart
@@ -63,13 +57,15 @@ module Spring
63
57
  def run(client)
64
58
  with_child do
65
59
  child.send_io client
66
- child.gets
60
+ child.gets or raise Errno::EPIPE
67
61
  end
68
62
 
69
- pid = child.gets
70
- pid = pid.chomp.to_i if pid
71
- log "got worker pid #{pid}"
72
- pid
63
+ pid = child.gets.to_i
64
+
65
+ unless pid.zero?
66
+ log "got worker pid #{pid}"
67
+ pid
68
+ end
73
69
  rescue Errno::ECONNRESET, Errno::EPIPE => e
74
70
  log "#{e} while reading from child; returning no pid"
75
71
  nil
@@ -84,39 +80,47 @@ module Spring
84
80
  private
85
81
 
86
82
  def start_child(preload = false)
87
- server.application_starting
88
-
89
83
  @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
84
 
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
- }
85
+ Bundler.with_clean_env do
86
+ @pid = Process.spawn(
87
+ {
88
+ "RAILS_ENV" => app_env,
89
+ "RACK_ENV" => app_env,
90
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
91
+ "SPRING_PRELOAD" => preload ? "1" : "0"
92
+ },
93
+ "ruby",
94
+ "-I", File.expand_path("../..", __FILE__),
95
+ "-e", "require 'spring/application/boot'",
96
+ 3 => child_socket
97
+ )
98
+ end
100
99
 
101
- app = Application.new(child_socket)
102
- app.preload if preload
103
- app.run
104
- }
100
+ start_wait_thread(pid, child) if child.gets
105
101
  child_socket.close
106
102
  end
107
103
 
108
- def start_wait_thread
109
- @wait_thread = Thread.new {
110
- Thread.current.abort_on_exception = true
111
-
112
- while alive?
113
- _, status = Process.wait2(pid)
114
- @pid = nil
104
+ def start_wait_thread(pid, child)
105
+ Process.detach(pid)
115
106
 
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? }
107
+ Thread.new {
108
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
109
+ # as if it does we're no longer interested in the child
110
+ loop do
111
+ IO.select([child])
112
+ break if child.recv(1, Socket::MSG_PEEK).empty?
113
+ sleep 0.01
119
114
  end
115
+
116
+ log "child #{pid} shutdown"
117
+
118
+ synchronize {
119
+ if @pid == pid
120
+ @pid = nil
121
+ restart
122
+ end
123
+ }
120
124
  }
121
125
  end
122
126
  end
@@ -0,0 +1,12 @@
1
+ require "socket"
2
+ require "thread"
3
+
4
+ # readline must be required before we setpgid, otherwise the require may hang,
5
+ # if readline has been built against libedit. See issue #70.
6
+ require "readline"
7
+
8
+ require "spring/configuration"
9
+ require "spring/env"
10
+ require "spring/process_title_updater"
11
+ require "spring/json"
12
+ require "spring/watcher"
@@ -23,22 +23,27 @@ CODE
23
23
  # process we'll execute the lines which come after the LOADER block, which
24
24
  # is what we want.
25
25
  #
26
- # Gem.try_activate would be called inside rubygems due to the #require.
27
- # However, when that happens $! gets set and it appears that there is a
28
- # LoadError, which can cause problems. So we activate the gem separately
29
- # to requiring the file.
30
- SPRING = <<CODE
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'
31
30
  #!/usr/bin/env ruby
32
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
+
33
35
  unless defined?(Spring)
34
36
  require "rubygems"
35
37
  require "bundler"
36
38
 
39
+ match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m)
40
+ version = match && match[1]
41
+
42
+ ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR)
37
43
  ENV["GEM_HOME"] = ""
38
- ENV["GEM_PATH"] = Bundler.bundle_path.to_s
39
44
  Gem.paths = ENV
40
45
 
41
- Gem.try_activate("spring/binstub")
46
+ gem "spring", version
42
47
  require "spring/binstub"
43
48
  end
44
49
  CODE
@@ -167,10 +172,8 @@ CODE
167
172
  when :add
168
173
  bindir.mkdir unless bindir.exist?
169
174
 
170
- unless spring_binstub.exist?
171
- File.write(spring_binstub, SPRING)
172
- spring_binstub.chmod 0755
173
- end
175
+ File.write(spring_binstub, SPRING)
176
+ spring_binstub.chmod 0755
174
177
 
175
178
  items.each(&:add)
176
179
  when :remove
@@ -22,15 +22,24 @@ module Spring
22
22
 
23
23
  def application_root_path
24
24
  @application_root_path ||= begin
25
- path = Pathname.new(File.expand_path(application_root || find_project_root))
25
+ if application_root
26
+ path = Pathname.new(File.expand_path(application_root))
27
+ else
28
+ path = project_root_path
29
+ end
30
+
26
31
  raise MissingApplication.new(path) unless path.join("config/application.rb").exist?
27
32
  path
28
33
  end
29
34
  end
30
35
 
36
+ def project_root_path
37
+ @project_root_path ||= find_project_root(Pathname.new(File.expand_path(Dir.pwd)))
38
+ end
39
+
31
40
  private
32
41
 
33
- def find_project_root(current_dir = Pathname.new(Dir.pwd))
42
+ def find_project_root(current_dir)
34
43
  if current_dir.join(gemfile).exist?
35
44
  current_dir
36
45
  elsif current_dir.root?
@@ -14,14 +14,19 @@ module Spring
14
14
  attr_reader :log_file
15
15
 
16
16
  def initialize(root = nil)
17
- @root = root
18
- @log_file = File.open(ENV["SPRING_LOG"] || "/dev/null", "a")
17
+ @root = root
18
+ @project_root = root
19
+ @log_file = File.open(ENV["SPRING_LOG"] || "/dev/null", "a")
19
20
  end
20
21
 
21
22
  def root
22
23
  @root ||= Spring.application_root_path
23
24
  end
24
25
 
26
+ def project_root
27
+ @project_root ||= Spring.project_root_path
28
+ end
29
+
25
30
  def version
26
31
  Spring::VERSION
27
32
  end
@@ -33,7 +38,7 @@ module Spring
33
38
  end
34
39
 
35
40
  def application_id
36
- Digest::MD5.hexdigest(root.to_s)
41
+ Digest::MD5.hexdigest(project_root.to_s)
37
42
  end
38
43
 
39
44
  def socket_path
@@ -60,7 +65,7 @@ module Spring
60
65
  end
61
66
 
62
67
  def server_running?
63
- pidfile = pidfile_path.open('r')
68
+ pidfile = pidfile_path.open('r+')
64
69
  !pidfile.flock(File::LOCK_EX | File::LOCK_NB)
65
70
  rescue Errno::ENOENT
66
71
  false
@@ -71,12 +76,8 @@ module Spring
71
76
  end
72
77
  end
73
78
 
74
- def bundle_mtime
75
- [Bundler.default_lockfile, Bundler.default_gemfile].select(&:exist?).map(&:mtime).max
76
- end
77
-
78
79
  def log(message)
79
- log_file.puts "[#{Time.now}] #{message}"
80
+ log_file.puts "[#{Time.now}] [#{Process.pid}] #{message}"
80
81
  log_file.flush
81
82
  end
82
83
  end
@@ -1,26 +1,13 @@
1
1
  module Spring
2
- class << self
3
- attr_reader :original_env
4
- end
5
- @original_env = ENV.to_hash
2
+ ORIGINAL_ENV = ENV.to_hash
6
3
  end
7
4
 
8
- require "socket"
9
- require "thread"
10
-
11
- require "spring/configuration"
12
- require "spring/env"
5
+ require "spring/boot"
13
6
  require "spring/application_manager"
14
- require "spring/process_title_updater"
15
- require "spring/json"
16
7
 
17
- # Must be last, as it requires bundler/setup
8
+ # Must be last, as it requires bundler/setup, which alters the load path
18
9
  require "spring/commands"
19
10
 
20
- # readline must be required before we setpgid, otherwise the require may hang,
21
- # if readline has been built against libedit. See issue #70.
22
- require "readline"
23
-
24
11
  module Spring
25
12
  class Server
26
13
  def self.boot
@@ -31,7 +18,7 @@ module Spring
31
18
 
32
19
  def initialize(env = Env.new)
33
20
  @env = env
34
- @applications = Hash.new { |h, k| h[k] = ApplicationManager.new(self, k) }
21
+ @applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k) }
35
22
  @pidfile = env.pidfile_path.open('a')
36
23
  @mutex = Mutex.new
37
24
  end
@@ -48,7 +35,6 @@ module Spring
48
35
  ignore_signals
49
36
  set_exit_hook
50
37
  set_process_title
51
- watch_bundle
52
38
  start_server
53
39
  end
54
40
 
@@ -95,7 +81,7 @@ module Spring
95
81
  # Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
96
82
  # will kill the server/application.
97
83
  def ignore_signals
98
- IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
84
+ IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
99
85
  end
100
86
 
101
87
  def set_exit_hook
@@ -138,13 +124,5 @@ module Spring
138
124
  "spring server | #{env.app_name} | started #{distance} ago"
139
125
  }
140
126
  end
141
-
142
- def watch_bundle
143
- @bundle_mtime = env.bundle_mtime
144
- end
145
-
146
- def application_starting
147
- @mutex.synchronize { exit if env.bundle_mtime != @bundle_mtime }
148
- end
149
127
  end
150
128
  end
@@ -1,3 +1,3 @@
1
1
  module Spring
2
- VERSION = "1.1.0.beta1"
2
+ VERSION = "1.1.0.beta2"
3
3
  end
@@ -22,7 +22,7 @@ module Spring
22
22
  @files = Set.new
23
23
  @directories = Set.new
24
24
  @stale = false
25
- @io_listener = nil
25
+ @listeners = []
26
26
  end
27
27
 
28
28
  def add(*items)
@@ -55,15 +55,14 @@ module Spring
55
55
  @stale
56
56
  end
57
57
 
58
- def mark_stale
59
- @stale = true
60
- @io_listener.write "." if @io_listener
58
+ def on_stale(&block)
59
+ @listeners << block
61
60
  end
62
61
 
63
- def to_io
64
- read, write = IO.pipe
65
- @io_listener = write
66
- read
62
+ def mark_stale
63
+ return if stale?
64
+ @stale = true
65
+ @listeners.each(&:call)
67
66
  end
68
67
 
69
68
  def restart
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
10
10
  gem.email = ["j@jonathanleighton.com"]
11
11
  gem.description = %q{Rails application preloader}
12
12
  gem.summary = %q{Rails application preloader}
13
- gem.homepage = "http://github.com/jonleighton/spring"
13
+ gem.homepage = "http://github.com/rails/spring"
14
14
  gem.license = "MIT"
15
15
 
16
16
  gem.files = `git ls-files`.split($/)
@@ -21,28 +21,22 @@ class AppTest < ActiveSupport::TestCase
21
21
  @app ||= Spring::Test::Application.new("#{TEST_ROOT}/apps/tmp")
22
22
  end
23
23
 
24
- def debug(artifacts)
25
- artifacts = artifacts.dup
26
- artifacts.delete :status
27
- app.dump_streams(artifacts.delete(:command), artifacts)
28
- end
29
-
30
24
  def assert_output(artifacts, expected)
31
25
  expected.each do |stream, output|
32
26
  assert artifacts[stream].include?(output),
33
- "expected #{stream} to include '#{output}'.\n\n#{debug(artifacts)}"
27
+ "expected #{stream} to include '#{output}'.\n\n#{app.debug(artifacts)}"
34
28
  end
35
29
  end
36
30
 
37
31
  def assert_success(command, expected_output = nil)
38
32
  artifacts = app.run(*Array(command))
39
- assert artifacts[:status].success?, "expected successful exit status\n\n#{debug(artifacts)}"
33
+ assert artifacts[:status].success?, "expected successful exit status\n\n#{app.debug(artifacts)}"
40
34
  assert_output artifacts, expected_output if expected_output
41
35
  end
42
36
 
43
37
  def assert_failure(command, expected_output = nil)
44
38
  artifacts = app.run(*Array(command))
45
- assert !artifacts[:status].success?, "expected unsuccessful exit status\n\n#{debug(artifacts)}"
39
+ assert !artifacts[:status].success?, "expected unsuccessful exit status\n\n#{app.debug(artifacts)}"
46
40
  assert_output artifacts, expected_output if expected_output
47
41
  end
48
42
 
@@ -196,7 +190,7 @@ class AppTest < ActiveSupport::TestCase
196
190
 
197
191
  test "binstub when spring is uninstalled" do
198
192
  app.run! "gem uninstall --ignore-dependencies spring"
199
- File.write(app.gemfile, app.gemfile.read.sub("gem 'spring-commands-testunit'\n", ""))
193
+ File.write(app.gemfile, app.gemfile.read.gsub(/gem 'spring.*/, ""))
200
194
  assert_success "bin/rake -T", stdout: "rake db:migrate"
201
195
  end
202
196
 
@@ -296,16 +290,29 @@ CODE
296
290
  assert_success "bin/rake", stdout: "test"
297
291
  end
298
292
 
299
- test "changing the Gemfile restarts the server" do
293
+ test "changing the Gemfile works" do
300
294
  assert_success %(bin/rails runner 'require "sqlite3"')
301
295
 
302
296
  File.write(app.gemfile, app.gemfile.read.sub(%{gem 'sqlite3'}, %{# gem 'sqlite3'}))
303
- app.bundle
304
-
305
297
  app.await_reload
298
+
306
299
  assert_failure %(bin/rails runner 'require "sqlite3"'), stderr: "sqlite3"
307
300
  end
308
301
 
302
+ test "changing the Gemfile works when spring calls into itself" do
303
+ File.write(app.path("script.rb"), <<-CODE)
304
+ gemfile = Rails.root.join("Gemfile")
305
+ File.write(gemfile, "\#{gemfile.read}gem 'devise'\\n")
306
+ Bundler.with_clean_env do
307
+ system(#{app.env.inspect}, "bundle install")
308
+ end
309
+ output = `\#{Rails.root.join('bin/rails')} runner 'require "devise"; puts "done";'`
310
+ exit output == "done\n"
311
+ CODE
312
+
313
+ assert_success [%(bin/rails runner 'load Rails.root.join("script.rb")'), timeout: 60]
314
+ end
315
+
309
316
  test "changing the environment between runs" do
310
317
  File.write(app.application_config, "#{app.application_config.read}\nENV['BAR'] = 'bar'")
311
318
 
@@ -57,8 +57,12 @@ module Spring
57
57
  @stderr ||= IO.pipe
58
58
  end
59
59
 
60
+ def log_file_path
61
+ path("tmp/spring.log")
62
+ end
63
+
60
64
  def log_file
61
- @log_file ||= path("tmp/spring.log").open("w+")
65
+ @log_file ||= log_file_path.open("w+")
62
66
  end
63
67
 
64
68
  def env
@@ -68,7 +72,7 @@ module Spring
68
72
  "HOME" => user_home.to_s,
69
73
  "RAILS_ENV" => nil,
70
74
  "RACK_ENV" => nil,
71
- "SPRING_LOG" => log_file.path
75
+ "SPRING_LOG" => log_file_path.to_s
72
76
  }
73
77
  end
74
78
 
@@ -202,6 +206,12 @@ module Spring
202
206
  output
203
207
  end
204
208
 
209
+ def debug(artifacts)
210
+ artifacts = artifacts.dup
211
+ artifacts.delete :status
212
+ dump_streams(artifacts.delete(:command), artifacts)
213
+ end
214
+
205
215
  def await_reload
206
216
  raise "no pid" if @application_pids.nil? || @application_pids.empty?
207
217
 
@@ -212,13 +222,15 @@ module Spring
212
222
 
213
223
  def run!(*args)
214
224
  artifacts = run(*args)
215
- raise "command failed" unless artifacts[:status].success?
225
+ unless artifacts[:status].success?
226
+ raise "command failed\n\n#{debug(artifacts)}"
227
+ end
216
228
  artifacts
217
229
  end
218
230
 
219
231
  def bundle
220
232
  run! "(gem list bundler | grep bundler) || gem install bundler", timeout: nil
221
- run! "bundle check || bundle update", timeout: nil
233
+ run! "bundle update", timeout: nil
222
234
  end
223
235
 
224
236
  private
@@ -238,16 +250,23 @@ module Spring
238
250
  @version_constraint = version_constraint
239
251
  @version = RailsVersion.new(version_constraint.split(' ').last)
240
252
  @application = Application.new(root)
253
+ @bundled = false
241
254
  end
242
255
 
243
256
  def root
244
- "#{TEST_ROOT}/apps/rails-#{version.major}-#{version.minor}"
257
+ "#{TEST_ROOT}/apps/rails-#{version.major}-#{version.minor}-spring-#{Spring::VERSION}"
245
258
  end
246
259
 
247
260
  def system(command)
248
261
  Kernel.system("#{command} > /dev/null") or raise "command failed: #{command}"
249
262
  end
250
263
 
264
+ def bundle
265
+ return if @bundled
266
+ application.bundle
267
+ @bundled = true
268
+ end
269
+
251
270
  # Sporadic SSL errors keep causing test failures so there are anti-SSL workarounds here
252
271
  def generate
253
272
  Bundler.with_clean_env do
@@ -263,6 +282,8 @@ module Spring
263
282
  FileUtils.mkdir_p(application.user_home)
264
283
  FileUtils.rm_rf(application.path("test/performance"))
265
284
 
285
+ File.write(application.gemfile, "#{application.gemfile.read}gem 'spring', '#{Spring::VERSION}'\n")
286
+
266
287
  if version.needs_testunit?
267
288
  File.write(application.gemfile, "#{application.gemfile.read}gem 'spring-commands-testunit'\n")
268
289
  end
@@ -274,7 +295,7 @@ module Spring
274
295
  end
275
296
  end
276
297
 
277
- application.bundle
298
+ bundle
278
299
  application.run! "bundle exec rails g scaffold post title:string"
279
300
  application.run! "bundle exec rake db:migrate db:test:clone"
280
301
  end
@@ -287,7 +308,7 @@ module Spring
287
308
  unless @installed
288
309
  # Need to do this here too because the app may have been generated with
289
310
  # a different ruby
290
- application.bundle
311
+ bundle
291
312
 
292
313
  system("gem build spring.gemspec 2>/dev/null")
293
314
  application.run! "gem install ../../../spring-#{Spring::VERSION}.gem", timeout: nil
@@ -1,6 +1,7 @@
1
1
  require "helper"
2
2
  require "tmpdir"
3
3
  require "fileutils"
4
+ require "timeout"
4
5
  require "active_support/core_ext/numeric/time"
5
6
  require "spring/watcher"
6
7
  require "spring/watcher/polling"
@@ -120,22 +121,25 @@ module WatcherTests
120
121
  assert_stale
121
122
  end
122
123
 
123
- def test_can_io_select
124
+ def test_on_stale
124
125
  file = "#{@dir}/omg"
125
126
  touch file, Time.now - 2.seconds
126
127
 
127
128
  watcher.add file
128
129
  watcher.start
129
130
 
130
- io = watcher.to_io
131
+ stale = false
132
+ watcher.on_stale { stale = true }
131
133
 
132
- Thread.new {
133
- sleep LATENCY * 3
134
- touch file, Time.now
135
- }
134
+ touch file, Time.now
136
135
 
137
- assert IO.select([io], [], [], 1), "IO.select timed out before watcher was readable"
138
- assert watcher.stale?
136
+ Timeout.timeout(1) { sleep 0.01 until stale }
137
+ assert stale
138
+
139
+ # Check that we only get notified once
140
+ stale = false
141
+ sleep LATENCY * 3
142
+ assert !stale
139
143
  end
140
144
 
141
145
  def test_add_relative_path
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spring
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0.beta1
4
+ version: 1.1.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Leighton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-03 00:00:00.000000000 Z
11
+ date: 2014-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -55,8 +55,10 @@ files:
55
55
  - Rakefile
56
56
  - bin/spring
57
57
  - lib/spring/application.rb
58
+ - lib/spring/application/boot.rb
58
59
  - lib/spring/application_manager.rb
59
60
  - lib/spring/binstub.rb
61
+ - lib/spring/boot.rb
60
62
  - lib/spring/client.rb
61
63
  - lib/spring/client/binstub.rb
62
64
  - lib/spring/client/command.rb
@@ -92,7 +94,7 @@ files:
92
94
  - test/unit/commands_test.rb
93
95
  - test/unit/process_title_updater_test.rb
94
96
  - test/unit/watcher_test.rb
95
- homepage: http://github.com/jonleighton/spring
97
+ homepage: http://github.com/rails/spring
96
98
  licenses:
97
99
  - MIT
98
100
  metadata: {}