spring 1.1.0.beta1 → 1.1.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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: {}