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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bcc48c80995bdc907a7340bfd70b3b643cbdba6009a0860653789cce9dd309d
4
- data.tar.gz: 75c29bd962bb6907de4c9937b065febfb3f5e0de5aa0a8b9e65dbaa1da79f303
3
+ metadata.gz: 86ba32309fe8da07372146ac9ebda8cfb628eb5ed6028585f6372b35d88c7c01
4
+ data.tar.gz: 703acb9eda25c293dc701b1f0c255db0a2a0c4592b000637c2af2708b2bf8f55
5
5
  SHA512:
6
- metadata.gz: f71e8ed10a287dfad978f62c1536267e294d43f2256df10c1e35bfacf592d59276acd9d5d20400a6cc5e6be0926ba62fc849c4335f19f4b4d5f44d797134fc93
7
- data.tar.gz: 8dd496d5e6c21032b5b1ecad04823918efa69c63a4a5ac0cef974d3eafb13350b5993eced6ec93575486e426e76cefaaae371397e5f4a46feae75a3d824b648d
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
- def initialize(config:)
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
- response_event = Events::RequestEvent.take(request:)
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
- content_length = headers['content-length']&.first&.to_i
70
- return nil unless content_length&.positive?
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 = tories::ResponseFactory.html(body: "Thank you for visiting #{request.path} with body: '#{request.body}'")
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
- return unless @config.debug_mode
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
- puts 'Press ENTER to continue...'
96
- gets
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
- body_data = response.body.read || ''
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
- write_status_line(socket, response)
18
- write_host_header(socket, config)
19
- write_response_headers(socket, response)
20
- write_final_headers(socket, content_length, keep_alive)
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(socket, response)
32
- socket.puts "#{response.version} #{response.status}\r\n"
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(socket, config)
36
- socket.puts config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n"
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(socket, response)
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
- socket.puts "#{key}: #{value}\r\n"
47
+ head << "#{key}: #{value}\r\n"
44
48
  end
45
49
  end
46
50
 
47
- def write_final_headers(socket, content_length, keep_alive)
48
- socket.puts "Content-Length: #{content_length}\r\n"
49
- socket.puts "Connection: #{keep_alive ? 'keep-alive' : 'close'}\r\n"
50
- socket.puts "\r\n"
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
- def status(code:)
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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Loop
5
- VERSION = '0.8.3'
5
+ VERSION = '0.8.4'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lowloop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi