itsi-server 0.1.8 → 0.1.9
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/Rakefile +8 -1
- data/ext/itsi_server/src/lib.rs +5 -0
- data/ext/itsi_server/src/request/itsi_request.rs +30 -2
- data/ext/itsi_server/src/response/itsi_response.rs +12 -2
- data/ext/itsi_server/src/server/itsi_server.rs +127 -70
- data/ext/itsi_server/src/server/listener.rs +1 -1
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +18 -12
- data/ext/itsi_server/src/server/signal.rs +7 -0
- data/ext/itsi_server/src/server/thread_worker.rs +3 -4
- data/ext/itsi_server/src/server/tls.rs +11 -8
- data/ext/itsi_tracing/src/lib.rs +18 -1
- data/lib/itsi/request.rb +29 -21
- data/lib/itsi/server/rack/handler/itsi.rb +3 -4
- data/lib/itsi/server/rack_interface.rb +79 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/signal_trap.rb +24 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +67 -101
- metadata +6 -4
- data/lib/itsi/signals.rb +0 -23
- /data/lib/itsi/{index.html.erb → index.html} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 479b2a6c1ed83ad8996367e19ca025b9b53172a66a143128eb2399c98a1b2f7d
|
4
|
+
data.tar.gz: '0687d23ed59b8a9fe81055d85c74cb374b679eae6025a9c89307591cf881a7bf'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b6ee383356c06fb6dfe62f3f1a2c9e631d584246f300ad3b6eb2b507f508099560c97d661cedf762553b6d062d6daece0ec9c7244971d1576be929b1e2cbad2
|
7
|
+
data.tar.gz: 78045f8853d52c243b8a458dc46d85199daf8a6c15984492644826c3fa0c5edfdd61f8842892b03a887c85441f5ce96ae67d05fe8f85f0db5370a31732f3272c
|
data/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/ext/itsi_server/src/lib.rs
CHANGED
@@ -71,6 +71,7 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
71
71
|
server.define_singleton_method("new", function!(Server::new, -1))?;
|
72
72
|
server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
|
73
73
|
server.define_method("start", method!(Server::start, 0))?;
|
74
|
+
server.define_method("stop", method!(Server::stop, 0))?;
|
74
75
|
|
75
76
|
let request = ruby.get_inner(&ITSI_REQUEST);
|
76
77
|
request.define_method("path", method!(ItsiRequest::path, 0))?;
|
@@ -86,6 +87,8 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
86
87
|
request.define_method("port", method!(ItsiRequest::port, 0))?;
|
87
88
|
request.define_method("body", method!(ItsiRequest::body, 0))?;
|
88
89
|
request.define_method("response", method!(ItsiRequest::response, 0))?;
|
90
|
+
request.define_method("json?", method!(ItsiRequest::is_json, 0))?;
|
91
|
+
request.define_method("html?", method!(ItsiRequest::is_html, 0))?;
|
89
92
|
|
90
93
|
let body_proxy = ruby.get_inner(&ITSI_BODY_PROXY);
|
91
94
|
body_proxy.define_method("gets", method!(ItsiBodyProxy::gets, 0))?;
|
@@ -102,6 +105,8 @@ fn init(ruby: &Ruby) -> Result<()> {
|
|
102
105
|
response.define_method("close_read", method!(ItsiResponse::close_read, 0))?;
|
103
106
|
response.define_method("close", method!(ItsiResponse::close, 0))?;
|
104
107
|
response.define_method("hijack", method!(ItsiResponse::hijack, 1))?;
|
108
|
+
response.define_method("json?", method!(ItsiResponse::is_json, 0))?;
|
109
|
+
response.define_method("html?", method!(ItsiResponse::is_html, 0))?;
|
105
110
|
|
106
111
|
Ok(())
|
107
112
|
}
|
@@ -13,7 +13,7 @@ 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;
|
@@ -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,
|
@@ -175,8 +184,27 @@ impl ItsiRequest {
|
|
175
184
|
server,
|
176
185
|
listener,
|
177
186
|
version: format!("{:?}", &parts.version),
|
178
|
-
response: ItsiResponse::new(
|
187
|
+
response: ItsiResponse::new(
|
188
|
+
parts.clone(),
|
189
|
+
response_channel.0,
|
190
|
+
parts
|
191
|
+
.headers
|
192
|
+
.get("Accept")
|
193
|
+
.unwrap_or(&HeaderValue::from_static("text/html"))
|
194
|
+
.to_str()
|
195
|
+
.unwrap()
|
196
|
+
.to_string(),
|
197
|
+
),
|
179
198
|
start: Instant::now(),
|
199
|
+
content_type: parts
|
200
|
+
.headers
|
201
|
+
.get("Content-Type")
|
202
|
+
.unwrap_or(&HeaderValue::from_static(
|
203
|
+
"application/x-www-form-urlencoded",
|
204
|
+
))
|
205
|
+
.to_str()
|
206
|
+
.unwrap()
|
207
|
+
.to_string(),
|
180
208
|
parts,
|
181
209
|
},
|
182
210
|
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)]
|
@@ -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
|
|
@@ -2,20 +2,22 @@ use super::{
|
|
2
2
|
bind::Bind,
|
3
3
|
listener::Listener,
|
4
4
|
serve_strategy::{cluster_mode::ClusterMode, single_mode::SingleMode},
|
5
|
-
signal::{
|
5
|
+
signal::{
|
6
|
+
clear_signal_handlers, reset_signal_handlers, send_shutdown_event, SIGNAL_HANDLER_CHANNEL,
|
7
|
+
},
|
6
8
|
};
|
7
9
|
use crate::{request::itsi_request::ItsiRequest, server::serve_strategy::ServeStrategy};
|
8
10
|
use derive_more::Debug;
|
9
|
-
use itsi_rb_helpers::call_without_gvl;
|
10
|
-
use itsi_tracing::error;
|
11
|
+
use itsi_rb_helpers::{call_without_gvl, HeapVal};
|
12
|
+
use itsi_tracing::{error, run_silently};
|
11
13
|
use magnus::{
|
12
14
|
block::Proc,
|
13
15
|
error::Result,
|
14
|
-
scan_args::{get_kwargs, scan_args, Args, KwArgs},
|
16
|
+
scan_args::{get_kwargs, scan_args, Args, KwArgs, ScanArgsKw, ScanArgsOpt, ScanArgsRequired},
|
15
17
|
value::{InnerValue, Opaque, ReprValue},
|
16
|
-
RHash, Ruby, Symbol, Value,
|
18
|
+
ArgList, RHash, Ruby, Symbol, Value,
|
17
19
|
};
|
18
|
-
use parking_lot::Mutex;
|
20
|
+
use parking_lot::{Mutex, RwLock};
|
19
21
|
use std::{cmp::max, ops::Deref, sync::Arc};
|
20
22
|
use tracing::{info, instrument};
|
21
23
|
|
@@ -39,7 +41,7 @@ type AfterFork = Mutex<Arc<Option<Box<dyn Fn() + Send + Sync>>>>;
|
|
39
41
|
#[derive(Debug)]
|
40
42
|
pub struct ServerConfig {
|
41
43
|
#[debug(skip)]
|
42
|
-
pub app:
|
44
|
+
pub app: HeapVal,
|
43
45
|
#[allow(unused)]
|
44
46
|
pub workers: u8,
|
45
47
|
#[allow(unused)]
|
@@ -55,6 +57,9 @@ pub struct ServerConfig {
|
|
55
57
|
pub scheduler_class: Option<String>,
|
56
58
|
pub stream_body: Option<bool>,
|
57
59
|
pub worker_memory_limit: Option<u64>,
|
60
|
+
#[debug(skip)]
|
61
|
+
pub(crate) strategy: RwLock<Option<ServeStrategy>>,
|
62
|
+
pub silence: bool,
|
58
63
|
}
|
59
64
|
|
60
65
|
#[derive(Debug)]
|
@@ -63,6 +68,35 @@ pub enum RequestJob {
|
|
63
68
|
Shutdown,
|
64
69
|
}
|
65
70
|
|
71
|
+
// Define your helper function.
|
72
|
+
// Here P, A, C correspond to the types for the first tuple, second tuple, and extra parameters respectively.
|
73
|
+
fn extract_args<Req, Opt, Splat>(
|
74
|
+
scan_args: &Args<(), (), (), (), RHash, ()>,
|
75
|
+
primaries: &[&str],
|
76
|
+
rest: &[&str],
|
77
|
+
) -> Result<KwArgs<Req, Opt, Splat>>
|
78
|
+
where
|
79
|
+
Req: ScanArgsRequired,
|
80
|
+
Opt: ScanArgsOpt,
|
81
|
+
Splat: ScanArgsKw,
|
82
|
+
{
|
83
|
+
// Combine the primary and rest names into one Vec of Symbols.
|
84
|
+
let symbols: Vec<Symbol> = primaries
|
85
|
+
.iter()
|
86
|
+
.chain(rest.iter())
|
87
|
+
.map(|&name| Symbol::new(name))
|
88
|
+
.collect();
|
89
|
+
|
90
|
+
// Call the "slice" function with the combined symbols.
|
91
|
+
let hash = scan_args
|
92
|
+
.keywords
|
93
|
+
.funcall::<_, _, RHash>("slice", symbols.into_arg_list_with(&Ruby::get().unwrap()))
|
94
|
+
.unwrap();
|
95
|
+
|
96
|
+
// Finally, call get_kwargs with the original name slices.
|
97
|
+
get_kwargs(hash, primaries, rest)
|
98
|
+
}
|
99
|
+
|
66
100
|
impl Server {
|
67
101
|
#[instrument(
|
68
102
|
name = "Itsi",
|
@@ -73,39 +107,44 @@ impl Server {
|
|
73
107
|
pub fn new(args: &[Value]) -> Result<Self> {
|
74
108
|
let scan_args: Args<(), (), (), (), RHash, ()> = scan_args(args)?;
|
75
109
|
|
76
|
-
type
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
110
|
+
type Args1 = KwArgs<
|
111
|
+
(Value,),
|
112
|
+
(
|
113
|
+
// Workers
|
114
|
+
Option<u8>,
|
115
|
+
// Threads
|
116
|
+
Option<u8>,
|
117
|
+
// Shutdown Timeout
|
118
|
+
Option<f64>,
|
119
|
+
// Script Name
|
120
|
+
Option<String>,
|
121
|
+
// Binds
|
122
|
+
Option<Vec<String>>,
|
123
|
+
// Stream Body
|
124
|
+
Option<bool>,
|
125
|
+
),
|
126
|
+
(),
|
127
|
+
>;
|
128
|
+
|
129
|
+
type Args2 = KwArgs<
|
130
|
+
(),
|
131
|
+
(
|
132
|
+
// Before Fork
|
133
|
+
Option<Proc>,
|
134
|
+
// After Fork
|
135
|
+
Option<Proc>,
|
136
|
+
// Scheduler Class
|
137
|
+
Option<String>,
|
138
|
+
// Worker Memory Limit
|
139
|
+
Option<u64>,
|
140
|
+
// Silence
|
141
|
+
Option<bool>,
|
142
|
+
),
|
143
|
+
(),
|
144
|
+
>;
|
145
|
+
|
146
|
+
let args1: Args1 = extract_args(
|
147
|
+
&scan_args,
|
109
148
|
&["app"],
|
110
149
|
&[
|
111
150
|
"workers",
|
@@ -113,24 +152,24 @@ impl Server {
|
|
113
152
|
"shutdown_timeout",
|
114
153
|
"script_name",
|
115
154
|
"binds",
|
116
|
-
"before_fork",
|
117
|
-
"after_fork",
|
118
|
-
"scheduler_class",
|
119
155
|
"stream_body",
|
120
156
|
],
|
121
157
|
)?;
|
122
158
|
|
123
|
-
let args2:
|
124
|
-
scan_args
|
125
|
-
.keywords
|
126
|
-
.funcall::<_, _, RHash>("slice", (Symbol::new("worker_memory_limit"),))
|
127
|
-
.unwrap(),
|
159
|
+
let args2: Args2 = extract_args(
|
160
|
+
&scan_args,
|
128
161
|
&[],
|
129
|
-
&[
|
162
|
+
&[
|
163
|
+
"before_fork",
|
164
|
+
"after_fork",
|
165
|
+
"scheduler_class",
|
166
|
+
"worker_memory_limit",
|
167
|
+
"silence",
|
168
|
+
],
|
130
169
|
)?;
|
131
170
|
|
132
171
|
let config = ServerConfig {
|
133
|
-
app:
|
172
|
+
app: HeapVal::from(args1.required.0),
|
134
173
|
workers: max(args1.optional.0.unwrap_or(1), 1),
|
135
174
|
threads: max(args1.optional.1.unwrap_or(1), 1),
|
136
175
|
shutdown_timeout: args1.optional.2.unwrap_or(5.0),
|
@@ -144,7 +183,8 @@ impl Server {
|
|
144
183
|
.map(|s| s.parse())
|
145
184
|
.collect::<itsi_error::Result<Vec<Bind>>>()?,
|
146
185
|
),
|
147
|
-
|
186
|
+
stream_body: args1.optional.5,
|
187
|
+
before_fork: Mutex::new(args2.optional.0.map(|p| {
|
148
188
|
let opaque_proc = Opaque::from(p);
|
149
189
|
Box::new(move || {
|
150
190
|
opaque_proc
|
@@ -153,7 +193,7 @@ impl Server {
|
|
153
193
|
.unwrap();
|
154
194
|
}) as Box<dyn FnOnce() + Send + Sync>
|
155
195
|
})),
|
156
|
-
after_fork: Mutex::new(Arc::new(
|
196
|
+
after_fork: Mutex::new(Arc::new(args2.optional.1.map(|p| {
|
157
197
|
let opaque_proc = Opaque::from(p);
|
158
198
|
Box::new(move || {
|
159
199
|
opaque_proc
|
@@ -162,15 +202,18 @@ impl Server {
|
|
162
202
|
.unwrap();
|
163
203
|
}) as Box<dyn Fn() + Send + Sync>
|
164
204
|
}))),
|
165
|
-
scheduler_class:
|
166
|
-
|
167
|
-
|
205
|
+
scheduler_class: args2.optional.2.clone(),
|
206
|
+
worker_memory_limit: args2.optional.3,
|
207
|
+
silence: args2.optional.4.is_some_and(|s| s),
|
208
|
+
strategy: RwLock::new(None),
|
168
209
|
};
|
169
210
|
|
170
|
-
if
|
171
|
-
|
172
|
-
|
173
|
-
|
211
|
+
if !config.silence {
|
212
|
+
if let Some(scheduler_class) = args2.optional.2 {
|
213
|
+
info!(scheduler_class, fiber_scheduler = true);
|
214
|
+
} else {
|
215
|
+
info!(fiber_scheduler = false);
|
216
|
+
}
|
174
217
|
}
|
175
218
|
|
176
219
|
Ok(Server {
|
@@ -179,7 +222,7 @@ impl Server {
|
|
179
222
|
}
|
180
223
|
|
181
224
|
#[instrument(name = "Bind", skip_all, fields(binds=format!("{:?}", self.config.binds.lock())))]
|
182
|
-
pub(crate) fn
|
225
|
+
pub(crate) fn build_listeners(&self) -> Result<Arc<Vec<Arc<Listener>>>> {
|
183
226
|
let listeners = self
|
184
227
|
.config
|
185
228
|
.binds
|
@@ -195,11 +238,9 @@ impl Server {
|
|
195
238
|
Ok(Arc::new(listeners))
|
196
239
|
}
|
197
240
|
|
198
|
-
pub(crate) fn build_strategy(
|
199
|
-
self,
|
200
|
-
listeners: Arc<Vec<Arc<Listener>>>,
|
201
|
-
) -> Result<ServeStrategy> {
|
241
|
+
pub(crate) fn build_strategy(self, listeners: Arc<Vec<Arc<Listener>>>) -> Result<()> {
|
202
242
|
let server = Arc::new(self);
|
243
|
+
let server_clone = server.clone();
|
203
244
|
|
204
245
|
let strategy = if server.config.workers == 1 {
|
205
246
|
ServeStrategy::Single(Arc::new(SingleMode::new(
|
@@ -214,24 +255,40 @@ impl Server {
|
|
214
255
|
SIGNAL_HANDLER_CHANNEL.0.clone(),
|
215
256
|
)))
|
216
257
|
};
|
217
|
-
|
258
|
+
|
259
|
+
*server_clone.strategy.write() = Some(strategy);
|
260
|
+
Ok(())
|
261
|
+
}
|
262
|
+
|
263
|
+
pub fn stop(&self) -> Result<()> {
|
264
|
+
send_shutdown_event();
|
265
|
+
Ok(())
|
218
266
|
}
|
219
267
|
|
220
268
|
pub fn start(&self) -> Result<()> {
|
269
|
+
if self.silence {
|
270
|
+
run_silently(|| self.build_and_run_strategy())
|
271
|
+
} else {
|
272
|
+
self.build_and_run_strategy()
|
273
|
+
}
|
274
|
+
}
|
275
|
+
|
276
|
+
fn build_and_run_strategy(&self) -> Result<()> {
|
221
277
|
reset_signal_handlers();
|
222
278
|
let rself = self.clone();
|
223
|
-
let listeners = self.
|
279
|
+
let listeners = self.build_listeners()?;
|
224
280
|
let listeners_clone = listeners.clone();
|
225
281
|
call_without_gvl(move || -> Result<()> {
|
226
|
-
|
227
|
-
if let Err(e) = strategy.run() {
|
282
|
+
rself.clone().build_strategy(listeners_clone)?;
|
283
|
+
if let Err(e) = rself.clone().strategy.read().as_ref().unwrap().run() {
|
228
284
|
error!("Error running server: {}", e);
|
229
|
-
strategy.stop()?;
|
285
|
+
rself.strategy.read().as_ref().unwrap().stop()?;
|
230
286
|
}
|
231
|
-
drop(strategy);
|
232
287
|
Ok(())
|
233
288
|
})?;
|
234
289
|
clear_signal_handlers();
|
290
|
+
self.strategy.write().take();
|
291
|
+
info!("Server stopped");
|
235
292
|
Ok(())
|
236
293
|
}
|
237
294
|
}
|
@@ -117,7 +117,7 @@ impl TokioListener {
|
|
117
117
|
tokio::select! {
|
118
118
|
stream_event = StreamExt::next(&mut *state) => {
|
119
119
|
match stream_event {
|
120
|
-
Some(event) => info!("
|
120
|
+
Some(event) => info!("ACME Event: {:?}", event),
|
121
121
|
None => error!("Received no acme event"),
|
122
122
|
}
|
123
123
|
},
|
@@ -25,7 +25,10 @@ use std::{
|
|
25
25
|
};
|
26
26
|
use tokio::{
|
27
27
|
runtime::{Builder as RuntimeBuilder, Runtime},
|
28
|
-
sync::
|
28
|
+
sync::{
|
29
|
+
broadcast,
|
30
|
+
watch::{self, Sender},
|
31
|
+
},
|
29
32
|
task::JoinSet,
|
30
33
|
};
|
31
34
|
use tracing::instrument;
|
@@ -55,7 +58,7 @@ impl SingleMode {
|
|
55
58
|
let (thread_workers, sender) = build_thread_workers(
|
56
59
|
Pid::this(),
|
57
60
|
NonZeroU8::try_from(server.threads).unwrap(),
|
58
|
-
server.app,
|
61
|
+
server.app.clone(),
|
59
62
|
server.scheduler_class.clone(),
|
60
63
|
)?;
|
61
64
|
Ok(Self {
|
@@ -80,6 +83,9 @@ impl SingleMode {
|
|
80
83
|
}
|
81
84
|
|
82
85
|
pub fn stop(&self) -> Result<()> {
|
86
|
+
self.lifecycle_channel
|
87
|
+
.send(LifecycleEvent::Shutdown)
|
88
|
+
.expect("Failed to send shutdown event");
|
83
89
|
Ok(())
|
84
90
|
}
|
85
91
|
|
@@ -95,14 +101,17 @@ impl SingleMode {
|
|
95
101
|
.iter()
|
96
102
|
.map(|list| Arc::new(list.to_tokio_listener()))
|
97
103
|
.collect::<Vec<_>>();
|
104
|
+
let (shutdown_sender, _) = watch::channel::<RunningPhase>(RunningPhase::Running);
|
98
105
|
for listener in tokio_listeners.iter() {
|
99
106
|
let mut lifecycle_rx = self_ref.lifecycle_channel.subscribe();
|
100
107
|
let listener_info = Arc::new(listener.listener_info());
|
101
108
|
let self_ref = self_ref.clone();
|
102
109
|
let listener = listener.clone();
|
103
|
-
let
|
104
|
-
let listener_clone = listener.clone();
|
110
|
+
let shutdown_sender = shutdown_sender.clone();
|
105
111
|
|
112
|
+
|
113
|
+
let listener_clone = listener.clone();
|
114
|
+
let mut shutdown_receiver = shutdown_sender.clone().subscribe();
|
106
115
|
let shutdown_receiver_clone = shutdown_receiver.clone();
|
107
116
|
listener_task_set.spawn(async move {
|
108
117
|
listener_clone.spawn_state_task(shutdown_receiver_clone).await;
|
@@ -157,7 +166,7 @@ impl SingleMode {
|
|
157
166
|
&self,
|
158
167
|
stream: IoStream,
|
159
168
|
listener: Arc<ListenerInfo>,
|
160
|
-
shutdown_channel:
|
169
|
+
shutdown_channel: watch::Receiver<RunningPhase>,
|
161
170
|
) -> Result<()> {
|
162
171
|
let sender_clone = self.sender.clone();
|
163
172
|
let addr = stream.addr();
|
@@ -219,12 +228,11 @@ impl SingleMode {
|
|
219
228
|
pub async fn handle_lifecycle_event(
|
220
229
|
&self,
|
221
230
|
lifecycle_event: LifecycleEvent,
|
222
|
-
shutdown_sender:
|
231
|
+
shutdown_sender: Sender<RunningPhase>,
|
223
232
|
) -> Result<()> {
|
233
|
+
info!("Handling lifecycle event: {:?}", lifecycle_event);
|
224
234
|
if let LifecycleEvent::Shutdown = lifecycle_event {
|
225
|
-
shutdown_sender
|
226
|
-
.send(RunningPhase::ShutdownPending)
|
227
|
-
.expect("Failed to send shutdown pending signal");
|
235
|
+
shutdown_sender.send(RunningPhase::ShutdownPending).ok();
|
228
236
|
let deadline = Instant::now() + Duration::from_secs_f64(self.server.shutdown_timeout);
|
229
237
|
for worker in &*self.thread_workers {
|
230
238
|
worker.request_shutdown().await;
|
@@ -243,9 +251,7 @@ impl SingleMode {
|
|
243
251
|
}
|
244
252
|
|
245
253
|
info!("Sending shutdown signal");
|
246
|
-
shutdown_sender
|
247
|
-
.send(RunningPhase::Shutdown)
|
248
|
-
.expect("Failed to send shutdown signal");
|
254
|
+
shutdown_sender.send(RunningPhase::Shutdown).ok();
|
249
255
|
self.thread_workers.iter().for_each(|worker| {
|
250
256
|
worker.poll_shutdown(deadline);
|
251
257
|
});
|
@@ -10,6 +10,13 @@ pub static SIGNAL_HANDLER_CHANNEL: LazyLock<(
|
|
10
10
|
broadcast::Receiver<LifecycleEvent>,
|
11
11
|
)> = LazyLock::new(|| sync::broadcast::channel(5));
|
12
12
|
|
13
|
+
pub fn send_shutdown_event() {
|
14
|
+
SIGNAL_HANDLER_CHANNEL
|
15
|
+
.0
|
16
|
+
.send(LifecycleEvent::Shutdown)
|
17
|
+
.expect("Failed to send shutdown event");
|
18
|
+
}
|
19
|
+
|
13
20
|
pub static SIGINT_COUNT: AtomicI8 = AtomicI8::new(0);
|
14
21
|
fn receive_signal(signum: i32, _: sighandler_t) {
|
15
22
|
SIGINT_COUNT.fetch_add(-1, std::sync::atomic::Ordering::SeqCst);
|
@@ -1,7 +1,7 @@
|
|
1
1
|
use super::itsi_server::RequestJob;
|
2
2
|
use crate::{request::itsi_request::ItsiRequest, ITSI_SERVER};
|
3
3
|
use itsi_rb_helpers::{
|
4
|
-
call_with_gvl, call_without_gvl, create_ruby_thread, kill_threads, HeapValue,
|
4
|
+
call_with_gvl, call_without_gvl, create_ruby_thread, kill_threads, HeapVal, HeapValue,
|
5
5
|
};
|
6
6
|
use itsi_tracing::{debug, error, info, warn};
|
7
7
|
use magnus::{
|
@@ -52,7 +52,7 @@ pub struct TerminateWakerSignal(bool);
|
|
52
52
|
pub fn build_thread_workers(
|
53
53
|
pid: Pid,
|
54
54
|
threads: NonZeroU8,
|
55
|
-
app:
|
55
|
+
app: HeapVal,
|
56
56
|
scheduler_class: Option<String>,
|
57
57
|
) -> Result<(Arc<Vec<ThreadWorker>>, async_channel::Sender<RequestJob>)> {
|
58
58
|
let (sender, receiver) = async_channel::bounded(20);
|
@@ -79,11 +79,10 @@ pub fn build_thread_workers(
|
|
79
79
|
}
|
80
80
|
|
81
81
|
pub fn load_app(
|
82
|
-
app:
|
82
|
+
app: HeapVal,
|
83
83
|
scheduler_class: Option<String>,
|
84
84
|
) -> Result<(Opaque<Value>, Option<Opaque<Value>>)> {
|
85
85
|
call_with_gvl(|ruby| {
|
86
|
-
let app = app.get_inner_with(&ruby);
|
87
86
|
let app = Opaque::from(
|
88
87
|
app.funcall::<_, _, Value>(*ID_CALL, ())
|
89
88
|
.expect("Couldn't load app"),
|
@@ -48,23 +48,26 @@ pub fn configure_tls(
|
|
48
48
|
) -> Result<ItsiTlsAcceptor> {
|
49
49
|
let domains = query_params
|
50
50
|
.get("domains")
|
51
|
-
.map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
|
51
|
+
.map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
|
52
|
+
.or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
|
52
53
|
|
53
|
-
if query_params.get("cert").is_some_and(|c| c == "
|
54
|
+
if query_params.get("cert").is_some_and(|c| c == "acme") {
|
54
55
|
if let Some(domains) = domains {
|
55
56
|
let directory_url = &*ITSI_ACME_DIRECTORY_URL;
|
56
57
|
info!(
|
57
58
|
domains = format!("{:?}", domains),
|
58
59
|
directory_url, "Requesting acme cert"
|
59
60
|
);
|
61
|
+
let acme_contact_email = query_params
|
62
|
+
.get("acme_email")
|
63
|
+
.map(|s| s.to_string())
|
64
|
+
.or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
|
65
|
+
.ok_or_else(|| itsi_error::ItsiError::ArgumentError(
|
66
|
+
"acme_cert query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
|
67
|
+
))?;
|
60
68
|
|
61
69
|
let acme_config = AcmeConfig::new(domains)
|
62
|
-
.contact([format!("mailto:{}",
|
63
|
-
itsi_error::ItsiError::ArgumentError(
|
64
|
-
"ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
|
65
|
-
.to_string(),
|
66
|
-
)
|
67
|
-
})?)])
|
70
|
+
.contact([format!("mailto:{}", acme_contact_email)])
|
68
71
|
.cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
|
69
72
|
.directory(directory_url);
|
70
73
|
|
data/ext/itsi_tracing/src/lib.rs
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
use std::env;
|
2
2
|
|
3
3
|
use atty::{Stream, is};
|
4
|
+
use tracing::level_filters::LevelFilter;
|
4
5
|
pub use tracing::{debug, error, info, trace, warn};
|
5
6
|
pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
|
6
7
|
use tracing_subscriber::{
|
7
|
-
EnvFilter,
|
8
|
+
EnvFilter, Layer,
|
8
9
|
fmt::{self, format},
|
10
|
+
layer::SubscriberExt,
|
9
11
|
};
|
10
12
|
|
11
13
|
#[instrument]
|
@@ -39,3 +41,18 @@ pub fn init() {
|
|
39
41
|
.init();
|
40
42
|
}
|
41
43
|
}
|
44
|
+
|
45
|
+
pub fn run_silently<F, R>(f: F) -> R
|
46
|
+
where
|
47
|
+
F: FnOnce() -> R,
|
48
|
+
{
|
49
|
+
// Build a minimal subscriber that filters *everything* out
|
50
|
+
let no_op_subscriber =
|
51
|
+
tracing_subscriber::registry().with(fmt::layer().with_filter(LevelFilter::OFF));
|
52
|
+
|
53
|
+
// Turn that subscriber into a `Dispatch`
|
54
|
+
let no_op_dispatch = tracing::dispatcher::Dispatch::new(no_op_subscriber);
|
55
|
+
|
56
|
+
// Temporarily set `no_op_dispatch` as the *default* within this closure
|
57
|
+
tracing::dispatcher::with_default(&no_op_dispatch, f)
|
58
|
+
}
|
data/lib/itsi/request.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "stringio"
|
4
|
+
require "socket"
|
5
|
+
|
3
6
|
module Itsi
|
4
7
|
class Request
|
5
|
-
require "stringio"
|
6
|
-
require "socket"
|
7
|
-
|
8
8
|
attr_accessor :hijacked
|
9
9
|
|
10
|
-
def
|
10
|
+
def to_rack_env
|
11
11
|
path = self.path
|
12
12
|
host = self.host
|
13
13
|
version = self.version
|
14
|
-
body = self.body
|
15
14
|
{
|
16
15
|
"SERVER_SOFTWARE" => "Itsi",
|
17
16
|
"SCRIPT_NAME" => script_name,
|
@@ -25,31 +24,40 @@ module Itsi
|
|
25
24
|
"HTTP_HOST" => host,
|
26
25
|
"SERVER_PROTOCOL" => version,
|
27
26
|
"HTTP_VERSION" => version,
|
27
|
+
"itsi.request" => self,
|
28
|
+
"itsi.response" => response,
|
28
29
|
"rack.version" => [version],
|
29
30
|
"rack.url_scheme" => scheme,
|
30
|
-
"rack.input" =>
|
31
|
-
case body
|
32
|
-
when Array then File.open(body.first, "rb")
|
33
|
-
when String then StringIO.new(body)
|
34
|
-
else body
|
35
|
-
end,
|
31
|
+
"rack.input" => build_input_io,
|
36
32
|
"rack.errors" => $stderr,
|
37
33
|
"rack.multithread" => true,
|
38
34
|
"rack.multiprocess" => true,
|
39
35
|
"rack.run_once" => false,
|
40
36
|
"rack.hijack?" => true,
|
41
37
|
"rack.multipart.buffer_size" => 16_384,
|
42
|
-
"rack.hijack" =>
|
43
|
-
self.hijacked = true
|
44
|
-
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
45
|
-
response.hijack(server_sock.fileno)
|
46
|
-
server_sock.sync = true
|
47
|
-
app_sock.sync = true
|
48
|
-
app_sock.instance_variable_set("@server_sock", server_sock)
|
49
|
-
app_sock
|
50
|
-
end
|
51
|
-
end
|
38
|
+
"rack.hijack" => build_hijack_proc
|
52
39
|
}.tap { |r| headers.each { |(k, v)| r[k] = v } }
|
53
40
|
end
|
41
|
+
|
42
|
+
def build_hijack_proc
|
43
|
+
lambda do
|
44
|
+
self.hijacked = true
|
45
|
+
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
46
|
+
response.hijack(server_sock.fileno)
|
47
|
+
server_sock.sync = true
|
48
|
+
app_sock.sync = true
|
49
|
+
app_sock.instance_variable_set("@server_sock", server_sock)
|
50
|
+
app_sock
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_input_io
|
56
|
+
case body
|
57
|
+
when Array then File.open(body.first, "rb")
|
58
|
+
when String then StringIO.new(body)
|
59
|
+
else body
|
60
|
+
end
|
61
|
+
end
|
54
62
|
end
|
55
63
|
end
|
@@ -3,13 +3,12 @@ return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
|
|
3
3
|
module Rack
|
4
4
|
module Handler
|
5
5
|
module Itsi
|
6
|
-
|
7
6
|
def self.run(app, options = {})
|
8
7
|
::Itsi::Server.new(
|
9
|
-
app: ->{ app },
|
10
|
-
binds: ["
|
8
|
+
app: -> { app },
|
9
|
+
binds: ["http://#{options.fetch(:host, "127.0.0.1")}:#{options.fetch(:Port, 3001)}"],
|
11
10
|
workers: options.fetch(:workers, 1),
|
12
|
-
threads: options.fetch(:threads, 1)
|
11
|
+
threads: options.fetch(:threads, 1)
|
13
12
|
).start
|
14
13
|
end
|
15
14
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module RackInterface
|
4
|
+
# Interface to Rack applications.
|
5
|
+
# Here we build the env, and invoke the Rack app's call method.
|
6
|
+
# We then turn the Rack response into something Itsi server understands.
|
7
|
+
def call(app, request)
|
8
|
+
respond request, app.call(request.to_rack_env)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Itsi responses are asynchronous and can be streamed.
|
12
|
+
# Response chunks are sent using response.send_frame
|
13
|
+
# and the response is finished using response.close_write.
|
14
|
+
# If only a single chunk is written, you can use the #send_and_close method.
|
15
|
+
def respond(request, (status, headers, body))
|
16
|
+
response = request.response
|
17
|
+
|
18
|
+
# Don't try and respond if we've been hijacked.
|
19
|
+
# The hijacker is now responsible for this.
|
20
|
+
return if request.hijacked
|
21
|
+
|
22
|
+
# 1. Set Status
|
23
|
+
response.status = status
|
24
|
+
|
25
|
+
# 2. Set Headers
|
26
|
+
body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
|
27
|
+
headers.each do |key, value|
|
28
|
+
next response.add_header(key, value) unless value.is_a?(Array)
|
29
|
+
|
30
|
+
value.each do |v|
|
31
|
+
response.add_header(key, v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# 3. Set Body
|
36
|
+
# As soon as we start setting the response
|
37
|
+
# the server will begin to stream it to the client.
|
38
|
+
|
39
|
+
# If we're partially hijacked or returned a streaming body,
|
40
|
+
# stream this response.
|
41
|
+
|
42
|
+
if body_streamer
|
43
|
+
body_streamer.call(StreamIO.new(response))
|
44
|
+
|
45
|
+
# If we're enumerable with more than one chunk
|
46
|
+
# also stream, otherwise write in a single chunk
|
47
|
+
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
48
|
+
unless body.respond_to?(:each)
|
49
|
+
body = body.to_ary
|
50
|
+
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
51
|
+
end
|
52
|
+
# We offset this iteration intentionally,
|
53
|
+
# to optimize for the case where there's only one chunk.
|
54
|
+
buffer = nil
|
55
|
+
body.each do |part|
|
56
|
+
response.send_frame(buffer.to_s) if buffer
|
57
|
+
buffer = part
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
response.send_and_close(buffer.to_s)
|
62
|
+
rescue StandardError
|
63
|
+
binding.b
|
64
|
+
end
|
65
|
+
else
|
66
|
+
response.send_and_close(body.to_s)
|
67
|
+
end
|
68
|
+
ensure
|
69
|
+
response.close_write
|
70
|
+
body.close if body.respond_to?(:close)
|
71
|
+
end
|
72
|
+
|
73
|
+
# A streaming body is one that responds to #call and not #each.
|
74
|
+
def streaming_body?(body)
|
75
|
+
body.respond_to?(:call) && !body.respond_to?(:each)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module SchedulerInterface
|
4
|
+
# Simple wrapper to instantiate a scheduler, start it,
|
5
|
+
# and immediate have it invoke a scheduler proc
|
6
|
+
def start_scheduler_loop(scheduler_class, scheduler_task)
|
7
|
+
scheduler = scheduler_class.new
|
8
|
+
Fiber.set_scheduler(scheduler)
|
9
|
+
[scheduler, Fiber.schedule(&scheduler_task)]
|
10
|
+
end
|
11
|
+
|
12
|
+
# When running in scheduler mode,
|
13
|
+
# each request is wrapped in a Fiber.
|
14
|
+
def schedule(app, request)
|
15
|
+
Fiber.schedule do
|
16
|
+
call(app, request)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Itsi
|
2
|
+
module SignalTrap
|
3
|
+
|
4
|
+
DEFAULT_SIGNALS = ["DEFAULT", "", nil].freeze
|
5
|
+
INTERCEPTED_SIGNALS = ["INT"].freeze
|
6
|
+
|
7
|
+
def trap(signal, *args, &block)
|
8
|
+
unless INTERCEPTED_SIGNALS.include?(signal.to_s) && block.nil? && Itsi::Server.running?
|
9
|
+
return super(signal, *args, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
Itsi::Server.reset_signal_handlers
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
[Kernel, Signal].each do |receiver|
|
19
|
+
receiver.singleton_class.prepend(Itsi::SignalTrap)
|
20
|
+
end
|
21
|
+
|
22
|
+
[Object].each do |receiver|
|
23
|
+
receiver.include(Itsi::SignalTrap)
|
24
|
+
end
|
data/lib/itsi/server/version.rb
CHANGED
data/lib/itsi/server.rb
CHANGED
@@ -2,119 +2,85 @@
|
|
2
2
|
|
3
3
|
require_relative "server/version"
|
4
4
|
require_relative "server/itsi_server"
|
5
|
-
require_relative "
|
5
|
+
require_relative "server/rack_interface"
|
6
|
+
require_relative "server/signal_trap"
|
7
|
+
require_relative "server/scheduler_interface"
|
8
|
+
require_relative "server/rack/handler/itsi"
|
6
9
|
require_relative "request"
|
7
10
|
require_relative "stream_io"
|
8
|
-
require_relative "server/rack/handler/itsi"
|
9
|
-
require 'erb'
|
10
11
|
|
11
|
-
|
12
|
+
# When you Run Itsi without a Rack app,
|
13
|
+
# we start a tiny
|
14
|
+
DEFAULT_INDEX = IO.read("#{__dir__}/index.html").freeze
|
15
|
+
DEFAULT_BINDS = ["http://0.0.0.0:3000"].freeze
|
16
|
+
DEFAULT_APP = lambda {
|
17
|
+
require "json"
|
18
|
+
lambda do |env|
|
19
|
+
headers, body = \
|
20
|
+
if env["itsi.response"].json?
|
21
|
+
[
|
22
|
+
{ "Content-Type" => "application/json" },
|
23
|
+
[{ "message" => "You're running on Itsi!", "rack_env" => env,
|
24
|
+
"version" => Itsi::Server::VERSION }.to_json]
|
25
|
+
]
|
26
|
+
else
|
27
|
+
[
|
28
|
+
{ "Content-Type" => "text/html" },
|
29
|
+
[
|
30
|
+
format(
|
31
|
+
DEFAULT_INDEX,
|
32
|
+
REQUEST_METHOD: env["REQUEST_METHOD"],
|
33
|
+
PATH_INFO: env["PATH_INFO"],
|
34
|
+
SERVER_NAME: env["SERVER_NAME"],
|
35
|
+
SERVER_PORT: env["SERVER_PORT"],
|
36
|
+
REMOTE_ADDR: env["REMOTE_ADDR"],
|
37
|
+
HTTP_USER_AGENT: env["HTTP_USER_AGENT"]
|
38
|
+
)
|
39
|
+
]
|
40
|
+
]
|
41
|
+
end
|
42
|
+
[200, headers, body]
|
43
|
+
end
|
44
|
+
}
|
12
45
|
|
13
46
|
module Itsi
|
14
47
|
class Server
|
48
|
+
extend RackInterface
|
49
|
+
extend SchedulerInterface
|
15
50
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
def self.start(
|
21
|
-
app: ->(env){
|
22
|
-
[env['CONTENT_TYPE'], env['HTTP_ACCEPT']].include?('application/json') ?
|
23
|
-
[200, {"Content-Type" => "application/json"}, ["{\"message\": \"You're running on Itsi!\"}"]] :
|
24
|
-
[200, {"Content-Type" => "text/html"}, [
|
25
|
-
DEFAULT_INDEX % {
|
26
|
-
REQUEST_METHOD: env['REQUEST_METHOD'],
|
27
|
-
PATH_INFO: env['PATH_INFO'],
|
28
|
-
SERVER_NAME: env['SERVER_NAME'],
|
29
|
-
SERVER_PORT: env['SERVER_PORT'],
|
30
|
-
REMOTE_ADDR: env['REMOTE_ADDR'],
|
31
|
-
HTTP_USER_AGENT: env['HTTP_USER_AGENT']
|
32
|
-
}
|
33
|
-
]]
|
34
|
-
},
|
35
|
-
binds: ['http://0.0.0.0:3000'],
|
36
|
-
**opts
|
37
|
-
)
|
38
|
-
server = new(app: ->{app}, binds: binds, **opts)
|
39
|
-
@running = true
|
40
|
-
Signal.trap('INT', 'DEFAULT')
|
41
|
-
server.start
|
42
|
-
ensure
|
43
|
-
@running = false
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.call(app, request)
|
47
|
-
respond request, app.call(request.to_env)
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.streaming_body?(body)
|
51
|
-
body.respond_to?(:call) && !body.respond_to?(:each)
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.respond(request, (status, headers, body))
|
55
|
-
response = request.response
|
56
|
-
|
57
|
-
# Don't try and respond if we've been hijacked.
|
58
|
-
# The hijacker is now responsible for this.
|
59
|
-
return if request.hijacked
|
60
|
-
|
61
|
-
# 1. Set Status
|
62
|
-
response.status = status
|
63
|
-
|
64
|
-
# 2. Set Headers
|
65
|
-
headers.each do |key, value|
|
66
|
-
next response.add_header(key, value) unless value.is_a?(Array)
|
67
|
-
|
68
|
-
value.each do |v|
|
69
|
-
response.add_header(key, v)
|
70
|
-
end
|
51
|
+
class << self
|
52
|
+
def running?
|
53
|
+
!!@running
|
71
54
|
end
|
72
55
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if (body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack"))
|
81
|
-
body_streamer.call(StreamIO.new(response))
|
82
|
-
|
83
|
-
# If we're enumerable with more than one chunk
|
84
|
-
# also stream, otherwise write in a single chunk
|
85
|
-
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
86
|
-
unless body.respond_to?(:each)
|
87
|
-
body = body.to_ary
|
88
|
-
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
89
|
-
end
|
90
|
-
# We offset this iteration intentionally,
|
91
|
-
# to optimize for the case where there's only one chunk.
|
92
|
-
buffer = nil
|
93
|
-
body.each do |part|
|
94
|
-
response.send_frame(buffer.to_s) if buffer
|
95
|
-
buffer = part
|
96
|
-
end
|
97
|
-
|
98
|
-
response.send_and_close(buffer.to_s)
|
99
|
-
else
|
100
|
-
response.send_and_close(body.to_s)
|
56
|
+
def build(
|
57
|
+
app: DEFAULT_APP[],
|
58
|
+
binds: DEFAULT_BINDS,
|
59
|
+
**opts
|
60
|
+
)
|
61
|
+
new(app: -> { app }, binds: binds, **opts)
|
101
62
|
end
|
102
|
-
ensure
|
103
|
-
response.close_write
|
104
|
-
body.close if body.respond_to?(:close)
|
105
|
-
end
|
106
63
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
[scheduler, Fiber.schedule(&scheduler_task)]
|
111
|
-
end
|
64
|
+
def start_in_background_thread(silence: true, **opts)
|
65
|
+
start(background: true, silence: silence, **opts)
|
66
|
+
end
|
112
67
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
68
|
+
def start(background: false, **opts)
|
69
|
+
build(**opts).tap do |server|
|
70
|
+
previous_handler = Signal.trap("INT", "DEFAULT")
|
71
|
+
@running = true
|
72
|
+
if background
|
73
|
+
Thread.new do
|
74
|
+
server.start
|
75
|
+
@running = false
|
76
|
+
Signal.trap("INT", previous_handler)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
server.start
|
80
|
+
@running = false
|
81
|
+
Signal.trap("INT", previous_handler)
|
82
|
+
end
|
83
|
+
end
|
118
84
|
end
|
119
85
|
end
|
120
86
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: itsi-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wouter Coppieters
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-16 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rack
|
@@ -102,13 +102,15 @@ files:
|
|
102
102
|
- ext/itsi_tracing/Cargo.lock
|
103
103
|
- ext/itsi_tracing/Cargo.toml
|
104
104
|
- ext/itsi_tracing/src/lib.rs
|
105
|
-
- lib/itsi/index.html
|
105
|
+
- lib/itsi/index.html
|
106
106
|
- lib/itsi/request.rb
|
107
107
|
- lib/itsi/server.rb
|
108
108
|
- lib/itsi/server/rack/handler/itsi.rb
|
109
|
+
- lib/itsi/server/rack_interface.rb
|
110
|
+
- lib/itsi/server/scheduler_interface.rb
|
109
111
|
- lib/itsi/server/scheduler_mode.rb
|
112
|
+
- lib/itsi/server/signal_trap.rb
|
110
113
|
- lib/itsi/server/version.rb
|
111
|
-
- lib/itsi/signals.rb
|
112
114
|
- lib/itsi/stream_io.rb
|
113
115
|
- sig/itsi_server.rbs
|
114
116
|
homepage: https://itsi.fyi
|
data/lib/itsi/signals.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
module Itsi
|
2
|
-
module Signals
|
3
|
-
DEFAULT_SIGNALS = ["DEFAULT", ""].freeze
|
4
|
-
module SignalTrap
|
5
|
-
def self.trap(signal, *args, &block)
|
6
|
-
if DEFAULT_SIGNALS.include?(command.to_s) && block.nil?
|
7
|
-
Itsi::Server.reset_signal_handlers
|
8
|
-
nil
|
9
|
-
else
|
10
|
-
super(signal, *args, &block)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
[Kernel, Signal].each do |receiver|
|
18
|
-
receiver.singleton_class.prepend(Itsi::Signals::SignalTrap)
|
19
|
-
end
|
20
|
-
|
21
|
-
[Object].each do |receiver|
|
22
|
-
receiver.include(Itsi::Signals::SignalTrap)
|
23
|
-
end
|
File without changes
|