expedite 0.0.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/README.md +43 -0
- data/bin/expedite +7 -0
- data/lib/expedite.rb +19 -0
- data/lib/expedite/application.rb +316 -0
- data/lib/expedite/application/boot.rb +9 -0
- data/lib/expedite/application_manager.rb +180 -0
- data/lib/expedite/cli.rb +42 -0
- data/lib/expedite/cli/server.rb +16 -0
- data/lib/expedite/cli/stop.rb +16 -0
- data/lib/expedite/client.rb +235 -0
- data/lib/expedite/command/basic.rb +20 -0
- data/lib/expedite/command/boot.rb +27 -0
- data/lib/expedite/command/info.rb +47 -0
- data/lib/expedite/commands.rb +62 -0
- data/lib/expedite/env.rb +60 -0
- data/lib/expedite/errors.rb +8 -0
- data/lib/expedite/failsafe_thread.rb +15 -0
- data/lib/expedite/load_helper.rb +11 -0
- data/lib/expedite/send_json.rb +10 -0
- data/lib/expedite/server.rb +190 -0
- data/lib/expedite/signals.rb +5 -0
- data/lib/expedite/variants.rb +89 -0
- data/lib/expedite/version.rb +3 -0
- metadata +66 -0
data/lib/expedite/cli.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'expedite/cli/server'
|
|
2
|
+
require 'expedite/cli/stop'
|
|
3
|
+
|
|
4
|
+
module Expedite
|
|
5
|
+
module Cli
|
|
6
|
+
class Help
|
|
7
|
+
def run(args)
|
|
8
|
+
puts "Expected: <command>"
|
|
9
|
+
puts
|
|
10
|
+
puts "Commands:"
|
|
11
|
+
|
|
12
|
+
cmds = Expedite::Cli::COMMANDS
|
|
13
|
+
cmds.keys.sort!.each do |cmd|
|
|
14
|
+
c = cmds[cmd].new
|
|
15
|
+
puts " #{cmd}: #{c.summary}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def summary
|
|
20
|
+
'Prints usage documentation'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
COMMANDS = {
|
|
27
|
+
'help' => Cli::Help,
|
|
28
|
+
'server' => Cli::Server,
|
|
29
|
+
'stop' => Cli::Stop,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def run(args)
|
|
33
|
+
command(args.first).run(args[1..])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def command(cmd)
|
|
37
|
+
klass = COMMANDS[cmd]
|
|
38
|
+
raise NotImplementedError, "Unknown command '#{cmd}'" if klass.nil?
|
|
39
|
+
klass.new
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
module Expedite
|
|
3
|
+
module Command
|
|
4
|
+
class Basic
|
|
5
|
+
attr_reader :runs_in
|
|
6
|
+
|
|
7
|
+
def initialize(runs_in: :application, &block)
|
|
8
|
+
@runs_in = runs_in
|
|
9
|
+
@block = block
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(*args)
|
|
13
|
+
@block.call(*args)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def setup(_)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Expedite
|
|
2
|
+
module Command
|
|
3
|
+
class Boot
|
|
4
|
+
def call
|
|
5
|
+
variant = ARGV[0]
|
|
6
|
+
|
|
7
|
+
require "expedite/application"
|
|
8
|
+
|
|
9
|
+
Expedite::Application.new(
|
|
10
|
+
variant,
|
|
11
|
+
UNIXSocket.for_fd(@child_socket.fileno),
|
|
12
|
+
{},
|
|
13
|
+
Expedite::Env.new(log_file: @log_file)
|
|
14
|
+
).boot
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def setup(client)
|
|
18
|
+
@child_socket = client.recv_io
|
|
19
|
+
@log_file = client.recv_io
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def runs_in
|
|
23
|
+
:server
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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,
|
|
29
|
+
UNIXSocket.for_fd(@child_socket.fileno),
|
|
30
|
+
{},
|
|
31
|
+
Expedite::Env.new(log_file: @log_file)
|
|
32
|
+
).boot
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def setup(client)
|
|
36
|
+
@child_socket = client.recv_io
|
|
37
|
+
@log_file = client.recv_io
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def runs_in
|
|
41
|
+
:server
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Expedite::Commands.register("expedite/info", Expedite::Command::Info)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'expedite/command/basic'
|
|
2
|
+
require 'expedite/command/boot'
|
|
3
|
+
|
|
4
|
+
module Expedite
|
|
5
|
+
class Commands
|
|
6
|
+
def self.current
|
|
7
|
+
@current ||= Commands.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.lookup(name)
|
|
11
|
+
self.current.lookup(name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Registers a command. If multiple commands are registered with the
|
|
16
|
+
# same name, the last one takes precedence.
|
|
17
|
+
#
|
|
18
|
+
# [name] Name of the command. Expedite internal commands are prefixed
|
|
19
|
+
# with "expedite/"
|
|
20
|
+
# [klass_or_nil] Class of the command. If omitted, will default to
|
|
21
|
+
# Expedite::Command::Basic.
|
|
22
|
+
# [named_options] Command options. Passed to the initializer.
|
|
23
|
+
def self.register(name, klass_or_nil = nil, **named_options, &block)
|
|
24
|
+
self.current.register(name, klass_or_nil, **named_options, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Restores existing registrations to default
|
|
29
|
+
def self.reset
|
|
30
|
+
self.current.reset
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
reset
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lookup(name)
|
|
38
|
+
ret = @registrations[name]
|
|
39
|
+
raise NotImplementedError, "Command #{name.inspect} not found" if ret.nil?
|
|
40
|
+
ret
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register(name, klass_or_nil = nil, **named_options, &block)
|
|
44
|
+
cmd = if klass_or_nil.nil?
|
|
45
|
+
Command::Basic.new(**named_options, &block)
|
|
46
|
+
else
|
|
47
|
+
klass_or_nil.new(**named_options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@registrations[name] = cmd
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset
|
|
54
|
+
@registrations = {}
|
|
55
|
+
|
|
56
|
+
# Default registrations
|
|
57
|
+
register("expedite/boot", Expedite::Command::Boot)
|
|
58
|
+
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|