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.
@@ -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,16 @@
1
+ module Expedite
2
+ module Cli
3
+ class Server
4
+ def run(args)
5
+ require 'expedite/server'
6
+
7
+ server = Expedite::Server.new(foreground: true)
8
+ server.boot
9
+ end
10
+
11
+ def summary
12
+ 'Starts the expedite server'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Expedite
2
+ module Cli
3
+ class Stop
4
+ def run(args)
5
+ require 'expedite/server'
6
+
7
+ server = Expedite::Server.new
8
+ server.stop
9
+ end
10
+
11
+ def summary
12
+ 'Stops the expedite server'
13
+ end
14
+ end
15
+ end
16
+ 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