spring_standalone 0.1.13

Sign up to get free protection for your applications and to get access to all the features.
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,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,3 @@
1
+ module SpringStandalone
2
+ VERSION = "0.1.13"
3
+ 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