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 CHANGED
@@ -1,3 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ - 2.0.0
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
- At the moment only MRI 1.9.3 / Rails 3.2 is supported.
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
- $ ps ax | grep spring
80
- 26150 pts/3 Sl 0:00 spring server | rails-3-2 | started 2013-02-01 20:16:40 +0000
81
- 26155 pts/3 Sl 0:02 spring app | rails-3-2 | started 2013-02-01 20:16:40 +0000 | test mode
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
- $ ps ax | grep spring
151
- 26150 pts/3 Sl 0:00 spring server | rails-3-2 | started 2013-02-01 20:16:40 +0000
152
- 26556 pts/3 Sl 0:00 spring app | rails-3-2 | started 2013-02-01 20:20:07 +0000 | test mode
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
- $ ps ax | grep spring
182
- 26150 pts/3 Sl 0:00 spring server | rails-3-2 | started 2013-02-01 20:16:40 +0000
183
- 26556 pts/3 Sl 0:08 spring app | rails-3-2 | started 2013-02-01 20:20:07 +0000 | test mode
184
- 26707 pts/3 Sl 0:00 spring app | rails-3-2 | started 2013-02-01 20:22:41 +0000 | development mode
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
@@ -4,7 +4,7 @@ require "rake/testtask"
4
4
  namespace :test do
5
5
  Rake::TestTask.new(:unit) do |t|
6
6
  t.libs << "test"
7
- t.test_files = FileList["test/unit/*_test.rb"]
7
+ t.test_files = FileList["test/unit/**/*_test.rb"]
8
8
  t.verbose = true
9
9
  end
10
10
 
@@ -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
- redirect_output(client) do
61
- stdin = client.recv_io
62
- args_length = client.gets.to_i
63
- args = args_length.times.map { client.read(client.gets.to_i) }
64
- command = Spring.command(args.shift)
65
-
66
- setup command
67
-
68
- ActionDispatch::Reloader.cleanup!
69
- ActionDispatch::Reloader.prepare!
70
-
71
- pid = fork {
72
- Process.setsid
73
- STDIN.reopen(stdin)
74
- IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
75
- command.call(args)
76
- }
77
-
78
- manager.puts pid
79
- Process.wait pid
80
- end
81
- ensure
82
- client.puts
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 redirect_output(socket)
103
- STDOUT.reopen socket.recv_io
104
- STDERR.reopen socket.recv_io
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
- $0 = "spring app | #{spring_env.app_name} | started #{Time.now} | #{app_env} mode"
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)
@@ -3,6 +3,10 @@ module Spring
3
3
  class Binstub < Command
4
4
  attr_reader :bindir, :name
5
5
 
6
+ def self.description
7
+ "Generate spring based binstubs."
8
+ end
9
+
6
10
  def initialize(args)
7
11
  super
8
12
 
@@ -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 <<-EOT
8
- Usage: spring COMMAND [ARGS]
9
-
10
- The most common spring commands are:
11
- rake Run a rake task
12
- console Start the Rails console
13
- runner Execute a command with the Rails runner
14
- generate Trigger a Rails generator
15
-
16
- test Execute a Test::Unit test
17
- rspec Execute an RSpec spec
18
- cucumber Execute a Cucumber feature
19
- EOT
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
@@ -39,18 +39,19 @@ module Spring
39
39
  application.write arg
40
40
  end
41
41
 
42
- pid = server.gets.chomp
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 # FIXME: receive exit status from server
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, pgroup: SID.pgid)
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
@@ -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
- Process.kill('SIGTERM', env.pid) if env.pid
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