kino 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/Cargo.lock +133 -4
- data/README.md +56 -14
- data/exe/kino +0 -4
- data/ext/kino/Cargo.toml +4 -2
- data/ext/kino/src/access_log.rs +245 -0
- data/ext/kino/src/control.rs +12 -29
- data/ext/kino/src/cpus.rs +37 -0
- data/ext/kino/src/lib.rs +10 -1
- data/ext/kino/src/listen.rs +134 -0
- data/ext/kino/src/log.rs +144 -0
- data/ext/kino/src/queue.rs +7 -3
- data/ext/kino/src/registry.rs +3 -0
- data/ext/kino/src/request.rs +48 -2
- data/ext/kino/src/server.rs +114 -48
- data/ext/kino/src/style.rs +42 -39
- data/lib/kino/cli.rb +20 -9
- data/lib/kino/configuration.rb +20 -5
- data/lib/kino/errors_stream.rb +4 -3
- data/lib/kino/hook_fire.rb +3 -4
- data/lib/kino/log.rb +104 -0
- data/lib/kino/quarantine_monitor.rb +7 -4
- data/lib/kino/ractor_supervisor.rb +8 -3
- data/lib/kino/server.rb +60 -15
- data/lib/kino/templates/kino.rb.tt +11 -5
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +12 -13
- data/lib/kino/worker_hooks.rb +3 -1
- data/lib/kino.rb +11 -0
- data/lib/rackup/handler/kino.rb +88 -0
- data/sig/kino.rbs +62 -1
- metadata +21 -1
data/ext/kino/src/server.rs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! intake. Ruby is never on these threads; the only contact points are the
|
|
3
3
|
//! flume queue (in) and each request's Responder (out).
|
|
4
4
|
|
|
5
|
-
use std::net::SocketAddr;
|
|
5
|
+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
6
6
|
use std::sync::atomic::Ordering;
|
|
7
7
|
use std::sync::Arc;
|
|
8
8
|
use std::time::Duration;
|
|
@@ -13,6 +13,7 @@ use hyper_util::rt::TokioIo;
|
|
|
13
13
|
use magnus::{Error, Ruby};
|
|
14
14
|
use parking_lot::{Mutex, RwLock};
|
|
15
15
|
|
|
16
|
+
use crate::listen::Listener;
|
|
16
17
|
use crate::registry::{self, BoxedCtx, ServerInner, WorkerSlot};
|
|
17
18
|
use crate::request::RequestCtx;
|
|
18
19
|
use crate::response::{plain_response, HyperResponse, Responder};
|
|
@@ -78,15 +79,23 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
let listener =
|
|
82
|
-
.map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
let listener =
|
|
83
|
+
Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
84
|
+
// Ruby refuses this combination up front; this guards embedders
|
|
85
|
+
// calling the native layer directly.
|
|
86
|
+
if acceptor.is_some() && matches!(listener, Listener::Unix(..)) {
|
|
87
|
+
return Err(Error::new(
|
|
88
|
+
ruby.exception_arg_error(),
|
|
89
|
+
"TLS is not supported on a unix socket bind",
|
|
90
|
+
));
|
|
91
|
+
}
|
|
86
92
|
let local_port = listener
|
|
87
|
-
.
|
|
88
|
-
.map_err(|e| io_error(ruby, "listener setup failed", e))
|
|
89
|
-
|
|
93
|
+
.port()
|
|
94
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?;
|
|
95
|
+
let unix_path = match &listener {
|
|
96
|
+
Listener::Unix(_, path) => Some(path.clone()),
|
|
97
|
+
Listener::Tcp(_) => None,
|
|
98
|
+
};
|
|
90
99
|
|
|
91
100
|
let control_bind_addr: Option<String> = cfg_opt(ruby, config, "control_bind")?;
|
|
92
101
|
let control_token: Option<String> = cfg_opt(ruby, config, "control_token")?;
|
|
@@ -128,6 +137,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
128
137
|
quarantine_replacements: std::sync::atomic::AtomicU64::new(0),
|
|
129
138
|
topology: registry::Topology { mode, workers, threads, batch },
|
|
130
139
|
https: acceptor.is_some(),
|
|
140
|
+
unix_path,
|
|
131
141
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
132
142
|
lanes,
|
|
133
143
|
lane_cursor: std::sync::atomic::AtomicUsize::new(0),
|
|
@@ -137,8 +147,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
137
147
|
|
|
138
148
|
let tokio_listener = {
|
|
139
149
|
let _guard = runtime.enter();
|
|
140
|
-
|
|
141
|
-
.map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
150
|
+
AsyncListener::from_std(listener).map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
142
151
|
};
|
|
143
152
|
runtime.spawn(accept_loop(
|
|
144
153
|
tokio_listener,
|
|
@@ -182,8 +191,59 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
182
191
|
/// timeout: not a knob.
|
|
183
192
|
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
|
184
193
|
|
|
194
|
+
/// What a unix-socket connection reports as its addresses. The peer is
|
|
195
|
+
/// local by definition (REMOTE_ADDR 127.0.0.1), and a socket has no port,
|
|
196
|
+
/// so SERVER_PORT falls back to http's default when the Host header names
|
|
197
|
+
/// none, the way Puma reports unix-socket requests.
|
|
198
|
+
const UNIX_PEER: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
|
199
|
+
const UNIX_LOCAL: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80);
|
|
200
|
+
|
|
201
|
+
/// The accept loop's listener: TCP (optionally behind TLS), or a unix
|
|
202
|
+
/// socket, which carries plain HTTP only.
|
|
203
|
+
enum AsyncListener {
|
|
204
|
+
Tcp(tokio::net::TcpListener),
|
|
205
|
+
Unix(tokio::net::UnixListener),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// One accepted connection, before the protocol layer sees it.
|
|
209
|
+
enum Conn {
|
|
210
|
+
Tcp(tokio::net::TcpStream),
|
|
211
|
+
Unix(tokio::net::UnixStream),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
impl AsyncListener {
|
|
215
|
+
/// Register the bound listener with the current runtime.
|
|
216
|
+
fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
217
|
+
Ok(match listener {
|
|
218
|
+
Listener::Tcp(listener) => AsyncListener::Tcp(tokio::net::TcpListener::from_std(listener)?),
|
|
219
|
+
Listener::Unix(listener, _) => {
|
|
220
|
+
AsyncListener::Unix(tokio::net::UnixListener::from_std(listener)?)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// The next connection with its (peer, local) addresses.
|
|
226
|
+
async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
227
|
+
match self {
|
|
228
|
+
AsyncListener::Tcp(listener) => {
|
|
229
|
+
let (stream, remote_addr) = listener.accept().await?;
|
|
230
|
+
// Small responses must not wait on Nagle + delayed ACK.
|
|
231
|
+
let _ = stream.set_nodelay(true);
|
|
232
|
+
let local_addr = stream
|
|
233
|
+
.local_addr()
|
|
234
|
+
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
235
|
+
Ok((Conn::Tcp(stream), remote_addr, local_addr))
|
|
236
|
+
}
|
|
237
|
+
AsyncListener::Unix(listener) => {
|
|
238
|
+
let (stream, _) = listener.accept().await?;
|
|
239
|
+
Ok((Conn::Unix(stream), UNIX_PEER, UNIX_LOCAL))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
185
245
|
async fn accept_loop(
|
|
186
|
-
listener:
|
|
246
|
+
listener: AsyncListener,
|
|
187
247
|
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
188
248
|
server: Arc<ServerInner>,
|
|
189
249
|
max_connections: usize,
|
|
@@ -202,25 +262,20 @@ async fn accept_loop(
|
|
|
202
262
|
Err(_) => break, // semaphore closed
|
|
203
263
|
},
|
|
204
264
|
};
|
|
205
|
-
let (
|
|
265
|
+
let (conn, remote_addr, local_addr) = tokio::select! {
|
|
206
266
|
_ = shutdown_rx.changed() => break,
|
|
207
267
|
accepted = listener.accept() => match accepted {
|
|
208
|
-
Ok(
|
|
268
|
+
Ok(accepted) => accepted,
|
|
209
269
|
Err(_) => continue, // transient accept error; permit drops, retry
|
|
210
270
|
},
|
|
211
271
|
};
|
|
212
|
-
// Small responses must not wait on Nagle + delayed ACK.
|
|
213
|
-
let _ = stream.set_nodelay(true);
|
|
214
|
-
let local_addr = stream
|
|
215
|
-
.local_addr()
|
|
216
|
-
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
217
272
|
let server = server.clone();
|
|
218
273
|
let acceptor = acceptor.clone();
|
|
219
274
|
tokio::spawn(async move {
|
|
220
275
|
// Held for the connection's lifetime; dropping it frees a slot.
|
|
221
276
|
let _permit = permit;
|
|
222
|
-
match acceptor {
|
|
223
|
-
Some(acceptor) => {
|
|
277
|
+
match (conn, acceptor) {
|
|
278
|
+
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
224
279
|
// Handshake failures (port scans, plain HTTP to a TLS
|
|
225
280
|
// port) and stalled handshakes (slowloris) just drop the
|
|
226
281
|
// connection; the timeout bounds the latter.
|
|
@@ -228,7 +283,13 @@ async fn accept_loop(
|
|
|
228
283
|
let Ok(Ok(tls)) = handshake.await else { return };
|
|
229
284
|
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
230
285
|
}
|
|
231
|
-
|
|
286
|
+
(Conn::Tcp(stream), None) => {
|
|
287
|
+
serve_connection(stream, server, remote_addr, local_addr).await
|
|
288
|
+
}
|
|
289
|
+
// TLS over a unix socket is refused at bind time.
|
|
290
|
+
(Conn::Unix(stream), _) => {
|
|
291
|
+
serve_connection(stream, server, remote_addr, local_addr).await
|
|
292
|
+
}
|
|
232
293
|
}
|
|
233
294
|
});
|
|
234
295
|
}
|
|
@@ -310,18 +371,17 @@ async fn handle_request(
|
|
|
310
371
|
let (parts, body) = req.into_parts();
|
|
311
372
|
|
|
312
373
|
// Access-log metadata is captured only when logging is on: one Instant
|
|
313
|
-
// read plus
|
|
314
|
-
|
|
374
|
+
// read plus two small Strings per request. The arrival record is
|
|
375
|
+
// queued now, before the app sees the request, so a hang shows as an
|
|
376
|
+
// arrow with no answer; the completion record follows the response.
|
|
377
|
+
let log_meta = server.access_log.as_ref().map(|log| {
|
|
315
378
|
let target = match parts.uri.query() {
|
|
316
379
|
Some(q) => format!("{}?{}", parts.uri.path(), q),
|
|
317
380
|
None => parts.uri.path().to_string(),
|
|
318
381
|
};
|
|
319
|
-
(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
target,
|
|
323
|
-
parts.version,
|
|
324
|
-
)
|
|
382
|
+
let method = parts.method.to_string();
|
|
383
|
+
log.write_line(crate::access_log::arrival(&method, &target, remote_addr.ip()));
|
|
384
|
+
(std::time::Instant::now(), method, target)
|
|
325
385
|
});
|
|
326
386
|
|
|
327
387
|
// Body-size guard: an honestly-declared oversize body is refused with a
|
|
@@ -378,6 +438,7 @@ async fn handle_request(
|
|
|
378
438
|
|
|
379
439
|
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
380
440
|
let responder = Arc::new(Responder::new(head_tx));
|
|
441
|
+
let now = std::time::Instant::now();
|
|
381
442
|
let ctx = Box::new(RequestCtx {
|
|
382
443
|
method: parts.method,
|
|
383
444
|
uri: parts.uri,
|
|
@@ -393,7 +454,11 @@ async fn handle_request(
|
|
|
393
454
|
slot: None,
|
|
394
455
|
pin_slab: server.pin_slab.clone(),
|
|
395
456
|
responder,
|
|
396
|
-
enqueued_at:
|
|
457
|
+
enqueued_at: now,
|
|
458
|
+
timed: server.access_log.is_some(),
|
|
459
|
+
wait: Duration::ZERO,
|
|
460
|
+
admitted_at: now,
|
|
461
|
+
gc: None,
|
|
397
462
|
});
|
|
398
463
|
|
|
399
464
|
// Drop guard, not manual decrement: when a client aborts mid-request,
|
|
@@ -472,17 +537,20 @@ async fn handle_request(
|
|
|
472
537
|
}
|
|
473
538
|
};
|
|
474
539
|
|
|
475
|
-
if let (Some(log), Some((start, method, target
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
let
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
540
|
+
if let (Some(log), Some((start, method, target))) = (server.access_log.as_ref(), log_meta) {
|
|
541
|
+
// The worker attached its timing to the response head; a 503 or
|
|
542
|
+
// 504 never reached a worker and carries none.
|
|
543
|
+
let timing = response
|
|
544
|
+
.extensions()
|
|
545
|
+
.get::<crate::access_log::Timing>()
|
|
546
|
+
.copied();
|
|
547
|
+
log.write_line(crate::access_log::completion(
|
|
548
|
+
response.status().as_u16(),
|
|
549
|
+
&method,
|
|
550
|
+
&target,
|
|
551
|
+
start.elapsed(),
|
|
552
|
+
timing,
|
|
553
|
+
));
|
|
486
554
|
}
|
|
487
555
|
|
|
488
556
|
Ok(branded(response))
|
|
@@ -651,6 +719,10 @@ pub fn shutdown_runtime(_ruby: &Ruby, server_id: u64, timeout_ms: u64) -> Result
|
|
|
651
719
|
if let Some(runtime) = server.runtime.lock().take() {
|
|
652
720
|
runtime.shutdown_timeout(Duration::from_millis(timeout_ms));
|
|
653
721
|
}
|
|
722
|
+
// The listener is closed with the runtime; its socket file is not.
|
|
723
|
+
if let Some(path) = &server.unix_path {
|
|
724
|
+
crate::listen::cleanup_unix(path);
|
|
725
|
+
}
|
|
654
726
|
}
|
|
655
727
|
Ok(())
|
|
656
728
|
}
|
|
@@ -665,12 +737,6 @@ pub fn pin_keeper(
|
|
|
665
737
|
Ok(ruby.obj_wrap(crate::pin::PinKeeper(server.pin_slab.clone())))
|
|
666
738
|
}
|
|
667
739
|
|
|
668
|
-
/// Errors print in red on color terminals. Covers worker errors,
|
|
669
|
-
/// supervisor crash reports, and everything apps write to rack.errors.
|
|
670
|
-
pub fn log_error(message: String) {
|
|
671
|
-
eprintln!("{}", crate::style::red(&format!("[Kino] {message}")));
|
|
672
|
-
}
|
|
673
|
-
|
|
674
740
|
/// Full stats snapshot: [queued, in_flight, served, rejected, timeouts,
|
|
675
741
|
/// respawns, lane_depths]. lane_depths is nil unless lane dispatch is on.
|
|
676
742
|
#[allow(clippy::type_complexity)]
|
data/ext/kino/src/style.rs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
//! ANSI styling for
|
|
2
|
-
//!
|
|
3
|
-
//! startup banner has its own twin in Kino::CLI.) Color
|
|
4
|
-
//! decided once per stream; every styled string resets at
|
|
5
|
-
//! nothing bleeds.
|
|
1
|
+
//! ANSI styling for what the native layer writes to the terminal: the
|
|
2
|
+
//! access log on stdout and server log lines on either stream. (The
|
|
3
|
+
//! Ruby-side startup banner has its own twin in Kino::CLI.) Color
|
|
4
|
+
//! capability is decided once per stream; every styled string resets at
|
|
5
|
+
//! its end so nothing bleeds.
|
|
6
6
|
|
|
7
7
|
use std::sync::OnceLock;
|
|
8
8
|
|
|
9
9
|
#[derive(Clone, Copy)]
|
|
10
|
-
enum Stream {
|
|
10
|
+
pub enum Stream {
|
|
11
11
|
Stdout,
|
|
12
12
|
Stderr,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/// Whether `stream` is a color terminal: a tty, no NO_COLOR, TERM not dumb.
|
|
16
|
+
pub fn enabled(stream: Stream) -> bool {
|
|
16
17
|
use std::io::IsTerminal;
|
|
17
18
|
static STDOUT: OnceLock<bool> = OnceLock::new();
|
|
18
19
|
static STDERR: OnceLock<bool> = OnceLock::new();
|
|
@@ -27,55 +28,51 @@ fn enabled(stream: Stream) -> bool {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/// Wrap `text` in an SGR code (
|
|
31
|
-
///
|
|
32
|
-
fn
|
|
33
|
-
if
|
|
31
|
+
/// Wrap `text` in an SGR code ("1" bold, "91" bright red, "1;32" bold
|
|
32
|
+
/// green) when `color`, resetting at the end; plain otherwise.
|
|
33
|
+
pub fn sgr(code: &str, text: &str, color: bool) -> String {
|
|
34
|
+
if color {
|
|
34
35
|
format!("\x1b[{code}m{text}\x1b[0m")
|
|
35
36
|
} else {
|
|
36
37
|
text.to_string()
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
///
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/// Dark gray: timestamps and the timing breakdown recede behind the record.
|
|
42
|
+
pub const DIM: &str = "90";
|
|
43
|
+
/// Bold bright white: the arrival line, which has no status to color by.
|
|
44
|
+
pub const BOLD_WHITE: &str = "1;97";
|
|
45
|
+
/// Warnings are yellow.
|
|
46
|
+
pub const WARN: &str = "33";
|
|
47
|
+
/// Errors are bright red (91): the base red slot (31) is remapped to odd
|
|
48
|
+
/// hues by some terminal themes; 91 stays red.
|
|
49
|
+
pub const ERROR: &str = "91";
|
|
45
50
|
|
|
46
|
-
/// The SGR code for a status class
|
|
47
|
-
/// 2xx green, 3xx yellow, 4xx maroon
|
|
48
|
-
/// 5xx bright red, anything else uncolored.
|
|
49
|
-
fn status_sgr(status: u16) -> Option<&'static str> {
|
|
51
|
+
/// The SGR code for a status class, bold so the record leads its line
|
|
52
|
+
/// (basic 16-color palette only): 2xx green, 3xx yellow, 4xx maroon
|
|
53
|
+
/// (ANSI color 1, plain dark red), 5xx bright red, anything else uncolored.
|
|
54
|
+
pub fn status_sgr(status: u16) -> Option<&'static str> {
|
|
50
55
|
match status {
|
|
51
|
-
200..=299 => Some("32"),
|
|
52
|
-
300..=399 => Some("33"),
|
|
53
|
-
400..=499 => Some("31"),
|
|
54
|
-
500..=599 => Some("91"),
|
|
56
|
+
200..=299 => Some("1;32"),
|
|
57
|
+
300..=399 => Some("1;33"),
|
|
58
|
+
400..=499 => Some("1;31"),
|
|
59
|
+
500..=599 => Some("1;91"),
|
|
55
60
|
_ => None,
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/// Access-log lines on stdout, tinted by status class.
|
|
60
|
-
pub fn status_colored(status: u16, line: &str) -> String {
|
|
61
|
-
match status_sgr(status) {
|
|
62
|
-
Some(code) => paint(Stream::Stdout, code, line),
|
|
63
|
-
None => line.to_string(),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
64
|
#[cfg(test)]
|
|
68
65
|
mod tests {
|
|
69
|
-
use super::status_sgr;
|
|
66
|
+
use super::{sgr, status_sgr};
|
|
70
67
|
|
|
71
68
|
#[test]
|
|
72
69
|
fn status_classes_map_to_their_colors() {
|
|
73
|
-
assert_eq!(status_sgr(200), Some("32")); // green
|
|
74
|
-
assert_eq!(status_sgr(299), Some("32"));
|
|
75
|
-
assert_eq!(status_sgr(301), Some("33")); // yellow
|
|
76
|
-
assert_eq!(status_sgr(404), Some("31")); // maroon
|
|
77
|
-
assert_eq!(status_sgr(500), Some("91")); // bright red
|
|
78
|
-
assert_eq!(status_sgr(599), Some("91"));
|
|
70
|
+
assert_eq!(status_sgr(200), Some("1;32")); // green
|
|
71
|
+
assert_eq!(status_sgr(299), Some("1;32"));
|
|
72
|
+
assert_eq!(status_sgr(301), Some("1;33")); // yellow
|
|
73
|
+
assert_eq!(status_sgr(404), Some("1;31")); // maroon
|
|
74
|
+
assert_eq!(status_sgr(500), Some("1;91")); // bright red
|
|
75
|
+
assert_eq!(status_sgr(599), Some("1;91"));
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
#[test]
|
|
@@ -84,4 +81,10 @@ mod tests {
|
|
|
84
81
|
assert_eq!(status_sgr(199), None);
|
|
85
82
|
assert_eq!(status_sgr(600), None);
|
|
86
83
|
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn sgr_wraps_and_resets_only_when_coloring() {
|
|
87
|
+
assert_eq!(sgr("1;32", "ok", true), "\x1b[1;32mok\x1b[0m");
|
|
88
|
+
assert_eq!(sgr("1;32", "ok", false), "ok");
|
|
89
|
+
}
|
|
87
90
|
}
|
data/lib/kino/cli.rb
CHANGED
|
@@ -100,14 +100,14 @@ module Kino
|
|
|
100
100
|
end.join
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
# One-line stats dump (the SIGUSR1 handler
|
|
103
|
+
# One-line stats dump (what the SIGUSR1 handler logs). Excludes
|
|
104
104
|
# worker_status: it's an array with one entry per execution slot, and
|
|
105
105
|
# printing it inline would break the one-line contract (see /stats for
|
|
106
106
|
# per-worker detail).
|
|
107
107
|
# @param stats [Hash{Symbol => Object}] see {Kino::Server#stats}
|
|
108
108
|
# @return [String]
|
|
109
109
|
def stats_line(stats)
|
|
110
|
-
|
|
110
|
+
"stats #{stats.except(:worker_status).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}"
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# The two banner halves around Server#start: credits before, the ready
|
|
@@ -119,15 +119,27 @@ module Kino
|
|
|
119
119
|
puts dim("\nKino #{VERSION} presents:")
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# The ready block: what this process is (Ruby build with its JIT and
|
|
123
|
+
# parser flags, environment, topology, pid) and where it listens.
|
|
122
124
|
# @param server [Kino::Server] a started server
|
|
123
125
|
# @return [void]
|
|
124
126
|
def action!(server)
|
|
125
|
-
|
|
126
|
-
puts dim("-
|
|
127
|
+
stats = server.stats
|
|
128
|
+
puts dim("- ruby: #{RUBY_DESCRIPTION}")
|
|
129
|
+
puts dim("- env: #{ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"}")
|
|
130
|
+
puts dim("- mode: #{server.mode}, #{count(stats[:workers], "worker")} × #{count(stats[:threads], "thread")}")
|
|
131
|
+
puts dim("- pid: #{Process.pid}")
|
|
132
|
+
puts dim("- listening: #{server.url}")
|
|
133
|
+
puts dim("- control: #{server.control_url}") if server.control_url
|
|
127
134
|
puts dim("- Ctrl-C to drain and stop")
|
|
128
135
|
puts "\n#{bold("Action!")}\n\n"
|
|
129
136
|
end
|
|
130
137
|
|
|
138
|
+
# "1 worker", "8 workers".
|
|
139
|
+
def count(number, noun)
|
|
140
|
+
"#{number} #{noun}#{"s" unless number == 1}"
|
|
141
|
+
end
|
|
142
|
+
|
|
131
143
|
# Roll credits when the process ends: normal exit or crash (at_exit
|
|
132
144
|
# also runs after an uncaught exception; only a force-exit skips it).
|
|
133
145
|
# @return [void]
|
|
@@ -200,7 +212,7 @@ module Kino
|
|
|
200
212
|
def option_parser(options)
|
|
201
213
|
OptionParser.new do |opts|
|
|
202
214
|
opts.banner = "Usage: kino [options] [rackup file (default: config.ru)]"
|
|
203
|
-
opts.on("-C", "--config FILE", "Config file (default: kino.rb
|
|
215
|
+
opts.on("-C", "--config FILE", "Config file (default: kino.rb, then config/kino.rb)") { |v| options[:config_file] = v }
|
|
204
216
|
opts.on("--init [PATH]", "Write a commented sample config (default: kino.rb) and exit") do |v|
|
|
205
217
|
options[:init_path] = v || "kino.rb"
|
|
206
218
|
end
|
|
@@ -236,14 +248,13 @@ module Kino
|
|
|
236
248
|
require "kino"
|
|
237
249
|
require "rack"
|
|
238
250
|
|
|
239
|
-
config_file = options[:config_file]
|
|
240
|
-
config_file ||= ("kino.rb" if File.exist?("kino.rb"))
|
|
251
|
+
config_file = options[:config_file] || Configuration.default_path
|
|
241
252
|
|
|
242
253
|
config = Configuration.new
|
|
243
254
|
config.load_file(config_file) if config_file
|
|
244
255
|
config.merge!(options[:overrides])
|
|
245
256
|
# Default port 9292 when neither the file nor a flag chose one.
|
|
246
|
-
config.set(:port,
|
|
257
|
+
config.set(:port, Configuration::DEFAULT_SERVING_PORT) unless config.set?(:port)
|
|
247
258
|
config
|
|
248
259
|
end
|
|
249
260
|
|
|
@@ -252,6 +263,6 @@ module Kino
|
|
|
252
263
|
end
|
|
253
264
|
|
|
254
265
|
private_class_method :print_help, :option_parser, :write_sample,
|
|
255
|
-
:resolve_config, :serve
|
|
266
|
+
:resolve_config, :serve, :count
|
|
256
267
|
end
|
|
257
268
|
end
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "etc"
|
|
4
|
-
|
|
5
3
|
module Kino
|
|
6
4
|
# Server settings with Puma-style precedence:
|
|
7
5
|
# explicit Server.new kwargs > config file DSL > defaults.
|
|
@@ -11,7 +9,7 @@ module Kino
|
|
|
11
9
|
DEFAULTS = {
|
|
12
10
|
bind: "127.0.0.1",
|
|
13
11
|
port: 0,
|
|
14
|
-
workers: nil, # resolved to
|
|
12
|
+
workers: nil, # resolved to Kino.available_parallelism in #to_h
|
|
15
13
|
threads: nil, # resolved per mode in Server: 1 in :ractor, 3 in :threaded
|
|
16
14
|
mode: :auto,
|
|
17
15
|
queue_depth: 1024,
|
|
@@ -45,6 +43,21 @@ module Kino
|
|
|
45
43
|
# Source template for {.sample}.
|
|
46
44
|
SAMPLE_TEMPLATE = File.expand_path("templates/kino.rb.tt", __dir__)
|
|
47
45
|
|
|
46
|
+
# Where the `kino` CLI and the Rack handler look for a config file when
|
|
47
|
+
# none is named: the project root first, then the Rails-style config/.
|
|
48
|
+
DEFAULT_PATHS = %w[kino.rb config/kino.rb].freeze
|
|
49
|
+
|
|
50
|
+
# The port the CLI and the Rack handler serve on when neither a flag
|
|
51
|
+
# nor the file chose one (Server.new itself defaults to an ephemeral
|
|
52
|
+
# port, for embedding).
|
|
53
|
+
DEFAULT_SERVING_PORT = 9292
|
|
54
|
+
|
|
55
|
+
# The first of {DEFAULT_PATHS} that exists in the working directory.
|
|
56
|
+
# @return [String, nil]
|
|
57
|
+
def self.default_path
|
|
58
|
+
DEFAULT_PATHS.find { |path| File.exist?(path) }
|
|
59
|
+
end
|
|
60
|
+
|
|
48
61
|
# The fully-commented sample config (see `kino --init`).
|
|
49
62
|
# @return [String]
|
|
50
63
|
def self.sample
|
|
@@ -113,7 +126,7 @@ module Kino
|
|
|
113
126
|
# @return [Hash{Symbol => Object}] every setting, defaults filled in
|
|
114
127
|
def to_h
|
|
115
128
|
SETTINGS.to_h { |key| [key, self[key]] }.tap do |h|
|
|
116
|
-
h[:workers] ||=
|
|
129
|
+
h[:workers] ||= Kino.available_parallelism
|
|
117
130
|
end
|
|
118
131
|
end
|
|
119
132
|
|
|
@@ -146,7 +159,9 @@ module Kino
|
|
|
146
159
|
@config = config
|
|
147
160
|
end
|
|
148
161
|
|
|
149
|
-
# Address to listen on ("0.0.0.0" accepts non-local
|
|
162
|
+
# Address to listen on: a host ("0.0.0.0" accepts non-local
|
|
163
|
+
# connections), or "unix:///path/to.sock" for a unix domain socket
|
|
164
|
+
# (then `port` is unused).
|
|
150
165
|
def bind(host) = @config.set(:bind, host)
|
|
151
166
|
|
|
152
167
|
# Port to listen on; 0 picks an ephemeral port.
|
data/lib/kino/errors_stream.rb
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Kino
|
|
4
4
|
# @private
|
|
5
|
-
# rack.errors: stateless writer into the
|
|
5
|
+
# rack.errors: stateless writer into the server log (one line per
|
|
6
|
+
# call, labelled like every other line Kino writes). Frozen singleton,
|
|
6
7
|
# which also makes it Ractor-shareable; one instance serves all workers.
|
|
7
8
|
class ErrorsStream
|
|
8
9
|
def puts(message)
|
|
9
|
-
|
|
10
|
+
Log.error(message.to_s.chomp)
|
|
10
11
|
nil
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def write(message)
|
|
14
15
|
message = message.to_s
|
|
15
|
-
|
|
16
|
+
Log.error(message.chomp)
|
|
16
17
|
message.bytesize
|
|
17
18
|
end
|
|
18
19
|
|
data/lib/kino/hook_fire.rb
CHANGED
|
@@ -4,9 +4,8 @@ module Kino
|
|
|
4
4
|
# @private
|
|
5
5
|
# Fires a lifecycle hook and turns a raise into a logged line instead of
|
|
6
6
|
# letting it escape. Stateless and touches only its arguments plus
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# captured.
|
|
7
|
+
# Kino::Log (safe inside worker ractors), so it is safe to call from
|
|
8
|
+
# worker context: no main-ractor state is captured.
|
|
10
9
|
module HookFire
|
|
11
10
|
module_function
|
|
12
11
|
|
|
@@ -16,7 +15,7 @@ module Kino
|
|
|
16
15
|
begin
|
|
17
16
|
hook.call(*args)
|
|
18
17
|
rescue => e
|
|
19
|
-
|
|
18
|
+
Log.error("#{name} hook raised #{e.class}: #{e.message}")
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
end
|