spider-gazelle 1.2.0 → 2.0.0
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 +4 -4
- data/bin/sg +1 -64
- data/lib/rack/handler/spider-gazelle.rb +17 -26
- data/lib/rack/lock_patch.rb +27 -27
- data/lib/spider-gazelle.rb +165 -16
- data/lib/spider-gazelle/gazelle.rb +151 -134
- data/lib/spider-gazelle/gazelle/app_store.rb +86 -0
- data/lib/spider-gazelle/gazelle/http1.rb +496 -0
- data/lib/spider-gazelle/gazelle/request.rb +155 -0
- data/lib/spider-gazelle/logger.rb +122 -0
- data/lib/spider-gazelle/options.rb +213 -0
- data/lib/spider-gazelle/reactor.rb +69 -0
- data/lib/spider-gazelle/signaller.rb +214 -0
- data/lib/spider-gazelle/signaller/signal_parser.rb +66 -0
- data/lib/spider-gazelle/spider.rb +305 -343
- data/lib/spider-gazelle/spider/binding.rb +80 -0
- data/lib/spider-gazelle/upgrades/websocket.rb +92 -88
- data/spec/http1_spec.rb +173 -0
- data/spec/rack_lock_spec.rb +97 -97
- data/spider-gazelle.gemspec +6 -6
- metadata +24 -17
- data/lib/spider-gazelle/app_store.rb +0 -64
- data/lib/spider-gazelle/binding.rb +0 -53
- data/lib/spider-gazelle/connection.rb +0 -371
- data/lib/spider-gazelle/const.rb +0 -206
- data/lib/spider-gazelle/request.rb +0 -103
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'rack' # Ruby webserver abstraction
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Request < ::Libuv::Q::DeferredPromise
|
6
|
+
RACK_VERSION = 'rack.version'.freeze
|
7
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
8
|
+
RACK_MULTITHREAD = "rack.multithread".freeze
|
9
|
+
RACK_MULTIPROCESS = "rack.multiprocess".freeze
|
10
|
+
RACK_RUN_ONCE = "rack.run_once".freeze
|
11
|
+
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
12
|
+
EMPTY = ''.freeze
|
13
|
+
SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
|
14
|
+
HTTP_11 = "HTTP/1.1".freeze
|
15
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
|
16
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
17
|
+
CGI_VER = "CGI/1.2".freeze
|
18
|
+
SERVER = "SpiderGazelle".freeze
|
19
|
+
LOCALHOST = 'localhost'.freeze
|
20
|
+
|
21
|
+
|
22
|
+
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
23
|
+
# convert - signs to underscores
|
24
|
+
PROTO_ENV = {
|
25
|
+
RACK_VERSION => ::Rack::VERSION, # Should be an array of integers
|
26
|
+
RACK_ERRORS => $stderr, # An error stream that supports: puts, write and flush
|
27
|
+
RACK_MULTITHREAD => true, # can the app be simultaneously invoked by another thread?
|
28
|
+
RACK_MULTIPROCESS => true, # will the app be simultaneously be invoked in a separate process?
|
29
|
+
RACK_RUN_ONCE => false, # this isn't CGI so will always be false
|
30
|
+
|
31
|
+
SCRIPT_NAME => ENV['SCRIPT_NAME'.freeze] || EMPTY, # The virtual path of the app base (empty if root)
|
32
|
+
SERVER_PROTOCOL => HTTP_11,
|
33
|
+
|
34
|
+
GATEWAY_INTERFACE => CGI_VER,
|
35
|
+
SERVER_SOFTWARE => SERVER
|
36
|
+
}
|
37
|
+
|
38
|
+
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade
|
39
|
+
attr_reader :hijacked, :defer, :is_async
|
40
|
+
|
41
|
+
|
42
|
+
SERVER_PORT = "SERVER_PORT".freeze
|
43
|
+
REMOTE_ADDR = "REMOTE_ADDR".freeze
|
44
|
+
RACK_URL_SCHEME = "rack.url_scheme".freeze
|
45
|
+
ASYNC = "async.callback".freeze
|
46
|
+
|
47
|
+
def initialize(thread, app, port, remote_ip, scheme, async_callback)
|
48
|
+
super(thread, thread.defer)
|
49
|
+
|
50
|
+
@app = app
|
51
|
+
@body = ''
|
52
|
+
@header = ''
|
53
|
+
@url = ''
|
54
|
+
@env = PROTO_ENV.dup
|
55
|
+
@env[SERVER_PORT] = port
|
56
|
+
@env[REMOTE_ADDR] = remote_ip
|
57
|
+
@env[RACK_URL_SCHEME] = scheme
|
58
|
+
@env[ASYNC] = async_callback
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
CONTENT_LENGTH = "CONTENT_LENGTH".freeze
|
63
|
+
HTTP_CONTENT_LENGTH = "HTTP_CONTENT_LENGTH".freeze
|
64
|
+
CONTENT_TYPE = "CONTENT_TYPE".freeze
|
65
|
+
HTTP_CONTENT_TYPE = "HTTP_CONTENT_TYPE".freeze
|
66
|
+
DEFAULT_TYPE = "text/plain".freeze
|
67
|
+
REQUEST_URI= "REQUEST_URI".freeze
|
68
|
+
ASCII_8BIT = "ASCII-8BIT".freeze
|
69
|
+
RACK_INPUT = "rack.input".freeze
|
70
|
+
PATH_INFO = "PATH_INFO".freeze
|
71
|
+
REQUEST_PATH = "REQUEST_PATH".freeze
|
72
|
+
QUERY_STRING = "QUERY_STRING".freeze
|
73
|
+
HTTP_HOST = "HTTP_HOST".freeze
|
74
|
+
COLON = ":".freeze
|
75
|
+
SERVER_NAME = "SERVER_NAME".freeze
|
76
|
+
# Hijacking IO is supported
|
77
|
+
HIJACK_P = "rack.hijack?".freeze
|
78
|
+
# Callback for indicating that this socket will be hijacked
|
79
|
+
HIJACK = "rack.hijack".freeze
|
80
|
+
# The object for performing IO on after hijack is called
|
81
|
+
HIJACK_IO = "rack.hijack_io".freeze
|
82
|
+
QUESTION_MARK = "?".freeze
|
83
|
+
|
84
|
+
HTTP_UPGRADE = 'HTTP_UPGRADE'.freeze
|
85
|
+
USE_HTTP2 = 'h2c'.freeze
|
86
|
+
|
87
|
+
|
88
|
+
def execute!
|
89
|
+
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.length
|
90
|
+
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
91
|
+
@env[REQUEST_URI] = @url.freeze
|
92
|
+
|
93
|
+
# For Rack::Lint on 1.9, ensure that the encoding is always for spec
|
94
|
+
@body.force_encoding(ASCII_8BIT) if @body.respond_to?(:force_encoding)
|
95
|
+
@env[RACK_INPUT] = StringIO.new @body
|
96
|
+
|
97
|
+
# Break the request into its components
|
98
|
+
query_start = @url.index QUESTION_MARK
|
99
|
+
if query_start
|
100
|
+
path = @url[0...query_start].freeze
|
101
|
+
@env[PATH_INFO] = path
|
102
|
+
@env[REQUEST_PATH] = path
|
103
|
+
@env[QUERY_STRING] = @url[query_start + 1..-1].freeze
|
104
|
+
else
|
105
|
+
@env[PATH_INFO] = @url
|
106
|
+
@env[REQUEST_PATH] = @url
|
107
|
+
@env[QUERY_STRING] = EMPTY
|
108
|
+
end
|
109
|
+
|
110
|
+
# Grab the host name from the request
|
111
|
+
if host = @env[HTTP_HOST]
|
112
|
+
if colon = host.index(COLON)
|
113
|
+
@env[SERVER_NAME] = host[0, colon]
|
114
|
+
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
115
|
+
else
|
116
|
+
@env[SERVER_NAME] = host
|
117
|
+
end
|
118
|
+
else
|
119
|
+
@env[SERVER_NAME] = LOCALHOST
|
120
|
+
end
|
121
|
+
|
122
|
+
# Provide hijack options if this is an upgrade request
|
123
|
+
if @upgrade == true
|
124
|
+
if @env[HTTP_UPGRADE] == USE_HTTP2
|
125
|
+
# TODO:: implement the upgrade process here
|
126
|
+
else
|
127
|
+
@env[HIJACK_P] = true
|
128
|
+
@env[HIJACK] = method :hijack
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Execute the request
|
133
|
+
# NOTE:: Catch was overloaded by Promise so this does the
|
134
|
+
resp = ruby_catch(:async) { @app.call @env }
|
135
|
+
if resp.nil? || resp[0] == -1
|
136
|
+
@is_async = true
|
137
|
+
|
138
|
+
# close the body for deferred responses
|
139
|
+
unless resp.nil?
|
140
|
+
body = resp[2]
|
141
|
+
body.close if body.respond_to?(:close)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
resp
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def hijack
|
150
|
+
@hijacked = @loop.defer
|
151
|
+
@env.delete HIJACK # don't want to hold a reference to this request object
|
152
|
+
@env[HIJACK_IO] = @hijacked.promise
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'libuv'
|
2
|
+
|
3
|
+
module SpiderGazelle
|
4
|
+
class Logger
|
5
|
+
include Singleton
|
6
|
+
attr_reader :level, :thread, :pipe
|
7
|
+
attr_accessor :formatter
|
8
|
+
|
9
|
+
|
10
|
+
LEVEL = {
|
11
|
+
debug: 0,
|
12
|
+
info: 1,
|
13
|
+
warn: 2,
|
14
|
+
error: 3,
|
15
|
+
fatal: 4
|
16
|
+
}.freeze
|
17
|
+
DEFAULT_LEVEL = 1
|
18
|
+
LEVELS = LEVEL.keys.freeze
|
19
|
+
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@thread = ::Libuv::Loop.default
|
23
|
+
@level = DEFAULT_LEVEL
|
24
|
+
@write = method(:server_write)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def self.log(data)
|
29
|
+
Logger.instance.server_write(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def set_client(uv_io)
|
34
|
+
@pipe = uv_io
|
35
|
+
@write = method(:client_write)
|
36
|
+
end
|
37
|
+
|
38
|
+
def level=(level)
|
39
|
+
@level = LEVEL[level] || level
|
40
|
+
end
|
41
|
+
|
42
|
+
def verbose!(enabled = true)
|
43
|
+
@verbose = enabled
|
44
|
+
end
|
45
|
+
|
46
|
+
def debug(msg = nil)
|
47
|
+
if @level <= 0
|
48
|
+
msg = yield if block_given?
|
49
|
+
log(:debug, msg)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def info(msg = nil)
|
54
|
+
if @level <= 1
|
55
|
+
msg = yield if block_given?
|
56
|
+
log(:info, msg)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def warn(msg = nil)
|
61
|
+
if @level <= 2
|
62
|
+
msg = yield if block_given?
|
63
|
+
log(:warn, msg)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def error(msg = nil)
|
68
|
+
if @level <= 3
|
69
|
+
msg = yield if block_given?
|
70
|
+
log(:error, msg)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def fatal(msg = nil)
|
75
|
+
if @level <= 4
|
76
|
+
msg = yield if block_given?
|
77
|
+
log(:fatal, msg)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def verbose(msg = nil)
|
82
|
+
if @verbose
|
83
|
+
msg = yield if block_given?
|
84
|
+
@write.call ">> #{msg}\n"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def print_error(e, msg = '', trace = nil)
|
89
|
+
msg << ":\n" unless msg.empty?
|
90
|
+
msg << "#{e.message}\n"
|
91
|
+
backtrace = e.backtrace if e.respond_to?(:backtrace)
|
92
|
+
if backtrace
|
93
|
+
msg << "#{backtrace.join("\n")}\n"
|
94
|
+
elsif trace.nil?
|
95
|
+
trace = caller
|
96
|
+
end
|
97
|
+
msg << "Caller backtrace:\n#{trace.join("\n")}\n" if trace
|
98
|
+
error(msg)
|
99
|
+
end
|
100
|
+
|
101
|
+
# NOTE:: should only be called on reactor thread
|
102
|
+
def server_write(msg)
|
103
|
+
STDOUT.write msg
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
|
110
|
+
def log(level, msg)
|
111
|
+
output = "[#{level}] #{msg}\n"
|
112
|
+
@thread.schedule do
|
113
|
+
@write.call output
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# NOTE:: should only be called on reactor thread
|
118
|
+
def client_write(msg)
|
119
|
+
@pipe.write "\x02Logger log #{msg}\x03"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'optparse'
|
3
|
+
require 'spider-gazelle/logger'
|
4
|
+
|
5
|
+
|
6
|
+
module SpiderGazelle
|
7
|
+
module Options
|
8
|
+
DEFAULTS = {
|
9
|
+
host: "0.0.0.0",
|
10
|
+
port: 3000,
|
11
|
+
verbose: false,
|
12
|
+
tls: false,
|
13
|
+
backlog: 5000,
|
14
|
+
rackup: "#{Dir.pwd}/config.ru",
|
15
|
+
mode: :thread,
|
16
|
+
app_mode: :thread_pool,
|
17
|
+
isolate: true
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
|
21
|
+
# Options that can't be used when more than one set of options is being applied
|
22
|
+
APP_OPTIONS = [:port, :host, :verbose, :debug, :environment, :rackup, :mode, :backlog, :count, :name, :loglevel].freeze
|
23
|
+
MUTUALLY_EXCLUSIVE = {
|
24
|
+
|
25
|
+
# Only :password is valid when this option is present
|
26
|
+
update: APP_OPTIONS
|
27
|
+
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
|
31
|
+
def self.parse(args)
|
32
|
+
options = {}
|
33
|
+
|
34
|
+
parser = OptionParser.new do |opts|
|
35
|
+
# ================
|
36
|
+
# STANDARD OPTIONS
|
37
|
+
# ================
|
38
|
+
opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
|
39
|
+
options[:port] = arg
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on "-h", "--host ADDRESS", "bind to address (default: 0.0.0.0)" do |arg|
|
43
|
+
options[:host] = arg
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on "-v", "--verbose", "loud output" do
|
47
|
+
options[:verbose] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on "-d", "--debug", "debugging mode with lowered security and manual processes" do
|
51
|
+
options[:debug] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
|
55
|
+
options[:environment] = arg
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
|
59
|
+
options[:rackup] = arg
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on "-m", "--mode MODE", MODES, "Either process, thread or no_ipc (default: process)" do |arg|
|
63
|
+
options[:mode] = arg
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on "-a", "--app-mode MODE", APP_MODE, "How should requests be processed (default: thread_pool)" do |arg|
|
67
|
+
options[:host] = arg
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on "-b", "--backlog BACKLOG", Integer, "Number of pending connections allowed (default: 5000)" do |arg|
|
71
|
+
options[:backlog] = arg
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# =================
|
76
|
+
# TLS Configuration
|
77
|
+
# =================
|
78
|
+
opts.on "-t", "--use-tls PRIVATE_KEY_FILE", "Enables TLS on the port specified using the provided private key in PEM format" do |arg|
|
79
|
+
options[:tls] = true
|
80
|
+
options[:private_key] = arg
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on "-tc", "--tls-chain-file CERT_CHAIN_FILE", "The certificate chain to provide clients" do |arg|
|
84
|
+
options[:cert_chain] = arg
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on "-ts", "--tls-ciphers CIPHER_LIST", "A list of Ciphers that the server will accept" do |arg|
|
88
|
+
options[:ciphers] = arg
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on "-tv", "--tls-verify-peer", "Do we want to verify the client connections? (default: false)" do
|
92
|
+
options[:verify_peer] = true
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# ========================
|
97
|
+
# CHILD PROCESS INDICATORS
|
98
|
+
# ========================
|
99
|
+
opts.on "-g", "--gazelle PASSWORD", 'For internal use only' do |arg|
|
100
|
+
options[:gazelle] = arg
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on "-f", "--file IPC", 'For internal use only' do |arg|
|
104
|
+
options[:gazelle_ipc] = arg
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
opts.on "-s", "--spider PASSWORD", 'For internal use only' do |arg|
|
109
|
+
options[:spider] = arg
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
opts.on "-i", "--interactive-mode", 'Loads a multi-process version of spider-gazelle that can live update your app' do
|
114
|
+
options[:isolate] = false
|
115
|
+
end
|
116
|
+
|
117
|
+
opts.on "-c", "--count NUMBER", Integer, "Number of gazelle processes to launch (default: number of CPU cores)" do |arg|
|
118
|
+
options[:count] = arg
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# ==================
|
123
|
+
# SIGNALLING OPTIONS
|
124
|
+
# ==================
|
125
|
+
opts.on "-u", "--update", "Live migrates to a new process without dropping existing connections" do |arg|
|
126
|
+
options[:update] = true
|
127
|
+
end
|
128
|
+
|
129
|
+
opts.on "-up", "--update-password PASSWORD", "Sets a password for performing updates" do |arg|
|
130
|
+
options[:password] = arg
|
131
|
+
end
|
132
|
+
|
133
|
+
opts.on "-l", "--loglevel LEVEL", Logger::LEVELS, "Sets the log level" do |arg|
|
134
|
+
options[:loglevel] = arg
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
parser.banner = "sg <options> <rackup file>"
|
139
|
+
parser.on_tail "-h", "--help", "Show help" do
|
140
|
+
puts parser
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
parser.parse!(args)
|
144
|
+
|
145
|
+
# Check for rackup file
|
146
|
+
if args.last =~ /\.ru$/
|
147
|
+
options[:rackup] = args.last
|
148
|
+
end
|
149
|
+
|
150
|
+
# Unless this is a signal then we want to include the default options
|
151
|
+
unless options[:update]
|
152
|
+
options = DEFAULTS.merge(options)
|
153
|
+
|
154
|
+
unless File.exist? options[:rackup]
|
155
|
+
abort "No rackup found at #{options[:rackup]}"
|
156
|
+
end
|
157
|
+
|
158
|
+
options[:environment] ||= ENV['RACK_ENV'] || 'development'
|
159
|
+
ENV['RACK_ENV'] = options[:environment]
|
160
|
+
|
161
|
+
# isolation and process mode don't mix
|
162
|
+
options[:isolate] = false if options[:mode] == :process
|
163
|
+
end
|
164
|
+
|
165
|
+
options
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.sanitize(args)
|
169
|
+
# Use "\0" as this character won't be used in the command
|
170
|
+
cmdline = args.join("\0")
|
171
|
+
components = cmdline.split("\0--", -1)
|
172
|
+
|
173
|
+
# Ensure there is at least one component
|
174
|
+
# (This will occur when no options are provided)
|
175
|
+
components << '' if components.empty?
|
176
|
+
|
177
|
+
# Parse the commandline options
|
178
|
+
options = []
|
179
|
+
components.each do |app_opts|
|
180
|
+
options << parse(app_opts.split(/\0+/))
|
181
|
+
end
|
182
|
+
|
183
|
+
# Check for any invalid requests
|
184
|
+
exclusive = Set.new(MUTUALLY_EXCLUSIVE.keys)
|
185
|
+
|
186
|
+
if options.length > 1
|
187
|
+
|
188
|
+
# Some options can only be used by themselves
|
189
|
+
options.each do |opt|
|
190
|
+
keys = Set.new(opt.keys)
|
191
|
+
|
192
|
+
if exclusive.intersect? keys
|
193
|
+
invalid = exclusive & keys
|
194
|
+
|
195
|
+
abort "The requested actions can only be used in isolation: #{invalid.to_a}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Ensure there are no conflicting ports
|
200
|
+
ports = [options[0][:port]]
|
201
|
+
options[1..-1].each do |opt|
|
202
|
+
# If there is a clash we'll increment the port by 1
|
203
|
+
while ports.include? opt[:port]
|
204
|
+
opt[:port] += 1
|
205
|
+
end
|
206
|
+
ports << opt[:port]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
options
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|