expedite 0.0.2 → 0.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.
@@ -1,187 +0,0 @@
1
- # Based on https://github.com/rails/spring/blob/master/lib/spring/application_manager.rb
2
-
3
- require 'bundler'
4
- require 'expedite/failsafe_thread'
5
- require 'expedite/send_json'
6
- require 'expedite/variants'
7
-
8
- module Expedite
9
- class ApplicationManager
10
- include SendJson
11
-
12
- attr_reader :pid, :child, :name, :env, :status, :variant
13
-
14
- def initialize(name, env)
15
- @name = name
16
- @env = env
17
- @mutex = Mutex.new
18
- @state = :running
19
- @pid = nil
20
-
21
- @variant = Expedite::Variants.lookup(@name)
22
- end
23
-
24
- def log(message)
25
- env.log "[application_manager:#{name}] #{message}"
26
- end
27
-
28
- # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
29
- # line which messes with backtraces in e.g. rspec
30
- def synchronize
31
- @mutex.lock
32
- yield
33
- ensure
34
- @mutex.unlock
35
- end
36
-
37
- def start
38
- start_child
39
- end
40
-
41
- def restart
42
- return if @state == :stopping
43
- start_child(true)
44
- end
45
-
46
- def alive?
47
- @pid
48
- end
49
-
50
- def with_child
51
- synchronize do
52
- if alive?
53
- begin
54
- yield child
55
- rescue Errno::ECONNRESET, Errno::EPIPE
56
- # The child has died but has not been collected by the wait thread yet,
57
- # so start a new child and try again.
58
- log "child dead; starting"
59
- start
60
- yield child
61
- end
62
- else
63
- log "child not running; starting"
64
- start
65
- yield child
66
- end
67
- end
68
- end
69
-
70
- # Returns the pid of the process running the command, or nil if the application process died.
71
- def run(client)
72
- @client = client
73
- with_child do |child|
74
- child.send_io client
75
- child.gets or raise Errno::EPIPE
76
- end
77
-
78
- pid = child.gets.to_i
79
-
80
- unless pid.zero?
81
- log "got worker pid #{pid}"
82
- pid
83
- end
84
- rescue Errno::ECONNRESET, Errno::EPIPE => e
85
- log "#{e} while reading from child; returning no pid"
86
- nil
87
- ensure
88
- client.close
89
- end
90
-
91
- def stop
92
- log "stopping"
93
- @state = :stopping
94
-
95
- if pid
96
- Process.kill('TERM', pid)
97
- Process.wait(pid)
98
- end
99
- rescue Errno::ESRCH, Errno::ECHILD
100
- # Don't care
101
- end
102
-
103
- def keep_alive
104
- variant.keep_alive
105
- end
106
-
107
- def parent
108
- variant.parent
109
- end
110
-
111
- private
112
-
113
- def start_child(preload = false)
114
- if parent
115
- fork_child(preload)
116
- else
117
- spawn_child(preload)
118
- end
119
- end
120
-
121
- def fork_child(preload = false)
122
- @child, child_socket = UNIXSocket.pair
123
-
124
- # Compose command
125
- wr, rd = UNIXSocket.pair
126
- wr.send_io STDOUT
127
- wr.send_io STDERR
128
- wr.send_io STDIN
129
-
130
- send_json wr, 'args' => ['expedite/boot', name], 'env' => {}
131
- wr.send_io child_socket
132
- wr.send_io env.log_file
133
- wr.close
134
-
135
- @pid = env.applications[parent].run(rd)
136
-
137
- start_wait_thread(pid, child) if child.gets
138
- child_socket.close
139
- end
140
-
141
- def spawn_child(preload = false)
142
- @child, child_socket = UNIXSocket.pair
143
-
144
- Bundler.with_original_env do
145
- bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
146
- @pid = Process.spawn(
147
- {
148
- "EXPEDITE_VARIANT" => name,
149
- "EXPEDITE_ROOT" => env.root,
150
- },
151
- "ruby",
152
- *(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
153
- "-I", File.expand_path("../..", __FILE__),
154
- "-e", "require 'expedite/application/boot'",
155
- 3 => child_socket,
156
- 4 => env.log_file,
157
- )
158
- end
159
-
160
- start_wait_thread(pid, child) if child.gets
161
- child_socket.close
162
- end
163
-
164
- def start_wait_thread(pid, child)
165
- Process.detach(pid)
166
-
167
- Expedite.failsafe_thread do
168
- # The recv can raise an ECONNRESET, killing the thread, but that's ok
169
- # as if it does we're no longer interested in the child
170
- loop do
171
- IO.select([child])
172
- break if child.recv(1, Socket::MSG_PEEK).empty?
173
- sleep 0.01
174
- end
175
-
176
- log "child #{pid} shutdown"
177
-
178
- synchronize {
179
- if @pid == pid
180
- @pid = nil
181
- restart if keep_alive
182
- end
183
- }
184
- end
185
- end
186
- end
187
- end
@@ -1,235 +0,0 @@
1
-
2
- # Based on https://github.com/rails/spring/blob/master/lib/spring/client/run.rb
3
-
4
- require 'bundler'
5
- require 'json'
6
- require 'rbconfig'
7
- require 'socket'
8
-
9
- require 'expedite/env'
10
- require 'expedite/errors'
11
- require 'expedite/send_json'
12
-
13
- module Expedite
14
- class Client
15
- include SendJson
16
-
17
- FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
18
- CONNECT_TIMEOUT = 1
19
- BOOT_TIMEOUT = 20
20
-
21
- attr_reader :args, :env, :variant
22
- attr_reader :server
23
-
24
- def initialize(env: nil, variant: nil)
25
- @env = env || Env.new
26
- @variant = variant
27
-
28
- @signal_queue = []
29
- @server_booted = false
30
- end
31
-
32
- def log(message)
33
- env.log "[client] #{message}"
34
- end
35
-
36
- def connect
37
- @server = UNIXSocket.open(env.socket_path)
38
- end
39
-
40
- def call(*args)
41
- @args = args
42
- begin
43
- connect
44
- rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
45
- cold_run
46
- else
47
- warm_run
48
- end
49
- ensure
50
- server.close if server
51
- end
52
-
53
- def warm_run
54
- run
55
- rescue CommandNotFound
56
- raise
57
- require "expedite/command"
58
-
59
- if Expedite.command(args.first)
60
- # Command installed since Expedite started
61
- stop_server
62
- cold_run
63
- else
64
- raise
65
- end
66
- end
67
-
68
- def cold_run
69
- boot_server
70
- connect
71
- run
72
- end
73
-
74
- def run
75
- verify_server_version
76
-
77
- application, client = UNIXSocket.pair
78
-
79
- queue_signals
80
- connect_to_application(client)
81
- run_command(client, application)
82
- rescue Errno::ECONNRESET
83
- exit 1
84
- end
85
-
86
- def boot_server
87
- env.socket_path.unlink if env.socket_path.exist?
88
-
89
- pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
90
- timeout = Time.now + BOOT_TIMEOUT
91
-
92
- @server_booted = true
93
-
94
- until env.socket_path.exist?
95
- _, status = Process.waitpid2(pid, Process::WNOHANG)
96
-
97
- if status
98
- # Server did not start
99
- raise ArgumentError, "Server exited: #{status.exitstatus}"
100
- elsif Time.now > timeout
101
- $stderr.puts "Starting Expedite server with `#{env.server_command}` " \
102
- "timed out after #{BOOT_TIMEOUT} seconds"
103
- exit 1
104
- end
105
-
106
- sleep 0.1
107
- end
108
- end
109
-
110
- def server_booted?
111
- @server_booted
112
- end
113
-
114
- def gem_env
115
- bundle = Bundler.bundle_path.to_s
116
- paths = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
117
-
118
- {
119
- "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
120
- "GEM_HOME" => bundle
121
- }
122
- end
123
-
124
- def stop_server
125
- server.close
126
- @server = nil
127
- env.stop
128
- end
129
-
130
- def verify_server_version
131
- server_version = server.gets.chomp
132
- if server_version != env.version
133
- $stderr.puts "There is a version mismatch between the Expedite client " \
134
- "(#{env.version}) and the server (#{server_version})."
135
-
136
- if server_booted?
137
- $stderr.puts "We already tried to reboot the server, but the mismatch is still present."
138
- exit 1
139
- else
140
- $stderr.puts "Restarting to resolve."
141
- stop_server
142
- cold_run
143
- end
144
- end
145
- end
146
-
147
- def connect_to_application(client)
148
- server.send_io client
149
-
150
- send_json server, "args" => args, "variant" => variant
151
-
152
- if IO.select([server], [], [], CONNECT_TIMEOUT)
153
- server.gets or raise CommandNotFound
154
- else
155
- raise "Error connecting to Expedite server"
156
- end
157
- end
158
-
159
- def run_command(client, application)
160
- log "sending command"
161
-
162
- application.send_io STDOUT
163
- application.send_io STDERR
164
- application.send_io STDIN
165
-
166
- send_json application, "args" => args, "env" => ENV.to_hash
167
-
168
- pid = server.gets
169
- pid = pid.chomp if pid
170
-
171
- # We must not close the client socket until we are sure that the application has
172
- # received the FD. Otherwise the FD can end up getting closed while it's in the server
173
- # socket buffer on OS X. This doesn't happen on Linux.
174
- client.close
175
-
176
- if pid && !pid.empty?
177
- log "got pid: #{pid}"
178
-
179
- suspend_resume_on_tstp_cont(pid)
180
-
181
- forward_signals(application)
182
- status = application.read.to_i
183
-
184
- log "got exit status #{status}"
185
-
186
- exit status
187
- else
188
- log "got no pid"
189
- exit 1
190
- end
191
- ensure
192
- application.close
193
- end
194
-
195
- def queue_signals
196
- FORWARDED_SIGNALS.each do |sig|
197
- trap(sig) { @signal_queue << sig }
198
- end
199
- end
200
-
201
- def suspend_resume_on_tstp_cont(pid)
202
- trap("TSTP") {
203
- log "suspended"
204
- Process.kill("STOP", pid.to_i)
205
- Process.kill("STOP", Process.pid)
206
- }
207
- trap("CONT") {
208
- log "resumed"
209
- Process.kill("CONT", pid.to_i)
210
- }
211
- end
212
-
213
- def forward_signals(application)
214
- @signal_queue.each { |sig| kill sig, application }
215
-
216
- FORWARDED_SIGNALS.each do |sig|
217
- trap(sig) { forward_signal sig, application }
218
- end
219
- end
220
-
221
- def forward_signal(sig, application)
222
- if kill(sig, application) != 0
223
- # If the application process is gone, then don't block the
224
- # signal on this process.
225
- trap(sig, 'DEFAULT')
226
- Process.kill(sig, Process.pid)
227
- end
228
- end
229
-
230
- def kill(sig, application)
231
- application.puts(sig)
232
- application.gets.to_i
233
- end
234
- end
235
- end
@@ -1,49 +0,0 @@
1
- module Expedite
2
- module Command
3
- class Info
4
- def call
5
- puts "1"
6
- client.puts
7
- puts "2"
8
- unix_socket = UNIXSocket.for_fd(app_client.fileno)
9
- _stdout, stderr, _stdin = streams = 3.times.map do
10
- puts "4"
11
- unix_socket.recv_io
12
- end
13
- puts "5"
14
- client.puts Process.pid
15
- puts "6"
16
- unix_socket.puts 11 #application_pids.to_json
17
- puts "7"
18
- unix_socket.puts 10
19
- puts "8"
20
- unix_socket.close
21
- client.close
22
-
23
- variant = ARGV[0]
24
-
25
- require "expedite/application"
26
-
27
- Expedite::Application.new(
28
- variant: variant,
29
- manager: UNIXSocket.for_fd(@child_socket.fileno),
30
- env: Expedite::Env.new(
31
- root: ENV['EXPEDITE_ROOT'],
32
- log_file: @log_file,
33
- ),
34
- ).boot
35
- end
36
-
37
- def setup(client)
38
- @child_socket = client.recv_io
39
- @log_file = client.recv_io
40
- end
41
-
42
- def runs_in
43
- :server
44
- end
45
- end
46
- end
47
- end
48
-
49
- Expedite::Commands.register("expedite/info", Expedite::Command::Info)
@@ -1,10 +0,0 @@
1
- module Expedite
2
- module SendJson
3
- def send_json(socket, data)
4
- data = JSON.dump(data)
5
-
6
- socket.puts data.bytesize
7
- socket.write data
8
- end
9
- end
10
- end
@@ -1,188 +0,0 @@
1
- # Based on https://github.com/rails/spring/blob/master/lib/spring/server.rb
2
- require 'json'
3
- require 'socket'
4
- require "expedite/application_manager"
5
- require "expedite/env"
6
- require "expedite/signals"
7
-
8
- module Expedite
9
- class Server
10
- include Signals
11
-
12
- def self.boot(options = {})
13
- new(options).boot
14
- end
15
-
16
- attr_reader :env
17
-
18
- def initialize(foreground: true, env: nil)
19
- @foreground = foreground
20
- @env = env || default_env
21
- @pidfile = @env.pidfile_path.open('a')
22
- @mutex = Mutex.new
23
- end
24
-
25
- def foreground?
26
- @foreground
27
- end
28
-
29
- def log(message)
30
- env.log "[server] #{message}"
31
- end
32
-
33
- def boot
34
- env.load_helper
35
-
36
- write_pidfile
37
- set_pgid unless foreground?
38
- ignore_signals unless foreground?
39
- set_exit_hook
40
- set_process_title
41
- start_server
42
- exit 0
43
- end
44
-
45
- def pid
46
- @env.pidfile_path.read.to_i
47
- rescue Errno::ENOENT
48
- nil
49
- end
50
-
51
- def running?
52
- pidfile = @env.pidfile_path.open('r+')
53
- !pidfile.flock(File::LOCK_EX | File::LOCK_NB)
54
- rescue Errno::ENOENT
55
- false
56
- ensure
57
- if pidfile
58
- pidfile.flock(File::LOCK_UN)
59
- pidfile.close
60
- end
61
- end
62
-
63
- # timeout: Defaults to 2 seconds
64
- def stop
65
- if running?
66
- timeout = Time.now + @env.graceful_termination_timeout
67
- kill 'TERM'
68
- sleep 0.1 until !running? || Time.now >= timeout
69
-
70
- if running?
71
- kill 'KILL'
72
- :killed
73
- else
74
- :stopped
75
- end
76
- else
77
- :not_running
78
- end
79
- end
80
-
81
- def kill(sig)
82
- pid = self.pid
83
- Process.kill(sig, pid) if pid
84
- rescue Errno::ESRCH
85
- # already dead
86
- end
87
-
88
- def start_server
89
- server = UNIXServer.open(env.socket_path)
90
- log "started on #{env.socket_path}"
91
- loop { serve server.accept }
92
- rescue Interrupt
93
- end
94
-
95
- def serve(client)
96
- log "accepted client"
97
- client.puts env.version
98
-
99
- app_client = client.recv_io
100
- command = JSON.load(client.read(client.gets.to_i))
101
-
102
- args, variant = command.values_at('args', 'variant')
103
- cmd = args.first
104
- if true #Expedite.command(cmd)
105
- log "running command #{cmd}"
106
- client.puts
107
-
108
- target = env.applications[variant]
109
- client.puts target.run(app_client)
110
- else
111
- log "command not found #{cmd}"
112
- client.close
113
- end
114
- rescue SocketError => e
115
- raise e unless client.eof?
116
- ensure
117
- redirect_output
118
- end
119
-
120
- # Boot the server into the process group of the current session.
121
- # This will cause it to be automatically killed once the session
122
- # ends (i.e. when the user closes their terminal).
123
- def set_pgid
124
- # Process.setpgid(0, SID.pgid)
125
- end
126
-
127
- # Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
128
- # will kill the server/application.
129
- def ignore_signals
130
- IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
131
- end
132
-
133
- def set_exit_hook
134
- server_pid = Process.pid
135
-
136
- # We don't want this hook to run in any forks of the current process
137
- at_exit { shutdown if Process.pid == server_pid }
138
- end
139
-
140
- def shutdown
141
- log "shutting down"
142
-
143
- [env.socket_path, env.pidfile_path].each do |path|
144
- if path.exist?
145
- path.unlink rescue nil
146
- end
147
- end
148
-
149
- env.applications.values.map { |a| Expedite.failsafe_thread { a.stop } }.map(&:join)
150
- end
151
-
152
- def write_pidfile
153
- if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
154
- @pidfile.truncate(0)
155
- @pidfile.write("#{Process.pid}\n")
156
- @pidfile.fsync
157
- else
158
- raise "Failed to lock #{@env.pidfile_path}"
159
- end
160
- end
161
-
162
- # We need to redirect STDOUT and STDERR, otherwise the server will
163
- # keep the original FDs open which would break piping. (e.g.
164
- # `spring rake -T | grep db` would hang forever because the server
165
- # would keep the stdout FD open.)
166
- def redirect_output
167
- [STDOUT, STDERR].each { |stream| stream.reopen(env.log_file) }
168
- end
169
-
170
- def set_process_title
171
- $0 = "expedite server | #{env.app_name}"
172
- end
173
-
174
- private
175
-
176
- def default_env
177
- Env.new(log_file: default_log_file)
178
- end
179
-
180
- def default_log_file
181
- if foreground? && !ENV["SPRING_LOG"]
182
- $stdout
183
- else
184
- nil
185
- end
186
- end
187
- end
188
- end