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.
@@ -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 = std::net::TcpListener::bind((bind.as_str(), port))
82
- .map_err(|e| io_error(ruby, "bind failed", e))?;
83
- listener
84
- .set_nonblocking(true)
85
- .map_err(|e| io_error(ruby, "listener setup failed", e))?;
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
- .local_addr()
88
- .map_err(|e| io_error(ruby, "listener setup failed", e))?
89
- .port();
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
- tokio::net::TcpListener::from_std(listener)
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: tokio::net::TcpListener,
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 (stream, remote_addr) = tokio::select! {
265
+ let (conn, remote_addr, local_addr) = tokio::select! {
206
266
  _ = shutdown_rx.changed() => break,
207
267
  accepted = listener.accept() => match accepted {
208
- Ok(pair) => pair,
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
- None => serve_connection(stream, server, remote_addr, local_addr).await,
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 one small String per request.
314
- let log_meta = server.access_log.as_ref().map(|_| {
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
- std::time::Instant::now(),
321
- parts.method.to_string(),
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: std::time::Instant::now(),
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, version))) =
476
- (server.access_log.as_ref(), log_meta)
477
- {
478
- let status = response.status().as_u16();
479
- let line = format!(
480
- "{} [{}] \"{method} {target} {version:?}\" {status} {:.1}ms",
481
- remote_addr.ip(),
482
- httpdate::fmt_http_date(std::time::SystemTime::now()),
483
- start.elapsed().as_secs_f64() * 1000.0
484
- );
485
- log.write_line(crate::style::status_colored(status, &line));
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)]
@@ -1,18 +1,19 @@
1
- //! ANSI styling for the few places the native layer writes to the
2
- //! terminal: stderr error lines and the stdout access log. (The Ruby-side
3
- //! startup banner has its own twin in Kino::CLI.) Color-capability is
4
- //! decided once per stream; every styled string resets at its end so
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
- fn enabled(stream: Stream) -> bool {
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 (e.g. "31" red, "1" bold, "38;5;208"
31
- /// 256-color), plain when the stream isn't a color terminal.
32
- fn paint(stream: Stream, code: &str, text: &str) -> String {
33
- if enabled(stream) {
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
- /// Errors on stderr are bright red (91): the base red slot (31) is
41
- /// remapped to odd hues by some terminal themes; 91 stays red.
42
- pub fn red(text: &str) -> String {
43
- paint(Stream::Stderr, "91", text)
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 (basic 16-color palette only):
47
- /// 2xx green, 3xx yellow, 4xx maroon (ANSI color 1, plain dark red),
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's output). Excludes
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
- dim("Kino stats: #{stats.except(:worker_status).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}")
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
- puts dim("- mode: #{server.mode}")
126
- puts dim("- listening: http#{"s" if server.tls?}://#{server.bind}:#{server.port}")
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 if present)") { |v| options[:config_file] = v }
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, 9292) unless 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
@@ -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 Etc.nprocessors in #to_h
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] ||= Etc.nprocessors
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 connections).
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.
@@ -2,17 +2,18 @@
2
2
 
3
3
  module Kino
4
4
  # @private
5
- # rack.errors: stateless writer into the native logger. Frozen singleton,
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
- Native.log_error(message.to_s)
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
- Native.log_error(message)
16
+ Log.error(message.chomp)
16
17
  message.bytesize
17
18
  end
18
19
 
@@ -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
- # Native.log_error (already called from inside worker ractors today), so
8
- # it is safe to call from worker context: no main-ractor state is
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
- Native.log_error("#{name} hook raised #{e.class}: #{e.message}")
18
+ Log.error("#{name} hook raised #{e.class}: #{e.message}")
20
19
  end
21
20
  end
22
21
  end