pax_global_header00006660000000000000000000000064147164601640014523gustar00rootroot0000000000000052 comment=1370f52bb0e63575ebaf11b0700751f792cd0e96 .CHANGELOG.old000066400000000000000000000027331471646016400131410ustar00rootroot00000000000000v0.91.0 - HTTP/0.9 support, multiline header support, small fixes v0.90.0 - switch chunking+trailer handling to Ragel, v0.8.4 fixes v0.9.2 - Ruby 1.9.2 preview1 compatibility v0.9.1 - FD_CLOEXEC portability fix (v0.8.2 port) v0.9.0 - bodies: "Transfer-Encoding: chunked", rewindable streaming v0.8.4 - pass through unknown HTTP status codes v0.8.3 - Ruby 1.9.2 preview1 compatibility v0.8.2 - socket handling bugfixes and usability tweaks v0.8.1 - safer timeout handling, more consistent reload behavior v0.8.0 - enforce Rack dependency, minor performance improvements and fixes v0.7.1 - minor fixes, cleanups and documentation improvements v0.7.0 - rack.version is 1.0 v0.6.0 - cleanups + optimizations, signals to {in,de}crement processes v0.5.4 - fix data corruption with some small uploads (not curl) v0.5.3 - fix 100% CPU usage when idle, small cleanups v0.5.2 - force Status: header for compat, small cleanups v0.5.1 - exit correctly on INT/TERM, QUIT is still recommended, however v0.5.0 - {after,before}_fork API change, small tweaks/fixes v0.4.2 - fix Rails ARStore, FD leak prevention, descriptive proctitles v0.4.1 - Rails support, per-listener backlog and {snd,rcv}buf v0.2.3 - Unlink Tempfiles after use (they were closed, just not unlinked) v0.2.2 - small bug fixes, fix Rack multi-value headers (Set-Cookie:) v0.2.1 - Fix broken Manifest that cause unicorn_rails to not be bundled v0.2.0 - unicorn_rails launcher script. v0.1.0 - Unicorn - UNIX-only fork of Mongrel free of threading .document000066400000000000000000000006031471646016400127050ustar00rootroot00000000000000FAQ README TUNING PHILOSOPHY HACKING DESIGN CONTRIBUTORS LICENSE SIGNALS KNOWN_ISSUES TODO NEWS LATEST lib/unicorn.rb lib/unicorn/configurator.rb lib/unicorn/http_server.rb lib/unicorn/preread_input.rb lib/unicorn/stream_input.rb lib/unicorn/tee_input.rb lib/unicorn/util.rb lib/unicorn/oob_gc.rb lib/unicorn/worker.rb unicorn_1 unicorn_rails_1 ISSUES Sandbox Links Application_Timeouts .gitattributes000066400000000000000000000001251471646016400137600ustar00rootroot00000000000000*.gemspec diff=ruby *.rb diff=ruby *.ru diff=ruby Rakefile diff=ruby bin/* diff=ruby .gitignore000066400000000000000000000004101471646016400130520ustar00rootroot00000000000000*.o *.bundle *.log *.so *.rbc .DS_Store /.config /InstalledFiles /doc /local.mk /test/rbx-* /test/ruby-* ext/unicorn_http/Makefile ext/unicorn_http/unicorn_http.c log/ pkg/ /vendor /NEWS* /.manifest /GIT-VERSION-FILE /man /tmp /LATEST /lib/unicorn/version.rb /*_1 .mailmap000066400000000000000000000033231471646016400125110ustar00rootroot00000000000000# This list is used by "git shortlog" to fixup the ugly faux email addresses # "" that the "git svn" tool creates by default. # Eric Wong started this .mailmap file (and is the maintainer of it...) Eric Wong normalperson # This also includes all the Mongrel contributors that committed to the # Rubyforge SVN repo. Some real names were looked up on rubyforge.org # (http://rubyforge.org/users/$user), but we're not going expose any email # addresses here without their permission. Austin Godber godber godber Bradley Taylor bktaylor Ezra Zygmuntowicz ezmobius Filipe Lautert filipe Luis Lavena luislavena Matt Pelletier bricolage MenTaLguY mental Nick Sieger nicksieger Rick Olson technoweenie Wayne E. Seguin wayneeseguin Zed A. Shaw why the lucky stiff # Evan had his email address in the git history we branched from anyways Evan Weaver evanweaver .olddoc.yml000066400000000000000000000017501471646016400131370ustar00rootroot00000000000000--- cgit_url: https://yhbt.net/unicorn.git rdoc_url: https://yhbt.net/unicorn/ ml_url: - https://yhbt.net/unicorn-public/ - http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/ merge_html: unicorn_1: Documentation/unicorn.1.html unicorn_rails_1: Documentation/unicorn_rails.1.html noindex: - Unicorn::Const - LATEST - TODO - unicorn_rails_1 public_email: unicorn-public@yhbt.net imap_url: - imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0 - imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0 nntp_url: - nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn - nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn - nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general source_code: - git clone https://yhbt.net/unicorn.git - torsocks git clone http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn.git Application_Timeouts000066400000000000000000000057151471646016400151560ustar00rootroot00000000000000= Application Timeouts This article focuses on _application_ setup for Rack applications, but can be expanded to all applications that connect to external resources and expect short response times. This article is not specific to unicorn, but exists to discourage the overuse of the built-in {timeout}[link:Unicorn/Configurator.html#method-i-timeout] directive in unicorn. == ALL External Resources Are Considered Unreliable Network reliability can _never_ be guaranteed. Network failures cannot be detected reliably by the client (Rack application) in a reasonable timeframe, not even on a LAN. Thus, application authors must configure timeouts when interacting with external resources. Most database adapters allow configurable timeouts. Net::HTTP and Net::SMTP in the Ruby standard library allow configurable timeouts. Even for things as fast as {memcached}[https://memcached.org/], {dalli}[https://rubygems.org/gems/dalli], {memcached}[https://rubygems.org/gems/memcached] and {memcache-client}[https://rubygems.org/gems/memcache-client] RubyGems all offer configurable timeouts. Consult the relevant documentation for the libraries you use on how to configure these timeouts. == Rolling Your Own Socket Code Use non-blocking I/O and IO.select with a timeout to wait on sockets. == Timeout module in the Ruby standard library Ruby offers a Timeout module in its standard library. It has several caveats and is not always reliable: * /Some/ Ruby C extensions are not interrupted/timed-out gracefully by this module (report these bugs to extension authors, please) but pure-Ruby components should be. * Long-running tasks may run inside `ensure' clauses after timeout fires, causing the timeout to be ineffective. The Timeout module is a second-to-last-resort solution, timeouts using IO.select (or similar) are more reliable. If you depend on libraries that do not offer timeouts when connecting to external resources, kindly ask those library authors to provide configurable timeouts. === A Note About Filesystems Most operations to regular files on POSIX filesystems are NOT interruptable. Thus, the "timeout" module in the Ruby standard library can not reliably timeout systems with massive amounts of iowait. If your app relies on the filesystem, ensure all the data your application works with is small enough to fit in the kernel page cache. Otherwise increase the amount of physical memory you have to match, or employ a fast, low-latency storage system (solid state). Volumes mounted over NFS (and thus a potentially unreliable network) must be mounted with timeouts and applications must be prepared to handle network/server failures. == The Last Line Of Defense The {timeout}[link:Unicorn/Configurator.html#method-i-timeout] mechanism in unicorn is an extreme solution that should be avoided whenever possible. It will help catch bugs in your application where and when your application forgets to use timeouts, but it is expensive as it kills and respawns a worker process. CONTRIBUTORS000066400000000000000000000021331471646016400127460ustar00rootroot00000000000000Unicorn developers (let us know if we forgot you, ...or if you no longer wish to be associated with the doofus running this disaster :P): * Eric Wong (Bozo Doofus For Life, Bastard Operator From Hell) There's numerous contributors over email the years, all of our mail is archived @ https://yhbt.net/unicorn-public/ * Suraj N. Kurapati * Andrey Stikheev * Wayne Larsen * Iñaki Baz Castillo * Augusto Becciu * Hongli Lai * ... (help wanted) We would like to thank following folks for helping make Unicorn possible: * Ezra Zygmuntowicz - for helping Eric decide on a sane configuration format and reasonable defaults. * Christian Neukirchen - for Rack, which let us put more focus on the server and drastically cut down on the amount of code we have to maintain. * Zed A. Shaw - for Mongrel, without which Unicorn would not be possible The original Mongrel contributors: * Luis Lavena * Wilson Bilkovich * why the lucky stiff * Dan Kubb * MenTaLguY * Filipe Lautert * Rick Olson * Wayne E. Seguin * Kirk Haines * Bradley Taylor * Matt Pelletier * Ry Dahl * Nick Sieger * Evan Weaver * Marc-André Cournoyer COPYING000066400000000000000000001043671471646016400121350ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . DESIGN000066400000000000000000000112701471646016400117640ustar00rootroot00000000000000== Design Unicorn was designed to support poorly-written codebases back in 2008. Its unfortunate popularity has only proliferated the existence of poorly-written code ever since... * Simplicity: Unicorn is a traditional UNIX prefork web server. No threads are used at all, this makes applications easier to debug and fix. When your application goes awry, a BOFH can just "kill -9" the runaway worker process without worrying about tearing all clients down, just one. Only UNIX-like systems supporting fork() and file descriptor inheritance are supported. * The Ragel+C HTTP parser is taken from Mongrel. * All HTTP parsing and I/O is done much like Mongrel: 1. read/parse HTTP request headers in full 2. call Rack application 3. write HTTP response back to the client * Like Mongrel, neither keepalive nor pipelining are supported. These aren't needed since Unicorn is only designed to serve fast, low-latency clients directly. Do one thing, do it well; let nginx handle slow clients. * Configuration is purely in Ruby and eval(). Ruby is less ambiguous than YAML and lets lambdas for before_fork/after_fork/before_exec hooks be defined inline. An optional, separate config_file may be used to modify supported configuration changes (and also gives you plenty of rope if you RTFS :>) * One master process spawns and reaps worker processes. The Rack application itself is called only within the worker process (but can be loaded within the master). A copy-on-write friendly garbage collector like the one found in mainline Ruby 2.0.0 and later can be used to minimize memory usage along with the "preload_app true" directive (see Unicorn::Configurator). * The number of worker processes should be scaled to the number of CPUs, memory or even spindles you have. If you have an existing Mongrel cluster on a single-threaded app, using the same amount of processes should work. Let a full-HTTP-request-buffering reverse proxy like nginx manage concurrency to thousands of slow clients for you. Unicorn scaling should only be concerned about limits of your backend system(s). * Load balancing between worker processes is done by the OS kernel. All workers share a common set of listener sockets and does non-blocking accept() on them. The kernel will decide which worker process to give a socket to and workers will sleep if there is nothing to accept(). * Since non-blocking accept() is used, there can be a thundering herd when an occasional client connects when application *is not busy*. The thundering herd problem should not affect applications that are running all the time since worker processes will only select()/accept() outside of the application dispatch. * Additionally, thundering herds are much smaller than with configurations using existing prefork servers. Process counts should only be scaled to backend resources, _never_ to the number of expected clients like is typical with blocking prefork servers. So while we've seen instances of popular prefork servers configured to run many hundreds of worker processes, Unicorn deployments are typically only 2-4 processes per-core. * On-demand scaling of worker processes never happens automatically. Again, Unicorn is concerned about scaling to backend limits and should never configured in a fashion where it could be waiting on slow clients. For extremely rare circumstances, we provide TTIN and TTOU signal handlers to increment/decrement your process counts without reloading. Think of it as driving a car with manual transmission: you have a lot more control if you know what you're doing. * Blocking I/O is used for clients. This allows a simpler code path to be followed within the Ruby interpreter and fewer syscalls. Applications that use threads continue to work if Unicorn is only serving LAN or localhost clients. * SIGKILL is used to terminate the timed-out workers from misbehaving apps as reliably as possible on a UNIX system. The default timeout is a generous 60 seconds (same default as in Mongrel). * The poor performance of select() on large FD sets is avoided as few file descriptors are used in each worker. There should be no gain from moving to highly scalable but unportable event notification solutions for watching few file descriptors. * If the master process dies unexpectedly for any reason, workers will notice within :timeout/2 seconds and follow the master to its death. * There is never any explicit real-time dependency or communication between the worker processes nor to the master process. Synchronization is handled entirely by the OS kernel and shared resources are never accessed by the worker when it is servicing a client. Documentation/000077500000000000000000000000001471646016400137005ustar00rootroot00000000000000Documentation/.gitignore000066400000000000000000000000221471646016400156620ustar00rootroot00000000000000*.gz *.html *.txt Documentation/unicorn.1000066400000000000000000000170271471646016400154460ustar00rootroot00000000000000.TH "UNICORN" "1" "September 15, 2009" "Unicorn User Manual" "" .hy .SH NAME .PP unicorn \- a rackup\-like command to launch the Unicorn HTTP server .SH SYNOPSIS .PP unicorn [\-c CONFIG_FILE] [\-E RACK_ENV] [\-D] [RACKUP_FILE] .SH DESCRIPTION .PP A rackup(1)\-like command to launch Rack applications using Unicorn. It is expected to be started in your application root (APP_ROOT), but the "working_directory" directive may be used in the CONFIG_FILE. .PP While unicorn takes a myriad of command\-line options for compatibility with ruby(1) and rackup(1), it is recommended to stick to the few command\-line options specified in the SYNOPSIS and use the CONFIG_FILE as much as possible. .SH RACKUP FILE .PP This defaults to "config.ru" in APP_ROOT. It should be the same file used by rackup(1) and other Rack launchers, it uses the \f[I]Rack::Builder\f[] DSL. .PP Embedded command\-line options are mostly parsed for compatibility with rackup(1) but strongly discouraged. .SH UNICORN OPTIONS .TP .B \-c, \-\-config\-file CONFIG_FILE Path to the Unicorn\-specific config file. The config file is implemented as a Ruby DSL, so Ruby code may executed. See the RDoc/ri for the \f[I]Unicorn::Configurator\f[] class for the full list of directives available from the DSL. Using an absolute path for for CONFIG_FILE is recommended as it makes multiple instances of Unicorn easily distinguishable when viewing ps(1) output. .RS .RE .TP .B \-D, \-\-daemonize Run daemonized in the background. The process is detached from the controlling terminal and stdin is redirected to "/dev/null". Unlike many common UNIX daemons, we do not chdir to "/" upon daemonization to allow more control over the startup/upgrade process. Unless specified in the CONFIG_FILE, stderr and stdout will also be redirected to "/dev/null". .RS .RE .TP .B \-E, \-\-env RACK_ENV Run under the given RACK_ENV. See the RACK ENVIRONMENT section for more details. .RS .RE .TP .B \-l, \-\-listen ADDRESS Listens on a given ADDRESS. ADDRESS may be in the form of HOST:PORT or PATH, HOST:PORT is taken to mean a TCP socket and PATH is meant to be a path to a UNIX domain socket. Defaults to "0.0.0.0:8080" (all addresses on TCP port 8080) For production deployments, specifying the "listen" directive in CONFIG_FILE is recommended as it allows fine\-tuning of socket options. .RS .RE .TP .B \-N, \-\-no\-default\-middleware Disables loading middleware implied by RACK_ENV. This bypasses the configuration documented in the RACK ENVIRONMENT section, but still allows RACK_ENV to be used for application/framework\-specific purposes. .RS .RE .SH RACKUP COMPATIBILITY OPTIONS .TP .B \-o, \-\-host HOST Listen on a TCP socket belonging to HOST, default is "0.0.0.0" (all addresses). If specified multiple times on the command\-line, only the last\-specified value takes effect. This option only exists for compatibility with the rackup(1) command, use of "\-l"/"\-\-listen" switch is recommended instead. .RS .RE .TP .B \-p, \-\-port PORT Listen on the specified TCP PORT, default is 8080. If specified multiple times on the command\-line, only the last\-specified value takes effect. This option only exists for compatibility with the rackup(1) command, use of "\-l"/"\-\-listen" switch is recommended instead. .RS .RE .TP .B \-s, \-\-server SERVER No\-op, this exists only for compatibility with rackup(1). .RS .RE .SH RUBY OPTIONS .TP .B \-e, \-\-eval LINE Evaluate a LINE of Ruby code. This evaluation happens immediately as the command\-line is being parsed. .RS .RE .TP .B \-d, \-\-debug Turn on debug mode, the $DEBUG variable is set to true. .RS .RE .TP .B \-w, \-\-warn Turn on verbose warnings, the $VERBOSE variable is set to true. .RS .RE .TP .B \-I, \-\-include PATH specify $LOAD_PATH. PATH will be prepended to $LOAD_PATH. The \[aq]:\[aq] character may be used to delimit multiple directories. This directive may be used more than once. Modifications to $LOAD_PATH take place immediately and in the order they were specified on the command\-line. .RS .RE .TP .B \-r, \-\-require LIBRARY require a specified LIBRARY before executing the application. The "require" statement will be executed immediately and in the order they were specified on the command\-line. .RS .RE .SH SIGNALS .PP The following UNIX signals may be sent to the master process: .IP \[bu] 2 HUP \- reload config file, app, and gracefully restart all workers .IP \[bu] 2 INT/TERM \- quick shutdown, kills all workers immediately .IP \[bu] 2 QUIT \- graceful shutdown, waits for workers to finish their current request before finishing. .IP \[bu] 2 USR1 \- reopen all logs owned by the master and all workers See Unicorn::Util.reopen_logs for what is considered a log. .IP \[bu] 2 USR2 \- reexecute the running binary. A separate QUIT should be sent to the original process once the child is verified to be up and running. .IP \[bu] 2 WINCH \- gracefully stops workers but keep the master running. This will only work for daemonized processes. .IP \[bu] 2 TTIN \- increment the number of worker processes by one .IP \[bu] 2 TTOU \- decrement the number of worker processes by one .PP See the SIGNALS (https://yhbt.net/unicorn/SIGNALS.html) document for full description of all signals used by Unicorn. .SH RACK ENVIRONMENT .PP Accepted values of RACK_ENV and the middleware they automatically load (outside of RACKUP_FILE) are exactly as those in rackup(1): .IP \[bu] 2 development \- loads Rack::CommonLogger, Rack::ShowExceptions, and Rack::Lint middleware .IP \[bu] 2 deployment \- loads Rack::CommonLogger middleware .IP \[bu] 2 none \- loads no middleware at all, relying entirely on RACKUP_FILE .PP All unrecognized values for RACK_ENV are assumed to be "none". Production deployments are strongly encouraged to use "deployment" or "none" for maximum performance. .PP As of Unicorn 0.94.0, RACK_ENV is exported as a process\-wide environment variable as well. While not current a part of the Rack specification as of Rack 1.0.1, this has become a de facto standard in the Rack world. .PP Note the Rack::ContentLength middleware is also loaded by "deployment" and "development", but no other values of RACK_ENV. If needed, they must be individually specified in the RACKUP_FILE, some frameworks do not require them. .SH ENVIRONMENT VARIABLES .PP The RACK_ENV variable is set by the aforementioned \-E switch. All application or library\-specific environment variables (e.g. TMPDIR) may always be set in the Unicorn CONFIG_FILE in addition to the spawning shell. When transparently upgrading Unicorn, all environment variables set in the old master process are inherited by the new master process. Unicorn only uses (and will overwrite) the UNICORN_FD environment variable internally when doing transparent upgrades. .PP UNICORN_FD is a comma\-delimited list of one or more file descriptors used to implement USR2 upgrades. Init systems may bind listen sockets itself and spawn unicorn with UNICORN_FD set to the file descriptor numbers of the listen socket(s). .PP As of unicorn 5.0, LISTEN_PID and LISTEN_FDS are used for socket activation as documented in the sd_listen_fds(3) manpage. Users relying on this feature do not need to specify a listen socket in the unicorn config file. .SH SEE ALSO .IP \[bu] 2 \f[I]Rack::Builder\f[] ri/RDoc .IP \[bu] 2 \f[I]Unicorn::Configurator\f[] ri/RDoc .UR https://yhbt.net/unicorn/Unicorn/Configurator.html .UE .IP \[bu] 2 unicorn RDoc .UR https://yhbt.net/unicorn/ .UE .IP \[bu] 2 Rack RDoc .UR https://www.rubydoc.info/github/rack/rack/ .UE .IP \[bu] 2 Rackup HowTo .UR https://github.com/rack/rack/wiki/(tutorial)-rackup-howto .UE .SH AUTHORS The Unicorn Community . Documentation/unicorn_rails.1000066400000000000000000000161561471646016400166420ustar00rootroot00000000000000.TH "UNICORN_RAILS" "1" "September 17, 2009" "Unicorn User Manual" "" .hy .SH NAME .PP unicorn_rails \- unicorn launcher for Rails 1.x and 2.x users .SH SYNOPSIS .PP unicorn_rails [\-c CONFIG_FILE] [\-E RAILS_ENV] [\-D] [RACKUP_FILE] .SH DESCRIPTION .PP A rackup(1)\-like command to launch ancient Rails (2.x and earlier) applications using Unicorn. Rails 3 (and later) support Rack natively, so users are encouraged to use unicorn(1) instead of unicorn_rails(1). .PP It is expected to be started in your Rails application root (RAILS_ROOT), but the "working_directory" directive may be used in the CONFIG_FILE. .PP The outward interface resembles rackup(1), the internals and default middleware loading is designed like the \f[C]script/server\f[] command distributed with Rails. .PP While Unicorn takes a myriad of command\-line options for compatibility with ruby(1) and rackup(1), it is recommended to stick to the few command\-line options specified in the SYNOPSIS and use the CONFIG_FILE as much as possible. .SH UNICORN OPTIONS .TP .B \-c, \-\-config\-file CONFIG_FILE Path to the Unicorn\-specific config file. The config file is implemented as a Ruby DSL, so Ruby code may executed. See the RDoc/ri for the \f[I]Unicorn::Configurator\f[] class for the full list of directives available from the DSL. Using an absolute path for for CONFIG_FILE is recommended as it makes multiple instances of Unicorn easily distinguishable when viewing ps(1) output. .RS .RE .TP .B \-D, \-\-daemonize Run daemonized in the background. The process is detached from the controlling terminal and stdin is redirected to "/dev/null". Unlike many common UNIX daemons, we do not chdir to "/" upon daemonization to allow more control over the startup/upgrade process. Unless specified in the CONFIG_FILE, stderr and stdout will also be redirected to "/dev/null". Daemonization will \f[I]skip\f[] loading of the \f[I]Rails::Rack::LogTailer\f[] middleware under Rails >= 2.3.x. By default, unicorn_rails(1) will create a PID file in \f[I]"RAILS_ROOT/tmp/pids/unicorn.pid"\f[]. You may override this by specifying the "pid" directive to override this Unicorn config file. .RS .RE .TP .B \-E, \-\-env RAILS_ENV Run under the given RAILS_ENV. This sets the RAILS_ENV environment variable. Acceptable values are exactly those you expect in your Rails application, typically "development" or "production". .RS .RE .TP .B \-l, \-\-listen ADDRESS Listens on a given ADDRESS. ADDRESS may be in the form of HOST:PORT or PATH, HOST:PORT is taken to mean a TCP socket and PATH is meant to be a path to a UNIX domain socket. Defaults to "0.0.0.0:8080" (all addresses on TCP port 8080). For production deployments, specifying the "listen" directive in CONFIG_FILE is recommended as it allows fine\-tuning of socket options. .RS .RE .SH RACKUP COMPATIBILITY OPTIONS .TP .B \-o, \-\-host HOST Listen on a TCP socket belonging to HOST, default is "0.0.0.0" (all addresses). If specified multiple times on the command\-line, only the last\-specified value takes effect. This option only exists for compatibility with the rackup(1) command, use of "\-l"/"\-\-listen" switch is recommended instead. .RS .RE .TP .B \-p, \-\-port PORT Listen on the specified TCP PORT, default is 8080. If specified multiple times on the command\-line, only the last\-specified value takes effect. This option only exists for compatibility with the rackup(1) command, use of "\-l"/"\-\-listen" switch is recommended instead. .RS .RE .TP .B \-\-path PATH Mounts the Rails application at the given PATH (instead of "/"). This is equivalent to setting the RAILS_RELATIVE_URL_ROOT environment variable. This is only supported under Rails 2.3 or later at the moment. .RS .RE .SH RUBY OPTIONS .TP .B \-e, \-\-eval LINE Evaluate a LINE of Ruby code. This evaluation happens immediately as the command\-line is being parsed. .RS .RE .TP .B \-d, \-\-debug Turn on debug mode, the $DEBUG variable is set to true. For Rails >= 2.3.x, this loads the \f[I]Rails::Rack::Debugger\f[] middleware. .RS .RE .TP .B \-w, \-\-warn Turn on verbose warnings, the $VERBOSE variable is set to true. .RS .RE .TP .B \-I, \-\-include PATH specify $LOAD_PATH. PATH will be prepended to $LOAD_PATH. The \[aq]:\[aq] character may be used to delimit multiple directories. This directive may be used more than once. Modifications to $LOAD_PATH take place immediately and in the order they were specified on the command\-line. .RS .RE .TP .B \-r, \-\-require LIBRARY require a specified LIBRARY before executing the application. The "require" statement will be executed immediately and in the order they were specified on the command\-line. .RS .RE .SH RACKUP FILE .PP This defaults to "config.ru" in RAILS_ROOT. It should be the same file used by rackup(1) and other Rack launchers, it uses the \f[I]Rack::Builder\f[] DSL. Unlike many other Rack applications, RACKUP_FILE is completely \f[I]optional\f[] for Rails, but may be used to disable some of the default middleware for performance. .PP Embedded command\-line options are mostly parsed for compatibility with rackup(1) but strongly discouraged. .SH ENVIRONMENT VARIABLES .PP The RAILS_ENV variable is set by the aforementioned \-E switch. The RAILS_RELATIVE_URL_ROOT is set by the aforementioned \-\-path switch. Either of these variables may also be set in the shell or the Unicorn CONFIG_FILE. All application or library\-specific environment variables (e.g. TMPDIR, RAILS_ASSET_ID) may always be set in the Unicorn CONFIG_FILE in addition to the spawning shell. When transparently upgrading Unicorn, all environment variables set in the old master process are inherited by the new master process. Unicorn only uses (and will overwrite) the UNICORN_FD environment variable internally when doing transparent upgrades. .SH SIGNALS .PP The following UNIX signals may be sent to the master process: .IP \[bu] 2 HUP \- reload config file, app, and gracefully restart all workers .IP \[bu] 2 INT/TERM \- quick shutdown, kills all workers immediately .IP \[bu] 2 QUIT \- graceful shutdown, waits for workers to finish their current request before finishing. .IP \[bu] 2 USR1 \- reopen all logs owned by the master and all workers See Unicorn::Util.reopen_logs for what is considered a log. .IP \[bu] 2 USR2 \- reexecute the running binary. A separate QUIT should be sent to the original process once the child is verified to be up and running. .IP \[bu] 2 WINCH \- gracefully stops workers but keep the master running. This will only work for daemonized processes. .IP \[bu] 2 TTIN \- increment the number of worker processes by one .IP \[bu] 2 TTOU \- decrement the number of worker processes by one .PP See the SIGNALS (https://yhbt.net/unicorn/SIGNALS.html) document for full description of all signals used by Unicorn. .SH SEE ALSO .IP \[bu] 2 unicorn(1) .IP \[bu] 2 \f[I]Rack::Builder\f[] ri/RDoc .IP \[bu] 2 \f[I]Unicorn::Configurator\f[] ri/RDoc .UR https://yhbt.net/unicorn/Unicorn/Configurator.html .UE .IP \[bu] 2 unicorn RDoc .UR https://yhbt.net/unicorn/ .UE .IP \[bu] 2 Rack RDoc .UR https://www.rubydoc.info/github/rack/rack/ .UE .IP \[bu] 2 Rackup HowTo .UR https://github.com/rack/rack/wiki/(tutorial)-rackup-howto .UE .SH AUTHORS The Unicorn Community . FAQ000066400000000000000000000054401471646016400114240ustar00rootroot00000000000000= Frequently Asked Questions about Unicorn === Why is nginx getting ECONNRESET as a reverse proxy? Request body data (commonly from POST and PUT requests) may not be drained entirely by the application. This may happen when request bodies are gzipped, as unicorn reads request body data lazily to avoid overhead from bad requests. Ref: https://yhbt.net/unicorn-public/FC91211E-FD32-432C-92FC-0318714C2170@zendesk.com/ === Why aren't my Rails log files rotated when I use SIGUSR1? The Rails autoflush_log option must remain disabled with multiprocess servers such as unicorn. Buffering in userspace may cause lines to be partially written and lead to corruption in the presence of multiple processes. With reasonable amounts of logging, the performance impact of autoflush_log should be negligible on Linux and other modern kernels. === Why are my redirects going to "http" URLs when my site uses https? If your site is entirely behind https, then Rack applications that use "rack.url_scheme" can set the following in the Unicorn config file: HttpRequest::DEFAULTS["rack.url_scheme"] = "https" For frameworks that do not use "rack.url_scheme", you can also try setting one or both of the following: HttpRequest::DEFAULTS["HTTPS"] = "on" HttpRequest::DEFAULTS["HTTP_X_FORWARDED_PROTO"] = "https" Otherwise, you can configure your proxy (nginx) to send the "X-Forwarded-Proto: https" header only for parts of the site that use https. For nginx, you can do it with the following line in appropriate "location" blocks of your nginx config file: proxy_set_header X-Forwarded-Proto https; === Why are log messages from Unicorn are unformatted when using Rails? Current versions of Rails unfortunately overrides the default Logger formatter. You can undo this behavior with the default logger in your Unicorn config file: Configurator::DEFAULTS[:logger].formatter = Logger::Formatter.new Of course you can specify an entirely different logger as well with the "logger" directive described by Unicorn::Configurator. === Why am I getting "connection refused"/502 errors under high load? Short answer: your application cannot keep up. You can increase the size of the :backlog parameter if your kernel supports a larger listen() queue, but keep in mind having a large listen queue makes failover to a different machine more difficult. See the TUNING and Unicorn::Configurator documents for more information on :backlog-related topics. === I've installed Rack 1.1.x, why can't Unicorn load Rails (2.3.5)? Rails 2.3.5 is not compatible with Rack 1.1.x. Unicorn is compatible with both Rack 1.1.x and Rack 1.0.x, and RubyGems will load the latest version of Rack installed on the system. Uninstalling the Rack 1.1.x gem should solve gem loading issues with Rails 2.3.5. Rails 2.3.6 and later correctly support Rack 1.1.x. GIT-VERSION-GEN000077500000000000000000000020531471646016400131720ustar00rootroot00000000000000#!/usr/bin/env ruby DEF_VER = "v6.1.0" CONSTANT = "Unicorn::Const::UNICORN_VERSION" RVF = "lib/unicorn/version.rb" GVF = "GIT-VERSION-FILE" 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/, "") # generate the Ruby constant new_ruby_version = "#{CONSTANT} = '#{vn}'\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 # generate the makefile snippet new_make_version = "GIT_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 puts vn if $0 == __FILE__ GNUmakefile000066400000000000000000000223541471646016400131470ustar00rootroot00000000000000# use GNU Make to run tests in parallel, and without depending on RubyGems all:: test RLFLAGS = -G2 MRI = ruby RUBY = ruby RAKE = rake RAGEL = ragel RSYNC = rsync OLDDOC = olddoc RDOC = rdoc INSTALL = install PROVE = prove GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE @./GIT-VERSION-GEN -include GIT-VERSION-FILE -include local.mk ruby_bin := $(shell which $(RUBY)) ifeq ($(DLEXT),) # "so" for Linux DLEXT := $(shell $(RUBY) -rrbconfig -e 'puts RbConfig::CONFIG["DLEXT"]') endif ifeq ($(RUBY_VERSION),) RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION') endif RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))') # we should never package more than one ext to avoid DSO proliferation: # https://udrepper.livejournal.com/8790.html ext := $(firstword $(wildcard ext/*)) ragel: $(ext)/unicorn_http.c rl_files := $(wildcard $(ext)/*.rl) ragel: $(ext)/unicorn_http.c $(ext)/unicorn_http.c: $(rl_files) cd $(@D) && $(RAGEL) unicorn_http.rl -C $(RLFLAGS) -o $(@F) ext_pfx := test/$(RUBY_ENGINE)-$(RUBY_VERSION) tmp_bin := $(ext_pfx)/bin ext_h := $(wildcard $(ext)/*/*.h $(ext)/*.h) ext_src := $(sort $(wildcard $(ext)/*.c) $(ext_h) $(ext)/unicorn_http.c) ext_pfx_src := $(addprefix $(ext_pfx)/,$(ext_src)) ext_dir := $(ext_pfx)/$(ext) $(ext)/extconf.rb: @>>$@ $(ext_dir) $(tmp_bin) man/man1 doc/man1 pkg t/trash: @mkdir -p $@ $(ext_pfx)/$(ext)/%: $(ext)/% | $(ext_dir) $(INSTALL) -m 644 $< $@ $(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb | $(ext_dir) $(RM) -f $(@D)/*.o cd $(@D) && $(RUBY) $(CURDIR)/$(ext)/extconf.rb $(EXTCONF_ARGS) ext_sfx := _ext.$(DLEXT) ext_dl := $(ext_pfx)/$(ext)/$(notdir $(ext)_ext.$(DLEXT)) $(ext_dl): $(ext_src) $(ext_pfx_src) $(ext_pfx)/$(ext)/Makefile $(MAKE) -C $(@D) lib := $(CURDIR)/lib:$(CURDIR)/$(ext_pfx)/$(ext) http build: $(ext_dl) $(ext_pfx)/$(ext)/unicorn_http.c: ext/unicorn_http/unicorn_http.c # dunno how to implement this as concisely in Ruby, and hell, I love awk awk_slow := awk '/def test_/{print FILENAME"--"$$2".n"}' 2>/dev/null slow_tests := test/unit/test_server.rb test/exec/test_exec.rb \ test/unit/test_signals.rb test/unit/test_upload.rb log_suffix = .$(RUBY_ENGINE).$(RUBY_VERSION).log T := $(filter-out $(slow_tests), $(wildcard test/*/test*.rb)) T_n := $(shell $(awk_slow) $(slow_tests)) T_log := $(subst .rb,$(log_suffix),$(T)) T_n_log := $(subst .n,$(log_suffix),$(T_n)) base_bins := unicorn unicorn_rails bins := $(addprefix bin/, $(base_bins)) man1_rdoc := $(addsuffix _1, $(base_bins)) man1_bins := $(addsuffix .1, $(base_bins)) man1_paths := $(addprefix man/man1/, $(man1_bins)) tmp_bins = $(addprefix $(tmp_bin)/, unicorn unicorn_rails) pid := $(shell echo $$PPID) build: $(tmp_bins) $(tmp_bin)/%: bin/% | $(tmp_bin) $(INSTALL) -m 755 $< $@.$(pid) $(MRI) -i -p -e '$$_.gsub!(%r{^#!.*$$},"#!$(ruby_bin)")' $@.$(pid) mv $@.$(pid) $@ bins: $(tmp_bins) t_log := $(T_log) $(T_n_log) test: $(T) $(T_n) test-prove @cat $(t_log) | $(MRI) test/aggregate.rb @$(RM) $(t_log) test-exec: $(wildcard test/exec/test_*.rb) test-unit: $(wildcard test/unit/test_*.rb) $(slow_tests): $(ext_dl) @$(MAKE) $(shell $(awk_slow) $@) # ensure we can require just the HTTP parser without the rest of unicorn test-require: $(ext_dl) $(RUBY) --disable-gems -I$(ext_pfx)/$(ext) -runicorn_http -e Unicorn test_prereq := $(tmp_bins) $(ext_dl) SH_TEST_OPTS = ifdef V ifeq ($(V),2) SH_TEST_OPTS += --trace else SH_TEST_OPTS += --verbose endif endif # do we trust Ruby behavior to be stable? some tests are # (mostly) POSIX sh (not bash or ksh93, so no "set -o pipefail" # TRACER = strace -f -o $(t_pfx).strace -s 100000 # TRACER = /usr/bin/time -o $(t_pfx).time t_pfx = trash/$@-$(RUBY_ENGINE)-$(RUBY_VERSION) T_sh = $(wildcard t/t[0-9][0-9][0-9][0-9]-*.sh) $(T_sh): export RUBY := $(RUBY) $(T_sh): export PATH := $(CURDIR)/$(tmp_bin):$(PATH) $(T_sh): export RUBYLIB := $(lib):$(RUBYLIB) $(T_sh): dep $(test_prereq) t/random_blob t/trash/.gitignore cd t && $(TRACER) $(SHELL) $(SH_TEST_OPTS) $(@F) $(TEST_OPTS) t/trash/.gitignore : | t/trash echo '*' >$@ dependencies := curl deps := $(addprefix t/.dep+,$(dependencies)) $(deps): dep_bin = $(lastword $(subst +, ,$@)) $(deps): @which $(dep_bin) > $@.$(pid) 2>/dev/null || : @test -s $@.$(pid) || \ { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; } @mv $@.$(pid) $@ dep: $(deps) t/random_blob: dd if=/dev/urandom bs=1M count=30 of=$@.$(pid) mv $@.$(pid) $@ test-integration: $(T_sh) test-prove: t/random_blob $(PROVE) -vw check: test-require test test-integration test-all: check TEST_OPTS = -v check_test = grep '0 failures, 0 errors' $(t) >/dev/null ifndef V quiet_pre = @echo '* $(arg)$(extra)'; quiet_post = >$(t) 2>&1 && $(check_test) else # we can't rely on -o pipefail outside of bash 3+, # so we use a stamp file to indicate success and # have rm fail if the stamp didn't get created stamp = $@$(log_suffix).ok quiet_pre = @echo $(RUBY) $(arg) $(TEST_OPTS); ! test -f $(stamp) && ( quiet_post = && > $(stamp) )2>&1 | tee $(t); \ rm $(stamp) 2>/dev/null && $(check_test) endif # not all systems have setsid(8), we need it because we spam signals # stupidly in some tests... rb_setsid := $(RUBY) -e 'Process.setsid' -e 'exec *ARGV' # TRACER='strace -f -o $(t).strace -s 100000' run_test = $(quiet_pre) \ $(rb_setsid) $(TRACER) $(RUBY) -w $(arg) $(TEST_OPTS) $(quiet_post) || \ (sed "s,^,$(extra): ," >&2 < $(t); exit 1) %.n: arg = $(subst .n,,$(subst --, -n ,$@)) %.n: t = $(subst .n,$(log_suffix),$@) %.n: export PATH := $(CURDIR)/$(tmp_bin):$(PATH) %.n: export RUBYLIB := $(lib):$(RUBYLIB) %.n: $(test_prereq) $(run_test) $(T): arg = $@ $(T): t = $(subst .rb,$(log_suffix),$@) $(T): export PATH := $(CURDIR)/$(tmp_bin):$(PATH) $(T): export RUBYLIB := $(lib):$(RUBYLIB) $(T): $(test_prereq) $(run_test) install: $(bins) $(ext)/unicorn_http.c $(prep_setup_rb) $(RM) -r .install-tmp mkdir .install-tmp cp -p bin/* .install-tmp $(RUBY) setup.rb all $(RM) $^ mv .install-tmp/* bin/ $(RM) -r .install-tmp $(prep_setup_rb) setup_rb_files := .config InstalledFiles prep_setup_rb := @-$(RM) $(setup_rb_files);$(MAKE) -C $(ext) clean clean: -$(MAKE) -C $(ext) clean $(RM) $(ext)/Makefile $(RM) $(setup_rb_files) $(t_log) $(RM) -r $(ext_pfx) man t/trash $(RM) $(html1) man1 := $(addprefix Documentation/, unicorn.1 unicorn_rails.1) html1 := $(addsuffix .html, $(man1)) man : $(man1) | man/man1 $(INSTALL) -m 644 $(man1) man/man1 html : $(html1) | doc/man1 $(INSTALL) -m 644 $(html1) doc/man1 %.1.html: %.1 $(OLDDOC) man2html -o $@ ./$< pkg_extra := GIT-VERSION-FILE lib/unicorn/version.rb LATEST NEWS \ $(ext)/unicorn_http.c $(man1_paths) NEWS: $(OLDDOC) prepare .manifest: $(ext)/unicorn_http.c man NEWS (git ls-files && for i in $@ $(pkg_extra); do echo $$i; done) | \ LC_ALL=C sort > $@+ cmp $@+ $@ || mv $@+ $@ $(RM) $@+ PLACEHOLDERS = $(man1_rdoc) doc: .document $(ext)/unicorn_http.c man html .olddoc.yml $(PLACEHOLDERS) find bin lib -type f -name '*.rbc' -exec rm -f '{}' ';' $(RM) -r doc $(OLDDOC) prepare $(RDOC) -f dark216 $(OLDDOC) merge $(INSTALL) -m 644 COPYING doc/COPYING $(INSTALL) -m 644 NEWS.atom.xml doc/NEWS.atom.xml $(INSTALL) -m 644 $(shell LC_ALL=C grep '^[A-Z]' .document) doc/ $(INSTALL) -m 644 $(man1_paths) doc/ tar cf - $$(git ls-files examples/) | (cd doc && tar xf -) # publishes docs to https://yhbt.net/unicorn/ publish_doc: -git set-file-times $(MAKE) doc $(MAKE) doc_gz chmod 644 $$(find doc -type f) $(RSYNC) -av doc/ yhbt.net:/srv/yhbt/unicorn/ \ --exclude index.html* --exclude created.rid* git ls-files | xargs touch # Create gzip variants of the same timestamp as the original so nginx # "gzip_static on" can serve the gzipped versions directly. doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.gz$$') doc_gz: for i in $(docs); do \ gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done ifneq ($(VERSION),) rfpackage := unicorn pkggem := pkg/$(rfpackage)-$(VERSION).gem pkgtgz := pkg/$(rfpackage)-$(VERSION).tgz # ensures we're actually on the tagged $(VERSION), only used for release verify: test x"$(shell umask)" = x0022 git rev-parse --verify refs/tags/v$(VERSION)^{} git diff-index --quiet HEAD^0 test `git rev-parse --verify HEAD^0` = \ `git rev-parse --verify refs/tags/v$(VERSION)^{}` 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 --local $(CURDIR)/$< $(pkggem): .manifest fix-perms | pkg gem build $(rfpackage).gemspec mv $(@F) $@ $(pkgtgz): distdir = $(basename $@) $(pkgtgz): HEAD = v$(VERSION) $(pkgtgz): .manifest fix-perms @test -n "$(distdir)" $(RM) -r $(distdir) mkdir -p $(distdir) tar cf - $$(cat .manifest) | (cd $(distdir) && tar xf -) cd pkg && tar cf - $(basename $(@F)) | gzip -9 > $(@F)+ mv $@+ $@ package: $(pkgtgz) $(pkggem) release: verify package # push gem to Gemcutter gem push $(pkggem) else gem install-gem: GIT-VERSION-FILE $(MAKE) $@ VERSION=$(GIT_VERSION) endif $(PLACEHOLDERS): echo olddoc_placeholder > $@ check-warnings: @(for i in $$(git ls-files '*.rb' bin | grep -v '^setup\.rb$$'); \ do $(RUBY) --disable-gems -d -W2 -c \ $$i; done) | grep -v '^Syntax OK$$' || : .PHONY: .FORCE-GIT-VERSION-FILE doc $(T) $(slow_tests) man $(T_sh) clean HACKING000066400000000000000000000074701471646016400120660ustar00rootroot00000000000000= Unicorn Hacker's Guide == Polyglot Infrastructure Like Mongrel, we use Ruby where it makes sense, and Ragel with C where it helps performance. All of the code that actually runs your Rack application is written Ruby, Ragel or C. Ragel may be dropped in favor of a picohttpparser-based one in the future. As far as tests and documentation goes, we're not afraid to embrace Unix and use traditional Unix tools where they make sense and get the job done. === Tests Tests are good, but slow tests make development slow, so we make tests faster (in parallel) with GNU make (instead of Rake) and avoiding RubyGems. New tests are written in Perl 5 and use TAP to ensure stability and immunity from Ruby incompatibilities. Users of GNU-based systems (such as GNU/Linux) usually have GNU make installed as "make" instead of "gmake". Running the entire test suite with 4 tests in parallel: gmake -j4 check Running just one unit test: gmake test/unit/test_http_parser.rb Running just one test case in a unit test: gmake test/unit/test_http_parser.rb--test_parse_simple.n === HttpServer We strive to write as little code as possible while still maintaining readability. However, readability and flexibility may be sacrificed for performance in hot code paths. For Ruby, less code generally means faster code. Memory allocation should be minimized as much as practically possible. Buffers for IO#readpartial are preallocated in the hot paths to avoid building up garbage. Hash assignments use frozen strings to avoid the duplication behind-the-scenes. We spend as little time as possible inside signal handlers and instead defer handling them for predictability and robustness. Most of the Unix-specific things are in the Unicorn::HttpServer class. Unix systems programming experience will come in handy (or be learned) here. === Documentation Please wrap documentation at 72 characters-per-line or less (long URLs are exempt) so it is comfortably readable from terminals. When referencing mailing list posts, use https://yhbt.net/unicorn-public/$MESSAGE_ID/ if possible since the Message-ID remains searchable even if a particular site becomes unavailable. === Ruby/C Compatibility We target C Ruby 2.5 and later. We need the Ruby implementation to support fork, exec, pipe, UNIX signals, access to integer file descriptors and ability to use unlinked files. All of our C code is OS-independent and should run on compilers supported by the versions of Ruby we target. === Ragel Compatibility We target the latest released version of Ragel in Debian and will update our code to keep up with new releases. Packaged tarballs and gems include the generated source code so they will remain usable if compatibility is broken. == Contributing Contributions are welcome in the form of patches, pull requests, code review, testing, documentation, user support or any other feedback is welcome. The mailing list is the central coordination point for all user and developer feedback and bug reports. === Submitting Patches Follow conventions already established in the code and do not exceed 80 characters per line. Inline patches (from "git format-patch -M") to the mailing list are preferred because they allow code review and comments in the reply to the patch. We will adhere to mostly the same conventions for patch submissions as git itself. See the {SubmittingPatches}[https://git.kernel.org/cgit/git/git.git/tree/Documentation/SubmittingPatches] document distributed with git on on patch submission guidelines to follow. Just don't email the git mailing list or maintainer with Unicorn patches :) == Building a Gem You can build the Unicorn gem with the following command: gmake gem == Running Development Versions It is easy to install the contents of your git working directory: Via RubyGems gmake install-gem ISSUES000066400000000000000000000113771471646016400120360ustar00rootroot00000000000000= Issues mailto:unicorn-public@yhbt.net is the best place to report bugs, submit patches and/or obtain support after you have searched the {email archives}[https://yhbt.net/unicorn-public/] and {documentation}[https://yhbt.net/unicorn/]. * No subscription will ever be required to email us * Cc: all participants in a thread or commit, as subscription is optional * Do not {top post}[http://catb.org/jargon/html/T/top-post.html] in replies * Quote as little as possible of the message you're replying to * Do not send HTML mail or images, they hurt reader privacy and will be flagged as spam * Anonymous and pseudonymous messages will ALWAYS be welcome * The email submission port (587) is enabled on the yhbt.net MX: https://yhbt.net/unicorn-public/20141004232241.GA23908@dcvr.yhbt.net/t/ We will never have a centralized or formal bug tracker. Instead we can interoperate with any bug tracker which can Cc: us plain-text to mailto:unicorn-public@yhbt.net This includes the Debian BTS at https://bugs.debian.org/unicorn and possibly others. unicorn is a server; it does not depend on graphics/audio. Nobody communicating with us will ever be expected to go through the trouble of setting up graphics nor audio support. If your issue is of a sensitive nature or you're just shy in public, use anonymity tools such as Tor or Mixmaster; and rely on the public mail archives for responses. Be sure to scrub sensitive log messages and such. If you don't get a response within a few days, we may have forgotten about it so feel free to ask again. The project does not and will never endorse nor promote commercial services (including support). The author of unicorn must never be allowed to profit off the damage it's done to the entire Ruby world. == Bugs in related projects unicorn is sometimes affected by bugs in its dependencies. Bugs triggered by unicorn in mainline Ruby, rack, GNU C library (glibc), or the Linux kernel will be reported upstream and fixed. For bugs in Ruby itself, we may forward bugs to https://bugs.ruby-lang.org/ and discuss+fix them on the ruby-core list at mailto:ruby-core@ruby-lang.org Subscription to post is required to ruby-core, unfortunately: mailto:ruby-core-request@ruby-lang.org?subject=subscribe Unofficial archives are available at: https://public-inbox.org/ruby-core/ For uncommon bugs in Rack, we may forward bugs to mailto:rack-devel@googlegroups.com and discuss there. Subscription (without any web UI or Google account) is possible via: mailto:rack-devel+subscribe@googlegroups.com Note: not everyone can use the proprietary bug tracker used by Rack, but their mailing list remains operational. Unofficial archives are available at: https://public-inbox.org/rack-devel/ Uncommon bugs we encounter in the Linux kernel should be Cc:-ed to the Linux kernel mailing list (LKML) at mailto:linux-kernel@vger.kernel.org and subsystem maintainers such as mailto:netdev@vger.kernel.org (for networking issues). It is expected practice to Cc: anybody involved with any problematic commits (including those in the Signed-off-by: and other trailer lines). No subscription is necessary, and the our mailing list follows the same conventions as LKML for interopability. Archives are available at https://lore.kernel.org/lkml/ There is a kernel.org Bugzilla instance, but it is ignored by most. Likewise for any rare glibc bugs we might encounter, we should Cc: mailto:libc-alpha@sourceware.org Archives are available at: https://inbox.sourceware.org/libc-alpha/ Keep in mind glibc upstream does use Bugzilla for tracking bugs: https://sourceware.org/bugzilla/ == Submitting Patches See the HACKING document (and additionally, the {SubmittingPatches}[https://git.kernel.org/cgit/git/git.git/tree/Documentation/SubmittingPatches] document distributed with git) on guidelines for patch submission. == Contact Info Mail is publicly-archived, SMTP subscription is discouraged to avoid servers being a single-point-of-failure, so Cc: all participants. The HTTP(S) archives have links to per-thread Atom feeds and downloadable mboxes. Read-only IMAP(S) folders, POP3, and NNTP(S) newsgroups are available. * https://yhbt.net/unicorn-public/ * http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/ * imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0 * imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0 * nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn * nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general * https://yhbt.net/unicorn-public/_/text/help/#pop3 Full Atom feeds: * https://yhbt.net/unicorn-public/new.atom * http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/unicorn-public/new.atom We only accept plain-text mail: mailto:unicorn-public@yhbt.net KNOWN_ISSUES000066400000000000000000000074551471646016400130140ustar00rootroot00000000000000= Known Issues Occasionally odd {issues}[link:ISSUES.html] arise without a transparent or acceptable solution. Those issues are documented here. * Some libraries/applications may install signal handlers which conflict with signal handlers unicorn uses. Leaving "preload_app false" (the default) will allow unicorn to always override existing signal handlers. * Issues with FreeBSD jails can be worked around as documented by Tatsuya Ono: https://yhbt.net/unicorn-public/CAHBuKRj09FdxAgzsefJWotexw-7JYZGJMtgUp_dhjPz9VbKD6Q@mail.gmail.com/ * PRNGs (pseudo-random number generators) loaded before forking (e.g. "preload_app true") may need to have their internal state reset in the after_fork hook. Starting with unicorn 3.6.1, we have builtin workarounds for Kernel#rand and OpenSSL::Random users, but applications may use other PRNGs. * For notes on sandboxing tools such as Bundler or Isolate, see the {Sandbox}[link:Sandbox.html] page. * nginx with "sendfile on" under FreeBSD 8 is broken when uploads are buffered to disk. Disabling sendfile is required to work around this bug which should be fixed in newer versions of FreeBSD. * When using "preload_app true", with apps using background threads need to restart them in the after_fork hook because threads are never shared with child processes. Additionally, any synchronization primitives (Mutexes, Monitors, ConditionVariables) should be reinitialized in case they are held during fork time to avoid deadlocks. The core Ruby Logger class needlessly uses a MonitorMutex which can be disabled with a {monkey patch}[link:examples/logger_mp_safe.rb] == Known Issues (Old) * Under some versions of Ruby 1.8, it is necessary to call +srand+ in an after_fork hook to get correct random number generation. We have a builtin workaround for this starting with unicorn 3.6.1 See http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/36450 * On Ruby 1.8 prior to Ruby 1.8.7-p248, *BSD platforms have a broken stdio that causes failure for file uploads larger than 112K. Upgrade your version of Ruby or continue using unicorn 1.x/3.4.x. * Under Ruby 1.9.1, methods like Array#shuffle and Array#sample will segfault if called after forking. Upgrade to Ruby 1.9.2 or call "Kernel.rand" in your after_fork hook to reinitialize the random number generator. See http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/28655 * Rails 2.3.2 bundles its own version of Rack. This may cause subtle bugs when simultaneously loaded with the system-wide Rack Rubygem which unicorn depends on. Upgrading to Rails 2.3.4 (or later) is strongly recommended for all Rails 2.3.x users for this (and security reasons). Rails 2.2.x series (or before) did not bundle Rack and are should be unnaffected. If there is any reason which forces your application to use Rails 2.3.2 and you have no other choice, then you may edit your unicorn gemspec and remove the Rack dependency. ref: https://yhbt.net/unicorn-public/20091014221552.GA30624@dcvr.yhbt.net/ Note: the workaround described in the article above only made the issue more subtle and we didn't notice them immediately. * WONTFIX: code reloading and restarts with Sinatra 0.3.x (and likely older versions) apps is broken. The workaround is to force production mode to disable code reloading as well as disabling "run" in your Sinatra application: set :env, :production set :run, false Since this is no longer an issue with Sinatra 0.9.x apps, this will not be fixed on our end. Since unicorn is itself the application launcher, the at_exit handler used in old Sinatra always caused Mongrel to be launched whenever a unicorn worker was about to exit. Also remember we're capable of replacing the running binary without dropping any connections regardless of framework :) LICENSE000066400000000000000000000057351471646016400121060ustar00rootroot00000000000000Unicorn is copyrighted free software by all contributors, see logs in revision control for names and email addresses of all of them. You can redistribute it and/or modify it under either the terms of the GNU General Public License (GPL) as published by the Free Software Foundation (FSF), either version 2 of the License, or (at your option) any later version. We currently prefer the GPLv3 or later for derivative works, but the GPLv2 is fine. The complete texts of the GPLv2 and GPLv3 are below: GPLv2 - https://www.gnu.org/licenses/gpl-2.0.txt GPLv3 - https://www.gnu.org/licenses/gpl-3.0.txt You may (against our _preference_) also use the Ruby 1.8 license terms which we inherited from the original Mongrel project when we forked it: === Ruby 1.8-specific terms (if you're not using the GPL) 1. You may make and give away verbatim copies of the source form of the software without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may modify your copy of the software in any way, provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or by allowing the author to include your modifications in the software. b) use the modified software only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided. d) make other distribution arrangements with the author. 3. You may distribute the software in object code or executable form, provided that you do at least ONE of the following: a) distribute the executables and library files of the software, together with instructions (in the manual page or equivalent) on where to get the original distribution. b) accompany the distribution with the machine-readable source of the software. c) give non-standard executables non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 4. You may modify and include the part of the software into any other software (possibly commercial). But some files in the distribution are not written by the author, so that they are not under this terms. 5. The scripts and library files supplied as input to or produced as output from the software do not automatically fall under the copyright of the software, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this software. 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Links000066400000000000000000000036251471646016400121000ustar00rootroot00000000000000= Related Projects If you're interested in unicorn, you may be interested in some of the projects listed below. If you have any links to add/change/remove, please tell us at mailto:unicorn-public@yhbt.net! == Disclaimer The unicorn project is not responsible for the content in these links. Furthermore, the unicorn project has never, does not and will never endorse: * any for-profit entities or services * any non-{Free Software}[https://www.gnu.org/philosophy/free-sw.html] The existence of these links does not imply endorsement of any entities or services behind them. === For use with unicorn * {Bluepill}[https://github.com/arya/bluepill] - a simple process monitoring tool written in Ruby * {golden_brindle}[https://github.com/simonoff/golden_brindle] - tool to manage multiple unicorn instances/applications on a single server * {raindrops}[https://yhbt.net/raindrops/] - real-time stats for preforking Rack servers * {UnXF}[https://yhbt.net/unxf/] Un-X-Forward* the Rack environment, useful since unicorn is designed to be deployed behind a reverse proxy. === unicorn is written to work with * {Rack}[https://rack.github.io/] - a minimal interface between webservers supporting Ruby and Ruby frameworks * {Ruby}[https://www.ruby-lang.org/en/] - the programming language of Rack and unicorn * {nginx}[https://nginx.org/] (Free versions) - the reverse proxy for use with unicorn === Derivatives * {Green Unicorn}[https://gunicorn.org/] - a Python version of unicorn * {Starman}[https://metacpan.org/release/Starman/] - Plack/PSGI version of unicorn === Prior Work * {Mongrel}[https://rubygems.org/gems/mongrel] - the awesome webserver unicorn is based on. A historical archive of the mongrel dev list featuring early discussions of unicorn is available at: https://yhbt.net/mongrel-devel/ * {david}[https://yhbt.net/david.git] - a tool to explain why you need nginx in front of unicorn PHILOSOPHY000066400000000000000000000145021471646016400125120ustar00rootroot00000000000000= The Philosophy Behind unicorn Being a server that only runs on Unix-like platforms, unicorn is strongly tied to the Unix philosophy of doing one thing and (hopefully) doing it well. Despite using HTTP, unicorn is strictly a _backend_ application server for running Rack-based Ruby applications. == Avoid Complexity Instead of attempting to be efficient at serving slow clients, unicorn relies on a buffering reverse proxy to efficiently deal with slow clients. unicorn uses an old-fashioned preforking worker model with blocking I/O. Our processing model is the antithesis of more modern (and theoretically more efficient) server processing models using threads or non-blocking I/O with events. === Threads and Events Are Hard ...to many developers. Reasons for this is beyond the scope of this document. unicorn avoids concurrency within each worker process so you have fewer things to worry about when developing your application. Of course unicorn can use multiple worker processes to utilize multiple CPUs or spindles. Applications can still use threads internally, however. == Slow Clients Are Problematic Most benchmarks we've seen don't tell you this, and unicorn doesn't care about slow clients... but you should. A "slow client" can be any client outside of your datacenter. Network traffic within a local network is always faster than traffic that crosses outside of it. The laws of physics do not allow otherwise. Persistent connections were introduced in HTTP/1.1 reduce latency from connection establishment and TCP slow start. They also waste server resources when clients are idle. Persistent connections mean one of the unicorn worker processes (depending on your application, it can be very memory hungry) would spend a significant amount of its time idle keeping the connection alive and not doing anything else. Being single-threaded and using blocking I/O, a worker cannot serve other clients while keeping a connection alive. Thus unicorn does not implement persistent connections. If your application responses are larger than the socket buffer or if you're handling large requests (uploads), worker processes will also be bottlenecked by the speed of the *client* connection. You should not allow unicorn to serve clients outside of your local network. == Application Concurrency != Network Concurrency Performance is asymmetric across the different subsystems of the machine and parts of the network. CPUs and main memory can process gigabytes of data in a second; clients on the Internet are usually only capable of a tiny fraction of that. unicorn deployments should avoid dealing with slow clients directly and instead rely on a reverse proxy to shield it from the effects of slow I/O. == Improved Performance Through Reverse Proxying By acting as a buffer to shield unicorn from slow I/O, a reverse proxy will inevitably incur overhead in the form of extra data copies. However, as I/O within a local network is fast (and faster still with local sockets), this overhead is negligible for the vast majority of HTTP requests and responses. The ideal reverse proxy complements the weaknesses of unicorn. A reverse proxy for unicorn should meet the following requirements: 1. It should fully buffer all HTTP requests (and large responses). Each request should be "corked" in the reverse proxy and sent as fast as possible to the backend unicorn processes. This is the most important feature to look for when choosing a reverse proxy for unicorn. 2. It should spend minimal time in userspace. Network (and disk) I/O are system-level tasks and usually managed by the kernel. This may change if userspace TCP stacks become more popular in the future; but the reverse proxy should not waste time with application-level logic. These concerns should be separated 3. It should avoid context switches and CPU scheduling overhead. In many (most?) cases, network devices and their interrupts are only be handled by one CPU at a time. It should avoid contention within the system by serializing all network I/O into one (or few) userspace processes. Network I/O is not a CPU-intensive task and it is not helpful to use multiple CPU cores (at least not for GigE). 4. It should efficiently manage persistent connections (and pipelining) to slow clients. If you care to serve slow clients outside your network, then these features of HTTP/1.1 will help. 5. It should (optionally) serve static files. If you have static files on your site (especially large ones), they are far more efficiently served with as few data copies as possible (e.g. with sendfile() to completely avoid copying the data to userspace). nginx is the only (Free) solution we know of that meets the above requirements. Indeed, the folks behind unicorn have deployed nginx as a reverse-proxy not only for Ruby applications, but also for production applications running Apache/mod_perl, Apache/mod_php and Apache Tomcat. In every single case, performance improved because application servers were able to use backend resources more efficiently and spend less time waiting on slow I/O. == Worse Is Better Requirements and scope for applications change frequently and drastically. Thus languages like Ruby and frameworks like Rails were built to give developers fewer things to worry about in the face of rapid change. On the other hand, stable protocols which host your applications (HTTP and TCP) only change rarely. This is why we recommend you NOT tie your rapidly-changing application logic directly into the processes that deal with the stable outside world. Instead, use HTTP as a common RPC protocol to communicate between your frontend and backend. In short: separate your concerns. Of course a theoretical "perfect" solution would combine the pieces and _maybe_ give you better performance at the end of the day, but that is not the Unix way. == Just Worse in Some Cases unicorn is not suited for all applications. unicorn is optimized for applications that are CPU/memory/disk intensive and spend little time waiting on external resources (e.g. a database server or external API). unicorn is highly inefficient for Comet/reverse-HTTP/push applications where the HTTP connection spends a large amount of time idle. Nevertheless, the ease of troubleshooting, debugging, and management of unicorn may still outweigh the drawbacks for these applications. README000066400000000000000000000141001471646016400117430ustar00rootroot00000000000000= unicorn: Rack HTTP server for fast clients and Unix unicorn is an HTTP server for Rack applications that has done decades of damage to the entire Ruby ecosystem due to its ability to tolerate (and thus encourage) bad code. It is only designed to handle fast clients on low-latency, high-bandwidth connections and take advantage of features in Unix/Unix-like kernels. Slow clients must only be served by placing a reverse proxy capable of fully buffering both the the request and response in between unicorn and slow clients. == Features * Designed for Rack, Unix, fast clients, and ease-of-debugging. We cut out everything that is better supported by the operating system, {nginx}[https://nginx.org/] or {Rack}[https://rack.github.io/]. * Compatible with Ruby 2.5 and later. * Process management: unicorn reaps and restarts workers that die from broken code. There is no need to manage multiple processes or ports yourself. unicorn can spawn and manage any number of worker processes you choose to scale to your backend. * Load balancing is done entirely by the operating system kernel. Requests never pile up behind a busy worker process. * Does not care if your application is thread-safe or not, workers all run within their own isolated address space and only serve one client at a time for maximum robustness. * Builtin reopening of all log files in your application via USR1 signal. This allows logrotate to rotate files atomically and quickly via rename instead of the racy and slow copytruncate method. unicorn also takes steps to ensure multi-line log entries from one request all stay within the same file. * nginx-style binary upgrades without losing connections. You can upgrade unicorn, your entire application, libraries and even your Ruby interpreter without dropping clients. * transparent upgrades using systemd socket activation is supported since unicorn 5.0 * before_fork and after_fork hooks in case your application has special needs when dealing with forked processes. These should not be needed when the "preload_app" directive is false (the default). * Can be used with copy-on-write-friendly GC in Ruby 2.0+ to save memory (by setting "preload_app" to true). * Able to listen on multiple interfaces including UNIX sockets, each worker process can also bind to a private port via the after_fork hook for easy debugging. * Simple and easy Ruby DSL for configuration. * Decodes chunked requests on-the-fly. == License unicorn is copyright all contributors (see logs in git). It is based on Mongrel 1.1.5. Mongrel is copyright 2007 Zed A. Shaw and contributors. unicorn is licensed under (your choice) of the GPLv2 or later (GPLv3+ preferred), or Ruby (1.8)-specific terms. See the included LICENSE file for details. unicorn is 100% Free Software (including all development tools used). == Install The library consists of a C extension so you'll need a C compiler and Ruby development libraries/headers. You may install it via RubyGems on RubyGems.org: gem install unicorn You can get the latest source via git from the following locations (these versions may not be stable): git clone https://yhbt.net/unicorn.git git clone https://repo.or.cz/unicorn.git # mirror You may browse the code from the web: * https://yhbt.net/unicorn.git * https://repo.or.cz/w/unicorn.git (gitweb) See the HACKING guide on how to contribute and build prerelease gems from git. == Usage === Rack (including Rails 3+) applications In APP_ROOT, run: unicorn unicorn will bind to all interfaces on TCP port 8080 by default. You may use the +--listen/-l+ switch to bind to a different address:port or a UNIX socket. === Configuration File(s) unicorn will look for the config.ru file used by rackup in APP_ROOT. For deployments, it can use a config file for unicorn-specific options specified by the +--config-file/-c+ command-line switch. See Unicorn::Configurator for the syntax of the unicorn-specific options. The default settings are designed for maximum out-of-the-box compatibility with existing applications. Most command-line options for other Rack applications (above) are also supported. Run `unicorn -h` to see command-line options. == Disclaimer There is NO WARRANTY whatsoever if anything goes wrong, but {let us know}[link:ISSUES.html] and maybe someone can fix it. No commercial support will ever be provided by the amateur maintainer. unicorn is designed to only serve fast clients either on the local host or a fast LAN. See the PHILOSOPHY and DESIGN documents for more details regarding this. The use of unicorn in new deployments is STRONGLY DISCOURAGED due to the damage done to the entire Ruby ecosystem. Its unintentional popularity set Ruby back decades in parallelism, concurrency and robustness since it prolongs and proliferates the existence of poorly-written code. unicorn hackers are NOT responsible for your supply chain security: read and understand it yourself or get someone you trust to audit it. Malicious commits and releases will be made if under duress. The only defense you'll ever have is from reviewing the source code. No user or contributor will ever be expected to sacrifice their own security by running JavaScript or revealing any personal information. == Contact All feedback (bug reports, user/development dicussion, patches, pull requests) go to the public mailbox. See the ISSUES document for information on posting to mailto:unicorn-public@yhbt.net Mirror-able mail archives are at https://yhbt.net/unicorn-public/ Read-only NNTP access is available at: nntps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn and nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general Read-only IMAP access is also available at: imaps://;AUTH=ANONYMOUS@yhbt.net/inbox.comp.lang.ruby.unicorn.0 and imap://;AUTH=ANONYMOUS@7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.unicorn.0 Archives are also available over POP3, instructions at: https://yhbt.net/unicorn-public/_/text/help/#pop3 For the latest on unicorn releases, you may also finger us at unicorn@yhbt.net or check our NEWS page (and subscribe to our Atom feed). Rakefile000066400000000000000000000011421471646016400125320ustar00rootroot00000000000000# frozen_string_literal: false # optional rake-compiler support in case somebody needs to cross compile begin mk = "ext/unicorn_http/Makefile" if File.readable?(mk) warn "run 'gmake -C ext/unicorn_http clean' and\n" \ "remove #{mk} before using rake-compiler" elsif ENV['VERSION'] unless File.readable?("ext/unicorn_http/unicorn_http.c") abort "run 'gmake ragel' or 'make ragel' to generate the Ragel source" end spec = Gem::Specification.load('unicorn.gemspec') require 'rake/extensiontask' Rake::ExtensionTask.new('unicorn_http', spec) end rescue LoadError end SIGNALS000066400000000000000000000122551471646016400121170ustar00rootroot00000000000000== Signal handling In general, signals need only be sent to the master process. However, the signals Unicorn uses internally to communicate with the worker processes are documented here as well. With the exception of TTIN/TTOU, signal handling matches the behavior of {nginx}[http://nginx.org/] so it should be possible to easily share process management scripts between Unicorn and nginx. One example init script is distributed with unicorn: https://yhbt.net/unicorn/examples/init.sh === Master Process * HUP - reloads config file and gracefully restart all workers. If the "preload_app" directive is false (the default), then workers will also pick up any application code changes when restarted. If "preload_app" is true, then application code changes will have no effect; USR2 + QUIT (see below) must be used to load newer code in this case. When reloading the application, +Gem.refresh+ will be called so updated code for your application can pick up newly installed RubyGems. It is not recommended that you uninstall libraries your application depends on while Unicorn is running, as respawned workers may enter a spawn loop when they fail to load an uninstalled dependency. * INT/TERM - quick shutdown, kills all workers immediately * QUIT - graceful shutdown, waits for workers to finish their current request before finishing. * USR1 - reopen all logs owned by the master and all workers See Unicorn::Util.reopen_logs for what is considered a log. * USR2 - reexecute the running binary. A separate QUIT should be sent to the original process once the child is verified to be up and running. * WINCH - gracefully stops workers but keep the master running. This will only work for daemonized processes. * TTIN - increment the number of worker processes by one * TTOU - decrement the number of worker processes by one === Worker Processes Note: as of unicorn 4.8, the master uses a pipe to signal workers instead of kill(2) for most cases. Using signals still (and works and remains supported for external tools/libraries), however. Sending signals directly to the worker processes should not normally be needed. If the master process is running, any exited worker will be automatically respawned. * INT/TERM - Quick shutdown, immediately exit. Unless WINCH has been sent to the master (or the master is killed), the master process will respawn a worker to replace this one. Immediate shutdown is still triggered using kill(2) and not the internal pipe as of unicorn 4.8 * QUIT - Gracefully exit after finishing the current request. Unless WINCH has been sent to the master (or the master is killed), the master process will respawn a worker to replace this one. * USR1 - Reopen all logs owned by the worker process. See Unicorn::Util.reopen_logs for what is considered a log. Log files are not reopened until it is done processing the current request, so multiple log lines for one request (as done by Rails) will not be split across multiple logs. It is NOT recommended to send the USR1 signal directly to workers via "killall -USR1 unicorn" if you are using user/group-switching support in your workers. You will encounter incorrect file permissions and workers will need to be respawned. Sending USR1 to the master process first will ensure logs have the correct permissions before the master forwards the USR1 signal to workers. === Procedure to replace a running unicorn executable You may replace a running instance of unicorn with a new one without losing any incoming connections. Doing so will reload all of your application code, Unicorn config, Ruby executable, and all libraries. The only things that will not change (due to OS limitations) are: 1. The path to the unicorn executable script. If you want to change to a different installation of Ruby, you can modify the shebang line to point to your alternative interpreter. The procedure is exactly like that of nginx: 1. Send USR2 to the master process 2. Check your process manager or pid files to see if a new master spawned successfully. If you're using a pid file, the old process will have ".oldbin" appended to its path. You should have two master instances of unicorn running now, both of which will have workers servicing requests. Your process tree should look something like this: unicorn master (old) \_ unicorn worker[0] \_ unicorn worker[1] \_ unicorn worker[2] \_ unicorn worker[3] \_ unicorn master \_ unicorn worker[0] \_ unicorn worker[1] \_ unicorn worker[2] \_ unicorn worker[3] 3. You can now send WINCH to the old master process so only the new workers serve requests. If your unicorn process is bound to an interactive terminal (not daemonized), you can skip this step. Step 5 will be more difficult but you can also skip it if your process is not daemonized. 4. You should now ensure that everything is running correctly with the new workers as the old workers die off. 5. If everything seems ok, then send QUIT to the old master. You're done! If something is broken, then send HUP to the old master to reload the config and restart its workers. Then send QUIT to the new master process. Sandbox000066400000000000000000000073621471646016400124200ustar00rootroot00000000000000= Tips for using unicorn with Sandbox installation tools Since unicorn includes executables and is usually used to start a Ruby process, there are certain caveats to using it with tools that sandbox RubyGems installations such as {Bundler}[https://bundler.io/] or {Isolate}[https://github.com/jbarnette/isolate]. == General deployment If you're sandboxing your unicorn installation and using Capistrano (or similar), it's required that you sandbox your RubyGems in a per-application shared directory that can be used between different revisions. unicorn will stash its original command-line at startup for the USR2 upgrades, and cleaning up old revisions will cause revision-specific installations of unicorn to go missing and upgrades to fail. If you find yourself in this situation and can't afford downtime, you can override the existing unicorn executable path in the config file like this: Unicorn::HttpServer::START_CTX[0] = "/some/path/to/bin/unicorn" Then use HUP to reload, and then continue with the USR2+QUIT upgrade sequence. Environment variable pollution when exec-ing a new process (with USR2) is the primary issue with sandboxing tools such as Bundler and Isolate. == Bundler === Running If you're bundling unicorn, use "bundle exec unicorn" (or "bundle exec unicorn_rails") to start unicorn with the correct environment variables ref: https://yhbt.net/unicorn-public/9ECF07C4-5216-47BE-961D-AFC0F0C82060@internetfamo.us/ Otherwise (if you choose to not sandbox your unicorn installation), we expect the tips for Isolate (below) apply, too. === RUBYOPT pollution from SIGUSR2 upgrades This is no longer be an issue as of bundler 0.9.17 ref: https://yhbt.net/unicorn-public/8FC34B23-5994-41CC-B5AF-7198EF06909E@tramchase.com/ === BUNDLE_GEMFILE for Capistrano users You may need to set or reset the BUNDLE_GEMFILE environment variable in the before_exec hook: before_exec do |server| ENV["BUNDLE_GEMFILE"] = "/path/to/app/current/Gemfile" end === Other ENV pollution issues If you're using an older Bundler version (0.9.x), you may need to set or reset GEM_HOME, GEM_PATH and PATH environment variables in the before_exec hook as illustrated by https://gist.github.com/534668 === Ruby 2.0.0 close-on-exec and SIGUSR2 incompatibility Ruby 2.0.0 enforces FD_CLOEXEC on file descriptors by default. unicorn has been prepared for this behavior since unicorn 4.1.0, and bundler needs the "--keep-file-descriptors" option for "bundle exec": https://bundler.io/man/bundle-exec.1.html == Isolate === Running Installing "unicorn" as a system-wide Rubygem and using the isolate gem may cause issues if you're using any of the bundled application-level libraries in unicorn/app/* (for compatibility with CGI-based applications, Rails <= 2.2.2, or ExecCgi). For now workarounds include doing one of the following: 1. Isolating unicorn, setting GEM_HOME to your Isolate path, and running the isolated version of unicorn. You *must* set GEM_HOME before running your isolated unicorn install in this way. 2. Installing the same version of unicorn as a system-wide Rubygem *and* isolating unicorn as well. 3. Explicitly setting RUBYLIB or $LOAD_PATH to include any gem path where the unicorn gem is installed (e.g. /usr/lib/ruby/gems/3.0.0/gems/unicorn-VERSION/lib) === RUBYOPT pollution from SIGUSR2 upgrades If you are using Isolate, using Isolate 2.x is strongly recommended as environment modifications are idempotent. If you are stuck with 1.x versions of Isolate, it is recommended that you disable it with the before_exec hook prevent the PATH and RUBYOPT environment variable modifications from propagating between upgrades in your Unicorn config file: before_exec do |server| Isolate.disable end TODO000066400000000000000000000001101471646016400115470ustar00rootroot00000000000000* improve test suite (port to Perl 5 for stability and maintainability) TUNING000066400000000000000000000122671471646016400120260ustar00rootroot00000000000000= Tuning unicorn unicorn performance is generally as good as a (mostly) Ruby web server can provide. Most often the performance bottleneck is in the web application running on Unicorn rather than Unicorn itself. == unicorn Configuration See Unicorn::Configurator for details on the config file format. +worker_processes+ is the most-commonly needed tuning parameter. === Unicorn::Configurator#worker_processes * worker_processes should be scaled to the number of processes your backend system(s) can support. DO NOT scale it to the number of external network clients your application expects to be serving. unicorn is NOT for serving slow clients, that is the job of nginx. * worker_processes should be *at* *least* the number of CPU cores on a dedicated server (unless you do not have enough memory). If your application has occasionally slow responses that are /not/ CPU-intensive, you may increase this to workaround those inefficiencies. * Under Ruby 2.2 or later, Etc.nprocessors may be used to determine the number of CPU cores present. * worker_processes may be increased for Unicorn::OobGC users to provide more consistent response times. * Never, ever, increase worker_processes to the point where the system runs out of physical memory and hits swap. Production servers should never see heavy swap activity. === Unicorn::Configurator#listen Options * Setting a very low value for the :backlog parameter in "listen" directives can allow failover to happen more quickly if your cluster is configured for it. * If you're doing extremely simple benchmarks and getting connection errors under high request rates, increasing your :backlog parameter above the already-generous default of 1024 can help avoid connection errors. Keep in mind this is not recommended for real traffic if you have another machine to failover to (see above). * :rcvbuf and :sndbuf parameters generally do not need to be set for TCP listeners under Linux 2.6 because auto-tuning is enabled. UNIX domain sockets do not have auto-tuning buffer sizes; so increasing those will allow syscalls and task switches to be saved for larger requests and responses. If your app only generates small responses or expects small requests, you may shrink the buffer sizes to save memory, too. * Having socket buffers too large can also be detrimental or have little effect. Huge buffers can put more pressure on the allocator and may also thrash CPU caches, cancelling out performance gains one would normally expect. * UNIX domain sockets are slightly faster than TCP sockets, but only work if nginx is on the same machine. == Other unicorn settings * Setting "preload_app true" can allow copy-on-write-friendly GC to be used to save memory. It will probably not work out of the box with applications that open sockets or perform random I/O on files. Databases like TokyoCabinet use concurrency-safe pread()/pwrite() functions for safe sharing of database file descriptors across processes. * On POSIX-compliant filesystems, it is safe for multiple threads or processes to append to one log file as long as all the processes are have them unbuffered (File#sync = true) or they are record(line)-buffered in userspace before any writes. == Kernel Parameters (Linux sysctl and sysfs) WARNING: Do not change system parameters unless you know what you're doing! * Transparent hugepages (THP) improves performance in many cases, but can also increase memory use when relying on a copy-on-write(CoW)-friendly GC (Ruby 2.0+) with "preload_app true". CoW operates at the page level, so writing to a huge page would trigger a 2 MB copy (x86-64), as opposed to a 4 KB copy on a regular (non-huge) page. Consider only allowing THP to be used when it is requested via the madvise(2) syscall: echo madvise >/sys/kernel/mm/transparent_hugepage/enabled Or disabling it system-wide, via "never". n.b. "page" in this context only applies to the OS kernel, Ruby GC implementations also use this term for the same concept in a way that is agnostic to the OS. * net.core.rmem_max and net.core.wmem_max can increase the allowed size of :rcvbuf and :sndbuf respectively. This is mostly only useful for UNIX domain sockets which do not have auto-tuning buffer sizes. * For load testing/benchmarking with UNIX domain sockets, you should consider increasing net.core.somaxconn or else nginx will start failing to connect under heavy load. You may also consider setting a higher :backlog to listen on as noted earlier. * If you're running out of local ports, consider lowering net.ipv4.tcp_fin_timeout to 20-30 (default: 60 seconds). Also consider widening the usable port range by changing net.ipv4.ip_local_port_range. * Setting net.ipv4.tcp_timestamps=1 will also allow setting net.ipv4.tcp_tw_reuse=1 and net.ipv4.tcp_tw_recycle=1, which along with the above settings can slow down port exhaustion. Not all networks are compatible with these settings, check with your friendly network administrator before changing these. * Increasing the MTU size can reduce framing overhead for larger transfers. One often-overlooked detail is that the loopback device (usually "lo") can have its MTU increased, too. archive/000077500000000000000000000000001471646016400125105ustar00rootroot00000000000000archive/.gitignore000066400000000000000000000000261471646016400144760ustar00rootroot00000000000000/data /news /requests archive/slrnpull.conf000066400000000000000000000002731471646016400152340ustar00rootroot00000000000000# group_name max expire headers_only gmane.comp.lang.ruby.unicorn.general 1000000000 1000000000 0 # usage: slrnpull -d $PWD -h news.gmane.io --no-post bin/000077500000000000000000000000001471646016400116375ustar00rootroot00000000000000bin/unicorn000077500000000000000000000072241471646016400132470ustar00rootroot00000000000000#!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby # -*- encoding: binary -*- # frozen_string_literal: false require 'unicorn/launcher' require 'optparse' ENV["RACK_ENV"] ||= "development" rackup_opts = Unicorn::Configurator::RACKUP options = rackup_opts[:options] set_no_default_middleware = true op = OptionParser.new("", 24, ' ') do |opts| cmd = File.basename($0) opts.banner = "Usage: #{cmd} " \ "[ruby options] [#{cmd} options] [rackup config file]" opts.separator "Ruby options:" lineno = 1 opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line| eval line, TOPLEVEL_BINDING, "-e", lineno lineno += 1 end opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do $DEBUG = true end opts.on("-w", "--warn", "turn warnings on for your script") do $-w = true end opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path| $LOAD_PATH.unshift(*path.split(':')) end opts.on("-r", "--require LIBRARY", "require the library, before executing your script") do |library| require library end opts.separator "#{cmd} options:" # some of these switches exist for rackup command-line compatibility, opts.on("-o", "--host HOST", "listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h| rackup_opts[:host] = h rackup_opts[:set_listener] = true end opts.on("-p", "--port PORT", Integer, "use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |port| rackup_opts[:port] = port rackup_opts[:set_listener] = true end opts.on("-E", "--env RACK_ENV", "use RACK_ENV for defaults (default: development)") do |e| ENV["RACK_ENV"] = e end opts.on("-N", "--no-default-middleware", "do not load middleware implied by RACK_ENV") do |e| rackup_opts[:no_default_middleware] = true if set_no_default_middleware end opts.on("-D", "--daemonize", "run daemonized in the background") do |d| rackup_opts[:daemonize] = !!d end opts.on("-P", "--pid FILE", "DEPRECATED") do |f| warn %q{Use of --pid/-P is strongly discouraged} warn %q{Use the 'pid' directive in the Unicorn config file instead} options[:pid] = f end opts.on("-s", "--server SERVER", "this flag only exists for compatibility") do |s| warn "-s/--server only exists for compatibility with rackup" end # Unicorn-specific stuff opts.on("-l", "--listen {HOST:PORT|PATH}", "listen on HOST:PORT or PATH", "this may be specified multiple times", "(default: #{Unicorn::Const::DEFAULT_LISTEN})") do |address| options[:listeners] << address end opts.on("-c", "--config-file FILE", "Unicorn-specific config file") do |f| options[:config_file] = f end # I'm avoiding Unicorn-specific config options on the command-line. # IMNSHO, config options on the command-line are redundant given # config files and make things unnecessarily complicated with multiple # places to look for a config option. opts.separator "Common options:" opts.on_tail("-h", "--help", "Show this message") do puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '') exit end opts.on_tail("-v", "--version", "Show version") do puts "#{cmd} v#{Unicorn::Const::UNICORN_VERSION}" exit end opts.parse! ARGV end set_no_default_middleware = false app = Unicorn.builder(ARGV[0] || 'config.ru', op) op = nil if $DEBUG require 'pp' pp({ :unicorn_options => options, :app => app, :daemonize => rackup_opts[:daemonize], }) end Unicorn::Launcher.daemonize!(options) if rackup_opts[:daemonize] Unicorn::HttpServer.new(app, options).start.join bin/unicorn_rails000077500000000000000000000142361471646016400144420ustar00rootroot00000000000000#!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby # -*- encoding: binary -*- # frozen_string_literal: false require 'unicorn/launcher' require 'optparse' require 'fileutils' ENV['RAILS_ENV'] ||= "development" rackup_opts = Unicorn::Configurator::RACKUP options = rackup_opts[:options] op = OptionParser.new("", 24, ' ') do |opts| cmd = File.basename($0) opts.banner = "Usage: #{cmd} " \ "[ruby options] [#{cmd} options] [rackup config file]" opts.separator "Ruby options:" lineno = 1 opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line| eval line, TOPLEVEL_BINDING, "-e", lineno lineno += 1 end opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do $DEBUG = true end opts.on("-w", "--warn", "turn warnings on for your script") do $-w = true end opts.on("-I", "--include PATH", "specify $LOAD_PATH (may be used more than once)") do |path| $LOAD_PATH.unshift(*path.split(':')) end opts.on("-r", "--require LIBRARY", "require the library, before executing your script") do |library| require library end opts.separator "#{cmd} options:" # some of these switches exist for rackup command-line compatibility, opts.on("-o", "--host HOST", "listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h| rackup_opts[:host] = h rackup_opts[:set_listener] = true end opts.on("-p", "--port PORT", Integer, "use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |port| rackup_opts[:port] = port rackup_opts[:set_listener] = true end opts.on("-E", "--env RAILS_ENV", "use RAILS_ENV for defaults (default: development)") do |e| ENV['RAILS_ENV'] = e end opts.on("-D", "--daemonize", "run daemonized in the background") do |d| rackup_opts[:daemonize] = !!d end # Unicorn-specific stuff opts.on("-l", "--listen {HOST:PORT|PATH}", "listen on HOST:PORT or PATH", "this may be specified multiple times", "(default: #{Unicorn::Const::DEFAULT_LISTEN})") do |address| options[:listeners] << address end opts.on("-c", "--config-file FILE", "Unicorn-specific config file") do |f| options[:config_file] = f end opts.on("-P PATH", "DEPRECATED") do |v| warn %q{Use of -P is ambiguous and discouraged} warn %q{Use --path or RAILS_RELATIVE_URL_ROOT instead} ENV['RAILS_RELATIVE_URL_ROOT'] = v end opts.on("--path PATH", "Runs Rails app mounted at a specific path.", "(default: /)") do |v| ENV['RAILS_RELATIVE_URL_ROOT'] = v end # I'm avoiding Unicorn-specific config options on the command-line. # IMNSHO, config options on the command-line are redundant given # config files and make things unnecessarily complicated with multiple # places to look for a config option. opts.separator "Common options:" opts.on_tail("-h", "--help", "Show this message") do puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '') exit end opts.on_tail("-v", "--version", "Show version") do puts "#{cmd} v#{Unicorn::Const::UNICORN_VERSION}" exit end opts.parse! ARGV end def rails_dispatcher if ::Rails::VERSION::MAJOR >= 3 && ::File.exist?('config/application.rb') if ::File.read('config/application.rb') =~ /^module\s+([\w:]+)\s*$/ app_module = Object.const_get($1) begin result = app_module::Application rescue NameError end end end if result.nil? && defined?(ActionController::Dispatcher) result = ActionController::Dispatcher.new end result || abort("Unable to locate the application dispatcher class") end def rails_builder(ru, op, daemonize) return Unicorn.builder(ru, op) if ru # allow Configurator to parse cli switches embedded in the ru file Unicorn::Configurator::RACKUP.update(:file => :rails, :optparse => op) # this lambda won't run until after forking if preload_app is false # this runs after config file reloading lambda do |x, server| # Rails 3 includes a config.ru, use it if we find it after # working_directory is bound. ::File.exist?('config.ru') and return Unicorn.builder('config.ru', op).call(x, server) # Load Rails and (possibly) the private version of Rack it bundles. begin require ::File.expand_path('config/boot') require ::File.expand_path('config/environment') rescue LoadError => err abort "#$0 must be run inside RAILS_ROOT: #{err.inspect}" end defined?(::Rails::VERSION::STRING) or abort "Rails::VERSION::STRING not defined by config/{boot,environment}" # it seems Rails >=2.2 support Rack, but only >=2.3 requires it old_rails = case ::Rails::VERSION::MAJOR when 0, 1 then true when 2 then Rails::VERSION::MINOR < 3 ? true : false else false end Rack::Builder.new do map_path = ENV['RAILS_RELATIVE_URL_ROOT'] || '/' if old_rails if map_path != '/' # patches + tests welcome, but I really cbf to deal with this # since all apps I've ever dealt with just use "/" ... warn "relative URL roots may not work for older Rails" end warn "LogTailer not available for Rails < 2.3" unless daemonize warn "Debugger not available" if $DEBUG require 'unicorn/app/old_rails' map(map_path) do use Unicorn::App::OldRails::Static run Unicorn::App::OldRails.new end else use Rails::Rack::LogTailer unless daemonize use Rails::Rack::Debugger if $DEBUG map(map_path) do unless defined?(ActionDispatch::Static) use Rails::Rack::Static end run rails_dispatcher end end end.to_app end end app = rails_builder(ARGV[0], op, rackup_opts[:daemonize]) op = nil if $DEBUG require 'pp' pp({ :unicorn_options => options, :app => app, :daemonize => rackup_opts[:daemonize], }) end # ensure Rails standard tmp paths exist options[:after_reload] = lambda do FileUtils.mkdir_p(%w(cache pids sessions sockets).map! { |d| "tmp/#{d}" }) end if rackup_opts[:daemonize] options[:pid] = "tmp/pids/unicorn.pid" Unicorn::Launcher.daemonize!(options) end Unicorn::HttpServer.new(app, options).start.join examples/000077500000000000000000000000001471646016400127055ustar00rootroot00000000000000examples/big_app_gc.rb000066400000000000000000000002561471646016400153070ustar00rootroot00000000000000# frozen_string_literal: false # see {Unicorn::OobGC}[https://yhbt.net/unicorn/Unicorn/OobGC.html] # Unicorn::OobGC was broken in Unicorn v3.3.1 - v3.6.1 and fixed in v3.6.2 examples/echo.ru000066400000000000000000000012321471646016400141710ustar00rootroot00000000000000#\-E none # frozen_string_literal: false # # Example application that echoes read data back to the HTTP client. # This emulates the old echo protocol people used to run. # # An example of using this in a client would be to run: # curl --no-buffer -T- http://host:port/ # # Then type random stuff in your terminal to watch it get echoed back! class EchoBody < Struct.new(:input) def each(&block) while buf = input.read(4096) yield buf end self end end run lambda { |env| /\A100-continue\z/i =~ env['HTTP_EXPECT'] and return [100, {}, []] [ 200, { 'Content-Type' => 'application/octet-stream' }, EchoBody.new(env['rack.input']) ] } examples/init.sh000066400000000000000000000037421471646016400142120ustar00rootroot00000000000000#!/bin/sh set -e ### BEGIN INIT INFO # Provides: unicorn # Required-Start: $local_fs $network # Required-Stop: $local_fs $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start/stop unicorn Rack app server ### END INIT INFO # Example init script, this can be used with nginx, too, # since nginx and unicorn 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/unicorn.pid CMD="/usr/bin/unicorn -D -c $APP_ROOT/config/unicorn.rb" INIT_CONF=$APP_ROOT/config/init.conf UPGRADE_DELAY=${UPGRADE_DELAY-2} action="$1" set -u test -f "$INIT_CONF" && . $INIT_CONF OLD="$PID.oldbin" cd $APP_ROOT || exit 1 sig () { test -s "$PID" && kill -$1 $(cat $PID) } oldsig () { test -s "$OLD" && kill -$1 $(cat $OLD) } 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 oldsig 0 then echo >&2 "Old upgraded process still running with $OLD" exit 1 fi cur_pid= if test -s "$PID" then cur_pid=$(cat $PID) fi if test -n "$cur_pid" && kill -USR2 "$cur_pid" && sleep $UPGRADE_DELAY && new_pid=$(cat $PID) && test x"$new_pid" != x"$cur_pid" && kill -0 "$new_pid" && kill -QUIT "$cur_pid" then n=$TIMEOUT while kill -0 "$cur_pid" 2>/dev/null && test $n -ge 0 do printf '.' && sleep 1 && n=$(( $n - 1 )) done echo if test $n -lt 0 && kill -0 "$cur_pid" 2>/dev/null then echo >&2 "$cur_pid still running 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 " exit 1 ;; esac examples/logger_mp_safe.rb000066400000000000000000000015421471646016400162050ustar00rootroot00000000000000# frozen_string_literal: false # Multi-Processing-safe monkey patch for Logger # # This monkey patch fixes the case where "preload_app 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 examples/logrotate.conf000066400000000000000000000027661471646016400155670ustar00rootroot00000000000000# example logrotate config file, I usually keep this in # /etc/logrotate.d/unicorn_app on my Debian systems # # See the logrotate(8) manpage for more information: # https://linux.die.net/man/8/logrotate # # public logrotate-related discussion in our archives: # https://yhbt.net/unicorn-public/?q=logrotate # Modify the following glob to match the logfiles your app writes to: /var/log/unicorn_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. Unicorn supports the USR1 signal and we send it # as our "lastaction" action: lastaction # For systemd users, assuming you use two services # (as recommended) to allow zero-downtime upgrades. # Only one service needs to be started, but signaling # both here is harmless as long as they're both enabled systemctl kill -s SIGUSR1 unicorn@1.service systemctl kill -s SIGUSR1 unicorn@2.service # Examples for other process management systems appreciated # Mail us at unicorn-public@yhbt.net # (see above for archives) # If you use a pid file and assuming your pid file # is in /var/run/unicorn_app/pid pid=/var/run/unicorn_app/pid test -s $pid && kill -USR1 "$(cat $pid)" endscript } examples/nginx.conf000066400000000000000000000140411471646016400146770ustar00rootroot00000000000000# This is example contains the bare mininum to get nginx going with # unicorn servers. Generally these configuration settings # are applicable to other HTTP application servers (and not just Ruby # ones), so if you have one working well for proxying another app # server, feel free to continue using it. # # The only setting we feel strongly about is the fail_timeout=0 # directive in the "upstream" block. max_fails=0 also has the same # effect as fail_timeout=0 for current versions of nginx and may be # used in its place. # # Users are strongly encouraged to refer to nginx documentation for more # details and search for other example configs. # you generally only need one nginx worker unless you're serving # large amounts of static files which require blocking disk reads worker_processes 1; # # drop privileges, root is needed on most systems for binding to port 80 # # (or anything < 1024). Capability-based security may be available for # # your system and worth checking out so you won't need to be root to # # start nginx to bind on 80 user nobody nogroup; # for systems with a "nogroup" # user nobody nobody; # for systems with "nobody" as a group instead # Feel free to change all paths to suite your needs here, of course pid /path/to/nginx.pid; error_log /path/to/nginx.error.log; events { worker_connections 1024; # increase if you have lots of clients accept_mutex off; # "on" if nginx worker_processes > 1 # use epoll; # enable for Linux 2.6+ # use kqueue; # enable for FreeBSD, OSX } http { # nginx will find this file in the config directory set at nginx build time include mime.types; # fallback in case we can't determine a type default_type application/octet-stream; # click tracking! access_log /path/to/nginx.access.log combined; # you generally want to serve static files with nginx since # unicorn is not and will never be optimized for it sendfile on; tcp_nopush on; # off may be better for *some* Comet/long-poll stuff tcp_nodelay off; # on may be better for some Comet/long-poll stuff # we haven't checked to see if Rack::Deflate on the app server is # faster or not than doing compression via nginx. It's easier # to configure it all in one place here for static files and also # to disable gzip for clients who don't get gzip/deflate right. # There are other gzip settings that may be needed used to deal with # bad clients out there, see # https://nginx.org/en/docs/http/ngx_http_gzip_module.html gzip on; gzip_http_version 1.0; gzip_proxied any; gzip_min_length 500; gzip_disable "MSIE [1-6]\."; gzip_types text/plain text/html text/xml text/css text/comma-separated-values text/javascript application/x-javascript application/atom+xml; # this can be any application server, not just unicorn upstream app_server { # fail_timeout=0 means we always retry an upstream even if it failed # to return a good HTTP response (in case the unicorn master nukes a # single worker for timing out). # for UNIX domain socket setups: server unix:/path/to/.unicorn.sock fail_timeout=0; # for TCP setups, point these to your backend servers # server 192.168.0.7:8080 fail_timeout=0; # server 192.168.0.8:8080 fail_timeout=0; # server 192.168.0.9:8080 fail_timeout=0; } server { # enable one of the following if you're on Linux or FreeBSD # listen 80 default deferred; # for Linux # listen 80 default accept_filter=httpready; # for FreeBSD # If you have IPv6, you'll likely want to have two separate listeners. # One on IPv4 only (the default), and another on IPv6 only instead # of a single dual-stack listener. A dual-stack listener will make # for ugly IPv4 addresses in $remote_addr (e.g ":ffff:10.0.0.1" # instead of just "10.0.0.1") and potentially trigger bugs in # some software. # listen [::]:80 ipv6only=on; # deferred or accept_filter recommended client_max_body_size 4G; server_name _; # ~2 seconds is often enough for most folks to parse HTML/CSS and # retrieve needed images/icons/frames, connections are cheap in # nginx so increasing this is generally safe... keepalive_timeout 5; # path for static files root /path/to/app/current/public; # Prefer to serve static files directly from nginx to avoid unnecessary # data copies from the application server. # # try_files directive appeared in in nginx 0.7.27 and has stabilized # over time. Older versions of nginx (e.g. 0.6.x) requires # "if (!-f $request_filename)" which was less efficient: # https://yhbt.net/unicorn.git/tree/examples/nginx.conf?id=v3.3.1#n127 try_files $uri/index.html $uri.html $uri @app; location @app { # an HTTP header important enough to have its own Wikipedia entry: # https://en.wikipedia.org/wiki/X-Forwarded-For proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # enable this if you forward HTTPS traffic to unicorn, # this helps Rack set the proper URL scheme for doing redirects: # proxy_set_header X-Forwarded-Proto $scheme; # pass the Host: header from the client right along so redirects # can be set properly within the Rack application proxy_set_header Host $http_host; # we don't want nginx trying to do something clever with # redirects, we set the Host: header above already. proxy_redirect off; # It's also safe to set if you're using only serving fast clients # with unicorn + nginx, but not slow clients. You normally want # nginx to buffer responses to slow clients, even with Rails 3.1 # streaming because otherwise a slow client can become a bottleneck # of unicorn. # # The Rack application may also set "X-Accel-Buffering (yes|no)" # in the response headers do disable/enable buffering on a # per-response basis. # proxy_buffering off; proxy_pass http://app_server; } # Rails error pages error_page 500 502 503 504 /500.html; location = /500.html { root /path/to/app/current/public; } } } examples/unicorn.conf.minimal.rb000066400000000000000000000011541471646016400172610ustar00rootroot00000000000000# frozen_string_literal: false # Minimal sample configuration file for Unicorn (not Rack) when used # with daemonization (unicorn -D) started in your working directory. # # See https://yhbt.net/unicorn/Unicorn/Configurator.html for complete # documentation. # See also https://yhbt.net/unicorn/examples/unicorn.conf.rb for # a more verbose configuration using more features. listen 2007 # by default Unicorn listens on port 8080 worker_processes 2 # this should be >= nr_cpus pid "/path/to/app/shared/pids/unicorn.pid" stderr_path "/path/to/app/shared/log/unicorn.log" stdout_path "/path/to/app/shared/log/unicorn.log" examples/unicorn.conf.rb000066400000000000000000000110311471646016400156270ustar00rootroot00000000000000# frozen_string_literal: false # Sample verbose configuration file for Unicorn (not Rack) # # This configuration file documents many features of Unicorn # that may not be needed for some applications. See # https://yhbt.net/unicorn/examples/unicorn.conf.minimal.rb # for a much simpler configuration file. # # See https://yhbt.net/unicorn/Unicorn/Configurator.html for complete # documentation. # Use at least one worker per core if you're on a dedicated server, # more will usually help for _short_ waits on databases/caches. worker_processes 4 # Since Unicorn is never exposed to outside clients, it does not need to # run on the standard HTTP port (80), there is no reason to start Unicorn # as root unless it's from system init scripts. # If running the master process as root and the workers as an unprivileged # user, do this to switch euid/egid in the workers (also chowns logs): # user "unprivileged_user", "unprivileged_group" # Help ensure your application will always spawn in the symlinked # "current" directory that Capistrano sets up. working_directory "/path/to/app/current" # available in 0.94.0+ # listen on both a Unix domain socket and a TCP port, # we use a shorter backlog for quicker failover when busy listen "/path/to/.unicorn.sock", :backlog => 64 listen 8080, :tcp_nopush => true # nuke workers after 30 seconds instead of 60 seconds (the default) timeout 30 # feel free to point this anywhere accessible on the filesystem pid "/path/to/app/shared/pids/unicorn.pid" # By default, the Unicorn logger will write to stderr. # Additionally, ome applications/frameworks log to stderr or stdout, # so prevent them from going to /dev/null when daemonized here: stderr_path "/path/to/app/shared/log/unicorn.stderr.log" stdout_path "/path/to/app/shared/log/unicorn.stdout.log" # combine Ruby 2.0.0+ with "preload_app true" for memory savings preload_app true # Enable this flag to have unicorn test client connections by writing the # beginning of the HTTP headers before calling the application. This # prevents calling the application for connections that have disconnected # while queued. This is only guaranteed to detect clients on the same # host unicorn runs on, and unlikely to detect disconnects even on a # fast LAN. check_client_connection false # local variable to guard against running a hook multiple times run_once = true before_fork do |server, worker| # the following is highly recomended for Rails + "preload_app true" # as there's no need for the master process to hold a connection defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! # Occasionally, it may be necessary to run non-idempotent code in the # master before forking. Keep in mind the above disconnect! example # is idempotent and does not need a guard. if run_once # do_something_once_here ... run_once = false # prevent from firing again end # The following is only recommended for memory/DB-constrained # installations. It is not needed if your system can house # twice as many worker_processes as you have configured. # # # This allows a new master process to incrementally # # phase out the old master process with SIGTTOU to avoid a # # thundering herd (especially in the "preload_app false" case) # # when doing a transparent upgrade. The last worker spawned # # will then kill off the old master process with a SIGQUIT. # old_pid = "#{server.config[:pid]}.oldbin" # if old_pid != server.pid # begin # sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU # Process.kill(sig, File.read(old_pid).to_i) # rescue Errno::ENOENT, Errno::ESRCH # end # end # # Throttle the master from forking too quickly by sleeping. Due # to the implementation of standard Unix signal handlers, this # helps (but does not completely) prevent identical, repeated signals # from being lost when the receiving process is busy. # sleep 1 end after_fork do |server, worker| # per-process listener ports for debugging/admin/migrations # addr = "127.0.0.1:#{9293 + worker.nr}" # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) # the following is *required* for Rails + "preload_app true", defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection # if preload_app is true, then you may also want to check and # restart any other shared sockets/descriptors such as Memcached, # and Redis. TokyoCabinet file handles are safe to reuse # between any number of forked children (assuming your kernel # correctly implements pread()/pwrite() system calls) end examples/unicorn.socket000066400000000000000000000003451471646016400155760ustar00rootroot00000000000000# ==> /etc/systemd/system/unicorn.socket <== [Unit] Description = unicorn sockets [Socket] ListenStream = 127.0.0.1:8080 ListenStream = /tmp/path/to/.unicorn.sock Service = unicorn@1.service [Install] WantedBy = sockets.target examples/unicorn@.service000066400000000000000000000026241471646016400160500ustar00rootroot00000000000000# ==> /etc/systemd/system/unicorn@.service <== # Since SIGUSR2 upgrades do not work under systemd, this service file # allows starting two simultaneous services during upgrade time # (e.g. unicorn@1 unicorn@2) with the intention that they take # turns running in-between upgrades. This should allow upgrading # without downtime. [Unit] Description = unicorn Rack application server %i Wants = unicorn.socket After = unicorn.socket [Service] # bundler users must use the "--keep-file-descriptors" switch, here: # ExecStart = bundle exec --keep-file-descriptors unicorn -c ... ExecStart = /usr/bin/unicorn -c /path/to/unicorn.conf.rb /path/to/config.ru # NonBlocking MUST be true if using socket activation with unicorn. # Otherwise, there's a small window in-between when the non-blocking # flag is set by us and our accept4 call where systemd can momentarily # make the socket blocking, causing us to block on accept4: NonBlocking = true Sockets = unicorn.socket KillSignal = SIGQUIT User = nobody Group = nogroup ExecReload = /bin/kill -HUP $MAINPID # This is based on the Unicorn::Configurator#timeout directive, # adding a few seconds for scheduling differences: TimeoutStopSec = 62 # Only kill the master process, it may be harmful to signal # workers via default "control-group" setting since some # Ruby extensions and applications misbehave on interrupts KillMode = process [Install] WantedBy = multi-user.target ext/000077500000000000000000000000001471646016400116675ustar00rootroot00000000000000ext/unicorn_http/000077500000000000000000000000001471646016400144035ustar00rootroot00000000000000ext/unicorn_http/CFLAGS000066400000000000000000000006601471646016400152670ustar00rootroot00000000000000# CFLAGS used for development (gcc-dependent) # source this file if you want/need them CFLAGS= CFLAGS="$CFLAGS -Wall" CFLAGS="$CFLAGS -Wwrite-strings" CFLAGS="$CFLAGS -Wdeclaration-after-statement" CFLAGS="$CFLAGS -Wcast-qual" CFLAGS="$CFLAGS -Wstrict-prototypes" CFLAGS="$CFLAGS -Wshadow" CFLAGS="$CFLAGS -Wextra" CFLAGS="$CFLAGS -Wno-deprecated-declarations" CFLAGS="$CFLAGS -Waggregate-return" CFLAGS="$CFLAGS -Wchar-subscripts" ext/unicorn_http/c_util.h000066400000000000000000000053431471646016400160400ustar00rootroot00000000000000/* * Generic C functions and macros go here, there are no dependencies * on Unicorn internal structures or the Ruby C API in here. */ #ifndef UH_util_h #define UH_util_h #include #include #include #define MIN(a,b) (a < b ? a : b) #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) #if SIZEOF_OFF_T == SIZEOF_INT # define UH_OFF_T_MAX INT_MAX #elif SIZEOF_OFF_T == SIZEOF_LONG_LONG # define UH_OFF_T_MAX LLONG_MAX #else # error off_t size unknown for this platform! #endif /* SIZEOF_OFF_T check */ /* * ragel enforces fpc as a const, and merely casting can make picky * compilers unhappy, so we have this little helper do our dirty work */ static inline void *deconst(const void *in) { union { const void *in; void *out; } tmp; tmp.in = in; return tmp.out; } /* * Capitalizes all lower-case ASCII characters, locale-agnostic. * We don't .tr('-', '_') here since we need to ban /^content_length$/i * and /^transfer_encoding$/i to avoid confusion and smuggling attacks. */ static void upcase_char(char *c) { if (*c >= 'a' && *c <= 'z') *c &= ~0x20; } /* Downcases a single ASCII character. Locale-agnostic. */ static void downcase_char(char *c) { if (*c >= 'A' && *c <= 'Z') *c |= 0x20; } static int hexchar2int(int xdigit) { if (xdigit >= 'A' && xdigit <= 'F') return xdigit - 'A' + 10; if (xdigit >= 'a' && xdigit <= 'f') return xdigit - 'a' + 10; /* Ragel already does runtime range checking for us in Unicorn: */ assert(xdigit >= '0' && xdigit <= '9' && "invalid digit character"); return xdigit - '0'; } /* * multiplies +i+ by +base+ and increments the result by the parsed * integer value of +xdigit+. +xdigit+ is a character byte * representing a number the range of 0..(base-1) * returns the new value of +i+ on success * returns -1 on errors (including overflow) */ static off_t step_incr(off_t i, int xdigit, const int base) { static const off_t max = UH_OFF_T_MAX; const off_t next_max = (max - (max % base)) / base; off_t offset = hexchar2int(xdigit); if (offset > (base - 1)) return -1; if (i > next_max) return -1; i *= base; if ((offset > (base - 1)) || ((max - i) < offset)) return -1; return i + offset; } /* * parses a non-negative length according to base-10 and * returns it as an off_t value. Returns -1 on errors * (including overflow). */ static off_t parse_length(const char *value, size_t length) { off_t rv; for (rv = 0; length-- && rv >= 0; ++value) { if (*value >= '0' && *value <= '9') rv = step_incr(rv, *value, 10); else return -1; } return rv; } #define CONST_MEM_EQ(const_p, buf, len) \ ((sizeof(const_p) - 1) == len && !memcmp(const_p, buf, sizeof(const_p) - 1)) #endif /* UH_util_h */ ext/unicorn_http/common_field_optimization.h000066400000000000000000000064301471646016400220200ustar00rootroot00000000000000#ifndef common_field_optimization #define common_field_optimization #include "ruby.h" #include "c_util.h" struct common_field { const signed long len; const char *name; VALUE value; }; /* * A list of common HTTP headers we expect to receive. * This allows us to avoid repeatedly creating identical string * objects to be used with rb_hash_aset(). */ static struct common_field common_http_fields[] = { # define f(N) { (sizeof(N) - 1), N, Qnil } f("ACCEPT"), f("ACCEPT_CHARSET"), f("ACCEPT_ENCODING"), f("ACCEPT_LANGUAGE"), f("ALLOW"), f("AUTHORIZATION"), f("CACHE_CONTROL"), f("CONNECTION"), f("CONTENT_ENCODING"), f("CONTENT_LENGTH"), f("CONTENT_TYPE"), f("COOKIE"), f("DATE"), f("EXPECT"), f("FROM"), f("HOST"), f("IF_MATCH"), f("IF_MODIFIED_SINCE"), f("IF_NONE_MATCH"), f("IF_RANGE"), f("IF_UNMODIFIED_SINCE"), f("KEEP_ALIVE"), /* Firefox sends this */ f("MAX_FORWARDS"), f("PRAGMA"), f("PROXY_AUTHORIZATION"), f("RANGE"), f("REFERER"), f("TE"), f("TRAILER"), f("TRANSFER_ENCODING"), f("UPGRADE"), f("USER_AGENT"), f("VIA"), f("X_FORWARDED_FOR"), /* common for proxies */ f("X_FORWARDED_PROTO"), /* common for proxies */ f("X_REAL_IP"), /* common for proxies */ f("WARNING") # undef f }; #define HTTP_PREFIX "HTTP_" #define HTTP_PREFIX_LEN (sizeof(HTTP_PREFIX) - 1) static ID id_uminus; /* this dedupes under Ruby 2.5+ (December 2017) */ static VALUE str_dd_freeze(VALUE str) { if (STR_UMINUS_DEDUPE) return rb_funcall(str, id_uminus, 0); /* freeze,since it speeds up older MRI slightly */ OBJ_FREEZE(str); return str; } static VALUE str_new_dd_freeze(const char *ptr, long len) { return str_dd_freeze(rb_str_new(ptr, len)); } /* this function is not performance-critical, called only at load time */ static void init_common_fields(void) { int i; struct common_field *cf = common_http_fields; char tmp[64]; memcpy(tmp, HTTP_PREFIX, HTTP_PREFIX_LEN); for(i = ARRAY_SIZE(common_http_fields); --i >= 0; cf++) { /* Rack doesn't like certain headers prefixed with "HTTP_" */ if (!strcmp("CONTENT_LENGTH", cf->name) || !strcmp("CONTENT_TYPE", cf->name)) { cf->value = str_new_dd_freeze(cf->name, cf->len); } else { memcpy(tmp + HTTP_PREFIX_LEN, cf->name, cf->len + 1); cf->value = str_new_dd_freeze(tmp, HTTP_PREFIX_LEN + cf->len); } rb_gc_register_mark_object(cf->value); } } /* this function is called for every header set */ static VALUE find_common_field(const char *field, size_t flen) { int i; struct common_field *cf = common_http_fields; for(i = ARRAY_SIZE(common_http_fields); --i >= 0; cf++) { if (cf->len == (long)flen && !memcmp(cf->name, field, flen)) return cf->value; } return Qnil; } /* * We got a strange header that we don't have a memoized value for. * Fallback to creating a new string to use as a hash key. */ static VALUE uncommon_field(const char *field, size_t flen) { VALUE f = rb_str_new(NULL, HTTP_PREFIX_LEN + flen); memcpy(RSTRING_PTR(f), HTTP_PREFIX, HTTP_PREFIX_LEN); memcpy(RSTRING_PTR(f) + HTTP_PREFIX_LEN, field, flen); assert(*(RSTRING_PTR(f) + RSTRING_LEN(f)) == '\0' && "string didn't end with \\0"); /* paranoia */ return HASH_ASET_DEDUPE ? f : str_dd_freeze(f); } #endif /* common_field_optimization_h */ ext/unicorn_http/epollexclusive.h000066400000000000000000000070001471646016400176140ustar00rootroot00000000000000/* * This is only intended for use inside a unicorn worker, nowhere else. * EPOLLEXCLUSIVE somewhat mitigates the thundering herd problem for * mostly idle processes since we can't use blocking accept4. * This is NOT intended for use with multi-threaded servers, nor * single-threaded multi-client ("C10K") servers or anything advanced * like that. This use of epoll is only appropriate for a primitive, * single-client, single-threaded servers like unicorn that need to * support SIGKILL timeouts and parent death detection. */ #if defined(HAVE_EPOLL_CREATE1) # include # include # include # include #endif /* __linux__ */ #if defined(EPOLLEXCLUSIVE) && defined(HAVE_EPOLL_CREATE1) # define USE_EPOLL (1) #else # define USE_EPOLL (0) #endif #if USE_EPOLL #if defined(HAVE_RB_IO_DESCRIPTOR) /* Ruby 3.1+ */ # define my_fileno(io) rb_io_descriptor(io) #else /* Ruby <3.1 */ static int my_fileno(VALUE io) { rb_io_t *fptr; GetOpenFile(io, fptr); rb_io_check_closed(fptr); return fptr->fd; } #endif /* Ruby <3.1 */ /* * :nodoc: * returns IO object if EPOLLEXCLUSIVE works and arms readers */ static VALUE prep_readers(VALUE cls, VALUE readers) { long i; int epfd = epoll_create1(EPOLL_CLOEXEC); VALUE epio; if (epfd < 0) rb_sys_fail("epoll_create1"); epio = rb_funcall(cls, rb_intern("for_fd"), 1, INT2NUM(epfd)); Check_Type(readers, T_ARRAY); for (i = 0; i < RARRAY_LEN(readers); i++) { int rc, fd; struct epoll_event e; VALUE io = rb_ary_entry(readers, i); e.data.u64 = i; /* the reason readers shouldn't change */ /* * I wanted to use EPOLLET here, but maintaining our own * equivalent of ep->rdllist in Ruby-space doesn't fit * our design at all (and the kernel already has it's own * code path for doing it). So let the kernel spend * cycles on maintaining level-triggering. */ e.events = EPOLLEXCLUSIVE | EPOLLIN; fd = my_fileno(rb_io_get_io(io)); rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e); if (rc < 0) rb_sys_fail("epoll_ctl"); } return epio; } #endif /* USE_EPOLL */ #if USE_EPOLL struct ep_wait { struct epoll_event event; int epfd; int timeout_msec; }; static void *do_wait(void *ptr) /* runs w/o GVL */ { struct ep_wait *epw = ptr; /* * Linux delivers epoll events in the order received, and using * maxevents=1 ensures we pluck one item off ep->rdllist * at-a-time (c.f. fs/eventpoll.c in linux.git, it's quite * easy-to-understand for anybody familiar with Ruby C). */ return (void *)(long)epoll_wait(epw->epfd, &epw->event, 1, epw->timeout_msec); } /* :nodoc: */ /* readers must not change between prepare_readers and get_readers */ static VALUE get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec) { struct ep_wait epw; long n; Check_Type(ready, T_ARRAY); Check_Type(readers, T_ARRAY); epw.epfd = my_fileno(epio); epw.timeout_msec = NUM2INT(timeout_msec); n = (long)rb_thread_call_without_gvl(do_wait, &epw, RUBY_UBF_IO, NULL); if (n < 0) { if (errno != EINTR) rb_sys_fail("epoll_wait"); } else if (n > 0) { /* maxevents is hardcoded to 1 */ VALUE obj = rb_ary_entry(readers, epw.event.data.u64); if (RTEST(obj)) rb_ary_push(ready, obj); } /* n == 0 : timeout */ return Qfalse; } #endif /* USE_EPOLL */ static void init_epollexclusive(VALUE mUnicorn) { #if USE_EPOLL VALUE cWaiter = rb_define_class_under(mUnicorn, "Waiter", rb_cIO); rb_define_singleton_method(cWaiter, "prep_readers", prep_readers, 1); rb_define_method(cWaiter, "get_readers", get_readers, 3); #endif } ext/unicorn_http/ext_help.h000066400000000000000000000017771471646016400164000ustar00rootroot00000000000000#ifndef ext_help_h #define ext_help_h /* not all Ruby implementations support frozen objects (Rubinius does not) */ #if defined(OBJ_FROZEN) # define assert_frozen(f) assert(OBJ_FROZEN(f) && "unfrozen object") #else # define assert_frozen(f) do {} while (0) #endif /* !defined(OBJ_FROZEN) */ static inline int str_cstr_eq(VALUE val, const char *ptr, long len) { return (RSTRING_LEN(val) == len && !memcmp(ptr, RSTRING_PTR(val), len)); } #define STR_CSTR_EQ(val, const_str) \ str_cstr_eq(val, const_str, sizeof(const_str) - 1) /* strcasecmp isn't locale independent */ static int str_cstr_case_eq(VALUE val, const char *ptr, long len) { if (RSTRING_LEN(val) == len) { const char *v = RSTRING_PTR(val); for (; len--; ++ptr, ++v) { if ((*ptr == *v) || (*v >= 'A' && *v <= 'Z' && (*v | 0x20) == *ptr)) continue; return 0; } return 1; } return 0; } #define STR_CSTR_CASE_EQ(val, const_str) \ str_cstr_case_eq(val, const_str, sizeof(const_str) - 1) #endif /* ext_help_h */ ext/unicorn_http/extconf.rb000066400000000000000000000017331471646016400164020ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'mkmf' have_func("rb_hash_clear", "ruby.h") or abort 'Ruby 2.0+ required' message('checking if String#-@ (str_uminus) dedupes... ') begin a = -(%w(t e s t).join) b = -(%w(t e s t).join) if a.equal?(b) $CPPFLAGS += ' -DSTR_UMINUS_DEDUPE=1 ' message("yes\n") else $CPPFLAGS += ' -DSTR_UMINUS_DEDUPE=0 ' message("no, needs Ruby 2.5+\n") end rescue NoMethodError $CPPFLAGS += ' -DSTR_UMINUS_DEDUPE=0 ' message("no, String#-@ not available\n") end message('checking if Hash#[]= (rb_hash_aset) dedupes... ') h = {} x = {} r = rand.to_s h[%W(#{r}).join('')] = :foo x[%W(#{r}).join('')] = :foo if x.keys[0].equal?(h.keys[0]) $CPPFLAGS += ' -DHASH_ASET_DEDUPE=1 ' message("yes\n") else $CPPFLAGS += ' -DHASH_ASET_DEDUPE=0 ' message("no, needs Ruby 2.6+\n") end if have_func('epoll_create1', %w(sys/epoll.h)) have_func('rb_io_descriptor') # Ruby 3.1+ end create_makefile("unicorn_http") ext/unicorn_http/global_variables.h000066400000000000000000000061251471646016400200500ustar00rootroot00000000000000#ifndef global_variables_h #define global_variables_h static VALUE eHttpParserError; static VALUE e413; static VALUE e414; static VALUE g_rack_url_scheme; static VALUE g_request_method; static VALUE g_request_uri; static VALUE g_fragment; static VALUE g_query_string; static VALUE g_http_version; static VALUE g_request_path; static VALUE g_path_info; static VALUE g_server_name; static VALUE g_server_port; static VALUE g_server_protocol; static VALUE g_http_host; static VALUE g_http_x_forwarded_proto; static VALUE g_http_x_forwarded_ssl; static VALUE g_http_transfer_encoding; static VALUE g_content_length; static VALUE g_http_trailer; static VALUE g_http_connection; static VALUE g_port_80; static VALUE g_port_443; static VALUE g_localhost; static VALUE g_http; static VALUE g_https; static VALUE g_http_09; static VALUE g_http_10; static VALUE g_http_11; /** Defines common length and error messages for input length validation. */ #define DEF_MAX_LENGTH(N, length) \ static const size_t MAX_##N##_LENGTH = length; \ static const char * const MAX_##N##_LENGTH_ERR = \ "HTTP element " # N " is longer than the " # length " allowed length." NORETURN(static void parser_raise(VALUE klass, const char *)); /** * Validates the max length of given input and throws an HttpParserError * exception if over. */ #define VALIDATE_MAX_LENGTH(len, N) do { \ if (len > MAX_##N##_LENGTH) \ parser_raise(eHttpParserError, MAX_##N##_LENGTH_ERR); \ } while (0) #define VALIDATE_MAX_URI_LENGTH(len, N) do { \ if (len > MAX_##N##_LENGTH) \ parser_raise(e414, MAX_##N##_LENGTH_ERR); \ } while (0) /** Defines global strings in the init method. */ #define DEF_GLOBAL(N, val) do { \ g_##N = str_new_dd_freeze(val, (long)sizeof(val) - 1); \ rb_gc_register_mark_object(g_##N); \ } while (0) /* Defines the maximum allowed lengths for various input elements.*/ DEF_MAX_LENGTH(FIELD_NAME, 256); DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024); DEF_MAX_LENGTH(REQUEST_URI, 1024 * 15); DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */ DEF_MAX_LENGTH(REQUEST_PATH, 4096); /* common PATH_MAX on modern systems */ DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10)); static void init_globals(void) { DEF_GLOBAL(rack_url_scheme, "rack.url_scheme"); DEF_GLOBAL(request_method, "REQUEST_METHOD"); DEF_GLOBAL(request_uri, "REQUEST_URI"); DEF_GLOBAL(fragment, "FRAGMENT"); DEF_GLOBAL(query_string, "QUERY_STRING"); DEF_GLOBAL(http_version, "HTTP_VERSION"); DEF_GLOBAL(request_path, "REQUEST_PATH"); DEF_GLOBAL(path_info, "PATH_INFO"); DEF_GLOBAL(server_name, "SERVER_NAME"); DEF_GLOBAL(server_port, "SERVER_PORT"); DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL"); DEF_GLOBAL(http_x_forwarded_proto, "HTTP_X_FORWARDED_PROTO"); DEF_GLOBAL(http_x_forwarded_ssl, "HTTP_X_FORWARDED_SSL"); DEF_GLOBAL(port_80, "80"); DEF_GLOBAL(port_443, "443"); DEF_GLOBAL(localhost, "localhost"); DEF_GLOBAL(http, "http"); DEF_GLOBAL(https, "https"); DEF_GLOBAL(http_11, "HTTP/1.1"); DEF_GLOBAL(http_10, "HTTP/1.0"); DEF_GLOBAL(http_09, "HTTP/0.9"); } #undef DEF_GLOBAL #endif /* global_variables_h */ ext/unicorn_http/httpdate.c000066400000000000000000000047601471646016400163730ustar00rootroot00000000000000#include #include #include #include static const size_t buf_capa = sizeof("Thu, 01 Jan 1970 00:00:00 GMT"); static VALUE buf; static char *buf_ptr; static const char week[] = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat"; static const char months[] = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0" "Jul\0Aug\0Sep\0Oct\0Nov\0Dec"; /* for people on wonky systems only */ #ifndef HAVE_GMTIME_R # warning using fake gmtime_r static struct tm * my_gmtime_r(time_t *now, struct tm *tm) { struct tm *global = gmtime(now); if (global) *tm = *global; return tm; } # define gmtime_r my_gmtime_r #endif /* * Returns a string which represents the time as rfc1123-date of HTTP-date * defined by RFC 2616: * * day-of-week, DD month-name CCYY hh:mm:ss GMT * * Note that the result is always GMT. * * This method is identical to Time#httpdate in the Ruby standard library, * except it is implemented in C for performance. We always saw * Time#httpdate at or near the top of the profiler output so we * decided to rewrite this in C. * * Caveats: it relies on a Ruby implementation with the global VM lock, * a thread-safe version will be provided when a Unix-only, GVL-free Ruby * implementation becomes viable. */ static VALUE httpdate(VALUE self) { static time_t last; struct timeval now; struct tm tm; /* * Favor gettimeofday(2) over time(2), as the latter can return the * wrong value in the first 1 .. 2.5 ms of every second(!) * * https://lore.kernel.org/git/20230320230507.3932018-1-gitster@pobox.com/ * https://inbox.sourceware.org/libc-alpha/20230306160321.2942372-1-adhemerval.zanella@linaro.org/T/ * https://sourceware.org/bugzilla/show_bug.cgi?id=30200 */ if (gettimeofday(&now, NULL)) rb_sys_fail("gettimeofday"); if (last == now.tv_sec) return buf; last = now.tv_sec; gmtime_r(&now.tv_sec, &tm); /* we can make this thread-safe later if our Ruby loses the GVL */ snprintf(buf_ptr, buf_capa, "%s, %02d %s %4d %02d:%02d:%02d GMT", week + (tm.tm_wday * 4), tm.tm_mday, months + (tm.tm_mon * 4), tm.tm_year + 1900, tm.tm_hour, tm.tm_min, tm.tm_sec); return buf; } void init_unicorn_httpdate(void) { VALUE mod = rb_define_module("Unicorn"); mod = rb_define_module_under(mod, "HttpResponse"); buf = rb_str_new(0, buf_capa - 1); rb_gc_register_mark_object(buf); buf_ptr = RSTRING_PTR(buf); httpdate(Qnil); rb_define_method(mod, "httpdate", httpdate, 0); } ext/unicorn_http/unicorn_http.rl000066400000000000000000000730601471646016400174640ustar00rootroot00000000000000/** * Copyright (c) 2009 Eric Wong (all bugs are Eric's fault) * Copyright (c) 2005 Zed A. Shaw * You can redistribute it and/or modify it under the same terms as Ruby 1.8 or * the GPLv2+ (GPLv3+ preferred) */ #include "ruby.h" #include "ext_help.h" #include #include #include #include "common_field_optimization.h" #include "global_variables.h" #include "c_util.h" #include "epollexclusive.h" void init_unicorn_httpdate(void); #define UH_FL_CHUNKED 0x1 #define UH_FL_HASBODY 0x2 #define UH_FL_INBODY 0x4 #define UH_FL_HASTRAILER 0x8 #define UH_FL_INTRAILER 0x10 #define UH_FL_INCHUNK 0x20 #define UH_FL_REQEOF 0x40 #define UH_FL_KAVERSION 0x80 #define UH_FL_HASHEADER 0x100 #define UH_FL_TO_CLEAR 0x200 #define UH_FL_RESSTART 0x400 /* for check_client_connection */ #define UH_FL_HIJACK 0x800 #define UH_FL_RES_CHUNK_VER (1U << 12) #define UH_FL_RES_CHUNK_METHOD (1U << 13) /* all of these flags need to be set for keepalive to be supported */ #define UH_FL_KEEPALIVE (UH_FL_KAVERSION | UH_FL_REQEOF | UH_FL_HASHEADER) /* we can only chunk responses for non-HEAD HTTP/1.1 requests */ #define UH_FL_RES_CHUNKABLE (UH_FL_RES_CHUNK_VER | UH_FL_RES_CHUNK_METHOD) static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */ /* this is only intended for use with Rainbows! */ static VALUE set_maxhdrlen(VALUE self, VALUE len) { return UINT2NUM(MAX_HEADER_LEN = NUM2UINT(len)); } /* keep this small for other servers (e.g. yahns) since every client has one */ struct http_parser { int cs; /* Ragel internal state */ unsigned int flags; unsigned int mark; unsigned int offset; union { /* these 2 fields don't nest */ unsigned int field; unsigned int query; } start; union { unsigned int field_len; /* only used during header processing */ unsigned int dest_offset; /* only used during body processing */ } s; VALUE buf; VALUE env; VALUE cont; /* Qfalse: unset, Qnil: ignored header, T_STRING: append */ union { off_t content; off_t chunk; } len; }; static ID id_set_backtrace, id_is_chunked_p; static VALUE cHttpParser; static void finalize_header(struct http_parser *hp); static void parser_raise(VALUE klass, const char *msg) { VALUE exc = rb_exc_new2(klass, msg); VALUE bt = rb_ary_new(); rb_funcall(exc, id_set_backtrace, 1, bt); rb_exc_raise(exc); } static inline unsigned int ulong2uint(unsigned long n) { unsigned int i = (unsigned int)n; if (sizeof(unsigned int) != sizeof(unsigned long)) { if ((unsigned long)i != n) { rb_raise(rb_eRangeError, "too large to be 32-bit uint: %lu", n); } } return i; } #define REMAINING (unsigned long)(pe - p) #define LEN(AT, FPC) (ulong2uint(FPC - buffer) - hp->AT) #define MARK(M,FPC) (hp->M = ulong2uint((FPC) - buffer)) #define PTR_TO(F) (buffer + hp->F) #define STR_NEW(M,FPC) rb_str_new(PTR_TO(M), LEN(M, FPC)) #define STRIPPED_STR_NEW(M,FPC) stripped_str_new(PTR_TO(M), LEN(M, FPC)) #define HP_FL_TEST(hp,fl) ((hp)->flags & (UH_FL_##fl)) #define HP_FL_SET(hp,fl) ((hp)->flags |= (UH_FL_##fl)) #define HP_FL_UNSET(hp,fl) ((hp)->flags &= ~(UH_FL_##fl)) #define HP_FL_ALL(hp,fl) (HP_FL_TEST(hp, fl) == (UH_FL_##fl)) static int is_lws(char c) { return (c == ' ' || c == '\t'); } static VALUE stripped_str_new(const char *str, long len) { long end; for (end = len - 1; end >= 0 && is_lws(str[end]); end--); return rb_str_new(str, end + 1); } /* * handles values of the "Connection:" header, keepalive is implied * for HTTP/1.1 but needs to be explicitly enabled with HTTP/1.0 * Additionally, we require GET/HEAD requests to support keepalive. */ static void hp_keepalive_connection(struct http_parser *hp, VALUE val) { if (STR_CSTR_CASE_EQ(val, "keep-alive")) { /* basically have HTTP/1.0 masquerade as HTTP/1.1+ */ HP_FL_SET(hp, KAVERSION); } else if (STR_CSTR_CASE_EQ(val, "close")) { /* * it doesn't matter what HTTP version or request method we have, * if a client says "Connection: close", we disable keepalive */ HP_FL_UNSET(hp, KAVERSION); } else { /* * client could've sent anything, ignore it for now. Maybe * "HP_FL_UNSET(hp, KAVERSION);" just in case? * Raising an exception might be too mean... */ } } static void request_method(struct http_parser *hp, const char *ptr, size_t len) { VALUE v = rb_str_new(ptr, len); if (len != 4 || memcmp(ptr, "HEAD", 4)) HP_FL_SET(hp, RES_CHUNK_METHOD); rb_hash_aset(hp->env, g_request_method, v); } static void http_version(struct http_parser *hp, const char *ptr, size_t len) { VALUE v; HP_FL_SET(hp, HASHEADER); if (CONST_MEM_EQ("HTTP/1.1", ptr, len)) { /* HTTP/1.1 implies keepalive unless "Connection: close" is set */ HP_FL_SET(hp, KAVERSION); HP_FL_SET(hp, RES_CHUNK_VER); v = g_http_11; } else if (CONST_MEM_EQ("HTTP/1.0", ptr, len)) { v = g_http_10; } else { v = rb_str_new(ptr, len); } rb_hash_aset(hp->env, g_server_protocol, v); rb_hash_aset(hp->env, g_http_version, v); } static inline void hp_invalid_if_trailer(struct http_parser *hp) { if (HP_FL_TEST(hp, INTRAILER)) parser_raise(eHttpParserError, "invalid Trailer"); } static void write_cont_value(struct http_parser *hp, char *buffer, const char *p) { char *vptr; long end; long len = LEN(mark, p); long cont_len; if (hp->cont == Qfalse) parser_raise(eHttpParserError, "invalid continuation line"); if (NIL_P(hp->cont)) return; /* we're ignoring this header (probably Host:) */ assert(TYPE(hp->cont) == T_STRING && "continuation line is not a string"); assert(hp->mark > 0 && "impossible continuation line offset"); if (len == 0) return; cont_len = RSTRING_LEN(hp->cont); if (cont_len > 0) { --hp->mark; len = LEN(mark, p); } vptr = PTR_TO(mark); /* normalize tab to space */ if (cont_len > 0) { assert((' ' == *vptr || '\t' == *vptr) && "invalid leading white space"); *vptr = ' '; } for (end = len - 1; end >= 0 && is_lws(vptr[end]); end--); rb_str_buf_cat(hp->cont, vptr, end + 1); } static int is_chunked(VALUE v) { /* common case first */ if (STR_CSTR_CASE_EQ(v, "chunked")) return 1; /* * call Ruby function in unicorn/http_request.rb to deal with unlikely * comma-delimited case */ return rb_funcall(cHttpParser, id_is_chunked_p, 1, v) != Qfalse; } static void write_value(struct http_parser *hp, char *buffer, const char *p) { VALUE f, v, e; char *tip = PTR_TO(start.field); size_t i; static const size_t cl_len = sizeof("CONTENT_LENGTH") - 1; static const size_t tl_len = sizeof("TRANSFER_ENCODING") - 1; /* avoid confusion + smuggling attacks from `_' */ if (hp->s.field_len == cl_len && !memcmp(tip, "CONTENT_LENGTH", cl_len)) parser_raise(eHttpParserError, "invalid Content-Length"); else if (hp->s.field_len == tl_len && !memcmp(tip, "TRANSFER_ENCODING", tl_len)) parser_raise(eHttpParserError, "invalid Transfer-Encoding"); /* .tr('-', '_') since Rack expects it */ for (i = 0; i < hp->s.field_len; i++) if (tip[i] == '-') tip[i] = '_'; f = find_common_field(tip, hp->s.field_len); VALIDATE_MAX_LENGTH(LEN(mark, p), FIELD_VALUE); v = LEN(mark, p) == 0 ? rb_str_buf_new(128) : STRIPPED_STR_NEW(mark, p); if (NIL_P(f)) { const char *field = PTR_TO(start.field); size_t flen = hp->s.field_len; VALIDATE_MAX_LENGTH(flen, FIELD_NAME); /* * ignore "Version" headers since they conflict with the HTTP_VERSION * rack env variable. */ if (CONST_MEM_EQ("VERSION", field, flen)) { hp->cont = Qnil; return; } f = uncommon_field(field, flen); } else if (f == g_http_connection) { hp_keepalive_connection(hp, v); } else if (f == g_content_length && !HP_FL_TEST(hp, CHUNKED)) { if (hp->len.content) parser_raise(eHttpParserError, "Content-Length already set"); hp->len.content = parse_length(RSTRING_PTR(v), RSTRING_LEN(v)); if (hp->len.content < 0) parser_raise(eHttpParserError, "invalid Content-Length"); if (hp->len.content != 0) HP_FL_SET(hp, HASBODY); hp_invalid_if_trailer(hp); } else if (f == g_http_transfer_encoding) { if (is_chunked(v)) { if (HP_FL_TEST(hp, CHUNKED)) /* * RFC 7230 3.3.1: * A sender MUST NOT apply chunked more than once to a message body * (i.e., chunking an already chunked message is not allowed). */ parser_raise(eHttpParserError, "Transfer-Encoding double chunked"); HP_FL_SET(hp, CHUNKED); HP_FL_SET(hp, HASBODY); /* RFC 7230 3.3.3, 3: favor chunked if Content-Length exists */ hp->len.content = 0; } else if (HP_FL_TEST(hp, CHUNKED)) { /* * RFC 7230 3.3.3, point 3 states: * If a Transfer-Encoding header field is present in a request and * the chunked transfer coding is not the final encoding, the * message body length cannot be determined reliably; the server * MUST respond with the 400 (Bad Request) status code and then * close the connection. */ parser_raise(eHttpParserError, "invalid Transfer-Encoding"); } hp_invalid_if_trailer(hp); } else if (f == g_http_trailer) { HP_FL_SET(hp, HASTRAILER); hp_invalid_if_trailer(hp); } else { assert(TYPE(f) == T_STRING && "memoized object is not a string"); assert_frozen(f); } e = rb_hash_aref(hp->env, f); if (NIL_P(e)) { hp->cont = rb_hash_aset(hp->env, f, v); } else if (f == g_http_host) { /* * ignored, absolute URLs in REQUEST_URI take precedence over * the Host: header (ref: rfc 2616, section 5.2.1) */ hp->cont = Qnil; } else { rb_str_buf_cat(e, ",", 1); hp->cont = rb_str_buf_append(e, v); } } /** Machine **/ %%{ machine http_parser; action mark {MARK(mark, fpc); } action start_field { MARK(start.field, fpc); } action upcase_field { upcase_char(deconst(fpc)); } action downcase_char { downcase_char(deconst(fpc)); } action write_field { hp->s.field_len = LEN(start.field, fpc); } action start_value { MARK(mark, fpc); } action write_value { write_value(hp, buffer, fpc); } action write_cont_value { write_cont_value(hp, buffer, fpc); } action request_method { request_method(hp, PTR_TO(mark), LEN(mark, fpc)); } action scheme { rb_hash_aset(hp->env, g_rack_url_scheme, STR_NEW(mark, fpc)); } action host { rb_hash_aset(hp->env, g_http_host, STR_NEW(mark, fpc)); } action request_uri { VALUE str; VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), REQUEST_URI); str = rb_hash_aset(hp->env, g_request_uri, STR_NEW(mark, fpc)); /* * "OPTIONS * HTTP/1.1\r\n" is a valid request, but we can't have '*' * in REQUEST_PATH or PATH_INFO or else Rack::Lint will complain */ if (STR_CSTR_EQ(str, "*")) { str = rb_str_new(NULL, 0); rb_hash_aset(hp->env, g_path_info, str); rb_hash_aset(hp->env, g_request_path, str); } } action fragment { VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), FRAGMENT); rb_hash_aset(hp->env, g_fragment, STR_NEW(mark, fpc)); } action start_query {MARK(start.query, fpc); } action query_string { VALIDATE_MAX_URI_LENGTH(LEN(start.query, fpc), QUERY_STRING); rb_hash_aset(hp->env, g_query_string, STR_NEW(start.query, fpc)); } action http_version { http_version(hp, PTR_TO(mark), LEN(mark, fpc)); } action request_path { VALUE val; VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), REQUEST_PATH); val = rb_hash_aset(hp->env, g_request_path, STR_NEW(mark, fpc)); /* rack says PATH_INFO must start with "/" or be empty */ if (!STR_CSTR_EQ(val, "*")) rb_hash_aset(hp->env, g_path_info, val); } action add_to_chunk_size { hp->len.chunk = step_incr(hp->len.chunk, fc, 16); if (hp->len.chunk < 0) parser_raise(eHttpParserError, "invalid chunk size"); } action header_done { finalize_header(hp); cs = http_parser_first_final; if (HP_FL_TEST(hp, HASBODY)) { HP_FL_SET(hp, INBODY); if (HP_FL_TEST(hp, CHUNKED)) cs = http_parser_en_ChunkedBody; } else { HP_FL_SET(hp, REQEOF); assert(!HP_FL_TEST(hp, CHUNKED) && "chunked encoding without body!"); } /* * go back to Ruby so we can call the Rack application, we'll reenter * the parser iff the body needs to be processed. */ goto post_exec; } action end_trailers { cs = http_parser_first_final; goto post_exec; } action end_chunked_body { HP_FL_SET(hp, INTRAILER); cs = http_parser_en_Trailers; ++p; assert(p <= pe && "buffer overflow after chunked body"); goto post_exec; } action skip_chunk_data { skip_chunk_data_hack: { size_t nr = MIN((size_t)hp->len.chunk, REMAINING); memcpy(RSTRING_PTR(hp->cont) + hp->s.dest_offset, fpc, nr); hp->s.dest_offset += nr; hp->len.chunk -= nr; p += nr; assert(hp->len.chunk >= 0 && "negative chunk length"); if ((size_t)hp->len.chunk > REMAINING) { HP_FL_SET(hp, INCHUNK); goto post_exec; } else { fhold; fgoto chunk_end; } }} include unicorn_http_common "unicorn_http_common.rl"; }%% /** Data **/ %% write data; static void http_parser_init(struct http_parser *hp) { int cs = 0; hp->flags = 0; hp->mark = 0; hp->offset = 0; hp->start.field = 0; hp->s.field_len = 0; hp->len.content = 0; hp->cont = Qfalse; /* zero on MRI, should be optimized away by above */ %% write init; hp->cs = cs; } /** exec **/ static void http_parser_execute(struct http_parser *hp, char *buffer, size_t len) { const char *p, *pe; int cs = hp->cs; size_t off = hp->offset; if (cs == http_parser_first_final) return; assert(off <= len && "offset past end of buffer"); p = buffer+off; pe = buffer+len; assert((void *)(pe - p) == (void *)(len - off) && "pointers aren't same distance"); if (HP_FL_TEST(hp, INCHUNK)) { HP_FL_UNSET(hp, INCHUNK); goto skip_chunk_data_hack; } %% write exec; post_exec: /* "_out:" also goes here */ if (hp->cs != http_parser_error) hp->cs = cs; hp->offset = ulong2uint(p - buffer); assert(p <= pe && "buffer overflow after parsing execute"); assert(hp->offset <= len && "offset longer than length"); } static void hp_mark(void *ptr) { struct http_parser *hp = ptr; rb_gc_mark(hp->buf); rb_gc_mark(hp->env); rb_gc_mark(hp->cont); } static size_t hp_memsize(const void *ptr) { return sizeof(struct http_parser); } static const rb_data_type_t hp_type = { "unicorn_http", { hp_mark, RUBY_TYPED_DEFAULT_FREE, hp_memsize, /* reserved */ }, /* parent, data, [ flags ] */ }; static struct http_parser *data_get(VALUE self) { struct http_parser *hp; TypedData_Get_Struct(self, struct http_parser, &hp_type, hp); assert(hp && "failed to extract http_parser struct"); return hp; } /* * set rack.url_scheme to "https" or "http", no others are allowed by Rack * this resembles the Rack::Request#scheme method as of rack commit * 35bb5ba6746b5d346de9202c004cc926039650c7 */ static void set_url_scheme(VALUE env, VALUE *server_port) { VALUE scheme = rb_hash_aref(env, g_rack_url_scheme); if (NIL_P(scheme)) { /* * would anybody be horribly opposed to removing the X-Forwarded-SSL * and X-Forwarded-Proto handling from this parser? We've had it * forever and nobody has said anything against it, either. * Anyways, please send comments to our public mailing list: * unicorn-public@yhbt.net (no HTML mail, no subscription necessary) */ scheme = rb_hash_aref(env, g_http_x_forwarded_ssl); if (!NIL_P(scheme) && STR_CSTR_EQ(scheme, "on")) { *server_port = g_port_443; scheme = g_https; } else { scheme = rb_hash_aref(env, g_http_x_forwarded_proto); if (NIL_P(scheme)) { scheme = g_http; } else { long len = RSTRING_LEN(scheme); if (len >= 5 && !memcmp(RSTRING_PTR(scheme), "https", 5)) { if (len != 5) scheme = g_https; *server_port = g_port_443; } else { scheme = g_http; } } } rb_hash_aset(env, g_rack_url_scheme, scheme); } else if (STR_CSTR_EQ(scheme, "https")) { *server_port = g_port_443; } else { assert(*server_port == g_port_80 && "server_port not set"); } } /* * Parse and set the SERVER_NAME and SERVER_PORT variables * Not supporting X-Forwarded-Host/X-Forwarded-Port in here since * anybody who needs them is using an unsupported configuration and/or * incompetent. Rack::Request will handle X-Forwarded-{Port,Host} just * fine. */ static void set_server_vars(VALUE env, VALUE *server_port) { VALUE server_name = g_localhost; VALUE host = rb_hash_aref(env, g_http_host); if (!NIL_P(host)) { char *host_ptr = RSTRING_PTR(host); long host_len = RSTRING_LEN(host); char *colon; if (*host_ptr == '[') { /* ipv6 address format */ char *rbracket = memchr(host_ptr + 1, ']', host_len - 1); if (rbracket) colon = (rbracket[1] == ':') ? rbracket + 1 : NULL; else colon = memchr(host_ptr + 1, ':', host_len - 1); } else { colon = memchr(host_ptr, ':', host_len); } if (colon) { long port_start = colon - host_ptr + 1; server_name = rb_str_substr(host, 0, colon - host_ptr); if ((host_len - port_start) > 0) *server_port = rb_str_substr(host, port_start, host_len); } else { server_name = host; } } rb_hash_aset(env, g_server_name, server_name); rb_hash_aset(env, g_server_port, *server_port); } static void finalize_header(struct http_parser *hp) { VALUE server_port = g_port_80; set_url_scheme(hp->env, &server_port); set_server_vars(hp->env, &server_port); if (!HP_FL_TEST(hp, HASHEADER)) rb_hash_aset(hp->env, g_server_protocol, g_http_09); /* rack requires QUERY_STRING */ if (NIL_P(rb_hash_aref(hp->env, g_query_string))) rb_hash_aset(hp->env, g_query_string, rb_str_new(NULL, 0)); } static VALUE HttpParser_alloc(VALUE klass) { struct http_parser *hp; return TypedData_Make_Struct(klass, struct http_parser, &hp_type, hp); } /** * call-seq: * parser.new => parser * * Creates a new parser. */ static VALUE HttpParser_init(VALUE self) { struct http_parser *hp = data_get(self); http_parser_init(hp); hp->buf = rb_str_new(NULL, 0); hp->env = rb_hash_new(); return self; } /** * call-seq: * parser.clear => parser * * Resets the parser to it's initial state so that you can reuse it * rather than making new ones. */ static VALUE HttpParser_clear(VALUE self) { struct http_parser *hp = data_get(self); /* we can't safely reuse .buf and .env if hijacked */ if (HP_FL_TEST(hp, HIJACK)) return HttpParser_init(self); http_parser_init(hp); rb_hash_clear(hp->env); return self; } static void advance_str(VALUE str, off_t nr) { long len = RSTRING_LEN(str); if (len == 0) return; rb_str_modify(str); assert(nr <= len && "trying to advance past end of buffer"); len -= nr; if (len > 0) /* unlikely, len is usually 0 */ memmove(RSTRING_PTR(str), RSTRING_PTR(str) + nr, len); rb_str_set_len(str, len); } /** * call-seq: * parser.content_length => nil or Integer * * Returns the number of bytes left to run through HttpParser#filter_body. * This will initially be the value of the "Content-Length" HTTP header * after header parsing is complete and will decrease in value as * HttpParser#filter_body is called for each chunk. This should return * zero for requests with no body. * * This will return nil on "Transfer-Encoding: chunked" requests. */ static VALUE HttpParser_content_length(VALUE self) { struct http_parser *hp = data_get(self); return HP_FL_TEST(hp, CHUNKED) ? Qnil : OFFT2NUM(hp->len.content); } /** * Document-method: parse * call-seq: * parser.parse => env or nil * * Takes a Hash and a String of data, parses the String of data filling * in the Hash returning the Hash if parsing is finished, nil otherwise * When returning the env Hash, it may modify data to point to where * body processing should begin. * * Raises HttpParserError if there are parsing errors. */ static VALUE HttpParser_parse(VALUE self) { struct http_parser *hp = data_get(self); VALUE data = hp->buf; if (HP_FL_TEST(hp, TO_CLEAR)) HttpParser_clear(self); http_parser_execute(hp, RSTRING_PTR(data), RSTRING_LEN(data)); if (hp->offset > MAX_HEADER_LEN) parser_raise(e413, "HTTP header is too large"); if (hp->cs == http_parser_first_final || hp->cs == http_parser_en_ChunkedBody) { advance_str(data, hp->offset + 1); hp->offset = 0; if (HP_FL_TEST(hp, INTRAILER)) HP_FL_SET(hp, REQEOF); return hp->env; } if (hp->cs == http_parser_error) parser_raise(eHttpParserError, "Invalid HTTP format, parsing fails."); return Qnil; } /** * Document-method: parse * call-seq: * parser.add_parse(buffer) => env or nil * * adds the contents of +buffer+ to the internal buffer and attempts to * continue parsing. Returns the +env+ Hash on success or nil if more * data is needed. * * Raises HttpParserError if there are parsing errors. */ static VALUE HttpParser_add_parse(VALUE self, VALUE buffer) { struct http_parser *hp = data_get(self); Check_Type(buffer, T_STRING); rb_str_buf_append(hp->buf, buffer); return HttpParser_parse(self); } /** * Document-method: trailers * call-seq: * parser.trailers(req, data) => req or nil * * This is an alias for HttpParser#headers */ /** * Document-method: headers */ static VALUE HttpParser_headers(VALUE self, VALUE env, VALUE buf) { struct http_parser *hp = data_get(self); hp->env = env; hp->buf = buf; return HttpParser_parse(self); } static int chunked_eof(struct http_parser *hp) { return ((hp->cs == http_parser_first_final) || HP_FL_TEST(hp, INTRAILER)); } /** * call-seq: * parser.body_eof? => true or false * * Detects if we're done filtering the body or not. This can be used * to detect when to stop calling HttpParser#filter_body. */ static VALUE HttpParser_body_eof(VALUE self) { struct http_parser *hp = data_get(self); if (HP_FL_TEST(hp, CHUNKED)) return chunked_eof(hp) ? Qtrue : Qfalse; return hp->len.content == 0 ? Qtrue : Qfalse; } /** * call-seq: * parser.keepalive? => true or false * * This should be used to detect if a request can really handle * keepalives and pipelining. Currently, the rules are: * * 1. MUST be a GET or HEAD request * 2. MUST be HTTP/1.1 +or+ HTTP/1.0 with "Connection: keep-alive" * 3. MUST NOT have "Connection: close" set */ static VALUE HttpParser_keepalive(VALUE self) { struct http_parser *hp = data_get(self); return HP_FL_ALL(hp, KEEPALIVE) ? Qtrue : Qfalse; } /* :nodoc: */ static VALUE chunkable_response_p(VALUE self) { const struct http_parser *hp = data_get(self); return HP_FL_ALL(hp, RES_CHUNKABLE) ? Qtrue : Qfalse; } /** * call-seq: * parser.next? => true or false * * Exactly like HttpParser#keepalive?, except it will reset the internal * parser state on next parse if it returns true. */ static VALUE HttpParser_next(VALUE self) { struct http_parser *hp = data_get(self); if (HP_FL_ALL(hp, KEEPALIVE)) { HP_FL_SET(hp, TO_CLEAR); return Qtrue; } return Qfalse; } /** * call-seq: * parser.headers? => true or false * * This should be used to detect if a request has headers (and if * the response will have headers as well). HTTP/0.9 requests * should return false, all subsequent HTTP versions will return true */ static VALUE HttpParser_has_headers(VALUE self) { struct http_parser *hp = data_get(self); return HP_FL_TEST(hp, HASHEADER) ? Qtrue : Qfalse; } static VALUE HttpParser_buf(VALUE self) { return data_get(self)->buf; } static VALUE HttpParser_env(VALUE self) { return data_get(self)->env; } static VALUE HttpParser_hijacked_bang(VALUE self) { struct http_parser *hp = data_get(self); HP_FL_SET(hp, HIJACK); return self; } /** * call-seq: * parser.filter_body(dst, src) => nil/src * * Takes a String of +src+, will modify data if dechunking is done. * Returns +nil+ if there is more data left to process. Returns * +src+ if body processing is complete. When returning +src+, * it may modify +src+ so the start of the string points to where * the body ended so that trailer processing can begin. * * Raises HttpParserError if there are dechunking errors. * Basically this is a glorified memcpy(3) that copies +src+ * into +buf+ while filtering it through the dechunker. */ static VALUE HttpParser_filter_body(VALUE self, VALUE dst, VALUE src) { struct http_parser *hp = data_get(self); char *srcptr; long srclen; srcptr = RSTRING_PTR(src); srclen = RSTRING_LEN(src); StringValue(dst); if (HP_FL_TEST(hp, CHUNKED)) { if (!chunked_eof(hp)) { rb_str_modify(dst); rb_str_resize(dst, srclen); /* we can never copy more than srclen bytes */ hp->s.dest_offset = 0; hp->cont = dst; hp->buf = src; http_parser_execute(hp, srcptr, srclen); if (hp->cs == http_parser_error) parser_raise(eHttpParserError, "Invalid HTTP format, parsing fails."); assert(hp->s.dest_offset <= hp->offset && "destination buffer overflow"); advance_str(src, hp->offset); rb_str_set_len(dst, hp->s.dest_offset); if (RSTRING_LEN(dst) == 0 && chunked_eof(hp)) { assert(hp->len.chunk == 0 && "chunk at EOF but more to parse"); } else { src = Qnil; } } } else { /* no need to enter the Ragel machine for unchunked transfers */ assert(hp->len.content >= 0 && "negative Content-Length"); if (hp->len.content > 0) { long nr = MIN(srclen, hp->len.content); rb_str_modify(dst); rb_str_resize(dst, nr); /* * using rb_str_replace() to avoid memcpy() doesn't help in * most cases because a GC-aware programmer will pass an explicit * buffer to env["rack.input"].read and reuse the buffer in a loop. * This causes copy-on-write behavior to be triggered anyways * when the +src+ buffer is modified (when reading off the socket). */ hp->buf = src; memcpy(RSTRING_PTR(dst), srcptr, nr); hp->len.content -= nr; if (hp->len.content == 0) { HP_FL_SET(hp, REQEOF); hp->cs = http_parser_first_final; } advance_str(src, nr); src = Qnil; } } hp->offset = 0; /* for trailer parsing */ return src; } static VALUE HttpParser_rssset(VALUE self, VALUE boolean) { struct http_parser *hp = data_get(self); if (RTEST(boolean)) HP_FL_SET(hp, RESSTART); else HP_FL_UNSET(hp, RESSTART); return boolean; /* ignored by Ruby anyways */ } static VALUE HttpParser_rssget(VALUE self) { struct http_parser *hp = data_get(self); return HP_FL_TEST(hp, RESSTART) ? Qtrue : Qfalse; } #define SET_GLOBAL(var,str) do { \ var = find_common_field(str, sizeof(str) - 1); \ assert(!NIL_P(var) && "missed global field"); \ } while (0) void Init_unicorn_http(void) { VALUE mUnicorn; mUnicorn = rb_define_module("Unicorn"); cHttpParser = rb_define_class_under(mUnicorn, "HttpParser", rb_cObject); eHttpParserError = rb_define_class_under(mUnicorn, "HttpParserError", rb_eIOError); e413 = rb_define_class_under(mUnicorn, "RequestEntityTooLargeError", eHttpParserError); e414 = rb_define_class_under(mUnicorn, "RequestURITooLongError", eHttpParserError); id_uminus = rb_intern("-@"); init_globals(); rb_define_alloc_func(cHttpParser, HttpParser_alloc); rb_define_method(cHttpParser, "initialize", HttpParser_init, 0); rb_define_method(cHttpParser, "clear", HttpParser_clear, 0); rb_define_method(cHttpParser, "parse", HttpParser_parse, 0); rb_define_method(cHttpParser, "add_parse", HttpParser_add_parse, 1); rb_define_method(cHttpParser, "headers", HttpParser_headers, 2); rb_define_method(cHttpParser, "trailers", HttpParser_headers, 2); rb_define_method(cHttpParser, "filter_body", HttpParser_filter_body, 2); rb_define_method(cHttpParser, "content_length", HttpParser_content_length, 0); rb_define_method(cHttpParser, "body_eof?", HttpParser_body_eof, 0); rb_define_method(cHttpParser, "keepalive?", HttpParser_keepalive, 0); rb_define_method(cHttpParser, "chunkable_response?", chunkable_response_p, 0); rb_define_method(cHttpParser, "headers?", HttpParser_has_headers, 0); rb_define_method(cHttpParser, "next?", HttpParser_next, 0); rb_define_method(cHttpParser, "buf", HttpParser_buf, 0); rb_define_method(cHttpParser, "env", HttpParser_env, 0); rb_define_method(cHttpParser, "hijacked!", HttpParser_hijacked_bang, 0); rb_define_method(cHttpParser, "response_start_sent=", HttpParser_rssset, 1); rb_define_method(cHttpParser, "response_start_sent", HttpParser_rssget, 0); /* * The maximum size a single chunk when using chunked transfer encoding. * This is only a theoretical maximum used to detect errors in clients, * it is highly unlikely to encounter clients that send more than * several kilobytes at once. */ rb_define_const(cHttpParser, "CHUNK_MAX", OFFT2NUM(UH_OFF_T_MAX)); /* * The maximum size of the body as specified by Content-Length. * This is only a theoretical maximum, the actual limit is subject * to the limits of the file system used for +Dir.tmpdir+. */ rb_define_const(cHttpParser, "LENGTH_MAX", OFFT2NUM(UH_OFF_T_MAX)); rb_define_singleton_method(cHttpParser, "max_header_len=", set_maxhdrlen, 1); init_common_fields(); SET_GLOBAL(g_http_host, "HOST"); SET_GLOBAL(g_http_trailer, "TRAILER"); SET_GLOBAL(g_http_transfer_encoding, "TRANSFER_ENCODING"); SET_GLOBAL(g_content_length, "CONTENT_LENGTH"); SET_GLOBAL(g_http_connection, "CONNECTION"); id_set_backtrace = rb_intern("set_backtrace"); init_unicorn_httpdate(); id_is_chunked_p = rb_intern("is_chunked?"); init_epollexclusive(mUnicorn); } #undef SET_GLOBAL ext/unicorn_http/unicorn_http_common.rl000066400000000000000000000055251471646016400210350ustar00rootroot00000000000000%%{ machine unicorn_http_common; #### HTTP PROTOCOL GRAMMAR # line endings CRLF = ("\r\n" | "\n"); # character types CTL = (cntrl | 127); safe = ("$" | "-" | "_" | "."); extra = ("!" | "*" | "'" | "(" | ")" | ","); reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+"); sorta_safe = ("\"" | "<" | ">"); unsafe = (CTL | " " | "#" | "%" | sorta_safe); national = any -- (alpha | digit | reserved | extra | safe | unsafe); unreserved = (alpha | digit | safe | extra | national); escape = ("%" xdigit xdigit); uchar = (unreserved | escape | sorta_safe); pchar = (uchar | ":" | "@" | "&" | "=" | "+"); tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t"); lws = (" " | "\t"); content = ((any -- CTL) | lws); # elements token = (ascii -- (CTL | tspecials)); # URI schemes and absolute paths scheme = ( "http"i ("s"i)? ) $downcase_char >mark %scheme; hostname = ((alnum | "-" | "." | "_")+ | ("[" (":" | xdigit)+ "]")); host_with_port = (hostname (":" digit*)?) >mark %host; userinfo = ((unreserved | escape | ";" | ":" | "&" | "=" | "+")+ "@")*; path = ( pchar+ ( "/" pchar* )* ) ; query = ( uchar | reserved )* %query_string ; param = ( pchar | "/" )* ; params = ( param ( ";" param )* ) ; rel_path = (path? (";" params)? %request_path) ("?" %start_query query)?; absolute_path = ( "/"+ rel_path ); path_uri = absolute_path > mark %request_uri; Absolute_URI = (scheme "://" userinfo host_with_port path_uri); Request_URI = ((absolute_path | "*") >mark %request_uri) | Absolute_URI; Fragment = ( uchar | reserved )* >mark %fragment; Method = (token){1,20} >mark %request_method; GetOnly = "GET" >mark %request_method; http_number = ( digit+ "." digit+ ) ; HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ; Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ; field_name = ( token -- ":" )+ >start_field $upcase_field %write_field; field_value = content* >start_value %write_value; value_cont = lws+ content* >start_value %write_cont_value; message_header = ((field_name ":" lws* field_value)|value_cont) :> CRLF; chunk_ext_val = token*; chunk_ext_name = token*; chunk_extension = ( ";" " "* chunk_ext_name ("=" chunk_ext_val)? )*; last_chunk = "0"+ chunk_extension CRLF; chunk_size = (xdigit* [1-9a-fA-F] xdigit*) $add_to_chunk_size; chunk_end = CRLF; chunk_body = any >skip_chunk_data; chunk_begin = chunk_size chunk_extension CRLF; chunk = chunk_begin chunk_body chunk_end; ChunkedBody := chunk* last_chunk @end_chunked_body; Trailers := (message_header)* CRLF @end_trailers; FullRequest = Request_Line (message_header)* CRLF @header_done; SimpleRequest = GetOnly " " Request_URI ("#"Fragment){0,1} CRLF @header_done; main := FullRequest | SimpleRequest; }%% lib/000077500000000000000000000000001471646016400116355ustar00rootroot00000000000000lib/unicorn.rb000066400000000000000000000107031471646016400136400ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'etc' require 'stringio' require 'raindrops' require 'io/wait' begin require 'rack' rescue LoadError warn 'rack not available, functionality reduced' end # :stopdoc: # Unicorn module containing all of the classes (include C extensions) for # running a Unicorn web server. It contains a minimalist HTTP server with just # enough functionality to service web application requests fast as possible. # :startdoc: # unicorn exposes very little of an user-visible API and most of its # internals are subject to change. unicorn is designed to host Rack # applications, so applications should be written against the Rack SPEC # and not unicorn internals. module Unicorn # 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. This exception is visible to Rack # applications unless PrereadInput middleware is loaded. This # is a subclass of the standard EOFError class and applications should # not rescue it explicitly, but rescue EOFError instead. ClientShutdown = Class.new(EOFError) # :stopdoc: # This returns a lambda to pass in as the app, this does not "build" the # app (which we defer based on the outcome of "preload_app" in the # Unicorn config). The returned lambda will be called when it is # time to build the app. def self.builder(ru, op) # allow Configurator to parse cli switches embedded in the ru file op = Unicorn::Configurator::RACKUP.merge!(:file => ru, :optparse => op) if ru =~ /\.ru$/ && !defined?(Rack::Builder) abort "rack and Rack::Builder must be available for processing #{ru}" end # always called after config file parsing, may be called after forking lambda do |_, server| 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 if $DEBUG require 'pp' pp({ :inner_app => inner_app }) end return inner_app unless server.default_middleware middleware = { # order matters ContentLength: nil, CommonLogger: [ $stderr ], ShowExceptions: nil, Lint: nil, TempfileReaper: nil, } # return value, matches rackup defaults based on env # Unicorn does not support persistent connections, but Rainbows! # does. Users accustomed to the Rack::Server default # middlewares will need ContentLength middleware. case ENV["RACK_ENV"] when "development" when "deployment" middleware.delete(:ShowExceptions) middleware.delete(:Lint) else return inner_app end Rack::Builder.new do middleware.each do |m, args| use(Rack.const_get(m), *args) if Rack.const_defined?(m) end run inner_app end.to_app end end # returns an array of strings representing TCP listen socket addresses # and Unix domain socket paths. This is useful for use with # Raindrops::Middleware under Linux: https://yhbt.net/raindrops/ def self.listener_names Unicorn::HttpServer::LISTENERS.map do |io| Unicorn::SocketHelper.sock_name(io) end + Unicorn::HttpServer::NEW_LISTENERS end def self.log_error(logger, prefix, exc) message = exc.message message = message.dump if /[[:cntrl:]]/ =~ message logger.error "#{prefix}: #{message} (#{exc.class})" exc.backtrace.each { |line| logger.error(line) } end F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/ def self.pipe # :nodoc: IO.pipe.each do |io| # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft # limits. if defined?(F_SETPIPE_SZ) begin io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE) rescue Errno::EINVAL # old kernel rescue Errno::EPERM # resizes fail if Linux is close to the pipe limit for the user # or if the user does not have permissions to resize end end end end # :startdoc: end # :enddoc: %w(const socket_helper stream_input tee_input http_request configurator tmpio util http_response worker http_server).each do |s| require_relative "unicorn/#{s}" end lib/unicorn/000077500000000000000000000000001471646016400133125ustar00rootroot00000000000000lib/unicorn/app/000077500000000000000000000000001471646016400140725ustar00rootroot00000000000000lib/unicorn/app/old_rails.rb000066400000000000000000000017501471646016400163720ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: # This code is based on the original Rails handler in Mongrel # Copyright (c) 2005 Zed A. Shaw # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # Additional work donated by contributors. See CONTRIBUTORS for more info. require 'unicorn/cgi_wrapper' require 'dispatcher' module Unicorn; module App; end; end # Implements a handler that can run Rails. class Unicorn::App::OldRails autoload :Static, "unicorn/app/old_rails/static" def call(env) cgi = Unicorn::CGIWrapper.new(env) begin Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.body) rescue => e err = env['rack.errors'] err.write("#{e} #{e.message}\n") e.backtrace.each { |line| err.write("#{line}\n") } end cgi.out # finalize the response cgi.rack_response end end lib/unicorn/app/old_rails/000077500000000000000000000000001471646016400160425ustar00rootroot00000000000000lib/unicorn/app/old_rails/static.rb000066400000000000000000000040041471646016400176540ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: # This code is based on the original Rails handler in Mongrel # Copyright (c) 2005 Zed A. Shaw # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv3 # Static file handler for Rails < 2.3. This handler is only provided # as a convenience for developers. Performance-minded deployments should # use nginx (or similar) for serving static files. # # This supports page caching directly and will try to resolve a # request in the following order: # # * If the requested exact PATH_INFO exists as a file then serve it. # * If it exists at PATH_INFO+rest_operator+".html" exists # then serve that. # # This means that if you are using page caching it will actually work # with Unicorn and you should see a decent speed boost (but not as # fast as if you use a static server like nginx). class Unicorn::App::OldRails::Static < Struct.new(:app, :root, :file_server) FILE_METHODS = { 'GET' => true, 'HEAD' => true } # avoid allocating new strings for hash lookups REQUEST_METHOD = 'REQUEST_METHOD' REQUEST_URI = 'REQUEST_URI' PATH_INFO = 'PATH_INFO' def initialize(app) self.app = app self.root = "#{::RAILS_ROOT}/public" self.file_server = ::Rack::File.new(root) end def call(env) # short circuit this ASAP if serving non-file methods FILE_METHODS.include?(env[REQUEST_METHOD]) or return app.call(env) # first try the path as-is path_info = env[PATH_INFO].chomp("/") if File.file?("#{root}/#{::Rack::Utils.unescape(path_info)}") # File exists as-is so serve it up env[PATH_INFO] = path_info return file_server.call(env) end # then try the cached version: path_info << ActionController::Base.page_cache_extension if File.file?("#{root}/#{::Rack::Utils.unescape(path_info)}") env[PATH_INFO] = path_info return file_server.call(env) end app.call(env) # call OldRails end end if defined?(Unicorn::App::OldRails) lib/unicorn/cgi_wrapper.rb000066400000000000000000000117501471646016400161450ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: # This code is based on the original CGIWrapper from Mongrel # Copyright (c) 2005 Zed A. Shaw # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # # Additional work donated by contributors. See CONTRIBUTORS for more info. require 'cgi' module Unicorn; end # The beginning of a complete wrapper around Unicorn's internal HTTP # processing system but maintaining the original Ruby CGI module. Use # this only as a crutch to get existing CGI based systems working. It # should handle everything, but please notify us if you see special # warnings. This work is still very alpha so we need testers to help # work out the various corner cases. class Unicorn::CGIWrapper < ::CGI undef_method :env_table attr_reader :env_table attr_reader :body # these are stripped out of any keys passed to CGIWrapper.header function NPH = 'nph'.freeze # Completely ignored, Unicorn outputs the date regardless CONNECTION = 'connection'.freeze # Completely ignored. Why is CGI doing this? CHARSET = 'charset'.freeze # this gets appended to Content-Type COOKIE = 'cookie'.freeze # maps (Hash,Array,String) to "Set-Cookie" headers STATUS = 'status'.freeze # stored as @status Status = 'Status'.freeze # code + human-readable text, Rails sets this # some of these are common strings, but this is the only module # using them and the reason they're not in Unicorn::Const SET_COOKIE = 'Set-Cookie'.freeze CONTENT_TYPE = 'Content-Type'.freeze CONTENT_LENGTH = 'Content-Length'.freeze # this is NOT Const::CONTENT_LENGTH RACK_INPUT = 'rack.input'.freeze RACK_ERRORS = 'rack.errors'.freeze # this maps CGI header names to HTTP header names HEADER_MAP = { 'status' => Status, 'type' => CONTENT_TYPE, 'server' => 'Server'.freeze, 'language' => 'Content-Language'.freeze, 'expires' => 'Expires'.freeze, 'length' => CONTENT_LENGTH, } # Takes an a Rackable environment, plus any additional CGI.new # arguments These are used internally to create a wrapper around the # real CGI while maintaining Rack/Unicorn's view of the world. This # this will NOT deal well with large responses that take up a lot of # memory, but neither does the CGI nor the original CGIWrapper from # Mongrel... def initialize(rack_env, *args) @env_table = rack_env @status = nil @head = {} @headv = Hash.new { |hash,key| hash[key] = [] } @body = StringIO.new("") super(*args) end # finalizes the response in a way Rack applications would expect def rack_response # @head[CONTENT_LENGTH] ||= @body.size @headv[SET_COOKIE].concat(@output_cookies) if @output_cookies @headv.each_pair do |key,value| @head[key] ||= value.join("\n") unless value.empty? end # Capitalized "Status:", with human-readable status code (e.g. "200 OK") @status ||= @head.delete(Status) [ @status || 500, @head, [ @body.string ] ] end # The header is typically called to send back the header. In our case we # collect it into a hash for later usage. This can be called multiple # times to set different cookies. def header(options = "text/html") # if they pass in a string then just write the Content-Type if String === options @head[CONTENT_TYPE] ||= options else HEADER_MAP.each_pair do |from, to| from = options.delete(from) or next @head[to] = from.to_s end @head[CONTENT_TYPE] ||= "text/html" if charset = options.delete(CHARSET) @head[CONTENT_TYPE] << "; charset=#{charset}" end # lots of ways to set cookies if cookie = options.delete(COOKIE) set_cookies = @headv[SET_COOKIE] case cookie when Array cookie.each { |c| set_cookies << c.to_s } when Hash cookie.each_value { |c| set_cookies << c.to_s } else set_cookies << cookie.to_s end end @status ||= options.delete(STATUS) # all lower-case # drop the keys we don't want anymore options.delete(NPH) options.delete(CONNECTION) # finally, set the rest of the headers as-is, allowing duplicates options.each_pair { |k,v| @headv[k] << v } end # doing this fakes out the cgi library to think the headers are empty # we then do the real headers in the out function call later "" end # The dumb thing is people can call header or this or both and in # any order. So, we just reuse header and then finalize the # HttpResponse the right way. This will have no effect if called # the second time if the first "outputted" anything. def out(options = "text/html") header(options) @body.size == 0 or return @body << yield if block_given? end # Used to wrap the normal stdinput variable used inside CGI. def stdinput @env_table[RACK_INPUT] end # return a pointer to the StringIO body since it's STDOUT-like def stdoutput @body end end lib/unicorn/configurator.rb000066400000000000000000000665441471646016400163600ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'logger' # Implements a simple DSL for configuring a unicorn server. # # See https://yhbt.net/unicorn/examples/unicorn.conf.rb and # https://yhbt.net/unicorn/examples/unicorn.conf.minimal.rb # example configuration files. An example config file for use with # nginx is also available at # https://yhbt.net/unicorn/examples/nginx.conf # # See the link:/TUNING.html document for more information on tuning unicorn. class Unicorn::Configurator include Unicorn # :stopdoc: attr_accessor :set, :config_file, :after_reload # used to stash stuff for deferred processing of cli options in # config.ru after "working_directory" is bound. Do not rely on # this being around later on... RACKUP = { :daemonize => false, :host => Unicorn::Const::DEFAULT_HOST, :port => Unicorn::Const::DEFAULT_PORT, :set_listener => false, :options => { :listeners => [] } } # Default settings for Unicorn DEFAULTS = { :timeout => 60, :logger => Logger.new($stderr), :worker_processes => 1, :after_fork => lambda { |server, worker| server.logger.info("worker=#{worker.nr} spawned pid=#{$$}") }, :before_fork => lambda { |server, worker| server.logger.info("worker=#{worker.nr} spawning...") }, :before_exec => lambda { |server| server.logger.info("forked child re-executing...") }, :after_worker_exit => lambda { |server, worker, status| m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" if status.success? server.logger.info(m) else server.logger.error(m) end }, :after_worker_ready => lambda { |server, worker| server.logger.info("worker=#{worker.nr} ready") }, :pid => nil, :early_hints => false, :worker_exec => false, :preload_app => false, :check_client_connection => false, :rewindable_input => true, :client_body_buffer_size => Unicorn::Const::MAX_BODY, } #:startdoc: def initialize(defaults = {}) #:nodoc: self.set = Hash.new(:unset) @use_defaults = defaults.delete(:use_defaults) self.config_file = defaults.delete(:config_file) # after_reload is only used by unicorn_rails, unsupported otherwise self.after_reload = defaults.delete(:after_reload) set.merge!(DEFAULTS) if @use_defaults defaults.each { |key, value| self.__send__(key, value) } Hash === set[:listener_opts] or set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} } Array === set[:listeners] or set[:listeners] = [] reload(false) end def reload(merge_defaults = true) #:nodoc: if merge_defaults && @use_defaults set.merge!(DEFAULTS) if @use_defaults end instance_eval(File.read(config_file), config_file) if config_file parse_rackup_file RACKUP[:set_listener] and set[:listeners] << "#{RACKUP[:host]}:#{RACKUP[:port]}" RACKUP[:no_default_middleware] and set[:default_middleware] = false # unicorn_rails creates dirs here after working_directory is bound after_reload.call if after_reload # 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 commit!(server, options = {}) #:nodoc: skip = options[:skip] || [] if ready_pipe = RACKUP.delete(:ready_pipe) server.ready_pipe = ready_pipe end if set[:check_client_connection] set[:listeners].each do |address| if set[:listener_opts][address][:tcp_nopush] == true raise ArgumentError, "check_client_connection is incompatible with tcp_nopush:true" end end end set.each do |key, value| value == :unset and next skip.include?(key) and next server.__send__("#{key}=", value) end end def [](key) # :nodoc: set[key] end # sets object to the +obj+ Logger-like object. The new Logger-like # object must respond to the following methods: # * debug # * info # * warn # * error # * fatal # The default Logger will log its output to the path specified # by +stderr_path+. If you're running Unicorn daemonized, then # you must specify a path to prevent error messages from going # to /dev/null. def logger(obj) %w(debug info warn error fatal).each do |m| obj.respond_to?(m) and next raise ArgumentError, "logger=#{obj} does not respond to method=#{m}" end set[:logger] = obj end # sets after_fork hook to a given block. This block will be called by # the worker after forking. The following is an example hook which adds # a per-process listener to every worker: # # after_fork do |server,worker| # # per-process listener ports for debugging/admin: # addr = "127.0.0.1:#{9293 + worker.nr}" # # # the negative :tries parameter indicates we will retry forever # # waiting on the existing process to exit with a 5 second :delay # # Existing options for Unicorn::Configurator#listen such as # # :backlog, :rcvbuf, :sndbuf are available here as well. # server.listen(addr, :tries => -1, :delay => 5, :backlog => 128) # end def after_fork(*args, &block) set_hook(:after_fork, block_given? ? block : args[0]) end # sets after_worker_exit hook to a given block. This block will be called # by the master process after a worker exits: # # after_worker_exit do |server,worker,status| # # status is a Process::Status instance for the exited worker process # unless status.success? # server.logger.error("worker process failure: #{status.inspect}") # end # end # # after_worker_exit is only available in unicorn 5.3.0+ def after_worker_exit(*args, &block) set_hook(:after_worker_exit, block_given? ? block : args[0], 3) end # sets after_worker_ready hook to a given block. This block will be called # by a worker process after it has been fully loaded, directly before it # starts responding to requests: # # after_worker_ready do |server,worker| # server.logger.info("worker #{worker.nr} ready, dropping privileges") # worker.user('username', 'groupname') # end # # Do not use Configurator#user if you rely on changing users in the # after_worker_ready hook. # # after_worker_ready is only available in unicorn 5.3.0+ def after_worker_ready(*args, &block) set_hook(:after_worker_ready, block_given? ? block : args[0]) end # sets before_fork got be a given Proc object. This Proc # object will be called by the master process before forking # each worker. def before_fork(*args, &block) set_hook(:before_fork, block_given? ? block : args[0]) end # sets the before_exec hook to a given Proc object. This # Proc object will be called by the master process right # before exec()-ing the new unicorn binary. This is useful # for freeing certain OS resources that you do NOT wish to # share with the reexeced child process. # There is no corresponding after_exec hook (for obvious reasons). def before_exec(*args, &block) set_hook(:before_exec, block_given? ? block : args[0], 1) end # Strongly consider using link:/Application_Timeouts.html instead # of this misfeature. This misfeature has done decades of damage # to Ruby since it demotivates the use of fine-grained timeout # mechanisms. # # Sets the timeout of worker processes to +seconds+. Workers # handling the request/app.call/response cycle taking longer than # this time period will be forcibly killed (via SIGKILL). This # timeout is enforced by the master process itself and not subject # to the scheduling limitations by the worker process. Due the # low-complexity, low-overhead implementation, timeouts of less # than 3.0 seconds can be considered inaccurate and unsafe. # # For running Unicorn behind nginx, it is recommended to set # "fail_timeout=0" for in your nginx configuration like this # to have nginx always retry backends that may have had workers # SIGKILL-ed due to timeouts. # # upstream unicorn_backend { # # for UNIX domain socket setups: # server unix:/path/to/.unicorn.sock fail_timeout=0; # # # for TCP setups # server 192.168.0.7:8080 fail_timeout=0; # server 192.168.0.8:8080 fail_timeout=0; # server 192.168.0.9:8080 fail_timeout=0; # } # # See https://nginx.org/en/docs/http/ngx_http_upstream_module.html # for more details on nginx upstream configuration. def timeout(seconds) set_int(:timeout, seconds, 3) # POSIX says 31 days is the smallest allowed maximum timeout for select() max = 30 * 60 * 60 * 24 set[:timeout] = seconds > max ? max : seconds end # Whether to exec in each worker process after forking. This changes the # memory layout of each worker process, which is a security feature designed # to defeat possible address space discovery attacks. Note that using # worker_exec only makes sense if you are not preloading the application, # and will result in higher memory usage. # # worker_exec is only available in unicorn 5.3.0+ def worker_exec(bool) set_bool(:worker_exec, bool) end # sets the current number of worker_processes to +nr+. Each worker # process will serve exactly one client at a time. You can # increment or decrement this value at runtime by sending SIGTTIN # or SIGTTOU respectively to the master process without reloading # the rest of your Unicorn configuration. See the SIGNALS document # for more information. def worker_processes(nr) set_int(:worker_processes, nr, 1) end # sets whether to add default middleware in the development and # deployment RACK_ENVs. # # default_middleware is only available in unicorn 5.5.0+ def default_middleware(bool) set_bool(:default_middleware, bool) end # sets whether to enable the proposed early hints Rack API. # If enabled, Rails 5.2+ will automatically send a 103 Early Hint # for all the `javascript_include_tag` and `stylesheet_link_tag` # in your response. See: https://api.rubyonrails.org/v5.2/classes/ActionDispatch/Request.html#method-i-send_early_hints # See also https://tools.ietf.org/html/rfc8297 def early_hints(bool) set_bool(:early_hints, bool) end # sets listeners to the given +addresses+, replacing or augmenting the # current set. This is for the global listener pool shared by all # worker processes. For per-worker listeners, see the after_fork example # This is for internal API use only, do not use it in your Unicorn # config file. Use listen instead. def listeners(addresses) # :nodoc: Array === addresses or addresses = Array(addresses) addresses.map! { |addr| expand_addr(addr) } set[:listeners] = addresses end # Adds an +address+ to the existing listener set. May be specified more # than once. +address+ may be an Integer port number for a TCP port, an # "IP_ADDRESS:PORT" for TCP listeners or a pathname for UNIX domain sockets. # # listen 3000 # listen to port 3000 on all TCP interfaces # listen "127.0.0.1:3000" # listen to port 3000 on the loopback interface # listen "/path/to/.unicorn.sock" # listen on the given Unix domain socket # listen "[::1]:3000" # listen to port 3000 on the IPv6 loopback interface # # When using Unix domain sockets, be sure: # 1) the path matches the one used by nginx # 2) uses the same filesystem namespace as the nginx process # For systemd users using PrivateTmp=true (for either nginx or unicorn), # this means Unix domain sockets must not be placed in /tmp # # The following options may be specified (but are generally not needed): # # [:backlog => number of clients] # # This is the backlog of the listen() syscall. # # Some operating systems allow negative values here to specify the # maximum allowable value. In most cases, this number is only # recommendation and there are other OS-specific tunables and # variables that can affect this number. See the listen(2) # syscall documentation of your OS for the exact semantics of # this. # # If you are running unicorn on multiple machines, lowering this number # can help your load balancer detect when a machine is overloaded # and give requests to a different machine. # # Default: 1024 # # Note: with the Linux kernel, the net.core.somaxconn sysctl defaults # to 128, capping this value to 128. Raising the sysctl allows a # larger backlog (which may not be desirable with multiple, # load-balanced machines). # # [:rcvbuf => bytes, :sndbuf => bytes] # # Maximum receive and send buffer sizes (in bytes) of sockets. # # These correspond to the SO_RCVBUF and SO_SNDBUF settings which # can be set via the setsockopt(2) syscall. Some kernels # (e.g. Linux 2.4+) have intelligent auto-tuning mechanisms and # there is no need (and it is sometimes detrimental) to specify them. # # See the socket API documentation of your operating system # to determine the exact semantics of these settings and # other operating system-specific knobs where they can be # specified. # # Defaults: operating system defaults # # [:tcp_nodelay => true or false] # # Disables Nagle's algorithm on TCP sockets if +true+. # # Setting this to +true+ can make streaming responses in Rails 3.1 # appear more quickly at the cost of slightly higher bandwidth usage. # The effect of this option is most visible if nginx is not used, # but nginx remains highly recommended with unicorn. # # This has no effect on UNIX sockets. # # Default: +true+ (Nagle's algorithm disabled) in unicorn # This defaulted to +false+ in unicorn 3.x # # [:tcp_nopush => true or false] # # Enables/disables TCP_CORK in Linux or TCP_NOPUSH in FreeBSD # # This prevents partial TCP frames from being sent out and reduces # wakeups in nginx if it is on a different machine. Since unicorn # is only designed for applications that send the response body # quickly without keepalive, sockets will always be flushed on close # to prevent delays. # # This has no effect on UNIX sockets. # # Default: +false+ # This defaulted to +true+ in unicorn 3.4 - 3.7 # # [:ipv6only => true or false] # # This option makes IPv6-capable TCP listeners IPv6-only and unable # to receive IPv4 queries on dual-stack systems. A separate IPv4-only # listener is required if this is true. # # Enabling this option for the IPv6-only listener and having a # separate IPv4 listener is recommended if you wish to support IPv6 # on the same TCP port. Otherwise, the value of \env[\"REMOTE_ADDR\"] # will appear as an ugly IPv4-mapped-IPv6 address for IPv4 clients # (e.g ":ffff:10.0.0.1" instead of just "10.0.0.1"). # # Default: Operating-system dependent # # [:reuseport => true or false] # # This enables multiple, independently-started unicorn instances to # bind to the same port (as long as all the processes enable this). # # This option must be used when unicorn first binds the listen socket. # It cannot be enabled when a socket is inherited via SIGUSR2 # (but it will remain on if inherited), and it cannot be enabled # directly via SIGHUP. # # Note: there is a chance of connections being dropped if # one of the unicorn instances is stopped while using this. # # This is supported on *BSD systems and Linux 3.9 or later. # # ref: https://lwn.net/Articles/542629/ # # Default: false (unset) # # [:tries => Integer] # # Times to retry binding a socket if it is already in use # # A negative number indicates we will retry indefinitely, this is # useful for migrations and upgrades when individual workers # are binding to different ports. # # Default: 5 # # [:delay => seconds] # # Seconds to wait between successive +tries+ # # Default: 0.5 seconds # # [:umask => mode] # # Sets the file mode creation mask for UNIX sockets. If specified, # this is usually in octal notation. # # Typically UNIX domain sockets are created with more liberal # file permissions than the rest of the application. By default, # we create UNIX domain sockets to be readable and writable by # all local users to give them the same accessibility as # locally-bound TCP listeners. # # This has no effect on TCP listeners. # # Default: 0000 (world-read/writable) # # [:tcp_defer_accept => Integer] # # Defer accept() until data is ready (Linux-only) # # For Linux 2.6.32 and later, this is the number of retransmits to # defer an accept() for if no data arrives, but the client will # eventually be accepted after the specified number of retransmits # regardless of whether data is ready. # # For Linux before 2.6.32, this is a boolean option, and # accepts are _always_ deferred indefinitely if no data arrives. # This is similar to :accept_filter => "dataready" # under FreeBSD. # # Specifying +true+ is synonymous for the default value(s) below, # and +false+ or +nil+ is synonymous for a value of zero. # # A value of +1+ is a good optimization for local networks # and trusted clients. There is no good reason to ever # disable this with a +zero+ value with unicorn. # # Default: 1 # # [:accept_filter => String] # # defer accept() until data is ready (FreeBSD-only) # # This enables either the "dataready" or (default) "httpready" # accept() filter under FreeBSD. This is intended as an # optimization to reduce context switches with common GET/HEAD # requests. # # There is no good reason to change from the default. # # Default: "httpready" def listen(address, options = {}) address = expand_addr(address) if String === address [ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key| value = options[key] or next Integer === value or raise ArgumentError, "not an integer: #{key}=#{value.inspect}" end [ :tcp_nodelay, :tcp_nopush, :ipv6only, :reuseport ].each do |key| (value = options[key]).nil? and next TrueClass === value || FalseClass === value or raise ArgumentError, "not boolean: #{key}=#{value.inspect}" end unless (value = options[:delay]).nil? Numeric === value or raise ArgumentError, "not numeric: delay=#{value.inspect}" end set[:listener_opts][address].merge!(options) end set[:listeners] << address end # sets the +path+ for the PID file of the unicorn master process def pid(path); set_path(:pid, path); end # Enabling this preloads an application before forking worker # processes. This allows memory savings when using a # copy-on-write-friendly GC but can cause bad things to happen when # resources like sockets are opened at load time by the master # process and shared by multiple children. People enabling this are # highly encouraged to look at the before_fork/after_fork hooks to # properly close/reopen sockets. Files opened for logging do not # have to be reopened as (unbuffered-in-userspace) files opened with # the File::APPEND flag are written to atomically on UNIX. # # In addition to reloading the unicorn-specific config settings, # SIGHUP will reload application code in the working # directory/symlink when workers are gracefully restarted when # preload_app=false (the default). As reloading the application # sometimes requires RubyGems updates, +Gem.refresh+ is always # called before the application is loaded (for RubyGems users). # # During deployments, care should _always_ be taken to ensure your # applications are properly deployed and running. Using # preload_app=false (the default) means you _must_ check if # your application is responding properly after a deployment. # Improperly deployed applications can go into a spawn loop # if the application fails to load. While your children are # in a spawn loop, it is is possible to fix an application # by properly deploying all required code and dependencies. # Using preload_app=true means any application load error will # cause the master process to exit with an error. def preload_app(bool) set_bool(:preload_app, bool) end # Toggles making \env[\"rack.input\"] rewindable. # Disabling rewindability can improve performance by lowering # I/O and memory usage for applications that accept uploads. # Keep in mind that the Rack 1.x spec requires # \env[\"rack.input\"] to be rewindable, # but the Rack 2.x spec does not. # # +rewindable_input+ defaults to +true+ for compatibility. # Setting it to +false+ may be safe for applications and # frameworks developed for Rack 2.x and later. def rewindable_input(bool) set_bool(:rewindable_input, bool) end # The maximum size (in +bytes+) to buffer in memory before # resorting to a temporary file. Default is 112 kilobytes. # This option has no effect if "rewindable_input" is set to # +false+. def client_body_buffer_size(bytes) set_int(:client_body_buffer_size, bytes, 0) end # When enabled, unicorn will check the client connection by writing # the beginning of the HTTP headers before calling the application. # # This will prevent calling the application for clients who have # disconnected while their connection was queued. # # This only affects clients connecting over Unix domain sockets # and TCP via loopback (127.*.*.*). It is unlikely to detect # disconnects if the client is on a remote host (even on a fast LAN). # # This option cannot be used in conjunction with :tcp_nopush. def check_client_connection(bool) set_bool(:check_client_connection, bool) end # Allow redirecting $stderr to a given path. Unlike doing this from # the shell, this allows the unicorn process to know the path its # writing to and rotate the file if it is used for logging. The # file will be opened with the File::APPEND flag and writes # synchronized to the kernel (but not necessarily to _disk_) so # multiple processes can safely append to it. # # If you are daemonizing and using the default +logger+, it is important # to specify this as errors will otherwise be lost to /dev/null. # Some applications/libraries may also triggering warnings that go to # stderr, and they will end up here. def stderr_path(path) set_path(:stderr_path, path) end # Same as stderr_path, except for $stdout. Not many Rack applications # write to $stdout, but any that do will have their output written here. # It is safe to point this to the same location a stderr_path. # Like stderr_path, this defaults to /dev/null when daemonized. def stdout_path(path) set_path(:stdout_path, path) end # sets the working directory for Unicorn. This ensures SIGUSR2 will # start a new instance of Unicorn in this directory. This may be # a symlink, a common scenario for Capistrano users. Unlike # all other Unicorn 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) # just let chdir raise errors path = File.expand_path(path) if config_file && ! config_file.start_with?('/') && ! File.readable?("#{path}/#{config_file}") raise ArgumentError, "config_file=#{config_file} would not be accessible in" \ " working_directory=#{path}" end Dir.chdir(path) Unicorn::HttpServer::START_CTX[:cwd] = 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 hook, and only # if the Worker#user method is not called in the after_fork hook # +group+ is optional and will not change if unspecified. # # Do not use Configurator#user if you rely on changing users in the # after_worker_ready hook. Instead, you need to call Worker#user # directly in after_worker_ready. def user(user, group = nil) # raises ArgumentError on invalid user/group Etc.getpwnam(user) Etc.getgrnam(group) if group set[:user] = [ user, group ] 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 private def set_int(var, n, min) #:nodoc: Integer === n or raise ArgumentError, "not an integer: #{var}=#{n.inspect}" n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}" set[var] = n end def canonicalize_tcp(addr, port) packed = Socket.pack_sockaddr_in(port, addr) port, addr = Socket.unpack_sockaddr_in(packed) addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}" end def set_path(var, path) #:nodoc: case path when NilClass, String set[var] = path else raise ArgumentError end end def check_bool(var, bool) # :nodoc: case bool when true, false return bool end raise ArgumentError, "#{var}=#{bool.inspect} not a boolean" end def set_bool(var, bool) #:nodoc: set[var] = check_bool(var, bool) end def set_hook(var, my_proc, req_arity = 2) #:nodoc: case my_proc when Proc arity = my_proc.arity (arity == req_arity) or \ raise ArgumentError, "#{var}=#{my_proc.inspect} has invalid arity: " \ "#{arity} (need #{req_arity})" when NilClass my_proc = DEFAULTS[var] else raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}" end set[var] = my_proc end # this is called _after_ working_directory is bound. This only # parses the embedded switches in .ru files # (for "rackup" compatibility) def parse_rackup_file # :nodoc: ru = RACKUP[:file] or return # we only return here in unit tests # :rails means use (old) Rails autodetect if ru == :rails File.readable?('config.ru') or return ru = 'config.ru' end File.readable?(ru) or raise ArgumentError, "rackup file (#{ru}) not readable" # it could be a .rb file, too, we don't parse those manually ru.end_with?('.ru') or return /^#\\(.*)/ =~ File.read(ru) or return RACKUP[:optparse].parse!($1.split(/\s+/)) if RACKUP[:daemonize] # unicorn_rails wants a default pid path, (not plain 'unicorn') if after_reload spid = set[:pid] pid('tmp/pids/unicorn.pid') if spid.nil? || spid == :unset end unless RACKUP[:daemonized] Unicorn::Launcher.daemonize!(RACKUP[:options]) RACKUP[:ready_pipe] = RACKUP[:options].delete(:ready_pipe) end end end end lib/unicorn/const.rb000066400000000000000000000012541471646016400147670ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false module Unicorn::Const # :nodoc: # default TCP listen host address (0.0.0.0, all interfaces) DEFAULT_HOST = "0.0.0.0" # default TCP listen port (8080) DEFAULT_PORT = 8080 # default TCP listen address and port (0.0.0.0:8080) DEFAULT_LISTEN = "#{DEFAULT_HOST}:#{DEFAULT_PORT}" # The basic request body size we'll try to read at once (16 kilobytes). CHUNK_SIZE = 16 * 1024 # Maximum request body size before it is moved out of memory and into a # temporary file for reading (112 kilobytes). This is the default # value of client_body_buffer_size. MAX_BODY = 1024 * 112 end require_relative 'version' lib/unicorn/http_request.rb000066400000000000000000000140001471646016400163610ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: # no stable API here require 'unicorn_http' # TODO: remove redundant names Unicorn.const_set(:HttpRequest, Unicorn::HttpParser) class Unicorn::HttpParser # default parameters we merge into the request env for Rack handlers DEFAULTS = { "rack.errors" => $stderr, "rack.multiprocess" => true, "rack.multithread" => false, "rack.run_once" => false, "rack.version" => [1, 2], "rack.hijack?" => true, "SCRIPT_NAME" => "", # this is not in the Rack spec, but some apps may rely on it "SERVER_SOFTWARE" => "Unicorn #{Unicorn::Const::UNICORN_VERSION}" } NULL_IO = StringIO.new("") # :stopdoc: HTTP_RESPONSE_START = [ 'HTTP'.freeze, '/1.1 '.freeze ] EMPTY_ARRAY = [].freeze @@input_class = Unicorn::TeeInput @@check_client_connection = false @@tcpi_inspect_ok = Socket.const_defined?(:TCP_INFO) def self.input_class @@input_class end def self.input_class=(klass) @@input_class = klass end def self.check_client_connection @@check_client_connection end def self.check_client_connection=(bool) @@check_client_connection = bool end # :startdoc: # Does the majority of the IO processing. It has been written in # Ruby using about 8 different IO processing strategies. # # It is currently carefully constructed to make sure that it gets # the best possible performance for the common case: GET requests # that are fully complete after a single read(2) # # Anyone who thinks they can make it faster is more than welcome to # take a crack at it. # # returns an environment hash suitable for Rack if successful # This does minimal exception trapping and it is up to the caller # to handle any socket errors (e.g. user aborted upload). def read_headers(socket, ai) e = env # From https://www.ietf.org/rfc/rfc3875: # "Script authors should be aware that the REMOTE_ADDR and # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) # may not identify the ultimate source of the request. They # identify the client for the immediate request to the server; # that client may be a proxy, gateway, or other intermediary # acting on behalf of the actual source client." e['REMOTE_ADDR'] = ai.unix? ? '127.0.0.1' : ai.ip_address # short circuit the common case with small GET requests first socket.readpartial(16384, buf) if parse.nil? # Parser is not done, queue up more data to read and continue parsing # an Exception thrown from the parser will throw us out of the loop false until add_parse(socket.readpartial(16384)) end check_client_connection(socket, ai) if @@check_client_connection e['rack.input'] = 0 == content_length ? NULL_IO : @@input_class.new(socket, self) # for Rack hijacking in Rack 1.5 and later e['unicorn.socket'] = socket e['rack.hijack'] = self e.merge!(DEFAULTS) end # for rack.hijack, we respond to this method so no extra allocation # of a proc object def call hijacked! env['rack.hijack_io'] = env['unicorn.socket'] end def hijacked? env.include?('rack.hijack_io'.freeze) end if Raindrops.const_defined?(:TCP_Info) TCPI = Raindrops::TCP_Info.allocate def check_client_connection(socket, ai) # :nodoc: if ai.ip? # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state) raise Errno::EPIPE, "client closed connection".freeze, EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state) else write_http_header(socket) end end if Raindrops.const_defined?(:TCP) # raindrops 0.18.0+ supports FreeBSD + Linux using the same names # Evaluate these hash lookups at load time so we can # generate an opt_case_dispatch instruction eval <<-EOS def closed_state?(state) # :nodoc: case state when #{Raindrops::TCP[:ESTABLISHED]} false when #{Raindrops::TCP.values_at( :CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')} true else false end end EOS else # raindrops before 0.18 only supported TCP_INFO under Linux def closed_state?(state) # :nodoc: case state when 1 # ESTABLISHED false when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING true else false end end end else # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect. # Not that efficient, but probably still better than doing unnecessary # work after a client gives up. def check_client_connection(socket, ai) # :nodoc: if @@tcpi_inspect_ok && ai.ip? opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect if opt =~ /\bstate=(\S+)/ raise Errno::EPIPE, "client closed connection".freeze, EMPTY_ARRAY if closed_state_str?($1) else @@tcpi_inspect_ok = false write_http_header(socket) end opt.clear else write_http_header(socket) end end def closed_state_str?(state) case state when 'ESTABLISHED' false # not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D') when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING' true else false end end end def write_http_header(socket) # :nodoc: if headers? self.response_start_sent = true HTTP_RESPONSE_START.each { |c| socket.write(c) } end end # called by ext/unicorn_http/unicorn_http.rl via rb_funcall def self.is_chunked?(v) # :nodoc: vals = v.split(/[ \t]*,[ \t]*/).map!(&:downcase) if vals.pop == 'chunked'.freeze return true unless vals.include?('chunked'.freeze) raise Unicorn::HttpParserError, 'double chunked', [] end return false unless vals.include?('chunked'.freeze) raise Unicorn::HttpParserError, 'chunked not last', [] end end lib/unicorn/http_response.rb000066400000000000000000000060411471646016400165350ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: # Writes a Rack response to your client using the HTTP/1.1 specification. # You use it by simply doing: # # status, headers, body = rack_app.call(env) # http_response_write(socket, status, headers, body) # # Most header correctness (including Content-Length and Content-Type) # is the job of Rack, with the exception of the "Date" and "Status" header. module Unicorn::HttpResponse STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ? Rack::Utils::HTTP_STATUS_CODES : {} STATUS_WITH_NO_ENTITY_BODY = defined?( Rack::Utils::STATUS_WITH_NO_ENTITY_BODY) ? Rack::Utils::STATUS_WITH_NO_ENTITY_BODY : begin warn 'Rack::Utils::STATUS_WITH_NO_ENTITY_BODY missing' {} end # internal API, code will always be common-enough-for-even-old-Rack def err_response(code, response_start_sent) "#{response_start_sent ? '' : 'HTTP/1.1 '}" \ "#{code} #{STATUS_CODES[code]}\r\n\r\n" end def append_header(buf, key, value) case value when Array # Rack 3 value.each { |v| buf << "#{key}: #{v}\r\n" } when /\n/ # Rack 2 # avoiding blank, key-only cookies with /\n+/ value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" } else buf << "#{key}: #{value}\r\n" end end # writes the rack_response to socket as an HTTP response def http_response_write(socket, status, headers, body, req = Unicorn::HttpRequest.new) hijack = nil do_chunk = false if headers code = status.to_i msg = STATUS_CODES[code] start = req.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze term = STATUS_WITH_NO_ENTITY_BODY.include?(code) || false buf = "#{start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \ "Date: #{httpdate}\r\n" \ "Connection: close\r\n" headers.each do |key, value| case key when %r{\A(?:Date|Connection)\z}i next when %r{\AContent-Length\z}i append_header(buf, key, value) term = true when %r{\ATransfer-Encoding\z}i append_header(buf, key, value) term = true if /\bchunked\b/i === value # value may be Array :x when "rack.hijack" # This should only be hit under Rack >= 1.5, as this was an illegal # key in Rack < 1.5 hijack = value else append_header(buf, key, value) end end if !hijack && !term && req.chunkable_response? do_chunk = true buf << "Transfer-Encoding: chunked\r\n".freeze end socket.write(buf << "\r\n".freeze) buf.clear # remove this line if C Ruby gets escape analysis end if hijack req.hijacked! hijack.call(socket) elsif do_chunk begin body.each do |b| socket.write("#{b.bytesize.to_s(16)}\r\n", b, "\r\n".freeze) end ensure socket.write("0\r\n\r\n".freeze) end else body.each { |chunk| socket.write(chunk) } end end end lib/unicorn/http_server.rb000066400000000000000000000674221471646016400162170ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # This is the process manager of Unicorn. This manages worker # processes which in turn handle the I/O and application process. # Listener sockets are started in the master process and shared with # forked worker children. # # Users do not need to know the internals of this class, but reading the # {source}[https://yhbt.net/unicorn.git/tree/lib/unicorn/http_server.rb] # is education for programmers wishing to learn how unicorn works. # See Unicorn::Configurator for information on how to configure unicorn. class Unicorn::HttpServer # :stopdoc: attr_accessor :app, :timeout, :worker_processes, :before_fork, :after_fork, :before_exec, :listener_opts, :preload_app, :orig_app, :config, :ready_pipe, :user, :default_middleware, :early_hints attr_writer :after_worker_exit, :after_worker_ready, :worker_exec attr_reader :pid, :logger include Unicorn::SocketHelper include Unicorn::HttpResponse # all bound listener sockets # note: this is public used by raindrops, but not recommended for use # in new projects LISTENERS = [] # listeners we have yet to bind NEW_LISTENERS = [] # :startdoc: # We populate this at startup so we can figure out how to reexecute # and upgrade the currently running instance of Unicorn # This Hash is considered a stable interface and changing its contents # will allow you to switch between different installations of Unicorn # or even different installations of the same applications without # downtime. Keys of this constant Hash are described as follows: # # * 0 - the path to the unicorn 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 Unicorn. # # To change your unicorn executable to a different path without downtime, # you can set the following in your Unicorn config file, HUP and then # continue with the traditional USR2 + QUIT upgrade steps: # # Unicorn::HttpServer::START_CTX[0] = "/home/bofh/2.3.0/bin/unicorn" START_CTX = { :argv => ARGV.map(&:dup), 0 => $0.dup, } # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano # and like systems START_CTX[: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 # :stopdoc: # Creates a working server on host:port (strange things happen if # port isn't a Number). Use HttpServer::run to start the server and # HttpServer.run.join to join the thread that's processing # incoming requests on the socket. def initialize(app, options = {}) @app = app @reexec_pid = 0 @default_middleware = true options = options.dup @ready_pipe = options.delete(:ready_pipe) @init_listeners = options[:listeners] ? options[:listeners].dup : [] options[:use_defaults] = true self.config = Unicorn::Configurator.new(options) self.listener_opts = {} @immortal = [] # immortal inherited sockets from systemd # We use @self_pipe differently in the master and worker processes: # # * The master process never closes or reinitializes this once # initialized. Signal handlers in the master process will write to # it to wake up the master from IO.select in exactly the same manner # djb describes in https://cr.yp.to/docs/selfpipe.html # # * The workers immediately close the pipe they inherit. See the # Unicorn::Worker class for the pipe workers use. @self_pipe = [] @workers = {} # hash maps PIDs to Workers @sig_queue = [] # signal queue used for self-piping @pid = nil # we try inheriting listeners first, so we bind them later. # we don't write the pid file until we've bound listeners in case # unicorn 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) config.commit!(self, :skip => [:listeners, :pid]) @orig_app = app # list of signals we care about and trap in master. @queue_sigs = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ] @worker_data = if worker_data = ENV['UNICORN_WORKER'] worker_data = worker_data.split(',').map!(&:to_i) worker_data[1] = worker_data.slice!(1..2).map { |i| IO.for_fd(i) } worker_data end end # Runs the thing. Returns self so you can run join on it def start inherit_listeners! # this pipe is used to wake us up from select(2) in #join when signals # are trapped. See trap_deferred. @self_pipe.replace(Unicorn.pipe) @master_pid = @worker_data ? Process.ppid : $$ # setup signal handlers before writing pid file in case people get # trigger happy and send signals as soon as the pid file exists. # Note that signals don't actually get handled until the #join method @queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } } trap(:CHLD) { awaken_master } # write pid early for Mongrel compatibility if we're not inheriting sockets # This is needed for compatibility some Monit setups at least. # This unfortunately has the side effect of clobbering valid PID if # we upgrade and the upgrade breaks during preload_app==true && build_app! self.pid = config[:pid] build_app! if preload_app bind_new_listeners! spawn_missing_workers 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.start_with?('/') # 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! dead_names -= @immortal.map { |io| sock_name(io) } LISTENERS.delete_if do |io| if dead_names.include?(sock_name(io)) (io.close rescue nil).nil? # true else set_server_sockopt(io, listener_opts[sock_name(io)]) false end end (set_names - cur_names).each { |addr| listen(addr) } end def stdout_path=(path); redirect_io($stdout, path); end def stderr_path=(path); redirect_io($stderr, path); end def logger=(obj) Unicorn::HttpRequest::DEFAULTS["rack.logger"] = @logger = obj end def clobber_pid(path) 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.sync = true fp.write("#$$\n") File.rename(fp.path, path) fp.close end 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.end_with?('.oldbin') 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 # rename the old pid if possible if @pid && path begin File.rename(@pid, path) rescue Errno::ENOENT, Errno::EXDEV # a user may have accidentally removed the original, # obviously cross-FS renames don't work, either. clobber_pid(path) end else clobber_pid(path) 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, opt = {}.merge(listener_opts[address] || {})) address = config.expand_addr(address) return if String === address && listener_names.include?(address) delay = opt[:delay] || 0.5 tries = opt[:tries] || 5 begin io = bind_listen(address, opt) 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)" raise err if tries == 0 tries -= 1 logger.error "retrying in #{delay} seconds " \ "(#{tries < 0 ? 'infinite' : tries} tries left)" sleep(delay) retry rescue => err logger.fatal "error adding listener addr=#{address}" raise err end 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 respawn = true last_check = time_now proc_name 'master' logger.info "master process ready" # test_exec.rb relies on this message if @ready_pipe begin @ready_pipe.syswrite($$.to_s) rescue => e logger.warn("grandparent died too soon?: #{e.message} (#{e.class})") end @ready_pipe = @ready_pipe.close rescue nil end begin reap_all_workers case @sig_queue.shift when nil # avoid murdering workers after our master process (or the # machine) comes out of suspend/hibernation if (last_check + @timeout) >= (last_check = time_now) sleep_time = murder_lazy_workers else sleep_time = @timeout/2.0 + 1 @logger.debug("waiting #{sleep_time}s after suspend/hibernation") end maintain_worker_count if respawn master_sleep(sleep_time) when :QUIT # graceful shutdown break when :TERM, :INT # immediate shutdown stop(false) break when :USR1 # rotate logs logger.info "master reopening logs..." Unicorn::Util.reopen_logs logger.info "master done reopening logs" soft_kill_each_worker(:USR1) when :USR2 # exec binary, stay alive in case something went wrong reexec when :WINCH if $stdin.tty? logger.info "SIGWINCH ignored because we're not daemonized" else respawn = false logger.info "gracefully stopping all workers" soft_kill_each_worker(:QUIT) self.worker_processes = 0 end when :TTIN respawn = true self.worker_processes += 1 when :TTOU self.worker_processes -= 1 if self.worker_processes > 0 when :HUP respawn = true 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 rescue => e Unicorn.log_error(@logger, "master loop error", e) end while true stop # gracefully shutdown all workers on our way out logger.info "master complete" unlink_pid_safe(pid) if pid end # Terminates all workers, but does not exit master process def stop(graceful = true) self.listeners = [] limit = time_now + timeout until @workers.empty? || time_now > limit if graceful soft_kill_each_worker(:QUIT) else kill_each_worker(:TERM) end sleep(0.1) reap_all_workers end kill_each_worker(:KILL) end def rewindable_input Unicorn::HttpRequest.input_class.method_defined?(:rewind) end def rewindable_input=(bool) Unicorn::HttpRequest.input_class = bool ? Unicorn::TeeInput : Unicorn::StreamInput end def client_body_buffer_size Unicorn::TeeInput.client_body_buffer_size end def client_body_buffer_size=(bytes) Unicorn::TeeInput.client_body_buffer_size = bytes end def check_client_connection Unicorn::HttpRequest.check_client_connection end def check_client_connection=(bool) Unicorn::HttpRequest.check_client_connection = bool end private # wait for a signal hander to wake us up and then consume the pipe def master_sleep(sec) @self_pipe[0].wait(sec) or return # 11 bytes is the maximum string length which can be embedded within # the Ruby itself and not require a separate malloc (on 32-bit MRI 1.9+). # Most reads are only one byte here and uncommon, so it's not worth a # persistent buffer, either: @self_pipe[0].read_nonblock(11, exception: false) end def awaken_master return if $$ != @master_pid # wakeup master process from select @self_pipe[1].write_nonblock('.', exception: false) 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) and worker.close rescue nil @after_worker_exit.call(self, worker, status) end rescue Errno::ECHILD break end while true end # reexecutes the START_CTX 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 listener_fds = listener_sockets ENV['UNICORN_FD'] = listener_fds.keys.join(',') Dir.chdir(START_CTX[:cwd]) cmd = [ START_CTX[0] ].concat(START_CTX[:argv]) # exec(command, hash) works in at least 1.9.1+, but will only be # required in 1.9.4/2.0.0 at earliest. cmd << listener_fds logger.info "executing #{cmd.inspect} (in #{Dir.pwd})" before_exec.call(self) exec(*cmd) end proc_name 'master (old)' end def worker_spawn(worker) listener_fds = listener_sockets env = {} env['UNICORN_FD'] = listener_fds.keys.join(',') listener_fds[worker.to_io.fileno] = worker.to_io listener_fds[worker.master.fileno] = worker.master worker_info = [worker.nr, worker.to_io.fileno, worker.master.fileno] env['UNICORN_WORKER'] = worker_info.join(',') Process.spawn(env, START_CTX[0], *START_CTX[:argv], listener_fds) end def listener_sockets listener_fds = {} LISTENERS.each { |sock| listener_fds[sock.fileno] = sock } listener_fds end # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File def murder_lazy_workers next_sleep = @timeout - 1 now = time_now.to_i @workers.dup.each_pair do |wpid, worker| tick = worker.tick 0 == tick and next # skip workers that haven't processed any clients diff = now - tick tmp = @timeout - diff if tmp >= 0 next_sleep > tmp and next_sleep = tmp next end next_sleep = 0 logger.error "worker=#{worker.nr} PID:#{wpid} timeout " \ "(#{diff}s > #{@timeout}s), killing" kill_worker(:KILL, wpid) # take no prisoners for timeout violations end next_sleep <= 0 ? 1 : next_sleep end def after_fork_internal @self_pipe.each(&:close).clear # this is master-only, now @ready_pipe.close if @ready_pipe Unicorn::Configurator::RACKUP.clear @ready_pipe = @init_listeners = @before_exec = @before_fork = nil # 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) end def spawn_missing_workers if @worker_data worker = Unicorn::Worker.new(*@worker_data) after_fork_internal worker_loop(worker) exit end worker_nr = -1 until (worker_nr += 1) == @worker_processes @workers.value?(worker_nr) and next worker = Unicorn::Worker.new(worker_nr) before_fork.call(self, worker) pid = @worker_exec ? worker_spawn(worker) : fork unless pid after_fork_internal worker_loop(worker) exit end @workers[pid] = worker worker.atfork_parent end rescue => e @logger.error(e) rescue nil exit! end def maintain_worker_count (off = @workers.size - worker_processes) == 0 and return off < 0 and return spawn_missing_workers @workers.each_value { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) } 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 ensure # the socket is closed at the end of this function def handle_error(client, e) code = case e when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN # client disconnected on us and there's nothing we can do when Unicorn::RequestURITooLongError 414 when Unicorn::RequestEntityTooLargeError 413 when Unicorn::HttpParserError # try to tell the client they're bad 400 else Unicorn.log_error(@logger, "app error", e) 500 end if code code = err_response(code, @request.response_start_sent) client.write_nonblock(code, exception: false) end client.close rescue end def e103_response_write(client, headers) rss = @request.response_start_sent buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n" headers.each { |key, value| append_header(buf, key, value) } buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze) client.write(buf) end def e100_response_write(client, env) # We use String#freeze to avoid allocations under Ruby 2.1+ # Not many users hit this code path, so it's better to reduce the # constant table sizes even for Ruby 2.0 users who'll hit extra # allocations here. client.write(@request.response_start_sent ? "100 Continue\r\n\r\nHTTP/1.1 ".freeze : "HTTP/1.1 100 Continue\r\n\r\n".freeze) env.delete('HTTP_EXPECT'.freeze) end # once a client is accepted, it is processed in its entirety here # in 3 easy steps: read request, call app, write app response def process_client(client, ai) @request = Unicorn::HttpRequest.new env = @request.read_headers(client, ai) if early_hints env["rack.early_hints"] = lambda do |headers| e103_response_write(client, headers) end end env["rack.after_reply"] = [] status, headers, body = @app.call(env) begin return if @request.hijacked? if 100 == status.to_i e100_response_write(client, env) status, headers, body = @app.call(env) return if @request.hijacked? end @request.headers? or headers = nil http_response_write(client, status, headers, body, @request) ensure body.respond_to?(:close) and body.close end unless client.closed? # rack.hijack may've close this for us client.shutdown # in case of fork() in Rack app client.close # flush and uncork socket immediately, no keepalive end rescue => e handle_error(client, e) ensure env["rack.after_reply"].each(&:call) if env end def nuke_listeners!(readers) # only called from the worker, ordering is important here tmp = readers.dup readers.replace([false]) # ensure worker does not continue ASAP tmp.each { |io| io.close rescue nil } # break out of IO.select end # 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 init_worker_process(worker) worker.atfork_child # we'll re-trap :QUIT later for graceful shutdown iff we accept clients exit_sigs = [ :QUIT, :TERM, :INT ] exit_sigs.each { |sig| trap(sig) { exit!(0) } } exit!(0) if (@sig_queue & exit_sigs)[0] (@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) } trap(:CHLD, 'DEFAULT') @sig_queue.clear proc_name "worker[#{worker.nr}]" START_CTX.clear @workers.clear after_fork.call(self, worker) # can drop perms and create listeners LISTENERS.each { |sock| sock.close_on_exec = true } worker.user(*user) if user.kind_of?(Array) && ! worker.switched @config = nil build_app! unless preload_app @after_fork = @listener_opts = @orig_app = nil readers = LISTENERS.dup readers << worker trap(:QUIT) { nuke_listeners!(readers) } readers end def reopen_worker_logs(worker_nr) logger.info "worker=#{worker_nr} reopening logs..." Unicorn::Util.reopen_logs logger.info "worker=#{worker_nr} done reopening logs" false rescue => e logger.error(e) rescue nil exit!(77) # EX_NOPERM in sysexits.h end def prep_readers(readers) wtr = Unicorn::Waiter.prep_readers(readers) @timeout *= 500 # to milliseconds for epoll, but halved wtr rescue require_relative 'select_waiter' @timeout /= 2.0 # halved for IO.select Unicorn::SelectWaiter.new end # runs inside each forked worker, this sits around and waits # for connections and doesn't die until the parent dies (or is # given a INT, QUIT, or TERM signal) def worker_loop(worker) readers = init_worker_process(worker) waiter = prep_readers(readers) reopen = false # this only works immediately if the master sent us the signal # (which is the normal case) trap(:USR1) { reopen = true } ready = readers.dup @after_worker_ready.call(self, worker) begin reopen = reopen_worker_logs(worker.nr) if reopen worker.tick = time_now.to_i while sock = ready.shift client_ai = sock.accept_nonblock(exception: false) if client_ai != :wait_readable process_client(*client_ai) worker.tick = time_now.to_i end break if reopen end # timeout so we can .tick and keep parent from SIGKILL-ing us worker.tick = time_now.to_i waiter.get_readers(ready, readers, @timeout) rescue => e redo if reopen && readers[0] Unicorn.log_error(@logger, "listen loop error", e) if readers[0] end while readers[0] end # delivers a signal to a worker and fails gracefully if the worker # is no longer running. def kill_worker(signal, wpid) Process.kill(signal, wpid) rescue Errno::ESRCH worker = @workers.delete(wpid) and worker.close rescue nil end # delivers a signal to each worker def kill_each_worker(signal) @workers.keys.each { |wpid| kill_worker(signal, wpid) } end def soft_kill_each_worker(signal) @workers.each_value { |worker| worker.soft_kill(signal) } 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! loaded_app = app logger.info "reloading config_file=#{config.config_file}" config[:listeners].replace(@init_listeners) config.reload config.commit!(self) soft_kill_each_worker(:QUIT) Unicorn::Util.reopen_logs self.app = @orig_app build_app! if preload_app logger.info "done reloading config_file=#{config.config_file}" rescue StandardError, LoadError, SyntaxError => e Unicorn.log_error(@logger, "error reloading config_file=#{config.config_file}", e) self.app = loaded_app 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 build_app! if app.respond_to?(:arity) && (app.arity == 0 || app.arity == 2) if defined?(Gem) && Gem.respond_to?(:refresh) logger.info "Refreshing Gem list" Gem.refresh end self.app = app.arity == 0 ? app.call : app.call(nil, self) end end def proc_name(tag) $0 = ([ File.basename(START_CTX[0]), tag ]).concat(START_CTX[:argv]).join(' ') end def redirect_io(io, path) File.open(path, 'ab') { |fp| io.reopen(fp) } if path io.sync = true end def inherit_listeners! # inherit sockets from parents, they need to be plain Socket objects inherited = ENV['UNICORN_FD'].to_s.split(',') immortal = [] # emulate sd_listen_fds() for systemd sd_pid, sd_fds = ENV.values_at('LISTEN_PID', 'LISTEN_FDS') if sd_pid.to_i == $$ # n.b. $$ can never be zero # 3 = SD_LISTEN_FDS_START immortal = (3...(3 + sd_fds.to_i)).to_a inherited.concat(immortal) end # to ease debugging, we will not unset LISTEN_PID and LISTEN_FDS inherited.map! do |fd| io = Socket.for_fd(fd.to_i) @immortal << io if immortal.include?(fd) set_server_sockopt(io, listener_opts[sock_name(io)]) logger.info "inherited addr=#{sock_name(io)} fd=#{io.fileno}" io end config_listeners = config[:listeners].dup LISTENERS.replace(inherited) # we only use generic Socket objects for aggregate Socket#accept_nonblock # return value [ Socket, Addrinfo ]. This allows us to avoid having to # make getpeername(2) syscalls later on to fill in env['REMOTE_ADDR'] config_listeners -= listener_names if config_listeners.empty? && LISTENERS.empty? config_listeners << Unicorn::Const::DEFAULT_LISTEN @init_listeners << Unicorn::Const::DEFAULT_LISTEN START_CTX[:argv] << "-l#{Unicorn::Const::DEFAULT_LISTEN}" end NEW_LISTENERS.replace(config_listeners) end # call only after calling inherit_listeners! # This binds any listeners we did NOT inherit from the parent def bind_new_listeners! NEW_LISTENERS.each { |addr| listen(addr) }.clear raise ArgumentError, "no listeners" if LISTENERS.empty? end # try to use the monotonic clock in Ruby >= 2.1, it is immune to clock # offset adjustments and generates less garbage (Float vs Time object) begin Process.clock_gettime(Process::CLOCK_MONOTONIC) def time_now Process.clock_gettime(Process::CLOCK_MONOTONIC) end rescue NameError, NoMethodError def time_now # Ruby <= 2.0 Time.now end end end lib/unicorn/launcher.rb000066400000000000000000000037731471646016400154520ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: $stdout.sync = $stderr.sync = true $stdin.binmode $stdout.binmode $stderr.binmode require 'unicorn' module Unicorn::Launcher # 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 unicorn is designed to # run inside APP_ROOT. Unicorn 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.daemonize!(options) cfg = Unicorn::Configurator $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['UNICORN_FD'] # grandparent - reads pipe, exits when master is ready # \_ parent - exits immediately ASAP # \_ unicorn master - writes to pipe when ready rd, wr = Unicorn.pipe grandparent = $$ if fork wr.close # grandparent does not write else rd.close # unicorn master does not read Process.setsid exit if fork # parent dies now end if grandparent == $$ # this will block until HttpServer#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 # unicorn master process options[:ready_pipe] = wr end end # $stderr/$stderr can/will be redirected separately in the Unicorn config cfg::DEFAULTS[:stderr_path] ||= "/dev/null" cfg::DEFAULTS[:stdout_path] ||= "/dev/null" cfg::RACKUP[:daemonized] = true end end lib/unicorn/oob_gc.rb000066400000000000000000000060051471646016400150700ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Strongly consider https://github.com/tmm1/gctools if using Ruby 2.1+ # It is built on new APIs in Ruby 2.1, so it is more intelligent than # this historical implementation. # # Users on Ruby 2.0 (not 2.1+) may also want to check out # lib/middleware/unicorn_oobgc.rb from the Discourse project # (https://github.com/discourse/discourse) # # The following information is only for historical versions of Ruby. # # Runs GC after requests, after closing the client socket and # before attempting to accept more connections. # # This shouldn't hurt overall performance as long as the server cluster # is at <50% CPU capacity, and improves the performance of most memory # intensive requests. This serves to improve _client-visible_ # performance (possibly at the cost of overall performance). # # Increasing the number of +worker_processes+ may be necessary to # improve average client response times because some of your workers # will be busy doing GC and unable to service clients. Think of # using more workers with this module as a poor man's concurrent GC. # # We'll call GC after each request is been written out to the socket, so # the client never sees the extra GC hit it. # # This middleware is _only_ effective for applications that use a lot # of memory, and will hurt simpler apps/endpoints that can process # multiple requests before incurring GC. # # This middleware is only designed to work with unicorn, as it harms # performance with keepalive-enabled servers. # # Example (in config.ru): # # require 'unicorn/oob_gc' # # # GC ever two requests that hit /expensive/foo or /more_expensive/foo # # in your app. By default, this will GC once every 5 requests # # for all endpoints in your app # use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)} # # Feedback from users of early implementations of this module: # * https://yhbt.net/unicorn-public/0BFC98E9-072B-47EE-9A70-05478C20141B@lukemelia.com/ # * https://yhbt.net/unicorn-public/AANLkTilUbgdyDv9W1bi-s_W6kq9sOhWfmuYkKLoKGOLj@mail.gmail.com/ module Unicorn::OobGC # this pretends to be Rack middleware because it used to be # But we need to hook into unicorn internals so we need to close # the socket before clearing the request env. # # +interval+ is the number of requests matching the +path+ regular # expression before invoking GC. def self.new(app, interval = 5, path = %r{\A/}) @@nr = interval self.const_set :OOBGC_PATH, path self.const_set :OOBGC_INTERVAL, interval ObjectSpace.each_object(Unicorn::HttpServer) do |s| s.extend(self) end app # pretend to be Rack middleware since it was in the past end #:stopdoc: def process_client(*args) super(*args) # Unicorn::HttpServer#process_client env = instance_variable_get(:@request).env if OOBGC_PATH =~ env['PATH_INFO'] && ((@@nr -= 1) <= 0) @@nr = OOBGC_INTERVAL env.clear disabled = GC.enable GC.start GC.disable if disabled end end # :startdoc: end lib/unicorn/preread_input.rb000066400000000000000000000013011471646016400164730ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false module Unicorn # This middleware is used to ensure input is buffered to memory # or disk (depending on size) before the application is dispatched # by entirely consuming it (from TeeInput) beforehand. # # Usage (in config.ru): # # require 'unicorn/preread_input' # if defined?(Unicorn) # use Unicorn::PrereadInput # end # run YourApp.new class PrereadInput # :stopdoc: def initialize(app) @app = app end def call(env) buf = "" input = env["rack.input"] if input.respond_to?(:rewind) true while input.read(16384, buf) input.rewind end @app.call(env) end # :startdoc: end end lib/unicorn/select_waiter.rb000066400000000000000000000004211471646016400164660ustar00rootroot00000000000000# frozen_string_literal: false # fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE class Unicorn::SelectWaiter # :nodoc: def get_readers(ready, readers, timeout) # :nodoc: ret = IO.select(readers, nil, nil, timeout) and ready.replace(ret[0]) end end lib/unicorn/socket_helper.rb000066400000000000000000000151151471646016400164710ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :enddoc: require 'socket' module Unicorn module SocketHelper # internal interface DEFAULTS = { # The semantics for TCP_DEFER_ACCEPT changed in Linux 2.6.32+ # with commit d1b99ba41d6c5aa1ed2fc634323449dd656899e9 # This change shouldn't affect unicorn users behind nginx (a # value of 1 remains an optimization). :tcp_defer_accept => 1, # FreeBSD, we need to override this to 'dataready' if we # eventually support non-HTTP/1.x :accept_filter => 'httpready', # same default value as Mongrel :backlog => 1024, # favor latency over bandwidth savings :tcp_nopush => nil, :tcp_nodelay => true, } # configure platform-specific options (only tested on Linux 2.6 so far) def accf_arg(af_name) [ af_name, nil ].pack('a16a240') end if RUBY_PLATFORM =~ /freebsd/ && Socket.const_defined?(:SO_ACCEPTFILTER) def set_tcp_sockopt(sock, opt) # just in case, even LANs can break sometimes. Linux sysadmins # can lower net.ipv4.tcp_keepalive_* sysctl knobs to very low values. Socket.const_defined?(:SO_KEEPALIVE) and sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1) if Socket.const_defined?(:TCP_NODELAY) val = opt[:tcp_nodelay] val = DEFAULTS[:tcp_nodelay] if val.nil? sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, val ? 1 : 0) end val = opt[:tcp_nopush] unless val.nil? if Socket.const_defined?(:TCP_CORK) # Linux sock.setsockopt(:IPPROTO_TCP, :TCP_CORK, val) elsif Socket.const_defined?(:TCP_NOPUSH) # FreeBSD sock.setsockopt(:IPPROTO_TCP, :TCP_NOPUSH, val) end end # No good reason to ever have deferred accepts off in single-threaded # servers (except maybe benchmarking) if Socket.const_defined?(:TCP_DEFER_ACCEPT) # this differs from nginx, since nginx doesn't allow us to # configure the the timeout... seconds = opt[:tcp_defer_accept] seconds = DEFAULTS[:tcp_defer_accept] if [true,nil].include?(seconds) seconds = 0 unless seconds # nil/false means disable this sock.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, seconds) elsif respond_to?(:accf_arg) name = opt[:accept_filter] name = DEFAULTS[:accept_filter] if name.nil? sock.listen(opt[:backlog]) got = (sock.getsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER) rescue nil).to_s arg = accf_arg(name) begin sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, arg) rescue => e logger.error("#{sock_name(sock)} " \ "failed to set accept_filter=#{name} (#{e.inspect})") logger.error("perhaps accf_http(9) needs to be loaded".freeze) end if arg != got end end def set_server_sockopt(sock, opt) opt = DEFAULTS.merge(opt || {}) set_tcp_sockopt(sock, opt) if sock.local_address.ip? rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf) if rcvbuf || sndbuf log_buffer_sizes(sock, "before: ") sock.setsockopt(:SOL_SOCKET, :SO_RCVBUF, rcvbuf) if rcvbuf sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf log_buffer_sizes(sock, " after: ") end sock.listen(opt[:backlog]) rescue => e Unicorn.log_error(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 = '0.0.0.0:8080', opt = {}) return address unless String === address sock = if address.start_with?('/') 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 s = Socket.new(:UNIX, :STREAM) s.bind(Socket.sockaddr_un(address)) s ensure File.umask(old_umask) end elsif /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ =~ address new_tcp_server($1, $2.to_i, opt.merge(:ipv6=>true)) elsif /\A(\d+\.\d+\.\d+\.\d+):(\d+)\z/ =~ address new_tcp_server($1, $2.to_i, opt) else raise ArgumentError, "Don't know how to bind: #{address}" end set_server_sockopt(sock, opt) sock end def new_tcp_server(addr, port, opt) # n.b. we set FD_CLOEXEC in the workers sock = Socket.new(opt[:ipv6] ? :AF_INET6 : :AF_INET, :SOCK_STREAM) if opt.key?(:ipv6only) Socket.const_defined?(:IPV6_V6ONLY) or abort "Socket::IPV6_V6ONLY not defined, upgrade Ruby and/or your OS" sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0) end sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1) if Socket.const_defined?(:SO_REUSEPORT) && opt[:reuseport] sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1) end sock.bind(Socket.pack_sockaddr_in(port, addr)) sock end # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6 def tcp_name(sock) port, addr = Socket.unpack_sockaddr_in(sock.getsockname) addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}" end module_function :tcp_name # 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 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 module_function :sock_name end # module SocketHelper end # module Unicorn lib/unicorn/stream_input.rb000066400000000000000000000106451471646016400163570ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # When processing uploads, unicorn may expose a StreamInput object under # "rack.input" of the Rack environment when # Unicorn::Configurator#rewindable_input is set to +false+ class Unicorn::StreamInput # The I/O chunk size (in +bytes+) for I/O operations where # the size cannot be user-specified when a method is called. # The default is 16 kilobytes. @@io_chunk_size = Unicorn::Const::CHUNK_SIZE # :nodoc: # Initializes a new StreamInput object. You normally do not have to call # this unless you are writing an HTTP server. def initialize(socket, request) # :nodoc: @chunked = request.content_length.nil? @socket = socket @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) filter_body(@rbuf, @socket.readpartial(to_read, @buf)) rv << @rbuf to_read -= @rbuf.size end @rbuf.clear end rv = nil if rv.empty? && length != 0 else read_all(rv) end rv rescue EOFError return eof! 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)})/ begin @rbuf.sub!(re, '') and return $1 return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof? filter_body(once = '', @socket.readpartial(@@io_chunk_size, @buf)) @rbuf << once rescue EOFError return eof! 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 private def eof? if @parser.body_eof? while @chunked && ! @parser.parse @buf << @socket.readpartial(@@io_chunk_size) end @socket = nil true else false end rescue EOFError return eof! 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) @socket or return until eof? filter_body(@rbuf, @socket.readpartial(@@io_chunk_size, @buf)) dst << @rbuf end rescue EOFError return eof! ensure @rbuf.clear 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. @socket.shutdown if @socket ensure raise Unicorn::ClientShutdown, "bytes_read=#{@bytes_read}", [] end end lib/unicorn/tee_input.rb000066400000000000000000000112771471646016400156430ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Acts like tee(1) on an input input to provide a input-like stream # while providing rewindable semantics through a File/StringIO backing # 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, unicorn exposes a TeeInput object under # "rack.input" of the Rack environment by default. class Unicorn::TeeInput < Unicorn::StreamInput # The maximum size (in +bytes+) to buffer in memory before # resorting to a temporary file. Default is 112 kilobytes. @@client_body_buffer_size = Unicorn::Const::MAX_BODY # :nodoc: # sets the maximum size of request bodies to buffer in memory, # amounts larger than this are buffered to the filesystem def self.client_body_buffer_size=(bytes) # :nodoc: @@client_body_buffer_size = bytes end # returns the maximum size of request bodies to buffer in memory, # amounts larger than this are buffered to the filesystem def self.client_body_buffer_size # :nodoc: @@client_body_buffer_size end # for Rack::TempfileReaper in rack 1.6+ def new_tmpio # :nodoc: tmpio = Unicorn::TmpIO.new (@parser.env['rack.tempfiles'] ||= []) << tmpio tmpio end # Initializes a new TeeInput object. You normally do not have to call # this unless you are writing an HTTP server. def initialize(socket, request) # :nodoc: @len = request.content_length super @tmp = @len && @len <= @@client_body_buffer_size ? StringIO.new("") : new_tmpio 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) @socket ? 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 @socket ? 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 @socket @tmp.rewind # Rack does not specify what the return value is here end private # consumes the stream of the socket def consume! junk = "" nil while read(@@io_chunk_size, junk) end def tee(buffer) @tmp.write(buffer) if buffer buffer end end lib/unicorn/tmpio.rb000066400000000000000000000015041471646016400147670ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # :stopdoc: 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. class Unicorn::TmpIO < File # 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 path = nil # workaround File#path being tainted: # https://bugs.ruby-lang.org/issues/14485 fp = begin path = "#{Dir::tmpdir}/#{rand}" super(path, RDWR|CREAT|EXCL, 0600) rescue Errno::EEXIST retry end unlink(path) fp.binmode fp.sync = true fp end # pretend we're Tempfile for Rack::TempfileReaper alias close! close end lib/unicorn/util.rb000066400000000000000000000055211471646016400146170ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'fcntl' module Unicorn::Util # :nodoc: # :stopdoc: def self.is_log?(fp) append_flags = File::WRONLY | File::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_logs(uid, gid) ObjectSpace.each_object(File) do |fp| fp.chown(uid, gid) if is_log?(fp) end end # :startdoc: # 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 # # In Unicorn 3.5.x and earlier, files must be opened with an absolute # path to be considered a log file. def self.reopen_logs 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 # race 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 # stdin, stdout, stderr are special. The following dance should # guarantee there is no window where `fp' is unwritable in MRI # (or any correct Ruby implementation). # # Fwiw, GVL has zero bearing here. This is tricky because of # the unavoidable existence of stdio FILE * pointers for # std{in,out,err} in all programs which may use the standard C library if fp.fileno <= 2 # We do not want to hit fclose(3)->dup(2) window for std{in,out,err} # MRI will use freopen(3) here internally on std{in,out,err} fp.reopen(fp.path, "a") else # We should not need this workaround, Ruby can be fixed: # https://bugs.ruby-lang.org/issues/9036 # MRI will not call call fclose(3) or freopen(3) here # since there's no associated std{in,out,err} FILE * pointer # This should atomically use dup3(2) (or dup2(2)) syscall File.open(fp.path, "a") { |tmpfp| fp.reopen(tmpfp) } end fp.sync = true fp.flush # IO#sync=true may not implicitly flush 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 lib/unicorn/worker.rb000066400000000000000000000125051471646016400151530ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require "raindrops" # This class and its members can be considered a stable interface # and will not change in a backwards-incompatible fashion between # releases of unicorn. Knowledge of this class is generally not # not needed for most users of unicorn. # # Some users may want to access it in the before_fork/after_fork hooks. # See the Unicorn::Configurator RDoc for examples. class Unicorn::Worker # :stopdoc: attr_accessor :nr, :switched attr_reader :to_io # IO.select-compatible attr_reader :master PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE DROPS = [] def initialize(nr, pipe=nil) drop_index = nr / PER_DROP @raindrop = DROPS[drop_index] ||= Raindrops.new(PER_DROP) @offset = nr % PER_DROP @raindrop[@offset] = 0 @nr = nr @switched = false @to_io, @master = pipe || Unicorn.pipe end def atfork_child # :nodoc: # we _must_ close in child, parent just holds this open to signal @master = @master.close end # master fakes SIGQUIT using this def quit # :nodoc: @master = @master.close if @master end # parent does not read def atfork_parent # :nodoc: @to_io = @to_io.close end # call a signal handler immediately without triggering EINTR # We do not use the more obvious Process.kill(sig, $$) here since # that signal delivery may be deferred. We want to avoid signal delivery # while the Rack app.call is running because some database drivers # (e.g. ruby-pg) may cancel pending requests. def fake_sig(sig) # :nodoc: old_cb = trap(sig, "IGNORE") old_cb.call ensure trap(sig, old_cb) end # master sends fake signals to children def soft_kill(sig) # :nodoc: case sig when Integer signum = sig else signum = Signal.list[sig.to_s] or raise ArgumentError, "BUG: bad signal: #{sig.inspect}" end # writing and reading 4 bytes on a pipe is atomic on all POSIX platforms # Do not care in the odd case the buffer is full, here. @master.write_nonblock([signum].pack('l'), exception: false) rescue Errno::EPIPE # worker will be reaped soon end # this only runs when the Rack app.call is not running # act like Socket#accept_nonblock(exception: false) def accept_nonblock(*_unused) # :nodoc: case buf = @to_io.read_nonblock(4, exception: false) when String # unpack the buffer and trigger the signal handler signum = buf.unpack('l') fake_sig(signum[0]) # keep looping, more signals may be queued when nil # EOF: master died, but we are at a safe place to exit fake_sig(:QUIT) when :wait_readable # keep waiting return :wait_readable end while true # loop, as multiple signals may be sent end # worker objects may be compared to just plain Integers def ==(other_nr) # :nodoc: @nr == other_nr end # called in the worker process def tick=(value) # :nodoc: @raindrop[@offset] = value end # called in the master process def tick # :nodoc: @raindrop[@offset] end # called in both the master (reaping worker) and worker (SIGQUIT handler) def close # :nodoc: @master.close if @master @to_io.close if @to_io end # :startdoc: # In most cases, you should be using the Unicorn::Configurator#user # directive instead. This method should only be used if you need # fine-grained control of exactly when you want to change permissions # in your after_fork or after_worker_ready hooks, or if you want to # use the chroot support. # # Changes the worker process to the specified +user+ and +group+, # and chroots to the current working directory if +chroot+ is set. # 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) # # +group+ can be specified as a string, or as an array of two # strings. If an array of two strings is given, the first string # is used as the primary group of the process, and the second is # used as the group of the log files. # # 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. # # chroot support is only available in unicorn 5.3.0+ # user and group switching appeared in unicorn 0.94.0 (2009-11-05) def user(user, group = nil, chroot = false) # 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 if group if group.is_a?(Array) group, log_group = group log_gid = Etc.getgrnam(log_group).gid end gid = Etc.getgrnam(group).gid log_gid ||= gid end Unicorn::Util.chown_logs(uid, log_gid) if gid && Process.egid != gid Process.initgroups(user, gid) Process::GID.change_privilege(gid) end if chroot chroot = Dir.pwd if chroot == true Dir.chroot(chroot) Dir.chdir('/') end Process.euid != uid and Process::UID.change_privilege(uid) @switched = true end end setup.rb000066400000000000000000001065741471646016400125710ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # # setup.rb # # Copyright (c) 2000-2005 Minero Aoki # # This program is free software. # You can distribute/modify this program under the terms of # the GNU LGPL, Lesser General Public License version 2.1. # unless Enumerable.method_defined?(:map) # Ruby 1.4.6 module Enumerable alias map collect end end unless File.respond_to?(:read) # Ruby 1.6 def File.read(fname) open(fname) {|f| return f.read } end end unless Errno.const_defined?(:ENOTEMPTY) # Windows? module Errno class ENOTEMPTY # We do not raise this exception, implementation is not needed. end end end def File.binread(fname) open(fname, 'rb') {|f| return f.read } end # for corrupted Windows' stat(2) def File.dir?(path) File.directory?((path[-1,1] == '/') ? path : path + '/') end class ConfigTable include Enumerable def initialize(rbconfig) @rbconfig = rbconfig @items = [] @table = {} # options @install_prefix = nil @config_opt = nil @verbose = true @no_harm = false end attr_accessor :install_prefix attr_accessor :config_opt attr_writer :verbose def verbose? @verbose end attr_writer :no_harm def no_harm? @no_harm end def [](key) lookup(key).resolve(self) end def []=(key, val) lookup(key).set val end def names @items.map {|i| i.name } end def each(&block) @items.each(&block) end def key?(name) @table.key?(name) end def lookup(name) @table[name] or setup_rb_error "no such config item: #{name}" end def add(item) @items.push item @table[item.name] = item end def remove(name) item = lookup(name) @items.delete_if {|i| i.name == name } @table.delete_if {|name, i| i.name == name } item end def load_script(path, inst = nil) if File.file?(path) MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path end end def savefile '.config' end def load_savefile begin File.foreach(savefile()) do |line| k, v = *line.split(/=/, 2) self[k] = v.strip end rescue Errno::ENOENT setup_rb_error $!.message + "\n#{File.basename($0)} config first" end end def save @items.each {|i| i.value } File.open(savefile(), 'w') {|f| @items.each do |i| f.printf "%s=%s\n", i.name, i.value if i.value? and i.value end } end def load_standard_entries standard_entries(@rbconfig).each do |ent| add ent end end def standard_entries(rbconfig) c = rbconfig rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) major = c['MAJOR'].to_i minor = c['MINOR'].to_i teeny = c['TEENY'].to_i version = "#{major}.#{minor}" # ruby ver. >= 1.4.4? newpath_p = ((major >= 2) or ((major == 1) and ((minor >= 5) or ((minor == 4) and (teeny >= 4))))) if c['rubylibdir'] # V > 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = c['rubylibdir'] librubyverarch = c['archdir'] siteruby = c['sitedir'] siterubyver = c['sitelibdir'] siterubyverarch = c['sitearchdir'] elsif newpath_p # 1.4.4 <= V <= 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = c['sitedir'] siterubyver = "$siteruby/#{version}" siterubyverarch = "$siterubyver/#{c['arch']}" else # V < 1.4.4 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" siterubyver = siteruby siterubyverarch = "$siterubyver/#{c['arch']}" end parameterize = lambda {|path| path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') } if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } makeprog = arg.sub(/'/, '').split(/=/, 2)[1] else makeprog = 'make' end [ ExecItem.new('installdirs', 'std/site/home', 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ {|val, table| case val when 'std' table['rbdir'] = '$librubyver' table['sodir'] = '$librubyverarch' when 'site' table['rbdir'] = '$siterubyver' table['sodir'] = '$siterubyverarch' when 'home' setup_rb_error '$HOME was not set' unless ENV['HOME'] table['prefix'] = ENV['HOME'] table['rbdir'] = '$libdir/ruby' table['sodir'] = '$libdir/ruby' end }, PathItem.new('prefix', 'path', c['prefix'], 'path prefix of target environment'), PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 'the directory for commands'), PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 'the directory for libraries'), PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 'the directory for shared data'), PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 'the directory for man pages'), PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 'the directory for system configuration files'), PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 'the directory for local state data'), PathItem.new('libruby', 'path', libruby, 'the directory for ruby libraries'), PathItem.new('librubyver', 'path', librubyver, 'the directory for standard ruby libraries'), PathItem.new('librubyverarch', 'path', librubyverarch, 'the directory for standard ruby extensions'), PathItem.new('siteruby', 'path', siteruby, 'the directory for version-independent aux ruby libraries'), PathItem.new('siterubyver', 'path', siterubyver, 'the directory for aux ruby libraries'), PathItem.new('siterubyverarch', 'path', siterubyverarch, 'the directory for aux ruby binaries'), PathItem.new('rbdir', 'path', '$siterubyver', 'the directory for ruby scripts'), PathItem.new('sodir', 'path', '$siterubyverarch', 'the directory for ruby extentions'), PathItem.new('rubypath', 'path', rubypath, 'the path to set to #! line'), ProgramItem.new('rubyprog', 'name', rubypath, 'the ruby program using for installation'), ProgramItem.new('makeprog', 'name', makeprog, 'the make program to compile ruby extentions'), SelectItem.new('shebang', 'all/ruby/never', 'ruby', 'shebang line (#!) editing mode'), BoolItem.new('without-ext', 'yes/no', 'no', 'does not compile/install ruby extentions') ] end private :standard_entries def load_multipackage_entries multipackage_entries().each do |ent| add ent end end def multipackage_entries [ PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 'package names that you want to install'), PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 'package names that you do not want to install') ] end private :multipackage_entries ALIASES = { 'std-ruby' => 'librubyver', 'stdruby' => 'librubyver', 'rubylibdir' => 'librubyver', 'archdir' => 'librubyverarch', 'site-ruby-common' => 'siteruby', # For backward compatibility 'site-ruby' => 'siterubyver', # For backward compatibility 'bin-dir' => 'bindir', 'bin-dir' => 'bindir', 'rb-dir' => 'rbdir', 'so-dir' => 'sodir', 'data-dir' => 'datadir', 'ruby-path' => 'rubypath', 'ruby-prog' => 'rubyprog', 'ruby' => 'rubyprog', 'make-prog' => 'makeprog', 'make' => 'makeprog' } def fixup ALIASES.each do |ali, name| @table[ali] = @table[name] end @items.freeze @table.freeze @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ end def parse_opt(opt) m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" m.to_a[1,2] end def dllext @rbconfig['DLEXT'] end def value_config?(name) lookup(name).value? end class Item def initialize(name, template, default, desc) @name = name.freeze @template = template @value = default @default = default @description = desc end attr_reader :name attr_reader :description attr_accessor :default alias help_default default def help_opt "--#{@name}=#{@template}" end def value? true end def value @value end def resolve(table) @value.gsub(%r<\$([^/]+)>) { table[$1] } end def set(val) @value = check(val) end private def check(val) setup_rb_error "config: --#{name} requires argument" unless val val end end class BoolItem < Item def config_type 'bool' end def help_opt "--#{@name}" end private def check(val) return 'yes' unless val case val when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' else setup_rb_error "config: --#{@name} accepts only yes/no for argument" end end end class PathItem < Item def config_type 'path' end private def check(path) setup_rb_error "config: --#{@name} requires argument" unless path path[0,1] == '$' ? path : File.expand_path(path) end end class ProgramItem < Item def config_type 'program' end end class SelectItem < Item def initialize(name, selection, default, desc) super @ok = selection.split('/') end def config_type 'select' end private def check(val) unless @ok.include?(val.strip) setup_rb_error "config: use --#{@name}=#{@template} (#{val})" end val.strip end end class ExecItem < Item def initialize(name, selection, desc, &block) super name, selection, nil, desc @ok = selection.split('/') @action = block end def config_type 'exec' end def value? false end def resolve(table) setup_rb_error "$#{name()} wrongly used as option value" end undef set def evaluate(val, table) v = val.strip.downcase unless @ok.include?(v) setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" end @action.call v, table end end class PackageSelectionItem < Item def initialize(name, template, default, help_default, desc) super name, template, default, desc @help_default = help_default end attr_reader :help_default def config_type 'package' end private def check(val) unless File.dir?("packages/#{val}") setup_rb_error "config: no such package: #{val}" end val end end class MetaConfigEnvironment def initialize(config, installer) @config = config @installer = installer end def config_names @config.names end def config?(name) @config.key?(name) end def bool_config?(name) @config.lookup(name).config_type == 'bool' end def path_config?(name) @config.lookup(name).config_type == 'path' end def value_config?(name) @config.lookup(name).config_type != 'exec' end def add_config(item) @config.add item end def add_bool_config(name, default, desc) @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) end def add_path_config(name, default, desc) @config.add PathItem.new(name, 'path', default, desc) end def set_config_default(name, default) @config.lookup(name).default = default end def remove_config(name) @config.remove(name) end # For only multipackage def packages raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer @installer.packages end # For only multipackage def declare_packages(list) raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer @installer.packages = list end end end # class ConfigTable # This module requires: #verbose?, #no_harm? module FileOperations def mkdir_p(dirname, prefix = nil) dirname = prefix + File.expand_path(dirname) if prefix $stderr.puts "mkdir -p #{dirname}" if verbose? return if no_harm? # Does not check '/', it's too abnormal. dirs = File.expand_path(dirname).split(%r<(?=/)>) if /\A[a-z]:\z/i =~ dirs[0] disk = dirs.shift dirs[0] = disk + dirs[0] end dirs.each_index do |idx| path = dirs[0..idx].join('') Dir.mkdir path unless File.dir?(path) end end def rm_f(path) $stderr.puts "rm -f #{path}" if verbose? return if no_harm? force_remove_file path end def rm_rf(path) $stderr.puts "rm -rf #{path}" if verbose? return if no_harm? remove_tree path end def remove_tree(path) if File.symlink?(path) remove_file path elsif File.dir?(path) remove_tree0 path else force_remove_file path end end def remove_tree0(path) Dir.foreach(path) do |ent| next if ent == '.' next if ent == '..' entpath = "#{path}/#{ent}" if File.symlink?(entpath) remove_file entpath elsif File.dir?(entpath) remove_tree0 entpath else force_remove_file entpath end end begin Dir.rmdir path rescue Errno::ENOTEMPTY # directory may not be empty end end def move_file(src, dest) force_remove_file dest begin File.rename src, dest rescue File.open(dest, 'wb') {|f| f.write File.binread(src) } File.chmod File.stat(src).mode, dest File.unlink src end end def force_remove_file(path) begin remove_file path rescue end end def remove_file(path) File.chmod 0777, path File.unlink path end def install(from, dest, mode, prefix = nil) $stderr.puts "install #{from} #{dest}" if verbose? return if no_harm? realdest = prefix ? prefix + File.expand_path(dest) : dest realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) str = File.binread(from) if diff?(str, realdest) verbose_off { rm_f realdest if File.exist?(realdest) } File.open(realdest, 'wb') {|f| f.write str } File.chmod mode, realdest File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| if prefix f.puts realdest.sub(prefix, '') else f.puts realdest end } end end def diff?(new_content, path) return true unless File.exist?(path) new_content != File.binread(path) end def command(*args) $stderr.puts args.join(' ') if verbose? system(*args) or raise RuntimeError, "system(#{args.map{|a| a.inspect }.join(' ')}) failed" end def ruby(*args) command config('rubyprog'), *args end def make(task = nil) command(*[config('makeprog'), task].compact) end def extdir?(dir) File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") end def files_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.file?("#{dir}/#{ent}") } } end DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) def directories_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT } end end # This module requires: #srcdir_root, #objdir_root, #relpath module HookScriptAPI def get_config(key) @config[key] end alias config get_config # obsolete: use metaconfig to change configuration def set_config(key, val) @config[key] = val end # # srcdir/objdir (works only in the package directory) # def curr_srcdir "#{srcdir_root()}/#{relpath()}" end def curr_objdir "#{objdir_root()}/#{relpath()}" end def srcfile(path) "#{curr_srcdir()}/#{path}" end def srcexist?(path) File.exist?(srcfile(path)) end def srcdirectory?(path) File.dir?(srcfile(path)) end def srcfile?(path) File.file?(srcfile(path)) end def srcentries(path = '.') Dir.open("#{curr_srcdir()}/#{path}") {|d| return d.to_a - %w(. ..) } end def srcfiles(path = '.') srcentries(path).select {|fname| File.file?(File.join(curr_srcdir(), path, fname)) } end def srcdirectories(path = '.') srcentries(path).select {|fname| File.dir?(File.join(curr_srcdir(), path, fname)) } end end class ToplevelInstaller Version = '3.4.1' Copyright = 'Copyright (c) 2000-2005 Minero Aoki' TASKS = [ [ 'all', 'do config, setup, then install' ], [ 'config', 'saves your configurations' ], [ 'show', 'shows current configuration' ], [ 'setup', 'compiles ruby extentions and others' ], [ 'install', 'installs files' ], [ 'test', 'run all tests in test/' ], [ 'clean', "does `make clean' for each extention" ], [ 'distclean',"does `make distclean' for each extention" ] ] def ToplevelInstaller.invoke config = ConfigTable.new(load_rbconfig()) config.load_standard_entries config.load_multipackage_entries if multipackage? config.fixup klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) klass.new(File.dirname($0), config).invoke end def ToplevelInstaller.multipackage? File.dir?(File.dirname($0) + '/packages') end def ToplevelInstaller.load_rbconfig if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } ARGV.delete(arg) load File.expand_path(arg.split(/=/, 2)[1]) $".push 'rbconfig.rb' else require 'rbconfig' end ::Config::CONFIG end def initialize(ardir_root, config) @ardir = File.expand_path(ardir_root) @config = config # cache @valid_task_re = nil end def config(key) @config[key] end def inspect "#<#{self.class} #{__id__()}>" end def invoke run_metaconfigs case task = parsearg_global() when nil, 'all' parsearg_config init_installers exec_config exec_setup exec_install else case task when 'config', 'test' ; when 'clean', 'distclean' @config.load_savefile if File.exist?(@config.savefile) else @config.load_savefile end __send__ "parsearg_#{task}" init_installers __send__ "exec_#{task}" end end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig" end def init_installers @installer = Installer.new(@config, @ardir, File.expand_path('.')) end # # Hook Script API bases # def srcdir_root @ardir end def objdir_root '.' end def relpath '.' end # # Option Parsing # def parsearg_global while arg = ARGV.shift case arg when /\A\w+\z/ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) return arg when '-q', '--quiet' @config.verbose = false when '--verbose' @config.verbose = true when '--help' print_usage $stdout exit 0 when '--version' puts "#{File.basename($0)} version #{Version}" exit 0 when '--copyright' puts Copyright exit 0 else setup_rb_error "unknown global option '#{arg}'" end end nil end def valid_task?(t) valid_task_re() =~ t end def valid_task_re @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ end def parsearg_no_options unless ARGV.empty? task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" end end alias parsearg_show parsearg_no_options alias parsearg_setup parsearg_no_options alias parsearg_test parsearg_no_options alias parsearg_clean parsearg_no_options alias parsearg_distclean parsearg_no_options def parsearg_config evalopt = [] set = [] @config.config_opt = [] while i = ARGV.shift if /\A--?\z/ =~ i @config.config_opt = ARGV.dup break end name, value = *@config.parse_opt(i) if @config.value_config?(name) @config[name] = value else evalopt.push [name, value] end set.push name end evalopt.each do |name, value| @config.lookup(name).evaluate value, @config end # Check if configuration is valid set.each do |n| @config[n] if @config.value_config?(n) end end def parsearg_install @config.no_harm = false @config.install_prefix = '' while a = ARGV.shift case a when '--no-harm' @config.no_harm = true when /\A--prefix=/ path = a.split(/=/, 2)[1] path = File.expand_path(path) unless path[0,1] == '/' @config.install_prefix = path else setup_rb_error "install: unknown option #{a}" end end end def print_usage(out) out.puts 'Typical Installation Procedure:' out.puts " $ ruby #{File.basename $0} config" out.puts " $ ruby #{File.basename $0} setup" out.puts " # ruby #{File.basename $0} install (may require root privilege)" out.puts out.puts 'Detailed Usage:' out.puts " ruby #{File.basename $0} " out.puts " ruby #{File.basename $0} [] []" fmt = " %-24s %s\n" out.puts out.puts 'Global options:' out.printf fmt, '-q,--quiet', 'suppress message outputs' out.printf fmt, ' --verbose', 'output messages verbosely' out.printf fmt, ' --help', 'print this message' out.printf fmt, ' --version', 'print version and quit' out.printf fmt, ' --copyright', 'print copyright and quit' out.puts out.puts 'Tasks:' TASKS.each do |name, desc| out.printf fmt, name, desc end fmt = " %-24s %s [%s]\n" out.puts out.puts 'Options for CONFIG or ALL:' @config.each do |item| out.printf fmt, item.help_opt, item.description, item.help_default end out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" out.puts out.puts 'Options for INSTALL:' out.printf fmt, '--no-harm', 'only display what to do if given', 'off' out.printf fmt, '--prefix=path', 'install path prefix', '' out.puts end # # Task Handlers # def exec_config @installer.exec_config @config.save # must be final end def exec_setup @installer.exec_setup end def exec_install @installer.exec_install end def exec_test @installer.exec_test end def exec_show @config.each do |i| printf "%-20s %s\n", i.name, i.value if i.value? end end def exec_clean @installer.exec_clean end def exec_distclean @installer.exec_distclean end end # class ToplevelInstaller class ToplevelInstallerMulti < ToplevelInstaller include FileOperations def initialize(ardir_root, config) super @packages = directories_of("#{@ardir}/packages") raise 'no package exists' if @packages.empty? @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig", self @packages.each do |name| @config.load_script "#{@ardir}/packages/#{name}/metaconfig" end end attr_reader :packages def packages=(list) raise 'package list is empty' if list.empty? list.each do |name| raise "directory packages/#{name} does not exist"\ unless File.dir?("#{@ardir}/packages/#{name}") end @packages = list end def init_installers @installers = {} @packages.each do |pack| @installers[pack] = Installer.new(@config, "#{@ardir}/packages/#{pack}", "packages/#{pack}") end with = extract_selection(config('with')) without = extract_selection(config('without')) @selected = @installers.keys.select {|name| (with.empty? or with.include?(name)) \ and not without.include?(name) } end def extract_selection(list) a = list.split(/,/) a.each do |name| setup_rb_error "no such package: #{name}" unless @installers.key?(name) end a end def print_usage(f) super f.puts 'Inluded packages:' f.puts ' ' + @packages.sort.join(' ') f.puts end # # Task Handlers # def exec_config run_hook 'pre-config' each_selected_installers {|inst| inst.exec_config } run_hook 'post-config' @config.save # must be final end def exec_setup run_hook 'pre-setup' each_selected_installers {|inst| inst.exec_setup } run_hook 'post-setup' end def exec_install run_hook 'pre-install' each_selected_installers {|inst| inst.exec_install } run_hook 'post-install' end def exec_test run_hook 'pre-test' each_selected_installers {|inst| inst.exec_test } run_hook 'post-test' end def exec_clean rm_f @config.savefile run_hook 'pre-clean' each_selected_installers {|inst| inst.exec_clean } run_hook 'post-clean' end def exec_distclean rm_f @config.savefile run_hook 'pre-distclean' each_selected_installers {|inst| inst.exec_distclean } run_hook 'post-distclean' end # # lib # def each_selected_installers Dir.mkdir 'packages' unless File.dir?('packages') @selected.each do |pack| $stderr.puts "Processing the package `#{pack}' ..." if verbose? Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") Dir.chdir "packages/#{pack}" yield @installers[pack] Dir.chdir '../..' end end def run_hook(id) @root_installer.run_hook id end # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end end # class ToplevelInstallerMulti class Installer FILETYPES = %w( bin lib ext data conf man ) include FileOperations include HookScriptAPI def initialize(config, srcroot, objroot) @config = config @srcdir = File.expand_path(srcroot) @objdir = File.expand_path(objroot) @currdir = '.' end def inspect "#<#{self.class} #{File.basename(@srcdir)}>" end def noop(rel) end # # Hook Script API base methods # def srcdir_root @srcdir end def objdir_root @objdir end def relpath @currdir end # # Config Access # # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end def verbose_off begin save, @config.verbose = @config.verbose?, false yield ensure @config.verbose = save end end # # TASK config # def exec_config exec_task_traverse 'config' end alias config_dir_bin noop alias config_dir_lib noop def config_dir_ext(rel) extconf if extdir?(curr_srcdir()) end alias config_dir_data noop alias config_dir_conf noop alias config_dir_man noop def extconf ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt end # # TASK setup # def exec_setup exec_task_traverse 'setup' end def setup_dir_bin(rel) files_of(curr_srcdir()).each do |fname| update_shebang_line "#{curr_srcdir()}/#{fname}" end end alias setup_dir_lib noop def setup_dir_ext(rel) make if extdir?(curr_srcdir()) end alias setup_dir_data noop alias setup_dir_conf noop alias setup_dir_man noop def update_shebang_line(path) return if no_harm? return if config('shebang') == 'never' old = Shebang.load(path) if old $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 new = new_shebang(old) return if new.to_s == old.to_s else return unless config('shebang') == 'all' new = Shebang.new(config('rubypath')) end $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? open_atomic_writer(path) {|output| File.open(path, 'rb') {|f| f.gets if old # discard output.puts new.to_s output.print f.read } } end def new_shebang(old) if /\Aruby/ =~ File.basename(old.cmd) Shebang.new(config('rubypath'), old.args) elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' Shebang.new(config('rubypath'), old.args[1..-1]) else return old unless config('shebang') == 'all' Shebang.new(config('rubypath')) end end def open_atomic_writer(path, &block) tmpfile = File.basename(path) + '.tmp' begin File.open(tmpfile, 'wb', &block) File.rename tmpfile, File.basename(path) ensure File.unlink tmpfile if File.exist?(tmpfile) end end class Shebang def Shebang.load(path) line = nil File.open(path) {|f| line = f.gets } return nil unless /\A#!/ =~ line parse(line) end def Shebang.parse(line) cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') new(cmd, args) end def initialize(cmd, args = []) @cmd = cmd @args = args end attr_reader :cmd attr_reader :args def to_s "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") end end # # TASK install # def exec_install rm_f 'InstalledFiles' exec_task_traverse 'install' end def install_dir_bin(rel) install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 end def install_dir_lib(rel) install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 end def install_dir_ext(rel) return unless extdir?(curr_srcdir()) install_files rubyextentions('.'), "#{config('sodir')}/#{File.dirname(rel)}", 0555 end def install_dir_data(rel) install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 end def install_dir_conf(rel) # FIXME: should not remove current config files # (rename previous file to .old/.org) install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 end def install_dir_man(rel) install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 end def install_files(list, dest, mode) mkdir_p dest, @config.install_prefix list.each do |fname| install fname, dest, mode, @config.install_prefix end end def libfiles glob_reject(%w(*.y *.output), targetfiles()) end def rubyextentions(dir) ents = glob_select("*.#{@config.dllext}", targetfiles()) if ents.empty? setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" end ents end def targetfiles mapdir(existfiles() - hookfiles()) end def mapdir(ents) ents.map {|ent| if File.exist?(ent) then ent # objdir else "#{curr_srcdir()}/#{ent}" # srcdir end } end # picked up many entries from cvs-1.11.1/src/ignore.c JUNK_FILES = %w( core RCSLOG tags TAGS .make.state .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$ *.org *.in .* ) def existfiles glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) end def hookfiles %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| %w( config setup install clean ).map {|t| sprintf(fmt, t) } }.flatten end def glob_select(pat, ents) re = globs2re([pat]) ents.select {|ent| re =~ ent } end def glob_reject(pats, ents) re = globs2re(pats) ents.reject {|ent| re =~ ent } end GLOB2REGEX = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' } def globs2re(pats) /\A(?:#{ pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') })\z/ end # # TASK test # TESTDIR = 'test' def exec_test unless File.directory?('test') $stderr.puts 'no test in this package' if verbose? return end $stderr.puts 'Running tests...' if verbose? begin require 'test/unit' rescue LoadError setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' end runner = Test::Unit::AutoRunner.new(true) runner.to_run << TESTDIR runner.run end # # TASK clean # def exec_clean exec_task_traverse 'clean' rm_f @config.savefile rm_f 'InstalledFiles' end alias clean_dir_bin noop alias clean_dir_lib noop alias clean_dir_data noop alias clean_dir_conf noop alias clean_dir_man noop def clean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'clean' if File.file?('Makefile') end # # TASK distclean # def exec_distclean exec_task_traverse 'distclean' rm_f @config.savefile rm_f 'InstalledFiles' end alias distclean_dir_bin noop alias distclean_dir_lib noop def distclean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'distclean' if File.file?('Makefile') end alias distclean_dir_data noop alias distclean_dir_conf noop alias distclean_dir_man noop # # Traversing # def exec_task_traverse(task) run_hook "pre-#{task}" FILETYPES.each do |type| if type == 'ext' and config('without-ext') == 'yes' $stderr.puts 'skipping ext/* by user option' if verbose? next end traverse task, type, "#{task}_dir_#{type}" end run_hook "post-#{task}" end def traverse(task, rel, mid) dive_into(rel) { run_hook "pre-#{task}" __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') directories_of(curr_srcdir()).each do |d| traverse task, "#{rel}/#{d}", mid end run_hook "post-#{task}" } end def dive_into(rel) return unless File.dir?("#{@srcdir}/#{rel}") dir = File.basename(rel) Dir.mkdir dir unless File.dir?(dir) prevdir = Dir.pwd Dir.chdir dir $stderr.puts '---> ' + rel if verbose? @currdir = rel yield Dir.chdir prevdir $stderr.puts '<--- ' + rel if verbose? @currdir = File.dirname(rel) end def run_hook(id) path = [ "#{curr_srcdir()}/#{id}", "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } return unless path begin instance_eval File.read(path), path, 1 rescue raise if $DEBUG setup_rb_error "hook #{path} failed:\n" + $!.message end end end # class Installer class SetupError < StandardError; end def setup_rb_error(msg) raise SetupError, msg end if $0 == __FILE__ begin ToplevelInstaller.invoke rescue SetupError raise if $DEBUG $stderr.puts $!.message $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." exit 1 end end t/000077500000000000000000000000001471646016400113325ustar00rootroot00000000000000t/.gitignore000066400000000000000000000000431471646016400133170ustar00rootroot00000000000000/random_blob /.dep+* /*.crt /*.key t/GNUmakefile000066400000000000000000000001731471646016400134050ustar00rootroot00000000000000# there used to be more, here, but we stopped relying on recursive make all:: $(MAKE) -C .. test-integration .PHONY: all t/README000066400000000000000000000027721471646016400122220ustar00rootroot00000000000000= Unicorn integration test suite These are all integration tests that start the server on random, unused TCP ports or Unix domain sockets. They're all designed to run concurrently with other tests to minimize test time, but tests may be run independently as well. New tests are written in Perl 5 because we need a stable language to test real-world behavior and Ruby introduces incompatibilities at a far faster rate than Perl 5. Perl is Ruby's older cousin, so it should be easy-to-learn for Rubyists. Old tests are in Bourne shell and slowly being ported to Perl 5. == Requirements * {Ruby 2.5.0+}[https://www.ruby-lang.org/en/] * {Perl 5.14+}[https://www.perl.org/] # your distro should have it * {GNU make}[https://www.gnu.org/software/make/] * {curl}[https://curl.haxx.se/] We do not use bashisms or any non-portable, non-POSIX constructs in our shell code. We use the "pipefail" option if available and mainly test with {ksh}[http://kornshell.com/], but occasionally with {dash}[http://gondor.apana.org.au/~herbert/dash/] and {bash}[https://www.gnu.org/software/bash/], too. == Running Tests To run the entire test suite with 8 tests running at once: make -j8 && prove -vw To run one individual test (Perl5): prove -vw t/integration.t To run one individual test (shell): make t0000-simple-http.sh You may also increase verbosity by setting the "V" variable for GNU make. To disable trapping of stdout/stderr: make V=1 To enable the "set -x" option in shell scripts to trace execution make V=2 t/active-unix-socket.t000066400000000000000000000065041471646016400152460ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use IO::Socket::UNIX; use autodie; no autodie 'kill'; my %to_kill; END { kill('TERM', values(%to_kill)) if keys %to_kill } my $u1 = "$tmpdir/u1.sock"; my $u2 = "$tmpdir/u2.sock"; { write_file '>', "$tmpdir/u1.conf.rb", <', "$tmpdir/u2.conf.rb", <', "$tmpdir/u3.conf.rb", <join; is($?, 0, 'daemonized 1st process'); chomp($to_kill{u1} = slurp("$tmpdir/u.pid")); like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file'); chomp(my $worker_pid = readline(unix_start($u1, 'GET /pid'))); like($worker_pid, qr/\A\d+\z/s, 'captured worker pid'); ok(kill(0, $worker_pid), 'worker is kill-able'); # 2nd process conflicts on PID unicorn('-c', "$tmpdir/u2.conf.rb", @uarg)->join; isnt($?, 0, 'conflicting PID file fails to start'); chomp(my $pidf = slurp("$tmpdir/u.pid")); is($pidf, $to_kill{u1}, 'pid file contents unchanged after start failure'); chomp(my $pid2 = readline(unix_start($u1, 'GET /pid'))); is($worker_pid, $pid2, 'worker PID unchanged'); # 3rd process conflicts on socket unicorn('-c', "$tmpdir/u3.conf.rb", @uarg)->join; isnt($?, 0, 'conflicting UNIX socket fails to start'); chomp($pid2 = readline(unix_start($u1, 'GET /pid'))); is($worker_pid, $pid2, 'worker PID still unchanged'); chomp($pidf = slurp("$tmpdir/u.pid")); is($pidf, $to_kill{u1}, 'pid file contents unchanged after 2nd start failure'); { # teardown initial process via SIGKILL ok(kill('KILL', delete $to_kill{u1}), 'SIGKILL initial daemon'); close $p1; vec(my $rvec = '', fileno($p0), 1) = 1; is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP'); is(my $undef = <$p0>, undef, 'process closed pipe writer at exit'); ok(-f "$tmpdir/u.pid", 'pid file stayed after SIGKILL'); ok(-S $u1, 'socket stayed after SIGKILL'); is(IO::Socket::UNIX->new(Peer => $u1, Type => SOCK_STREAM), undef, 'fail to connect to u1'); for (1..50) { # wait for init process to reap worker kill(0, $worker_pid) or last; sleep 0.011; } ok(!kill(0, $worker_pid), 'worker gone after parent dies'); } # restart the first instance { pipe($p0, $p1); fcntl($p1, POSIX::F_SETFD, 0); unicorn('-c', "$tmpdir/u1.conf.rb", @uarg)->join; is($?, 0, 'daemonized 1st process'); chomp($to_kill{u1} = slurp("$tmpdir/u.pid")); like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file'); chomp($pid2 = readline(unix_start($u1, 'GET /pid'))); like($pid2, qr/\A\d+\z/, 'worker running'); ok(kill('TERM', delete $to_kill{u1}), 'SIGTERM restarted daemon'); close $p1; vec(my $rvec = '', fileno($p0), 1) = 1; is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP'); is(my $undef = <$p0>, undef, 'process closed pipe writer at exit'); ok(!-f "$tmpdir/u.pid", 'pid file gone after SIGTERM'); ok(-S $u1, 'socket stays after SIGTERM'); } check_stderr; undef $tmpdir; done_testing; t/back-out-of-upgrade.t000066400000000000000000000027631471646016400152630ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ # test backing out of USR2 upgrade use v5.14; BEGIN { require './t/lib.perl' }; use autodie; my $srv = tcp_server(); mkfifo_die $fifo; write_file '>', $u_conf, < $srv }); like(my $wpid_orig_1 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker pid'); ok $ar->do_kill('USR2'), 'USR2 to start upgrade'; ok $ar->do_kill('WINCH'), 'drop old worker'; like(my $wpid_new = slurp($fifo), qr/\Apid=\d+\z/a, 'got pid from new master'); chomp(my $new_pid = slurp($pid_file)); isnt $new_pid, $ar->{pid}, 'PID file changed'; chomp(my $pid_oldbin = slurp("$pid_file.oldbin")); is $pid_oldbin, $ar->{pid}, '.oldbin PID valid'; ok $ar->do_kill('HUP'), 'HUP old master'; like(my $wpid_orig_2 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker new pid'); ok kill('QUIT', $new_pid), 'abort old master'; kill_until_dead $new_pid; my ($st, $hdr, $req_pid) = do_req $srv, 'GET /'; chomp $req_pid; is $wpid_orig_2, "pid=$req_pid", 'new worker on old worker serves'; ok !-f "$pid_file.oldbin", '.oldbin PID file gone'; chomp(my $old_pid = slurp($pid_file)); is $old_pid, $ar->{pid}, 'PID file restored'; my @log = grep !/ERROR -- : reaped .*? exec\(\)-ed/, slurp($err_log); check_stderr @log; undef $tmpdir; done_testing; t/bin/000077500000000000000000000000001471646016400121025ustar00rootroot00000000000000t/bin/unused_listen000077500000000000000000000023361471646016400147150ustar00rootroot00000000000000#!/usr/bin/env ruby # -*- encoding: binary -*- # this is to remain compatible with the unused_port function in the # Unicorn test/test_helper.rb file require 'socket' require 'tmpdir' default_port = 8080 addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' retries = 100 base = 5000 port = sock = lock_path = nil begin begin port = base + rand(32768 - base) while port == default_port port = base + rand(32768 - base) end sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) sock.bind(Socket.pack_sockaddr_in(port, addr)) sock.listen(5) rescue Errno::EADDRINUSE, Errno::EACCES sock.close rescue nil retry if (retries -= 1) >= 0 end # since we'll end up closing the random port we just got, there's a race # condition could allow the random port we just chose to reselect itself # when running tests in parallel with gmake. Create a lock file while # we have the port here to ensure that does not happen. lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock" lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600) rescue Errno::EEXIST sock.close rescue nil retry end sock.close rescue nil puts %Q(listen=#{addr}:#{port} T_RM_LIST="$T_RM_LIST #{lock_path}") t/client_body_buffer_size.ru000066400000000000000000000004771471646016400165700ustar00rootroot00000000000000#\ -E none # frozen_string_literal: false app = lambda do |env| input = env['rack.input'] case env["PATH_INFO"] when "/tmp_class" body = input.instance_variable_get(:@tmp).class.name when "/input_class" body = input.class.name else return [ 500, {}, [] ] end [ 200, {}, [ body ] ] end run app t/client_body_buffer_size.t000066400000000000000000000052341471646016400164010ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; my $conf_fh = write_file '>', $u_conf, <autoflush(1); my $srv = tcp_server(); my $host_port = tcp_host_port($srv); my @uarg = (qw(-E none t/client_body_buffer_size.ru -c), $u_conf); my $ar = unicorn(@uarg, { 3 => $srv }); my ($c, $status, $hdr); my $mem_class = 'StringIO'; my $fs_class = 'Unicorn::TmpIO'; $c = tcp_start($srv, "PUT /input_class HTTP/1.0\r\nContent-Length: 0"); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $mem_class, 'zero-byte file is StringIO'); $c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1"); print $c '.'; ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $fs_class, '1 byte file is filesystem-backed'); my $fifo = "$tmpdir/fifo"; POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!"; seek($conf_fh, 0, SEEK_SET); truncate($conf_fh, 0); print $conf_fh <do_kill('HUP'); open my $fifo_fh, '<', $fifo; like(my $wpid = readline($fifo_fh), qr/\Apid=\d+\z/a , 'reloaded w/ default client_body_buffer_size'); $c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1"); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $mem_class, 'class for a 1 byte file is memory-backed'); my $one_meg = 1024 ** 2; $c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg"); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $fs_class, '1 megabyte file is FS-backed'); # reload with bigger client_body_buffer_size say $conf_fh "client_body_buffer_size $one_meg"; $ar->do_kill('HUP'); open $fifo_fh, '<', $fifo; like($wpid = readline($fifo_fh), qr/\Apid=\d+\z/a , 'reloaded w/ bigger client_body_buffer_size'); $c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg"); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $mem_class, '1 megabyte file is now memory-backed'); my $too_big = $one_meg + 1; $c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $too_big"); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); is(readline($c), $fs_class, '1 megabyte + 1 byte file is FS-backed'); undef $ar; check_stderr; undef $tmpdir; done_testing; t/detach.ru000066400000000000000000000004371471646016400131360ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentType, "text/plain" fifo_path = ENV["TEST_FIFO"] or abort "TEST_FIFO not set" run lambda { |env| pid = fork do File.open(fifo_path, "wb") do |fp| fp.write "HIHI" end end Process.detach(pid) [ 200, {}, [ pid.to_s ] ] } t/env.ru000066400000000000000000000002251471646016400124710ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, "text/plain" run lambda { |env| [ 200, {}, [ env.inspect << "\n" ] ] } t/fails-rack-lint.ru000066400000000000000000000004521471646016400146630ustar00rootroot00000000000000# frozen_string_literal: false # This rack app returns an invalid status code, which will cause # Rack::Lint to throw an exception if it is present. This # is used to check whether Rack::Lint is in the stack or not. run lambda {|env| return [42, {}, ["Rack::Lint wasn't there if you see this"]]} t/heartbeat-timeout.ru000066400000000000000000000005361471646016400153310ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentLength headers = { 'content-type' => 'text/plain' } run lambda { |env| case env['PATH_INFO'] when "/block-forever" Process.kill(:STOP, $$) sleep # in case STOP signal is not received in time [ 500, headers, [ "Should never get here\n" ] ] else [ 200, headers, [ "#$$" ] ] end } t/heartbeat-timeout.t000066400000000000000000000035211471646016400151430ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); mkdir "$tmpdir/alt"; my $srv = tcp_server(); write_file '>', $u_conf, < $srv }); my ($status, $hdr, $wpid) = do_req($srv, 'GET /pid HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds'); like($wpid, qr/\A[0-9]+\z/, 'worker is running'); my $t0 = clock_gettime(CLOCK_MONOTONIC); my $c = tcp_start($srv, 'GET /block-forever HTTP/1.0'); vec(my $rvec = '', fileno($c), 1) = 1; is(select($rvec, undef, undef, 6), 1, 'got readiness'); $c->blocking(0); is(sysread($c, my $buf, 128), 0, 'got EOF response'); my $elapsed = clock_gettime(CLOCK_MONOTONIC) - $t0; ok($elapsed > 3, 'timeout took >3s'); my @timeout_err = slurp($err_log); truncate($err_log, 0); is(grep(/timeout \(\d+s > 3s\), killing/, @timeout_err), 1, 'noted timeout error') or diag explain(\@timeout_err); # did it respawn? ($status, $hdr, my $new_pid) = do_req($srv, 'GET /pid HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds'); isnt($new_pid, $wpid, 'spawned new worker'); diag 'SIGSTOP for 4 seconds...'; $ar->do_kill('STOP'); sleep 4; $ar->do_kill('CONT'); for my $i (1..2) { ($status, $hdr, my $spid) = do_req($srv, 'GET /pid HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, "PID request succeeds #$i after STOP+CONT"); is($new_pid, $spid, "worker pid unchanged after STOP+CONT #$i"); if ($i == 1) { diag 'sleeping 2s to ensure timeout is not delayed'; sleep 2; } } $ar->join('TERM'); check_stderr; undef $tmpdir; done_testing; t/integration.ru000066400000000000000000000074151471646016400142340ustar00rootroot00000000000000#!ruby # frozen_string_literal: false # Copyright (C) unicorn hackers # License: GPL-3.0+ # this goes for t/integration.t We'll try to put as many tests # in here as possible to avoid startup overhead of Ruby. def early_hints(env, val) env['rack.early_hints'].call('link' => val) # val may be ary or string [ 200, {}, [ val.class.to_s ] ] end $orig_rack_200 = nil def tweak_status_code $orig_rack_200 = Rack::Utils::HTTP_STATUS_CODES[200] Rack::Utils::HTTP_STATUS_CODES[200] = "HI" [ 200, {}, [] ] end def restore_status_code $orig_rack_200 or return [ 500, {}, [] ] Rack::Utils::HTTP_STATUS_CODES[200] = $orig_rack_200 [ 200, {}, [] ] end class WriteOnClose def each(&block) @callback = block end def close @callback.call "7\r\nGoodbye\r\n0\r\n\r\n" end end def write_on_close [ 200, { 'transfer-encoding' => 'chunked' }, WriteOnClose.new ] end def env_dump(env, dump_body = false) require 'json' h = {} env.each do |k,v| case v when String, Integer, true, false; h[k] = v else case k when 'rack.version', 'rack.after_reply'; h[k] = v when 'rack.input'; h[k] = v.class.to_s end end end h['unicorn_test.body'] = env['rack.input'].read if dump_body h.to_json end def rack_input_tests(env) return [ 100, {}, [] ] if /\A100-continue\z/i =~ env['HTTP_EXPECT'] cap = 16384 require 'digest/md5' dig = Digest::MD5.new input = env['rack.input'] case env['PATH_INFO'] when '/rack_input/size_first'; input.size when '/rack_input/rewind_first'; input.rewind when '/rack_input'; # OK else abort "bad path: #{env['PATH_INFO']}" end if buf = input.read(rand(cap)) begin raise "#{buf.size} > #{cap}" if buf.size > cap dig.update(buf) end while input.read(rand(cap), buf) buf.clear # remove this call if Ruby ever gets escape analysis end h = { 'content-type' => 'text/plain' } if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']] cmd5_bin = cmd5_b64.unpack('m')[0] if cmd5_bin != dig.digest h['content-length'] = cmd5_b64.size.to_s return [ 500, h, [ cmd5_b64 ] ] end end h['content-length'] = '32' [ 200, h, [ dig.hexdigest ] ] end $nr_aborts = 0 run(lambda do |env| case env['REQUEST_METHOD'] when 'GET' case env['PATH_INFO'] when '/rack-2-newline-headers'; [ 200, { 'X-R2' => "a\nb\nc" }, [] ] when '/rack-3-array-headers'; [ 200, { 'x-r3' => %w(a b c) }, [] ] when '/nil-header-value'; [ 200, { 'X-Nil' => nil }, [] ] when '/unknown-status-pass-through'; [ '666 I AM THE BEAST', {}, [] ] when '/env_dump'; [ 200, {}, [ env_dump(env) ] ] when '/write_on_close'; write_on_close when '/pid'; [ 200, {}, [ "#$$\n" ] ] when '/early_hints_rack2'; early_hints(env, "r\n2") when '/early_hints_rack3'; early_hints(env, %w(r 3)) when '/broken_app'; raise RuntimeError, 'hello' when '/aborted'; $nr_aborts += 1; [ 200, {}, [] ] when '/nr_aborts'; [ 200, { 'nr-aborts' => "#$nr_aborts" }, [] ] when '/nil'; nil when '/read_fifo'; [ 200, {}, [ File.read(env['HTTP_READ_FIFO']) ] ] else '/'; [ 200, {}, [ env_dump(env) ] ] end # case PATH_INFO (GET) when 'POST' case env['PATH_INFO'] when '/tweak-status-code'; tweak_status_code when '/restore-status-code'; restore_status_code when '/env_dump'; [ 200, {}, [ env_dump(env, true) ] ] end # case PATH_INFO (POST) # ... when 'PUT' case env['PATH_INFO'] when %r{\A/rack_input}; rack_input_tests(env) when '/env_dump'; [ 200, {}, [ env_dump(env) ] ] end when 'OPTIONS' case env['REQUEST_URI'] when '*'; [ 200, {}, [ env_dump(env) ] ] end end # case REQUEST_METHOD end) # run t/integration.t000066400000000000000000000411271471646016400140470ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ # This is the main integration test for fast-ish things to minimize # Ruby startup time penalties. use v5.14; BEGIN { require './t/lib.perl' }; use autodie; use Socket qw(SOL_SOCKET SO_KEEPALIVE SHUT_WR); our $srv = tcp_server(); our $host_port = tcp_host_port($srv); if ('ensure Perl does not set SO_KEEPALIVE by default') { my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE); unpack('i', $val) == 0 or setsockopt($srv, SOL_SOCKET, SO_KEEPALIVE, pack('i', 0)); $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE); } my $t0 = time; my $u1 = "$tmpdir/u1"; my $conf_fh = write_file '>', $u_conf, <autoflush(1); my $ar = unicorn(qw(-E none t/integration.ru -c), $u_conf, { 3 => $srv }); my $curl = which('curl'); local $ENV{NO_PROXY} = '*'; # for curl my $fifo = "$tmpdir/fifo"; POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!"; my %PUT = ( chunked_md5 => sub { my ($in, $out, $path, %opt) = @_; my $dig = Digest::MD5->new; print $out <add($buf); } print $out "0\r\nContent-MD5: ", $dig->b64digest, "\r\n\r\n"; }, identity => sub { my ($in, $out, $path, %opt) = @_; my $clen = $opt{-s} // -s $in; print $out < $bs ? $bs : $clen; $r = read($in, $buf, $len); die 'premature EOF' if $r == 0; print $out $buf; $clen -= $r; } }, ); my ($c, $status, $hdr, $bdy); # response header tests ($status, $hdr) = do_req($srv, 'GET /rack-2-newline-headers HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid'); my $orig_200_status = $status; is_deeply([ grep(/^X-R2: /, @$hdr) ], [ 'X-R2: a', 'X-R2: b', 'X-R2: c' ], 'rack 2 LF-delimited headers supported') or diag(explain($hdr)); { my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE); is(unpack('i', $val), 1, 'SO_KEEPALIVE set on inherited socket'); } SKIP: { # Date header check my @d = grep(/^Date: /i, @$hdr); is(scalar(@d), 1, 'got one date header') or diag(explain(\@d)); eval { require HTTP::Date } or skip "HTTP::Date missing: $@", 1; $d[0] =~ s/^Date: //i or die 'BUG: did not strip date: prefix'; my $t = HTTP::Date::str2time($d[0]); my $now = time; ok($t >= ($t0 - 1) && $t > 0 && $t <= ($now + 1), 'valid date') or diag(explain(["t=$t t0=$t0 now=$now", $!, \@d])); }; ($status, $hdr) = do_req($srv, 'GET /rack-3-array-headers HTTP/1.0'); is_deeply([ grep(/^x-r3: /, @$hdr) ], [ 'x-r3: a', 'x-r3: b', 'x-r3: c' ], 'rack 3 array headers supported') or diag(explain($hdr)); my $JSON_PP; SKIP: { eval { require JSON::PP } or skip "JSON::PP missing: $@", 1; $JSON_PP = JSON::PP->new; my $get_json = sub { my (@req) = @_; my @r = do_req $srv, @req; my $env = eval { $JSON_PP->decode($r[2]) }; diag "$@ (r[2]=$r[2])" if $@; is ref($env), 'HASH', "@req response body is JSON"; (@r, $env) }; ($status, $hdr, my $json, my $env) = $get_json->('GET /env_dump'); is($status, undef, 'no status for HTTP/0.9'); is($hdr, undef, 'no header for HTTP/0.9'); unlike($json, qr/^Connection: /smi, 'no connection header for 0.9'); unlike($json, qr!\AHTTP/!s, 'no HTTP/1.x prefix for 0.9'); is($env->{SERVER_PROTOCOL}, 'HTTP/0.9', 'SERVER_PROTOCOL is 0.9'); is $env->{'rack.url_scheme'}, 'http', 'rack.url_scheme default'; is $env->{'rack.input'}, 'StringIO', 'StringIO for no content'; my $req = 'OPTIONS *'; ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0"); is $env->{REQUEST_PATH}, '', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '', "$req => PATH_INFO"; is $env->{REQUEST_URI}, '*', "$req => REQUEST_URI"; $req = 'GET http://e:3/env_dump?y=z'; ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0"); is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO"; is $env->{QUERY_STRING}, 'y=z', "$req => QUERY_STRING"; $req = 'GET http://e:3/env_dump#frag'; ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0"); is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO"; is $env->{QUERY_STRING}, '', "$req => QUERY_STRING"; is $env->{FRAGMENT}, 'frag', "$req => FRAGMENT"; $req = 'GET http://e:3/env_dump?a=b#frag'; ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0"); is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO"; is $env->{QUERY_STRING}, 'a=b', "$req => QUERY_STRING"; is $env->{FRAGMENT}, 'frag', "$req => FRAGMENT"; for my $proto (qw(https http)) { $req = "X-Forwarded-Proto: $proto"; ($status, $hdr, $json, $env) = $get_json->( "GET /env_dump HTTP/1.0\r\n". "X-Forwarded-Proto: $proto"); is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO"; is $env->{'rack.url_scheme'}, $proto, "$req => rack.url_scheme"; } $req = 'X-Forwarded-Proto: ftp'; # invalid proto ($status, $hdr, $json, $env) = $get_json->( "GET /env_dump HTTP/1.0\r\n". "X-Forwarded-Proto: ftp"); is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH"; is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO"; is $env->{'rack.url_scheme'}, 'http', "$req => rack.url_scheme"; ($status, $hdr, $json, $env) = $get_json->("PUT /env_dump HTTP/1.0\r\n". 'Content-Length: 0'); is $env->{'rack.input'}, 'StringIO', 'content-length: 0 uses StringIO'; ($status, $hdr, $json, $env) = $get_json->("PUT /env_dump HTTP/1.0\r\n". 'Content-Length: 1'); is $env->{'rack.input'}, 'Unicorn::TeeInput', 'content-length: 1 uses TeeInput'; } # cf. ($status, $hdr) = do_req($srv, 'GET /nil-header-value HTTP/1.0'); is_deeply([grep(/^X-Nil:/, @$hdr)], ['X-Nil: '], 'nil header value accepted for broken apps') or diag(explain($hdr)); check_stderr; ($status, $hdr, $bdy) = do_req($srv, 'GET /broken_app HTTP/1.0'); like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on broken endpoint'); is($bdy, undef, 'no response body after exception'); seek $errfh, 0, SEEK_SET; { my $nxt; while (!defined($nxt) && defined($_ = <$errfh>)) { $nxt = <$errfh> if /app error/; } ok $nxt, 'got app error' and like $nxt, qr/\bintegration\.ru/, 'got backtrace'; } seek $errfh, 0, SEEK_SET; truncate $errfh, 0; ($status, $hdr, $bdy) = do_req($srv, 'GET /nil HTTP/1.0'); like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on nil endpoint'); like slurp($err_log), qr/app error/, 'exception logged for nil'; seek $errfh, 0, SEEK_SET; truncate $errfh, 0; my $ck_early_hints = sub { my ($note) = @_; $c = unix_start($u1, 'GET /early_hints_rack2 HTTP/1.0'); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 2 value'); is_deeply(['link: r', 'link: 2'], $hdr, 'rack 2 hints match '.$note); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards'); is(readline($c), 'String', 'early hints used a String for rack 2'); $c = unix_start($u1, 'GET /early_hints_rack3 HTTP/1.0'); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 3'); is_deeply(['link: r', 'link: 3'], $hdr, 'rack 3 hints match '.$note); ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards'); is(readline($c), 'Array', 'early hints used a String for rack 3'); }; $ck_early_hints->('ccc off'); # we'll retest later if ('TODO: ensure Rack::Utils::HTTP_STATUS_CODES is available') { ($status, $hdr) = do_req $srv, 'POST /tweak-status-code HTTP/1.0'; like($status, qr!\AHTTP/1\.[01] 200 HI\b!, 'status tweaked'); ($status, $hdr) = do_req $srv, 'POST /restore-status-code HTTP/1.0'; is($status, $orig_200_status, 'original status restored'); } SKIP: { eval { require HTTP::Tiny } or skip "HTTP::Tiny missing: $@", 1; my $ht = HTTP::Tiny->new; my $res = $ht->get("http://$host_port/write_on_close"); is($res->{content}, 'Goodbye', 'write-on-close body read'); } if ('bad requests') { ($status, $hdr) = do_req $srv, 'GET /env_dump HTTP/1/1'; like($status, qr!\AHTTP/1\.[01] 400 \b!, 'got 400 on bad request'); for my $abs_uri (qw(ssh+http://e/ ftp://e/x http+ssh://e/x)) { ($status, $hdr) = do_req $srv, "GET $abs_uri HTTP/1.0"; like $status, qr!\AHTTP/1\.[01] 400 \b!, "400 on $abs_uri"; } $c = tcp_start($srv); print $c 'GET /'; my $buf = join('', (0..9), 'ab'); for (0..1023) { print $c $buf } print $c " HTTP/1.0\r\n\r\n"; ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on REQUEST_PATH > (12 * 1024)'); $c = tcp_start($srv); print $c 'GET /hello-world?a'; $buf = join('', (0..9)); for (0..1023) { print $c $buf } print $c " HTTP/1.0\r\n\r\n"; ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on QUERY_STRING > (10 * 1024)'); $c = tcp_start($srv); print $c 'GET /hello-world#a'; $buf = join('', (0..9), 'a'..'f'); for (0..63) { print $c $buf } print $c " HTTP/1.0\r\n\r\n"; ($status, $hdr) = slurp_hdr($c); like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on FRAGMENT > (1024)'); } # input tests my ($blob_size, $blob_hash); SKIP: { skip 'SKIP_EXPENSIVE on', 1 if $ENV{SKIP_EXPENSIVE}; CORE::open(my $rh, '<', 't/random_blob') or skip "t/random_blob not generated $!", 1; $blob_size = -s $rh; require Digest::MD5; $blob_hash = Digest::MD5->new->addfile($rh)->hexdigest; my $ck_hash = sub { my ($sub, $path, %opt) = @_; seek($rh, 0, SEEK_SET); $c = tcp_start($srv); $c->autoflush($opt{sync} // 0); $PUT{$sub}->($rh, $c, $path, %opt); defined($opt{overwrite}) and print { $c } ('x' x $opt{overwrite}); $c->flush or die $!; shutdown($c, SHUT_WR); ($status, $hdr) = slurp_hdr($c); is(readline($c), $blob_hash, "$sub $path"); }; $ck_hash->('identity', '/rack_input', -s => $blob_size); $ck_hash->('chunked_md5', '/rack_input'); $ck_hash->('identity', '/rack_input/size_first', -s => $blob_size); $ck_hash->('identity', '/rack_input/rewind_first', -s => $blob_size); $ck_hash->('chunked_md5', '/rack_input/size_first'); $ck_hash->('chunked_md5', '/rack_input/rewind_first'); $ck_hash->('identity', '/rack_input', -s => $blob_size, sync => 1); $ck_hash->('chunked_md5', '/rack_input', sync => 1); # ensure small overwrites don't get checksummed $ck_hash->('identity', '/rack_input', -s => $blob_size, overwrite => 1); # one extra byte unlike(slurp($err_log), qr/ClientShutdown/, 'no overreads after client SHUT_WR'); # excessive overwrite truncated $c = tcp_start($srv); $c->autoflush(0); print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 1\r\n\r\n"; if (1) { local $SIG{PIPE} = 'IGNORE'; my $buf = "\0" x 8192; my $n = 0; my $end = time + 5; $! = 0; while (print $c $buf and time < $end) { ++$n } ok($!, 'overwrite truncated') or diag "n=$n err=$! ".time; undef $c; } # client shutdown early $c = tcp_start($srv); $c->autoflush(0); print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 16384\r\n\r\n"; if (1) { local $SIG{PIPE} = 'IGNORE'; print $c 'too short body'; shutdown($c, SHUT_WR); vec(my $rvec = '', fileno($c), 1) = 1; select($rvec, undef, undef, 10) or BAIL_OUT "timed out"; my $buf = <$c>; is($buf, undef, 'server aborted after client SHUT_WR'); undef $c; } $curl // skip 'no curl found in PATH', 1; my ($copt, $cout); my $url = "http://$host_port/rack_input"; my $do_curl = sub { my (@arg) = @_; pipe(my $cout, $copt->{1}); open $copt->{2}, '>', "$tmpdir/curl.err"; my $cpid = spawn($curl, '-sSf', @arg, $url, $copt); close(delete $copt->{1}); is(readline($cout), $blob_hash, "curl @arg response"); is(waitpid($cpid, 0), $cpid, "curl @arg exited"); is($?, 0, "no error from curl @arg"); is(slurp("$tmpdir/curl.err"), '', "no stderr from curl @arg"); }; $do_curl->(qw(-T t/random_blob)); seek($rh, 0, SEEK_SET); $copt->{0} = $rh; $do_curl->('-T-'); diag 'testing Unicorn::PrereadInput...'; local $srv = tcp_server(); local $host_port = tcp_host_port($srv); check_stderr; truncate($errfh, 0); my $pri = unicorn(qw(-E none t/preread_input.ru), { 3 => $srv }); $url = "http://$host_port/"; $do_curl->(qw(-T t/random_blob)); seek($rh, 0, SEEK_SET); $copt->{0} = $rh; $do_curl->('-T-'); my @pr_err = slurp("$tmpdir/err.log"); is(scalar(grep(/app dispatch:/, @pr_err)), 2, 'app dispatched twice'); # abort a chunked request by blocking curl on a FIFO: $c = tcp_start($srv, "PUT / HTTP/1.1\r\nTransfer-Encoding: chunked"); close $c; @pr_err = slurp("$tmpdir/err.log"); is(scalar(grep(/app dispatch:/, @pr_err)), 2, 'app did not dispatch on aborted request'); undef $pri; check_stderr; diag 'Unicorn::PrereadInput middleware tests done'; } # disallow /content_length/i and /transfer_encoding/i due to confusion+ # smuggling attacks # cf. SKIP: { $JSON_PP or skip "JSON::PP missing: $@", 1; my $body = "1\r\nZ\r\n0\r\n\r\n"; my $blen = length $body; my $post = "POST /env_dump HTTP/1.0\r\n"; for my $x (["Content-Length: $blen", $body], [ "Transfer-Encoding: chunked", 'Z']) { ($status, $hdr, $bdy) = do_req $srv, $post, $x->[0], "\r\n\r\n", $body; like $status, qr!\AHTTP/1\.[01] 200!, 'Content-Length POST'; my $env = $JSON_PP->decode($bdy); is $env->{'unicorn_test.body'}, $x->[1], "$x->[0]-only"; } for my $cl (qw(Content-Length Content_Length)) { for my $te (qw(Transfer-Encoding Transfer_Encoding)) { ($status, $hdr, $bdy) = do_req $srv, $post, "$te: chunked\r\n", "$cl: $blen\r\n", "\r\n", $body; if ("$cl$te" =~ /_/) { like $status, qr!\AHTTP/1\.[01] 400 \b!, "got 400 on bad request w/ $cl + $te"; } else { # RFC 7230 favors Transfer-Encoding :< like $status, qr!\AHTTP/1\.[01] 200 \b!, "got 200 w/ both $cl + $te"; my $env = $JSON_PP->decode($bdy); is $env->{'unicorn_test.body'}, 'Z', 'Transfer-Encoding favored over Content-Length (RFC 7230 3.3.3#3)'; } } } } # ... more stuff here # SIGHUP-able stuff goes here if ('check_client_connection') { print $conf_fh <do_kill('HUP'); open my $fifo_fh, '<', $fifo; my $wpid = readline($fifo_fh); like($wpid, qr/\Apid=\d+\z/a , 'new worker ready'); $ck_early_hints->('ccc on'); $c = tcp_start $srv, 'GET /env_dump HTTP/1.0'; vec(my $rvec = '', fileno($c), 1) = 1; select($rvec, undef, undef, 10) or BAIL_OUT 'timed out env_dump'; ($status, $hdr) = slurp_hdr($c); like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response'; ok $hdr, 'got all headers'; # start a slow TCP request my $rfifo = "$tmpdir/rfifo"; mkfifo_die $rfifo; $c = tcp_start $srv, "GET /read_fifo HTTP/1.0\r\nRead-FIFO: $rfifo"; tcp_start $srv, 'GET /aborted HTTP/1.0' for (1..100); write_file '>', $rfifo, 'TFIN'; ($status, $hdr) = slurp_hdr($c); like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response'; $bdy = <$c>; is $bdy, 'TFIN', 'got slow response from TCP socket'; # slow Unix socket request $c = unix_start $u1, "GET /read_fifo HTTP/1.0\r\nRead-FIFO: $rfifo"; vec($rvec = '', fileno($c), 1) = 1; select($rvec, undef, undef, 10) or BAIL_OUT 'timed out Unix CCC'; unix_start $u1, 'GET /aborted HTTP/1.0' for (1..100); write_file '>', $rfifo, 'UFIN'; ($status, $hdr) = slurp_hdr($c); like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response'; $bdy = <$c>; is $bdy, 'UFIN', 'got slow response from Unix socket'; ($status, $hdr, $bdy) = do_req $srv, 'GET /nr_aborts HTTP/1.0'; like "@$hdr", qr/nr-aborts: 0\b/, 'aborted connections unseen by Rack app'; } if ('max_header_len internal API') { undef $c; my $req = 'GET / HTTP/1.0'; my $len = length($req."\r\n\r\n"); print $conf_fh <do_kill('HUP'); open my $fifo_fh, '<', $fifo; my $wpid = readline($fifo_fh); like($wpid, qr/\Apid=\d+\z/a , 'new worker ready'); close $fifo_fh; $wpid =~ s/\Apid=// or die; ok(CORE::kill(0, $wpid), 'worker PID retrieved'); ($status, $hdr) = do_req($srv, $req); like($status, qr!\AHTTP/1\.[01] 200\b!, 'minimal request succeeds'); ($status, $hdr) = do_req($srv, 'GET /xxxxxx HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 413\b!, 'big request fails'); } undef $ar; check_stderr; undef $tmpdir; done_testing; t/lib.perl000066400000000000000000000175501471646016400127740ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ package UnicornTest; use v5.14; use parent qw(Exporter); use autodie; use Test::More; use Socket qw(SOMAXCONN); use Time::HiRes qw(sleep time); use IO::Socket::INET; use IO::Socket::UNIX; use Carp qw(croak); use POSIX qw(dup2 _exit setpgid :signal_h SEEK_SET F_SETFD); use File::Temp 0.19 (); # 0.19 for ->newdir our ($tmpdir, $errfh, $err_log, $u_sock, $u_conf, $daemon_pid, $pid_file, $wtest_sock, $fifo); our @EXPORT = qw(unicorn slurp tcp_server tcp_start unicorn $tmpdir $errfh $err_log $u_sock $u_conf $daemon_pid $pid_file $wtest_sock $fifo SEEK_SET tcp_host_port which spawn check_stderr unix_start slurp_hdr do_req stop_daemon sleep time mkfifo_die kill_until_dead write_file); my ($base) = ($0 =~ m!\b([^/]+)\.[^\.]+\z!); $tmpdir = File::Temp->newdir("unicorn-$base-XXXX", TMPDIR => 1); $wtest_sock = "$tmpdir/wtest.sock"; $err_log = "$tmpdir/err.log"; $pid_file = "$tmpdir/pid"; $fifo = "$tmpdir/fifo"; $u_sock = "$tmpdir/u.sock"; $u_conf = "$tmpdir/u.conf.rb"; open($errfh, '+>>', $err_log); if (my $t = $ENV{TAIL}) { my @tail = $t =~ /tail/ ? split(/\s+/, $t) : (qw(tail -F)); push @tail, $err_log; my $pid = fork; if ($pid == 0) { open STDOUT, '>&', \*STDERR; exec @tail; die "exec(@tail): $!"; } say "# @tail"; sleep 0.2; UnicornTest::AutoReap->new($pid); } sub kill_until_dead ($;%) { my ($pid, %opt) = @_; my $tries = $opt{tries} // 1000; my $sig = $opt{sig} // 0; while (CORE::kill($sig, $pid) && --$tries) { sleep(0.01) } $tries or croak "PID: $pid died after signal ($sig)"; } sub stop_daemon (;$) { my ($is_END) = @_; kill('TERM', $daemon_pid); kill_until_dead $daemon_pid; if ($is_END && CORE::kill(0, $daemon_pid)) { # after done_testing CORE::kill('KILL', $daemon_pid); die "daemon_pid=$daemon_pid did not die"; } else { ok(!CORE::kill(0, $daemon_pid), 'daemonized unicorn gone'); undef $daemon_pid; } }; END { diag slurp($err_log) if $tmpdir; stop_daemon(1) if defined $daemon_pid; }; sub check_stderr (@) { my @log = @_; slurp($err_log) if !@log; diag("@log") if $ENV{V}; my @err = grep(!/NameError.*Unicorn::Waiter/, grep(/error/i, @log)); @err = grep(!/failed to set accept_filter=/, @err); @err = grep(!/perhaps accf_.*? needs to be loaded/, @err); is_deeply(\@err, [], 'no unexpected errors in stderr'); is_deeply([grep(/SIGKILL/, @log)], [], 'no SIGKILL in stderr'); } sub slurp_hdr { my ($c) = @_; local $/ = "\r\n\r\n"; # affects both readline+chomp chomp(my $hdr = readline($c)); my ($status, @hdr) = split(/\r\n/, $hdr); diag explain([ $status, \@hdr ]) if $ENV{V}; ($status, \@hdr); } sub unix_server (;$@) { my $l = shift // $u_sock; IO::Socket::UNIX->new(Listen => SOMAXCONN, Local => $l, Blocking => 0, Type => SOCK_STREAM, @_); } sub unix_connect ($) { IO::Socket::UNIX->new(Peer => $_[0], Type => SOCK_STREAM); } sub tcp_server { my %opt = ( ReuseAddr => 1, Proto => 'tcp', Type => SOCK_STREAM, Listen => SOMAXCONN, Blocking => 0, @_, ); eval { die 'IPv4-only' if $ENV{TEST_IPV4_ONLY}; require IO::Socket::INET6; IO::Socket::INET6->new(%opt, LocalAddr => '[::1]') } || eval { die 'IPv6-only' if $ENV{TEST_IPV6_ONLY}; IO::Socket::INET->new(%opt, LocalAddr => '127.0.0.1') } || BAIL_OUT "failed to create TCP server: $! ($@)"; } sub tcp_host_port { my ($s) = @_; my ($h, $p) = ($s->sockhost, $s->sockport); my $ipv4 = $s->sockdomain == AF_INET; if (wantarray) { $ipv4 ? ($h, $p) : ("[$h]", $p); } else { $ipv4 ? "$h:$p" : "[$h]:$p"; } } sub unix_start ($@) { my ($dst, @req) = @_; my $s = unix_connect($dst) or BAIL_OUT "unix connect $dst: $!"; $s->autoflush(1); print $s @req, "\r\n\r\n" if @req; $s; } sub tcp_start ($@) { my ($dst, @req) = @_; my $addr = tcp_host_port($dst); my $s = ref($dst)->new( Proto => 'tcp', Type => SOCK_STREAM, PeerAddr => $addr, ) or BAIL_OUT "failed to connect to $addr: $!"; $s->autoflush(1); print $s @req, "\r\n\r\n" if @req; $s; } sub slurp { open my $fh, '<', $_[0]; local $/ if !wantarray; readline($fh); } sub spawn { my $env = ref($_[0]) eq 'HASH' ? shift : undef; my $opt = ref($_[-1]) eq 'HASH' ? pop : {}; my @cmd = @_; my $old = POSIX::SigSet->new; my $set = POSIX::SigSet->new; $set->fillset or die "sigfillset: $!"; sigprocmask(SIG_SETMASK, $set, $old) or die "SIG_SETMASK: $!"; pipe(my $r, my $w); my $pid = fork; if ($pid == 0) { close $r; $SIG{__DIE__} = sub { warn(@_); syswrite($w, my $num = $! + 0); _exit(1); }; # pretend to be systemd (cf. sd_listen_fds(3)) my $cfd; for ($cfd = 0; ($cfd < 3) || defined($opt->{$cfd}); $cfd++) { my $io = $opt->{$cfd} // next; my $pfd = fileno($io); if ($pfd == $cfd) { fcntl($io, F_SETFD, 0); } else { dup2($pfd, $cfd) // die "dup2($pfd, $cfd): $!"; } } if (($cfd - 3) > 0) { $env->{LISTEN_PID} = $$; $env->{LISTEN_FDS} = $cfd - 3; } if (defined(my $pgid = $opt->{pgid})) { setpgid(0, $pgid) // die "setpgid(0, $pgid): $!"; } $SIG{$_} = 'DEFAULT' for grep(!/^__/, keys %SIG); if (defined(my $cd = $opt->{-C})) { chdir $cd } $old->delset(POSIX::SIGCHLD) or die "sigdelset CHLD: $!"; sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK: ~CHLD: $!"; @ENV{keys %$env} = values(%$env) if $env; exec { $cmd[0] } @cmd; die "exec @cmd: $!"; } close $w; sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK(old): $!"; if (my $cerrnum = do { local $/, <$r> }) { $! = $cerrnum; die "@cmd PID=$pid died: $!"; } $pid; } sub which { my ($file) = @_; return $file if index($file, '/') >= 0; for my $p (split(/:/, $ENV{PATH})) { $p .= "/$file"; return $p if -x $p; } undef; } # returns an AutoReap object sub unicorn { my %env; if (ref($_[0]) eq 'HASH') { my $e = shift; %env = %$e; } my @args = @_; push(@args, {}) if ref($args[-1]) ne 'HASH'; $args[-1]->{2} //= $errfh; # stderr default state $ruby = which($ENV{RUBY} // 'ruby'); state $lib = File::Spec->rel2abs('lib'); state $ver = $ENV{TEST_RUBY_VERSION} // `$ruby -e 'print RUBY_VERSION'`; state $eng = $ENV{TEST_RUBY_ENGINE} // `$ruby -e 'print RUBY_ENGINE'`; state $ext = File::Spec->rel2abs("test/$eng-$ver/ext/unicorn_http"); state $exe = File::Spec->rel2abs("test/$eng-$ver/bin/unicorn"); state $rl = $ENV{RUBYLIB} ? "$lib:$ext:$ENV{RUBYLIB}" : "$lib:$ext"; $env{RUBYLIB} = $rl; my $pid = spawn(\%env, $ruby, $exe, @args); UnicornTest::AutoReap->new($pid); } sub do_req ($@) { my ($dst, @req) = @_; my $c = ref($dst) ? tcp_start($dst, @req) : unix_start($dst, @req); return $c if !wantarray; my ($status, $hdr); # read headers iff HTTP/1.x request, HTTP/0.9 remains supported my ($first) = (join('', @req) =~ m!\A([^\r\n]+)!); ($status, $hdr) = slurp_hdr($c) if $first =~ m{\s*HTTP/\S+$}; my $bdy = do { local $/; <$c> }; close $c; ($status, $hdr, $bdy); } sub mkfifo_die ($;$) { POSIX::mkfifo($_[0], $_[1] // 0600) or croak "mkfifo: $!"; } sub write_file ($$@) { # mode, filename, LIST (for print) open(my $fh, shift, shift); print $fh @_; # return $fh for futher writes if user wants it: defined(wantarray) && !wantarray ? $fh : close $fh; } # automatically kill + reap children when this goes out-of-scope package UnicornTest::AutoReap; use v5.14; use autodie; sub new { my (undef, $pid) = @_; bless { pid => $pid, owner => $$ }, __PACKAGE__ } sub do_kill { my ($self, $sig) = @_; kill($sig // 'TERM', $self->{pid}); } sub join { my ($self, $sig) = @_; my $pid = delete $self->{pid} or return; kill($sig, $pid) if defined $sig; my $ret = waitpid($pid, 0); $ret == $pid or die "BUG: waitpid($pid) != $ret"; } sub DESTROY { my ($self) = @_; return if $self->{owner} != $$; $self->join('TERM'); } package main; # inject ourselves into the t/*.t script UnicornTest->import; Test::More->import; # try to ensure ->DESTROY fires: $SIG{TERM} = sub { exit(15 + 128) }; $SIG{INT} = sub { exit(2 + 128) }; $SIG{PIPE} = sub { exit(13 + 128) }; 1; t/listener_names.ru000066400000000000000000000003101471646016400147040ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, "text/plain" names = Unicorn.listener_names.inspect # rely on preload_app=true run(lambda { |_| [ 200, {}, [ names ] ] }) t/my-tap-lib.sh000066400000000000000000000106451471646016400136470ustar00rootroot00000000000000#!/bin/sh # Copyright (c) 2009, 2010 Eric Wong # # TAP-producing shell library for POSIX-compliant Bourne shells We do # not _rely_ on Bourne Again features, though we will use "set -o # pipefail" from ksh93 or bash 3 if available # # Only generic, non-project/non-language-specific stuff goes here. We # only have POSIX dependencies for the core tests (without --verbose), # though we'll enable useful non-POSIX things if they're available. # # This test library is intentionally unforgiving, it does not support # skipping tests nor continuing after any failure. Any failures # immediately halt execution as do any references to undefined # variables. # # When --verbose is specified, we always prefix stdout/stderr # output with "#" to avoid confusing TAP consumers. Otherwise # the normal stdout/stderr streams are redirected to /dev/null # dup normal stdout(fd=1) and stderr (fd=2) to fd=3 and fd=4 respectively # normal TAP output goes to fd=3, nothing should go to fd=4 exec 3>&1 4>&2 # ensure a sane environment TZ=UTC LC_ALL=C LANG=C export LANG LC_ALL TZ unset CDPATH # pipefail is non-POSIX, but very useful in ksh93/bash ( set -o pipefail 2>/dev/null ) && set -o pipefail SED=${SED-sed} # Unlike other test frameworks, we are unforgiving and bail immediately # on any failures. We do this because we're lazy about error handling # and also because we believe anything broken should not be allowed to # propagate throughout the rest of the test set -e set -u # name of our test T=${0##*/} t_expect_nr=-1 t_nr=0 t_current= t_complete=false # list of files to remove unconditionally on exit T_RM_LIST= # list of files to remove only on successful exit T_OK_RM_LIST= # emit output to stdout, it'll be parsed by the TAP consumer # so it must be TAP-compliant output t_echo () { echo >&3 "$@" } # emits non-parsed information to stdout, it will be prefixed with a '#' # to not throw off TAP consumers t_info () { t_echo '#' "$@" } # exit with an error and print a diagnostic die () { echo >&2 "$@" exit 1 } # our at_exit handler, it'll fire for all exits except SIGKILL (unavoidable) t_at_exit () { code=$? set +e if test $code -eq 0 then $t_complete || { t_info "t_done not called" code=1 } elif test -n "$t_current" then t_echo "not ok $t_nr - $t_current" fi if test $t_expect_nr -ne -1 then test $t_expect_nr -eq $t_nr || { t_info "planned $t_expect_nr tests but ran $t_nr" test $code -ne 0 || code=1 } fi $t_complete || { t_info "unexpected test failure" test $code -ne 0 || code=1 } rm -f $T_RM_LIST test $code -eq 0 && rm -f $T_OK_RM_LIST set +x exec >&3 2>&4 t_close_fds exit $code } # close test-specific extra file descriptors t_close_fds () { exec 3>&- 4>&- } # call this at the start of your test to specify the number of tests # you plan to run t_plan () { test "$1" -ge 1 || die "must plan at least one test" test $t_expect_nr -eq -1 || die "tried to plan twice in one test" t_expect_nr=$1 shift t_echo 1..$t_expect_nr "#" "$@" trap t_at_exit EXIT } _t_checkup () { test $t_expect_nr -le 0 && die "no tests planned" test -n "$t_current" && t_echo "ok $t_nr - $t_current" true } # finalizes any previously test and starts a new one t_begin () { _t_checkup t_nr=$(( $t_nr + 1 )) t_current="$1" # just in case somebody wanted to cheat us: set -e } # finalizes the current test without starting a new one t_end () { _t_checkup t_current= } # run this to signify the end of your test t_done () { _t_checkup t_current= t_complete=true test $t_expect_nr -eq $t_nr || exit 1 exit 0 } # create and assign named-pipes to variable _names_ passed to this function t_fifos () { for _id in "$@" do _name=$_id _tmp=$(mktemp -t $T.$$.$_id.XXXXXXXX) eval "$_id=$_tmp" rm -f $_tmp mkfifo $_tmp T_RM_LIST="$T_RM_LIST $_tmp" done } t_verbose=false t_trace=false while test "$#" -ne 0 do arg="$1" shift case $arg in -v|--verbose) t_verbose=true ;; --trace) t_trace=true t_verbose=true ;; *) die "Unknown option: $arg" ;; esac done # we always only setup stdout, nothing should end up in the "real" stderr if $t_verbose then if test x"$(which mktemp 2>/dev/null)" = x then die "mktemp(1) not available for --verbose" fi t_fifos t_stdout t_stderr ( # use a subshell so seds are not waitable $SED -e 's/^/#: /' < $t_stdout & $SED -e 's/^/#! /' < $t_stderr & ) & wait exec > $t_stdout 2> $t_stderr else exec > /dev/null 2> /dev/null fi $t_trace && set -x true t/oob_gc.ru000066400000000000000000000005731471646016400131370ustar00rootroot00000000000000#\-E none # frozen_string_literal: false require 'unicorn/oob_gc' use Rack::ContentLength use Rack::ContentType, "text/plain" use Unicorn::OobGC $gc_started = false # Mock GC.start def GC.start $gc_started = true end run lambda { |env| if "/gc_reset" == env["PATH_INFO"] && "POST" == env["REQUEST_METHOD"] $gc_started = false end [ 200, {}, [ "#$gc_started\n" ] ] } t/oob_gc_path.ru000066400000000000000000000006051471646016400141470ustar00rootroot00000000000000#\-E none # frozen_string_literal: false require 'unicorn/oob_gc' use Rack::ContentLength use Rack::ContentType, "text/plain" use Unicorn::OobGC, 5, /BAD/ $gc_started = false # Mock GC.start def GC.start $gc_started = true end run lambda { |env| if "/gc_reset" == env["PATH_INFO"] && "POST" == env["REQUEST_METHOD"] $gc_started = false end [ 200, {}, [ "#$gc_started\n" ] ] } t/pid.ru000066400000000000000000000002111471646016400124500ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, "text/plain" run lambda { |env| [ 200, {}, [ "#$$\n" ] ] } t/preread_input.ru000066400000000000000000000012521471646016400145430ustar00rootroot00000000000000#\-E none # frozen_string_literal: false require 'digest/md5' require 'unicorn/preread_input' use Unicorn::PrereadInput nr = 0 run lambda { |env| $stderr.write "app dispatch: #{nr += 1}\n" input = env["rack.input"] dig = Digest::MD5.new if buf = input.read(16384) begin dig.update(buf) end while input.read(16384, buf) buf.clear # remove this call if Ruby ever gets escape analysis end if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']] cmd5_bin = cmd5_b64.unpack('m')[0] return [500, {}, [ cmd5_b64 ] ] if cmd5_bin != dig.digest end [ 200, {}, [ dig.hexdigest ] ] } t/reload-bad-config.t000066400000000000000000000026571471646016400147660ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; my $srv = tcp_server(); my $host_port = tcp_host_port($srv); my $ru = "$tmpdir/config.ru"; write_file '>', $ru, <<'EOM'; use Rack::ContentLength use Rack::ContentType, 'text/plain' config = ru = "hello world\n" # check for config variable conflicts, too run lambda { |env| [ 200, {}, [ ru.to_s ] ] } EOM write_file '>', $u_conf, < $srv }); my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid at start'); is($bdy, "hello world\n", 'body matches expected'); write_file '>>', $ru, <<'EOM'; ....this better be a syntax error in any version of ruby... EOM $ar->do_kill('HUP'); # reload my @l; for (1..1000) { @l = grep(/(?:done|error) reloading/, slurp($err_log)) and last; sleep 0.011; } diag slurp($err_log) if $ENV{V}; ok(grep(/error reloading/, @l), 'got error reloading'); open my $fh, '>', $err_log; # truncate close $fh; ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0'); like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid afte reload'); is($bdy, "hello world\n", 'body matches expected after reload'); check_stderr; undef $tmpdir; # quiet t/lib.perl END{} done_testing; t/reopen-logs.ru000066400000000000000000000005231471646016400141340ustar00rootroot00000000000000# frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, "text/plain" run lambda { |env| # our File objects for stderr/stdout should always have #path # and be sync=true ok = $stderr.sync && $stdout.sync && String === $stderr.path && String === $stdout.path [ 200, {}, [ "#{ok}\n" ] ] } t/reopen-logs.t000066400000000000000000000020111471646016400137430ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; my $srv = tcp_server(); my $out_log = "$tmpdir/out.log"; write_file '>', $u_conf, < $srv } ); my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0'); is($bdy, "true\n", 'logs opened'); rename($err_log, "$err_log.rot"); rename($out_log, "$out_log.rot"); $auto_reap->do_kill('USR1'); my $tries = 1000; while (!-f $err_log && --$tries) { sleep 0.01 }; while (!-f $out_log && --$tries) { sleep 0.01 }; ok(-f $out_log, 'stdout_path recreated after USR1'); ok(-f $err_log, 'stderr_path recreated after USR1'); ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0'); is($bdy, "true\n", 'logs reopened with sync==true'); $auto_reap->join('QUIT'); is($?, 0, 'no error on exit'); check_stderr; undef $tmpdir; done_testing; t/t0010-reap-logging.sh000077500000000000000000000017751471646016400150200ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 9 "reap worker logging messages" t_begin "setup and start" && { unicorn_setup cat >> $unicorn_config < $r_err } t_begin "kill 2nd worker gracefully" && { pid_2=$(curl http://$listen/) kill -QUIT $pid_2 } t_begin "wait for 3rd worker=0 to start " && { test '.' = $(cat $fifo) } t_begin "ensure log of 2nd reap is a INFO" && { grep 'INFO.*reaped.*worker=0' $r_err | grep $pid_2 > $r_err } t_begin "killing succeeds" && { kill $unicorn_pid wait kill -0 $unicorn_pid && false } t_begin "check stderr" && { check_stderr } t_done t/t0012-reload-empty-config.sh000077500000000000000000000036131471646016400163050ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 9 "reloading unset config resets defaults" t_begin "setup and start" && { unicorn_setup rtmpfiles unicorn_config_orig before_reload after_reload cat $unicorn_config > $unicorn_config_orig cat >> $unicorn_config < $tmp } t_begin "replace config file with original(-ish)" && { grep -v ^pid < $unicorn_config_orig > $unicorn_config cat >> $unicorn_config </dev/null do sleep 1 done while ! grep reaped < $r_err >/dev/null do sleep 1 done grep 'done reloading' $r_err >/dev/null } t_begin "ensure worker is started" && { curl -sSf http://$listen/ > $tmp } t_begin "pid file no longer exists" && { if test -f $pid then die "pid=$pid should not exist" fi } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "check stderr" && { check_stderr } t_begin "ensure reloading restored settings" && { awk < $after_reload -F'|' ' $1 != "before_fork" && $2 != $3 { print $0; exit(1) } ' } t_done t/t0013-rewindable-input-false.sh000077500000000000000000000006741471646016400170060ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 4 "rewindable_input toggled to false" t_begin "setup and start" && { unicorn_setup echo rewindable_input false >> $unicorn_config unicorn -D -c $unicorn_config t0013.ru unicorn_wait_start } t_begin "ensure worker is started" && { test xOK = x$(curl -T t0013.ru -H Expect: -vsSf http://$listen/) } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "check stderr" && { check_stderr } t_done t/t0013.ru000066400000000000000000000003721471646016400124530ustar00rootroot00000000000000#\ -E none # frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, 'text/plain' app = lambda do |env| case env['rack.input'] when Unicorn::StreamInput [ 200, {}, %w(OK) ] else [ 500, {}, %w(NO) ] end end run app t/t0014-rewindable-input-true.sh000077500000000000000000000006561471646016400166740ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 4 "rewindable_input toggled to true" t_begin "setup and start" && { unicorn_setup echo rewindable_input true >> $unicorn_config unicorn -D -c $unicorn_config t0014.ru unicorn_wait_start } t_begin "ensure worker is started" && { test xOK = x$(curl -T t0014.ru -sSf http://$listen/) } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "check stderr" && { check_stderr } t_done t/t0014.ru000066400000000000000000000003671471646016400124600ustar00rootroot00000000000000#\ -E none # frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, 'text/plain' app = lambda do |env| case env['rack.input'] when Unicorn::TeeInput [ 200, {}, %w(OK) ] else [ 500, {}, %w(NO) ] end end run app t/t0015-configurator-internals.sh000077500000000000000000000010231471646016400171330ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 4 "configurator internals tests (from FAQ)" t_begin "setup and start" && { unicorn_setup cat >> $unicorn_config <"https"' } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "no errors" && check_stderr t_done t/t0020-at_exit-handler.sh000077500000000000000000000021131471646016400155010ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 5 "at_exit/END handlers work as expected" t_begin "setup and startup" && { unicorn_setup cat >> $unicorn_config </dev/null 2>&1 do sleep 1 done } t_begin "check stderr" && check_stderr dbgcat r_err dbgcat r_out t_begin "all at_exit handlers ran" && { grep "$worker_pid BOTH" $r_out grep "$unicorn_pid BOTH" $r_out grep "$worker_pid END BOTH" $r_out grep "$unicorn_pid END BOTH" $r_out grep "$worker_pid WORKER ONLY" $r_out grep "$worker_pid END WORKER ONLY" $r_out } t_done t/t0021-process_detach.sh000077500000000000000000000011011471646016400154140ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 5 "Process.detach on forked background process works" t_begin "setup and startup" && { t_fifos process_detach unicorn_setup TEST_FIFO=$process_detach \ unicorn -E none -D detach.ru -c $unicorn_config unicorn_wait_start } t_begin "read detached PID with HTTP/1.0" && { detached_pid=$(curl -0 -sSf http://$listen/) t_info "detached_pid=$detached_pid" } t_begin "read background FIFO" && { test xHIHI = x"$(cat $process_detach)" } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "check stderr" && check_stderr t_done t/t0022-listener_names-preload_app.sh000066400000000000000000000012371471646016400177320ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh # Raindrops::Middleware depends on Unicorn.listener_names, # ensure we don't break Raindrops::Middleware when preload_app is true t_plan 4 "Unicorn.listener_names available with preload_app=true" t_begin "setup and startup" && { unicorn_setup echo preload_app true >> $unicorn_config unicorn -E none -D listener_names.ru -c $unicorn_config unicorn_wait_start } t_begin "read listener names includes listener" && { resp=$(curl -sSf http://$listen/) ok=false t_info "resp=$resp" case $resp in *\"$listen\"*) ok=true ;; esac $ok } t_begin "killing succeeds" && { kill $unicorn_pid } t_begin "check stderr" && check_stderr t_done t/t0300-no-default-middleware.sh000066400000000000000000000006411471646016400166040ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 3 "test the -N / --no-default-middleware option" t_begin "setup and start" && { unicorn_setup unicorn -N -D -c $unicorn_config fails-rack-lint.ru unicorn_wait_start } t_begin "check exit status with Rack::Lint not present" && { test 500 -ne "$(curl -sf -o/dev/null -w'%{http_code}' http://$listen/)" } t_begin "killing succeeds" && { kill $unicorn_pid check_stderr } t_done t/t0301-no-default-middleware-ignored-in-config.sh000077500000000000000000000007751471646016400221140ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 3 "-N / --no-default-middleware option not supported in config.ru" t_begin "setup and start" && { unicorn_setup RACK_ENV=development unicorn -D -c $unicorn_config t0301.ru unicorn_wait_start } t_begin "check switches parsed as expected and -N ignored for Rack::Lint" && { debug=false lint= eval "$(curl -sf http://$listen/vars)" test x"$debug" = xtrue test x"$lint" != x test -f "$lint" } t_begin "killing succeeds" && { kill $unicorn_pid check_stderr } t_done t/t0301.ru000066400000000000000000000005051471646016400124510ustar00rootroot00000000000000#\-N --debug # frozen_string_literal: false run(lambda do |env| case env['PATH_INFO'] when '/vars' b = "debug=#{$DEBUG.inspect}\n" \ "lint=#{caller.grep(%r{rack/lint\.rb})[0].split(':')[0]}\n" end h = { 'content-length' => b.size.to_s, 'content-type' => 'text/plain', } [ 200, h, [ b ] ] end) t/t9001-oob_gc.sh000077500000000000000000000022511471646016400136740ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 9 "OobGC test" t_begin "setup and start" && { unicorn_setup unicorn -D -c $unicorn_config oob_gc.ru unicorn_wait_start } t_begin "test default interval (4 requests)" && { test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "GC starting-request returns immediately" && { test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "GC is started after 5 requests" && { test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "reset GC" && { test xfalse = x$(curl -vsSf -X POST http://$listen/gc_reset 2>> $tmp) } t_begin "test default interval again (3 requests)" && { test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "GC is started after 5 requests" && { test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "killing succeeds" && { kill -QUIT $unicorn_pid } t_begin "check_stderr" && check_stderr dbgcat r_err t_done t/t9002-oob_gc-path.sh000077500000000000000000000046321471646016400146340ustar00rootroot00000000000000#!/bin/sh . ./test-lib.sh t_plan 12 "OobGC test with limited path" t_begin "setup and start" && { unicorn_setup unicorn -D -c $unicorn_config oob_gc_path.ru unicorn_wait_start } t_begin "test default is noop" && { test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "4 bad requests to bump counter" && { test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) } t_begin "GC-starting request returns immediately" && { test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) } t_begin "GC was started after 5 requests" && { test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "reset GC" && { test xfalse = x$(curl -vsSf -X POST http://$listen/gc_reset 2>> $tmp) } t_begin "test default is noop" && { test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "4 bad requests to bump counter" && { test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) } t_begin "GC-starting request returns immediately" && { test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp) } t_begin "GC was started after 5 requests" && { test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp) } t_begin "killing succeeds" && { kill -QUIT $unicorn_pid } t_begin "check_stderr" && check_stderr t_done t/test-lib.sh000066400000000000000000000047541471646016400134230ustar00rootroot00000000000000#!/bin/sh # Copyright (c) 2009 Rainbows! hackers # Copyright (c) 2010 Unicorn hackers . ./my-tap-lib.sh set +u # sometimes we rely on http_proxy to avoid wasting bandwidth with Isolate # and multiple Ruby versions NO_PROXY=${UNICORN_TEST_ADDR-127.0.0.1} export NO_PROXY set -e RUBY="${RUBY-ruby}" RUBY_VERSION=${RUBY_VERSION-$($RUBY -e 'puts RUBY_VERSION')} RUBY_ENGINE=${RUBY_ENGINE-$($RUBY -e 'puts((RUBY_ENGINE rescue "ruby"))')} t_pfx=$PWD/trash/$T-$RUBY_ENGINE-$RUBY_VERSION set -u PATH=$PWD/bin:$PATH export PATH test -x $PWD/bin/unused_listen || die "must be run in 't' directory" wait_for_pid () { path="$1" nr=30 while ! test -s "$path" && test $nr -gt 0 do nr=$(($nr - 1)) sleep 1 done } # "unix_time" is not in POSIX, but in GNU, and FreeBSD 9.0 (possibly earlier) unix_time () { $RUBY -e 'puts Time.now.to_i' } # "wc -l" outputs leading whitespace on *BSDs, filter it out for portability count_lines () { wc -l | tr -d '[:space:]' } # "wc -c" outputs leading whitespace on *BSDs, filter it out for portability count_bytes () { wc -c | tr -d '[:space:]' } # given a list of variable names, create temporary files and assign # the pathnames to those variables rtmpfiles () { for id in "$@" do name=$id case $name in *fifo) _tmp=$t_pfx.$id eval "$id=$_tmp" rm -f $_tmp mkfifo $_tmp T_RM_LIST="$T_RM_LIST $_tmp" ;; *socket) _tmp="$(mktemp -t $id.$$.XXXXXXXX)" if test $(printf "$_tmp" |count_bytes) -gt 108 then echo >&2 "$_tmp too long, tests may fail" echo >&2 "Try to set TMPDIR to a shorter path" fi eval "$id=$_tmp" rm -f $_tmp T_RM_LIST="$T_RM_LIST $_tmp" ;; *) _tmp=$t_pfx.$id eval "$id=$_tmp" > $_tmp T_OK_RM_LIST="$T_OK_RM_LIST $_tmp" ;; esac done } dbgcat () { id=$1 eval '_file=$'$id echo "==> $id <==" sed -e "s/^/$id:/" < $_file } check_stderr () { set +u _r_err=${1-${r_err}} set -u if grep -v $T $_r_err | grep -i Error | \ grep -v NameError.*Unicorn::Waiter then die "Errors found in $_r_err" elif grep SIGKILL $_r_err then die "SIGKILL found in $_r_err" fi } # unicorn_setup unicorn_setup () { eval $(unused_listen) port=$(expr $listen : '[^:]*:\([0-9]*\)') host=$(expr $listen : '\([^:][^:]*\):[0-9][0-9]*') rtmpfiles unicorn_config pid r_err r_out fifo tmp ok cat > $unicorn_config < # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; use POSIX qw(mkfifo); my $u_sock = "$tmpdir/u.sock"; my $fifo = "$tmpdir/fifo"; mkfifo($fifo, 0666) or die "mkfifo($fifo): $!"; write_file '>', $u_conf, <join; is($?, 0, 'daemonized properly'); open my $fh, '<', "$tmpdir/pid"; chomp(my $pid = <$fh>); ok(kill(0, $pid), 'daemonized PID works'); my $quit = sub { kill('QUIT', $pid) if $pid; $pid = undef }; END { $quit->() }; open $fh, '<', $fifo; my $worker_nr = <$fh>; close $fh; is($worker_nr, '0', 'initial worker spawned'); my ($status, $hdr, $worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0'); like($status, qr/ 200\b/, 'got 200 response'); like($worker_pid, qr/\A[0-9]+\n\z/s, 'PID in response'); chomp $worker_pid; ok(kill(0, $worker_pid), 'worker_pid is valid'); ok(kill('WINCH', $pid), 'SIGWINCH can be sent'); my $tries = 1000; while (CORE::kill(0, $worker_pid) && --$tries) { sleep 0.01 } ok(!CORE::kill(0, $worker_pid), 'worker not running'); ok(kill('TTIN', $pid), 'SIGTTIN to restart worker'); open $fh, '<', $fifo; $worker_nr = <$fh>; close $fh; is($worker_nr, '0', 'worker restarted'); ($status, $hdr, my $new_worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0'); like($status, qr/ 200\b/, 'got 200 response'); like($new_worker_pid, qr/\A[0-9]+\n\z/, 'got new worker PID'); chomp $new_worker_pid; ok(kill(0, $new_worker_pid), 'got a valid worker PID'); isnt($worker_pid, $new_worker_pid, 'worker PID changed'); $quit->(); check_stderr; undef $tmpdir; done_testing; t/working_directory.t000066400000000000000000000044001471646016400152610ustar00rootroot00000000000000#!perl -w # Copyright (C) unicorn hackers # License: GPL-3.0+ use v5.14; BEGIN { require './t/lib.perl' }; use autodie; mkdir "$tmpdir/alt"; my $ru = "$tmpdir/alt/config.ru"; write_file '>', $u_conf, <', $ru, <join; # will daemonize chomp($daemon_pid = slurp($pid_file)); my ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0'); is($bdy, "1\n", 'got expected $master_ppid'); stop_daemon; check_stderr; if ('test without CLI switches in config.ru') { truncate $err_log, 0; write_file '>', $ru, $common_ru; unicorn('-D', '-l', $u_sock, '-c', $u_conf)->join; # will daemonize chomp($daemon_pid = slurp($pid_file)); ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0'); is($bdy, "1\n", 'got expected $master_ppid'); stop_daemon; check_stderr; } if ('ensures broken working_directory (missing config.ru) is OK') { truncate $err_log, 0; unlink $ru; my $auto_reap = unicorn('-c', $u_conf); $auto_reap->join; isnt($?, 0, 'exited with error due to missing config.ru'); like(slurp($err_log), qr/rackup file \Q(config.ru)\E not readable/, 'noted unreadability of config.ru in stderr'); } if ('fooapp.rb (not config.ru) works with working_directory') { truncate $err_log, 0; my $fooapp = "$tmpdir/alt/fooapp.rb"; write_file '>', $fooapp, < 'text/plain', 'content-length' => b.bytesize.to_s } [ 200, h, [ b ] ] end end EOM my $srv = tcp_server; my $auto_reap = unicorn(qw(-c), $u_conf, qw(-I. fooapp.rb), { -C => '/', 3 => $srv }); ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0'); is($bdy, "dir=$tmpdir/alt", 'fooapp.rb (w/o config.ru) w/ working_directory'); $auto_reap->join('TERM'); is($?, 0, 'fooapp.rb process exited'); check_stderr; } undef $tmpdir; done_testing; test/000077500000000000000000000000001471646016400120465ustar00rootroot00000000000000test/aggregate.rb000077500000000000000000000006401471646016400143240ustar00rootroot00000000000000#!/usr/bin/ruby -n # -*- encoding: binary -*- # frozen_string_literal: false BEGIN { $tests = $assertions = $failures = $errors = 0 } $_ =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/ or next $tests += $1.to_i $assertions += $2.to_i $failures += $3.to_i $errors += $4.to_i END { printf("\n%d tests, %d assertions, %d failures, %d errors\n", $tests, $assertions, $failures, $errors) } test/benchmark/000077500000000000000000000000001471646016400140005ustar00rootroot00000000000000test/benchmark/README000066400000000000000000000037361471646016400146710ustar00rootroot00000000000000= Performance Unicorn is pretty fast, and we want it to get faster. Unicorn strives to get HTTP requests to your application and write HTTP responses back as quickly as possible. Unicorn does not do any background processing while your app runs, so your app will get all the CPU time provided to it by your OS kernel. A gentle reminder: Unicorn is NOT for serving clients over slow network connections. Use nginx (or something similar) to complement Unicorn if you have slow clients. == dd.ru This is a pure I/O benchmark. In the context of Unicorn, this is the only one that matters. It is a standard rackup-compatible .ru file and may be used with other Rack-compatible servers. unicorn -E none dd.ru You can change the size and number of chunks in the response with the "bs" and "count" environment variables. The following command will cause dd.ru to return 4 chunks of 16384 bytes each, leading to 65536 byte response: bs=16384 count=4 unicorn -E none dd.ru Or if you want to add logging (small performance impact): unicorn -E deployment dd.ru Eric runs then runs clients on a LAN it in several different ways: client@host1 -> unicorn@host1(tcp) client@host2 -> unicorn@host1(tcp) client@host3 -> nginx@host1 -> unicorn@host1(tcp) client@host3 -> nginx@host1 -> unicorn@host1(unix) client@host3 -> nginx@host2 -> unicorn@host1(tcp) The benchmark client is usually httperf. Another gentle reminder: performance with slow networks/clients is NOT our problem. That is the job of nginx (or similar). == ddstream.ru Standalone Rack app intended to show how BAD we are at slow clients. See usage in comments. == readinput.ru Standalone Rack app intended to show how bad we are with slow uploaders. See usage in comments. == Contributors This directory is intended to remain stable. Do not make changes to benchmarking code which can change performance and invalidate results across revisions. Instead, write new benchmarks and update coments/documentation as necessary. test/benchmark/dd.ru000066400000000000000000000014211471646016400147350ustar00rootroot00000000000000# frozen_string_literal: false # This benchmark is the simplest test of the I/O facilities in # unicorn. It is meant to return a fixed-sized blob to test # the performance of things in Unicorn, _NOT_ the app. # # Adjusting this benchmark is done via the "bs" (byte size) and "count" # environment variables. "count" designates the count of elements of # "bs" length in the Rack response body. The defaults are bs=4096, count=1 # to return one 4096-byte chunk. bs = ENV['bs'] ? ENV['bs'].to_i : 4096 count = ENV['count'] ? ENV['count'].to_i : 1 slice = (' ' * bs).freeze body = (1..count).map { slice }.freeze hdr = { 'Content-Length' => (bs * count).to_s.freeze, 'Content-Type' => 'text/plain'.freeze }.freeze response = [ 200, hdr, body ].freeze run(lambda { |env| response }) test/benchmark/ddstream.ru000066400000000000000000000032661471646016400161620ustar00rootroot00000000000000# frozen_string_literal: false # This app is intended to test large HTTP responses with or without # a fully-buffering reverse proxy such as nginx. Without a fully-buffering # reverse proxy, unicorn will be unresponsive when client count exceeds # worker_processes. # # To demonstrate how bad unicorn is at slowly reading clients: # # # in one terminal, start unicorn with one worker: # unicorn -E none -l 127.0.0.1:8080 test/benchmark/ddstream.ru # # # in a different terminal, start more slow curl processes than # # unicorn workers and watch time outputs # curl --limit-rate 8K --trace-time -vsN http://127.0.0.1:8080/ >/dev/null & # curl --limit-rate 8K --trace-time -vsN http://127.0.0.1:8080/ >/dev/null & # wait # # The last client won't see a response until the first one is done reading # # nginx note: do not change the default "proxy_buffering" behavior. # Setting "proxy_buffering off" prevents nginx from protecting unicorn. # totally standalone rack app to stream a giant response class BigResponse def initialize(bs, count) @buf = "#{bs.to_s(16)}\r\n#{' ' * bs}\r\n" @count = count @res = [ 200, { 'Transfer-Encoding' => -'chunked', 'Content-Type' => 'text/plain' }, self ] end # rack response body iterator def each (1..@count).each { yield @buf } yield -"0\r\n\r\n" end # rack app entry endpoint def call(_env) @res end end # default to a giant (128M) response because kernel socket buffers # can be ridiculously large on some systems bs = ENV['bs'] ? ENV['bs'].to_i : 65536 count = ENV['count'] ? ENV['count'].to_i : 2048 warn "serving response with bs=#{bs} count=#{count} (#{bs*count} bytes)" run BigResponse.new(bs, count) test/benchmark/readinput.ru000066400000000000000000000026661471646016400163550ustar00rootroot00000000000000# frozen_string_literal: false # This app is intended to test large HTTP requests with or without # a fully-buffering reverse proxy such as nginx. Without a fully-buffering # reverse proxy, unicorn will be unresponsive when client count exceeds # worker_processes. DOC = < body.size.to_s, "Content-Type" => "text/plain", } [ 200, h, [ body ] ] }) test/benchmark/uconnect.perl000077500000000000000000000036031471646016400165070ustar00rootroot00000000000000#!/usr/bin/perl -w # Benchmark script to spawn some processes and hammer a local unicorn # to test accept loop performance. This only does Unix sockets. # There's plenty of TCP benchmarking tools out there, and TCP port reuse # has predictability problems since unicorn can't do persistent connections. # Written in Perl for the same reason: predictability. # Ruby GC is not as predictable as Perl refcounting. use strict; use Socket qw(AF_UNIX SOCK_STREAM sockaddr_un); use POSIX qw(:sys_wait_h); use Getopt::Std; # -c / -n switches stolen from ab(1) my $usage = "$0 [-c CONCURRENCY] [-n NUM_REQUESTS] SOCKET_PATH\n"; our $opt_c = 2; our $opt_n = 1000; getopts('c:n:') or die $usage; my $unix_path = shift or die $usage; use constant REQ => "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; use constant REQ_LEN => length(REQ); use constant BUFSIZ => 8192; $^F = 99; # don't waste syscall time with FD_CLOEXEC my %workers; # pid => worker num die "-n $opt_n not evenly divisible by -c $opt_c\n" if $opt_n % $opt_c; my $n_per_worker = $opt_n / $opt_c; my $addr = sockaddr_un($unix_path); for my $num (1..$opt_c) { defined(my $pid = fork) or die "fork failed: $!\n"; if ($pid) { $workers{$pid} = $num; } else { work($n_per_worker); } } reap_worker(0) while scalar keys %workers; exit; sub work { my ($n) = @_; my ($buf, $x); for (1..$n) { socket(S, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; connect(S, $addr) or die "connect: $!"; defined($x = syswrite(S, REQ)) or die "write: $!"; $x == REQ_LEN or die "short write: $x != ".REQ_LEN."\n"; do { $x = sysread(S, $buf, BUFSIZ); unless (defined $x) { next if $!{EINTR}; die "sysread: $!\n"; } } until ($x == 0); } exit 0; } sub reap_worker { my ($flags) = @_; my $pid = waitpid(-1, $flags); return if !defined $pid || $pid <= 0; my $p = delete $workers{$pid} || '(unknown)'; warn("$pid [$p] exited with $?\n") if $?; $p; } test/exec/000077500000000000000000000000001471646016400127725ustar00rootroot00000000000000test/exec/README000066400000000000000000000004361471646016400136550ustar00rootroot00000000000000These tests require the "unicorn" executable script to be installed in PATH and rack being directly "require"-able ("rubygems" will not be loaded for you). The tester is responsible for setting up RUBYLIB and PATH environment variables (or running tests via GNU Make instead of Rake). test/exec/test_exec.rb000066400000000000000000000763611471646016400153170ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Don't add to this file, new tests are in Perl 5. See t/README FLOCK_PATH = File.expand_path(__FILE__) require './test/test_helper' do_test = true $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn" redirect_test_io do do_test = system($unicorn_bin, '-v') end unless do_test warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \ "skipping this test" end unless try_require('rack') warn "Unable to load Rack, skipping this test" do_test = false end class ExecTest < Test::Unit::TestCase trap(:QUIT, 'IGNORE') HI = <<-EOS use Rack::ContentLength run proc { |env| [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] } EOS SHOW_RACK_ENV = <<-EOS use Rack::ContentLength run proc { |env| [ 200, { 'content-type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ] } EOS HELLO = <<-EOS class Hello def call(env) [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] end end EOS COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP) HEAVY_WORKERS = 2 HEAVY_CFG = <<-EOS worker_processes #{HEAVY_WORKERS} timeout 30 logger Logger.new('#{COMMON_TMP.path}') before_fork do |server, worker| server.logger.info "before_fork: worker=\#{worker.nr}" end EOS WORKING_DIRECTORY_CHECK_RU = <<-EOS use Rack::ContentLength run lambda { |env| pwd = ENV['PWD'] a = ::File.stat(pwd) b = ::File.stat(Dir.pwd) if (a.ino == b.ino && a.dev == b.dev) [ 200, { 'content-type' => 'text/plain' }, [ pwd ] ] else [ 404, { 'content-type' => 'text/plain' }, [] ] end } EOS def setup @pwd = Dir.pwd @tmpfile = Tempfile.new('unicorn_exec_test') @tmpdir = @tmpfile.path @tmpfile.close! Dir.mkdir(@tmpdir) Dir.chdir(@tmpdir) @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' @port = unused_port(@addr) @sockets = [] @start_pid = $$ end def teardown return if @start_pid != $$ Dir.chdir(@pwd) FileUtils.rmtree(@tmpdir) @sockets.each { |path| File.unlink(path) rescue nil } loop do Process.kill('-QUIT', 0) begin Process.waitpid(-1, Process::WNOHANG) or break rescue Errno::ECHILD break end end end def test_working_directory_rel_path_config_file other = Tempfile.new('unicorn.wd') File.unlink(other.path) Dir.mkdir(other.path) File.open("config.ru", "wb") do |fp| fp.syswrite WORKING_DIRECTORY_CHECK_RU end FileUtils.cp("config.ru", other.path + "/config.ru") Dir.chdir(@tmpdir) tmp = File.open('unicorn.config', 'wb') tmp.syswrite < 0 end rescue Errno::ENOENT (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry end assert_equal current_pid, File.read(pid_file).to_i tries = DEFAULT_TRIES while File.exist?(old_file) (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break end assert ! File.exist?(old_file), "oldbin=#{old_file} gone" port2 = unused_port(@addr) # fix the bug ucfg.sysseek(0) ucfg.truncate(0) ucfg.syswrite("listen %(#@addr:#@port)\n") ucfg.syswrite("listen %(#@addr:#{port2})\n") ucfg.syswrite("pid %(#{pid_file})\n") Process.kill(:USR2, current_pid) wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i assert_not_equal current_pid, new_pid assert_equal current_pid, File.read(old_file).to_i results = retry_hit(["http://#{@addr}:#{@port}/", "http://#{@addr}:#{port2}/"]) assert_equal String, results[0].class assert_equal String, results[1].class Process.kill(:QUIT, current_pid) Process.kill(:QUIT, new_pid) end def test_broken_reexec_ru File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid_file = "#{@tmpdir}/test.pid" old_file = "#{pid_file}.oldbin" ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("pid %(#{pid_file})\n") ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n") pid = xfork do redirect_test_io do exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_for_file(pid_file) Process.waitpid(pid) Process.kill(:USR2, File.read(pid_file).to_i) wait_for_file(old_file) wait_for_file(pid_file) old_pid = File.read(old_file).to_i Process.kill(:QUIT, old_pid) wait_for_death(old_pid) File.unlink("config.ru") # break reloading current_pid = File.read(pid_file).to_i Process.kill(:USR2, current_pid) # wait for pid_file to restore itself tries = DEFAULT_TRIES begin while current_pid != File.read(pid_file).to_i sleep(DEFAULT_RES) and (tries -= 1) > 0 end rescue Errno::ENOENT (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry end tries = DEFAULT_TRIES while File.exist?(old_file) (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break end assert ! File.exist?(old_file), "oldbin=#{old_file} gone" assert_equal current_pid, File.read(pid_file).to_i # fix the bug File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } Process.kill(:USR2, current_pid) wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i assert_not_equal current_pid, new_pid assert_equal current_pid, File.read(old_file).to_i results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class Process.kill(:QUIT, current_pid) Process.kill(:QUIT, new_pid) end def test_unicorn_config_listener_swap port_cli = unused_port File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen '#@addr:#@port'\n") pid = xfork do redirect_test_io do exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}") end end results = retry_hit(["http://#@addr:#{port_cli}/"]) assert_equal String, results[0].class results = retry_hit(["http://#@addr:#@port/"]) assert_equal String, results[0].class port2 = unused_port(@addr) ucfg.sysseek(0) ucfg.truncate(0) ucfg.syswrite("listen '#@addr:#{port2}'\n") Process.kill(:HUP, pid) results = retry_hit(["http://#@addr:#{port2}/"]) assert_equal String, results[0].class results = retry_hit(["http://#@addr:#{port_cli}/"]) assert_equal String, results[0].class reuse = TCPServer.new(@addr, @port) reuse.close assert_shutdown(pid) end def test_unicorn_config_listen_with_options File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n") ucfg.syswrite(" :rcvbuf => 4096,\n") ucfg.syswrite(" :sndbuf => 4096\n") pid = xfork do redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class assert_shutdown(pid) end def test_unicorn_config_per_worker_listen port2 = unused_port pid_spit = 'use Rack::ContentLength;' \ 'run proc { |e| [ 200, {"content-type"=>"text/plain"}, ["#$$\\n"] ] }' File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) } tmp = Tempfile.new('test.socket') File.unlink(tmp.path) ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen '#@addr:#@port'\n") ucfg.syswrite("after_fork { |s,w|\n") ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n") ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n") ucfg.syswrite("\n}\n") pid = xfork do redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class worker_pid = results[0].to_i assert_not_equal pid, worker_pid s = unix_socket(tmp.path) s.syswrite("GET / HTTP/1.0\r\n\r\n") results = '' loop { results << s.sysread(4096) } rescue nil s.close assert_equal worker_pid, results.split(/\r\n/).last.to_i results = hit(["http://#@addr:#{port2}/"]) assert_equal String, results[0].class assert_equal worker_pid, results[0].to_i assert_shutdown(pid) end def test_unicorn_config_listen_augments_cli port2 = unused_port(@addr) File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen '#{@addr}:#{@port}'\n") pid = xfork do redirect_test_io do exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}") end end uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" } results = retry_hit(uris) assert_equal results.size, uris.size assert_equal String, results[0].class assert_equal String, results[1].class assert_shutdown(pid) end def test_weird_config_settings File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } ucfg = Tempfile.new('unicorn_test_config') proc_total = HEAVY_WORKERS + 1 # + 1 for master ucfg.syswrite(HEAVY_CFG) pid = xfork do redirect_test_io do exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}") end end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_master_ready(COMMON_TMP.path) wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS) bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/) assert_equal HEAVY_WORKERS, bf.size rotate = Tempfile.new('unicorn_rotate') File.rename(COMMON_TMP.path, rotate.path) Process.kill(:USR1, pid) wait_for_file(COMMON_TMP.path) assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists" # USR1 should've been passed to all workers tries = DEFAULT_TRIES log = File.readlines(rotate.path) while (tries -= 1) > 0 && log.grep(/reopening logs\.\.\./).size < proc_total sleep DEFAULT_RES log = File.readlines(rotate.path) end assert_equal proc_total, log.grep(/reopening logs\.\.\./).size assert_equal 0, log.grep(/done reopening logs/).size tries = DEFAULT_TRIES log = File.readlines(COMMON_TMP.path) while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total sleep DEFAULT_RES log = File.readlines(COMMON_TMP.path) end assert_equal proc_total, log.grep(/done reopening logs/).size assert_equal 0, log.grep(/reopening logs\.\.\./).size Process.kill(:QUIT, pid) pid, status = Process.waitpid2(pid) assert status.success?, "exited successfully" end def test_read_embedded_cli_switches File.open("config.ru", "wb") do |fp| fp.syswrite("#\\ -p #{@port} -o #{@addr}\n") fp.syswrite(HI) end pid = fork { redirect_test_io { exec($unicorn_bin) } } results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class assert_shutdown(pid) end def test_load_module libdir = "#{@tmpdir}/lib" FileUtils.mkpath([ libdir ]) config_path = "#{libdir}/hello.rb" File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) } pid = fork do redirect_test_io do Dir.chdir("/") exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path) end end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class assert_shutdown(pid) end def test_reexec File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid_file = "#{@tmpdir}/test.pid" pid = fork do redirect_test_io do exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}") end end reexec_basic_test(pid, pid_file) end def test_reexec_alt_config config_file = "#{@tmpdir}/foo.ru" File.open(config_file, "wb") { |fp| fp.syswrite(HI) } pid_file = "#{@tmpdir}/test.pid" pid = fork do redirect_test_io do exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file) end end reexec_basic_test(pid, pid_file) end def test_socket_unlinked_restore results = nil sock = Tempfile.new('unicorn_test_sock') sock_path = sock.path @sockets << sock_path sock.close! ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen \"#{sock_path}\"\n") File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } } wait_for_file(sock_path) assert File.socket?(sock_path) sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) assert_equal String, results.class File.unlink(sock_path) Process.kill(:HUP, pid) wait_for_file(sock_path) assert File.socket?(sock_path) sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) assert_equal String, results.class end def test_unicorn_config_file pid_file = "#{@tmpdir}/test.pid" sock = Tempfile.new('unicorn_test_sock') sock_path = sock.path sock.close! @sockets << sock_path log = Tempfile.new('unicorn_test_log') ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen \"#{sock_path}\"\n") ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{log.path}')\n") ucfg.close File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork do redirect_test_io do exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", "-c#{ucfg.path}") end end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_master_ready(log.path) assert File.exist?(pid_file), "pid_file created" assert_equal pid, File.read(pid_file).to_i assert File.socket?(sock_path), "socket created" sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) assert_equal String, results.class # try reloading the config sock = Tempfile.new('new_test_sock') new_sock_path = sock.path @sockets << new_sock_path sock.close! new_log = Tempfile.new('unicorn_test_log') new_log.sync = true assert_equal 0, new_log.size ucfg = File.open(ucfg.path, "wb") ucfg.syswrite("listen \"#{sock_path}\"\n") ucfg.syswrite("listen \"#{new_sock_path}\"\n") ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{new_log.path}')\n") ucfg.close Process.kill(:HUP, pid) wait_for_file(new_sock_path) assert File.socket?(new_sock_path), "socket exists" @sockets.each do |path| sock = unix_socket(path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) assert_equal String, results.class end assert_not_equal 0, new_log.size reexec_usr2_quit_test(pid, pid_file) end def test_daemonize_reexec pid_file = "#{@tmpdir}/test.pid" log = Tempfile.new('unicorn_test_log') ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{log.path}')\n") ucfg.close File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork do redirect_test_io do exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_for_file(pid_file) new_pid = File.read(pid_file).to_i assert_not_equal pid, new_pid pid, status = Process.waitpid2(pid) assert status.success?, "original process exited successfully" Process.kill(0, new_pid) reexec_usr2_quit_test(new_pid, pid_file) end def test_daemonize_redirect_fail pid_file = "#{@tmpdir}/test.pid" ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("pid #{pid_file}\"\n") err = Tempfile.new('stderr') out = Tempfile.new('stdout ') File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork do $stderr.reopen(err.path, "a") $stdout.reopen(out.path, "a") exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end pid, status = Process.waitpid2(pid) assert ! status.success?, "original process exited successfully" sleep 1 # can't waitpid on a daemonized process :< assert err.stat.size > 0 end def test_reexec_fd_leak unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget... warn "FD leak test only works on Linux at the moment" return end pid_file = "#{@tmpdir}/test.pid" log = Tempfile.new('unicorn_test_log') log.sync = true ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{log.path}')\n") ucfg.syswrite("stderr_path '#{log.path}'\n") ucfg.syswrite("stdout_path '#{log.path}'\n") ucfg.close File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork do redirect_test_io do exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end end wait_master_ready(log.path) wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) orig_pid = pid = File.read(pid_file).to_i orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) assert $?.success? expect_size = orig_fds.size Process.kill(:USR2, pid) wait_for_file("#{pid_file}.oldbin") Process.kill(:QUIT, pid) wait_for_death(pid) wait_master_ready(log.path) wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) pid = File.read(pid_file).to_i assert_not_equal orig_pid, pid curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) assert $?.success? # we could've inherited descriptors the first time around assert expect_size >= curr_fds.size, curr_fds.inspect expect_size = curr_fds.size Process.kill(:USR2, pid) wait_for_file("#{pid_file}.oldbin") Process.kill(:QUIT, pid) wait_for_death(pid) wait_master_ready(log.path) wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) pid = File.read(pid_file).to_i curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) assert $?.success? assert_equal expect_size, curr_fds.size, curr_fds.inspect Process.kill(:QUIT, pid) wait_for_death(pid) end def hup_test_common(preload, check_client=false) File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) } pid_file = Tempfile.new('pid') ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("listen '#@addr:#@port'\n") ucfg.syswrite("pid '#{pid_file.path}'\n") ucfg.syswrite("preload_app true\n") if preload ucfg.syswrite("check_client_connection true\n") if check_client ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n") ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n") pid = xfork { redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) } } _, status = Process.waitpid2(pid) assert status.success? wait_master_ready("test_stderr.#$$.log") wait_workers_ready("test_stderr.#$$.log", 1) uri = URI.parse("http://#@addr:#@port/") pids = Tempfile.new('worker_pids') r, w = IO.pipe hitter = fork { r.close bodies = Hash.new(0) at_exit { pids.syswrite(bodies.inspect) } trap(:TERM) { exit(0) } nr = 0 loop { rv = Net::HTTP.get(uri) pid = rv.to_i exit!(1) if pid <= 0 bodies[pid] += 1 nr += 1 if nr == 1 w.syswrite('1') elsif bodies.size > 1 w.syswrite('2') sleep end } } w.close assert_equal '1', r.read(1) daemon_pid = File.read(pid_file.path).to_i assert daemon_pid > 0 Process.kill(:HUP, daemon_pid) assert_equal '2', r.read(1) Process.kill(:TERM, hitter) _, hitter_status = Process.waitpid2(hitter) assert(hitter_status.success?, "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \ "#{File.read("test_stderr.#$$.log")}") pids.sysseek(0) pids = eval(pids.read) assert_kind_of(Hash, pids) assert_equal 2, pids.size pids.keys.each { |x| assert_kind_of(Integer, x) assert x > 0 assert pids[x] > 0 } Process.kill(:QUIT, daemon_pid) wait_for_death(daemon_pid) end def test_preload_app_hup hup_test_common(true) end def test_hup hup_test_common(false) end def test_check_client_hup hup_test_common(false, true) end def test_default_listen_hup_holds_listener default_listen_lock do res, pid_path = default_listen_setup daemon_pid = File.read(pid_path).to_i Process.kill(:HUP, daemon_pid) wait_workers_ready("test_stderr.#$$.log", 1) res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) assert_match %r{\d+}, res2.first assert res2.first != res.first Process.kill(:QUIT, daemon_pid) wait_for_death(daemon_pid) end end def test_default_listen_upgrade_holds_listener default_listen_lock do res, pid_path = default_listen_setup daemon_pid = File.read(pid_path).to_i Process.kill(:USR2, daemon_pid) wait_for_file("#{pid_path}.oldbin") wait_for_file(pid_path) Process.kill(:QUIT, daemon_pid) wait_for_death(daemon_pid) daemon_pid = File.read(pid_path).to_i wait_workers_ready("test_stderr.#$$.log", 1) File.truncate("test_stderr.#$$.log", 0) res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) assert_match %r{\d+}, res2.first assert res2.first != res.first Process.kill(:HUP, daemon_pid) wait_workers_ready("test_stderr.#$$.log", 1) File.truncate("test_stderr.#$$.log", 0) res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) assert res2.first != res3.first Process.kill(:QUIT, daemon_pid) wait_for_death(daemon_pid) end end def default_listen_setup File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) } pid_path = (tmp = Tempfile.new('pid')).path tmp.close! ucfg = Tempfile.new('unicorn_test_config') ucfg.syswrite("pid '#{pid_path}'\n") ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n") ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n") pid = xfork { redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) } } _, status = Process.waitpid2(pid) assert status.success? wait_master_ready("test_stderr.#$$.log") wait_workers_ready("test_stderr.#$$.log", 1) File.truncate("test_stderr.#$$.log", 0) res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) assert_match %r{\d+}, res.first [ res, pid_path ] end # we need to flock() something to prevent these tests from running def default_listen_lock(&block) fp = File.open(FLOCK_PATH, "rb") begin fp.flock(File::LOCK_EX) begin TCPServer.new(Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT).close rescue Errno::EADDRINUSE, Errno::EACCES warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}" return false end # unused_port should never take this, but we may run an environment # where tests are being run against older unicorns... lock_path = "#{Dir::tmpdir}/unicorn_test." \ "#{Unicorn::Const::DEFAULT_LISTEN}.lock" begin File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600) yield rescue Errno::EEXIST lock_path = nil return false ensure File.unlink(lock_path) if lock_path end ensure fp.flock(File::LOCK_UN) end end end if do_test test/test_helper.rb000066400000000000000000000176011471646016400147160ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # # Additional work donated by contributors. See git history # for more information. STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard # FIXME: move curl-dependent tests into t/ ENV['NO_PROXY'] ||= ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' # Some tests watch a log file or a pid file to spring up to check state # Can't rely on inotify on non-Linux and logging to a pipe makes things # more complicated DEFAULT_TRIES = 1000 DEFAULT_RES = 0.2 require 'test/unit' require 'net/http' require 'digest/sha1' require 'uri' require 'stringio' require 'pathname' require 'tempfile' require 'fileutils' require 'logger' require 'unicorn' require 'io/nonblock' if ENV['DEBUG'] require 'ruby-debug' Debugger.start end unless RUBY_VERSION < '3.1' warn "Unicorn was only tested against MRI up to 3.0.\n" \ "It might not properly work with #{RUBY_VERSION}" end def redirect_test_io orig_err = STDERR.dup orig_out = STDOUT.dup rdr_pid = $$ new_out = File.open("test_stdout.#$$.log", "a") new_err = File.open("test_stderr.#$$.log", "a") new_out.sync = new_err.sync = true if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise require 'shellwords' cmd = tail.shellsplit cmd << new_out.path cmd << new_err.path pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true }) sleep 0.1 # wait for tail(1) to startup end STDERR.reopen(new_err) STDOUT.reopen(new_out) STDERR.sync = STDOUT.sync = true at_exit do if rdr_pid == $$ File.unlink(new_out.path) rescue nil File.unlink(new_err.path) rescue nil end end begin yield ensure STDERR.reopen(orig_err) STDOUT.reopen(orig_out) Process.kill(:TERM, pid) if pid end end # which(1) exit codes cannot be trusted on some systems # We use UNIX shell utilities in some tests because we don't trust # ourselves to write Ruby 100% correctly :) def which(bin) ex = ENV['PATH'].split(/:/).detect do |x| x << "/#{bin}" File.executable?(x) end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}" ex end # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.) def hit(uris) results = [] uris.each do |u| res = nil if u.kind_of? String u = 'http://127.0.0.1:8080/' if u == 'http://0.0.0.0:8080/' res = Net::HTTP.get(URI.parse(u)) else url = URI.parse(u[0]) res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) } end assert res != nil, "Didn't get a response: #{u}" results << res end return results end # unused_port provides an unused port on +addr+ usable for TCP that is # guaranteed to be unused across all unicorn builds on that system. It # prevents race conditions by using a lock file other unicorn builds # will see. This is required if you perform several builds in parallel # with a continuous integration system or run tests in parallel via # gmake. This is NOT guaranteed to be race-free if you run other # processes that bind to random ports for testing (but the window # for a race condition is very small). You may also set UNICORN_TEST_ADDR # to override the default test address (127.0.0.1). def unused_port(addr = '127.0.0.1') retries = 100 base = 5000 port = sock = nil begin begin port = base + rand(32768 - base) while port == Unicorn::Const::DEFAULT_PORT port = base + rand(32768 - base) end sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) sock.bind(Socket.pack_sockaddr_in(port, addr)) sock.listen(5) rescue Errno::EADDRINUSE, Errno::EACCES sock.close rescue nil retry if (retries -= 1) >= 0 end # since we'll end up closing the random port we just got, there's a race # condition could allow the random port we just chose to reselect itself # when running tests in parallel with gmake. Create a lock file while # we have the port here to ensure that does not happen . lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock" File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600).close at_exit { File.unlink(lock_path) rescue nil } rescue Errno::EEXIST sock.close rescue nil retry end sock.close rescue nil port end def try_require(lib) begin require lib true rescue LoadError false end end # sometimes the server may not come up right away def retry_hit(uris = []) tries = DEFAULT_TRIES begin hit(uris) rescue Errno::EINVAL, Errno::ECONNREFUSED => err if (tries -= 1) > 0 sleep DEFAULT_RES retry end raise err end end def assert_shutdown(pid) wait_master_ready("test_stderr.#{pid}.log") Process.kill(:QUIT, pid) pid, status = Process.waitpid2(pid) assert status.success?, "exited successfully" end def wait_workers_ready(path, nr_workers) tries = DEFAULT_TRIES lines = [] while (tries -= 1) > 0 begin lines = File.readlines(path).grep(/worker=\d+ ready/) lines.size == nr_workers and return rescue Errno::ENOENT end sleep DEFAULT_RES end raise "#{nr_workers} workers never became ready:" \ "\n\t#{lines.join("\n\t")}\n" end def wait_master_ready(master_log) tries = DEFAULT_TRIES while (tries -= 1) > 0 begin File.readlines(master_log).grep(/master process ready/)[0] and return rescue Errno::ENOENT end sleep DEFAULT_RES end raise "master process never became ready" end def reexec_usr2_quit_test(pid, pid_file) assert File.exist?(pid_file), "pid file OK" assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file" Process.kill(:USR2, pid) retry_hit(["http://#{@addr}:#{@port}/"]) wait_for_file("#{pid_file}.oldbin") wait_for_file(pid_file) old_pid = File.read("#{pid_file}.oldbin").to_i new_pid = File.read(pid_file).to_i # kill old master process assert_not_equal pid, new_pid assert_equal pid, old_pid Process.kill(:QUIT, old_pid) retry_hit(["http://#{@addr}:#{@port}/"]) wait_for_death(old_pid) assert_equal new_pid, File.read(pid_file).to_i retry_hit(["http://#{@addr}:#{@port}/"]) Process.kill(:QUIT, new_pid) end def reexec_basic_test(pid, pid_file) results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class Process.kill(0, pid) master_log = "#{@tmpdir}/test_stderr.#{pid}.log" wait_master_ready(master_log) File.truncate(master_log, 0) nr = 50 kill_point = 2 nr.times do |i| hit(["http://#{@addr}:#{@port}/#{i}"]) i == kill_point and Process.kill(:HUP, pid) end wait_master_ready(master_log) assert File.exist?(pid_file), "pid=#{pid_file} exists" new_pid = File.read(pid_file).to_i assert_not_equal pid, new_pid Process.kill(0, new_pid) Process.kill(:QUIT, new_pid) end def wait_for_file(path) tries = DEFAULT_TRIES while (tries -= 1) > 0 && ! File.exist?(path) sleep DEFAULT_RES end assert File.exist?(path), "path=#{path} exists #{caller.inspect}" end def xfork(&block) fork do ObjectSpace.each_object(Tempfile) do |tmp| ObjectSpace.undefine_finalizer(tmp) end yield end end # can't waitpid on detached processes def wait_for_death(pid) tries = DEFAULT_TRIES while (tries -= 1) > 0 begin Process.kill(0, pid) begin Process.waitpid(pid, Process::WNOHANG) rescue Errno::ECHILD end sleep(DEFAULT_RES) rescue Errno::ESRCH return end end raise "PID:#{pid} never died!" end def reset_sig_handlers %w(WINCH QUIT INT TERM USR1 USR2 HUP TTIN TTOU CHLD).each do |sig| trap(sig, "DEFAULT") end end def tcp_socket(*args) sock = TCPSocket.new(*args) sock.nonblock = false sock end def unix_socket(*args) sock = UNIXSocket.new(*args) sock.nonblock = false sock end test/unit/000077500000000000000000000000001471646016400130255ustar00rootroot00000000000000test/unit/test_configurator.rb000066400000000000000000000126231471646016400171170ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'test/unit' require 'tempfile' require 'unicorn' TestStruct = Struct.new( *(Unicorn::Configurator::DEFAULTS.keys + %w(listener_opts listeners))) class TestConfigurator < Test::Unit::TestCase def test_config_init Unicorn::Configurator.new {} end def test_expand_addr meth = Unicorn::Configurator.new.method(:expand_addr) assert_equal "/var/run/unicorn.sock", meth.call("/var/run/unicorn.sock") assert_equal "#{Dir.pwd}/foo/bar.sock", meth.call("unix:foo/bar.sock") path = meth.call("~/foo/bar.sock") assert_equal "/", path[0..0] assert_match %r{/foo/bar\.sock\z}, path path = meth.call("~root/foo/bar.sock") assert_equal "/", path[0..0] assert_match %r{/foo/bar\.sock\z}, path assert_equal "1.2.3.4:2007", meth.call('1.2.3.4:2007') assert_equal "0.0.0.0:2007", meth.call('0.0.0.0:2007') assert_equal "0.0.0.0:2007", meth.call(':2007') assert_equal "0.0.0.0:2007", meth.call('*:2007') assert_equal "0.0.0.0:2007", meth.call('2007') assert_equal "0.0.0.0:2007", meth.call(2007) %w([::1]:2007 [::]:2007).each do |addr| assert_equal addr, meth.call(addr.dup) end # for Rainbows! users only assert_equal "[::]:80", meth.call("[::]") assert_equal "127.6.6.6:80", meth.call("127.6.6.6") # the next two aren't portable, consider them unsupported for now # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007') # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007') end def test_config_invalid tmp = Tempfile.new('unicorn_config') tmp.syswrite(%q(asdfasdf "hello-world")) assert_raises(NoMethodError) do Unicorn::Configurator.new(:config_file => tmp.path) end end def test_config_non_existent tmp = Tempfile.new('unicorn_config') path = tmp.path tmp.close! assert_raises(Errno::ENOENT) do Unicorn::Configurator.new(:config_file => path) end end def test_config_defaults cfg = Unicorn::Configurator.new(:use_defaults => true) test_struct = TestStruct.new cfg.commit!(test_struct) Unicorn::Configurator::DEFAULTS.each do |key,value| assert_equal value, test_struct.__send__(key) end end def test_config_defaults_skip cfg = Unicorn::Configurator.new(:use_defaults => true) skip = [ :logger ] test_struct = TestStruct.new cfg.commit!(test_struct, :skip => skip) Unicorn::Configurator::DEFAULTS.each do |key,value| next if skip.include?(key) assert_equal value, test_struct.__send__(key) end assert_nil test_struct.logger end def test_listen_options tmp = Tempfile.new('unicorn_config') expect = { :sndbuf => 1, :rcvbuf => 2, :backlog => 10 }.freeze listener = "127.0.0.1:12345" tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") cfg = Unicorn::Configurator.new(:config_file => tmp.path) test_struct = TestStruct.new cfg.commit!(test_struct) assert(listener_opts = test_struct.listener_opts) assert_equal expect, listener_opts[listener] end def test_listen_option_bad tmp = Tempfile.new('unicorn_config') expect = { :sndbuf => "five" } listener = "127.0.0.1:12345" tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") assert_raises(ArgumentError) do Unicorn::Configurator.new(:config_file => tmp.path) end end def test_listen_option_bad_delay tmp = Tempfile.new('unicorn_config') expect = { :delay => "five" } listener = "127.0.0.1:12345" tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") assert_raises(ArgumentError) do Unicorn::Configurator.new(:config_file => tmp.path) end end def test_listen_option_float_delay tmp = Tempfile.new('unicorn_config') expect = { :delay => 0.5 } listener = "127.0.0.1:12345" tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") Unicorn::Configurator.new(:config_file => tmp.path) end def test_listen_option_int_delay tmp = Tempfile.new('unicorn_config') expect = { :delay => 5 } listener = "127.0.0.1:12345" tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") Unicorn::Configurator.new(:config_file => tmp.path) end def test_check_client_connection tmp = Tempfile.new('unicorn_config') test_struct = TestStruct.new tmp.syswrite("check_client_connection true\n") assert_nothing_raised do Unicorn::Configurator.new(:config_file => tmp.path).commit!(test_struct) end assert test_struct.check_client_connection end def test_check_client_connection_with_tcp_bad tmp = Tempfile.new('unicorn_config') test_struct = TestStruct.new listener = "127.0.0.1:12345" tmp.syswrite("check_client_connection true\n") tmp.syswrite("listen '#{listener}', :tcp_nopush => true\n") assert_raises(ArgumentError) do Unicorn::Configurator.new(:config_file => tmp.path).commit!(test_struct) end end def test_after_fork_proc test_struct = TestStruct.new [ proc { |a,b| }, Proc.new { |a,b| }, lambda { |a,b| } ].each do |my_proc| Unicorn::Configurator.new(:after_fork => my_proc).commit!(test_struct) assert_equal my_proc, test_struct.after_fork end end def test_after_fork_wrong_arity [ proc { |a| }, Proc.new { }, lambda { |a,b,c| } ].each do |my_proc| assert_raises(ArgumentError) do Unicorn::Configurator.new(:after_fork => my_proc) end end end end test/unit/test_droplet.rb000066400000000000000000000015161471646016400160650ustar00rootroot00000000000000# frozen_string_literal: false require 'test/unit' require 'unicorn' class TestDroplet < Test::Unit::TestCase def test_create_many_droplets now = Time.now.to_i (0..1024).each do |i| droplet = Unicorn::Worker.new(i) assert droplet.respond_to?(:tick) assert_equal 0, droplet.tick assert_equal(now, droplet.tick = now) assert_equal now, droplet.tick assert_equal(0, droplet.tick = 0) assert_equal 0, droplet.tick end end def test_shared_process droplet = Unicorn::Worker.new(0) _, status = Process.waitpid2(fork { droplet.tick += 1; exit!(0) }) assert status.success?, status.inspect assert_equal 1, droplet.tick _, status = Process.waitpid2(fork { droplet.tick += 1; exit!(0) }) assert status.success?, status.inspect assert_equal 2, droplet.tick end end test/unit/test_http_parser.rb000066400000000000000000000745161471646016400167610ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # # Additional work donated by contributors. See git history # for more information. require './test/test_helper' include Unicorn class HttpParserTest < Test::Unit::TestCase def test_parse_simple parser = HttpParser.new req = parser.env http = parser.buf http << "GET / HTTP/1.1\r\n\r\n" assert_equal req, parser.parse assert_equal '', http assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/', req['REQUEST_PATH'] assert_equal 'HTTP/1.1', req['HTTP_VERSION'] assert_equal '/', req['REQUEST_URI'] assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] assert parser.keepalive? parser.clear req.clear http << "G" assert_nil parser.parse assert_equal "G", http assert req.empty? # try parsing again to ensure we were reset correctly http << "ET /hello-world HTTP/1.1\r\n\r\n" assert parser.parse assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/hello-world', req['REQUEST_PATH'] assert_equal 'HTTP/1.1', req['HTTP_VERSION'] assert_equal '/hello-world', req['REQUEST_URI'] assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] assert_equal '', http assert parser.keepalive? end def test_tab_lws parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost:\tfoo.bar\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert_equal "foo.bar", req['HTTP_HOST'] end def test_connection_close_no_ka parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nConnection: close\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert_equal "GET", req['REQUEST_METHOD'] assert ! parser.keepalive? end def test_connection_keep_alive_ka parser = HttpParser.new req = parser.env parser.buf << "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert parser.keepalive? end def test_connection_keep_alive_no_body parser = HttpParser.new req = parser.env parser.buf << "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert parser.keepalive? end def test_connection_keep_alive_no_body_empty parser = HttpParser.new req = parser.env parser.buf << "POST / HTTP/1.1\r\n" \ "Content-Length: 0\r\n" \ "Connection: keep-alive\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert parser.keepalive? end def test_connection_keep_alive_ka_bad_version parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n" assert_equal req.object_id, parser.parse.object_id assert parser.keepalive? end def test_parse_server_host_default_port parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: foo\r\n\r\n" assert_equal req, parser.parse assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal '', parser.buf assert parser.keepalive? end def test_parse_server_host_alt_port parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n" assert_equal req, parser.parse assert_equal 'foo', req['SERVER_NAME'] assert_equal '999', req['SERVER_PORT'] assert_equal '', parser.buf assert parser.keepalive? end def test_parse_server_host_empty_port parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n" assert_equal req, parser.parse assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal '', parser.buf assert parser.keepalive? end def test_parse_server_host_xfp_https parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: foo:\r\n" \ "X-Forwarded-Proto: https\r\n\r\n" assert_equal req, parser.parse assert_equal 'foo', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] assert_equal '', parser.buf assert parser.keepalive? end def test_parse_xfp_https_chained parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\n" \ "X-Forwarded-Proto: https,http\r\n\r\n" assert_equal req, parser.parse assert_equal '443', req['SERVER_PORT'], req.inspect assert_equal 'https', req['rack.url_scheme'], req.inspect assert_equal '', parser.buf end def test_parse_xfp_https_chained_backwards parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\n" \ "X-Forwarded-Proto: http,https\r\n\r\n" assert_equal req, parser.parse assert_equal '80', req['SERVER_PORT'], req.inspect assert_equal 'http', req['rack.url_scheme'], req.inspect assert_equal '', parser.buf end def test_parse_xfp_gopher_is_ignored parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\n" \ "X-Forwarded-Proto: gopher\r\n\r\n" assert_equal req, parser.parse assert_equal '80', req['SERVER_PORT'], req.inspect assert_equal 'http', req['rack.url_scheme'], req.inspect assert_equal '', parser.buf end def test_parse_x_forwarded_ssl_on parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\n" \ "X-Forwarded-Ssl: on\r\n\r\n" assert_equal req, parser.parse assert_equal '443', req['SERVER_PORT'], req.inspect assert_equal 'https', req['rack.url_scheme'], req.inspect assert_equal '', parser.buf end def test_parse_x_forwarded_ssl_off parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.0\r\nX-Forwarded-Ssl: off\r\n\r\n" assert_equal req, parser.parse assert_equal '80', req['SERVER_PORT'], req.inspect assert_equal 'http', req['rack.url_scheme'], req.inspect assert_equal '', parser.buf end def test_parse_strange_headers parser = HttpParser.new req = parser.env should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" parser.buf << should_be_good assert_equal req, parser.parse assert_equal '', parser.buf assert parser.keepalive? end # legacy test case from Mongrel that we never supported before... # I still consider Pound irrelevant, unfortunately stupid clients that # send extremely big headers do exist and they've managed to find Unicorn... def test_nasty_pound_header parser = HttpParser.new nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n" req = parser.env parser.buf << nasty_pound_header.dup assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m expect = $1.dup expect.gsub!(/\r\n\t/, ' ') assert_equal req, parser.parse assert_equal '', parser.buf assert_equal expect, req['HTTP_X_SSL_BULLSHIT'] end def test_multiline_header_0d0a parser = HttpParser.new parser.buf << "GET / HTTP/1.0\r\n" \ "X-Multiline-Header: foo bar\r\n\tcha cha\r\n\tzha zha\r\n\r\n" req = parser.env assert_equal req, parser.parse assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER'] end def test_multiline_header_0a parser = HttpParser.new parser.buf << "GET / HTTP/1.0\n" \ "X-Multiline-Header: foo bar\n\tcha cha\n\tzha zha\n\n" req = parser.env assert_equal req, parser.parse assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER'] end def test_continuation_eats_leading_spaces parser = HttpParser.new header = "GET / HTTP/1.1\r\n" \ "X-ASDF: \r\n" \ "\t\r\n" \ " \r\n" \ " ASDF\r\n\r\n" parser.buf << header req = parser.env assert_equal req, parser.parse assert_equal '', parser.buf assert_equal 'ASDF', req['HTTP_X_ASDF'] end def test_continuation_eats_scattered_leading_spaces parser = HttpParser.new header = "GET / HTTP/1.1\r\n" \ "X-ASDF: hi\r\n" \ " y\r\n" \ "\t\r\n" \ " x\r\n" \ " ASDF\r\n\r\n" req = parser.env parser.buf << header assert_equal req, parser.parse assert_equal '', parser.buf assert_equal 'hi y x ASDF', req['HTTP_X_ASDF'] end def test_continuation_eats_trailing_spaces parser = HttpParser.new header = "GET / HTTP/1.1\r\n" \ "X-ASDF: \r\n" \ "\t\r\n" \ " b \r\n" \ " ASDF\r\n\r\n" parser.buf << header req = parser.env assert_equal req, parser.parse assert_equal '', parser.buf assert_equal 'b ASDF', req['HTTP_X_ASDF'] end def test_continuation_with_absolute_uri_and_ignored_host_header parser = HttpParser.new header = "GET http://example.com/ HTTP/1.1\r\n" \ "Host: \r\n" \ " YHBT.net\r\n" \ "\r\n" parser.buf << header req = parser.env assert_equal req, parser.parse assert_equal 'example.com', req['HTTP_HOST'] end # this may seem to be testing more of an implementation detail, but # it also helps ensure we're safe in the presence of multiple parsers # in case we ever go multithreaded/evented... def test_resumable_continuations nr = 1000 header = "GET / HTTP/1.1\r\n" \ "X-ASDF: \r\n" \ " hello\r\n" tmp = [] nr.times { |i| parser = HttpParser.new req = parser.env parser.buf << "#{header} #{i}\r\n" assert parser.parse.nil? asdf = req['HTTP_X_ASDF'] assert_equal "hello #{i}", asdf tmp << [ parser, asdf ] } tmp.each_with_index { |(parser, asdf), i| parser.buf << " .\r\n\r\n" assert parser.parse assert_equal "hello #{i} .", asdf } end def test_invalid_continuation parser = HttpParser.new header = "GET / HTTP/1.1\r\n" \ " y\r\n" \ "Host: hello\r\n" \ "\r\n" parser.buf << header assert_raises(HttpParserError) { parser.parse } end def test_parse_ie6_urls %w(/some/random/path" /some/random/path> /some/random/path< /we/love/you/ie6?q=<""> /url?<="&>=" /mal"formed"? ).each do |path| parser = HttpParser.new req = parser.env sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) assert_equal req, parser.headers(req, sorta_safe) assert_equal path, req['REQUEST_URI'] assert_equal '', sorta_safe assert parser.keepalive? end end def test_parse_error parser = HttpParser.new req = parser.env bad_http = "GET / SsUTF/1.1" assert_raises(HttpParserError) { parser.headers(req, bad_http) } # make sure we can recover parser.clear req.clear assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n") assert ! parser.keepalive? end def test_piecemeal parser = HttpParser.new req = parser.env http = "GET" assert_nil parser.headers(req, http) assert_nil parser.headers(req, http) assert_nil parser.headers(req, http << " / HTTP/1.0") assert_equal '/', req['REQUEST_PATH'] assert_equal '/', req['REQUEST_URI'] assert_equal 'GET', req['REQUEST_METHOD'] assert_nil parser.headers(req, http << "\r\n") assert_equal 'HTTP/1.0', req['HTTP_VERSION'] assert_nil parser.headers(req, http << "\r") assert_equal req, parser.headers(req, http << "\n") assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] assert_equal "", http assert ! parser.keepalive? end # not common, but underscores do appear in practice def test_absolute_uri_underscores parser = HttpParser.new req = parser.env http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n" parser.buf << http assert_equal req, parser.parse assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'under_score.example.com', req['HTTP_HOST'] assert_equal 'under_score.example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", parser.buf assert ! parser.keepalive? end # some dumb clients add users because they're stupid def test_absolute_uri_w_user parser = HttpParser.new req = parser.env http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n" parser.buf << http assert_equal req, parser.parse assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", parser.buf assert ! parser.keepalive? end # since Mongrel supported anything URI.parse supported, we're stuck # supporting everything URI.parse supports def test_absolute_uri_uri_parse "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char| parser = HttpParser.new req = parser.env http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/', req['REQUEST_URI'] assert_equal '/', req['REQUEST_PATH'] assert_equal '', req['QUERY_STRING'] assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", http assert ! parser.keepalive? end end def test_absolute_uri parser = HttpParser.new req = parser.env parser.buf << "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n" assert_equal req, parser.parse assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", parser.buf assert ! parser.keepalive? end # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however... def test_absolute_uri_https parser = HttpParser.new req = parser.env http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \ "X-Forwarded-Proto: http\r\n\r\n" parser.buf << http assert_equal req, parser.parse assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] assert_equal "", parser.buf assert parser.keepalive? end # Host: header should be ignored for absolute URIs def test_absolute_uri_with_port parser = HttpParser.new req = parser.env parser.buf << "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.parse assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'example.com:8080', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '8080', req['SERVER_PORT'] assert_equal "", parser.buf assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_uri_with_empty_port parser = HttpParser.new req = parser.env parser.buf << "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.parse assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] assert_equal 'example.com:', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] assert_equal "", parser.buf assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_ipv6_uri parser = HttpParser.new req = parser.env url = "http://[::1]/foo?q=bar" http = "GET #{url} HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] uri = URI.parse(url) assert_equal "[::1]", uri.host, "URI.parse changed upstream for #{url}? host=#{uri.host}" assert_equal "[::1]", req['HTTP_HOST'] assert_equal "[::1]", req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", http assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_ipv6_uri_alpha parser = HttpParser.new req = parser.env url = "http://[::a]/" http = "GET #{url} HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] uri = URI.parse(url) assert_equal "[::a]", uri.host, "URI.parse changed upstream for #{url}? host=#{uri.host}" assert_equal "[::a]", req['HTTP_HOST'] assert_equal "[::a]", req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] end def test_absolute_ipv6_uri_alpha_2 parser = HttpParser.new req = parser.env url = "http://[::B]/" http = "GET #{url} HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] uri = URI.parse(url) assert_equal "[::B]", uri.host, "URI.parse changed upstream for #{url}? host=#{uri.host}" assert_equal "[::B]", req['HTTP_HOST'] assert_equal "[::B]", req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] end def test_absolute_ipv6_uri_with_empty_port parser = HttpParser.new req = parser.env url = "https://[::1]:/foo?q=bar" http = "GET #{url} HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] uri = URI.parse(url) assert_equal "[::1]", uri.host, "URI.parse changed upstream for #{url}? host=#{uri.host}" assert_equal "[::1]:", req['HTTP_HOST'] assert_equal "[::1]", req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] assert_equal "", http assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_ipv6_uri_with_port parser = HttpParser.new req = parser.env url = "https://[::1]:666/foo?q=bar" http = "GET #{url} HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] assert_equal 'q=bar', req['QUERY_STRING'] uri = URI.parse(url) assert_equal "[::1]", uri.host, "URI.parse changed upstream for #{url}? host=#{uri.host}" assert_equal "[::1]:666", req['HTTP_HOST'] assert_equal "[::1]", req['SERVER_NAME'] assert_equal '666', req['SERVER_PORT'] assert_equal "", http assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_ipv6_host_header parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\n" \ "Host: [::1]\r\n\r\n" assert_equal req, parser.parse assert_equal "[::1]", req['HTTP_HOST'] assert_equal "[::1]", req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "", parser.buf assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_ipv6_host_header_with_port parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\n" \ "Host: [::1]:666\r\n\r\n" assert_equal req, parser.parse assert_equal "[::1]", req['SERVER_NAME'] assert_equal '666', req['SERVER_PORT'] assert_equal "[::1]:666", req['HTTP_HOST'] assert_equal "", parser.buf assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_ipv6_host_header_with_empty_port parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: [::1]:\r\n\r\n" assert_equal req, parser.parse assert_equal "[::1]", req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] assert_equal "[::1]:", req['HTTP_HOST'] assert_equal "", parser.buf assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end # XXX Highly unlikely..., just make sure we don't segfault or assert on it def test_broken_ipv6_host_header parser = HttpParser.new req = parser.env parser.buf << "GET / HTTP/1.1\r\nHost: [::1:\r\n\r\n" assert_equal req, parser.parse assert_equal "[", req['SERVER_NAME'] assert_equal ':1:', req['SERVER_PORT'] assert_equal "[::1:", req['HTTP_HOST'] assert_equal "", parser.buf end def test_put_body_oneshot parser = HttpParser.new req = parser.env parser.buf << "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde" assert_equal req, parser.parse assert_equal '/', req['REQUEST_PATH'] assert_equal '/', req['REQUEST_URI'] assert_equal 'PUT', req['REQUEST_METHOD'] assert_equal 'HTTP/1.0', req['HTTP_VERSION'] assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] assert_equal "abcde", parser.buf assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_put_body_later parser = HttpParser.new req = parser.env parser.buf << "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n" assert_equal req, parser.parse assert_equal '/l', req['REQUEST_PATH'] assert_equal '/l', req['REQUEST_URI'] assert_equal 'PUT', req['REQUEST_METHOD'] assert_equal 'HTTP/1.0', req['HTTP_VERSION'] assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] assert_equal "", parser.buf assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_unknown_methods %w(GETT HEADR XGET XHEAD).each { |m| parser = HttpParser.new req = parser.env s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" ok = parser.headers(req, s) assert ok assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] assert_equal 'page=1', req['QUERY_STRING'] assert_equal "", s assert_equal m, req['REQUEST_METHOD'] assert parser.keepalive? # TODO: read HTTP/1.2 when it's final } end def test_fragment_in_uri parser = HttpParser.new req = parser.env get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" parser.buf << get ok = parser.parse assert ok assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] assert_equal 'page=1', req['QUERY_STRING'] assert_equal '', parser.buf assert parser.keepalive? end # lame random garbage maker def rand_data(min, max, readable=true) count = min + ((rand(max)+1) *10).to_i res = count.to_s + "/" if readable res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40) else res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20) end return res end def test_horrible_queries parser = HttpParser.new # then that large header names are caught 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n" assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do parser.buf << get parser.parse parser.clear end end # then that large mangled field values are caught 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do parser.buf << get parser.parse parser.clear end end # then large headers are rejected too get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" get << "X-Test: test\r\n" * (80 * 1024) parser.buf << get assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do parser.parse end parser.clear # finally just that random garbage gets blocked all the time 10.times do |c| get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises(Unicorn::HttpParserError,Unicorn::RequestURITooLongError) do parser.buf << get parser.parse parser.clear end end end def test_leading_tab parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost:\texample.com\r\n\r\n" assert parser.add_parse(get) assert_equal 'example.com', parser.env['HTTP_HOST'] end def test_trailing_whitespace parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: example.com \r\n\r\n" assert parser.add_parse(get) assert_equal 'example.com', parser.env['HTTP_HOST'] end def test_trailing_tab parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: example.com\t\r\n\r\n" assert parser.add_parse(get) assert_equal 'example.com', parser.env['HTTP_HOST'] end def test_trailing_multiple_linear_whitespace parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: example.com\t \t \t\r\n\r\n" assert parser.add_parse(get) assert_equal 'example.com', parser.env['HTTP_HOST'] end def test_embedded_linear_whitespace_ok parser = HttpParser.new get = "GET / HTTP/1.1\r\nX-Space: hello\t world\t \r\n\r\n" assert parser.add_parse(get) assert_equal "hello\t world", parser.env["HTTP_X_SPACE"] end def test_null_byte_header parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: \0\r\n\r\n" assert_raises(HttpParserError) { parser.add_parse(get) } end def test_null_byte_in_middle parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: hello\0world\r\n\r\n" assert_raises(HttpParserError) { parser.add_parse(get) } end def test_null_byte_at_end parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: hello\0\r\n\r\n" assert_raises(HttpParserError) { parser.add_parse(get) } end def test_empty_header parser = HttpParser.new get = "GET / HTTP/1.1\r\nHost: \r\n\r\n" assert parser.add_parse(get) assert_equal '', parser.env['HTTP_HOST'] end def test_memsize require 'objspace' if ObjectSpace.respond_to?(:memsize_of) n = ObjectSpace.memsize_of(Unicorn::HttpParser.new) assert_kind_of Integer, n # need to update this when 128-bit machines come out # n.b. actual struct size on 64-bit is 56 bytes + 40 bytes for RVALUE # Ruby <= 2.2 objspace did not count the 40-byte RVALUE, 2.3 does. assert_operator n, :<=, 96 assert_operator n, :>, 0 end rescue LoadError # not all Ruby implementations have objspace end def test_dedupe parser = HttpParser.new # n.b. String#freeze optimization doesn't work under modern test-unit exp = -'HTTP_HOST' get = "GET / HTTP/1.1\r\nHost: example.com\r\nHavpbea-fhpxf: true\r\n\r\n" assert parser.add_parse(get) key = parser.env.keys.detect { |k| k == exp } assert_same exp, key if RUBY_VERSION.to_r >= 2.6 # 2.6.0-rc1+ exp = -'HTTP_HAVPBEA_FHPXF' key = parser.env.keys.detect { |k| k == exp } assert_same exp, key end end if RUBY_VERSION.to_r >= 2.5 && RUBY_ENGINE == 'ruby' end test/unit/test_http_parser_ng.rb000066400000000000000000000532241471646016400174360ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require './test/test_helper' require 'digest/md5' include Unicorn class HttpParserNgTest < Test::Unit::TestCase def setup @parser = HttpParser.new end # RFC 7230 allows gzip/deflate/compress Transfer-Encoding, # but "chunked" must be last if used def test_is_chunked [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x| assert_raise(HttpParserError) { HttpParser.is_chunked?(x) } end [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x| assert HttpParser.is_chunked?(x) end [ 'gzip', 'xhunked', 'xchunked' ].each do |x| assert !HttpParser.is_chunked?(x) end end def test_parser_max_len assert_raises(RangeError) do HttpParser.max_header_len = 0xffffffff + 1 end end def test_next_clear r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" @parser.buf << r @parser.parse @parser.response_start_sent = true assert @parser.keepalive? assert @parser.next? assert @parser.response_start_sent # persistent client makes another request: @parser.buf << r @parser.parse assert @parser.keepalive? assert @parser.next? assert_equal false, @parser.response_start_sent end def test_response_start_sent assert_equal false, @parser.response_start_sent, "default is false" @parser.response_start_sent = true assert_equal true, @parser.response_start_sent @parser.response_start_sent = false assert_equal false, @parser.response_start_sent @parser.response_start_sent = true @parser.clear assert_equal false, @parser.response_start_sent end def test_connection_TE @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: TE\r\n" @parser.buf << "TE: trailers\r\n\r\n" @parser.parse assert @parser.keepalive? assert @parser.next? end def test_keepalive_requests_with_next? req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".freeze expect = { "SERVER_NAME" => "example.com", "HTTP_HOST" => "example.com", "rack.url_scheme" => "http", "REQUEST_PATH" => "/", "SERVER_PROTOCOL" => "HTTP/1.1", "PATH_INFO" => "/", "HTTP_VERSION" => "HTTP/1.1", "REQUEST_URI" => "/", "SERVER_PORT" => "80", "REQUEST_METHOD" => "GET", "QUERY_STRING" => "" }.freeze 100.times do |nr| @parser.buf << req assert_equal expect, @parser.parse assert @parser.next? end end def test_default_keepalive_is_off assert ! @parser.keepalive? assert ! @parser.next? @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" @parser.parse assert @parser.keepalive? @parser.clear assert ! @parser.keepalive? assert ! @parser.next? end def test_identity_byte_headers req = @parser.env str = "PUT / HTTP/1.1\r\n" str << "Content-Length: 123\r\n" str << "\r" hdr = @parser.buf str.each_byte { |byte| hdr << byte.chr assert_nil @parser.parse } hdr << "\n" assert_equal req.object_id, @parser.parse.object_id assert_equal '123', req['CONTENT_LENGTH'] assert_equal 0, hdr.size assert ! @parser.keepalive? assert @parser.headers? assert_equal 123, @parser.content_length dst = "" buf = '.' * 123 @parser.filter_body(dst, buf) assert_equal '.' * 123, dst assert_equal "", buf assert @parser.keepalive? end def test_identity_step_headers req = @parser.env str = @parser.buf str << "PUT / HTTP/1.1\r\n" assert ! @parser.parse str << "Content-Length: 123\r\n" assert ! @parser.parse str << "\r\n" assert_equal req.object_id, @parser.parse.object_id assert_equal '123', req['CONTENT_LENGTH'] assert_equal 0, str.size assert ! @parser.keepalive? assert @parser.headers? dst = "" buf = '.' * 123 @parser.filter_body(dst, buf) assert_equal '.' * 123, dst assert_equal "", buf assert @parser.keepalive? end def test_identity_oneshot_header req = @parser.env str = @parser.buf str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n" assert_equal req.object_id, @parser.parse.object_id assert_equal '123', req['CONTENT_LENGTH'] assert_equal 0, str.size assert ! @parser.keepalive? assert @parser.headers? dst = "" buf = '.' * 123 @parser.filter_body(dst, buf) assert_equal '.' * 123, dst assert_equal "", buf end def test_identity_oneshot_header_with_body body = ('a' * 123).freeze req = @parser.env str = @parser.buf str << "PUT / HTTP/1.1\r\n" \ "Content-Length: #{body.length}\r\n" \ "\r\n#{body}" assert_equal req.object_id, @parser.parse.object_id assert_equal '123', req['CONTENT_LENGTH'] assert_equal 123, str.size assert_equal body, str tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal 0, str.size assert_equal tmp, body assert_equal "", @parser.filter_body(tmp, str) assert @parser.keepalive? end def test_identity_oneshot_header_with_body_partial str = @parser.buf str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na" assert_equal Hash, @parser.parse.class assert_equal 1, str.size assert_equal 'a', str tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal "", str assert_equal "a", tmp str << ' ' * 122 rv = @parser.filter_body(tmp, str) assert_equal 122, tmp.size assert_nil rv assert_equal "", str assert_equal str.object_id, @parser.filter_body(tmp, str).object_id assert @parser.keepalive? end def test_identity_oneshot_header_with_body_slop str = @parser.buf str << "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG" assert_equal Hash, @parser.parse.class assert_equal 2, str.size assert_equal 'aG', str tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal "G", str assert_equal "G", @parser.filter_body(tmp, str) assert_equal 1, tmp.size assert_equal "a", tmp assert @parser.keepalive? end def test_chunked str = @parser.buf req = @parser.env str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" assert_equal req, @parser.parse, "msg=#{str}" assert_equal 0, str.size tmp = "" assert_nil @parser.filter_body(tmp, str << "6") assert_equal 0, tmp.size assert_nil @parser.filter_body(tmp, str << "\r\n") assert_equal 0, str.size assert_equal 0, tmp.size tmp = "" assert_nil @parser.filter_body(tmp, str << "..") assert_equal "..", tmp assert_nil @parser.filter_body(tmp, str << "abcd\r\n0\r\n") assert_equal "abcd", tmp assert_equal str.object_id, @parser.filter_body(tmp, str << "PUT").object_id assert_equal "PUT", str assert ! @parser.keepalive? str << "TY: FOO\r\n\r\n" assert_equal req, @parser.parse assert_equal "FOO", req["HTTP_PUTTY"] assert @parser.keepalive? end def test_chunked_empty str = @parser.buf req = @parser.env str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" assert_equal req, @parser.parse, "msg=#{str}" assert_equal 0, str.size tmp = "" assert_equal str, @parser.filter_body(tmp, str << "0\r\n\r\n") assert_equal "", tmp end def test_two_chunks str = @parser.buf str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" req = @parser.env assert_equal req, @parser.parse assert_equal 0, str.size tmp = "" assert_nil @parser.filter_body(tmp, str << "6") assert_equal 0, tmp.size assert_nil @parser.filter_body(tmp, str << "\r\n") assert_equal "", str assert_equal 0, tmp.size tmp = "" assert_nil @parser.filter_body(tmp, str << "..") assert_equal 2, tmp.size assert_equal "..", tmp assert_nil @parser.filter_body(tmp, str << "abcd\r\n1") assert_equal "abcd", tmp assert_nil @parser.filter_body(tmp, str << "\r") assert_equal "", tmp assert_nil @parser.filter_body(tmp, str << "\n") assert_equal "", tmp assert_nil @parser.filter_body(tmp, str << "z") assert_equal "z", tmp assert_nil @parser.filter_body(tmp, str << "\r\n") assert_nil @parser.filter_body(tmp, str << "0") assert_nil @parser.filter_body(tmp, str << "\r") rv = @parser.filter_body(tmp, str << "\nGET") assert_equal "GET", rv assert_equal str.object_id, rv.object_id assert ! @parser.keepalive? end def test_big_chunk str = @parser.buf str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ "4000\r\nabcd" req = @parser.env assert_equal req, @parser.parse tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal '', str str << ' ' * 16300 assert_nil @parser.filter_body(tmp, str) assert_equal '', str str << ' ' * 80 assert_nil @parser.filter_body(tmp, str) assert_equal '', str assert ! @parser.body_eof? assert_equal "", @parser.filter_body(tmp, str << "\r\n0\r\n") assert_equal "", tmp assert @parser.body_eof? str << "\r\n" assert_equal req, @parser.parse assert_equal "", str assert @parser.body_eof? assert @parser.keepalive? end def test_two_chunks_oneshot str = @parser.buf req = @parser.env str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ "1\r\na\r\n2\r\n..\r\n0\r\n" assert_equal req, @parser.parse tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal 'a..', tmp rv = @parser.filter_body(tmp, str) assert_equal rv.object_id, str.object_id assert ! @parser.keepalive? end def test_chunks_bytewise chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n" str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" buf = @parser.buf buf << str req = @parser.env assert_equal req, @parser.parse assert_equal "", buf tmp = '' body = '' str = chunked[0..-2] str.each_byte { |byte| assert_nil @parser.filter_body(tmp, buf << byte.chr) body << tmp } assert_equal 'abcdefghijklmnop0123456789abcdefg', body rv = @parser.filter_body(tmp, buf<< "\n") assert_equal rv.object_id, buf.object_id assert ! @parser.keepalive? end def test_trailers req = @parser.env str = @parser.buf str << "PUT / HTTP/1.1\r\n" \ "Trailer: Content-MD5\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "1\r\na\r\n2\r\n..\r\n0\r\n" assert_equal req, @parser.parse assert_equal 'Content-MD5', req['HTTP_TRAILER'] assert_nil req['HTTP_CONTENT_MD5'] tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal 'a..', tmp md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze rv = @parser.filter_body(tmp, str) assert_equal rv.object_id, str.object_id assert_equal '', str md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze str << md5_hdr assert_nil @parser.trailers(req, str) assert_equal md5_b64, req['HTTP_CONTENT_MD5'] assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str str << "\r" assert_nil @parser.parse str << "\nGET / " assert_equal req, @parser.parse assert_equal "GET / ", str assert @parser.keepalive? end def test_trailers_slowly str = @parser.buf str << "PUT / HTTP/1.1\r\n" \ "Trailer: Content-MD5\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "1\r\na\r\n2\r\n..\r\n0\r\n" req = @parser.env assert_equal req, @parser.parse assert_equal 'Content-MD5', req['HTTP_TRAILER'] assert_nil req['HTTP_CONTENT_MD5'] tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal 'a..', tmp md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze rv = @parser.filter_body(tmp, str) assert_equal rv.object_id, str.object_id assert_equal '', str assert_nil @parser.trailers(req, str) md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze md5_hdr.each_byte { |byte| str << byte.chr assert_nil @parser.trailers(req, str) } assert_equal md5_b64, req['HTTP_CONTENT_MD5'] assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str str << "\r" assert_nil @parser.parse str << "\n" assert_equal req, @parser.parse end def test_max_chunk str = @parser.buf str << "PUT / HTTP/1.1\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" req = @parser.env assert_equal req, @parser.parse assert_nil @parser.content_length @parser.filter_body('', str) assert ! @parser.keepalive? end def test_max_body n = HttpParser::LENGTH_MAX @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" req = @parser.env @parser.headers(req, @parser.buf) assert_equal n, req['CONTENT_LENGTH'].to_i assert ! @parser.keepalive? end def test_overflow_chunk n = HttpParser::CHUNK_MAX + 1 str = @parser.buf req = @parser.env str << "PUT / HTTP/1.1\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" assert_equal req, @parser.parse assert_nil @parser.content_length assert_raise(HttpParserError) { @parser.filter_body('', str) } end def test_overflow_content_length n = HttpParser::LENGTH_MAX + 1 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" assert_raise(HttpParserError) { @parser.parse } end def test_bad_chunk @parser.buf << "PUT / HTTP/1.1\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "#zzz\r\na\r\n2\r\n..\r\n0\r\n" req = @parser.env assert_equal req, @parser.parse assert_nil @parser.content_length assert_raise(HttpParserError) { @parser.filter_body("", @parser.buf) } end def test_bad_content_length @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n" assert_raise(HttpParserError) { @parser.parse } end def test_bad_trailers str = @parser.buf req = @parser.env str << "PUT / HTTP/1.1\r\n" \ "Trailer: Transfer-Encoding\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "1\r\na\r\n2\r\n..\r\n0\r\n" assert_equal req, @parser.parse assert_equal 'Transfer-Encoding', req['HTTP_TRAILER'] tmp = '' assert_nil @parser.filter_body(tmp, str) assert_equal 'a..', tmp assert_equal '', str str << "Transfer-Encoding: identity\r\n\r\n" assert_raise(HttpParserError) { @parser.parse } end def test_repeat_headers str = "PUT / HTTP/1.1\r\n" \ "Trailer: Content-MD5\r\n" \ "Trailer: Content-SHA1\r\n" \ "transfer-Encoding: chunked\r\n\r\n" \ "1\r\na\r\n2\r\n..\r\n0\r\n" req = @parser.env @parser.buf << str assert_equal req, @parser.parse assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER'] assert ! @parser.keepalive? end def test_parse_simple_request parser = HttpParser.new req = parser.env parser.buf << "GET /read-rfc1945-if-you-dont-believe-me\r\n" assert_equal req, parser.parse assert_equal '', parser.buf expect = { "SERVER_NAME"=>"localhost", "rack.url_scheme"=>"http", "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me", "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me", "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me", "SERVER_PORT"=>"80", "SERVER_PROTOCOL"=>"HTTP/0.9", "REQUEST_METHOD"=>"GET", "QUERY_STRING"=>"" } assert_equal expect, req assert ! parser.headers? end def test_path_info_semicolon qs = "QUERY_STRING" pi = "PATH_INFO" req = {} str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n" { "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, "/1;a=b" => { qs => "", pi => "/1;a=b" }, "/1;a=b?" => { qs => "", pi => "/1;a=b" }, "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, "*" => { qs => "", pi => "" }, }.each do |uri,expect| assert_equal req, @parser.headers(req.clear, str % [ uri ]) req = req.dup @parser.clear assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" assert_equal expect[qs], req[qs], "#{qs} mismatch" assert_equal expect[pi], req[pi], "#{pi} mismatch" next if uri == "*" uri = URI.parse("http://example.com#{uri}") assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees" assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees" end end def test_path_info_semicolon_absolute qs = "QUERY_STRING" pi = "PATH_INFO" req = {} str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n" { "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, "/1;a=b" => { qs => "", pi => "/1;a=b" }, "/1;a=b?" => { qs => "", pi => "/1;a=b" }, "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, }.each do |uri,expect| assert_equal req, @parser.headers(req.clear, str % [ uri ]) req = req.dup @parser.clear assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch" assert_equal expect[qs], req[qs], "#{qs} mismatch" assert_equal expect[pi], req[pi], "#{pi} mismatch" end end def test_negative_content_length req = {} str = "PUT / HTTP/1.1\r\n" \ "Content-Length: -1\r\n" \ "\r\n" assert_raises(HttpParserError) do @parser.headers(req, str) end end def test_invalid_content_length req = {} str = "PUT / HTTP/1.1\r\n" \ "Content-Length: zzzzz\r\n" \ "\r\n" assert_raises(HttpParserError) do @parser.headers(req, str) end end def test_duplicate_content_length str = "PUT / HTTP/1.1\r\n" \ "Content-Length: 1\r\n" \ "Content-Length: 9\r\n" \ "\r\n" assert_raises(HttpParserError) { @parser.headers({}, str) } end def test_chunked_overrides_content_length order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ] %w(a b).each do |x| str = "PUT /#{x} HTTP/1.1\r\n" \ "#{order.join("\r\n")}" \ "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n" order.reverse! env = @parser.headers({}, str) assert_nil @parser.content_length assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING'] assert_equal '666', env['CONTENT_LENGTH'], 'Content-Length logged so the app can log a possible client bug/attack' @parser.filter_body(dst = '', str) assert_equal 'helloworld', dst @parser.parse # handle the non-existent trailer assert @parser.next? end end def test_chunked_order_good str = "PUT /x HTTP/1.1\r\n" \ "Transfer-Encoding: gzip\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" env = @parser.headers({}, str) assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING'] assert_nil @parser.content_length @parser.clear str = "PUT /x HTTP/1.1\r\n" \ "Transfer-Encoding: gzip, chunked\r\n" \ "\r\n" env = @parser.headers({}, str) assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING'] assert_nil @parser.content_length end def test_chunked_order_bad str = "PUT /x HTTP/1.1\r\n" \ "Transfer-Encoding: chunked\r\n" \ "Transfer-Encoding: gzip\r\n" \ "\r\n" assert_raise(HttpParserError) { @parser.headers({}, str) } end def test_double_chunked str = "PUT /x HTTP/1.1\r\n" \ "Transfer-Encoding: chunked\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" assert_raise(HttpParserError) { @parser.headers({}, str) } @parser.clear str = "PUT /x HTTP/1.1\r\n" \ "Transfer-Encoding: chunked,chunked\r\n" \ "\r\n" assert_raise(HttpParserError) { @parser.headers({}, str) } end def test_backtrace_is_empty begin @parser.headers({}, "AAADFSFDSFD\r\n\r\n") assert false, "should never get here line:#{__LINE__}" rescue HttpParserError => e assert_equal [], e.backtrace return end assert false, "should never get here line:#{__LINE__}" end def test_ignore_version_header @parser.buf << "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n" req = @parser.env assert_equal req, @parser.parse assert_equal '', @parser.buf expect = { "SERVER_NAME" => "localhost", "rack.url_scheme" => "http", "REQUEST_PATH" => "/", "SERVER_PROTOCOL" => "HTTP/1.1", "PATH_INFO" => "/", "HTTP_VERSION" => "HTTP/1.1", "REQUEST_URI" => "/", "SERVER_PORT" => "80", "REQUEST_METHOD" => "GET", "QUERY_STRING" => "" } assert_equal expect, req end def test_pipelined_requests host = "example.com" expect = { "HTTP_HOST" => host, "SERVER_NAME" => host, "REQUEST_PATH" => "/", "rack.url_scheme" => "http", "SERVER_PROTOCOL" => "HTTP/1.1", "PATH_INFO" => "/", "HTTP_VERSION" => "HTTP/1.1", "REQUEST_URI" => "/", "SERVER_PORT" => "80", "REQUEST_METHOD" => "GET", "QUERY_STRING" => "" } req1 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" req2 = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n" @parser.buf << (req1 + req2) env1 = @parser.parse.dup assert_equal expect, env1 assert_equal req2, @parser.buf assert ! @parser.env.empty? assert @parser.next? assert @parser.keepalive? assert @parser.headers? assert_equal expect, @parser.env env2 = @parser.parse.dup host.replace "www.example.com" assert_equal "www.example.com", expect["HTTP_HOST"] assert_equal "www.example.com", expect["SERVER_NAME"] assert_equal expect, env2 assert_equal "", @parser.buf end end test/unit/test_server.rb000066400000000000000000000153041471646016400157220ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # # Additional work donated by contributors. See git history # for more information. require './test/test_helper' include Unicorn class TestHandler def call(env) while env['rack.input'].read(4096) end [200, { 'content-type' => 'text/plain' }, ['hello!\n']] rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n") raise e end end class TestRackAfterReply def initialize @called = false end def call(env) while env['rack.input'].read(4096) end env["rack.after_reply"] << -> { @called = true } [200, { 'content-type' => 'text/plain' }, ["after_reply_called: #{@called}"]] rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n") raise e end end class WebServerTest < Test::Unit::TestCase def setup @valid_request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n" @port = unused_port @tester = TestHandler.new redirect_test_io do @server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] ) @server.start end end def teardown redirect_test_io do wait_workers_ready("test_stderr.#$$.log", 1) File.truncate("test_stderr.#$$.log", 0) @server.stop(false) end reset_sig_handlers end def test_preload_app_config teardown tmp = Tempfile.new('test_preload_app_config') ObjectSpace.undefine_finalizer(tmp) app = lambda { || tmp.sysseek(0) tmp.truncate(0) tmp.syswrite($$) lambda { |env| [ 200, { 'content-type' => 'text/plain' }, [ "#$$\n" ] ] } } redirect_test_io do @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] ) @server.start end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i assert loader_pid != 0 assert_equal worker_pid, loader_pid teardown redirect_test_io do @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"], :preload_app => true) @server.start end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i assert_equal $$, loader_pid assert worker_pid != loader_pid ensure tmp.close! end def test_after_reply teardown redirect_test_io do @server = HttpServer.new(TestRackAfterReply.new, :listeners => [ "127.0.0.1:#@port"]) @server.start end sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") responses = sock.read(4096) assert_match %r{\AHTTP/1.[01] 200\b}, responses assert_match %r{^after_reply_called: false}, responses sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") responses = sock.read(4096) assert_match %r{\AHTTP/1.[01] 200\b}, responses assert_match %r{^after_reply_called: true}, responses sock.close end def test_simple_server results = hit(["http://localhost:#{@port}/test"]) assert_equal 'hello!\n', results[0], "Handler didn't really run" end def test_client_malformed_body bs = 15653984 sock = tcp_socket('127.0.0.1', @port) sock.syswrite("PUT /hello HTTP/1.1\r\n") sock.syswrite("Host: example.com\r\n") sock.syswrite("Transfer-Encoding: chunked\r\n") sock.syswrite("Trailer: X-Foo\r\n") sock.syswrite("\r\n") sock.syswrite("%x\r\n" % [ bs ]) sock.syswrite("F" * bs) begin File.open("/dev/urandom", "rb") { |fp| sock.syswrite(fp.sysread(16384)) } rescue end assert_nil sock.close next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) assert_equal 'hello!\n', next_client lines = File.readlines("test_stderr.#$$.log") lines = lines.grep(/^Unicorn::HttpParserError: .* true$/) assert_equal 1, lines.size end def do_test(string, chunk, close_after=nil, shutdown_delay=0) # Do not use instance variables here, because it needs to be thread safe socket = tcp_socket("127.0.0.1", @port); request = StringIO.new(string) chunks_out = 0 while data = request.read(chunk) chunks_out += socket.write(data) socket.flush sleep 0.2 if close_after and chunks_out > close_after socket.close sleep 1 end end sleep(shutdown_delay) socket.write(" ") # Some platforms only raise the exception on attempted write socket.flush end def test_trickle_attack do_test(@valid_request, 3) end def test_close_client assert_raises IOError do do_test(@valid_request, 10, 20) end end def test_bad_client redirect_test_io do do_test("GET /test HTTP/BAD", 3) end end def test_logger_set assert_equal @server.logger, Unicorn::HttpRequest::DEFAULTS["rack.logger"] end def test_logger_changed tmp = Logger.new($stdout) @server.logger = tmp assert_equal tmp, Unicorn::HttpRequest::DEFAULTS["rack.logger"] end def test_bad_client_400 sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n") assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096) assert_nil sock.close end def test_http_0_9 sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET /hello\r\n") assert_match 'hello!\n', sock.sysread(4096) assert_nil sock.close end def test_header_is_too_long redirect_test_io do long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n" assert_raises Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EINVAL, IOError do do_test(long, long.length/2, 10) end end end def test_file_streamed_request body = "a" * (Unicorn::Const::MAX_BODY * 2) long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body do_test(long, Unicorn::Const::CHUNK_SIZE * 2 - 400) end def test_file_streamed_request_bad_body body = "a" * (Unicorn::Const::MAX_BODY * 2) long = "GET /test HTTP/1.1\r\nContent-ength: #{body.length}\r\n\r\n" + body assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) { do_test(long, Unicorn::Const::CHUNK_SIZE * 2 - 400) } end def test_listener_names assert_equal [ "127.0.0.1:#@port" ], Unicorn.listener_names end end test/unit/test_signals.rb000066400000000000000000000131251471646016400160530ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or # the GPLv2+ (GPLv3+ preferred) # # Ensure we stay sane in the face of signals being sent to us require './test/test_helper' include Unicorn class Dd def initialize(bs, count) @count = count @buf = ' ' * bs end def each(&block) @count.times { yield @buf } end end class SignalsTest < Test::Unit::TestCase def setup @bs = 1 * 1024 * 1024 @count = 100 @port = unused_port @sock = Tempfile.new('unicorn.sock') @tmp = Tempfile.new('unicorn.write') @tmp.sync = true File.unlink(@sock.path) File.unlink(@tmp.path) @server_opts = { :listeners => [ "127.0.0.1:#@port", @sock.path ], :after_fork => lambda { |server,worker| trap(:HUP) { @tmp.syswrite('.') } }, } @server = nil end def teardown reset_sig_handlers end def test_worker_dies_on_dead_master pid = fork { app = lambda { |env| [ 200, {'x-pid' => "#$$" }, [] ] } opts = @server_opts.merge(:timeout => 3) redirect_test_io { HttpServer.new(app, opts).start.join } } wait_workers_ready("test_stderr.#{pid}.log", 1) sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = sock.readpartial(4096) assert_nil sock.close buf =~ /\bx-pid: (\d+)\b/ or raise Exception child = $1.to_i wait_master_ready("test_stderr.#{pid}.log") wait_workers_ready("test_stderr.#{pid}.log", 1) Process.kill(:KILL, pid) Process.waitpid(pid) File.unlink("test_stderr.#{pid}.log", "test_stdout.#{pid}.log") t0 = Time.now assert child assert t0 assert_raises(Errno::ESRCH) { loop { Process.kill(0, child); sleep 0.2 } } assert((Time.now - t0) < 60) end def test_sleepy_kill rd, wr = IO.pipe pid = fork { rd.close app = lambda { |env| wr.syswrite('.'); sleep; [ 200, {}, [] ] } redirect_test_io { HttpServer.new(app, @server_opts).start.join } } wr.close wait_workers_ready("test_stderr.#{pid}.log", 1) sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = rd.readpartial(1) wait_master_ready("test_stderr.#{pid}.log") Process.kill(:INT, pid) Process.waitpid(pid) assert_equal '.', buf buf = nil assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) do buf = sock.sysread(4096) end assert_nil buf end def test_timeout_slow_response pid = fork { app = lambda { |env| sleep } opts = @server_opts.merge(:timeout => 3) redirect_test_io { HttpServer.new(app, opts).start.join } } t0 = Time.now wait_workers_ready("test_stderr.#{pid}.log", 1) sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = nil assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) do buf = sock.sysread(4096) end diff = Time.now - t0 assert_nil buf assert diff > 1.0, "diff was #{diff.inspect}" assert diff < 60.0 ensure Process.kill(:TERM, pid) rescue nil end def test_response_write app = lambda { |env| [ 200, { 'content-type' => 'text/plain', 'x-pid' => Process.pid.to_s }, Dd.new(@bs, @count) ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } wait_workers_ready("test_stderr.#{$$}.log", 1) sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = '' header_len = pid = nil buf = sock.sysread(16384, buf) pid = buf[/\r\nx-pid: (\d+)\r\n/, 1].to_i header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size assert pid > 0, "pid not positive: #{pid.inspect}" read = buf.size size_before = @tmp.stat.size assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) do loop do 3.times { Process.kill(:HUP, pid) } sock.sysread(16384, buf) read += buf.size 3.times { Process.kill(:HUP, pid) } end end redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged assert size_before < @tmp.stat.size got = read - header_len expect = @bs * @count assert_equal(expect, got, "expect=#{expect} got=#{got}") assert_nil sock.close end def test_request_read app = lambda { |env| while env['rack.input'].read(4096) end [ 200, {'content-type'=>'text/plain', 'x-pid'=>Process.pid.to_s}, [] ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } wait_workers_ready("test_stderr.#{$$}.log", 1) sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") pid = sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i assert_nil sock.close assert pid > 0, "pid not positive: #{pid.inspect}" sock = tcp_socket('127.0.0.1', @port) sock.syswrite("PUT / HTTP/1.0\r\n") sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n") 1000.times { Process.kill(:HUP, pid) } size_before = @tmp.stat.size killer = fork { loop { Process.kill(:HUP, pid); sleep(0.01) } } buf = ' ' * @bs @count.times { sock.syswrite(buf) } Process.kill(:KILL, killer) Process.waitpid2(killer) redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged assert size_before < @tmp.stat.size assert_equal pid, sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i assert_nil sock.close end end test/unit/test_socket_helper.rb000066400000000000000000000115211471646016400172400ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require './test/test_helper' require 'tempfile' class TestSocketHelper < Test::Unit::TestCase include Unicorn::SocketHelper attr_reader :logger GET_SLASH = "GET / HTTP/1.0\r\n\r\n".freeze def setup @log_tmp = Tempfile.new 'logger' @logger = Logger.new(@log_tmp.path) @test_addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' @test6_addr = ENV['UNICORN_TEST6_ADDR'] || '::1' GC.disable end def teardown GC.enable end def test_bind_listen_tcp port = unused_port @test_addr @tcp_listener_name = "#@test_addr:#{port}" @tcp_listener = bind_listen(@tcp_listener_name) assert Socket === @tcp_listener assert @tcp_listener.local_address.ip? assert_equal @tcp_listener_name, sock_name(@tcp_listener) end def test_bind_listen_options port = unused_port @test_addr tcp_listener_name = "#@test_addr:#{port}" tmp = Tempfile.new 'unix.sock' unix_listener_name = tmp.path File.unlink(tmp.path) [ { :backlog => 5 }, { :sndbuf => 4096 }, { :rcvbuf => 4096 }, { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 } ].each do |opts| tcp_listener = bind_listen(tcp_listener_name, opts) assert tcp_listener.local_address.ip? tcp_listener.close unix_listener = bind_listen(unix_listener_name, opts) assert unix_listener.local_address.unix? unix_listener.close end end def test_bind_listen_unix old_umask = File.umask(0777) tmp = Tempfile.new 'unix.sock' @unix_listener_path = tmp.path File.unlink(@unix_listener_path) @unix_listener = bind_listen(@unix_listener_path) assert Socket === @unix_listener assert @unix_listener.local_address.unix? assert_equal @unix_listener_path, sock_name(@unix_listener) assert File.readable?(@unix_listener_path), "not readable" assert File.writable?(@unix_listener_path), "not writable" assert_equal 0777, File.umask assert_equal @unix_listener, bind_listen(@unix_listener) ensure File.umask(old_umask) end def test_bind_listen_unix_umask old_umask = File.umask(0777) tmp = Tempfile.new 'unix.sock' @unix_listener_path = tmp.path File.unlink(@unix_listener_path) @unix_listener = bind_listen(@unix_listener_path, :umask => 077) assert_equal @unix_listener_path, sock_name(@unix_listener) assert_equal 0140700, File.stat(@unix_listener_path).mode assert_equal 0777, File.umask ensure File.umask(old_umask) end def test_bind_listen_unix_rebind test_bind_listen_unix new_listener = nil assert_raises(Errno::EADDRINUSE) do new_listener = bind_listen(@unix_listener_path) end File.unlink(@unix_listener_path) new_listener = bind_listen(@unix_listener_path) assert new_listener.fileno != @unix_listener.fileno assert_equal sock_name(new_listener), sock_name(@unix_listener) assert_equal @unix_listener_path, sock_name(new_listener) pid = fork do begin client, _ = new_listener.accept client.syswrite('abcde') exit 0 rescue => e warn "#{e.message} (#{e.class})" exit 1 end end s = unix_socket(@unix_listener_path) IO.select([s]) assert_equal 'abcde', s.sysread(5) pid, status = Process.waitpid2(pid) assert status.success? end def test_tcp_defer_accept_default return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert cur >= 1 end def test_tcp_defer_accept_disable return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :tcp_defer_accept => false) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert_equal 0, cur end def test_tcp_defer_accept_nr return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :tcp_defer_accept => 60) cur = sock.getsockopt(Socket::SOL_TCP, TCP_DEFER_ACCEPT).unpack('i')[0] assert cur > 1 end def test_ipv6only port = begin unused_port "#@test6_addr" rescue Errno::EINVAL return end sock = bind_listen "[#@test6_addr]:#{port}", :ipv6only => true cur = sock.getsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY).unpack('i')[0] assert_equal 1, cur rescue Errno::EAFNOSUPPORT end def test_reuseport return unless defined?(Socket::SO_REUSEPORT) port = unused_port @test_addr name = "#@test_addr:#{port}" sock = bind_listen(name, :reuseport => true) cur = sock.getsockopt(:SOL_SOCKET, :SO_REUSEPORT).int assert_operator cur, :>, 0 rescue Errno::ENOPROTOOPT # kernel does not support SO_REUSEPORT (older Linux) end end test/unit/test_stream_input.rb000066400000000000000000000127271471646016400171340ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'test/unit' require 'digest/sha1' require 'unicorn' class TestStreamInput < Test::Unit::TestCase def setup @rs = "\n" $/ == "\n" or abort %q{test broken if \$/ != "\\n"} @env = {} @rd, @wr = UNIXSocket.pair @rd.sync = @wr.sync = true @start_pid = $$ end def teardown return if $$ != @start_pid @rd.close rescue nil @wr.close rescue nil Process.waitall end def test_read_negative r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) assert_raises(ArgumentError) { si.read(-1) } assert_equal 'hello', si.read end def test_read_small r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) assert_equal 'hello', si.read assert_equal '', si.read assert_nil si.read(5) assert_nil si.gets end def test_gets_oneliner r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) assert_equal 'hello', si.gets assert_nil si.gets end def test_gets_multiline r = init_request("a\nb\n\n") si = Unicorn::StreamInput.new(@rd, r) assert_equal "a\n", si.gets assert_equal "b\n", si.gets assert_equal "\n", si.gets assert_nil si.gets end def test_gets_empty_rs r = init_request("a\nb\n\n") si = Unicorn::StreamInput.new(@rd, r) pid = fork do # to avoid $/ warning (hopefully) $/ = nil @rd.close @wr.write(si.gets) @wr.close end @wr.close assert_equal "a\nb\n\n", @rd.read pid, status = Process.waitpid2(pid) assert_predicate status, :success? end def test_read_with_equal_len r = init_request("abcde") si = Unicorn::StreamInput.new(@rd, r) assert_equal "abcde", si.read(5) assert_nil si.read(5) end def test_big_body_multi r = init_request('.', Unicorn::Const::MAX_BODY + 1) si = Unicorn::StreamInput.new(@rd, r) assert_equal Unicorn::Const::MAX_BODY, @parser.content_length assert ! @parser.body_eof? nr = Unicorn::Const::MAX_BODY / 4 pid = fork { @rd.close nr.times { @wr.write('....') } @wr.close } @wr.close assert_equal '.', si.read(1) nr.times { |x| assert_equal '....', si.read(4), "nr=#{x}" } assert_nil si.read(1) pid, status = Process.waitpid2(pid) assert status.success? end def test_gets_long r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size) si = Unicorn::StreamInput.new(@rd, r) status = line = nil pid = fork { @rd.close 3.times { @wr.write("ffff" * 4096) } @wr.write "#{@rs}foo#{@rs}" @wr.close } @wr.close line = si.gets assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line) line = si.gets assert_equal "foo#{@rs}", line assert_nil si.gets pid, status = Process.waitpid2(pid) assert status.success? end def test_read_with_buffer r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) buf = '' rv = si.read(4, buf) assert_equal 'hell', rv assert_equal 'hell', buf assert_equal rv.object_id, buf.object_id assert_equal 'o', si.read assert_equal nil, si.read(5, buf) end def test_read_with_buffer_clobbers r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) buf = 'foo' assert_equal 'hello', si.read(nil, buf) assert_equal 'hello', buf assert_equal '', si.read(nil, buf) assert_equal '', buf buf = 'asdf' assert_nil si.read(5, buf) assert_equal '', buf end def test_read_zero r = init_request('hello') si = Unicorn::StreamInput.new(@rd, r) assert_equal '', si.read(0) buf = 'asdf' rv = si.read(0, buf) assert_equal rv.object_id, buf.object_id assert_equal '', buf assert_equal 'hello', si.read assert_nil si.read(5) assert_equal '', si.read(0) buf = 'hello' rv = si.read(0, buf) assert_equal rv.object_id, buf.object_id assert_equal '', rv end def test_gets_read_mix r = init_request("hello\nasdfasdf") si = Unicorn::StreamInput.new(@rd, r) assert_equal "hello\n", si.gets assert_equal "asdfasdf", si.read(9) assert_nil si.read(9) end def test_gets_read_mix_chunked r = @parser = Unicorn::HttpParser.new body = "6\r\nhello" @buf = "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n#{body}" assert_equal @env, @parser.headers(@env, @buf) assert_equal body, @buf si = Unicorn::StreamInput.new(@rd, r) @wr.syswrite "\n\r\n" assert_equal "hello\n", si.gets @wr.syswrite "8\r\nasdfasdf\r\n" assert_equal"asdfasdf", si.read(9) + si.read(9) @wr.syswrite "0\r\n\r\n" assert_nil si.read(9) end def test_gets_read_mix_big r = init_request("hello\n#{'.' * 65536}") si = Unicorn::StreamInput.new(@rd, r) assert_equal "hello\n", si.gets assert_equal '.' * 16384, si.read(16384) assert_equal '.' * 16383, si.read(16383) assert_equal '.' * 16384, si.read(16384) assert_equal '.' * 16385, si.read(16385) assert_nil si.gets end def init_request(body, size = nil) @parser = Unicorn::HttpParser.new body = body.to_s.freeze @buf = "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Content-Length: #{size || body.size}\r\n" \ "\r\n#{body}" assert_equal @env, @parser.headers(@env, @buf) assert_equal body, @buf @parser end end test/unit/test_tee_input.rb000066400000000000000000000174541471646016400164200ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require 'test/unit' require 'digest/sha1' require 'unicorn' class TeeInput < Unicorn::TeeInput attr_accessor :tmp, :len end class TestTeeInput < Test::Unit::TestCase def setup @rd, @wr = UNIXSocket.pair @rd.sync = @wr.sync = true @start_pid = $$ @rs = "\n" $/ == "\n" or abort %q{test broken if \$/ != "\\n"} end def teardown return if $$ != @start_pid @rd.close rescue nil @wr.close rescue nil begin Process.wait rescue Errno::ECHILD break end while true end def check_tempfiles tmp = @parser.env["rack.tempfiles"] assert_instance_of Array, tmp assert_operator tmp.size, :>=, 1 assert_instance_of Unicorn::TmpIO, tmp[0] end def test_gets_long r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size) ti = TeeInput.new(@rd, r) status = line = nil pid = fork { @rd.close 3.times { @wr.write("ffff" * 4096) } @wr.write "#{@rs}foo#{@rs}" @wr.close } @wr.close line = ti.gets assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line) line = ti.gets assert_equal "foo#{@rs}", line assert_nil ti.gets pid, status = Process.waitpid2(pid) assert status.success? end def test_gets_short r = init_request("hello", 5 + "#{@rs}foo".size) ti = TeeInput.new(@rd, r) status = line = nil pid = fork { @rd.close @wr.write "#{@rs}foo" @wr.close } @wr.close line = ti.gets assert_equal("hello#{@rs}", line) line = ti.gets assert_equal "foo", line assert_nil ti.gets pid, status = Process.waitpid2(pid) assert status.success? end def test_small_body r = init_request('hello') ti = TeeInput.new(@rd, r) assert_equal 0, @parser.content_length assert @parser.body_eof? assert_equal StringIO, ti.tmp.class assert_equal 0, ti.tmp.pos assert_equal 5, ti.size assert_equal 'hello', ti.read assert_equal '', ti.read assert_nil ti.read(4096) assert_equal 5, ti.size end def test_read_with_buffer r = init_request('hello') ti = TeeInput.new(@rd, r) buf = '' rv = ti.read(4, buf) assert_equal 'hell', rv assert_equal 'hell', buf assert_equal rv.object_id, buf.object_id assert_equal 'o', ti.read assert_equal nil, ti.read(5, buf) assert_equal 0, ti.rewind assert_equal 'hello', ti.read(5, buf) assert_equal 'hello', buf end def test_big_body r = init_request('.' * Unicorn::Const::MAX_BODY << 'a') ti = TeeInput.new(@rd, r) assert_equal 0, @parser.content_length assert @parser.body_eof? assert_kind_of File, ti.tmp assert_equal 0, ti.tmp.pos assert_equal Unicorn::Const::MAX_BODY + 1, ti.size check_tempfiles end def test_read_in_full_if_content_length a, b = 300, 3 r = init_request('.' * b, 300) assert_equal 300, @parser.content_length ti = TeeInput.new(@rd, r) pid = fork { @wr.write('.' * 197) sleep 1 # still a *potential* race here that would make the test moot... @wr.write('.' * 100) } assert_equal a, ti.read(a).size _, status = Process.waitpid2(pid) assert status.success? @wr.close end def test_big_body_multi r = init_request('.', Unicorn::Const::MAX_BODY + 1) ti = TeeInput.new(@rd, r) assert_equal Unicorn::Const::MAX_BODY, @parser.content_length assert ! @parser.body_eof? assert_kind_of File, ti.tmp assert_equal 0, ti.tmp.pos assert_equal Unicorn::Const::MAX_BODY + 1, ti.size nr = Unicorn::Const::MAX_BODY / 4 pid = fork { @rd.close nr.times { @wr.write('....') } @wr.close } @wr.close assert_equal '.', ti.read(1) assert_equal Unicorn::Const::MAX_BODY + 1, ti.size nr.times { |x| assert_equal '....', ti.read(4), "nr=#{x}" assert_equal Unicorn::Const::MAX_BODY + 1, ti.size } assert_nil ti.read(1) pid, status = Process.waitpid2(pid) assert status.success? check_tempfiles end def test_chunked @parser = Unicorn::HttpParser.new @parser.buf << "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" assert @parser.parse assert_equal "", @parser.buf pid = fork { @rd.close 5.times { @wr.write("5\r\nabcde\r\n") } @wr.write("0\r\n\r\n") } @wr.close ti = TeeInput.new(@rd, @parser) assert_nil @parser.content_length assert_nil ti.len assert ! @parser.body_eof? assert_equal 25, ti.size assert @parser.body_eof? assert_equal 25, ti.len assert_equal 0, ti.tmp.pos ti.rewind assert_equal 0, ti.tmp.pos assert_equal 'abcdeabcdeabcdeabcde', ti.read(20) assert_equal 20, ti.tmp.pos ti.rewind assert_equal 0, ti.tmp.pos assert_kind_of File, ti.tmp status = nil pid, status = Process.waitpid2(pid) assert status.success? check_tempfiles end def test_chunked_ping_pong @parser = Unicorn::HttpParser.new buf = @parser.buf buf << "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" assert @parser.parse assert_equal "", buf chunks = %w(aa bbb cccc dddd eeee) rd, wr = IO.pipe pid = fork { chunks.each do |chunk| rd.read(1) == "." and @wr.write("#{'%x' % [ chunk.size]}\r\n#{chunk}\r\n") end @wr.write("0\r\n\r\n") } ti = TeeInput.new(@rd, @parser) assert_nil @parser.content_length assert_nil ti.len assert ! @parser.body_eof? chunks.each do |chunk| wr.write('.') assert_equal chunk, ti.read(16384) end _, status = Process.waitpid2(pid) assert status.success? end def test_chunked_with_trailer @parser = Unicorn::HttpParser.new buf = @parser.buf buf << "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Trailer: Hello\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" assert @parser.parse assert_equal "", buf pid = fork { @rd.close 5.times { @wr.write("5\r\nabcde\r\n") } @wr.write("0\r\n") @wr.write("Hello: World\r\n\r\n") } @wr.close ti = TeeInput.new(@rd, @parser) assert_nil @parser.content_length assert_nil ti.len assert ! @parser.body_eof? assert_equal 25, ti.size assert_equal "World", @parser.env['HTTP_HELLO'] pid, status = Process.waitpid2(pid) assert status.success? end def test_chunked_and_size_slow @parser = Unicorn::HttpParser.new buf = @parser.buf buf << "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Trailer: Hello\r\n" \ "Transfer-Encoding: chunked\r\n" \ "\r\n" assert @parser.parse assert_equal "", buf @wr.write("9\r\nabcde") ti = TeeInput.new(@rd, @parser) assert_nil @parser.content_length assert_equal "abcde", ti.read(9) assert ! @parser.body_eof? @wr.write("fghi\r\n0\r\nHello: World\r\n\r\n") assert_equal 9, ti.size assert_equal "fghi", ti.read(9) assert_equal nil, ti.read(9) assert_equal "World", @parser.env['HTTP_HELLO'] end def test_gets_read_mix r = init_request("hello\nasdfasdf") ti = Unicorn::TeeInput.new(@rd, r) assert_equal "hello\n", ti.gets assert_equal "asdfasdf", ti.read(9) assert_nil ti.read(9) end private def init_request(body, size = nil) @parser = Unicorn::HttpParser.new body = body.to_s.freeze buf = @parser.buf buf << "POST / HTTP/1.1\r\n" \ "Host: localhost\r\n" \ "Content-Length: #{size || body.size}\r\n" \ "\r\n#{body}" assert @parser.parse assert_equal body, buf @buf = buf @parser end end test/unit/test_util.rb000066400000000000000000000074301471646016400153720ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false require './test/test_helper' require 'tempfile' class TestUtil < Test::Unit::TestCase EXPECT_FLAGS = File::WRONLY | File::APPEND def test_reopen_logs_noop tmp = Tempfile.new('') fp = File.open(tmp.path, 'ab') fp.sync = true ext = fp.external_encoding rescue nil int = fp.internal_encoding rescue nil before = fp.stat.inspect Unicorn::Util.reopen_logs assert_equal before, File.stat(fp.path).inspect assert_equal ext, (fp.external_encoding rescue nil) assert_equal int, (fp.internal_encoding rescue nil) assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & fp.fcntl(Fcntl::F_GETFL)) tmp.close! fp.close end def test_reopen_logs_renamed tmp = Tempfile.new('') tmp_path = tmp.path.freeze fp = File.open(tmp_path, 'ab') fp.sync = true ext = fp.external_encoding rescue nil int = fp.internal_encoding rescue nil before = fp.stat.inspect to = Tempfile.new('') File.rename(tmp_path, to.path) assert ! File.exist?(tmp_path) Unicorn::Util.reopen_logs assert_equal tmp_path, tmp.path assert File.exist?(tmp_path) assert before != File.stat(tmp_path).inspect assert_equal fp.stat.inspect, File.stat(tmp_path).inspect assert_equal ext, (fp.external_encoding rescue nil) assert_equal int, (fp.internal_encoding rescue nil) assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & fp.fcntl(Fcntl::F_GETFL)) assert fp.sync tmp.close! to.close! fp.close end def test_reopen_logs_renamed_with_encoding tmp = Tempfile.new('') tmp_path = tmp.path.dup.freeze Encoding.list.sample(5).each { |encoding| File.open(tmp_path, "a:#{encoding.to_s}") { |fp| fp.sync = true assert_equal encoding, fp.external_encoding assert_nil fp.internal_encoding File.unlink(tmp_path) assert ! File.exist?(tmp_path) Unicorn::Util.reopen_logs assert_equal tmp_path, fp.path assert File.exist?(tmp_path) assert_equal fp.stat.inspect, File.stat(tmp_path).inspect assert_equal encoding, fp.external_encoding assert_nil fp.internal_encoding assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & fp.fcntl(Fcntl::F_GETFL)) assert fp.sync } } tmp.close! end def test_reopen_logs_renamed_with_internal_encoding tmp = Tempfile.new('') tmp_path = tmp.path.dup.freeze full = Encoding.list full.sample(2).each { |ext| full.sample(2).each { |int| next if ext == int File.open(tmp_path, "a:#{ext.to_s}:#{int.to_s}") { |fp| fp.sync = true assert_equal ext, fp.external_encoding if ext != Encoding::BINARY assert_equal int, fp.internal_encoding end File.unlink(tmp_path) assert ! File.exist?(tmp_path) Unicorn::Util.reopen_logs assert_equal tmp_path, fp.path assert File.exist?(tmp_path) assert_equal fp.stat.inspect, File.stat(tmp_path).inspect assert_equal ext, fp.external_encoding if ext != Encoding::BINARY assert_equal int, fp.internal_encoding end assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & fp.fcntl(Fcntl::F_GETFL)) assert fp.sync } } } tmp.close! end def test_pipe r, w = Unicorn.pipe assert r assert w return if RUBY_PLATFORM !~ /linux/ begin f_getpipe_sz = 1032 IO.pipe do |a, b| a_sz = a.fcntl(f_getpipe_sz) b.fcntl(f_getpipe_sz) assert_kind_of Integer, a_sz r_sz = r.fcntl(f_getpipe_sz) assert_equal Raindrops::PAGE_SIZE, r_sz assert_operator a_sz, :>=, r_sz end rescue Errno::EINVAL # Linux <= 2.6.34 end ensure w.close r.close end end test/unit/test_waiter.rb000066400000000000000000000020431471646016400157030ustar00rootroot00000000000000# frozen_string_literal: false require 'test/unit' require 'unicorn' require 'unicorn/select_waiter' class TestSelectWaiter < Test::Unit::TestCase def test_select_timeout # n.b. this is level-triggered sw = Unicorn::SelectWaiter.new IO.pipe do |r,w| sw.get_readers(ready = [], [r], 0) assert_equal [], ready w.syswrite '.' sw.get_readers(ready, [r], 1000) assert_equal [r], ready sw.get_readers(ready, [r], 0) assert_equal [r], ready end end def test_linux # ugh, also level-triggered, unlikely to change IO.pipe do |r,w| wtr = Unicorn::Waiter.prep_readers([r]) wtr.get_readers(ready = [], [r], 0) assert_equal [], ready w.syswrite '.' wtr.get_readers(ready = [], [r], 1000) assert_equal [r], ready wtr.get_readers(ready = [], [r], 1000) assert_equal [r], ready, 'still ready (level-triggered :<)' assert_nil wtr.close end rescue SystemCallError => e warn "#{e.message} (#{e.class})" end if Unicorn.const_defined?(:Waiter) end unicorn.gemspec000066400000000000000000000036521471646016400141170ustar00rootroot00000000000000# -*- encoding: binary -*- # frozen_string_literal: false manifest = File.exist?('.manifest') ? IO.readlines('.manifest').map!(&:chomp!) : `git ls-files`.split("\n") # don't bother with tests that fork, not worth our time to get working # with `gem check -t` ... (of course we care for them when testing with # GNU make when they can run in parallel) test_files = manifest.grep(%r{\Atest/unit/test_.*\.rb\z}).map do |f| File.readlines(f).grep(/\bfork\b/).empty? ? f : nil end.compact Gem::Specification.new do |s| s.name = %q{unicorn} s.version = (ENV['VERSION'] || '6.1.0').dup s.authors = ['unicorn hackers'] s.summary = 'Rack HTTP server for fast clients and Unix' s.description = File.read('README').split("\n\n")[1] s.email = %q{unicorn-public@yhbt.net} s.executables = %w(unicorn unicorn_rails) s.extensions = %w(ext/unicorn_http/extconf.rb) s.extra_rdoc_files = IO.readlines('.document').map!(&:chomp!).keep_if do |f| File.exist?(f) end s.files = manifest s.homepage = 'https://yhbt.net/unicorn/' s.test_files = test_files # 2.5.0 is the minimum supported version. We don't specify # a maximum version to make it easier to test pre-releases, # but we do warn users if they install unicorn on an untested # version in extconf.rb s.required_ruby_version = ">= 2.5.0" # We do not have a hard dependency on rack, it's possible to load # things which respond to #call. HTTP status lines in responses # won't have descriptive text, only the numeric status. s.add_development_dependency(%q) s.add_dependency(%q, '~> 0.7') s.add_development_dependency('test-unit', '~> 3.0') # Note: To avoid ambiguity, we intentionally avoid the SPDX-compatible # 'Ruby' here since Ruby 1.9.3 switched to BSD-2-Clause, but we # inherited our license from Mongrel when Ruby was at 1.8. # We cannot automatically switch licenses when Ruby changes. s.licenses = ['GPL-2.0+', 'Ruby-1.8'] end