spring 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/README.md +69 -11
- data/Rakefile +1 -1
- data/lib/spring/application.rb +36 -34
- data/lib/spring/application_manager.rb +3 -1
- data/lib/spring/client.rb +3 -1
- data/lib/spring/client/binstub.rb +4 -0
- data/lib/spring/client/help.rb +69 -13
- data/lib/spring/client/run.rb +7 -9
- data/lib/spring/client/status.rb +30 -0
- data/lib/spring/client/stop.rb +27 -1
- data/lib/spring/commands.rb +46 -10
- data/lib/spring/configuration.rb +8 -0
- data/lib/spring/env.rb +5 -2
- data/lib/spring/process_title_updater.rb +65 -0
- data/lib/spring/server.rb +34 -3
- data/lib/spring/sid.rb +31 -8
- data/lib/spring/version.rb +1 -1
- data/test/acceptance/app_test.rb +119 -116
- data/test/apps/rails-3-2/.gitignore +1 -1
- data/test/unit/client/help_test.rb +50 -0
- data/test/unit/commands_test.rb +21 -2
- data/test/unit/process_title_updater_test.rb +24 -0
- metadata +9 -5
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -40,7 +40,17 @@ real shell and a terminal running the rails "commands console".
|
|
40
40
|
|
41
41
|
## Compatibility
|
42
42
|
|
43
|
-
|
43
|
+
Ruby versions supported:
|
44
|
+
|
45
|
+
* MRI 1.9.3
|
46
|
+
* MRI 2.0.0
|
47
|
+
|
48
|
+
Rails versions supported:
|
49
|
+
|
50
|
+
* 3.2
|
51
|
+
|
52
|
+
Spring makes extensive use of `Process#fork`, so won't be able to run on
|
53
|
+
any platform which doesn't support that (Windows, JRuby).
|
44
54
|
|
45
55
|
## Usage
|
46
56
|
|
@@ -76,9 +86,11 @@ sys 0m0.066s
|
|
76
86
|
That booted our app in the background:
|
77
87
|
|
78
88
|
```
|
79
|
-
$
|
80
|
-
|
81
|
-
|
89
|
+
$ spring status
|
90
|
+
Spring is running:
|
91
|
+
|
92
|
+
26150 spring server | rails-3-2 | started 3 secs ago
|
93
|
+
26155 spring app | rails-3-2 | started 3 secs ago | test mode
|
82
94
|
```
|
83
95
|
|
84
96
|
We can see two processes, one is the Spring server, the other is the
|
@@ -147,9 +159,11 @@ automatically. Let's "edit" `config/application.rb`:
|
|
147
159
|
|
148
160
|
```
|
149
161
|
$ touch config/application.rb
|
150
|
-
$
|
151
|
-
|
152
|
-
|
162
|
+
$ spring status
|
163
|
+
Spring is running:
|
164
|
+
|
165
|
+
26150 spring server | rails-3-2 | started 36 secs ago
|
166
|
+
26556 spring app | rails-3-2 | started 1 sec ago | test mode
|
153
167
|
```
|
154
168
|
|
155
169
|
The application process detected the change and exited. The server process
|
@@ -178,10 +192,19 @@ We now have 3 processes: the server, and application in test mode and
|
|
178
192
|
the application in development mode.
|
179
193
|
|
180
194
|
```
|
181
|
-
$
|
182
|
-
|
183
|
-
|
184
|
-
|
195
|
+
$ bin/spring status
|
196
|
+
Spring is running:
|
197
|
+
|
198
|
+
26150 spring server | rails-3-2 | started 1 min ago
|
199
|
+
26556 spring app | rails-3-2 | started 42 secs ago | test mode
|
200
|
+
26707 spring app | rails-3-2 | started 2 secs ago | development mode
|
201
|
+
```
|
202
|
+
|
203
|
+
To stop the background processes:
|
204
|
+
|
205
|
+
```
|
206
|
+
$ bin/spring stop
|
207
|
+
Spring stopped.
|
185
208
|
```
|
186
209
|
|
187
210
|
## Commands
|
@@ -246,6 +269,41 @@ Spring where your app is located:
|
|
246
269
|
Spring.application_root = './test/dummy'
|
247
270
|
```
|
248
271
|
|
272
|
+
### preload files
|
273
|
+
|
274
|
+
Every Spring command has the ability to preload a set of files. The
|
275
|
+
`test` command for example preloads `test_helper` (it also adds the
|
276
|
+
`test/` directory to your load path). If the
|
277
|
+
defaults don't work for your application you can configure the
|
278
|
+
preloads for every command:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
# if your test helper is called "helper"
|
282
|
+
Commands::Command::Test.preloads = %w(helper)
|
283
|
+
|
284
|
+
# if you don't want to preload spec_helper.rb
|
285
|
+
Commands::Command::RSpec.preloads = []
|
286
|
+
|
287
|
+
# if you want to preload additional files for the console
|
288
|
+
Commands::Command::Console.preloads << 'extenstions/console_helper'
|
289
|
+
```
|
290
|
+
|
291
|
+
### after fork callbacks
|
292
|
+
|
293
|
+
You might want to run code after Spring forked off the process but
|
294
|
+
before the actual command is run. You might want to use an
|
295
|
+
`after_fork` callback if you have to connect to an external service,
|
296
|
+
do some general cleanup or set up dynamic configuration.
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
Spring.after_fork do
|
300
|
+
# run arbitrary code
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
If you want to register multiple callbacks you can simply call
|
305
|
+
`Spring.after_fork` multiple times with different blocks.
|
306
|
+
|
249
307
|
### tmp directory
|
250
308
|
|
251
309
|
Spring needs a tmp directory. This will default to `Rails.root.join('tmp', 'spring')`.
|
data/Rakefile
CHANGED
data/lib/spring/application.rb
CHANGED
@@ -19,9 +19,6 @@ module Spring
|
|
19
19
|
@manager = manager
|
20
20
|
@watcher = watcher
|
21
21
|
@setup = Set.new
|
22
|
-
|
23
|
-
@stdout = IO.new(STDOUT.fileno)
|
24
|
-
@stderr = IO.new(STDERR.fileno)
|
25
22
|
end
|
26
23
|
|
27
24
|
def start
|
@@ -57,30 +54,39 @@ module Spring
|
|
57
54
|
end
|
58
55
|
|
59
56
|
def serve(client)
|
60
|
-
|
61
|
-
|
62
|
-
args_length
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
57
|
+
streams = 3.times.map { client.recv_io }
|
58
|
+
args_length = client.gets.to_i
|
59
|
+
args = args_length.times.map { client.read(client.gets.to_i) }
|
60
|
+
command = Spring.command(args.shift)
|
61
|
+
|
62
|
+
setup command
|
63
|
+
|
64
|
+
ActionDispatch::Reloader.cleanup!
|
65
|
+
ActionDispatch::Reloader.prepare!
|
66
|
+
|
67
|
+
pid = fork {
|
68
|
+
Process.setsid
|
69
|
+
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
|
70
|
+
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
|
71
|
+
invoke_after_fork_callbacks
|
72
|
+
command.call(args)
|
73
|
+
}
|
74
|
+
|
75
|
+
manager.puts pid
|
76
|
+
|
77
|
+
# Wait in a separate thread so we can run multiple commands at once
|
78
|
+
Thread.new {
|
79
|
+
_, status = Process.wait2 pid
|
80
|
+
streams.each(&:close)
|
81
|
+
client.puts(status.exitstatus)
|
82
|
+
client.close
|
83
|
+
}
|
84
|
+
|
85
|
+
rescue => e
|
86
|
+
streams.each(&:close) if streams
|
87
|
+
client.puts(1)
|
83
88
|
client.close
|
89
|
+
raise
|
84
90
|
end
|
85
91
|
|
86
92
|
# The command might need to require some files in the
|
@@ -99,14 +105,10 @@ module Spring
|
|
99
105
|
end
|
100
106
|
end
|
101
107
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
yield
|
107
|
-
ensure
|
108
|
-
STDOUT.reopen @stdout
|
109
|
-
STDERR.reopen @stderr
|
108
|
+
def invoke_after_fork_callbacks
|
109
|
+
Spring.after_fork_callbacks.each do |callback|
|
110
|
+
callback.call
|
111
|
+
end
|
110
112
|
end
|
111
113
|
end
|
112
114
|
end
|
@@ -70,7 +70,9 @@ module Spring
|
|
70
70
|
[STDOUT, STDERR].each { |s| s.reopen('/dev/null', 'w') } if silence
|
71
71
|
@client.close if @client
|
72
72
|
ENV['RAILS_ENV'] = ENV['RACK_ENV'] = app_env
|
73
|
-
|
73
|
+
ProcessTitleUpdater.run { |distance|
|
74
|
+
"spring app | #{spring_env.app_name} | started #{distance} ago | #{app_env} mode"
|
75
|
+
}
|
74
76
|
Application.new(child_socket).start
|
75
77
|
}
|
76
78
|
child_socket.close
|
data/lib/spring/client.rb
CHANGED
@@ -4,13 +4,15 @@ require "spring/client/run"
|
|
4
4
|
require "spring/client/help"
|
5
5
|
require "spring/client/binstub"
|
6
6
|
require "spring/client/stop"
|
7
|
+
require "spring/client/status"
|
7
8
|
|
8
9
|
module Spring
|
9
10
|
module Client
|
10
11
|
COMMANDS = {
|
11
12
|
"help" => Client::Help,
|
12
13
|
"binstub" => Client::Binstub,
|
13
|
-
"stop" => Client::Stop
|
14
|
+
"stop" => Client::Stop,
|
15
|
+
"status" => Client::Status
|
14
16
|
}
|
15
17
|
|
16
18
|
def self.run(args)
|
data/lib/spring/client/help.rb
CHANGED
@@ -3,20 +3,76 @@ require "spring/version"
|
|
3
3
|
module Spring
|
4
4
|
module Client
|
5
5
|
class Help < Command
|
6
|
+
attr_reader :spring_commands, :application_commands
|
7
|
+
|
8
|
+
def self.description
|
9
|
+
"Print available commands."
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(args, spring_commands = nil, application_commands = nil)
|
13
|
+
super args
|
14
|
+
|
15
|
+
@spring_commands = spring_commands || Spring::Client::COMMANDS
|
16
|
+
@application_commands = application_commands || Spring.commands
|
17
|
+
end
|
18
|
+
|
6
19
|
def call
|
7
|
-
puts
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
puts formatted_help
|
21
|
+
end
|
22
|
+
|
23
|
+
def formatted_help
|
24
|
+
["Usage: spring COMMAND [ARGS]\n",
|
25
|
+
*spring_command_help,
|
26
|
+
'',
|
27
|
+
*application_command_help].join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def spring_command_help
|
31
|
+
["Commands for spring itself:\n",
|
32
|
+
*client_commands.map { |c,n| display_value(c,n) }]
|
33
|
+
end
|
34
|
+
|
35
|
+
def application_command_help
|
36
|
+
["Commands for your application:\n",
|
37
|
+
*registered_commands.map { |c,n| display_value(c,n) }]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def client_commands
|
43
|
+
spring_commands.invert
|
44
|
+
end
|
45
|
+
|
46
|
+
def registered_commands
|
47
|
+
Hash[unique_commands.collect { |c| [c, command_aliases(c)] }]
|
48
|
+
end
|
49
|
+
|
50
|
+
def all_commands
|
51
|
+
@all_commands ||= client_commands.merge(registered_commands)
|
52
|
+
end
|
53
|
+
|
54
|
+
def unique_commands
|
55
|
+
application_commands.collect { |k,v| v }.uniq
|
56
|
+
end
|
57
|
+
|
58
|
+
def command_aliases(command)
|
59
|
+
spring_commands.merge(application_commands).select { |k,v| v == command }.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
def description_for_command(command)
|
63
|
+
if command.respond_to?(:description)
|
64
|
+
command.description
|
65
|
+
else
|
66
|
+
"No description given."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def display_value(command, names)
|
71
|
+
" #{ Array(names).join(', ').ljust(max_name_width) } #{ description_for_command(command) }"
|
72
|
+
end
|
73
|
+
|
74
|
+
def max_name_width
|
75
|
+
@max_name_width ||= all_commands.collect { |_,n| Array(n).join(', ').length }.max
|
20
76
|
end
|
21
77
|
end
|
22
78
|
end
|
data/lib/spring/client/run.rb
CHANGED
@@ -39,18 +39,19 @@ module Spring
|
|
39
39
|
application.write arg
|
40
40
|
end
|
41
41
|
|
42
|
-
pid = server.gets
|
42
|
+
pid = server.gets
|
43
|
+
pid = pid.chomp if pid
|
43
44
|
|
44
45
|
# We must not close the client socket until we are sure that the application has
|
45
46
|
# received the FD. Otherwise the FD can end up getting closed while it's in the server
|
46
47
|
# socket buffer on OS X. This doesn't happen on Linux.
|
47
48
|
client.close
|
48
49
|
|
49
|
-
if pid.empty?
|
50
|
-
exit 1
|
51
|
-
else
|
50
|
+
if pid && !pid.empty?
|
52
51
|
forward_signals(pid.to_i)
|
53
|
-
application.read
|
52
|
+
exit application.read.to_i
|
53
|
+
else
|
54
|
+
exit 1
|
54
55
|
end
|
55
56
|
rescue Errno::ECONNRESET
|
56
57
|
exit 1
|
@@ -59,12 +60,9 @@ module Spring
|
|
59
60
|
server.close if server
|
60
61
|
end
|
61
62
|
|
62
|
-
# Boot the server into the process group of the current session.
|
63
|
-
# This will cause it to be automatically killed once the session
|
64
|
-
# ends (i.e. when the user closes their terminal).
|
65
63
|
def boot_server
|
66
64
|
env.socket_path.unlink if env.socket_path.exist?
|
67
|
-
Process.spawn(*SERVER_COMMAND
|
65
|
+
Process.spawn(*SERVER_COMMAND)
|
68
66
|
sleep 0.1 until env.socket_path.exist?
|
69
67
|
end
|
70
68
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Spring
|
2
|
+
module Client
|
3
|
+
class Status < Command
|
4
|
+
def self.description
|
5
|
+
"Show current status."
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
if env.server_running?
|
10
|
+
puts "Spring is running:"
|
11
|
+
puts
|
12
|
+
print_process env.pid
|
13
|
+
application_pids.each { |pid| print_process pid }
|
14
|
+
else
|
15
|
+
puts "Spring is not running."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def print_process(pid)
|
20
|
+
puts `ps -p #{pid} -o pid= -o args=`
|
21
|
+
end
|
22
|
+
|
23
|
+
def application_pids
|
24
|
+
candidates = `ps -o ppid= -o pid=`.lines
|
25
|
+
candidates.select { |l| l =~ /^#{env.pid} / }
|
26
|
+
.map { |l| l.split(" ").last }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/spring/client/stop.rb
CHANGED
@@ -3,8 +3,34 @@ require "spring/version"
|
|
3
3
|
module Spring
|
4
4
|
module Client
|
5
5
|
class Stop < Command
|
6
|
+
TIMEOUT = 2 # seconds
|
7
|
+
|
8
|
+
def self.description
|
9
|
+
"Stop all spring processes for this project."
|
10
|
+
end
|
11
|
+
|
6
12
|
def call
|
7
|
-
|
13
|
+
if env.server_running?
|
14
|
+
timeout = Time.now + TIMEOUT
|
15
|
+
kill 'TERM'
|
16
|
+
sleep 0.1 until !env.server_running? || Time.now >= timeout
|
17
|
+
|
18
|
+
if env.server_running?
|
19
|
+
STDERR.puts "Spring did not stop; killing forcibly."
|
20
|
+
kill 'KILL'
|
21
|
+
else
|
22
|
+
puts "Spring stopped."
|
23
|
+
end
|
24
|
+
else
|
25
|
+
puts "Spring is not running"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def kill(sig)
|
30
|
+
pid = env.pid
|
31
|
+
Process.kill(sig, pid) if pid
|
32
|
+
rescue Errno::ESRCH
|
33
|
+
# already dead
|
8
34
|
end
|
9
35
|
end
|
10
36
|
end
|