itsi-server 0.1.1 → 0.1.2

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/exe/itsi +88 -28
  3. data/ext/itsi_error/Cargo.toml +2 -0
  4. data/ext/itsi_error/src/from.rs +70 -0
  5. data/ext/itsi_error/src/lib.rs +10 -37
  6. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  7. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  8. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  9. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  10. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  11. data/ext/itsi_scheduler/Cargo.toml +24 -0
  12. data/ext/itsi_scheduler/extconf.rb +6 -0
  13. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  17. data/ext/itsi_scheduler/src/lib.rs +38 -0
  18. data/ext/itsi_server/Cargo.toml +14 -2
  19. data/ext/itsi_server/extconf.rb +1 -1
  20. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  21. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  22. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  23. data/ext/itsi_server/src/lib.rs +58 -7
  24. data/ext/itsi_server/src/request/itsi_request.rs +238 -104
  25. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  26. data/ext/itsi_server/src/response/mod.rs +1 -0
  27. data/ext/itsi_server/src/server/bind.rs +50 -20
  28. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  29. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  30. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  31. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  32. data/ext/itsi_server/src/server/itsi_server.rs +181 -133
  33. data/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
  34. data/ext/itsi_server/src/server/listener.rs +169 -128
  35. data/ext/itsi_server/src/server/mod.rs +7 -1
  36. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  37. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  38. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  39. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  40. data/ext/itsi_server/src/server/signal.rs +57 -0
  41. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  42. data/ext/itsi_server/src/server/tls.rs +42 -28
  43. data/ext/itsi_tracing/Cargo.toml +4 -0
  44. data/ext/itsi_tracing/src/lib.rs +36 -6
  45. data/lib/itsi/request.rb +30 -14
  46. data/lib/itsi/server/rack/handler/itsi.rb +25 -0
  47. data/lib/itsi/server/scheduler_mode.rb +6 -0
  48. data/lib/itsi/server/version.rb +1 -1
  49. data/lib/itsi/server.rb +68 -2
  50. data/lib/itsi/signals.rb +18 -0
  51. data/lib/itsi/stream_io.rb +38 -0
  52. metadata +41 -14
  53. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  54. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -0,0 +1,368 @@
1
+ use super::itsi_server::RequestJob;
2
+ use crate::{request::itsi_request::ItsiRequest, ITSI_SERVER};
3
+ use itsi_rb_helpers::{
4
+ call_with_gvl, call_without_gvl, create_ruby_thread, kill_threads, HeapValue,
5
+ };
6
+ use itsi_tracing::{debug, error, info, warn};
7
+ use magnus::{
8
+ error::Result,
9
+ value::{InnerValue, Lazy, LazyId, Opaque, ReprValue},
10
+ Module, RClass, Ruby, Thread, Value,
11
+ };
12
+ use nix::unistd::Pid;
13
+ use parking_lot::{Mutex, RwLock};
14
+ use std::{
15
+ num::NonZeroU8,
16
+ ops::Deref,
17
+ sync::{
18
+ atomic::{AtomicBool, Ordering},
19
+ Arc,
20
+ },
21
+ thread,
22
+ time::{Duration, Instant},
23
+ };
24
+ use tokio::{runtime::Builder as RuntimeBuilder, sync::watch};
25
+ use tracing::instrument;
26
+ pub struct ThreadWorker {
27
+ pub id: String,
28
+ pub app: Opaque<Value>,
29
+ pub receiver: Arc<async_channel::Receiver<RequestJob>>,
30
+ pub sender: async_channel::Sender<RequestJob>,
31
+ pub thread: RwLock<Option<HeapValue<Thread>>>,
32
+ pub terminated: Arc<AtomicBool>,
33
+ pub scheduler_class: Option<Opaque<Value>>,
34
+ }
35
+
36
+ static ID_CALL: LazyId = LazyId::new("call");
37
+ static ID_ALIVE: LazyId = LazyId::new("alive?");
38
+ static ID_SCHEDULER: LazyId = LazyId::new("scheduler");
39
+ static ID_SCHEDULE: LazyId = LazyId::new("schedule");
40
+ static ID_BLOCK: LazyId = LazyId::new("block");
41
+ static ID_YIELD: LazyId = LazyId::new("yield");
42
+ static ID_CONST_GET: LazyId = LazyId::new("const_get");
43
+ static CLASS_FIBER: Lazy<RClass> = Lazy::new(|ruby| {
44
+ ruby.module_kernel()
45
+ .const_get::<_, RClass>("Fiber")
46
+ .unwrap()
47
+ });
48
+
49
+ pub struct TerminateWakerSignal(bool);
50
+
51
+ #[instrument(name = "Boot", parent=None, skip(threads, app, pid, scheduler_class))]
52
+ pub fn build_thread_workers(
53
+ pid: Pid,
54
+ threads: NonZeroU8,
55
+ app: Opaque<Value>,
56
+ scheduler_class: Option<String>,
57
+ ) -> Result<(Arc<Vec<ThreadWorker>>, async_channel::Sender<RequestJob>)> {
58
+ let (sender, receiver) = async_channel::bounded(20);
59
+ let receiver_ref = Arc::new(receiver);
60
+ let sender_ref = sender;
61
+ let (app, scheduler_class) = load_app(app, scheduler_class)?;
62
+ Ok((
63
+ Arc::new(
64
+ (1..=u8::from(threads))
65
+ .map(|id| {
66
+ info!(pid = pid.as_raw(), id, "Thread");
67
+ ThreadWorker::new(
68
+ format!("{:?}#{:?}", pid, id),
69
+ app,
70
+ receiver_ref.clone(),
71
+ sender_ref.clone(),
72
+ scheduler_class,
73
+ )
74
+ })
75
+ .collect::<Result<Vec<_>>>()?,
76
+ ),
77
+ sender_ref,
78
+ ))
79
+ }
80
+
81
+ pub fn load_app(
82
+ app: Opaque<Value>,
83
+ scheduler_class: Option<String>,
84
+ ) -> Result<(Opaque<Value>, Option<Opaque<Value>>)> {
85
+ call_with_gvl(|ruby| {
86
+ let app = app.get_inner_with(&ruby);
87
+ let app = Opaque::from(
88
+ app.funcall::<_, _, Value>(*ID_CALL, ())
89
+ .expect("Couldn't load app"),
90
+ );
91
+ let scheduler_class = if let Some(scheduler_class) = scheduler_class {
92
+ Some(Opaque::from(
93
+ ruby.module_kernel()
94
+ .funcall::<_, _, Value>(*ID_CONST_GET, (scheduler_class,))?,
95
+ ))
96
+ } else {
97
+ None
98
+ };
99
+ Ok((app, scheduler_class))
100
+ })
101
+ }
102
+ impl ThreadWorker {
103
+ pub fn new(
104
+ id: String,
105
+ app: Opaque<Value>,
106
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
107
+ sender: async_channel::Sender<RequestJob>,
108
+ scheduler_class: Option<Opaque<Value>>,
109
+ ) -> Result<Self> {
110
+ let mut worker = Self {
111
+ id,
112
+ app,
113
+ receiver,
114
+ sender,
115
+ thread: RwLock::new(None),
116
+ terminated: Arc::new(AtomicBool::new(false)),
117
+ scheduler_class,
118
+ };
119
+ worker.run()?;
120
+ Ok(worker)
121
+ }
122
+
123
+ #[instrument(skip(self), fields(id = self.id))]
124
+ pub async fn request_shutdown(&self) {
125
+ match self.sender.send(RequestJob::Shutdown).await {
126
+ Ok(_) => {}
127
+ Err(err) => error!("Failed to send shutdown request: {}", err),
128
+ };
129
+ info!("Requesting shutdown");
130
+ }
131
+
132
+ #[instrument(skip(self, deadline), fields(id = self.id))]
133
+ pub fn poll_shutdown(&self, deadline: Instant) -> bool {
134
+ call_with_gvl(|_ruby| {
135
+ if let Some(thread) = self.thread.read().deref() {
136
+ if Instant::now() > deadline {
137
+ warn!("Worker shutdown timed out. Killing thread");
138
+ self.terminated.store(true, Ordering::SeqCst);
139
+ kill_threads(vec![thread.as_value()]);
140
+ }
141
+ if thread.funcall::<_, _, bool>(*ID_ALIVE, ()).unwrap_or(false) {
142
+ return true;
143
+ }
144
+ info!("Thread has shut down");
145
+ }
146
+ self.thread.write().take();
147
+
148
+ false
149
+ })
150
+ }
151
+
152
+ pub fn run(&mut self) -> Result<()> {
153
+ let id = self.id.clone();
154
+ let app = self.app;
155
+ let receiver = self.receiver.clone();
156
+ let terminated = self.terminated.clone();
157
+ let scheduler_class = self.scheduler_class;
158
+ call_with_gvl(|_| {
159
+ *self.thread.write() = Some(
160
+ create_ruby_thread(move || {
161
+ if let Some(scheduler_class) = scheduler_class {
162
+ if let Err(err) =
163
+ Self::fiber_accept_loop(id, app, receiver, scheduler_class, terminated)
164
+ {
165
+ error!("Error in fiber_accept_loop: {:?}", err);
166
+ }
167
+ } else {
168
+ Self::accept_loop(id, app, receiver, terminated);
169
+ }
170
+ })
171
+ .into(),
172
+ );
173
+ Ok::<(), magnus::Error>(())
174
+ })?;
175
+ Ok(())
176
+ }
177
+
178
+ pub fn build_scheduler_proc(
179
+ app: Opaque<Value>,
180
+ leader: &Arc<Mutex<Option<RequestJob>>>,
181
+ receiver: &Arc<async_channel::Receiver<RequestJob>>,
182
+ terminated: &Arc<AtomicBool>,
183
+ waker_sender: &watch::Sender<TerminateWakerSignal>,
184
+ ) -> magnus::block::Proc {
185
+ let leader = leader.clone();
186
+ let receiver = receiver.clone();
187
+ let terminated = terminated.clone();
188
+ let waker_sender = waker_sender.clone();
189
+ Ruby::get().unwrap().proc_from_fn(move |ruby, _args, _blk| {
190
+ let scheduler = ruby
191
+ .get_inner(&CLASS_FIBER)
192
+ .funcall::<_, _, Value>(*ID_SCHEDULER, ())
193
+ .unwrap();
194
+ let server = ruby.get_inner(&ITSI_SERVER);
195
+ let thread_current = ruby.thread_current();
196
+ let leader_clone = leader.clone();
197
+ let receiver = receiver.clone();
198
+ let terminated = terminated.clone();
199
+ let waker_sender = waker_sender.clone();
200
+ let mut batch = Vec::with_capacity(MAX_BATCH_SIZE as usize);
201
+
202
+ static MAX_BATCH_SIZE: i32 = 25;
203
+ call_without_gvl(move || loop {
204
+ let mut idle_counter = 0;
205
+ if let Some(v) = leader_clone.lock().take() {
206
+ match v {
207
+ RequestJob::ProcessRequest(itsi_request) => {
208
+ batch.push(RequestJob::ProcessRequest(itsi_request))
209
+ }
210
+ RequestJob::Shutdown => {
211
+ waker_sender.send(TerminateWakerSignal(true)).unwrap();
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ for _ in 0..MAX_BATCH_SIZE {
217
+ if let Ok(req) = receiver.try_recv() {
218
+ batch.push(req);
219
+ } else {
220
+ break;
221
+ }
222
+ }
223
+
224
+ let shutdown_requested = call_with_gvl(|_| {
225
+ for req in batch.drain(..) {
226
+ match req {
227
+ RequestJob::ProcessRequest(request) => {
228
+ let response = request.response.clone();
229
+ if let Err(err) =
230
+ server.funcall::<_, _, Value>(*ID_SCHEDULE, (app, request))
231
+ {
232
+ ItsiRequest::internal_error(ruby, response, err)
233
+ }
234
+ }
235
+ RequestJob::Shutdown => return true,
236
+ }
237
+ }
238
+ false
239
+ });
240
+
241
+ if shutdown_requested || terminated.load(Ordering::Relaxed) {
242
+ waker_sender.send(TerminateWakerSignal(true)).unwrap();
243
+ break;
244
+ }
245
+
246
+ let yield_result = if receiver.is_empty() {
247
+ waker_sender.send(TerminateWakerSignal(false)).unwrap();
248
+ idle_counter = (idle_counter + 1) % 100;
249
+ call_with_gvl(|ruby| {
250
+ if idle_counter == 0 {
251
+ ruby.gc_start();
252
+ }
253
+ scheduler.funcall::<_, _, Value>(*ID_BLOCK, (thread_current, None::<u8>))
254
+ })
255
+ } else {
256
+ call_with_gvl(|_| scheduler.funcall::<_, _, Value>(*ID_YIELD, ()))
257
+ };
258
+
259
+ if yield_result.is_err() {
260
+ break;
261
+ }
262
+ })
263
+ })
264
+ }
265
+
266
+ #[instrument(skip_all, fields(thread_worker=id))]
267
+ pub fn fiber_accept_loop(
268
+ id: String,
269
+ app: Opaque<Value>,
270
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
271
+ scheduler_class: Opaque<Value>,
272
+ terminated: Arc<AtomicBool>,
273
+ ) -> Result<()> {
274
+ let ruby = Ruby::get().unwrap();
275
+ let (waker_sender, waker_receiver) = watch::channel(TerminateWakerSignal(false));
276
+ let leader: Arc<Mutex<Option<RequestJob>>> = Arc::new(Mutex::new(None));
277
+ let server = ruby.get_inner(&ITSI_SERVER);
278
+ let scheduler_proc =
279
+ Self::build_scheduler_proc(app, &leader, &receiver, &terminated, &waker_sender);
280
+ let (scheduler, scheduler_fiber) = server.funcall::<_, _, (Value, Value)>(
281
+ "start_scheduler_loop",
282
+ (scheduler_class, scheduler_proc),
283
+ )?;
284
+ Self::start_waker_thread(
285
+ scheduler.into(),
286
+ scheduler_fiber.into(),
287
+ leader,
288
+ receiver,
289
+ waker_receiver,
290
+ );
291
+ Ok(())
292
+ }
293
+
294
+ #[allow(clippy::await_holding_lock)]
295
+ pub fn start_waker_thread(
296
+ scheduler: Opaque<Value>,
297
+ scheduler_fiber: Opaque<Value>,
298
+ leader: Arc<Mutex<Option<RequestJob>>>,
299
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
300
+ mut waker_receiver: watch::Receiver<TerminateWakerSignal>,
301
+ ) {
302
+ create_ruby_thread(move || {
303
+ let scheduler = scheduler.get_inner_with(&Ruby::get().unwrap());
304
+ let leader = leader.clone();
305
+ call_without_gvl(|| {
306
+ RuntimeBuilder::new_current_thread()
307
+ .build()
308
+ .expect("Failed to build Tokio runtime")
309
+ .block_on(async {
310
+ loop {
311
+ waker_receiver.changed().await.ok();
312
+ if waker_receiver.borrow().0 {
313
+ break;
314
+ }
315
+ tokio::select! {
316
+ _ = waker_receiver.changed() => {
317
+ if waker_receiver.borrow().0 {
318
+ break;
319
+ }
320
+ },
321
+ next_msg = receiver.recv() => {
322
+ *leader.lock() = next_msg.ok();
323
+ call_with_gvl(|_| {
324
+ scheduler
325
+ .funcall::<_, _, Value>(
326
+ "unblock",
327
+ (None::<u8>, scheduler_fiber),
328
+ )
329
+ .ok();
330
+ });
331
+ }
332
+ }
333
+ }
334
+ })
335
+ });
336
+ });
337
+ }
338
+
339
+ #[instrument(skip_all, fields(thread_worker=id))]
340
+ pub fn accept_loop(
341
+ id: String,
342
+ app: Opaque<Value>,
343
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
344
+ terminated: Arc<AtomicBool>,
345
+ ) {
346
+ let ruby = Ruby::get().unwrap();
347
+ let server = ruby.get_inner(&ITSI_SERVER);
348
+ call_without_gvl(|| loop {
349
+ match receiver.recv_blocking() {
350
+ Ok(RequestJob::ProcessRequest(request)) => {
351
+ if terminated.load(Ordering::Relaxed) {
352
+ break;
353
+ }
354
+ call_with_gvl(|_ruby| {
355
+ request.process(&ruby, server, app).ok();
356
+ })
357
+ }
358
+ Ok(RequestJob::Shutdown) => {
359
+ debug!("Shutting down thread worker");
360
+ break;
361
+ }
362
+ Err(_) => {
363
+ thread::sleep(Duration::from_micros(1));
364
+ }
365
+ }
366
+ });
367
+ }
368
+ }
@@ -15,6 +15,7 @@ const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
15
15
  // If a non-local host or optional domain parameter is provided,
16
16
  // an automated certificate will attempt to be fetched using let's encrypt.
17
17
  pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Result<ServerConfig> {
18
+ info!("TLS Options {:?}", query_params);
18
19
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
19
20
  (query_params.get("cert"), query_params.get("key"))
20
21
  {
@@ -22,28 +23,24 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
22
23
  let certs = load_certs(cert_path);
23
24
  let key = load_private_key(key_path);
24
25
  (certs, key)
25
- } else if query_params
26
- .get("cert")
27
- .map(|v| v == "auto")
28
- .unwrap_or(false)
29
- {
30
- let domain_param = query_params.get("domain");
26
+ } else {
27
+ let domains_param = query_params
28
+ .get("domains")
29
+ .map(|v| v.split(',').map(String::from).collect());
31
30
  let host_string = host.to_string();
32
- let domain = domain_param.or_else(|| {
31
+ let domains = domains_param.or_else(|| {
33
32
  if host_string != "localhost" {
34
- Some(&host_string)
33
+ Some(vec![host_string])
35
34
  } else {
36
35
  None
37
36
  }
38
37
  });
39
38
 
40
- if let Some(domain) = domain {
41
- retrieve_acme_cert(domain)?
39
+ if let Some(domains) = domains {
40
+ retrieve_acme_cert(domains)?
42
41
  } else {
43
- generate_ca_signed_cert(host)?
42
+ generate_ca_signed_cert(vec![host.to_owned()])?
44
43
  }
45
- } else {
46
- generate_ca_signed_cert(host)?
47
44
  };
48
45
 
49
46
  let mut config = ServerConfig::builder()
@@ -106,33 +103,50 @@ pub fn load_private_key(path: &str) -> PrivateKey {
106
103
  PrivateKey(key_data)
107
104
  }
108
105
 
109
- pub fn generate_ca_signed_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
106
+ pub fn generate_ca_signed_cert(domains: Vec<String>) -> Result<(Vec<Certificate>, PrivateKey)> {
110
107
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
111
108
 
112
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
109
+ let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
110
+ let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
111
+ .expect("Failed to parse embedded CA certificate")
112
+ .self_signed(&ca_kp)
113
+ .expect("Failed to self-sign embedded CA cert");
114
114
 
115
- let ca_cert = params.self_signed(&ca_kp).unwrap();
116
- let ee_key = KeyPair::generate().unwrap();
115
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
117
116
  let mut ee_params = CertificateParams::default();
118
117
 
119
- // Set the domain in the subject alternative names (SAN)
120
- ee_params.subject_alt_names = vec![SanType::DnsName(domain.try_into()?)];
121
- // Optionally, set the Common Name (CN) in the distinguished name:
118
+ info!(
119
+ "Generated certificate will be valid for domains {:?}",
120
+ domains
121
+ );
122
+ use std::net::IpAddr;
123
+
124
+ ee_params.subject_alt_names = domains
125
+ .iter()
126
+ .map(|domain| {
127
+ if let Ok(ip) = domain.parse::<IpAddr>() {
128
+ SanType::IpAddress(ip)
129
+ } else {
130
+ SanType::DnsName(domain.clone().try_into().unwrap())
131
+ }
132
+ })
133
+ .collect();
134
+
122
135
  ee_params
123
136
  .distinguished_name
124
- .push(DnType::CommonName, domain);
137
+ .push(DnType::CommonName, domains[0].clone());
125
138
 
126
139
  ee_params.use_authority_key_identifier_extension = true;
127
140
 
128
- let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ee_key).unwrap();
141
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
129
142
  let ee_cert_der = ee_cert.der().to_vec();
130
143
  let ee_cert = Certificate(ee_cert_der);
131
- Ok((vec![ee_cert], PrivateKey(ee_key.serialize_der())))
144
+ let ca_cert = Certificate(ca_cert.der().to_vec());
145
+ Ok((vec![ee_cert, ca_cert], PrivateKey(ee_key.serialize_der())))
132
146
  }
133
147
 
134
- /// Retrieves an ACME certificate for a given domain.
135
- pub fn retrieve_acme_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
136
- warn!("Retrieving ACME cert for {}", domain);
137
- generate_ca_signed_cert(domain)
148
+ /// TODO: Retrieves an ACME certificate for a given domain.
149
+ pub fn retrieve_acme_cert(domains: Vec<String>) -> Result<(Vec<Certificate>, PrivateKey)> {
150
+ warn!("Retrieving ACME cert for {}", domains.join(", "));
151
+ generate_ca_signed_cert(domains)
138
152
  }
@@ -9,4 +9,8 @@ tracing-subscriber = { version = "0.3.19", features = [
9
9
  "env-filter",
10
10
  "std",
11
11
  "fmt",
12
+ "json",
13
+ "ansi",
12
14
  ] }
15
+ tracing-attributes = "0.1"
16
+ atty = "0.2.14"
@@ -1,11 +1,41 @@
1
+ use std::env;
2
+
3
+ use atty::{Stream, is};
1
4
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
5
+ pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
6
+ use tracing_subscriber::{
7
+ EnvFilter,
8
+ fmt::{self, format},
9
+ };
3
10
 
11
+ #[instrument]
4
12
  pub fn init() {
5
- let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
6
- let format = fmt::format().with_level(true).with_target(false).compact();
7
- tracing_subscriber::fmt()
8
- .with_env_filter(env_filter)
13
+ let env_filter = EnvFilter::builder()
14
+ .with_env_var("ITSI_LOG")
15
+ .try_from_env()
16
+ .unwrap_or_else(|_| EnvFilter::new("info"));
17
+
18
+ let format = fmt::format()
19
+ .compact()
20
+ .with_file(false)
21
+ .with_level(true)
22
+ .with_line_number(false)
23
+ .with_source_location(false)
24
+ .with_target(false)
25
+ .with_thread_ids(false);
26
+
27
+ let is_tty = is(Stream::Stdout);
28
+
29
+ let subscriber = tracing_subscriber::fmt()
9
30
  .event_format(format)
10
- .init();
31
+ .with_env_filter(env_filter);
32
+
33
+ if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
34
+ subscriber.with_ansi(true).init();
35
+ } else {
36
+ subscriber
37
+ .fmt_fields(format::JsonFields::default())
38
+ .event_format(fmt::format().json())
39
+ .init();
40
+ }
11
41
  }
data/lib/itsi/request.rb CHANGED
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  module Itsi
6
4
  class Request
5
+ require "stringio"
6
+ require "socket"
7
+
8
+ attr_accessor :hijacked
9
+
7
10
  def to_env
11
+ path = self.path
12
+ host = self.host
13
+ version = self.version
14
+ body = self.body
8
15
  {
9
16
  "SERVER_SOFTWARE" => "Itsi",
10
17
  "SCRIPT_NAME" => script_name,
@@ -13,27 +20,36 @@ module Itsi
13
20
  "REQUEST_PATH" => path,
14
21
  "QUERY_STRING" => query_string,
15
22
  "REMOTE_ADDR" => remote_addr,
16
- "SERVER_NAME" => host,
17
23
  "SERVER_PORT" => port.to_s,
24
+ "SERVER_NAME" => host,
25
+ "HTTP_HOST" => host,
18
26
  "SERVER_PROTOCOL" => version,
19
27
  "HTTP_VERSION" => version,
20
- "HTTP_HOST" => host,
21
- "rack.input" => StringIO.new(body),
28
+ "rack.version" => [version],
29
+ "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,
22
36
  "rack.errors" => $stderr,
23
- "rack.version" => version,
24
37
  "rack.multithread" => true,
25
38
  "rack.multiprocess" => true,
26
39
  "rack.run_once" => false,
27
- "rack.multipart.buffer_size" => 16_384
28
- }.merge(
29
- headers.transform_keys do |k|
30
- case k
31
- when "content-length" then "CONTENT_LENGTH"
32
- when "content-type" then "CONTENT_TYPE"
33
- else "HTTP_#{k.upcase.tr("-", "_")}"
40
+ "rack.hijack?" => true,
41
+ "rack.multipart.buffer_size" => 16_384,
42
+ "rack.hijack" => lambda do
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
34
50
  end
35
51
  end
36
- )
52
+ }.tap { |r| headers.each { |(k, v)| r[k] = v } }
37
53
  end
38
54
  end
39
55
  end
@@ -0,0 +1,25 @@
1
+ return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
2
+
3
+ module Rack
4
+ module Handler
5
+ module Itsi
6
+
7
+ def self.run(app, options = {})
8
+ ::Itsi::Server.new(
9
+ app: ->{ app },
10
+ binds: ["#{options.fetch(:host, "127.0.0.1")}:#{options.fetch(:Port, 3001)}"],
11
+ workers: options.fetch(:workers, 1),
12
+ threads: options.fetch(:threads, 1),
13
+ ).start
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ if defined?(Rackup)
20
+ ::Rackup::Handler.register("itsi", Rack::Handler::Itsi)
21
+ ::Rackup::Handler.register("Itsi", Rack::Handler::Itsi)
22
+ elsif defined?(Rack)
23
+ ::Rack::Handler.register("itsi", Rack::Handler::Itsi)
24
+ ::Rack::Handler.register("Itsi", Rack::Handler::Itsi)
25
+ end
@@ -0,0 +1,6 @@
1
+ if defined?(ActiveSupport::IsolatedExecutionState) && !ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
2
+ Itsi.log_info \
3
+ "ActiveSupport Isolated Execution state detected. Automatically switching to :fiber mode. "\
4
+ "Use ENV['ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL'] to disable this behavior"
5
+ ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
6
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end