syntropy 0.39.0 → 0.41.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/CHANGELOG.md +8 -0
- data/TODO.md +44 -142
- data/cmd/console.rb +0 -2
- data/cmd/new/template/config/production.rb +2 -1
- data/cmd/new.rb +1 -1
- data/cmd/serve.rb +26 -0
- data/examples/agent/Dockerfile +25 -0
- data/examples/agent/Gemfile +3 -0
- data/examples/agent/README.md +40 -0
- data/examples/agent/TODO.md +5 -0
- data/examples/agent/app/_layout/default.rb +15 -0
- data/examples/agent/app/_lib/storage.rb +13 -0
- data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/agent/app/assets/agent.js +63 -0
- data/examples/agent/app/assets/minigfm.js +207 -0
- data/examples/agent/app/assets/model.js +45 -0
- data/examples/agent/app/assets/style.css +25 -0
- data/examples/agent/app/assets/tools.js +74 -0
- data/examples/agent/app/assets/ui.js +50 -0
- data/examples/agent/app/index.rb +19 -0
- data/examples/agent/app/test.rb +7 -0
- data/examples/agent/config/Caddyfile +5 -0
- data/examples/agent/config/development.rb +5 -0
- data/examples/agent/config/production.rb +5 -0
- data/examples/agent/config/test.rb +5 -0
- data/examples/agent/docker-compose.yml +71 -0
- data/examples/agent/response_example.json +108 -0
- data/examples/agent/test/test_app.rb +14 -0
- data/examples/blog/app/foo.rb +1 -0
- data/examples/blog/config/production.rb +2 -1
- data/lib/syntropy/app.rb +6 -2
- data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
- data/lib/syntropy/dev_mode.rb +8 -3
- data/lib/syntropy/http/client.rb +17 -1
- data/lib/syntropy/http/client_connection.rb +16 -0
- data/lib/syntropy/http/io_extensions.rb +32 -4
- data/lib/syntropy/http/server.rb +55 -24
- data/lib/syntropy/http/server_connection.rb +75 -4
- data/lib/syntropy/logger.rb +8 -0
- data/lib/syntropy/markdown.rb +44 -20
- data/lib/syntropy/module_loader.rb +29 -16
- data/lib/syntropy/request/request_info.rb +1 -1
- data/lib/syntropy/request/response.rb +75 -6
- data/lib/syntropy/request/validation.rb +6 -0
- data/lib/syntropy/storage/connection_pool.rb +34 -0
- data/lib/syntropy/test.rb +4 -4
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +7 -1
- data/syntropy.gemspec +4 -4
- data/test/test_markdown.rb +3 -3
- data/test/test_module_loader.rb +4 -1
- metadata +34 -11
data/lib/syntropy/app.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Syntropy
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
# Creates a builtin applet with the given environment hash. By default the
|
|
31
31
|
# builtin applet is mounted at /.syntropy.
|
|
32
32
|
#
|
|
@@ -102,6 +102,7 @@ module Syntropy
|
|
|
102
102
|
# @param req [Syntropy::Request] HTTP request
|
|
103
103
|
# @return [void]
|
|
104
104
|
def call(req)
|
|
105
|
+
# before = GC.stat(:total_allocated_objects)
|
|
105
106
|
path = req.path
|
|
106
107
|
route = @router_proc.(path, req.route_params)
|
|
107
108
|
if !route
|
|
@@ -126,6 +127,9 @@ module Syntropy
|
|
|
126
127
|
end
|
|
127
128
|
error_handler = get_error_handler(route)
|
|
128
129
|
error_handler.(req, e)
|
|
130
|
+
# ensure
|
|
131
|
+
# after = GC.stat(:total_allocated_objects)
|
|
132
|
+
# @logger&.debug(message: "Allocated: #{after - before}")
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
# Returns the route entry for the given path. If compute_proc is true,
|
|
@@ -483,7 +487,7 @@ module Syntropy
|
|
|
483
487
|
#
|
|
484
488
|
# @return [void]
|
|
485
489
|
def start
|
|
486
|
-
@module_loader.load('_setup',
|
|
490
|
+
@module_loader.load('_setup', raise_on_missing_export: false)
|
|
487
491
|
|
|
488
492
|
@machine.spin do
|
|
489
493
|
# we do startup stuff asynchronously, in order to first let Syntropy do
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
# residing by default at `/.syntropy/auto_refresh/watch.js`.
|
|
10
10
|
|
|
11
11
|
# Returns a hash holding references to queues for ongoing `watch.sse` requests.
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash] hash of watchers
|
|
12
14
|
def watchers
|
|
13
15
|
@watchers ||= {}
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# Signals a file change by pushing to all watcher queues.
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
17
21
|
def signal!
|
|
18
22
|
watchers.each_key { @machine.push(it, true) }
|
|
19
23
|
end
|
|
@@ -21,6 +25,9 @@ end
|
|
|
21
25
|
# Handles incoming requests to the `watch.sse` route. Adds a queue to the list
|
|
22
26
|
# of watchers, and waits for the queue to be signalled. In the absence of file
|
|
23
27
|
# change, a timeout occurs after one minute, and the request is terminated.
|
|
28
|
+
#
|
|
29
|
+
# @param req [Syntropy::Request] request
|
|
30
|
+
# @return [void]
|
|
24
31
|
def call(req)
|
|
25
32
|
queue = UM::Queue.new
|
|
26
33
|
watchers[queue] = true
|
data/lib/syntropy/dev_mode.rb
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Injects debug information into Papercraft emitted HTML tags.
|
|
4
|
+
#
|
|
5
|
+
# @param level [Integer] nesting level
|
|
6
|
+
# @param fn [String] source filename
|
|
7
|
+
# @param line [Integer] source line
|
|
8
|
+
# @param col [Integer] source column
|
|
9
|
+
# @return [Hash] HTML data attributes
|
|
10
|
+
Papercraft::Compiler.html_debug_attribute_injector = ->(level, fn, line, col) {
|
|
4
11
|
{
|
|
5
12
|
'data-syntropy-level' => level,
|
|
6
13
|
'data-syntropy-fn' => fn,
|
|
7
14
|
'data-syntropy-loc' => "zed://file/#{fn}:#{line}:#{col}"
|
|
8
15
|
}
|
|
9
16
|
}
|
|
10
|
-
|
|
11
|
-
Papercraft::Compiler.html_debug_attribute_injector = TAG_DEBUG_PROC
|
data/lib/syntropy/http/client.rb
CHANGED
|
@@ -7,10 +7,16 @@ module Syntropy
|
|
|
7
7
|
module HTTP
|
|
8
8
|
# HTTP Client class.
|
|
9
9
|
class Client
|
|
10
|
+
# Initializes an HTTP client.
|
|
10
11
|
def initialize(machine)
|
|
11
12
|
@machine = machine
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
# Performs a GET request.
|
|
16
|
+
#
|
|
17
|
+
# @param url [String] URL
|
|
18
|
+
# @param headers [Hash] request headers
|
|
19
|
+
# @return [Array] array containing response headers and body
|
|
14
20
|
def get(url, **headers, &)
|
|
15
21
|
uri = URI.parse(url)
|
|
16
22
|
headers = headers.merge(
|
|
@@ -22,7 +28,11 @@ module Syntropy
|
|
|
22
28
|
|
|
23
29
|
private
|
|
24
30
|
|
|
25
|
-
#
|
|
31
|
+
# Performs an HTTP request, returning the response headers and body.
|
|
32
|
+
#
|
|
33
|
+
# @param uri [URI] request URI
|
|
34
|
+
# @param headers [Hash] request headers
|
|
35
|
+
# @return [Array] array containing response headers and body
|
|
26
36
|
def req(uri, **headers)
|
|
27
37
|
connection = make_connection(uri.scheme, uri.host, uri.port)
|
|
28
38
|
response_headers = connection.req(**headers)
|
|
@@ -33,6 +43,12 @@ module Syntropy
|
|
|
33
43
|
end
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
# Creates an HTTP connection.
|
|
47
|
+
#
|
|
48
|
+
# @param _scheme [String] connection scheme
|
|
49
|
+
# @param host [String] host
|
|
50
|
+
# @param port [Integer] port
|
|
51
|
+
# @return [Syntropy::HTTP::ClientConnection]
|
|
36
52
|
def make_connection(_scheme, host, port)
|
|
37
53
|
ip = (host =~ /^\d+\.\d+\.\d+\.\d+$/) ? host : @machine.resolve(host)[0]
|
|
38
54
|
|
|
@@ -5,15 +5,27 @@ require 'syntropy/http/io_extensions'
|
|
|
5
5
|
|
|
6
6
|
module Syntropy
|
|
7
7
|
module HTTP
|
|
8
|
+
# ClientConnection represents an HTTP client connection.
|
|
8
9
|
class ClientConnection
|
|
9
10
|
attr_reader :fd, :response_headers, :logger
|
|
10
11
|
|
|
12
|
+
# Initializes a ClientConnection.
|
|
13
|
+
#
|
|
14
|
+
# @param machine [UringMachine] machine instance
|
|
15
|
+
# @param fd [Integer] file descriptor
|
|
16
|
+
# @param io_mode [Symbol] IO mode
|
|
17
|
+
# @return [void]
|
|
11
18
|
def initialize(machine, fd, io_mode: :socket)
|
|
12
19
|
@machine = machine
|
|
13
20
|
@fd = fd
|
|
14
21
|
@io = machine.io(fd, io_mode)
|
|
15
22
|
end
|
|
16
23
|
|
|
24
|
+
# Performs a request with the given body and headers.
|
|
25
|
+
#
|
|
26
|
+
# @param body [String, nil] request body
|
|
27
|
+
# @param headers [Hash] request headers
|
|
28
|
+
# @return [Hash] response headers
|
|
17
29
|
def req(body: nil, **headers)
|
|
18
30
|
if body
|
|
19
31
|
headers = headers.merge(
|
|
@@ -28,6 +40,10 @@ module Syntropy
|
|
|
28
40
|
@io.http_read_response_headers
|
|
29
41
|
end
|
|
30
42
|
|
|
43
|
+
# Returns the response body
|
|
44
|
+
#
|
|
45
|
+
# @param headers [Hash] response headers
|
|
46
|
+
# @return [String, nil] response body
|
|
31
47
|
def get_response_body(headers)
|
|
32
48
|
@io.http_read_body(headers)
|
|
33
49
|
end
|
|
@@ -4,6 +4,7 @@ require 'syntropy/errors'
|
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
6
|
module HTTP
|
|
7
|
+
# HTTP protocol extensions for UringMachine::IO
|
|
7
8
|
module ProtocolMethods
|
|
8
9
|
RE_REQUEST_LINE = /^(get|head|options|trace|put|delete|post|patch|connect)\s+([^\s]+)\s+HTTP\/([019\.]{1,3})/i
|
|
9
10
|
RE_RESPONSE_LINE = /^HTTP\/1\.1\s+(\d{3})(\s+.+)?$/i
|
|
@@ -14,7 +15,9 @@ module Syntropy
|
|
|
14
15
|
MAX_HEADER_LINE_LEN = 1 << 13 # 8KB
|
|
15
16
|
MAX_CHUNK_SIZE_LEN = 16
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
+
# Reads HTTP request headers.
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash] request headers
|
|
18
21
|
def http_read_request_headers
|
|
19
22
|
line = read_line(MAX_REQUEST_LINE_LEN)
|
|
20
23
|
return nil if !line
|
|
@@ -43,6 +46,9 @@ module Syntropy
|
|
|
43
46
|
headers
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
# Reads HTTP response headers.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] response headers
|
|
46
52
|
def http_read_response_headers
|
|
47
53
|
line = read_line(MAX_RESPONSE_LINE_LEN)
|
|
48
54
|
return nil if !line
|
|
@@ -73,6 +79,10 @@ module Syntropy
|
|
|
73
79
|
headers
|
|
74
80
|
end
|
|
75
81
|
|
|
82
|
+
# Reads an HTTP request/response body.
|
|
83
|
+
#
|
|
84
|
+
# @param headers [Hash] request/response headers
|
|
85
|
+
# @return [String] body
|
|
76
86
|
def http_read_body(headers)
|
|
77
87
|
content_length = headers['content-length']
|
|
78
88
|
if content_length
|
|
@@ -95,6 +105,10 @@ module Syntropy
|
|
|
95
105
|
nil
|
|
96
106
|
end
|
|
97
107
|
|
|
108
|
+
# Skips an HTTP reuqest/response body.
|
|
109
|
+
#
|
|
110
|
+
# @param headers [Hash] request/response headers
|
|
111
|
+
# @return [void]
|
|
98
112
|
def http_skip_body(headers)
|
|
99
113
|
content_length = headers['content-length']
|
|
100
114
|
if content_length
|
|
@@ -109,10 +123,12 @@ module Syntropy
|
|
|
109
123
|
while http_skip_cte_chunk
|
|
110
124
|
end
|
|
111
125
|
end
|
|
112
|
-
|
|
113
|
-
nil
|
|
114
126
|
end
|
|
115
127
|
|
|
128
|
+
# Reads an HTTP body chunk. If no chunks remain to be read, returns nil.
|
|
129
|
+
#
|
|
130
|
+
# @param headers [Hash] request/response headers
|
|
131
|
+
# @return [String, nil] body chunk
|
|
116
132
|
def http_read_body_chunk(headers)
|
|
117
133
|
content_length = headers['content-length']
|
|
118
134
|
if content_length
|
|
@@ -129,6 +145,10 @@ module Syntropy
|
|
|
129
145
|
nil
|
|
130
146
|
end
|
|
131
147
|
|
|
148
|
+
# Writes HTTP request headers.
|
|
149
|
+
#
|
|
150
|
+
# @param headers [Hash] request headers
|
|
151
|
+
# @return [void]
|
|
132
152
|
def http_write_request_headers(headers)
|
|
133
153
|
method = headers[':method'] || (raise BadRequestError)
|
|
134
154
|
path = headers[':path'] || (raise BadRequestError)
|
|
@@ -149,6 +169,11 @@ module Syntropy
|
|
|
149
169
|
|
|
150
170
|
private
|
|
151
171
|
|
|
172
|
+
# Read a chunk (in chunked transfer encoding) into the given buffer. If no
|
|
173
|
+
# buffer is given, returns the chunk, otherwise returns the buffer.
|
|
174
|
+
#
|
|
175
|
+
# @param buffer [String, nil] buffer
|
|
176
|
+
# @return [String] buffer or chunk
|
|
152
177
|
def http_read_cte_chunk(buffer)
|
|
153
178
|
chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
|
|
154
179
|
return nil if !chunk_size_str
|
|
@@ -165,6 +190,9 @@ module Syntropy
|
|
|
165
190
|
buffer ? (buffer << chunk) : chunk
|
|
166
191
|
end
|
|
167
192
|
|
|
193
|
+
# Skips a chunk (in chunked transfer encoding).
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
168
196
|
def http_skip_cte_chunk
|
|
169
197
|
chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
|
|
170
198
|
return if !chunk_size_str
|
|
@@ -172,7 +200,7 @@ module Syntropy
|
|
|
172
200
|
chunk_size = chunk_size_str.to_i(16)
|
|
173
201
|
if chunk_size == 0
|
|
174
202
|
read_line(0)
|
|
175
|
-
return
|
|
203
|
+
return
|
|
176
204
|
end
|
|
177
205
|
|
|
178
206
|
chunk = skip(chunk_size)
|
data/lib/syntropy/http/server.rb
CHANGED
|
@@ -4,41 +4,28 @@ require 'syntropy/http/server_connection'
|
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
6
|
module HTTP
|
|
7
|
+
# HTTP::Server implements an HTTP server.
|
|
7
8
|
class Server
|
|
8
9
|
PENDING_REQUESTS_GRACE_PERIOD = 0.1
|
|
9
10
|
PENDING_REQUESTS_TIMEOUT_PERIOD = 5
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
env[:app]
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.static_app(env); end
|
|
22
|
-
|
|
12
|
+
# Initializes a server instance.
|
|
13
|
+
#
|
|
14
|
+
# @param machine [UringMachine] machine instance
|
|
15
|
+
# @param env [Hash] app environment
|
|
16
|
+
# @param app [Proc, Syntropy::App] app instance
|
|
17
|
+
# @return [void]
|
|
23
18
|
def initialize(machine, env, &app)
|
|
24
19
|
@machine = machine
|
|
25
20
|
@env = env
|
|
26
|
-
@app = app
|
|
21
|
+
@app = app
|
|
27
22
|
@server_fds = []
|
|
28
23
|
@accept_fibers = []
|
|
29
24
|
end
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Server.syntropy_app(@machine, @env)
|
|
35
|
-
when :static
|
|
36
|
-
Server.static_app(@env)
|
|
37
|
-
else
|
|
38
|
-
raise "Invalid app type #{@env[:app_type].inspect}"
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
26
|
+
# Runs the server.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
42
29
|
def run
|
|
43
30
|
setup
|
|
44
31
|
@machine.await(@accept_fibers)
|
|
@@ -46,12 +33,18 @@ module Syntropy
|
|
|
46
33
|
graceful_shutdown
|
|
47
34
|
end
|
|
48
35
|
|
|
36
|
+
# Stops the server with graceful shutdown.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
49
39
|
def stop!
|
|
50
40
|
graceful_shutdown
|
|
51
41
|
end
|
|
52
42
|
|
|
53
43
|
private
|
|
54
44
|
|
|
45
|
+
# Sets up the server.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
55
48
|
def setup
|
|
56
49
|
bind_info = get_bind_entries
|
|
57
50
|
bind_info.each do |(host, port)|
|
|
@@ -67,6 +60,9 @@ module Syntropy
|
|
|
67
60
|
@connection_fibers = Set.new
|
|
68
61
|
end
|
|
69
62
|
|
|
63
|
+
# Returns bind entries from the app environment.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<Array>] array containing host/port tuples
|
|
70
66
|
def get_bind_entries
|
|
71
67
|
bind = @env[:bind]
|
|
72
68
|
case bind
|
|
@@ -80,11 +76,20 @@ module Syntropy
|
|
|
80
76
|
end
|
|
81
77
|
end
|
|
82
78
|
|
|
79
|
+
# Parses a bind string into a host/port tuple.
|
|
80
|
+
#
|
|
81
|
+
# @param bind_string [String] bind string
|
|
82
|
+
# @return [Array<String, Integer>] array containing host and port
|
|
83
83
|
def bind_info(bind_string)
|
|
84
84
|
parts = bind_string.split(':')
|
|
85
85
|
[parts[0], parts[1].to_i]
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Sets up a TCP socket listening on the given host and port.
|
|
89
|
+
#
|
|
90
|
+
# @param host [String] host
|
|
91
|
+
# @param port [Integer] port
|
|
92
|
+
# @return [Integer] socket fd
|
|
88
93
|
def setup_server_socket(host, port)
|
|
89
94
|
fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
90
95
|
@machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
|
@@ -94,6 +99,10 @@ module Syntropy
|
|
|
94
99
|
fd
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
# Setup server extensions. This is used for adding additional headers to
|
|
103
|
+
# the response, namely the Server the Date headers.
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
97
106
|
def setup_server_extensions
|
|
98
107
|
extensions = @env[:server_extensions]
|
|
99
108
|
return if !extensions
|
|
@@ -109,6 +118,10 @@ module Syntropy
|
|
|
109
118
|
end
|
|
110
119
|
end
|
|
111
120
|
|
|
121
|
+
# Updates server headers.
|
|
122
|
+
#
|
|
123
|
+
# @param server_name [String] server name
|
|
124
|
+
# @return [void]
|
|
112
125
|
def update_server_headers(server_name)
|
|
113
126
|
@env[:server_date] = Time.now
|
|
114
127
|
if server_name
|
|
@@ -118,12 +131,20 @@ module Syntropy
|
|
|
118
131
|
end
|
|
119
132
|
end
|
|
120
133
|
|
|
134
|
+
# Accepts incoming connections, spinning up a fiber for connection.
|
|
135
|
+
#
|
|
136
|
+
# @param listen_fd [Integer] listening socket fd
|
|
137
|
+
# @return [void]
|
|
121
138
|
def accept_incoming(listen_fd)
|
|
122
139
|
@machine.accept_each(listen_fd) { start_connection(it) }
|
|
123
140
|
rescue UM::Terminate
|
|
124
141
|
@machine.shutdown(listen_fd, UM::SHUT_RD)
|
|
125
142
|
end
|
|
126
143
|
|
|
144
|
+
# Starts handling a connection on a separate fiber.
|
|
145
|
+
#
|
|
146
|
+
# @param fd [Integer] socket fd
|
|
147
|
+
# @return [void]
|
|
127
148
|
def start_connection(fd)
|
|
128
149
|
conn = ServerConnection.new(@machine, fd, @env, &@app)
|
|
129
150
|
f = @machine.spin(conn) do
|
|
@@ -134,17 +155,27 @@ module Syntropy
|
|
|
134
155
|
@connection_fibers << f
|
|
135
156
|
end
|
|
136
157
|
|
|
158
|
+
# Closes all listening socket fd's.
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
137
161
|
def close_all_server_fds
|
|
138
162
|
@server_fds.each { @machine.close_async(it) }
|
|
139
163
|
end
|
|
140
164
|
|
|
141
165
|
STOP = UM::Terminate.new
|
|
142
166
|
|
|
167
|
+
# Stops all fibers listening for incoming connections.
|
|
168
|
+
#
|
|
169
|
+
# @return [void]
|
|
143
170
|
def stop_accept_fibers
|
|
144
171
|
@accept_fibers.each { @machine.schedule(it, STOP) if !it.done? }
|
|
145
172
|
@machine.await(@accept_fibers)
|
|
146
173
|
end
|
|
147
174
|
|
|
175
|
+
# Performs a graceful shutdown by stopping listening, then waiting for
|
|
176
|
+
# connection fibers to stop with a timeout of 5 seconds.
|
|
177
|
+
#
|
|
178
|
+
# @return [void]
|
|
148
179
|
def graceful_shutdown
|
|
149
180
|
@env[:logger]&.info(message: 'Shutting down gracefully...')
|
|
150
181
|
|
|
@@ -13,6 +13,14 @@ module Syntropy
|
|
|
13
13
|
class ServerConnection
|
|
14
14
|
attr_reader :fd, :response_headers, :logger
|
|
15
15
|
|
|
16
|
+
# Initializes a server connection.
|
|
17
|
+
#
|
|
18
|
+
# @param machine [UringMachine] machine instance
|
|
19
|
+
# @param fd [Integer] file descriptor
|
|
20
|
+
# @param env [Hash] app environment
|
|
21
|
+
# @param io_mode [Symbol] IO mode
|
|
22
|
+
# @param app [Proc, Syntropy::App] server app
|
|
23
|
+
# @return [void]
|
|
16
24
|
def initialize(machine, fd, env, io_mode: :socket, &app)
|
|
17
25
|
@machine = machine
|
|
18
26
|
@fd = fd
|
|
@@ -26,6 +34,9 @@ module Syntropy
|
|
|
26
34
|
@response_cookies = nil
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Runs the connection.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
29
40
|
def run
|
|
30
41
|
loop do
|
|
31
42
|
persist = serve_request
|
|
@@ -33,6 +44,10 @@ module Syntropy
|
|
|
33
44
|
end
|
|
34
45
|
rescue UM::Terminate
|
|
35
46
|
# server is terminated, do nothing
|
|
47
|
+
rescue SystemCallError
|
|
48
|
+
@logger&.error(
|
|
49
|
+
message: 'IO Error, closing...'
|
|
50
|
+
)
|
|
36
51
|
rescue StandardError => e
|
|
37
52
|
@logger&.error(
|
|
38
53
|
message: 'Uncaught error while running connection',
|
|
@@ -76,9 +91,10 @@ module Syntropy
|
|
|
76
91
|
# @return [void]
|
|
77
92
|
def handle_error(request, err)
|
|
78
93
|
case err
|
|
94
|
+
when Errno::ECONNRESET
|
|
95
|
+
# ignore
|
|
79
96
|
when SystemCallError
|
|
80
|
-
log_error(err, '
|
|
81
|
-
false
|
|
97
|
+
log_error(err, 'Syscall error')
|
|
82
98
|
when ProtocolError
|
|
83
99
|
log_error(err, err.message)
|
|
84
100
|
respond(request, err.message, ':status' => err.http_status)
|
|
@@ -99,6 +115,10 @@ module Syntropy
|
|
|
99
115
|
@logger&.error(message: "#{message}, closing connection", error: err)
|
|
100
116
|
end
|
|
101
117
|
|
|
118
|
+
# Reads the request body.
|
|
119
|
+
#
|
|
120
|
+
# @param req [Syntropy::Request] request
|
|
121
|
+
# @return [String, nil] request body
|
|
102
122
|
def get_body(req)
|
|
103
123
|
headers = req.headers
|
|
104
124
|
return nil if headers[':body-done-reading']
|
|
@@ -108,6 +128,10 @@ module Syntropy
|
|
|
108
128
|
body
|
|
109
129
|
end
|
|
110
130
|
|
|
131
|
+
# Reads a request body chunk.
|
|
132
|
+
#
|
|
133
|
+
# @param req [Syntropy::Request] request
|
|
134
|
+
# @return [String, nil] request body chunk
|
|
111
135
|
def get_body_chunk(req)
|
|
112
136
|
headers = req.headers
|
|
113
137
|
return nil if headers[':body-done-reading']
|
|
@@ -117,6 +141,9 @@ module Syntropy
|
|
|
117
141
|
chunk
|
|
118
142
|
end
|
|
119
143
|
|
|
144
|
+
# Returns true if the request is done
|
|
145
|
+
#
|
|
146
|
+
# @return [bool] whether the request is complete
|
|
120
147
|
def complete?(req)
|
|
121
148
|
req.headers[':body-done-reading']
|
|
122
149
|
end
|
|
@@ -135,6 +162,11 @@ module Syntropy
|
|
|
135
162
|
|
|
136
163
|
DELETE_COOKIE = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly"
|
|
137
164
|
|
|
165
|
+
# Adds a Set-Cookie header to the response headers.
|
|
166
|
+
#
|
|
167
|
+
# @param key [String] cookie name
|
|
168
|
+
# @param value [String] cookie value
|
|
169
|
+
# @return [void]
|
|
138
170
|
def set_cookie(key, value)
|
|
139
171
|
(@response_cookies ||= {})[key] = value || DELETE_COOKIE
|
|
140
172
|
end
|
|
@@ -210,6 +242,13 @@ module Syntropy
|
|
|
210
242
|
@done = true
|
|
211
243
|
end
|
|
212
244
|
|
|
245
|
+
# Responds by rendering a static file.
|
|
246
|
+
#
|
|
247
|
+
# @param req [Syntropy::Request] request
|
|
248
|
+
# @param path [String] file path
|
|
249
|
+
# @param env [Hash] app environment
|
|
250
|
+
# @param cache_headers [Hash] cache headers
|
|
251
|
+
# @return [void]
|
|
213
252
|
def respond_with_static_file(req, path, env, cache_headers)
|
|
214
253
|
fd = @machine.open(path, UM::O_RDONLY)
|
|
215
254
|
env ||= {}
|
|
@@ -240,6 +279,9 @@ module Syntropy
|
|
|
240
279
|
end
|
|
241
280
|
end
|
|
242
281
|
|
|
282
|
+
# Closes the connection.
|
|
283
|
+
#
|
|
284
|
+
# @return [void]
|
|
243
285
|
def close
|
|
244
286
|
return if @closed
|
|
245
287
|
|
|
@@ -248,6 +290,9 @@ module Syntropy
|
|
|
248
290
|
@machine.close_async(@fd)
|
|
249
291
|
end
|
|
250
292
|
|
|
293
|
+
# Yields the uhnderlying connection IO and fd to the given block.
|
|
294
|
+
#
|
|
295
|
+
# @return [void]
|
|
251
296
|
def with_stream
|
|
252
297
|
yield @io, @fd
|
|
253
298
|
end
|
|
@@ -260,6 +305,9 @@ module Syntropy
|
|
|
260
305
|
MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
|
|
261
306
|
MAX_CHUNK_SIZE_LEN = 16
|
|
262
307
|
|
|
308
|
+
# Returns true if the connection should be persisted.
|
|
309
|
+
#
|
|
310
|
+
# @param headers [Hash] request headers
|
|
263
311
|
def persist_connection?(headers)
|
|
264
312
|
connection = headers['connection']&.downcase
|
|
265
313
|
return connection != 'close'
|
|
@@ -285,14 +333,23 @@ module Syntropy
|
|
|
285
333
|
lines
|
|
286
334
|
end
|
|
287
335
|
|
|
336
|
+
# Formats a response status line.
|
|
337
|
+
#
|
|
338
|
+
# @param body [String] response body
|
|
339
|
+
# @param status [Integer] HTTP status code
|
|
340
|
+
# @return [String] rendered status line
|
|
288
341
|
def format_status_line(body, status)
|
|
289
342
|
if !body
|
|
290
343
|
empty_status_line(status)
|
|
291
344
|
else
|
|
292
|
-
with_body_status_line(
|
|
345
|
+
with_body_status_line(body, status)
|
|
293
346
|
end
|
|
294
347
|
end
|
|
295
348
|
|
|
349
|
+
# Returns the status line for an empty response.
|
|
350
|
+
#
|
|
351
|
+
# @param status [Integer] HTTP status code
|
|
352
|
+
# @return [String] rendered status line
|
|
296
353
|
def empty_status_line(status)
|
|
297
354
|
if status == 204
|
|
298
355
|
+"HTTP/1.1 #{status}\r\n"
|
|
@@ -301,10 +358,21 @@ module Syntropy
|
|
|
301
358
|
end
|
|
302
359
|
end
|
|
303
360
|
|
|
304
|
-
|
|
361
|
+
# Returns a status line for responses with body.
|
|
362
|
+
#
|
|
363
|
+
# @param body [String] response body
|
|
364
|
+
# @param status [Integer] HTTP status code
|
|
365
|
+
# @return [String] rendered status line
|
|
366
|
+
def with_body_status_line(body, status)
|
|
305
367
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
|
306
368
|
end
|
|
307
369
|
|
|
370
|
+
# Renders headers into a lines array.
|
|
371
|
+
#
|
|
372
|
+
# @param lines [Array<String>] array of header lines
|
|
373
|
+
# @param key [String] header name
|
|
374
|
+
# @param value [String, Array<String>] header value(s)
|
|
375
|
+
# @return [void]
|
|
308
376
|
def collect_header_lines(lines, key, value)
|
|
309
377
|
if value.is_a?(Array)
|
|
310
378
|
value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
|
|
@@ -313,6 +381,9 @@ module Syntropy
|
|
|
313
381
|
end
|
|
314
382
|
end
|
|
315
383
|
|
|
384
|
+
# Adds Set-Cookie headers to the response headers.
|
|
385
|
+
#
|
|
386
|
+
# @return [void]
|
|
316
387
|
def add_set_cookie_headers
|
|
317
388
|
@response_headers ||= {}
|
|
318
389
|
sc = (@response_headers['Set-Cookie'] ||= [])
|
data/lib/syntropy/logger.rb
CHANGED