spring 1.0.0 → 1.1.0.beta2

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,15 @@
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
14
9
  end
15
10
 
16
11
  def log(message)
17
- server.log(message)
12
+ spring_env.log "[application_manager:#{app_env}] #{message}"
18
13
  end
19
14
 
20
15
  # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
@@ -28,7 +23,6 @@ module Spring
28
23
 
29
24
  def start
30
25
  start_child
31
- start_wait_thread
32
26
  end
33
27
 
34
28
  def restart
@@ -63,13 +57,15 @@ module Spring
63
57
  def run(client)
64
58
  with_child do
65
59
  child.send_io client
66
- child.gets
60
+ child.gets or raise Errno::EPIPE
67
61
  end
68
62
 
69
- pid = child.gets
70
- pid = pid.chomp.to_i if pid
71
- log "got worker pid #{pid}"
72
- pid
63
+ pid = child.gets.to_i
64
+
65
+ unless pid.zero?
66
+ log "got worker pid #{pid}"
67
+ pid
68
+ end
73
69
  rescue Errno::ECONNRESET, Errno::EPIPE => e
74
70
  log "#{e} while reading from child; returning no pid"
75
71
  nil
@@ -84,39 +80,47 @@ module Spring
84
80
  private
85
81
 
86
82
  def start_child(preload = false)
87
- server.application_starting
88
-
89
83
  @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
84
 
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
- }
85
+ Bundler.with_clean_env do
86
+ @pid = Process.spawn(
87
+ {
88
+ "RAILS_ENV" => app_env,
89
+ "RACK_ENV" => app_env,
90
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
91
+ "SPRING_PRELOAD" => preload ? "1" : "0"
92
+ },
93
+ "ruby",
94
+ "-I", File.expand_path("../..", __FILE__),
95
+ "-e", "require 'spring/application/boot'",
96
+ 3 => child_socket
97
+ )
98
+ end
100
99
 
101
- app = Application.new(child_socket)
102
- app.preload if preload
103
- app.run
104
- }
100
+ start_wait_thread(pid, child) if child.gets
105
101
  child_socket.close
106
102
  end
107
103
 
108
- def start_wait_thread
109
- @wait_thread = Thread.new {
110
- Thread.current.abort_on_exception = true
111
-
112
- while alive?
113
- _, status = Process.wait2(pid)
114
- @pid = nil
104
+ def start_wait_thread(pid, child)
105
+ Process.detach(pid)
115
106
 
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? }
107
+ Thread.new {
108
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
109
+ # as if it does we're no longer interested in the child
110
+ loop do
111
+ IO.select([child])
112
+ break if child.recv(1, Socket::MSG_PEEK).empty?
113
+ sleep 0.01
119
114
  end
115
+
116
+ log "child #{pid} shutdown"
117
+
118
+ synchronize {
119
+ if @pid == pid
120
+ @pid = nil
121
+ restart
122
+ end
123
+ }
120
124
  }
121
125
  end
122
126
  end
@@ -0,0 +1,12 @@
1
+ command = File.basename($0)
2
+
3
+ if command == "spring"
4
+ load Gem.bin_path("spring", "spring")
5
+ else
6
+ disable = ENV["DISABLE_SPRING"]
7
+
8
+ if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
9
+ ARGV.unshift(command)
10
+ load Gem.bin_path("spring", "spring")
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require "socket"
2
+ require "thread"
3
+
4
+ # readline must be required before we setpgid, otherwise the require may hang,
5
+ # if readline has been built against libedit. See issue #70.
6
+ require "readline"
7
+
8
+ require "spring/configuration"
9
+ require "spring/env"
10
+ require "spring/process_title_updater"
11
+ require "spring/json"
12
+ require "spring/watcher"
@@ -1,46 +1,165 @@
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
+ match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m)
40
+ version = match && match[1]
41
+
42
+ ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR)
43
+ ENV["GEM_HOME"] = ""
44
+ Gem.paths = ENV
45
+
46
+ gem "spring", version
47
+ require "spring/binstub"
48
+ end
49
+ CODE
50
+
51
+ OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
52
+
53
+ class Item
54
+ attr_reader :command, :existing
55
+
56
+ def initialize(command)
57
+ @command = command
58
+
59
+ if command.binstub.exist?
60
+ @existing = command.binstub.read
61
+ elsif command.name == "rails"
62
+ scriptfile = Spring.application_root_path.join("script/rails")
63
+ @existing = scriptfile.read if scriptfile.exist?
64
+ end
65
+ end
66
+
67
+ def status(text, stream = $stdout)
68
+ stream.puts "* #{command.binstub_name}: #{text}"
69
+ end
70
+
71
+ def add
72
+ if existing
73
+ if existing.include?(OLD_BINSTUB)
74
+ fallback = existing.match(/#{Regexp.escape OLD_BINSTUB}\n(.*)else/m)[1]
75
+ fallback.gsub!(/^ /, "")
76
+ fallback = nil if fallback.include?("exec")
77
+ generate(fallback)
78
+ status "upgraded"
79
+ elsif existing =~ /load .*spring/
80
+ status "spring already present"
81
+ else
82
+ head, shebang, tail = existing.partition(SHEBANG)
83
+
84
+ if shebang.include?("ruby")
85
+ unless command.binstub.exist?
86
+ FileUtils.touch command.binstub
87
+ command.binstub.chmod 0755
88
+ end
89
+
90
+ File.write(command.binstub, "#{head}#{shebang}#{LOADER}#{tail}")
91
+ status "spring inserted"
92
+ else
93
+ status "doesn't appear to be ruby, so cannot use spring", $stderr
94
+ exit 1
95
+ end
96
+ end
97
+ else
98
+ generate
99
+ status "generated with spring"
100
+ end
101
+ end
102
+
103
+ def generate(fallback = nil)
104
+ unless fallback
105
+ fallback = "require 'bundler/setup'\n" \
106
+ "load Gem.bin_path('#{command.gem_name}', '#{command.exec_name}')\n"
107
+ end
108
+
109
+ File.write(command.binstub, "#!/usr/bin/env ruby\n#{LOADER}#{fallback}")
110
+ command.binstub.chmod 0755
111
+ end
112
+
113
+ def remove
114
+ if existing
115
+ File.write(command.binstub, existing.sub(LOADER, ""))
116
+ status "spring removed"
117
+ end
118
+ end
119
+ end
120
+
121
+ attr_reader :bindir, :items
5
122
 
6
123
  def self.description
7
124
  "Generate spring based binstubs. Use --all to generate a binstub for all known commands."
8
125
  end
9
126
 
127
+ def self.rails_command
128
+ @rails_command ||= CommandWrapper.new("rails")
129
+ end
130
+
10
131
  def self.call(args)
11
132
  require "spring/commands"
12
133
  super
13
134
  end
14
135
 
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
136
  def initialize(args)
26
137
  super
27
138
 
28
- @bindir = env.root.join("bin")
29
- @commands = args.drop(1).inject({}) { |mem, name| mem.merge(find_commands(name)) }
139
+ @bindir = env.root.join("bin")
140
+ @all = false
141
+ @mode = :add
142
+ @items = args.drop(1)
143
+ .map { |name| find_commands name }
144
+ .inject(Set.new, :|)
145
+ .map { |command| Item.new(command) }
30
146
  end
31
147
 
32
148
  def find_commands(name)
33
149
  case name
34
150
  when "--all"
35
- commands = Spring.commands
151
+ @all = true
152
+ commands = Spring.commands.dup
36
153
  commands.delete_if { |name, _| name.start_with?("rails_") }
37
- commands["rails"] = RailsCommand.new
38
- commands
154
+ commands.values + [self.class.rails_command]
155
+ when "--remove"
156
+ @mode = :remove
157
+ []
39
158
  when "rails"
40
- { name => RailsCommand.new }
159
+ [self.class.rails_command]
41
160
  else
42
161
  if command = Spring.commands[name]
43
- { name => command }
162
+ [command]
44
163
  else
45
164
  $stderr.puts "The '#{name}' command is not known to spring."
46
165
  exit 1
@@ -49,32 +168,25 @@ CODE
49
168
  end
50
169
 
51
170
  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
171
+ case @mode
172
+ when :add
173
+ bindir.mkdir unless bindir.exist?
59
174
 
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
175
+ File.write(spring_binstub, SPRING)
176
+ spring_binstub.chmod 0755
70
177
 
71
- def fallback(name, command)
72
- if command.respond_to?(:fallback)
73
- command.fallback
178
+ items.each(&:add)
179
+ when :remove
180
+ spring_binstub.delete if @all
181
+ items.each(&:remove)
74
182
  else
75
- %{exec "bundle", "exec", "#{name}", *ARGV}
183
+ raise ArgumentError
76
184
  end
77
185
  end
186
+
187
+ def spring_binstub
188
+ bindir.join("spring")
189
+ end
78
190
  end
79
191
  end
80
192
  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,83 @@
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
+ $0 = exec
41
+ load exec
42
+ end
43
+ end
44
+
45
+ def gem_name
46
+ if command.respond_to?(:gem_name)
47
+ command.gem_name
48
+ else
49
+ exec_name
50
+ end
51
+ end
52
+
53
+ def exec_name
54
+ if command.respond_to?(:exec_name)
55
+ command.exec_name
56
+ else
57
+ name
58
+ end
59
+ end
60
+
61
+ def binstub
62
+ Spring.application_root_path.join(binstub_name)
63
+ end
64
+
65
+ def binstub_name
66
+ "bin/#{name}"
67
+ end
68
+
69
+ def exec
70
+ if binstub.exist?
71
+ binstub.to_s
72
+ else
73
+ Gem.bin_path(gem_name, exec_name)
74
+ end
75
+ end
76
+
77
+ def env(args)
78
+ if command.respond_to?(:env)
79
+ command.env(args)
80
+ end
81
+ end
82
+ end
83
+ 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