tp2 0.5 → 0.7
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/.github/workflows/test.yml +24 -0
- data/.gitignore +1 -1
- data/CHANGELOG.md +16 -0
- data/README.md +9 -2
- data/Rakefile +1 -1
- data/bin/tp2 +87 -0
- data/examples/app.rb +7 -0
- data/examples/rack.ru +3 -0
- data/examples/simple.rb +2 -20
- data/lib/tp2/http1_adapter.rb +34 -63
- data/lib/tp2/rack_adapter.rb +35 -0
- data/lib/tp2/request_extensions.rb +0 -2
- data/lib/tp2/server.rb +92 -23
- data/lib/tp2/version.rb +1 -1
- data/lib/tp2.rb +57 -0
- data/test/test_http1_adapter.rb +10 -19
- data/test/test_server.rb +14 -11
- data/tp2.gemspec +3 -1
- metadata +24 -5
- data/Gemfile.lock +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed2dc84d547495b29f7f569f5898e14a29ea7114683374d10150618d6fed6ba7
|
4
|
+
data.tar.gz: 6cfb58517566700905799005ff089e951f3c5f728310c2c3e5f4fdb6307c3033
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2785997104626457256a3dc48326b3e37783a21e2f5e8c06d20e7a2afb4443227258f50c0a33fda0e0703f280f44a95ea4b9fbc327c8d971750baef4f9af87b
|
7
|
+
data.tar.gz: 5540476e00ce0787c3f967c9a9621bcfd09e629ba0617c9853be04d523e9d530f113cab95afc60b67378017ecd9d9e62731762576eb13e7035c035d4fbb3e6ea
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: Tests
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
os: [ubuntu-latest]
|
11
|
+
ruby: [3.4, 'head']
|
12
|
+
|
13
|
+
name: >-
|
14
|
+
${{matrix.os}}, ${{matrix.ruby}}
|
15
|
+
|
16
|
+
runs-on: ${{matrix.os}}
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v3
|
19
|
+
- uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{matrix.ruby}}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: Run tests
|
24
|
+
run: bundle exec rake test
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# Version 0.7 2025-06-03
|
2
|
+
|
3
|
+
- Introduce tp2 binary
|
4
|
+
- Add support for loading tp2 apps and Rack apps
|
5
|
+
- Add support for binding to multiple addresses
|
6
|
+
- Show banner when running binary
|
7
|
+
|
8
|
+
# Version 0.6 2025-05-05
|
9
|
+
|
10
|
+
- Update UringMachine to 0.10
|
11
|
+
- UseUM::Stream for HTTP parsing
|
12
|
+
|
13
|
+
# Version 0.5.2 2025-05-05
|
14
|
+
|
15
|
+
- Update UringMachine to 0.8.2
|
16
|
+
|
1
17
|
# Version 0.5 2025-05-05
|
2
18
|
|
3
19
|
- Implement graceful shutdown
|
data/README.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# TP2
|
2
2
|
|
3
3
|
TP2 is an experimental HTTP server based on
|
4
|
-
[UringMachine](https://github.com/digital-fabric/uringmachine) and [Qeweney](https://github.com/digital-fabric/qeweney).
|
4
|
+
[UringMachine](https://github.com/digital-fabric/uringmachine) and [Qeweney](https://github.com/digital-fabric/qeweney).
|
5
5
|
|
6
|
+
## Features
|
7
|
+
|
8
|
+
- HTTP/1.1
|
9
|
+
- Support for streaming requests and responses
|
10
|
+
- Automatic chunked transfer encoding
|
11
|
+
- Fast, native HTTP protocol parsing (no dependency)
|
12
|
+
- Graceful shutdown
|
data/Rakefile
CHANGED
data/bin/tp2
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "tp2"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
|
8
|
+
opts = {}
|
9
|
+
|
10
|
+
parser = OptionParser.new do |o|
|
11
|
+
o.banner = "Usage: tp2 [options]"
|
12
|
+
|
13
|
+
o.on("-c", "--config CONFIG_FILE", String, "TP2 config file to use (default: app.rb)") {
|
14
|
+
opts[:app_type] = :tp2
|
15
|
+
opts[:app_location] = it
|
16
|
+
}
|
17
|
+
o.on("-r", "--rack RACK_FILE", String, "Rack config file to use (default: config.ru)") {
|
18
|
+
opts[:app_type] = :rack
|
19
|
+
opts[:app_location] = it
|
20
|
+
}
|
21
|
+
o.on("-d", "--dir PATH", String, "Static files at path") {
|
22
|
+
opts[:app_type] = :static
|
23
|
+
opts[:app_location] = it
|
24
|
+
}
|
25
|
+
|
26
|
+
o.on("-b", "--bind BIND", String, "Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.") {
|
27
|
+
opts[:bind] ||= []
|
28
|
+
opts[:bind] << it
|
29
|
+
}
|
30
|
+
|
31
|
+
o.on("-h", "--help", "Show this help message") do
|
32
|
+
puts o
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on("-v", "--version", "Show version") do
|
37
|
+
puts "TP2 version #{TP2::VERSION}"
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
parser.parse!
|
44
|
+
rescue StandardError => e
|
45
|
+
puts e.message
|
46
|
+
puts e.backtrace.join("\n")
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
opts[:location] = ARGV.shift || '.'
|
51
|
+
|
52
|
+
def check_app_file(opts, fn, type)
|
53
|
+
fn = File.join(opts[:location], fn)
|
54
|
+
return false if !File.file?(fn)
|
55
|
+
|
56
|
+
opts[:app_type] = type
|
57
|
+
opts[:app_location] = File.expand_path(fn)
|
58
|
+
end
|
59
|
+
|
60
|
+
def detect_app(opts)
|
61
|
+
if File.file?(opts[:location])
|
62
|
+
case File.extname(opts[:location])
|
63
|
+
when '.rb'
|
64
|
+
opts[:app_type] = :tp2
|
65
|
+
opts[:app_location] = File.expand_path(opts[:location])
|
66
|
+
when '.ru'
|
67
|
+
opts[:app_type] = :rack
|
68
|
+
opts[:app_location] = File.expand_path(opts[:location])
|
69
|
+
else
|
70
|
+
raise "Don't know how to serve #{opts[:location]}"
|
71
|
+
end
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
if File.directory?(opts[:location])
|
76
|
+
return if check_app_file(opts, 'app.rb', :tp2)
|
77
|
+
return if check_app_file(opts, 'config.ru', :rack)
|
78
|
+
end
|
79
|
+
|
80
|
+
opts[:app_type] = :static
|
81
|
+
opts[:app_location] = Dir.pwd
|
82
|
+
end
|
83
|
+
|
84
|
+
detect_app(opts) if !opts[:app_type]
|
85
|
+
|
86
|
+
puts TP2::BANNER
|
87
|
+
TP2.run(opts)
|
data/examples/app.rb
ADDED
data/examples/rack.ru
ADDED
data/examples/simple.rb
CHANGED
@@ -12,23 +12,5 @@ app = ->(req) {
|
|
12
12
|
req.respond('foobar', ':status' => Qeweney::Status::TEAPOT)
|
13
13
|
}
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
puts "Listening on port 1234..."
|
18
|
-
|
19
|
-
server_fiber = machine.spin { server.run }
|
20
|
-
|
21
|
-
sig_queue = UM::Queue.new
|
22
|
-
trap('SIGINT') { machine.push(sig_queue, :SIGINT) }
|
23
|
-
|
24
|
-
puts "Running... (pid: #{Process.pid})"
|
25
|
-
STDOUT.flush
|
26
|
-
|
27
|
-
# wait for signal
|
28
|
-
sig = machine.shift(sig_queue)
|
29
|
-
|
30
|
-
puts "Got signal (#{sig}), shutting down gracefully..."
|
31
|
-
machine.schedule(server_fiber, UM::Terminate.new)
|
32
|
-
machine.join(server_fiber)
|
33
|
-
|
34
|
-
puts
|
15
|
+
TP2.config(&app)
|
16
|
+
TP2.run
|
data/lib/tp2/http1_adapter.rb
CHANGED
@@ -10,8 +10,7 @@ module TP2
|
|
10
10
|
def initialize(machine, fd, &app)
|
11
11
|
@machine = machine
|
12
12
|
@fd = fd
|
13
|
-
@
|
14
|
-
@sio = StringIO.new(@buffer)
|
13
|
+
@stream = UM::Stream.new(machine, fd)
|
15
14
|
@app = app
|
16
15
|
end
|
17
16
|
|
@@ -68,7 +67,7 @@ module TP2
|
|
68
67
|
|
69
68
|
return nil if headers[':body-done-reading']
|
70
69
|
|
71
|
-
# if content-length is not specified, we read to EOF, up to max 1MB size
|
70
|
+
# if content-length is not specified, we read to EOF, up to max 1MB size
|
72
71
|
chunk = read(1 << 20, nil, false)
|
73
72
|
headers[':body-done-reading'] = true
|
74
73
|
chunk
|
@@ -80,10 +79,6 @@ module TP2
|
|
80
79
|
|
81
80
|
# response API
|
82
81
|
|
83
|
-
CRLF = "\r\n"
|
84
|
-
ZERO_CRLF_CRLF = "0\r\n\r\n"
|
85
|
-
CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
|
86
|
-
|
87
82
|
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
88
83
|
|
89
84
|
# Sends response including headers and body. Waits for the request to complete
|
@@ -139,13 +134,16 @@ module TP2
|
|
139
134
|
# @return [void]
|
140
135
|
def finish(_request)
|
141
136
|
# request.tx_incr(5)
|
142
|
-
@machine.send(@fd,
|
137
|
+
@machine.send(@fd, "0\r\n\r\n", "0\r\n\r\n".bytesize, SEND_FLAGS)
|
143
138
|
end
|
144
139
|
|
145
140
|
private
|
146
141
|
|
147
|
-
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+(http\/[0-9\.]{1,3})/i
|
148
|
-
RE_HEADER_LINE = /^([a-z0-9\-]+)\:\s+(.+)/i
|
142
|
+
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+(http\/[0-9\.]{1,3})/i.freeze
|
143
|
+
RE_HEADER_LINE = /^([a-z0-9\-]+)\:\s+(.+)/i.freeze
|
144
|
+
MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
|
145
|
+
MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
|
146
|
+
MAX_CHUNK_SIZE_LEN = 16
|
149
147
|
|
150
148
|
class ProtocolError < StandardError
|
151
149
|
end
|
@@ -160,39 +158,30 @@ module TP2
|
|
160
158
|
end
|
161
159
|
|
162
160
|
def parse_headers
|
163
|
-
|
161
|
+
buf = String.new(capacity: 4096)
|
162
|
+
headers = get_request_line(buf)
|
164
163
|
return nil if !headers
|
165
|
-
|
164
|
+
|
166
165
|
while true
|
167
|
-
line = get_line
|
166
|
+
line = @stream.get_line(buf, MAX_HEADER_LINE_LEN)
|
168
167
|
break if line.nil? || line.empty?
|
169
168
|
|
170
169
|
m = line.match(RE_HEADER_LINE)
|
171
170
|
raise ProtocolError, 'Invalid header' if !m
|
172
|
-
|
171
|
+
|
173
172
|
headers[m[1].downcase] = m[2]
|
174
173
|
end
|
175
|
-
|
176
|
-
headers
|
177
|
-
end
|
178
174
|
|
179
|
-
|
180
|
-
while true
|
181
|
-
line = @sio.gets(chomp: true)
|
182
|
-
return line if line
|
183
|
-
|
184
|
-
res = @machine.read(@fd, @buffer, 65536, -1)
|
185
|
-
return nil if res == 0
|
186
|
-
end
|
175
|
+
headers
|
187
176
|
end
|
188
177
|
|
189
|
-
def get_request_line
|
190
|
-
line = get_line
|
178
|
+
def get_request_line(buf)
|
179
|
+
line = @stream.get_line(buf, MAX_REQUEST_LINE_LEN)
|
191
180
|
return nil if !line
|
192
|
-
|
181
|
+
|
193
182
|
m = line.match(RE_REQUEST_LINE)
|
194
183
|
raise ProtocolError, 'Invalid request line' if !m
|
195
|
-
|
184
|
+
|
196
185
|
{
|
197
186
|
':method' => m[1].downcase,
|
198
187
|
':path' => m[2],
|
@@ -209,48 +198,30 @@ module TP2
|
|
209
198
|
end
|
210
199
|
|
211
200
|
def read(len, buf = nil, raise_on_eof = true)
|
212
|
-
|
213
|
-
if
|
214
|
-
|
215
|
-
if buf
|
216
|
-
buf << from_sio
|
217
|
-
else
|
218
|
-
buf = +from_sio
|
219
|
-
end
|
220
|
-
else
|
221
|
-
left = len
|
222
|
-
buf ||= +''
|
201
|
+
str = @stream.get_string(buf, len)
|
202
|
+
if !str && raise_on_eof
|
203
|
+
raise ProtocolError, "Missing data"
|
223
204
|
end
|
224
205
|
|
225
|
-
|
226
|
-
res = @machine.read(@fd, buf, left, -1)
|
227
|
-
if res == 0
|
228
|
-
raise ProtocolError, "Incomplete body" if raise_on_eof
|
229
|
-
|
230
|
-
return buf
|
231
|
-
end
|
232
|
-
|
233
|
-
|
234
|
-
left -= res
|
235
|
-
end
|
236
|
-
buf
|
206
|
+
str
|
237
207
|
end
|
238
208
|
|
239
|
-
def read_chunk(headers,
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
209
|
+
def read_chunk(headers, buffer)
|
210
|
+
tmp = String.new(capacity: 256)
|
211
|
+
chunk_size_str = @stream.get_line(tmp, MAX_CHUNK_SIZE_LEN)
|
212
|
+
return nil if !chunk_size_str
|
213
|
+
|
214
|
+
chunk_size = chunk_size_str.to_i(16)
|
244
215
|
if chunk_size == 0
|
245
216
|
headers[':body-done-reading'] = true
|
246
|
-
get_line
|
217
|
+
@stream.get_line(tmp, 0)
|
247
218
|
return nil
|
248
219
|
end
|
249
220
|
|
250
|
-
chunk =
|
251
|
-
get_line
|
221
|
+
chunk = @stream.get_string(nil, chunk_size)
|
222
|
+
@stream.get_line(tmp, 0)
|
252
223
|
|
253
|
-
chunk
|
224
|
+
buffer ? (buffer << chunk) : chunk
|
254
225
|
end
|
255
226
|
|
256
227
|
def http1_1?(request)
|
@@ -274,7 +245,7 @@ module TP2
|
|
274
245
|
|
275
246
|
collect_header_lines(lines, k, v)
|
276
247
|
end
|
277
|
-
lines <<
|
248
|
+
lines << "\r\n"
|
278
249
|
lines
|
279
250
|
end
|
280
251
|
|
@@ -298,7 +269,7 @@ module TP2
|
|
298
269
|
if chunked
|
299
270
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
300
271
|
else
|
301
|
-
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.
|
272
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
302
273
|
end
|
303
274
|
end
|
304
275
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
module TP2
|
6
|
+
module RackAdapter
|
7
|
+
class << self
|
8
|
+
def run(&app)
|
9
|
+
->(req) { respond(req, app.(env(req))) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def load(path)
|
13
|
+
src = IO.read(path)
|
14
|
+
instance_eval(src, path, 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
def env(request)
|
18
|
+
Qeweney.rack_env_from_request(request)
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond(request, (status_code, headers, body))
|
22
|
+
headers[':status'] = status_code.to_s
|
23
|
+
|
24
|
+
content =
|
25
|
+
if body.respond_to?(:to_path)
|
26
|
+
File.open(body.to_path, 'rb') { |f| f.read }
|
27
|
+
else
|
28
|
+
body.first
|
29
|
+
end
|
30
|
+
|
31
|
+
request.respond(content, headers)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/tp2/server.rb
CHANGED
@@ -2,22 +2,57 @@
|
|
2
2
|
|
3
3
|
require 'tp2/http1_adapter'
|
4
4
|
require 'tp2/request_extensions'
|
5
|
+
require 'tp2/rack_adapter'
|
5
6
|
|
6
7
|
module TP2
|
7
8
|
class Server
|
8
9
|
PENDING_REQUESTS_GRACE_PERIOD = 0.1
|
9
10
|
PENDING_REQUESTS_TIMEOUT_PERIOD = 5
|
10
11
|
|
11
|
-
def
|
12
|
+
def self.rack_app(opts)
|
13
|
+
raise "Missing app location" if !opts[:app_location]
|
14
|
+
|
15
|
+
TP2::RackAdapter.load(opts[:app_location])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.tp2_app(opts)
|
19
|
+
if opts[:app_location]
|
20
|
+
puts "Loading app at #{opts[:app_location]}"
|
21
|
+
require opts[:app_location]
|
22
|
+
|
23
|
+
opts.merge!(TP2.config)
|
24
|
+
end
|
25
|
+
opts[:app]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.static_app(opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def initialize(machine, opts, &app)
|
12
33
|
@machine = machine
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
34
|
+
@opts = opts
|
35
|
+
@app = app || app_from_opts
|
36
|
+
@server_fds = []
|
37
|
+
@accept_fibers = []
|
16
38
|
end
|
17
39
|
|
40
|
+
def app_from_opts
|
41
|
+
case @opts[:app_type]
|
42
|
+
when nil, :tp2
|
43
|
+
Server.tp2_app(@opts)
|
44
|
+
when :rack
|
45
|
+
Server.rack_app(@opts)
|
46
|
+
when :static
|
47
|
+
Server.static_app(@opts)
|
48
|
+
else
|
49
|
+
raise "Invalid app type #{@opts[:app_type].inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
18
53
|
def run
|
19
54
|
setup
|
20
|
-
|
55
|
+
@machine.join(*@accept_fibers)
|
21
56
|
rescue UM::Terminate
|
22
57
|
graceful_shutdown
|
23
58
|
end
|
@@ -25,51 +60,85 @@ module TP2
|
|
25
60
|
private
|
26
61
|
|
27
62
|
def setup
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
63
|
+
bind_info = get_bind_entries
|
64
|
+
bind_info.each do |(host, port)|
|
65
|
+
fd = setup_server_socket(host, port)
|
66
|
+
@server_fds << fd
|
67
|
+
@accept_fibers << @machine.spin { accept_incoming(fd) }
|
68
|
+
end
|
69
|
+
bind_string = bind_info.map { it.join(':') }.join(", ")
|
70
|
+
puts "Listening on #{bind_string}"
|
71
|
+
|
33
72
|
# map fibers
|
34
|
-
@
|
73
|
+
@connection_fiber_map = {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_bind_entries
|
77
|
+
bind = @opts[:bind]
|
78
|
+
case bind
|
79
|
+
when Array
|
80
|
+
bind.map { bind_info(it) }
|
81
|
+
when String
|
82
|
+
[bind_info(bind)]
|
83
|
+
else
|
84
|
+
# default
|
85
|
+
[['0.0.0.0', 1234]]
|
86
|
+
end
|
87
|
+
end
|
35
88
|
|
36
|
-
|
89
|
+
def bind_info(bind_string)
|
90
|
+
parts = bind_string.split(':')
|
91
|
+
[parts[0], parts[1].to_i]
|
37
92
|
end
|
38
93
|
|
39
|
-
def
|
40
|
-
@machine.
|
94
|
+
def setup_server_socket(host, port)
|
95
|
+
fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
96
|
+
@machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
97
|
+
@machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEPORT, true)
|
98
|
+
@machine.bind(fd, host, port)
|
99
|
+
@machine.listen(fd, UM::SOMAXCONN)
|
100
|
+
fd
|
101
|
+
end
|
102
|
+
|
103
|
+
def accept_incoming(listen_fd)
|
104
|
+
@machine.accept_each(listen_fd) do |fd|
|
41
105
|
conn = HTTP1Adapter.new(@machine, fd, &@app)
|
42
106
|
f = @machine.spin(conn) do
|
43
107
|
it.run
|
44
108
|
ensure
|
45
|
-
@
|
109
|
+
@connection_fiber_map.delete(f)
|
46
110
|
end
|
47
|
-
@
|
111
|
+
@connection_fiber_map[f] = true
|
48
112
|
end
|
49
113
|
end
|
50
114
|
|
115
|
+
def close_all_server_fds
|
116
|
+
@server_fds.each { @machine.close(it) }
|
117
|
+
end
|
118
|
+
|
51
119
|
def graceful_shutdown
|
52
120
|
# stop listening
|
53
|
-
|
121
|
+
close_all_server_fds
|
122
|
+
@machine.snooze
|
54
123
|
|
55
|
-
return if @
|
124
|
+
return if @connection_fiber_map.empty?
|
56
125
|
|
57
126
|
# sleep for a bit, let requests finish
|
58
127
|
@machine.sleep(PENDING_REQUESTS_GRACE_PERIOD)
|
59
|
-
return if @
|
128
|
+
return if @connection_fiber_map.empty?
|
60
129
|
|
61
130
|
# terminate pending fibers
|
62
|
-
pending = @
|
131
|
+
pending = @connection_fiber_map.keys
|
63
132
|
signal = UM::Terminate.new
|
64
133
|
pending.each { @machine.schedule(it, signal) }
|
65
134
|
|
66
135
|
@machine.timeout(PENDING_REQUESTS_TIMEOUT_PERIOD, UM::Terminate) do
|
67
|
-
@machine.join(*@
|
68
|
-
|
136
|
+
@machine.join(*@connection_fiber_map.keys)
|
137
|
+
rescue UM::Terminate
|
69
138
|
# timeout on waiting for adapters to finish running, do nothing
|
70
139
|
end
|
71
140
|
ensure
|
72
|
-
@machine.close(@server_fd)
|
141
|
+
@machine.close(@server_fd) rescue nil
|
73
142
|
end
|
74
143
|
end
|
75
144
|
end
|
data/lib/tp2/version.rb
CHANGED
data/lib/tp2.rb
CHANGED
@@ -1,4 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'uringmachine'
|
4
|
+
require 'tp2/version'
|
4
5
|
require 'tp2/server'
|
6
|
+
|
7
|
+
module TP2
|
8
|
+
BANNER = (
|
9
|
+
"\n" +
|
10
|
+
" ooo\n" +
|
11
|
+
" oo\n" +
|
12
|
+
" o\n" +
|
13
|
+
" \\|/ TP2 - a modern web server for Ruby apps\n" +
|
14
|
+
" / \\ \n" +
|
15
|
+
" / \\ https://github.com/noteflakes/tp2\n" +
|
16
|
+
"⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺\n"
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def run(opts = nil, &app)
|
22
|
+
return if @in_run
|
23
|
+
|
24
|
+
opts ||= @config || {}
|
25
|
+
begin
|
26
|
+
@in_run = true
|
27
|
+
machine = UM.new
|
28
|
+
server = Server.new(machine, opts, &app)
|
29
|
+
|
30
|
+
setup_signal_handling(machine)
|
31
|
+
|
32
|
+
server.run
|
33
|
+
ensure
|
34
|
+
@in_run = false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def config(opts = nil, &app)
|
39
|
+
return @config if !opts && !app
|
40
|
+
|
41
|
+
@config = opts || {}
|
42
|
+
@config[:app] = app if app
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def setup_signal_handling(machine)
|
48
|
+
queue = UM::Queue.new
|
49
|
+
trap('SIGINT') { machine.push(queue, :SIGINT) }
|
50
|
+
machine.spin { terminate_on_signal(machine, queue, Fiber.current) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# waits for signal from queue, then terminates given fiber
|
54
|
+
# to be done
|
55
|
+
def terminate_on_signal(machine, queue, fiber)
|
56
|
+
sig = machine.shift(queue)
|
57
|
+
puts "\nGot signal (#{sig}), shutting down gracefully..."
|
58
|
+
machine.schedule(fiber, UM::Terminate.new)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/test/test_http1_adapter.rb
CHANGED
@@ -69,13 +69,16 @@ class HTTP1AdapterTest < Minitest::Test
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def test_pipelined_requests
|
72
|
-
|
72
|
+
msg = <<~HTTP.crlf_lines
|
73
73
|
GET /foo HTTP/1.1
|
74
74
|
Server: foo.com
|
75
75
|
|
76
76
|
SCHMET /bar HTTP/1.1
|
77
77
|
|
78
|
+
|
79
|
+
|
78
80
|
HTTP
|
81
|
+
write_http_request msg
|
79
82
|
|
80
83
|
@adapter.run
|
81
84
|
assert_equal 2, @reqs.size
|
@@ -142,7 +145,7 @@ class HTTP1AdapterTest < Minitest::Test
|
|
142
145
|
end
|
143
146
|
|
144
147
|
def test_pipelined_requests_with_body_chunked
|
145
|
-
|
148
|
+
msg = <<~HTTP.crlf_lines
|
146
149
|
POST /foo HTTP/1.1
|
147
150
|
Server: foo.com
|
148
151
|
Transfer-Encoding: chunked
|
@@ -161,7 +164,10 @@ class HTTP1AdapterTest < Minitest::Test
|
|
161
164
|
123456789abcdefghijklmnopqrstuv
|
162
165
|
0
|
163
166
|
|
167
|
+
|
168
|
+
|
164
169
|
HTTP
|
170
|
+
write_http_request(msg)
|
165
171
|
|
166
172
|
@bodies = []
|
167
173
|
@hook = ->(req) { @bodies << req.read }
|
@@ -197,6 +203,8 @@ class HTTP1AdapterTest < Minitest::Test
|
|
197
203
|
end
|
198
204
|
|
199
205
|
def test_body_to_eof
|
206
|
+
skip
|
207
|
+
|
200
208
|
write_http_request <<~HTTP.crlf_lines
|
201
209
|
POST /foo HTTP/1.1
|
202
210
|
Server: foo.com
|
@@ -452,23 +460,6 @@ class HTTP1AdapterTest < Minitest::Test
|
|
452
460
|
p e.backtrace.join("\n")
|
453
461
|
end
|
454
462
|
|
455
|
-
opts = {
|
456
|
-
upgrade: {
|
457
|
-
echo: lambda do |adapter, _headers|
|
458
|
-
conn = adapter.conn
|
459
|
-
conn << <<~HTTP.crlf_lines
|
460
|
-
HTTP/1.1 101 Switching Protocols
|
461
|
-
Upgrade: echo
|
462
|
-
Connection: Upgrade
|
463
|
-
|
464
|
-
HTTP
|
465
|
-
|
466
|
-
conn.read_loop { |data| conn << data }
|
467
|
-
done = true
|
468
|
-
end
|
469
|
-
}
|
470
|
-
}
|
471
|
-
|
472
463
|
msg = "GET / HTTP/1.1\r\nUpgrade: echo\r\nConnection: upgrade\r\n\r\n"
|
473
464
|
write_http_request(msg, false)
|
474
465
|
@machine.spin { @adapter.serve_request rescue nil }
|
data/test/test_server.rb
CHANGED
@@ -2,9 +2,6 @@
|
|
2
2
|
|
3
3
|
require_relative './helper'
|
4
4
|
|
5
|
-
class String
|
6
|
-
end
|
7
|
-
|
8
5
|
class ServerTest < Minitest::Test
|
9
6
|
def make_socket_pair
|
10
7
|
port = 10000 + rand(30000)
|
@@ -28,7 +25,7 @@ class ServerTest < Minitest::Test
|
|
28
25
|
def setup
|
29
26
|
@machine = UM.new
|
30
27
|
@port = 10000 + rand(30000)
|
31
|
-
@server = TP2::Server.new(@machine,
|
28
|
+
@server = TP2::Server.new(@machine, { bind: "127.0.0.1:#{@port}" }) { @app&.call(it) }
|
32
29
|
@f_server = @machine.spin do
|
33
30
|
@server.run
|
34
31
|
rescue STOP
|
@@ -106,9 +103,17 @@ class ServerTest < Minitest::Test
|
|
106
103
|
end
|
107
104
|
|
108
105
|
def test_pipelined_requests_with_body
|
109
|
-
skip
|
106
|
+
# skip
|
110
107
|
|
111
|
-
|
108
|
+
@bodies = []
|
109
|
+
@reqs = []
|
110
|
+
@app = ->(req) {
|
111
|
+
@reqs << req
|
112
|
+
@bodies << req.read
|
113
|
+
req.respond("method: #{req.method}")
|
114
|
+
}
|
115
|
+
|
116
|
+
msg = <<~HTTP.crlf_lines
|
112
117
|
POST /foo HTTP/1.1
|
113
118
|
Server: foo.com
|
114
119
|
Content-Length: 3
|
@@ -119,12 +124,10 @@ class ServerTest < Minitest::Test
|
|
119
124
|
|
120
125
|
defghi
|
121
126
|
HTTP
|
127
|
+
write_http_request msg
|
122
128
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
@adapter.run
|
127
|
-
assert_equal 2, @reqs.size
|
129
|
+
read_client_side
|
130
|
+
assert_equal 2, @bodies.size
|
128
131
|
|
129
132
|
req0 = @reqs.shift
|
130
133
|
headers = req0.headers
|
data/tp2.gemspec
CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.extra_rdoc_files = ['README.md']
|
19
19
|
s.require_paths = ['lib']
|
20
20
|
s.required_ruby_version = '>= 3.4'
|
21
|
+
s.executables = ['tp2']
|
21
22
|
|
22
|
-
s.add_dependency 'uringmachine', '0.
|
23
|
+
s.add_dependency 'uringmachine', '0.12'
|
23
24
|
s.add_dependency 'qeweney', '0.21'
|
25
|
+
s.add_dependency 'rack', '3.1.15'
|
24
26
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tp2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.7'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -15,14 +15,14 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - '='
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '0.
|
18
|
+
version: '0.12'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - '='
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '0.
|
25
|
+
version: '0.12'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: qeweney
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,22 +37,41 @@ dependencies:
|
|
37
37
|
- - '='
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '0.21'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rack
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - '='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.1.15
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.1.15
|
40
54
|
email: sharon@noteflakes.com
|
41
|
-
executables:
|
55
|
+
executables:
|
56
|
+
- tp2
|
42
57
|
extensions: []
|
43
58
|
extra_rdoc_files:
|
44
59
|
- README.md
|
45
60
|
files:
|
61
|
+
- ".github/workflows/test.yml"
|
46
62
|
- ".gitignore"
|
47
63
|
- CHANGELOG.md
|
48
64
|
- Gemfile
|
49
|
-
- Gemfile.lock
|
50
65
|
- README.md
|
51
66
|
- Rakefile
|
52
67
|
- TODO.md
|
68
|
+
- bin/tp2
|
69
|
+
- examples/app.rb
|
70
|
+
- examples/rack.ru
|
53
71
|
- examples/simple.rb
|
54
72
|
- lib/tp2.rb
|
55
73
|
- lib/tp2/http1_adapter.rb
|
74
|
+
- lib/tp2/rack_adapter.rb
|
56
75
|
- lib/tp2/request_extensions.rb
|
57
76
|
- lib/tp2/server.rb
|
58
77
|
- lib/tp2/version.rb
|
data/Gemfile.lock
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
tp2 (0.5)
|
5
|
-
qeweney (= 0.21)
|
6
|
-
uringmachine (= 0.8)
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
11
|
-
docile (1.4.1)
|
12
|
-
escape_utils (1.3.0)
|
13
|
-
ffi (1.17.2-aarch64-linux-gnu)
|
14
|
-
ffi (1.17.2-aarch64-linux-musl)
|
15
|
-
ffi (1.17.2-arm-linux-gnu)
|
16
|
-
ffi (1.17.2-arm-linux-musl)
|
17
|
-
ffi (1.17.2-arm64-darwin)
|
18
|
-
ffi (1.17.2-x86_64-darwin)
|
19
|
-
ffi (1.17.2-x86_64-linux-gnu)
|
20
|
-
ffi (1.17.2-x86_64-linux-musl)
|
21
|
-
listen (3.9.0)
|
22
|
-
rb-fsevent (~> 0.10, >= 0.10.3)
|
23
|
-
rb-inotify (~> 0.9, >= 0.9.10)
|
24
|
-
minitest (5.25.4)
|
25
|
-
qeweney (0.21)
|
26
|
-
escape_utils (= 1.3.0)
|
27
|
-
rake (13.2.1)
|
28
|
-
rake-compiler (1.2.9)
|
29
|
-
rake
|
30
|
-
rb-fsevent (0.11.2)
|
31
|
-
rb-inotify (0.11.1)
|
32
|
-
ffi (~> 1.0)
|
33
|
-
simplecov (0.22.0)
|
34
|
-
docile (~> 1.1)
|
35
|
-
simplecov-html (~> 0.11)
|
36
|
-
simplecov_json_formatter (~> 0.1)
|
37
|
-
simplecov-html (0.13.1)
|
38
|
-
simplecov_json_formatter (0.1.4)
|
39
|
-
uringmachine (0.8)
|
40
|
-
yard (0.9.37)
|
41
|
-
|
42
|
-
PLATFORMS
|
43
|
-
aarch64-linux-gnu
|
44
|
-
aarch64-linux-musl
|
45
|
-
arm-linux-gnu
|
46
|
-
arm-linux-musl
|
47
|
-
arm64-darwin
|
48
|
-
x86_64-darwin
|
49
|
-
x86_64-linux-gnu
|
50
|
-
x86_64-linux-musl
|
51
|
-
|
52
|
-
DEPENDENCIES
|
53
|
-
listen (= 3.9.0)
|
54
|
-
minitest (= 5.25.4)
|
55
|
-
rake (= 13.2.1)
|
56
|
-
rake-compiler (= 1.2.9)
|
57
|
-
simplecov (= 0.22.0)
|
58
|
-
tp2!
|
59
|
-
yard (= 0.9.37)
|
60
|
-
|
61
|
-
BUNDLED WITH
|
62
|
-
2.6.2
|