itsi 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 +1 -0
- data/crates/itsi_server/src/lib.rs +5 -0
- data/crates/itsi_server/src/request/itsi_request.rs +30 -2
- data/crates/itsi_server/src/response/itsi_response.rs +12 -2
- data/crates/itsi_server/src/server/itsi_server.rs +127 -70
- data/crates/itsi_server/src/server/listener.rs +1 -1
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +18 -12
- data/crates/itsi_server/src/server/signal.rs +7 -0
- data/crates/itsi_server/src/server/thread_worker.rs +3 -4
- 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_server/src/lib.rs +5 -0
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +30 -2
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +12 -2
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +127 -70
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +1 -1
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +18 -12
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +7 -0
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +3 -4
- 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/Rakefile +8 -1
- data/gems/server/ext/itsi_server/src/lib.rs +5 -0
- data/gems/server/ext/itsi_server/src/request/itsi_request.rs +30 -2
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +12 -2
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +127 -70
- data/gems/server/ext/itsi_server/src/server/listener.rs +1 -1
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +18 -12
- data/gems/server/ext/itsi_server/src/server/signal.rs +7 -0
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +3 -4
- 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/rack/handler/itsi.rb +3 -4
- data/gems/server/lib/itsi/server/rack_interface.rb +79 -0
- data/gems/server/lib/itsi/server/scheduler_interface.rb +21 -0
- data/gems/server/lib/itsi/server/signal_trap.rb +24 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +67 -101
- data/gems/server/test/helpers/test_helper.rb +28 -0
- data/gems/server/test/test_itsi_server.rb +275 -3
- data/lib/itsi/version.rb +1 -1
- data/sandbox/deploy/main.tf +1 -0
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/tasks.txt +0 -6
- metadata +13 -11
- 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
@@ -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
|
|
@@ -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
|
+
}
|
@@ -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
|
@@ -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
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/reporters"
|
4
|
+
require "itsi/server"
|
5
|
+
require "itsi/scheduler"
|
6
|
+
|
7
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
8
|
+
|
9
|
+
def free_bind
|
10
|
+
server = TCPServer.new("0.0.0.0", 0)
|
11
|
+
port = server.addr[1]
|
12
|
+
server.close
|
13
|
+
"http://0.0.0.0:#{port}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def run_app(app, **opts)
|
17
|
+
bind = free_bind
|
18
|
+
server = Itsi::Server.start_in_background_thread(
|
19
|
+
app: app,
|
20
|
+
binds: [bind],
|
21
|
+
**opts
|
22
|
+
)
|
23
|
+
|
24
|
+
sleep 0.1
|
25
|
+
yield URI(bind), server
|
26
|
+
ensure
|
27
|
+
server&.stop
|
28
|
+
end
|