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,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
@@ -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,3 @@
1
+ module Spring
2
+ VERSION = "2.1.1"
3
+ 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