pitchfork 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +3 -0
- data/docs/CONFIGURATION.md +32 -0
- data/docs/FORK_SAFETY.md +2 -1
- data/ext/pitchfork_http/common_field_optimization.h +8 -5
- data/ext/pitchfork_http/extconf.rb +3 -0
- data/ext/pitchfork_http/pitchfork_http.c +292 -468
- data/ext/pitchfork_http/pitchfork_http.rl +32 -24
- data/lib/pitchfork/chunked.rb +6 -3
- data/lib/pitchfork/configurator.rb +1 -1
- data/lib/pitchfork/http_parser.rb +0 -1
- data/lib/pitchfork/http_response.rb +3 -1
- data/lib/pitchfork/http_server.rb +40 -20
- data/lib/pitchfork/listeners.rb +65 -0
- data/lib/pitchfork/socket_helper.rb +12 -2
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +0 -5
- data/pitchfork.gemspec +2 -1
- metadata +18 -4
- data/Gemfile.lock +0 -32
@@ -36,12 +36,6 @@ void init_pitchfork_memory_page(VALUE);
|
|
36
36
|
|
37
37
|
static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */
|
38
38
|
|
39
|
-
/* this is only intended for use with Rainbows! */
|
40
|
-
static VALUE set_maxhdrlen(VALUE self, VALUE len)
|
41
|
-
{
|
42
|
-
return UINT2NUM(MAX_HEADER_LEN = NUM2UINT(len));
|
43
|
-
}
|
44
|
-
|
45
39
|
/* keep this small for other servers (e.g. yahns) since every client has one */
|
46
40
|
struct http_parser {
|
47
41
|
int cs; /* Ragel internal state */
|
@@ -95,7 +89,7 @@ static inline unsigned int ulong2uint(unsigned long n)
|
|
95
89
|
#define LEN(AT, FPC) (ulong2uint(FPC - buffer) - hp->AT)
|
96
90
|
#define MARK(M,FPC) (hp->M = ulong2uint((FPC) - buffer))
|
97
91
|
#define PTR_TO(F) (buffer + hp->F)
|
98
|
-
#define STR_NEW(M,FPC)
|
92
|
+
#define STR_NEW(M,FPC) str_new(PTR_TO(M), LEN(M, FPC))
|
99
93
|
#define STRIPPED_STR_NEW(M,FPC) stripped_str_new(PTR_TO(M), LEN(M, FPC))
|
100
94
|
|
101
95
|
#define HP_FL_TEST(hp,fl) ((hp)->flags & (UH_FL_##fl))
|
@@ -108,13 +102,29 @@ static int is_lws(char c)
|
|
108
102
|
return (c == ' ' || c == '\t');
|
109
103
|
}
|
110
104
|
|
111
|
-
static VALUE
|
105
|
+
static inline VALUE str_new(const char *str, long len)
|
106
|
+
{
|
107
|
+
VALUE rb_str = rb_str_new(str, len);
|
108
|
+
/* The Rack spec states:
|
109
|
+
> If the string values for CGI keys contain non-ASCII characters, they should use ASCII-8BIT encoding.
|
110
|
+
If they are ASCII, only the server is free to encode them as it wishes.
|
111
|
+
We chose to encode them as UTF-8 as any reasonable application would expect that today, and having the
|
112
|
+
same encoding as literal strings makes for slightly faster comparisons.
|
113
|
+
We'd like to also encode other strings that would be valid UTF-8 into UTF-8, but that would
|
114
|
+
violate the spec, so we leave them in BINARY aka ASCII-8BIT. */
|
115
|
+
if (rb_enc_str_asciionly_p(rb_str)) {
|
116
|
+
RB_ENCODING_SET_INLINED(rb_str, rb_utf8_encindex());
|
117
|
+
}
|
118
|
+
return rb_str;
|
119
|
+
}
|
120
|
+
|
121
|
+
static inline VALUE stripped_str_new(const char *str, long len)
|
112
122
|
{
|
113
123
|
long end;
|
114
124
|
|
115
125
|
for (end = len - 1; end >= 0 && is_lws(str[end]); end--);
|
116
126
|
|
117
|
-
return
|
127
|
+
return str_new(str, end + 1);
|
118
128
|
}
|
119
129
|
|
120
130
|
/*
|
@@ -330,24 +340,18 @@ static void write_value(VALUE self, struct http_parser *hp,
|
|
330
340
|
}
|
331
341
|
action host { rb_hash_aset(hp->env, g_http_host, STR_NEW(mark, fpc)); }
|
332
342
|
action request_uri {
|
333
|
-
VALUE str;
|
334
|
-
|
335
343
|
VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), REQUEST_URI);
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
344
|
+
rb_hash_aset(hp->env, g_request_uri, STR_NEW(mark, fpc));
|
345
|
+
}
|
346
|
+
action fragment {
|
347
|
+
VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), FRAGMENT);
|
348
|
+
VALUE str = rb_hash_aset(hp->env, g_fragment, STR_NEW(mark, fpc));
|
341
349
|
if (STR_CSTR_EQ(str, "*")) {
|
342
|
-
str = rb_str_new(
|
350
|
+
VALUE str = rb_str_new("*", 1);
|
343
351
|
rb_hash_aset(hp->env, g_path_info, str);
|
344
352
|
rb_hash_aset(hp->env, g_request_path, str);
|
345
353
|
}
|
346
354
|
}
|
347
|
-
action fragment {
|
348
|
-
VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), FRAGMENT);
|
349
|
-
rb_hash_aset(hp->env, g_fragment, STR_NEW(mark, fpc));
|
350
|
-
}
|
351
355
|
action start_query {MARK(start.query, fpc); }
|
352
356
|
action query_string {
|
353
357
|
VALIDATE_MAX_URI_LENGTH(LEN(start.query, fpc), QUERY_STRING);
|
@@ -612,6 +616,12 @@ static VALUE HttpParser_alloc(VALUE klass)
|
|
612
616
|
return TypedData_Make_Struct(klass, struct http_parser, &hp_type, hp);
|
613
617
|
}
|
614
618
|
|
619
|
+
#ifndef HAVE_RB_HASH_NEW_CAPA
|
620
|
+
static inline VALUE rb_hash_new_capa(long capa) {
|
621
|
+
return rb_hash_new();
|
622
|
+
}
|
623
|
+
#endif
|
624
|
+
|
615
625
|
/**
|
616
626
|
* call-seq:
|
617
627
|
* parser.new => parser
|
@@ -624,7 +634,7 @@ static VALUE HttpParser_init(VALUE self)
|
|
624
634
|
|
625
635
|
http_parser_init(hp);
|
626
636
|
RB_OBJ_WRITE(self, &hp->buf, rb_str_new(NULL, 0));
|
627
|
-
RB_OBJ_WRITE(self, &hp->env,
|
637
|
+
RB_OBJ_WRITE(self, &hp->env, rb_hash_new_capa(32)); // Even the simplest request will have 10 keys
|
628
638
|
|
629
639
|
return self;
|
630
640
|
}
|
@@ -1010,8 +1020,6 @@ RUBY_FUNC_EXPORTED void Init_pitchfork_http(void)
|
|
1010
1020
|
*/
|
1011
1021
|
rb_define_const(cHttpParser, "LENGTH_MAX", OFFT2NUM(UH_OFF_T_MAX));
|
1012
1022
|
|
1013
|
-
rb_define_singleton_method(cHttpParser, "max_header_len=", set_maxhdrlen, 1);
|
1014
|
-
|
1015
1023
|
init_common_fields();
|
1016
1024
|
SET_GLOBAL(g_http_host, "HOST");
|
1017
1025
|
SET_GLOBAL(g_http_trailer, "TRAILER");
|
data/lib/pitchfork/chunked.rb
CHANGED
@@ -104,12 +104,15 @@ module Pitchfork
|
|
104
104
|
def call(env)
|
105
105
|
status, headers, body = response = @app.call(env)
|
106
106
|
|
107
|
-
if
|
107
|
+
if !env.key?('rack.hijack_io') && # full highjack
|
108
|
+
!headers['rack.hijack'] && # partial hijack
|
109
|
+
body.respond_to?(:each) && # body must be enumerable (i.e. non-streaming)
|
110
|
+
chunkable_version?(env[Rack::SERVER_PROTOCOL]) &&
|
108
111
|
!STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
|
109
112
|
!headers[Rack::CONTENT_LENGTH] &&
|
110
|
-
!headers[
|
113
|
+
!headers["transfer-encoding"]
|
111
114
|
|
112
|
-
headers[
|
115
|
+
headers["transfer-encoding"] = 'chunked'
|
113
116
|
if headers['trailer']
|
114
117
|
response[2] = TrailerBody.new(body)
|
115
118
|
else
|
@@ -230,7 +230,7 @@ module Pitchfork
|
|
230
230
|
def listen(address, options = {})
|
231
231
|
address = expand_addr(address)
|
232
232
|
if String === address
|
233
|
-
[ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key|
|
233
|
+
[ :umask, :backlog, :sndbuf, :rcvbuf, :tries, :queues, :queues_per_worker].each do |key|
|
234
234
|
value = options[key] or next
|
235
235
|
Integer === value or
|
236
236
|
raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'pitchfork/pitchfork_http'
|
5
|
+
require 'pitchfork/listeners'
|
5
6
|
require 'pitchfork/flock'
|
6
7
|
require 'pitchfork/soft_timeout'
|
7
8
|
require 'pitchfork/shared_memory'
|
@@ -79,8 +80,7 @@ module Pitchfork
|
|
79
80
|
attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
80
81
|
:before_fork, :after_worker_fork, :after_mold_fork, :before_service_worker_ready, :before_service_worker_exit,
|
81
82
|
:listener_opts, :children,
|
82
|
-
:orig_app, :config, :ready_pipe,
|
83
|
-
:default_middleware, :early_hints
|
83
|
+
:orig_app, :config, :ready_pipe, :early_hints
|
84
84
|
attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
|
85
85
|
:refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready
|
86
86
|
|
@@ -91,7 +91,7 @@ module Pitchfork
|
|
91
91
|
# all bound listener sockets
|
92
92
|
# note: this is public used by raindrops, but not recommended for use
|
93
93
|
# in new projects
|
94
|
-
LISTENERS =
|
94
|
+
LISTENERS = Listeners.new
|
95
95
|
|
96
96
|
NOOP = '.'
|
97
97
|
|
@@ -196,6 +196,10 @@ module Pitchfork
|
|
196
196
|
# replaces current listener set with +listeners+. This will
|
197
197
|
# close the socket if it will not exist in the new listener set
|
198
198
|
def listeners=(listeners)
|
199
|
+
unless LISTENERS.empty?
|
200
|
+
raise "Listeners can only be initialized once"
|
201
|
+
end
|
202
|
+
|
199
203
|
cur_names, dead_names = [], []
|
200
204
|
listener_names.each do |name|
|
201
205
|
if name.start_with?('/')
|
@@ -205,19 +209,7 @@ module Pitchfork
|
|
205
209
|
cur_names << name
|
206
210
|
end
|
207
211
|
end
|
208
|
-
|
209
|
-
dead_names.concat(cur_names - set_names).uniq!
|
210
|
-
|
211
|
-
LISTENERS.delete_if do |io|
|
212
|
-
if dead_names.include?(sock_name(io))
|
213
|
-
(io.close rescue nil).nil? # true
|
214
|
-
else
|
215
|
-
set_server_sockopt(io, listener_opts[sock_name(io)])
|
216
|
-
false
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
(set_names - cur_names).each { |addr| listen(addr) }
|
212
|
+
listener_names(listeners).each { |addr| listen(addr) }
|
221
213
|
end
|
222
214
|
|
223
215
|
def logger=(obj)
|
@@ -233,12 +225,16 @@ module Pitchfork
|
|
233
225
|
# A negative value for +:tries+ indicates the listen will be
|
234
226
|
# retried indefinitely, this is useful when workers belonging to
|
235
227
|
# different masters are spawned during a transparent upgrade.
|
236
|
-
def listen(address, opt =
|
228
|
+
def listen(address, opt = listener_opts[address] || {})
|
237
229
|
address = config.expand_addr(address)
|
238
230
|
return if String === address && listener_names.include?(address)
|
239
231
|
|
232
|
+
opt = opt.dup
|
240
233
|
delay = opt[:delay] || 0.5
|
241
234
|
tries = opt[:tries] || 5
|
235
|
+
queues = opt[:queues] ||= 1
|
236
|
+
opt[:reuseport] = true if queues > 1
|
237
|
+
|
242
238
|
begin
|
243
239
|
io = bind_listen(address, opt)
|
244
240
|
unless TCPServer === io || UNIXServer === io
|
@@ -247,7 +243,7 @@ module Pitchfork
|
|
247
243
|
end
|
248
244
|
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
249
245
|
Info.keep_io(io)
|
250
|
-
LISTENERS << io
|
246
|
+
LISTENERS << io unless queues > 1
|
251
247
|
io
|
252
248
|
rescue Errno::EADDRINUSE => err
|
253
249
|
logger.error "adding listener failed addr=#{address} (in use)"
|
@@ -261,6 +257,29 @@ module Pitchfork
|
|
261
257
|
logger.fatal "error adding listener addr=#{address}"
|
262
258
|
raise err
|
263
259
|
end
|
260
|
+
|
261
|
+
if queues > 1
|
262
|
+
ios = [io]
|
263
|
+
|
264
|
+
(queues - 1).times do
|
265
|
+
io = bind_listen(address, opt)
|
266
|
+
unless TCPServer === io || UNIXServer === io
|
267
|
+
io.autoclose = false
|
268
|
+
io = server_cast(io)
|
269
|
+
end
|
270
|
+
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno} (SO_REUSEPORT)"
|
271
|
+
Info.keep_io(io)
|
272
|
+
ios << io
|
273
|
+
rescue => err
|
274
|
+
logger.fatal "error adding listener addr=#{address}"
|
275
|
+
raise err
|
276
|
+
end
|
277
|
+
|
278
|
+
io = Listeners::Group.new(ios, queues_per_worker: opt[:queues_per_worker] || queues - 1)
|
279
|
+
LISTENERS << io
|
280
|
+
end
|
281
|
+
|
282
|
+
io
|
264
283
|
end
|
265
284
|
|
266
285
|
# monitors children and receives signals forever
|
@@ -371,7 +390,8 @@ module Pitchfork
|
|
371
390
|
@respawn = false
|
372
391
|
SharedMemory.shutting_down!
|
373
392
|
wait_for_pending_workers
|
374
|
-
|
393
|
+
LISTENERS.each(&:close).clear
|
394
|
+
|
375
395
|
limit = Pitchfork.time_now + timeout
|
376
396
|
until @children.empty? || Pitchfork.time_now > limit
|
377
397
|
if graceful
|
@@ -897,7 +917,7 @@ module Pitchfork
|
|
897
917
|
|
898
918
|
@config = nil
|
899
919
|
@listener_opts = @orig_app = nil
|
900
|
-
readers = LISTENERS.
|
920
|
+
readers = LISTENERS.for_worker(worker.nr)
|
901
921
|
readers << worker
|
902
922
|
trap(:QUIT) { nuke_listeners!(readers) }
|
903
923
|
trap(:TERM) { nuke_listeners!(readers) }
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
|
5
|
+
class Listeners
|
6
|
+
class Group
|
7
|
+
def initialize(listeners, queues_per_worker:)
|
8
|
+
@listeners = listeners
|
9
|
+
@queues_per_worker = queues_per_worker
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
@listeners.each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def for_worker(nr)
|
17
|
+
index = nr % @listeners.size
|
18
|
+
|
19
|
+
listeners = @listeners.slice(index..-1) + @listeners.slice(0...index)
|
20
|
+
listeners.take(@queues_per_worker)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
include Enumerable
|
25
|
+
|
26
|
+
def initialize(listeners = [])
|
27
|
+
@listeners = listeners
|
28
|
+
end
|
29
|
+
|
30
|
+
def for_worker(nr)
|
31
|
+
ios = []
|
32
|
+
@listeners.each do |listener|
|
33
|
+
if listener.is_a?(Group)
|
34
|
+
ios += listener.for_worker(nr)
|
35
|
+
else
|
36
|
+
ios << listener
|
37
|
+
end
|
38
|
+
end
|
39
|
+
ios
|
40
|
+
end
|
41
|
+
|
42
|
+
def each(&block)
|
43
|
+
@listeners.each do |listener|
|
44
|
+
if listener.is_a?(Group)
|
45
|
+
listener.each(&block)
|
46
|
+
else
|
47
|
+
yield listener
|
48
|
+
end
|
49
|
+
end
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def clear
|
54
|
+
@listeners.clear
|
55
|
+
end
|
56
|
+
|
57
|
+
def <<(listener)
|
58
|
+
@listeners << listener
|
59
|
+
end
|
60
|
+
|
61
|
+
def empty?
|
62
|
+
@listeners.empty?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -26,6 +26,8 @@ module Pitchfork
|
|
26
26
|
:tcp_nodelay => true,
|
27
27
|
}
|
28
28
|
|
29
|
+
private
|
30
|
+
|
29
31
|
# configure platform-specific options (only tested on Linux 2.6 so far)
|
30
32
|
def accf_arg(af_name)
|
31
33
|
[ af_name, nil ].pack('a16a240')
|
@@ -64,7 +66,7 @@ module Pitchfork
|
|
64
66
|
elsif respond_to?(:accf_arg)
|
65
67
|
name = opt[:accept_filter]
|
66
68
|
name = DEFAULTS[:accept_filter] if name.nil?
|
67
|
-
sock.listen(opt
|
69
|
+
sock.listen(compute_backlog(opt))
|
68
70
|
got = (sock.getsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER) rescue nil).to_s
|
69
71
|
arg = accf_arg(name)
|
70
72
|
begin
|
@@ -89,11 +91,19 @@ module Pitchfork
|
|
89
91
|
sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf
|
90
92
|
log_buffer_sizes(sock, " after: ")
|
91
93
|
end
|
92
|
-
sock.listen(opt
|
94
|
+
sock.listen(compute_backlog(opt))
|
93
95
|
rescue => e
|
94
96
|
Pitchfork.log_error(logger, "#{sock_name(sock)} #{opt.inspect}", e)
|
95
97
|
end
|
96
98
|
|
99
|
+
def compute_backlog(opt)
|
100
|
+
backlog = opt[:backlog]
|
101
|
+
if backlog > 0 && opt[:queues]
|
102
|
+
return backlog / opt[:queues]
|
103
|
+
end
|
104
|
+
backlog
|
105
|
+
end
|
106
|
+
|
97
107
|
def log_buffer_sizes(sock, pfx = '')
|
98
108
|
rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
|
99
109
|
sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
data/pitchfork.gemspec
CHANGED
@@ -19,7 +19,8 @@ Gem::Specification.new do |s|
|
|
19
19
|
|
20
20
|
s.required_ruby_version = ">= 2.5.0"
|
21
21
|
|
22
|
-
s.add_dependency(
|
22
|
+
s.add_dependency('rack', '>= 2.0')
|
23
|
+
s.add_dependency('logger')
|
23
24
|
|
24
25
|
# Note: To avoid ambiguity, we intentionally avoid the SPDX-compatible
|
25
26
|
# 'Ruby' here since Ruby 1.9.3 switched to BSD-2-Clause, but we
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pitchfork
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
description: |-
|
28
42
|
`pitchfork` is a preforking HTTP server for Rack applications designed
|
29
43
|
to minimize memory usage by maximizing Copy-on-Write performance.
|
@@ -45,7 +59,6 @@ files:
|
|
45
59
|
- COPYING
|
46
60
|
- Dockerfile
|
47
61
|
- Gemfile
|
48
|
-
- Gemfile.lock
|
49
62
|
- LICENSE
|
50
63
|
- README.md
|
51
64
|
- Rakefile
|
@@ -94,6 +107,7 @@ files:
|
|
94
107
|
- lib/pitchfork/http_server.rb
|
95
108
|
- lib/pitchfork/info.rb
|
96
109
|
- lib/pitchfork/launcher.rb
|
110
|
+
- lib/pitchfork/listeners.rb
|
97
111
|
- lib/pitchfork/mem_info.rb
|
98
112
|
- lib/pitchfork/message.rb
|
99
113
|
- lib/pitchfork/refork_condition.rb
|
@@ -127,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
141
|
- !ruby/object:Gem::Version
|
128
142
|
version: '0'
|
129
143
|
requirements: []
|
130
|
-
rubygems_version: 3.5.
|
144
|
+
rubygems_version: 3.5.11
|
131
145
|
signing_key:
|
132
146
|
specification_version: 4
|
133
147
|
summary: Rack HTTP server for fast clients and Unix
|
data/Gemfile.lock
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
pitchfork (0.14.0)
|
5
|
-
rack (>= 2.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
minitest (5.22.2)
|
11
|
-
nio4r (2.7.0)
|
12
|
-
puma (6.4.2)
|
13
|
-
nio4r (~> 2.0)
|
14
|
-
rack (3.0.11)
|
15
|
-
rake (13.0.6)
|
16
|
-
rake-compiler (1.2.1)
|
17
|
-
rake
|
18
|
-
|
19
|
-
PLATFORMS
|
20
|
-
aarch64-linux
|
21
|
-
arm64-darwin
|
22
|
-
x86_64-linux
|
23
|
-
|
24
|
-
DEPENDENCIES
|
25
|
-
minitest
|
26
|
-
pitchfork!
|
27
|
-
puma
|
28
|
-
rake
|
29
|
-
rake-compiler
|
30
|
-
|
31
|
-
BUNDLED WITH
|
32
|
-
2.3.27
|