spring_standalone 0.1.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +418 -0
- data/bin/spring_sa +49 -0
- data/lib/spring_standalone/application.rb +367 -0
- data/lib/spring_standalone/application/boot.rb +19 -0
- data/lib/spring_standalone/application_manager.rb +141 -0
- data/lib/spring_standalone/binstub.rb +13 -0
- data/lib/spring_standalone/boot.rb +10 -0
- data/lib/spring_standalone/client.rb +48 -0
- data/lib/spring_standalone/client/binstub.rb +198 -0
- data/lib/spring_standalone/client/command.rb +18 -0
- data/lib/spring_standalone/client/help.rb +62 -0
- data/lib/spring_standalone/client/rails.rb +34 -0
- data/lib/spring_standalone/client/run.rb +232 -0
- data/lib/spring_standalone/client/server.rb +18 -0
- data/lib/spring_standalone/client/status.rb +30 -0
- data/lib/spring_standalone/client/stop.rb +22 -0
- data/lib/spring_standalone/client/version.rb +11 -0
- data/lib/spring_standalone/command_wrapper.rb +82 -0
- data/lib/spring_standalone/commands.rb +50 -0
- data/lib/spring_standalone/commands/rake.rb +30 -0
- data/lib/spring_standalone/configuration.rb +58 -0
- data/lib/spring_standalone/env.rb +116 -0
- data/lib/spring_standalone/errors.rb +36 -0
- data/lib/spring_standalone/failsafe_thread.rb +14 -0
- data/lib/spring_standalone/json.rb +626 -0
- data/lib/spring_standalone/process_title_updater.rb +65 -0
- data/lib/spring_standalone/server.rb +150 -0
- data/lib/spring_standalone/sid.rb +42 -0
- data/lib/spring_standalone/version.rb +3 -0
- data/lib/spring_standalone/watcher.rb +30 -0
- data/lib/spring_standalone/watcher/abstract.rb +117 -0
- data/lib/spring_standalone/watcher/polling.rb +98 -0
- metadata +106 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
module SpringStandalone
|
2
|
+
# Yes, I know this reimplements a bunch of stuff in Active Support, but
|
3
|
+
# I don't want the SpringStandalone client to depend on AS, in order to keep its
|
4
|
+
# load time down.
|
5
|
+
class ProcessTitleUpdater
|
6
|
+
SECOND = 1
|
7
|
+
MINUTE = 60
|
8
|
+
HOUR = 60*60
|
9
|
+
|
10
|
+
def self.run(&block)
|
11
|
+
updater = new(&block)
|
12
|
+
|
13
|
+
SpringStandalone.failsafe_thread {
|
14
|
+
$0 = updater.value
|
15
|
+
loop { $0 = updater.next }
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :block
|
20
|
+
|
21
|
+
def initialize(start = Time.now, &block)
|
22
|
+
@start = start
|
23
|
+
@block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def interval
|
27
|
+
distance = Time.now - @start
|
28
|
+
|
29
|
+
if distance < MINUTE
|
30
|
+
SECOND
|
31
|
+
elsif distance < HOUR
|
32
|
+
MINUTE
|
33
|
+
else
|
34
|
+
HOUR
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def next
|
39
|
+
sleep interval
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
43
|
+
def value
|
44
|
+
block.call(distance_in_words)
|
45
|
+
end
|
46
|
+
|
47
|
+
def distance_in_words(now = Time.now)
|
48
|
+
distance = now - @start
|
49
|
+
|
50
|
+
if distance < MINUTE
|
51
|
+
pluralize(distance, "sec")
|
52
|
+
elsif distance < HOUR
|
53
|
+
pluralize(distance / MINUTE, "min")
|
54
|
+
else
|
55
|
+
pluralize(distance / HOUR, "hour")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def pluralize(amount, unit)
|
62
|
+
"#{amount.to_i} #{amount.to_i == 1 ? unit : "#{unit}s"}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module SpringStandalone
|
2
|
+
ORIGINAL_ENV = ENV.to_hash
|
3
|
+
end
|
4
|
+
|
5
|
+
require "spring_standalone/boot"
|
6
|
+
require "spring_standalone/application_manager"
|
7
|
+
|
8
|
+
# Must be last, as it requires bundler/setup, which alters the load path
|
9
|
+
require "spring_standalone/commands"
|
10
|
+
|
11
|
+
module SpringStandalone
|
12
|
+
class Server
|
13
|
+
def self.boot(options = {})
|
14
|
+
new(options).boot
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :env
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@foreground = options.fetch(:foreground, false)
|
21
|
+
@env = options[:env] || default_env
|
22
|
+
@applications = Hash.new { |h, k| h[k] = ApplicationManager.new(k, env) }
|
23
|
+
@pidfile = env.pidfile_path.open('a')
|
24
|
+
@mutex = Mutex.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def foreground?
|
28
|
+
@foreground
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(message)
|
32
|
+
env.log "[server] #{message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def boot
|
36
|
+
SpringStandalone.verify_environment
|
37
|
+
|
38
|
+
write_pidfile
|
39
|
+
set_pgid unless foreground?
|
40
|
+
ignore_signals unless foreground?
|
41
|
+
set_exit_hook
|
42
|
+
set_process_title
|
43
|
+
start_server
|
44
|
+
end
|
45
|
+
|
46
|
+
def start_server
|
47
|
+
server = UNIXServer.open(env.socket_name)
|
48
|
+
log "started on #{env.socket_name}"
|
49
|
+
loop { serve server.accept }
|
50
|
+
rescue Interrupt
|
51
|
+
end
|
52
|
+
|
53
|
+
def serve(client)
|
54
|
+
log "accepted client"
|
55
|
+
client.puts env.version
|
56
|
+
|
57
|
+
app_client = client.recv_io
|
58
|
+
command = JSON.load(client.read(client.gets.to_i))
|
59
|
+
|
60
|
+
args, default_app_env = command.values_at('args', 'default_app_env')
|
61
|
+
|
62
|
+
if SpringStandalone.command?(args.first)
|
63
|
+
log "running command #{args.first}"
|
64
|
+
client.puts
|
65
|
+
client.puts @applications[app_env_for(args, default_app_env)].run(app_client)
|
66
|
+
else
|
67
|
+
log "command not found #{args.first}"
|
68
|
+
client.close
|
69
|
+
end
|
70
|
+
rescue SocketError => e
|
71
|
+
raise e unless client.eof?
|
72
|
+
ensure
|
73
|
+
redirect_output
|
74
|
+
end
|
75
|
+
|
76
|
+
def app_env_for(args, default_app_env)
|
77
|
+
SpringStandalone.command(args.first).env(args.drop(1)) || default_app_env
|
78
|
+
end
|
79
|
+
|
80
|
+
# Boot the server into the process group of the current session.
|
81
|
+
# This will cause it to be automatically killed once the session
|
82
|
+
# ends (i.e. when the user closes their terminal).
|
83
|
+
def set_pgid
|
84
|
+
Process.setpgid(0, SID.pgid)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
|
88
|
+
# will kill the server/application.
|
89
|
+
def ignore_signals
|
90
|
+
IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
|
91
|
+
end
|
92
|
+
|
93
|
+
def set_exit_hook
|
94
|
+
server_pid = Process.pid
|
95
|
+
|
96
|
+
# We don't want this hook to run in any forks of the current process
|
97
|
+
at_exit { shutdown if Process.pid == server_pid }
|
98
|
+
end
|
99
|
+
|
100
|
+
def shutdown
|
101
|
+
log "shutting down"
|
102
|
+
|
103
|
+
[env.socket_path, env.pidfile_path].each do |path|
|
104
|
+
if path.exist?
|
105
|
+
path.unlink rescue nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
@applications.values.map { |a| SpringStandalone.failsafe_thread { a.stop } }.map(&:join)
|
110
|
+
end
|
111
|
+
|
112
|
+
def write_pidfile
|
113
|
+
if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
114
|
+
@pidfile.truncate(0)
|
115
|
+
@pidfile.write("#{Process.pid}\n")
|
116
|
+
@pidfile.fsync
|
117
|
+
else
|
118
|
+
exit 1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# We need to redirect STDOUT and STDERR, otherwise the server will
|
123
|
+
# keep the original FDs open which would break piping. (e.g.
|
124
|
+
# `spring rake -T | grep db` would hang forever because the server
|
125
|
+
# would keep the stdout FD open.)
|
126
|
+
def redirect_output
|
127
|
+
[STDOUT, STDERR].each { |stream| stream.reopen(env.log_file) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def set_process_title
|
131
|
+
ProcessTitleUpdater.run { |distance|
|
132
|
+
"spring server | #{env.app_name} | started #{distance} ago"
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def default_env
|
139
|
+
Env.new(log_file: default_log_file)
|
140
|
+
end
|
141
|
+
|
142
|
+
def default_log_file
|
143
|
+
if foreground? && !ENV["SPRING_LOG"]
|
144
|
+
$stdout
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
begin
|
2
|
+
# If rubygems is present, keep it out of the way when loading fiddle,
|
3
|
+
# otherwise if fiddle is not installed then rubygems will load all
|
4
|
+
# gemspecs in its (futile) search for fiddle, which is slow.
|
5
|
+
if respond_to?(:gem_original_require, true)
|
6
|
+
gem_original_require 'fiddle'
|
7
|
+
else
|
8
|
+
require 'fiddle'
|
9
|
+
end
|
10
|
+
rescue LoadError
|
11
|
+
end
|
12
|
+
|
13
|
+
module SpringStandalone
|
14
|
+
module SID
|
15
|
+
def self.fiddle_func
|
16
|
+
@fiddle_func ||= Fiddle::Function.new(
|
17
|
+
DL::Handle::DEFAULT['getsid'],
|
18
|
+
[Fiddle::TYPE_INT],
|
19
|
+
Fiddle::TYPE_INT
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.sid
|
24
|
+
@sid ||= begin
|
25
|
+
if Process.respond_to?(:getsid)
|
26
|
+
# Ruby 2
|
27
|
+
Process.getsid
|
28
|
+
elsif defined?(Fiddle) and defined?(DL)
|
29
|
+
# Ruby 1.9.3 compiled with libffi support
|
30
|
+
fiddle_func.call(0)
|
31
|
+
else
|
32
|
+
# last resort: shell out
|
33
|
+
`ps -p #{Process.pid} -o sess=`.to_i
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.pgid
|
39
|
+
Process.getpgid(sid)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spring_standalone/watcher/abstract"
|
2
|
+
require "spring_standalone/configuration"
|
3
|
+
|
4
|
+
module SpringStandalone
|
5
|
+
class << self
|
6
|
+
attr_accessor :watch_interval
|
7
|
+
attr_writer :watcher
|
8
|
+
attr_reader :watch_method
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.watch_method=(method)
|
12
|
+
if method.is_a?(Class)
|
13
|
+
@watch_method = method
|
14
|
+
else
|
15
|
+
require "spring_standalone/watcher/#{method}"
|
16
|
+
@watch_method = Watcher.const_get(method.to_s.gsub(/(^.|_.)/) { $1[-1].upcase })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
self.watch_interval = 0.2
|
21
|
+
self.watch_method = :polling
|
22
|
+
|
23
|
+
def self.watcher
|
24
|
+
@watcher ||= watch_method.new(SpringStandalone.application_root_path, watch_interval)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.watch(*items)
|
28
|
+
watcher.add(*items)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "set"
|
2
|
+
require "pathname"
|
3
|
+
require "mutex_m"
|
4
|
+
|
5
|
+
module SpringStandalone
|
6
|
+
module Watcher
|
7
|
+
# A user of a watcher can use IO.select to wait for changes:
|
8
|
+
#
|
9
|
+
# watcher = MyWatcher.new(root, latency)
|
10
|
+
# IO.select([watcher]) # watcher is running in background
|
11
|
+
# watcher.stale? # => true
|
12
|
+
class Abstract
|
13
|
+
include Mutex_m
|
14
|
+
|
15
|
+
attr_reader :files, :directories, :root, :latency
|
16
|
+
|
17
|
+
def initialize(root, latency)
|
18
|
+
super()
|
19
|
+
|
20
|
+
@root = File.realpath(root)
|
21
|
+
@latency = latency
|
22
|
+
@files = Set.new
|
23
|
+
@directories = Set.new
|
24
|
+
@stale = false
|
25
|
+
@listeners = []
|
26
|
+
|
27
|
+
@on_debug = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_debug(&block)
|
31
|
+
@on_debug = block
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug
|
35
|
+
@on_debug.call(yield) if @on_debug
|
36
|
+
end
|
37
|
+
|
38
|
+
def add(*items)
|
39
|
+
debug { "watcher: add: #{items.inspect}" }
|
40
|
+
|
41
|
+
items = items.flatten.map do |item|
|
42
|
+
item = Pathname.new(item)
|
43
|
+
|
44
|
+
if item.relative?
|
45
|
+
Pathname.new("#{root}/#{item}")
|
46
|
+
else
|
47
|
+
item
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
items = items.select do |item|
|
52
|
+
if item.symlink?
|
53
|
+
item.readlink.exist?.tap do |exists|
|
54
|
+
if !exists
|
55
|
+
debug { "add: ignoring dangling symlink: #{item.inspect} -> #{item.readlink.inspect}" }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
item.exist?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
synchronize {
|
64
|
+
items.each do |item|
|
65
|
+
if item.directory?
|
66
|
+
directories << item.realpath.to_s
|
67
|
+
else
|
68
|
+
begin
|
69
|
+
files << item.realpath.to_s
|
70
|
+
rescue Errno::ENOENT
|
71
|
+
# Race condition. Ignore symlinks whose target was removed
|
72
|
+
# since the check above, or are deeply chained.
|
73
|
+
debug { "add: ignoring now-dangling symlink: #{item.inspect} -> #{item.readlink.inspect}" }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
subjects_changed
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def stale?
|
83
|
+
@stale
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_stale(&block)
|
87
|
+
debug { "added listener: #{block.inspect}" }
|
88
|
+
@listeners << block
|
89
|
+
end
|
90
|
+
|
91
|
+
def mark_stale
|
92
|
+
return if stale?
|
93
|
+
@stale = true
|
94
|
+
debug { "marked stale, calling listeners: listeners=#{@listeners.inspect}" }
|
95
|
+
@listeners.each(&:call)
|
96
|
+
end
|
97
|
+
|
98
|
+
def restart
|
99
|
+
debug { "restarting" }
|
100
|
+
stop
|
101
|
+
start
|
102
|
+
end
|
103
|
+
|
104
|
+
def start
|
105
|
+
raise NotImplementedError
|
106
|
+
end
|
107
|
+
|
108
|
+
def stop
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
def subjects_changed
|
113
|
+
raise NotImplementedError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "spring_standalone/watcher/abstract"
|
2
|
+
|
3
|
+
module SpringStandalone
|
4
|
+
module Watcher
|
5
|
+
class Polling < Abstract
|
6
|
+
attr_reader :mtime
|
7
|
+
|
8
|
+
def initialize(root, latency)
|
9
|
+
super
|
10
|
+
@mtime = 0
|
11
|
+
@poller = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_stale
|
15
|
+
synchronize do
|
16
|
+
computed = compute_mtime
|
17
|
+
if mtime < computed
|
18
|
+
debug { "check_stale: mtime=#{mtime.inspect} < computed=#{computed.inspect}" }
|
19
|
+
mark_stale
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(*)
|
25
|
+
check_stale if @poller
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
debug { "start: poller=#{@poller.inspect}" }
|
31
|
+
unless @poller
|
32
|
+
@poller = Thread.new {
|
33
|
+
Thread.current.abort_on_exception = true
|
34
|
+
|
35
|
+
begin
|
36
|
+
until stale?
|
37
|
+
Kernel.sleep latency
|
38
|
+
check_stale
|
39
|
+
end
|
40
|
+
rescue Exception => e
|
41
|
+
debug do
|
42
|
+
"poller: aborted: #{e.class}: #{e}\n #{e.backtrace.join("\n ")}"
|
43
|
+
end
|
44
|
+
raise
|
45
|
+
ensure
|
46
|
+
@poller = nil
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop
|
53
|
+
debug { "stopping poller: #{@poller.inspect}" }
|
54
|
+
if @poller
|
55
|
+
@poller.kill
|
56
|
+
@poller = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def running?
|
61
|
+
@poller && @poller.alive?
|
62
|
+
end
|
63
|
+
|
64
|
+
def subjects_changed
|
65
|
+
computed = compute_mtime
|
66
|
+
debug { "subjects_changed: mtime #{@mtime} -> #{computed}" }
|
67
|
+
@mtime = computed
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def compute_mtime
|
73
|
+
expanded_files.map do |f|
|
74
|
+
# Get the mtime of symlink targets. Ignore dangling symlinks.
|
75
|
+
if File.symlink?(f)
|
76
|
+
begin
|
77
|
+
File.mtime(f)
|
78
|
+
rescue Errno::ENOENT
|
79
|
+
0
|
80
|
+
end
|
81
|
+
# If a file no longer exists, treat it as changed.
|
82
|
+
else
|
83
|
+
begin
|
84
|
+
File.mtime(f)
|
85
|
+
rescue Errno::ENOENT
|
86
|
+
debug { "compute_mtime: no longer exists: #{f}" }
|
87
|
+
Float::MAX
|
88
|
+
end
|
89
|
+
end.to_f
|
90
|
+
end.max || 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def expanded_files
|
94
|
+
files + Dir["{#{directories.map { |d| "#{d}/**/*" }.join(",")}}"]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|