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.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +419 -0
- data/bin/spring +49 -0
- data/lib/spring/application.rb +384 -0
- data/lib/spring/application/boot.rb +19 -0
- data/lib/spring/application_manager.rb +141 -0
- data/lib/spring/binstub.rb +13 -0
- data/lib/spring/boot.rb +10 -0
- data/lib/spring/client.rb +48 -0
- data/lib/spring/client/binstub.rb +197 -0
- data/lib/spring/client/command.rb +18 -0
- data/lib/spring/client/help.rb +62 -0
- data/lib/spring/client/rails.rb +34 -0
- data/lib/spring/client/run.rb +232 -0
- data/lib/spring/client/server.rb +18 -0
- data/lib/spring/client/status.rb +30 -0
- data/lib/spring/client/stop.rb +22 -0
- data/lib/spring/client/version.rb +11 -0
- data/lib/spring/command_wrapper.rb +82 -0
- data/lib/spring/commands.rb +50 -0
- data/lib/spring/commands/rails.rb +112 -0
- data/lib/spring/commands/rake.rb +30 -0
- data/lib/spring/configuration.rb +58 -0
- data/lib/spring/env.rb +116 -0
- data/lib/spring/errors.rb +36 -0
- data/lib/spring/failsafe_thread.rb +14 -0
- data/lib/spring/json.rb +626 -0
- data/lib/spring/process_title_updater.rb +65 -0
- data/lib/spring/server.rb +150 -0
- data/lib/spring/sid.rb +42 -0
- data/lib/spring/version.rb +3 -0
- data/lib/spring/watcher.rb +30 -0
- data/lib/spring/watcher/abstract.rb +117 -0
- data/lib/spring/watcher/polling.rb +98 -0
- metadata +121 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
module Spring
|
2
|
+
# Yes, I know this reimplements a bunch of stuff in Active Support, but
|
3
|
+
# I don't want the Spring 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
|
+
Spring.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 Spring
|
2
|
+
ORIGINAL_ENV = ENV.to_hash
|
3
|
+
end
|
4
|
+
|
5
|
+
require "spring/boot"
|
6
|
+
require "spring/application_manager"
|
7
|
+
|
8
|
+
# Must be last, as it requires bundler/setup, which alters the load path
|
9
|
+
require "spring/commands"
|
10
|
+
|
11
|
+
module Spring
|
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
|
+
Spring.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_rails_env = command.values_at('args', 'default_rails_env')
|
61
|
+
|
62
|
+
if Spring.command?(args.first)
|
63
|
+
log "running command #{args.first}"
|
64
|
+
client.puts
|
65
|
+
client.puts @applications[rails_env_for(args, default_rails_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 rails_env_for(args, default_rails_env)
|
77
|
+
Spring.command(args.first).env(args.drop(1)) || default_rails_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| Spring.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
|
data/lib/spring/sid.rb
ADDED
@@ -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 Spring
|
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/watcher/abstract"
|
2
|
+
require "spring/configuration"
|
3
|
+
|
4
|
+
module Spring
|
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/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(Spring.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 Spring
|
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/watcher/abstract"
|
2
|
+
|
3
|
+
module Spring
|
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
|