spring 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/README.md +8 -8
- data/Rakefile +16 -5
- data/lib/spring.rb +37 -18
- data/lib/spring/application.rb +9 -3
- data/lib/spring/application_manager.rb +17 -6
- data/lib/spring/commands.rb +1 -1
- data/lib/spring/env.rb +2 -1
- data/lib/spring/server.rb +17 -9
- data/lib/spring/version.rb +1 -1
- data/spring.gemspec +1 -0
- data/test/acceptance/app_test.rb +22 -13
- data/test/apps/rails-3-2/config/{initializers/spring.rb → spring.rb} +0 -0
- data/test/unit/application_watcher_test.rb +10 -9
- metadata +21 -4
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -79,8 +79,8 @@ That booted our app in the background:
|
|
79
79
|
|
80
80
|
```
|
81
81
|
$ ps ax | grep spring
|
82
|
-
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
83
|
-
8698 pts/6 Sl 0:02 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
82
|
+
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
83
|
+
8698 pts/6 Sl 0:02 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
84
84
|
```
|
85
85
|
|
86
86
|
We can see two processes, one is the Spring server, the other is the
|
@@ -118,8 +118,8 @@ automatically. Note that the application process id is 8698 above. Let's
|
|
118
118
|
```
|
119
119
|
$ touch config/application.rb
|
120
120
|
$ ps ax | grep spring
|
121
|
-
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
122
|
-
8876 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
121
|
+
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
122
|
+
8876 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
123
123
|
```
|
124
124
|
|
125
125
|
The application process detected the change and exited. The server process
|
@@ -151,9 +151,9 @@ the application in development mode.
|
|
151
151
|
|
152
152
|
```
|
153
153
|
$ ps ax | grep spring
|
154
|
-
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
155
|
-
8876 pts/6 Sl 0:15 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
156
|
-
9088 pts/6 Sl 0:01 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup
|
154
|
+
8692 pts/6 Sl 0:00 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
155
|
+
8876 pts/6 Sl 0:15 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
156
|
+
9088 pts/6 Sl 0:01 /home/turnip/.rbenv/versions/1.9.3-p194/bin/ruby -r bundler/setup -r spring/server -e Spring::Server.boot
|
157
157
|
```
|
158
158
|
|
159
159
|
Running rake is faster the second time:
|
@@ -177,7 +177,7 @@ sys 0m0.070s
|
|
177
177
|
|
178
178
|
The following commands are shipped by default.
|
179
179
|
|
180
|
-
Custom commands can be specified in `config/
|
180
|
+
Custom commands can be specified in `config/spring.rb`. See
|
181
181
|
[`lib/spring/commands.rb`](https://github.com/jonleighton/spring/blob/master/lib/spring/commands.rb)
|
182
182
|
for examples.
|
183
183
|
|
data/Rakefile
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
namespace :test do
|
5
|
+
Rake::TestTask.new(:unit) do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList["test/unit/*_test.rb"]
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:acceptance) do |t|
|
12
|
+
t.libs << "test"
|
13
|
+
t.test_files = FileList["test/acceptance/*_test.rb"]
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'run all tests'
|
18
|
+
task all: [:unit, :acceptance]
|
8
19
|
end
|
9
20
|
|
10
|
-
task default: :
|
21
|
+
task default: 'test:all'
|
data/lib/spring.rb
CHANGED
@@ -12,7 +12,8 @@ class Spring
|
|
12
12
|
SERVER_COMMAND = [
|
13
13
|
File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')),
|
14
14
|
"-r", "bundler/setup",
|
15
|
-
|
15
|
+
"-r", "spring/server",
|
16
|
+
"-e", "Spring::Server.boot"
|
16
17
|
]
|
17
18
|
|
18
19
|
def self.run(args)
|
@@ -26,44 +27,62 @@ class Spring
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def server_running?
|
29
|
-
env.
|
30
|
+
if env.pidfile_path.exist?
|
31
|
+
pidfile = env.pidfile_path.open('r')
|
32
|
+
!pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
ensure
|
37
|
+
if pidfile
|
38
|
+
pidfile.flock(File::LOCK_UN)
|
39
|
+
pidfile.close
|
40
|
+
end
|
30
41
|
end
|
31
42
|
|
43
|
+
# Boot the server into the process group of the current session.
|
44
|
+
# This will cause it to be automatically killed once the session
|
45
|
+
# ends (i.e. when the user closes their terminal).
|
32
46
|
def boot_server
|
33
|
-
|
34
|
-
# This will cause it to be automatically killed once the session
|
35
|
-
# ends (i.e. when the user closes their terminal).
|
47
|
+
env.socket_path.unlink if env.socket_path.exist?
|
36
48
|
Process.spawn(*SERVER_COMMAND, pgroup: SID.pgid)
|
37
|
-
sleep 0.1 until
|
49
|
+
sleep 0.1 until env.socket_path.exist?
|
38
50
|
end
|
39
51
|
|
40
52
|
def run(args)
|
41
53
|
boot_server unless server_running?
|
42
54
|
|
43
|
-
|
44
|
-
|
45
|
-
|
55
|
+
application, client = UNIXSocket.pair
|
56
|
+
|
57
|
+
server = UNIXSocket.open(env.socket_name)
|
58
|
+
server.send_io client
|
59
|
+
server.puts rails_env_for(args.first)
|
60
|
+
|
61
|
+
status = server.read(1)
|
62
|
+
|
63
|
+
server.close
|
64
|
+
client.close
|
46
65
|
|
47
|
-
|
66
|
+
return false unless status == "0"
|
48
67
|
|
49
|
-
|
50
|
-
|
51
|
-
|
68
|
+
application.send_io STDOUT
|
69
|
+
application.send_io STDERR
|
70
|
+
application.send_io stdin_slave
|
52
71
|
|
53
|
-
|
72
|
+
application.puts args.length
|
54
73
|
|
55
74
|
args.each do |arg|
|
56
|
-
|
57
|
-
|
75
|
+
application.puts arg.length
|
76
|
+
application.write arg
|
58
77
|
end
|
59
78
|
|
60
79
|
# FIXME: receive exit status from server
|
61
|
-
|
80
|
+
application.read
|
62
81
|
true
|
63
82
|
rescue Errno::ECONNRESET
|
64
83
|
false
|
65
84
|
ensure
|
66
|
-
|
85
|
+
application.close if application
|
67
86
|
end
|
68
87
|
|
69
88
|
private
|
data/lib/spring/application.rb
CHANGED
@@ -47,10 +47,16 @@ class Spring
|
|
47
47
|
loop do
|
48
48
|
watch_application
|
49
49
|
|
50
|
-
client = manager.recv_io
|
51
|
-
client.autoclose = false # prevent GC closing the FD
|
50
|
+
client = manager.recv_io(UNIXSocket)
|
52
51
|
|
53
|
-
|
52
|
+
# Confirm that we have received the client socket. This is necessary on OS X
|
53
|
+
# to prevent a timing error. The client needs to keep the FD that we are receiving
|
54
|
+
# open until it has been received here. Unlike on Linux, it's not sufficient for
|
55
|
+
# the FD to just be in the socket buffer, it has to actually get received at this
|
56
|
+
# end before it can be closed by the client.
|
57
|
+
manager.puts
|
58
|
+
|
59
|
+
serve client
|
54
60
|
end
|
55
61
|
end
|
56
62
|
|
@@ -29,20 +29,31 @@ class Spring
|
|
29
29
|
@pid
|
30
30
|
end
|
31
31
|
|
32
|
+
# The return value of this method indicates whether or not the application
|
33
|
+
# successfully received the client. It might not successfully receive the
|
34
|
+
# client if e.g. the application has an exception during initialization, causing
|
35
|
+
# the application process to die.
|
32
36
|
def run(client)
|
33
37
|
@client = client
|
34
38
|
|
35
39
|
synchronize do
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
if alive?
|
41
|
+
begin
|
42
|
+
child.send_io @client
|
43
|
+
rescue Errno::EPIPE
|
44
|
+
# EPIPE indicates child has died but has not been collected by the wait thread yet
|
45
|
+
start
|
46
|
+
child.send_io @client
|
47
|
+
end
|
48
|
+
else
|
42
49
|
start
|
43
50
|
child.send_io @client
|
44
51
|
end
|
52
|
+
|
53
|
+
child.gets
|
45
54
|
end
|
55
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
56
|
+
false
|
46
57
|
ensure
|
47
58
|
@client.close
|
48
59
|
@client = nil
|
data/lib/spring/commands.rb
CHANGED
data/lib/spring/env.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "pathname"
|
2
2
|
require "spring/sid"
|
3
|
+
require "fileutils"
|
3
4
|
|
4
5
|
class Spring
|
5
6
|
class Env
|
@@ -11,7 +12,7 @@ class Spring
|
|
11
12
|
|
12
13
|
def tmp_path
|
13
14
|
path = root.join('tmp/spring')
|
14
|
-
path
|
15
|
+
FileUtils.mkdir_p(path) unless path.exist?
|
15
16
|
path
|
16
17
|
end
|
17
18
|
|
data/lib/spring/server.rb
CHANGED
@@ -14,6 +14,7 @@ class Spring
|
|
14
14
|
def initialize(env = Env.new)
|
15
15
|
@env = env
|
16
16
|
@applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k) }
|
17
|
+
@pidfile = env.pidfile_path.open('a')
|
17
18
|
end
|
18
19
|
|
19
20
|
def boot
|
@@ -25,7 +26,18 @@ class Spring
|
|
25
26
|
write_pidfile
|
26
27
|
|
27
28
|
server = UNIXServer.open(env.socket_name)
|
28
|
-
loop {
|
29
|
+
loop { serve server.accept }
|
30
|
+
end
|
31
|
+
|
32
|
+
def serve(client)
|
33
|
+
app_client = client.recv_io
|
34
|
+
rails_env = client.gets.chomp
|
35
|
+
|
36
|
+
if @applications[rails_env].run(app_client)
|
37
|
+
client.write "0"
|
38
|
+
else
|
39
|
+
client.write "1"
|
40
|
+
end
|
29
41
|
end
|
30
42
|
|
31
43
|
def set_exit_hook
|
@@ -42,12 +54,10 @@ class Spring
|
|
42
54
|
end
|
43
55
|
|
44
56
|
def write_pidfile
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
file.write("#{Process.pid}\n")
|
50
|
-
file.fsync
|
57
|
+
if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
58
|
+
@pidfile.truncate(0)
|
59
|
+
@pidfile.write("#{Process.pid}\n")
|
60
|
+
@pidfile.fsync
|
51
61
|
else
|
52
62
|
STDERR.puts "#{file.path} is locked; it looks like a server is already running"
|
53
63
|
exit(1)
|
@@ -55,5 +65,3 @@ class Spring
|
|
55
65
|
end
|
56
66
|
end
|
57
67
|
end
|
58
|
-
|
59
|
-
Spring::Server.boot if __FILE__ == $0
|
data/lib/spring/version.rb
CHANGED
data/spring.gemspec
CHANGED
data/test/acceptance/app_test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'helper'
|
2
2
|
require 'io/wait'
|
3
|
+
require "timeout"
|
3
4
|
|
4
5
|
class AppTest < ActiveSupport::TestCase
|
5
6
|
BINFILE = File.expand_path('../bin/spring', TEST_ROOT)
|
@@ -36,20 +37,28 @@ class AppTest < ActiveSupport::TestCase
|
|
36
37
|
)
|
37
38
|
end
|
38
39
|
|
39
|
-
_, status = Process.wait2
|
40
|
+
_, status = Timeout.timeout(opts.fetch(:timeout, 5)) { Process.wait2 }
|
40
41
|
|
41
|
-
out, err =
|
42
|
+
out, err = read_streams
|
42
43
|
|
43
44
|
@times << (Time.now - start_time) if opts.fetch(:timer, true)
|
44
45
|
|
45
|
-
# p status
|
46
|
-
# puts "---"
|
47
|
-
# puts out
|
48
|
-
# puts "***"
|
49
|
-
# puts err
|
50
|
-
# puts "---"
|
51
|
-
|
52
46
|
[status, out, err]
|
47
|
+
rescue Timeout::Error
|
48
|
+
print_streams *read_streams
|
49
|
+
raise
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_streams(out, err)
|
53
|
+
puts "---"
|
54
|
+
puts out
|
55
|
+
puts "***"
|
56
|
+
puts err
|
57
|
+
puts "---"
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_streams
|
61
|
+
[stdout, stderr].map(&:first).map { |s| s.ready? ? s.readpartial(10240) : "" }
|
53
62
|
end
|
54
63
|
|
55
64
|
def await_reload
|
@@ -58,17 +67,17 @@ class AppTest < ActiveSupport::TestCase
|
|
58
67
|
|
59
68
|
def assert_successful_run(*args)
|
60
69
|
status, _, _ = app_run(*args)
|
61
|
-
assert status.success
|
70
|
+
assert status.success?, 'The run should be successful but it was '
|
62
71
|
end
|
63
72
|
|
64
73
|
def assert_unsuccessful_run(*args)
|
65
74
|
status, _, _ = app_run(*args)
|
66
|
-
assert !status.success
|
75
|
+
assert !status.success?, 'The run should not be successful but it was'
|
67
76
|
end
|
68
77
|
|
69
78
|
def assert_stdout(command, expected)
|
70
79
|
_, stdout, _ = app_run(command)
|
71
|
-
assert stdout.include?(expected)
|
80
|
+
assert stdout.include?(expected), "expected '#{expected}' to be printed to stdout. But it wasn't, the stdout is:\n#{stdout}"
|
72
81
|
end
|
73
82
|
|
74
83
|
def assert_speedup(opts = {})
|
@@ -92,7 +101,7 @@ class AppTest < ActiveSupport::TestCase
|
|
92
101
|
|
93
102
|
@times = []
|
94
103
|
|
95
|
-
app_run "bundle check || bundle update", timer: false
|
104
|
+
app_run "bundle check || bundle update", timer: false, timeout: nil
|
96
105
|
app_run "bundle exec rake db:migrate", timer: false
|
97
106
|
end
|
98
107
|
|
File without changes
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require "helper"
|
2
2
|
require "fileutils"
|
3
|
+
require "active_support/core_ext/numeric/time"
|
3
4
|
require "spring/application_watcher"
|
4
5
|
|
5
6
|
class ApplicationWatcherTest < Test::Unit::TestCase
|
@@ -12,21 +13,21 @@ class ApplicationWatcherTest < Test::Unit::TestCase
|
|
12
13
|
FileUtils.rm_r(@dir)
|
13
14
|
end
|
14
15
|
|
15
|
-
def touch(file)
|
16
|
-
|
17
|
-
|
16
|
+
def touch(file, mtime = nil)
|
17
|
+
options = {}
|
18
|
+
options[:mtime] = mtime if mtime
|
19
|
+
FileUtils.touch(file, options)
|
18
20
|
end
|
19
21
|
|
20
22
|
def test_file_mtime
|
21
23
|
file = "#{@dir}/omg"
|
22
|
-
touch file
|
24
|
+
touch file, Time.now - 2.seconds
|
23
25
|
|
24
26
|
watcher = Spring::ApplicationWatcher.new
|
25
27
|
watcher.add_files [file]
|
26
28
|
|
27
29
|
assert !watcher.stale?
|
28
|
-
|
29
|
-
touch file
|
30
|
+
touch file, Time.now
|
30
31
|
assert watcher.stale?
|
31
32
|
end
|
32
33
|
|
@@ -39,16 +40,16 @@ class ApplicationWatcherTest < Test::Unit::TestCase
|
|
39
40
|
|
40
41
|
assert !watcher.stale?
|
41
42
|
|
42
|
-
touch "#{@dir}/1/foo"
|
43
|
+
touch "#{@dir}/1/foo", Time.now - 1.minute
|
43
44
|
assert !watcher.stale?
|
44
45
|
|
45
|
-
touch "#{@dir}/1/foo.rb"
|
46
|
+
touch "#{@dir}/1/foo.rb", 2.seconds
|
46
47
|
assert watcher.stale?
|
47
48
|
|
48
49
|
watcher.reset
|
49
50
|
assert !watcher.stale?
|
50
51
|
|
51
|
-
touch "#{@dir}/2/foo"
|
52
|
+
touch "#{@dir}/2/foo", Time.now
|
52
53
|
assert watcher.stale?
|
53
54
|
end
|
54
55
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spring
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -27,6 +27,22 @@ dependencies:
|
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
30
46
|
description: Rails application preloader
|
31
47
|
email:
|
32
48
|
- j@jonathanleighton.com
|
@@ -36,6 +52,7 @@ extensions: []
|
|
36
52
|
extra_rdoc_files: []
|
37
53
|
files:
|
38
54
|
- .gitignore
|
55
|
+
- .travis.yml
|
39
56
|
- Gemfile
|
40
57
|
- LICENSE.txt
|
41
58
|
- README.md
|
@@ -88,10 +105,10 @@ files:
|
|
88
105
|
- test/apps/rails-3-2/config/initializers/mime_types.rb
|
89
106
|
- test/apps/rails-3-2/config/initializers/secret_token.rb
|
90
107
|
- test/apps/rails-3-2/config/initializers/session_store.rb
|
91
|
-
- test/apps/rails-3-2/config/initializers/spring.rb
|
92
108
|
- test/apps/rails-3-2/config/initializers/wrap_parameters.rb
|
93
109
|
- test/apps/rails-3-2/config/locales/en.yml
|
94
110
|
- test/apps/rails-3-2/config/routes.rb
|
111
|
+
- test/apps/rails-3-2/config/spring.rb
|
95
112
|
- test/apps/rails-3-2/db/migrate/20121109171227_create_posts.rb
|
96
113
|
- test/apps/rails-3-2/db/schema.rb
|
97
114
|
- test/apps/rails-3-2/db/seeds.rb
|
@@ -182,10 +199,10 @@ test_files:
|
|
182
199
|
- test/apps/rails-3-2/config/initializers/mime_types.rb
|
183
200
|
- test/apps/rails-3-2/config/initializers/secret_token.rb
|
184
201
|
- test/apps/rails-3-2/config/initializers/session_store.rb
|
185
|
-
- test/apps/rails-3-2/config/initializers/spring.rb
|
186
202
|
- test/apps/rails-3-2/config/initializers/wrap_parameters.rb
|
187
203
|
- test/apps/rails-3-2/config/locales/en.yml
|
188
204
|
- test/apps/rails-3-2/config/routes.rb
|
205
|
+
- test/apps/rails-3-2/config/spring.rb
|
189
206
|
- test/apps/rails-3-2/db/migrate/20121109171227_create_posts.rb
|
190
207
|
- test/apps/rails-3-2/db/schema.rb
|
191
208
|
- test/apps/rails-3-2/db/seeds.rb
|