spring 1.0.0 → 1.1.1

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.
@@ -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
@@ -0,0 +1,13 @@
1
+ command = File.basename($0)
2
+ bin_path = File.expand_path("../../../bin/spring", __FILE__)
3
+
4
+ if command == "spring"
5
+ load bin_path
6
+ else
7
+ disable = ENV["DISABLE_SPRING"]
8
+
9
+ if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
10
+ ARGV.unshift(command)
11
+ load bin_path
12
+ end
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"
@@ -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"] = ""
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,7 @@ 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
30
28
  end
31
29
  end
32
30
  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,86 @@
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 process_title
45
+ [name, *ARGV].join(" ")
46
+ end
47
+
48
+ def gem_name
49
+ if command.respond_to?(:gem_name)
50
+ command.gem_name
51
+ else
52
+ exec_name
53
+ end
54
+ end
55
+
56
+ def exec_name
57
+ if command.respond_to?(:exec_name)
58
+ command.exec_name
59
+ else
60
+ name
61
+ end
62
+ end
63
+
64
+ def binstub
65
+ Spring.application_root_path.join(binstub_name)
66
+ end
67
+
68
+ def binstub_name
69
+ "bin/#{name}"
70
+ end
71
+
72
+ def exec
73
+ if binstub.exist?
74
+ binstub.to_s
75
+ else
76
+ Gem.bin_path(gem_name, exec_name)
77
+ end
78
+ end
79
+
80
+ def env(args)
81
+ if command.respond_to?(:env)
82
+ command.env(args)
83
+ end
84
+ end
85
+ end
86
+ 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
@@ -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
@@ -1,4 +1,5 @@
1
1
  require "spring/watcher"
2
+ require "spring/command_wrapper"
2
3
 
3
4
  module Spring
4
5
  @commands = {}
@@ -7,8 +8,8 @@ module Spring
7
8
  attr_reader :commands
8
9
  end
9
10
 
10
- def self.register_command(name, klass)
11
- commands[name] = klass
11
+ def self.register_command(name, command = nil)
12
+ commands[name] = CommandWrapper.new(name, command)
12
13
  end
13
14
 
14
15
  def self.command?(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