syntropy 0.28.1 → 0.29.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/.ruby-version +1 -0
- data/CHANGELOG.md +10 -0
- data/README.md +1 -3
- data/bin/syntropy +2 -3
- data/cmd/setup/template/site/Dockerfile +1 -1
- data/lib/syntropy/app.rb +17 -9
- data/lib/syntropy/applets/builtin/default_error_handler/style.css +1 -1
- data/lib/syntropy/connection.rb +402 -0
- data/lib/syntropy/errors.rb +12 -0
- data/lib/syntropy/logger.rb +103 -0
- data/lib/syntropy/request_extensions.rb +28 -2
- data/lib/syntropy/server.rb +173 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +52 -1
- data/syntropy.gemspec +4 -5
- data/test/helper.rb +6 -2
- data/test/test_app.rb +1 -1
- data/test/test_caching.rb +1 -1
- data/test/test_connection.rb +649 -0
- data/test/test_server.rb +336 -0
- metadata +12 -22
- data/lib/syntropy/file_watch.rb +0 -28
- data/test/test_file_watch.rb +0 -36
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
require 'qeweney'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
+
class Qeweney::Request
|
|
7
|
+
attr_accessor :start_stamp
|
|
8
|
+
|
|
9
|
+
def respond_with_static_file(path, etag, last_modified, opts)
|
|
10
|
+
cache_headers = (etag || last_modified) ? {
|
|
11
|
+
'etag' => etag,
|
|
12
|
+
'last-modified' => last_modified
|
|
13
|
+
} : {}
|
|
14
|
+
|
|
15
|
+
adapter.respond_with_static_file(self, path, opts, cache_headers)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set_response_headers(headers)
|
|
19
|
+
adapter.set_response_headers(headers)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set_cookie(*)
|
|
23
|
+
adapter.set_cookie(*)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def upgrade(protocol, custom_headers = nil, &block)
|
|
27
|
+
super(protocol, custom_headers)
|
|
28
|
+
adapter.with_stream(&block)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
6
32
|
module Syntropy
|
|
7
33
|
# Extensions for the Qeweney::Request class
|
|
8
34
|
module RequestExtensions
|
|
@@ -37,9 +63,9 @@ module Syntropy
|
|
|
37
63
|
# @param accepted [Array<String>] list of accepted HTTP methods
|
|
38
64
|
# @return [String] request's HTTP method
|
|
39
65
|
def validate_http_method(*accepted)
|
|
40
|
-
|
|
66
|
+
return method if accepted.include?(method)
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
raise Syntropy::Error.method_not_allowed
|
|
43
69
|
end
|
|
44
70
|
|
|
45
71
|
# Responds according to the given map. The given map defines the responses
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy/connection'
|
|
4
|
+
require 'syntropy/request_extensions'
|
|
5
|
+
|
|
6
|
+
module Syntropy
|
|
7
|
+
class Server
|
|
8
|
+
PENDING_REQUESTS_GRACE_PERIOD = 0.1
|
|
9
|
+
PENDING_REQUESTS_TIMEOUT_PERIOD = 5
|
|
10
|
+
|
|
11
|
+
def self.syntropy_app(_machine, env)
|
|
12
|
+
if env[:app_location]
|
|
13
|
+
env[:logger]&.info(message: 'Loading web app', location: env[:app_location])
|
|
14
|
+
require env[:app_location]
|
|
15
|
+
|
|
16
|
+
env.merge!(Syntropy.config)
|
|
17
|
+
end
|
|
18
|
+
env[:app]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.static_app(env); end
|
|
22
|
+
|
|
23
|
+
def initialize(machine, env, &app)
|
|
24
|
+
@machine = machine
|
|
25
|
+
@env = env
|
|
26
|
+
@app = app || app_from_env
|
|
27
|
+
@server_fds = []
|
|
28
|
+
@accept_fibers = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def app_from_env
|
|
32
|
+
case @env[:app_type]
|
|
33
|
+
when nil, :syntropy
|
|
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
|
+
|
|
42
|
+
def run
|
|
43
|
+
setup
|
|
44
|
+
@machine.await(@accept_fibers)
|
|
45
|
+
rescue UM::Terminate
|
|
46
|
+
graceful_shutdown
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop!
|
|
50
|
+
graceful_shutdown
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def setup
|
|
56
|
+
bind_info = get_bind_entries
|
|
57
|
+
bind_info.each do |(host, port)|
|
|
58
|
+
fd = setup_server_socket(host, port)
|
|
59
|
+
@server_fds << fd
|
|
60
|
+
@accept_fibers << @machine.spin { accept_incoming(fd) }
|
|
61
|
+
end
|
|
62
|
+
bind_string = bind_info.map { it.join(':') }.join(', ')
|
|
63
|
+
@env[:logger]&.info(message: "Listening on #{bind_string}")
|
|
64
|
+
setup_server_extensions
|
|
65
|
+
|
|
66
|
+
# map fibers
|
|
67
|
+
@connection_fibers = Set.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get_bind_entries
|
|
71
|
+
bind = @env[:bind]
|
|
72
|
+
case bind
|
|
73
|
+
when Array
|
|
74
|
+
bind.map { bind_info(it) }
|
|
75
|
+
when String
|
|
76
|
+
[bind_info(bind)]
|
|
77
|
+
else
|
|
78
|
+
# default
|
|
79
|
+
[['0.0.0.0', 1234]]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def bind_info(bind_string)
|
|
84
|
+
parts = bind_string.split(':')
|
|
85
|
+
[parts[0], parts[1].to_i]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def setup_server_socket(host, port)
|
|
89
|
+
fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
90
|
+
@machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
|
91
|
+
@machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEPORT, true)
|
|
92
|
+
@machine.bind(fd, host, port)
|
|
93
|
+
@machine.listen(fd, UM::SOMAXCONN)
|
|
94
|
+
fd
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def setup_server_extensions
|
|
98
|
+
extensions = @env[:server_extensions]
|
|
99
|
+
return if !extensions
|
|
100
|
+
|
|
101
|
+
server_name = extensions[:name]
|
|
102
|
+
if extensions[:date]
|
|
103
|
+
@date_header_fiber = @machine.spin {
|
|
104
|
+
@machine.periodically(1) { update_server_headers(server_name) }
|
|
105
|
+
}
|
|
106
|
+
update_server_headers(server_name)
|
|
107
|
+
elsif server_name
|
|
108
|
+
@env[:server_headers] = "Server: #{server_name}\r\n"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def update_server_headers(server_name)
|
|
113
|
+
@env[:server_date] = Time.now
|
|
114
|
+
if server_name
|
|
115
|
+
@env[:server_headers] = "Server: #{server_name}\r\nDate: #{@env[:server_date].httpdate}\r\n"
|
|
116
|
+
else
|
|
117
|
+
@env[:server_headers] = "Date: #{Time.now.httpdate}\r\n"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def accept_incoming(listen_fd)
|
|
122
|
+
@machine.accept_each(listen_fd) { start_client_connection(it) }
|
|
123
|
+
rescue UM::Terminate
|
|
124
|
+
# terminated
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def start_client_connection(fd)
|
|
128
|
+
conn = Connection.new(self, @machine, fd, @env, &@app)
|
|
129
|
+
f = @machine.spin(conn) do
|
|
130
|
+
it.run
|
|
131
|
+
ensure
|
|
132
|
+
@connection_fibers.delete(f)
|
|
133
|
+
end
|
|
134
|
+
@connection_fibers << f
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def close_all_server_fds
|
|
138
|
+
@server_fds.each { @machine.close_async(it) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
STOP = UM::Terminate.new
|
|
142
|
+
|
|
143
|
+
def stop_accept_fibers
|
|
144
|
+
@accept_fibers.each { @machine.schedule(it, STOP) if !it.done? }
|
|
145
|
+
@machine.await(@accept_fibers)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def graceful_shutdown
|
|
149
|
+
@env[:logger]&.info(message: 'Shutting down gracefully...')
|
|
150
|
+
|
|
151
|
+
# stop listening
|
|
152
|
+
close_all_server_fds
|
|
153
|
+
stop_accept_fibers
|
|
154
|
+
@machine.snooze
|
|
155
|
+
|
|
156
|
+
return if @connection_fibers.empty?
|
|
157
|
+
|
|
158
|
+
# sleep for a bit, let requests finish
|
|
159
|
+
@machine.sleep(PENDING_REQUESTS_GRACE_PERIOD)
|
|
160
|
+
return if @connection_fibers.empty?
|
|
161
|
+
|
|
162
|
+
# terminate pending fibers
|
|
163
|
+
pending = @connection_fibers.to_a
|
|
164
|
+
pending.each { @machine.schedule(it, STOP) }
|
|
165
|
+
|
|
166
|
+
@machine.timeout(PENDING_REQUESTS_TIMEOUT_PERIOD, UM::Terminate) do
|
|
167
|
+
@machine.await(@connection_fibers)
|
|
168
|
+
rescue UM::Terminate
|
|
169
|
+
# timeout on waiting for adapters to finish running, do nothing
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'qeweney'
|
|
4
4
|
require 'uringmachine'
|
|
5
|
-
require 'tp2'
|
|
6
5
|
require 'papercraft'
|
|
7
6
|
|
|
7
|
+
require 'syntropy/logger'
|
|
8
|
+
require 'syntropy/connection'
|
|
9
|
+
require 'syntropy/server'
|
|
8
10
|
require 'syntropy/app'
|
|
9
11
|
require 'syntropy/connection_pool'
|
|
10
12
|
require 'syntropy/errors'
|
|
@@ -16,6 +18,7 @@ require 'syntropy/routing_tree'
|
|
|
16
18
|
require 'syntropy/json_api'
|
|
17
19
|
require 'syntropy/side_run'
|
|
18
20
|
require 'syntropy/utils'
|
|
21
|
+
require 'syntropy/version'
|
|
19
22
|
|
|
20
23
|
module Syntropy
|
|
21
24
|
Status = Qeweney::Status
|
|
@@ -50,4 +53,52 @@ module Syntropy
|
|
|
50
53
|
" #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/digital-fabric/syntropy\n"\
|
|
51
54
|
" #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
|
|
52
55
|
"#{YELLOW}+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\e[0m\n\n"
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
def run(env = {}, &app)
|
|
59
|
+
if @in_run
|
|
60
|
+
@env = env
|
|
61
|
+
@env[:app] = app if app
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
env ||= @env || {}
|
|
66
|
+
begin
|
|
67
|
+
@in_run = true
|
|
68
|
+
machine = env[:machine] || UM.new
|
|
69
|
+
machine.puts(env[:banner]) if env[:banner]
|
|
70
|
+
|
|
71
|
+
env[:logger]&.info(message: "Running Syntropy #{Syntropy::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
|
|
72
|
+
|
|
73
|
+
server = Server.new(machine, env, &app)
|
|
74
|
+
|
|
75
|
+
setup_signal_handling(machine, Fiber.current)
|
|
76
|
+
server.run
|
|
77
|
+
ensure
|
|
78
|
+
@in_run = false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def env(env = nil, &app)
|
|
83
|
+
return @env if !env && !app
|
|
84
|
+
|
|
85
|
+
@env = env || {}
|
|
86
|
+
@env[:app] = app if app
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def setup_signal_handling(machine, fiber)
|
|
92
|
+
queue = UM::Queue.new
|
|
93
|
+
trap('SIGINT') { machine.push(queue, :SIGINT) }
|
|
94
|
+
machine.spin { watch_for_int_signal(machine, queue, fiber) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# waits for signal from queue, then terminates given fiber
|
|
98
|
+
# to be done
|
|
99
|
+
def watch_for_int_signal(machine, queue, fiber)
|
|
100
|
+
machine.shift(queue)
|
|
101
|
+
machine.schedule(fiber, UM::Terminate.new)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
53
104
|
end
|
data/syntropy.gemspec
CHANGED
|
@@ -13,9 +13,9 @@ Gem::Specification.new do |s|
|
|
|
13
13
|
s.metadata = {
|
|
14
14
|
'homepage_uri' => 'https://github.com/digital-fabric/syntropy',
|
|
15
15
|
'documentation_uri' => 'https://www.rubydoc.info/gems/syntropy',
|
|
16
|
-
'changelog_uri' => 'https://github.com/digital-fabric/syntropy/blob/
|
|
16
|
+
'changelog_uri' => 'https://github.com/digital-fabric/syntropy/blob/main/CHANGELOG.md'
|
|
17
17
|
}
|
|
18
|
-
s.rdoc_options = ['--title', '
|
|
18
|
+
s.rdoc_options = ['--title', 'Syntropy', '--main', 'README.md']
|
|
19
19
|
s.extra_rdoc_files = ['README.md']
|
|
20
20
|
s.require_paths = ['lib']
|
|
21
21
|
s.required_ruby_version = '>= 3.4'
|
|
@@ -24,11 +24,10 @@ Gem::Specification.new do |s|
|
|
|
24
24
|
s.add_dependency 'extralite', '~>2.14'
|
|
25
25
|
s.add_dependency 'papercraft', '~>3.2.0'
|
|
26
26
|
s.add_dependency 'qeweney', '~>0.24'
|
|
27
|
-
s.add_dependency '
|
|
28
|
-
s.add_dependency 'uringmachine', '~>0.23.1'
|
|
27
|
+
s.add_dependency 'uringmachine', '~>1.0.0'
|
|
29
28
|
|
|
30
29
|
s.add_dependency 'listen', '~>3.9.0'
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
s.add_dependency 'json'
|
|
33
32
|
s.add_dependency 'logger'
|
|
34
33
|
|
data/test/helper.rb
CHANGED
|
@@ -11,6 +11,12 @@ require 'fileutils'
|
|
|
11
11
|
STDOUT.sync = true
|
|
12
12
|
STDERR.sync = true
|
|
13
13
|
|
|
14
|
+
class ::String
|
|
15
|
+
def crlf_lines
|
|
16
|
+
chomp.gsub("\n", "\r\n").chomp
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
module ::Kernel
|
|
15
21
|
def mock_req(headers, body = nil)
|
|
16
22
|
Qeweney::MockAdapter.mock(headers, body).tap { it.setup_mock_request }
|
|
@@ -113,5 +119,3 @@ class Qeweney::Request
|
|
|
113
119
|
response_headers['Content-Type']
|
|
114
120
|
end
|
|
115
121
|
end
|
|
116
|
-
|
|
117
|
-
# puts "Polyphony backend: #{Thread.current.backend.kind}"
|
data/test/test_app.rb
CHANGED
|
@@ -122,7 +122,7 @@ class AppTest < Minitest::Test
|
|
|
122
122
|
assert_equal Status::INTERNAL_SERVER_ERROR, req.response_status
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def test_automatic_redirect_on_trailing_slash
|
|
125
|
+
def test_automatic_redirect_on_trailing_slash
|
|
126
126
|
req = make_request(':method' => 'GET', ':path' => '/test/rss/')
|
|
127
127
|
assert_equal Status::MOVED_PERMANENTLY, req.response_status
|
|
128
128
|
assert_equal '/test/rss', req.response_headers['Location']
|
data/test/test_caching.rb
CHANGED
|
@@ -40,7 +40,7 @@ class CachingTest < Minitest::Test
|
|
|
40
40
|
@app = Syntropy::App.new(**@env)
|
|
41
41
|
|
|
42
42
|
@c_fd, @s_fd = make_socket_pair
|
|
43
|
-
@adapter =
|
|
43
|
+
@adapter = Syntropy::Connection.new(nil, @machine, @s_fd, @env) { @app.(it) }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def teardown
|