about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--COPYING674
-rwxr-xr-xGIT-VERSION-GEN41
-rw-r--r--GNUmakefile90
-rw-r--r--README127
-rw-r--r--Rakefile60
-rwxr-xr-xbin/yahns32
-rw-r--r--examples/README3
-rw-r--r--examples/init.sh76
-rw-r--r--examples/logger_mp_safe.rb28
-rw-r--r--examples/logrotate.conf32
-rw-r--r--examples/yahns_multi.conf.rb89
-rw-r--r--examples/yahns_rack_basic.conf.rb27
-rw-r--r--lib/yahns.rb66
-rw-r--r--lib/yahns/acceptor.rb28
-rw-r--r--lib/yahns/client_expire.rb40
-rw-r--r--lib/yahns/client_expire_portable.rb39
-rw-r--r--lib/yahns/config.rb341
-rw-r--r--lib/yahns/daemon.rb51
-rw-r--r--lib/yahns/fdmap.rb90
-rw-r--r--lib/yahns/http_client.rb196
-rw-r--r--lib/yahns/http_context.rb66
-rw-r--r--lib/yahns/http_response.rb183
-rw-r--r--lib/yahns/log.rb73
-rw-r--r--lib/yahns/queue.rb7
-rw-r--r--lib/yahns/queue_egg.rb23
-rw-r--r--lib/yahns/queue_epoll.rb57
-rw-r--r--lib/yahns/rack.rb80
-rw-r--r--lib/yahns/server.rb328
-rw-r--r--lib/yahns/server_mp.rb184
-rw-r--r--lib/yahns/sigevent.rb7
-rw-r--r--lib/yahns/sigevent_efd.rb18
-rw-r--r--lib/yahns/sigevent_pipe.rb29
-rw-r--r--lib/yahns/socket_helper.rb117
-rw-r--r--lib/yahns/stream_file.rb34
-rw-r--r--lib/yahns/stream_input.rb150
-rw-r--r--lib/yahns/tee_input.rb114
-rw-r--r--lib/yahns/tiny_input.rb7
-rw-r--r--lib/yahns/tmpio.rb27
-rw-r--r--lib/yahns/wbuf.rb36
-rw-r--r--lib/yahns/wbuf_common.rb32
-rw-r--r--lib/yahns/worker.rb58
-rw-r--r--test/covshow.rb29
-rw-r--r--test/helper.rb115
-rw-r--r--test/server_helper.rb64
-rw-r--r--test/test_bin.rb98
-rw-r--r--test/test_config.rb56
-rw-r--r--test/test_output_buffering.rb288
-rw-r--r--test/test_queue.rb59
-rw-r--r--test/test_rack.rb26
-rw-r--r--test/test_serve_static.rb42
-rw-r--r--test/test_server.rb382
-rw-r--r--test/test_stream_file.rb30
-rw-r--r--test/test_wbuf.rb136
-rw-r--r--yahns.gemspec19
55 files changed, 5119 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..da7576a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+/GIT-VERSION-FILE
+/NEWS
+/pkg
+/lib/yahns/version.rb
+/coverage.dump
+*.tar.gz
+*.log
+/man
+*.gem
+*.gz
+.manifest
+.gem-manifest
+.tgz-manifest
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
new file mode 100755
index 0000000..458b1c2
--- /dev/null
+++ b/GIT-VERSION-GEN
@@ -0,0 +1,41 @@
+#!/usr/bin/env ruby
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+CONSTANT = "Yahns::VERSION"
+RVF = "lib/yahns/version.rb"
+GVF = "GIT-VERSION-FILE"
+DEF_VER = "v0.0.0"
+vn = DEF_VER
+
+# First see if there is a version file (included in release tarballs),
+# then try git-describe, then default.
+if File.exist?(".git")
+  describe = `git describe --abbrev=4 HEAD 2>/dev/null`.strip
+  case describe
+  when /\Av[0-9]*/
+    vn = describe
+    system(*%w(git update-index -q --refresh))
+    unless `git diff-index --name-only HEAD --`.chomp.empty?
+      vn << "-dirty"
+    end
+    vn.tr!('-', '.')
+  end
+end
+
+vn = vn.sub!(/\Av/, "")
+new_ruby_version = "#{CONSTANT} = '#{vn}' # :nodoc:\n"
+cur_ruby_version = File.read(RVF) rescue nil
+if new_ruby_version != cur_ruby_version
+  File.open(RVF, "w") { |fp| fp.write(new_ruby_version) }
+end
+File.chmod(0644, RVF)
+
+# generate the makefile snippet
+new_make_version = "VERSION = #{vn}\n"
+cur_make_version = File.read(GVF) rescue nil
+if new_make_version != cur_make_version
+  File.open(GVF, "w") { |fp| fp.write(new_make_version) }
+end
+File.chmod(0644, GVF)
+
+puts vn if $0 == __FILE__
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000..f0f8cdf
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,90 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+all::
+pkg = yahns
+RUBY = ruby
+GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
+        @./GIT-VERSION-GEN
+-include GIT-VERSION-FILE
+lib := lib
+
+all:: test
+test_units := $(wildcard test/test_*.rb)
+test: $(test_units)
+$(test_units):
+        $(RUBY) -w -I $(lib) $@ -v
+
+test-mt: export N = $(shell nproc 2>/dev/null || echo 4)
+test-mt:
+        $(RUBY) -w -I $(lib) $(addprefix -r./,$(test_units)) -eexit --
+
+check-warnings:
+        @(for i in $$(git ls-files '*.rb'| grep -v '^setup\.rb$$'); \
+          do $(RUBY) -d -W2 -c $$i; done) | grep -v '^Syntax OK$$' || :
+
+check: test
+coverage: export COVERAGE=1
+coverage:
+        > coverage.dump
+        $(MAKE) check
+        $(RUBY) ./test/covshow.rb
+
+coverage-mt: export COVERAGE=1
+coverage-mt:
+        > coverage.dump
+        $(MAKE) test-mt
+        $(RUBY) ./test/covshow.rb
+
+pkggem := pkg/$(pkg)-$(VERSION).gem
+pkgtgz := pkg/$(pkg)-$(VERSION).tar.gz
+
+fix-perms:
+        git ls-tree -r HEAD | awk '/^100644 / {print $$NF}' | xargs chmod 644
+        git ls-tree -r HEAD | awk '/^100755 / {print $$NF}' | xargs chmod 755
+
+gem: $(pkggem)
+
+install-gem: $(pkggem)
+        gem install $(CURDIR)/$<
+
+$(pkggem): .gem-manifest
+        VERSION=$(VERSION) gem build $(pkg).gemspec
+        mkdir -p pkg
+        mv $(@F) $@
+
+pkg_extra := GIT-VERSION-FILE lib/yahns/version.rb NEWS
+NEWS:
+        rake -s $@
+
+gem-man:
+        $(MAKE) -C Documentation/ gem-man
+tgz-man:
+        $(MAKE) -C Documentation/ install-man mandir=$(CURDIR)/man
+.PHONY: tgz-man gem-man
+
+.gem-manifest: .manifest
+        # (ls man/*.?; cat .manifest) | LC_ALL=C sort > $@+
+        LC_ALL=C sort < .manifest > $@+
+        cmp $@+ $@ || mv $@+ $@; rm -f $@+
+.tgz-manifest: .manifest
+        LC_ALL=C sort < .manifest > $@+
+        cmp $@+ $@ || mv $@+ $@; rm -f $@+
+.manifest: NEWS fix-perms
+        rm -rf man
+        (git ls-files; \
+         for i in $(pkg_extra); do echo $$i; done) | \
+         LC_ALL=C sort > $@+
+        cmp $@+ $@ || mv $@+ $@; rm -f $@+
+$(pkgtgz): distdir = pkg/$(pkg)-$(VERSION)
+$(pkgtgz): .tgz-manifest
+        @test -n "$(distdir)"
+        $(RM) -r $(distdir)
+        mkdir -p $(distdir)
+        tar cf - $$(cat .tgz-manifest) | (cd $(distdir) && tar xf -)
+        cd pkg && tar cf - $(pkg)-$(VERSION) | gzip -9 > $(@F)+
+        mv $@+ $@
+
+package: $(pkgtgz) $(pkggem)
+
+.PHONY: all .FORCE-GIT-VERSION-FILE test $(test_units) NEWS
+.PHONY: check-warnings fix-perms
diff --git a/README b/README
new file mode 100644
index 0000000..82fad54
--- /dev/null
+++ b/README
@@ -0,0 +1,127 @@
+yahns - sleepy, multi-threaded, non-blocking application server for Ruby
+------------------------------------------------------------------------
+
+A Free Software, multi-threaded, non-blocking network application server
+designed for low _idle_ power consumption.  It is primarily optimized
+for applications with occasional users which see little or no traffic.
+yahns currently hosts Rack/HTTP applications, but may eventually support
+other application types.  Unlike some existing servers, yahns is
+extremely sensitive to fatal bugs in the applications it hosts.
+
+Features
+--------
+
+* _zero_ wakeups when all clients are idle
+* idle client connections may live forever if there is no FD pressure
+* suitable for slow clients, fast clients, or a mixture of both
+* HTTP/0.9 support
+* HTTP/1.1 persistent connections and pipelining
+* decodes HTTP chunked encoding for requests
+* parses HTTP/1.1 trailers in requests
+* supports streaming responses with lazy buffering for slow clients
+* optional streaming input for fast clients
+* able to host multiple applications with different settings
+* uses epoll to scale to many idle connections
+* abuses epoll as a load balancer between threads inside a process
+* optional multi-process support (in addition to threads)
+* fairly balances new clients between multiple processes (on Linux)
+
+Supported Platforms
+-------------------
+
+yahns is developed primarily for modern GNU/Linux systems.
+
+We may support kqueue for FreeBSD/OpenBSD/NetBSD if there is significant
+interest.  Non-Free systems/dependencies will never be supported
+
+Supported Ruby implementations:
+* (Matz) Ruby 2.0 or later
+* Rubinius 2.0 or later (planned)
+
+Contact
+-------
+
+We are happy to see feedback of all types via plain-text email.
+Please send comments, user/dev discussion, patches, bug reports,
+and pull requests to Eric Wong at: normalperson@yhbt.net
+
+Public mailing list coming soon.
+
+This README is our homepage, we would rather be working on HTTP servers
+all day than worrying about the next browser vulnerability because
+HTML/CSS/JS is too complicated for us.
+
+* http://yahns.yhbt.net/README
+
+Hacking
+-------
+
+We use git and follow the same development model as git itself
+(mailing list-oriented, benevolent dictator).
+
+  git clone git://yahns.yhbt.net/yahns
+
+Please use git-format-patch(1) and git-send-email(1) distributed with
+the git(7) suite for generating and sending patches.  Please format
+pull requests with the git-request-pull(1) script (also distributed
+with git(7)) and send them via email.
+
+See http://www.git-scm.com/ for more information on git.
+
+Design
+------
+
+yahns is designed to optimimally use multiple threads with non-blocking I/O.
+The event loop is not a traditional single-threaded design with a mutex
+slapped on as an afterthought, but designed from the beginning to utilize
+multiple threads.
+
+* two classes of long-lived, persistent threads
+  1. blocking acceptors
+  2. non-blocking event loop workers
+* epoll acts as a queue (by using one-shot notifications)
+* acceptors accept new clients and put them in the epoll "queue"
+* workers pull clients off the queue, rearming them to epoll on EAGAIN
+
+The end result is clients transition freely and fairly between threads
+and will always be able to find the next idle thread to run on.
+
+This design works with kqueue, too, and we will support kqueue if there
+is interest.  In fact, got our design inspiration from the name "kqueue"
+when working on another project.  We may also support libkqueue:
+
+    http://sourceforge.net/projects/libkqueue/
+
+In addition to multiple threads, yahns optionally supports multiple
+processes to work around low FD limits as well as contention in the:
+
+* kernel (socket (de)allocation from accept/close)
+* standard C library (malloc/free)
+* Ruby VM (GVL, GC)
+* application it hosts
+
+Copyright
+---------
+
+Copyright 2013, Eric Wong <normalperson@yhbt.net> and all contributors.
+License: GPLv3 or later <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+yahns is copyrighted Free Software by all contributors, see logs in
+revision control for names and email addresses of all of them.  yahns
+contains code from Mongrel, unicorn, and Rainbows! which may also be
+licensed under the GPLv2 or later.
+
+yahns is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; either version 3 of the License, or (at your
+option) any later version.
+
+yahns is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, see https://www.gnu.org/licenses/gpl-3.0.txt
+
+lrg nabgure ubeevoyl-anzrq freire :>
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..5d36835
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,60 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'tempfile'
+include Rake::DSL
+task "NEWS" do
+  latest = nil
+  fp = Tempfile.new("NEWS", ".")
+  fp.sync = true
+  `git tag -l`.split(/\n/).reverse.each do |tag|
+    %r{\Av(.+)} =~ tag or next
+    version = $1
+    header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3)
+    header = header.split(/\n/)
+    tagger = header.grep(/\Atagger /)[0]
+    time = Time.at(tagger.split(/ /)[-2].to_i).utc
+    latest ||= time
+    date = time.strftime("%Y-%m-%d")
+    fp.puts "# #{version} / #{date}\n\n#{subject}"
+    if body && body.strip.size > 0
+      fp.puts "\n\n#{body}"
+    end
+    fp.puts
+  end
+  fp.write("Unreleased\n\n") unless fp.size > 0
+  fp.puts "# COPYRIGHT"
+  bdfl = 'Eric Wong <normalperson@yhbt.net>'
+  fp.puts "Copyright (C) 2013, #{bdfl} and all contributors"
+  fp.puts "License: GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.txt)"
+  fp.rewind
+  assert_equal fp.read, File.read("NEWS") rescue nil
+  fp.chmod 0644
+  File.rename(fp.path, "NEWS")
+  fp.close!
+end
+
+task rsync_docs: "NEWS" do
+  dest = ENV["RSYNC_DEST"] || "yahns.yhbt.net:/srv/yahns/"
+  top = %w(NEWS README COPYING)
+  files = []
+
+  # git-set-file-times is distributed with rsync,
+  # Also available at: http://yhbt.net/git-set-file-times
+  # on Debian systems: /usr/share/doc/rsync/scripts/git-set-file-times.gz
+  sh("git", "set-file-times", "examples", *top)
+
+  `git ls-files Documentation/*.txt`.split(/\n/).concat(top).each do |txt|
+    gz = "#{txt}.gz"
+    tmp = "#{gz}.#$$"
+    sh("gzip -9 < #{txt} > #{tmp}")
+    st = File.stat(txt)
+    File.utime(st.atime, st.mtime, tmp) # make nginx gzip_static happy
+    File.rename(tmp, gz)
+    files << txt
+    files << gz
+  end
+  sh("rsync --chmod=Fugo=r -av #{files.join(' ')} #{dest}")
+
+  examples = `git ls-files examples`.split("\n")
+  sh("rsync --chmod=Fugo=r -av #{examples.join(' ')} #{dest}/examples/")
+end
diff --git a/bin/yahns b/bin/yahns
new file mode 100755
index 0000000..5c4d34e
--- /dev/null
+++ b/bin/yahns
@@ -0,0 +1,32 @@
+#!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+$stdout.sync = $stderr.sync = true
+require 'yahns'
+require 'optparse'
+config_file = daemonize = nil
+OptionParser.new("", 24, "  ") do |opts|
+  cmd = File.basename($0)
+  opts.banner = "Usage: #{cmd} [options]"
+  opts.separator "#{cmd} options:"
+  opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
+    daemonize = !!d
+  end
+  opts.on("-c", "--config-file FILE", "yahns config file") do |f|
+    config_file = f
+  end
+  opts.separator "Common options:"
+  opts.on_tail("-h", "--help", "Show this message") do
+    puts opts.to_s
+    exit
+  end
+  opts.on_tail("-v", "--version", "Show version") do
+    puts "#{cmd} v#{Yahns::VERSION}"
+    exit
+  end
+  opts.parse!(ARGV)
+end
+server = Yahns::Server.new(Yahns::Config.new(config_file))
+Yahns::Daemon.daemon(server) if daemonize
+server.start.join
diff --git a/examples/README b/examples/README
new file mode 100644
index 0000000..c87947a
--- /dev/null
+++ b/examples/README
@@ -0,0 +1,3 @@
+All files in this example directory (including this one) are CC0:
+To the extent possible under law, Eric Wong has waived all copyright and
+related or neighboring rights to these examples.
diff --git a/examples/init.sh b/examples/init.sh
new file mode 100644
index 0000000..9464220
--- /dev/null
+++ b/examples/init.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+# To the extent possible under law, Eric Wong has waived all copyright and
+# related or neighboring rights to this examples
+set -e
+# Example init script, this can be used with nginx, too,
+# since nginx and yahns accept the same signals
+
+# Feel free to change any of the following variables for your app:
+TIMEOUT=${TIMEOUT-60}
+APP_ROOT=/home/x/my_app/current
+PID=$APP_ROOT/tmp/pids/yahns.pid
+CMD="/usr/bin/yahns -D -c $APP_ROOT/config/yahns.rb"
+INIT_CONF=$APP_ROOT/config/init.conf
+action="$1"
+set -u
+
+test -f "$INIT_CONF" && . $INIT_CONF
+
+old_pid="$PID.oldbin"
+
+cd $APP_ROOT || exit 1
+
+sig () {
+        test -s "$PID" && kill -$1 `cat $PID`
+}
+
+oldsig () {
+        test -s $old_pid && kill -$1 `cat $old_pid`
+}
+
+case $action in
+start)
+        sig 0 && echo >&2 "Already running" && exit 0
+        $CMD
+        ;;
+stop)
+        sig QUIT && exit 0
+        echo >&2 "Not running"
+        ;;
+force-stop)
+        sig TERM && exit 0
+        echo >&2 "Not running"
+        ;;
+restart|reload)
+        sig HUP && echo reloaded OK && exit 0
+        echo >&2 "Couldn't reload, starting '$CMD' instead"
+        $CMD
+        ;;
+upgrade)
+        if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
+        then
+                n=$TIMEOUT
+                while test -s $old_pid && test $n -ge 0
+                do
+                        printf '.' && sleep 1 && n=$(( $n - 1 ))
+                done
+                echo
+
+                if test $n -lt 0 && test -s $old_pid
+                then
+                        echo >&2 "$old_pid still exists after $TIMEOUT seconds"
+                        exit 1
+                fi
+                exit 0
+        fi
+        echo >&2 "Couldn't upgrade, starting '$CMD' instead"
+        $CMD
+        ;;
+reopen-logs)
+        sig USR1
+        ;;
+*)
+        echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
+        exit 1
+        ;;
+esac
diff --git a/examples/logger_mp_safe.rb b/examples/logger_mp_safe.rb
new file mode 100644
index 0000000..569d661
--- /dev/null
+++ b/examples/logger_mp_safe.rb
@@ -0,0 +1,28 @@
+# To the extent possible under law, Eric Wong has waived all copyright and
+# related or neighboring rights to this examples
+#
+# Multi-Processing-safe monkey patch for Logger
+#
+# This monkey patch fixes the case where "preload: true" is used and
+# the application spawns a background thread upon being loaded.
+#
+# This removes all lock from the Logger code and solely relies on the
+# underlying filesystem to handle write(2) system calls atomically when
+# O_APPEND is used.  This is safe in the presence of both multiple
+# threads (native or green) and multiple processes when writing to
+# a filesystem with POSIX O_APPEND semantics.
+#
+# It should be noted that the original locking on Logger could _never_ be
+# considered reliable on non-POSIX filesystems with multiple processes,
+# either, so nothing is lost in that case.
+
+require 'logger'
+class Logger::LogDevice
+  def write(message)
+    @dev.syswrite(message)
+  end
+
+  def close
+    @dev.close
+  end
+end
diff --git a/examples/logrotate.conf b/examples/logrotate.conf
new file mode 100644
index 0000000..ebc92a5
--- /dev/null
+++ b/examples/logrotate.conf
@@ -0,0 +1,32 @@
+# To the extent possible under law, Eric Wong has waived all copyright and
+# related or neighboring rights to this examples
+#
+# example logrotate config file, I usually keep this in
+# /etc/logrotate.d/yahns_app on my Debian systems
+#
+# See the logrotate(8) manpage for more information:
+#    http://linux.die.net/man/8/logrotate
+
+# Modify the following glob to match the logfiles your app writes to:
+/var/log/yahns_app/*.log {
+        # this first block is mostly just personal preference, though
+        # I wish logrotate offered an "hourly" option...
+        daily
+        missingok
+        rotate 180
+        compress # must use with delaycompress below
+        dateext
+
+        # this is important if using "compress" since we need to call
+        # the "lastaction" script below before compressing:
+        delaycompress
+
+        # note the lack of the evil "copytruncate" option in this
+        # config.  yahns supports the USR1 signal and we send it
+        # as our "lastaction" action:
+        lastaction
+                # assuming your pid file is in /var/run/yahns_app/pid
+                pid=/var/run/yahns_app/pid
+                test -s $pid && kill -USR1 "$(cat $pid)"
+        endscript
+}
diff --git a/examples/yahns_multi.conf.rb b/examples/yahns_multi.conf.rb
new file mode 100644
index 0000000..cdcb445
--- /dev/null
+++ b/examples/yahns_multi.conf.rb
@@ -0,0 +1,89 @@
+# To the extent possible under law, Eric Wong has waived all copyright and
+# related or neighboring rights to this example.
+
+# By default, this based on the soft limit of RLIMIT_NOFILE
+#   count = Process.getrlimit(:NOFILE)[0]) * 0.5
+# yahns will start expiring idle clients once we hit it
+client_expire_threshold 0.5
+
+# each queue definition configures a thread pool and epoll_wait usage
+# The default queue is always present
+queue(:default) do
+  worker_threads 7 # this is the default value
+  max_events 1 # 1: fairest, best in all multi-threaded cases
+end
+
+# This is an example of a small queue with fewer workers and unfair scheduling.
+# It is rarely necessary or even advisable to configure multiple queues.
+queue(:small) do
+  worker_threads 2
+
+  # increase max_events only under one of the following circumstances:
+  # 1) worker_threads is 1
+  # 2) epoll_wait lock contention inside the kernel is the biggest bottleneck
+  #    (this is unlikely outside of "hello world" apps)
+  max_events 64
+end
+
+# This is an example of a Rack application configured in yahns
+# All values below are defaults
+app(:rack, "/path/to/config.ru", preload: false) do
+  listen 8080, backlog: 1024, tcp_nodelay: false
+  client_max_body_size 1024*1024
+  check_client_connection false
+  logger Logger.new($stderr)
+  client_timeout 15
+  input_buffering true
+  output_buffering true # output buffering is always lazy if enabled
+  persistent_connections true
+  errors $stderr
+  queue :default
+end
+
+# same as first, just listen on different port and small queue
+app(:rack, "/path/to/config.ru") do
+  listen "10.0.0.1:10000"
+  client_max_body_size 1024*1024*10
+  check_client_connection true
+  logger Logger.new("/path/to/another/log")
+  client_timeout 30
+  persistent_connections true
+  errors "/path/to/errors.log"
+  queue :small
+end
+
+# totally different app
+app(:rack, "/path/to/another.ru", preload: true) do
+  listen 8081, sndbuf: 1024 * 1024
+  listen "/path/to/unix.sock"
+  client_max_body_size 1024*1024*1024
+  input_buffering :lazy
+  output_buffering false
+  client_timeout 4
+  persistent_connections false
+
+  # different apps may share the same queue, but listen on different ports.
+  queue :default
+end
+
+# yet another totally different app, this app is not-thread safe but fast
+# enough for multi-process to not matter.
+# Use unicorn if you need multi-process performance on single-threaded apps
+app(:rack, "/path/to/not_thread_safe.ru") do
+  # listeners _always_ get a private thread in yahns
+  listen "/path/to/yet_another.sock"
+  listen 8082
+
+  # inline private queue definition here
+  queue do
+    worker_threads 1 # single-threaded queue
+    max_events 64 # very high max_events is perfectly fair for single thread
+  end
+
+  # single (or few)-threaded apps must use full buffering if serving
+  # untrusted/slow clients.
+  input_buffering true
+  output_buffering true
+end
+# Note: this file is used by test_config.rb, be sure to update that
+# if we update this
diff --git a/examples/yahns_rack_basic.conf.rb b/examples/yahns_rack_basic.conf.rb
new file mode 100644
index 0000000..ea367cd
--- /dev/null
+++ b/examples/yahns_rack_basic.conf.rb
@@ -0,0 +1,27 @@
+# To the extent possible under law, Eric Wong has waived all copyright and
+# related or neighboring rights to this examples
+# A typical Rack example for hosting a single Rack application with yahns
+# and only frequently-useful config values
+
+worker_processes 1
+# working_directory "/path/to/my_app"
+stdout_path "/path/to/my_logs/out.log"
+stderr_path "/path/to/my_logs/err.log"
+pid "/path/to/my_pids/yahns.pid"
+client_expire_threshold 0.5
+
+queue do
+  worker_threads 50
+end
+
+app(:rack, "config.ru", preload: false) do
+  listen 8080
+  client_max_body_size 1024 * 1024
+  input_buffering true
+  output_buffering true # this lazy by default
+  client_timeout 5
+  persistent_connections true
+end
+
+# Note: this file is used by test_config.rb, be sure to update that
+# if we update this
diff --git a/lib/yahns.rb b/lib/yahns.rb
new file mode 100644
index 0000000..373fca0
--- /dev/null
+++ b/lib/yahns.rb
@@ -0,0 +1,66 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'unicorn' # pulls in raindrops, kgio, fcntl, etc, stringio, and logger
+require 'sleepy_penguin'
+
+# yahns exposes no user-visible API outside of the config file
+# Internals are subject to change.
+module Yahns # :nodoc:
+  # We populate this at startup so we can figure out how to reexecute
+  # and upgrade the currently running instance of yahns
+  # Unlike unicorn, this Hash is NOT a stable/public interface.
+  #
+  # * 0 - the path to the yahns executable
+  # * :argv - a deep copy of the ARGV array the executable originally saw
+  # * :cwd - the working directory of the application, this is where
+  # you originally started yahns.
+  #
+  # To change your yahns executable to a different path without downtime,
+  # you can set the following in your yahns config file, HUP and then
+  # continue with the traditional USR2 + QUIT upgrade steps:
+  #
+  #   Yahns::START[0] = "/home/bofh/2.0.0/bin/yahns"
+  START = {
+    :argv => ARGV.map { |arg| arg.dup },
+    0 => $0.dup,
+  }
+
+  # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
+  # and like systems
+  START[:cwd] = begin
+    a = File.stat(pwd = ENV['PWD'])
+    b = File.stat(Dir.pwd)
+    a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
+  rescue
+    Dir.pwd
+  end
+
+  # Raised inside TeeInput when a client closes the socket inside the
+  # application dispatch.  This is always raised with an empty backtrace
+  # since there is nothing in the application stack that is responsible
+  # for client shutdowns/disconnects.
+  class ClientShutdown < EOFError # :nodoc:
+  end
+end
+
+# FIXME: require lazily
+require_relative 'yahns/log'
+require_relative 'yahns/queue_epoll'
+require_relative 'yahns/stream_input'
+require_relative 'yahns/tee_input'
+require_relative 'yahns/queue_egg'
+require_relative 'yahns/client_expire'
+require_relative 'yahns/http_response'
+require_relative 'yahns/http_client'
+require_relative 'yahns/http_context'
+require_relative 'yahns/queue'
+require_relative 'yahns/config'
+require_relative 'yahns/tmpio'
+require_relative 'yahns/worker'
+require_relative 'yahns/sigevent'
+require_relative 'yahns/daemon'
+require_relative 'yahns/socket_helper'
+require_relative 'yahns/server'
+require_relative 'yahns/fdmap'
+require_relative 'yahns/acceptor'
+require_relative 'yahns/wbuf'
diff --git a/lib/yahns/acceptor.rb b/lib/yahns/acceptor.rb
new file mode 100644
index 0000000..b5e7b0e
--- /dev/null
+++ b/lib/yahns/acceptor.rb
@@ -0,0 +1,28 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv3 or later (see COPYING for details)
+module Yahns::Acceptor # :nodoc:
+  def spawn_acceptor(logger, client_class, queue)
+    accept_flags = Kgio::SOCK_NONBLOCK | Kgio::SOCK_CLOEXEC
+    Thread.new do
+      Thread.current.abort_on_exception = true
+      qev_flags = client_class.superclass::QEV_FLAGS
+      begin
+        # We want the accept/accept4 syscall to be _blocking_
+        # so it can distribute work evenly between processes
+        if client = kgio_accept(client_class, accept_flags)
+          client.yahns_init
+
+          # it is not safe to touch client in this thread after this,
+          # a worker thread may grab client right away
+          queue.queue_add(client, qev_flags)
+        end
+      rescue Errno::EMFILE, Errno::ENFILE => e
+        logger.error("#{e.message}, consider raising open file limits")
+        queue.fdmap.desperate_expire_for(self, 5)
+        sleep 1 # let other threads do some work
+      rescue => e
+        Yahns::Log.exception(logger, "accept loop error", e) unless closed?
+      end until closed?
+    end
+  end
+end
diff --git a/lib/yahns/client_expire.rb b/lib/yahns/client_expire.rb
new file mode 100644
index 0000000..7da9498
--- /dev/null
+++ b/lib/yahns/client_expire.rb
@@ -0,0 +1,40 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# included in Yahns::HttpClient
+#
+# this provides the ability to expire idle clients once we hit a soft limit
+# on idle clients
+#
+# we absolutely DO NOT issue IO#close in here, only BasicSocket#shutdown
+module Yahns::ClientExpire # :nodoc:
+  def yahns_expire(timeout) # rarely called
+    return 0 if closed? # still racy, but avoid the exception in most cases
+
+    info = Raindrops::TCP_Info.new(self)
+    return 0 if info.tcpi_state != 1 # TCP_ESTABLISHED == 1
+
+    # Linux struct tcp_info timers are in milliseconds
+    timeout *= 1000
+
+    send_timedout = !!(info.tcpi_last_data_sent > timeout)
+
+    # tcpi_last_data_recv is not valid unless tcpi_ato (ACK timeout) is set
+    if 0 == info.tcpi_ato
+      sd = send_timedout && (info.tcpi_last_ack_recv > timeout)
+    else
+      sd = send_timedout && (info.tcpi_last_data_recv > timeout)
+    end
+    if sd
+      shutdown
+      1
+    else
+      0
+    end
+  # we also do not expire UNIX domain sockets
+  # (since those are the most trusted of local clients)
+  # the IO#closed? check is racy
+  rescue
+    0
+  end
+end
diff --git a/lib/yahns/client_expire_portable.rb b/lib/yahns/client_expire_portable.rb
new file mode 100644
index 0000000..2ea7706
--- /dev/null
+++ b/lib/yahns/client_expire_portable.rb
@@ -0,0 +1,39 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+module Yahns::ClientExpire # :nodoc:
+  def __timestamp
+    Time.now.to_f
+  end
+
+  def yahns_expire(timeout)
+    return 0 if closed? # still racy, but avoid the exception in most cases
+    if (__timestamp - @last_io_at) > timeout
+      shutdown
+      1
+    else
+      0
+    end
+  rescue # the IO#closed? check is racy
+    0
+  end
+
+  def kgio_read(*args)
+    @last_io_at = __timestamp
+    super
+  end
+
+  def kgio_write(*args)
+    @last_io_at = __timestamp
+    super
+  end
+
+  def kgio_trywrite(*args)
+    @last_io_at = __timestamp
+    super
+  end
+
+  def kgio_tryread(*args)
+    @last_io_at = __timestamp
+    super
+  end
+end
diff --git a/lib/yahns/config.rb b/lib/yahns/config.rb
new file mode 100644
index 0000000..1d4d110
--- /dev/null
+++ b/lib/yahns/config.rb
@@ -0,0 +1,341 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# Implements a DSL for configuring a yahns server.
+# See http://yahns.yhbt.net/examples/yahns_multi.conf.rb for a full
+# example configuration file.
+class Yahns::Config # :nodoc:
+  APP_CLASS = {} # public, see yahns/rack for usage example
+  CfgBlock = Struct.new(:type, :ctx) # :nodoc:
+  attr_reader :config_file, :config_listeners, :set
+  attr_reader :qeggs, :app_ctx
+
+  def initialize(config_file = nil)
+    @config_file = config_file
+    @block = nil
+    config_reload!
+  end
+
+  def _check_in_block(ctx, var)
+    if ctx == nil
+      return var if @block == nil
+      msg = "#{var} must be called outside of #{@block.type}"
+    else
+      return var if @block && ctx == @block.type
+      msg = @block ? "may not be used inside a #{@block.type} block" :
+                     "must be used with a #{ctx} block"
+    end
+    raise ArgumentError, msg
+  end
+
+  def config_reload! #:nodoc:
+    # app_instance:app_ctx is a 1:N relationship
+    @config_listeners = {} # name/address -> options
+    @app_ctx = []
+    @set = Hash.new(:unset)
+    @qeggs = {}
+    @app_instances = {}
+
+    # set defaults:
+    client_expire_threshold(0.5) # default is half of the open file limit
+
+    instance_eval(File.read(@config_file), @config_file) if @config_file
+
+    # working_directory binds immediately (easier error checking that way),
+    # now ensure any paths we changed are correctly set.
+    [ :pid, :stderr_path, :stdout_path ].each do |var|
+      String === (path = @set[var]) or next
+      path = File.expand_path(path)
+      File.writable?(path) || File.writable?(File.dirname(path)) or \
+            raise ArgumentError, "directory for #{var}=#{path} not writable"
+    end
+  end
+
+  def logger(obj)
+    var = :logger
+    %w(debug info warn error fatal).each do |m|
+      obj.respond_to?(m) and next
+      raise ArgumentError, "#{var}=#{obj} does not respond to method=#{m}"
+    end
+    if @block
+      if @block.ctx.respond_to?(:logger=)
+        @block.ctx.logger = obj
+      else
+        raise ArgumentError, "#{var} not valid inside #{@block.type}"
+      end
+    else
+      @set[var] = obj
+    end
+  end
+
+  def worker_processes(nr)
+    # TODO: allow zero
+    var = _check_in_block(nil, :worker_processes)
+    @set[var] = _check_int(var, nr, 1)
+  end
+
+  # sets the +path+ for the PID file of the yahns master process
+  def pid(path)
+    _set_path(:pid, path)
+  end
+
+  def stderr_path(path)
+    _set_path(:stderr_path, path)
+  end
+
+  def stdout_path(path)
+    _set_path(:stdout_path, path)
+  end
+
+  def value(var)
+    val = @set[var]
+    val == :unset ? nil : val
+  end
+
+  # sets the working directory for yahns.  This ensures SIGUSR2 will
+  # start a new instance of yahns in this directory.  This may be
+  # a symlink, a common scenario for Capistrano users.  Unlike
+  # all other yahns configuration directives, this binds immediately
+  # for error checking and cannot be undone by unsetting it in the
+  # configuration file and reloading.
+  def working_directory(path)
+    var = :working_directory
+    @app_ctx.empty? or
+      raise ArgumentError, "#{var} must be declared before any apps"
+
+    # just let chdir raise errors
+    path = File.expand_path(path)
+    if @config_file &&
+       @config_file[0] != ?/ &&
+       ! File.readable?("#{path}/#@config_file")
+      raise ArgumentError,
+            "config_file=#@config_file would not be accessible in" \
+            " #{var}=#{path}"
+    end
+    Dir.chdir(path)
+    @set[var] = ENV["PWD"] = path
+  end
+
+  # Runs worker processes as the specified +user+ and +group+.
+  # The master process always stays running as the user who started it.
+  # This switch will occur after calling the after_fork hooks, and only
+  # if the Worker#user method is not called in the after_fork hooks
+  # +group+ is optional and will not change if unspecified.
+  def user(user, group = nil)
+    var = :user
+    @block and raise "#{var} is not valid inside #{@block.type}"
+    # raises ArgumentError on invalid user/group
+    Etc.getpwnam(user)
+    Etc.getgrnam(group) if group
+    @set[var] = [ user, group ]
+  end
+
+  def _set_path(var, path) #:nodoc:
+    _check_in_block(nil, var)
+    case path
+    when NilClass, String
+      @set[var] = path
+    else
+      raise ArgumentError
+    end
+  end
+
+  def listen(address, options = {})
+    options = options.dup
+    var = _check_in_block(:app, :listen)
+    address = expand_addr(address)
+    String === address or
+      raise ArgumentError, "address=#{address.inspect} must be a string"
+    [ :umask, :backlog, :sndbuf, :rcvbuf ].each do |key|
+      value = options[key] or next
+      Integer === value or
+        raise ArgumentError, "#{var}: not an integer: #{key}=#{value.inspect}"
+    end
+    [ :ipv6only ].each do |key|
+      (value = options[key]).nil? and next
+      [ true, false ].include?(value) or
+        raise ArgumentError, "#{var}: not boolean: #{key}=#{value.inspect}"
+    end
+
+    options[:yahns_app_ctx] = @block.ctx
+    @config_listeners.include?(address) and
+      raise ArgumentError, "listen #{address} already in use"
+    @config_listeners[address] = options
+  end
+
+  # expands "unix:path/to/foo" to a socket relative to the current path
+  # expands pathnames of sockets if relative to "~" or "~username"
+  # expands "*:port and ":port" to "0.0.0.0:port"
+  def expand_addr(address) #:nodoc:
+    return "0.0.0.0:#{address}" if Integer === address
+    return address unless String === address
+
+    case address
+    when %r{\Aunix:(.*)\z}
+      File.expand_path($1)
+    when %r{\A~}
+      File.expand_path(address)
+    when %r{\A(?:\*:)?(\d+)\z}
+      "0.0.0.0:#$1"
+    when %r{\A\[([a-fA-F0-9:]+)\]\z}, %r/\A((?:\d+\.){3}\d+)\z/
+      canonicalize_tcp($1, 80)
+    when %r{\A\[([a-fA-F0-9:]+)\]:(\d+)\z}, %r{\A(.*):(\d+)\z}
+      canonicalize_tcp($1, $2.to_i)
+    else
+      address
+    end
+  end
+
+  def canonicalize_tcp(addr, port)
+    packed = Socket.pack_sockaddr_in(port, addr)
+    port, addr = Socket.unpack_sockaddr_in(packed)
+    /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
+  end
+
+  def queue(name = :default, &block)
+    var = :queue
+    qegg = @qeggs[name] ||= Yahns::QueueEgg.new
+    prev_block = @block
+    _check_in_block(:app, var) if prev_block
+    if block_given?
+      @block = CfgBlock.new(:queue, qegg)
+      instance_eval(&block)
+      @block = prev_block
+    end
+    prev_block.ctx.qegg = qegg if prev_block
+  end
+
+  # queue parameters (Yahns::QueueEgg)
+  %w(max_events worker_threads).each do |_v|
+    eval(
+    %Q(def #{_v}(val);) <<
+    %Q(  _check_in_block(:queue, :#{_v});) <<
+    %Q(  @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, 1));) <<
+    %Q(end)
+    )
+  end
+
+  def _check_int(var, n, min)
+    Integer === n or raise ArgumentError, "not an integer: #{var}=#{n.inspect}"
+    n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}"
+    n
+  end
+
+  # global
+  def client_expire_threshold(val)
+    var = _check_in_block(nil, :client_expire_threshold)
+    val > 0 or raise ArgumentError, "#{var} must be > 0"
+    case val
+    when Float
+      val <= 1.0 or raise ArgumentError, "#{var} must be <= 1.0 if a ratio"
+    when Integer
+    else
+      raise ArgumentError, "#{var} must be a float or integer"
+    end
+    @set[var] = val
+  end
+
+  # type = :rack
+  def app(type, *args, &block)
+    var = _check_in_block(nil, :app)
+    file = "yahns/#{type.to_s}"
+    begin
+      require file
+    rescue LoadError => e
+      raise ArgumentError, "#{type.inspect} is not a supported app type",
+            e.backtrace
+    end
+    klass = APP_CLASS[type] or
+      raise TypeError,
+        "#{var}: #{file} did not register #{type} in #{self.class}::APP_CLASS"
+
+    # apps may have multiple configurator contexts
+    app = @app_instances[klass.instance_key(*args)] = klass.new(*args)
+    ctx = app.config_context
+    if block_given?
+      @block = CfgBlock.new(:app, ctx)
+      instance_eval(&block)
+      @block = nil
+    end
+    @app_ctx << ctx
+  end
+
+  def _check_bool(var, val)
+    return val if [ true, false ].include?(val)
+    raise ArgumentError, "#{var} must be boolean"
+  end
+
+  # boolean config directives for app
+  %w(check_client_connection
+     output_buffering
+     persistent_connections).each do |_v|
+    eval(
+    %Q(def #{_v}(bool);) <<
+    %Q(  _check_in_block(:app, :#{_v});) <<
+    %Q(  @block.ctx.__send__("#{_v}=", _check_bool(:#{_v}, bool));) <<
+    %Q(end)
+    )
+  end
+
+  # integer config directives for app
+  {
+    # config name, minimum value
+    client_body_buffer_size: 1,
+    client_max_body_size: 0,
+    client_header_buffer_size: 1,
+    client_max_header_size: 1,
+    client_timeout: 0,
+  }.each do |_v,minval|
+    eval(
+    %Q(def #{_v}(val);) <<
+    %Q(  _check_in_block(:app, :#{_v});) <<
+    %Q(  @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, #{minval}));) <<
+    %Q(end)
+    )
+  end
+
+  def input_buffering(val)
+    var = _check_in_block(:app, :input_buffering)
+    ok = [ :lazy, true, false ]
+    ok.include?(val) or
+      raise ArgumentError, "`#{var}' must be one of: #{ok.inspect}"
+    @block.ctx.__send__("#{var}=", val)
+  end
+
+  # used to configure rack.errors destination
+  def errors(val)
+    var = _check_in_block(:app, :errors)
+    if String === val
+      # we've already bound working_directory by the time we get here
+      val = File.open(File.expand_path(val), "a")
+      val.binmode
+      val.sync = true
+    else
+      rt = %w(puts write flush).map(&:to_sym) # match Rack::Lint
+      rt.all? { |m| val.respond_to?(m) } or raise ArgumentError,
+                   "`#{var}' destination must respond to all of: #{rt.inspect}"
+    end
+    @block.ctx.__send__("#{var}=", val)
+  end
+
+  def commit!(server)
+    # redirect IOs
+    { stdout_path: $stdout, stderr_path: $stderr }.each do |key, io|
+      path = @set[key]
+      if path == :unset && server.daemon_pipe
+        @set[key] = path = "/dev/null"
+      end
+      File.open(path, 'a') { |fp| io.reopen(fp) } if String === path
+      io.sync = true
+    end
+
+    [ :logger, :pid, :worker_processes ].each do |var|
+      val = @set[var]
+      server.__send__("#{var}=", val) if val != :unset
+    end
+    queue(:default) if @qeggs.empty?
+    @qeggs.each_value { |qegg| qegg.logger ||= server.logger }
+    @app_ctx.each { |app| app.logger ||= server.logger }
+  end
+end
diff --git a/lib/yahns/daemon.rb b/lib/yahns/daemon.rb
new file mode 100644
index 0000000..d12ff69
--- /dev/null
+++ b/lib/yahns/daemon.rb
@@ -0,0 +1,51 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+module Yahns::Daemon # :nodoc:
+  # We don't do a lot of standard daemonization stuff:
+  #   * umask is whatever was set by the parent process at startup
+  #     and can be set in config.ru and config_file, so making it
+  #     0000 and potentially exposing sensitive log data can be bad
+  #     policy.
+  #   * don't bother to chdir("/") here since yahns is designed to
+  #     run inside APP_ROOT.  Yahns will also re-chdir() to
+  #     the directory it was started in when being re-executed
+  #     to pickup code changes if the original deployment directory
+  #     is a symlink or otherwise got replaced.
+  def self.daemon(yahns_server)
+    $stdin.reopen("/dev/null")
+
+    # We only start a new process group if we're not being reexecuted
+    # and inheriting file descriptors from our parent
+    unless ENV['YAHNS_FD']
+      # grandparent - reads pipe, exits when master is ready
+      #  \_ parent  - exits immediately ASAP
+      #      \_ yahns master - writes to pipe when ready
+
+      # We cannot use Yahns::Sigevent (eventfd) here because we need
+      # to detect EOF on unexpected death, not just read/write
+      rd, wr = IO.pipe
+      grandparent = $$
+      if fork
+        wr.close # grandparent does not write
+      else
+        rd.close # yahns master does not read
+        Process.setsid
+        exit if fork # parent dies now
+      end
+
+      if grandparent == $$
+        # this will block until Server#join runs (or it dies)
+        master_pid = (rd.readpartial(16) rescue nil).to_i
+        unless master_pid > 1
+          warn "master failed to start, check stderr log for details"
+          exit!(1)
+        end
+        exit 0
+      else # yahns master process
+        yahns_server.daemon_pipe = wr
+      end
+    end
+    # $stderr/$stderr can/will be redirected separately in the Yahns config
+  end
+end
diff --git a/lib/yahns/fdmap.rb b/lib/yahns/fdmap.rb
new file mode 100644
index 0000000..0272421
--- /dev/null
+++ b/lib/yahns/fdmap.rb
@@ -0,0 +1,90 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'thread'
+
+# only initialize this after forking, this is highly volatile and won't
+# be able to share data across processes at all.
+# This is really a singleton
+
+class Yahns::Fdmap # :nodoc:
+  def initialize(logger, client_expire_threshold)
+    @logger = logger
+
+    if Float === client_expire_threshold
+      client_expire_threshold *= Process.getrlimit(:NOFILE)[0]
+    elsif client_expire_treshhold < 0
+      client_expire_threshold = Process.getrlimit(:NOFILE)[0] -
+                                client_expire_threshold
+    end
+    @client_expire_threshold = client_expire_threshold.to_i
+
+    # This is an array because any sane OS will frequently reuse FDs
+    # to keep this tightly-packed and favor lower FD numbers
+    # (consider select(2) performance (not that we use select))
+    # An (unpacked) Hash (in MRI) uses 5 more words per entry than an Array,
+    # and we should expect this array to have around 60K elements
+    @fdmap_ary = []
+    @fdmap_mtx = Mutex.new
+    @last_expire = 0.0
+    @count = 0
+  end
+
+  # called immediately after accept()
+  def add(io)
+    fd = io.fileno
+    @fdmap_mtx.synchronize do
+      if (@count += 1) > @client_expire_threshold
+        __expire_for(io)
+      else
+        @fdmap_ary[fd] = io
+      end
+    end
+  end
+
+  # this is only called in Errno::EMFILE/Errno::ENFILE situations
+  def desperate_expire_for(io, timeout)
+    @fdmap_mtx.synchronize { __expire_for(io, timeout) }
+  end
+
+  # called before IO#close
+  def decr
+    # don't bother clearing the element in @fdmap_ary, it'll just be
+    # overwritten when another client connects (soon).  We must not touch
+    # @fdmap_ary[io.fileno] after IO#close on io
+    @fdmap_mtx.synchronize { @count -= 1 }
+  end
+
+  def delete(io) # use with rack.hijack (via yahns)
+    fd = io.fileno
+    @fdmap_mtx.synchronize do
+      @fdmap_ary[fd] = nil
+      @count -= 1
+    end
+  end
+
+  # expire a bunch of idle clients and register the current one
+  # We should not be calling this too frequently, it is expensive
+  # This is called while @fdmap_mtx is held
+  def __expire_for(io, timeout = nil)
+    nr = 0
+    now = Time.now.to_f
+    (now - @last_expire) >= 1.0 or return # don't expire too frequently
+
+    # @fdmap_ary may be huge, so always expire a bunch at once to
+    # avoid getting to this method too frequently
+    @fdmap_ary.each do |c|
+      c.respond_to?(:yahns_expire) or next
+      nr += c.yahns_expire(timeout || c.class.client_timeout)
+    end
+
+    @fdmap_ary[io.fileno] = io
+    @last_expire = Time.now.to_f
+    msg = timeout ? "timeout=#{timeout})" : "client_timeout"
+    @logger.info("dropping #{nr} of #@count clients for #{msg}")
+  end
+
+  # used for graceful shutdown
+  def size
+    @fdmap_mtx.synchronize { @count }
+  end
+end
diff --git a/lib/yahns/http_client.rb b/lib/yahns/http_client.rb
new file mode 100644
index 0000000..8171460
--- /dev/null
+++ b/lib/yahns/http_client.rb
@@ -0,0 +1,196 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'yahns/tiny_input'
+class Yahns::HttpClient < Kgio::Socket # :nodoc:
+  NULL_IO = Yahns::TinyInput.new("")
+
+  include Yahns::HttpResponse
+  include Yahns::ClientExpire
+  QEV_FLAGS = Yahns::Queue::QEV_RD # used by acceptor
+  HTTP_RESPONSE_START = [ 'HTTP', '/1.1 ' ]
+
+  # A frozen format for this is about 15% faster (note from Mongrel)
+  REMOTE_ADDR = 'REMOTE_ADDR'.freeze
+  RACK_INPUT = 'rack.input'.freeze
+  RACK_HIJACK = 'rack.hijack'.freeze
+  RACK_HIJACK_IO = "rack.hijack_io".freeze
+
+  # called from acceptor thread
+  def yahns_init
+    @hs = Unicorn::HttpRequest.new
+    @response_start_sent = false
+    @state = :headers # :body, :trailers, :pipelined, Wbuf, StreamFile
+    @input = nil
+  end
+
+  # use if writes are deferred by buffering, this return value goes to
+  # the main epoll/kqueue worker loop
+  # returns :wait_readable, :wait_writable, or nil
+  def step_write
+    case rv = @state.wbuf_flush(self)
+    when :wait_writable, :wait_readable
+      return rv # tell epoll/kqueue to wait on this more
+    when :delete # :delete on hijack
+      @state = :delete
+      return :delete
+    when Yahns::StreamFile
+      @state = rv # continue looping
+    when true, false # done
+      return http_response_done(rv)
+    else
+      raise "BUG: #{@state.inspect}#wbuf_flush returned #{rv.inspect}"
+    end while true
+  end
+
+  def mkinput_preread
+    @state = :body
+    @input = self.class.tmpio_for(@hs.content_length)
+    rbuf = Thread.current[:yahns_rbuf]
+    @hs.filter_body(rbuf, @hs.buf)
+    @input.write(rbuf)
+  end
+
+  def input_ready
+    empty_body = 0 == @hs.content_length
+    k = self.class
+    case k.input_buffering
+    when true
+      # common case is an empty body
+      return NULL_IO if empty_body
+
+      # content_length is nil (chunked) or len > 0
+      mkinput_preread # keep looping
+      false
+    else # :lazy, false
+      empty_body ? NULL_IO : k.mkinput(self, @hs)
+    end
+  end
+
+  # the main entry point of the epoll/kqueue worker loop
+  def yahns_step
+    # always write unwritten data first if we have any
+    return step_write if Yahns::WbufCommon === @state
+
+    # only read if we had nothing to write in this event loop iteration
+    k = self.class
+    rbuf = Thread.current[:yahns_rbuf] # running under spawn_worker_threads
+
+    case @state
+    when :pipelined
+      if @hs.parse
+        input = input_ready and return app_call(input)
+        # @state == :body if we get here point (input_ready -> mkinput_preread)
+      else
+        @state = :headers
+      end
+      # continue to outer loop
+    when :headers
+      case rv = kgio_tryread(k.client_header_buffer_size, rbuf)
+      when String
+        if @hs.add_parse(rv)
+          input = input_ready and return app_call(input)
+          break # to outer loop to reevaluate @state == :body
+        end
+        # keep looping on kgio_tryread
+      when :wait_readable, :wait_writable, nil
+        return rv
+      end while true
+    when :body
+      if @hs.body_eof?
+        if @hs.content_length || @hs.parse # hp.parse == trailers done!
+          @input.rewind
+          return app_call(@input)
+        else # possible Transfer-Encoding:chunked, keep looping
+          @state = :trailers
+        end
+      else
+        case rv = kgio_tryread(k.client_body_buffer_size, rbuf)
+        when String
+          @hs.filter_body(rbuf, @hs.buf << rbuf)
+          @input.write(rbuf)
+          # keep looping on kgio_tryread...
+        when :wait_readable, :wait_writable
+          return rv # have epoll/kqueue wait for more
+        when nil # unexpected EOF
+          return @input.close # nil
+        end # continue to outer loop (case @state)
+      end
+    when :trailers
+      case rv = kgio_tryread(k.client_header_buffer_size, rbuf)
+      when String
+        if @hs.add_parse(rbuf)
+          @input.rewind
+          return app_call(@input)
+        end
+        # keep looping on kgio_tryread...
+      when :wait_readable, :wait_writable
+        return rv # wait for more
+      when nil # unexpected EOF
+        return @input.close # nil
+      end while true
+    end while true # outer loop
+  rescue => e
+    handle_error(e)
+  end
+
+  def app_call(input)
+    env = @hs.env
+    env[REMOTE_ADDR] = @kgio_addr
+    env[RACK_HIJACK] = hijack_proc(env)
+    env[RACK_INPUT] = @input ||= input
+    k = self.class
+
+    if k.check_client_connection && @hs.headers?
+      @response_start_sent = true
+      # FIXME: we should buffer this just in case
+      HTTP_RESPONSE_START.each { |c| kgio_write(c) }
+    end
+
+    # run the rack app
+    response = k.app.call(env.merge!(k.app_defaults))
+    return :delete if env.include?(RACK_HIJACK_IO)
+
+    # this returns :wait_readable, :wait_writable, :delete, or nil:
+    http_response_write(*response)
+  end
+
+  def hijack_proc(env)
+    proc { env[RACK_HIJACK_IO] = self }
+  end
+
+  # called automatically by kgio_write
+  def kgio_wait_writable(timeout = self.class.client_timeout)
+    super timeout
+  end
+
+  # called automatically by kgio_read
+  def kgio_wait_readable(timeout = self.class.client_timeout)
+    super timeout
+  end
+
+  # if we get any error, try to write something back to the client
+  # assuming we haven't closed the socket, but don't get hung up
+  # if the socket is already closed or broken.  We'll always return
+  # nil to ensure the socket is closed at the end of this function
+  def handle_error(e)
+    code = case e
+    when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN
+      return # don't send response, drop the connection
+    when Unicorn::RequestURITooLongError
+      414
+    when Unicorn::RequestEntityTooLargeError
+      413
+    when Unicorn::HttpParserError # try to tell the client they're bad
+      400
+    else
+      Yahns::Log.exception(@hs.env["rack.logger"], "app error", e)
+      500
+    end
+    kgio_trywrite(err_response(code))
+  rescue
+  ensure
+    shutdown rescue nil
+    return # always drop the connection on uncaught errors
+  end
+end
diff --git a/lib/yahns/http_context.rb b/lib/yahns/http_context.rb
new file mode 100644
index 0000000..1af41df
--- /dev/null
+++ b/lib/yahns/http_context.rb
@@ -0,0 +1,66 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'yahns/tiny_input'
+
+# subclasses of Yahns::HttpClient will class extend this
+
+module Yahns::HttpContext # :nodoc:
+  attr_accessor :check_client_connection
+  attr_accessor :client_body_buffer_size
+  attr_accessor :client_header_buffer_size
+  attr_accessor :client_max_body_size
+  attr_accessor :client_max_header_size
+  attr_accessor :input_buffering  # :lazy, true, false
+  attr_accessor :output_buffering # true, false
+  attr_accessor :persistent_connections # true or false only
+  attr_accessor :client_timeout
+  attr_accessor :qegg
+  attr_reader :app
+  attr_reader :app_defaults
+
+  def http_ctx_init(yahns_rack)
+    @yahns_rack = yahns_rack
+    @app_defaults = yahns_rack.app_defaults
+    @check_client_connection = false
+    @client_body_buffer_size = 112 * 1024
+    @client_header_buffer_size = 4000
+    @client_max_body_size = 1024 * 1024
+    @input_buffering = true
+    @output_buffering = true
+    @persistent_connections = true
+    @client_timeout = 15
+    @qegg = nil
+  end
+
+  # call this after forking
+  def after_fork_init
+    @app = @yahns_rack.app_after_fork
+  end
+
+  # call this immediately after successful accept()/accept4()
+  def logger=(l) # cold
+    @logger = @app_defaults["rack.logger"] = l
+  end
+
+  def logger
+    @app_defaults["rack.logger"]
+  end
+
+  def mkinput(client, hs)
+    (@input_buffering ? Yahns::TeeInput : Yahns::StreamInput).new(client, hs)
+  end
+
+  def errors=(dest)
+    @app_defaults["rack.errors"] = dest
+  end
+
+  def errors
+    @app_defaults["rack.errors"]
+  end
+
+  def tmpio_for(len)
+    len && len <= @client_body_buffer_size ?
+      Yahns::TinyInput.new("") : Yahns::TmpIO.new
+  end
+end
diff --git a/lib/yahns/http_response.rb b/lib/yahns/http_response.rb
new file mode 100644
index 0000000..aad2762
--- /dev/null
+++ b/lib/yahns/http_response.rb
@@ -0,0 +1,183 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'stream_file'
+
+# Writes a Rack response to your client using the HTTP/1.1 specification.
+# You use it by simply doing:
+#
+#   status, headers, body = rack_app.call(env)
+#   http_response_write(status, headers, body)
+#
+# Most header correctness (including Content-Length and Content-Type)
+# is the job of Rack, with the exception of the "Date" header.
+module Yahns::HttpResponse # :nodoc:
+  include Unicorn::HttpResponse
+
+  # avoid GC overhead for frequently used-strings:
+  CONN_KA = "Connection: keep-alive\r\n\r\n"
+  CONN_CLOSE = "Connection: close\r\n\r\n"
+  Z = ""
+  RESPONSE_START = "HTTP/1.1 "
+
+  def response_start
+    @response_start_sent ? Z : RESPONSE_START
+  end
+
+  def response_wait_write(rv)
+    # call the kgio_wait_readable or kgio_wait_writable method
+    ok = __send__("kgio_#{rv}") and return ok
+    k = self.class
+    k.logger.info("fd=#{fileno} ip=#@kgio_addr timeout on :#{rv} after "\
+                  "#{k.client_timeout}s")
+    nil
+  end
+
+  def err_response(code)
+    "#{response_start}#{CODES[code]}\r\n\r\n"
+  end
+
+  def response_header_blocked(ret, header, body, alive, offset, count)
+    if body.respond_to?(:to_path)
+      alive = Yahns::StreamFile.new(body, alive, offset, count)
+      body = nil
+    end
+    wbuf = Yahns::Wbuf.new(body, alive)
+    rv = wbuf.wbuf_write(self, header)
+    body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) } if body
+    wbuf_maybe(wbuf, rv, alive)
+  end
+
+  def wbuf_maybe(wbuf, rv, alive)
+    case rv # trysendfile return value
+    when nil
+      case alive
+      when :delete
+        @state = :delete
+      when true, false
+        http_response_done(alive)
+      end
+    else
+      @state = wbuf
+      rv
+    end
+  end
+
+  def http_response_done(alive)
+    @input = @input.discard if @input
+    if alive
+      @response_start_sent = false
+      # @hs.buf will have data if the client pipelined
+      if @hs.buf.empty?
+        @state = :headers
+        :wait_readable
+      else
+        @state = :pipelined
+        # may need to wait for readability if SSL,
+        # only need writability if plain TCP
+        :wait_readwrite
+      end
+    else
+      # shutdown is needed in case the app forked, we rescue here since
+      # StreamInput may issue shutdown as well
+      shutdown rescue nil
+      nil # trigger close
+    end
+  end
+
+  # writes the rack_response to socket as an HTTP response
+  # returns :wait_readable, :wait_writable, :forget, or nil
+  def http_response_write(status, headers, body)
+    status = CODES[status.to_i] || status
+    offset = 0
+    count = hijack = nil
+    alive = @hs.next?
+
+    if @hs.headers?
+      buf = "#{response_start}#{status}\r\nDate: #{httpdate}\r\n"
+      headers.each do |key, value|
+        case key
+        when %r{\ADate\z}
+          next
+        when %r{\AContent-Range\z}i
+          if %r{\Abytes (\d+)-(\d+)/\d+\z} =~ value
+            offset = $1.to_i
+            count = $2.to_i - offset + 1
+          end
+        when %r{\AConnection\z}i
+          # allow Rack apps to tell us they want to drop the client
+          alive = !!(value =~ /\bclose\b/i)
+        when "rack.hijack"
+          hijack = value
+          body = nil # ensure we do not close body
+        else
+          if value =~ /\n/
+            # avoiding blank, key-only cookies with /\n+/
+            buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
+          else
+            buf << "#{key}: #{value}\r\n"
+          end
+        end
+      end
+      buf << (alive ? CONN_KA : CONN_CLOSE)
+      case rv = kgio_trywrite(buf)
+      when nil # all done, likely
+        break
+      when String
+        buf = rv # hope the skb grows
+      when :wait_writable, :wait_readable
+        if self.class.output_buffering
+          alive = hijack ? hijack : alive
+          rv = response_header_blocked(rv, buf, body, alive, offset, count)
+          body = nil # ensure we do not close body in ensure
+          return rv
+        else
+          response_wait_write(rv) or return
+        end
+      end while true
+    end
+
+    if hijack
+      hijack.call(self)
+      return :delete # trigger EPOLL_CTL_DEL
+    end
+
+    if body.respond_to?(:to_path)
+      @state = body = Yahns::StreamFile.new(body, alive, offset, count)
+      return step_write
+    end
+
+    wbuf = rv = nil
+    body.each do |chunk|
+      if wbuf
+        rv = wbuf.wbuf_write(self, chunk)
+      else
+        case rv = kgio_trywrite(chunk)
+        when nil # all done, likely and good!
+          break
+        when String
+          chunk = rv # hope the skb grows when we loop into the trywrite
+        when :wait_writable, :wait_readable
+          if self.class.output_buffering
+            wbuf = Yahns::Wbuf.new(body, alive)
+            rv = wbuf.wbuf_write(self, chunk)
+            break
+          else
+            response_wait_write(rv) or return
+          end
+        end while true
+      end
+    end
+
+    # if we buffered the write body, we must return :wait_writable
+    # (or :wait_readable for SSL) and hit Yahns::HttpClient#step_write
+    if wbuf
+      body = nil # ensure we do not close the body in ensure
+      wbuf_maybe(wbuf, rv, alive)
+    else
+      http_response_done(alive)
+    end
+  ensure
+    body.respond_to?(:close) and body.close
+  end
+end
diff --git a/lib/yahns/log.rb b/lib/yahns/log.rb
new file mode 100644
index 0000000..59baa85
--- /dev/null
+++ b/lib/yahns/log.rb
@@ -0,0 +1,73 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# logging-related utility functions for all of yahns
+module Yahns::Log # :nodoc:
+  def self.exception(logger, prefix, exc)
+    message = exc.message
+    message = message.dump if /[[:cntrl:]]/ =~ message # prevent code injection
+    logger.error "#{prefix}: #{message} (#{exc.class})"
+    exc.backtrace.each { |line| logger.error(line) }
+  end
+
+  def self.is_log?(fp)
+    append_flags = IO::WRONLY | IO::APPEND
+
+    ! fp.closed? &&
+      fp.stat.file? &&
+      fp.sync &&
+      (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
+  rescue IOError, Errno::EBADF
+    false
+  end
+
+  def self.chown_all(uid, gid)
+    ObjectSpace.each_object(File) do |fp|
+      fp.chown(uid, gid) if is_log?(fp)
+    end
+  end
+
+  # This reopens ALL logfiles in the process that have been rotated
+  # using logrotate(8) (without copytruncate) or similar tools.
+  # A +File+ object is considered for reopening if it is:
+  #   1) opened with the O_APPEND and O_WRONLY flags
+  #   2) the current open file handle does not match its original open path
+  #   3) unbuffered (as far as userspace buffering goes, not O_SYNC)
+  # Returns the number of files reopened
+  def self.reopen_all
+    to_reopen = []
+    nr = 0
+    ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
+
+    to_reopen.each do |fp|
+      orig_st = begin
+        fp.stat
+      rescue IOError, Errno::EBADF
+        next
+      end
+
+      begin
+        b = File.stat(fp.path)
+        next if orig_st.ino == b.ino && orig_st.dev == b.dev
+      rescue Errno::ENOENT
+      end
+
+      begin
+        File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
+        fp.sync = true
+        new_st = fp.stat
+
+        # this should only happen in the master:
+        if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
+          fp.chown(orig_st.uid, orig_st.gid)
+        end
+
+        nr += 1
+      rescue IOError, Errno::EBADF
+        # not much we can do...
+      end
+    end
+    nr
+  end
+end
diff --git a/lib/yahns/queue.rb b/lib/yahns/queue.rb
new file mode 100644
index 0000000..add4f78
--- /dev/null
+++ b/lib/yahns/queue.rb
@@ -0,0 +1,7 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+if SleepyPenguin.const_defined?(:Epoll)
+  require_relative 'queue_epoll'
+else
+  require_relative 'queue_kqueue' # TODO
+end
diff --git a/lib/yahns/queue_egg.rb b/lib/yahns/queue_egg.rb
new file mode 100644
index 0000000..a2abc2f
--- /dev/null
+++ b/lib/yahns/queue_egg.rb
@@ -0,0 +1,23 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this represents a Yahns::Queue before its vivified.  This only
+# lives in the parent process and should be clobbered after qc_vivify
+class Yahns::QueueEgg # :nodoc:
+  attr_writer :max_events, :worker_threads
+  attr_accessor :logger
+
+  def initialize
+    @max_events = 1 # 1 is good if worker_threads > 1
+    @worker_threads = 7 # any default is wrong for most apps...
+    @logger = nil
+  end
+
+  # only call after forking
+  def qc_vivify(fdmap)
+    queue = Yahns::Queue.new
+    queue.fdmap = fdmap
+    queue.spawn_worker_threads(@logger, @worker_threads, @max_events)
+    queue
+  end
+end
diff --git a/lib/yahns/queue_epoll.rb b/lib/yahns/queue_epoll.rb
new file mode 100644
index 0000000..c9febc4
--- /dev/null
+++ b/lib/yahns/queue_epoll.rb
@@ -0,0 +1,57 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
+  include SleepyPenguin
+  attr_accessor :fdmap # Yahns::Fdmap
+
+  # public
+  QEV_RD = Epoll::IN | Epoll::ONESHOT
+  QEV_WR = Epoll::OUT | Epoll::ONESHOT
+  QEV_RDWR = QEV_RD | QEV_WR
+
+  def self.new
+    super(SleepyPenguin::Epoll::CLOEXEC)
+  end
+
+  # for HTTP and HTTPS servers, we rely on the io writing to us, first
+  # flags: QEV_RD/QEV_WR (usually QEV_RD)
+  def queue_add(io, flags)
+    @fdmap.add(io)
+    epoll_ctl(Epoll::CTL_ADD, io, flags)
+  end
+
+  # returns an array of infinitely running threads
+  def spawn_worker_threads(logger, worker_threads, max_events)
+    worker_threads.times do
+      Thread.new do
+        Thread.current[:yahns_rbuf] = ""
+        begin
+          epoll_wait(max_events) do |_, io| # don't care for flags for now
+            case rv = io.yahns_step
+            when :wait_readable
+              epoll_ctl(Epoll::CTL_MOD, io, QEV_RD)
+            when :wait_writable
+              epoll_ctl(Epoll::CTL_MOD, io, QEV_WR)
+            when :wait_readwrite
+              epoll_ctl(Epoll::CTL_MOD, io, QEV_RDWR)
+            when :delete # only used by rack.hijack
+              epoll_ctl(Epoll::CTL_DEL, io, 0)
+              @fdmap.delete(io)
+            when nil
+              # this is be the ONLY place where we call IO#close on
+              # things inside the queue
+              io.close
+              @fdmap.decr
+            else
+              raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
+            end
+          end
+        rescue => e
+          break if (IOError === e || Errno::EBADF === e) && closed?
+          Yahns::Log.exception(logger, 'queue loop', e)
+        end while true
+      end
+    end
+  end
+end
diff --git a/lib/yahns/rack.rb b/lib/yahns/rack.rb
new file mode 100644
index 0000000..27857ec
--- /dev/null
+++ b/lib/yahns/rack.rb
@@ -0,0 +1,80 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'rack'
+class Yahns::Rack # :nodoc:
+  attr_reader :preload
+
+  # enforce a single instance for the identical config.ru
+  def self.instance_key(*args)
+    ru = args[0]
+
+    # it's safe to expand_path now since we enforce working_directory in the
+    # top-level config is called before any apps are created
+    # ru may also be a Rack::Builder object or any already-built Rack app
+    ru.respond_to?(:call) ? ru.object_id : File.expand_path(ru)
+  end
+
+  def initialize(ru, opts = {})
+    # always called after config file parsing, may be called after forking
+    @app = lambda do
+      ENV["RACK_ENV"] ||= "none"
+      if ru.respond_to?(:call)
+        inner_app = ru.respond_to?(:to_app) ? ru.to_app : ru
+      else
+        inner_app = case ru
+        when /\.ru$/
+          raw = File.read(ru)
+          raw.sub!(/^__END__\n.*/, '')
+          eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
+        else
+          require ru
+          Object.const_get(File.basename(ru, '.rb').capitalize)
+        end
+      end
+      inner_app
+    end
+    @ru = ru
+    @preload = opts[:preload]
+    build_app! if @preload
+  end
+
+  def config_context
+    ctx_class = Class.new(Yahns::HttpClient)
+    ctx_class.extend(Yahns::HttpContext)
+    ctx_class.http_ctx_init(self)
+    ctx_class
+  end
+
+  def build_app!
+    if @app.respond_to?(:arity) && @app.arity == 0
+      Gem.refresh if defined?(Gem) && Gem.respond_to?(:refresh)
+      @app = @app.call
+    end
+  end
+
+  # allow different HttpContext instances to have different Rack defaults
+  def app_defaults
+    {
+      # logger is set in http_context
+      "rack.errors" => $stderr,
+      "rack.multiprocess" => true,
+      "rack.multithread" => true,
+      "rack.run_once" => false,
+      "rack.hijack?" => true,
+      "rack.version" => [ 1, 2 ],
+      "SCRIPT_NAME" => "",
+
+      # this is not in the Rack spec, but some apps may rely on it
+      "SERVER_SOFTWARE" => "yahns"
+    }
+  end
+
+  def app_after_fork
+    build_app! unless @preload
+    @app
+  end
+end
+
+# register ourselves
+Yahns::Config::APP_CLASS[:rack] = Yahns::Rack
diff --git a/lib/yahns/server.rb b/lib/yahns/server.rb
new file mode 100644
index 0000000..c7a5a57
--- /dev/null
+++ b/lib/yahns/server.rb
@@ -0,0 +1,328 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::Server # :nodoc:
+  QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
+  attr_accessor :daemon_pipe
+  attr_accessor :logger
+  attr_writer :worker_processes
+  include Yahns::SocketHelper
+
+  def initialize(config)
+    @reexec_pid = 0
+    @daemon_pipe = nil # writable IO or true
+    @config = config
+    @workers = {} # pid -> workers
+    @sig_queue = [] # nil in forked workers
+    @logger = Logger.new($stderr)
+    @sev = Yahns::Sigevent.new
+    @listeners = []
+    @pid = nil
+    @worker_processes = nil
+    @user = nil
+  end
+
+  def sqwakeup(sig)
+    @sig_queue << sig
+    @sev.sev_signal
+  end
+
+  def start
+    @config.commit!(self)
+    inherit_listeners!
+    # we try inheriting listeners first, so we bind them later.
+    # we don't write the pid file until we've bound listeners in case
+    # yahns was started twice by mistake.  Even though our #pid= method
+    # checks for stale/existing pid files, race conditions are still
+    # possible (and difficult/non-portable to avoid) and can be likely
+    # to clobber the pid if the second start was in quick succession
+    # after the first, so we rely on the listener binding to fail in
+    # that case.  Some tests (in and outside of this source tree) and
+    # monitoring tools may also rely on pid files existing before we
+    # attempt to connect to the listener(s)
+
+    # setup signal handlers before writing pid file in case people get
+    # trigger happy and send signals as soon as the pid file exists.
+    QUEUE_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
+    self.pid = @config.value(:pid) # write pid file
+    bind_new_listeners!
+    if @worker_processes
+      require 'yahns/server_mp'
+      extend Yahns::ServerMP
+      mp_init
+    end
+    self
+  end
+
+  # replaces current listener set with +listeners+.  This will
+  # close the socket if it will not exist in the new listener set
+  def listeners=(listeners)
+    cur_names, dead_names = [], []
+    listener_names.each do |name|
+      if ?/ == name[0]
+        # mark unlinked sockets as dead so we can rebind them
+        (File.socket?(name) ? cur_names : dead_names) << name
+      else
+        cur_names << name
+      end
+    end
+    set_names = listener_names(listeners)
+    dead_names.concat(cur_names - set_names).uniq!
+
+    @listeners.delete_if do |io|
+      if dead_names.include?(sock_name(io))
+        (io.close rescue nil).nil? # true
+      else
+        set_server_sockopt(io, sock_opts(io))
+        false
+      end
+    end
+
+    (set_names - cur_names).each { |addr| listen(addr) }
+  end
+
+  # sets the path for the PID file of the master process
+  def pid=(path)
+    if path
+      if x = valid_pid?(path)
+        return path if @pid && path == @pid && x == $$
+        if x == @reexec_pid && @pid =~ /\.oldbin\z/
+          @logger.warn("will not set pid=#{path} while reexec-ed "\
+                       "child is running PID:#{x}")
+          return
+        end
+        raise ArgumentError, "Already running on PID:#{x} " \
+                             "(or pid=#{path} is stale)"
+      end
+    end
+    unlink_pid_safe(@pid) if @pid
+
+    if path
+      fp = begin
+        tmp = "#{File.dirname(path)}/#{rand}.#$$"
+        File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
+      rescue Errno::EEXIST
+        retry
+      end
+      fp.syswrite("#$$\n")
+      File.rename(fp.path, path)
+      fp.close
+    end
+    @pid = path
+  end
+
+  # add a given address to the +listeners+ set, idempotently
+  # Allows workers to add a private, per-process listener via the
+  # after_fork hook.  Very useful for debugging and testing.
+  # +:tries+ may be specified as an option for the number of times
+  # to retry, and +:delay+ may be specified as the time in seconds
+  # to delay between retries.
+  # A negative value for +:tries+ indicates the listen will be
+  # retried indefinitely, this is useful when workers belonging to
+  # different masters are spawned during a transparent upgrade.
+  def listen(address)
+    address = @config.expand_addr(address)
+    return if String === address && listener_names.include?(address)
+
+    begin
+      io = bind_listen(address, sock_opts(address))
+      unless Kgio::TCPServer === io || Kgio::UNIXServer === io
+        io = server_cast(io)
+      end
+      @logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
+      @listeners << io
+      io
+    rescue Errno::EADDRINUSE => err
+      @logger.error "adding listener failed addr=#{address} (in use)"
+    rescue => err
+      @logger.fatal "error adding listener addr=#{address}"
+      raise err
+    end
+  end
+
+  def daemon_ready
+    @daemon_pipe or return
+    @daemon_pipe.syswrite("#$$")
+    @daemon_pipe.close
+    @daemon_pipe = true # for SIGWINCH
+  end
+
+  # reexecutes the Yahns::START with a new binary
+  def reexec
+    if @reexec_pid > 0
+      begin
+        Process.kill(0, @reexec_pid)
+        @logger.error "reexec-ed child already running PID:#@reexec_pid"
+        return
+      rescue Errno::ESRCH
+        @reexec_pid = 0
+      end
+    end
+
+    if @pid
+      old_pid = "#@pid.oldbin"
+      begin
+        self.pid = old_pid  # clear the path for a new pid file
+      rescue ArgumentError
+        @logger.error "old PID:#{valid_pid?(old_pid)} running with " \
+                      "existing pid=#{old_pid}, refusing rexec"
+        return
+      rescue => e
+        @logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
+        return
+      end
+    end
+
+    @reexec_pid = fork do
+      redirects = {}
+      listeners.each do |sock|
+        sock.close_on_exec = false
+        redirects[sock.fileno] = sock
+      end
+      ENV['YAHNS_FD'] = redirects.keys.map(&:to_s).join(',')
+      Dir.chdir(@config.value(:working_directory) || Yahns::START[:cwd])
+      cmd = [ Yahns::START[0] ].concat(Yahns::START[:argv])
+      @logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
+      cmd << redirects
+      exec(*cmd)
+    end
+    proc_name 'master (old)'
+  end
+
+  # unlinks a PID file at given +path+ if it contains the current PID
+  # still potentially racy without locking the directory (which is
+  # non-portable and may interact badly with other programs), but the
+  # window for hitting the race condition is small
+  def unlink_pid_safe(path)
+    (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
+  end
+
+  # returns a PID if a given path contains a non-stale PID file,
+  # nil otherwise.
+  def valid_pid?(path)
+    wpid = File.read(path).to_i
+    wpid <= 0 and return
+    Process.kill(0, wpid)
+    wpid
+  rescue Errno::EPERM
+    @logger.info "pid=#{path} possibly stale, got EPERM signalling PID:#{wpid}"
+    nil
+  rescue Errno::ESRCH, Errno::ENOENT
+    # don't unlink stale pid files, racy without non-portable locking...
+  end
+
+  def load_config!
+    @logger.info "reloading config_file=#{@config.config_file}"
+    @config.config_reload!
+    @config.commit!(self)
+    kill_each_worker(:QUIT)
+    Yahns::Log.reopen_all
+    @logger.info "done reloading config_file=#{@config.config_file}"
+  rescue StandardError, LoadError, SyntaxError => e
+    Yahns::Log.exception(@logger,
+                     "error reloading config_file=#{@config.config_file}", e)
+  end
+
+  # returns an array of string names for the given listener array
+  def listener_names(listeners = @listeners)
+    listeners.map { |io| sock_name(io) }
+  end
+
+  def sock_opts(io)
+    @config.config_listeners[sock_name(io)]
+  end
+
+  def inherit_listeners!
+    # inherit sockets from parents, they need to be plain Socket objects
+    # before they become Kgio::UNIXServer or Kgio::TCPServer
+    inherited = ENV['YAHNS_FD'].to_s.split(/,/).map do |fd|
+      io = Socket.for_fd(fd.to_i)
+      set_server_sockopt(io, sock_opts(io))
+      @logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
+      server_cast(io)
+    end
+
+    @listeners.replace(inherited)
+  end
+
+  # call only after calling inherit_listeners!
+  # This binds any listeners we did NOT inherit from the parent
+  def bind_new_listeners!
+    self.listeners = @config.config_listeners.keys
+    raise ArgumentError, "no listeners" if @listeners.empty?
+    @listeners.each { |l| l.extend(Yahns::Acceptor) }
+  end
+
+  def proc_name(tag)
+    s = Yahns::START
+    $0 = ([ File.basename(s[0]), tag ]).concat(s[:argv]).join(' ')
+  end
+
+  # spins up processing threads of the server
+  def fdmap_init
+    thresh = @config.value(:client_expire_threshold)
+
+    # keeps track of all connections, like ObjectSpace, but only for IOs
+    fdmap = Yahns::Fdmap.new(@logger, thresh)
+
+    # initialize queues (epoll/kqueue) and associated worker threads
+    queues = {}
+    @config.qeggs.each do |name, qegg|
+      queue = qegg.qc_vivify(fdmap) # worker threads run after this
+      queues[qegg] = queue
+    end
+
+    # spin up applications (which are preload: false)
+    @config.app_ctx.each { |ctx| ctx.after_fork_init }
+
+    # spin up acceptors, clients flow into worker queues after this
+    @listeners.each do |l|
+      ctx = sock_opts(l)[:yahns_app_ctx]
+      qegg = ctx.qegg || @config.qeggs[:default]
+
+      # acceptors feed the the queues
+      l.spawn_acceptor(@logger, ctx, queues[qegg])
+    end
+    fdmap
+  end
+
+  def usr1_reopen(prefix)
+    @logger.info "#{prefix}reopening logs..."
+    Yahns::Log.reopen_all
+    @logger.info "#{prefix}done reopening logs"
+  end
+
+  def sp_sig_handle(alive)
+    @sev.kgio_wait_readable(alive ? nil : 0.01)
+    @sev.yahns_step
+    case sig = @sig_queue.shift
+    when :QUIT, :TERM, :INT
+      self.listeners = [] # stop accepting new connections
+      exit(0) unless alive
+      return false
+    when :USR1
+      usr1_reopen('')
+    when :USR2
+      reexec
+    when :HUP
+      reexec
+      return false
+    when :TTIN, :TTOU, :WINCH
+      @logger.info("SIG#{sig} ignored in single-process mode")
+    end
+    alive
+  end
+
+  # single-threaded only, this is overriden if @worker_processes is non-nil
+  def join
+    daemon_ready
+    fdmap = fdmap_init
+    alive = true
+    begin
+      alive = sp_sig_handle(alive)
+    rescue => e
+      Yahns::Log.exception(@logger, "main loop", e)
+    end while alive || fdmap.size > 0
+    unlink_pid_safe(@pid) if @pid
+  end
+end
diff --git a/lib/yahns/server_mp.rb b/lib/yahns/server_mp.rb
new file mode 100644
index 0000000..8818bac
--- /dev/null
+++ b/lib/yahns/server_mp.rb
@@ -0,0 +1,184 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+module Yahns::ServerMP # :nodoc:
+  EXIT_SIGS = [ :QUIT, :TERM, :INT ]
+
+  def mp_init
+    trap(:CHLD) { @sev.sev_signal }
+  end
+
+  # reaps all unreaped workers
+  def reap_all_workers
+    begin
+      wpid, status = Process.waitpid2(-1, Process::WNOHANG)
+      wpid or return
+      if @reexec_pid == wpid
+        @logger.error "reaped #{status.inspect} exec()-ed"
+        @reexec_pid = 0
+        self.pid = @pid.chomp('.oldbin') if @pid
+        proc_name 'master'
+      else
+        worker = @workers.delete(wpid)
+        worker_id = worker ? worker.nr : "(unknown)"
+        m = "reaped #{status.inspect} worker=#{worker_id}"
+        status.success? ? @logger.info(m) : @logger.error(m)
+      end
+    rescue Errno::ECHILD
+      return
+    end while true
+  end
+
+  def maintain_worker_count
+    (off = @workers.size - @worker_processes) == 0 and return
+    off < 0 and return spawn_missing_workers
+    @workers.each_pair do |wpid, worker|
+      worker.nr >= @worker_processes and Process.kill(:QUIT, wpid)
+    end
+  end
+
+  # delivers a signal to each worker
+  def kill_each_worker(signal)
+    @workers.each_key { |wpid| Process.kill(signal, wpid) }
+  end
+
+  # this is the first thing that runs after forking in a child
+  # gets rid of stuff the worker has no business keeping track of
+  # to free some resources and drops all sig handlers.
+  # traps for USR1, USR2, and HUP may be set in the after_fork Proc
+  # by the user.
+  def after_fork_internal(worker)
+    worker.atfork_child
+
+    # daemon_pipe may be true for non-initial workers
+    @daemon_pipe = @daemon_pipe.close if @daemon_pipe.respond_to?(:close)
+
+    srand # in case this pops up again: https://bugs.ruby-lang.org/issues/4338
+
+    # The OpenSSL PRNG is seeded with only the pid, and apps with frequently
+    # dying workers can recycle pids
+    OpenSSL::Random.seed(rand.to_s) if defined?(OpenSSL::Random)
+    # we'll re-trap EXIT_SIGS later for graceful shutdown iff we accept clients
+    EXIT_SIGS.each { |sig| trap(sig) { exit!(0) } }
+    exit!(0) if (@sig_queue & EXIT_SIGS)[0] # did we inherit sigs from parent?
+    @sig_queue = []
+
+    # ignore WINCH, TTIN, TTOU, HUP in the workers
+    (Yahns::Server::QUEUE_SIGS - EXIT_SIGS).each { |sig| trap(sig, nil) }
+    trap(:CHLD, 'DEFAULT')
+    @logger.info("worker=#{worker.nr} spawned pid=#$$")
+    proc_name "worker[#{worker.nr}]"
+    Yahns::START.clear
+    @sev.close
+    @sev = Yahns::Sigevent.new
+    worker.user(*@user) if @user
+    @user = @workers = nil
+  end
+
+  def spawn_missing_workers
+    worker_nr = -1
+    until (worker_nr += 1) == @worker_processes
+      @workers.value?(worker_nr) and next
+      worker = Yahns::Worker.new(worker_nr)
+      @logger.info("worker=#{worker_nr} spawning...")
+      if pid = fork
+        @workers[pid] = worker.atfork_parent
+      else
+        after_fork_internal(worker)
+        run_mp_worker(worker)
+      end
+    end
+  rescue => e
+    Yahns::Log.exception(@logger, "spawning worker", e)
+    exit!
+  end
+
+  # monitors children and receives signals forever
+  # (or until a termination signal is sent).  This handles signals
+  # one-at-a-time time and we'll happily drop signals in case somebody
+  # is signalling us too often.
+  def join
+    spawn_missing_workers
+    state = :respawn # :QUIT, :WINCH
+    proc_name 'master'
+    @logger.info "master process ready"
+    daemon_ready
+    begin
+      @sev.kgio_wait_readable
+      @sev.yahns_step
+      reap_all_workers
+      case @sig_queue.shift
+      when *EXIT_SIGS # graceful shutdown (twice for non graceful)
+        self.listeners = []
+        kill_each_worker(:QUIT)
+        state = :QUIT
+      when :USR1 # rotate logs
+        usr1_reopen("master ")
+        kill_each_worker(:USR1)
+      when :USR2 # exec binary, stay alive in case something went wrong
+        reexec
+      when :WINCH
+        if @daemon_pipe
+          state = :WINCH
+          @logger.info "gracefully stopping all workers"
+          kill_each_worker(:QUIT)
+          @worker_processes = 0
+        else
+          @logger.info "SIGWINCH ignored because we're not daemonized"
+        end
+      when :TTIN
+        state = :respawn unless state == :QUIT
+        @worker_processes += 1
+      when :TTOU
+        @worker_processes -= 1 if @worker_processes > 0
+      when :HUP
+        state = :respawn unless state == :QUIT
+        if @config.config_file
+          load_config!
+        else # exec binary and exit if there's no config file
+          @logger.info "config_file not present, reexecuting binary"
+          reexec
+        end
+      end while @sig_queue[0]
+      maintain_worker_count if state == :respawn
+    rescue => e
+      Yahns::Log.exception(@logger, "master loop error", e)
+    end while state != :QUIT || @workers.size > 0
+    @logger.info "master complete"
+    unlink_pid_safe(@pid) if @pid
+  end
+
+  def fdmap_init_mp
+    fdmap = fdmap_init # builds apps (if not preloading)
+    EXIT_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
+    @config = nil
+    fdmap
+  end
+
+  def run_mp_worker(worker)
+    fdmap = fdmap_init_mp
+    alive = true
+    begin
+      alive = mp_sig_handle(worker, alive)
+    rescue => e
+      Yahns::Log.exception(@logger, "main worker loop", e)
+    end while alive || fdmap.size > 0
+    exit
+  end
+
+  def mp_sig_handle(worker, alive)
+    # not performance critical
+    r = IO.select([worker, @sev], nil, nil, alive ? nil : 0.01) and
+      r[0].each { |io| io.yahns_step }
+    case sig = @sig_queue.shift
+    when *EXIT_SIGS
+      self.listeners = []
+      exit(0) unless alive # drop connections immediately if signaled twice
+      @logger.info("received SIG#{sig}, gracefully exiting")
+      return false
+    when :USR1
+      usr1_reopen("worker ")
+    end
+    alive
+  end
+end
diff --git a/lib/yahns/sigevent.rb b/lib/yahns/sigevent.rb
new file mode 100644
index 0000000..aa95f4b
--- /dev/null
+++ b/lib/yahns/sigevent.rb
@@ -0,0 +1,7 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+if SleepyPenguin.const_defined?(:EventFD)
+  require_relative 'sigevent_efd'
+else
+  require_relative 'sigevent_pipe'
+end
diff --git a/lib/yahns/sigevent_efd.rb b/lib/yahns/sigevent_efd.rb
new file mode 100644
index 0000000..8f10ad6
--- /dev/null
+++ b/lib/yahns/sigevent_efd.rb
@@ -0,0 +1,18 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::Sigevent < SleepyPenguin::EventFD # :nodoc:
+  include Kgio::DefaultWaiters
+  def self.new
+    super(0, SleepyPenguin::EventFD::CLOEXEC)
+  end
+
+  def sev_signal
+    incr(1) # eventfd_write
+  end
+
+  def yahns_step
+    value(true) # eventfd_read, we ignore this data
+    :wait_readable
+  end
+end
diff --git a/lib/yahns/sigevent_pipe.rb b/lib/yahns/sigevent_pipe.rb
new file mode 100644
index 0000000..6e1be53
--- /dev/null
+++ b/lib/yahns/sigevent_pipe.rb
@@ -0,0 +1,29 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::Sigevent # :nodoc:
+  attr_reader :to_io
+  def initialize
+    @to_io, @wr = Kgio::Pipe.new
+  end
+
+  def kgio_wait_readable
+    @to_io.kgio_wait_readable
+  end
+
+  def sev_signal
+    @wr.kgio_write(".")
+  end
+
+  def yahns_step
+    # 11 byte strings -> no malloc on YARV
+    while String === @to_io.kgio_tryread(11)
+    end
+    :wait_readable
+  end
+
+  def close
+    @to_io.close
+    @wr.close
+  end
+end
diff --git a/lib/yahns/socket_helper.rb b/lib/yahns/socket_helper.rb
new file mode 100644
index 0000000..61f2b0f
--- /dev/null
+++ b/lib/yahns/socket_helper.rb
@@ -0,0 +1,117 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+# this is only meant for Yahns::Server
+module Yahns::SocketHelper # :nodoc:
+  def set_server_sockopt(sock, opt)
+    opt = {backlog: 1024}.merge!(opt) if opt
+
+    TCPSocket === sock and sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1)
+    sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
+
+    if opt[:rcvbuf] || opt[:sndbuf]
+      log_buffer_sizes(sock, "before: ")
+      { SO_RCVBUF: :rcvbuf, SO_SNDBUF: :sndbuf }.each do |optname,cfgname|
+        val = opt[cfgname] and sock.setsockopt(:SOL_SOCKET, optname, val)
+      end
+      log_buffer_sizes(sock, " after: ")
+    end
+    sock.listen(opt[:backlog])
+  rescue => e
+    Yahns::Log.exception(@logger, "#{sock_name(sock)} #{opt.inspect}", e)
+  end
+
+  def log_buffer_sizes(sock, pfx = '')
+    rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
+    sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
+    @logger.info("#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}")
+  end
+
+  # creates a new server, socket. address may be a HOST:PORT or
+  # an absolute path to a UNIX socket.  address can even be a Socket
+  # object in which case it is immediately returned
+  def bind_listen(address, opt)
+    return address unless String === address
+    opt ||= {}
+
+    sock = if address[0] == ?/
+      if File.exist?(address)
+        if File.socket?(address)
+          begin
+            UNIXSocket.new(address).close
+            # fall through, try to bind(2) and fail with EADDRINUSE
+            # (or succeed from a small race condition we can't sanely avoid).
+          rescue Errno::ECONNREFUSED
+            @logger.info "unlinking existing socket=#{address}"
+            File.unlink(address)
+          end
+        else
+          raise ArgumentError,
+                "socket=#{address} specified but it is not a socket!"
+        end
+      end
+      old_umask = File.umask(opt[:umask] || 0)
+      begin
+        Kgio::UNIXServer.new(address)
+      ensure
+        File.umask(old_umask)
+      end
+    elsif /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ =~ address
+      new_ipv6_server($1, $2.to_i, opt)
+    elsif /\A(\d+\.\d+\.\d+\.\d+):(\d+)\z/ =~ address
+      Kgio::TCPServer.new($1, $2.to_i)
+    else
+      raise ArgumentError, "Don't know how to bind: #{address}"
+    end
+    set_server_sockopt(sock, opt)
+    sock
+  end
+
+  def new_ipv6_server(addr, port, opt)
+    opt.key?(:ipv6only) or return Kgio::TCPServer.new(addr, port)
+    sock = Socket.new(:AF_INET6, :SOCK_STREAM, 0)
+    sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0)
+    sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
+    sock.bind(Socket.pack_sockaddr_in(port, addr))
+    sock.autoclose = false
+    Kgio::TCPServer.for_fd(sock.fileno)
+  end
+
+  # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
+  def tcp_name(sock)
+    port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
+    /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
+  end
+
+  # Returns the configuration name of a socket as a string.  sock may
+  # be a string value, in which case it is returned as-is
+  # Warning: TCP sockets may not always return the name given to it.
+  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)
+      rescue ArgumentError
+        Socket.unpack_sockaddr_un(sock.getsockname)
+      end
+    else
+      raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}"
+    end
+  end
+
+  # casts a given Socket to be a TCPServer or UNIXServer
+  def server_cast(sock)
+    sock.autoclose = false
+    begin
+      Socket.unpack_sockaddr_in(sock.getsockname)
+      Kgio::TCPServer.for_fd(sock.fileno)
+    rescue ArgumentError
+      Kgio::UNIXServer.for_fd(sock.fileno)
+    end
+  end
+end
diff --git a/lib/yahns/stream_file.rb b/lib/yahns/stream_file.rb
new file mode 100644
index 0000000..eba9632
--- /dev/null
+++ b/lib/yahns/stream_file.rb
@@ -0,0 +1,34 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'wbuf_common'
+
+class Yahns::StreamFile # :nodoc:
+  include Yahns::WbufCommon
+
+  def initialize(body, persist, offset, count)
+    if body.respond_to?(:to_io)
+      @tmpio = body.to_io
+    else
+      path = body.to_path
+      if path =~ %r{\A/dev/fd/(\d+)\z}
+        @tmpio = IO.for_fd($1.to_i)
+        @tmpio.autoclose = false
+      else
+        @tmpio = File.open(path)
+      end
+    end
+    @sf_offset = offset
+    @sf_count = count || @tmpio.stat.size
+    @wbuf_persist = persist # whether or not we keep the connection alive
+    @body = body
+  end
+
+  # called by last wbuf_flush
+  def wbuf_close(client)
+    if File === @tmpio && @tmpio != @body
+      @tmpio.close
+    end
+    wbuf_close_common(client)
+  end
+end
diff --git a/lib/yahns/stream_input.rb b/lib/yahns/stream_input.rb
new file mode 100644
index 0000000..f0a43b3
--- /dev/null
+++ b/lib/yahns/stream_input.rb
@@ -0,0 +1,150 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
+
+# When processing uploads, Yahns may expose a StreamInput object under
+# "rack.input" of the (future) Rack (2.x) environment.
+class Yahns::StreamInput # :nodoc:
+  # Initializes a new StreamInput object.  You normally do not have to call
+  # this unless you are writing an HTTP server.
+  def initialize(client, request)
+    @chunked = request.content_length.nil?
+    @client = client
+    @parser = request
+    @buf = request.buf
+    @rbuf = ''
+    @bytes_read = 0
+    filter_body(@rbuf, @buf) unless @buf.empty?
+  end
+
+  # :call-seq:
+  #   ios.read([length [, buffer ]]) => string, buffer, or nil
+  #
+  # Reads at most length bytes from the I/O stream, or to the end of
+  # file if length is omitted or is nil. length must be a non-negative
+  # integer or nil. If the optional buffer argument is present, it
+  # must reference a String, which will receive the data.
+  #
+  # At end of file, it returns nil or '' depend on length.
+  # ios.read() and ios.read(nil) returns ''.
+  # ios.read(length [, buffer]) returns nil.
+  #
+  # If the Content-Length of the HTTP request is known (as is the common
+  # case for POST requests), then ios.read(length [, buffer]) will block
+  # until the specified length is read (or it is the last chunk).
+  # Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
+  # ios.read(length [, buffer]) will return immediately if there is
+  # any data and only block when nothing is available (providing
+  # IO#readpartial semantics).
+  def read(length = nil, rv = '')
+    if length
+      if length <= @rbuf.size
+        length < 0 and raise ArgumentError, "negative length #{length} given"
+        rv.replace(@rbuf.slice!(0, length))
+      else
+        to_read = length - @rbuf.size
+        rv.replace(@rbuf.slice!(0, @rbuf.size))
+        until to_read == 0 || eof? || (rv.size > 0 && @chunked)
+          @client.kgio_read(to_read, @buf) or eof!
+          filter_body(@rbuf, @buf)
+          rv << @rbuf
+          to_read -= @rbuf.size
+        end
+        @rbuf.replace('')
+      end
+      rv = nil if rv.empty? && length != 0
+    else
+      read_all(rv)
+    end
+    rv
+  end
+
+  def __rsize
+    @client.class.client_body_buffer_size
+  end
+
+  # :call-seq:
+  #   ios.gets   => string or nil
+  #
+  # Reads the next ``line'' from the I/O stream; lines are separated
+  # by the global record separator ($/, typically "\n"). A global
+  # record separator of nil reads the entire unread contents of ios.
+  # Returns nil if called at the end of file.
+  # This takes zero arguments for strict Rack::Lint compatibility,
+  # unlike IO#gets.
+  def gets
+    sep = $/
+    if sep.nil?
+      read_all(rv = '')
+      return rv.empty? ? nil : rv
+    end
+    re = /\A(.*?#{Regexp.escape(sep)})/
+    rsize = __rsize
+    begin
+      @rbuf.sub!(re, '') and return $1
+      return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
+      @client.kgio_read(rsize, @buf) or eof!
+      filter_body(once = '', @buf)
+      @rbuf << once
+    end while true
+  end
+
+  # :call-seq:
+  #   ios.each { |line| block }  => ios
+  #
+  # Executes the block for every ``line'' in *ios*, where lines are
+  # separated by the global record separator ($/, typically "\n").
+  def each
+    while line = gets
+      yield line
+    end
+
+    self # Rack does not specify what the return value is here
+  end
+
+  def eof?
+    if @parser.body_eof?
+      rsize = __rsize
+      while @chunked && ! @parser.parse
+        once = @client.kgio_read(rsize) or eof!
+        @buf << once
+      end
+      @client = nil
+      true
+    else
+      false
+    end
+  end
+
+  def filter_body(dst, src)
+    rv = @parser.filter_body(dst, src)
+    @bytes_read += dst.size
+    rv
+  end
+
+  def read_all(dst)
+    dst.replace(@rbuf)
+    @client or return
+    rsize = @client.class.client_body_buffer_size
+    until eof?
+      @client.kgio_read(rsize, @buf) or eof!
+      filter_body(@rbuf, @buf)
+      dst << @rbuf
+    end
+  ensure
+    @rbuf.replace('')
+  end
+
+  def eof!
+    # in case client only did a premature shutdown(SHUT_WR)
+    # we do support clients that shutdown(SHUT_WR) after the
+    # _entire_ request has been sent, and those will not have
+    # raised EOFError on us.
+    @client.shutdown if @client
+  ensure
+    raise Yahns::ClientShutdown, "bytes_read=#{@bytes_read}", []
+  end
+
+  def discard # return nil
+  end
+end
diff --git a/lib/yahns/tee_input.rb b/lib/yahns/tee_input.rb
new file mode 100644
index 0000000..0d91a89
--- /dev/null
+++ b/lib/yahns/tee_input.rb
@@ -0,0 +1,114 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
+
+# acts like tee(1) on an input input to provide a input-like stream
+# while providing rewindable semantics through a File/StringIO backing
+# store.  On the first pass, the input is only read on demand so your
+# Rack application can use input notification (upload progress and
+# like).  This should fully conform to the Rack::Lint::InputWrapper
+# specification on the public API.  This class is intended to be a
+# strict interpretation of Rack::Lint::InputWrapper functionality and
+# will not support any deviations from it.
+#
+# When processing uploads, Yahns exposes a TeeInput object under
+# "rack.input" of the Rack environment.
+class Yahns::TeeInput < Yahns::StreamInput # :nodoc:
+  # Initializes a new TeeInput object.  You normally do not have to call
+  # this unless you are writing an HTTP server.
+  def initialize(client, request)
+    @len = request.content_length
+    super
+    @tmp = client.class.tmpio_for(@len)
+  end
+
+  # :call-seq:
+  #   ios.size  => Integer
+  #
+  # Returns the size of the input.  For requests with a Content-Length
+  # header value, this will not read data off the socket and just return
+  # the value of the Content-Length header as an Integer.
+  #
+  # For Transfer-Encoding:chunked requests, this requires consuming
+  # all of the input stream before returning since there's no other
+  # way to determine the size of the request body beforehand.
+  #
+  # This method is no longer part of the Rack specification as of
+  # Rack 1.2, so its use is not recommended.  This method only exists
+  # for compatibility with Rack applications designed for Rack 1.1 and
+  # earlier.  Most applications should only need to call +read+ with a
+  # specified +length+ in a loop until it returns +nil+.
+  def size
+    @len and return @len
+    pos = @tmp.pos
+    consume!
+    @tmp.pos = pos
+    @len = @tmp.size
+  end
+
+  # :call-seq:
+  #   ios.read([length [, buffer ]]) => string, buffer, or nil
+  #
+  # Reads at most length bytes from the I/O stream, or to the end of
+  # file if length is omitted or is nil. length must be a non-negative
+  # integer or nil. If the optional buffer argument is present, it
+  # must reference a String, which will receive the data.
+  #
+  # At end of file, it returns nil or "" depend on length.
+  # ios.read() and ios.read(nil) returns "".
+  # ios.read(length [, buffer]) returns nil.
+  #
+  # If the Content-Length of the HTTP request is known (as is the common
+  # case for POST requests), then ios.read(length [, buffer]) will block
+  # until the specified length is read (or it is the last chunk).
+  # Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
+  # ios.read(length [, buffer]) will return immediately if there is
+  # any data and only block when nothing is available (providing
+  # IO#readpartial semantics).
+  def read(*args)
+    @client ? tee(super) : @tmp.read(*args)
+  end
+
+  # :call-seq:
+  #   ios.gets   => string or nil
+  #
+  # Reads the next ``line'' from the I/O stream; lines are separated
+  # by the global record separator ($/, typically "\n"). A global
+  # record separator of nil reads the entire unread contents of ios.
+  # Returns nil if called at the end of file.
+  # This takes zero arguments for strict Rack::Lint compatibility,
+  # unlike IO#gets.
+  def gets
+    @client ? tee(super) : @tmp.gets
+  end
+
+  # :call-seq:
+  #   ios.rewind    => 0
+  #
+  # Positions the *ios* pointer to the beginning of input, returns
+  # the offset (zero) of the +ios+ pointer.  Subsequent reads will
+  # start from the beginning of the previously-buffered input.
+  def rewind
+    return 0 if 0 == @tmp.size
+    consume! if @client
+    @tmp.rewind # Rack does not specify what the return value is here
+  end
+
+  # consumes the stream of the socket
+  def consume!
+    junk = ""
+    rsize = __rsize
+    nil while read(rsize, junk)
+  end
+
+  def tee(buffer)
+    if buffer && buffer.size > 0
+      @tmp.write(buffer)
+    end
+    buffer
+  end
+
+  def discard
+    @tmp = @tmp.close
+  end
+end
diff --git a/lib/yahns/tiny_input.rb b/lib/yahns/tiny_input.rb
new file mode 100644
index 0000000..55bdd03
--- /dev/null
+++ b/lib/yahns/tiny_input.rb
@@ -0,0 +1,7 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::TinyInput < StringIO # :nodoc:
+  def discard # just returns nil
+  end
+end
diff --git a/lib/yahns/tmpio.rb b/lib/yahns/tmpio.rb
new file mode 100644
index 0000000..60751c0
--- /dev/null
+++ b/lib/yahns/tmpio.rb
@@ -0,0 +1,27 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
+require 'tmpdir'
+
+# some versions of Ruby had a broken Tempfile which didn't work
+# well with unlinked files.  This one is much shorter, easier
+# to understand, and slightly faster (no delegation).
+class Yahns::TmpIO < File # :nodoc:
+
+  # creates and returns a new File object.  The File is unlinked
+  # immediately, switched to binary mode, and userspace output
+  # buffering is disabled
+  def self.new
+    fp = begin
+      super("#{Dir.tmpdir}/#{rand}", RDWR|CREAT|EXCL, 0600)
+    rescue Errno::EEXIST
+      retry
+    end
+    unlink(fp.path)
+    fp.binmode
+    fp.sync = true
+    fp
+  end
+
+  alias discard close
+end
diff --git a/lib/yahns/wbuf.rb b/lib/yahns/wbuf.rb
new file mode 100644
index 0000000..4828056
--- /dev/null
+++ b/lib/yahns/wbuf.rb
@@ -0,0 +1,36 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'wbuf_common'
+
+class Yahns::Wbuf # :nodoc:
+  include Yahns::WbufCommon
+
+  def initialize(body, persist)
+    @tmpio = Yahns::TmpIO.new
+    @sf_offset = @sf_count = 0
+    @wbuf_persist = persist # whether or not we keep the connection alive
+    @body = body
+  end
+
+  def wbuf_write(client, buf)
+    @sf_count += @tmpio.write(buf)
+    case rv = client.trysendfile(@tmpio, @sf_offset, @sf_count)
+    when Integer
+      @sf_count -= rv
+      @sf_offset += rv
+    when :wait_writable, :wait_readable
+      return rv
+    else
+      raise "BUG: #{rv.nil ? "EOF" : rv.inspect} on tmpio " \
+            "sf_offset=#@sf_offset sf_count=#@sf_count"
+    end while @sf_count > 0
+    nil
+  end
+
+  # called by last wbuf_flush
+  def wbuf_close(client)
+    @tmpio = @tmpio.close
+    wbuf_close_common(client)
+  end
+end
diff --git a/lib/yahns/wbuf_common.rb b/lib/yahns/wbuf_common.rb
new file mode 100644
index 0000000..e621311
--- /dev/null
+++ b/lib/yahns/wbuf_common.rb
@@ -0,0 +1,32 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al.
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'sendfile'
+module Yahns::WbufCommon # :nodoc:
+  # returns nil on success, :wait_*able when blocked
+  # currently, we rely on each thread having exclusive access to the
+  # client socket, so this is never called concurrently with wbuf_write
+  def wbuf_flush(client)
+    case rv = client.trysendfile(@tmpio, @sf_offset, @sf_count)
+    when Integer
+      return wbuf_close(client) if (@sf_count -= rv) == 0 # all sent!
+
+      @sf_offset += rv # keep going otherwise
+    when :wait_writable, :wait_readable
+      return rv
+    else
+      raise "BUG: #{rv.nil? ? "EOF" : rv.inspect} on tmpio=#{@tmpio.inspect} " \
+            "sf_offset=#@sf_offset sf_count=#@sf_count"
+    end while true
+  end
+
+  def wbuf_close_common(client)
+    @body.close if @body.respond_to?(:close)
+    if @wbuf_persist.respond_to?(:call) # hijack
+      @wbuf_persist.call(client)
+      :delete
+    else
+      @wbuf_persist # true or false or Yahns::StreamFile
+    end
+  end
+end
diff --git a/lib/yahns/worker.rb b/lib/yahns/worker.rb
new file mode 100644
index 0000000..980f7bd
--- /dev/null
+++ b/lib/yahns/worker.rb
@@ -0,0 +1,58 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+class Yahns::Worker # :nodoc:
+  attr_accessor :nr
+  attr_reader :to_io
+
+  def initialize(nr)
+    @nr = nr
+    @to_io, @wr = Kgio::Pipe.new
+  end
+
+  def atfork_child
+    @wr = @wr.close # nil @wr to save space in worker process
+  end
+
+  def atfork_parent
+    @to_io = @to_io.close
+    self
+  end
+
+  # used in the worker process.
+  # This causes the worker to gracefully exit if the master
+  # dies unexpectedly.
+  def yahns_step
+    @to_io.kgio_tryread(11) == nil and Process.kill(:QUIT, $$)
+    :wait_readable
+  end
+
+  # worker objects may be compared to just plain Integers
+  def ==(other_nr) # :nodoc:
+    @nr == other_nr
+  end
+
+  # Changes the worker process to the specified +user+ and +group+
+  # This is only intended to be called from within the worker
+  # process from the +after_fork+ hook.  This should be called in
+  # the +after_fork+ hook after any privileged functions need to be
+  # run (e.g. to set per-worker CPU affinity, niceness, etc)
+  #
+  # Any and all errors raised within this method will be propagated
+  # directly back to the caller (usually the +after_fork+ hook.
+  # These errors commonly include ArgumentError for specifying an
+  # invalid user/group and Errno::EPERM for insufficient privileges
+  def user(user, group = nil)
+    # we do not protect the caller, checking Process.euid == 0 is
+    # insufficient because modern systems have fine-grained
+    # capabilities.  Let the caller handle any and all errors.
+    uid = Etc.getpwnam(user).uid
+    gid = Etc.getgrnam(group).gid if group
+    Yahns::Log.chown_all(uid, gid)
+    if gid && Process.egid != gid
+      Process.initgroups(user, gid)
+      Process::GID.change_privilege(gid)
+    end
+    Process.euid != uid and Process::UID.change_privilege(uid)
+  end
+end
diff --git a/test/covshow.rb b/test/covshow.rb
new file mode 100644
index 0000000..2fd48c6
--- /dev/null
+++ b/test/covshow.rb
@@ -0,0 +1,29 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# this works with the __covmerge method in test/helper.rb
+# run this file after all tests are run
+
+# load the merged dump data
+res = Marshal.load(IO.binread("coverage.dump"))
+
+# Dirty little text formatter.  I tried simplecov but the default
+# HTML+JS is unusable without a GUI (I hate GUIs :P) and it would've
+# taken me longer to search the Internets to find a plain-text
+# formatter I like...
+res.keys.sort.each do |filename|
+  cov = res[filename]
+  puts "==> #{filename} <=="
+  File.readlines(filename).each_with_index do |line, i|
+    n = cov[i]
+    if n == 0 # BAD
+      print("  *** 0 #{line}")
+    elsif n
+      printf("% 7u %s", n, line)
+    elsif line =~ /\S/ # probably a line with just "end" in it
+      print("        #{line}")
+    else # blank line
+      print "\n" # don't output trailing whitespace on blank lines
+    end
+  end
+end
diff --git a/test/helper.rb b/test/helper.rb
new file mode 100644
index 0000000..ab9a04f
--- /dev/null
+++ b/test/helper.rb
@@ -0,0 +1,115 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+$stdout.sync = $stderr.sync = Thread.abort_on_exception = true
+require 'thread'
+
+# Global Test Lock, to protect:
+#   Process.wait*, Dir.chdir, ENV, trap, require, etc...
+GTL = Mutex.new
+
+# fork-aware coverage data gatherer, see also test/covshow.rb
+if ENV["COVERAGE"]
+  require "coverage"
+  COVMATCH = %r{/lib/yahns\b.*rb\z}
+  COVTMP = File.open("coverage.dump", IO::CREAT|IO::RDWR)
+  COVTMP.binmode
+  COVTMP.sync = true
+
+  def __covmerge
+    res = Coverage.result
+
+    # we own this file (at least until somebody tries to use NFS :x)
+    COVTMP.flock(File::LOCK_EX)
+
+    COVTMP.rewind
+    prev = COVTMP.read
+    prev = prev.empty? ? {} : Marshal.load(prev)
+    res.each do |filename, counts|
+      # filter out stuff that's not in our project
+      COVMATCH =~ filename or next
+
+      merge = prev[filename] || []
+      merge = merge
+      counts.each_with_index do |count, i|
+        count or next
+        merge[i] = (merge[i] || 0) + count
+      end
+      prev[filename] = merge
+    end
+    COVTMP.rewind
+    COVTMP.truncate(0)
+    COVTMP.write(Marshal.dump(prev))
+    COVTMP.flock(File::LOCK_UN)
+  end
+
+  Coverage.start
+  at_exit { at_exit { __covmerge } }
+end
+
+gem 'minitest'
+require 'minitest/autorun'
+require "tempfile"
+
+Testcase = begin
+  Minitest::Test # minitest 5
+rescue NameError
+  Minitest::Unit::TestCase # minitest 4
+end
+
+FIFOS = []
+def tmpfifo
+  tmp = Tempfile.new(%w(yahns-test .fifo))
+  path = tmp.path
+  tmp.close!
+  assert system(*%W(mkfifo #{path})), "mkfifo #{path}"
+
+  GTL.synchronize do
+    if FIFOS.empty?
+      at_exit do
+        FIFOS.each { |(pid,_path)| File.unlink(_path) if $$ == pid }
+      end
+    end
+    FIFOS << [ $$, path ]
+  end
+  path
+end
+
+require 'tmpdir'
+class Dir
+  require 'fileutils'
+  def Dir.mktmpdir
+    begin
+      d = "#{Dir.tmpdir}/#$$.#{rand}"
+      Dir.mkdir(d)
+    rescue Errno::EEXIST
+    end while true
+    begin
+      yield d
+    ensure
+      FileUtils.remove_entry(d)
+    end
+  end
+end unless Dir.respond_to?(:mktmpdir)
+
+def tmpfile(*args)
+  tmp = Tempfile.new(*args)
+  tmp.sync = true
+  tmp.binmode
+  tmp
+end
+
+require 'io/wait'
+# needed for Rubinius 2.0.0, we only use IO#nread in tests
+class IO
+  # this ignores buffers
+  def nread
+    buf = "\0" * 8
+    ioctl(0x541B, buf)
+    buf.unpack("l_")[0]
+  end
+end if ! IO.method_defined?(:nread) && RUBY_PLATFORM =~ /linux/
+
+require 'yahns'
+
+# needed for parallel (MT) tests)
+require 'yahns/rack'
diff --git a/test/server_helper.rb b/test/server_helper.rb
new file mode 100644
index 0000000..78d2f94
--- /dev/null
+++ b/test/server_helper.rb
@@ -0,0 +1,64 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'timeout'
+require 'socket'
+require 'net/http'
+
+module ServerHelper
+  def check_err(err = @err)
+    err = File.open(err.path, "r") if err.respond_to?(:path)
+    err.rewind
+    lines = err.readlines.delete_if { |l| l =~ /INFO/ }
+    assert lines.empty?, lines.join("\n")
+    err.close
+  end
+
+  def poke_until_dead(pid)
+    Timeout.timeout(10) do
+      begin
+        Process.kill(0, pid)
+        sleep(0.01)
+      rescue Errno::ESRCH
+        break
+      end while true
+    end
+    assert_raises(Errno::ESRCH) { Process.kill(0, pid) }
+  end
+
+  def quit_wait(pid)
+    pid or return
+    Process.kill(:QUIT, pid)
+    _, status = Timeout.timeout(10) { Process.waitpid2(pid) }
+    assert status.success?, status.inspect
+  rescue Timeout::Error
+    if RUBY_PLATFORM =~ /linux/
+      system("lsof -p #{pid}")
+      warn "#{pid} failed to die, waiting for user to inspect"
+      sleep
+    end
+    raise
+  end
+
+  def get_tcp_client(host, port, tries = 500)
+    begin
+      c = TCPSocket.new(host, port)
+      return c
+    rescue Errno::ECONNREFUSED
+      raise if tries < 0
+      tries -= 1
+    end while sleep(0.01)
+  end
+
+  def server_helper_teardown
+    @srv.close unless @srv.closed?
+    @ru.close! if @ru
+    check_err
+  end
+
+  def server_helper_setup
+    @srv = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0)
+    @err = tmpfile(%w(srv .err))
+    @ru = nil
+  end
+end
diff --git a/test/test_bin.rb b/test/test_bin.rb
new file mode 100644
index 0000000..4a47a93
--- /dev/null
+++ b/test/test_bin.rb
@@ -0,0 +1,98 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'server_helper'
+class TestBin < Testcase
+  parallelize_me!
+  include ServerHelper
+  alias teardown server_helper_teardown
+
+  def setup
+    server_helper_setup
+    @cmd = %W(ruby -I lib bin/yahns)
+  end
+
+  def test_bin_daemon_noworker_inherit
+    bin_daemon(false, true)
+  end
+
+  def test_bin_daemon_worker_inherit
+    bin_daemon(true, true)
+  end
+
+  def test_bin_daemon_noworker_bind
+    bin_daemon(false, false)
+  end
+
+  def test_bin_daemon_worker_bind
+    bin_daemon(true, false)
+  end
+
+  def bin_daemon(worker, inherit)
+    @srv.close unless inherit
+    @pid = tmpfile(%w(test_bin_daemon .pid))
+    @ru = tmpfile(%w(test_bin_daemon .ru))
+    @ru.write("require 'rack/lobster'; run Rack::Lobster.new\n")
+    cfg = tmpfile(%w(test_bin_daemon_conf .rb))
+    pid = tmpfile(%w(daemon .pid))
+    cfg.puts "pid '#{@pid.path}'"
+    cfg.puts "stderr_path '#{@err.path}'"
+    cfg.puts "worker_processes 1" if worker
+    cfg.puts "app(:rack, '#{@ru.path}', preload: false) do"
+    cfg.puts "  listen ENV['YAHNS_TEST_LISTEN']"
+    cfg.puts "end"
+    @cmd.concat(%W(-D -c #{cfg.path}))
+    addr = IO.pipe
+    pid = fork do
+      if inherit
+        @cmd << { @srv.fileno => @srv }
+        ENV["YAHNS_FD"] = @srv.fileno.to_s
+      else
+        @srv = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0)
+      end
+      host, port = @srv.addr[3], @srv.addr[1]
+      listen = ENV["YAHNS_TEST_LISTEN"] = "#{host}:#{port}"
+      addr[1].write(listen)
+      addr[1].close
+      addr[0].close
+      exec(*@cmd)
+    end
+    addr[1].close
+    listen = Timeout.timeout(10) { addr[0].read }
+    addr[0].close
+    host, port = listen.split(/:/, 2)
+    port = port.to_i
+    assert_operator port, :>, 0
+
+    unless inherit
+      # daemon_pipe guarantees socket will be usable after this:
+      Timeout.timeout(10) do # Ruby startup is slow!
+        _, status = Process.waitpid2(pid)
+        assert status.success?, status.inspect
+      end
+    end
+
+    Net::HTTP.start(host, port) do |http|
+      req = Net::HTTP::Get.new("/")
+      res = http.request(req)
+      assert_equal 200, res.code.to_i
+      assert_equal "keep-alive", res["Connection"]
+    end
+  rescue => e
+    warn "#{e.message} (#{e.class})"
+    e.backtrace.each { |l| warn "#{l}" }
+    raise
+  ensure
+    cfg.close! if cfg
+    pid = File.read(@pid.path)
+    pid = pid.to_i
+    assert_operator pid, :>, 0
+    Process.kill(:QUIT, pid)
+    if inherit
+      _, status = Timeout.timeout(10) { Process.waitpid2(pid) }
+      assert status.success?, status.inspect
+    else
+      poke_until_dead pid
+    end
+    @pid.close! if @pid
+  end
+end
diff --git a/test/test_config.rb b/test/test_config.rb
new file mode 100644
index 0000000..2afcecb
--- /dev/null
+++ b/test/test_config.rb
@@ -0,0 +1,56 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'rack/lobster'
+
+class TestConfig < Testcase
+  parallelize_me!
+
+  def test_initialize
+    cfg = Yahns::Config.new
+    assert_instance_of Yahns::Config, cfg
+  end
+
+  def test_multi_conf_example
+    tmpdir = Dir.mktmpdir
+
+    # modify the example config file for testing
+    path = "examples/yahns_multi.conf.rb"
+    cfgs = File.read(path)
+    cfgs.gsub!(%r{/path/to/}, "#{tmpdir}/")
+    conf = File.open("#{tmpdir}/yahns_multi.conf.rb", "w")
+    conf.sync = true
+    conf.write(cfgs)
+    File.open("#{tmpdir}/another.ru", "w") do |fp|
+      fp.puts("run Rack::Lobster.new\n")
+    end
+    FileUtils.mkpath("#{tmpdir}/another")
+
+    cfg = GTL.synchronize { Yahns::Config.new(conf.path) }
+    assert_instance_of Yahns::Config, cfg
+  ensure
+    FileUtils.rm_rf(tmpdir) if tmpdir
+  end
+
+  def test_rack_basic_conf_example
+    tmpdir = Dir.mktmpdir
+
+    # modify the example config file for testing
+    path = "examples/yahns_rack_basic.conf.rb"
+    cfgs = File.read(path)
+    cfgs.gsub!(%r{/path/to/}, "#{tmpdir}/")
+    Dir.mkdir("#{tmpdir}/my_app")
+    Dir.mkdir("#{tmpdir}/my_logs")
+    Dir.mkdir("#{tmpdir}/my_pids")
+    conf = File.open("#{tmpdir}/yahns_rack_basic.conf.rb", "w")
+    conf.sync = true
+    conf.write(cfgs)
+    File.open("#{tmpdir}/my_app/config.ru", "w") do |fp|
+      fp.puts("run Rack::Lobster.new\n")
+    end
+    cfg = GTL.synchronize { Yahns::Config.new(conf.path) }
+    assert_instance_of Yahns::Config, cfg
+  ensure
+    FileUtils.rm_rf(tmpdir) if tmpdir
+  end
+end
diff --git a/test/test_output_buffering.rb b/test/test_output_buffering.rb
new file mode 100644
index 0000000..6fe22ba
--- /dev/null
+++ b/test/test_output_buffering.rb
@@ -0,0 +1,288 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'server_helper'
+require 'digest/md5'
+require 'rack/file'
+
+class TestOutputBuffering < Testcase
+  parallelize_me!
+  include ServerHelper
+  alias setup server_helper_setup
+  alias teardown server_helper_teardown
+
+  GPLv3 = File.read("COPYING")
+  RAND = IO.binread("/dev/urandom", 666) * 119
+  dig = Digest::MD5.new
+  NR = 1337
+  MD5 = Thread.new do
+    NR.times { dig << RAND }
+    dig.hexdigest
+  end
+
+  class BigBody
+    def each
+      NR.times { yield RAND }
+    end
+  end
+
+  def test_output_buffer_false_curl
+    output_buffer(false, :curl)
+  end
+
+  def test_output_buffer_false_http09
+    output_buffer(false, :http09)
+  end
+
+  def test_output_buffer_true_curl
+    output_buffer(true, :curl)
+  end
+
+  def test_output_buffer_true_http09
+    output_buffer(true, :http09)
+  end
+
+  def output_buffer(btype, check_type, delay = 4)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    len = (RAND.size * NR).to_s
+    cfg.instance_eval do
+      ru = lambda do |e|
+        [ 200, {'Content-Length'=>len}, BigBody.new ]
+      end
+      GTL.synchronize do
+        app(:rack, ru) do
+          listen "#{host}:#{port}"
+          output_buffering btype
+        end
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+
+    case check_type
+    when :curl
+      # curl is faster for piping gigantic wads of data than Net::HTTP
+      sh_sleep = delay ? "sleep #{delay} && " : ""
+      md5 = `curl -sSf http://#{host}:#{port}/ | (#{sh_sleep} md5sum)`
+      assert $?.success?, $?.inspect
+      (md5 =~ /([a-f0-9]{32})/i) or raise "bad md5: #{md5.inspect}"
+      md5 = $1
+      assert_equal MD5.value, md5
+    when :http09
+      # HTTP/0.9
+      c = TCPSocket.new(host, port)
+      c.write("GET /\r\n\r\n")
+      md5in = IO.pipe
+      md5out = IO.pipe
+      sleep(delay) if delay
+      md5pid = Process.spawn("md5sum", :in => md5in[0], :out => md5out[1])
+      md5in[0].close
+      md5out[1].close
+      assert_equal(NR * RAND.size, IO.copy_stream(c, md5in[1]))
+      c.close
+      md5in[1].close
+      _, status = Timeout.timeout(10) { Process.waitpid2(md5pid) }
+      assert status.success?, status.inspect
+      md5 = md5out[0].read
+      (md5 =~ /([a-f0-9]{32})/i) or raise "bad md5: #{md5.inspect}"
+      md5 = $1
+      assert_equal MD5.value, md5
+      md5out[0].close
+    else
+      raise "TESTBUG"
+    end
+  rescue => e
+    Yahns::Log.exception(Logger.new($stderr), "test", e)
+    raise
+  ensure
+    quit_wait(pid)
+  end
+
+  class BigHeader
+    A = "A" * 65536
+    def initialize(h)
+      @h = h
+    end
+    def each
+      NR.times do |n|
+        yield("X-#{n}", A)
+      end
+      @h.each { |k,v| yield(k,v) }
+    end
+  end
+
+  def test_big_header
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda do |e|
+        case e["PATH_INFO"]
+        when "/COPYING"
+          Rack::File.new(Dir.pwd).call(e)
+          gplv3 = File.open("COPYING")
+          def gplv3.each
+            raise "SHOULD NOT BE CALLED"
+          end
+          size = gplv3.stat.size
+          len = size.to_s
+          ranges = Rack::Utils.byte_ranges(e, size)
+          status = 200
+          h = { "Content-Type" => "text/plain", "Content-Length" => len }
+          if ranges && ranges.size == 1
+            status = 206
+            range = ranges[0]
+            h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
+            size = range.end - range.begin + 1
+            len.replace(size.to_s)
+          end
+          [ status , BigHeader.new(h), gplv3 ]
+        when "/"
+          h = { "Content-Type" => "text/plain", "Content-Length" => "4" }
+          [ 200, BigHeader.new(h), ["BIG\n"] ]
+        else
+          raise "WTF"
+        end
+      end
+      GTL.synchronize do
+        app(:rack, ru) do
+          listen "#{host}:#{port}"
+        end
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    threads = []
+
+    # start with just a big header
+    threads << Thread.new do
+      c = TCPSocket.new(host, port)
+      c.write "GET / HTTP/1.0\r\n\r\n"
+      begin
+        sleep 1
+      end while c.nread == 0
+      nr = 0
+      last = nil
+      c.each_line do |line|
+        case line
+        when %r{\AX-} then nr += 1
+        else
+          last = line
+        end
+      end
+      assert_equal NR, nr
+      assert_equal "BIG\n", last
+      c.close
+    end
+
+    threads << Thread.new do
+      c = TCPSocket.new(host, port)
+      c.write "GET /COPYING HTTP/1.0\r\n\r\n"
+      begin
+        sleep 1
+      end while c.nread == 0
+      nr = 0
+      c.each_line do |line|
+        case line
+        when %r{\AX-} then nr += 1
+        else
+          break if line == "\r\n"
+        end
+      end
+      assert_equal NR, nr
+      assert_equal GPLv3, c.read
+      c.close
+    end
+
+    threads << Thread.new do
+      c = TCPSocket.new(host, port)
+      c.write "GET /COPYING HTTP/1.0\r\nRange: bytes=5-46\r\n\r\n"
+      begin
+        sleep 1
+      end while c.nread == 0
+      nr = 0
+      c.each_line do |line|
+        case line
+        when %r{\AX-} then nr += 1
+        else
+          break if line == "\r\n"
+        end
+      end
+      assert_equal NR, nr
+      assert_equal GPLv3[5..46], c.read
+      c.close
+    end
+    threads.each do |t|
+      assert_equal t, t.join(30)
+      assert_nil t.value
+    end
+  ensure
+    quit_wait(pid)
+  end
+
+  def test_client_timeout
+    err = @err
+    apperr = tmpfile(%w(app .err))
+    cfg = Yahns::Config.new
+    size = RAND.size * NR
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda do |e|
+        if e["PATH_INFO"] == "/bh"
+          h = { "Content-Type" => "text/plain", "Content-Length" => "4" }
+          [ 200, BigHeader.new(h), ["BIG\n"] ]
+        else
+          [ 200, {'Content-Length' => size.to_s }, BigBody.new ]
+        end
+      end
+      GTL.synchronize do
+        app(:rack, ru) do
+          listen "#{host}:#{port}"
+          output_buffering false
+          client_timeout 3
+          logger(Logger.new(apperr.path))
+        end
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    threads = []
+    threads << Thread.new do
+      c = get_tcp_client(host, port)
+      c.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
+      sleep(5) # wait for timeout
+      assert_operator c.nread, :>, 0
+      c
+    end
+
+    threads << Thread.new do
+      c = get_tcp_client(host, port)
+      c.write("GET /bh HTTP/1.1\r\nHost: example.com\r\n\r\n")
+      sleep(5) # wait for timeout
+      assert_operator c.nread, :>, 0
+      c
+    end
+    threads.each { |t| t.join(10) }
+    assert_operator size, :>, threads[0].value.read.size
+    assert_operator size, :>, threads[1].value.read.size
+    msg = File.readlines(apperr.path)
+    msg = msg.grep(/timeout on :wait_writable after 3s$/)
+    assert_equal 2, msg.size
+  ensure
+    quit_wait(pid)
+  end
+end if `which curl 2>/dev/null`.strip =~ /curl/ &&
+       `which md5sum 2>/dev/null`.strip =~ /md5sum/
diff --git a/test/test_queue.rb b/test/test_queue.rb
new file mode 100644
index 0000000..6d61aef
--- /dev/null
+++ b/test/test_queue.rb
@@ -0,0 +1,59 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'timeout'
+require 'stringio'
+
+class TestQueue < Testcase
+  parallelize_me!
+
+  def setup
+    @q = Yahns::Queue.new
+    @err = StringIO.new
+    @logger = Logger.new(@err)
+    @q.fdmap = @fdmap = Yahns::Fdmap.new(@logger, 0.5)
+    assert @q.close_on_exec?
+  end
+
+  def test_queue
+    r, w = IO.pipe
+    assert_equal 0, @fdmap.size
+    @q.queue_add(r, Yahns::Queue::QEV_RD)
+    assert_equal 1, @fdmap.size
+    def r.yahns_step
+      begin
+        case read_nonblock(11)
+        when "delete"
+          return :delete
+        end
+      rescue Errno::EAGAIN
+        return :wait_readable
+      rescue EOFError
+        return nil
+      end while true
+    end
+    w.write('.')
+    Timeout.timeout(10) do
+      Thread.pass until r.nread > 0
+      @q.spawn_worker_threads(@logger, 1, 1)
+      Thread.pass until r.nread == 0
+
+      w.write("delete")
+      Thread.pass until r.nread == 0
+      Thread.pass until @fdmap.size == 0
+
+      # should not raise
+      @q.queue_add(r, Yahns::Queue::QEV_RD)
+      assert_equal 1, @fdmap.size
+      w.close
+      Thread.pass until @fdmap.size == 0
+    end
+    assert r.closed?
+  ensure
+    [ r, w ].each { |io| io.close unless io.closed? }
+  end
+
+  def teardown
+    @q.close
+  end
+end
diff --git a/test/test_rack.rb b/test/test_rack.rb
new file mode 100644
index 0000000..bd0d5b5
--- /dev/null
+++ b/test/test_rack.rb
@@ -0,0 +1,26 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'rack/lobster'
+require 'yahns/rack'
+class TestRack < Testcase
+  parallelize_me!
+
+  def test_rack
+    tmp = tmpfile(%W(config .ru))
+    tmp.write "run Rack::Lobster.new\n"
+    rapp = GTL.synchronize { Yahns::Rack.new(tmp.path) }
+    assert_kind_of Rack::Lobster, GTL.synchronize { rapp.app_after_fork }
+    defaults = rapp.app_defaults
+    assert_kind_of Hash, defaults
+  end
+
+  def test_rack_preload
+    tmp = tmpfile(%W(config .ru))
+    tmp.write "run Rack::Lobster.new\n"
+    rapp = GTL.synchronize { Yahns::Rack.new(tmp.path, preload: true) }
+    assert_kind_of Rack::Lobster, rapp.instance_variable_get(:@app)
+    defaults = rapp.app_defaults
+    assert_kind_of Hash, defaults
+  end
+end
diff --git a/test/test_serve_static.rb b/test/test_serve_static.rb
new file mode 100644
index 0000000..b9856e9
--- /dev/null
+++ b/test/test_serve_static.rb
@@ -0,0 +1,42 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'server_helper'
+require 'rack/file'
+
+class TestServeStatic < Testcase
+  parallelize_me!
+  include ServerHelper
+  alias setup server_helper_setup
+  alias teardown server_helper_teardown
+
+  def test_serve_static
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      GTL.synchronize do
+        app(:rack, Rack::File.new(Dir.pwd)) { listen "#{host}:#{port}" }
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    gplv3 = File.read("COPYING")
+    Net::HTTP.start(host, port) do |http|
+      res = http.request(Net::HTTP::Get.new("/COPYING"))
+      assert_equal gplv3, res.body
+
+      req = Net::HTTP::Get.new("/COPYING", "Range" => "bytes=5-46")
+      res = http.request(req)
+      assert_equal gplv3[5..46], res.body
+    end
+  rescue => e
+    Yahns::Log.exception(Logger.new($stderr), "test", e)
+    raise
+  ensure
+    quit_wait(pid)
+  end
+end
diff --git a/test/test_server.rb b/test/test_server.rb
new file mode 100644
index 0000000..5c86268
--- /dev/null
+++ b/test/test_server.rb
@@ -0,0 +1,382 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'server_helper'
+
+class TestServer < Testcase
+  parallelize_me!
+  include ServerHelper
+
+  alias setup server_helper_setup
+  alias teardown server_helper_teardown
+
+  def test_single_process
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda { |_| [ 200, {'Content-Length'=>'2'}, ['HI'] ] }
+      GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    run_client(host, port) { |res| assert_equal "HI", res.body }
+    c = get_tcp_client(host, port)
+
+    # test pipelining
+    r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
+    c.write(r + r)
+    buf = ""
+    Timeout.timeout(10) do
+      until buf =~ /HI.+HI/m
+        buf << c.readpartial(4096)
+      end
+    end
+
+    # trickle pipelining
+    c.write(r + "GET ")
+    buf = ""
+    Timeout.timeout(10) do
+      until buf =~ /HI\z/
+        buf << c.readpartial(4096)
+      end
+    end
+    c.write("/ HTTP/1.1\r\nHost: example.com\r\n\r\n")
+    Timeout.timeout(10) do
+      until buf =~ /HI.+HI/m
+        buf << c.readpartial(4096)
+      end
+    end
+  rescue => e
+    Yahns::Log.exception(Logger.new($stderr), "test", e)
+    raise
+  ensure
+    c.close if c
+    quit_wait(pid)
+  end
+
+  def test_input_body_true; input_body(true); end
+  def test_input_body_false; input_body(false); end
+  def test_input_body_lazy; input_body(:lazy); end
+
+  def input_body(btype)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda {|e|[ 200, {'Content-Length'=>'2'},[e["rack.input"].read]]}
+      GTL.synchronize do
+        app(:rack, ru) do
+          listen "#{host}:#{port}"
+          input_buffering btype
+        end
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    c = get_tcp_client(host, port)
+    buf = "PUT / HTTP/1.0\r\nContent-Length: 2\r\n\r\nHI"
+    c.write(buf)
+    IO.select([c], nil, nil, 5)
+    rv = c.read(666)
+    head, body = rv.split(/\r\n\r\n/)
+    assert_match(%r{^Content-Length: 2\r\n}, head)
+    assert_equal "HI", body, "#{rv.inspect} - #{btype.inspect}"
+    c.close
+
+    # pipelined oneshot
+    buf = "PUT / HTTP/1.1\r\nContent-Length: 2\r\n\r\nHI"
+    c = get_tcp_client(host, port)
+    c.write(buf + buf)
+    buf = ""
+    Timeout.timeout(10) do
+      until buf =~ /HI.+HI/m
+        buf << c.readpartial(4096)
+      end
+    end
+    assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date"
+    rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHI)/m, "")
+    first = $1
+    assert rv
+    assert_equal first, buf
+
+    # pipelined trickle
+    buf = "PUT / HTTP/1.1\r\nContent-Length: 5\r\n\r\nHIBYE"
+    (buf + buf).each_byte do |b|
+      c.write(b.chr)
+      sleep(0.01) if b.chr == ":"
+      Thread.pass
+    end
+    buf = ""
+    Timeout.timeout(10) do
+      until buf =~ /HIBYE.+HIBYE/m
+        buf << c.readpartial(4096)
+      end
+    end
+    assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date"
+    rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHIBYE)/m, "")
+    first = $1
+    assert rv
+    assert_equal first, buf
+  rescue => e
+    Yahns::Log.exception(Logger.new($stderr), "test", e)
+    raise
+  ensure
+    c.close if c
+    quit_wait(pid)
+  end
+
+  def test_trailer_true; trailer(true); end
+  def test_trailer_false; trailer(false); end
+  def test_trailer_lazy; trailer(:lazy); end
+  def test_slow_trailer_true; trailer(true, 0.02); end
+  def test_slow_trailer_false; trailer(false, 0.02); end
+  def test_slow_trailer_lazy; trailer(:lazy, 0.02); end
+
+  def trailer(btype, delay = false)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda do |e|
+        body = e["rack.input"].read
+        s = e["HTTP_XBT"] + "\n" + body
+        [ 200, {'Content-Length'=>s.size.to_s}, [ s ] ]
+      end
+      GTL.synchronize do
+        app(:rack, ru) do
+          listen "#{host}:#{port}"
+          input_buffering btype
+        end
+      end
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    c = get_tcp_client(host, port)
+    buf = "PUT / HTTP/1.0\r\nTrailer:xbt\r\nTransfer-Encoding: chunked\r\n\r\n"
+    c.write(buf)
+    xbt = btype.to_s
+    sleep(delay) if delay
+    c.write(sprintf("%x\r\n", xbt.size))
+    sleep(delay) if delay
+    c.write(xbt)
+    sleep(delay) if delay
+    c.write("\r\n")
+    sleep(delay) if delay
+    c.write("0\r\nXBT: ")
+    sleep(delay) if delay
+    c.write("#{xbt}\r\n\r\n")
+    IO.select([c], nil, nil, 5000) or raise "timed out"
+    rv = c.read(666)
+    _, body = rv.split(/\r\n\r\n/)
+    a, b = body.split(/\n/)
+    assert_equal xbt, a
+    assert_equal xbt, b
+  ensure
+    c.close if c
+    quit_wait(pid)
+  end
+
+  def test_check_client_connection
+    msgs = %w(ZZ zz)
+    err = @err
+    cfg = Yahns::Config.new
+    bpipe = IO.pipe
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      ru = lambda { |e|
+        case e['PATH_INFO']
+        when '/sleep'
+          a = Object.new
+          a.instance_variable_set(:@bpipe, bpipe)
+          a.instance_variable_set(:@msgs, msgs)
+          def a.each
+            @msgs.each do |msg|
+              yield @bpipe[0].read(msg.size)
+            end
+          end
+        when '/cccfail'
+          # we should not get here if check_client_connection worked
+          abort "CCCFAIL"
+        else
+          a = %w(HI)
+        end
+        [ 200, {'Content-Length'=>'2'}, a ]
+      }
+      GTL.synchronize {
+        app(:rack, ru) {
+          listen "#{host}:#{port}"
+          check_client_connection true
+          # needed to avoid concurrency with check_client_connection
+          queue { worker_threads 1 }
+          output_buffering false
+        }
+      }
+      logger(Logger.new(err.path))
+    end
+    srv = Yahns::Server.new(cfg)
+
+    # ensure we set worker_threads correctly
+    eggs = srv.instance_variable_get(:@config).qeggs
+    assert_equal 1, eggs.size
+    assert_equal 1, eggs[:default].instance_variable_get(:@worker_threads)
+
+    pid = fork do
+      bpipe[1].close
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    bpipe[0].close
+    a = get_tcp_client(host, port)
+    b = get_tcp_client(host, port)
+    a.write("GET /sleep HTTP/1.0\r\n\r\n")
+    r = IO.select([a], nil, nil, 4)
+    assert r, "nothing ready"
+    assert_equal a, r[0][0]
+    buf = a.read(8)
+    assert_equal "HTTP/1.1", buf
+
+    # hope the kernel sees this before it sees the bpipe ping-ponging below
+    b.write("GET /cccfail HTTP/1.0\r\n\r\n")
+    b.shutdown
+    b.close
+
+    # ping-pong a bit to stall the server
+    msgs.each do |msg|
+      bpipe[1].write(msg)
+      Timeout.timeout(10) { buf << a.readpartial(10) until buf =~ /#{msg}/ }
+    end
+    bpipe[1].close
+    assert_equal msgs.join, buf.split(/\r\n\r\n/)[1]
+
+    # do things still work?
+    run_client(host, port) { |res| assert_equal "HI", res.body }
+  ensure
+    quit_wait(pid)
+  end
+
+  def test_mp
+    pid, host, port = new_mp_server
+    wpid = nil
+    run_client(host, port) do |res|
+      wpid ||= res.body.to_i
+    end
+  ensure
+    quit_wait(pid)
+    if wpid
+      assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid) }
+      assert_raises(Errno::ECHILD) { Process.waitpid2(wpid) }
+    end
+  end
+
+  # Linux blocking accept() has fair behavior between multiple tasks
+  def test_mp_balance
+    skip("linux-only test") unless RUBY_PLATFORM =~ /linux/
+    pid, host, port = new_mp_server(2)
+    seen = {}
+
+    # wait for both processes to spin up
+    Timeout.timeout(10) do
+      run_client(host, port) { |res| seen[res.body] = 1 } until seen.size == 2
+    end
+
+    prev = nil
+    req = Net::HTTP::Get.new("/")
+    # we should bounce new connections between 2 processes
+    4.times do
+      Net::HTTP.start(host, port) do |http|
+        res = http.request(req)
+        assert_equal 200, res.code.to_i
+        assert_equal "keep-alive", res["Connection"]
+        refute_equal prev, res.body, "same PID accepted twice"
+        prev = res.body.dup
+        seen[prev] += 1
+        666.times { Thread.pass } # have the other acceptor to wake up
+      end
+    end
+    assert_equal 2, seen.size
+  ensure
+    quit_wait(pid)
+  end
+
+  def test_mp_worker_die
+    pid, host, port = new_mp_server
+    wpid1 = wpid2 = nil
+    run_client(host, port) do |res|
+      wpid1 ||= res.body.to_i
+    end
+    Process.kill(:QUIT, wpid1)
+    poke_until_dead(wpid1)
+    run_client(host, port) do |res|
+      wpid2 ||= res.body.to_i
+    end
+    refute_equal wpid2, wpid1
+  ensure
+    quit_wait(pid)
+    assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid2) } if wpid2
+  end
+
+  def test_mp_dead_parent
+    pid, host, port = new_mp_server
+    wpid = nil
+    run_client(host, port) do |res|
+      wpid ||= res.body.to_i
+    end
+    Process.kill(:KILL, pid)
+    _, status = Process.waitpid2(pid)
+    assert status.signaled?, status.inspect
+    poke_until_dead(wpid)
+  end
+
+  def run_client(host, port)
+    c = get_tcp_client(host, port)
+    Net::HTTP.start(host, port) do |http|
+      res = http.request(Net::HTTP::Get.new("/"))
+      assert_equal 200, res.code.to_i
+      assert_equal "keep-alive", res["Connection"]
+      yield res
+      res = http.request(Net::HTTP::Get.new("/"))
+      assert_equal 200, res.code.to_i
+      assert_equal "keep-alive", res["Connection"]
+      yield res
+    end
+    c.write "GET / HTTP/1.0\r\n\r\n"
+    res = Timeout.timeout(10) { c.read }
+    head, _ = res.split(/\r\n\r\n/)
+    head = head.split(/\r\n/)
+    assert_equal "HTTP/1.1 200 OK", head[0]
+    assert_equal "Connection: close", head[-1]
+    c.close
+  end
+
+  def new_mp_server(nr = 1)
+    ru = @ru = tmpfile(%w(config .ru))
+    @ru.puts('a = $$.to_s')
+    @ru.puts('run lambda { |_| [ 200, {"Content-Length"=>a.size.to_s},[a]]}')
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      worker_processes 2
+      GTL.synchronize { app(:rack, ru.path) { listen "#{host}:#{port}" } }
+      logger(Logger.new(File.open(err.path, "a")))
+    end
+    srv = Yahns::Server.new(cfg)
+    pid = fork do
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+    [ pid, host, port ]
+  end
+end
diff --git a/test/test_stream_file.rb b/test/test_stream_file.rb
new file mode 100644
index 0000000..6574a97
--- /dev/null
+++ b/test/test_stream_file.rb
@@ -0,0 +1,30 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'timeout'
+
+class TestStreamFile < Testcase
+  parallelize_me!
+  DevFD = Struct.new(:to_path)
+
+  def test_stream_file
+    fp = File.open("COPYING")
+    sf = Yahns::StreamFile.new(fp, true, 0, fp.stat.size)
+    refute sf.respond_to?(:close)
+    sf.wbuf_close(nil)
+    assert fp.closed?
+  end
+
+  def test_fd
+    fp = File.open("COPYING")
+    obj = DevFD.new("/dev/fd/#{fp.fileno}")
+    sf = Yahns::StreamFile.new(obj, true, 0, fp.stat.size)
+    io = sf.instance_variable_get :@tmpio
+    assert_instance_of IO, io.to_io
+    assert_equal fp.fileno, io.fileno
+    refute sf.respond_to?(:close)
+    sf.wbuf_close(nil)
+    refute fp.closed?
+    refute io.closed?
+  end
+end
diff --git a/test/test_wbuf.rb b/test/test_wbuf.rb
new file mode 100644
index 0000000..dc6bc24
--- /dev/null
+++ b/test/test_wbuf.rb
@@ -0,0 +1,136 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'helper'
+require 'timeout'
+
+class TestWbuf < Testcase
+  parallelize_me!
+
+  def test_wbuf
+    buf = "*" * (16384 * 2)
+    nr = 1000
+    [ true, false ].each do |persist|
+      wbuf = Yahns::Wbuf.new([], persist)
+      a, b = UNIXSocket.pair
+      assert_nil wbuf.wbuf_write(a, "HIHI")
+      assert_equal "HIHI", b.read(4)
+      nr.times { wbuf.wbuf_write(a, buf) }
+      assert_equal :wait_writable, wbuf.wbuf_flush(a)
+      done = IO.pipe
+      thr = Thread.new do
+        rv = []
+        until rv[-1] == persist
+          IO.select(nil, [a])
+          tmp = wbuf.wbuf_flush(a)
+          rv << tmp
+        end
+        done[1].syswrite '.'
+        rv
+      end
+
+      wait = true
+      begin
+        if wait
+          r = IO.select([b,done[0]], nil, nil, 5)
+        end
+        b.read_nonblock((rand * 1024) + 666, buf)
+        wait = (r[0] & done).empty?
+      rescue Errno::EAGAIN
+        break
+      end while true
+
+      assert_equal thr, thr.join(5)
+      rv = thr.value
+      assert_equal persist, rv.pop
+      assert(rv.all? { |x| x == :wait_writable })
+      a.close
+      b.close
+      done.each { |io| io.close }
+    end
+  end
+
+  def test_wbuf_blocked
+    a, b = UNIXSocket.pair
+    buf = "." * 4096
+    4.times do
+      begin
+        a.write_nonblock(buf)
+      rescue Errno::EAGAIN
+        break
+      end while true
+    end
+    wbuf = Yahns::Wbuf.new([], true)
+    assert_equal :wait_writable, wbuf.wbuf_write(a, buf)
+    assert_equal :wait_writable, wbuf.wbuf_flush(a)
+
+    # drain the buffer
+    Timeout.timeout(10) { b.read(b.nread) until b.nread == 0 }
+
+    # b.nread will increase after this
+    assert_nil wbuf.wbuf_write(a, "HI")
+    nr = b.nread
+    assert_operator nr, :>, 0
+    assert_equal b, IO.select([b], nil, nil, 5)[0][0]
+    b.read(nr - 2) if nr > 2
+    assert_equal b, IO.select([b], nil, nil, 5)[0][0]
+    assert_equal "HI", b.read(2)
+    begin
+      wbuf.wbuf_flush(a)
+      assert false
+    rescue => e
+    end
+    assert_match(%r{BUG: EOF on tmpio}, e.message)
+  ensure
+    a.close
+    b.close
+  end
+
+  def test_wbuf_flush_close
+    pipe = IO.pipe
+    persist = true
+    wbuf = Yahns::Wbuf.new(pipe[0], persist)
+    refute wbuf.respond_to?(:close) # we don't want this for HttpResponse body
+    sp = UNIXSocket.pair
+    rv = nil
+
+    buf = ("*" * 16384) << "\n"
+    thr = Thread.new do
+      1000.times { pipe[1].write(buf) }
+      pipe[1].close
+    end
+
+    pipe[0].each { |chunk| rv = wbuf.wbuf_write(sp[1], chunk) }
+    assert_equal thr, thr.join(5)
+    assert_equal :wait_writable, rv
+
+    done = IO.pipe
+    thr = Thread.new do
+      rv = []
+      until rv[-1] == persist
+        IO.select(nil, [sp[1]])
+        rv << wbuf.wbuf_flush(sp[1])
+      end
+      done[1].syswrite '.'
+      rv
+    end
+
+    wait = true
+    begin
+      if wait
+        r = IO.select([sp[0],done[0]], nil, nil, 5)
+      end
+      sp[0].read_nonblock(16384, buf)
+      wait = (r[0] & done).empty?
+    rescue Errno::EAGAIN
+      break
+    end while true
+
+    assert_equal thr, thr.join(5)
+    rv = thr.value
+    assert_equal true, rv.pop
+    assert rv.all? { |x| x == :wait_writable }
+    assert pipe[0].closed?
+    sp.each(&:close)
+    done.each(&:close)
+  end
+end
diff --git a/yahns.gemspec b/yahns.gemspec
new file mode 100644
index 0000000..68aad2e
--- /dev/null
+++ b/yahns.gemspec
@@ -0,0 +1,19 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+Gem::Specification.new do |s|
+  manifest = File.read('.gem-manifest').split(/\n/)
+  s.name = %q{yahns}
+  s.version = ENV["VERSION"]
+  s.authors = ["#{name} hackers"]
+  s.summary = "sleepy, multi-threaded, non-blocking application server"
+  s.description = File.read("README").split(/\n\n/)[1].strip
+  s.email = %q{yahns@yhbt.net}
+  s.executables = manifest.grep(%r{\Abin/}).map { |s| s.sub(%r{\Abin/}, "") }
+  s.files = manifest
+  s.add_dependency(%q<kgio>, '~> 2.8')
+  s.add_dependency(%q<sleepy_penguin>, '~> 3.2')
+  s.add_dependency(%q<sendfile>, '~> 1.2.1')
+  s.add_dependency(%q<unicorn>, '~> 4.6.3')
+  s.homepage = "http://yahns.yhbt.net/README"
+  s.licenses = "GPLv3+"
+end