itsi 0.1.8 → 0.1.11
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/Cargo.lock +11 -2
- data/Rakefile +6 -2
- data/crates/itsi_rb_helpers/src/lib.rs +27 -4
- data/crates/itsi_server/Cargo.toml +4 -1
- data/crates/itsi_server/src/lib.rs +74 -1
- data/crates/itsi_server/src/request/itsi_request.rs +32 -11
- data/crates/itsi_server/src/response/itsi_response.rs +14 -4
- data/crates/itsi_server/src/server/bind.rs +16 -12
- data/crates/itsi_server/src/server/itsi_server.rs +146 -95
- data/crates/itsi_server/src/server/listener.rs +10 -10
- data/crates/itsi_server/src/server/process_worker.rs +10 -3
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/crates/itsi_server/src/server/signal.rs +4 -0
- data/crates/itsi_server/src/server/thread_worker.rs +55 -24
- data/crates/itsi_server/src/server/tls.rs +11 -8
- data/crates/itsi_tracing/src/lib.rs +18 -1
- data/gems/scheduler/Cargo.lock +12 -12
- data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +27 -4
- data/gems/scheduler/ext/itsi_server/Cargo.toml +4 -1
- data/gems/scheduler/ext/itsi_server/src/lib.rs +74 -1
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +32 -11
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +14 -4
- data/gems/scheduler/ext/itsi_server/src/server/bind.rs +16 -12
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +146 -95
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +10 -10
- data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +10 -3
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +4 -0
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +55 -24
- data/gems/scheduler/ext/itsi_server/src/server/tls.rs +11 -8
- data/gems/scheduler/ext/itsi_tracing/src/lib.rs +18 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_file_io.rb +0 -1
- data/gems/scheduler/test/test_kernel_sleep.rb +3 -4
- data/gems/server/Cargo.lock +11 -2
- data/gems/server/Rakefile +8 -1
- data/gems/server/exe/itsi +53 -23
- data/gems/server/ext/itsi_rb_helpers/src/lib.rs +27 -4
- data/gems/server/ext/itsi_server/Cargo.toml +4 -1
- data/gems/server/ext/itsi_server/src/lib.rs +74 -1
- data/gems/server/ext/itsi_server/src/request/itsi_request.rs +32 -11
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +14 -4
- data/gems/server/ext/itsi_server/src/server/bind.rs +16 -12
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +146 -95
- data/gems/server/ext/itsi_server/src/server/listener.rs +10 -10
- data/gems/server/ext/itsi_server/src/server/process_worker.rs +10 -3
- data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/gems/server/ext/itsi_server/src/server/signal.rs +4 -0
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +55 -24
- data/gems/server/ext/itsi_server/src/server/tls.rs +11 -8
- data/gems/server/ext/itsi_tracing/src/lib.rs +18 -1
- data/gems/server/lib/itsi/request.rb +29 -21
- data/gems/server/lib/itsi/server/Itsi.rb +127 -0
- data/gems/server/lib/itsi/server/config.rb +36 -0
- data/gems/server/lib/itsi/server/options_dsl.rb +401 -0
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +18 -7
- data/gems/server/lib/itsi/server/rack_interface.rb +75 -0
- data/gems/server/lib/itsi/server/scheduler_interface.rb +21 -0
- data/gems/server/lib/itsi/server/signal_trap.rb +23 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +71 -101
- data/gems/server/test/helpers/test_helper.rb +30 -0
- data/gems/server/test/test_itsi_server.rb +294 -3
- data/lib/itsi/version.rb +1 -1
- data/location_dsl.rb +381 -0
- data/sandbox/deploy/main.tf +1 -0
- data/sandbox/itsi_itsi_file/Itsi.rb +119 -0
- data/sandbox/itsi_sandbox_async/Gemfile +1 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +2 -2
- data/tasks.txt +25 -8
- metadata +21 -14
- data/gems/server/lib/itsi/signals.rb +0 -23
- data/gems/server/test/test_helper.rb +0 -7
- /data/gems/server/lib/itsi/{index.html.erb → index.html} +0 -0
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
3
|
class TestKernelSleep < Minitest::Test
|
5
4
|
include Itsi::Scheduler::TestHelper
|
6
5
|
|
@@ -14,7 +13,7 @@ class TestKernelSleep < Minitest::Test
|
|
14
13
|
# Run the scheduler in a dedicated thread to avoid interference with the
|
15
14
|
# main thread’s scheduler state.
|
16
15
|
with_scheduler do |_scheduler|
|
17
|
-
|
16
|
+
10.times do
|
18
17
|
Fiber.schedule do
|
19
18
|
sleep 0.05
|
20
19
|
results << "first"
|
@@ -26,9 +25,9 @@ class TestKernelSleep < Minitest::Test
|
|
26
25
|
|
27
26
|
ends_at = Time.now
|
28
27
|
# We expect 10 sleep completions overall (2 per fiber).
|
29
|
-
assert_equal
|
28
|
+
assert_equal 20, results.size
|
30
29
|
# Because all sleeps run concurrently, the total elapsed time should be about 1 second.
|
31
|
-
assert_in_delta 0.1, ends_at - start_at, 0.
|
30
|
+
assert_in_delta 0.1, ends_at - start_at, 0.1, "Total elapsed time should be close to 0.1 second"
|
32
31
|
end
|
33
32
|
|
34
33
|
def test_sleep_zero_duration
|
data/gems/server/Cargo.lock
CHANGED
@@ -993,6 +993,7 @@ dependencies = [
|
|
993
993
|
"crossbeam",
|
994
994
|
"derive_more",
|
995
995
|
"dirs",
|
996
|
+
"fnv",
|
996
997
|
"fs2",
|
997
998
|
"futures",
|
998
999
|
"http",
|
@@ -1009,7 +1010,9 @@ dependencies = [
|
|
1009
1010
|
"pin-project",
|
1010
1011
|
"rb-sys",
|
1011
1012
|
"rcgen",
|
1013
|
+
"regex",
|
1012
1014
|
"ring",
|
1015
|
+
"route-recognizer",
|
1013
1016
|
"rustls",
|
1014
1017
|
"rustls-pemfile",
|
1015
1018
|
"socket2",
|
@@ -1730,6 +1733,12 @@ dependencies = [
|
|
1730
1733
|
"windows-sys 0.52.0",
|
1731
1734
|
]
|
1732
1735
|
|
1736
|
+
[[package]]
|
1737
|
+
name = "route-recognizer"
|
1738
|
+
version = "0.3.1"
|
1739
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
1740
|
+
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
1741
|
+
|
1733
1742
|
[[package]]
|
1734
1743
|
name = "rustc-demangle"
|
1735
1744
|
version = "0.1.24"
|
@@ -2131,9 +2140,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|
2131
2140
|
|
2132
2141
|
[[package]]
|
2133
2142
|
name = "tokio"
|
2134
|
-
version = "1.
|
2143
|
+
version = "1.44.1"
|
2135
2144
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
2136
|
-
checksum = "
|
2145
|
+
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
|
2137
2146
|
dependencies = [
|
2138
2147
|
"backtrace",
|
2139
2148
|
"bytes",
|
data/gems/server/Rakefile
CHANGED
@@ -3,7 +3,14 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "minitest/test_task"
|
5
5
|
|
6
|
-
|
6
|
+
|
7
|
+
Minitest::TestTask.create(:test) do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.warning = false
|
11
|
+
t.test_globs = ['test/**/*.rb']
|
12
|
+
t.test_prelude = 'require "helpers/test_helper.rb"'
|
13
|
+
end
|
7
14
|
|
8
15
|
require "rubocop/rake_task"
|
9
16
|
|
data/gems/server/exe/itsi
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
3
4
|
require "optparse"
|
4
5
|
|
5
6
|
# Default options used when starting Osprey from the CLI using `osprey`
|
@@ -19,7 +20,9 @@ DEFAULT_OPTIONS = {
|
|
19
20
|
# Scheduler class
|
20
21
|
scheduler_class: nil,
|
21
22
|
# Whether to stream the body or not
|
22
|
-
stream_body: false
|
23
|
+
stream_body: false,
|
24
|
+
# Config file
|
25
|
+
config_file: nil
|
23
26
|
}
|
24
27
|
|
25
28
|
options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
|
@@ -28,6 +31,10 @@ options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
|
|
28
31
|
OptionParser.new do |opts|
|
29
32
|
opts.banner = "Usage: itsi [options]"
|
30
33
|
|
34
|
+
opts.on("-C", "--config CONFIG_FILE", String, "Itsi Configuration file to use (default: Itsi.rb)") do |config_file|
|
35
|
+
options[:config_file] = config_file
|
36
|
+
end
|
37
|
+
|
31
38
|
opts.on("-w", "--workers WORKERS", Integer, "Number of workers (default: #{options[:workers]})") do |w|
|
32
39
|
options[:workers] = w
|
33
40
|
end
|
@@ -41,11 +48,12 @@ OptionParser.new do |opts|
|
|
41
48
|
end
|
42
49
|
|
43
50
|
opts.on("--worker-memory-limit MEMORY_LIMIT", Integer,
|
44
|
-
"Memory limit for each worker (default: #{options[:worker_memory_limit] ||
|
51
|
+
"Memory limit for each worker (default: #{options[:worker_memory_limit] || "None"}). If this limit is breached the worker is gracefully restarted") do |ml|
|
45
52
|
options[:worker_memory_limit] = ml
|
46
53
|
end
|
47
54
|
|
48
|
-
opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String],
|
55
|
+
opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String],
|
56
|
+
"Scheduler class to use (default: nil). Provide blank or true to use Itsi::Scheduler, or a classname to use an alternative scheduler") do |scheduler_class|
|
49
57
|
if scheduler_class.nil? || scheduler_class == "true"
|
50
58
|
options[:scheduler_class] = "Itsi::Scheduler"
|
51
59
|
elsif scheduler_class == "false"
|
@@ -65,8 +73,8 @@ OptionParser.new do |opts|
|
|
65
73
|
end
|
66
74
|
end
|
67
75
|
|
68
|
-
|
69
|
-
|
76
|
+
opts.on("-b", "--bind BIND", String,
|
77
|
+
"Bind address (default: #{options[:binds].join(", ")}). You can specify this flag multiple times to bind to multiple addresses.") do |bind|
|
70
78
|
options[:binds].pop if options[:binds].first.frozen?
|
71
79
|
options[:binds] << bind
|
72
80
|
end
|
@@ -74,6 +82,7 @@ OptionParser.new do |opts|
|
|
74
82
|
opts.on("-c", "--cert_path CERT_PATH", String,
|
75
83
|
"Path to the SSL certificate file (must follow a --bind option). You can specify this flag multiple times.") do |cp|
|
76
84
|
raise OptionParser::InvalidOption, "--cert_path must follow a --bind" if options[:binds].empty?
|
85
|
+
|
77
86
|
require "uri"
|
78
87
|
|
79
88
|
# Modify the last bind entry to add/update the cert query parameter
|
@@ -84,8 +93,10 @@ OptionParser.new do |opts|
|
|
84
93
|
options[:binds][-1] = "#{uri.host}?#{query_string}"
|
85
94
|
end
|
86
95
|
|
87
|
-
opts.on("-k", "--key_path KEY_PATH", String,
|
96
|
+
opts.on("-k", "--key_path KEY_PATH", String,
|
97
|
+
"Path to the SSL key file (must follow a --bind option). You can specify this flag multiple times.") do |kp|
|
88
98
|
raise OptionParser::InvalidOption, "--key_path must follow a --bind" if options[:binds].empty?
|
99
|
+
|
89
100
|
require "uri"
|
90
101
|
|
91
102
|
# Modify the last bind entry to add/update the key query parameter
|
@@ -115,30 +126,49 @@ OptionParser.new do |opts|
|
|
115
126
|
end
|
116
127
|
end.parse!
|
117
128
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
129
|
+
require "itsi/server/config"
|
130
|
+
if ARGV.pop == "init"
|
131
|
+
Itsi::Server::Config.write_default
|
132
|
+
exit(0)
|
133
|
+
# Add initialization code here
|
134
|
+
end
|
135
|
+
|
136
|
+
options = Itsi::Server::Config.load(options)
|
137
|
+
loader = nil
|
138
|
+
|
139
|
+
if options[:app] || File.exist?(options[:rackup_file])
|
140
|
+
loader ||= lambda do
|
122
141
|
require "rack"
|
142
|
+
require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
|
143
|
+
require "itsi/scheduler" if [true, "Itsi::Scheduler"].include?(options[:scheduler_class])
|
144
|
+
require "itsi/server"
|
145
|
+
return options[:app] if options[:app]
|
146
|
+
Array(Rack::Builder.parse_file(options[:rackup_file])).first
|
147
|
+
end
|
123
148
|
|
124
|
-
|
149
|
+
if options[:preload] == true
|
150
|
+
# If preload is true, we'll invoke our loader now, and just return a no-op proc
|
151
|
+
# which returns the app as loader. We still need to optionally load the scheduler class and rack
|
152
|
+
# in case the app has been provided directly in Itsi.rb instead of in config.ru
|
153
|
+
require "rack"
|
125
154
|
require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
155
|
+
require "itsi/scheduler" if [true, "Itsi::Scheduler"].include?(options[:scheduler_class])
|
156
|
+
loader = (options[:app] || loader[]).method(:itself).to_proc
|
157
|
+
elsif options[:preload]
|
158
|
+
# If a group name is given, we'll stick to just loading the gem group by the same name from the Gemfile.
|
159
|
+
# But we'll fall through to the default delayed loader for the full app.
|
160
|
+
require "bundler"
|
161
|
+
Bundler.setup
|
162
|
+
Bundler.require(options[:preload])
|
163
|
+
nil
|
135
164
|
end
|
165
|
+
end
|
136
166
|
|
137
167
|
# Make sure Itsi is loaded, if not already loaded by the rack_app above.
|
138
168
|
# Start the Itsi server
|
139
169
|
require "itsi/server"
|
140
170
|
|
141
|
-
Itsi::Server.
|
142
|
-
|
171
|
+
Itsi::Server.start(
|
172
|
+
loader: loader,
|
143
173
|
**options.except(:preload, :rackup_file)
|
144
|
-
)
|
174
|
+
)
|
@@ -1,7 +1,8 @@
|
|
1
|
-
use std::{os::raw::c_void, ptr::null_mut
|
1
|
+
use std::{os::raw::c_void, ptr::null_mut};
|
2
2
|
|
3
3
|
use magnus::{
|
4
4
|
RArray, Ruby, Thread, Value,
|
5
|
+
block::Proc,
|
5
6
|
rb_sys::FromRawValue,
|
6
7
|
value::{LazyId, ReprValue},
|
7
8
|
};
|
@@ -17,6 +18,7 @@ static ID_LIST: LazyId = LazyId::new("list");
|
|
17
18
|
static ID_EQ: LazyId = LazyId::new("==");
|
18
19
|
static ID_ALIVE: LazyId = LazyId::new("alive?");
|
19
20
|
static ID_THREAD_VARIABLE_GET: LazyId = LazyId::new("thread_variable_get");
|
21
|
+
static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
|
20
22
|
|
21
23
|
pub fn schedule_thread() {
|
22
24
|
unsafe {
|
@@ -120,20 +122,30 @@ where
|
|
120
122
|
*result_box
|
121
123
|
}
|
122
124
|
|
123
|
-
pub fn fork(after_fork:
|
125
|
+
pub fn fork(after_fork: Option<HeapValue<Proc>>) -> Option<i32> {
|
124
126
|
let ruby = Ruby::get().unwrap();
|
125
127
|
let fork_result = ruby
|
126
128
|
.module_kernel()
|
127
129
|
.funcall::<_, _, Option<i32>>(*ID_FORK, ())
|
128
130
|
.unwrap();
|
129
131
|
if fork_result.is_none() {
|
130
|
-
if let Some(
|
131
|
-
|
132
|
+
if let Some(proc) = after_fork {
|
133
|
+
call_proc_and_log_errors(proc)
|
132
134
|
}
|
133
135
|
}
|
134
136
|
fork_result
|
135
137
|
}
|
136
138
|
|
139
|
+
pub fn call_proc_and_log_errors(proc: HeapValue<Proc>) {
|
140
|
+
if let Err(e) = proc.call::<_, Value>(()) {
|
141
|
+
if let Some(value) = e.value() {
|
142
|
+
print_rb_backtrace(value);
|
143
|
+
} else {
|
144
|
+
eprintln!("Error occurred {:?}", e);
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
137
149
|
pub fn kill_threads<T>(threads: Vec<T>)
|
138
150
|
where
|
139
151
|
T: ReprValue,
|
@@ -176,3 +188,14 @@ pub fn terminate_non_fork_safe_threads() {
|
|
176
188
|
|
177
189
|
kill_threads(non_fork_safe_threads);
|
178
190
|
}
|
191
|
+
|
192
|
+
pub fn print_rb_backtrace(rb_err: Value) {
|
193
|
+
let backtrace = rb_err
|
194
|
+
.funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
|
195
|
+
.unwrap_or_default();
|
196
|
+
|
197
|
+
eprintln!("Ruby exception {:?}", rb_err);
|
198
|
+
for line in backtrace {
|
199
|
+
eprintln!("{}", line);
|
200
|
+
}
|
201
|
+
}
|
@@ -24,7 +24,7 @@ rcgen = { version = "0.13.2", features = ["x509-parser", "pem"] }
|
|
24
24
|
base64 = "0.22.1"
|
25
25
|
http-body-util = "0.1.2"
|
26
26
|
hyper = { version = "1.5.0", features = ["full", "server", "http1", "http2"] }
|
27
|
-
tokio = { version = "1", features = ["full"] }
|
27
|
+
tokio = { version = "1.44.1", features = ["full"] }
|
28
28
|
hyper-util = { version = "0.1.10", features = ["full"] }
|
29
29
|
derive_more = { version = "2.0.1", features = ["debug"] }
|
30
30
|
http = "1.3.1"
|
@@ -45,3 +45,6 @@ fs2 = "0.4.3"
|
|
45
45
|
ring = "0.17.14"
|
46
46
|
async-trait = "0.1.87"
|
47
47
|
dirs = "6.0.0"
|
48
|
+
regex = "1.11.1"
|
49
|
+
route-recognizer = "0.3.1"
|
50
|
+
fnv = "1.0.7"
|
@@ -1,5 +1,8 @@
|
|
1
1
|
use body_proxy::itsi_body_proxy::ItsiBodyProxy;
|
2
|
-
use magnus::{
|
2
|
+
use magnus::{
|
3
|
+
error::Result, function, method, value::Lazy, Module, Object, RClass, RHash, RModule, Ruby,
|
4
|
+
};
|
5
|
+
use regex::{Regex, RegexSet};
|
3
6
|
use request::itsi_request::ItsiRequest;
|
4
7
|
use response::itsi_response::ItsiResponse;
|
5
8
|
use server::{itsi_server::Server, signal::reset_signal_handlers};
|
@@ -54,6 +57,70 @@ pub fn log_error(msg: String) {
|
|
54
57
|
error!(msg);
|
55
58
|
}
|
56
59
|
|
60
|
+
const ROUTES: [&str; 39] = [
|
61
|
+
r"(?-u)^/organisations/(?<organisation_id>\d+)/users/(?<user_id>\d+)$",
|
62
|
+
r"(?-u)^/projects/(?<project_id>\d+)/tasks/(?<task_id>\d+)$",
|
63
|
+
r"(?-u)^/products/(?<product_id>\d+)(?:/reviews/(?<review_id>\d+))?$",
|
64
|
+
r"(?-u)^/orders/(?<order_id>\d+)/items(?:/(?<item_id>\d+))?$",
|
65
|
+
r"(?-u)^/posts/(?<post_id>\d+)/comments(?:/(?<comment_id>\d+))?$",
|
66
|
+
r"(?-u)^/teams/(?<team_id>\d+)(?:/members/(?<member_id>\d+))?$",
|
67
|
+
r"(?-u)^/categories/(?<category_id>\d+)/subcategories(?:/(?<subcategory_id>\d+))?$",
|
68
|
+
r"(?-u)^/departments/(?<department_id>\d+)/employees/(?<employee_id>\d+)$",
|
69
|
+
r"(?-u)^/events/(?<event_id>\d+)(?:/sessions/(?<session_id>\d+))?$",
|
70
|
+
r"(?-u)^/invoices/(?<invoice_id>\d+)/payments(?:/(?<payment_id>\d+))?$",
|
71
|
+
r"(?-u)^/tickets/(?<ticket_id>\d+)(?:/responses/(?<response_id>\d+))?$",
|
72
|
+
r"(?-u)^/forums/(?<forum_id>\d+)(?:/threads/(?<thread_id>\d+))?$",
|
73
|
+
r"(?-u)^/subscriptions/(?<subscription_id>\d+)/plans(?:/(?<plan_id>\d+))?$",
|
74
|
+
r"(?-u)^/profiles/(?<profile_id>\d+)/settings$",
|
75
|
+
r"(?-u)^/organizations/(?<organization_id>\d+)/billing(?:/(?<billing_id>\d+))?$",
|
76
|
+
r"(?-u)^/vendors/(?<vendor_id>\d+)/products(?:/(?<product_id>\d+))?$",
|
77
|
+
r"(?-u)^/courses/(?<course_id>\d+)/modules(?:/(?<module_id>\d+))?$",
|
78
|
+
r"(?-u)^/accounts/(?<account_id>\d+)(?:/transactions/(?<transaction_id>\d+))?$",
|
79
|
+
r"(?-u)^/warehouses/(?<warehouse_id>\d+)/inventory(?:/(?<inventory_id>\d+))?$",
|
80
|
+
r"(?-u)^/campaigns/(?<campaign_id>\d+)/ads(?:/(?<ad_id>\d+))?$",
|
81
|
+
r"(?-u)^/applications/(?<application_id>\d+)/stages(?:/(?<stage_id>\d+))?$",
|
82
|
+
r"(?-u)^/notifications/(?<notification_id>\d+)$",
|
83
|
+
r"(?-u)^/albums/(?<album_id>\d+)/photos(?:/(?<photo_id>\d+))?$",
|
84
|
+
r"(?-u)^/news/(?<news_id>\d+)/articles(?:/(?<article_id>\d+))?$",
|
85
|
+
r"(?-u)^/libraries/(?<library_id>\d+)/books(?:/(?<book_id>\d+))?$",
|
86
|
+
r"(?-u)^/universities/(?<university_id>\d+)/students(?:/(?<student_id>\d+))?$",
|
87
|
+
r"(?-u)^/banks/(?<bank_id>\d+)/branches(?:/(?<branch_id>\d+))?$",
|
88
|
+
r"(?-u)^/vehicles/(?<vehicle_id>\d+)/services(?:/(?<service_id>\d+))?$",
|
89
|
+
r"(?-u)^/hotels/(?<hotel_id>\d+)/rooms(?:/(?<room_id>\d+))?$",
|
90
|
+
r"(?-u)^/doctors/(?<doctor_id>\d+)/appointments(?:/(?<appointment_id>\d+))?$",
|
91
|
+
r"(?-u)^/gyms/(?<gym_id>\d+)/memberships(?:/(?<membership_id>\d+))?$",
|
92
|
+
r"(?-u)^/restaurants/(?<restaurant_id>\d+)/menus(?:/(?<menu_id>\d+))?$",
|
93
|
+
r"(?-u)^/parks/(?<park_id>\d+)/events(?:/(?<event_id>\d+))?$",
|
94
|
+
r"(?-u)^/theaters/(?<theater_id>\d+)/shows(?:/(?<show_id>\d+))?$",
|
95
|
+
r"(?-u)^/museums/(?<museum_id>\d+)/exhibits(?:/(?<exhibit_id>\d+))?$",
|
96
|
+
r"(?-u)^/stadiums/(?<stadium_id>\d+)/games(?:/(?<game_id>\d+))?$",
|
97
|
+
r"(?-u)^/schools/(?<school_id>\d+)/classes(?:/(?<class_id>\d+))?$",
|
98
|
+
r"(?-u)^/clubs/(?<club_id>\d+)/events(?:/(?<event_id>\d+))?$",
|
99
|
+
r"(?-u)^/festivals/(?<festival_id>\d+)/tickets(?:/(?<ticket_id>\d+))?$",
|
100
|
+
];
|
101
|
+
use std::sync::LazyLock;
|
102
|
+
|
103
|
+
static REGEX_SET: LazyLock<RegexSet> = LazyLock::new(|| RegexSet::new(ROUTES).unwrap());
|
104
|
+
static REGEXES: LazyLock<Vec<Regex>> =
|
105
|
+
LazyLock::new(|| ROUTES.iter().map(|&r| Regex::new(r).unwrap()).collect());
|
106
|
+
|
107
|
+
fn match_route(input: String) -> Result<Option<(usize, Option<RHash>)>> {
|
108
|
+
if let Some(index) = REGEX_SET.matches(&input).iter().next() {
|
109
|
+
let regex = ®EXES[index];
|
110
|
+
if let Some(captures) = regex.captures(&input) {
|
111
|
+
let params = RHash::with_capacity(captures.len());
|
112
|
+
for name in regex.capture_names().flatten() {
|
113
|
+
if let Some(value) = captures.name(name) {
|
114
|
+
params.aset(name, value.as_str()).ok();
|
115
|
+
}
|
116
|
+
}
|
117
|
+
return Ok(Some((index, Some(params))));
|
118
|
+
}
|
119
|
+
return Ok(Some((index, None)));
|
120
|
+
}
|
121
|
+
Ok(None)
|
122
|
+
}
|
123
|
+
|
57
124
|
#[magnus::init]
|
58
125
|
fn init(ruby: &Ruby) -> Result<()> {
|
59
126
|
itsi_tracing::init();
|
@@ -62,6 +129,7 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
62
129
|
.ok();
|
63
130
|
|
64
131
|
let itsi = ruby.get_inner(&ITSI_MODULE);
|
132
|
+
itsi.define_singleton_method("match_route", function!(match_route, 1))?;
|
65
133
|
itsi.define_singleton_method("log_debug", function!(log_debug, 1))?;
|
66
134
|
itsi.define_singleton_method("log_info", function!(log_info, 1))?;
|
67
135
|
itsi.define_singleton_method("log_warn", function!(log_warn, 1))?;
|
@@ -71,6 +139,7 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
71
139
|
server.define_singleton_method("new", function!(Server::new, -1))?;
|
72
140
|
server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
|
73
141
|
server.define_method("start", method!(Server::start, 0))?;
|
142
|
+
server.define_method("stop", method!(Server::stop, 0))?;
|
74
143
|
|
75
144
|
let request = ruby.get_inner(&ITSI_REQUEST);
|
76
145
|
request.define_method("path", method!(ItsiRequest::path, 0))?;
|
@@ -86,6 +155,8 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
86
155
|
request.define_method("port", method!(ItsiRequest::port, 0))?;
|
87
156
|
request.define_method("body", method!(ItsiRequest::body, 0))?;
|
88
157
|
request.define_method("response", method!(ItsiRequest::response, 0))?;
|
158
|
+
request.define_method("json?", method!(ItsiRequest::is_json, 0))?;
|
159
|
+
request.define_method("html?", method!(ItsiRequest::is_html, 0))?;
|
89
160
|
|
90
161
|
let body_proxy = ruby.get_inner(&ITSI_BODY_PROXY);
|
91
162
|
body_proxy.define_method("gets", method!(ItsiBodyProxy::gets, 0))?;
|
@@ -102,6 +173,8 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
102
173
|
response.define_method("close_read", method!(ItsiResponse::close_read, 0))?;
|
103
174
|
response.define_method("close", method!(ItsiResponse::close, 0))?;
|
104
175
|
response.define_method("hijack", method!(ItsiResponse::hijack, 1))?;
|
176
|
+
response.define_method("json?", method!(ItsiResponse::is_json, 0))?;
|
177
|
+
response.define_method("html?", method!(ItsiResponse::is_html, 0))?;
|
105
178
|
|
106
179
|
Ok(())
|
107
180
|
}
|
@@ -13,10 +13,11 @@ use crate::{
|
|
13
13
|
use bytes::Bytes;
|
14
14
|
use derive_more::Debug;
|
15
15
|
use futures::StreamExt;
|
16
|
-
use http::{request::Parts, Response, StatusCode};
|
16
|
+
use http::{request::Parts, HeaderValue, Response, StatusCode};
|
17
17
|
use http_body_util::{combinators::BoxBody, BodyExt, Empty};
|
18
18
|
use hyper::{body::Incoming, Request};
|
19
19
|
use itsi_error::from::CLIENT_CONNECTION_CLOSED;
|
20
|
+
use itsi_rb_helpers::print_rb_backtrace;
|
20
21
|
use itsi_tracing::{debug, error};
|
21
22
|
use magnus::{
|
22
23
|
error::{ErrorType, Result as MagnusResult},
|
@@ -33,7 +34,6 @@ use tokio::sync::{
|
|
33
34
|
};
|
34
35
|
static ID_CALL: LazyId = LazyId::new("call");
|
35
36
|
static ID_MESSAGE: LazyId = LazyId::new("message");
|
36
|
-
static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
|
37
37
|
|
38
38
|
#[derive(Debug)]
|
39
39
|
#[magnus::wrap(class = "Itsi::Request", free_immediately, size)]
|
@@ -49,6 +49,7 @@ pub struct ItsiRequest {
|
|
49
49
|
pub server: Arc<Server>,
|
50
50
|
pub response: ItsiResponse,
|
51
51
|
pub start: Instant,
|
52
|
+
pub content_type: String,
|
52
53
|
}
|
53
54
|
|
54
55
|
impl fmt::Display for ItsiRequest {
|
@@ -82,6 +83,14 @@ impl ItsiRequest {
|
|
82
83
|
}
|
83
84
|
}
|
84
85
|
|
86
|
+
pub fn is_json(&self) -> bool {
|
87
|
+
self.content_type.eq("application/json")
|
88
|
+
}
|
89
|
+
|
90
|
+
pub fn is_html(&self) -> bool {
|
91
|
+
self.content_type.eq("text/html")
|
92
|
+
}
|
93
|
+
|
85
94
|
pub fn process(
|
86
95
|
self,
|
87
96
|
ruby: &Ruby,
|
@@ -106,14 +115,7 @@ impl ItsiRequest {
|
|
106
115
|
debug!("Connection closed by client");
|
107
116
|
response.close();
|
108
117
|
} else if let Some(rb_err) = err.value() {
|
109
|
-
|
110
|
-
.funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
|
111
|
-
.unwrap_or_default();
|
112
|
-
|
113
|
-
error!("Error occurred in Handler: {:?}", rb_err);
|
114
|
-
for line in backtrace {
|
115
|
-
error!("{}", line);
|
116
|
-
}
|
118
|
+
print_rb_backtrace(rb_err);
|
117
119
|
response.internal_server_error(err.to_string());
|
118
120
|
} else {
|
119
121
|
response.internal_server_error(err.to_string());
|
@@ -175,8 +177,27 @@ impl ItsiRequest {
|
|
175
177
|
server,
|
176
178
|
listener,
|
177
179
|
version: format!("{:?}", &parts.version),
|
178
|
-
response: ItsiResponse::new(
|
180
|
+
response: ItsiResponse::new(
|
181
|
+
parts.clone(),
|
182
|
+
response_channel.0,
|
183
|
+
parts
|
184
|
+
.headers
|
185
|
+
.get("Accept")
|
186
|
+
.unwrap_or(&HeaderValue::from_static("text/html"))
|
187
|
+
.to_str()
|
188
|
+
.unwrap()
|
189
|
+
.to_string(),
|
190
|
+
),
|
179
191
|
start: Instant::now(),
|
192
|
+
content_type: parts
|
193
|
+
.headers
|
194
|
+
.get("Content-Type")
|
195
|
+
.unwrap_or(&HeaderValue::from_static(
|
196
|
+
"application/x-www-form-urlencoded",
|
197
|
+
))
|
198
|
+
.to_str()
|
199
|
+
.unwrap()
|
200
|
+
.to_string(),
|
180
201
|
parts,
|
181
202
|
},
|
182
203
|
response_channel.1,
|
@@ -37,6 +37,7 @@ use crate::server::serve_strategy::single_mode::RunningPhase;
|
|
37
37
|
#[derive(Debug, Clone)]
|
38
38
|
pub struct ItsiResponse {
|
39
39
|
pub data: Arc<ResponseData>,
|
40
|
+
pub accept: String,
|
40
41
|
}
|
41
42
|
|
42
43
|
#[derive(Debug)]
|
@@ -76,7 +77,6 @@ impl ItsiResponse {
|
|
76
77
|
(ReceiverStream::new(receiver), shutdown_rx),
|
77
78
|
|(mut receiver, mut shutdown_rx)| async move {
|
78
79
|
if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
|
79
|
-
warn!("Disconnecting streaming client.");
|
80
80
|
return None;
|
81
81
|
}
|
82
82
|
loop {
|
@@ -279,7 +279,8 @@ impl ItsiResponse {
|
|
279
279
|
if let Some(writer) = writer.write().as_ref() {
|
280
280
|
writer
|
281
281
|
.blocking_send(Some(frame))
|
282
|
-
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
|
282
|
+
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
|
283
|
+
.ok();
|
283
284
|
}
|
284
285
|
Ok(0)
|
285
286
|
}
|
@@ -293,11 +294,19 @@ impl ItsiResponse {
|
|
293
294
|
Ok(true)
|
294
295
|
}
|
295
296
|
|
297
|
+
pub fn is_html(&self) -> bool {
|
298
|
+
self.accept.starts_with("text/html")
|
299
|
+
}
|
300
|
+
|
301
|
+
pub fn is_json(&self) -> bool {
|
302
|
+
self.accept.starts_with("application/json")
|
303
|
+
}
|
304
|
+
|
296
305
|
pub fn close_read(&self) -> MagnusResult<bool> {
|
297
|
-
|
306
|
+
Ok(true)
|
298
307
|
}
|
299
308
|
|
300
|
-
pub fn new(parts: Parts, response_writer: mpsc::Sender<Option<Bytes
|
309
|
+
pub fn new(parts: Parts, response_writer: mpsc::Sender<Option<Bytes>>, accept: String) -> Self {
|
301
310
|
Self {
|
302
311
|
data: Arc::new(ResponseData {
|
303
312
|
response: RwLock::new(Some(Response::new(BoxBody::new(Empty::new())))),
|
@@ -306,6 +315,7 @@ impl ItsiResponse {
|
|
306
315
|
hijacked_socket: RwLock::new(None),
|
307
316
|
parts,
|
308
317
|
}),
|
318
|
+
accept,
|
309
319
|
}
|
310
320
|
}
|
311
321
|
|
@@ -101,7 +101,7 @@ impl FromStr for Bind {
|
|
101
101
|
"IPv6 addresses must use [ ] when specifying a port".to_owned(),
|
102
102
|
));
|
103
103
|
} else {
|
104
|
-
(h,
|
104
|
+
(h, p.parse::<u16>().ok()) // Treat as a hostname
|
105
105
|
}
|
106
106
|
} else {
|
107
107
|
(url, None)
|
@@ -110,18 +110,22 @@ impl FromStr for Bind {
|
|
110
110
|
let address = if let Ok(ip) = host.parse::<IpAddr>() {
|
111
111
|
BindAddress::Ip(ip)
|
112
112
|
} else {
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
113
|
+
match protocol {
|
114
|
+
BindProtocol::Https | BindProtocol::Http => resolve_hostname(host)
|
115
|
+
.map(BindAddress::Ip)
|
116
|
+
.ok_or(ItsiError::ArgumentError(format!(
|
117
|
+
"Failed to resolve hostname {}",
|
118
|
+
host
|
119
|
+
)))?,
|
120
|
+
BindProtocol::Unix | BindProtocol::Unixs => BindAddress::UnixSocket(host.into()),
|
121
|
+
}
|
119
122
|
};
|
120
|
-
|
121
|
-
|
122
|
-
BindProtocol::
|
123
|
-
BindProtocol::
|
124
|
-
BindProtocol::
|
123
|
+
|
124
|
+
let port = match protocol {
|
125
|
+
BindProtocol::Http => port.or(Some(80)),
|
126
|
+
BindProtocol::Https => port.or(Some(443)),
|
127
|
+
BindProtocol::Unix => None,
|
128
|
+
BindProtocol::Unixs => None,
|
125
129
|
};
|
126
130
|
|
127
131
|
let tls_config = match protocol {
|