spring 1.0.0 → 1.1.0.beta3

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,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,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