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,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,8 @@
1
+
2
+ module Expedite
3
+ class Error < StandardError
4
+ end
5
+
6
+ class CommandNotFound < Error
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/failsafe_thread.rb
2
+ require 'thread'
3
+
4
+ module Expedite
5
+ class << self
6
+ def failsafe_thread
7
+ Thread.new {
8
+ begin
9
+ yield
10
+ rescue
11
+ end
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module Expedite
2
+ module LoadHelper
3
+ def load_helper
4
+ helper = "expedite_helper.rb"
5
+ if File.exist?(helper)
6
+ log "loading #{helper}"
7
+ load(helper)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
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
@@ -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,5 @@
1
+ module Expedite
2
+ module Signals
3
+ IGNORE_SIGNALS = %w(INT QUIT)
4
+ end
5
+ 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
@@ -0,0 +1,3 @@
1
+ module Expedite
2
+ VERSION = '0.0.1'
3
+ 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: []