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/env.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'expedite/version'
|
|
4
|
+
|
|
5
|
+
module Expedite
|
|
6
|
+
class Env
|
|
7
|
+
attr_accessor :root
|
|
8
|
+
attr_accessor :application_id, :app_name, :log_file
|
|
9
|
+
attr_reader :applications
|
|
10
|
+
|
|
11
|
+
def initialize(root: nil, app_name: nil, log_file: nil)
|
|
12
|
+
@root = root || Dir.pwd
|
|
13
|
+
@app_name = app_name || File.basename(@root)
|
|
14
|
+
@log_file = log_file || File.open(File::NULL, "a")
|
|
15
|
+
@tmp_path = nil
|
|
16
|
+
|
|
17
|
+
@application_id = Digest::SHA1.hexdigest(@root)
|
|
18
|
+
|
|
19
|
+
env = self
|
|
20
|
+
@applications = Hash.new do |h, k|
|
|
21
|
+
h[k] = ApplicationManager.new(k, env)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def version
|
|
26
|
+
Expedite::VERSION
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tmp_path
|
|
30
|
+
return @tmp_path unless @tmp_path.nil?
|
|
31
|
+
|
|
32
|
+
require "tmpdir"
|
|
33
|
+
path = Pathname.new(File.join(Dir.tmpdir, "expedite-#{Process.uid}"))
|
|
34
|
+
require "fileutils"
|
|
35
|
+
FileUtils.mkdir_p(path) unless path.exist?
|
|
36
|
+
@tmp_path = path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def socket_path
|
|
40
|
+
tmp_path.join(application_id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pidfile_path
|
|
44
|
+
tmp_path.join("#{application_id}.pid")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def log(message)
|
|
48
|
+
log_file.puts "[#{Time.now}] [#{Process.pid}] #{message}"
|
|
49
|
+
log_file.flush
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def server_command
|
|
53
|
+
"#{File.expand_path("../../../bin/expedite", __FILE__)} server --background"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def graceful_termination_timeout
|
|
57
|
+
2
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
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/load_helper'
|
|
7
|
+
require "expedite/signals"
|
|
8
|
+
|
|
9
|
+
module Expedite
|
|
10
|
+
class Server
|
|
11
|
+
include LoadHelper
|
|
12
|
+
include Signals
|
|
13
|
+
|
|
14
|
+
def self.boot(options = {})
|
|
15
|
+
new(options).boot
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :env
|
|
19
|
+
|
|
20
|
+
def initialize(foreground: true, env: nil)
|
|
21
|
+
@foreground = foreground
|
|
22
|
+
@env = env || default_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
|
+
load_helper
|
|
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
|
+
exit 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pid
|
|
48
|
+
@env.pidfile_path.read.to_i
|
|
49
|
+
rescue Errno::ENOENT
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def running?
|
|
54
|
+
pidfile = @env.pidfile_path.open('r+')
|
|
55
|
+
!pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
|
56
|
+
rescue Errno::ENOENT
|
|
57
|
+
false
|
|
58
|
+
ensure
|
|
59
|
+
if pidfile
|
|
60
|
+
pidfile.flock(File::LOCK_UN)
|
|
61
|
+
pidfile.close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# timeout: Defaults to 2 seconds
|
|
66
|
+
def stop
|
|
67
|
+
if running?
|
|
68
|
+
timeout = Time.now + @env.graceful_termination_timeout
|
|
69
|
+
kill 'TERM'
|
|
70
|
+
sleep 0.1 until !running? || Time.now >= timeout
|
|
71
|
+
|
|
72
|
+
if running?
|
|
73
|
+
kill 'KILL'
|
|
74
|
+
:killed
|
|
75
|
+
else
|
|
76
|
+
:stopped
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
:not_running
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def kill(sig)
|
|
84
|
+
pid = self.pid
|
|
85
|
+
Process.kill(sig, pid) if pid
|
|
86
|
+
rescue Errno::ESRCH
|
|
87
|
+
# already dead
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def start_server
|
|
91
|
+
server = UNIXServer.open(env.socket_path)
|
|
92
|
+
log "started on #{env.socket_path}"
|
|
93
|
+
loop { serve server.accept }
|
|
94
|
+
rescue Interrupt
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def serve(client)
|
|
98
|
+
log "accepted client"
|
|
99
|
+
client.puts env.version
|
|
100
|
+
|
|
101
|
+
app_client = client.recv_io
|
|
102
|
+
command = JSON.load(client.read(client.gets.to_i))
|
|
103
|
+
|
|
104
|
+
args, variant = command.values_at('args', 'variant')
|
|
105
|
+
cmd = args.first
|
|
106
|
+
if true #Expedite.command(cmd)
|
|
107
|
+
log "running command #{cmd}"
|
|
108
|
+
client.puts
|
|
109
|
+
|
|
110
|
+
target = env.applications[variant]
|
|
111
|
+
client.puts target.run(app_client)
|
|
112
|
+
else
|
|
113
|
+
log "command not found #{cmd}"
|
|
114
|
+
client.close
|
|
115
|
+
end
|
|
116
|
+
rescue SocketError => e
|
|
117
|
+
raise e unless client.eof?
|
|
118
|
+
ensure
|
|
119
|
+
redirect_output
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Boot the server into the process group of the current session.
|
|
123
|
+
# This will cause it to be automatically killed once the session
|
|
124
|
+
# ends (i.e. when the user closes their terminal).
|
|
125
|
+
def set_pgid
|
|
126
|
+
# Process.setpgid(0, SID.pgid)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
|
|
130
|
+
# will kill the server/application.
|
|
131
|
+
def ignore_signals
|
|
132
|
+
IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def set_exit_hook
|
|
136
|
+
server_pid = Process.pid
|
|
137
|
+
|
|
138
|
+
# We don't want this hook to run in any forks of the current process
|
|
139
|
+
at_exit { shutdown if Process.pid == server_pid }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def shutdown
|
|
143
|
+
log "shutting down"
|
|
144
|
+
|
|
145
|
+
[env.socket_path, env.pidfile_path].each do |path|
|
|
146
|
+
if path.exist?
|
|
147
|
+
path.unlink rescue nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
env.applications.values.map { |a| Expedite.failsafe_thread { a.stop } }.map(&:join)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def write_pidfile
|
|
155
|
+
if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
|
156
|
+
@pidfile.truncate(0)
|
|
157
|
+
@pidfile.write("#{Process.pid}\n")
|
|
158
|
+
@pidfile.fsync
|
|
159
|
+
else
|
|
160
|
+
raise "Failed to lock #{@env.pidfile_path}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# We need to redirect STDOUT and STDERR, otherwise the server will
|
|
165
|
+
# keep the original FDs open which would break piping. (e.g.
|
|
166
|
+
# `spring rake -T | grep db` would hang forever because the server
|
|
167
|
+
# would keep the stdout FD open.)
|
|
168
|
+
def redirect_output
|
|
169
|
+
[STDOUT, STDERR].each { |stream| stream.reopen(env.log_file) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def set_process_title
|
|
173
|
+
$0 = "expedite server | #{env.app_name}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def default_env
|
|
179
|
+
Env.new(log_file: default_log_file)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def default_log_file
|
|
183
|
+
if foreground? && !ENV["SPRING_LOG"]
|
|
184
|
+
$stdout
|
|
185
|
+
else
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
module Expedite
|
|
3
|
+
class Variant
|
|
4
|
+
attr_accessor :parent
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# [parent] Name of parent variant.
|
|
8
|
+
# [after_fork] Block is executed when variant is first preloaded.
|
|
9
|
+
def initialize(parent: nil, &after_fork)
|
|
10
|
+
@parent = parent
|
|
11
|
+
@after_fork_proc = after_fork
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Called when variant if first preloaded. This version calls the after_fork
|
|
16
|
+
# block provided in the initializer.
|
|
17
|
+
def after_fork(variant)
|
|
18
|
+
@after_fork_proc&.call(variant)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Variants
|
|
23
|
+
Registration = Struct.new(:matcher, :variant) do
|
|
24
|
+
def match?(name)
|
|
25
|
+
File.fnmatch?(matcher, name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.current
|
|
30
|
+
@current ||= Variants.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Retrieves the specified variant
|
|
35
|
+
def self.lookup(variant)
|
|
36
|
+
self.current.lookup(variant)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Registers a variant. Variants are matched in the
|
|
41
|
+
# order they are registered.
|
|
42
|
+
#
|
|
43
|
+
# [matcher] Wildcard to match a name against.
|
|
44
|
+
# [named_options] Variant options.
|
|
45
|
+
# [after_fork] Optional block that is called when
|
|
46
|
+
# variant is preloaded.
|
|
47
|
+
#
|
|
48
|
+
# = Example
|
|
49
|
+
# Expedite::Variants.register('base' do |name|
|
|
50
|
+
# puts "Base #{name} started"
|
|
51
|
+
# end
|
|
52
|
+
# Expedite::Variants.register('development/abc', parent: 'base') do |name|
|
|
53
|
+
# puts "Variant #{name} started"
|
|
54
|
+
# end
|
|
55
|
+
def self.register(matcher, **named_options, &after_fork)
|
|
56
|
+
self.current.register(matcher, **named_options, &after_fork)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Resets registrations to default
|
|
61
|
+
def self.reset
|
|
62
|
+
self.current.reset
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def initialize
|
|
66
|
+
@registrations = []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def lookup(variant)
|
|
70
|
+
ret = @registrations.find do |r|
|
|
71
|
+
r.match?(variant)
|
|
72
|
+
end
|
|
73
|
+
raise NotImplementedError, "Variant #{variant.inspect} not found" if ret.nil?
|
|
74
|
+
ret.variant
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register(matcher, **named_options, &after_fork)
|
|
78
|
+
@registrations << Registration.new(
|
|
79
|
+
matcher,
|
|
80
|
+
Variant.new(**named_options, &after_fork)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reset
|
|
85
|
+
@registrations = {}
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: expedite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bing-Chang Lai
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-06-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Manages Ruby processes that can be used to spawn child processes faster.
|
|
14
|
+
email: johnny.lai@me.com
|
|
15
|
+
executables:
|
|
16
|
+
- expedite
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- bin/expedite
|
|
22
|
+
- lib/expedite.rb
|
|
23
|
+
- lib/expedite/application.rb
|
|
24
|
+
- lib/expedite/application/boot.rb
|
|
25
|
+
- lib/expedite/application_manager.rb
|
|
26
|
+
- lib/expedite/cli.rb
|
|
27
|
+
- lib/expedite/cli/server.rb
|
|
28
|
+
- lib/expedite/cli/stop.rb
|
|
29
|
+
- lib/expedite/client.rb
|
|
30
|
+
- lib/expedite/command/basic.rb
|
|
31
|
+
- lib/expedite/command/boot.rb
|
|
32
|
+
- lib/expedite/command/info.rb
|
|
33
|
+
- lib/expedite/commands.rb
|
|
34
|
+
- lib/expedite/env.rb
|
|
35
|
+
- lib/expedite/errors.rb
|
|
36
|
+
- lib/expedite/failsafe_thread.rb
|
|
37
|
+
- lib/expedite/load_helper.rb
|
|
38
|
+
- lib/expedite/send_json.rb
|
|
39
|
+
- lib/expedite/server.rb
|
|
40
|
+
- lib/expedite/signals.rb
|
|
41
|
+
- lib/expedite/variants.rb
|
|
42
|
+
- lib/expedite/version.rb
|
|
43
|
+
homepage: https://rubygems.org/gems/expedite
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata: {}
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.1.2
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Expedite startup of Ruby process
|
|
66
|
+
test_files: []
|