about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--.olddoc.yml11
-rw-r--r--CONTRIBUTORS8
-rw-r--r--DESIGN4
-rw-r--r--Documentation/unicorn.12
-rwxr-xr-xGIT-VERSION-GEN2
-rw-r--r--GNUmakefile15
-rw-r--r--HACKING22
-rw-r--r--ISSUES45
-rw-r--r--README63
-rw-r--r--Rakefile1
-rw-r--r--Sandbox2
-rw-r--r--TODO4
-rwxr-xr-xbin/unicorn1
-rwxr-xr-xbin/unicorn_rails1
-rw-r--r--examples/big_app_gc.rb1
-rw-r--r--examples/echo.ru2
-rw-r--r--examples/logger_mp_safe.rb1
-rw-r--r--examples/unicorn.conf.minimal.rb1
-rw-r--r--examples/unicorn.conf.rb1
-rw-r--r--ext/unicorn_http/c_util.h18
-rw-r--r--ext/unicorn_http/common_field_optimization.h1
-rw-r--r--ext/unicorn_http/epollexclusive.h128
-rw-r--r--ext/unicorn_http/ext_help.h24
-rw-r--r--ext/unicorn_http/extconf.rb16
-rw-r--r--ext/unicorn_http/global_variables.h2
-rw-r--r--ext/unicorn_http/httpdate.c21
-rw-r--r--ext/unicorn_http/unicorn_http.rl39
-rw-r--r--ext/unicorn_http/unicorn_http_common.rl2
-rw-r--r--lib/unicorn.rb11
-rw-r--r--lib/unicorn/app/old_rails.rb1
-rw-r--r--lib/unicorn/app/old_rails/static.rb1
-rw-r--r--lib/unicorn/cgi_wrapper.rb1
-rw-r--r--lib/unicorn/configurator.rb8
-rw-r--r--lib/unicorn/const.rb1
-rw-r--r--lib/unicorn/http_request.rb20
-rw-r--r--lib/unicorn/http_response.rb47
-rw-r--r--lib/unicorn/http_server.rb144
-rw-r--r--lib/unicorn/launcher.rb1
-rw-r--r--lib/unicorn/oob_gc.rb11
-rw-r--r--lib/unicorn/preread_input.rb1
-rw-r--r--lib/unicorn/select_waiter.rb7
-rw-r--r--lib/unicorn/socket_helper.rb37
-rw-r--r--lib/unicorn/stream_input.rb21
-rw-r--r--lib/unicorn/tee_input.rb1
-rw-r--r--lib/unicorn/tmpio.rb1
-rw-r--r--lib/unicorn/util.rb1
-rw-r--r--lib/unicorn/worker.rb11
-rw-r--r--setup.rb1
-rw-r--r--t/README21
-rw-r--r--t/active-unix-socket.t117
-rw-r--r--t/back-out-of-upgrade.t44
-rwxr-xr-xt/bin/content-md5-put36
-rwxr-xr-xt/bin/sha1sum.rb17
-rw-r--r--t/broken-app.ru1
-rw-r--r--t/client_body_buffer_size.ru (renamed from t/t0116.ru)3
-rw-r--r--t/client_body_buffer_size.t80
-rw-r--r--t/detach.ru1
-rw-r--r--t/env.ru1
-rw-r--r--t/fails-rack-lint.ru1
-rw-r--r--t/heartbeat-timeout.ru5
-rw-r--r--t/heartbeat-timeout.t62
-rw-r--r--t/hijack.ru55
-rw-r--r--t/integration.ru116
-rw-r--r--t/integration.t357
-rw-r--r--t/lib.perl309
-rw-r--r--t/listener_names.ru1
-rw-r--r--t/oob_gc.ru4
-rw-r--r--t/oob_gc_path.ru4
-rw-r--r--t/pid.ru1
-rw-r--r--t/preread_input.ru22
-rw-r--r--t/rack-input-tests.ru21
-rw-r--r--t/reload-bad-config.t54
-rw-r--r--t/reopen-logs.ru (renamed from t/t0006.ru)1
-rw-r--r--t/reopen-logs.t39
-rwxr-xr-xt/t0000-http-basic.sh50
-rwxr-xr-xt/t0001-reload-bad-config.sh53
-rwxr-xr-xt/t0002-config-conflict.sh49
-rwxr-xr-xt/t0002-parser-error.sh94
-rwxr-xr-xt/t0003-working_directory.sh51
-rwxr-xr-xt/t0004-heartbeat-timeout.sh69
-rwxr-xr-xt/t0004-working_directory_broken.sh24
-rwxr-xr-xt/t0005-working_directory_app.rb.sh40
-rwxr-xr-xt/t0006-reopen-logs.sh83
-rwxr-xr-xt/t0007-working_directory_no_embed_cli.sh44
-rwxr-xr-xt/t0008-back_out_of_upgrade.sh110
-rwxr-xr-xt/t0009-winch_ttin.sh59
-rwxr-xr-xt/t0011-active-unix-socket.sh79
-rw-r--r--t/t0013.ru1
-rw-r--r--t/t0014.ru1
-rwxr-xr-xt/t0018-write-on-close.sh23
-rwxr-xr-xt/t0019-max_header_len.sh49
-rwxr-xr-xt/t0100-rack-input-tests.sh124
-rwxr-xr-xt/t0116-client_body_buffer_size.sh80
-rwxr-xr-xt/t0200-rack-hijack.sh51
-rw-r--r--t/t0300-no-default-middleware.sh2
-rw-r--r--t/t0301.ru5
-rwxr-xr-xt/t9000-preread-input.sh48
-rw-r--r--t/test-lib.sh7
-rw-r--r--t/winch_ttin.t67
-rw-r--r--t/working_directory.t94
-rw-r--r--t/write-on-close.ru11
-rwxr-xr-xtest/aggregate.rb1
-rw-r--r--test/benchmark/dd.ru1
-rw-r--r--test/benchmark/ddstream.ru1
-rw-r--r--test/benchmark/readinput.ru1
-rw-r--r--test/benchmark/stack.ru1
-rw-r--r--test/exec/test_exec.rb111
-rw-r--r--test/test_helper.rb21
-rw-r--r--test/unit/test_ccc.rb10
-rw-r--r--test/unit/test_configurator.rb1
-rw-r--r--test/unit/test_droplet.rb1
-rw-r--r--test/unit/test_http_parser.rb1
-rw-r--r--test/unit/test_http_parser_ng.rb1
-rw-r--r--test/unit/test_request.rb50
-rw-r--r--test/unit/test_response.rb111
-rw-r--r--test/unit/test_server.rb119
-rw-r--r--test/unit/test_signals.rb27
-rw-r--r--test/unit/test_socket_helper.rb75
-rw-r--r--test/unit/test_stream_input.rb28
-rw-r--r--test/unit/test_tee_input.rb22
-rw-r--r--test/unit/test_upload.rb301
-rw-r--r--test/unit/test_util.rb8
-rw-r--r--test/unit/test_waiter.rb35
-rw-r--r--unicorn.gemspec8
124 files changed, 2053 insertions, 2388 deletions
diff --git a/.olddoc.yml b/.olddoc.yml
index 0609bdb..9780a83 100644
--- a/.olddoc.yml
+++ b/.olddoc.yml
@@ -3,7 +3,7 @@ cgit_url: https://yhbt.net/unicorn.git
 rdoc_url: https://yhbt.net/unicorn/
 ml_url:
 - https://yhbt.net/unicorn-public/
-- http://ou63pmih66umazou.onion/unicorn-public/
+- http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/
 merge_html:
   unicorn_1: Documentation/unicorn.1.html
   unicorn_rails_1: Documentation/unicorn_rails.1.html
@@ -13,10 +13,13 @@ noindex:
 - TODO
 - unicorn_rails_1
 public_email: unicorn-public@yhbt.net
+imap_url:
+- imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0
+- imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0
 nntp_url:
-- nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
-- nntp://ou63pmih66umazou.onion/inbox.comp.lang.ruby.unicorn
+- nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
+- nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn
 - nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
 source_code:
 - git clone https://yhbt.net/unicorn.git
-- torsocks git clone http://ou63pmih66umazou.onion/unicorn.git
+- torsocks git clone http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn.git
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index bda399b..9991fdc 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -1,5 +1,9 @@
-Unicorn developers (let us know if we forgot you):
-* Eric Wong (BDFL, BOFH)
+Unicorn developers (let us know if we forgot you, ...or if you no longer wish
+to be associated with the doofus running this disaster :P):
+* Eric Wong (Bozo Doofus For Life, Bastard Operator From Hell)
+
+There's numerous contributors over email the years, all of our mail
+is archived @ https://yhbt.net/unicorn-public/
 * Suraj N. Kurapati
 * Andrey Stikheev
 * Wayne Larsen
diff --git a/DESIGN b/DESIGN
index 46d7923..0bac24f 100644
--- a/DESIGN
+++ b/DESIGN
@@ -1,5 +1,9 @@
 == Design
 
+Unicorn was designed to support poorly-written codebases back in 2008.
+Its unfortunate popularity has only proliferated the existence of
+poorly-written code ever since...
+
 * Simplicity: Unicorn is a traditional UNIX prefork web server.
   No threads are used at all, this makes applications easier to debug
   and fix.  When your application goes awry, a BOFH can just
diff --git a/Documentation/unicorn.1 b/Documentation/unicorn.1
index d76d40f..b2c5e70 100644
--- a/Documentation/unicorn.1
+++ b/Documentation/unicorn.1
@@ -176,7 +176,7 @@ As of Unicorn 0.94.0, RACK_ENV is exported as a process\-wide environment
 variable as well.  While not current a part of the Rack specification as
 of Rack 1.0.1, this has become a de facto standard in the Rack world.
 .PP
-Note the Rack::ContentLength and Rack::Chunked middlewares are also
+Note the Rack::ContentLength middleware is also
 loaded by "deployment" and "development", but no other values of
 RACK_ENV.  If needed, they must be individually specified in the
 RACKUP_FILE, some frameworks do not require them.
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 19d4a64..d11b1e5 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-DEF_VER = "v5.6.0"
+DEF_VER = "v6.1.0"
 CONSTANT = "Unicorn::Const::UNICORN_VERSION"
 RVF = "lib/unicorn/version.rb"
 GVF = "GIT-VERSION-FILE"
diff --git a/GNUmakefile b/GNUmakefile
index d80e608..70e7e10 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -11,6 +11,7 @@ RSYNC = rsync
 OLDDOC = olddoc
 RDOC = rdoc
 INSTALL = install
+PROVE = prove
 
 GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
         @./GIT-VERSION-GEN
@@ -42,13 +43,13 @@ ext_h := $(wildcard $(ext)/*/*.h $(ext)/*.h)
 ext_src := $(sort $(wildcard $(ext)/*.c) $(ext_h) $(ext)/unicorn_http.c)
 ext_pfx_src := $(addprefix $(ext_pfx)/,$(ext_src))
 ext_dir := $(ext_pfx)/$(ext)
-$(ext)/extconf.rb: $(wildcard $(ext)/*.h)
+$(ext)/extconf.rb:
         @>>$@
 $(ext_dir) $(tmp_bin) man/man1 doc/man1 pkg t/trash:
         @mkdir -p $@
 $(ext_pfx)/$(ext)/%: $(ext)/% | $(ext_dir)
         $(INSTALL) -m 644 $< $@
-$(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb $(ext_h) | $(ext_dir)
+$(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb | $(ext_dir)
         $(RM) -f $(@D)/*.o
         cd $(@D) && $(RUBY) $(CURDIR)/$(ext)/extconf.rb $(EXTCONF_ARGS)
 ext_sfx := _ext.$(DLEXT)
@@ -86,7 +87,7 @@ $(tmp_bin)/%: bin/% | $(tmp_bin)
 bins: $(tmp_bins)
 
 t_log := $(T_log) $(T_n_log)
-test: $(T) $(T_n)
+test: $(T) $(T_n) test-prove
         @cat $(t_log) | $(MRI) test/aggregate.rb
         @$(RM) $(t_log)
 
@@ -125,7 +126,7 @@ $(T_sh): dep $(test_prereq) t/random_blob t/trash/.gitignore
 t/trash/.gitignore : | t/trash
         echo '*' >$@
 
-dependencies := socat curl
+dependencies := curl
 deps := $(addprefix t/.dep+,$(dependencies))
 $(deps): dep_bin = $(lastword $(subst +, ,$@))
 $(deps):
@@ -141,6 +142,9 @@ t/random_blob:
 
 test-integration: $(T_sh)
 
+test-prove: t/random_blob
+        $(PROVE) -vw
+
 check: test-require test test-integration
 test-all: check
 
@@ -245,7 +249,8 @@ publish_doc:
         $(MAKE) doc
         $(MAKE) doc_gz
         chmod 644 $$(find doc -type f)
-        $(RSYNC) -av doc/ yhbt.net:/srv/yhbt/unicorn/
+        $(RSYNC) -av doc/ yhbt.net:/srv/yhbt/unicorn/ \
+                --exclude index.html* --exclude created.rid*
         git ls-files | xargs touch
 
 # Create gzip variants of the same timestamp as the original so nginx
diff --git a/HACKING b/HACKING
index 976bf92..777e75e 100644
--- a/HACKING
+++ b/HACKING
@@ -6,6 +6,8 @@ Like Mongrel, we use Ruby where it makes sense, and Ragel with C where
 it helps performance.  All of the code that actually runs your Rack
 application is written Ruby, Ragel or C.
 
+Ragel may be dropped in favor of a picohttpparser-based one in the future.
+
 As far as tests and documentation goes, we're not afraid to embrace Unix
 and use traditional Unix tools where they make sense and get the job
 done.
@@ -16,6 +18,9 @@ Tests are good, but slow tests make development slow, so we make tests
 faster (in parallel) with GNU make (instead of Rake) and avoiding
 RubyGems.
 
+New tests are written in Perl 5 and use TAP <https://testanything.org/>
+to ensure stability and immunity from Ruby incompatibilities.
+
 Users of GNU-based systems (such as GNU/Linux) usually have GNU make
 installed as "make" instead of "gmake".
 
@@ -50,9 +55,6 @@ programming experience will come in handy (or be learned) here.
 
 === Documentation
 
-Due to the lack of RDoc-to-manpage converters we know about, we're
-writing manpages in Markdown and converting to troff/HTML with Pandoc.
-
 Please wrap documentation at 72 characters-per-line or less (long URLs
 are exempt) so it is comfortably readable from terminals.
 
@@ -63,7 +65,7 @@ becomes unavailable.
 
 === Ruby/C Compatibility
 
-We target mainline Ruby 1.9.3 and later.  We need the Ruby
+We target C Ruby 2.5 and later.  We need the Ruby
 implementation to support fork, exec, pipe, UNIX signals, access to
 integer file descriptors and ability to use unlinked files.
 
@@ -72,10 +74,10 @@ supported by the versions of Ruby we target.
 
 === Ragel Compatibility
 
-We target the latest released version of Ragel and will update our code
-to keep up with new releases.  Packaged tarballs and gems include the
-generated source code so they will remain usable if compatibility is
-broken.
+We target the latest released version of Ragel in Debian and will update
+our code to keep up with new releases.  Packaged tarballs and gems
+include the generated source code so they will remain usable if
+compatibility is broken.
 
 == Contributing
 
@@ -102,10 +104,6 @@ don't email the git mailing list or maintainer with Unicorn patches :)
 
 == Building a Gem
 
-In order to build the gem, you must install the following components:
-
- * pandoc
-
 You can build the Unicorn gem with the following command:
 
   gmake gem
diff --git a/ISSUES b/ISSUES
index 4513ad5..d6c2a7a 100644
--- a/ISSUES
+++ b/ISSUES
@@ -20,6 +20,10 @@ can interoperate with any bug tracker which can Cc: us plain-text to
 mailto:unicorn-public@yhbt.net   This includes the Debian BTS
 at https://bugs.debian.org/unicorn and possibly others.
 
+unicorn is a server; it does not depend on graphics/audio.  Nobody
+communicating with us will ever be expected to go through the trouble
+of setting up graphics nor audio support.
+
 If your issue is of a sensitive nature or you're just shy in public,
 use anonymity tools such as Tor or Mixmaster; and rely on the public
 mail archives for responses.  Be sure to scrub sensitive log messages
@@ -28,6 +32,10 @@ and such.
 If you don't get a response within a few days, we may have forgotten
 about it so feel free to ask again.
 
+The project does not and will never endorse nor promote commercial
+services (including support).  The author of unicorn must never be
+allowed to profit off the damage it's done to the entire Ruby world.
+
 == Bugs in related projects
 
 unicorn is sometimes affected by bugs in its dependencies.  Bugs
@@ -61,7 +69,7 @@ There is a kernel.org Bugzilla instance, but it is ignored by most.
 
 Likewise for any rare glibc bugs we might encounter, we should Cc:
 mailto:libc-alpha@sourceware.org
-Unofficial archives are available at: https://public-inbox.org/libc-alpha/
+Archives are available at: https://inbox.sourceware.org/libc-alpha/
 Keep in mind glibc upstream does use Bugzilla for tracking bugs:
 https://sourceware.org/bugzilla/
 
@@ -73,27 +81,22 @@ document distributed with git) on guidelines for patch submission.
 
 == Contact Info
 
-* public: mailto:unicorn-public@yhbt.net
-* nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
-* nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
-* imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0
-* https://yhbt.net/unicorn-public/
-* http://ou63pmih66umazou.onion/unicorn-public/
-
-Mailing list subscription is optional, so Cc: all participants.
-
-You can follow along via NNTP or IMAP (read-only):
+Mail is publicly-archived, SMTP subscription is discouraged to avoid
+servers being a single-point-of-failure, so Cc: all participants.
 
-        nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
-        nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
-        imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0
-        imap://ou63pmih66umazou.onion/inbox.comp.lang.ruby.unicorn.0
+The HTTP(S) archives have links to per-thread Atom feeds and downloadable
+mboxes.  Read-only IMAP(S) folders, POP3, and NNTP(S) newsgroups are available.
 
-Or Atom feeds:
+* https://yhbt.net/unicorn-public/
+* http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/
+* imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0
+* imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0
+* nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
+* nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
+* https://yhbt.net/unicorn-public/_/text/help/#pop3
 
-        https://yhbt.net/unicorn-public/new.atom
-        http://ou63pmih66umazou.onion/unicorn-public/new.atom
+Full Atom feeds:
+* https://yhbt.net/unicorn-public/new.atom
+* http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/new.atom
 
-        The HTML archives at https://yhbt.net/unicorn-public/
-        also has links to per-thread Atom feeds and downloadable
-        mboxes.
+We only accept plain-text mail: mailto:unicorn-public@yhbt.net
diff --git a/README b/README
index 35a7388..b60ed00 100644
--- a/README
+++ b/README
@@ -1,10 +1,13 @@
 = unicorn: Rack HTTP server for fast clients and Unix
 
-unicorn is an HTTP server for Rack applications designed to only serve
-fast clients on low-latency, high-bandwidth connections and take
-advantage of features in Unix/Unix-like kernels.  Slow clients should
-only be served by placing a reverse proxy capable of fully buffering
-both the the request and response in between unicorn and slow clients.
+unicorn is an HTTP server for Rack applications that has done
+decades of damage to the entire Ruby ecosystem due to its ability
+to tolerate (and thus encourage) bad code.  It is only designed
+to handle fast clients on low-latency, high-bandwidth connections
+and take advantage of features in Unix/Unix-like kernels.
+Slow clients must only be served by placing a reverse proxy capable of
+fully buffering both the the request and response in between unicorn
+and slow clients.
 
 == Features
 
@@ -12,11 +15,10 @@ both the the request and response in between unicorn and slow clients.
   cut out everything that is better supported by the operating system,
   {nginx}[https://nginx.org/] or {Rack}[https://rack.github.io/].
 
-* Compatible with Ruby 1.9.3 and later.
-  unicorn 4.x remains supported for Ruby 1.8 users.
+* Compatible with Ruby 2.5 and later.
 
-* Process management: unicorn will reap and restart workers that
-  die from broken apps.  There is no need to manage multiple processes
+* Process management: unicorn reaps and restarts workers that die
+  from broken code.  There is no need to manage multiple processes
   or ports yourself.  unicorn can spawn and manage any number of
   worker processes you choose to scale to your backend.
 
@@ -58,7 +60,7 @@ both the the request and response in between unicorn and slow clients.
 
 == License
 
-unicorn is copyright 2009-2018 by all contributors (see logs in git).
+unicorn is copyright all contributors (see logs in git).
 It is based on Mongrel 1.1.5.
 Mongrel is copyright 2007 Zed A. Shaw and contributors.
 
@@ -80,8 +82,8 @@ You may install it via RubyGems on RubyGems.org:
 You can get the latest source via git from the following locations
 (these versions may not be stable):
 
-  https://yhbt.net/unicorn.git
-  https://repo.or.cz/unicorn.git (mirror)
+  git clone https://yhbt.net/unicorn.git
+  git clone https://repo.or.cz/unicorn.git # mirror
 
 You may browse the code from the web:
 
@@ -119,33 +121,44 @@ supported.  Run `unicorn -h` to see command-line options.
 == Disclaimer
 
 There is NO WARRANTY whatsoever if anything goes wrong, but
-{let us know}[link:ISSUES.html] and we'll try our best to fix it.
+{let us know}[link:ISSUES.html] and maybe someone can fix it.
+No commercial support will ever be provided by the amateur maintainer.
 
 unicorn is designed to only serve fast clients either on the local host
 or a fast LAN.  See the PHILOSOPHY and DESIGN documents for more details
 regarding this.
 
-Due to its ability to tolerate crashes and isolate clients, unicorn
-is unfortunately known to prolong the existence of bugs in applications
-and libraries which run on top of it.
+The use of unicorn in new deployments is STRONGLY DISCOURAGED due to the
+damage done to the entire Ruby ecosystem.  Its unintentional popularity
+set Ruby back decades in parallelism, concurrency and robustness since
+it prolongs and proliferates the existence of poorly-written code.
+
+unicorn hackers are NOT responsible for your supply chain security:
+read and understand it yourself or get someone you trust to audit it.
+Malicious commits and releases will be made if under duress.  The only
+defense you'll ever have is from reviewing the source code.
+
+No user or contributor will ever be expected to sacrifice their own
+security by running JavaScript or revealing any personal information.
 
 == Contact
 
 All feedback (bug reports, user/development dicussion, patches, pull
-requests) go to the mailing list/newsgroup.  See the ISSUES document for
-information on the {mailing list}[mailto:unicorn-public@yhbt.net].
+requests) go to the public mailbox.  See the ISSUES document for
+information on posting to mailto:unicorn-public@yhbt.net
 
-The mailing list is archived at https://yhbt.net/unicorn-public/
+Mirror-able mail archives are at https://yhbt.net/unicorn-public/
 
 Read-only NNTP access is available at:
-nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn and
+nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn and
 nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
 
-Read-only IMAP access is also avaialble at:
-imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0 and
-imap://ou63pmih66umazou.onion/inbox.comp.lang.ruby.unicorn.0
-AUTH=ANONYMOUS mechanism is supported, as is any username+password
-combination.
+Read-only IMAP access is also available at:
+imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0 and
+imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0
+
+Archives are also available over POP3, instructions at:
+https://yhbt.net/unicorn-public/_/text/help/#pop3
 
 For the latest on unicorn releases, you may also finger us at
 unicorn@yhbt.net or check our NEWS page (and subscribe to our Atom
diff --git a/Rakefile b/Rakefile
index 37569ce..fe1588b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # optional rake-compiler support in case somebody needs to cross compile
 begin
   mk = "ext/unicorn_http/Makefile"
diff --git a/Sandbox b/Sandbox
index 651e5cd..d770586 100644
--- a/Sandbox
+++ b/Sandbox
@@ -87,7 +87,7 @@ For now workarounds include doing one of the following:
 
 3. Explicitly setting RUBYLIB or $LOAD_PATH to include any gem path
    where the unicorn gem is installed
-   (e.g. /usr/lib/ruby/gems/1.9.3/gems/unicorn-VERSION/lib)
+   (e.g. /usr/lib/ruby/gems/3.0.0/gems/unicorn-VERSION/lib)
 
 === RUBYOPT pollution from SIGUSR2 upgrades
 
diff --git a/TODO b/TODO
index ebbccdc..a3b18fd 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1 @@
-* Documentation improvements
-
-* improve test suite
+* improve test suite (port to Perl 5 for stability and maintainability)
diff --git a/bin/unicorn b/bin/unicorn
index 00c8464..af8353c 100755
--- a/bin/unicorn
+++ b/bin/unicorn
@@ -1,5 +1,6 @@
 #!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'unicorn/launcher'
 require 'optparse'
 
diff --git a/bin/unicorn_rails b/bin/unicorn_rails
index 354c1df..374fd8e 100755
--- a/bin/unicorn_rails
+++ b/bin/unicorn_rails
@@ -1,5 +1,6 @@
 #!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'unicorn/launcher'
 require 'optparse'
 require 'fileutils'
diff --git a/examples/big_app_gc.rb b/examples/big_app_gc.rb
index c1bae10..0baea26 100644
--- a/examples/big_app_gc.rb
+++ b/examples/big_app_gc.rb
@@ -1,2 +1,3 @@
+# frozen_string_literal: false
 # see {Unicorn::OobGC}[https://yhbt.net/unicorn/Unicorn/OobGC.html]
 # Unicorn::OobGC was broken in Unicorn v3.3.1 - v3.6.1 and fixed in v3.6.2
diff --git a/examples/echo.ru b/examples/echo.ru
index 14908c5..453a5e6 100644
--- a/examples/echo.ru
+++ b/examples/echo.ru
@@ -1,4 +1,5 @@
 #\-E none
+# frozen_string_literal: false
 #
 # Example application that echoes read data back to the HTTP client.
 # This emulates the old echo protocol people used to run.
@@ -19,7 +20,6 @@ class EchoBody < Struct.new(:input)
 
 end
 
-use Rack::Chunked
 run lambda { |env|
   /\A100-continue\z/i =~ env['HTTP_EXPECT'] and return [100, {}, []]
   [ 200, { 'Content-Type' => 'application/octet-stream' },
diff --git a/examples/logger_mp_safe.rb b/examples/logger_mp_safe.rb
index 05ad3fa..f2c0500 100644
--- a/examples/logger_mp_safe.rb
+++ b/examples/logger_mp_safe.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # Multi-Processing-safe monkey patch for Logger
 #
 # This monkey patch fixes the case where "preload_app true" is used and
diff --git a/examples/unicorn.conf.minimal.rb b/examples/unicorn.conf.minimal.rb
index 46fd634..4f96ede 100644
--- a/examples/unicorn.conf.minimal.rb
+++ b/examples/unicorn.conf.minimal.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # Minimal sample configuration file for Unicorn (not Rack) when used
 # with daemonization (unicorn -D) started in your working directory.
 #
diff --git a/examples/unicorn.conf.rb b/examples/unicorn.conf.rb
index d90bdc4..5bae830 100644
--- a/examples/unicorn.conf.rb
+++ b/examples/unicorn.conf.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # Sample verbose configuration file for Unicorn (not Rack)
 #
 # This configuration file documents many features of Unicorn
diff --git a/ext/unicorn_http/c_util.h b/ext/unicorn_http/c_util.h
index ab1fc0e..5774615 100644
--- a/ext/unicorn_http/c_util.h
+++ b/ext/unicorn_http/c_util.h
@@ -8,23 +8,15 @@
 
 #include <unistd.h>
 #include <assert.h>
+#include <limits.h>
 
 #define MIN(a,b) (a < b ? a : b)
 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
 
-#ifndef SIZEOF_OFF_T
-#  define SIZEOF_OFF_T 4
-#  warning SIZEOF_OFF_T not defined, guessing 4.  Did you run extconf.rb?
-#endif
-
-#if SIZEOF_OFF_T == 4
-#  define UH_OFF_T_MAX 0x7fffffff
-#elif SIZEOF_OFF_T == 8
-#  if SIZEOF_LONG == 4
-#    define UH_OFF_T_MAX 0x7fffffffffffffffLL
-#  else
-#    define UH_OFF_T_MAX 0x7fffffffffffffff
-#  endif
+#if SIZEOF_OFF_T == SIZEOF_INT
+#  define UH_OFF_T_MAX INT_MAX
+#elif SIZEOF_OFF_T == SIZEOF_LONG_LONG
+#  define UH_OFF_T_MAX LLONG_MAX
 #else
 #  error off_t size unknown for this platform!
 #endif /* SIZEOF_OFF_T check */
diff --git a/ext/unicorn_http/common_field_optimization.h b/ext/unicorn_http/common_field_optimization.h
index 0659fc7..250e43e 100644
--- a/ext/unicorn_http/common_field_optimization.h
+++ b/ext/unicorn_http/common_field_optimization.h
@@ -83,7 +83,6 @@ static void init_common_fields(void)
   struct common_field *cf = common_http_fields;
   char tmp[64];
 
-  id_uminus = rb_intern("-@");
   memcpy(tmp, HTTP_PREFIX, HTTP_PREFIX_LEN);
 
   for(i = ARRAY_SIZE(common_http_fields); --i >= 0; cf++) {
diff --git a/ext/unicorn_http/epollexclusive.h b/ext/unicorn_http/epollexclusive.h
new file mode 100644
index 0000000..c74a779
--- /dev/null
+++ b/ext/unicorn_http/epollexclusive.h
@@ -0,0 +1,128 @@
+/*
+ * This is only intended for use inside a unicorn worker, nowhere else.
+ * EPOLLEXCLUSIVE somewhat mitigates the thundering herd problem for
+ * mostly idle processes since we can't use blocking accept4.
+ * This is NOT intended for use with multi-threaded servers, nor
+ * single-threaded multi-client ("C10K") servers or anything advanced
+ * like that.  This use of epoll is only appropriate for a primitive,
+ * single-client, single-threaded servers like unicorn that need to
+ * support SIGKILL timeouts and parent death detection.
+ */
+#if defined(HAVE_EPOLL_CREATE1)
+#  include <sys/epoll.h>
+#  include <errno.h>
+#  include <ruby/io.h>
+#  include <ruby/thread.h>
+#endif /* __linux__ */
+
+#if defined(EPOLLEXCLUSIVE) && defined(HAVE_EPOLL_CREATE1)
+#  define USE_EPOLL (1)
+#else
+#  define USE_EPOLL (0)
+#endif
+
+#if USE_EPOLL
+#if defined(HAVE_RB_IO_DESCRIPTOR) /* Ruby 3.1+ */
+#        define my_fileno(io) rb_io_descriptor(io)
+#else /* Ruby <3.1 */
+static int my_fileno(VALUE io)
+{
+        rb_io_t *fptr;
+        GetOpenFile(io, fptr);
+        rb_io_check_closed(fptr);
+        return fptr->fd;
+}
+#endif /* Ruby <3.1 */
+
+/*
+ * :nodoc:
+ * returns IO object if EPOLLEXCLUSIVE works and arms readers
+ */
+static VALUE prep_readers(VALUE cls, VALUE readers)
+{
+        long i;
+        int epfd = epoll_create1(EPOLL_CLOEXEC);
+        VALUE epio;
+
+        if (epfd < 0) rb_sys_fail("epoll_create1");
+
+        epio = rb_funcall(cls, rb_intern("for_fd"), 1, INT2NUM(epfd));
+
+        Check_Type(readers, T_ARRAY);
+        for (i = 0; i < RARRAY_LEN(readers); i++) {
+                int rc, fd;
+                struct epoll_event e;
+                VALUE io = rb_ary_entry(readers, i);
+
+                e.data.u64 = i; /* the reason readers shouldn't change */
+
+                /*
+                 * I wanted to use EPOLLET here, but maintaining our own
+                 * equivalent of ep->rdllist in Ruby-space doesn't fit
+                 * our design at all (and the kernel already has it's own
+                 * code path for doing it).  So let the kernel spend
+                 * cycles on maintaining level-triggering.
+                 */
+                e.events = EPOLLEXCLUSIVE | EPOLLIN;
+                fd = my_fileno(rb_io_get_io(io));
+                rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e);
+                if (rc < 0) rb_sys_fail("epoll_ctl");
+        }
+        return epio;
+}
+#endif /* USE_EPOLL */
+
+#if USE_EPOLL
+struct ep_wait {
+        struct epoll_event event;
+        int epfd;
+        int timeout_msec;
+};
+
+static void *do_wait(void *ptr) /* runs w/o GVL */
+{
+        struct ep_wait *epw = ptr;
+        /*
+         * Linux delivers epoll events in the order received, and using
+         * maxevents=1 ensures we pluck one item off ep->rdllist
+         * at-a-time (c.f. fs/eventpoll.c in linux.git, it's quite
+         * easy-to-understand for anybody familiar with Ruby C).
+         */
+        return (void *)(long)epoll_wait(epw->epfd, &epw->event, 1,
+                                        epw->timeout_msec);
+}
+
+/* :nodoc: */
+/* readers must not change between prepare_readers and get_readers */
+static VALUE
+get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec)
+{
+        struct ep_wait epw;
+        long n;
+
+        Check_Type(ready, T_ARRAY);
+        Check_Type(readers, T_ARRAY);
+
+        epw.epfd = my_fileno(epio);
+        epw.timeout_msec = NUM2INT(timeout_msec);
+        n = (long)rb_thread_call_without_gvl(do_wait, &epw, RUBY_UBF_IO, NULL);
+        if (n < 0) {
+                if (errno != EINTR) rb_sys_fail("epoll_wait");
+        } else if (n > 0) { /* maxevents is hardcoded to 1 */
+                VALUE obj = rb_ary_entry(readers, epw.event.data.u64);
+
+                if (RTEST(obj))
+                        rb_ary_push(ready, obj);
+        } /* n == 0 : timeout */
+        return Qfalse;
+}
+#endif /* USE_EPOLL */
+
+static void init_epollexclusive(VALUE mUnicorn)
+{
+#if USE_EPOLL
+        VALUE cWaiter = rb_define_class_under(mUnicorn, "Waiter", rb_cIO);
+        rb_define_singleton_method(cWaiter, "prep_readers", prep_readers, 1);
+        rb_define_method(cWaiter, "get_readers", get_readers, 3);
+#endif
+}
diff --git a/ext/unicorn_http/ext_help.h b/ext/unicorn_http/ext_help.h
index 747c36c..86a187e 100644
--- a/ext/unicorn_http/ext_help.h
+++ b/ext/unicorn_http/ext_help.h
@@ -8,30 +8,6 @@
 #  define assert_frozen(f) do {} while (0)
 #endif /* !defined(OBJ_FROZEN) */
 
-#if !defined(OFFT2NUM)
-#  if SIZEOF_OFF_T == SIZEOF_LONG
-#    define OFFT2NUM(n) LONG2NUM(n)
-#  else
-#    define OFFT2NUM(n) LL2NUM(n)
-#  endif
-#endif /* ! defined(OFFT2NUM) */
-
-#if !defined(SIZET2NUM)
-#  if SIZEOF_SIZE_T == SIZEOF_LONG
-#    define SIZET2NUM(n) ULONG2NUM(n)
-#  else
-#    define SIZET2NUM(n) ULL2NUM(n)
-#  endif
-#endif /* ! defined(SIZET2NUM) */
-
-#if !defined(NUM2SIZET)
-#  if SIZEOF_SIZE_T == SIZEOF_LONG
-#    define NUM2SIZET(n) ((size_t)NUM2ULONG(n))
-#  else
-#    define NUM2SIZET(n) ((size_t)NUM2ULL(n))
-#  endif
-#endif /* ! defined(NUM2SIZET) */
-
 static inline int str_cstr_eq(VALUE val, const char *ptr, long len)
 {
   return (RSTRING_LEN(val) == len && !memcmp(ptr, RSTRING_PTR(val), len));
diff --git a/ext/unicorn_http/extconf.rb b/ext/unicorn_http/extconf.rb
index 95514bc..de896fe 100644
--- a/ext/unicorn_http/extconf.rb
+++ b/ext/unicorn_http/extconf.rb
@@ -1,17 +1,8 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'mkmf'
 
-unless RUBY_VERSION < '3.1'
-  warn "Unicorn was only tested against MRI up to 3.0.\n" \
-       "It might not properly work with #{RUBY_VERSION}"
-end
-
-have_macro("SIZEOF_OFF_T", "ruby.h") or check_sizeof("off_t", "sys/types.h")
-have_macro("SIZEOF_SIZE_T", "ruby.h") or check_sizeof("size_t", "sys/types.h")
-have_macro("SIZEOF_LONG", "ruby.h") or check_sizeof("long", "sys/types.h")
-have_func("rb_str_set_len", "ruby.h") or abort 'Ruby 1.9.3+ required'
-have_func("rb_hash_clear", "ruby.h") # Ruby 2.0+
-have_func("gmtime_r", "time.h")
+have_func("rb_hash_clear", "ruby.h") or abort 'Ruby 2.0+ required'
 
 message('checking if String#-@ (str_uminus) dedupes... ')
 begin
@@ -43,4 +34,7 @@ else
   message("no, needs Ruby 2.6+\n")
 end
 
+if have_func('epoll_create1', %w(sys/epoll.h))
+  have_func('rb_io_descriptor') # Ruby 3.1+
+end
 create_makefile("unicorn_http")
diff --git a/ext/unicorn_http/global_variables.h b/ext/unicorn_http/global_variables.h
index f8e694c..c9ceebd 100644
--- a/ext/unicorn_http/global_variables.h
+++ b/ext/unicorn_http/global_variables.h
@@ -55,7 +55,7 @@ NORETURN(static void parser_raise(VALUE klass, const char *));
 
 /** Defines global strings in the init method. */
 #define DEF_GLOBAL(N, val) do { \
-  g_##N = rb_obj_freeze(rb_str_new(val, sizeof(val) - 1)); \
+  g_##N = str_new_dd_freeze(val, (long)sizeof(val) - 1); \
   rb_gc_register_mark_object(g_##N); \
 } while (0)
 
diff --git a/ext/unicorn_http/httpdate.c b/ext/unicorn_http/httpdate.c
index b59d038..0faf5da 100644
--- a/ext/unicorn_http/httpdate.c
+++ b/ext/unicorn_http/httpdate.c
@@ -1,5 +1,6 @@
 #include <ruby.h>
 #include <time.h>
+#include <sys/time.h>
 #include <stdio.h>
 
 static const size_t buf_capa = sizeof("Thu, 01 Jan 1970 00:00:00 GMT");
@@ -11,6 +12,7 @@ static const char months[] = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0"
 
 /* for people on wonky systems only */
 #ifndef HAVE_GMTIME_R
+# warning using fake gmtime_r
 static struct tm * my_gmtime_r(time_t *now, struct tm *tm)
 {
         struct tm *global = gmtime(now);
@@ -42,13 +44,24 @@ static struct tm * my_gmtime_r(time_t *now, struct tm *tm)
 static VALUE httpdate(VALUE self)
 {
         static time_t last;
-        time_t now = time(NULL); /* not a syscall on modern 64-bit systems */
+        struct timeval now;
         struct tm tm;
 
-        if (last == now)
+        /*
+         * Favor gettimeofday(2) over time(2), as the latter can return the
+         * wrong value in the first 1 .. 2.5 ms of every second(!)
+         *
+         * https://lore.kernel.org/git/20230320230507.3932018-1-gitster@pobox.com/
+         * https://inbox.sourceware.org/libc-alpha/20230306160321.2942372-1-adhemerval.zanella@linaro.org/T/
+         * https://sourceware.org/bugzilla/show_bug.cgi?id=30200
+         */
+        if (gettimeofday(&now, NULL))
+                rb_sys_fail("gettimeofday");
+
+        if (last == now.tv_sec)
                 return buf;
-        last = now;
-        gmtime_r(&now, &tm);
+        last = now.tv_sec;
+        gmtime_r(&now.tv_sec, &tm);
 
         /* we can make this thread-safe later if our Ruby loses the GVL */
         snprintf(buf_ptr, buf_capa,
diff --git a/ext/unicorn_http/unicorn_http.rl b/ext/unicorn_http/unicorn_http.rl
index 21e09d6..fb5dcde 100644
--- a/ext/unicorn_http/unicorn_http.rl
+++ b/ext/unicorn_http/unicorn_http.rl
@@ -12,6 +12,7 @@
 #include "common_field_optimization.h"
 #include "global_variables.h"
 #include "c_util.h"
+#include "epollexclusive.h"
 
 void init_unicorn_httpdate(void);
 
@@ -27,10 +28,15 @@ void init_unicorn_httpdate(void);
 #define UH_FL_TO_CLEAR 0x200
 #define UH_FL_RESSTART 0x400 /* for check_client_connection */
 #define UH_FL_HIJACK 0x800
+#define UH_FL_RES_CHUNK_VER (1U << 12)
+#define UH_FL_RES_CHUNK_METHOD (1U << 13)
 
 /* all of these flags need to be set for keepalive to be supported */
 #define UH_FL_KEEPALIVE (UH_FL_KAVERSION | UH_FL_REQEOF | UH_FL_HASHEADER)
 
+/* we can only chunk responses for non-HEAD HTTP/1.1 requests */
+#define UH_FL_RES_CHUNKABLE (UH_FL_RES_CHUNK_VER | UH_FL_RES_CHUNK_METHOD)
+
 static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */
 
 /* this is only intended for use with Rainbows! */
@@ -65,18 +71,6 @@ struct http_parser {
 static ID id_set_backtrace, id_is_chunked_p;
 static VALUE cHttpParser;
 
-#ifdef HAVE_RB_HASH_CLEAR /* Ruby >= 2.0 */
-#  define my_hash_clear(h) (void)rb_hash_clear(h)
-#else /* !HAVE_RB_HASH_CLEAR - Ruby <= 1.9.3 */
-
-static ID id_clear;
-
-static void my_hash_clear(VALUE h)
-{
-  rb_funcall(h, id_clear, 0);
-}
-#endif /* HAVE_RB_HASH_CLEAR */
-
 static void finalize_header(struct http_parser *hp);
 
 static void parser_raise(VALUE klass, const char *msg)
@@ -156,6 +150,9 @@ request_method(struct http_parser *hp, const char *ptr, size_t len)
 {
   VALUE v = rb_str_new(ptr, len);
 
+  if (len != 4 || memcmp(ptr, "HEAD", 4))
+    HP_FL_SET(hp, RES_CHUNK_METHOD);
+
   rb_hash_aset(hp->env, g_request_method, v);
 }
 
@@ -169,6 +166,7 @@ http_version(struct http_parser *hp, const char *ptr, size_t len)
   if (CONST_MEM_EQ("HTTP/1.1", ptr, len)) {
     /* HTTP/1.1 implies keepalive unless "Connection: close" is set */
     HP_FL_SET(hp, KAVERSION);
+    HP_FL_SET(hp, RES_CHUNK_VER);
     v = g_http_11;
   } else if (CONST_MEM_EQ("HTTP/1.0", ptr, len)) {
     v = g_http_10;
@@ -650,7 +648,7 @@ static VALUE HttpParser_clear(VALUE self)
     return HttpParser_init(self);
 
   http_parser_init(hp);
-  my_hash_clear(hp->env);
+  rb_hash_clear(hp->env);
 
   return self;
 }
@@ -812,6 +810,14 @@ static VALUE HttpParser_keepalive(VALUE self)
   return HP_FL_ALL(hp, KEEPALIVE) ? Qtrue : Qfalse;
 }
 
+/* :nodoc: */
+static VALUE chunkable_response_p(VALUE self)
+{
+  const struct http_parser *hp = data_get(self);
+
+  return HP_FL_ALL(hp, RES_CHUNKABLE) ? Qtrue : Qfalse;
+}
+
 /**
  * call-seq:
  *    parser.next? => true or false
@@ -979,6 +985,7 @@ void Init_unicorn_http(void)
   e414 = rb_define_class_under(mUnicorn, "RequestURITooLongError",
                                eHttpParserError);
 
+  id_uminus = rb_intern("-@");
   init_globals();
   rb_define_alloc_func(cHttpParser, HttpParser_alloc);
   rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
@@ -991,6 +998,7 @@ void Init_unicorn_http(void)
   rb_define_method(cHttpParser, "content_length", HttpParser_content_length, 0);
   rb_define_method(cHttpParser, "body_eof?", HttpParser_body_eof, 0);
   rb_define_method(cHttpParser, "keepalive?", HttpParser_keepalive, 0);
+  rb_define_method(cHttpParser, "chunkable_response?", chunkable_response_p, 0);
   rb_define_method(cHttpParser, "headers?", HttpParser_has_headers, 0);
   rb_define_method(cHttpParser, "next?", HttpParser_next, 0);
   rb_define_method(cHttpParser, "buf", HttpParser_buf, 0);
@@ -1025,9 +1033,8 @@ void Init_unicorn_http(void)
   id_set_backtrace = rb_intern("set_backtrace");
   init_unicorn_httpdate();
 
-#ifndef HAVE_RB_HASH_CLEAR
-  id_clear = rb_intern("clear");
-#endif
   id_is_chunked_p = rb_intern("is_chunked?");
+
+  init_epollexclusive(mUnicorn);
 }
 #undef SET_GLOBAL
diff --git a/ext/unicorn_http/unicorn_http_common.rl b/ext/unicorn_http/unicorn_http_common.rl
index 0988b54..507e570 100644
--- a/ext/unicorn_http/unicorn_http_common.rl
+++ b/ext/unicorn_http/unicorn_http_common.rl
@@ -4,7 +4,7 @@
 
 #### HTTP PROTOCOL GRAMMAR
 # line endings
-  CRLF = ("\r\n" | "\n");
+  CRLF = ("\r\n" | "\n");
 
 # character types
   CTL = (cntrl | 127);
diff --git a/lib/unicorn.rb b/lib/unicorn.rb
index d5991fe..fb91679 100644
--- a/lib/unicorn.rb
+++ b/lib/unicorn.rb
@@ -1,7 +1,7 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'etc'
 require 'stringio'
-require 'kgio'
 require 'raindrops'
 require 'io/wait'
 
@@ -66,7 +66,6 @@ module Unicorn
 
       middleware = { # order matters
         ContentLength: nil,
-        Chunked: nil,
         CommonLogger: [ $stderr ],
         ShowExceptions: nil,
         Lint: nil,
@@ -75,8 +74,8 @@ module Unicorn
 
       # return value, matches rackup defaults based on env
       # Unicorn does not support persistent connections, but Rainbows!
-      # and Zbatery both do.  Users accustomed to the Rack::Server default
-      # middlewares will need ContentLength/Chunked middlewares.
+      # does.  Users accustomed to the Rack::Server default
+      # middlewares will need ContentLength middleware.
       case ENV["RACK_ENV"]
       when "development"
       when "deployment"
@@ -113,9 +112,7 @@ module Unicorn
   F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
 
   def self.pipe # :nodoc:
-    Kgio::Pipe.new.each do |io|
-      io.close_on_exec = true  # remove this when we only support Ruby >= 2.0
-
+    IO.pipe.each do |io|
       # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
       # limits.
       if defined?(F_SETPIPE_SZ)
diff --git a/lib/unicorn/app/old_rails.rb b/lib/unicorn/app/old_rails.rb
index 1e8c41a..54b3e69 100644
--- a/lib/unicorn/app/old_rails.rb
+++ b/lib/unicorn/app/old_rails.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # :enddoc:
 # This code is based on the original Rails handler in Mongrel
diff --git a/lib/unicorn/app/old_rails/static.rb b/lib/unicorn/app/old_rails/static.rb
index 2257270..cf34e02 100644
--- a/lib/unicorn/app/old_rails/static.rb
+++ b/lib/unicorn/app/old_rails/static.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 # :enddoc:
 # This code is based on the original Rails handler in Mongrel
 # Copyright (c) 2005 Zed A. Shaw
diff --git a/lib/unicorn/cgi_wrapper.rb b/lib/unicorn/cgi_wrapper.rb
index d9b7fe5..fb43605 100644
--- a/lib/unicorn/cgi_wrapper.rb
+++ b/lib/unicorn/cgi_wrapper.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # :enddoc:
 # This code is based on the original CGIWrapper from Mongrel
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb
index ecdf03e..3c81596 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require 'logger'
 
 # Implements a simple DSL for configuring a unicorn server.
@@ -216,7 +217,12 @@ class Unicorn::Configurator
     set_hook(:before_exec, block_given? ? block : args[0], 1)
   end
 
-  # sets the timeout of worker processes to +seconds+.  Workers
+  # Strongly consider using link:/Application_Timeouts.html instead
+  # of this misfeature.  This misfeature has done decades of damage
+  # to Ruby since it demotivates the use of fine-grained timeout
+  # mechanisms.
+  #
+  # Sets the timeout of worker processes to +seconds+.  Workers
   # handling the request/app.call/response cycle taking longer than
   # this time period will be forcibly killed (via SIGKILL).  This
   # timeout is enforced by the master process itself and not subject
diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb
index 33ab4ac..8032863 100644
--- a/lib/unicorn/const.rb
+++ b/lib/unicorn/const.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 module Unicorn::Const # :nodoc:
   # default TCP listen host address (0.0.0.0, all interfaces)
diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb
index 6ca4592..a48dab7 100644
--- a/lib/unicorn/http_request.rb
+++ b/lib/unicorn/http_request.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 # :enddoc:
 # no stable API here
 require 'unicorn_http'
@@ -61,8 +62,7 @@ class Unicorn::HttpParser
   # returns an environment hash suitable for Rack if successful
   # This does minimal exception trapping and it is up to the caller
   # to handle any socket errors (e.g. user aborted upload).
-  def read(socket)
-    clear
+  def read_headers(socket, ai)
     e = env
 
     # From https://www.ietf.org/rfc/rfc3875:
@@ -72,17 +72,17 @@ class Unicorn::HttpParser
     #  identify the client for the immediate request to the server;
     #  that client may be a proxy, gateway, or other intermediary
     #  acting on behalf of the actual source client."
-    e['REMOTE_ADDR'] = socket.kgio_addr
+    e['REMOTE_ADDR'] = ai.unix? ? '127.0.0.1' : ai.ip_address
 
     # short circuit the common case with small GET requests first
-    socket.kgio_read!(16384, buf)
+    socket.readpartial(16384, buf)
     if parse.nil?
       # Parser is not done, queue up more data to read and continue parsing
       # an Exception thrown from the parser will throw us out of the loop
-      false until add_parse(socket.kgio_read!(16384))
+      false until add_parse(socket.readpartial(16384))
     end
 
-    check_client_connection(socket) if @@check_client_connection
+    check_client_connection(socket, ai) if @@check_client_connection
 
     e['rack.input'] = 0 == content_length ?
                       NULL_IO : @@input_class.new(socket, self)
@@ -108,8 +108,8 @@ class Unicorn::HttpParser
   if Raindrops.const_defined?(:TCP_Info)
     TCPI = Raindrops::TCP_Info.allocate
 
-    def check_client_connection(socket) # :nodoc:
-      if Unicorn::TCPClient === socket
+    def check_client_connection(socket, ai) # :nodoc:
+      if ai.ip?
         # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state)
         raise Errno::EPIPE, "client closed connection".freeze,
               EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state)
@@ -153,8 +153,8 @@ class Unicorn::HttpParser
     # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect.
     # Not that efficient, but probably still better than doing unnecessary
     # work after a client gives up.
-    def check_client_connection(socket) # :nodoc:
-      if Unicorn::TCPClient === socket && @@tcpi_inspect_ok
+    def check_client_connection(socket, ai) # :nodoc:
+      if @@tcpi_inspect_ok && ai.ip?
         opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect
         if opt =~ /\bstate=(\S+)/
           raise Errno::EPIPE, "client closed connection".freeze,
diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb
index b23e521..3634165 100644
--- a/lib/unicorn/http_response.rb
+++ b/lib/unicorn/http_response.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 # :enddoc:
 # Writes a Rack response to your client using the HTTP/1.1 specification.
 # You use it by simply doing:
@@ -12,6 +13,12 @@ module Unicorn::HttpResponse
 
   STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ?
                  Rack::Utils::HTTP_STATUS_CODES : {}
+  STATUS_WITH_NO_ENTITY_BODY = defined?(
+                 Rack::Utils::STATUS_WITH_NO_ENTITY_BODY) ?
+                 Rack::Utils::STATUS_WITH_NO_ENTITY_BODY : begin
+    warn 'Rack::Utils::STATUS_WITH_NO_ENTITY_BODY missing'
+    {}
+  end
 
   # internal API, code will always be common-enough-for-even-old-Rack
   def err_response(code, response_start_sent)
@@ -19,15 +26,28 @@ module Unicorn::HttpResponse
       "#{code} #{STATUS_CODES[code]}\r\n\r\n"
   end
 
+  def append_header(buf, key, value)
+    case value
+    when Array # Rack 3
+      value.each { |v| buf << "#{key}: #{v}\r\n" }
+    when /\n/ # Rack 2
+      # avoiding blank, key-only cookies with /\n+/
+      value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" }
+    else
+      buf << "#{key}: #{value}\r\n"
+    end
+  end
+
   # writes the rack_response to socket as an HTTP response
   def http_response_write(socket, status, headers, body,
                           req = Unicorn::HttpRequest.new)
     hijack = nil
-
+    do_chunk = false
     if headers
       code = status.to_i
       msg = STATUS_CODES[code]
       start = req.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze
+      term = STATUS_WITH_NO_ENTITY_BODY.include?(code) || false
       buf = "#{start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \
             "Date: #{httpdate}\r\n" \
             "Connection: close\r\n"
@@ -35,25 +55,38 @@ module Unicorn::HttpResponse
         case key
         when %r{\A(?:Date|Connection)\z}i
           next
+        when %r{\AContent-Length\z}i
+          append_header(buf, key, value)
+          term = true
+        when %r{\ATransfer-Encoding\z}i
+          append_header(buf, key, value)
+          term = true if /\bchunked\b/i === value # value may be Array :x
         when "rack.hijack"
           # This should only be hit under Rack >= 1.5, as this was an illegal
           # key in Rack < 1.5
           hijack = value
         else
-          if value =~ /\n/
-            # avoiding blank, key-only cookies with /\n+/
-            value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" }
-          else
-            buf << "#{key}: #{value}\r\n"
-          end
+          append_header(buf, key, value)
         end
       end
+      if !hijack && !term && req.chunkable_response?
+        do_chunk = true
+        buf << "Transfer-Encoding: chunked\r\n".freeze
+      end
       socket.write(buf << "\r\n".freeze)
     end
 
     if hijack
       req.hijacked!
       hijack.call(socket)
+    elsif do_chunk
+      begin
+        body.each do |b|
+          socket.write("#{b.bytesize.to_s(16)}\r\n", b, "\r\n".freeze)
+        end
+      ensure
+        socket.write("0\r\n\r\n".freeze)
+      end
     else
       body.each { |chunk| socket.write(chunk) }
     end
diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb
index 05dad99..08fbe40 100644
--- a/lib/unicorn/http_server.rb
+++ b/lib/unicorn/http_server.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # This is the process manager of Unicorn. This manages worker
 # processes which in turn handle the I/O and application process.
@@ -69,7 +70,6 @@ class Unicorn::HttpServer
   # incoming requests on the socket.
   def initialize(app, options = {})
     @app = app
-    @request = Unicorn::HttpRequest.new
     @reexec_pid = 0
     @default_middleware = true
     options = options.dup
@@ -78,6 +78,7 @@ class Unicorn::HttpServer
     options[:use_defaults] = true
     self.config = Unicorn::Configurator.new(options)
     self.listener_opts = {}
+    @immortal = [] # immortal inherited sockets from systemd
 
     # We use @self_pipe differently in the master and worker processes:
     #
@@ -111,9 +112,7 @@ class Unicorn::HttpServer
 
     @worker_data = if worker_data = ENV['UNICORN_WORKER']
       worker_data = worker_data.split(',').map!(&:to_i)
-      worker_data[1] = worker_data.slice!(1..2).map do |i|
-        Kgio::Pipe.for_fd(i)
-      end
+      worker_data[1] = worker_data.slice!(1..2).map { |i| IO.for_fd(i) }
       worker_data
     end
   end
@@ -159,6 +158,7 @@ class Unicorn::HttpServer
     end
     set_names = listener_names(listeners)
     dead_names.concat(cur_names - set_names).uniq!
+    dead_names -= @immortal.map { |io| sock_name(io) }
 
     LISTENERS.delete_if do |io|
       if dead_names.include?(sock_name(io))
@@ -188,7 +188,8 @@ class Unicorn::HttpServer
       rescue Errno::EEXIST
         retry
       end
-      fp.syswrite("#$$\n")
+      fp.sync = true
+      fp.write("#$$\n")
       File.rename(fp.path, path)
       fp.close
     end
@@ -241,10 +242,6 @@ class Unicorn::HttpServer
     tries = opt[:tries] || 5
     begin
       io = bind_listen(address, opt)
-      unless Kgio::TCPServer === io || Kgio::UNIXServer === io
-        io.autoclose = false
-        io = server_cast(io)
-      end
       logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
       LISTENERS << io
       io
@@ -387,12 +384,13 @@ class Unicorn::HttpServer
     # the Ruby itself and not require a separate malloc (on 32-bit MRI 1.9+).
     # Most reads are only one byte here and uncommon, so it's not worth a
     # persistent buffer, either:
-    @self_pipe[0].kgio_tryread(11)
+    @self_pipe[0].read_nonblock(11, exception: false)
   end
 
   def awaken_master
     return if $$ != @master_pid
-    @self_pipe[1].kgio_trywrite('.') # wakeup master process from select
+    # wakeup master process from select
+    @self_pipe[1].write_nonblock('.', exception: false)
   end
 
   # reaps all unreaped workers
@@ -446,11 +444,6 @@ class Unicorn::HttpServer
       Dir.chdir(START_CTX[:cwd])
       cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
 
-      # avoid leaking FDs we don't know about, but let before_exec
-      # unset FD_CLOEXEC, if anything else in the app eventually
-      # relies on FD inheritence.
-      close_sockets_on_exec(listener_fds)
-
       # exec(command, hash) works in at least 1.9.1+, but will only be
       # required in 1.9.4/2.0.0 at earliest.
       cmd << listener_fds
@@ -472,29 +465,15 @@ class Unicorn::HttpServer
     worker_info = [worker.nr, worker.to_io.fileno, worker.master.fileno]
     env['UNICORN_WORKER'] = worker_info.join(',')
 
-    close_sockets_on_exec(listener_fds)
-
     Process.spawn(env, START_CTX[0], *START_CTX[:argv], listener_fds)
   end
 
   def listener_sockets
     listener_fds = {}
-    LISTENERS.each do |sock|
-      sock.close_on_exec = false
-      listener_fds[sock.fileno] = sock
-    end
+    LISTENERS.each { |sock| listener_fds[sock.fileno] = sock }
     listener_fds
   end
 
-  def close_sockets_on_exec(sockets)
-    (3..1024).each do |io|
-      next if sockets.include?(io)
-      io = IO.for_fd(io) rescue next
-      io.autoclose = false
-      io.close_on_exec = true
-    end
-  end
-
   # forcibly terminate all workers that haven't checked in in timeout seconds.  The timeout is implemented using an unlinked File
   def murder_lazy_workers
     next_sleep = @timeout - 1
@@ -582,35 +561,25 @@ class Unicorn::HttpServer
       500
     end
     if code
-      client.kgio_trywrite(err_response(code, @request.response_start_sent))
+      code = err_response(code, @request.response_start_sent)
+      client.write_nonblock(code, exception: false)
     end
     client.close
   rescue
   end
 
   def e103_response_write(client, headers)
-    response = if @request.response_start_sent
-      "103 Early Hints\r\n"
-    else
-      "HTTP/1.1 103 Early Hints\r\n"
-    end
-
-    headers.each_pair do |k, vs|
-      next if !vs || vs.empty?
-      values = vs.to_s.split("\n".freeze)
-      values.each do |v|
-        response << "#{k}: #{v}\r\n"
-      end
-    end
-    response << "\r\n".freeze
-    response << "HTTP/1.1 ".freeze if @request.response_start_sent
-    client.write(response)
+    rss = @request.response_start_sent
+    buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n"
+    headers.each { |key, value| append_header(buf, key, value) }
+    buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze)
+    client.write(buf)
   end
 
   def e100_response_write(client, env)
     # We use String#freeze to avoid allocations under Ruby 2.1+
     # Not many users hit this code path, so it's better to reduce the
-    # constant table sizes even for 1.9.3-2.0 users who'll hit extra
+    # constant table sizes even for Ruby 2.0 users who'll hit extra
     # allocations here.
     client.write(@request.response_start_sent ?
                  "100 Continue\r\n\r\nHTTP/1.1 ".freeze :
@@ -620,8 +589,9 @@ class Unicorn::HttpServer
 
   # once a client is accepted, it is processed in its entirety here
   # in 3 easy steps: read request, call app, write app response
-  def process_client(client)
-    env = @request.read(client)
+  def process_client(client, ai)
+    @request = Unicorn::HttpRequest.new
+    env = @request.read_headers(client, ai)
 
     if early_hints
       env["rack.early_hints"] = lambda do |headers|
@@ -629,6 +599,8 @@ class Unicorn::HttpServer
       end
     end
 
+    env["rack.after_reply"] = []
+
     status, headers, body = @app.call(env)
 
     begin
@@ -651,6 +623,8 @@ class Unicorn::HttpServer
     end
   rescue => e
     handle_error(client, e)
+  ensure
+    env["rack.after_reply"].each(&:call) if env
   end
 
   def nuke_listeners!(readers)
@@ -681,7 +655,6 @@ class Unicorn::HttpServer
     LISTENERS.each { |sock| sock.close_on_exec = true }
 
     worker.user(*user) if user.kind_of?(Array) && ! worker.switched
-    self.timeout /= 2.0 # halve it for select()
     @config = nil
     build_app! unless preload_app
     @after_fork = @listener_opts = @orig_app = nil
@@ -695,59 +668,54 @@ class Unicorn::HttpServer
     logger.info "worker=#{worker_nr} reopening logs..."
     Unicorn::Util.reopen_logs
     logger.info "worker=#{worker_nr} done reopening logs"
+    false
   rescue => e
     logger.error(e) rescue nil
     exit!(77) # EX_NOPERM in sysexits.h
   end
 
+  def prep_readers(readers)
+    wtr = Unicorn::Waiter.prep_readers(readers)
+    @timeout *= 500 # to milliseconds for epoll, but halved
+    wtr
+  rescue
+    require_relative 'select_waiter'
+    @timeout /= 2.0 # halved for IO.select
+    Unicorn::SelectWaiter.new
+  end
+
   # runs inside each forked worker, this sits around and waits
   # for connections and doesn't die until the parent dies (or is
   # given a INT, QUIT, or TERM signal)
   def worker_loop(worker)
-    ppid = @master_pid
     readers = init_worker_process(worker)
-    nr = 0 # this becomes negative if we need to reopen logs
+    waiter = prep_readers(readers)
+    reopen = false
 
     # this only works immediately if the master sent us the signal
     # (which is the normal case)
-    trap(:USR1) { nr = -65536 }
+    trap(:USR1) { reopen = true }
 
     ready = readers.dup
-    nr_listeners = readers.size
     @after_worker_ready.call(self, worker)
 
     begin
-      nr < 0 and reopen_worker_logs(worker.nr)
-      nr = 0
+      reopen = reopen_worker_logs(worker.nr) if reopen
       worker.tick = time_now.to_i
-      tmp = ready.dup
-      while sock = tmp.shift
-        # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all,
-        # but that will return false
-        if client = sock.kgio_tryaccept
-          process_client(client)
-          nr += 1
+      while sock = ready.shift
+        client_ai = sock.accept_nonblock(exception: false)
+        if client_ai != :wait_readable
+          process_client(*client_ai)
           worker.tick = time_now.to_i
         end
-        break if nr < 0
+        break if reopen
       end
 
-      # make the following bet: if we accepted clients this round,
-      # we're probably reasonably busy, so avoid calling select()
-      # and do a speculative non-blocking accept() on ready listeners
-      # before we sleep again in select().
-      if nr == nr_listeners
-        tmp = ready.dup
-        redo
-      end
-
-      ppid == Process.ppid or return
-
-      # timeout used so we can detect parent death:
+      # timeout so we can .tick and keep parent from SIGKILL-ing us
       worker.tick = time_now.to_i
-      ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0]
+      waiter.get_readers(ready, readers, @timeout)
     rescue => e
-      redo if nr < 0 && readers[0]
+      redo if reopen && readers[0]
       Unicorn.log_error(@logger, "listen loop error", e) if readers[0]
     end while readers[0]
   end
@@ -835,21 +803,21 @@ class Unicorn::HttpServer
 
   def inherit_listeners!
     # inherit sockets from parents, they need to be plain Socket objects
-    # before they become Kgio::UNIXServer or Kgio::TCPServer
     inherited = ENV['UNICORN_FD'].to_s.split(',')
+    immortal = []
 
     # emulate sd_listen_fds() for systemd
     sd_pid, sd_fds = ENV.values_at('LISTEN_PID', 'LISTEN_FDS')
     if sd_pid.to_i == $$ # n.b. $$ can never be zero
       # 3 = SD_LISTEN_FDS_START
-      inherited.concat((3...(3 + sd_fds.to_i)).to_a)
+      immortal = (3...(3 + sd_fds.to_i)).to_a
+      inherited.concat(immortal)
     end
     # to ease debugging, we will not unset LISTEN_PID and LISTEN_FDS
 
     inherited.map! do |fd|
       io = Socket.for_fd(fd.to_i)
-      io.autoclose = false
-      io = server_cast(io)
+      @immortal << io if immortal.include?(fd)
       set_server_sockopt(io, listener_opts[sock_name(io)])
       logger.info "inherited addr=#{sock_name(io)} fd=#{io.fileno}"
       io
@@ -858,11 +826,9 @@ class Unicorn::HttpServer
     config_listeners = config[:listeners].dup
     LISTENERS.replace(inherited)
 
-    # we start out with generic Socket objects that get cast to either
-    # Kgio::TCPServer or Kgio::UNIXServer objects; but since the Socket
-    # objects share the same OS-level file descriptor as the higher-level
-    # *Server objects; we need to prevent Socket objects from being
-    # garbage-collected
+    # we only use generic Socket objects for aggregate Socket#accept_nonblock
+    # return value [ Socket, Addrinfo ].  This allows us to avoid having to
+    # make getpeername(2) syscalls later on to fill in env['REMOTE_ADDR']
     config_listeners -= listener_names
     if config_listeners.empty? && LISTENERS.empty?
       config_listeners << Unicorn::Const::DEFAULT_LISTEN
diff --git a/lib/unicorn/launcher.rb b/lib/unicorn/launcher.rb
index 78e8f39..bd3324e 100644
--- a/lib/unicorn/launcher.rb
+++ b/lib/unicorn/launcher.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # :enddoc:
 $stdout.sync = $stderr.sync = true
diff --git a/lib/unicorn/oob_gc.rb b/lib/unicorn/oob_gc.rb
index 3b2f488..efd9177 100644
--- a/lib/unicorn/oob_gc.rb
+++ b/lib/unicorn/oob_gc.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Strongly consider https://github.com/tmm1/gctools if using Ruby 2.1+
 # It is built on new APIs in Ruby 2.1, so it is more intelligent than
@@ -60,17 +61,17 @@ module Unicorn::OobGC
     self.const_set :OOBGC_INTERVAL, interval
     ObjectSpace.each_object(Unicorn::HttpServer) do |s|
       s.extend(self)
-      self.const_set :OOBGC_ENV, s.instance_variable_get(:@request).env
     end
     app # pretend to be Rack middleware since it was in the past
   end
 
   #:stopdoc:
-  def process_client(client)
-    super(client) # Unicorn::HttpServer#process_client
-    if OOBGC_PATH =~ OOBGC_ENV['PATH_INFO'] && ((@@nr -= 1) <= 0)
+  def process_client(*args)
+    super(*args) # Unicorn::HttpServer#process_client
+    env = instance_variable_get(:@request).env
+    if OOBGC_PATH =~ env['PATH_INFO'] && ((@@nr -= 1) <= 0)
       @@nr = OOBGC_INTERVAL
-      OOBGC_ENV.clear
+      env.clear
       disabled = GC.enable
       GC.start
       GC.disable if disabled
diff --git a/lib/unicorn/preread_input.rb b/lib/unicorn/preread_input.rb
index 12eb3e8..c62cc09 100644
--- a/lib/unicorn/preread_input.rb
+++ b/lib/unicorn/preread_input.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 module Unicorn
 # This middleware is used to ensure input is buffered to memory
diff --git a/lib/unicorn/select_waiter.rb b/lib/unicorn/select_waiter.rb
new file mode 100644
index 0000000..d11ea57
--- /dev/null
+++ b/lib/unicorn/select_waiter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: false
+# fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE
+class Unicorn::SelectWaiter # :nodoc:
+  def get_readers(ready, readers, timeout) # :nodoc:
+    ret = IO.select(readers, nil, nil, timeout) and ready.replace(ret[0])
+  end
+end
diff --git a/lib/unicorn/socket_helper.rb b/lib/unicorn/socket_helper.rb
index 8a6f6ee..986932f 100644
--- a/lib/unicorn/socket_helper.rb
+++ b/lib/unicorn/socket_helper.rb
@@ -1,20 +1,9 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 # :enddoc:
 require 'socket'
 
 module Unicorn
-
-  # Instead of using a generic Kgio::Socket for everything,
-  # tag TCP sockets so we can use TCP_INFO under Linux without
-  # incurring extra syscalls for Unix domain sockets.
-  # TODO: remove these when we remove kgio
-  TCPClient = Class.new(Kgio::Socket) # :nodoc:
-  class TCPSrv < Kgio::TCPServer # :nodoc:
-    def kgio_tryaccept # :nodoc:
-      super(TCPClient)
-    end
-  end
-
   module SocketHelper
 
     # internal interface
@@ -91,7 +80,7 @@ module Unicorn
     def set_server_sockopt(sock, opt)
       opt = DEFAULTS.merge(opt || {})
 
-      TCPSocket === sock and set_tcp_sockopt(sock, opt)
+      set_tcp_sockopt(sock, opt) if sock.local_address.ip?
 
       rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf)
       if rcvbuf || sndbuf
@@ -135,7 +124,9 @@ module Unicorn
         end
         old_umask = File.umask(opt[:umask] || 0)
         begin
-          Kgio::UNIXServer.new(address)
+          s = Socket.new(:UNIX, :STREAM)
+          s.bind(Socket.sockaddr_un(address))
+          s
         ensure
           File.umask(old_umask)
         end
@@ -163,8 +154,7 @@ module Unicorn
         sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
       end
       sock.bind(Socket.pack_sockaddr_in(port, addr))
-      sock.autoclose = false
-      TCPSrv.for_fd(sock.fileno)
+      sock
     end
 
     # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
@@ -180,10 +170,6 @@ module Unicorn
     def sock_name(sock)
       case sock
       when String then sock
-      when UNIXServer
-        Socket.unpack_sockaddr_un(sock.getsockname)
-      when TCPServer
-        tcp_name(sock)
       when Socket
         begin
           tcp_name(sock)
@@ -196,16 +182,5 @@ module Unicorn
     end
 
     module_function :sock_name
-
-    # casts a given Socket to be a TCPServer or UNIXServer
-    def server_cast(sock)
-      begin
-        Socket.unpack_sockaddr_in(sock.getsockname)
-        TCPSrv.for_fd(sock.fileno)
-      rescue ArgumentError
-        Kgio::UNIXServer.for_fd(sock.fileno)
-      end
-    end
-
   end # module SocketHelper
 end # module Unicorn
diff --git a/lib/unicorn/stream_input.rb b/lib/unicorn/stream_input.rb
index 41d28a0..23a9976 100644
--- a/lib/unicorn/stream_input.rb
+++ b/lib/unicorn/stream_input.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # When processing uploads, unicorn may expose a StreamInput object under
 # "rack.input" of the Rack environment when
@@ -49,8 +50,7 @@ class Unicorn::StreamInput
         to_read = length - @rbuf.size
         rv.replace(@rbuf.slice!(0, @rbuf.size))
         until to_read == 0 || eof? || (rv.size > 0 && @chunked)
-          @socket.kgio_read(to_read, @buf) or eof!
-          filter_body(@rbuf, @buf)
+          filter_body(@rbuf, @socket.readpartial(to_read, @buf))
           rv << @rbuf
           to_read -= @rbuf.size
         end
@@ -61,6 +61,8 @@ class Unicorn::StreamInput
       read_all(rv)
     end
     rv
+  rescue EOFError
+    return eof!
   end
 
   # :call-seq:
@@ -83,9 +85,10 @@ class Unicorn::StreamInput
     begin
       @rbuf.sub!(re, '') and return $1
       return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
-      @socket.kgio_read(@@io_chunk_size, @buf) or eof!
-      filter_body(once = '', @buf)
+      filter_body(once = '', @socket.readpartial(@@io_chunk_size, @buf))
       @rbuf << once
+    rescue EOFError
+      return eof!
     end while true
   end
 
@@ -107,14 +110,15 @@ private
   def eof?
     if @parser.body_eof?
       while @chunked && ! @parser.parse
-        once = @socket.kgio_read(@@io_chunk_size) or eof!
-        @buf << once
+        @buf << @socket.readpartial(@@io_chunk_size)
       end
       @socket = nil
       true
     else
       false
     end
+  rescue EOFError
+    return eof!
   end
 
   def filter_body(dst, src)
@@ -127,10 +131,11 @@ private
     dst.replace(@rbuf)
     @socket or return
     until eof?
-      @socket.kgio_read(@@io_chunk_size, @buf) or eof!
-      filter_body(@rbuf, @buf)
+      filter_body(@rbuf, @socket.readpartial(@@io_chunk_size, @buf))
       dst << @rbuf
     end
+  rescue EOFError
+    return eof!
   ensure
     @rbuf.clear
   end
diff --git a/lib/unicorn/tee_input.rb b/lib/unicorn/tee_input.rb
index 2ccc2d9..b3c6535 100644
--- a/lib/unicorn/tee_input.rb
+++ b/lib/unicorn/tee_input.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Acts like tee(1) on an input input to provide a input-like stream
 # while providing rewindable semantics through a File/StringIO backing
diff --git a/lib/unicorn/tmpio.rb b/lib/unicorn/tmpio.rb
index 0bbf6ec..deecd80 100644
--- a/lib/unicorn/tmpio.rb
+++ b/lib/unicorn/tmpio.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 # :stopdoc:
 require 'tmpdir'
 
diff --git a/lib/unicorn/util.rb b/lib/unicorn/util.rb
index b826de4..f28d929 100644
--- a/lib/unicorn/util.rb
+++ b/lib/unicorn/util.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require 'fcntl'
 module Unicorn::Util # :nodoc:
diff --git a/lib/unicorn/worker.rb b/lib/unicorn/worker.rb
index 5ddf379..d2445d5 100644
--- a/lib/unicorn/worker.rb
+++ b/lib/unicorn/worker.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 require "raindrops"
 
 # This class and its members can be considered a stable interface
@@ -65,15 +66,15 @@ class Unicorn::Worker
     end
     # writing and reading 4 bytes on a pipe is atomic on all POSIX platforms
     # Do not care in the odd case the buffer is full, here.
-    @master.kgio_trywrite([signum].pack('l'))
+    @master.write_nonblock([signum].pack('l'), exception: false)
   rescue Errno::EPIPE
     # worker will be reaped soon
   end
 
   # this only runs when the Rack app.call is not running
-  # act like a listener
-  def kgio_tryaccept # :nodoc:
-    case buf = @to_io.kgio_tryread(4)
+  # act like Socket#accept_nonblock(exception: false)
+  def accept_nonblock(*_unused) # :nodoc:
+    case buf = @to_io.read_nonblock(4, exception: false)
     when String
       # unpack the buffer and trigger the signal handler
       signum = buf.unpack('l')
@@ -82,7 +83,7 @@ class Unicorn::Worker
     when nil # EOF: master died, but we are at a safe place to exit
       fake_sig(:QUIT)
     when :wait_readable # keep waiting
-      return false
+      return :wait_readable
     end while true # loop, as multiple signals may be sent
   end
 
diff --git a/setup.rb b/setup.rb
index cf1abd9..96cf75a 100644
--- a/setup.rb
+++ b/setup.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 #
 # setup.rb
 #
diff --git a/t/README b/t/README
index 0d9b697..7bd093d 100644
--- a/t/README
+++ b/t/README
@@ -5,16 +5,19 @@ TCP ports or Unix domain sockets.  They're all designed to run
 concurrently with other tests to minimize test time, but tests may be
 run independently as well.
 
-We write our tests in Bourne shell because that's what we're
-comfortable writing integration tests with.
+New tests are written in Perl 5 because we need a stable language
+to test real-world behavior and Ruby introduces incompatibilities
+at a far faster rate than Perl 5.  Perl is Ruby's older cousin, so
+it should be easy-to-learn for Rubyists.
+
+Old tests are in Bourne shell and slowly being ported to Perl 5.
 
 == Requirements
 
-* {Ruby 1.9.3+}[https://www.ruby-lang.org/en/] (duh!)
+* {Ruby 2.5.0+}[https://www.ruby-lang.org/en/]
+* {Perl 5.14+}[https://www.perl.org/] # your distro should have it
 * {GNU make}[https://www.gnu.org/software/make/]
-* {socat}[http://www.dest-unreach.org/socat/]
 * {curl}[https://curl.haxx.se/]
-* standard UNIX shell utilities (Bourne sh, awk, sed, grep, ...)
 
 We do not use bashisms or any non-portable, non-POSIX constructs
 in our shell code.  We use the "pipefail" option if available and
@@ -26,9 +29,13 @@ with {dash}[http://gondor.apana.org.au/~herbert/dash/] and
 
 To run the entire test suite with 8 tests running at once:
 
-  make -j8
+  make -j8 && prove -vw
+
+To run one individual test (Perl5):
+
+  prove -vw t/integration.t
 
-To run one individual test:
+To run one individual test (shell):
 
   make t0000-simple-http.sh
 
diff --git a/t/active-unix-socket.t b/t/active-unix-socket.t
new file mode 100644
index 0000000..ff731b5
--- /dev/null
+++ b/t/active-unix-socket.t
@@ -0,0 +1,117 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use IO::Socket::UNIX;
+use autodie;
+no autodie 'kill';
+my %to_kill;
+END { kill('TERM', values(%to_kill)) if keys %to_kill }
+my $u1 = "$tmpdir/u1.sock";
+my $u2 = "$tmpdir/u2.sock";
+{
+        open my $fh, '>', "$tmpdir/u1.conf.rb";
+        print $fh <<EOM;
+pid "$tmpdir/u.pid"
+listen "$u1"
+stderr_path "$err_log"
+EOM
+        close $fh;
+
+        open $fh, '>', "$tmpdir/u2.conf.rb";
+        print $fh <<EOM;
+pid "$tmpdir/u.pid"
+listen "$u2"
+stderr_path "$tmpdir/err2.log"
+EOM
+        close $fh;
+
+        open $fh, '>', "$tmpdir/u3.conf.rb";
+        print $fh <<EOM;
+pid "$tmpdir/u3.pid"
+listen "$u1"
+stderr_path "$tmpdir/err3.log"
+EOM
+        close $fh;
+}
+
+my @uarg = qw(-D -E none t/integration.ru);
+
+# this pipe will be used to notify us when all daemons die:
+pipe(my $p0, my $p1);
+fcntl($p1, POSIX::F_SETFD, 0);
+
+# start the first instance
+unicorn('-c', "$tmpdir/u1.conf.rb", @uarg)->join;
+is($?, 0, 'daemonized 1st process');
+chomp($to_kill{u1} = slurp("$tmpdir/u.pid"));
+like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file');
+
+chomp(my $worker_pid = readline(unix_start($u1, 'GET /pid')));
+like($worker_pid, qr/\A\d+\z/s, 'captured worker pid');
+ok(kill(0, $worker_pid), 'worker is kill-able');
+
+
+# 2nd process conflicts on PID
+unicorn('-c', "$tmpdir/u2.conf.rb", @uarg)->join;
+isnt($?, 0, 'conflicting PID file fails to start');
+
+chomp(my $pidf = slurp("$tmpdir/u.pid"));
+is($pidf, $to_kill{u1}, 'pid file contents unchanged after start failure');
+
+chomp(my $pid2 = readline(unix_start($u1, 'GET /pid')));
+is($worker_pid, $pid2, 'worker PID unchanged');
+
+
+# 3rd process conflicts on socket
+unicorn('-c', "$tmpdir/u3.conf.rb", @uarg)->join;
+isnt($?, 0, 'conflicting UNIX socket fails to start');
+
+chomp($pid2 = readline(unix_start($u1, 'GET /pid')));
+is($worker_pid, $pid2, 'worker PID still unchanged');
+
+chomp($pidf = slurp("$tmpdir/u.pid"));
+is($pidf, $to_kill{u1}, 'pid file contents unchanged after 2nd start failure');
+
+{ # teardown initial process via SIGKILL
+        ok(kill('KILL', delete $to_kill{u1}), 'SIGKILL initial daemon');
+        close $p1;
+        vec(my $rvec = '', fileno($p0), 1) = 1;
+        is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP');
+        is(my $undef = <$p0>, undef, 'process closed pipe writer at exit');
+        ok(-f "$tmpdir/u.pid", 'pid file stayed after SIGKILL');
+        ok(-S $u1, 'socket stayed after SIGKILL');
+        is(IO::Socket::UNIX->new(Peer => $u1, Type => SOCK_STREAM), undef,
+                'fail to connect to u1');
+        for (1..50) { # wait for init process to reap worker
+                kill(0, $worker_pid) or last;
+                sleep 0.011;
+        }
+        ok(!kill(0, $worker_pid), 'worker gone after parent dies');
+}
+
+# restart the first instance
+{
+        pipe($p0, $p1);
+        fcntl($p1, POSIX::F_SETFD, 0);
+        unicorn('-c', "$tmpdir/u1.conf.rb", @uarg)->join;
+        is($?, 0, 'daemonized 1st process');
+        chomp($to_kill{u1} = slurp("$tmpdir/u.pid"));
+        like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file');
+
+        chomp($pid2 = readline(unix_start($u1, 'GET /pid')));
+        like($pid2, qr/\A\d+\z/, 'worker running');
+
+        ok(kill('TERM', delete $to_kill{u1}), 'SIGTERM restarted daemon');
+        close $p1;
+        vec(my $rvec = '', fileno($p0), 1) = 1;
+        is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP');
+        is(my $undef = <$p0>, undef, 'process closed pipe writer at exit');
+        ok(!-f "$tmpdir/u.pid", 'pid file gone after SIGTERM');
+        ok(-S $u1, 'socket stays after SIGTERM');
+}
+
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/back-out-of-upgrade.t b/t/back-out-of-upgrade.t
new file mode 100644
index 0000000..cf3b09f
--- /dev/null
+++ b/t/back-out-of-upgrade.t
@@ -0,0 +1,44 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+# test backing out of USR2 upgrade
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+my $srv = tcp_server();
+mkfifo_die $fifo;
+write_file '>', $u_conf, <<EOM;
+preload_app true
+stderr_path "$err_log"
+pid "$pid_file"
+after_fork { |s,w| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+my $ar = unicorn(qw(-E none t/pid.ru -c), $u_conf, { 3 => $srv });
+
+like(my $wpid_orig_1 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker pid');
+
+ok $ar->do_kill('USR2'), 'USR2 to start upgrade';
+ok $ar->do_kill('WINCH'), 'drop old worker';
+
+like(my $wpid_new = slurp($fifo), qr/\Apid=\d+\z/a, 'got pid from new master');
+chomp(my $new_pid = slurp($pid_file));
+isnt $new_pid, $ar->{pid}, 'PID file changed';
+chomp(my $pid_oldbin = slurp("$pid_file.oldbin"));
+is $pid_oldbin, $ar->{pid}, '.oldbin PID valid';
+
+ok $ar->do_kill('HUP'), 'HUP old master';
+like(my $wpid_orig_2 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker new pid');
+ok kill('QUIT', $new_pid), 'abort old master';
+kill_until_dead $new_pid;
+
+my ($st, $hdr, $req_pid) = do_req $srv, 'GET /';
+chomp $req_pid;
+is $wpid_orig_2, "pid=$req_pid", 'new worker on old worker serves';
+
+ok !-f "$pid_file.oldbin", '.oldbin PID file gone';
+chomp(my $old_pid = slurp($pid_file));
+is $old_pid, $ar->{pid}, 'PID file restored';
+
+my @log = grep !/ERROR -- : reaped .*? exec\(\)-ed/, slurp($err_log);
+check_stderr @log;
+undef $tmpdir;
+done_testing;
diff --git a/t/bin/content-md5-put b/t/bin/content-md5-put
deleted file mode 100755
index 01da0bb..0000000
--- a/t/bin/content-md5-put
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env ruby
-# -*- encoding: binary -*-
-# simple chunked HTTP PUT request generator (and just that),
-# it reads stdin and writes to stdout so socat can write to a
-# UNIX or TCP socket (or to another filter or file) along with
-# a Content-MD5 trailer.
-require 'digest/md5'
-$stdout.sync = $stderr.sync = true
-$stdout.binmode
-$stdin.binmode
-
-bs = ENV['bs'] ? ENV['bs'].to_i : 4096
-
-if ARGV.grep("--no-headers").empty?
-  $stdout.write(
-      "PUT / HTTP/1.1\r\n" \
-      "Host: example.com\r\n" \
-      "Transfer-Encoding: chunked\r\n" \
-      "Trailer: Content-MD5\r\n" \
-      "\r\n"
-    )
-end
-
-digest = Digest::MD5.new
-if buf = $stdin.readpartial(bs)
-  begin
-    digest.update(buf)
-    $stdout.write("%x\r\n" % [ buf.size ])
-    $stdout.write(buf)
-    $stdout.write("\r\n")
-  end while $stdin.read(bs, buf)
-end
-
-digest = [ digest.digest ].pack('m').strip
-$stdout.write("0\r\n")
-$stdout.write("Content-MD5: #{digest}\r\n\r\n")
diff --git a/t/bin/sha1sum.rb b/t/bin/sha1sum.rb
deleted file mode 100755
index 53d68ce..0000000
--- a/t/bin/sha1sum.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env ruby
-# -*- encoding: binary -*-
-# Reads from stdin and outputs the SHA1 hex digest of the input
-
-require 'digest/sha1'
-$stdout.sync = $stderr.sync = true
-$stdout.binmode
-$stdin.binmode
-bs = 16384
-digest = Digest::SHA1.new
-if buf = $stdin.read(bs)
-  begin
-    digest.update(buf)
-  end while $stdin.read(bs, buf)
-end
-
-$stdout.syswrite("#{digest.hexdigest}\n")
diff --git a/t/broken-app.ru b/t/broken-app.ru
index d05d7ab..5966bff 100644
--- a/t/broken-app.ru
+++ b/t/broken-app.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # we do not want Rack::Lint or anything to protect us
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
diff --git a/t/t0116.ru b/t/client_body_buffer_size.ru
index fab5fce..1a0fb16 100644
--- a/t/t0116.ru
+++ b/t/client_body_buffer_size.ru
@@ -1,6 +1,5 @@
 #\ -E none
-use Rack::ContentLength
-use Rack::ContentType, 'text/plain'
+# frozen_string_literal: false
 app = lambda do |env|
   input = env['rack.input']
   case env["PATH_INFO"]
diff --git a/t/client_body_buffer_size.t b/t/client_body_buffer_size.t
new file mode 100644
index 0000000..d479901
--- /dev/null
+++ b/t/client_body_buffer_size.t
@@ -0,0 +1,80 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+open my $conf_fh, '>', $u_conf;
+$conf_fh->autoflush(1);
+print $conf_fh <<EOM;
+client_body_buffer_size 0
+EOM
+my $srv = tcp_server();
+my $host_port = tcp_host_port($srv);
+my @uarg = (qw(-E none t/client_body_buffer_size.ru -c), $u_conf);
+my $ar = unicorn(@uarg, { 3 => $srv });
+my ($c, $status, $hdr);
+my $mem_class = 'StringIO';
+my $fs_class = 'Unicorn::TmpIO';
+
+$c = tcp_start($srv, "PUT /input_class HTTP/1.0\r\nContent-Length: 0");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, 'zero-byte file is StringIO');
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1");
+print $c '.';
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 byte file is filesystem-backed');
+
+
+my $fifo = "$tmpdir/fifo";
+POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!";
+seek($conf_fh, 0, SEEK_SET);
+truncate($conf_fh, 0);
+print $conf_fh <<EOM;
+after_fork { |_,_| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+$ar->do_kill('HUP');
+open my $fifo_fh, '<', $fifo;
+like(my $wpid = readline($fifo_fh), qr/\Apid=\d+\z/a ,
+        'reloaded w/ default client_body_buffer_size');
+
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, 'class for a 1 byte file is memory-backed');
+
+
+my $one_meg = 1024 ** 2;
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 megabyte file is FS-backed');
+
+# reload with bigger client_body_buffer_size
+say $conf_fh "client_body_buffer_size $one_meg";
+$ar->do_kill('HUP');
+open $fifo_fh, '<', $fifo;
+like($wpid = readline($fifo_fh), qr/\Apid=\d+\z/a ,
+        'reloaded w/ bigger client_body_buffer_size');
+
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, '1 megabyte file is now memory-backed');
+
+my $too_big = $one_meg + 1;
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $too_big");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 megabyte + 1 byte file is FS-backed');
+
+
+undef $ar;
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/detach.ru b/t/detach.ru
index bbd998e..8d35951 100644
--- a/t/detach.ru
+++ b/t/detach.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentType, "text/plain"
 fifo_path = ENV["TEST_FIFO"] or abort "TEST_FIFO not set"
 run lambda { |env|
diff --git a/t/env.ru b/t/env.ru
index 388412e..86c3cfa 100644
--- a/t/env.ru
+++ b/t/env.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env| [ 200, {}, [ env.inspect << "\n" ] ] }
diff --git a/t/fails-rack-lint.ru b/t/fails-rack-lint.ru
index 82bfb5f..8b8b5ec 100644
--- a/t/fails-rack-lint.ru
+++ b/t/fails-rack-lint.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # This rack app returns an invalid status code, which will cause
 # Rack::Lint to throw an exception if it is present.  This
 # is used to check whether Rack::Lint is in the stack or not.
diff --git a/t/heartbeat-timeout.ru b/t/heartbeat-timeout.ru
index d9904e8..ccc6a8e 100644
--- a/t/heartbeat-timeout.ru
+++ b/t/heartbeat-timeout.ru
@@ -1,5 +1,6 @@
+# frozen_string_literal: false
 use Rack::ContentLength
-headers = { 'Content-Type' => 'text/plain' }
+headers = { 'content-type' => 'text/plain' }
 run lambda { |env|
   case env['PATH_INFO']
   when "/block-forever"
@@ -7,6 +8,6 @@ run lambda { |env|
     sleep # in case STOP signal is not received in time
     [ 500, headers, [ "Should never get here\n" ] ]
   else
-    [ 200, headers, [ "#$$\n" ] ]
+    [ 200, headers, [ "#$$" ] ]
   end
 }
diff --git a/t/heartbeat-timeout.t b/t/heartbeat-timeout.t
new file mode 100644
index 0000000..694867a
--- /dev/null
+++ b/t/heartbeat-timeout.t
@@ -0,0 +1,62 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+mkdir "$tmpdir/alt";
+my $srv = tcp_server();
+open my $fh, '>', $u_conf;
+print $fh <<EOM;
+pid "$tmpdir/pid"
+preload_app true
+stderr_path "$err_log"
+timeout 3 # WORST FEATURE EVER
+EOM
+close $fh;
+
+my $ar = unicorn(qw(-E none t/heartbeat-timeout.ru -c), $u_conf, { 3 => $srv });
+
+my ($status, $hdr, $wpid) = do_req($srv, 'GET /pid HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds');
+like($wpid, qr/\A[0-9]+\z/, 'worker is running');
+
+my $t0 = clock_gettime(CLOCK_MONOTONIC);
+my $c = tcp_start($srv, 'GET /block-forever HTTP/1.0');
+vec(my $rvec = '', fileno($c), 1) = 1;
+is(select($rvec, undef, undef, 6), 1, 'got readiness');
+$c->blocking(0);
+is(sysread($c, my $buf, 128), 0, 'got EOF response');
+my $elapsed = clock_gettime(CLOCK_MONOTONIC) - $t0;
+ok($elapsed > 3, 'timeout took >3s');
+
+my @timeout_err = slurp($err_log);
+truncate($err_log, 0);
+is(grep(/timeout \(\d+s > 3s\), killing/, @timeout_err), 1,
+    'noted timeout error') or diag explain(\@timeout_err);
+
+# did it respawn?
+($status, $hdr, my $new_pid) = do_req($srv, 'GET /pid HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds');
+isnt($new_pid, $wpid, 'spawned new worker');
+
+diag 'SIGSTOP for 4 seconds...';
+$ar->do_kill('STOP');
+sleep 4;
+$ar->do_kill('CONT');
+for my $i (1..2) {
+        ($status, $hdr, my $spid) = do_req($srv, 'GET /pid HTTP/1.0');
+        like($status, qr!\AHTTP/1\.[01] 200\b!,
+                "PID request succeeds #$i after STOP+CONT");
+        is($new_pid, $spid, "worker pid unchanged after STOP+CONT #$i");
+        if ($i == 1) {
+                diag 'sleeping 2s to ensure timeout is not delayed';
+                sleep 2;
+        }
+}
+
+$ar->join('TERM');
+check_stderr;
+undef $tmpdir;
+
+done_testing;
diff --git a/t/hijack.ru b/t/hijack.ru
deleted file mode 100644
index 02260e2..0000000
--- a/t/hijack.ru
+++ /dev/null
@@ -1,55 +0,0 @@
-use Rack::Lint
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-class DieIfUsed
-  @@n = 0
-  def each
-    abort "body.each called after response hijack\n"
-  end
-
-  def close
-    warn "closed DieIfUsed #{@@n += 1}\n"
-  end
-end
-
-envs = []
-
-run lambda { |env|
-  case env["PATH_INFO"]
-  when "/hijack_req"
-    if env["rack.hijack?"]
-      io = env["rack.hijack"].call
-      envs << env
-      if io.respond_to?(:read_nonblock) &&
-         env["rack.hijack_io"].respond_to?(:read_nonblock)
-
-        # exercise both, since we Rack::Lint may use different objects
-        env["rack.hijack_io"].write("HTTP/1.0 200 OK\r\n\r\n")
-        io.write("request.hijacked")
-        io.close
-        return [ 500, {}, DieIfUsed.new ]
-      end
-    end
-    [ 500, {}, [ "hijack BAD\n" ] ]
-  when "/hijack_res"
-    r = "response.hijacked"
-    [ 200,
-      {
-        "Content-Length" => r.bytesize.to_s,
-        "rack.hijack" => proc do |io|
-          envs << env
-          io.write(r)
-          io.close
-        end
-      },
-      DieIfUsed.new
-    ]
-  when "/normal_env_id"
-    b = "#{env.object_id}\n"
-    h = {
-      'Content-Type' => 'text/plain',
-      'Content-Length' => b.bytesize.to_s,
-    }
-    [ 200, h, [ b ] ]
-  end
-}
diff --git a/t/integration.ru b/t/integration.ru
new file mode 100644
index 0000000..6df481c
--- /dev/null
+++ b/t/integration.ru
@@ -0,0 +1,116 @@
+#!ruby
+# frozen_string_literal: false
+# Copyright (C) unicorn hackers <unicorn-public@80x24.org>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+# this goes for t/integration.t  We'll try to put as many tests
+# in here as possible to avoid startup overhead of Ruby.
+
+def early_hints(env, val)
+  env['rack.early_hints'].call('link' => val) # val may be ary or string
+  [ 200, {}, [ val.class.to_s ] ]
+end
+
+$orig_rack_200 = nil
+def tweak_status_code
+  $orig_rack_200 = Rack::Utils::HTTP_STATUS_CODES[200]
+  Rack::Utils::HTTP_STATUS_CODES[200] = "HI"
+  [ 200, {}, [] ]
+end
+
+def restore_status_code
+  $orig_rack_200 or return [ 500, {}, [] ]
+  Rack::Utils::HTTP_STATUS_CODES[200] = $orig_rack_200
+  [ 200, {}, [] ]
+end
+
+class WriteOnClose
+  def each(&block)
+    @callback = block
+  end
+
+  def close
+    @callback.call "7\r\nGoodbye\r\n0\r\n\r\n"
+  end
+end
+
+def write_on_close
+  [ 200, { 'transfer-encoding' => 'chunked' }, WriteOnClose.new ]
+end
+
+def env_dump(env)
+  require 'json'
+  h = {}
+  env.each do |k,v|
+    case v
+    when String, Integer, true, false; h[k] = v
+    else
+      case k
+      when 'rack.version', 'rack.after_reply'; h[k] = v
+      end
+    end
+  end
+  h.to_json
+end
+
+def rack_input_tests(env)
+  return [ 100, {}, [] ] if /\A100-continue\z/i =~ env['HTTP_EXPECT']
+  cap = 16384
+  require 'digest/md5'
+  dig = Digest::MD5.new
+  input = env['rack.input']
+  case env['PATH_INFO']
+  when '/rack_input/size_first'; input.size
+  when '/rack_input/rewind_first'; input.rewind
+  when '/rack_input'; # OK
+  else
+    abort "bad path: #{env['PATH_INFO']}"
+  end
+  if buf = input.read(rand(cap))
+    begin
+      raise "#{buf.size} > #{cap}" if buf.size > cap
+      dig.update(buf)
+    end while input.read(rand(cap), buf)
+    buf.clear # remove this call if Ruby ever gets escape analysis
+  end
+  h = { 'content-type' => 'text/plain' }
+  if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i
+    cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']]
+    cmd5_bin = cmd5_b64.unpack('m')[0]
+    if cmd5_bin != dig.digest
+      h['content-length'] = cmd5_b64.size.to_s
+      return [ 500, h, [ cmd5_b64 ] ]
+    end
+  end
+  h['content-length'] = '32'
+  [ 200, h, [ dig.hexdigest ] ]
+end
+
+run(lambda do |env|
+  case env['REQUEST_METHOD']
+  when 'GET'
+    case env['PATH_INFO']
+    when '/rack-2-newline-headers'; [ 200, { 'X-R2' => "a\nb\nc" }, [] ]
+    when '/rack-3-array-headers'; [ 200, { 'x-r3' => %w(a b c) }, [] ]
+    when '/nil-header-value'; [ 200, { 'X-Nil' => nil }, [] ]
+    when '/unknown-status-pass-through'; [ '666 I AM THE BEAST', {}, [] ]
+    when '/env_dump'; [ 200, {}, [ env_dump(env) ] ]
+    when '/write_on_close'; write_on_close
+    when '/pid'; [ 200, {}, [ "#$$\n" ] ]
+    when '/early_hints_rack2'; early_hints(env, "r\n2")
+    when '/early_hints_rack3'; early_hints(env, %w(r 3))
+    when '/broken_app'; raise RuntimeError, 'hello'
+    else '/'; [ 200, {}, [ env_dump(env) ] ]
+    end # case PATH_INFO (GET)
+  when 'POST'
+    case env['PATH_INFO']
+    when '/tweak-status-code'; tweak_status_code
+    when '/restore-status-code'; restore_status_code
+    end # case PATH_INFO (POST)
+    # ...
+  when 'PUT'
+    case env['PATH_INFO']
+    when %r{\A/rack_input}; rack_input_tests(env)
+    end
+  end # case REQUEST_METHOD
+end) # run
diff --git a/t/integration.t b/t/integration.t
new file mode 100644
index 0000000..d17ace0
--- /dev/null
+++ b/t/integration.t
@@ -0,0 +1,357 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+# This is the main integration test for fast-ish things to minimize
+# Ruby startup time penalties.
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+use Socket qw(SOL_SOCKET SO_KEEPALIVE SHUT_WR);
+our $srv = tcp_server();
+our $host_port = tcp_host_port($srv);
+
+if ('ensure Perl does not set SO_KEEPALIVE by default') {
+        my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
+        unpack('i', $val) == 0 or
+                setsockopt($srv, SOL_SOCKET, SO_KEEPALIVE, pack('i', 0));
+        $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
+}
+my $t0 = time;
+open my $conf_fh, '>', $u_conf;
+$conf_fh->autoflush(1);
+my $u1 = "$tmpdir/u1";
+print $conf_fh <<EOM;
+early_hints true
+listen "$u1"
+EOM
+my $ar = unicorn(qw(-E none t/integration.ru -c), $u_conf, { 3 => $srv });
+my $curl = which('curl');
+local $ENV{NO_PROXY} = '*'; # for curl
+my $fifo = "$tmpdir/fifo";
+POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!";
+my %PUT = (
+        chunked_md5 => sub {
+                my ($in, $out, $path, %opt) = @_;
+                my $dig = Digest::MD5->new;
+                print $out <<EOM;
+PUT $path HTTP/1.1\r
+Transfer-Encoding: chunked\r
+Trailer: Content-MD5\r
+\r
+EOM
+                my ($buf, $r);
+                while (1) {
+                        $r = read($in, $buf, 999 + int(rand(0xffff)));
+                        last if $r == 0;
+                        printf $out "%x\r\n", length($buf);
+                        print $out $buf, "\r\n";
+                        $dig->add($buf);
+                }
+                print $out "0\r\nContent-MD5: ", $dig->b64digest, "\r\n\r\n";
+        },
+        identity => sub {
+                my ($in, $out, $path, %opt) = @_;
+                my $clen = $opt{-s} // -s $in;
+                print $out <<EOM;
+PUT $path HTTP/1.0\r
+Content-Length: $clen\r
+\r
+EOM
+                my ($buf, $r, $len, $bs);
+                while ($clen) {
+                        $bs = 999 + int(rand(0xffff));
+                        $len = $clen > $bs ? $bs : $clen;
+                        $r = read($in, $buf, $len);
+                        die 'premature EOF' if $r == 0;
+                        print $out $buf;
+                        $clen -= $r;
+                }
+        },
+);
+
+my ($c, $status, $hdr, $bdy);
+
+# response header tests
+($status, $hdr) = do_req($srv, 'GET /rack-2-newline-headers HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+my $orig_200_status = $status;
+is_deeply([ grep(/^X-R2: /, @$hdr) ],
+        [ 'X-R2: a', 'X-R2: b', 'X-R2: c' ],
+        'rack 2 LF-delimited headers supported') or diag(explain($hdr));
+
+{
+        my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
+        is(unpack('i', $val), 1, 'SO_KEEPALIVE set on inherited socket');
+}
+
+SKIP: { # Date header check
+        my @d = grep(/^Date: /i, @$hdr);
+        is(scalar(@d), 1, 'got one date header') or diag(explain(\@d));
+        eval { require HTTP::Date } or skip "HTTP::Date missing: $@", 1;
+        $d[0] =~ s/^Date: //i or die 'BUG: did not strip date: prefix';
+        my $t = HTTP::Date::str2time($d[0]);
+        my $now = time;
+        ok($t >= ($t0 - 1) && $t > 0 && $t <= ($now + 1), 'valid date') or
+                diag(explain(["t=$t t0=$t0 now=$now", $!, \@d]));
+};
+
+
+($status, $hdr) = do_req($srv, 'GET /rack-3-array-headers HTTP/1.0');
+is_deeply([ grep(/^x-r3: /, @$hdr) ],
+        [ 'x-r3: a', 'x-r3: b', 'x-r3: c' ],
+        'rack 3 array headers supported') or diag(explain($hdr));
+
+SKIP: {
+        eval { require JSON::PP } or skip "JSON::PP missing: $@", 1;
+        ($status, $hdr, my $json) = do_req $srv, 'GET /env_dump';
+        is($status, undef, 'no status for HTTP/0.9');
+        is($hdr, undef, 'no header for HTTP/0.9');
+        unlike($json, qr/^Connection: /smi, 'no connection header for 0.9');
+        unlike($json, qr!\AHTTP/!s, 'no HTTP/1.x prefix for 0.9');
+        my $env = JSON::PP->new->decode($json);
+        is(ref($env), 'HASH', 'JSON decoded body to hashref');
+        is($env->{SERVER_PROTOCOL}, 'HTTP/0.9', 'SERVER_PROTOCOL is 0.9');
+}
+
+# cf. <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com>
+($status, $hdr) = do_req($srv, 'GET /nil-header-value HTTP/1.0');
+is_deeply([grep(/^X-Nil:/, @$hdr)], ['X-Nil: '],
+        'nil header value accepted for broken apps') or diag(explain($hdr));
+
+check_stderr;
+($status, $hdr, $bdy) = do_req($srv, 'GET /broken_app HTTP/1.0');
+like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on broken endpoint');
+is($bdy, undef, 'no response body after exception');
+truncate($errfh, 0);
+
+my $ck_early_hints = sub {
+        my ($note) = @_;
+        $c = unix_start($u1, 'GET /early_hints_rack2 HTTP/1.0');
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 2 value');
+        is_deeply(['link: r', 'link: 2'], $hdr, 'rack 2 hints match '.$note);
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
+        is(readline($c), 'String', 'early hints used a String for rack 2');
+
+        $c = unix_start($u1, 'GET /early_hints_rack3 HTTP/1.0');
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 3');
+        is_deeply(['link: r', 'link: 3'], $hdr, 'rack 3 hints match '.$note);
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
+        is(readline($c), 'Array', 'early hints used a String for rack 3');
+};
+$ck_early_hints->('ccc off'); # we'll retest later
+
+if ('TODO: ensure Rack::Utils::HTTP_STATUS_CODES is available') {
+        ($status, $hdr) = do_req $srv, 'POST /tweak-status-code HTTP/1.0';
+        like($status, qr!\AHTTP/1\.[01] 200 HI\b!, 'status tweaked');
+
+        ($status, $hdr) = do_req $srv, 'POST /restore-status-code HTTP/1.0';
+        is($status, $orig_200_status, 'original status restored');
+}
+
+SKIP: {
+        eval { require HTTP::Tiny } or skip "HTTP::Tiny missing: $@", 1;
+        my $ht = HTTP::Tiny->new;
+        my $res = $ht->get("http://$host_port/write_on_close");
+        is($res->{content}, 'Goodbye', 'write-on-close body read');
+}
+
+if ('bad requests') {
+        ($status, $hdr) = do_req $srv, 'GET /env_dump HTTP/1/1';
+        like($status, qr!\AHTTP/1\.[01] 400 \b!, 'got 400 on bad request');
+
+        $c = tcp_start($srv);
+        print $c 'GET /';
+        my $buf = join('', (0..9), 'ab');
+        for (0..1023) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!,
+                '414 on REQUEST_PATH > (12 * 1024)');
+
+        $c = tcp_start($srv);
+        print $c 'GET /hello-world?a';
+        $buf = join('', (0..9));
+        for (0..1023) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!,
+                '414 on QUERY_STRING > (10 * 1024)');
+
+        $c = tcp_start($srv);
+        print $c 'GET /hello-world#a';
+        $buf = join('', (0..9), 'a'..'f');
+        for (0..63) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on FRAGMENT > (1024)');
+}
+
+# input tests
+my ($blob_size, $blob_hash);
+SKIP: {
+        skip 'SKIP_EXPENSIVE on', 1 if $ENV{SKIP_EXPENSIVE};
+        CORE::open(my $rh, '<', 't/random_blob') or
+                skip "t/random_blob not generated $!", 1;
+        $blob_size = -s $rh;
+        require Digest::MD5;
+        $blob_hash = Digest::MD5->new->addfile($rh)->hexdigest;
+
+        my $ck_hash = sub {
+                my ($sub, $path, %opt) = @_;
+                seek($rh, 0, SEEK_SET);
+                $c = tcp_start($srv);
+                $c->autoflush($opt{sync} // 0);
+                $PUT{$sub}->($rh, $c, $path, %opt);
+                defined($opt{overwrite}) and
+                        print { $c } ('x' x $opt{overwrite});
+                $c->flush or die $!;
+                shutdown($c, SHUT_WR);
+                ($status, $hdr) = slurp_hdr($c);
+                is(readline($c), $blob_hash, "$sub $path");
+        };
+        $ck_hash->('identity', '/rack_input', -s => $blob_size);
+        $ck_hash->('chunked_md5', '/rack_input');
+        $ck_hash->('identity', '/rack_input/size_first', -s => $blob_size);
+        $ck_hash->('identity', '/rack_input/rewind_first', -s => $blob_size);
+        $ck_hash->('chunked_md5', '/rack_input/size_first');
+        $ck_hash->('chunked_md5', '/rack_input/rewind_first');
+
+        $ck_hash->('identity', '/rack_input', -s => $blob_size, sync => 1);
+        $ck_hash->('chunked_md5', '/rack_input', sync => 1);
+
+        # ensure small overwrites don't get checksummed
+        $ck_hash->('identity', '/rack_input', -s => $blob_size,
+                        overwrite => 1); # one extra byte
+        unlike(slurp($err_log), qr/ClientShutdown/,
+                'no overreads after client SHUT_WR');
+
+        # excessive overwrite truncated
+        $c = tcp_start($srv);
+        $c->autoflush(0);
+        print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 1\r\n\r\n";
+        if (1) {
+                local $SIG{PIPE} = 'IGNORE';
+                my $buf = "\0" x 8192;
+                my $n = 0;
+                my $end = time + 5;
+                $! = 0;
+                while (print $c $buf and time < $end) { ++$n }
+                ok($!, 'overwrite truncated') or diag "n=$n err=$! ".time;
+                undef $c;
+        }
+
+        # client shutdown early
+        $c = tcp_start($srv);
+        $c->autoflush(0);
+        print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 16384\r\n\r\n";
+        if (1) {
+                local $SIG{PIPE} = 'IGNORE';
+                print $c 'too short body';
+                shutdown($c, SHUT_WR);
+                vec(my $rvec = '', fileno($c), 1) = 1;
+                select($rvec, undef, undef, 10) or BAIL_OUT "timed out";
+                my $buf = <$c>;
+                is($buf, undef, 'server aborted after client SHUT_WR');
+                undef $c;
+        }
+
+        $curl // skip 'no curl found in PATH', 1;
+
+        my ($copt, $cout);
+        my $url = "http://$host_port/rack_input";
+        my $do_curl = sub {
+                my (@arg) = @_;
+                pipe(my $cout, $copt->{1});
+                open $copt->{2}, '>', "$tmpdir/curl.err";
+                my $cpid = spawn($curl, '-sSf', @arg, $url, $copt);
+                close(delete $copt->{1});
+                is(readline($cout), $blob_hash, "curl @arg response");
+                is(waitpid($cpid, 0), $cpid, "curl @arg exited");
+                is($?, 0, "no error from curl @arg");
+                is(slurp("$tmpdir/curl.err"), '', "no stderr from curl @arg");
+        };
+
+        $do_curl->(qw(-T t/random_blob));
+
+        seek($rh, 0, SEEK_SET);
+        $copt->{0} = $rh;
+        $do_curl->('-T-');
+
+        diag 'testing Unicorn::PrereadInput...';
+        local $srv = tcp_server();
+        local $host_port = tcp_host_port($srv);
+        check_stderr;
+        truncate($errfh, 0);
+
+        my $pri = unicorn(qw(-E none t/preread_input.ru), { 3 => $srv });
+        $url = "http://$host_port/";
+
+        $do_curl->(qw(-T t/random_blob));
+        seek($rh, 0, SEEK_SET);
+        $copt->{0} = $rh;
+        $do_curl->('-T-');
+
+        my @pr_err = slurp("$tmpdir/err.log");
+        is(scalar(grep(/app dispatch:/, @pr_err)), 2, 'app dispatched twice');
+
+        # abort a chunked request by blocking curl on a FIFO:
+        $c = tcp_start($srv, "PUT / HTTP/1.1\r\nTransfer-Encoding: chunked");
+        close $c;
+        @pr_err = slurp("$tmpdir/err.log");
+        is(scalar(grep(/app dispatch:/, @pr_err)), 2,
+                        'app did not dispatch on aborted request');
+        undef $pri;
+        check_stderr;
+        diag 'Unicorn::PrereadInput middleware tests done';
+}
+
+# ... more stuff here
+
+# SIGHUP-able stuff goes here
+
+if ('check_client_connection') {
+        print $conf_fh <<EOM; # appending to existing
+check_client_connection true
+after_fork { |_,_| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+        $ar->do_kill('HUP');
+        open my $fifo_fh, '<', $fifo;
+        my $wpid = readline($fifo_fh);
+        like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
+        $ck_early_hints->('ccc on');
+}
+
+if ('max_header_len internal API') {
+        undef $c;
+        my $req = 'GET / HTTP/1.0';
+        my $len = length($req."\r\n\r\n");
+        print $conf_fh <<EOM; # appending to existing
+Unicorn::HttpParser.max_header_len = $len
+EOM
+        $ar->do_kill('HUP');
+        open my $fifo_fh, '<', $fifo;
+        my $wpid = readline($fifo_fh);
+        like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
+        close $fifo_fh;
+        $wpid =~ s/\Apid=// or die;
+        ok(CORE::kill(0, $wpid), 'worker PID retrieved');
+
+        ($status, $hdr) = do_req($srv, $req);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'minimal request succeeds');
+
+        ($status, $hdr) = do_req($srv, 'GET /xxxxxx HTTP/1.0');
+        like($status, qr!\AHTTP/1\.[01] 413\b!, 'big request fails');
+}
+
+
+undef $ar;
+
+check_stderr;
+
+undef $tmpdir;
+done_testing;
diff --git a/t/lib.perl b/t/lib.perl
new file mode 100644
index 0000000..8c842b1
--- /dev/null
+++ b/t/lib.perl
@@ -0,0 +1,309 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@80x24.org>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+package UnicornTest;
+use v5.14;
+use parent qw(Exporter);
+use autodie;
+use Test::More;
+use Socket qw(SOMAXCONN);
+use Time::HiRes qw(sleep time);
+use IO::Socket::INET;
+use IO::Socket::UNIX;
+use Carp qw(croak);
+use POSIX qw(dup2 _exit setpgid :signal_h SEEK_SET F_SETFD);
+use File::Temp 0.19 (); # 0.19 for ->newdir
+our ($tmpdir, $errfh, $err_log, $u_sock, $u_conf, $daemon_pid,
+        $pid_file, $wtest_sock, $fifo);
+our @EXPORT = qw(unicorn slurp tcp_server tcp_start unicorn
+        $tmpdir $errfh $err_log $u_sock $u_conf $daemon_pid $pid_file
+        $wtest_sock $fifo
+        SEEK_SET tcp_host_port which spawn check_stderr unix_start slurp_hdr
+        do_req stop_daemon sleep time mkfifo_die kill_until_dead write_file);
+
+my ($base) = ($0 =~ m!\b([^/]+)\.[^\.]+\z!);
+$tmpdir = File::Temp->newdir("unicorn-$base-XXXX", TMPDIR => 1);
+
+$wtest_sock = "$tmpdir/wtest.sock";
+$err_log = "$tmpdir/err.log";
+$pid_file = "$tmpdir/pid";
+$fifo = "$tmpdir/fifo";
+$u_sock = "$tmpdir/u.sock";
+$u_conf = "$tmpdir/u.conf.rb";
+open($errfh, '>>', $err_log);
+
+if (my $t = $ENV{TAIL}) {
+        my @tail = $t =~ /tail/ ? split(/\s+/, $t) : (qw(tail -F));
+        push @tail, $err_log;
+        my $pid = fork;
+        if ($pid == 0) {
+                open STDOUT, '>&', \*STDERR;
+                exec @tail;
+                die "exec(@tail): $!";
+        }
+        say "# @tail";
+        sleep 0.2;
+        UnicornTest::AutoReap->new($pid);
+}
+
+sub kill_until_dead ($;%) {
+        my ($pid, %opt) = @_;
+        my $tries = $opt{tries} // 1000;
+        my $sig = $opt{sig} // 0;
+        while (CORE::kill($sig, $pid) && --$tries) { sleep(0.01) }
+        $tries or croak "PID: $pid died after signal ($sig)";
+}
+
+sub stop_daemon (;$) {
+        my ($is_END) = @_;
+        kill('TERM', $daemon_pid);
+        kill_until_dead $daemon_pid;
+        if ($is_END && CORE::kill(0, $daemon_pid)) { # after done_testing
+                CORE::kill('KILL', $daemon_pid);
+                die "daemon_pid=$daemon_pid did not die";
+        } else {
+                ok(!CORE::kill(0, $daemon_pid), 'daemonized unicorn gone');
+                undef $daemon_pid;
+        }
+};
+
+END {
+        diag slurp($err_log) if $tmpdir;
+        stop_daemon(1) if defined $daemon_pid;
+};
+
+sub check_stderr (@) {
+        my @log = @_;
+        slurp($err_log) if !@log;
+        diag("@log") if $ENV{V};
+        my @err = grep(!/NameError.*Unicorn::Waiter/, grep(/error/i, @log));
+        @err = grep(!/failed to set accept_filter=/, @err);
+        @err = grep(!/perhaps accf_.*? needs to be loaded/, @err);
+        is_deeply(\@err, [], 'no unexpected errors in stderr');
+        is_deeply([grep(/SIGKILL/, @log)], [], 'no SIGKILL in stderr');
+}
+
+sub slurp_hdr {
+        my ($c) = @_;
+        local $/ = "\r\n\r\n"; # affects both readline+chomp
+        chomp(my $hdr = readline($c));
+        my ($status, @hdr) = split(/\r\n/, $hdr);
+        diag explain([ $status, \@hdr ]) if $ENV{V};
+        ($status, \@hdr);
+}
+
+sub unix_server (;$@) {
+        my $l = shift // $u_sock;
+        IO::Socket::UNIX->new(Listen => SOMAXCONN, Local => $l, Blocking => 0,
+                                Type => SOCK_STREAM, @_);
+}
+
+sub unix_connect ($) {
+        IO::Socket::UNIX->new(Peer => $_[0], Type => SOCK_STREAM);
+}
+
+sub tcp_server {
+        my %opt = (
+                ReuseAddr => 1,
+                Proto => 'tcp',
+                Type => SOCK_STREAM,
+                Listen => SOMAXCONN,
+                Blocking => 0,
+                @_,
+        );
+        eval {
+                die 'IPv4-only' if $ENV{TEST_IPV4_ONLY};
+                require IO::Socket::INET6;
+                IO::Socket::INET6->new(%opt, LocalAddr => '[::1]')
+        } || eval {
+                die 'IPv6-only' if $ENV{TEST_IPV6_ONLY};
+                IO::Socket::INET->new(%opt, LocalAddr => '127.0.0.1')
+        } || BAIL_OUT "failed to create TCP server: $! ($@)";
+}
+
+sub tcp_host_port {
+        my ($s) = @_;
+        my ($h, $p) = ($s->sockhost, $s->sockport);
+        my $ipv4 = $s->sockdomain == AF_INET;
+        if (wantarray) {
+                $ipv4 ? ($h, $p) : ("[$h]", $p);
+        } else {
+                $ipv4 ? "$h:$p" : "[$h]:$p";
+        }
+}
+
+sub unix_start ($@) {
+        my ($dst, @req) = @_;
+        my $s = unix_connect($dst) or BAIL_OUT "unix connect $dst: $!";
+        $s->autoflush(1);
+        print $s @req, "\r\n\r\n" if @req;
+        $s;
+}
+
+sub tcp_start ($@) {
+        my ($dst, @req) = @_;
+        my $addr = tcp_host_port($dst);
+        my $s = ref($dst)->new(
+                Proto => 'tcp',
+                Type => SOCK_STREAM,
+                PeerAddr => $addr,
+        ) or BAIL_OUT "failed to connect to $addr: $!";
+        $s->autoflush(1);
+        print $s @req, "\r\n\r\n" if @req;
+        $s;
+}
+
+sub slurp {
+        open my $fh, '<', $_[0];
+        local $/ if !wantarray;
+        readline($fh);
+}
+
+sub spawn {
+        my $env = ref($_[0]) eq 'HASH' ? shift : undef;
+        my $opt = ref($_[-1]) eq 'HASH' ? pop : {};
+        my @cmd = @_;
+        my $old = POSIX::SigSet->new;
+        my $set = POSIX::SigSet->new;
+        $set->fillset or die "sigfillset: $!";
+        sigprocmask(SIG_SETMASK, $set, $old) or die "SIG_SETMASK: $!";
+        pipe(my $r, my $w);
+        my $pid = fork;
+        if ($pid == 0) {
+                close $r;
+                $SIG{__DIE__} = sub {
+                        warn(@_);
+                        syswrite($w, my $num = $! + 0);
+                        _exit(1);
+                };
+
+                # pretend to be systemd (cf. sd_listen_fds(3))
+                my $cfd;
+                for ($cfd = 0; ($cfd < 3) || defined($opt->{$cfd}); $cfd++) {
+                        my $io = $opt->{$cfd} // next;
+                        my $pfd = fileno($io);
+                        if ($pfd == $cfd) {
+                                fcntl($io, F_SETFD, 0);
+                        } else {
+                                dup2($pfd, $cfd) // die "dup2($pfd, $cfd): $!";
+                        }
+                }
+                if (($cfd - 3) > 0) {
+                        $env->{LISTEN_PID} = $$;
+                        $env->{LISTEN_FDS} = $cfd - 3;
+                }
+
+                if (defined(my $pgid = $opt->{pgid})) {
+                        setpgid(0, $pgid) // die "setpgid(0, $pgid): $!";
+                }
+                $SIG{$_} = 'DEFAULT' for grep(!/^__/, keys %SIG);
+                if (defined(my $cd = $opt->{-C})) { chdir $cd }
+                $old->delset(POSIX::SIGCHLD) or die "sigdelset CHLD: $!";
+                sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK: ~CHLD: $!";
+                @ENV{keys %$env} = values(%$env) if $env;
+                exec { $cmd[0] } @cmd;
+                die "exec @cmd: $!";
+        }
+        close $w;
+        sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK(old): $!";
+        if (my $cerrnum = do { local $/, <$r> }) {
+                $! = $cerrnum;
+                die "@cmd PID=$pid died: $!";
+        }
+        $pid;
+}
+
+sub which {
+        my ($file) = @_;
+        return $file if index($file, '/') >= 0;
+        for my $p (split(/:/, $ENV{PATH})) {
+                $p .= "/$file";
+                return $p if -x $p;
+        }
+        undef;
+}
+
+# returns an AutoReap object
+sub unicorn {
+        my %env;
+        if (ref($_[0]) eq 'HASH') {
+                my $e = shift;
+                %env = %$e;
+        }
+        my @args = @_;
+        push(@args, {}) if ref($args[-1]) ne 'HASH';
+        $args[-1]->{2} //= $errfh; # stderr default
+
+        state $ruby = which($ENV{RUBY} // 'ruby');
+        state $lib = File::Spec->rel2abs('lib');
+        state $ver = $ENV{TEST_RUBY_VERSION} // `$ruby -e 'print RUBY_VERSION'`;
+        state $eng = $ENV{TEST_RUBY_ENGINE} // `$ruby -e 'print RUBY_ENGINE'`;
+        state $ext = File::Spec->rel2abs("test/$eng-$ver/ext/unicorn_http");
+        state $exe = File::Spec->rel2abs("test/$eng-$ver/bin/unicorn");
+        state $rl = $ENV{RUBYLIB} ? "$lib:$ext:$ENV{RUBYLIB}" : "$lib:$ext";
+        $env{RUBYLIB} = $rl;
+        my $pid = spawn(\%env, $ruby, $exe, @args);
+        UnicornTest::AutoReap->new($pid);
+}
+
+sub do_req ($@) {
+        my ($dst, @req) = @_;
+        my $c = ref($dst) ? tcp_start($dst, @req) : unix_start($dst, @req);
+        return $c if !wantarray;
+        my ($status, $hdr);
+        # read headers iff HTTP/1.x request, HTTP/0.9 remains supported
+        my ($first) = (join('', @req) =~ m!\A([^\r\n]+)!);
+        ($status, $hdr) = slurp_hdr($c) if $first =~ m{\s*HTTP/\S+$};
+        my $bdy = do { local $/; <$c> };
+        close $c;
+        ($status, $hdr, $bdy);
+}
+
+sub mkfifo_die ($;$) {
+        POSIX::mkfifo($_[0], $_[1] // 0600) or croak "mkfifo: $!";
+}
+
+sub write_file ($$@) { # mode, filename, LIST (for print)
+        open(my $fh, shift, shift);
+        print $fh @_;
+        # return $fh for futher writes if user wants it:
+        defined(wantarray) && !wantarray ? $fh : close $fh;
+}
+
+# automatically kill + reap children when this goes out-of-scope
+package UnicornTest::AutoReap;
+use v5.14;
+use autodie;
+
+sub new {
+        my (undef, $pid) = @_;
+        bless { pid => $pid, owner => $$ }, __PACKAGE__
+}
+
+sub do_kill {
+        my ($self, $sig) = @_;
+        kill($sig // 'TERM', $self->{pid});
+}
+
+sub join {
+        my ($self, $sig) = @_;
+        my $pid = delete $self->{pid} or return;
+        kill($sig, $pid) if defined $sig;
+        my $ret = waitpid($pid, 0);
+        $ret == $pid or die "BUG: waitpid($pid) != $ret";
+}
+
+sub DESTROY {
+        my ($self) = @_;
+        return if $self->{owner} != $$;
+        $self->join('TERM');
+}
+
+package main; # inject ourselves into the t/*.t script
+UnicornTest->import;
+Test::More->import;
+# try to ensure ->DESTROY fires:
+$SIG{TERM} = sub { exit(15 + 128) };
+$SIG{INT} = sub { exit(2 + 128) };
+$SIG{PIPE} = sub { exit(13 + 128) };
+1;
diff --git a/t/listener_names.ru b/t/listener_names.ru
index edb4e6a..f52c59b 100644
--- a/t/listener_names.ru
+++ b/t/listener_names.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 names = Unicorn.listener_names.inspect # rely on preload_app=true
diff --git a/t/oob_gc.ru b/t/oob_gc.ru
index c253540..2ae58a8 100644
--- a/t/oob_gc.ru
+++ b/t/oob_gc.ru
@@ -1,4 +1,5 @@
 #\-E none
+# frozen_string_literal: false
 require 'unicorn/oob_gc'
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
@@ -7,9 +8,6 @@ $gc_started = false
 
 # Mock GC.start
 def GC.start
-  ObjectSpace.each_object(Kgio::Socket) do |x|
-    x.closed? or abort "not closed #{x}"
-  end
   $gc_started = true
 end
 run lambda { |env|
diff --git a/t/oob_gc_path.ru b/t/oob_gc_path.ru
index af8e3b9..5358222 100644
--- a/t/oob_gc_path.ru
+++ b/t/oob_gc_path.ru
@@ -1,4 +1,5 @@
 #\-E none
+# frozen_string_literal: false
 require 'unicorn/oob_gc'
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
@@ -7,9 +8,6 @@ $gc_started = false
 
 # Mock GC.start
 def GC.start
-  ObjectSpace.each_object(Kgio::Socket) do |x|
-    x.closed? or abort "not closed #{x}"
-  end
   $gc_started = true
 end
 run lambda { |env|
diff --git a/t/pid.ru b/t/pid.ru
index f5fd31f..b49b137 100644
--- a/t/pid.ru
+++ b/t/pid.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env| [ 200, {}, [ "#$$\n" ] ] }
diff --git a/t/preread_input.ru b/t/preread_input.ru
index 79685c4..5f68fe9 100644
--- a/t/preread_input.ru
+++ b/t/preread_input.ru
@@ -1,17 +1,23 @@
 #\-E none
-require 'digest/sha1'
+# frozen_string_literal: false
+require 'digest/md5'
 require 'unicorn/preread_input'
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
 use Unicorn::PrereadInput
 nr = 0
 run lambda { |env|
   $stderr.write "app dispatch: #{nr += 1}\n"
   input = env["rack.input"]
-  dig = Digest::SHA1.new
-  while buf = input.read(16384)
-    dig.update(buf)
+  dig = Digest::MD5.new
+  if buf = input.read(16384)
+    begin
+      dig.update(buf)
+    end while input.read(16384, buf)
+    buf.clear # remove this call if Ruby ever gets escape analysis
   end
-
-  [ 200, {}, [ "#{dig.hexdigest}\n" ] ]
+  if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i
+    cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']]
+    cmd5_bin = cmd5_b64.unpack('m')[0]
+    return [500, {}, [ cmd5_b64 ] ] if cmd5_bin != dig.digest
+  end
+  [ 200, {}, [ dig.hexdigest ] ]
 }
diff --git a/t/rack-input-tests.ru b/t/rack-input-tests.ru
deleted file mode 100644
index 8c35630..0000000
--- a/t/rack-input-tests.ru
+++ /dev/null
@@ -1,21 +0,0 @@
-# SHA1 checksum generator
-require 'digest/sha1'
-use Rack::ContentLength
-cap = 16384
-app = lambda do |env|
-  /\A100-continue\z/i =~ env['HTTP_EXPECT'] and
-    return [ 100, {}, [] ]
-  digest = Digest::SHA1.new
-  input = env['rack.input']
-  input.size if env["PATH_INFO"] == "/size_first"
-  input.rewind if env["PATH_INFO"] == "/rewind_first"
-  if buf = input.read(rand(cap))
-    begin
-      raise "#{buf.size} > #{cap}" if buf.size > cap
-      digest.update(buf)
-    end while input.read(rand(cap), buf)
-  end
-
-  [ 200, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ]
-end
-run app
diff --git a/t/reload-bad-config.t b/t/reload-bad-config.t
new file mode 100644
index 0000000..c023b88
--- /dev/null
+++ b/t/reload-bad-config.t
@@ -0,0 +1,54 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+my $srv = tcp_server();
+my $host_port = tcp_host_port($srv);
+my $ru = "$tmpdir/config.ru";
+my $u_conf = "$tmpdir/u.conf.rb";
+
+open my $fh, '>', $ru;
+print $fh <<'EOM';
+use Rack::ContentLength
+use Rack::ContentType, 'text/plain'
+config = ru = "hello world\n" # check for config variable conflicts, too
+run lambda { |env| [ 200, {}, [ ru.to_s ] ] }
+EOM
+close $fh;
+
+open $fh, '>', $u_conf;
+print $fh <<EOM;
+preload_app true
+stderr_path "$err_log"
+EOM
+close $fh;
+
+my $ar = unicorn(qw(-E none -c), $u_conf, $ru, { 3 => $srv });
+my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid at start');
+is($bdy, "hello world\n", 'body matches expected');
+
+open $fh, '>>', $ru;
+say $fh '....this better be a syntax error in any version of ruby...';
+close $fh;
+
+$ar->do_kill('HUP'); # reload
+my @l;
+for (1..1000) {
+        @l = grep(/(?:done|error) reloading/, slurp($err_log)) and
+                last;
+        sleep 0.011;
+}
+diag slurp($err_log) if $ENV{V};
+ok(grep(/error reloading/, @l), 'got error reloading');
+open $fh, '>', $err_log;
+close $fh;
+
+($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid afte reload');
+is($bdy, "hello world\n", 'body matches expected after reload');
+
+check_stderr;
+undef $tmpdir; # quiet t/lib.perl END{}
+done_testing;
diff --git a/t/t0006.ru b/t/reopen-logs.ru
index c39e8f6..488da85 100644
--- a/t/t0006.ru
+++ b/t/reopen-logs.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env|
diff --git a/t/reopen-logs.t b/t/reopen-logs.t
new file mode 100644
index 0000000..76a4dbd
--- /dev/null
+++ b/t/reopen-logs.t
@@ -0,0 +1,39 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+my $srv = tcp_server();
+my $u_conf = "$tmpdir/u.conf.rb";
+my $out_log = "$tmpdir/out.log";
+open my $fh, '>', $u_conf;
+print $fh <<EOM;
+stderr_path "$err_log"
+stdout_path "$out_log"
+EOM
+close $fh;
+
+my $auto_reap = unicorn('-c', $u_conf, 't/reopen-logs.ru', { 3 => $srv } );
+my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+is($bdy, "true\n", 'logs opened');
+
+rename($err_log, "$err_log.rot");
+rename($out_log, "$out_log.rot");
+
+$auto_reap->do_kill('USR1');
+
+my $tries = 1000;
+while (!-f $err_log && --$tries) { sleep 0.01 };
+while (!-f $out_log && --$tries) { sleep 0.01 };
+
+ok(-f $out_log, 'stdout_path recreated after USR1');
+ok(-f $err_log, 'stderr_path recreated after USR1');
+
+($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+is($bdy, "true\n", 'logs reopened with sync==true');
+
+$auto_reap->join('QUIT');
+is($?, 0, 'no error on exit');
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/t0000-http-basic.sh b/t/t0000-http-basic.sh
deleted file mode 100755
index 8ab58ac..0000000
--- a/t/t0000-http-basic.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 8 "simple HTTP connection tests"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config env.ru
-        unicorn_wait_start
-}
-
-t_begin "single request" && {
-        curl -sSfv http://$listen/
-}
-
-t_begin "check stderr has no errors" && {
-        check_stderr
-}
-
-t_begin "HTTP/0.9 request should not return headers" && {
-        (
-                printf 'GET /\r\n'
-                cat $fifo > $tmp &
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-}
-
-t_begin "env.inspect should've put everything on one line" && {
-        test 1 -eq $(count_lines < $tmp)
-}
-
-t_begin "no headers in output" && {
-        if grep ^Connection: $tmp
-        then
-                die "Connection header found in $tmp"
-        elif grep ^HTTP/ $tmp
-        then
-                die "HTTP/ found in $tmp"
-        fi
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr has no errors" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0001-reload-bad-config.sh b/t/t0001-reload-bad-config.sh
deleted file mode 100755
index 55bb355..0000000
--- a/t/t0001-reload-bad-config.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 7 "reload config.ru error with preload_app true"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles ru
-
-        cat > $ru <<\EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-x = { "hello" => "world" }
-run lambda { |env| [ 200, {}, [ x.inspect << "\n" ] ] }
-EOF
-        echo 'preload_app true' >> $unicorn_config
-        unicorn -D -c $unicorn_config $ru
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "introduce syntax error in rackup file" && {
-        echo '...' >> $ru
-}
-
-t_begin "reload signal succeeds" && {
-        kill -HUP $unicorn_pid
-        while ! egrep '(done|error) reloading' $r_err >/dev/null
-        do
-                sleep 1
-        done
-
-        grep 'error reloading' $r_err >/dev/null
-        > $r_err
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0002-config-conflict.sh b/t/t0002-config-conflict.sh
deleted file mode 100755
index d7b2181..0000000
--- a/t/t0002-config-conflict.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 6 "config variables conflict with preload_app"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles ru rutmp
-
-        cat > $ru <<\EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-config = ru = { "hello" => "world" }
-run lambda { |env| [ 200, {}, [ ru.inspect << "\n" ] ] }
-EOF
-        echo 'preload_app true' >> $unicorn_config
-        unicorn -D -c $unicorn_config $ru
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "modify rackup file" && {
-        sed -e 's/world/WORLD/' < $ru > $rutmp
-        mv $rutmp $ru
-}
-
-t_begin "reload signal succeeds" && {
-        kill -HUP $unicorn_pid
-        while ! egrep '(done|error) reloading' < $r_err >/dev/null
-        do
-                sleep 1
-        done
-
-        grep 'done reloading' $r_err >/dev/null
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"WORLD"}'
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_done
diff --git a/t/t0002-parser-error.sh b/t/t0002-parser-error.sh
deleted file mode 100755
index 9dc1cd2..0000000
--- a/t/t0002-parser-error.sh
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 11 "parser error test"
-
-t_begin "setup and startup" && {
-        unicorn_setup
-        unicorn -D env.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "send a bad request" && {
-        (
-                printf 'GET / HTTP/1/1\r\nHost: example.com\r\n\r\n'
-                cat $fifo > $tmp &
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-}
-
-dbgcat tmp
-
-t_begin "response should be a 400" && {
-        grep -F 'HTTP/1.1 400 Bad Request' $tmp
-}
-
-t_begin "send a huge Request URI (REQUEST_PATH > (12 * 1024))" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<1024;i++) print i}')
-                do
-                        printf '0123456789ab'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (REQUEST_PATH)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "send a huge Request URI (QUERY_STRING > (10 * 1024))" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /hello-world?a'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<1024;i++) print i}')
-                do
-                        printf '0123456789'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (QUERY_STRING)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "send a huge Request URI (FRAGMENT > 1024)" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /hello-world#a'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<64;i++) print i}')
-                do
-                        printf '0123456789abcdef'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (FRAGMENT)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "server stderr should be clean" && check_stderr
-
-t_begin "term signal sent" && kill $unicorn_pid
-
-t_done
diff --git a/t/t0003-working_directory.sh b/t/t0003-working_directory.sh
deleted file mode 100755
index 79988d8..0000000
--- a/t/t0003-working_directory.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "config.ru inside alt working_directory"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/config.ru <<EOF
-#\--daemonize --host $host --port $port
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-run lambda { |env| [ 200, {}, [ "#{\$master_ppid}\\n" ] ] }
-EOF
-        # we have --host/--port in config.ru instead
-        grep -v ^listen $unicorn_config > $unicorn_config_tmp
-
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config_tmp
-
-        # allows ppid to be 1 in before_fork
-        echo "preload_app true" >> $unicorn_config_tmp
-        cat >> $unicorn_config_tmp <<\EOF
-before_fork do |server,worker|
-  $master_ppid = Process.ppid # should be zero to detect daemonization
-end
-EOF
-
-        mv $unicorn_config_tmp $unicorn_config
-
-        # rely on --daemonize switch, no & or -D
-        unicorn -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body ppid == 1 (daemonized)" && {
-        test "$body" -eq 1
-}
-
-t_done
diff --git a/t/t0004-heartbeat-timeout.sh b/t/t0004-heartbeat-timeout.sh
deleted file mode 100755
index 2965283..0000000
--- a/t/t0004-heartbeat-timeout.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 11 "heartbeat/timeout test"
-
-t_begin "setup and startup" && {
-        unicorn_setup
-        echo timeout 3 >> $unicorn_config
-        echo preload_app true >> $unicorn_config
-        unicorn -D heartbeat-timeout.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "read worker PID" && {
-        worker_pid=$(curl -sSf http://$listen/)
-        t_info "worker_pid=$worker_pid"
-}
-
-t_begin "sleep for a bit, ensure worker PID does not change" && {
-        sleep 4
-        test $(curl -sSf http://$listen/) -eq $worker_pid
-}
-
-t_begin "block the worker process to force it to die" && {
-        rm $ok
-        t0=$(unix_time)
-        err="$(curl -sSf http://$listen/block-forever 2>&1 || > $ok)"
-        t1=$(unix_time)
-        elapsed=$(($t1 - $t0))
-        t_info "elapsed=$elapsed err=$err"
-        test x"$err" != x"Should never get here"
-        test x"$err" != x"$worker_pid"
-}
-
-t_begin "ensure worker was killed" && {
-        test -e $ok
-        test 1 -eq $(grep timeout $r_err | grep killing | count_lines)
-}
-
-t_begin "ensure timeout took at least 3 seconds" && {
-        test $elapsed -ge 3
-}
-
-t_begin "we get a fresh new worker process" && {
-        new_worker_pid=$(curl -sSf http://$listen/)
-        test $new_worker_pid -ne $worker_pid
-}
-
-t_begin "truncate the server error log" && {
-        > $r_err
-}
-
-t_begin "SIGSTOP and SIGCONT on unicorn master does not kill worker" && {
-        kill -STOP $unicorn_pid
-        sleep 4
-        kill -CONT $unicorn_pid
-        sleep 2
-        test $new_worker_pid -eq $(curl -sSf http://$listen/)
-}
-
-t_begin "stop server" && {
-        kill -QUIT $unicorn_pid
-}
-
-t_begin "check stderr" && check_stderr
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0004-working_directory_broken.sh b/t/t0004-working_directory_broken.sh
deleted file mode 100755
index ca9d382..0000000
--- a/t/t0004-working_directory_broken.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 3 "config.ru is missing inside alt working_directory"
-
-t_begin "setup" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp ok
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config_tmp
-}
-
-t_begin "fails to start up w/o config.ru" && {
-        unicorn -c $unicorn_config_tmp || echo ok > $ok
-}
-
-t_begin "fallback code was run" && {
-        test x"$(cat $ok)" = xok
-}
-
-t_done
diff --git a/t/t0005-working_directory_app.rb.sh b/t/t0005-working_directory_app.rb.sh
deleted file mode 100755
index 0fbab4f..0000000
--- a/t/t0005-working_directory_app.rb.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "fooapp.rb inside alt working_directory"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/fooapp.rb <<\EOF
-class Fooapp
-  def self.call(env)
-    # Rack::Lint in 1.5.0 requires headers to be a hash
-    h = [%w(Content-Type text/plain), %w(Content-Length 2)]
-    h = Rack::Utils::HeaderHash.new(h)
-    [ 200, h, %w(HI) ]
-  end
-end
-EOF
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config
-        cd /
-        unicorn -D -c $unicorn_config -I. fooapp.rb
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body expected" && {
-        test x"$body" = xHI
-}
-
-t_done
diff --git a/t/t0006-reopen-logs.sh b/t/t0006-reopen-logs.sh
deleted file mode 100755
index a6e7a17..0000000
--- a/t/t0006-reopen-logs.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 15 "reopen rotated logs"
-
-t_begin "setup and startup" && {
-        rtmpfiles curl_out curl_err r_rot
-        unicorn_setup
-        unicorn -D t0006.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "ensure server is responsive" && {
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-}
-
-t_begin "ensure stderr log is clean" && check_stderr
-
-t_begin "external log rotation" && {
-        rm -f $r_rot
-        mv $r_err $r_rot
-}
-
-t_begin "send reopen log signal (USR1)" && {
-        kill -USR1 $unicorn_pid
-}
-
-t_begin "wait for rotated log to reappear" && {
-        nr=60
-        while ! test -f $r_err && test $nr -ge 0
-        do
-                sleep 1
-                nr=$(( $nr - 1 ))
-        done
-}
-
-t_begin "ensure server is still responsive" && {
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-}
-
-t_begin "wait for worker to reopen logs" && {
-        nr=60
-        re="worker=.* done reopening logs"
-        while ! grep "$re" < $r_err >/dev/null && test $nr -ge 0
-        do
-                sleep 1
-                nr=$(( $nr - 1 ))
-        done
-}
-
-dbgcat r_rot
-dbgcat r_err
-
-t_begin "ensure no errors from curl" && {
-        test ! -s $curl_err
-}
-
-t_begin "current server stderr is clean" && check_stderr
-
-t_begin "rotated stderr is clean" && {
-        check_stderr $r_rot
-}
-
-t_begin "server is now writing logs to new stderr" && {
-        before_rot=$(count_bytes < $r_rot)
-        before_err=$(count_bytes < $r_err)
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-        after_rot=$(count_bytes < $r_rot)
-        after_err=$(count_bytes < $r_err)
-        test $after_rot -eq $before_rot
-        test $after_err -gt $before_err
-}
-
-t_begin "stop server" && {
-        kill $unicorn_pid
-}
-
-dbgcat r_err
-
-t_begin "current server stderr is clean" && check_stderr
-t_begin "rotated stderr is clean" && check_stderr $r_rot
-
-t_done
diff --git a/t/t0007-working_directory_no_embed_cli.sh b/t/t0007-working_directory_no_embed_cli.sh
deleted file mode 100755
index 77d6707..0000000
--- a/t/t0007-working_directory_no_embed_cli.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "config.ru inside alt working_directory (no embedded switches)"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/config.ru <<EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-run lambda { |env| [ 200, {}, [ "#{\$master_ppid}\\n" ] ] }
-EOF
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config
-
-        # allows ppid to be 1 in before_fork
-        echo "preload_app true" >> $unicorn_config
-        cat >> $unicorn_config <<\EOF
-before_fork do |server,worker|
-  $master_ppid = Process.ppid # should be zero to detect daemonization
-end
-EOF
-
-        cd /
-        unicorn -D -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body ppid == 1 (daemonized)" && {
-        test "$body" -eq 1
-}
-
-t_done
diff --git a/t/t0008-back_out_of_upgrade.sh b/t/t0008-back_out_of_upgrade.sh
deleted file mode 100755
index 96d4057..0000000
--- a/t/t0008-back_out_of_upgrade.sh
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 13 "backout of USR2 upgrade"
-
-worker_wait_start () {
-        test xSTART = x"$(cat $fifo)"
-        unicorn_pid=$(cat $pid)
-}
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -f $pid.oldbin
-
-cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  # test script will block while reading from $fifo,
-  # so notify the script on the first worker we spawn
-  # by opening the FIFO
-  if worker.nr == 0
-    File.open("$fifo", "wb") { |fp| fp.syswrite "START" }
-  end
-end
-EOF
-        unicorn -D -c $unicorn_config pid.ru
-        worker_wait_start
-        orig_master_pid=$unicorn_pid
-}
-
-t_begin "read original worker pid" && {
-        orig_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$orig_worker_pid" && kill -0 $orig_worker_pid
-}
-
-t_begin "upgrade to new master" && {
-        kill -USR2 $orig_master_pid
-}
-
-t_begin "kill old worker" && {
-        kill -WINCH $orig_master_pid
-}
-
-t_begin "wait for new worker to start" && {
-        worker_wait_start
-        test $unicorn_pid -ne $orig_master_pid
-        new_master_pid=$unicorn_pid
-}
-
-t_begin "old master pid is stashed in $pid.oldbin" && {
-        test -s "$pid.oldbin"
-        test $orig_master_pid -eq $(cat $pid.oldbin)
-}
-
-t_begin "ensure old worker is no longer running" && {
-        i=0
-        while kill -0 $orig_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "capture pid of new worker" && {
-        new_worker_pid=$(curl -sSf http://$listen/)
-}
-
-t_begin "reload old master process" && {
-        kill -HUP $orig_master_pid
-        worker_wait_start
-}
-
-t_begin "gracefully kill new master and ensure it dies" && {
-        kill -QUIT $new_master_pid
-        i=0
-        while kill -0 $new_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "ensure $pid.oldbin does not exist" && {
-        i=0
-        while test -s $pid.oldbin
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-        while ! test -s $pid
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "ensure $pid is correct" && {
-        cur_master_pid=$(cat $pid)
-        test $orig_master_pid -eq $cur_master_pid
-}
-
-t_begin "killing succeeds" && {
-        kill $orig_master_pid
-}
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0009-winch_ttin.sh b/t/t0009-winch_ttin.sh
deleted file mode 100755
index 6e56e30..0000000
--- a/t/t0009-winch_ttin.sh
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 8 "SIGTTIN succeeds after SIGWINCH"
-
-t_begin "setup and start" && {
-        unicorn_setup
-cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  # test script will block while reading from $fifo,
-  File.open("$fifo", "wb") { |fp| fp.syswrite worker.nr.to_s }
-end
-EOF
-        unicorn -D -c $unicorn_config pid.ru
-        unicorn_wait_start
-        test 0 -eq $(cat $fifo) || die "worker.nr != 0"
-}
-
-t_begin "read worker pid" && {
-        orig_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$orig_worker_pid" && kill -0 $orig_worker_pid
-}
-
-t_begin "stop all workers" && {
-        kill -WINCH $unicorn_pid
-}
-
-# we have to do this next step before delivering TTIN
-# signals aren't guaranteed to delivered in order
-t_begin "wait for worker to die" && {
-        i=0
-        while kill -0 $orig_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "start one worker back up" && {
-        kill -TTIN $unicorn_pid
-}
-
-t_begin "wait for new worker to start" && {
-        test 0 -eq $(cat $fifo) || die "worker.nr != 0"
-        new_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$new_worker_pid" && kill -0 $new_worker_pid
-        test $orig_worker_pid -ne $new_worker_pid || \
-           die "worker wasn't replaced"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && check_stderr
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0011-active-unix-socket.sh b/t/t0011-active-unix-socket.sh
deleted file mode 100755
index fae0b6c..0000000
--- a/t/t0011-active-unix-socket.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 11 "existing UNIX domain socket check"
-
-read_pid_unix () {
-        x=$(printf 'GET / HTTP/1.0\r\n\r\n' | \
-            socat - UNIX:$unix_socket | \
-            tail -1)
-        test -n "$x"
-        y="$(expr "$x" : '\([0-9][0-9]*\)')"
-        test x"$x" = x"$y"
-        test -n "$y"
-        echo "$y"
-}
-
-t_begin "setup and start" && {
-        rtmpfiles unix_socket unix_config
-        rm -f $unix_socket
-        unicorn_setup
-        grep -v ^listen < $unicorn_config > $unix_config
-        echo "listen '$unix_socket'" >> $unix_config
-        unicorn -D -c $unix_config pid.ru
-        unicorn_wait_start
-        orig_master_pid=$unicorn_pid
-}
-
-t_begin "get pid of worker" && {
-        worker_pid=$(read_pid_unix)
-        t_info "worker_pid=$worker_pid"
-}
-
-t_begin "fails to start with existing pid file" && {
-        rm -f $ok
-        unicorn -D -c $unix_config pid.ru || echo ok > $ok
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "worker pid unchanged" && {
-        test x"$(read_pid_unix)" = x$worker_pid
-        > $r_err
-}
-
-t_begin "fails to start with listening UNIX domain socket bound" && {
-        rm $ok $pid
-        unicorn -D -c $unix_config pid.ru || echo ok > $ok
-        test x"$(cat $ok)" = xok
-        > $r_err
-}
-
-t_begin "worker pid unchanged (again)" && {
-        test x"$(read_pid_unix)" = x$worker_pid
-}
-
-t_begin "nuking the existing Unicorn succeeds" && {
-        kill -9 $unicorn_pid
-        while kill -0 $unicorn_pid
-        do
-                sleep 1
-        done
-        check_stderr
-}
-
-t_begin "succeeds in starting with leftover UNIX domain socket bound" && {
-        test -S $unix_socket
-        unicorn -D -c $unix_config pid.ru
-        unicorn_wait_start
-}
-
-t_begin "worker pid changed" && {
-        test x"$(read_pid_unix)" != x$worker_pid
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "no errors" && check_stderr
-
-t_done
diff --git a/t/t0013.ru b/t/t0013.ru
index 48a3a34..e425093 100644
--- a/t/t0013.ru
+++ b/t/t0013.ru
@@ -1,4 +1,5 @@
 #\ -E none
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, 'text/plain'
 app = lambda do |env|
diff --git a/t/t0014.ru b/t/t0014.ru
index b0bd2b7..686d214 100644
--- a/t/t0014.ru
+++ b/t/t0014.ru
@@ -1,4 +1,5 @@
 #\ -E none
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, 'text/plain'
 app = lambda do |env|
diff --git a/t/t0018-write-on-close.sh b/t/t0018-write-on-close.sh
deleted file mode 100755
index 3afefea..0000000
--- a/t/t0018-write-on-close.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 4 "write-on-close tests for funky response-bodies"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config write-on-close.ru
-        unicorn_wait_start
-}
-
-t_begin "write-on-close response body succeeds" && {
-        test xGoodbye = x"$(curl -sSf http://$listen/)"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0019-max_header_len.sh b/t/t0019-max_header_len.sh
deleted file mode 100755
index 6a355b4..0000000
--- a/t/t0019-max_header_len.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 5 "max_header_len setting (only intended for Rainbows!)"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        req='GET / HTTP/1.0\r\n\r\n'
-        len=$(printf "$req" | count_bytes)
-        echo Unicorn::HttpParser.max_header_len = $len >> $unicorn_config
-        unicorn -D -c $unicorn_config env.ru
-        unicorn_wait_start
-}
-
-t_begin "minimal request succeeds" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf "$req"
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-
-        fgrep "HTTP/1.1 200 OK" $tmp
-}
-
-t_begin "big request fails" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'GET /xxxxxx HTTP/1.0\r\n\r\n'
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-        fgrep "HTTP/1.1 413" $tmp
-}
-
-dbgcat tmp
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0100-rack-input-tests.sh b/t/t0100-rack-input-tests.sh
deleted file mode 100755
index ee7a437..0000000
--- a/t/t0100-rack-input-tests.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-test -r random_blob || die "random_blob required, run with 'make $0'"
-
-t_plan 10 "rack.input read tests"
-
-t_begin "setup and startup" && {
-        rtmpfiles curl_out curl_err
-        unicorn_setup
-        unicorn -E none -D rack-input-tests.ru -c $unicorn_config
-        blob_sha1=$(rsha1 < random_blob)
-        blob_size=$(count_bytes < random_blob)
-        t_info "blob_sha1=$blob_sha1"
-        unicorn_wait_start
-}
-
-t_begin "corked identity request" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT / HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                content-md5-put < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked identity request (input#size first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /size_first HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked identity request (input#rewind first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /rewind_first HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request (input#size first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /size_first HTTP/1.1\r\n'
-                printf 'Host: example.com\r\n'
-                printf 'Transfer-Encoding: chunked\r\n'
-                printf 'Trailer: Content-MD5\r\n'
-                printf '\r\n'
-                content-md5-put --no-headers < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request (input#rewind first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /rewind_first HTTP/1.1\r\n'
-                printf 'Host: example.com\r\n'
-                printf 'Transfer-Encoding: chunked\r\n'
-                printf 'Trailer: Content-MD5\r\n'
-                printf '\r\n'
-                content-md5-put --no-headers < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "regular request" && {
-        curl -sSf -T random_blob http://$listen/ > $curl_out 2> $curl_err
-        test x$blob_sha1 = x$(cat $curl_out)
-        test ! -s $curl_err
-}
-
-t_begin "chunked request" && {
-        curl -sSf -T- < random_blob http://$listen/ > $curl_out 2> $curl_err
-        test x$blob_sha1 = x$(cat $curl_out)
-        test ! -s $curl_err
-}
-
-dbgcat r_err
-
-t_begin "shutdown" && {
-        kill $unicorn_pid
-}
-
-t_done
diff --git a/t/t0116-client_body_buffer_size.sh b/t/t0116-client_body_buffer_size.sh
deleted file mode 100755
index c9e17c7..0000000
--- a/t/t0116-client_body_buffer_size.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 12 "client_body_buffer_size settings"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp one_meg
-        dd if=/dev/zero bs=1M count=1 of=$one_meg
-        cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  File.open("$fifo", "wb") { |fp| fp.syswrite "START" }
-end
-EOF
-        cat $unicorn_config > $unicorn_config_tmp
-        echo client_body_buffer_size 0 >> $unicorn_config
-        unicorn -D -c $unicorn_config t0116.ru
-        unicorn_wait_start
-        fs_class=Unicorn::TmpIO
-        mem_class=StringIO
-
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "class for a zero-byte file should be StringIO" && {
-        > $tmp
-        test xStringIO = x"$(curl -T $tmp -sSf http://$listen/input_class)"
-}
-
-t_begin "class for a 1 byte file should be filesystem-backed" && {
-        echo > $tmp
-        test x$fs_class = x"$(curl -T $tmp -sSf http://$listen/tmp_class)"
-}
-
-t_begin "reload with default client_body_buffer_size" && {
-        mv $unicorn_config_tmp $unicorn_config
-        kill -HUP $unicorn_pid
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "class for a 1 byte file should be memory-backed" && {
-        echo > $tmp
-        test x$mem_class = x"$(curl -T $tmp -sSf http://$listen/tmp_class)"
-}
-
-t_begin "class for a random blob file should be filesystem-backed" && {
-        resp="$(curl -T random_blob -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "one megabyte file should be filesystem-backed" && {
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "reload with a big client_body_buffer_size" && {
-        echo "client_body_buffer_size(1024 * 1024)" >> $unicorn_config
-        kill -HUP $unicorn_pid
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "one megabyte file should be memory-backed" && {
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$mem_class = x"$resp"
-}
-
-t_begin "one megabyte + 1 byte file should be filesystem-backed" && {
-        echo >> $one_meg
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0200-rack-hijack.sh b/t/t0200-rack-hijack.sh
deleted file mode 100755
index fee0791..0000000
--- a/t/t0200-rack-hijack.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 9 "rack.hijack tests (Rack 1.5+ (Rack::VERSION >= [ 1,2]))"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config hijack.ru
-        unicorn_wait_start
-}
-
-t_begin "normal env reused between requests" && {
-        env_a="$(curl -sSf http://$listen/normal_env_id)"
-        b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_a" = x"$b"
-}
-
-t_begin "check request hijack" && {
-        test "xrequest.hijacked" = x"$(curl -sSfv http://$listen/hijack_req)"
-}
-
-t_begin "env changed after request hijack" && {
-        env_b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_a" != x"$env_b"
-}
-
-t_begin "check response hijack" && {
-        test "xresponse.hijacked" = x"$(curl -sSfv http://$listen/hijack_res)"
-}
-
-t_begin "env changed after response hijack" && {
-        env_c="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_b" != x"$env_c"
-}
-
-t_begin "env continues to be reused between requests" && {
-        b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_c" = x"$b"
-}
-
-t_begin "killing succeeds after hijack" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr for hijacked body close" && {
-        check_stderr
-        grep 'closed DieIfUsed 1\>' $r_err
-        grep 'closed DieIfUsed 2\>' $r_err
-        ! grep 'closed DieIfUsed 3\>' $r_err
-}
-
-t_done
diff --git a/t/t0300-no-default-middleware.sh b/t/t0300-no-default-middleware.sh
index 779dc02..00feacc 100644
--- a/t/t0300-no-default-middleware.sh
+++ b/t/t0300-no-default-middleware.sh
@@ -9,7 +9,7 @@ t_begin "setup and start" && {
 }
 
 t_begin "check exit status with Rack::Lint not present" && {
-        test 42 -eq "$(curl -sf -o/dev/null -w'%{http_code}' http://$listen/)"
+        test 500 -ne "$(curl -sf -o/dev/null -w'%{http_code}' http://$listen/)"
 }
 
 t_begin "killing succeeds" && {
diff --git a/t/t0301.ru b/t/t0301.ru
index 1ae8ea7..54929b1 100644
--- a/t/t0301.ru
+++ b/t/t0301.ru
@@ -1,4 +1,5 @@
 #\-N --debug
+# frozen_string_literal: false
 run(lambda do |env|
   case env['PATH_INFO']
   when '/vars'
@@ -6,8 +7,8 @@ run(lambda do |env|
         "lint=#{caller.grep(%r{rack/lint\.rb})[0].split(':')[0]}\n"
   end
   h = {
-    'Content-Length' => b.size.to_s,
-    'Content-Type' => 'text/plain',
+    'content-length' => b.size.to_s,
+    'content-type' => 'text/plain',
   }
   [ 200, h, [ b ] ]
 end)
diff --git a/t/t9000-preread-input.sh b/t/t9000-preread-input.sh
deleted file mode 100755
index d6c73ab..0000000
--- a/t/t9000-preread-input.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 9 "PrereadInput middleware tests"
-
-t_begin "setup and start" && {
-        random_blob_sha1=$(rsha1 < random_blob)
-        unicorn_setup
-        unicorn  -D -c $unicorn_config preread_input.ru
-        unicorn_wait_start
-}
-
-t_begin "single identity request" && {
-        curl -sSf -T random_blob http://$listen/ > $tmp
-}
-
-t_begin "sha1 matches" && {
-        test x"$(cat $tmp)" = x"$random_blob_sha1"
-}
-
-t_begin "single chunked request" && {
-        curl -sSf -T- < random_blob http://$listen/ > $tmp
-}
-
-t_begin "sha1 matches" && {
-        test x"$(cat $tmp)" = x"$random_blob_sha1"
-}
-
-t_begin "app only dispatched twice" && {
-        test 2 -eq "$(grep 'app dispatch:' < $r_err | count_lines )"
-}
-
-t_begin "aborted chunked request" && {
-        rm -f $tmp
-        curl -sSf -T- < $fifo http://$listen/ > $tmp &
-        curl_pid=$!
-        kill -9 $curl_pid
-        wait
-}
-
-t_begin "app only dispatched twice" && {
-        test 2 -eq "$(grep 'app dispatch:' < $r_err | count_lines )"
-}
-
-t_begin "killing succeeds" && {
-        kill -QUIT $unicorn_pid
-}
-
-t_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 7f97958..8613144 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -94,7 +94,8 @@ check_stderr () {
         set +u
         _r_err=${1-${r_err}}
         set -u
-        if grep -v $T $_r_err | grep -i Error
+        if grep -v $T $_r_err | grep -i Error | \
+                grep -v NameError.*Unicorn::Waiter
         then
                 die "Errors found in $_r_err"
         elif grep SIGKILL $_r_err
@@ -122,7 +123,3 @@ unicorn_wait_start () {
         # no need to play tricks with FIFOs since we got "ready_pipe" now
         unicorn_pid=$(cat $pid)
 }
-
-rsha1 () {
-        sha1sum.rb
-}
diff --git a/t/winch_ttin.t b/t/winch_ttin.t
new file mode 100644
index 0000000..c507959
--- /dev/null
+++ b/t/winch_ttin.t
@@ -0,0 +1,67 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+use POSIX qw(mkfifo);
+my $u_conf = "$tmpdir/u.conf.rb";
+my $u_sock = "$tmpdir/u.sock";
+my $fifo = "$tmpdir/fifo";
+mkfifo($fifo, 0666) or die "mkfifo($fifo): $!";
+
+open my $fh, '>', $u_conf;
+print $fh <<EOM;
+pid "$tmpdir/pid"
+listen "$u_sock"
+stderr_path "$err_log"
+after_fork do |server, worker|
+  # test script will block while reading from $fifo,
+  File.open("$fifo", "wb") { |fp| fp.syswrite worker.nr.to_s }
+end
+EOM
+close $fh;
+
+unicorn('-D', '-c', $u_conf, 't/integration.ru')->join;
+is($?, 0, 'daemonized properly');
+open $fh, '<', "$tmpdir/pid";
+chomp(my $pid = <$fh>);
+ok(kill(0, $pid), 'daemonized PID works');
+my $quit = sub { kill('QUIT', $pid) if $pid; $pid = undef };
+END { $quit->() };
+
+open $fh, '<', $fifo;
+my $worker_nr = <$fh>;
+close $fh;
+is($worker_nr, '0', 'initial worker spawned');
+
+my ($status, $hdr, $worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0');
+like($status, qr/ 200\b/, 'got 200 response');
+like($worker_pid, qr/\A[0-9]+\n\z/s, 'PID in response');
+chomp $worker_pid;
+ok(kill(0, $worker_pid), 'worker_pid is valid');
+
+ok(kill('WINCH', $pid), 'SIGWINCH can be sent');
+
+my $tries = 1000;
+while (CORE::kill(0, $worker_pid) && --$tries) { sleep 0.01 }
+ok(!CORE::kill(0, $worker_pid), 'worker not running');
+
+ok(kill('TTIN', $pid), 'SIGTTIN to restart worker');
+
+open $fh, '<', $fifo;
+$worker_nr = <$fh>;
+close $fh;
+is($worker_nr, '0', 'worker restarted');
+
+($status, $hdr, my $new_worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0');
+like($status, qr/ 200\b/, 'got 200 response');
+like($new_worker_pid, qr/\A[0-9]+\n\z/, 'got new worker PID');
+chomp $new_worker_pid;
+ok(kill(0, $new_worker_pid), 'got a valid worker PID');
+isnt($worker_pid, $new_worker_pid, 'worker PID changed');
+
+$quit->();
+
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/working_directory.t b/t/working_directory.t
new file mode 100644
index 0000000..f9254eb
--- /dev/null
+++ b/t/working_directory.t
@@ -0,0 +1,94 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+mkdir "$tmpdir/alt";
+my $ru = "$tmpdir/alt/config.ru";
+open my $fh, '>', $u_conf;
+print $fh <<EOM;
+pid "$pid_file"
+preload_app true
+stderr_path "$err_log"
+working_directory "$tmpdir/alt" # the whole point of this test
+before_fork { |_,_| \$master_ppid = Process.ppid }
+EOM
+close $fh;
+
+my $common_ru = <<'EOM';
+use Rack::ContentLength
+use Rack::ContentType, 'text/plain'
+run lambda { |env| [ 200, {}, [ "#{$master_ppid}\n" ] ] }
+EOM
+
+open $fh, '>', $ru;
+print $fh <<EOM;
+#\\--daemonize --listen $u_sock
+$common_ru
+EOM
+close $fh;
+
+unicorn('-c', $u_conf)->join; # will daemonize
+chomp($daemon_pid = slurp($pid_file));
+
+my ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0');
+is($bdy, "1\n", 'got expected $master_ppid');
+
+stop_daemon;
+check_stderr;
+
+if ('test without CLI switches in config.ru') {
+        truncate $err_log, 0;
+        open $fh, '>', $ru;
+        print $fh $common_ru;
+        close $fh;
+
+        unicorn('-D', '-l', $u_sock, '-c', $u_conf)->join; # will daemonize
+        chomp($daemon_pid = slurp($pid_file));
+
+        ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0');
+        is($bdy, "1\n", 'got expected $master_ppid');
+
+        stop_daemon;
+        check_stderr;
+}
+
+if ('ensures broken working_directory (missing config.ru) is OK') {
+        truncate $err_log, 0;
+        unlink $ru;
+
+        my $auto_reap = unicorn('-c', $u_conf);
+        $auto_reap->join;
+        isnt($?, 0, 'exited with error due to missing config.ru');
+
+        like(slurp($err_log), qr/rackup file \Q(config.ru)\E not readable/,
+                'noted unreadability of config.ru in stderr');
+}
+
+if ('fooapp.rb (not config.ru) works with working_directory') {
+        truncate $err_log, 0;
+        my $fooapp = "$tmpdir/alt/fooapp.rb";
+        open $fh, '>', $fooapp;
+        print $fh <<EOM;
+class Fooapp
+  def self.call(env)
+    b = "dir=#{Dir.pwd}"
+    h = { 'content-type' => 'text/plain', 'content-length' => b.bytesize.to_s }
+    [ 200, h, [ b ] ]
+  end
+end
+EOM
+        close $fh;
+        my $srv = tcp_server;
+        my $auto_reap = unicorn(qw(-c), $u_conf, qw(-I. fooapp.rb),
+                                { -C => '/', 3 => $srv });
+        ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+        is($bdy, "dir=$tmpdir/alt",
+                'fooapp.rb (w/o config.ru) w/ working_directory');
+        $auto_reap->join('TERM');
+        is($?, 0, 'fooapp.rb process exited');
+        check_stderr;
+}
+
+undef $tmpdir;
+done_testing;
diff --git a/t/write-on-close.ru b/t/write-on-close.ru
deleted file mode 100644
index 54a2f2e..0000000
--- a/t/write-on-close.ru
+++ /dev/null
@@ -1,11 +0,0 @@
-class WriteOnClose
-  def each(&block)
-    @callback = block
-  end
-
-  def close
-    @callback.call "7\r\nGoodbye\r\n0\r\n\r\n"
-  end
-end
-use Rack::ContentType, "text/plain"
-run(lambda { |_| [ 200, [%w(Transfer-Encoding chunked)], WriteOnClose.new ] })
diff --git a/test/aggregate.rb b/test/aggregate.rb
index 5eebbe5..0f32b2f 100755
--- a/test/aggregate.rb
+++ b/test/aggregate.rb
@@ -1,5 +1,6 @@
 #!/usr/bin/ruby -n
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 BEGIN { $tests = $assertions = $failures = $errors = 0 }
 
diff --git a/test/benchmark/dd.ru b/test/benchmark/dd.ru
index 111fa2e..5bd2739 100644
--- a/test/benchmark/dd.ru
+++ b/test/benchmark/dd.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # This benchmark is the simplest test of the I/O facilities in
 # unicorn.  It is meant to return a fixed-sized blob to test
 # the performance of things in Unicorn, _NOT_ the app.
diff --git a/test/benchmark/ddstream.ru b/test/benchmark/ddstream.ru
index b14c973..fd40ced 100644
--- a/test/benchmark/ddstream.ru
+++ b/test/benchmark/ddstream.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # This app is intended to test large HTTP responses with or without
 # a fully-buffering reverse proxy such as nginx. Without a fully-buffering
 # reverse proxy, unicorn will be unresponsive when client count exceeds
diff --git a/test/benchmark/readinput.ru b/test/benchmark/readinput.ru
index c91bec3..95c0226 100644
--- a/test/benchmark/readinput.ru
+++ b/test/benchmark/readinput.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # This app is intended to test large HTTP requests with or without
 # a fully-buffering reverse proxy such as nginx. Without a fully-buffering
 # reverse proxy, unicorn will be unresponsive when client count exceeds
diff --git a/test/benchmark/stack.ru b/test/benchmark/stack.ru
index fc9193f..17a565b 100644
--- a/test/benchmark/stack.ru
+++ b/test/benchmark/stack.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 run(lambda { |env|
   body = "#{caller.size}\n"
   h = {
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb
index 32734c1..807f724 100644
--- a/test/exec/test_exec.rb
+++ b/test/exec/test_exec.rb
@@ -1,6 +1,6 @@
 # -*- encoding: binary -*-
-
-# Copyright (c) 2009 Eric Wong
+# frozen_string_literal: false
+# Don't add to this file, new tests are in Perl 5. See t/README
 FLOCK_PATH = File.expand_path(__FILE__)
 require './test/test_helper'
 
@@ -25,20 +25,20 @@ class ExecTest < Test::Unit::TestCase
 
   HI = <<-EOS
 use Rack::ContentLength
-run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
+run proc { |env| [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] }
   EOS
 
   SHOW_RACK_ENV = <<-EOS
 use Rack::ContentLength
 run proc { |env|
-  [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
+  [ 200, { 'content-type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
 }
   EOS
 
   HELLO = <<-EOS
 class Hello
   def call(env)
-    [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
+    [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ]
   end
 end
   EOS
@@ -62,9 +62,9 @@ run lambda { |env|
   a = ::File.stat(pwd)
   b = ::File.stat(Dir.pwd)
   if (a.ino == b.ino && a.dev == b.dev)
-    [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ]
+    [ 200, { 'content-type' => 'text/plain' }, [ pwd ] ]
   else
-    [ 404, { 'Content-Type' => 'text/plain' }, [] ]
+    [ 404, { 'content-type' => 'text/plain' }, [] ]
   end
 }
   EOS
@@ -97,59 +97,6 @@ run lambda { |env|
     end
   end
 
-  def test_sd_listen_fds_emulation
-    # [ruby-core:69895] [Bug #11336] fixed by r51576
-    return if RUBY_VERSION.to_f < 2.3
-
-    File.open("config.ru", "wb") { |fp| fp.write(HI) }
-    sock = TCPServer.new(@addr, @port)
-
-    [ %W(-l #@addr:#@port), nil ].each do |l|
-      sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
-
-      pid = xfork do
-        redirect_test_io do
-          # pretend to be systemd
-          ENV['LISTEN_PID'] = "#$$"
-          ENV['LISTEN_FDS'] = '1'
-
-          # 3 = SD_LISTEN_FDS_START
-          args = [ $unicorn_bin ]
-          args.concat(l) if l
-          args << { 3 => sock }
-          exec(*args)
-        end
-      end
-      res = hit(["http://#@addr:#@port/"])
-      assert_equal [ "HI\n" ], res
-      assert_shutdown(pid)
-      assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
-                  'unicorn should always set SO_KEEPALIVE on inherited sockets'
-    end
-  ensure
-    sock.close if sock
-  end
-
-  def test_inherit_listener_unspecified
-    File.open("config.ru", "wb") { |fp| fp.write(HI) }
-    sock = TCPServer.new(@addr, @port)
-    sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
-
-    pid = xfork do
-      redirect_test_io do
-        ENV['UNICORN_FD'] = sock.fileno.to_s
-        exec($unicorn_bin, sock.fileno => sock.fileno)
-      end
-    end
-    res = hit(["http://#@addr:#@port/"])
-    assert_equal [ "HI\n" ], res
-    assert_shutdown(pid)
-    assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
-                'unicorn should always set SO_KEEPALIVE on inherited sockets'
-  ensure
-    sock.close if sock
-  end
-
   def test_working_directory_rel_path_config_file
     other = Tempfile.new('unicorn.wd')
     File.unlink(other.path)
@@ -255,7 +202,13 @@ after_fork do |server, worker|
 end
 EOF
     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
-    File.open("#{other.path}/fifo", "rb").close
+    begin
+      fifo = File.open("#{other.path}/fifo", "rb")
+    rescue Errno::EINTR
+      # OpenBSD raises Errno::EINTR when opening
+      return if RUBY_PLATFORM =~ /openbsd/
+    end
+    fifo.close
 
     assert ! File.exist?("stderr_log_here")
     assert ! File.exist?("stdout_log_here")
@@ -292,16 +245,6 @@ EOF
     end
   end
 
-  def test_basic
-    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
-    pid = fork do
-      redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
-    end
-    results = retry_hit(["http://#{@addr}:#{@port}/"])
-    assert_equal String, results[0].class
-    assert_shutdown(pid)
-  end
-
   def test_rack_env_unset
     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
@@ -557,7 +500,7 @@ EOF
   def test_unicorn_config_per_worker_listen
     port2 = unused_port
     pid_spit = 'use Rack::ContentLength;' \
-      'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
+      'run proc { |e| [ 200, {"content-type"=>"text/plain"}, ["#$$\\n"] ] }'
     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
     tmp = Tempfile.new('test.socket')
     File.unlink(tmp.path)
@@ -574,7 +517,7 @@ EOF
     assert_equal String, results[0].class
     worker_pid = results[0].to_i
     assert_not_equal pid, worker_pid
-    s = UNIXSocket.new(tmp.path)
+    s = unix_socket(tmp.path)
     s.syswrite("GET / HTTP/1.0\r\n\r\n")
     results = ''
     loop { results << s.sysread(4096) } rescue nil
@@ -665,20 +608,6 @@ EOF
     assert_shutdown(pid)
   end
 
-  def test_config_ru_alt_path
-    config_path = "#{@tmpdir}/foo.ru"
-    File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
-    pid = fork do
-      redirect_test_io do
-        Dir.chdir("/")
-        exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
-      end
-    end
-    results = retry_hit(["http://#{@addr}:#{@port}/"])
-    assert_equal String, results[0].class
-    assert_shutdown(pid)
-  end
-
   def test_load_module
     libdir = "#{@tmpdir}/lib"
     FileUtils.mkpath([ libdir ])
@@ -732,7 +661,7 @@ EOF
     wait_for_file(sock_path)
     assert File.socket?(sock_path)
 
-    sock = UNIXSocket.new(sock_path)
+    sock = unix_socket(sock_path)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     results = sock.sysread(4096)
 
@@ -742,7 +671,7 @@ EOF
     wait_for_file(sock_path)
     assert File.socket?(sock_path)
 
-    sock = UNIXSocket.new(sock_path)
+    sock = unix_socket(sock_path)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     results = sock.sysread(4096)
 
@@ -777,7 +706,7 @@ EOF
     assert_equal pid, File.read(pid_file).to_i
     assert File.socket?(sock_path), "socket created"
 
-    sock = UNIXSocket.new(sock_path)
+    sock = unix_socket(sock_path)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     results = sock.sysread(4096)
 
@@ -803,7 +732,7 @@ EOF
     wait_for_file(new_sock_path)
     assert File.socket?(new_sock_path), "socket exists"
     @sockets.each do |path|
-      sock = UNIXSocket.new(path)
+      sock = unix_socket(path)
       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
       results = sock.sysread(4096)
       assert_equal String, results.class
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 974d2f2..0bf3c90 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
@@ -28,6 +29,7 @@ require 'tempfile'
 require 'fileutils'
 require 'logger'
 require 'unicorn'
+require 'io/nonblock'
 
 if ENV['DEBUG']
   require 'ruby-debug'
@@ -42,6 +44,7 @@ end
 def redirect_test_io
   orig_err = STDERR.dup
   orig_out = STDOUT.dup
+  rdr_pid = $$
   new_out = File.open("test_stdout.#$$.log", "a")
   new_err = File.open("test_stderr.#$$.log", "a")
   new_out.sync = new_err.sync = true
@@ -59,8 +62,10 @@ def redirect_test_io
   STDERR.sync = STDOUT.sync = true
 
   at_exit do
-    File.unlink(new_out.path) rescue nil
-    File.unlink(new_err.path) rescue nil
+    if rdr_pid == $$
+      File.unlink(new_out.path) rescue nil
+      File.unlink(new_err.path) rescue nil
+    end
   end
 
   begin
@@ -288,3 +293,15 @@ def reset_sig_handlers
     trap(sig, "DEFAULT")
   end
 end
+
+def tcp_socket(*args)
+  sock = TCPSocket.new(*args)
+  sock.nonblock = false
+  sock
+end
+
+def unix_socket(*args)
+  sock = UNIXSocket.new(*args)
+  sock.nonblock = false
+  sock
+end
diff --git a/test/unit/test_ccc.rb b/test/unit/test_ccc.rb
index 3be1439..a0a2bff 100644
--- a/test/unit/test_ccc.rb
+++ b/test/unit/test_ccc.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: false
 require 'socket'
 require 'unicorn'
 require 'io/wait'
 require 'tempfile'
 require 'test/unit'
+require './test/test_helper'
 
 class TestCccTCPI < Test::Unit::TestCase
   def test_ccc_tcpi
@@ -28,7 +30,7 @@ class TestCccTCPI < Test::Unit::TestCase
         # will wake up when writer closes
         sleep_pipe[0].read if env['PATH_INFO'] == '/sleep'
 
-        [ 200, [ %w(Content-Length 0),  %w(Content-Type text/plain) ], [] ]
+        [ 200, {'content-length'=>'0', 'content-type'=>'text/plain'}, [] ]
       end
       ENV['UNICORN_FD'] = srv.fileno.to_s
       opts = {
@@ -42,7 +44,7 @@ class TestCccTCPI < Test::Unit::TestCase
     wr.close
 
     # make sure the server is running, at least
-    client = TCPSocket.new(host, port)
+    client = tcp_socket(host, port)
     client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
     assert client.wait(10), 'never got response from server'
     res = client.read
@@ -51,13 +53,13 @@ class TestCccTCPI < Test::Unit::TestCase
     client.close
 
     # start a slow request...
-    sleeper = TCPSocket.new(host, port)
+    sleeper = tcp_socket(host, port)
     sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n")
 
     # and a bunch of aborted ones
     nr = 100
     nr.times do |i|
-      client = TCPSocket.new(host, port)
+      client = tcp_socket(host, port)
       client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \
                    "Host: example.com\r\n\r\n")
       client.close
diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb
index 1298f0e..1a89aca 100644
--- a/test/unit/test_configurator.rb
+++ b/test/unit/test_configurator.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require 'test/unit'
 require 'tempfile'
diff --git a/test/unit/test_droplet.rb b/test/unit/test_droplet.rb
index 81ad82b..4b2d2d0 100644
--- a/test/unit/test_droplet.rb
+++ b/test/unit/test_droplet.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 require 'test/unit'
 require 'unicorn'
 
diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb
index 697af44..adcc84f 100644
--- a/test/unit/test_http_parser.rb
+++ b/test/unit/test_http_parser.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb
index 425d5ad..fd47246 100644
--- a/test/unit/test_http_parser_ng.rb
+++ b/test/unit/test_http_parser_ng.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require './test/test_helper'
 require 'digest/md5'
diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb
index 6cb0268..9d1b350 100644
--- a/test/unit/test_request.rb
+++ b/test/unit/test_request.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Copyright (c) 2009 Eric Wong
 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
@@ -10,19 +11,14 @@ include Unicorn
 
 class RequestTest < Test::Unit::TestCase
 
-  class MockRequest < StringIO
-    alias_method :readpartial, :sysread
-    alias_method :kgio_read!, :sysread
-    alias_method :read_nonblock, :sysread
-    def kgio_addr
-      '127.0.0.1'
-    end
-  end
+  MockRequest = Class.new(StringIO)
+
+  AI = Addrinfo.new(Socket.sockaddr_un('/unicorn/sucks'))
 
   def setup
     @request = HttpRequest.new
     @app = lambda do |env|
-      [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
+      [ 200, { 'content-length' => '0', 'content-type' => 'text/plain' }, [] ]
     end
     @lint = Rack::Lint.new(@app)
   end
@@ -30,7 +26,7 @@ class RequestTest < Test::Unit::TestCase
   def test_options
     client = MockRequest.new("OPTIONS * HTTP/1.1\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal '', env['REQUEST_PATH']
     assert_equal '', env['PATH_INFO']
     assert_equal '*', env['REQUEST_URI']
@@ -40,7 +36,7 @@ class RequestTest < Test::Unit::TestCase
   def test_absolute_uri_with_query
     client = MockRequest.new("GET http://e:3/x?y=z HTTP/1.1\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal '/x', env['REQUEST_PATH']
     assert_equal '/x', env['PATH_INFO']
     assert_equal 'y=z', env['QUERY_STRING']
@@ -50,7 +46,7 @@ class RequestTest < Test::Unit::TestCase
   def test_absolute_uri_with_fragment
     client = MockRequest.new("GET http://e:3/x#frag HTTP/1.1\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal '/x', env['REQUEST_PATH']
     assert_equal '/x', env['PATH_INFO']
     assert_equal '', env['QUERY_STRING']
@@ -61,7 +57,7 @@ class RequestTest < Test::Unit::TestCase
   def test_absolute_uri_with_query_and_fragment
     client = MockRequest.new("GET http://e:3/x?a=b#frag HTTP/1.1\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal '/x', env['REQUEST_PATH']
     assert_equal '/x', env['PATH_INFO']
     assert_equal 'a=b', env['QUERY_STRING']
@@ -73,7 +69,7 @@ class RequestTest < Test::Unit::TestCase
     %w(ssh+http://e/ ftp://e/x http+ssh://e/x).each do |abs_uri|
       client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \
                                "Host: foo\r\n\r\n")
-      assert_raises(HttpParserError) { @request.read(client) }
+      assert_raises(HttpParserError) { @request.read_headers(client, AI) }
     end
   end
 
@@ -81,7 +77,7 @@ class RequestTest < Test::Unit::TestCase
     client = MockRequest.new("GET / HTTP/1.1\r\n" \
                              "X-Forwarded-Proto: https\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal "https", env['rack.url_scheme']
     assert_kind_of Array, @lint.call(env)
   end
@@ -90,7 +86,7 @@ class RequestTest < Test::Unit::TestCase
     client = MockRequest.new("GET / HTTP/1.1\r\n" \
                              "X-Forwarded-Proto: http\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal "http", env['rack.url_scheme']
     assert_kind_of Array, @lint.call(env)
   end
@@ -99,14 +95,14 @@ class RequestTest < Test::Unit::TestCase
     client = MockRequest.new("GET / HTTP/1.1\r\n" \
                              "X-Forwarded-Proto: ftp\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal "http", env['rack.url_scheme']
     assert_kind_of Array, @lint.call(env)
   end
 
   def test_rack_lint_get
     client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal "http", env['rack.url_scheme']
     assert_equal '127.0.0.1', env['REMOTE_ADDR']
     assert_kind_of Array, @lint.call(env)
@@ -114,7 +110,7 @@ class RequestTest < Test::Unit::TestCase
 
   def test_no_content_stringio
     client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal StringIO, env['rack.input'].class
   end
 
@@ -122,7 +118,7 @@ class RequestTest < Test::Unit::TestCase
     client = MockRequest.new("PUT / HTTP/1.1\r\n" \
                              "Content-Length: 0\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal StringIO, env['rack.input'].class
   end
 
@@ -130,7 +126,7 @@ class RequestTest < Test::Unit::TestCase
     client = MockRequest.new("PUT / HTTP/1.1\r\n" \
                              "Content-Length: 1\r\n" \
                              "Host: foo\r\n\r\n")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert_equal Unicorn::TeeInput, env['rack.input'].class
   end
 
@@ -141,7 +137,7 @@ class RequestTest < Test::Unit::TestCase
       "Content-Length: 5\r\n" \
       "\r\n" \
       "abcde")
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert ! env.include?(:http_body)
     assert_kind_of Array, @lint.call(env)
   end
@@ -152,14 +148,6 @@ class RequestTest < Test::Unit::TestCase
     buf = (' ' * bs).freeze
     length = bs * count
     client = Tempfile.new('big_put')
-    def client.kgio_addr; '127.0.0.1'; end
-    def client.kgio_read(*args)
-      readpartial(*args)
-    rescue EOFError
-    end
-    def client.kgio_read!(*args)
-      readpartial(*args)
-    end
     client.syswrite(
       "PUT / HTTP/1.1\r\n" \
       "Host: foo\r\n" \
@@ -167,7 +155,7 @@ class RequestTest < Test::Unit::TestCase
       "\r\n")
     count.times { assert_equal bs, client.syswrite(buf) }
     assert_equal 0, client.sysseek(0)
-    env = @request.read(client)
+    env = @request.read_headers(client, AI)
     assert ! env.include?(:http_body)
     assert_equal length, env['rack.input'].size
     count.times {
diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb
deleted file mode 100644
index fbe433f..0000000
--- a/test/unit/test_response.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- encoding: binary -*-
-
-# Copyright (c) 2005 Zed A. Shaw
-# You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
-# the GPLv2+ (GPLv3+ preferred)
-#
-# Additional work donated by contributors.  See git history
-# for more information.
-
-require './test/test_helper'
-require 'time'
-
-include Unicorn
-
-class ResponseTest < Test::Unit::TestCase
-  include Unicorn::HttpResponse
-
-  def test_httpdate
-    before = Time.now.to_i - 1
-    str = httpdate
-    assert_kind_of(String, str)
-    middle = Time.parse(str).to_i
-    after = Time.now.to_i
-    assert before <= middle
-    assert middle <= after
-  end
-
-  def test_response_headers
-    out = StringIO.new
-    http_response_write(out, 200, {"X-Whatever" => "stuff"}, ["cool"])
-    assert ! out.closed?
-
-    assert out.length > 0, "output didn't have data"
-  end
-
-  # ref: <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com>
-  def test_response_header_broken_nil
-    out = StringIO.new
-    http_response_write(out, 200, {"Nil" => nil}, %w(hysterical raisin))
-    assert ! out.closed?
-
-    assert_match %r{^Nil: \r\n}sm, out.string, 'nil accepted'
-  end
-
-  def test_response_string_status
-    out = StringIO.new
-    http_response_write(out,'200', {}, [])
-    assert ! out.closed?
-    assert out.length > 0, "output didn't have data"
-  end
-
-  def test_response_200
-    io = StringIO.new
-    http_response_write(io, 200, {}, [])
-    assert ! io.closed?
-    assert io.length > 0, "output didn't have data"
-  end
-
-  def test_response_with_default_reason
-    code = 400
-    io = StringIO.new
-    http_response_write(io, code, {}, [])
-    assert ! io.closed?
-    lines = io.string.split(/\r\n/)
-    assert_match(/.* Bad Request$/, lines.first,
-                 "wrong default reason phrase")
-  end
-
-  def test_rack_multivalue_headers
-    out = StringIO.new
-    http_response_write(out,200, {"X-Whatever" => "stuff\nbleh"}, [])
-    assert ! out.closed?
-    assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string)
-  end
-
-  # Even though Rack explicitly forbids "Status" in the header hash,
-  # some broken clients still rely on it
-  def test_status_header_added
-    out = StringIO.new
-    http_response_write(out,200, {"X-Whatever" => "stuff"}, [])
-    assert ! out.closed?
-  end
-
-  def test_unknown_status_pass_through
-    out = StringIO.new
-    http_response_write(out,"666 I AM THE BEAST", {}, [] )
-    assert ! out.closed?
-    headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/)
-    assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0])
-  end
-
-  def test_modified_rack_http_status_codes_late
-    r, w = IO.pipe
-    pid = fork do
-      r.close
-      # Users may want to globally override the status text associated
-      # with an HTTP status code in their app.
-      Rack::Utils::HTTP_STATUS_CODES[200] = "HI"
-      http_response_write(w, 200, {}, [])
-      w.close
-    end
-    w.close
-    assert_equal "HTTP/1.1 200 HI\r\n", r.gets
-    r.read # just drain the pipe
-    pid, status = Process.waitpid2(pid)
-    assert status.success?, status.inspect
-  ensure
-    r.close
-    w.close unless w.closed?
-  end
-end
diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb
index 384fa6b..5a2252f 100644
--- a/test/unit/test_server.rb
+++ b/test/unit/test_server.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
@@ -16,21 +17,28 @@ class TestHandler
   def call(env)
     while env['rack.input'].read(4096)
     end
-    [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
+    [200, { 'content-type' => 'text/plain' }, ['hello!\n']]
   rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
     $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
     raise e
   end
 end
 
-class TestEarlyHintsHandler
+class TestRackAfterReply
+  def initialize
+    @called = false
+  end
+
   def call(env)
     while env['rack.input'].read(4096)
     end
-    env['rack.early_hints'].call(
-      "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload"
-    )
-    [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
+
+    env["rack.after_reply"] << -> { @called = true }
+
+    [200, { 'content-type' => 'text/plain' }, ["after_reply_called: #{@called}"]]
+  rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
+    $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
+    raise e
   end
 end
 
@@ -63,7 +71,7 @@ class WebServerTest < Test::Unit::TestCase
       tmp.sysseek(0)
       tmp.truncate(0)
       tmp.syswrite($$)
-      lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "#$$\n" ] ] }
+      lambda { |env| [ 200, { 'content-type' => 'text/plain' }, [ "#$$\n" ] ] }
     }
     redirect_test_io do
       @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
@@ -94,38 +102,30 @@ class WebServerTest < Test::Unit::TestCase
     tmp.close!
   end
 
-  def test_early_hints
+  def test_after_reply
     teardown
+
     redirect_test_io do
-      @server = HttpServer.new(TestEarlyHintsHandler.new,
-                               :listeners => [ "127.0.0.1:#@port"],
-                               :early_hints => true)
+      @server = HttpServer.new(TestRackAfterReply.new,
+                               :listeners => [ "127.0.0.1:#@port"])
       @server.start
     end
 
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
 
     responses = sock.read(4096)
-    assert_match %r{\AHTTP/1.[01] 103\b}, responses
-    assert_match %r{^Link: </style\.css>}, responses
-    assert_match %r{^Link: </script\.js>}, responses
-
-    assert_match %r{^HTTP/1.[01] 200\b}, responses
-  end
+    assert_match %r{\AHTTP/1.[01] 200\b}, responses
+    assert_match %r{^after_reply_called: false}, responses
 
-  def test_broken_app
-    teardown
-    app = lambda { |env| raise RuntimeError, "hello" }
-    # [200, {}, []] }
-    redirect_test_io do
-      @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
-      @server.start
-    end
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
-    assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096)
-    assert_nil sock.close
+
+    responses = sock.read(4096)
+    assert_match %r{\AHTTP/1.[01] 200\b}, responses
+    assert_match %r{^after_reply_called: true}, responses
+
+    sock.close
   end
 
   def test_simple_server
@@ -133,62 +133,9 @@ class WebServerTest < Test::Unit::TestCase
     assert_equal 'hello!\n', results[0], "Handler didn't really run"
   end
 
-  def test_client_shutdown_writes
-    bs = 15609315 * rand
-    sock = TCPSocket.new('127.0.0.1', @port)
-    sock.syswrite("PUT /hello HTTP/1.1\r\n")
-    sock.syswrite("Host: example.com\r\n")
-    sock.syswrite("Transfer-Encoding: chunked\r\n")
-    sock.syswrite("Trailer: X-Foo\r\n")
-    sock.syswrite("\r\n")
-    sock.syswrite("%x\r\n" % [ bs ])
-    sock.syswrite("F" * bs)
-    sock.syswrite("\r\n0\r\nX-")
-    "Foo: bar\r\n\r\n".each_byte do |x|
-      sock.syswrite x.chr
-      sleep 0.05
-    end
-    # we wrote the entire request before shutting down, server should
-    # continue to process our request and never hit EOFError on our sock
-    sock.shutdown(Socket::SHUT_WR)
-    buf = sock.read
-    assert_equal 'hello!\n', buf.split(/\r\n\r\n/).last
-    next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
-    assert_equal 'hello!\n', next_client
-    lines = File.readlines("test_stderr.#$$.log")
-    assert lines.grep(/^Unicorn::ClientShutdown: /).empty?
-    assert_nil sock.close
-  end
-
-  def test_client_shutdown_write_truncates
-    bs = 15609315 * rand
-    sock = TCPSocket.new('127.0.0.1', @port)
-    sock.syswrite("PUT /hello HTTP/1.1\r\n")
-    sock.syswrite("Host: example.com\r\n")
-    sock.syswrite("Transfer-Encoding: chunked\r\n")
-    sock.syswrite("Trailer: X-Foo\r\n")
-    sock.syswrite("\r\n")
-    sock.syswrite("%x\r\n" % [ bs ])
-    sock.syswrite("F" * (bs / 2.0))
-
-    # shutdown prematurely, this will force the server to abort
-    # processing on us even during app dispatch
-    sock.shutdown(Socket::SHUT_WR)
-    IO.select([sock], nil, nil, 60) or raise "Timed out"
-    buf = sock.read
-    assert_equal "", buf
-    next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
-    assert_equal 'hello!\n', next_client
-    lines = File.readlines("test_stderr.#$$.log")
-    lines = lines.grep(/^Unicorn::ClientShutdown: bytes_read=\d+/)
-    assert_equal 1, lines.size
-    assert_match %r{\AUnicorn::ClientShutdown: bytes_read=\d+ true$}, lines[0]
-    assert_nil sock.close
-  end
-
   def test_client_malformed_body
     bs = 15653984
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("PUT /hello HTTP/1.1\r\n")
     sock.syswrite("Host: example.com\r\n")
     sock.syswrite("Transfer-Encoding: chunked\r\n")
@@ -210,7 +157,7 @@ class WebServerTest < Test::Unit::TestCase
 
   def do_test(string, chunk, close_after=nil, shutdown_delay=0)
     # Do not use instance variables here, because it needs to be thread safe
-    socket = TCPSocket.new("127.0.0.1", @port);
+    socket = tcp_socket("127.0.0.1", @port);
     request = StringIO.new(string)
     chunks_out = 0
 
@@ -255,14 +202,14 @@ class WebServerTest < Test::Unit::TestCase
   end
 
   def test_bad_client_400
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n")
     assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096)
     assert_nil sock.close
   end
 
   def test_http_0_9
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET /hello\r\n")
     assert_match 'hello!\n', sock.sysread(4096)
     assert_nil sock.close
diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb
index 4d9fdc5..49ff3c7 100644
--- a/test/unit/test_signals.rb
+++ b/test/unit/test_signals.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 # Copyright (c) 2009 Eric Wong
 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
@@ -47,16 +48,16 @@ class SignalsTest < Test::Unit::TestCase
 
   def test_worker_dies_on_dead_master
     pid = fork {
-      app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] }
+      app = lambda { |env| [ 200, {'x-pid' => "#$$" }, [] ] }
       opts = @server_opts.merge(:timeout => 3)
       redirect_test_io { HttpServer.new(app, opts).start.join }
     }
     wait_workers_ready("test_stderr.#{pid}.log", 1)
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     buf = sock.readpartial(4096)
     assert_nil sock.close
-    buf =~ /\bX-Pid: (\d+)\b/ or raise Exception
+    buf =~ /\bx-pid: (\d+)\b/ or raise Exception
     child = $1.to_i
     wait_master_ready("test_stderr.#{pid}.log")
     wait_workers_ready("test_stderr.#{pid}.log", 1)
@@ -79,7 +80,7 @@ class SignalsTest < Test::Unit::TestCase
     }
     wr.close
     wait_workers_ready("test_stderr.#{pid}.log", 1)
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     buf = rd.readpartial(1)
     wait_master_ready("test_stderr.#{pid}.log")
@@ -102,7 +103,7 @@ class SignalsTest < Test::Unit::TestCase
     }
     t0 = Time.now
     wait_workers_ready("test_stderr.#{pid}.log", 1)
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
 
     buf = nil
@@ -120,17 +121,17 @@ class SignalsTest < Test::Unit::TestCase
 
   def test_response_write
     app = lambda { |env|
-      [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s },
+      [ 200, { 'content-type' => 'text/plain', 'x-pid' => Process.pid.to_s },
         Dd.new(@bs, @count) ]
     }
     redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
     wait_workers_ready("test_stderr.#{$$}.log", 1)
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
     buf = ''
     header_len = pid = nil
     buf = sock.sysread(16384, buf)
-    pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+    pid = buf[/\r\nx-pid: (\d+)\r\n/, 1].to_i
     header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size
     assert pid > 0, "pid not positive: #{pid.inspect}"
     read = buf.size
@@ -158,18 +159,18 @@ class SignalsTest < Test::Unit::TestCase
     app = lambda { |env|
       while env['rack.input'].read(4096)
       end
-      [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ]
+      [ 200, {'content-type'=>'text/plain', 'x-pid'=>Process.pid.to_s}, [] ]
     }
     redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
 
     wait_workers_ready("test_stderr.#{$$}.log", 1)
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
-    pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+    pid = sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i
     assert_nil sock.close
 
     assert pid > 0, "pid not positive: #{pid.inspect}"
-    sock = TCPSocket.new('127.0.0.1', @port)
+    sock = tcp_socket('127.0.0.1', @port)
     sock.syswrite("PUT / HTTP/1.0\r\n")
     sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n")
     1000.times { Process.kill(:HUP, pid) }
@@ -182,7 +183,7 @@ class SignalsTest < Test::Unit::TestCase
     redirect_test_io { @server.stop(true) }
     # can't check for == since pending signals get merged
     assert size_before < @tmp.stat.size
-    assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+    assert_equal pid, sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i
     assert_nil sock.close
   end
 end
diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb
index fbc7bb9..4363474 100644
--- a/test/unit/test_socket_helper.rb
+++ b/test/unit/test_socket_helper.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require './test/test_helper'
 require 'tempfile'
@@ -24,7 +25,8 @@ class TestSocketHelper < Test::Unit::TestCase
     port = unused_port @test_addr
     @tcp_listener_name = "#@test_addr:#{port}"
     @tcp_listener = bind_listen(@tcp_listener_name)
-    assert TCPServer === @tcp_listener
+    assert Socket === @tcp_listener
+    assert @tcp_listener.local_address.ip?
     assert_equal @tcp_listener_name, sock_name(@tcp_listener)
   end
 
@@ -38,10 +40,10 @@ class TestSocketHelper < Test::Unit::TestCase
       { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 }
     ].each do |opts|
       tcp_listener = bind_listen(tcp_listener_name, opts)
-      assert TCPServer === tcp_listener
+      assert tcp_listener.local_address.ip?
       tcp_listener.close
       unix_listener = bind_listen(unix_listener_name, opts)
-      assert UNIXServer === unix_listener
+      assert unix_listener.local_address.unix?
       unix_listener.close
     end
   end
@@ -52,11 +54,13 @@ class TestSocketHelper < Test::Unit::TestCase
     @unix_listener_path = tmp.path
     File.unlink(@unix_listener_path)
     @unix_listener = bind_listen(@unix_listener_path)
-    assert UNIXServer === @unix_listener
+    assert Socket === @unix_listener
+    assert @unix_listener.local_address.unix?
     assert_equal @unix_listener_path, sock_name(@unix_listener)
     assert File.readable?(@unix_listener_path), "not readable"
     assert File.writable?(@unix_listener_path), "not writable"
     assert_equal 0777, File.umask
+    assert_equal @unix_listener, bind_listen(@unix_listener)
   ensure
     File.umask(old_umask)
   end
@@ -67,7 +71,6 @@ class TestSocketHelper < Test::Unit::TestCase
     @unix_listener_path = tmp.path
     File.unlink(@unix_listener_path)
     @unix_listener = bind_listen(@unix_listener_path, :umask => 077)
-    assert UNIXServer === @unix_listener
     assert_equal @unix_listener_path, sock_name(@unix_listener)
     assert_equal 0140700, File.stat(@unix_listener_path).mode
     assert_equal 0777, File.umask
@@ -75,28 +78,6 @@ class TestSocketHelper < Test::Unit::TestCase
     File.umask(old_umask)
   end
 
-  def test_bind_listen_unix_idempotent
-    test_bind_listen_unix
-    a = bind_listen(@unix_listener)
-    assert_equal a.fileno, @unix_listener.fileno
-    unix_server = server_cast(@unix_listener)
-    assert UNIXServer === unix_server
-    a = bind_listen(unix_server)
-    assert_equal a.fileno, unix_server.fileno
-    assert_equal a.fileno, @unix_listener.fileno
-  end
-
-  def test_bind_listen_tcp_idempotent
-    test_bind_listen_tcp
-    a = bind_listen(@tcp_listener)
-    assert_equal a.fileno, @tcp_listener.fileno
-    tcp_server = server_cast(@tcp_listener)
-    assert TCPServer === tcp_server
-    a = bind_listen(tcp_server)
-    assert_equal a.fileno, tcp_server.fileno
-    assert_equal a.fileno, @tcp_listener.fileno
-  end
-
   def test_bind_listen_unix_rebind
     test_bind_listen_unix
     new_listener = nil
@@ -107,48 +88,26 @@ class TestSocketHelper < Test::Unit::TestCase
     File.unlink(@unix_listener_path)
     new_listener = bind_listen(@unix_listener_path)
 
-    assert UNIXServer === new_listener
     assert new_listener.fileno != @unix_listener.fileno
     assert_equal sock_name(new_listener), sock_name(@unix_listener)
     assert_equal @unix_listener_path, sock_name(new_listener)
     pid = fork do
-      client = server_cast(new_listener).accept
-      client.syswrite('abcde')
-      exit 0
+      begin
+        client, _ = new_listener.accept
+        client.syswrite('abcde')
+        exit 0
+      rescue => e
+        warn "#{e.message} (#{e.class})"
+        exit 1
+      end
     end
-    s = UNIXSocket.new(@unix_listener_path)
+    s = unix_socket(@unix_listener_path)
     IO.select([s])
     assert_equal 'abcde', s.sysread(5)
     pid, status = Process.waitpid2(pid)
     assert status.success?
   end
 
-  def test_server_cast
-    test_bind_listen_unix
-    test_bind_listen_tcp
-    unix_listener_socket = Socket.for_fd(@unix_listener.fileno)
-    assert Socket === unix_listener_socket
-    @unix_server = server_cast(unix_listener_socket)
-    assert_equal @unix_listener.fileno, @unix_server.fileno
-    assert UNIXServer === @unix_server
-    assert_equal(@unix_server.path, @unix_listener.path,
-                 "##{@unix_server.path} != #{@unix_listener.path}")
-    assert File.socket?(@unix_server.path)
-    assert_equal @unix_listener_path, sock_name(@unix_server)
-
-    tcp_listener_socket = Socket.for_fd(@tcp_listener.fileno)
-    assert Socket === tcp_listener_socket
-    @tcp_server = server_cast(tcp_listener_socket)
-    assert_equal @tcp_listener.fileno, @tcp_server.fileno
-    assert TCPServer === @tcp_server
-    assert_equal @tcp_listener_name, sock_name(@tcp_server)
-  end
-
-  def test_sock_name
-    test_server_cast
-    sock_name(@unix_server)
-  end
-
   def test_tcp_defer_accept_default
     return unless defined?(TCP_DEFER_ACCEPT)
     port = unused_port @test_addr
diff --git a/test/unit/test_stream_input.rb b/test/unit/test_stream_input.rb
index 1a07ec3..7ee98e4 100644
--- a/test/unit/test_stream_input.rb
+++ b/test/unit/test_stream_input.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require 'test/unit'
 require 'digest/sha1'
@@ -6,16 +7,16 @@ require 'unicorn'
 
 class TestStreamInput < Test::Unit::TestCase
   def setup
-    @rs = $/
+    @rs = "\n"
+    $/ == "\n" or abort %q{test broken if \$/ != "\\n"}
     @env = {}
-    @rd, @wr = Kgio::UNIXSocket.pair
+    @rd, @wr = UNIXSocket.pair
     @rd.sync = @wr.sync = true
     @start_pid = $$
   end
 
   def teardown
     return if $$ != @start_pid
-    $/ = @rs
     @rd.close rescue nil
     @wr.close rescue nil
     Process.waitall
@@ -54,11 +55,18 @@ class TestStreamInput < Test::Unit::TestCase
   end
 
   def test_gets_empty_rs
-    $/ = nil
     r = init_request("a\nb\n\n")
     si = Unicorn::StreamInput.new(@rd, r)
-    assert_equal "a\nb\n\n", si.gets
-    assert_nil si.gets
+    pid = fork do # to avoid $/ warning (hopefully)
+      $/ = nil
+      @rd.close
+      @wr.write(si.gets)
+      @wr.close
+    end
+    @wr.close
+    assert_equal "a\nb\n\n", @rd.read
+    pid, status = Process.waitpid2(pid)
+    assert_predicate status, :success?
   end
 
   def test_read_with_equal_len
@@ -90,21 +98,21 @@ class TestStreamInput < Test::Unit::TestCase
   end
 
   def test_gets_long
-    r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
+    r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size)
     si = Unicorn::StreamInput.new(@rd, r)
     status = line = nil
     pid = fork {
       @rd.close
       3.times { @wr.write("ffff" * 4096) }
-      @wr.write "#$/foo#$/"
+      @wr.write "#{@rs}foo#{@rs}"
       @wr.close
     }
     @wr.close
     line = si.gets
     assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size)
-    assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line)
+    assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line)
     line = si.gets
-    assert_equal "foo#$/", line
+    assert_equal "foo#{@rs}", line
     assert_nil si.gets
     pid, status = Process.waitpid2(pid)
     assert status.success?
diff --git a/test/unit/test_tee_input.rb b/test/unit/test_tee_input.rb
index 4647e66..8f05c77 100644
--- a/test/unit/test_tee_input.rb
+++ b/test/unit/test_tee_input.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require 'test/unit'
 require 'digest/sha1'
@@ -9,17 +10,16 @@ class TeeInput < Unicorn::TeeInput
 end
 
 class TestTeeInput < Test::Unit::TestCase
-
   def setup
-    @rs = $/
-    @rd, @wr = Kgio::UNIXSocket.pair
+    @rd, @wr = UNIXSocket.pair
     @rd.sync = @wr.sync = true
     @start_pid = $$
+    @rs = "\n"
+    $/ == "\n" or abort %q{test broken if \$/ != "\\n"}
   end
 
   def teardown
     return if $$ != @start_pid
-    $/ = @rs
     @rd.close rescue nil
     @wr.close rescue nil
     begin
@@ -37,38 +37,38 @@ class TestTeeInput < Test::Unit::TestCase
   end
 
   def test_gets_long
-    r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
+    r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size)
     ti = TeeInput.new(@rd, r)
     status = line = nil
     pid = fork {
       @rd.close
       3.times { @wr.write("ffff" * 4096) }
-      @wr.write "#$/foo#$/"
+      @wr.write "#{@rs}foo#{@rs}"
       @wr.close
     }
     @wr.close
     line = ti.gets
     assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size)
-    assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line)
+    assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line)
     line = ti.gets
-    assert_equal "foo#$/", line
+    assert_equal "foo#{@rs}", line
     assert_nil ti.gets
     pid, status = Process.waitpid2(pid)
     assert status.success?
   end
 
   def test_gets_short
-    r = init_request("hello", 5 + "#$/foo".size)
+    r = init_request("hello", 5 + "#{@rs}foo".size)
     ti = TeeInput.new(@rd, r)
     status = line = nil
     pid = fork {
       @rd.close
-      @wr.write "#$/foo"
+      @wr.write "#{@rs}foo"
       @wr.close
     }
     @wr.close
     line = ti.gets
-    assert_equal("hello#$/", line)
+    assert_equal("hello#{@rs}", line)
     line = ti.gets
     assert_equal "foo", line
     assert_nil ti.gets
diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb
deleted file mode 100644
index 8d90e50..0000000
--- a/test/unit/test_upload.rb
+++ /dev/null
@@ -1,301 +0,0 @@
-# -*- encoding: binary -*-
-
-# Copyright (c) 2009 Eric Wong
-require './test/test_helper'
-require 'digest/md5'
-
-include Unicorn
-
-class UploadTest < Test::Unit::TestCase
-
-  def setup
-    @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
-    @port = unused_port
-    @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'}
-    @bs = 4096
-    @count = 256
-    @server = nil
-
-    # we want random binary data to test 1.9 encoding-aware IO craziness
-    @random = File.open('/dev/urandom','rb')
-    @sha1 = Digest::SHA1.new
-    @sha1_app = lambda do |env|
-      input = env['rack.input']
-      resp = {}
-
-      @sha1.reset
-      while buf = input.read(@bs)
-        @sha1.update(buf)
-      end
-      resp[:sha1] = @sha1.hexdigest
-
-      # rewind and read again
-      input.rewind
-      @sha1.reset
-      while buf = input.read(@bs)
-        @sha1.update(buf)
-      end
-
-      if resp[:sha1] == @sha1.hexdigest
-        resp[:sysread_read_byte_match] = true
-      end
-
-      if expect_size = env['HTTP_X_EXPECT_SIZE']
-        if expect_size.to_i == input.size
-          resp[:expect_size_match] = true
-        end
-      end
-      resp[:size] = input.size
-      resp[:content_md5] = env['HTTP_CONTENT_MD5']
-
-      [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
-    end
-  end
-
-  def teardown
-    redirect_test_io { @server.stop(false) } if @server
-    @random.close
-    reset_sig_handlers
-  end
-
-  def test_put
-    start_server(@sha1_app)
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-    @count.times do |i|
-      buf = @random.sysread(@bs)
-      @sha1.update(buf)
-      sock.syswrite(buf)
-    end
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal length, resp[:size]
-    assert_equal @sha1.hexdigest, resp[:sha1]
-  end
-
-  def test_put_content_md5
-    md5 = Digest::MD5.new
-    start_server(@sha1_app)
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
-                  "Trailer: Content-MD5\r\n\r\n")
-    @count.times do |i|
-      buf = @random.sysread(@bs)
-      @sha1.update(buf)
-      md5.update(buf)
-      sock.syswrite("#{'%x' % buf.size}\r\n")
-      sock.syswrite(buf << "\r\n")
-    end
-    sock.syswrite("0\r\n")
-
-    content_md5 = [ md5.digest! ].pack('m').strip.freeze
-    sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n")
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal length, resp[:size]
-    assert_equal @sha1.hexdigest, resp[:sha1]
-    assert_equal content_md5, resp[:content_md5]
-  end
-
-  def test_put_trickle_small
-    @count, @bs = 2, 128
-    start_server(@sha1_app)
-    assert_equal 256, length
-    sock = TCPSocket.new(@addr, @port)
-    hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n"
-    @count.times do
-      buf = @random.sysread(@bs)
-      @sha1.update(buf)
-      hdr << buf
-      sock.syswrite(hdr)
-      hdr = ''
-      sleep 0.6
-    end
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal length, resp[:size]
-    assert_equal @sha1.hexdigest, resp[:sha1]
-  end
-
-  def test_put_keepalive_truncates_small_overwrite
-    start_server(@sha1_app)
-    sock = TCPSocket.new(@addr, @port)
-    to_upload = length + 1
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n")
-    @count.times do
-      buf = @random.sysread(@bs)
-      @sha1.update(buf)
-      sock.syswrite(buf)
-    end
-    sock.syswrite('12345') # write 4 bytes more than we expected
-    @sha1.update('1')
-
-    buf = sock.readpartial(4096)
-    while buf !~ /\r\n\r\n/
-      buf << sock.readpartial(4096)
-    end
-    read = buf.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal to_upload, resp[:size]
-    assert_equal @sha1.hexdigest, resp[:sha1]
-  end
-
-  def test_put_excessive_overwrite_closed
-    tmp = Tempfile.new('overwrite_check')
-    tmp.sync = true
-    start_server(lambda { |env|
-      nr = 0
-      while buf = env['rack.input'].read(65536)
-        nr += buf.size
-      end
-      tmp.write(nr.to_s)
-      [ 200, @hdr, [] ]
-    })
-    sock = TCPSocket.new(@addr, @port)
-    buf = ' ' * @bs
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-
-    @count.times { sock.syswrite(buf) }
-    assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
-      ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
-    end
-    sock.gets
-    tmp.rewind
-    assert_equal length, tmp.read.to_i
-  end
-
-  # Despite reading numerous articles and inspecting the 1.9.1-p0 C
-  # source, Eric Wong will never trust that we're always handling
-  # encoding-aware IO objects correctly.  Thus this test uses shell
-  # utilities that should always operate on files/sockets on a
-  # byte-level.
-  def test_uncomfortable_with_onenine_encodings
-    # POSIX doesn't require all of these to be present on a system
-    which('curl') or return
-    which('sha1sum') or return
-    which('dd') or return
-
-    start_server(@sha1_app)
-
-    tmp = Tempfile.new('dd_dest')
-    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
-                        "bs=#{@bs}", "count=#{@count}"),
-           "dd #@random to #{tmp}")
-    sha1_re = %r!\b([a-f0-9]{40})\b!
-    sha1_out = `sha1sum #{tmp.path}`
-    assert $?.success?, 'sha1sum ran OK'
-
-    assert_match(sha1_re, sha1_out)
-    sha1 = sha1_re.match(sha1_out)[1]
-    resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
-    assert $?.success?, 'curl ran OK'
-    assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/sysread_read_byte_match/, resp)
-
-    # small StringIO path
-    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
-                        "bs=1024", "count=1"),
-           "dd #@random to #{tmp}")
-    sha1_re = %r!\b([a-f0-9]{40})\b!
-    sha1_out = `sha1sum #{tmp.path}`
-    assert $?.success?, 'sha1sum ran OK'
-
-    assert_match(sha1_re, sha1_out)
-    sha1 = sha1_re.match(sha1_out)[1]
-    resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
-    assert $?.success?, 'curl ran OK'
-    assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/sysread_read_byte_match/, resp)
-  end
-
-  def test_chunked_upload_via_curl
-    # POSIX doesn't require all of these to be present on a system
-    which('curl') or return
-    which('sha1sum') or return
-    which('dd') or return
-
-    start_server(@sha1_app)
-
-    tmp = Tempfile.new('dd_dest')
-    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
-                        "bs=#{@bs}", "count=#{@count}"),
-           "dd #@random to #{tmp}")
-    sha1_re = %r!\b([a-f0-9]{40})\b!
-    sha1_out = `sha1sum #{tmp.path}`
-    assert $?.success?, 'sha1sum ran OK'
-
-    assert_match(sha1_re, sha1_out)
-    sha1 = sha1_re.match(sha1_out)[1]
-    cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
-           -isSf --no-buffer -T- " \
-          "http://#@addr:#@port/"
-    resp = Tempfile.new('resp')
-    resp.sync = true
-
-    rd, wr = IO.pipe.each do |io|
-      io.sync = io.close_on_exec = true
-    end
-    pid = spawn(*cmd, { 0 => rd, 1 => resp })
-    rd.close
-
-    tmp.rewind
-    @count.times { |i|
-      wr.write(tmp.read(@bs))
-      sleep(rand / 10) if 0 == i % 8
-    }
-    wr.close
-    pid, status = Process.waitpid2(pid)
-
-    resp.rewind
-    resp = resp.read
-    assert status.success?, 'curl ran OK'
-    assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/sysread_read_byte_match/, resp)
-    assert_match(/expect_size_match/, resp)
-  end
-
-  def test_curl_chunked_small
-    # POSIX doesn't require all of these to be present on a system
-    which('curl') or return
-    which('sha1sum') or return
-    which('dd') or return
-
-    start_server(@sha1_app)
-
-    tmp = Tempfile.new('dd_dest')
-    # small StringIO path
-    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
-                        "bs=1024", "count=1"),
-           "dd #@random to #{tmp}")
-    sha1_re = %r!\b([a-f0-9]{40})\b!
-    sha1_out = `sha1sum #{tmp.path}`
-    assert $?.success?, 'sha1sum ran OK'
-
-    assert_match(sha1_re, sha1_out)
-    sha1 = sha1_re.match(sha1_out)[1]
-    resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
-            -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
-    assert $?.success?, 'curl ran OK'
-    assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/sysread_read_byte_match/, resp)
-    assert_match(/expect_size_match/, resp)
-  end
-
-  private
-
-  def length
-    @bs * @count
-  end
-
-  def start_server(app)
-    redirect_test_io do
-      @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )
-      @server.start
-    end
-  end
-
-end
diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb
index 4a820ea..ce53b86 100644
--- a/test/unit/test_util.rb
+++ b/test/unit/test_util.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 
 require './test/test_helper'
 require 'tempfile'
@@ -51,7 +52,7 @@ class TestUtil < Test::Unit::TestCase
   def test_reopen_logs_renamed_with_encoding
     tmp = Tempfile.new('')
     tmp_path = tmp.path.dup.freeze
-    Encoding.list.each { |encoding|
+    Encoding.list.sample(5).each { |encoding|
       File.open(tmp_path, "a:#{encoding.to_s}") { |fp|
         fp.sync = true
         assert_equal encoding, fp.external_encoding
@@ -74,8 +75,9 @@ class TestUtil < Test::Unit::TestCase
   def test_reopen_logs_renamed_with_internal_encoding
     tmp = Tempfile.new('')
     tmp_path = tmp.path.dup.freeze
-    Encoding.list.each { |ext|
-      Encoding.list.each { |int|
+    full = Encoding.list
+    full.sample(2).each { |ext|
+      full.sample(2).each { |int|
         next if ext == int
         File.open(tmp_path, "a:#{ext.to_s}:#{int.to_s}") { |fp|
           fp.sync = true
diff --git a/test/unit/test_waiter.rb b/test/unit/test_waiter.rb
new file mode 100644
index 0000000..a20994b
--- /dev/null
+++ b/test/unit/test_waiter.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: false
+require 'test/unit'
+require 'unicorn'
+require 'unicorn/select_waiter'
+class TestSelectWaiter < Test::Unit::TestCase
+
+  def test_select_timeout # n.b. this is level-triggered
+    sw = Unicorn::SelectWaiter.new
+    IO.pipe do |r,w|
+      sw.get_readers(ready = [], [r], 0)
+      assert_equal [], ready
+      w.syswrite '.'
+      sw.get_readers(ready, [r], 1000)
+      assert_equal [r], ready
+      sw.get_readers(ready, [r], 0)
+      assert_equal [r], ready
+    end
+  end
+
+  def test_linux # ugh, also level-triggered, unlikely to change
+    IO.pipe do |r,w|
+      wtr = Unicorn::Waiter.prep_readers([r])
+      wtr.get_readers(ready = [], [r], 0)
+      assert_equal [], ready
+      w.syswrite '.'
+      wtr.get_readers(ready = [], [r], 1000)
+      assert_equal [r], ready
+      wtr.get_readers(ready = [], [r], 1000)
+      assert_equal [r], ready, 'still ready (level-triggered :<)'
+      assert_nil wtr.close
+    end
+  rescue SystemCallError => e
+    warn "#{e.message} (#{e.class})"
+  end if Unicorn.const_defined?(:Waiter)
+end
diff --git a/unicorn.gemspec b/unicorn.gemspec
index e6af44d..36700a8 100644
--- a/unicorn.gemspec
+++ b/unicorn.gemspec
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: false
 manifest = File.exist?('.manifest') ?
   IO.readlines('.manifest').map!(&:chomp!) : `git ls-files`.split("\n")
 
@@ -11,7 +12,7 @@ end.compact
 
 Gem::Specification.new do |s|
   s.name = %q{unicorn}
-  s.version = (ENV['VERSION'] || '5.6.0').dup
+  s.version = (ENV['VERSION'] || '6.1.0').dup
   s.authors = ['unicorn hackers']
   s.summary = 'Rack HTTP server for fast clients and Unix'
   s.description = File.read('README').split("\n\n")[1]
@@ -25,18 +26,17 @@ Gem::Specification.new do |s|
   s.homepage = 'https://yhbt.net/unicorn/'
   s.test_files = test_files
 
-  # 1.9.3 is the minumum supported version. We don't specify
+  # 2.5.0 is the minimum supported version. We don't specify
   # a maximum version to make it easier to test pre-releases,
   # but we do warn users if they install unicorn on an untested
   # version in extconf.rb
-  s.required_ruby_version = ">= 1.9.3"
+  s.required_ruby_version = ">= 2.5.0"
 
   # We do not have a hard dependency on rack, it's possible to load
   # things which respond to #call.  HTTP status lines in responses
   # won't have descriptive text, only the numeric status.
   s.add_development_dependency(%q<rack>)
 
-  s.add_dependency(%q<kgio>, '~> 2.6')
   s.add_dependency(%q<raindrops>, '~> 0.7')
 
   s.add_development_dependency('test-unit', '~> 3.0')