kino 0.4.0 → 0.6.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 +56 -0
- data/Cargo.lock +115 -24
- data/README.md +113 -41
- data/doc/architecture.md +61 -7
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +267 -60
- data/ext/kino/src/env_strings.rs +172 -12
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +12 -7
- data/ext/kino/src/listen.rs +4 -1
- data/ext/kino/src/log.rs +10 -2
- data/ext/kino/src/queue.rs +61 -44
- data/ext/kino/src/registry.rs +103 -10
- data/ext/kino/src/request.rs +150 -69
- data/ext/kino/src/response.rs +40 -9
- data/ext/kino/src/server.rs +759 -97
- data/ext/kino/src/tls.rs +129 -12
- data/lib/kino/configuration.rb +16 -0
- data/lib/kino/server.rb +10 -0
- data/lib/kino/templates/kino.rb.tt +13 -2
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/sig/kino.rbs +3 -0
- metadata +16 -1
data/ext/kino/src/tls.rs
CHANGED
|
@@ -17,7 +17,13 @@ fn pem_bytes(input: &str) -> Result<Vec<u8>, String> {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
|
|
20
|
+
pub fn build_acceptor(cert: &str, key: &str, http2: bool) -> Result<TlsAcceptor, String> {
|
|
21
|
+
Ok(TlsAcceptor::from(Arc::new(server_config(
|
|
22
|
+
cert, key, http2,
|
|
23
|
+
)?)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn server_config(cert: &str, key: &str, http2: bool) -> Result<ServerConfig, String> {
|
|
21
27
|
let cert_pem = pem_bytes(cert)?;
|
|
22
28
|
let key_pem = pem_bytes(key)?;
|
|
23
29
|
|
|
@@ -36,14 +42,19 @@ pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
|
|
|
36
42
|
.with_no_client_auth()
|
|
37
43
|
.with_single_cert(certs, key)
|
|
38
44
|
.map_err(|e| format!("TLS config rejected: {e}"))?;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// Preference order matters: h2 first, so an HTTP/2-capable client
|
|
46
|
+
// negotiates it; a client offering only http/1.1 still matches.
|
|
47
|
+
config.alpn_protocols = if http2 {
|
|
48
|
+
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
|
49
|
+
} else {
|
|
50
|
+
vec![b"http/1.1".to_vec()]
|
|
51
|
+
};
|
|
52
|
+
Ok(config)
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
#[cfg(test)]
|
|
45
56
|
mod tests {
|
|
46
|
-
use super::build_acceptor;
|
|
57
|
+
use super::{build_acceptor, server_config};
|
|
47
58
|
|
|
48
59
|
const CERT: &str = "-----BEGIN CERTIFICATE-----
|
|
49
60
|
MIIBfjCCASWgAwIBAgIUTs9+cVJSzJjy4TSi9YLEd+i4KiIwCgYIKoZIzj0EAwIw
|
|
@@ -65,32 +76,138 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
|
|
|
65
76
|
|
|
66
77
|
#[test]
|
|
67
78
|
fn inline_pem_builds_an_acceptor() {
|
|
68
|
-
assert!(build_acceptor(CERT, KEY).is_ok());
|
|
79
|
+
assert!(build_acceptor(CERT, KEY, true).is_ok());
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
#[test]
|
|
72
83
|
fn missing_file_paths_error_without_panicking() {
|
|
73
|
-
let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem")
|
|
74
|
-
.err()
|
|
84
|
+
let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem", true)
|
|
85
|
+
.err()
|
|
86
|
+
.expect("missing files");
|
|
75
87
|
assert!(err.contains("cannot read"));
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
#[test]
|
|
79
91
|
fn pem_without_certificates_is_rejected() {
|
|
80
|
-
let err = build_acceptor(KEY, KEY
|
|
92
|
+
let err = build_acceptor(KEY, KEY, true)
|
|
93
|
+
.err()
|
|
94
|
+
.expect("a key is not a cert");
|
|
81
95
|
assert!(err.contains("no certificates found"));
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
#[test]
|
|
85
99
|
fn pem_without_a_key_is_rejected() {
|
|
86
|
-
let err = build_acceptor(CERT, CERT
|
|
100
|
+
let err = build_acceptor(CERT, CERT, true)
|
|
101
|
+
.err()
|
|
102
|
+
.expect("a cert is not a key");
|
|
87
103
|
assert!(err.contains("no private key found"));
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
#[test]
|
|
91
107
|
fn mismatched_cert_and_garbage_key_are_rejected() {
|
|
92
|
-
let err = build_acceptor(
|
|
93
|
-
|
|
108
|
+
let err = build_acceptor(
|
|
109
|
+
CERT,
|
|
110
|
+
"-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----",
|
|
111
|
+
true,
|
|
112
|
+
)
|
|
113
|
+
.err()
|
|
114
|
+
.expect("garbage key");
|
|
94
115
|
assert!(!err.is_empty());
|
|
95
116
|
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn alpn_advertises_h2_first_when_http2_is_on() {
|
|
120
|
+
let config = server_config(CERT, KEY, true).expect("valid PEM");
|
|
121
|
+
assert_eq!(
|
|
122
|
+
config.alpn_protocols,
|
|
123
|
+
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn alpn_advertises_only_http11_when_http2_is_off() {
|
|
129
|
+
let config = server_config(CERT, KEY, false).expect("valid PEM");
|
|
130
|
+
assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// A real rustls handshake over an in-memory pipe: an h2-capable
|
|
134
|
+
/// client must land on "h2", proving the advertisement is not just a
|
|
135
|
+
/// config field but what the wire negotiates. The client skips
|
|
136
|
+
/// certificate verification (the fixture cert is CA-flagged, which
|
|
137
|
+
/// webpki refuses as an end-entity cert): ALPN is under test here,
|
|
138
|
+
/// not the trust chain.
|
|
139
|
+
#[tokio::test]
|
|
140
|
+
async fn handshake_negotiates_h2_with_a_capable_client() {
|
|
141
|
+
use std::sync::Arc;
|
|
142
|
+
|
|
143
|
+
use tokio_rustls::rustls::client::danger::{
|
|
144
|
+
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
|
|
145
|
+
};
|
|
146
|
+
use tokio_rustls::rustls::crypto::WebPkiSupportedAlgorithms;
|
|
147
|
+
use tokio_rustls::rustls::pki_types::ServerName;
|
|
148
|
+
use tokio_rustls::rustls::ClientConfig;
|
|
149
|
+
use tokio_rustls::{TlsAcceptor, TlsConnector};
|
|
150
|
+
|
|
151
|
+
#[derive(Debug)]
|
|
152
|
+
struct AcceptAny(WebPkiSupportedAlgorithms);
|
|
153
|
+
|
|
154
|
+
impl ServerCertVerifier for AcceptAny {
|
|
155
|
+
fn verify_server_cert(
|
|
156
|
+
&self,
|
|
157
|
+
_end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
158
|
+
_intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
|
|
159
|
+
_server_name: &ServerName<'_>,
|
|
160
|
+
_ocsp_response: &[u8],
|
|
161
|
+
_now: tokio_rustls::rustls::pki_types::UnixTime,
|
|
162
|
+
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
|
|
163
|
+
Ok(ServerCertVerified::assertion())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn verify_tls12_signature(
|
|
167
|
+
&self,
|
|
168
|
+
_message: &[u8],
|
|
169
|
+
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
170
|
+
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
|
|
171
|
+
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
|
172
|
+
Ok(HandshakeSignatureValid::assertion())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn verify_tls13_signature(
|
|
176
|
+
&self,
|
|
177
|
+
_message: &[u8],
|
|
178
|
+
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
|
|
179
|
+
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
|
|
180
|
+
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
|
181
|
+
Ok(HandshakeSignatureValid::assertion())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
|
|
185
|
+
self.0.supported_schemes()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let acceptor =
|
|
190
|
+
TlsAcceptor::from(Arc::new(server_config(CERT, KEY, true).expect("valid PEM")));
|
|
191
|
+
|
|
192
|
+
let provider = tokio_rustls::rustls::crypto::ring::default_provider();
|
|
193
|
+
let verifier = AcceptAny(provider.signature_verification_algorithms);
|
|
194
|
+
let mut client = ClientConfig::builder()
|
|
195
|
+
.dangerous()
|
|
196
|
+
.with_custom_certificate_verifier(Arc::new(verifier))
|
|
197
|
+
.with_no_client_auth();
|
|
198
|
+
client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
199
|
+
let connector = TlsConnector::from(Arc::new(client));
|
|
200
|
+
|
|
201
|
+
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
|
|
202
|
+
let server = tokio::spawn(async move { acceptor.accept(server_io).await });
|
|
203
|
+
let domain = ServerName::try_from("localhost").expect("server name");
|
|
204
|
+
let client_stream = connector
|
|
205
|
+
.connect(domain, client_io)
|
|
206
|
+
.await
|
|
207
|
+
.expect("client handshake");
|
|
208
|
+
let server_stream = server.await.expect("join").expect("server handshake");
|
|
209
|
+
|
|
210
|
+
assert_eq!(client_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
|
|
211
|
+
assert_eq!(server_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
|
|
212
|
+
}
|
|
96
213
|
}
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -26,8 +26,11 @@ module Kino
|
|
|
26
26
|
after_request_complete: nil,
|
|
27
27
|
on_worker_exit: nil,
|
|
28
28
|
shutdown_timeout: 30,
|
|
29
|
+
io_shards: false,
|
|
30
|
+
io_threads: nil,
|
|
29
31
|
tokio_threads: nil,
|
|
30
32
|
tls: nil,
|
|
33
|
+
http2: true,
|
|
31
34
|
environment: nil,
|
|
32
35
|
pidfile: nil,
|
|
33
36
|
control_bind: nil,
|
|
@@ -148,6 +151,8 @@ module Kino
|
|
|
148
151
|
# queue_depth 2048
|
|
149
152
|
# queue_timeout 0.5
|
|
150
153
|
# shutdown_timeout 15
|
|
154
|
+
# io_shards true
|
|
155
|
+
# io_threads 6
|
|
151
156
|
# tokio_threads 4
|
|
152
157
|
# tls cert: "cert.pem", key: "key.pem"
|
|
153
158
|
#
|
|
@@ -229,12 +234,23 @@ module Kino
|
|
|
229
234
|
# Graceful-shutdown drain deadline in seconds.
|
|
230
235
|
def shutdown_timeout(seconds) = @config.set(:shutdown_timeout, seconds)
|
|
231
236
|
|
|
237
|
+
# Run native HTTP I/O on current-thread shards instead of Tokio's shared pool.
|
|
238
|
+
def io_shards(enabled = true) = @config.set(:io_shards, !!enabled)
|
|
239
|
+
|
|
240
|
+
# Native HTTP I/O shard count; default with io_shards: half available CPUs.
|
|
241
|
+
def io_threads(count) = @config.set(:io_threads, Integer(count))
|
|
242
|
+
|
|
232
243
|
# Threads for the tokio (Rust I/O) runtime; default: one per core.
|
|
233
244
|
def tokio_threads(count) = @config.set(:tokio_threads, Integer(count))
|
|
234
245
|
|
|
235
246
|
# TLS termination; file paths or inline PEM strings.
|
|
236
247
|
def tls(cert:, key:) = @config.set(:tls, {cert: cert, key: key})
|
|
237
248
|
|
|
249
|
+
# HTTP/2: negotiated via ALPN on TLS binds, served to
|
|
250
|
+
# prior-knowledge clients on plaintext. On by default; set false
|
|
251
|
+
# to serve HTTP/1 only.
|
|
252
|
+
def http2(enabled = true) = @config.set(:http2, !!enabled)
|
|
253
|
+
|
|
238
254
|
# Sets RACK_ENV (unless already set) before the CLI loads the app.
|
|
239
255
|
def environment(env) = @config.set(:environment, env.to_s)
|
|
240
256
|
|
data/lib/kino/server.rb
CHANGED
|
@@ -98,11 +98,18 @@ module Kino
|
|
|
98
98
|
@lanes = !!settings[:lanes]
|
|
99
99
|
@log_requests = !!settings[:log_requests]
|
|
100
100
|
@shutdown_timeout = settings[:shutdown_timeout]
|
|
101
|
+
@io_shards = !!settings[:io_shards]
|
|
102
|
+
@io_threads = Integer(settings[:io_threads]) unless settings[:io_threads].nil?
|
|
103
|
+
if @io_threads && @io_threads < 1
|
|
104
|
+
raise ArgumentError, "io_threads must be >= 1"
|
|
105
|
+
end
|
|
106
|
+
Log.warn("io_threads has no effect unless io_shards is true") if @io_threads && !@io_shards
|
|
101
107
|
@tokio_threads = settings[:tokio_threads]
|
|
102
108
|
@tls = validate_tls(settings[:tls])
|
|
103
109
|
if @tls && unix?
|
|
104
110
|
raise ArgumentError, "TLS is not supported on a unix socket bind; terminate TLS at the proxy in front"
|
|
105
111
|
end
|
|
112
|
+
@http2 = settings.fetch(:http2, true) ? true : false
|
|
106
113
|
@pidfile = settings[:pidfile]
|
|
107
114
|
@control_bind = settings[:control_bind]&.to_s
|
|
108
115
|
@control_token = settings[:control_token]&.to_s
|
|
@@ -145,8 +152,11 @@ module Kino
|
|
|
145
152
|
request_timeout_ms: @request_timeout_ms,
|
|
146
153
|
max_connections: @max_connections,
|
|
147
154
|
max_body_size: @max_body_size,
|
|
155
|
+
io_shards: @io_shards,
|
|
156
|
+
io_threads: @io_threads,
|
|
148
157
|
tokio_threads: @tokio_threads,
|
|
149
158
|
tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
|
|
159
|
+
http2: @http2,
|
|
150
160
|
lanes: @lanes, log_requests: @log_requests,
|
|
151
161
|
mode: @mode.to_s, workers: @workers, threads: @threads, batch: @batch,
|
|
152
162
|
control_bind: @control_bind, control_token: @control_token
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
# PEM strings also work).
|
|
21
21
|
# tls cert: "config/certs/server.pem", key: "config/certs/server.key"
|
|
22
22
|
|
|
23
|
+
# HTTP/2 is on by default: negotiated via ALPN on TLS binds, and served
|
|
24
|
+
# to prior-knowledge (h2c) clients on plaintext. Everything else keeps
|
|
25
|
+
# getting HTTP/1.1. Set false to serve HTTP/1 only.
|
|
26
|
+
# http2 false
|
|
27
|
+
|
|
23
28
|
## Topology
|
|
24
29
|
|
|
25
30
|
# How many workers to run. Each worker handles requests independently;
|
|
@@ -125,8 +130,14 @@
|
|
|
125
130
|
|
|
126
131
|
## Runtime
|
|
127
132
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
133
|
+
# Run native HTTP I/O on current-thread shards instead of Tokio's shared
|
|
134
|
+
# worker pool, reducing scheduler contention on very fast handlers.
|
|
135
|
+
# io_shards true
|
|
136
|
+
|
|
137
|
+
# I/O shard count. Default with io_shards: half available CPUs.
|
|
138
|
+
# io_threads 6
|
|
139
|
+
|
|
140
|
+
# Threads for the Tokio multi-thread runtime. Default: one per available CPU.
|
|
130
141
|
# tokio_threads 4
|
|
131
142
|
|
|
132
143
|
## Control plane
|
data/lib/kino/version.rb
CHANGED
data/lib/kino/worker.rb
CHANGED
|
@@ -20,12 +20,16 @@ module Kino
|
|
|
20
20
|
|
|
21
21
|
def run(server_id, worker_id, app, batch_size = 1, hooks = nil)
|
|
22
22
|
fire_after_worker_boot(hooks, worker_id)
|
|
23
|
+
# One resolution of (server, worker) into a native handle; every take
|
|
24
|
+
# after this skips the registry lookup. nil = server already gone.
|
|
25
|
+
worker = Native.worker(server_id, worker_id)
|
|
26
|
+
return unless worker
|
|
23
27
|
if batch_size <= 1
|
|
24
|
-
env =
|
|
25
|
-
env = handle_one(env,
|
|
28
|
+
env = worker.take_one
|
|
29
|
+
env = handle_one(env, worker, app, hooks) while env
|
|
26
30
|
else
|
|
27
|
-
batch =
|
|
28
|
-
batch = process(batch,
|
|
31
|
+
batch = worker.take_batch(batch_size)
|
|
32
|
+
batch = process(batch, worker, app, batch_size, hooks) while batch
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
|
|
@@ -35,22 +39,21 @@ module Kino
|
|
|
35
39
|
NOT_FUSED = Object.new.freeze
|
|
36
40
|
|
|
37
41
|
# Handle one request; returns the next env (fused take) or nil.
|
|
38
|
-
def handle_one(env,
|
|
42
|
+
def handle_one(env, worker, app, hooks)
|
|
39
43
|
result = serve(env, app, hooks) do |request, status, headers, chunks|
|
|
40
|
-
request.respond_and_take_one(
|
|
44
|
+
request.respond_and_take_one(worker, status, headers, chunks)
|
|
41
45
|
end
|
|
42
|
-
result.equal?(NOT_FUSED) ?
|
|
46
|
+
result.equal?(NOT_FUSED) ? worker.take_one : result
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Handle every env in the batch; returns the next batch (the last
|
|
46
50
|
# simple response rides the fused respond_and_take) or nil on shutdown.
|
|
47
|
-
def process(batch,
|
|
51
|
+
def process(batch, worker, app, batch_size, hooks)
|
|
48
52
|
last = batch.size - 1
|
|
49
53
|
batch.each_with_index do |env, index|
|
|
50
54
|
result = serve(env, app, hooks) do |request, status, headers, chunks|
|
|
51
55
|
if index == last
|
|
52
|
-
request.respond_and_take(
|
|
53
|
-
status, headers, chunks)
|
|
56
|
+
request.respond_and_take(worker, batch_size, status, headers, chunks)
|
|
54
57
|
else
|
|
55
58
|
request.send_simple(status, headers, chunks)
|
|
56
59
|
NOT_FUSED
|
|
@@ -58,7 +61,7 @@ module Kino
|
|
|
58
61
|
end
|
|
59
62
|
return result if index == last && !result.equal?(NOT_FUSED)
|
|
60
63
|
end
|
|
61
|
-
|
|
64
|
+
worker.take_batch(batch_size)
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
# Run one request through the app. Complete bodies are yielded so the
|
data/sig/kino.rbs
CHANGED
|
@@ -126,8 +126,11 @@ module Kino
|
|
|
126
126
|
def after_request_complete: (?^(Hash[String, untyped], Integer) -> void handler) ?{ (Hash[String, untyped], Integer) -> void } -> untyped
|
|
127
127
|
def on_worker_exit: (?^(Integer, Exception?) -> void handler) ?{ (Integer, Exception?) -> void } -> untyped
|
|
128
128
|
def shutdown_timeout: (Numeric seconds) -> untyped
|
|
129
|
+
def io_shards: (?boolish enabled) -> untyped
|
|
130
|
+
def io_threads: (int? count) -> untyped
|
|
129
131
|
def tokio_threads: (int count) -> untyped
|
|
130
132
|
def tls: (cert: String, key: String) -> untyped
|
|
133
|
+
def http2: (?boolish enabled) -> untyped
|
|
131
134
|
def environment: (String | Symbol env) -> untyped
|
|
132
135
|
def pidfile: (String path) -> untyped
|
|
133
136
|
def control_bind: (String addr) -> String
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kino
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: 0.9.128
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: httpx
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.3'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.3'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: rackup
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -180,6 +194,7 @@ files:
|
|
|
180
194
|
- ext/kino/src/cpus.rs
|
|
181
195
|
- ext/kino/src/env_strings.rs
|
|
182
196
|
- ext/kino/src/gvl.rs
|
|
197
|
+
- ext/kino/src/io_shards.rs
|
|
183
198
|
- ext/kino/src/lib.rs
|
|
184
199
|
- ext/kino/src/listen.rs
|
|
185
200
|
- ext/kino/src/log.rs
|