syntropy 0.38.1 → 0.40.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 +16 -0
- data/README.md +2 -0
- data/TODO.md +55 -144
- data/cmd/console.rb +2 -4
- data/cmd/new/template/config/production.rb +2 -1
- data/cmd/new.rb +1 -1
- data/cmd/serve.rb +29 -4
- data/cmd/test.rb +12 -4
- 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/app/posts/[id]/edit.rb +3 -3
- data/examples/blog/app/posts/[id]/index.rb +8 -10
- data/examples/blog/app/posts/index.rb +6 -8
- data/examples/blog/app/posts/new.rb +3 -3
- data/examples/blog/config/production.rb +2 -1
- data/lib/syntropy/app.rb +12 -60
- 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/controller_extensions.rb +1 -1
- 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 +72 -2
- data/lib/syntropy/logger.rb +8 -0
- data/lib/syntropy/markdown.rb +139 -11
- data/lib/syntropy/module_loader.rb +110 -68
- data/lib/syntropy/request/request_info.rb +8 -0
- data/lib/syntropy/request/response.rb +76 -6
- data/lib/syntropy/request/validation.rb +6 -0
- data/lib/syntropy/storage/connection_pool.rb +34 -0
- data/lib/syntropy/test.rb +14 -5
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +9 -2
- data/syntropy.gemspec +4 -4
- data/test/fixtures/app/_layout/default.rb +1 -1
- data/test/fixtures/app/_layout/kuku.rb +8 -0
- data/test/fixtures/app/_lib/circular/a.rb +2 -0
- data/test/fixtures/app/_lib/circular/b.rb +2 -0
- data/test/fixtures/app/_lib/circular/c.rb +2 -0
- data/test/fixtures/app/mod/concurrent.rb +9 -0
- data/test/test_app.rb +1 -1
- data/test/test_caching.rb +3 -3
- data/test/test_markdown.rb +268 -0
- data/test/test_module_loader.rb +32 -4
- data/test/test_request.rb +7 -0
- data/test/test_response.rb +30 -0
- data/test/test_schema.rb +1 -0
- metadata +40 -11
|
@@ -8,9 +8,7 @@ def get(req)
|
|
|
8
8
|
post = @posts.get(id)
|
|
9
9
|
raise Syntropy::Error.not_found if !post
|
|
10
10
|
|
|
11
|
-
req.respond_html(
|
|
12
|
-
@template.render(post:, req:)
|
|
13
|
-
)
|
|
11
|
+
req.respond_html(@template.render(post:, req:))
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def post(req)
|
|
@@ -32,15 +30,15 @@ def delete(req)
|
|
|
32
30
|
id = req.route_params['id'].to_i
|
|
33
31
|
|
|
34
32
|
deleted = @posts.delete(id)
|
|
35
|
-
raise BadRequestError,
|
|
33
|
+
raise BadRequestError, 'Failed to delete post' if deleted != 1
|
|
36
34
|
|
|
37
35
|
req.flash[:notice] = 'Post was successfully destroyed.'
|
|
38
|
-
req.redirect
|
|
36
|
+
req.redirect '..', Syntropy::HTTP::SEE_OTHER
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
@template = @layout.apply { |post:, **props|
|
|
42
|
-
h1
|
|
43
|
-
p
|
|
39
|
+
@template = @layout.apply { |post:, req:, **props|
|
|
40
|
+
h1 'My blog'
|
|
41
|
+
p req.flash[:notice], style: 'color: green'
|
|
44
42
|
div {
|
|
45
43
|
h2 {
|
|
46
44
|
a post[:title]
|
|
@@ -48,9 +46,9 @@ end
|
|
|
48
46
|
p post[:body]
|
|
49
47
|
}
|
|
50
48
|
p {
|
|
51
|
-
a
|
|
49
|
+
a 'Edit', href: req.rel('./edit')
|
|
52
50
|
span '|'
|
|
53
|
-
a
|
|
51
|
+
a 'Back to posts', href: req.rel('..')
|
|
54
52
|
}
|
|
55
53
|
div {
|
|
56
54
|
form(method: 'post') {
|
|
@@ -5,9 +5,7 @@ export dispatch_by_http_method
|
|
|
5
5
|
|
|
6
6
|
def get(req)
|
|
7
7
|
posts = @posts.get_all
|
|
8
|
-
req.respond_html(
|
|
9
|
-
@template.render(posts:, req:)
|
|
10
|
-
)
|
|
8
|
+
req.respond_html(@template.render(posts:, req:))
|
|
11
9
|
end
|
|
12
10
|
|
|
13
11
|
def post(req)
|
|
@@ -17,16 +15,16 @@ def post(req)
|
|
|
17
15
|
id = @posts.create(title, body)
|
|
18
16
|
|
|
19
17
|
req.flash[:notice] = 'Post was successfully created.'
|
|
20
|
-
req.redirect("
|
|
18
|
+
req.redirect("./#{id}")
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
@template = @layout.apply {
|
|
21
|
+
@template = @layout.apply { |req:, **props|
|
|
24
22
|
h1 "My awesome blog"
|
|
25
|
-
p
|
|
23
|
+
p req.flash[:notice], style: 'color: green'
|
|
26
24
|
props[:posts].each { |post|
|
|
27
25
|
div {
|
|
28
26
|
h2 {
|
|
29
|
-
a post[:title], href: "
|
|
27
|
+
a post[:title], href: req.rel("./#{post[:id]}")
|
|
30
28
|
}
|
|
31
29
|
p post[:body]
|
|
32
30
|
}
|
|
@@ -34,7 +32,7 @@ end
|
|
|
34
32
|
|
|
35
33
|
div {
|
|
36
34
|
p {
|
|
37
|
-
a "New post", href: '
|
|
35
|
+
a "New post", href: req.rel('./new')
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -5,14 +5,14 @@ export dispatch_by_http_method
|
|
|
5
5
|
|
|
6
6
|
def get(req)
|
|
7
7
|
req.respond_html(
|
|
8
|
-
@template.render
|
|
8
|
+
@template.render(req:)
|
|
9
9
|
)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
@template = @layout.apply {
|
|
12
|
+
@template = @layout.apply { |req:, **props|
|
|
13
13
|
h1 "Create blog post"
|
|
14
14
|
div {
|
|
15
|
-
form(action: "
|
|
15
|
+
form(action: req.rel(".."), method: 'post') {
|
|
16
16
|
div {
|
|
17
17
|
label 'Title', for: 'title'
|
|
18
18
|
input name: 'title', type: 'text'
|
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,
|
|
@@ -251,6 +255,8 @@ module Syntropy
|
|
|
251
255
|
}
|
|
252
256
|
end
|
|
253
257
|
|
|
258
|
+
DEFAULT_CACHE_CONTROL = 'max-age=604800' # one week
|
|
259
|
+
|
|
254
260
|
# Serves a static file from the given target hash with cache validation.
|
|
255
261
|
#
|
|
256
262
|
# @param req [Syntropy::Request] request
|
|
@@ -259,7 +265,7 @@ module Syntropy
|
|
|
259
265
|
def serve_static_file(req, target)
|
|
260
266
|
validate_static_file_info(target)
|
|
261
267
|
cache_opts = {
|
|
262
|
-
cache_control:
|
|
268
|
+
cache_control: DEFAULT_CACHE_CONTROL,
|
|
263
269
|
last_modified: target[:last_modified_date],
|
|
264
270
|
etag: target[:etag]
|
|
265
271
|
}
|
|
@@ -317,63 +323,9 @@ module Syntropy
|
|
|
317
323
|
# @param route [Hash] route entry
|
|
318
324
|
# @return [Proc] route proc
|
|
319
325
|
def markdown_route_proc(route)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
req.respond_by_http_method(
|
|
324
|
-
'head' => [nil, headers],
|
|
325
|
-
'get' => -> { [render_markdown(route), headers] }
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# Renders and returns the given markdown route as HTML.
|
|
331
|
-
#
|
|
332
|
-
# @param route [Hash] route entry
|
|
333
|
-
# @return [String] rendered HTML
|
|
334
|
-
def render_markdown(route)
|
|
335
|
-
atts, md = Syntropy::Markdown.parse(route[:target][:fn], @env)
|
|
336
|
-
|
|
337
|
-
layout = compute_markdown_layout(route, atts)
|
|
338
|
-
Papercraft.html(layout, md:, **atts)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def compute_markdown_layout(route, atts)
|
|
342
|
-
if (layout = atts[:layout])
|
|
343
|
-
route[:applied_layouts] ||= {}
|
|
344
|
-
route[:applied_layouts][layout] ||= markdown_layout_template(layout)
|
|
345
|
-
else
|
|
346
|
-
default_markdown_layout_template
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# Returns a markdown template based on the given layout.
|
|
351
|
-
#
|
|
352
|
-
# @param layout [String] layout name
|
|
353
|
-
# @return [Proc] layout template
|
|
354
|
-
def markdown_layout_template(layout)
|
|
355
|
-
@layouts ||= {}
|
|
356
|
-
template = @module_loader.load("_layout/#{layout}")
|
|
357
|
-
@layouts[layout] = Papercraft.apply(template) { |md:, **| markdown(md) }
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
# Returns the default markdown layout, which renders to HTML and includes a
|
|
361
|
-
# title, the markdown content, and emits code for auto refreshing the page
|
|
362
|
-
# on file change.
|
|
363
|
-
#
|
|
364
|
-
# @return [Proc] default Markdown layout template
|
|
365
|
-
def default_markdown_layout_template
|
|
366
|
-
@default_markdown_layout ||= ->(md:, **atts) {
|
|
367
|
-
html5 {
|
|
368
|
-
head {
|
|
369
|
-
title atts[:title]
|
|
370
|
-
}
|
|
371
|
-
body {
|
|
372
|
-
markdown md
|
|
373
|
-
auto_refresh! if Syntropy.dev_mode
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
326
|
+
env = @env.merge(module_loader: @module_loader)
|
|
327
|
+
atts, md = Syntropy::Markdown.parse_file(route[:target][:fn], env)
|
|
328
|
+
Syntropy::Markdown.make_controller(env, atts, md)
|
|
377
329
|
end
|
|
378
330
|
|
|
379
331
|
# Returns the route proc for a module route.
|
|
@@ -535,7 +487,7 @@ module Syntropy
|
|
|
535
487
|
#
|
|
536
488
|
# @return [void]
|
|
537
489
|
def start
|
|
538
|
-
@module_loader.load('_setup',
|
|
490
|
+
@module_loader.load('_setup', raise_on_missing_export: false)
|
|
539
491
|
|
|
540
492
|
@machine.spin do
|
|
541
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
|
|
@@ -53,7 +53,7 @@ module Syntropy
|
|
|
53
53
|
raise 'Not a directory' if !File.directory?(full_path)
|
|
54
54
|
|
|
55
55
|
Dir[File.join(full_path, '*.md')].sort.map {
|
|
56
|
-
atts, markdown = Syntropy::Markdown.
|
|
56
|
+
atts, markdown = Syntropy::Markdown.parse_file(it, @env)
|
|
57
57
|
{ atts:, markdown: }
|
|
58
58
|
}
|
|
59
59
|
end
|
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
|
|