spring_standalone 0.1.13

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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +418 -0
  4. data/bin/spring_sa +49 -0
  5. data/lib/spring_standalone/application.rb +367 -0
  6. data/lib/spring_standalone/application/boot.rb +19 -0
  7. data/lib/spring_standalone/application_manager.rb +141 -0
  8. data/lib/spring_standalone/binstub.rb +13 -0
  9. data/lib/spring_standalone/boot.rb +10 -0
  10. data/lib/spring_standalone/client.rb +48 -0
  11. data/lib/spring_standalone/client/binstub.rb +198 -0
  12. data/lib/spring_standalone/client/command.rb +18 -0
  13. data/lib/spring_standalone/client/help.rb +62 -0
  14. data/lib/spring_standalone/client/rails.rb +34 -0
  15. data/lib/spring_standalone/client/run.rb +232 -0
  16. data/lib/spring_standalone/client/server.rb +18 -0
  17. data/lib/spring_standalone/client/status.rb +30 -0
  18. data/lib/spring_standalone/client/stop.rb +22 -0
  19. data/lib/spring_standalone/client/version.rb +11 -0
  20. data/lib/spring_standalone/command_wrapper.rb +82 -0
  21. data/lib/spring_standalone/commands.rb +50 -0
  22. data/lib/spring_standalone/commands/rake.rb +30 -0
  23. data/lib/spring_standalone/configuration.rb +58 -0
  24. data/lib/spring_standalone/env.rb +116 -0
  25. data/lib/spring_standalone/errors.rb +36 -0
  26. data/lib/spring_standalone/failsafe_thread.rb +14 -0
  27. data/lib/spring_standalone/json.rb +626 -0
  28. data/lib/spring_standalone/process_title_updater.rb +65 -0
  29. data/lib/spring_standalone/server.rb +150 -0
  30. data/lib/spring_standalone/sid.rb +42 -0
  31. data/lib/spring_standalone/version.rb +3 -0
  32. data/lib/spring_standalone/watcher.rb +30 -0
  33. data/lib/spring_standalone/watcher/abstract.rb +117 -0
  34. data/lib/spring_standalone/watcher/polling.rb +98 -0
  35. metadata +106 -0
@@ -0,0 +1,13 @@
1
+ command = File.basename($0)
2
+ bin_path = File.expand_path("../../../bin/spring_sa", __FILE__)
3
+
4
+ if command == "spring_sa"
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_standalone/configuration"
5
+ require "spring_standalone/env"
6
+ require "spring_standalone/process_title_updater"
7
+ require "spring_standalone/json"
8
+ require "spring_standalone/watcher"
9
+ require "spring_standalone/failsafe_thread"
10
+
@@ -0,0 +1,48 @@
1
+ require "spring_standalone/errors"
2
+ require "spring_standalone/json"
3
+
4
+ require "spring_standalone/client/command"
5
+ require "spring_standalone/client/run"
6
+ require "spring_standalone/client/help"
7
+ require "spring_standalone/client/binstub"
8
+ require "spring_standalone/client/stop"
9
+ require "spring_standalone/client/status"
10
+ #require "spring_standalone/client/rails"
11
+ require "spring_standalone/client/version"
12
+ require "spring_standalone/client/server"
13
+
14
+ module SpringStandalone
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,198 @@
1
+ require 'set'
2
+
3
+ module SpringStandalone
4
+ module Client
5
+ class Binstub < Command
6
+ SHEBANG = /\#\!.*\n(\#.*\n)*/
7
+
8
+ # If loading the bin/spring_sa file works, it'll run SpringStandalone 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 SpringStandalone
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_sa', __FILE__)
17
+ rescue LoadError => e
18
+ raise unless e.message.include?('spring_sa')
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 SpringStandalone without using Bundler, in order to be fast.
30
+ # It gets overwritten when you run the `spring_sa binstub` command.
31
+
32
+ unless defined?(SpringStandalone)
33
+ require 'rubygems'
34
+ require 'bundler'
35
+
36
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
37
+ spring_standalone = lockfile.specs.detect { |spec| spec.name == 'spring_standalone' }
38
+ if spring_standalone
39
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
40
+ gem 'spring_standalone', spring_standalone.version
41
+ require 'spring_standalone/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_sa', __FILE__)\nrescue LoadError\nend\n},
50
+ %{begin\n spring_bin_path = File.expand_path('../spring_sa', __FILE__)\n load spring_bin_path\nrescue LoadError => e\n raise unless e.message.end_with? spring_bin_path, 'spring_standalone/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 = SpringStandalone.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 "SpringStandalone 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 "SpringStandalone inserted"
97
+ else
98
+ status "doesn't appear to be ruby, so cannot use SpringStandalone", $stderr
99
+ exit 1
100
+ end
101
+ end
102
+ else
103
+ generate
104
+ status "generated with SpringStandalone"
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 "SpringStandalone removed"
122
+ end
123
+ end
124
+ end
125
+
126
+ attr_reader :bindir, :items
127
+
128
+ def self.description
129
+ "Generate SpringStandalone 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_standalone/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 = SpringStandalone.commands.dup
158
+ commands.values
159
+ # commands.delete_if { |command_name, _| command_name.start_with?("rails_") }
160
+ # commands.values + [self.class.rails_command]
161
+ when "--remove"
162
+ @mode = :remove
163
+ []
164
+ # when "rails"
165
+ # [self.class.rails_command]
166
+ else
167
+ if command = SpringStandalone.commands[name]
168
+ [command]
169
+ else
170
+ $stderr.puts "The '#{name}' command is not known to spring_standalone."
171
+ exit 1
172
+ end
173
+ end
174
+ end
175
+
176
+ def call
177
+ case @mode
178
+ when :add
179
+ bindir.mkdir unless bindir.exist?
180
+
181
+ File.write(spring_sa_binstub, SPRING)
182
+ spring_sa_binstub.chmod 0755
183
+
184
+ items.each(&:add)
185
+ when :remove
186
+ spring_sa_binstub.delete if @all
187
+ items.each(&:remove)
188
+ else
189
+ raise ArgumentError
190
+ end
191
+ end
192
+
193
+ def spring_sa_binstub
194
+ bindir.join("spring_sa")
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,18 @@
1
+ require "spring_standalone/env"
2
+
3
+ module SpringStandalone
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_standalone/version"
2
+
3
+ module SpringStandalone
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_standalone/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 || SpringStandalone::Client::COMMANDS.dup
21
+ @application_commands = application_commands || SpringStandalone.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_sa COMMAND [ARGS]\n",
35
+ *command_help("SpringStandalone 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 SpringStandalone
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 SpringStandalone: #{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_standalone/configuration"
27
+ ARGV.shift
28
+ load Dir.glob(SpringStandalone.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 SpringStandalone
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_standalone/commands"
45
+
46
+ if SpringStandalone.command?(args.first)
47
+ # Command installed since SpringStandalone 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 SpringStandalone 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 SpringStandalone 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_app_env" => default_app_env
136
+
137
+ if IO.select([server], [], [], CONNECT_TIMEOUT)
138
+ server.gets or raise CommandNotFound
139
+ else
140
+ raise "Error connecting to SpringStandalone 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_app_env
228
+ ENV['APP_ENV'] || ENV['RACK_ENV'] || 'development'
229
+ end
230
+ end
231
+ end
232
+ end