lowloop 0.8.3 → 0.8.4
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/lib/connections/connection_manager.rb +13 -3
- data/lib/connections/request_parser.rb +4 -3
- data/lib/lowloop.rb +22 -8
- data/lib/responses/response_builder.rb +20 -16
- data/lib/responses/response_factory.rb +14 -3
- data/lib/support/low_frame.rb +14 -0
- data/lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86ba32309fe8da07372146ac9ebda8cfb628eb5ed6028585f6372b35d88c7c01
|
|
4
|
+
data.tar.gz: 703acb9eda25c293dc701b1f0c255db0a2a0c4592b000637c2af2708b2bf8f55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26a1b66b508158426fa93749757976b0c24e4b1143a1a9b2630f7074ead1c17302a82efe7e0e82846f76afc7d2bb680e733bd6cc8da1d34e35603549b2c8068e
|
|
7
|
+
data.tar.gz: 64965b0ded7fe458cc2da52cd6c89011a26a9c87c1d122b3e618ed8c67ce761df98fe26f2afcb09cb5b7ce53cf474b3475a2c74117c583b255909078667b3498
|
|
@@ -4,14 +4,17 @@ require 'io/wait'
|
|
|
4
4
|
|
|
5
5
|
require_relative 'request_parser'
|
|
6
6
|
require_relative '../responses/response_builder'
|
|
7
|
+
require_relative '../responses/response_factory'
|
|
7
8
|
|
|
8
9
|
module Low
|
|
9
10
|
class ConnectionManager
|
|
10
11
|
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
|
11
12
|
DEFAULT_REQUEST_TIMEOUT = 10
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
# on_error is called with an error raised while responding to a request, before responding with a 500.
|
|
15
|
+
def initialize(config:, on_error: nil)
|
|
14
16
|
@config = config
|
|
17
|
+
@on_error = on_error
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def handle_connection(socket:)
|
|
@@ -35,8 +38,7 @@ module Low
|
|
|
35
38
|
version ||= request.version
|
|
36
39
|
keep_alive = keep_alive?(request)
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
response = response_event.response
|
|
41
|
+
response = respond_to(request:)
|
|
40
42
|
|
|
41
43
|
ResponseBuilder.respond(config:, socket:, response:, keep_alive:)
|
|
42
44
|
end
|
|
@@ -65,5 +67,13 @@ module Low
|
|
|
65
67
|
private
|
|
66
68
|
|
|
67
69
|
attr_reader :config
|
|
70
|
+
|
|
71
|
+
# An error in the application responds with a 500 and keeps the connection, rather than closing it without a response.
|
|
72
|
+
def respond_to(request:)
|
|
73
|
+
Events::RequestEvent.take(request:).response
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
@on_error&.call(e)
|
|
76
|
+
ResponseFactory.error(error: e, debug: config.debug_mode)
|
|
77
|
+
end
|
|
68
78
|
end
|
|
69
79
|
end
|
|
@@ -66,10 +66,11 @@ module Low
|
|
|
66
66
|
def parse_body(stream: IO::Stream::Generic, method: String, headers: ::Protocol::HTTP::Headers)
|
|
67
67
|
return nil unless %w[POST PUT PATCH].include?(method)
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# A single value header like content-length is a String.
|
|
70
|
+
content_length = headers['content-length'].to_i
|
|
71
|
+
return nil unless content_length.positive?
|
|
71
72
|
|
|
72
|
-
stream.read(content_length)
|
|
73
|
+
::Protocol::HTTP::Body::Buffered.wrap(stream.read(content_length))
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
data/lib/lowloop.rb
CHANGED
|
@@ -19,7 +19,7 @@ class LowLoop
|
|
|
19
19
|
|
|
20
20
|
def initialize(config:, router: nil, renderer: nil, show_output: true)
|
|
21
21
|
@config = config
|
|
22
|
-
@connection_manager = Low::ConnectionManager.new(config:)
|
|
22
|
+
@connection_manager = Low::ConnectionManager.new(config:, on_error: method(:render_error))
|
|
23
23
|
@frame = LowFrame.new(renderer: (renderer if config.matrix_mode), fps: 10, show_output:)
|
|
24
24
|
|
|
25
25
|
Low::Events::RequestEvent.define do |observers|
|
|
@@ -37,6 +37,7 @@ class LowLoop
|
|
|
37
37
|
|
|
38
38
|
loop do
|
|
39
39
|
socket = server.accept
|
|
40
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
|
|
40
41
|
|
|
41
42
|
task.async do
|
|
42
43
|
@connection_manager.handle_connection(socket:)
|
|
@@ -52,7 +53,7 @@ class LowLoop
|
|
|
52
53
|
# Fallback mode for when there's no dependencies and you want to know that the server is still working.
|
|
53
54
|
def mirror(event:)
|
|
54
55
|
request = event.request
|
|
55
|
-
response =
|
|
56
|
+
response = Low::ResponseFactory.html(body: "Thank you for visiting #{request.path} with body: '#{request.body&.join}'")
|
|
56
57
|
Low::Events::ResponseEvent.new(response:)
|
|
57
58
|
end
|
|
58
59
|
|
|
@@ -87,13 +88,26 @@ class LowLoop
|
|
|
87
88
|
puts Paint[error.message, :red]
|
|
88
89
|
puts ''
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Fiber.blocking do
|
|
93
|
-
puts Paint[error.backtrace.join("\n"), :blue]
|
|
91
|
+
if @config.debug_mode
|
|
92
|
+
puts Paint[error.backtrace&.join("\n"), :blue]
|
|
94
93
|
puts ''
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
pause_frame if @frame.renderer
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Renderers replaces the screen every frame, so pause it to keep the error on screen until ENTER is pressed.
|
|
100
|
+
def pause_frame
|
|
101
|
+
return if @frame.paused?
|
|
102
|
+
|
|
103
|
+
@frame.pause
|
|
104
|
+
puts 'Press ENTER to continue...'
|
|
105
|
+
|
|
106
|
+
# Non-blockinly wait for IO. The main event loop keeps responding to requests while the frame is paused.
|
|
107
|
+
Async do
|
|
108
|
+
$stdin.gets
|
|
109
|
+
ensure
|
|
110
|
+
@frame.resume
|
|
97
111
|
end
|
|
98
112
|
end
|
|
99
113
|
end
|
|
@@ -10,44 +10,48 @@ module Low
|
|
|
10
10
|
if file_body
|
|
11
11
|
content_length = response.body.file.size
|
|
12
12
|
else
|
|
13
|
-
|
|
13
|
+
# A response can have no body, like a node returning "Status[201]", and "join" reads every chunk of a body.
|
|
14
|
+
body_data = response.body&.join || ''
|
|
14
15
|
content_length = body_data.bytesize
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# Build the whole head in memory then write once, as every socket write is a syscall.
|
|
19
|
+
head = +''
|
|
20
|
+
write_status_line(head, response)
|
|
21
|
+
write_host_header(head, config)
|
|
22
|
+
write_response_headers(head, response)
|
|
23
|
+
write_final_headers(head, content_length, keep_alive)
|
|
21
24
|
|
|
22
25
|
if file_body
|
|
26
|
+
socket.write(head)
|
|
23
27
|
IO.copy_stream(response.body.file, socket)
|
|
24
28
|
else
|
|
25
|
-
socket.write(body_data)
|
|
29
|
+
socket.write(head << body_data)
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
private
|
|
30
34
|
|
|
31
|
-
def write_status_line(
|
|
32
|
-
|
|
35
|
+
def write_status_line(head, response)
|
|
36
|
+
head << "#{response.version} #{response.status}\r\n"
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
def write_host_header(
|
|
36
|
-
|
|
39
|
+
def write_host_header(head, config)
|
|
40
|
+
head << (config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n")
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
def write_response_headers(
|
|
43
|
+
def write_response_headers(head, response)
|
|
40
44
|
response.headers.fields.each do |key, value|
|
|
41
45
|
next if %w[content-length connection].include?(key.to_s.downcase)
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
head << "#{key}: #{value}\r\n"
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
|
|
47
|
-
def write_final_headers(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
def write_final_headers(head, content_length, keep_alive)
|
|
52
|
+
head << "Content-Length: #{content_length}\r\n"
|
|
53
|
+
head << "Connection: #{keep_alive ? 'keep-alive' : 'close'}\r\n"
|
|
54
|
+
head << "\r\n"
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi'
|
|
3
4
|
require 'protocol/http'
|
|
4
5
|
require 'protocol/http/body/file'
|
|
5
6
|
|
|
@@ -9,7 +10,6 @@ module Low
|
|
|
9
10
|
def html(body:)
|
|
10
11
|
headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
|
|
11
12
|
body = Protocol::HTTP::Body::Buffered.wrap(body.strip)
|
|
12
|
-
|
|
13
13
|
Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -17,13 +17,24 @@ module Low
|
|
|
17
17
|
headers = Protocol::HTTP::Headers.new([['content-type', content_type]])
|
|
18
18
|
file = File.open(path, 'rb')
|
|
19
19
|
body = Protocol::HTTP::Body::File.new(file)
|
|
20
|
-
|
|
21
20
|
Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
# Shows the error and where it happened in debug mode (or show a 500 status).
|
|
24
|
+
def error(error:, debug: false)
|
|
25
25
|
headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
|
|
26
|
+
trace = CGI.escapeHTML("#{error.class}: #{error.message}\n\n#{error.backtrace&.join("\n")}") if debug
|
|
27
|
+
|
|
28
|
+
html = <<~HTML.strip
|
|
29
|
+
<h1>500 Internal Server Error</h1>
|
|
30
|
+
#{"<pre>#{trace}</pre>" if debug}
|
|
31
|
+
HTML
|
|
26
32
|
|
|
33
|
+
Protocol::HTTP::Response.new('HTTP/1.1', 500, headers, Protocol::HTTP::Body::Buffered.wrap(html))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def status(code:)
|
|
37
|
+
headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
|
|
27
38
|
Protocol::HTTP::Response.new('HTTP/1.1', code, headers, nil)
|
|
28
39
|
end
|
|
29
40
|
end
|
data/lib/support/low_frame.rb
CHANGED
|
@@ -20,12 +20,26 @@ class LowFrame
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def render
|
|
23
|
+
return if @paused
|
|
23
24
|
return unless @last_frame.nil? || (current_timestamp - @last_frame) >= @frame_time
|
|
24
25
|
|
|
25
26
|
@last_frame = current_timestamp
|
|
26
27
|
@renderer.render(screen_size: @screen_size)
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Stops rendering so that output isn't replaced by the next frame.
|
|
31
|
+
def pause
|
|
32
|
+
@paused = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resume
|
|
36
|
+
@paused = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def paused?
|
|
40
|
+
@paused == true
|
|
41
|
+
end
|
|
42
|
+
|
|
29
43
|
def setup
|
|
30
44
|
print "\e[?25l" # Hide cursor.
|
|
31
45
|
system 'clear'
|
data/lib/version.rb
CHANGED