spring-jruby 1.4.3

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +364 -0
  4. data/bin/spring +49 -0
  5. data/lib/spring-jruby/application.rb +283 -0
  6. data/lib/spring-jruby/application/boot.rb +19 -0
  7. data/lib/spring-jruby/binstub.rb +13 -0
  8. data/lib/spring-jruby/boot.rb +9 -0
  9. data/lib/spring-jruby/client.rb +46 -0
  10. data/lib/spring-jruby/client/binstub.rb +188 -0
  11. data/lib/spring-jruby/client/command.rb +18 -0
  12. data/lib/spring-jruby/client/help.rb +62 -0
  13. data/lib/spring-jruby/client/rails.rb +34 -0
  14. data/lib/spring-jruby/client/run.rb +167 -0
  15. data/lib/spring-jruby/client/status.rb +30 -0
  16. data/lib/spring-jruby/client/stop.rb +22 -0
  17. data/lib/spring-jruby/client/version.rb +11 -0
  18. data/lib/spring-jruby/command_wrapper.rb +82 -0
  19. data/lib/spring-jruby/commands.rb +51 -0
  20. data/lib/spring-jruby/commands/rails.rb +112 -0
  21. data/lib/spring-jruby/commands/rake.rb +30 -0
  22. data/lib/spring-jruby/configuration.rb +60 -0
  23. data/lib/spring-jruby/env.rb +109 -0
  24. data/lib/spring-jruby/errors.rb +36 -0
  25. data/lib/spring-jruby/impl/application.rb +7 -0
  26. data/lib/spring-jruby/impl/application_manager.rb +7 -0
  27. data/lib/spring-jruby/impl/fork/application.rb +69 -0
  28. data/lib/spring-jruby/impl/fork/application_manager.rb +137 -0
  29. data/lib/spring-jruby/impl/fork/run.rb +47 -0
  30. data/lib/spring-jruby/impl/pool/application.rb +47 -0
  31. data/lib/spring-jruby/impl/pool/application_manager.rb +226 -0
  32. data/lib/spring-jruby/impl/pool/run.rb +27 -0
  33. data/lib/spring-jruby/impl/run.rb +7 -0
  34. data/lib/spring-jruby/io_helpers.rb +92 -0
  35. data/lib/spring-jruby/json.rb +626 -0
  36. data/lib/spring-jruby/platform.rb +23 -0
  37. data/lib/spring-jruby/process_title_updater.rb +65 -0
  38. data/lib/spring-jruby/server.rb +130 -0
  39. data/lib/spring-jruby/sid.rb +42 -0
  40. data/lib/spring-jruby/test.rb +18 -0
  41. data/lib/spring-jruby/test/acceptance_test.rb +371 -0
  42. data/lib/spring-jruby/test/application.rb +217 -0
  43. data/lib/spring-jruby/test/application_generator.rb +134 -0
  44. data/lib/spring-jruby/test/rails_version.rb +40 -0
  45. data/lib/spring-jruby/test/watcher_test.rb +167 -0
  46. data/lib/spring-jruby/version.rb +3 -0
  47. data/lib/spring-jruby/watcher.rb +30 -0
  48. data/lib/spring-jruby/watcher/abstract.rb +86 -0
  49. data/lib/spring-jruby/watcher/polling.rb +61 -0
  50. metadata +137 -0
@@ -0,0 +1,36 @@
1
+ module Spring
2
+ class ClientError < StandardError; end
3
+
4
+ class UnknownProject < StandardError
5
+ attr_reader :current_dir
6
+
7
+ def initialize(current_dir)
8
+ @current_dir = current_dir
9
+ end
10
+
11
+ def message
12
+ "Spring was unable to locate the root of your project. There was no Gemfile " \
13
+ "present in the current directory (#{current_dir}) or any of the parent " \
14
+ "directories."
15
+ end
16
+ end
17
+
18
+ class MissingApplication < ClientError
19
+ attr_reader :project_root
20
+
21
+ def initialize(project_root)
22
+ @project_root = project_root
23
+ end
24
+
25
+ def message
26
+ "Spring was unable to find your config/application.rb file. " \
27
+ "Your project root was detected at #{project_root}, so spring " \
28
+ "looked for #{project_root}/config/application.rb but it doesn't exist. You can " \
29
+ "configure the root of your application by setting Spring.application_root in " \
30
+ "config/spring.rb."
31
+ end
32
+ end
33
+
34
+ class CommandNotFound < ClientError
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ require "spring-jruby/platform"
2
+
3
+ if Spring.fork?
4
+ require "spring-jruby/impl/fork/application"
5
+ else
6
+ require "spring-jruby/impl/pool/application"
7
+ end
@@ -0,0 +1,7 @@
1
+ require "spring-jruby/platform"
2
+
3
+ if Spring.fork?
4
+ require "spring-jruby/impl/fork/application_manager"
5
+ else
6
+ require "spring-jruby/impl/pool/application_manager"
7
+ end
@@ -0,0 +1,69 @@
1
+ module Spring
2
+ module ApplicationImpl
3
+ def notify_manager_ready
4
+ manager.puts
5
+ end
6
+
7
+ def receive_streams(client)
8
+ 3.times.map { IOWrapper.recv_io(client).to_io }
9
+ end
10
+
11
+ def reopen_streams(streams)
12
+ [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
13
+ end
14
+
15
+ def eager_preload
16
+ with_pty { preload }
17
+ end
18
+
19
+ def with_pty
20
+ PTY.open do |master, slave|
21
+ [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
22
+ Thread.new { master.read }
23
+ yield
24
+ reset_streams
25
+ end
26
+ end
27
+
28
+ def reset_streams
29
+ [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
30
+ STDIN.reopen("/dev/null")
31
+ end
32
+
33
+ def wait(pid, streams, client)
34
+ @mutex.synchronize { @waiting << pid }
35
+
36
+ # Wait in a separate thread so we can run multiple commands at once
37
+ Thread.new {
38
+ begin
39
+ _, status = Process.wait2 pid
40
+ log "#{pid} exited with #{status.exitstatus}"
41
+
42
+ streams.each(&:close)
43
+ client.puts(status.exitstatus)
44
+ client.close
45
+ ensure
46
+ @mutex.synchronize { @waiting.delete pid }
47
+ exit_if_finished
48
+ end
49
+ }
50
+ end
51
+
52
+ def fork_child(client, streams, child_started)
53
+ pid = fork { yield }
54
+ child_started[0] = true
55
+
56
+ disconnect_database
57
+ reset_streams
58
+
59
+ log "forked #{pid}"
60
+ manager.puts pid
61
+
62
+ wait pid, streams, client
63
+ end
64
+
65
+ def before_command
66
+ # NOP
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,137 @@
1
+ module Spring
2
+ class ApplicationManager
3
+ attr_reader :pid, :child, :app_env, :spring_env, :status
4
+
5
+ def initialize(app_env)
6
+ @app_env = app_env
7
+ @spring_env = Env.new
8
+ @mutex = Mutex.new
9
+ @state = :running
10
+ end
11
+
12
+ def log(message)
13
+ spring_env.log "[application_manager:#{app_env}] #{message}"
14
+ end
15
+
16
+ # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
17
+ # line which messes with backtraces in e.g. rspec
18
+ def synchronize
19
+ @mutex.lock
20
+ yield
21
+ ensure
22
+ @mutex.unlock
23
+ end
24
+
25
+ def start
26
+ start_child
27
+ end
28
+
29
+ def restart
30
+ return if @state == :stopping
31
+ start_child(true)
32
+ end
33
+
34
+ def alive?
35
+ @pid
36
+ end
37
+
38
+ def with_child
39
+ synchronize do
40
+ if alive?
41
+ begin
42
+ yield
43
+ rescue Errno::ECONNRESET, Errno::EPIPE
44
+ # The child has died but has not been collected by the wait thread yet,
45
+ # so start a new child and try again.
46
+ log "child dead; starting"
47
+ start
48
+ yield
49
+ end
50
+ else
51
+ log "child not running; starting"
52
+ start
53
+ yield
54
+ end
55
+ end
56
+ end
57
+
58
+ # Returns the pid of the process running the command, or nil if the application process died.
59
+ def run(client)
60
+ with_child do
61
+ child.send_io client
62
+ child.gets or raise Errno::EPIPE
63
+ end
64
+
65
+ pid = child.gets.to_i
66
+
67
+ unless pid.zero?
68
+ log "got worker pid #{pid}"
69
+ pid
70
+ end
71
+ rescue Errno::ECONNRESET, Errno::EPIPE => e
72
+ log "#{e} while reading from child; returning no pid"
73
+ nil
74
+ ensure
75
+ client.close
76
+ end
77
+
78
+ def stop
79
+ log "stopping"
80
+ @state = :stopping
81
+
82
+ if pid
83
+ Process.kill('TERM', pid)
84
+ Process.wait(pid)
85
+ end
86
+ rescue Errno::ESRCH, Errno::ECHILD
87
+ # Don't care
88
+ end
89
+
90
+ private
91
+
92
+ def start_child(preload = false)
93
+ @child, child_socket = UNIXSocket.pair
94
+
95
+ Bundler.with_clean_env do
96
+ @pid = Process.spawn(
97
+ {
98
+ "RAILS_ENV" => app_env,
99
+ "RACK_ENV" => app_env,
100
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
101
+ "SPRING_PRELOAD" => preload ? "1" : "0"
102
+ },
103
+ "ruby",
104
+ "-I", File.expand_path("../..", __FILE__),
105
+ "-e", "require 'spring-jruby/application/boot'",
106
+ 3 => child_socket
107
+ )
108
+ end
109
+
110
+ start_wait_thread(pid, child) if child.gets
111
+ child_socket.close
112
+ end
113
+
114
+ def start_wait_thread(pid, child)
115
+ Process.detach(pid)
116
+
117
+ Thread.new {
118
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
119
+ # as if it does we're no longer interested in the child
120
+ loop do
121
+ IO.select([child])
122
+ break if child.recv(1, Socket::MSG_PEEK).empty?
123
+ sleep 0.01
124
+ end
125
+
126
+ log "child #{pid} shutdown"
127
+
128
+ synchronize {
129
+ if @pid == pid
130
+ @pid = nil
131
+ restart
132
+ end
133
+ }
134
+ }
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,47 @@
1
+ module Spring
2
+ module Client
3
+ module RunImpl
4
+ TIMEOUT = 1
5
+ FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO) & Signal.list.keys
6
+
7
+ def queue_signals
8
+ RunImpl::FORWARDED_SIGNALS.each do |sig|
9
+ trap(sig) { @signal_queue << sig }
10
+ end
11
+ end
12
+
13
+ def send_std_io_to(application)
14
+ application.send_io STDOUT
15
+ application.send_io STDERR
16
+ application.send_io STDIN
17
+ end
18
+
19
+ def run_on(application, pid)
20
+ forward_signals(pid.to_i)
21
+ status = application.read.to_i
22
+
23
+ log "got exit status #{status}"
24
+
25
+ exit status
26
+ end
27
+
28
+ def forward_signals(pid)
29
+ @signal_queue.each { |sig| kill sig, pid }
30
+
31
+ RunImpl::FORWARDED_SIGNALS.each do |sig|
32
+ trap(sig) { forward_signal sig, pid }
33
+ end
34
+ rescue Errno::ESRCH
35
+ end
36
+
37
+ def forward_signal(sig, pid)
38
+ kill(sig, pid)
39
+ rescue Errno::ESRCH
40
+ # If the application process is gone, then don't block the
41
+ # signal on this process.
42
+ trap(sig, 'DEFAULT')
43
+ Process.kill(sig, Process.pid)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ module Spring
2
+ module ApplicationImpl
3
+ def notify_manager_ready
4
+ manager.puts Process.pid
5
+ end
6
+
7
+ def receive_streams(client)
8
+ []
9
+ end
10
+
11
+ def reopen_streams(streams)
12
+ # NOP
13
+ end
14
+
15
+ def eager_preload
16
+ preload
17
+ end
18
+
19
+ def screen_attached?
20
+ !system(%{screen -ls | grep "#{ENV['SPRING_SCREEN_NAME']}" | grep Detached > /dev/null})
21
+ end
22
+
23
+ def screen_move_to_bottom
24
+ puts "\033[22B"
25
+ end
26
+
27
+ def fork_child(client, streams, child_started)
28
+ manager.puts ENV["SPRING_SCREEN_NAME"]
29
+ child_started[0] = true
30
+ exitstatus = 0
31
+ begin
32
+ log "started #{Process.pid}"
33
+ yield
34
+ rescue SystemExit => ex
35
+ exitstatus = ex.status
36
+ end
37
+
38
+ log "#{Process.pid} exited with #{exitstatus}"
39
+ exit
40
+ end
41
+
42
+ def before_command
43
+ screen_move_to_bottom
44
+ sleep 0.1 until screen_attached?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,226 @@
1
+ module Spring
2
+ class ApplicationManager
3
+ class Worker
4
+ attr_reader :screen_pid, :pid, :uuid, :socket, :screen_name
5
+ attr_accessor :on_done
6
+
7
+ def initialize(env, args)
8
+ @spring_env = Env.new
9
+ channel, @remote_socket = WorkerChannel.pair
10
+ @uuid = File.basename(@remote_socket.path).gsub('.sock', '')
11
+
12
+ Bundler.with_clean_env do
13
+ spawn_screen(
14
+ env.merge("SPRING_SOCKET" => @remote_socket.path),
15
+ args
16
+ )
17
+ end
18
+
19
+ @socket = channel.to_io
20
+ end
21
+
22
+ def spawn_screen(env, args)
23
+ @screen_name = "spring_#{@uuid}"
24
+
25
+ @screen_pid =
26
+ Process.spawn(
27
+ env.merge("SPRING_SCREEN_NAME" => screen_name),
28
+ "screen", "-d", "-m", "-S", screen_name,
29
+ *args
30
+ )
31
+
32
+ log "(spawn #{@screen_pid})"
33
+ end
34
+
35
+ def await_boot
36
+ Process.detach(screen_pid)
37
+ @pid = socket.gets.to_i
38
+ start_wait_thread(pid, socket) unless pid.zero?
39
+ @remote_socket.close
40
+ end
41
+
42
+ def start_wait_thread(pid, child)
43
+ Thread.new {
44
+ begin
45
+ Process.kill(0, pid) while sleep(1)
46
+ rescue Errno::ESRCH
47
+ end
48
+
49
+ log "child #{pid} shutdown"
50
+
51
+ on_done.call(self) if on_done
52
+ }
53
+ end
54
+
55
+ def log(message)
56
+ @spring_env.log "[worker:#{uuid}] #{message}"
57
+ end
58
+ end
59
+
60
+ class WorkerPool
61
+ def initialize(app_env, *app_args)
62
+ @app_env = app_env
63
+ @app_args = app_args
64
+ @spring_env = Env.new
65
+
66
+ @workers = []
67
+ @workers_in_use = []
68
+ @spawning_workers = []
69
+
70
+ @check_mutex = Mutex.new
71
+ @workers_mutex = Mutex.new
72
+
73
+ run
74
+ end
75
+
76
+ def add_worker
77
+ worker = Worker.new(@app_env, @app_args)
78
+ worker.on_done = method(:worker_done)
79
+ @workers_mutex.synchronize { @spawning_workers << worker }
80
+ Thread.new do
81
+ worker.await_boot
82
+ log "+ worker #{worker.pid} (#{worker.uuid})"
83
+ @workers_mutex.synchronize do
84
+ @spawning_workers.delete(worker)
85
+ @workers << worker
86
+ end
87
+ end
88
+ end
89
+
90
+ def worker_done(worker)
91
+ log "- worker #{worker.pid} (#{worker.uuid})"
92
+ @workers_mutex.synchronize do
93
+ @workers_in_use.delete(worker)
94
+ end
95
+ end
96
+
97
+ def get_worker(spawn_new = true)
98
+ add_worker if spawn_new && all_size == 0
99
+
100
+ worker = nil
101
+ while worker.nil? && all_size > 0
102
+ @workers_mutex.synchronize do
103
+ worker = @workers.shift
104
+ @workers_in_use << worker if worker
105
+ end
106
+ break if worker
107
+ sleep 1
108
+ end
109
+
110
+ Thread.new { check_min_free_workers } if spawn_new
111
+
112
+ worker
113
+ end
114
+
115
+ def check_min_free_workers
116
+ if @check_mutex.try_lock
117
+ # TODO: mutex, and dont do it if already in progress
118
+ # do this in thread
119
+ while all_size < Spring.pool_min_free_workers
120
+ unless Spring.pool_spawn_parallel
121
+ sleep 0.1 until @workers_mutex.synchronize { @spawning_workers.empty? }
122
+ end
123
+ add_worker
124
+ end
125
+ @check_mutex.unlock
126
+ end
127
+ end
128
+
129
+ def all_size
130
+ @workers_mutex.synchronize { @workers.size + @spawning_workers.size }
131
+ end
132
+
133
+ def stop!
134
+ if spawning_worker_pids.include?(nil)
135
+ log "Waiting for workers to quit..."
136
+ sleep 0.1 while spawning_worker_pids.include?(nil)
137
+ end
138
+
139
+ @workers_mutex.synchronize do
140
+ (@spawning_workers + @workers_in_use + @workers).each do |worker|
141
+ kill_worker(worker)
142
+ end
143
+ end
144
+ end
145
+ private
146
+ def kill_worker(worker)
147
+ log "- worker #{worker.pid} (#{worker.uuid})."
148
+ system("kill -9 #{worker.pid} > /dev/null 2>&1")
149
+ system("screen -S #{worker.screen_name} -X quit > /dev/null 2>&1")
150
+ rescue
151
+ end
152
+
153
+ def spawning_worker_pids
154
+ @spawning_workers.map { |worker| worker.pid }
155
+ end
156
+
157
+ def run
158
+ system("screen -wipe > /dev/null 2>&1")
159
+
160
+ check_min_free_workers
161
+ end
162
+
163
+ def log(message)
164
+ @spring_env.log "[worker:pool] #{message}"
165
+ end
166
+ end
167
+
168
+ def initialize(app_env)
169
+ @app_env = app_env
170
+ @spring_env = Env.new
171
+ @pool =
172
+ WorkerPool.new(
173
+ {
174
+ "RAILS_ENV" => app_env,
175
+ "RACK_ENV" => app_env,
176
+ "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
177
+ "SPRING_PRELOAD" => "1",
178
+ },
179
+ Spring.ruby_bin,
180
+ "-I", File.expand_path("../..", __FILE__),
181
+ "-e", "require 'spring-jruby/application/boot'"
182
+ )
183
+ end
184
+
185
+ # Returns the name of the screen running the command, or nil if the application process died.
186
+ def run(client)
187
+ name = nil
188
+ with_child do |child|
189
+ client.forward_to(child.socket)
190
+ child.socket.gets or raise Errno::EPIPE
191
+
192
+ name = child.socket.gets
193
+ end
194
+
195
+ unless name.nil?
196
+ log "got worker name #{name}"
197
+ name
198
+ end
199
+ rescue Errno::ECONNRESET, Errno::EPIPE => e
200
+ log "#{e} while reading from child; returning no name"
201
+ nil
202
+ ensure
203
+ client.close
204
+ end
205
+
206
+ def stop
207
+ log "stopping"
208
+
209
+ @pool.stop!
210
+ rescue Errno::ESRCH, Errno::ECHILD
211
+ # Don't care
212
+ end
213
+
214
+ protected
215
+
216
+ attr_reader :app_env, :spring_env
217
+
218
+ def log(message)
219
+ spring_env.log "[application_manager:#{app_env}] #{message}"
220
+ end
221
+
222
+ def with_child
223
+ yield(@pool.get_worker)
224
+ end
225
+ end
226
+ end