spring 1.1.0.beta1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fe796fa453e40306865cb1cf6e6541ca01571d5d
4
- data.tar.gz: 7210043ae71cdb012a21734c86984922a8127afe
3
+ metadata.gz: a2357648d0fc0f884098f352ab693e66556de6df
4
+ data.tar.gz: df070385ccdf9fbbb6d48770f21886dbab93516e
5
5
  SHA512:
6
- metadata.gz: 3c1bcd8097999582b66b2507029e807be714455fe95f70a52e222a3221c5e3921e89ba1131e3b16bd0d775eaae7f65c660b87e65c22526d3e2e79544a226e532
7
- data.tar.gz: a0c20068387c73fd9203e6fc408ab1789240e9b244e4e627dc233f04f9850e3c984aca138a81f59b7c553958859e02349c15659910aa789ee95b5595cf95db5c
6
+ metadata.gz: cc65ad0c92b3a134505805c9b8ed999029b49030ab0ca8e46473c47c18a6ff17804de47897a7493e28765dc3fa6dc18df2de5beb4be7b8f6bdf04bd979d2a7da
7
+ data.tar.gz: 1b408b559590cf9a1b13da2d390d191964809107edc00c4b67e6c54384d05df1a5f9673fcc796318e1962c1f3c4e9fd49141596a3692c80b831d9cc933dd67c5
data/.travis.yml CHANGED
@@ -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
data/CHANGELOG.md CHANGED
@@ -1,8 +1,9 @@
1
- ## Next version
1
+ ## 1.1.0
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
data/bin/spring CHANGED
@@ -21,6 +21,7 @@ if defined?(Gem)
21
21
 
22
22
  if unstubbed.any?
23
23
  warn "Warning: Running `gem pristine --all` to regenerate your installed gemspecs " \
24
+ "(and deleting then reinstalling your bundle if you use bundle --path) " \
24
25
  "will improve the startup performance of Spring."
25
26
  end
26
27
  end
@@ -0,0 +1,16 @@
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
+
8
+ Signal.trap("TERM") { app.terminate }
9
+ Signal.trap("TTOU", "IGNORE")
10
+
11
+ Spring::ProcessTitleUpdater.run { |distance|
12
+ "spring app | #{app.app_name} | started #{distance} ago | #{app.app_env} mode"
13
+ }
14
+
15
+ app.preload if ENV.delete("SPRING_PRELOAD") == "1"
16
+ app.run
@@ -1,40 +1,83 @@
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
18
+
19
+ def state(val)
20
+ return if exiting?
21
+ log "#{@state} -> #{val}"
22
+ @state = val
23
+ end
24
+
25
+ def state!(val)
26
+ state val
27
+ @interrupt.last.write "."
28
+ end
8
29
 
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
30
+ def app_env
31
+ ENV['RAILS_ENV']
32
+ end
17
33
 
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]
34
+ def app_name
35
+ spring_env.app_name
21
36
  end
22
37
 
23
38
  def log(message)
24
- spring_env.log "[application:#{ENV['RAILS_ENV']}] #{message}"
39
+ spring_env.log "[application:#{app_env}] #{message}"
25
40
  end
26
41
 
27
42
  def preloaded?
28
43
  @preloaded
29
44
  end
30
45
 
46
+ def preload_failed?
47
+ @preloaded == :failure
48
+ end
49
+
31
50
  def exiting?
32
- @exiting
51
+ @state == :exiting
52
+ end
53
+
54
+ def terminating?
55
+ @state == :terminating
56
+ end
57
+
58
+ def watcher_stale?
59
+ @state == :watcher_stale
60
+ end
61
+
62
+ def initialized?
63
+ @state == :initialized
64
+ end
65
+
66
+ def start_watcher
67
+ @watcher = Spring.watcher
68
+ @watcher.on_stale { state! :watcher_stale }
69
+ @watcher.start
33
70
  end
34
71
 
35
72
  def preload
36
73
  log "preloading app"
37
74
 
75
+ begin
76
+ require "spring/commands"
77
+ ensure
78
+ start_watcher
79
+ end
80
+
38
81
  require Spring.application_root_path.join("config", "application")
39
82
 
40
83
  # config/environments/test.rb will have config.cache_classes = true. However
@@ -50,45 +93,42 @@ module Spring
50
93
  Rails.application.config.cache_classes = false
51
94
  disconnect_database
52
95
 
96
+ @preloaded = :success
97
+ rescue Exception => e
98
+ @preloaded = :failure
99
+ watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
100
+ raise e unless initialized?
101
+ ensure
53
102
  watcher.add loaded_application_features
54
103
  watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"
55
- watcher.add Rails.application.paths["config/initializers"]
56
- watcher.add Rails.application.paths["config/database"]
57
104
 
58
- @preloaded = true
105
+ if defined?(Rails)
106
+ watcher.add Rails.application.paths["config/initializers"]
107
+ watcher.add Rails.application.paths["config/database"]
108
+ end
59
109
  end
60
110
 
61
111
  def run
62
- log "running"
63
- watcher.start
112
+ state :running
113
+ manager.puts
64
114
 
65
115
  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
116
+ IO.select [manager, @interrupt.first]
117
+
118
+ if terminating? || watcher_stale? || preload_failed?
119
+ exit
74
120
  else
75
121
  serve manager.recv_io(UNIXSocket)
76
122
  end
77
123
  end
78
124
  end
79
125
 
80
- def try_exit
81
- @mutex.synchronize {
82
- exit if exiting? && @waiting == 0
83
- }
84
- end
85
-
86
126
  def serve(client)
87
127
  log "got client"
88
128
  manager.puts
89
129
 
90
- streams = 3.times.map { client.recv_io }
91
- [STDOUT, STDERR].zip(streams).each { |a, b| a.reopen(b) }
130
+ stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
131
+ [STDOUT, STDERR].zip([stdout, stderr]).each { |a, b| a.reopen(b) }
92
132
 
93
133
  preload unless preloaded?
94
134
 
@@ -105,13 +145,15 @@ module Spring
105
145
 
106
146
  pid = fork {
107
147
  Process.setsid
108
- STDIN.reopen(streams.last)
148
+ STDIN.reopen(stdin)
109
149
  IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
150
+ trap("TERM", "DEFAULT")
110
151
 
111
152
  ARGV.replace(args)
153
+ $0 = command.process_title
112
154
 
113
155
  # 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 }
156
+ original_env.each { |k, v| ENV.delete k if ENV[k] == v }
115
157
 
116
158
  # Load in the current env vars, except those which *were* changed when spring started
117
159
  env.each { |k, v| ENV[k] ||= v }
@@ -147,15 +189,37 @@ module Spring
147
189
  client.close
148
190
 
149
191
  @mutex.synchronize { @waiting -= 1 }
150
- try_exit
192
+ exit_if_finished
151
193
  }
152
194
 
153
- rescue => e
195
+ rescue Exception => e
154
196
  log "exception: #{e}"
155
- streams.each(&:close) if streams
156
- client.puts(1)
197
+ manager.puts unless pid
198
+
199
+ if streams && !e.is_a?(SystemExit)
200
+ print_exception(stderr, e)
201
+ streams.each(&:close)
202
+ end
203
+
204
+ client.puts(1) if pid
157
205
  client.close
158
- raise
206
+ end
207
+
208
+ def terminate
209
+ state! :terminating
210
+ end
211
+
212
+ def exit
213
+ state :exiting
214
+ manager.shutdown(:RDWR)
215
+ exit_if_finished
216
+ sleep
217
+ end
218
+
219
+ def exit_if_finished
220
+ @mutex.synchronize {
221
+ Kernel.exit if exiting? && @waiting == 0
222
+ }
159
223
  end
160
224
 
161
225
  # The command might need to require some files in the
@@ -174,7 +238,8 @@ module Spring
174
238
  end
175
239
 
176
240
  def loaded_application_features
177
- $LOADED_FEATURES.select { |f| f.start_with?(File.realpath(Rails.root)) }
241
+ root = Spring.application_root_path.to_s
242
+ $LOADED_FEATURES.select { |f| f.start_with?(root) }
178
243
  end
179
244
 
180
245
  def disconnect_database
@@ -202,5 +267,11 @@ module Spring
202
267
  end
203
268
  end
204
269
  end
270
+
271
+ def print_exception(stream, error)
272
+ first, rest = error.backtrace.first, error.backtrace.drop(1)
273
+ stream.puts("#{first}: #{error} (#{error.class})")
274
+ rest.each { |line| stream.puts("\tfrom #{line}") }
275
+ end
205
276
  end
206
277
  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,58 @@ module Spring
78
76
  end
79
77
 
80
78
  def stop
81
- Process.kill('TERM', pid) if pid
79
+ @state = :stopping
80
+
81
+ if pid
82
+ Process.kill('TERM', pid)
83
+ Process.wait(pid)
84
+ end
82
85
  end
83
86
 
84
87
  private
85
88
 
86
89
  def start_child(preload = false)
87
- server.application_starting
88
-
89
90
  @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
91
 
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
- }
92
+ Bundler.with_clean_env do
93
+ @pid = Process.spawn(
94
+ {
95
+ "RAILS_ENV" => app_env,
96
+ "RACK_ENV" => app_env,
97
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
98
+ "SPRING_PRELOAD" => preload ? "1" : "0"
99
+ },
100
+ "ruby",
101
+ "-I", File.expand_path("../..", __FILE__),
102
+ "-e", "require 'spring/application/boot'",
103
+ 3 => child_socket
104
+ )
105
+ end
100
106
 
101
- app = Application.new(child_socket)
102
- app.preload if preload
103
- app.run
104
- }
107
+ start_wait_thread(pid, child) if child.gets
105
108
  child_socket.close
106
109
  end
107
110
 
108
- def start_wait_thread
109
- @wait_thread = Thread.new {
110
- Thread.current.abort_on_exception = true
111
+ def start_wait_thread(pid, child)
112
+ Process.detach(pid)
111
113
 
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? }
114
+ Thread.new {
115
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
116
+ # as if it does we're no longer interested in the child
117
+ loop do
118
+ IO.select([child])
119
+ break if child.recv(1, Socket::MSG_PEEK).empty?
120
+ sleep 0.01
119
121
  end
122
+
123
+ log "child #{pid} shutdown"
124
+
125
+ synchronize {
126
+ if @pid == pid
127
+ @pid = nil
128
+ restart
129
+ end
130
+ }
120
131
  }
121
132
  end
122
133
  end
@@ -1,12 +1,13 @@
1
- command = File.basename($0)
1
+ command = File.basename($0)
2
+ bin_path = File.expand_path("../../../bin/spring", __FILE__)
2
3
 
3
4
  if command == "spring"
4
- load Gem.bin_path("spring", "spring")
5
+ load bin_path
5
6
  else
6
7
  disable = ENV["DISABLE_SPRING"]
7
8
 
8
9
  if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
9
10
  ARGV.unshift(command)
10
- load Gem.bin_path("spring", "spring")
11
+ load bin_path
11
12
  end
12
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"
@@ -23,23 +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
 
37
- ENV["GEM_HOME"] = ""
38
- ENV["GEM_PATH"] = Bundler.bundle_path.to_s
39
- Gem.paths = ENV
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
40
43
 
41
- Gem.try_activate("spring/binstub")
42
- require "spring/binstub"
44
+ gem "spring", match[1]
45
+ require "spring/binstub"
46
+ end
43
47
  end
44
48
  CODE
45
49
 
@@ -167,10 +171,8 @@ CODE
167
171
  when :add
168
172
  bindir.mkdir unless bindir.exist?
169
173
 
170
- unless spring_binstub.exist?
171
- File.write(spring_binstub, SPRING)
172
- spring_binstub.chmod 0755
173
- end
174
+ File.write(spring_binstub, SPRING)
175
+ spring_binstub.chmod 0755
174
176
 
175
177
  items.each(&:add)
176
178
  when :remove
@@ -37,11 +37,14 @@ module Spring
37
37
  if command.respond_to?(:call)
38
38
  command.call
39
39
  else
40
- $0 = exec
41
40
  load exec
42
41
  end
43
42
  end
44
43
 
44
+ def process_title
45
+ ["spring", name, *ARGV].join(" ")
46
+ end
47
+
45
48
  def gem_name
46
49
  if command.respond_to?(:gem_name)
47
50
  command.gem_name
@@ -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?
data/lib/spring/env.rb CHANGED
@@ -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
data/lib/spring/server.rb CHANGED
@@ -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
@@ -106,13 +92,13 @@ module Spring
106
92
  end
107
93
 
108
94
  def shutdown
109
- @applications.values.each(&:stop)
110
-
111
95
  [env.socket_path, env.pidfile_path].each do |path|
112
96
  if path.exist?
113
97
  path.unlink rescue nil
114
98
  end
115
99
  end
100
+
101
+ @applications.values.map { |a| Thread.new { a.stop } }.map(&:join)
116
102
  end
117
103
 
118
104
  def write_pidfile
@@ -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"
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
data/spring.gemspec CHANGED
@@ -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
 
@@ -202,6 +202,12 @@ module Spring
202
202
  output
203
203
  end
204
204
 
205
+ def debug(artifacts)
206
+ artifacts = artifacts.dup
207
+ artifacts.delete :status
208
+ dump_streams(artifacts.delete(:command), artifacts)
209
+ end
210
+
205
211
  def await_reload
206
212
  raise "no pid" if @application_pids.nil? || @application_pids.empty?
207
213
 
@@ -212,13 +218,15 @@ module Spring
212
218
 
213
219
  def run!(*args)
214
220
  artifacts = run(*args)
215
- raise "command failed" unless artifacts[:status].success?
221
+ unless artifacts[:status].success?
222
+ raise "command failed\n\n#{debug(artifacts)}"
223
+ end
216
224
  artifacts
217
225
  end
218
226
 
219
227
  def bundle
220
228
  run! "(gem list bundler | grep bundler) || gem install bundler", timeout: nil
221
- run! "bundle check || bundle update", timeout: nil
229
+ run! "bundle update", timeout: nil
222
230
  end
223
231
 
224
232
  private
@@ -238,14 +246,22 @@ module Spring
238
246
  @version_constraint = version_constraint
239
247
  @version = RailsVersion.new(version_constraint.split(' ').last)
240
248
  @application = Application.new(root)
249
+ @bundled = false
241
250
  end
242
251
 
243
252
  def root
244
- "#{TEST_ROOT}/apps/rails-#{version.major}-#{version.minor}"
253
+ "#{TEST_ROOT}/apps/rails-#{version.major}-#{version.minor}-spring-#{Spring::VERSION}"
245
254
  end
246
255
 
247
256
  def system(command)
248
- Kernel.system("#{command} > /dev/null") or raise "command failed: #{command}"
257
+ if ENV["SPRING_DEBUG"]
258
+ puts "$ #{command}\n"
259
+ else
260
+ command = "(#{command}) > /dev/null"
261
+ end
262
+
263
+ Kernel.system(command) or raise "command failed: #{command}"
264
+ puts if ENV["SPRING_DEBUG"]
249
265
  end
250
266
 
251
267
  # Sporadic SSL errors keep causing test failures so there are anti-SSL workarounds here
@@ -263,6 +279,8 @@ module Spring
263
279
  FileUtils.mkdir_p(application.user_home)
264
280
  FileUtils.rm_rf(application.path("test/performance"))
265
281
 
282
+ File.write(application.gemfile, "#{application.gemfile.read}gem 'spring', '#{Spring::VERSION}'\n")
283
+
266
284
  if version.needs_testunit?
267
285
  File.write(application.gemfile, "#{application.gemfile.read}gem 'spring-commands-testunit'\n")
268
286
  end
@@ -274,7 +292,8 @@ module Spring
274
292
  end
275
293
  end
276
294
 
277
- application.bundle
295
+ install_spring
296
+
278
297
  application.run! "bundle exec rails g scaffold post title:string"
279
298
  application.run! "bundle exec rake db:migrate db:test:clone"
280
299
  end
@@ -284,28 +303,26 @@ module Spring
284
303
  end
285
304
 
286
305
  def install_spring
287
- unless @installed
288
- # Need to do this here too because the app may have been generated with
289
- # a different ruby
290
- application.bundle
306
+ return if @installed
291
307
 
292
- system("gem build spring.gemspec 2>/dev/null")
293
- application.run! "gem install ../../../spring-#{Spring::VERSION}.gem", timeout: nil
308
+ system("gem build spring.gemspec 2>&1")
309
+ application.run! "gem install ../../../spring-#{Spring::VERSION}.gem", timeout: nil
294
310
 
295
- FileUtils.rm_rf application.path("bin")
311
+ application.bundle
296
312
 
297
- if application.path("bin_original").exist?
298
- FileUtils.cp_r application.path("bin_original"), application.path("bin")
299
- end
313
+ FileUtils.rm_rf application.path("bin")
300
314
 
301
- application.run! "#{application.spring} binstub --all"
302
- @installed = true
315
+ if application.path("bin_original").exist?
316
+ FileUtils.cp_r application.path("bin_original"), application.path("bin")
303
317
  end
318
+
319
+ application.run! "#{application.spring} binstub --all"
320
+ @installed = true
304
321
  end
305
322
 
306
323
  def copy_to(path)
307
- FileUtils.rm_rf(path.to_s)
308
- FileUtils.cp_r(application.root.to_s, path.to_s)
324
+ system("rm -rf #{path}")
325
+ system("cp -r #{application.root} #{path}")
309
326
  end
310
327
  end
311
328
  end
@@ -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
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-17 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: {}
@@ -107,9 +109,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
109
  version: '0'
108
110
  required_rubygems_version: !ruby/object:Gem::Requirement
109
111
  requirements:
110
- - - '>'
112
+ - - '>='
111
113
  - !ruby/object:Gem::Version
112
- version: 1.3.1
114
+ version: '0'
113
115
  requirements: []
114
116
  rubyforge_project:
115
117
  rubygems_version: 2.1.9