nrispring 2.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.
@@ -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,10 @@
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"
9
+ require "spring/failsafe_thread"
10
+
@@ -0,0 +1,48 @@
1
+ require "spring/errors"
2
+ require "spring/json"
3
+
4
+ require "spring/client/command"
5
+ require "spring/client/run"
6
+ require "spring/client/help"
7
+ require "spring/client/binstub"
8
+ require "spring/client/stop"
9
+ require "spring/client/status"
10
+ require "spring/client/rails"
11
+ require "spring/client/version"
12
+ require "spring/client/server"
13
+
14
+ module Spring
15
+ module Client
16
+ COMMANDS = {
17
+ "help" => Client::Help,
18
+ "-h" => Client::Help,
19
+ "--help" => Client::Help,
20
+ "binstub" => Client::Binstub,
21
+ "stop" => Client::Stop,
22
+ "status" => Client::Status,
23
+ "rails" => Client::Rails,
24
+ "-v" => Client::Version,
25
+ "--version" => Client::Version,
26
+ "server" => Client::Server,
27
+ }
28
+
29
+ def self.run(args)
30
+ command_for(args.first).call(args)
31
+ rescue CommandNotFound
32
+ Client::Help.call(args)
33
+ rescue ClientError => e
34
+ $stderr.puts e.message
35
+ exit 1
36
+ end
37
+
38
+ def self.command_for(name)
39
+ COMMANDS[name] || Client::Run
40
+ end
41
+ end
42
+ end
43
+
44
+ # allow users to add hooks that do not run in the server
45
+ # or modify start/stop
46
+ if File.exist?("config/spring_client.rb")
47
+ require "./config/spring_client.rb"
48
+ end
@@ -0,0 +1,197 @@
1
+ require 'set'
2
+
3
+ module Spring
4
+ module Client
5
+ class Binstub < Command
6
+ SHEBANG = /\#\!.*\n(\#.*\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 => e
18
+ raise unless e.message.include?('spring')
19
+ end
20
+ CODE
21
+
22
+ # The defined? check ensures these lines don't execute when we load the
23
+ # binstub from the application process. Which means that in the application
24
+ # process we'll execute the lines which come after the LOADER block, which
25
+ # is what we want.
26
+ SPRING = <<'CODE'
27
+ #!/usr/bin/env ruby
28
+
29
+ # This file loads Spring without using Bundler, in order to be fast.
30
+ # It gets overwritten when you run the `spring binstub` command.
31
+
32
+ unless defined?(Spring)
33
+ require 'rubygems'
34
+ require 'bundler'
35
+
36
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
37
+ spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
38
+ if spring
39
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
40
+ gem 'spring', spring.version
41
+ require 'spring/binstub'
42
+ end
43
+ end
44
+ CODE
45
+
46
+ OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
47
+
48
+ BINSTUB_VARIATIONS = Regexp.union [
49
+ %{begin\n load File.expand_path('../spring', __FILE__)\nrescue LoadError\nend\n},
50
+ %{begin\n spring_bin_path = File.expand_path('../spring', __FILE__)\n load spring_bin_path\nrescue LoadError => e\n raise unless e.message.end_with? spring_bin_path, 'spring/binstub'\nend\n},
51
+ LOADER
52
+ ].map { |binstub| /#{Regexp.escape(binstub).gsub("'", "['\"]")}/ }
53
+
54
+ class Item
55
+ attr_reader :command, :existing
56
+
57
+ def initialize(command)
58
+ @command = command
59
+
60
+ if command.binstub.exist?
61
+ @existing = command.binstub.read
62
+ elsif command.name == "rails"
63
+ scriptfile = Spring.application_root_path.join("script/rails")
64
+ @existing = scriptfile.read if scriptfile.exist?
65
+ end
66
+ end
67
+
68
+ def status(text, stream = $stdout)
69
+ stream.puts "* #{command.binstub_name}: #{text}"
70
+ end
71
+
72
+ def add
73
+ if existing
74
+ if existing.include?(OLD_BINSTUB)
75
+ fallback = existing.match(/#{Regexp.escape OLD_BINSTUB}\n(.*)else/m)[1]
76
+ fallback.gsub!(/^ /, "")
77
+ fallback = nil if fallback.include?("exec")
78
+ generate(fallback)
79
+ status "upgraded"
80
+ elsif existing.include?(LOADER)
81
+ status "Spring already present"
82
+ elsif existing =~ BINSTUB_VARIATIONS
83
+ upgraded = existing.sub(BINSTUB_VARIATIONS, LOADER)
84
+ File.write(command.binstub, upgraded)
85
+ status "upgraded"
86
+ else
87
+ head, shebang, tail = existing.partition(SHEBANG)
88
+
89
+ if shebang.include?("ruby")
90
+ unless command.binstub.exist?
91
+ FileUtils.touch command.binstub
92
+ command.binstub.chmod 0755
93
+ end
94
+
95
+ File.write(command.binstub, "#{head}#{shebang}#{LOADER}#{tail}")
96
+ status "Spring inserted"
97
+ else
98
+ status "doesn't appear to be ruby, so cannot use Spring", $stderr
99
+ exit 1
100
+ end
101
+ end
102
+ else
103
+ generate
104
+ status "generated with Spring"
105
+ end
106
+ end
107
+
108
+ def generate(fallback = nil)
109
+ unless fallback
110
+ fallback = "require 'bundler/setup'\n" \
111
+ "load Gem.bin_path('#{command.gem_name}', '#{command.exec_name}')\n"
112
+ end
113
+
114
+ File.write(command.binstub, "#!/usr/bin/env ruby\n#{LOADER}#{fallback}")
115
+ command.binstub.chmod 0755
116
+ end
117
+
118
+ def remove
119
+ if existing
120
+ File.write(command.binstub, existing.sub(BINSTUB_VARIATIONS, ""))
121
+ status "Spring removed"
122
+ end
123
+ end
124
+ end
125
+
126
+ attr_reader :bindir, :items
127
+
128
+ def self.description
129
+ "Generate Spring based binstubs. Use --all to generate a binstub for all known commands. Use --remove to revert."
130
+ end
131
+
132
+ def self.rails_command
133
+ @rails_command ||= CommandWrapper.new("rails")
134
+ end
135
+
136
+ def self.call(args)
137
+ require "spring/commands"
138
+ super
139
+ end
140
+
141
+ def initialize(args)
142
+ super
143
+
144
+ @bindir = env.root.join("bin")
145
+ @all = false
146
+ @mode = :add
147
+ @items = args.drop(1)
148
+ .map { |name| find_commands name }
149
+ .inject(Set.new, :|)
150
+ .map { |command| Item.new(command) }
151
+ end
152
+
153
+ def find_commands(name)
154
+ case name
155
+ when "--all"
156
+ @all = true
157
+ commands = Spring.commands.dup
158
+ commands.delete_if { |command_name, _| command_name.start_with?("rails_") }
159
+ commands.values + [self.class.rails_command]
160
+ when "--remove"
161
+ @mode = :remove
162
+ []
163
+ when "rails"
164
+ [self.class.rails_command]
165
+ else
166
+ if command = Spring.commands[name]
167
+ [command]
168
+ else
169
+ $stderr.puts "The '#{name}' command is not known to spring."
170
+ exit 1
171
+ end
172
+ end
173
+ end
174
+
175
+ def call
176
+ case @mode
177
+ when :add
178
+ bindir.mkdir unless bindir.exist?
179
+
180
+ File.write(spring_binstub, SPRING)
181
+ spring_binstub.chmod 0755
182
+
183
+ items.each(&:add)
184
+ when :remove
185
+ spring_binstub.delete if @all
186
+ items.each(&:remove)
187
+ else
188
+ raise ArgumentError
189
+ end
190
+ end
191
+
192
+ def spring_binstub
193
+ bindir.join("spring")
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,18 @@
1
+ require "spring/env"
2
+
3
+ module Spring
4
+ module Client
5
+ class Command
6
+ def self.call(args)
7
+ new(args).call
8
+ end
9
+
10
+ attr_reader :args, :env
11
+
12
+ def initialize(args)
13
+ @args = args
14
+ @env = Env.new
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,62 @@
1
+ require "spring/version"
2
+
3
+ module Spring
4
+ module Client
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 self.call(args)
13
+ require "spring/commands"
14
+ super
15
+ end
16
+
17
+ def initialize(args, spring_commands = nil, application_commands = nil)
18
+ super args
19
+
20
+ @spring_commands = spring_commands || Spring::Client::COMMANDS.dup
21
+ @application_commands = application_commands || Spring.commands.dup
22
+
23
+ @spring_commands.delete_if { |k, v| k.start_with?("-") }
24
+
25
+ @application_commands["rails"] = @spring_commands.delete("rails")
26
+ end
27
+
28
+ def call
29
+ puts formatted_help
30
+ end
31
+
32
+ def formatted_help
33
+ ["Version: #{env.version}\n",
34
+ "Usage: spring COMMAND [ARGS]\n",
35
+ *command_help("Spring itself", spring_commands),
36
+ '',
37
+ *command_help("your application", application_commands)].join("\n")
38
+ end
39
+
40
+ def command_help(subject, commands)
41
+ ["Commands for #{subject}:\n",
42
+ *commands.sort_by(&:first).map { |name, command| display(name, command) }.compact]
43
+ end
44
+
45
+ private
46
+
47
+ def all_commands
48
+ spring_commands.merge application_commands
49
+ end
50
+
51
+ def display(name, command)
52
+ if command.description
53
+ " #{name.ljust(max_name_width)} #{command.description}"
54
+ end
55
+ end
56
+
57
+ def max_name_width
58
+ @max_name_width ||= all_commands.keys.map(&:length).max
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ require "set"
2
+
3
+ module Spring
4
+ module Client
5
+ class Rails < Command
6
+ COMMANDS = Set.new %w(console runner generate destroy test)
7
+
8
+ ALIASES = {
9
+ "c" => "console",
10
+ "r" => "runner",
11
+ "g" => "generate",
12
+ "d" => "destroy",
13
+ "t" => "test"
14
+ }
15
+
16
+ def self.description
17
+ "Run a rails command. The following sub commands will use Spring: #{COMMANDS.to_a.join ', '}."
18
+ end
19
+
20
+ def call
21
+ command_name = ALIASES[args[1]] || args[1]
22
+
23
+ if COMMANDS.include?(command_name)
24
+ Run.call(["rails_#{command_name}", *args.drop(2)])
25
+ else
26
+ require "spring/configuration"
27
+ ARGV.shift
28
+ load Dir.glob(Spring.application_root_path.join("{bin,script}/rails")).first
29
+ exit
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,232 @@
1
+ require "rbconfig"
2
+ require "socket"
3
+ require "bundler"
4
+
5
+ module Spring
6
+ module Client
7
+ class Run < Command
8
+ FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
9
+ CONNECT_TIMEOUT = 1
10
+ BOOT_TIMEOUT = 20
11
+
12
+ attr_reader :server
13
+
14
+ def initialize(args)
15
+ super
16
+
17
+ @signal_queue = []
18
+ @server_booted = false
19
+ end
20
+
21
+ def log(message)
22
+ env.log "[client] #{message}"
23
+ end
24
+
25
+ def connect
26
+ @server = UNIXSocket.open(env.socket_name)
27
+ end
28
+
29
+ def call
30
+ begin
31
+ connect
32
+ rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
33
+ cold_run
34
+ else
35
+ warm_run
36
+ end
37
+ ensure
38
+ server.close if server
39
+ end
40
+
41
+ def warm_run
42
+ run
43
+ rescue CommandNotFound
44
+ require "spring/commands"
45
+
46
+ if Spring.command?(args.first)
47
+ # Command installed since Spring started
48
+ stop_server
49
+ cold_run
50
+ else
51
+ raise
52
+ end
53
+ end
54
+
55
+ def cold_run
56
+ boot_server
57
+ connect
58
+ run
59
+ end
60
+
61
+ def run
62
+ verify_server_version
63
+
64
+ application, client = UNIXSocket.pair
65
+
66
+ queue_signals
67
+ connect_to_application(client)
68
+ run_command(client, application)
69
+ rescue Errno::ECONNRESET
70
+ exit 1
71
+ end
72
+
73
+ def boot_server
74
+ env.socket_path.unlink if env.socket_path.exist?
75
+
76
+ pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
77
+ timeout = Time.now + BOOT_TIMEOUT
78
+
79
+ @server_booted = true
80
+
81
+ until env.socket_path.exist?
82
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
83
+
84
+ if status
85
+ exit status.exitstatus
86
+ elsif Time.now > timeout
87
+ $stderr.puts "Starting Spring server with `#{env.server_command}` " \
88
+ "timed out after #{BOOT_TIMEOUT} seconds"
89
+ exit 1
90
+ end
91
+
92
+ sleep 0.1
93
+ end
94
+ end
95
+
96
+ def server_booted?
97
+ @server_booted
98
+ end
99
+
100
+ def gem_env
101
+ bundle = Bundler.bundle_path.to_s
102
+ paths = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
103
+
104
+ {
105
+ "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
106
+ "GEM_HOME" => bundle
107
+ }
108
+ end
109
+
110
+ def stop_server
111
+ server.close
112
+ @server = nil
113
+ env.stop
114
+ end
115
+
116
+ def verify_server_version
117
+ server_version = server.gets.chomp
118
+ if server_version != env.version
119
+ $stderr.puts "There is a version mismatch between the Spring client " \
120
+ "(#{env.version}) and the server (#{server_version})."
121
+
122
+ if server_booted?
123
+ $stderr.puts "We already tried to reboot the server, but the mismatch is still present."
124
+ exit 1
125
+ else
126
+ $stderr.puts "Restarting to resolve."
127
+ stop_server
128
+ cold_run
129
+ end
130
+ end
131
+ end
132
+
133
+ def connect_to_application(client)
134
+ server.send_io client
135
+ send_json server, "args" => args, "default_rails_env" => default_rails_env
136
+
137
+ if IO.select([server], [], [], CONNECT_TIMEOUT)
138
+ server.gets or raise CommandNotFound
139
+ else
140
+ raise "Error connecting to Spring server"
141
+ end
142
+ end
143
+
144
+ def run_command(client, application)
145
+ log "sending command"
146
+
147
+ application.send_io STDOUT
148
+ application.send_io STDERR
149
+ application.send_io STDIN
150
+
151
+ send_json application, "args" => args, "env" => ENV.to_hash
152
+
153
+ pid = server.gets
154
+ pid = pid.chomp if pid
155
+
156
+ # We must not close the client socket until we are sure that the application has
157
+ # received the FD. Otherwise the FD can end up getting closed while it's in the server
158
+ # socket buffer on OS X. This doesn't happen on Linux.
159
+ client.close
160
+
161
+ if pid && !pid.empty?
162
+ log "got pid: #{pid}"
163
+
164
+ suspend_resume_on_tstp_cont(pid)
165
+
166
+ forward_signals(application)
167
+ status = application.read.to_i
168
+
169
+ log "got exit status #{status}"
170
+
171
+ exit status
172
+ else
173
+ log "got no pid"
174
+ exit 1
175
+ end
176
+ ensure
177
+ application.close
178
+ end
179
+
180
+ def queue_signals
181
+ FORWARDED_SIGNALS.each do |sig|
182
+ trap(sig) { @signal_queue << sig }
183
+ end
184
+ end
185
+
186
+ def suspend_resume_on_tstp_cont(pid)
187
+ trap("TSTP") {
188
+ log "suspended"
189
+ Process.kill("STOP", pid.to_i)
190
+ Process.kill("STOP", Process.pid)
191
+ }
192
+ trap("CONT") {
193
+ log "resumed"
194
+ Process.kill("CONT", pid.to_i)
195
+ }
196
+ end
197
+
198
+ def forward_signals(application)
199
+ @signal_queue.each { |sig| kill sig, application }
200
+
201
+ FORWARDED_SIGNALS.each do |sig|
202
+ trap(sig) { forward_signal sig, application }
203
+ end
204
+ end
205
+
206
+ def forward_signal(sig, application)
207
+ if kill(sig, application) != 0
208
+ # If the application process is gone, then don't block the
209
+ # signal on this process.
210
+ trap(sig, 'DEFAULT')
211
+ Process.kill(sig, Process.pid)
212
+ end
213
+ end
214
+
215
+ def kill(sig, application)
216
+ application.puts(sig)
217
+ application.gets.to_i
218
+ end
219
+
220
+ def send_json(socket, data)
221
+ data = JSON.dump(data)
222
+
223
+ socket.puts data.bytesize
224
+ socket.write data
225
+ end
226
+
227
+ def default_rails_env
228
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
229
+ end
230
+ end
231
+ end
232
+ end