spring 1.0.0 → 1.3.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -1
  4. data/CHANGELOG.md +81 -0
  5. data/CONTRIBUTING.md +52 -0
  6. data/Gemfile +0 -2
  7. data/README.md +137 -54
  8. data/Rakefile +1 -1
  9. data/bin/spring +17 -0
  10. data/lib/spring/application/boot.rb +18 -0
  11. data/lib/spring/application.rb +183 -76
  12. data/lib/spring/application_manager.rb +54 -40
  13. data/lib/spring/binstub.rb +13 -0
  14. data/lib/spring/boot.rb +8 -0
  15. data/lib/spring/client/binstub.rb +150 -39
  16. data/lib/spring/client/help.rb +4 -12
  17. data/lib/spring/client/rails.rb +2 -3
  18. data/lib/spring/client/run.rb +69 -11
  19. data/lib/spring/client/status.rb +2 -2
  20. data/lib/spring/client/stop.rb +6 -21
  21. data/lib/spring/client/version.rb +11 -0
  22. data/lib/spring/client.rb +8 -5
  23. data/lib/spring/command_wrapper.rb +82 -0
  24. data/lib/spring/commands/rails.rb +44 -13
  25. data/lib/spring/commands/rake.rb +0 -4
  26. data/lib/spring/commands.rb +14 -4
  27. data/lib/spring/configuration.rb +11 -2
  28. data/lib/spring/env.rb +36 -10
  29. data/lib/spring/server.rb +9 -35
  30. data/lib/spring/sid.rb +1 -1
  31. data/lib/spring/test/acceptance_test.rb +346 -0
  32. data/lib/spring/test/application.rb +217 -0
  33. data/lib/spring/test/application_generator.rb +121 -0
  34. data/lib/spring/test/rails_version.rb +40 -0
  35. data/lib/spring/test/watcher_test.rb +167 -0
  36. data/lib/spring/test.rb +18 -0
  37. data/lib/spring/version.rb +1 -1
  38. data/lib/spring/watcher/abstract.rb +7 -8
  39. data/lib/spring/watcher/polling.rb +2 -0
  40. data/lib/spring/watcher.rb +4 -8
  41. data/spring.gemspec +2 -2
  42. data/test/acceptance_test.rb +4 -0
  43. data/test/helper.rb +2 -2
  44. data/test/unit/client/version_test.rb +14 -0
  45. data/test/unit/commands_test.rb +23 -1
  46. data/test/unit/watcher_test.rb +2 -184
  47. metadata +30 -17
  48. data/lib/spring/watcher/listen.rb +0 -52
  49. data/test/acceptance/app_test.rb +0 -511
@@ -1,46 +1,164 @@
1
+ require 'set'
2
+
1
3
  module Spring
2
4
  module Client
3
5
  class Binstub < Command
4
- attr_reader :bindir, :commands
6
+ SHEBANG = /\#\!.*\n/
7
+
8
+ # If loading the bin/spring file works, it'll run spring which will
9
+ # eventually call Kernel.exit. This means that in the client process
10
+ # we will never execute the lines after this block. But if the spring
11
+ # client is not invoked for whatever reason, then the Kernel.exit won't
12
+ # happen, and so we'll fall back to the lines after this block, which
13
+ # should cause the "unsprung" version of the command to run.
14
+ LOADER = <<CODE
15
+ begin
16
+ load File.expand_path("../spring", __FILE__)
17
+ rescue LoadError
18
+ end
19
+ CODE
20
+
21
+ # The defined? check ensures these lines don't execute when we load the
22
+ # binstub from the application process. Which means that in the application
23
+ # process we'll execute the lines which come after the LOADER block, which
24
+ # is what we want.
25
+ #
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'
30
+ #!/usr/bin/env ruby
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
+
35
+ unless defined?(Spring)
36
+ require "rubygems"
37
+ require "bundler"
38
+
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"] = nil
42
+ Gem.paths = ENV
43
+
44
+ gem "spring", match[1]
45
+ require "spring/binstub"
46
+ end
47
+ end
48
+ CODE
49
+
50
+ OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
51
+
52
+ class Item
53
+ attr_reader :command, :existing
54
+
55
+ def initialize(command)
56
+ @command = command
57
+
58
+ if command.binstub.exist?
59
+ @existing = command.binstub.read
60
+ elsif command.name == "rails"
61
+ scriptfile = Spring.application_root_path.join("script/rails")
62
+ @existing = scriptfile.read if scriptfile.exist?
63
+ end
64
+ end
65
+
66
+ def status(text, stream = $stdout)
67
+ stream.puts "* #{command.binstub_name}: #{text}"
68
+ end
69
+
70
+ def add
71
+ if existing
72
+ if existing.include?(OLD_BINSTUB)
73
+ fallback = existing.match(/#{Regexp.escape OLD_BINSTUB}\n(.*)else/m)[1]
74
+ fallback.gsub!(/^ /, "")
75
+ fallback = nil if fallback.include?("exec")
76
+ generate(fallback)
77
+ status "upgraded"
78
+ elsif existing =~ /load .*spring/
79
+ status "spring already present"
80
+ else
81
+ head, shebang, tail = existing.partition(SHEBANG)
82
+
83
+ if shebang.include?("ruby")
84
+ unless command.binstub.exist?
85
+ FileUtils.touch command.binstub
86
+ command.binstub.chmod 0755
87
+ end
88
+
89
+ File.write(command.binstub, "#{head}#{shebang}#{LOADER}#{tail}")
90
+ status "spring inserted"
91
+ else
92
+ status "doesn't appear to be ruby, so cannot use spring", $stderr
93
+ exit 1
94
+ end
95
+ end
96
+ else
97
+ generate
98
+ status "generated with spring"
99
+ end
100
+ end
101
+
102
+ def generate(fallback = nil)
103
+ unless fallback
104
+ fallback = "require 'bundler/setup'\n" \
105
+ "load Gem.bin_path('#{command.gem_name}', '#{command.exec_name}')\n"
106
+ end
107
+
108
+ File.write(command.binstub, "#!/usr/bin/env ruby\n#{LOADER}#{fallback}")
109
+ command.binstub.chmod 0755
110
+ end
111
+
112
+ def remove
113
+ if existing
114
+ File.write(command.binstub, existing.sub(LOADER, ""))
115
+ status "spring removed"
116
+ end
117
+ end
118
+ end
119
+
120
+ attr_reader :bindir, :items
5
121
 
6
122
  def self.description
7
123
  "Generate spring based binstubs. Use --all to generate a binstub for all known commands."
8
124
  end
9
125
 
126
+ def self.rails_command
127
+ @rails_command ||= CommandWrapper.new("rails")
128
+ end
129
+
10
130
  def self.call(args)
11
131
  require "spring/commands"
12
132
  super
13
133
  end
14
134
 
15
- class RailsCommand
16
- def fallback
17
- <<CODE
18
- APP_PATH = File.expand_path('../../config/application', __FILE__)
19
- require_relative '../config/boot'
20
- require 'rails/commands'
21
- CODE
22
- end
23
- end
24
-
25
135
  def initialize(args)
26
136
  super
27
137
 
28
- @bindir = env.root.join("bin")
29
- @commands = args.drop(1).inject({}) { |mem, name| mem.merge(find_commands(name)) }
138
+ @bindir = env.root.join("bin")
139
+ @all = false
140
+ @mode = :add
141
+ @items = args.drop(1)
142
+ .map { |name| find_commands name }
143
+ .inject(Set.new, :|)
144
+ .map { |command| Item.new(command) }
30
145
  end
31
146
 
32
147
  def find_commands(name)
33
148
  case name
34
149
  when "--all"
35
- commands = Spring.commands
150
+ @all = true
151
+ commands = Spring.commands.dup
36
152
  commands.delete_if { |name, _| name.start_with?("rails_") }
37
- commands["rails"] = RailsCommand.new
38
- commands
153
+ commands.values + [self.class.rails_command]
154
+ when "--remove"
155
+ @mode = :remove
156
+ []
39
157
  when "rails"
40
- { name => RailsCommand.new }
158
+ [self.class.rails_command]
41
159
  else
42
160
  if command = Spring.commands[name]
43
- { name => command }
161
+ [command]
44
162
  else
45
163
  $stderr.puts "The '#{name}' command is not known to spring."
46
164
  exit 1
@@ -49,32 +167,25 @@ CODE
49
167
  end
50
168
 
51
169
  def call
52
- bindir.mkdir unless bindir.exist?
53
- commands.each { |name, command| generate_binstub(name, command) }
54
- end
55
-
56
- def generate_binstub(name, command)
57
- File.write(bindir.join(name), <<CODE)
58
- #!/usr/bin/env ruby
170
+ case @mode
171
+ when :add
172
+ bindir.mkdir unless bindir.exist?
59
173
 
60
- if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?
61
- #{fallback(name, command).strip.gsub(/^/, " ")}
62
- else
63
- ARGV.unshift "#{name}"
64
- load Gem.bin_path("spring", "spring")
65
- end
66
- CODE
67
-
68
- bindir.join(name).chmod 0755
69
- end
174
+ File.write(spring_binstub, SPRING)
175
+ spring_binstub.chmod 0755
70
176
 
71
- def fallback(name, command)
72
- if command.respond_to?(:fallback)
73
- command.fallback
177
+ items.each(&:add)
178
+ when :remove
179
+ spring_binstub.delete if @all
180
+ items.each(&:remove)
74
181
  else
75
- %{exec "bundle", "exec", "#{name}", *ARGV}
182
+ raise ArgumentError
76
183
  end
77
184
  end
185
+
186
+ def spring_binstub
187
+ bindir.join("spring")
188
+ end
78
189
  end
79
190
  end
80
191
  end
@@ -20,6 +20,8 @@ module Spring
20
20
  @spring_commands = spring_commands || Spring::Client::COMMANDS.dup
21
21
  @application_commands = application_commands || Spring.commands.dup
22
22
 
23
+ @spring_commands.delete_if { |k, v| k.start_with?("-") }
24
+
23
25
  @application_commands["rails"] = @spring_commands.delete("rails")
24
26
  end
25
27
 
@@ -46,19 +48,9 @@ module Spring
46
48
  spring_commands.merge application_commands
47
49
  end
48
50
 
49
- def description_for_command(command)
50
- if command.respond_to?(:description)
51
- command.description
52
- elsif command.respond_to?(:exec_name)
53
- "Runs the #{command.exec_name} command"
54
- else
55
- "No description given."
56
- end
57
- end
58
-
59
51
  def display(name, command)
60
- if description = description_for_command(command)
61
- " #{name.ljust(max_name_width)} #{description}"
52
+ if command.description
53
+ " #{name.ljust(max_name_width)} #{command.description}"
62
54
  end
63
55
  end
64
56
 
@@ -24,9 +24,8 @@ module Spring
24
24
  else
25
25
  require "spring/configuration"
26
26
  ARGV.shift
27
- Object.const_set(:APP_PATH, Spring.application_root_path.join("config/application").to_s)
28
- require Spring.application_root_path.join("config/boot")
29
- require "rails/commands"
27
+ load Dir.glob(Spring.application_root_path.join("{bin,script}/rails")).first
28
+ exit
30
29
  end
31
30
  end
32
31
  end
@@ -5,6 +5,12 @@ module Spring
5
5
  module Client
6
6
  class Run < Command
7
7
  FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
8
+ TIMEOUT = 1
9
+
10
+ def initialize(args)
11
+ super
12
+ @signal_queue = []
13
+ end
8
14
 
9
15
  def log(message)
10
16
  env.log "[client] #{message}"
@@ -15,26 +21,54 @@ module Spring
15
21
  end
16
22
 
17
23
  def call
18
- boot_server unless env.server_running?
24
+ if env.server_running?
25
+ warm_run
26
+ else
27
+ cold_run
28
+ end
29
+ rescue Errno::ECONNRESET
30
+ exit 1
31
+ ensure
32
+ server.close if @server
33
+ end
34
+
35
+ def warm_run
36
+ run
37
+ rescue CommandNotFound
38
+ require "spring/commands"
39
+
40
+ if Spring.command?(args.first)
41
+ # Command installed since spring started
42
+ stop_server
43
+ cold_run
44
+ else
45
+ raise
46
+ end
47
+ end
48
+
49
+ def cold_run
50
+ boot_server
51
+ run
52
+ end
53
+
54
+ def run
19
55
  verify_server_version
20
56
 
21
57
  application, client = UNIXSocket.pair
22
58
 
59
+ queue_signals
23
60
  connect_to_application(client)
24
61
  run_command(client, application)
25
- rescue Errno::ECONNRESET
26
- exit 1
27
- ensure
28
- server.close if @server
29
62
  end
30
63
 
31
64
  def boot_server
32
65
  env.socket_path.unlink if env.socket_path.exist?
33
66
 
34
- pid = fork {
35
- require "spring/server"
36
- Spring::Server.boot
37
- }
67
+ pid = Process.spawn(
68
+ "ruby",
69
+ "-r", "spring/server",
70
+ "-e", "Spring::Server.boot"
71
+ )
38
72
 
39
73
  until env.socket_path.exist?
40
74
  _, status = Process.waitpid2(pid, Process::WNOHANG)
@@ -43,6 +77,12 @@ module Spring
43
77
  end
44
78
  end
45
79
 
80
+ def stop_server
81
+ server.close
82
+ @server = nil
83
+ env.stop
84
+ end
85
+
46
86
  def verify_server_version
47
87
  server_version = server.gets.chomp
48
88
  if server_version != env.version
@@ -59,7 +99,12 @@ ERROR
59
99
  def connect_to_application(client)
60
100
  server.send_io client
61
101
  send_json server, "args" => args, "default_rails_env" => default_rails_env
62
- server.gets or raise CommandNotFound
102
+
103
+ if IO.select([server], [], [], TIMEOUT)
104
+ server.gets or raise CommandNotFound
105
+ else
106
+ raise "Error connecting to Spring server"
107
+ end
63
108
  end
64
109
 
65
110
  def run_command(client, application)
@@ -96,14 +141,23 @@ ERROR
96
141
  application.close
97
142
  end
98
143
 
144
+ def queue_signals
145
+ FORWARDED_SIGNALS.each do |sig|
146
+ trap(sig) { @signal_queue << sig }
147
+ end
148
+ end
149
+
99
150
  def forward_signals(pid)
151
+ @signal_queue.each { |sig| kill sig, pid }
152
+
100
153
  FORWARDED_SIGNALS.each do |sig|
101
154
  trap(sig) { forward_signal sig, pid }
102
155
  end
156
+ rescue Errno::ESRCH
103
157
  end
104
158
 
105
159
  def forward_signal(sig, pid)
106
- Process.kill(sig, -Process.getpgid(pid))
160
+ kill(sig, pid)
107
161
  rescue Errno::ESRCH
108
162
  # If the application process is gone, then don't block the
109
163
  # signal on this process.
@@ -111,6 +165,10 @@ ERROR
111
165
  Process.kill(sig, Process.pid)
112
166
  end
113
167
 
168
+ def kill(sig, pid)
169
+ Process.kill(sig, -Process.getpgid(pid))
170
+ end
171
+
114
172
  def send_json(socket, data)
115
173
  data = JSON.dump(data)
116
174
 
@@ -17,11 +17,11 @@ module Spring
17
17
  end
18
18
 
19
19
  def print_process(pid)
20
- puts `ps -p #{pid} -o pid= -o args=`
20
+ puts `ps -p #{pid} -o pid= -o command=`
21
21
  end
22
22
 
23
23
  def application_pids
24
- candidates = `ps -A -o ppid= -o pid=`.lines
24
+ candidates = `ps -ax -o ppid= -o pid=`.lines
25
25
  candidates.select { |l| l =~ /^(\s+)?#{env.pid} / }
26
26
  .map { |l| l.split(" ").last }
27
27
  end
@@ -3,35 +3,20 @@ require "spring/version"
3
3
  module Spring
4
4
  module Client
5
5
  class Stop < Command
6
- TIMEOUT = 2 # seconds
7
-
8
6
  def self.description
9
7
  "Stop all spring processes for this project."
10
8
  end
11
9
 
12
10
  def call
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
11
+ case env.stop
12
+ when :stopped
13
+ puts "Spring stopped."
14
+ when :killed
15
+ $stderr.puts "Spring did not stop; killing forcibly."
16
+ when :not_running
25
17
  puts "Spring is not running"
26
18
  end
27
19
  end
28
-
29
- def kill(sig)
30
- pid = env.pid
31
- Process.kill(sig, pid) if pid
32
- rescue Errno::ESRCH
33
- # already dead
34
- end
35
20
  end
36
21
  end
37
22
  end
@@ -0,0 +1,11 @@
1
+ require "spring/version"
2
+
3
+ module Spring
4
+ module Client
5
+ class Version < Command
6
+ def call
7
+ puts "Spring version #{Spring::VERSION}"
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/spring/client.rb CHANGED
@@ -8,15 +8,18 @@ require "spring/client/binstub"
8
8
  require "spring/client/stop"
9
9
  require "spring/client/status"
10
10
  require "spring/client/rails"
11
+ require "spring/client/version"
11
12
 
12
13
  module Spring
13
14
  module Client
14
15
  COMMANDS = {
15
- "help" => Client::Help,
16
- "binstub" => Client::Binstub,
17
- "stop" => Client::Stop,
18
- "status" => Client::Status,
19
- "rails" => Client::Rails
16
+ "help" => Client::Help,
17
+ "binstub" => Client::Binstub,
18
+ "stop" => Client::Stop,
19
+ "status" => Client::Status,
20
+ "rails" => Client::Rails,
21
+ "-v" => Client::Version,
22
+ "--version" => Client::Version
20
23
  }
21
24
 
22
25
  def self.run(args)
@@ -0,0 +1,82 @@
1
+ require "spring/configuration"
2
+
3
+ module Spring
4
+ class CommandWrapper
5
+ attr_reader :name, :command
6
+
7
+ def initialize(name, command = nil)
8
+ @name = name
9
+ @command = command
10
+ @setup = false
11
+ end
12
+
13
+ def description
14
+ if command.respond_to?(:description)
15
+ command.description
16
+ else
17
+ "Runs the #{name} command"
18
+ end
19
+ end
20
+
21
+ def setup?
22
+ @setup
23
+ end
24
+
25
+ def setup
26
+ if !setup? && command.respond_to?(:setup)
27
+ command.setup
28
+ @setup = true
29
+ return true
30
+ else
31
+ @setup = true
32
+ return false
33
+ end
34
+ end
35
+
36
+ def call
37
+ if command.respond_to?(:call)
38
+ command.call
39
+ else
40
+ load exec
41
+ end
42
+ end
43
+
44
+ def gem_name
45
+ if command.respond_to?(:gem_name)
46
+ command.gem_name
47
+ else
48
+ exec_name
49
+ end
50
+ end
51
+
52
+ def exec_name
53
+ if command.respond_to?(:exec_name)
54
+ command.exec_name
55
+ else
56
+ name
57
+ end
58
+ end
59
+
60
+ def binstub
61
+ Spring.application_root_path.join(binstub_name)
62
+ end
63
+
64
+ def binstub_name
65
+ "bin/#{name}"
66
+ end
67
+
68
+ def exec
69
+ if binstub.exist?
70
+ binstub.to_s
71
+ else
72
+ Gem.bin_path(gem_name, exec_name)
73
+ end
74
+ end
75
+
76
+ def env(args)
77
+ if command.respond_to?(:env)
78
+ command.env(args)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -3,8 +3,7 @@ module Spring
3
3
  class Rails
4
4
  def call
5
5
  ARGV.unshift command_name
6
- Object.const_set(:APP_PATH, ::Rails.root.join('config/application'))
7
- require "rails/commands"
6
+ load Dir.glob(::Rails.root.join("{bin,script}/rails")).first
8
7
  end
9
8
 
10
9
  def description
@@ -14,7 +13,19 @@ module Spring
14
13
 
15
14
  class RailsConsole < Rails
16
15
  def env(args)
17
- args.first if args.first && !args.first.index("-")
16
+ return args.first if args.first && !args.first.index("-")
17
+
18
+ environment = nil
19
+
20
+ args.each.with_index do |arg, i|
21
+ if arg =~ /--environment=(\w+)/
22
+ environment = $1
23
+ elsif i > 0 && args[i - 1] == "-e"
24
+ environment = arg
25
+ end
26
+ end
27
+
28
+ environment
18
29
  end
19
30
 
20
31
  def command_name
@@ -35,21 +46,41 @@ module Spring
35
46
  end
36
47
 
37
48
  class RailsRunner < Rails
38
- def env(tail)
39
- previous_option = nil
40
- tail.reverse.each do |option|
41
- case option
42
- when /--environment=(\w+)/ then return $1
43
- when '-e' then return previous_option
44
- end
45
- previous_option = option
46
- end
47
- nil
49
+ def call
50
+ ARGV.replace extract_environment(ARGV).first
51
+ super
52
+ end
53
+
54
+ def env(args)
55
+ extract_environment(args).last
48
56
  end
49
57
 
50
58
  def command_name
51
59
  "runner"
52
60
  end
61
+
62
+ def extract_environment(args)
63
+ environment = nil
64
+
65
+ args = args.select.with_index { |arg, i|
66
+ case arg
67
+ when "-e"
68
+ false
69
+ when /--environment=(\w+)/
70
+ environment = $1
71
+ false
72
+ else
73
+ if i > 0 && args[i - 1] == "-e"
74
+ environment = arg
75
+ false
76
+ else
77
+ true
78
+ end
79
+ end
80
+ }
81
+
82
+ [args, environment]
83
+ end
53
84
  end
54
85
 
55
86
  Spring.register_command "rails_console", RailsConsole.new
@@ -23,10 +23,6 @@ module Spring
23
23
 
24
24
  nil
25
25
  end
26
-
27
- def exec_name
28
- "rake"
29
- end
30
26
  end
31
27
 
32
28
  Spring.register_command "rake", Rake.new