This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.7 (May 12, 2018)
|
||||
* Fix segfault on large numbers of connections [#843]
|
||||
|
||||
## 1.2.6 (April 30, 2018)
|
||||
* *Fix segfault when an Exception is raised from unbind callback (for real this time!)*
|
||||
* Fix race condition while initializing the machine [#756]
|
||||
* Fix for newer compilers where bind() and std::bind() conflict [#830, #831]
|
||||
* Be verbose about SSL connection errors [#807]
|
||||
* Avoid explicitly calling class methods when in class scope
|
||||
* Java: Add EM_PROTO_SSL/TLS definitions [#773, #791]
|
||||
* Java: return zero when sending data to a closed connection [#475, #804]
|
||||
* Pure Ruby: Connection::error? calls report_connection_error_status [#801]
|
||||
|
||||
## 1.2.5 (July 27, 2017)
|
||||
* Java: Use long for larger values in oneshot timer intervals [#784, #794]
|
||||
|
||||
## 1.2.4 (July 27, 2017)
|
||||
* Java: Add EM_PROTO_SSL/TLS definitions [#773, #791]
|
||||
* Fix IPv6 UDP get_peername [#788]
|
||||
* Allow for larger values in oneshot timer intervals [#784, #793]
|
||||
* Update extconf.rb to allow MinGW builds with OpenSSL 1.1.0 [#785]
|
||||
|
||||
## 1.2.3 (February 22, 2017)
|
||||
* Pure Ruby: Add get_sockname [#308, #772]
|
||||
* Fix segfault when an Exception is raised from unbind callback [#765, #766]
|
||||
* Allow destructors to throw when compiling in >= C++11 [#767]
|
||||
|
||||
## 1.2.2 (January 23, 2017)
|
||||
* Java: Fix Fixnum deprecated warning in Ruby 2.4+ [#759]
|
||||
* Fix uncaught C++ exception in file watcher and raise InvalidSignature [#512, #757]
|
||||
* Fix connection count off-by-one for epoll and kqueue [#750]
|
||||
* Fix uninitialized variable warning in EM::P::HttpClient [#749]
|
||||
* Fix missing initial value for EventableDescriptor NextHeartbeat [#748]
|
||||
* Fix hostname resolution on Solaris, Ilumos, SmartOS, et al [#745, #746]
|
||||
* Improve reliability of tests, reduce public Internet accesses in tests [#656, #666, #749]
|
||||
|
||||
## 1.2.1 (November 15, 2016)
|
||||
* Throw strerror(errno) when getsockname or getpeername fail [#683]
|
||||
* Use a single concrete implementation of getpeername/getsockname, the rest pure virtuals [#683]
|
||||
* Use gai_strerror to get the failure string from getaddrinfo [#744]
|
||||
* Fix deregistering descriptor when using KQUEUE [#728]
|
||||
* Fix to enable to work an example code in EM::Pool [#731]
|
||||
* LineText2: Add regular expression delimiter support [#706]
|
||||
* Pure Ruby: EM rescue ECONNREFUSED on initial TCP connect [#741]
|
||||
* Pure Ruby: EM SSL (working start_tls) [#712]
|
||||
* Pure Ruby: EM fixes [#707]
|
||||
* Java: Use Acceptors to get peer and sock names if not present in Connections [#743]
|
||||
|
||||
## 1.2.0.1 (March 15, 2016)
|
||||
* Fix crash when accepting IPv6 connections due to struct sockaddr_in [#698, #699]
|
||||
|
||||
## 1.2.0 (March 15, 2016)
|
||||
* Integrate work from the EventMachine-LE 1.1.x versions [#570]
|
||||
* Add start_tls options :ecdh_curve, :dhparam, :fail_if_no_peer_cert [#195, #275, #399, #665]
|
||||
* Add start_tls option :ssl_version for choosing SSL/TLS versions and ciphers [#359, #348, #603, #654]
|
||||
* Add start_tls option :sni_hostname to be passed to TLS params [#593]
|
||||
* Add method EM::Channel#num_subscribers to get the number of subscribers to a channel [#640]
|
||||
* Add support for proc-sources in EM::Iterator [#639]
|
||||
* Factor out method cleanup_machine to cleanup code from EM.run [#650]
|
||||
* Replace Exception class with StandardError [#637]
|
||||
* Close socket on close_connection even after close_connection_after_writing [#694]
|
||||
* Allow reusing of datagram socket/setting bind device [#662]
|
||||
* Handle deferred exceptions in reactor thread [#486]
|
||||
* Reimplement Queue to avoid shift/push performance problem [#311]
|
||||
* Windows: Switch from gethostbyname to getaddrinfo, support IPv6 addresses [#303, #630]
|
||||
* Windows: Use rake-compiler-dock to cross-compile gems [#627]
|
||||
* Windows: Add AppVeyor configuration for Windows CI testing [#578]
|
||||
* Windows: Bump rake-compiler to version 0.9.x [#542]
|
||||
* Fix compilation on AIX (w/ XLC) [#693]
|
||||
* Fix build on OpenBSD [#690]
|
||||
* Fix OpenSSL compile issue on AIX 7.1 [#678]
|
||||
* Fix EventMachine.fork_reactor keeps the threadpool of the original process [#425]
|
||||
* Fix to prevent event machine from stopping when a raise is done in an unbind [#327]
|
||||
|
||||
## 1.0.9.1 (January 14, 2016)
|
||||
* Fix EPROTO not defined on Windows [#676]
|
||||
* Fix missing cast to struct sockaddr * [#671]
|
||||
* Fix bug in OpenSSL path detection [#675]
|
||||
|
||||
## 1.0.9 (January 13, 2016)
|
||||
* Try more ways to detect OpenSSL [#602, #643, #661, #663, #668, #669]
|
||||
* Use WSAGetLastError in pipe.cpp same as ed.cpp [#659]
|
||||
* Test compiler flags with the C++ compiler and add them to CXXFLAGS [#634, #651]
|
||||
* Restore silent-fail on unsupported EM.epoll and EM.kqueue [#638, #649]
|
||||
* getDescriptorByFileno deprecated in JRuby 1.7.x, removed in JRuby 9000 [#642, #648]
|
||||
* Add -Wno-address always-true because on Windows rb_fd_select [#578]
|
||||
* Remove the WITHOUT_SSL constant [#578]
|
||||
* Fix SSL error when the server replies a TLS Alert to our ClientHello [#544, #653]
|
||||
* Use WSAStringToAddress in lieu of inet_pton for IPv6 address detection on Windows [#595, #632]
|
||||
* Fix nasty TCP/IPv6 bug [#595, #632]
|
||||
* Use select_large_fdset on Solaris [#611, #625]
|
||||
* Detect the Solaris Studio compiler [#611, #625]
|
||||
* Throw a message with strerror included [#136, #621]
|
||||
|
||||
## 1.0.8 (August 6, 2015)
|
||||
* fix kqueue assertion failed, postpone ArmKqueueWriter until all events are processed [#51, #176, #372, #401, #619]
|
||||
* fix Rubinius GC, crank the machine from Ruby space when running Rubinius [#201, #202, #617]
|
||||
* test to show that LineText2 preserves whitespace and newlines [#32, #622]
|
||||
* bump up compiler warnings and resolve them [#616]
|
||||
* fix Windows x64 use uintptr_t instead of unsigned long for binding pointers [#612, #615]
|
||||
* fix linetext2 unroll tail recursion to avoid stack level too deep [#609]
|
||||
* fix for compilation with SSL on windows [#601]
|
||||
* open file descriptors and sockets with O_CLOEXEC where possible [#298, #488, #591]
|
||||
* fix SmtpClient: send second EHLO after STARTTLS. [#589]
|
||||
* fix nul-terminated strings in C, use StringValueCStr instead of StringValuePtr
|
||||
|
||||
## 1.0.7 (February 10, 2015)
|
||||
* fix delay in kqueue/epoll reactor shutdown when timers exist [#587]
|
||||
* fix memory leak introduced in v1.0.5 [#586]
|
||||
* expose EM.set_simultaneous_accept_count [#420]
|
||||
* fix busy loop when EM.run and EM.next_tick are invoked from exception handler [#452]
|
||||
|
||||
## 1.0.6 (February 3, 2015)
|
||||
* add support for Rubinius Process::Status [#568]
|
||||
* small bugfixes for SmtpServer [#449]
|
||||
* update buftok.rb [#547]
|
||||
* fix assertion on Write() [#525]
|
||||
* work around mkmf.rb bug preventing gem installation [#574]
|
||||
* add pause/resume support to jruby reactor [#556]
|
||||
* fix pure ruby reactor to use 127.0.0.1 instead of localhost [#439]
|
||||
* fix compilation under macruby [#243]
|
||||
* add chunked encoding to http client [#111]
|
||||
* fix errors on win32 when dealing with pipes [1ea45498] [#105]
|
||||
|
||||
## 1.0.5 (February 2, 2015)
|
||||
* use monotonic clocks on Linux, OS X, Solaris, and Windows [#563]
|
||||
* use the rb_fd_* API to get autosized fd_sets [#502]
|
||||
* add basic tests that the DNS resolver isn't leaking timers [#571]
|
||||
* update to test-unit 2.x and improve various unit tests [#551]
|
||||
* remove EventMachine_t::Popen code marked by ifdef OBSOLETE [#551]
|
||||
* ruby 2.0 may fail at Queue.pop, so rescue and complain to $stderr [#551]
|
||||
* set file handle to INVALID_HANDLE_VALUE after closing the file [#565]
|
||||
* use `defined?` instead of rescuing NameError for flow control [#535]
|
||||
* fix closing files and sockets on Windows [#564]
|
||||
* fix file uploads in Windows [#562]
|
||||
* catch failure to fork [#539]
|
||||
* use chunks for SSL write [#545]
|
||||
|
||||
## 1.0.4 (December 19, 2014)
|
||||
* add starttls_options to smtp server [#552]
|
||||
* fix closesocket on windows [#497]
|
||||
* fix build on ruby 2.2 [#503]
|
||||
* fix build error on ruby 1.9 [#508]
|
||||
* fix timer leak during dns resolution [#489]
|
||||
* add concurrency validation to EM::Iterator [#468]
|
||||
* add get_file_descriptor to get fd for a signature [#467]
|
||||
* add EM.attach_server and EM.attach_socket_server [#465, #466]
|
||||
* calling pause from receive_data takes effect immediately [#464]
|
||||
* reactor_running? returns false after fork [#455]
|
||||
* fix infinite loop on double close [edc4d0e6, #441, #445]
|
||||
* fix compilation issue on llvm [#433]
|
||||
* fix socket error codes on win32 [ff811a81]
|
||||
* fix EM.stop latency when timers exist [8b613d05, #426]
|
||||
* fix infinite loop when system time changes [1427a2c80, #428]
|
||||
* fix crash when callin attach/detach in the same tick [#427]
|
||||
* fix compilation issue on solaris [#416]
|
||||
|
||||
## 1.0.3 (March 8, 2013)
|
||||
* EM.system was broken in 1.0.2 release [#413]
|
||||
|
||||
## 1.0.2 (March 8, 2013)
|
||||
* binary win32 gems now include fastfilereader shim [#222]
|
||||
* fix long-standing connection timeout issues [27fdd5b, igrigorik/em-http-request#222]
|
||||
* http and line protocol cleanups [#193, #151]
|
||||
* reactor return value cleanup [#225]
|
||||
* fix double require from gemspec [#284]
|
||||
* fix smtp server reset behavior [#351]
|
||||
* fix EM.system argument handling [#322]
|
||||
* ruby 1.9 compat in smtp server and stomp protocols [#349, #315]
|
||||
* fix pause from post_init [#380]
|
||||
|
||||
## 1.0.1 (February 27, 2013)
|
||||
* use rb_wait_for_single_fd() on ruby 2.0 to fix rb_thread_select() deprecation [#363]
|
||||
* fix epoll/kqueue mode in ruby 2.0 by removing calls to rb_enable_interrupt() [#248, #389]
|
||||
* fix memory leak when verifying ssl cerificates [#403]
|
||||
* fix initial connection delay [#393, #374]
|
||||
* fix build on windows [#371]
|
||||
@@ -0,0 +1,281 @@
|
||||
.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) 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
|
||||
this service 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 make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. 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.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
EventMachine is copyrighted free software owned by Francis Cianfrocca
|
||||
(blackhedd ... gmail.com). The Owner of this software permits you to
|
||||
redistribute and/or modify the software under either the terms of the GPL
|
||||
version 2 (see the file GPL), or the conditions below ("Ruby License"):
|
||||
|
||||
1. You may make and give away verbatim copies of the source form of this
|
||||
software without restriction, provided that you retain 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) give non-standard binaries non-standard names, with
|
||||
instructions on where to get the original software distribution.
|
||||
|
||||
d) make other distribution arrangements with the Owner.
|
||||
|
||||
3. You may distribute the software in object code or binary form,
|
||||
provided that you do at least ONE of the following:
|
||||
|
||||
a) distribute the binaries and library files of the software,
|
||||
together with instructions (in a 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 binaries non-standard names, with
|
||||
instructions on where to get the original software distribution.
|
||||
|
||||
d) make other distribution arrangements with the Owner.
|
||||
|
||||
4. You may modify and include parts of the software into any other
|
||||
software (possibly commercial), provided you comply with the terms in
|
||||
Sections 1, 2, and 3 above. But some files in the distribution
|
||||
are not written by the Owner, so they may be made available to you
|
||||
under different terms.
|
||||
|
||||
For the list of those files and their copying conditions, see the
|
||||
file LEGAL.
|
||||
|
||||
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 whoever 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.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# About EventMachine [](https://travis-ci.org/eventmachine/eventmachine) [](https://codeclimate.com/github/eventmachine/eventmachine/maintainability)
|
||||
|
||||
|
||||
## What is EventMachine ##
|
||||
|
||||
EventMachine is an event-driven I/O and lightweight concurrency library for Ruby.
|
||||
It provides event-driven I/O using the [Reactor pattern](http://en.wikipedia.org/wiki/Reactor_pattern),
|
||||
much like [JBoss Netty](http://www.jboss.org/netty), [Apache MINA](http://mina.apache.org/),
|
||||
Python's [Twisted](http://twistedmatrix.com), [Node.js](http://nodejs.org), libevent and libev.
|
||||
|
||||
EventMachine is designed to simultaneously meet two key needs:
|
||||
|
||||
* Extremely high scalability, performance and stability for the most demanding production environments.
|
||||
* An API that eliminates the complexities of high-performance threaded network programming,
|
||||
allowing engineers to concentrate on their application logic.
|
||||
|
||||
This unique combination makes EventMachine a premier choice for designers of critical networked
|
||||
applications, including Web servers and proxies, email and IM production systems, authentication/authorization
|
||||
processors, and many more.
|
||||
|
||||
EventMachine has been around since the early 2000s and is a mature and battle-tested library.
|
||||
|
||||
|
||||
## What EventMachine is good for? ##
|
||||
|
||||
* Scalable event-driven servers. Examples: [Thin](http://code.macournoyer.com/thin/) or [Goliath](https://github.com/postrank-labs/goliath/).
|
||||
* Scalable asynchronous clients for various protocols, RESTful APIs and so on. Examples: [em-http-request](https://github.com/igrigorik/em-http-request) or [amqp gem](https://github.com/ruby-amqp/amqp).
|
||||
* Efficient network proxies with custom logic. Examples: [Proxymachine](https://github.com/mojombo/proxymachine/).
|
||||
* File and network monitoring tools. Examples: [eventmachine-tail](https://github.com/jordansissel/eventmachine-tail) and [logstash](https://github.com/logstash/logstash).
|
||||
|
||||
|
||||
|
||||
## What platforms are supported by EventMachine? ##
|
||||
|
||||
EventMachine supports Ruby 1.8.7 through 2.6, REE, JRuby and **works well on Windows** as well
|
||||
as many operating systems from the Unix family (Linux, Mac OS X, BSD flavors).
|
||||
|
||||
|
||||
|
||||
## Install the gem ##
|
||||
|
||||
Install it with [RubyGems](https://rubygems.org/)
|
||||
|
||||
gem install eventmachine
|
||||
|
||||
or add this to your Gemfile if you use [Bundler](http://gembundler.com/):
|
||||
|
||||
gem "eventmachine"
|
||||
|
||||
|
||||
|
||||
## Getting started ##
|
||||
|
||||
For an introduction to EventMachine, check out:
|
||||
|
||||
* [blog post about EventMachine by Ilya Grigorik](http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/).
|
||||
* [EventMachine Introductions by Dan Sinclair](http://everburning.com/news/eventmachine-introductions.html).
|
||||
|
||||
|
||||
### Server example: Echo server ###
|
||||
|
||||
Here's a fully-functional echo server written with EventMachine:
|
||||
|
||||
```ruby
|
||||
require 'eventmachine'
|
||||
|
||||
module EchoServer
|
||||
def post_init
|
||||
puts "-- someone connected to the echo server!"
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
send_data ">>>you sent: #{data}"
|
||||
close_connection if data =~ /quit/i
|
||||
end
|
||||
|
||||
def unbind
|
||||
puts "-- someone disconnected from the echo server!"
|
||||
end
|
||||
end
|
||||
|
||||
# Note that this will block current thread.
|
||||
EventMachine.run {
|
||||
EventMachine.start_server "127.0.0.1", 8081, EchoServer
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## EventMachine documentation ##
|
||||
|
||||
Currently we only have [reference documentation](http://rdoc.info/github/eventmachine/eventmachine/frames) and a [wiki](https://github.com/eventmachine/eventmachine/wiki).
|
||||
|
||||
|
||||
## Community and where to get help ##
|
||||
|
||||
* Join the [mailing list](http://groups.google.com/group/eventmachine) (Google Group)
|
||||
* Join IRC channel #eventmachine on irc.freenode.net
|
||||
|
||||
|
||||
## License and copyright ##
|
||||
|
||||
EventMachine is copyrighted free software made available under the terms
|
||||
of either the GPL or Ruby's License.
|
||||
|
||||
Copyright: (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
|
||||
|
||||
## Alternatives ##
|
||||
|
||||
If you are unhappy with EventMachine and want to use Ruby, check out [Celluloid](https://celluloid.io/).
|
||||
@@ -0,0 +1,27 @@
|
||||
# EventMachine documentation guides #
|
||||
|
||||
Welcome to the documentation guides for [EventMachine](http://github.com/eventmachine/eventmachine),
|
||||
a fast and simple event-processing library for Ruby programs (à la JBoss Netty, Twisted, Node.js
|
||||
and so on).
|
||||
|
||||
## Guide list ##
|
||||
|
||||
* {file:docs/GettingStarted.md Getting started with EventMachine}
|
||||
* {file:docs/EventDrivenServers.md Writing event-driven servers}
|
||||
* {file:docs/EventDrivenClients.md Writing event-driven clients}
|
||||
* {file:docs/ConnectionFailureAndRecovery.md Connection Failure and Recovery}
|
||||
* {file:docs/TLS.md TLS (aka SSL)}
|
||||
* {file:docs/Ecosystem.md EventMachine ecosystem}: Thin, Goliath, em-http-request, em-websockets, Proxymachine and beyond
|
||||
* {file:docs/BlockingEventLoop.md On blocking the event loop: why it is harmful for performance and how to avoid it}
|
||||
* {file:docs/LightweightConcurrency.md Lightweight concurrency with EventMachine}
|
||||
* {file:docs/Deferrables.md Deferrables}
|
||||
* {file:docs/ModernKernelInputOutputAPIs.md Brief introduction to epoll, kqueue, select}
|
||||
* {file:docs/WorkingWithOtherIOSources.md Working with other IO sources such as the keyboard}
|
||||
|
||||
|
||||
## Tell us what you think! ##
|
||||
|
||||
Please take a moment and tell us what you think about this guide on the [EventMachine mailing list](http://bit.ly/jW3cR3)
|
||||
or in the #eventmachine channel on irc.freenode.net: what was unclear? What wasn't covered?
|
||||
Maybe you don't like the guide style or the grammar and spelling are incorrect? Reader feedback is
|
||||
key to making documentation better.
|
||||
@@ -0,0 +1,521 @@
|
||||
# @title Getting Started with Ruby EventMachine
|
||||
# @markup markdown
|
||||
# @author Michael S. Klishin, Dan Sinclair
|
||||
|
||||
# Getting started with Ruby EventMachine #
|
||||
|
||||
|
||||
## About this guide ##
|
||||
|
||||
This guide is a quick tutorial that helps you to get started with EventMachine for writing event-driven
|
||||
servers, clients and using it as a lightweight concurrency library.
|
||||
It should take about 20 minutes to read and study the provided code examples. This guide covers
|
||||
|
||||
* Installing EventMachine via [Rubygems](http://rubygems.org) and [Bundler](http://gembundler.com).
|
||||
* Building an Echo server, the "Hello, world"-like code example of network servers.
|
||||
* Building a simple chat, both server and client.
|
||||
* Building a very small asynchronous Websockets client.
|
||||
|
||||
|
||||
## Covered versions ##
|
||||
|
||||
This guide covers EventMachine v0.12.10 and 1.0 (including betas).
|
||||
|
||||
|
||||
## Level ##
|
||||
|
||||
This guide assumes you are comfortable (but not necessary a guru) with the command line. On Microsoft Windows™,
|
||||
we recommend you to use [JRuby](http://jruby.org) when running these examples.
|
||||
|
||||
|
||||
## Installing EventMachine ##
|
||||
|
||||
### Make sure you have Ruby installed ###
|
||||
|
||||
This guide assumes you have one of the supported Ruby implementations installed:
|
||||
|
||||
* Ruby 1.8.7
|
||||
* Ruby 1.9.2
|
||||
* [JRuby](http://jruby.org) (we recommend 1.6)
|
||||
* [Rubinius](http://rubini.us) 1.2 or higher
|
||||
* [Ruby Enterprise Edition](http://www.rubyenterpriseedition.com)
|
||||
|
||||
EventMachine works on Microsoft Windows™.
|
||||
|
||||
|
||||
### With Rubygems ###
|
||||
|
||||
To install the EventMachine gem do
|
||||
|
||||
gem install eventmachine
|
||||
|
||||
|
||||
### With Bundler ###
|
||||
|
||||
gem "eventmachine"
|
||||
|
||||
|
||||
### Verifying your installation ###
|
||||
|
||||
Lets verify your installation with this quick IRB session:
|
||||
|
||||
irb -rubygems
|
||||
|
||||
ruby-1.9.2-p180 :001 > require "eventmachine"
|
||||
=> true
|
||||
ruby-1.9.2-p180 :002 > EventMachine::VERSION
|
||||
=> "1.0.0.beta.3"
|
||||
|
||||
|
||||
## An Echo Server Example ##
|
||||
|
||||
Lets begin with the classic "Hello, world"-like example, an echo server. The echo server responds clients with the
|
||||
same data that was provided. First, here's the code:
|
||||
|
||||
{include:file:examples/guides/getting\_started/01\_eventmachine\_echo_server.rb}
|
||||
|
||||
|
||||
When run, the server binds to port 10000. We can connect using Telnet and verify it's working:
|
||||
|
||||
telnet localhost 10000
|
||||
|
||||
On my machine the output looks like:
|
||||
|
||||
~ telnet localhost 10000
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
|
||||
Let's send something to our server. Type in "Hello, EventMachine" and hit Enter. The server will respond with
|
||||
the same string:
|
||||
|
||||
~ telnet localhost 10000
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
Hello, EventMachine
|
||||
# (here we hit Enter)
|
||||
Hello, EventMachine
|
||||
# (this ^^^ is our echo server reply)
|
||||
|
||||
It works! Congratulations, you now can tell your Node.js-loving friends that you "have done some event-driven programming, too".
|
||||
Oh, and to stop Telnet, hit Control + Shift + ] and then Control + C.
|
||||
|
||||
Lets walk this example line by line and see what's going on. These lines
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
probably look familiar: you use [RubyGems](http://rubygems.org) (or [Bundler](http://gembundler.com/)) for dependencies and then require EventMachine gem. Boring.
|
||||
|
||||
Next:
|
||||
|
||||
class EchoServer < EventMachine::Connection
|
||||
def receive_data(data)
|
||||
send_data(data)
|
||||
end
|
||||
end
|
||||
|
||||
Is the implementation of our echo server. We define a class that inherits from {EventMachine::Connection}
|
||||
and a handler (aka callback) for one event: when we receive data from a client.
|
||||
|
||||
EventMachine handles the connection setup, receiving data and passing it to our handler, {EventMachine::Connection#receive_data}.
|
||||
|
||||
Then we implement our protocol logic, which in the case of Echo is pretty trivial: we send back whatever we receive.
|
||||
To do so, we're using {EventMachine::Connection#send_data}.
|
||||
|
||||
Lets modify the example to recognize `exit` command:
|
||||
|
||||
{include:file:examples/guides/getting\_started/02\_eventmachine\_echo_server\_that\_recognizes\_exit\_command.rb}
|
||||
|
||||
Our `receive\_data` changed slightly and now looks like this:
|
||||
|
||||
def receive_data(data)
|
||||
if data.strip =~ /exit$/i
|
||||
EventMachine.stop_event_loop
|
||||
else
|
||||
send_data(data)
|
||||
end
|
||||
end
|
||||
|
||||
Because incoming data has trailing newline character, we strip it off before matching it against a simple regular
|
||||
expression. If the data ends in `exit`, we stop EventMachine event loop with {EventMachine.stop_event_loop}. This unblocks
|
||||
main thread and it finishes execution, and our little program exits as the result.
|
||||
|
||||
To summarize this first example:
|
||||
|
||||
* Subclass {EventMachine::Connection} and override {EventMachine::Connection#send_data} to handle incoming data.
|
||||
* Use {EventMachine.run} to start EventMachine event loop and then bind echo server with {EventMachine.start_server}.
|
||||
* To stop the event loop, use {EventMachine.stop_event_loop} (aliased as {EventMachine.stop})
|
||||
|
||||
Lets move on to a slightly more sophisticated example that will introduce several more features and methods
|
||||
EventMachine has to offer.
|
||||
|
||||
|
||||
## A Simple Chat Server Example ##
|
||||
|
||||
Next we will write a simple chat. Initially clients will still use telnet to connect, but then we will add little
|
||||
client application that will serve as a proxy between telnet and the chat server. This example is certainly longer
|
||||
(~ 150 lines with whitespace and comments) so instead of looking at the final version and going through it line by line,
|
||||
we will instead begin with a very simple version that only keeps track of connected clients and then add features
|
||||
as we go.
|
||||
|
||||
To set some expectations about our example:
|
||||
|
||||
* It will keep track of connected clients
|
||||
* It will support a couple of commands, à la IRC
|
||||
* It will support direct messages using Twitter-like @usernames
|
||||
* It won't use MongoDB, fibers or distributed map/reduce for anything but will be totally [Web Scale™](http://bit.ly/webscaletm) nonetheless. Maybe even [ROFLscale](http://bit.ly/roflscalevideo).
|
||||
|
||||
### Step one: detecting connections and disconnectons ###
|
||||
|
||||
First step looks like this:
|
||||
|
||||
{include:file:examples/guides/getting\_started/04\_simple\_chat\_server\_step\_one.rb}
|
||||
|
||||
We see familiar {EventMachine.run} and {EventMachine.start_server}, but also {EventMachine::Connection#post_init} and {EventMachine::Connection#unbind} we haven't
|
||||
met yet. We don't use them in this code, so when are they run? Like {EventMachine::Connection#receive_data}, these methods are callbacks. EventMachine calls them
|
||||
when certain events happen:
|
||||
|
||||
* {EventMachine#post_init} is called by the event loop immediately after the network connection has been established.
|
||||
In the chat server example case, this is when a new client connects.
|
||||
* {EventMachine#unbind} is called when client disconnects, connection is closed or is lost (because of a network issue, for example).
|
||||
|
||||
All our chat server does so far is logging connections or disconnections. What we want it to do next is to keep track of connected clients.
|
||||
|
||||
|
||||
### Step two: keep track of connected clients ###
|
||||
|
||||
Next iteration of the code looks like this:
|
||||
|
||||
{include:file:examples/guides/getting\_started/05\_simple\_chat\_server\_step\_two.rb}
|
||||
|
||||
While the code we added is very straightforward, we have to clarify one this first: subclasses of {EventMachine::Connection} are instantiated by
|
||||
EventMachine for every new connected peer. So for 10 connected chat clients, there will be 10 separate `SimpleChatServer` instances in our
|
||||
server process. Like any other objects, they can be stored in a collection, can provide public API other objects use, can instantiate or inject
|
||||
dependencies and in general live a happy life all Ruby objects live until garbage collection happens.
|
||||
|
||||
In the example above we use a @@class_variable to keep track of connected clients. In Ruby, @@class variables are accessible from instance
|
||||
methods so we can add new connections to the list from `SimpleChatServer#post_init` and remove them in `SimpleChatServer#unbind`. We can also
|
||||
filter connections by some criteria, as `SimpleChatServer#other_peers demonstrates`.
|
||||
|
||||
So, we keep track of connections but how do we identify them? For a chat app, it's pretty common to use usernames for that. Lets ask our clients
|
||||
to enter usernames when they connect.
|
||||
|
||||
|
||||
### Step three: adding usernames ##
|
||||
|
||||
To add usernames, we need to add a few things:
|
||||
|
||||
* We need to invite newly connected clients to enter their username.
|
||||
* A reader (getter) method on our {EventMachine::Connection} subclass.
|
||||
* An idea of connection state (keeping track of whether a particular participant had entered username before).
|
||||
|
||||
Here is one way to do it:
|
||||
|
||||
{include:file:examples/guides/getting\_started/06\_simple\_chat\_server\_step\_three.rb}
|
||||
|
||||
This is quite an update so lets take a look at each method individually. First, `SimpleChatServer#post_init`:
|
||||
|
||||
def post_init
|
||||
@username = nil
|
||||
puts "A client has connected..."
|
||||
ask_username
|
||||
end
|
||||
|
||||
To keep track of username we ask chat participants for, we add @username instance variable to our connection class. Connection
|
||||
instances are just Ruby objects associated with a particular connected peer, so using @ivars is very natural. To make username
|
||||
value accessible to other objects, we added a reader method that was not shown on the snippet above.
|
||||
|
||||
Lets dig into `SimpleChatServer#ask_username`:
|
||||
|
||||
def ask_username
|
||||
self.send_line("[info] Enter your username:")
|
||||
end # ask_username
|
||||
|
||||
# ...
|
||||
|
||||
def send_line(line)
|
||||
self.send_data("#{line}\n")
|
||||
end # send_line(line)
|
||||
|
||||
Nothing new here, we are using {EventMachine::Connection#send_data} which we have seen before.
|
||||
|
||||
|
||||
In `SimpleChatServer#receive_data` we now have to check if the username was entered or we need
|
||||
to ask for it:
|
||||
|
||||
def receive_data(data)
|
||||
if entered_username?
|
||||
handle_chat_message(data.strip)
|
||||
else
|
||||
handle_username(data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
# ...
|
||||
|
||||
def entered_username?
|
||||
!@username.nil? && !@username.empty?
|
||||
end # entered_username?
|
||||
|
||||
Finally, handler of chat messages is not yet implemented:
|
||||
|
||||
def handle_chat_message(msg)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
Lets try this example out using Telnet:
|
||||
|
||||
~ telnet localhost 10000
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
[info] Enter your username:
|
||||
antares_
|
||||
[info] Ohai, antares_
|
||||
|
||||
and the server output:
|
||||
|
||||
A client has connected...
|
||||
antares_ has joined
|
||||
|
||||
This version requires you to remember how to terminate your Telnet session (Ctrl + Shift + ], then Ctrl + C).
|
||||
It is annoying, so why don't we add the same `exit` command to our chat server?
|
||||
|
||||
|
||||
### Step four: adding exit command and delivering chat messages ####
|
||||
|
||||
{include:file:examples/guides/getting\_started/07\_simple\_chat\_server\_step\_four.rb}
|
||||
|
||||
TBD
|
||||
|
||||
Lets test-drive this version. Client A:
|
||||
|
||||
~ telnet localhost 10000
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
[info] Enter your username:
|
||||
michael
|
||||
[info] Ohai, michael
|
||||
Hi everyone
|
||||
michael: Hi everyone
|
||||
joe has joined the room
|
||||
# here ^^^ client B connects, lets greet him
|
||||
hi joe
|
||||
michael: hi joe
|
||||
joe: hey michael
|
||||
# ^^^ client B replies
|
||||
exit
|
||||
# ^^^ out command in action
|
||||
Connection closed by foreign host.
|
||||
|
||||
Client B:
|
||||
|
||||
~ telnet localhost 10000
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
[info] Enter your username:
|
||||
joe
|
||||
[info] Ohai, joe
|
||||
michael: hi joe
|
||||
# ^^^ client A greets us, lets reply
|
||||
hey michael
|
||||
joe: hey michael
|
||||
exit
|
||||
# ^^^ out command in action
|
||||
Connection closed by foreign host.
|
||||
|
||||
And finally, the server output:
|
||||
|
||||
A client has connected...
|
||||
michael has joined
|
||||
A client has connected...
|
||||
_antares has joined
|
||||
[info] _antares has left
|
||||
[info] michael has left
|
||||
|
||||
Our little char server now supports usernames, sending messages and the `exit` command. Next up, private (aka direct) messages.
|
||||
|
||||
|
||||
### Step five: adding direct messages and one more command ###
|
||||
|
||||
To add direct messages, we come up with a simple convention: private messages begin with @username and may have optional colon before
|
||||
message text, like this:
|
||||
|
||||
@joe: hey, how do you like eventmachine?
|
||||
|
||||
This convention makes parsing of messages simple so that we can concentrate on delivering them to a particular client connection.
|
||||
Remember when we added `username` reader on our connection class? That tiny change makes this step possible: when a new direct
|
||||
message comes in, we extract username and message text and then find then connection for @username in question:
|
||||
|
||||
#
|
||||
# Message handling
|
||||
#
|
||||
|
||||
def handle_chat_message(msg)
|
||||
if command?(msg)
|
||||
self.handle_command(msg)
|
||||
else
|
||||
if direct_message?(msg)
|
||||
self.handle_direct_message(msg)
|
||||
else
|
||||
self.announce(msg, "#{@username}:")
|
||||
end
|
||||
end
|
||||
end # handle_chat_message(msg)
|
||||
|
||||
def direct_message?(input)
|
||||
input =~ DM_REGEXP
|
||||
end # direct_message?(input)
|
||||
|
||||
def handle_direct_message(input)
|
||||
username, message = parse_direct_message(input)
|
||||
|
||||
if connection = @@connected_clients.find { |c| c.username == username }
|
||||
puts "[dm] @#{@username} => @#{username}"
|
||||
connection.send_line("[dm] @#{@username}: #{message}")
|
||||
else
|
||||
send_line "@#{username} is not in the room. Here's who is: #{usernames.join(', ')}"
|
||||
end
|
||||
end # handle_direct_message(input)
|
||||
|
||||
def parse_direct_message(input)
|
||||
return [$1, $2] if input =~ DM_REGEXP
|
||||
end # parse_direct_message(input)
|
||||
|
||||
This snippet demonstrates how one connection instance can obtain another connection instance and send data to it.
|
||||
This is a very powerful feature, consider just a few use cases:
|
||||
|
||||
* Peer-to-peer protocols
|
||||
* Content-aware routing
|
||||
* Efficient streaming with optional filtering
|
||||
|
||||
Less common use cases include extending C++ core of EventMachine to provide access to hardware that streams events that
|
||||
can be re-broadcasted to any interested parties connected via TCP, UDP or something like AMQP or WebSockets. With this,
|
||||
sky is the limit. Actually, EventMachine has several features for efficient proxying data between connections.
|
||||
We will not cover them in this guide.
|
||||
|
||||
One last feature that we are going to add to our chat server is the `status` command that tells you current server time and how many people
|
||||
are there in the chat room:
|
||||
|
||||
#
|
||||
# Commands handling
|
||||
#
|
||||
|
||||
def command?(input)
|
||||
input =~ /(exit|status)$/i
|
||||
end # command?(input)
|
||||
|
||||
def handle_command(cmd)
|
||||
case cmd
|
||||
when /exit$/i then self.close_connection
|
||||
when /status$/i then self.send_line("[chat server] It's #{Time.now.strftime('%H:%M')} and there are #{self.number_of_connected_clients} people in the room")
|
||||
end
|
||||
end # handle_command(cmd)
|
||||
|
||||
Hopefully this piece of code is easy to follow. Try adding a few more commands, for example, the `whoishere` command that lists people
|
||||
currently in the chat room.
|
||||
|
||||
In the end, our chat server looks like this:
|
||||
|
||||
{include:file:examples/guides/getting\_started/08\_simple\_chat\_server\_step\_five.rb}
|
||||
|
||||
We are almost done with the server but there are some closing thoughts.
|
||||
|
||||
|
||||
### Step six: final version ###
|
||||
|
||||
Just in case, here is the final version of the chat server code we have built:
|
||||
|
||||
{include:file:examples/guides/getting\_started/03\_simple\_chat\_server.rb}
|
||||
|
||||
|
||||
### Step seven: future directions and some closing thoughts ###
|
||||
|
||||
The chat server is just about 150 lines of Ruby including empty lines and comments, yet it has a few features most of chat server
|
||||
examples never add. We did not, however, implement many other features that popular IRC clients like [Colloquy](http://colloquy.info) have:
|
||||
|
||||
* Chat moderation
|
||||
* Multiple rooms
|
||||
* Connection timeout detection
|
||||
|
||||
How would one go about implementing them? We thought it is worth discussing what else EventMachine has to offer and what ecosystem projects
|
||||
one can use to build a really feature-rich Web-based IRC chat client.
|
||||
|
||||
With multiple rooms it's more or less straightforward, just add one more hash and a bunch of commands and use the information about which rooms participant
|
||||
is in when you are delivering messages. There is nothing in EventMachine itself that can make the job much easier for developer.
|
||||
|
||||
To implement chat moderation feature you may want to do a few things:
|
||||
|
||||
* Work with client IP addresses. Maybe we want to consider everyone who connects from certain IPs a moderator.
|
||||
* Access persistent data about usernames of moderators and their credentials.
|
||||
|
||||
Does EventMachine have anything to offer here? It does. To obtain peer IP address, take a look at {EventMachine::Connection#get_peername}. The name of this method is
|
||||
a little bit misleading and originates from low-level socket programming APIs.
|
||||
|
||||
#### A whirlwind tour of the EventMachine ecosystem ####
|
||||
|
||||
To work with data stores you can use several database drivers that ship with EventMachine itself, however, quite often there are some 3rd party projects in
|
||||
the EventMachine ecosystem that have more features, are faster or just better maintained. So we figured it will be helpful to provide a few pointers
|
||||
to some of those projects:
|
||||
|
||||
* For MySQL, check out [em-mysql](https://github.com/eventmachine/em-mysql) project.
|
||||
* For PostgreSQL, have a look at Mike Perham's [EventMachine-based PostgreSQL driver](https://github.com/mperham/em_postgresql).
|
||||
* For Redis, there is a young but already popular [em-hiredis](https://github.com/mloughran/em-hiredis) library that combines EventMachine's non-blocking I/O with
|
||||
extreme performance of the official Redis C client, [hiredis](https://github.com/antirez/hiredis).
|
||||
* For MongoDB, see [em-mongo](https://github.com/bcg/em-mongo)
|
||||
* For Cassandra, Mike Perham [added transport agnosticism feature](http://www.mikeperham.com/2010/02/09/cassandra-and-eventmachine/) to the [cassandra gem](https://rubygems.org/gems/cassandra).
|
||||
|
||||
[Riak](http://www.basho.com/products_riak_overview.php) and CouchDB talk HTTP so it's possible to use [em-http-request](https://github.com/igrigorik/em-http-request).
|
||||
If you are aware of EventMachine-based non-blocking drivers for these databases, as well as for HBase, let us know on the [EventMachine mailing list](http://groups.google.com/group/eventmachine).
|
||||
Also, EventMachine supports TLS (aka SSL) and works well on [JRuby](http://jruby.org) and Windows.
|
||||
|
||||
Learn more in our {file:docs/Ecosystem.md EventMachine ecosystem} and {file:docs/TLS.md TLS (aka SSL)} guides.
|
||||
|
||||
|
||||
#### Connection loss detection ####
|
||||
|
||||
Finally, connection loss detection. When our chat participant closes her laptop lid, how do we know that she is no longer active? The answer is, when EventMachine
|
||||
detects TCP connectin closure, it calls {EventMachine::Connection#unbind}. Version 1.0.beta3 and later also pass an optional argument to that method. The argument
|
||||
indicates what error (if any) caused the connection to be closed.
|
||||
|
||||
Learn more in our {file:docs/ConnectionFailureAndRecovery.md Connection Failure and Recovery} guide.
|
||||
|
||||
|
||||
#### What the Chat Server Example doesn't demonstrate ####
|
||||
|
||||
This chat server also leaves out something production quality clients and servers must take care of: buffering. We intentionally did not include any buffering in
|
||||
our chat server example: it would only distract you from learning what you really came here to learn: how to use EventMachine to build blazing fast asynchronous
|
||||
networking programs quickly. However, {EventMachine::Connection#receive_data} does not offer any guarantees that you will be receiving "whole messages" all the time,
|
||||
largely because the underlying transport (UDP or TCP) does not offer such guarantees. Many protocols, for example, AMQP, mandate that large content chunks are
|
||||
split into smaller _frames_ of certain size. This means that [amq-client](https://github.com/ruby-amqp/amq-client) library, for instance, that has EventMachine-based driver,
|
||||
has to deal with figuring out when exactly we received "the whole message". To do so, it uses buffering and employs various checks to detect _frame boundaries_.
|
||||
So **don't be deceived by the simplicity of this chat example**: it intentionally leaves framing out, but real world protocols usually require it.
|
||||
|
||||
|
||||
|
||||
## A (Proxying) Chat Client Example ##
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
## Wrapping up ##
|
||||
|
||||
This tutorial ends here. Congratulations! You have learned quite a bit about EventMachine.
|
||||
|
||||
|
||||
## What to read next ##
|
||||
|
||||
The documentation is organized as a {file:docs/DocumentationGuidesIndex.md number of guides}, covering all kinds of
|
||||
topics. TBD
|
||||
|
||||
|
||||
## Tell us what you think! ##
|
||||
|
||||
Please take a moment and tell us what you think about this guide on the [EventMachine mailing list](http://bit.ly/jW3cR3)
|
||||
or in the #eventmachine channel on irc.freenode.net: what was unclear? What wasn't covered?
|
||||
Maybe you don't like the guide style or the grammar and spelling are incorrect? Reader feedback is
|
||||
key to making documentation better.
|
||||
@@ -0,0 +1,211 @@
|
||||
01Oct06: Replaced EventMachine#open_datagram_server with a version that can
|
||||
take a Class or a Module, instead of just a Module. Thanks to Tobias
|
||||
Gustafsson for pointing out the missing case.
|
||||
04Oct06: Supported subsecond timer resolutions, per request by Jason Roelofs.
|
||||
05Oct06: Added EventMachine#set_quantum, which sets the timer resolution.
|
||||
15Nov06: Added Connection#set_comm_inactivity_timeout.
|
||||
15Nov06: Checked in a Line-and-Text Protocol Handler.
|
||||
18Nov06: Checked in a Header-and-Body Protocol Handler.
|
||||
22Nov06: Changed EventMachine#reconnect: no longer excepts when called on an
|
||||
already-connected handler.
|
||||
28Nov06: Supported a binary-unix gem.
|
||||
19Dec06: Added EventMachine#set_effective_user.
|
||||
05Jan07: Upped max outstanding timers to 1000.
|
||||
15May07: Applied Solaris patches from Brett Eisenberg
|
||||
22May07: Cleaned up the license text in all the source files.
|
||||
22May07: Released version 0.7.2
|
||||
|
||||
23May07: Per suggestion from Bill Kelly, fixed a bug with the initialization
|
||||
of the network libraries under Windows. The goal is to enable EM to
|
||||
be used without Ruby.
|
||||
28May07: Applied patch from Bill Kelly, refactors the declarations of
|
||||
event names to make EM easier to use from C programs without Ruby.
|
||||
31May07: Added a preliminary implementation of EventMachine#popen.
|
||||
01Jun07: Added EM, a "pseudo-alias" for EventMachine.
|
||||
01Jun07: Added EM#next_tick.
|
||||
01Jun07: Added EM::Connection#get_outbound_data_size
|
||||
05Jun07: Removed the code which loads a pure-Ruby EM library in case the
|
||||
compiled extension is unavailable. Suggested by Moshe Litvin.
|
||||
06Jun07: Preliminary epoll implementation.
|
||||
12Jun07: Added an evented popen implementation that, like Ruby's, is
|
||||
full-duplex and makes the subprocess PID available to the caller.
|
||||
06Jul07: Performance-tweaked the callback dispatcher in eventmachine.rb.
|
||||
10Jul07: Released version 0.8.0.
|
||||
12Jul07: Applied patches from Tim Pease to fix Solaris build problems.
|
||||
15Jul07: Created a new provisional source branch, experiments/jruby-1.
|
||||
This is a preliminary implementation of the EM reactor in Java,
|
||||
suitable for use with JRuby.
|
||||
17Jul07: Added EventMachine#stop_server, per request from Kirk Haines,
|
||||
and associated unit tests.
|
||||
22Jul07: Added EventMachine#stream_file_data. This is a very fast and scalable
|
||||
way of sending data from static files over network connections. It
|
||||
has separate implementations for small files and large file, and
|
||||
has tunings to minimize memory consumption.
|
||||
26Jul07: Added some patches by Kirk Haines to improve the behavior of
|
||||
EM::Connection#send_file_data_to_connection.
|
||||
26Jul07: Added a C++ module for directly integrating EM into C++ programs
|
||||
with no Ruby dependencies. Needs example code.
|
||||
29Jul07: Added EventMachine::Protocols::LineText2.
|
||||
29Jul07: Added EventMachine::Protocols::Stomp.
|
||||
30Jul07: Added sys/stat.h to project.h to fix compilation bug on Darwin.
|
||||
13Aug07: Added EventMachine#reactor_running?
|
||||
15Aug07: Added parameters for EventMachine::Connection:start_tls that can be
|
||||
used to specify client-side private keys and certificates.
|
||||
17Aug07: Added EventMachine#run_block, a sugaring for a common use case.
|
||||
24Aug07: Added a preliminary keyboard handler. Needs docs and testing on
|
||||
windows.
|
||||
26Aug07: Created EventMachine::Spawnable, an implementation of Erlang-like
|
||||
processes.
|
||||
27Aug07: Silenced some -w warnings, requested by James Edward Gray II.
|
||||
30Aug07: Added cookies to EM::HttpClient#request.
|
||||
04Sep07: Added an initial implementation of an evented SMTP client.
|
||||
04Sep07: Added an initial implementation of an evented SMTP server.
|
||||
10Sep07: Changed EM#spawn to run spawned blocks in the context of the
|
||||
SpawnedProcess object, not of whatever was the active object at the
|
||||
time of the spawn.
|
||||
14Sep07: Heartbeats weren't working with EPOLL. Noticed by Brian Candler.
|
||||
15Sep07: Added some features, tests and documents to Deferrable.
|
||||
16Sep07: Added [:content] parameter to EM::Protocols::SmtpClient#send.
|
||||
16Sep07: Bumped version to 0.9.0 in anticipation of a release.
|
||||
18Sep07: Released version 0.9.0.
|
||||
19Sep07: Added #receive_reset to EM::Protocols::SmtpServer.
|
||||
19Sep07: User overrides of EM::Protocols::SmtpServer#receive_recipient can now
|
||||
return a Deferrable. Also fixed bug: SmtpClient now raises a protocol
|
||||
error if none of its RCPT TO: commands are accepted by the server.
|
||||
26Sep07: Fixed missing keyboard support for Windows.
|
||||
03Oct07: Added a default handler for RuntimeErrors emitted from user-written
|
||||
code. Suggested by Brian Candler.
|
||||
19Oct07: Set the SO_BROADCAST option automatically on all UDP sockets.
|
||||
10Nov07: Forced integer conversion of send_datagram's port parameter.
|
||||
Suggested by Matthieu Riou.
|
||||
12Nov07: Added saslauth.rb, a protocol module to replace the Cyrus SASL
|
||||
daemons saslauthd and pwcheck.
|
||||
15Nov07: Fixed bug reported by Mark Zvillius. We were failing to dispatch
|
||||
zero-length datagrams under certain conditions.
|
||||
19Nov07: Added EventMachine#set_max_timers. Requested by Matthieu Riou and
|
||||
others.
|
||||
19Nov07: Fixed bug with EM::Connection#start_tls. Was not working with server
|
||||
connections. Reported by Michael S. Fischer.
|
||||
26Nov07: Supported a hack for EventMachine#popen so it can return an exit
|
||||
status from subprocesses. Requested by Michael S. Fischer.
|
||||
30Nov07: Changed Pipe descriptors so that the child-side of the socketpair is
|
||||
NOT set nonblocking. Suggested by Duane Johnson.
|
||||
05Dec07: Re-enabled the pure-Ruby implementation.
|
||||
06Dec07: Released Version 0.10.0.
|
||||
13Dec07: Added EM::DeferrableChildProcess
|
||||
24Dec07: Added a SASL client for simple password authentication.
|
||||
27Dec07: Removed the hookable error handler. No one was using it and it significantly
|
||||
degraded performance.
|
||||
30Dec07: Implemented Kqueue support for OSX and BSD.
|
||||
04Jan08: Fixed bug in epoll ("Bad file descriptor"), patch supplied by Chris
|
||||
Heath.
|
||||
04Jan08: Fixed bug reported by Michael S. Fischer. We were terminating
|
||||
SSL connections that sent data before the handshake was complete.
|
||||
08Jan08: Added an OpenBSD branch for extconf.rb, contributed by Guillaume
|
||||
Sellier.
|
||||
19Jan08: Added EM::Connection::get_sockname per request by Michael Fischer.
|
||||
19Jan08: Supported IPv6 addresses.
|
||||
30Apr08: Set the NODELAY option on sockets that we connect to other servers.
|
||||
Omission noted by Roger Pack.
|
||||
14May08: Generated a 0.12 release.
|
||||
15May08: Supported EM#get_sockname for acceptors (TCP server sockets).
|
||||
Requested by Roger Pack.
|
||||
15May08; Accepted a patch from Dan Aquino that allows the interval of a
|
||||
PeriodicTimer to be changed on the fly.
|
||||
15Jun08: Supported nested calls to EM#run. Many people contributed ideas to
|
||||
this, notably raggi and tmm1.
|
||||
20Jul08: Accepted patch from tmm1 for EM#fork_reactor.
|
||||
28Jul08: Added a Postgres3 implementation, written by FCianfrocca.
|
||||
14Aug08: Added a patch by Mike Murphy to support basic auth in the http
|
||||
client.
|
||||
28Aug08: Added a patch by tmm1 to fix a longstanding problem with Java
|
||||
data-sends.
|
||||
13Sep08: Added LineText2#set_binary_mode, a back-compatibility alias.
|
||||
13Sep08: Modified the load order of protocol libraries in eventmachine.rb
|
||||
to permit a modification of HeaderAndContentProtocol.
|
||||
13Sep08: Modified HeaderAndContent to use LineText2, which is less buggy
|
||||
than LineAndTextProtocol. This change may be reversed if we can fix
|
||||
the bugs in buftok.
|
||||
13Sep08: Improved the password handling in the Postgres protocol handler.
|
||||
15Sep08: Added attach/detach, contributed by Aman Gupta (tmm1) and Riham Aldakkak,
|
||||
to support working with file descriptors not created in the reactor.
|
||||
16Sep08: Added an optional version string to the HTTP client. This is a hack
|
||||
that allows a client to specify a version 1.0 request, which
|
||||
keeps the server from sending a chunked response. The right way to
|
||||
solve this, of course, is to support chunked responses.
|
||||
23Sep08: ChangeLog Summary for Merge of branches/raggi
|
||||
Most notable work and patches by Aman Gupta, Roger Pack, and James Tucker.
|
||||
Patches / Tickets also submitted by: Jeremy Evans, aanand, darix, mmmurf,
|
||||
danielaquino, macournoyer.
|
||||
- Moved docs into docs/ dir
|
||||
- Major refactor of rakefile, added generic rakefile helpers in tasks
|
||||
- Added example CPP build rakefile in tasks/cpp.rake
|
||||
- Moved rake tests out to tasks/tests.rake
|
||||
- Added svn ignores where appropriate
|
||||
- Fixed jruby build on older java platforms
|
||||
- Gem now builds from Rakefile rather than directly via extconf
|
||||
- Gem unified for jruby, C++ and pure ruby.
|
||||
- Correction for pure C++ build, removing ruby dependency
|
||||
- Fix for CYGWIN builds on ipv6
|
||||
- Major refactor for extconf.rb
|
||||
- Working mingw builds
|
||||
- extconf optionally uses pkg_config over manual configuration
|
||||
- extconf builds for 1.9 on any system that has 1.9
|
||||
- extconf no longer links pthread explicitly
|
||||
- looks for kqueue on all *nix systems
|
||||
- better error output on std::runtime_error, now says where it came from
|
||||
- Fixed some tests on jruby
|
||||
- Added test for general send_data flaw, required for a bugfix in jruby build
|
||||
- Added timeout to epoll tests
|
||||
- Added fixes for java reactor ruby api
|
||||
- Small addition of some docs in httpclient.rb and httpcli2.rb
|
||||
- Some refactor and fixes in smtpserver.rb
|
||||
- Added parenthesis where possible to avoid excess ruby warnings
|
||||
- Refactor of $eventmachine_library logic for accuracy and maintenance, jruby
|
||||
- EM::start_server now supports unix sockets
|
||||
- EM::connect now supports unix sockets
|
||||
- EM::defer @threadqueue now handled more gracefully
|
||||
- Added better messages on exceptions raised
|
||||
- Fix edge case in timer fires
|
||||
- Explicitly require buftok.rb
|
||||
- Add protocols to autoload, rather than require them all immediately
|
||||
- Fix a bug in pr_eventmachine for outbound_q
|
||||
- Refactors to take some of the use of defer out of tests.
|
||||
- Fixes in EM.defer under start/stop conditions. Reduced scope of threads.
|
||||
23Sep08: Added patch from tmm1 to avoid popen errors on exit.
|
||||
30Sep08: Added File.exists? checks in the args for start_tls, as suggested by
|
||||
Brian Lopez (brianmario).
|
||||
10Nov08: ruby 1.9 compatibility enhancements
|
||||
28Nov08: Allow for older ruby builds where RARRAY_LEN is not defined
|
||||
03Dec08: allow passing arguments to popen handlers
|
||||
13Jan09: SSL support for httpclient2 (David Smalley)
|
||||
22Jan09: Fixed errors on OSX with the kqueue reactor, fixed errors in the pure
|
||||
ruby reactor. Added EM.current_time. Added EM.epoll? and EM.kqueue?
|
||||
27Jan09: Reactor errors are now raised as ruby RuntimeErrors.
|
||||
28Jan09: Documentation patch from alloy
|
||||
29Jan09: (Late sign-off) Use a longer timeout for connect_server (Ilya
|
||||
Grigorik)
|
||||
07Feb09: Fix signal handling issues with threads+epoll
|
||||
07Feb09: Use rb_thread_schedule in the epoll reactor
|
||||
07Feb09: Use TRAP_BEG/END and rb_thread_schedule in kqueue reactor
|
||||
08Feb09: Added fastfilereader from swiftiply
|
||||
08Feb09: 1.9 fix for rb_trap_immediate
|
||||
08Feb09: Enable rb_thread_blocking_region for 1.9.0 and 1.9.1
|
||||
10Feb09: Support win32 builds for fastfilereader
|
||||
10Feb09: Added a new event to indicate completion of SSL handshake on TCP
|
||||
connections
|
||||
10Feb09: Working get_peer_cert method. Returns the certificate as a Ruby
|
||||
String in PEM format. (Jake Douglas)
|
||||
10Feb09: Added EM.get_max_timers
|
||||
11Feb09: Fix compile options for sun compiler (Alasdairrr)
|
||||
11Feb09: get_status returns a Process::Status object
|
||||
12Feb09: Add EM::Protocols::Memcache with simple get/set functionality
|
||||
19Feb09: Add catch-all EM.error_handler
|
||||
20Feb09: Support miniunit (1.9)
|
||||
20Feb09: Return success on content-length = 0 instead of start waiting forever
|
||||
(Ugo Riboni)
|
||||
25Feb09: Allow next_tick to be used to pre-schedule reactor operations before
|
||||
EM.run
|
||||
26Feb09: Added EM.get_connection_count
|
||||
01Mar09: Switch back to extconf for compiling gem extensions
|
||||
01Mar09: fixed a small bug with basic auth (mmmurf)
|
||||
@@ -0,0 +1,246 @@
|
||||
EventMachine (EM) adds two different formalisms for lightweight concurrency
|
||||
to the Ruby programmer's toolbox: spawned processes and deferrables. This
|
||||
note will show you how to use deferrables. For more information, see the
|
||||
separate document LIGHTWEIGHT_CONCURRENCY.
|
||||
|
||||
=== What are Deferrables?
|
||||
|
||||
EventMachine's Deferrable borrows heavily from the "deferred" object in
|
||||
Python's "Twisted" event-handling framework. Here's a minimal example that
|
||||
illustrates Deferrable:
|
||||
|
||||
require 'eventmachine'
|
||||
|
||||
class MyClass
|
||||
include EM::Deferrable
|
||||
|
||||
def print_value x
|
||||
puts "MyClass instance received #{x}"
|
||||
end
|
||||
end
|
||||
|
||||
EM.run {
|
||||
df = MyClass.new
|
||||
df.callback {|x|
|
||||
df.print_value(x)
|
||||
EM.stop
|
||||
}
|
||||
|
||||
EM::Timer.new(2) {
|
||||
df.set_deferred_status :succeeded, 100
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
This program will spin for two seconds, print out the string "MyClass
|
||||
instance received 100" and then exit. The Deferrable pattern relies on
|
||||
an unusual metaphor that may be unfamiliar to you, unless you've used
|
||||
Python's Twisted. You may need to read the following material through
|
||||
more than once before you get the idea.
|
||||
|
||||
EventMachine::Deferrable is simply a Ruby Module that you can include
|
||||
in your own classes. (There also is a class named
|
||||
EventMachine::DefaultDeferrable for when you want to create one without
|
||||
including it in code of your own.)
|
||||
|
||||
An object that includes EventMachine::Deferrable is like any other Ruby
|
||||
object: it can be created whenever you want, returned from your functions,
|
||||
or passed as an argument to other functions.
|
||||
|
||||
The Deferrable pattern allows you to specify any number of Ruby code
|
||||
blocks (callbacks or errbacks) that will be executed at some future time
|
||||
when the status of the Deferrable object changes.
|
||||
|
||||
How might that be useful? Well, imagine that you're implementing an HTTP
|
||||
server, but you need to make a call to some other server in order to fulfill
|
||||
a client request.
|
||||
|
||||
When you receive a request from one of your clients, you can create and
|
||||
return a Deferrable object. Some other section of your program can add a
|
||||
callback to the Deferrable that will cause the client's request to be
|
||||
fulfilled. Simultaneously, you initiate an event-driven or threaded client
|
||||
request to some different server. And then your EM program will continue to
|
||||
process other events and service other client requests.
|
||||
|
||||
When your client request to the other server completes some time later, you
|
||||
will call the #set_deferred_status method on the Deferrable object, passing
|
||||
either a success or failure status, and an arbitrary number of parameters
|
||||
(which might include the data you received from the other server).
|
||||
|
||||
At that point, the status of the Deferrable object becomes known, and its
|
||||
callback or errback methods are immediately executed. Callbacks and errbacks
|
||||
are code blocks that are attached to Deferrable objects at any time through
|
||||
the methods #callback and #errback.
|
||||
|
||||
The deep beauty of this pattern is that it decouples the disposition of one
|
||||
operation (such as a client request to an outboard server) from the
|
||||
subsequent operations that depend on that disposition (which may include
|
||||
responding to a different client or any other operation).
|
||||
|
||||
The code which invokes the deferred operation (that will eventually result
|
||||
in a success or failure status together with associated data) is completely
|
||||
separate from the code which depends on that status and data. This achieves
|
||||
one of the primary goals for which threading is typically used in
|
||||
sophisticated applications, with none of the nondeterminacy or debugging
|
||||
difficulties of threads.
|
||||
|
||||
As soon as the deferred status of a Deferrable becomes known by way of a call
|
||||
to #set_deferred_status, the Deferrable will IMMEDIATELY execute all of its
|
||||
callbacks or errbacks in the order in which they were added to the Deferrable.
|
||||
|
||||
Callbacks and errbacks can be added to a Deferrable object at any time, not
|
||||
just when the object is created. They can even be added after the status of
|
||||
the object has been determined! (In this case, they will be executed
|
||||
immediately when they are added.)
|
||||
|
||||
A call to Deferrable#set_deferred_status takes :succeeded or :failed as its
|
||||
first argument. (This determines whether the object will call its callbacks
|
||||
or its errbacks.) #set_deferred_status also takes zero or more additional
|
||||
parameters, that will in turn be passed as parameters to the callbacks or
|
||||
errbacks.
|
||||
|
||||
In general, you can only call #set_deferred_status ONCE on a Deferrable
|
||||
object. A call to #set_deferred_status will not return until all of the
|
||||
associated callbacks or errbacks have been called. If you add callbacks or
|
||||
errbacks AFTER making a call to #set_deferred_status, those additional
|
||||
callbacks or errbacks will execute IMMEDIATELY. Any given callback or
|
||||
errback will be executed AT MOST once.
|
||||
|
||||
It's possible to call #set_deferred_status AGAIN, during the execution a
|
||||
callback or errback. This makes it possible to change the parameters which
|
||||
will be sent to the callbacks or errbacks farther down the chain, enabling
|
||||
some extremely elegant use-cases. You can transform the data returned from
|
||||
a deferred operation in arbitrary ways as needed by subsequent users, without
|
||||
changing any of the code that generated the original data.
|
||||
|
||||
A call to #set_deferred_status will not return until all of the associated
|
||||
callbacks or errbacks have been called. If you add callbacks or errbacks
|
||||
AFTER making a call to #set_deferred_status, those additional callbacks or
|
||||
errbacks will execute IMMEDIATELY.
|
||||
|
||||
Let's look at some more sample code. It turns out that many of the internal
|
||||
protocol implementations in the EventMachine package rely on Deferrable. One
|
||||
of these is EM::Protocols::HttpClient.
|
||||
|
||||
To make an evented HTTP request, use the module function
|
||||
EM::Protocols::HttpClient#request, which returns a Deferrable object.
|
||||
Here's how:
|
||||
|
||||
require 'eventmachine'
|
||||
|
||||
EM.run {
|
||||
df = EM::Protocols::HttpClient.request( :host=>"www.example.com",
|
||||
:request=>"/index.html" )
|
||||
|
||||
df.callback {|response|
|
||||
puts "Succeeded: #{response[:content]}"
|
||||
EM.stop
|
||||
}
|
||||
|
||||
df.errback {|response|
|
||||
puts "ERROR: #{response[:status]}"
|
||||
EM.stop
|
||||
}
|
||||
}
|
||||
|
||||
(See the documentation of EventMachine::Protocols::HttpClient for information
|
||||
on the object returned by #request.)
|
||||
|
||||
In this code, we make a call to HttpClient#request, which immediately returns
|
||||
a Deferrable object. In the background, an HTTP client request is being made
|
||||
to www.example.com, although your code will continue to run concurrently.
|
||||
|
||||
At some future point, the HTTP client request will complete, and the code in
|
||||
EM::Protocols::HttpClient will process either a valid HTTP response (including
|
||||
returned content), or an error.
|
||||
|
||||
At that point, EM::Protocols::HttpClient will call
|
||||
EM::Deferrable#set_deferred_status on the Deferrable object that was returned
|
||||
to your program, as the return value from EM::Protocols::HttpClient.request.
|
||||
You don't have to do anything to make this happen. All you have to do is tell
|
||||
the Deferrable what to do in case of either success, failure, or both.
|
||||
|
||||
In our code sample, we set one callback and one errback. The former will be
|
||||
called if the HTTP call succeeds, and the latter if it fails. (For
|
||||
simplicity, we have both of them calling EM#stop to end the program, although
|
||||
real programs would be very unlikely to do this.)
|
||||
|
||||
Setting callbacks and errbacks is optional. They are handlers to defined
|
||||
events in the lifecycle of the Deferrable event. It's not an error if you
|
||||
fail to set either a callback, an errback, or both. But of course your
|
||||
program will then fail to receive those notifications.
|
||||
|
||||
If through some bug it turns out that #set_deferred_status is never called
|
||||
on a Deferrable object, then that object's callbacks or errbacks will NEVER
|
||||
be called. It's also possible to set a timeout on a Deferrable. If the
|
||||
timeout elapses before any other call to #set_deferred_status, the Deferrable
|
||||
object will behave as is you had called set_deferred_status(:failed) on it.
|
||||
|
||||
|
||||
Now let's modify the example to illustrate some additional points:
|
||||
|
||||
require 'eventmachine'
|
||||
|
||||
EM.run {
|
||||
df = EM::Protocols::HttpClient.request( :host=>"www.example.com",
|
||||
:request=>"/index.html" )
|
||||
|
||||
df.callback {|response|
|
||||
df.set_deferred_status :succeeded, response[:content]
|
||||
}
|
||||
|
||||
df.callback {|string|
|
||||
puts "Succeeded: #{string}"
|
||||
EM.stop
|
||||
}
|
||||
|
||||
df.errback {|response|
|
||||
puts "ERROR: #{response[:status]}"
|
||||
EM.stop
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Just for the sake of illustration, we've now set two callbacks instead of
|
||||
one. If the deferrable operation (the HTTP client-request) succeeds, then
|
||||
both of the callbacks will be executed in order.
|
||||
|
||||
But notice that we've also made our own call to #set_deferred_status in the
|
||||
first callback. This isn't required, because the HttpClient implementation
|
||||
already made a call to #set_deferred_status. (Otherwise, of course, the
|
||||
callback would not be executing.)
|
||||
|
||||
But we used #set_deferred_status in the first callback in order to change the
|
||||
parameters that will be sent to subsequent callbacks in the chain. In this
|
||||
way, you can construct powerful sequences of layered functionality. If you
|
||||
want, you can even change the status of the Deferrable from :succeeded to
|
||||
:failed, which would abort the chain of callback calls, and invoke the chain
|
||||
of errbacks instead.
|
||||
|
||||
Now of course it's somewhat trivial to define two callbacks in the same
|
||||
method, even with the parameter-changing effect we just described. It would
|
||||
be much more interesting to pass the Deferrable to some other function (for
|
||||
example, a function defined in another module or a different gem), that would
|
||||
in turn add callbacks and/or errbacks of its own. That would illustrate the
|
||||
true power of the Deferrable pattern: to isolate the HTTP client-request
|
||||
from other functions that use the data that it returns without caring where
|
||||
those data came from.
|
||||
|
||||
Remember that you can add a callback or an errback to a Deferrable at any
|
||||
point in time, regardless of whether the status of the deferred operation is
|
||||
known (more precisely, regardless of when #set_deferred_status is called on
|
||||
the object). Even hours or days later.
|
||||
|
||||
When you add a callback or errback to a Deferrable object on which
|
||||
#set_deferred_status has not yet been called, the callback/errback is queued
|
||||
up for future execution, inside the Deferrable object. When you add a
|
||||
callback or errback to a Deferrable on which #set_deferred_status has
|
||||
already been called, the callback/errback will be executed immediately.
|
||||
Your code doesn't have to worry about the ordering, and there are no timing
|
||||
issues, as there would be with a threaded approach.
|
||||
|
||||
For more information on Deferrables and their typical usage patterns, look
|
||||
in the EM unit tests. There are also quite a few sugarings (including
|
||||
EM::Deferrable#future) that make typical Deferrable usages syntactically
|
||||
easier to work with.
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
EventMachine now supports epoll, bringing large increases in performance and scalability to Ruby programs.
|
||||
|
||||
Epoll(7) is a alternative mechanism for multiplexed I/O that is available in Linux 2.6 kernels.
|
||||
It features significantly greater performance than the standard select(2) mechanism, when used in
|
||||
applications that require very large numbers of open I/O descriptors.
|
||||
|
||||
EventMachine has always used select(2) because its behavior is well standardized and broadly supported.
|
||||
But select becomes unreasonably slow when a program has a
|
||||
very large number of file descriptors or sockets. Ruby's version of select hardcodes a limit
|
||||
of 1024 descriptors per process, but heavily loaded processes will start to show performance
|
||||
degradation even after only a few hundred descriptors are in use.
|
||||
|
||||
Epoll is an extended version of the poll(2) call, and it solves the problems with select. Programs
|
||||
based on epoll can easily scale past Ruby's 1024-descriptor limit, potentially to tens of thousands
|
||||
of connectors, with no significant impact on performance.
|
||||
|
||||
(Another alternative which is very similar to epoll in principle is kqueue, supplied on BSD and its
|
||||
variants.)
|
||||
|
||||
|
||||
|
||||
This note shows you how to use epoll in your programs.
|
||||
|
||||
=== Compiling EventMachine to use epoll.
|
||||
|
||||
You don't have to do anything to get epoll support in EventMachine.
|
||||
When you compile EventMachine on a platform that supports epoll, EM will
|
||||
automatically generate a Makefile that includes epoll. (At this writing, this will only work
|
||||
on Linux 2.6 kernels.) If you compile EM on a platform without epoll, then epoll support will
|
||||
be omitted from the Makefile, and EM will work just as it always has.
|
||||
|
||||
=== Using epoll in your programs.
|
||||
|
||||
First, you need to tell EventMachine to use epoll instead of select (but see below, as this requirement
|
||||
will be removed in a future EventMachine version). Second, you need to prepare your program to use
|
||||
more than 1024 descriptors, an operation that generally requires superuser privileges. Third, you will probably
|
||||
want your process to drop the superuser privileges after you increase your process's descriptor limit.
|
||||
|
||||
=== Using EventMachine#epoll
|
||||
|
||||
Call the method EventMachine#epoll anytime before you call EventMachine#run, and your program will
|
||||
automatically use epoll, if available. It's safe to call EventMachine#epoll on any platform because
|
||||
it compiles to a no-op on platforms that don't support epoll.
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
EM.epoll
|
||||
EM.run {
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
EventMachine#epoll was included in this initial release only to avoid changing the behavior of existing
|
||||
programs. However, it's expected that a future release of EM will convert EventMachine#epoll to a no-op,
|
||||
and run epoll by default on platforms that support it.
|
||||
|
||||
=== Using EventMachine#set_descriptor_table_size
|
||||
|
||||
In Linux (as in every Unix-like platform), every process has a internal table that determines the maximum
|
||||
number of file and socket descriptors you may have open at any given time. The size of this table is
|
||||
generally fixed at 1024, although it may be increased within certain system-defined hard and soft limits.
|
||||
|
||||
If you want your EventMachine program to support more than 1024 total descriptors, you must use
|
||||
EventMachine#set_descriptor_table_size, as follows:
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
new_size = EM.set_descriptor_table_size( 60000 )
|
||||
$>.puts "New descriptor-table size is #{new_size}"
|
||||
|
||||
EM.run {
|
||||
...
|
||||
}
|
||||
|
||||
If successful, this example will increase the maximum number of descriptors that epoll can use to 60,000.
|
||||
Call EventMachine#set_descriptor_table_size without an argument at any time to find out the current
|
||||
size of the descriptor table.
|
||||
|
||||
Using EventMachine#set_descriptor_table_size ONLY affects the number of descriptors that can be used
|
||||
by epoll. It has no useful effect on platforms that don't support epoll, and it does NOT increase the
|
||||
number of descriptors that Ruby's own I/O functions can use.
|
||||
|
||||
#set_descriptor_table_size can fail if your process is not running as superuser, or if you try to set a
|
||||
table size that exceeds the hard limits imposed by your system. In the latter case, try a smaller number.
|
||||
|
||||
|
||||
=== Using EventMachine#set_effective_user
|
||||
|
||||
In general, you must run your program with elevated or superuser privileges if you want to increase
|
||||
your descriptor-table size beyond 1024 descriptors. This is easy enough to verify. Try running the
|
||||
sample program given above, that increases the descriptor limit to 60,000. You will probably find that
|
||||
the table size will not be increased if you don't run your program as root or with elevated privileges.
|
||||
|
||||
But of course network servers, especially long-running ones, should not run with elevated privileges.
|
||||
You will want to drop superuser privileges as soon as possible after initialization. To do this,
|
||||
use EventMachine#set_effective_user:
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
# (Here, program is running as superuser)
|
||||
|
||||
EM.set_descriptor_table_size( 60000 )
|
||||
EM.set_effective_user( "nobody" )
|
||||
# (Here, program is running as nobody)
|
||||
|
||||
EM.run {
|
||||
...
|
||||
}
|
||||
|
||||
Of course, you will need to replace "nobody" in the example with the name of an unprivileged user
|
||||
that is valid on your system. What if you want to drop privileges after opening a server socket
|
||||
on a privileged (low-numbered) port? Easy, just call #set_effective_user after opening your sockets:
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
# (Here, program is running as superuser)
|
||||
|
||||
EM.set_descriptor_table_size( 60000 )
|
||||
|
||||
EM.run {
|
||||
EM.start_server( "0.0.0.0", 80, MyHttpServer )
|
||||
EM.start_server( "0.0.0.0", 443, MyEncryptedHttpServer )
|
||||
|
||||
EM.set_effective_user( "nobody" )
|
||||
# (Here, program is running as nobody)
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
Because EventMachine#set_effective_user is used to enforce security
|
||||
requirements, it has no nonfatal errors. If you try to set a nonexistent or invalid effective user,
|
||||
#set_effective_user will abort your program, rather than continue to run with elevated privileges.
|
||||
|
||||
EventMachine#set_effective_user is a silent no-op on platforms that don't support it, such as Windows.
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
If you have obtained an EventMachine source-tarball (.tar.gz):
|
||||
unzip and untar the tarball, and enter the directory that is
|
||||
created. In that directory, say:
|
||||
ruby setup.rb
|
||||
(You may need to be root to execute this command.)
|
||||
|
||||
To create documentation for EventMachine, simply type:
|
||||
rake rdoc
|
||||
in the distro directory. Rdocs will be created in subdirectory rdoc.
|
||||
|
||||
If you have obtained a gem version of EventMachine, install it in the
|
||||
usual way (gem install eventmachine). You may need superuser privileges
|
||||
to execute this command.
|
||||
@@ -0,0 +1,42 @@
|
||||
EventMachine (EM) can respond to keyboard events. This gives your event-driven
|
||||
programs the ability to respond to input from local users.
|
||||
|
||||
Programming EM to handle keyboard input in Ruby is simplicity itself. Just use
|
||||
EventMachine#open_keyboard, and supply the name of a Ruby module or class that
|
||||
will receive the input:
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
module MyKeyboardHandler
|
||||
def receive_data keystrokes
|
||||
puts "I received the following data from the keyboard: #{keystrokes}"
|
||||
end
|
||||
end
|
||||
|
||||
EM.run {
|
||||
EM.open_keyboard(MyKeyboardHandler)
|
||||
}
|
||||
|
||||
If you want EM to send line-buffered keyboard input to your program, just
|
||||
include the LineText2 protocol module in your handler class or module:
|
||||
|
||||
require 'rubygems'
|
||||
require 'eventmachine'
|
||||
|
||||
module MyKeyboardHandler
|
||||
include EM::Protocols::LineText2
|
||||
def receive_line data
|
||||
puts "I received the following line from the keyboard: #{data}"
|
||||
end
|
||||
end
|
||||
|
||||
EM.run {
|
||||
EM.open_keyboard(MyKeyboardHandler)
|
||||
}
|
||||
|
||||
As we said, simplicity itself. You can call EventMachine#open_keyboard at any
|
||||
time while the EM reactor loop is running. In other words, the method
|
||||
invocation may appear anywhere in an EventMachine#run block, or in any code
|
||||
invoked in the #run block.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
LEGAL NOTICE INFORMATION
|
||||
------------------------
|
||||
|
||||
EventMachine is Copyright (C) 2006-07 by Francis Cianfrocca.
|
||||
|
||||
EventMachine is copyrighted software owned by Francis Cianfrocca
|
||||
(blackhedd ... gmail.com). You may redistribute and/or modify this
|
||||
software as long as you comply with either the terms of the GPL
|
||||
(see the file GPL), or Ruby's license (see the file COPYING).
|
||||
|
||||
Your use of all the files in this distribution is controlled by these
|
||||
license terms, except for those files specifically mentioned below:
|
||||
|
||||
|
||||
|
||||
setup.rb
|
||||
This file is Copyright (C) 2000-2005 by Minero Aoki
|
||||
You can distribute/modify this file under the terms of
|
||||
the GNU LGPL, Lesser General Public License version 2.1.
|
||||
|
||||
|
||||
lib/em/buftok.rb
|
||||
This file is Copyright (C) 2007 by Tony Arcieri. This file is
|
||||
covered by the terms of Ruby's License (see the file COPYING).
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
EventMachine (EM) adds two different formalisms for lightweight concurrency to
|
||||
the Ruby programmer's toolbox: spawned processes and deferrables. This note
|
||||
will show you how to use them.
|
||||
|
||||
|
||||
=== What is Lightweight Concurrency?
|
||||
|
||||
We use the term "Lightweight Concurrency" (LC) to refer to concurrency
|
||||
mechanisms that are lighter than Ruby threads. By "lighter," we mean: less
|
||||
resource-intensive in one or more dimensions, usually including memory and
|
||||
CPU usage. In general, you turn to LC in the hope of improving the
|
||||
performance and scalability of your programs.
|
||||
|
||||
In addition to the two EventMachine mechanisms we will discuss here, Ruby
|
||||
has at least one other LC construct: Fibers, which are currently under
|
||||
development in Ruby 1.9.
|
||||
|
||||
The technical feature that makes all of these LC mechanisms different from
|
||||
standard Ruby threads is that they are not scheduled automatically.
|
||||
|
||||
When you create and run Ruby threads, you can assume (within certain
|
||||
constraints) that your threads will all be scheduled fairly by Ruby's runtime.
|
||||
Ruby itself is responsible for giving each of your threads its own share of
|
||||
the total runtime.
|
||||
|
||||
But with LC, your program is responsible for causing different execution
|
||||
paths to run. In effect, your program has to act as a "thread scheduler."
|
||||
Scheduled entities in LC run to completion and are never preempted. The
|
||||
runtime system has far less work to do since it has no need to interrupt
|
||||
threads or to schedule them fairly. This is what makes LC lighter and faster.
|
||||
|
||||
You'll learn exactly how LC scheduling works in practice as we work through
|
||||
specific examples.
|
||||
|
||||
|
||||
=== EventMachine Lightweight Concurrency
|
||||
|
||||
Recall that EM provides a reactor loop that must be running in order for
|
||||
your programs to perform event-driven logic. An EM program typically has a
|
||||
structure like this:
|
||||
|
||||
require 'eventmachine'
|
||||
|
||||
# your initializations
|
||||
|
||||
EM.run {
|
||||
# perform event-driven I/O here, including network clients,
|
||||
# servers, timers, and thread-pool operations.
|
||||
}
|
||||
|
||||
# your cleanup
|
||||
# end of the program
|
||||
|
||||
|
||||
EventMachine#run executes the reactor loop, which causes your code to be
|
||||
called as events of interest to your program occur. The block you pass to
|
||||
EventMachine#run is executed right after the reactor loop starts, and is
|
||||
the right place to start socket acceptors, etc.
|
||||
|
||||
Because the reactor loop runs constantly in an EM program (until it is
|
||||
stopped by a call to EventMachine#stop), it has the ability to schedule
|
||||
blocks of code for asynchronous execution. Unlike a pre-emptive thread
|
||||
scheduler, it's NOT able to interrupt code blocks while they execute. But
|
||||
the scheduling capability it does have is enough to enable lightweight
|
||||
concurrency.
|
||||
|
||||
|
||||
For information on Spawned Processes, see the separate document
|
||||
SPAWNED_PROCESSES.
|
||||
|
||||
For information on Deferrables, see the separate document DEFERRABLES.
|
||||
|
||||
|
||||
=== [SIDEBAR]: I Heard That EventMachine Doesn't Work With Ruby Threads.
|
||||
|
||||
This is incorrect. EM is fully interoperable with all versions of Ruby
|
||||
threads, and has been since its earliest releases.
|
||||
|
||||
It's very true that EM encourages an "evented" (non-threaded) programming
|
||||
style. The specific benefits of event-driven programming are far better
|
||||
performance and scalability for well-written programs, and far easier
|
||||
debugging.
|
||||
|
||||
The benefit of using threads for similar applications is a possibly more
|
||||
intuitive programming model, as well as the fact that threads are already
|
||||
familiar to most programmers. Also, bugs in threaded programs often fail
|
||||
to show up until programs go into production. These factors create the
|
||||
illusion that threaded programs are easier to write.
|
||||
|
||||
However, some operations that occur frequently in professional-caliber
|
||||
applications simply can't be done without threads. (The classic example
|
||||
is making calls to database client-libraries that block on network I/O
|
||||
until they complete.)
|
||||
|
||||
EventMachine not only allows the use of Ruby threads in these cases, but
|
||||
it even provides a built-in thread-pool object to make them easier to
|
||||
work with.
|
||||
|
||||
You may have heard a persistent criticism that evented I/O is fundamentally
|
||||
incompatible with Ruby threads. It is true that some well-publicized attempts
|
||||
to incorporate event-handling libraries into Ruby were not successful. But
|
||||
EventMachine was designed from the ground up with Ruby compatibility in mind,
|
||||
so EM never suffered from the problems that defeated the earlier attempts.
|
||||
|
||||
|
||||
=== [SIDEBAR]: I Heard That EventMachine Doesn't Work Very Well On Windows.
|
||||
|
||||
This too is incorrect. EventMachine is an extension written in C++ and Java,
|
||||
and therefore it requires compilation. Many Windows computers (and some Unix
|
||||
computers, especially in production environments) don't have a build stack.
|
||||
Attempting to install EventMachine on a machine without a compiler usually
|
||||
produces a confusing error.
|
||||
|
||||
In addition, Ruby has a much-debated issue with Windows compiler versions.
|
||||
Ruby on Windows works best with Visual Studio 6, a compiler version that is
|
||||
long out-of-print, no longer supported by Microsoft, and difficult to obtain.
|
||||
(This problem is not specific to EventMachine.)
|
||||
|
||||
Shortly after EventMachine was first released, the compiler issues led to
|
||||
criticism that EM was incompatible with Windows. Since that time, every
|
||||
EventMachine release has been supplied in a precompiled binary form for
|
||||
Windows users, that does not require you to compile the code yourself. EM
|
||||
binary Gems for Windows are compiled using Visual Studio 6.
|
||||
|
||||
EventMachine does supply some advanced features (such as Linux EPOLL support,
|
||||
reduced-privilege operation, UNIX-domain sockets, etc.) that have no
|
||||
meaningful implementation on Windows. Apart from these special cases, all EM
|
||||
functionality (including lightweight concurrency) works perfectly well on
|
||||
Windows.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
EventMachine is supplied in three alternative versions.
|
||||
|
||||
1) A version that includes a Ruby extension written in C++. This version requires compilation;
|
||||
2) A version for JRuby that contains a precompiled JAR file written in Java;
|
||||
3) A pure Ruby version that has no external dependencies and can run in any Ruby environment.
|
||||
|
||||
The Java version of EventMachine is packaged in a distinct manner and must be installed using a
|
||||
special procedure. This version is described fully in a different document, and not considered
|
||||
further here.
|
||||
|
||||
The C++ and pure-Ruby versions, however, are shipped in the same distribution. You use the same
|
||||
files (either tarball or Ruby gem) to install both of these versions.
|
||||
|
||||
If you intend to use the C++ version, you must successfully compile EventMachine after you install it.
|
||||
(The gem installation attempts to perform this step automatically.)
|
||||
|
||||
If you choose not to compile the EventMachine C++ extension, or if your compilation fails for any
|
||||
reason, you still have a fully-functional installation of the pure-Ruby version of EM.
|
||||
|
||||
However, for technical reasons, a default EM installation (whether or not the compilation succeeds)
|
||||
will always assume that the compiled ("extension") implementation should be used.
|
||||
|
||||
If you want your EM program to use the pure Ruby version, you must specifically request it. There
|
||||
are two ways to do this: by setting either a Ruby global variable, or an environment string.
|
||||
|
||||
The following code will invoke the pure-Ruby implementation of EM:
|
||||
|
||||
$eventmachine_library = :pure_ruby
|
||||
require 'eventmachine'
|
||||
|
||||
EM.library_type #=> "pure_ruby"
|
||||
|
||||
Notice that this requires a code change and is not the preferred way to select pure Ruby, unless
|
||||
for some reason you are absolutely sure you will never want the compiled implementation.
|
||||
|
||||
Setting the following environment string has the same effect:
|
||||
|
||||
export EVENTMACHINE_LIBRARY="pure_ruby"
|
||||
|
||||
This technique gives you the flexibility to select either version at runtime with no code changes.
|
||||
|
||||
Support
|
||||
|
||||
The EventMachine development team has committed to support precisely the same APIs for all the
|
||||
various implementations of EM.
|
||||
|
||||
This means that you can expect any EM program to behave identically, whether you use pure Ruby,
|
||||
the compiled C++ extension, or JRuby. Deviations from this behavior are to be considered bugs
|
||||
and should be reported as such.
|
||||
|
||||
There is a small number of exceptions to this rule, which arise from underlying platform
|
||||
distinctions. Notably, EM#epoll is a silent no-op in the pure Ruby implementation.
|
||||
|
||||
|
||||
When Should You Use the Pure-Ruby Implementation of EM?
|
||||
|
||||
|
||||
Use the pure Ruby implementation of EM when you must support a platform for which no C++ compiler
|
||||
is available, or on which the standard EM C++ code can't be compiled.
|
||||
|
||||
Keep in mind that you don't need a C++ compiler in order to deploy EM applications that rely on
|
||||
the compiled version, so long as appropriate C++ runtime libraries are available on the target platform.
|
||||
|
||||
In extreme cases, you may find that you can develop software with the compiled EM version, but are
|
||||
not allowed to install required runtime libraries on the deployment system(s). This would be another
|
||||
case in which the pure Ruby implementation can be useful.
|
||||
|
||||
In general you should avoid the pure Ruby version of EM when performance and scalability are important.
|
||||
EM in pure Ruby will necessarily run slower than the compiled version. Depending on your application
|
||||
this may or may not be a key issue.
|
||||
|
||||
Also, since EPOLL is not supported in pure Ruby, your applications will be affected by Ruby's built-in
|
||||
limit of 1024 file and socket descriptors that may be open in a single process. For maximum scalability
|
||||
and performance, always use EPOLL if possible.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
RUBY/EventMachine RELEASE NOTES
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.9.0, released xxXXX07
|
||||
Added Erlang-like distributed-computing features
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.8.0, released 23Jun07
|
||||
Added an epoll implementation for Linux 2.6 kernels.
|
||||
Added evented #popen.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.7.3, released 22May07
|
||||
Added a large variety of small features. See the ChangeLog.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.7.1, released xxNov06
|
||||
Added protocol handlers for line-oriented protocols.
|
||||
Various bug fixes.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.7.0, released 20Nov06
|
||||
Added a fix in em.cpp/ConnectToServer to fix a fatal exception that
|
||||
occurred in FreeBSD when connecting successfully to a remote server.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.6.0, released xxJul06
|
||||
Added deferred operations, suggested by Don Stocks, amillionhitpoints@yahoo.com.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.5.4, released xxJun06
|
||||
Added get_peername support for streams and datagrams.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.5.3, released 17May06
|
||||
Fixed bugs in extconf.rb, thanks to Daniel Harple, dharple@generalconsumption.org.
|
||||
Added proper setup.rb and rake tasks, thanks to Austin Ziegler.
|
||||
Fixed a handful of reported problems with builds on various platforms.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.5.2, released 05May06
|
||||
Made several nonvisible improvements to the Windows
|
||||
implementation.
|
||||
Added an exception-handling patch contributed by Jeff Rose, jeff@rosejn.net.
|
||||
Added a dir-config patch contributed anonymously.
|
||||
Supported builds on Solaris.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.5.1, released 05May06
|
||||
Made it possible to pass a Class rather than a Module
|
||||
to a protocol handler.
|
||||
Added Windows port.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.5.0, released 30Apr06
|
||||
Added a preliminary SSL/TLS extension. This will probably
|
||||
change over the next few releases.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.4.5, released 29Apr06
|
||||
Changed ext files so the ruby.h is installed after unistd.h
|
||||
otherwise it doesn't compile on gcc 4.1
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.4.2, released 19Apr06
|
||||
Changed the Ruby-glue so the extension will play nicer
|
||||
in the sandbox with Ruby threads.
|
||||
Added an EventMachine::run_without_threads API to
|
||||
switch off the thread-awareness for better performance
|
||||
in programs that do not spin any Ruby threads.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.4.1, released 15Apr06
|
||||
Reworked the shared-object interface to make it easier to
|
||||
use EventMachine from languages other than Ruby.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.3.2, released 12Apr06
|
||||
Added support for a user-supplied block in EventMachine#connect.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.3.1, released 11Apr06
|
||||
Fixed bug that prevented EventMachine from being run multiple
|
||||
times in a single process.
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.3.0, released 10Apr06
|
||||
Added method EventHandler::Connection::post_init
|
||||
|
||||
--------------------------------------------------
|
||||
Version: 0.2.0, released 10Apr06
|
||||
Added method EventHandler::stop
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
This note details the usage of EventMachine's built-in support for SMTP. EM
|
||||
supports both client and server connections, which will be described in
|
||||
separate sections.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
EventMachine (EM) adds two different formalisms for lightweight concurrency
|
||||
to the Ruby programmer's toolbox: spawned processes and deferrables. This
|
||||
note will show you how to use spawned processes. For more information, see
|
||||
the separate document LIGHTWEIGHT_CONCURRENCY.
|
||||
|
||||
|
||||
=== What are Spawned Processes?
|
||||
|
||||
Spawned Processes in EventMachine are inspired directly by the "processes"
|
||||
found in the Erlang programming language. EM deliberately borrows much (but
|
||||
not all) of Erlang's terminology. However, EM's spawned processes differ from
|
||||
Erlang's in ways that reflect not only Ruby style, but also the fact that
|
||||
Ruby is not a functional language like Erlang.
|
||||
|
||||
Let's proceed with a complete, working code sample that we will analyze line
|
||||
by line. Here's an EM implementation of the "ping-pong" program that also
|
||||
appears in the Erlang tutorial:
|
||||
|
||||
|
||||
require 'eventmachine'
|
||||
|
||||
EM.run {
|
||||
pong = EM.spawn {|x, ping|
|
||||
puts "Pong received #{x}"
|
||||
ping.notify( x-1 )
|
||||
}
|
||||
|
||||
ping = EM.spawn {|x|
|
||||
if x > 0
|
||||
puts "Pinging #{x}"
|
||||
pong.notify x, self
|
||||
else
|
||||
EM.stop
|
||||
end
|
||||
}
|
||||
|
||||
ping.notify 3
|
||||
}
|
||||
|
||||
If you run this program, you'll see the following output:
|
||||
|
||||
Pinging 3
|
||||
Pong received 3
|
||||
Pinging 2
|
||||
Pong received 2
|
||||
Pinging 1
|
||||
Pong received 1
|
||||
|
||||
Let's take it step by step.
|
||||
|
||||
EventMachine#spawn works very much like the built-in function spawn in
|
||||
Erlang. It returns a reference to a Ruby object of class
|
||||
EventMachine::SpawnedProcess, which is actually a schedulable entity. In
|
||||
Erlang, the value returned from spawn is called a "process identifier" or
|
||||
"pid." But we'll refer to the Ruby object returned from EM#spawn simply as a
|
||||
"spawned process."
|
||||
|
||||
You pass a Ruby block with zero or more parameters to EventMachine#spawn.
|
||||
Like all Ruby blocks, this one is a closure, so it can refer to variables
|
||||
defined in the local context when you call EM#spawn.
|
||||
|
||||
However, the code block passed to EM#spawn does NOT execute immediately by
|
||||
default. Rather, it will execute only when the Spawned Object is "notified."
|
||||
In Erlang, this process is called "message passing," and is done with the
|
||||
operator !, but in Ruby it's done simply by calling the #notify method of a
|
||||
spawned-process object. The parameters you pass to #notify must match those
|
||||
defined in the block that was originally passed to EM#spawn.
|
||||
|
||||
When you call the #notify method of a spawned-process object, EM's reactor
|
||||
core will execute the code block originally passed to EM#spawn, at some point
|
||||
in the future. (#notify itself merely adds a notification to the object's
|
||||
message queue and ALWAYS returns immediately.)
|
||||
|
||||
When a SpawnedProcess object executes a notification, it does so in the
|
||||
context of the SpawnedProcess object itself. The notified code block can see
|
||||
local context from the point at which EM#spawn was called. However, the value
|
||||
of "self" inside the notified code block is a reference to the SpawnedProcesss
|
||||
object itself.
|
||||
|
||||
An EM spawned process is nothing more than a Ruby object with a message
|
||||
queue attached to it. You can have any number of spawned processes in your
|
||||
program without compromising scalability. You can notify a spawned process
|
||||
any number of times, and each notification will cause a "message" to be
|
||||
placed in the queue of the spawned process. Spawned processes with non-empty
|
||||
message queues are scheduled for execution automatically by the EM reactor.
|
||||
Spawned processes with no visible references are garbage-collected like any
|
||||
other Ruby object.
|
||||
|
||||
Back to our code sample:
|
||||
|
||||
pong = EM.spawn {|x, ping|
|
||||
puts "Pong received #{x}"
|
||||
ping.notify( x-1 )
|
||||
}
|
||||
|
||||
This simply creates a spawned process and assigns it to the local variable
|
||||
pong. You can see that the spawned code block takes a numeric parameter and a
|
||||
reference to another spawned process. When pong is notified, it expects to
|
||||
receive arguments corresponding to these two parameters. It simply prints out
|
||||
the number it receives as the first argument. Then it notifies the spawned
|
||||
process referenced by the second argument, passing it the first argument
|
||||
minus 1.
|
||||
|
||||
And then the block ends, which is crucial because otherwise nothing else
|
||||
can run. (Remember that in LC, scheduled entities run to completion and are
|
||||
never preempted.)
|
||||
|
||||
On to the next bit of the code sample:
|
||||
|
||||
ping = EM.spawn {|x|
|
||||
if x > 0
|
||||
puts "Pinging #{x}"
|
||||
pong.notify x, self
|
||||
else
|
||||
EM.stop
|
||||
end
|
||||
}
|
||||
|
||||
Here, we're spawning a process that takes a single (numeric) parameter. If
|
||||
the parameter is greater than zero, the block writes it to the console. It
|
||||
then notifies the spawned process referenced by the pong local variable,
|
||||
passing as arguments its number argument, and a reference to itself. The
|
||||
latter reference, as you saw above, is used by pong to send a return
|
||||
notification.
|
||||
|
||||
If the ping process receives a zero value, it will stop the reactor loop and
|
||||
end the program.
|
||||
|
||||
Now we've created a pair of spawned processes, but nothing else has happened.
|
||||
If we stop now, the program will spin in the EM reactor loop, doing nothing
|
||||
at all. Our spawned processes will never be scheduled for execution.
|
||||
|
||||
But look at the next line in the code sample:
|
||||
|
||||
ping.notify 3
|
||||
|
||||
This line gets the ping-pong ball rolling. We call ping's #notify method,
|
||||
passing the argument 3. This causes a message to be sent to the ping spawned
|
||||
process. The message contains the single argument, and it causes the EM
|
||||
reactor to schedule the ping process. And this in turn results in the
|
||||
execution of the Ruby code block passed to EM#spawn when ping was created.
|
||||
Everything else proceeds as a result of the messages that are subsequently
|
||||
passed to each other by the spawned processes.
|
||||
|
||||
[TODO, present the outbound network i/o use case, and clarify that spawned
|
||||
processes are interleaved with normal i/o operations and don't interfere
|
||||
with them at all. Also, blame Erlang for the confusing term "process"]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
TODO List:
|
||||
|
||||
12Aug06: Noticed by Don Stocks. A TCP connect-request that results
|
||||
in a failed DNS resolution fires a fatal error back to user code.
|
||||
Uuuuuugly. We should probably cause an unbind event to get fired
|
||||
instead, and add some parameterization so the caller can detect
|
||||
the nature of the failure.
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class EchoServer < EM::Connection
|
||||
def receive_data(data)
|
||||
send_data(data)
|
||||
end
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, EchoServer)
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class EchoServer < EM::Connection
|
||||
def receive_data(data)
|
||||
if data.strip =~ /exit$/i
|
||||
EventMachine.stop
|
||||
else
|
||||
send_data(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, EchoServer)
|
||||
end
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
@@connected_clients = Array.new
|
||||
DM_REGEXP = /^@([a-zA-Z0-9]+)\s*:?\s*(.+)/.freeze
|
||||
|
||||
attr_reader :username
|
||||
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
@username = nil
|
||||
|
||||
puts "A client has connected..."
|
||||
ask_username
|
||||
end
|
||||
|
||||
def unbind
|
||||
@@connected_clients.delete(self)
|
||||
puts "[info] #{@username} has left" if entered_username?
|
||||
end
|
||||
|
||||
def receive_data(data)
|
||||
if entered_username?
|
||||
handle_chat_message(data.strip)
|
||||
else
|
||||
handle_username(data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Username handling
|
||||
#
|
||||
|
||||
def entered_username?
|
||||
!@username.nil? && !@username.empty?
|
||||
end # entered_username?
|
||||
|
||||
def handle_username(input)
|
||||
if input.empty?
|
||||
send_line("Blank usernames are not allowed. Try again.")
|
||||
ask_username
|
||||
else
|
||||
@username = input
|
||||
@@connected_clients.push(self)
|
||||
self.other_peers.each { |c| c.send_data("#{@username} has joined the room\n") }
|
||||
puts "#{@username} has joined"
|
||||
|
||||
self.send_line("[info] Ohai, #{@username}")
|
||||
end
|
||||
end # handle_username(input)
|
||||
|
||||
def ask_username
|
||||
self.send_line("[info] Enter your username:")
|
||||
end # ask_username
|
||||
|
||||
|
||||
#
|
||||
# Message handling
|
||||
#
|
||||
|
||||
def handle_chat_message(msg)
|
||||
if command?(msg)
|
||||
self.handle_command(msg)
|
||||
else
|
||||
if direct_message?(msg)
|
||||
self.handle_direct_message(msg)
|
||||
else
|
||||
self.announce(msg, "#{@username}:")
|
||||
end
|
||||
end
|
||||
end # handle_chat_message(msg)
|
||||
|
||||
def direct_message?(input)
|
||||
input =~ DM_REGEXP
|
||||
end # direct_message?(input)
|
||||
|
||||
def handle_direct_message(input)
|
||||
username, message = parse_direct_message(input)
|
||||
|
||||
if connection = @@connected_clients.find { |c| c.username == username }
|
||||
puts "[dm] @#{@username} => @#{username}"
|
||||
connection.send_line("[dm] @#{@username}: #{message}")
|
||||
else
|
||||
send_line "@#{username} is not in the room. Here's who is: #{usernames.join(', ')}"
|
||||
end
|
||||
end # handle_direct_message(input)
|
||||
|
||||
def parse_direct_message(input)
|
||||
return [$1, $2] if input =~ DM_REGEXP
|
||||
end # parse_direct_message(input)
|
||||
|
||||
|
||||
#
|
||||
# Commands handling
|
||||
#
|
||||
|
||||
def command?(input)
|
||||
input =~ /(exit|status)$/i
|
||||
end # command?(input)
|
||||
|
||||
def handle_command(cmd)
|
||||
case cmd
|
||||
when /exit$/i then self.close_connection
|
||||
when /status$/i then self.send_line("[chat server] It's #{Time.now.strftime('%H:%M')} and there are #{self.number_of_connected_clients} people in the room")
|
||||
end
|
||||
end # handle_command(cmd)
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
#
|
||||
|
||||
def announce(msg = nil, prefix = "[chat server]")
|
||||
@@connected_clients.each { |c| c.send_line("#{prefix} #{msg}") } unless msg.empty?
|
||||
end # announce(msg)
|
||||
|
||||
def number_of_connected_clients
|
||||
@@connected_clients.size
|
||||
end # number_of_connected_clients
|
||||
|
||||
def other_peers
|
||||
@@connected_clients.reject { |c| self == c }
|
||||
end # other_peers
|
||||
|
||||
def send_line(line)
|
||||
self.send_data("#{line}\n")
|
||||
end # send_line(line)
|
||||
|
||||
def usernames
|
||||
@@connected_clients.map { |c| c.username }
|
||||
end # usernames
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
puts "A client has connected..."
|
||||
end
|
||||
|
||||
def unbind
|
||||
puts "A client has left..."
|
||||
end
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
@@connected_clients = Array.new
|
||||
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
@@connected_clients.push(self)
|
||||
puts "A client has connected..."
|
||||
end
|
||||
|
||||
def unbind
|
||||
@@connected_clients.delete(self)
|
||||
puts "A client has left..."
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
#
|
||||
|
||||
def other_peers
|
||||
@@connected_clients.reject { |c| self == c }
|
||||
end # other_peers
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
@@connected_clients = Array.new
|
||||
|
||||
|
||||
attr_reader :username
|
||||
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
@username = nil
|
||||
|
||||
puts "A client has connected..."
|
||||
ask_username
|
||||
end
|
||||
|
||||
def unbind
|
||||
@@connected_clients.delete(self)
|
||||
puts "A client has left..."
|
||||
end
|
||||
|
||||
def receive_data(data)
|
||||
if entered_username?
|
||||
handle_chat_message(data.strip)
|
||||
else
|
||||
handle_username(data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Username handling
|
||||
#
|
||||
|
||||
def entered_username?
|
||||
!@username.nil? && !@username.empty?
|
||||
end # entered_username?
|
||||
|
||||
def handle_username(input)
|
||||
if input.empty?
|
||||
send_line("Blank usernames are not allowed. Try again.")
|
||||
ask_username
|
||||
else
|
||||
@username = input
|
||||
@@connected_clients.push(self)
|
||||
self.other_peers.each { |c| c.send_data("#{@username} has joined the room\n") }
|
||||
puts "#{@username} has joined"
|
||||
|
||||
self.send_line("[info] Ohai, #{@username}")
|
||||
end
|
||||
end # handle_username(input)
|
||||
|
||||
def ask_username
|
||||
self.send_line("[info] Enter your username:")
|
||||
end # ask_username
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Message handling
|
||||
#
|
||||
|
||||
def handle_chat_message(msg)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
#
|
||||
|
||||
def other_peers
|
||||
@@connected_clients.reject { |c| self == c }
|
||||
end # other_peers
|
||||
|
||||
def send_line(line)
|
||||
self.send_data("#{line}\n")
|
||||
end # send_line(line)
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
@@connected_clients = Array.new
|
||||
|
||||
|
||||
attr_reader :username
|
||||
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
@username = nil
|
||||
|
||||
puts "A client has connected..."
|
||||
ask_username
|
||||
end
|
||||
|
||||
def unbind
|
||||
@@connected_clients.delete(self)
|
||||
puts "[info] #{@username} has left" if entered_username?
|
||||
end
|
||||
|
||||
def receive_data(data)
|
||||
if entered_username?
|
||||
handle_chat_message(data.strip)
|
||||
else
|
||||
handle_username(data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Username handling
|
||||
#
|
||||
|
||||
def entered_username?
|
||||
!@username.nil? && !@username.empty?
|
||||
end # entered_username?
|
||||
|
||||
def handle_username(input)
|
||||
if input.empty?
|
||||
send_line("Blank usernames are not allowed. Try again.")
|
||||
ask_username
|
||||
else
|
||||
@username = input
|
||||
@@connected_clients.push(self)
|
||||
self.other_peers.each { |c| c.send_data("#{@username} has joined the room\n") }
|
||||
puts "#{@username} has joined"
|
||||
|
||||
self.send_line("[info] Ohai, #{@username}")
|
||||
end
|
||||
end # handle_username(input)
|
||||
|
||||
def ask_username
|
||||
self.send_line("[info] Enter your username:")
|
||||
end # ask_username
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Message handling
|
||||
#
|
||||
|
||||
def handle_chat_message(msg)
|
||||
if command?(msg)
|
||||
self.handle_command(msg)
|
||||
else
|
||||
self.announce(msg, "#{@username}:")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Commands handling
|
||||
#
|
||||
|
||||
def command?(input)
|
||||
input =~ /exit$/i
|
||||
end # command?(input)
|
||||
|
||||
def handle_command(cmd)
|
||||
case cmd
|
||||
when /exit$/i then self.close_connection
|
||||
end
|
||||
end # handle_command(cmd)
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
#
|
||||
|
||||
def announce(msg = nil, prefix = "[chat server]")
|
||||
@@connected_clients.each { |c| c.send_line("#{prefix} #{msg}") } unless msg.empty?
|
||||
end # announce(msg)
|
||||
|
||||
def other_peers
|
||||
@@connected_clients.reject { |c| self == c }
|
||||
end # other_peers
|
||||
|
||||
def send_line(line)
|
||||
self.send_data("#{line}\n")
|
||||
end # send_line(line)
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems' # or use Bundler.setup
|
||||
require 'eventmachine'
|
||||
|
||||
class SimpleChatServer < EM::Connection
|
||||
|
||||
@@connected_clients = Array.new
|
||||
DM_REGEXP = /^@([a-zA-Z0-9]+)\s*:?\s+(.+)/.freeze
|
||||
|
||||
attr_reader :username
|
||||
|
||||
|
||||
#
|
||||
# EventMachine handlers
|
||||
#
|
||||
|
||||
def post_init
|
||||
@username = nil
|
||||
|
||||
puts "A client has connected..."
|
||||
ask_username
|
||||
end
|
||||
|
||||
def unbind
|
||||
@@connected_clients.delete(self)
|
||||
puts "[info] #{@username} has left" if entered_username?
|
||||
end
|
||||
|
||||
def receive_data(data)
|
||||
if entered_username?
|
||||
handle_chat_message(data.strip)
|
||||
else
|
||||
handle_username(data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Username handling
|
||||
#
|
||||
|
||||
def entered_username?
|
||||
!@username.nil? && !@username.empty?
|
||||
end # entered_username?
|
||||
|
||||
def handle_username(input)
|
||||
if input.empty?
|
||||
send_line("Blank usernames are not allowed. Try again.")
|
||||
ask_username
|
||||
else
|
||||
@username = input
|
||||
@@connected_clients.push(self)
|
||||
self.other_peers.each { |c| c.send_data("#{@username} has joined the room\n") }
|
||||
puts "#{@username} has joined"
|
||||
|
||||
self.send_line("[info] Ohai, #{@username}")
|
||||
end
|
||||
end # handle_username(input)
|
||||
|
||||
def ask_username
|
||||
self.send_line("[info] Enter your username:")
|
||||
end # ask_username
|
||||
|
||||
|
||||
#
|
||||
# Message handling
|
||||
#
|
||||
|
||||
def handle_chat_message(msg)
|
||||
if command?(msg)
|
||||
self.handle_command(msg)
|
||||
else
|
||||
if direct_message?(msg)
|
||||
self.handle_direct_message(msg)
|
||||
else
|
||||
self.announce(msg, "#{@username}:")
|
||||
end
|
||||
end
|
||||
end # handle_chat_message(msg)
|
||||
|
||||
def direct_message?(input)
|
||||
input =~ DM_REGEXP
|
||||
end # direct_message?(input)
|
||||
|
||||
def handle_direct_message(input)
|
||||
username, message = parse_direct_message(input)
|
||||
|
||||
if connection = @@connected_clients.find { |c| c.username == username }
|
||||
puts "[dm] @#{@username} => @#{username}"
|
||||
connection.send_line("[dm] @#{@username}: #{message}")
|
||||
else
|
||||
send_line "@#{username} is not in the room. Here's who is: #{usernames.join(', ')}"
|
||||
end
|
||||
end # handle_direct_message(input)
|
||||
|
||||
def parse_direct_message(input)
|
||||
return [$1, $2] if input =~ DM_REGEXP
|
||||
end # parse_direct_message(input)
|
||||
|
||||
|
||||
#
|
||||
# Commands handling
|
||||
#
|
||||
|
||||
def command?(input)
|
||||
input =~ /(exit|status)$/i
|
||||
end # command?(input)
|
||||
|
||||
def handle_command(cmd)
|
||||
case cmd
|
||||
when /exit$/i then self.close_connection
|
||||
when /status$/i then self.send_line("[chat server] It's #{Time.now.strftime('%H:%M')} and there are #{self.number_of_connected_clients} people in the room")
|
||||
end
|
||||
end # handle_command(cmd)
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
#
|
||||
|
||||
def announce(msg = nil, prefix = "[chat server]")
|
||||
@@connected_clients.each { |c| c.send_line("#{prefix} #{msg}") } unless msg.empty?
|
||||
end # announce(msg)
|
||||
|
||||
def other_peers
|
||||
@@connected_clients.reject { |c| self == c }
|
||||
end # other_peers
|
||||
|
||||
def send_line(line)
|
||||
self.send_data("#{line}\n")
|
||||
end # send_line(line)
|
||||
end
|
||||
|
||||
EventMachine.run do
|
||||
# hit Control + C to stop
|
||||
Signal.trap("INT") { EventMachine.stop }
|
||||
Signal.trap("TERM") { EventMachine.stop }
|
||||
|
||||
EventMachine.start_server("0.0.0.0", 10000, SimpleChatServer)
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
require File.dirname(__FILE__) + '/helper'
|
||||
|
||||
EM.run do
|
||||
|
||||
# Create a channel to push data to, this could be stocks...
|
||||
RandChannel = EM::Channel.new
|
||||
|
||||
# The server simply subscribes client connections to the channel on connect,
|
||||
# and unsubscribes them on disconnect.
|
||||
class Server < EM::Connection
|
||||
def self.start(host = '127.0.0.1', port = 8000)
|
||||
EM.start_server(host, port, self)
|
||||
end
|
||||
|
||||
def post_init
|
||||
@sid = RandChannel.subscribe { |m| send_data "#{m.inspect}\n" }
|
||||
end
|
||||
|
||||
def unbind
|
||||
RandChannel.unsubscribe @sid
|
||||
end
|
||||
end
|
||||
Server.start
|
||||
|
||||
# Two client connections, that just print what they receive.
|
||||
2.times do
|
||||
EM.connect('127.0.0.1', 8000) do |c|
|
||||
c.extend EM::P::LineText2
|
||||
def c.receive_line(line)
|
||||
puts "Subscriber: #{signature} got #{line}"
|
||||
end
|
||||
EM.add_timer(2) { c.close_connection }
|
||||
end
|
||||
end
|
||||
|
||||
# This part of the example is more fake, but imagine sleep was in fact a
|
||||
# long running calculation to achieve the value.
|
||||
40.times do
|
||||
EM.defer lambda { v = sleep(rand * 2); RandChannel << [Time.now, v] }
|
||||
end
|
||||
|
||||
EM.add_timer(5) { EM.stop }
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/helper'
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
require File.dirname(__FILE__) + '/helper'
|
||||
|
||||
EM.run do
|
||||
array = (1..100).to_a
|
||||
|
||||
tickloop = EM.tick_loop do
|
||||
if array.empty?
|
||||
:stop
|
||||
else
|
||||
puts array.shift
|
||||
end
|
||||
end
|
||||
|
||||
tickloop.on_stop { EM.stop }
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
require File.dirname(__FILE__) + '/helper'
|
||||
|
||||
class TickCounter
|
||||
attr_reader :start_time, :count
|
||||
|
||||
def initialize
|
||||
reset
|
||||
@tick_loop = EM.tick_loop(method(:tick))
|
||||
end
|
||||
|
||||
def reset
|
||||
@count = 0
|
||||
@start_time = EM.current_time
|
||||
end
|
||||
|
||||
def tick
|
||||
@count += 1
|
||||
end
|
||||
|
||||
def rate
|
||||
@count / (EM.current_time - @start_time)
|
||||
end
|
||||
end
|
||||
|
||||
period = 5
|
||||
EM.run do
|
||||
counter = TickCounter.new
|
||||
EM.add_periodic_timer(period) do
|
||||
puts "Ticks per second: #{counter.rate} (mean of last #{period}s)"
|
||||
counter.reset
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
||||
require 'eventmachine'
|
||||
@@ -0,0 +1,269 @@
|
||||
|
||||
SHELL = /bin/sh
|
||||
|
||||
# V=0 quiet, V=1 verbose. other values don't work.
|
||||
V = 0
|
||||
V0 = $(V:0=)
|
||||
Q1 = $(V:1=)
|
||||
Q = $(Q1:0=@)
|
||||
ECHO1 = $(V:1=@ :)
|
||||
ECHO = $(ECHO1:0=@ echo)
|
||||
NULLCMD = :
|
||||
|
||||
#### Start of system configuration section. ####
|
||||
|
||||
srcdir = .
|
||||
topdir = /usr/include/ruby-3.2.0
|
||||
hdrdir = $(topdir)
|
||||
arch_hdrdir = /usr/include/x86_64-linux-gnu/ruby-3.2.0
|
||||
PATH_SEPARATOR = :
|
||||
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
|
||||
prefix = $(DESTDIR)/usr
|
||||
rubysitearchprefix = $(sitearchlibdir)/$(RUBY_BASE_NAME)
|
||||
rubyarchprefix = $(archlibdir)/$(RUBY_BASE_NAME)
|
||||
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
|
||||
exec_prefix = $(prefix)
|
||||
vendorarchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/vendor_ruby
|
||||
sitearchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/site_ruby
|
||||
rubyarchhdrdir = $(archincludedir)/$(RUBY_VERSION_NAME)
|
||||
vendorhdrdir = $(rubyhdrdir)/vendor_ruby
|
||||
sitehdrdir = $(rubyhdrdir)/site_ruby
|
||||
rubyhdrdir = $(includedir)/$(RUBY_VERSION_NAME)
|
||||
vendorarchdir = $(rubysitearchprefix)/vendor_ruby/$(ruby_version)
|
||||
vendorlibdir = $(vendordir)/$(ruby_version)
|
||||
vendordir = $(rubylibprefix)/vendor_ruby
|
||||
sitearchdir = $(DESTDIR)/usr/local/lib/x86_64-linux-gnu/site_ruby
|
||||
sitelibdir = $(sitedir)/$(ruby_version)
|
||||
sitedir = $(DESTDIR)/usr/local/lib/site_ruby
|
||||
rubyarchdir = $(rubyarchprefix)/$(ruby_version)
|
||||
rubylibdir = $(rubylibprefix)/$(ruby_version)
|
||||
sitearchincludedir = $(includedir)/$(sitearch)
|
||||
archincludedir = $(includedir)/$(arch)
|
||||
sitearchlibdir = $(libdir)/$(sitearch)
|
||||
archlibdir = $(libdir)/$(arch)
|
||||
ridir = $(datarootdir)/$(RI_BASE_NAME)
|
||||
mandir = $(datarootdir)/man
|
||||
localedir = $(datarootdir)/locale
|
||||
libdir = $(exec_prefix)/lib
|
||||
psdir = $(docdir)
|
||||
pdfdir = $(docdir)
|
||||
dvidir = $(docdir)
|
||||
htmldir = $(docdir)
|
||||
infodir = $(datarootdir)/info
|
||||
docdir = $(datarootdir)/doc/$(PACKAGE)
|
||||
oldincludedir = $(DESTDIR)/usr/include
|
||||
includedir = $(prefix)/include
|
||||
runstatedir = $(DESTDIR)/var/run
|
||||
localstatedir = $(DESTDIR)/var
|
||||
sharedstatedir = $(prefix)/com
|
||||
sysconfdir = $(DESTDIR)/etc
|
||||
datadir = $(datarootdir)
|
||||
datarootdir = $(prefix)/share
|
||||
libexecdir = $(exec_prefix)/libexec
|
||||
sbindir = $(exec_prefix)/sbin
|
||||
bindir = $(exec_prefix)/bin
|
||||
archdir = $(rubyarchdir)
|
||||
|
||||
|
||||
CC_WRAPPER =
|
||||
CC = x86_64-linux-gnu-gcc
|
||||
CXX = x86_64-linux-gnu-g++
|
||||
LIBRUBY = $(LIBRUBY_SO)
|
||||
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
|
||||
LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
|
||||
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static $(MAINLIBS)
|
||||
empty =
|
||||
OUTFLAG = -o $(empty)
|
||||
COUTFLAG = -o $(empty)
|
||||
CSRCFLAG = $(empty)
|
||||
|
||||
RUBY_EXTCONF_H =
|
||||
cflags = $(optflags) $(debugflags) $(warnflags)
|
||||
cxxflags =
|
||||
optflags = -O3 -fno-fast-math
|
||||
debugflags = -ggdb3
|
||||
warnflags = -Wall -Wextra -Wdeprecated-declarations -Wdiv-by-zero -Wduplicated-cond -Wimplicit-function-declaration -Wimplicit-int -Wmisleading-indentation -Wpointer-arith -Wwrite-strings -Wold-style-definition -Wimplicit-fallthrough=0 -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-packed-bitfield-compat -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wunused-variable -Wundef
|
||||
cppflags =
|
||||
CCDLFLAGS = -fPIC
|
||||
CFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 -fPIC $(ARCH_FLAG)
|
||||
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
|
||||
DEFS =
|
||||
CPPFLAGS = -DHAVE_OPENSSL_SSL_H -DHAVE_OPENSSL_ERR_H -DWITH_SSL -DBUILD_FOR_RUBY -DHAVE_RB_THREAD_CALL_WITHOUT_GVL -DHAVE_RB_THREAD_FD_SELECT -DHAVE_TYPE_RB_FDSET_T -DHAVE_RB_WAIT_FOR_SINGLE_FD -DHAVE_RB_TIME_NEW -DHAVE_INOTIFY_INIT -DHAVE_INOTIFY -DHAVE_WRITEV -DHAVE_PIPE2 -DHAVE_ACCEPT4 -DHAVE_CONST_SOCK_CLOEXEC -DOS_UNIX -DHAVE_EPOLL_CREATE -DHAVE_EPOLL -DHAVE_CLOCK_GETTIME -DHAVE_CONST_CLOCK_MONOTONIC_RAW -DHAVE_CONST_CLOCK_MONOTONIC -DHAVE_MAKE_PAIR -Wdate-time -D_FORTIFY_SOURCE=3 $(DEFS) $(cppflags)
|
||||
CXXFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 $(ARCH_FLAG)
|
||||
ldflags = -L. -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed
|
||||
dldflags = -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now
|
||||
ARCH_FLAG =
|
||||
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
|
||||
LDSHARED = $(CXX) -shared
|
||||
LDSHAREDXX = $(CXX) -shared
|
||||
AR = x86_64-linux-gnu-gcc-ar
|
||||
EXEEXT =
|
||||
|
||||
RUBY_INSTALL_NAME = $(RUBY_BASE_NAME)3.2
|
||||
RUBY_SO_NAME = ruby-3.2
|
||||
RUBYW_INSTALL_NAME =
|
||||
RUBY_VERSION_NAME = $(RUBY_BASE_NAME)-$(ruby_version)
|
||||
RUBYW_BASE_NAME = rubyw
|
||||
RUBY_BASE_NAME = ruby
|
||||
|
||||
arch = x86_64-linux-gnu
|
||||
sitearch = $(arch)
|
||||
ruby_version = 3.2.0
|
||||
ruby = $(bindir)/$(RUBY_BASE_NAME)3.2
|
||||
RUBY = $(ruby)
|
||||
BUILTRUBY = $(bindir)/$(RUBY_BASE_NAME)3.2
|
||||
ruby_headers = $(hdrdir)/ruby.h $(hdrdir)/ruby/backward.h $(hdrdir)/ruby/ruby.h $(hdrdir)/ruby/defines.h $(hdrdir)/ruby/missing.h $(hdrdir)/ruby/intern.h $(hdrdir)/ruby/st.h $(hdrdir)/ruby/subst.h $(arch_hdrdir)/ruby/config.h
|
||||
|
||||
RM = rm -f
|
||||
RM_RF = rm -fr
|
||||
RMDIRS = rmdir --ignore-fail-on-non-empty -p
|
||||
MAKEDIRS = /bin/mkdir -p
|
||||
INSTALL = /usr/bin/install -c
|
||||
INSTALL_PROG = $(INSTALL) -m 0755
|
||||
INSTALL_DATA = $(INSTALL) -m 644
|
||||
COPY = cp
|
||||
TOUCH = exit >
|
||||
|
||||
#### End of system configuration section. ####
|
||||
|
||||
preload =
|
||||
libpath = . $(archlibdir)
|
||||
LIBPATH = -L. -L$(archlibdir)
|
||||
DEFFILE =
|
||||
|
||||
CLEANFILES = mkmf.log
|
||||
DISTCLEANFILES =
|
||||
DISTCLEANDIRS =
|
||||
|
||||
extout =
|
||||
extout_prefix =
|
||||
target_prefix =
|
||||
LOCAL_LIBS =
|
||||
LIBS = $(LIBRUBYARG_SHARED) -lssl -lcrypto -lcrypto -lssl -lm -lpthread -lc
|
||||
ORIG_SRCS = binder.cpp cmain.cpp ed.cpp em.cpp kb.cpp page.cpp pipe.cpp rubymain.cpp ssl.cpp
|
||||
SRCS = $(ORIG_SRCS)
|
||||
OBJS = binder.o cmain.o ed.o em.o kb.o page.o pipe.o rubymain.o ssl.o
|
||||
HDRS = $(srcdir)/binder.h $(srcdir)/ed.h $(srcdir)/em.h $(srcdir)/eventmachine.h $(srcdir)/page.h $(srcdir)/project.h $(srcdir)/ssl.h
|
||||
LOCAL_HDRS =
|
||||
TARGET = rubyeventmachine
|
||||
TARGET_NAME = rubyeventmachine
|
||||
TARGET_ENTRY = Init_$(TARGET_NAME)
|
||||
DLLIB = $(TARGET).so
|
||||
EXTSTATIC =
|
||||
STATIC_LIB =
|
||||
|
||||
TIMESTAMP_DIR = .
|
||||
BINDIR = $(bindir)
|
||||
RUBYCOMMONDIR = $(sitedir)$(target_prefix)
|
||||
RUBYLIBDIR = $(sitelibdir)$(target_prefix)
|
||||
RUBYARCHDIR = $(sitearchdir)$(target_prefix)
|
||||
HDRDIR = $(sitehdrdir)$(target_prefix)
|
||||
ARCHHDRDIR = $(sitearchhdrdir)$(target_prefix)
|
||||
TARGET_SO_DIR =
|
||||
TARGET_SO = $(TARGET_SO_DIR)$(DLLIB)
|
||||
CLEANLIBS = $(TARGET_SO) false
|
||||
CLEANOBJS = $(OBJS) *.bak
|
||||
TARGET_SO_DIR_TIMESTAMP = $(TIMESTAMP_DIR)/.sitearchdir.time
|
||||
|
||||
all: $(DLLIB)
|
||||
static: $(STATIC_LIB)
|
||||
.PHONY: all install static install-so install-rb
|
||||
.PHONY: clean clean-so clean-static clean-rb
|
||||
|
||||
clean-static::
|
||||
clean-rb-default::
|
||||
clean-rb::
|
||||
clean-so::
|
||||
clean: clean-so clean-static clean-rb-default clean-rb
|
||||
-$(Q)$(RM_RF) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES) .*.time
|
||||
|
||||
distclean-rb-default::
|
||||
distclean-rb::
|
||||
distclean-so::
|
||||
distclean-static::
|
||||
distclean: clean distclean-so distclean-static distclean-rb-default distclean-rb
|
||||
-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
|
||||
-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
|
||||
-$(Q)$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true
|
||||
|
||||
realclean: distclean
|
||||
install: install-so install-rb
|
||||
|
||||
install-so: $(DLLIB) $(TARGET_SO_DIR_TIMESTAMP)
|
||||
$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
|
||||
clean-static::
|
||||
-$(Q)$(RM) $(STATIC_LIB)
|
||||
install-rb: pre-install-rb do-install-rb install-rb-default
|
||||
install-rb-default: pre-install-rb-default do-install-rb-default
|
||||
pre-install-rb: Makefile
|
||||
pre-install-rb-default: Makefile
|
||||
do-install-rb:
|
||||
do-install-rb-default:
|
||||
pre-install-rb-default:
|
||||
@$(NULLCMD)
|
||||
$(TARGET_SO_DIR_TIMESTAMP):
|
||||
$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)
|
||||
$(Q) $(TOUCH) $@
|
||||
|
||||
site-install: site-install-so site-install-rb
|
||||
site-install-so: install-so
|
||||
site-install-rb: install-rb
|
||||
|
||||
.SUFFIXES: .c .m .cc .mm .cxx .cpp .o .S
|
||||
|
||||
.cc.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cc.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.mm.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.mm.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.cxx.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cxx.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.cpp.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cpp.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.c.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.c.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.m.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.m.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
$(TARGET_SO): $(OBJS) Makefile
|
||||
$(ECHO) linking shared-object $(DLLIB)
|
||||
-$(Q)$(RM) $(@)
|
||||
$(Q) $(LDSHAREDXX) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
|
||||
|
||||
|
||||
|
||||
$(OBJS): $(HDRS) $(ruby_headers)
|
||||
@@ -0,0 +1,124 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: binder.cpp
|
||||
Date: 07Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#include "project.h"
|
||||
|
||||
#define DEV_URANDOM "/dev/urandom"
|
||||
|
||||
|
||||
std::map<uintptr_t, Bindable_t*> Bindable_t::BindingBag;
|
||||
|
||||
|
||||
/********************************
|
||||
STATIC Bindable_t::CreateBinding
|
||||
********************************/
|
||||
|
||||
uintptr_t Bindable_t::CreateBinding()
|
||||
{
|
||||
static uintptr_t num = 0;
|
||||
while(BindingBag[++num]) {}
|
||||
return num;
|
||||
}
|
||||
|
||||
#if 0
|
||||
string Bindable_t::CreateBinding()
|
||||
{
|
||||
static int index = 0;
|
||||
static string seed;
|
||||
|
||||
if ((index >= 1000000) || (seed.length() == 0)) {
|
||||
#ifdef OS_UNIX
|
||||
int fd = open (DEV_URANDOM, O_RDONLY);
|
||||
if (fd < 0)
|
||||
throw std::runtime_error ("No entropy device");
|
||||
|
||||
unsigned char u[16];
|
||||
size_t r = read (fd, u, sizeof(u));
|
||||
if (r < sizeof(u))
|
||||
throw std::runtime_error ("Unable to read entropy device");
|
||||
|
||||
unsigned char *u1 = (unsigned char*)u;
|
||||
char u2 [sizeof(u) * 2 + 1];
|
||||
|
||||
for (size_t i=0; i < sizeof(u); i++)
|
||||
sprintf (u2 + (i * 2), "%02x", u1[i]);
|
||||
|
||||
seed = string (u2);
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef OS_WIN32
|
||||
UUID uuid;
|
||||
UuidCreate (&uuid);
|
||||
unsigned char *uuidstring = NULL;
|
||||
UuidToString (&uuid, &uuidstring);
|
||||
if (!uuidstring)
|
||||
throw std::runtime_error ("Unable to read uuid");
|
||||
seed = string ((const char*)uuidstring);
|
||||
|
||||
RpcStringFree (&uuidstring);
|
||||
#endif
|
||||
|
||||
index = 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
stringstream ss;
|
||||
ss << seed << (++index);
|
||||
return ss.str();
|
||||
}
|
||||
#endif
|
||||
|
||||
/*****************************
|
||||
STATIC: Bindable_t::GetObject
|
||||
*****************************/
|
||||
|
||||
Bindable_t *Bindable_t::GetObject (const uintptr_t binding)
|
||||
{
|
||||
std::map<uintptr_t, Bindable_t*>::const_iterator i = BindingBag.find (binding);
|
||||
if (i != BindingBag.end())
|
||||
return i->second;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
Bindable_t::Bindable_t
|
||||
**********************/
|
||||
|
||||
Bindable_t::Bindable_t()
|
||||
{
|
||||
Binding = Bindable_t::CreateBinding();
|
||||
BindingBag [Binding] = this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************
|
||||
Bindable_t::~Bindable_t
|
||||
***********************/
|
||||
|
||||
Bindable_t::~Bindable_t() NO_EXCEPT_FALSE
|
||||
{
|
||||
BindingBag.erase (Binding);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: binder.h
|
||||
Date: 07Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef __ObjectBindings__H_
|
||||
#define __ObjectBindings__H_
|
||||
|
||||
|
||||
#if __cplusplus >= 201103L
|
||||
#define NO_EXCEPT_FALSE noexcept(false)
|
||||
#else
|
||||
#define NO_EXCEPT_FALSE
|
||||
#endif
|
||||
|
||||
class Bindable_t
|
||||
{
|
||||
public:
|
||||
static uintptr_t CreateBinding();
|
||||
static Bindable_t *GetObject (const uintptr_t);
|
||||
static std::map<uintptr_t, Bindable_t*> BindingBag;
|
||||
|
||||
public:
|
||||
Bindable_t();
|
||||
virtual ~Bindable_t() NO_EXCEPT_FALSE;
|
||||
|
||||
const uintptr_t GetBinding() {return Binding;}
|
||||
|
||||
private:
|
||||
uintptr_t Binding;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif // __ObjectBindings__H_
|
||||
|
||||
@@ -0,0 +1,988 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: cmain.cpp
|
||||
Date: 06Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#include "project.h"
|
||||
|
||||
/* 21Sep09: ruby 1.9 defines macros for common i/o functions that point to rb_w32_* implementations.
|
||||
We need to undef the stat to fix a build failure in evma_send_file_data_to_connection.
|
||||
See http://groups.google.com/group/eventmachine/browse_thread/thread/fc60d9bb738ffc71
|
||||
*/
|
||||
#if defined(BUILD_FOR_RUBY) && defined(OS_WIN32)
|
||||
#undef stat
|
||||
#undef fstat
|
||||
#endif
|
||||
|
||||
static EventMachine_t *EventMachine;
|
||||
static Poller_t Poller = Poller_Default;
|
||||
|
||||
extern "C" void ensure_eventmachine (const char *caller = "unknown caller")
|
||||
{
|
||||
if (!EventMachine) {
|
||||
const int err_size = 128;
|
||||
char err_string[err_size];
|
||||
snprintf (err_string, err_size, "eventmachine not initialized: %s", caller);
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
rb_raise(rb_eRuntimeError, "%s", err_string);
|
||||
#else
|
||||
throw std::runtime_error (err_string);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/***********************
|
||||
evma_initialize_library
|
||||
***********************/
|
||||
|
||||
extern "C" void evma_initialize_library (EMCallback cb)
|
||||
{
|
||||
if (EventMachine)
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
rb_raise(rb_eRuntimeError, "eventmachine already initialized: evma_initialize_library");
|
||||
#else
|
||||
throw std::runtime_error ("eventmachine already initialized: evma_initialize_library");
|
||||
#endif
|
||||
|
||||
EventMachine = new EventMachine_t (cb, Poller);
|
||||
}
|
||||
|
||||
|
||||
/********************
|
||||
evma_release_library
|
||||
********************/
|
||||
|
||||
extern "C" void evma_release_library()
|
||||
{
|
||||
ensure_eventmachine("evma_release_library");
|
||||
delete EventMachine;
|
||||
EventMachine = NULL;
|
||||
}
|
||||
|
||||
|
||||
/*********************
|
||||
evma_run_machine_once
|
||||
*********************/
|
||||
|
||||
extern "C" bool evma_run_machine_once()
|
||||
{
|
||||
ensure_eventmachine("evma_run_machine_once");
|
||||
return EventMachine->RunOnce();
|
||||
}
|
||||
|
||||
|
||||
/****************
|
||||
evma_run_machine
|
||||
****************/
|
||||
|
||||
extern "C" void evma_run_machine()
|
||||
{
|
||||
ensure_eventmachine("evma_run_machine");
|
||||
EventMachine->Run();
|
||||
}
|
||||
|
||||
|
||||
/**************************
|
||||
evma_install_oneshot_timer
|
||||
**************************/
|
||||
|
||||
extern "C" const uintptr_t evma_install_oneshot_timer (uint64_t milliseconds)
|
||||
{
|
||||
ensure_eventmachine("evma_install_oneshot_timer");
|
||||
return EventMachine->InstallOneshotTimer (milliseconds);
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
evma_connect_to_server
|
||||
**********************/
|
||||
|
||||
extern "C" const uintptr_t evma_connect_to_server (const char *bind_addr, int bind_port, const char *server, int port)
|
||||
{
|
||||
ensure_eventmachine("evma_connect_to_server");
|
||||
return EventMachine->ConnectToServer (bind_addr, bind_port, server, port);
|
||||
}
|
||||
|
||||
/***************************
|
||||
evma_connect_to_unix_server
|
||||
***************************/
|
||||
|
||||
extern "C" const uintptr_t evma_connect_to_unix_server (const char *server)
|
||||
{
|
||||
ensure_eventmachine("evma_connect_to_unix_server");
|
||||
return EventMachine->ConnectToUnixServer (server);
|
||||
}
|
||||
|
||||
/**************
|
||||
evma_attach_fd
|
||||
**************/
|
||||
|
||||
extern "C" const uintptr_t evma_attach_fd (int file_descriptor, int watch_mode)
|
||||
{
|
||||
ensure_eventmachine("evma_attach_fd");
|
||||
return EventMachine->AttachFD (file_descriptor, watch_mode ? true : false);
|
||||
}
|
||||
|
||||
/**************
|
||||
evma_detach_fd
|
||||
**************/
|
||||
|
||||
extern "C" int evma_detach_fd (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_detach_fd");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return EventMachine->DetachFD (ed);
|
||||
else
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
rb_raise(rb_eRuntimeError, "invalid binding to detach");
|
||||
#else
|
||||
throw std::runtime_error ("invalid binding to detach");
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************
|
||||
evma_get_file_descriptor
|
||||
************************/
|
||||
|
||||
extern "C" int evma_get_file_descriptor (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_file_descriptor");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetSocket();
|
||||
else
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
rb_raise(rb_eRuntimeError, "invalid binding to get_fd");
|
||||
#else
|
||||
throw std::runtime_error ("invalid binding to get_fd");
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/***********************
|
||||
evma_is_notify_readable
|
||||
***********************/
|
||||
|
||||
extern "C" int evma_is_notify_readable (const uintptr_t binding)
|
||||
{
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
return cd->IsNotifyReadable() ? 1 : 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************
|
||||
evma_set_notify_readable
|
||||
************************/
|
||||
|
||||
extern "C" void evma_set_notify_readable (const uintptr_t binding, int mode)
|
||||
{
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
cd->SetNotifyReadable (mode ? true : false);
|
||||
}
|
||||
|
||||
/***********************
|
||||
evma_is_notify_writable
|
||||
***********************/
|
||||
|
||||
extern "C" int evma_is_notify_writable (const uintptr_t binding)
|
||||
{
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
return cd->IsNotifyWritable() ? 1 : 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************
|
||||
evma_set_notify_writable
|
||||
************************/
|
||||
|
||||
extern "C" void evma_set_notify_writable (const uintptr_t binding, int mode)
|
||||
{
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
cd->SetNotifyWritable (mode ? true : false);
|
||||
}
|
||||
|
||||
/**********
|
||||
evma_pause
|
||||
**********/
|
||||
|
||||
extern "C" int evma_pause (const uintptr_t binding)
|
||||
{
|
||||
EventableDescriptor *cd = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
return cd->Pause() ? 1 : 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/***********
|
||||
evma_resume
|
||||
***********/
|
||||
|
||||
extern "C" int evma_resume (const uintptr_t binding)
|
||||
{
|
||||
EventableDescriptor *cd = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
return cd->Resume() ? 1 : 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**************
|
||||
evma_is_paused
|
||||
**************/
|
||||
|
||||
extern "C" int evma_is_paused (const uintptr_t binding)
|
||||
{
|
||||
EventableDescriptor *cd = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
return cd->IsPaused() ? 1 : 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/************************
|
||||
evma_num_close_scheduled
|
||||
************************/
|
||||
|
||||
extern "C" int evma_num_close_scheduled ()
|
||||
{
|
||||
ensure_eventmachine("evma_num_close_scheduled");
|
||||
return EventMachine->NumCloseScheduled;
|
||||
}
|
||||
|
||||
/**********************
|
||||
evma_create_tcp_server
|
||||
**********************/
|
||||
|
||||
extern "C" const uintptr_t evma_create_tcp_server (const char *address, int port)
|
||||
{
|
||||
ensure_eventmachine("evma_create_tcp_server");
|
||||
return EventMachine->CreateTcpServer (address, port);
|
||||
}
|
||||
|
||||
/******************************
|
||||
evma_create_unix_domain_server
|
||||
******************************/
|
||||
|
||||
extern "C" const uintptr_t evma_create_unix_domain_server (const char *filename)
|
||||
{
|
||||
ensure_eventmachine("evma_create_unix_domain_server");
|
||||
return EventMachine->CreateUnixDomainServer (filename);
|
||||
}
|
||||
|
||||
/***********************
|
||||
evma_attach_sd
|
||||
************************/
|
||||
|
||||
extern "C" const uintptr_t evma_attach_sd (int sd)
|
||||
{
|
||||
ensure_eventmachine("evma_attach_sd");
|
||||
return EventMachine->AttachSD (sd);
|
||||
}
|
||||
|
||||
/*************************
|
||||
evma_open_datagram_socket
|
||||
*************************/
|
||||
|
||||
extern "C" const uintptr_t evma_open_datagram_socket (const char *address, int port)
|
||||
{
|
||||
ensure_eventmachine("evma_open_datagram_socket");
|
||||
return EventMachine->OpenDatagramSocket (address, port);
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_open_keyboard
|
||||
******************/
|
||||
|
||||
extern "C" const uintptr_t evma_open_keyboard()
|
||||
{
|
||||
ensure_eventmachine("evma_open_keyboard");
|
||||
return EventMachine->OpenKeyboard();
|
||||
}
|
||||
|
||||
/*******************
|
||||
evma_watch_filename
|
||||
*******************/
|
||||
|
||||
extern "C" const uintptr_t evma_watch_filename (const char *fname)
|
||||
{
|
||||
ensure_eventmachine("evma_watch_filename");
|
||||
return EventMachine->WatchFile(fname);
|
||||
}
|
||||
|
||||
/*********************
|
||||
evma_unwatch_filename
|
||||
*********************/
|
||||
|
||||
extern "C" void evma_unwatch_filename (const uintptr_t sig)
|
||||
{
|
||||
ensure_eventmachine("evma_unwatch_file");
|
||||
EventMachine->UnwatchFile(sig);
|
||||
}
|
||||
|
||||
/**************
|
||||
evma_watch_pid
|
||||
**************/
|
||||
|
||||
extern "C" const uintptr_t evma_watch_pid (int pid)
|
||||
{
|
||||
ensure_eventmachine("evma_watch_pid");
|
||||
return EventMachine->WatchPid(pid);
|
||||
}
|
||||
|
||||
/****************
|
||||
evma_unwatch_pid
|
||||
****************/
|
||||
|
||||
extern "C" void evma_unwatch_pid (const uintptr_t sig)
|
||||
{
|
||||
ensure_eventmachine("evma_unwatch_pid");
|
||||
EventMachine->UnwatchPid(sig);
|
||||
}
|
||||
|
||||
/****************************
|
||||
evma_send_data_to_connection
|
||||
****************************/
|
||||
|
||||
extern "C" int evma_send_data_to_connection (const uintptr_t binding, const char *data, int data_length)
|
||||
{
|
||||
ensure_eventmachine("evma_send_data_to_connection");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->SendOutboundData(data, data_length);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_send_datagram
|
||||
******************/
|
||||
|
||||
extern "C" int evma_send_datagram (const uintptr_t binding, const char *data, int data_length, const char *address, int port)
|
||||
{
|
||||
ensure_eventmachine("evma_send_datagram");
|
||||
DatagramDescriptor *dd = dynamic_cast <DatagramDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (dd)
|
||||
return dd->SendOutboundDatagram(data, data_length, address, port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/*********************
|
||||
evma_close_connection
|
||||
*********************/
|
||||
|
||||
extern "C" void evma_close_connection (const uintptr_t binding, int after_writing)
|
||||
{
|
||||
ensure_eventmachine("evma_close_connection");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
ed->ScheduleClose (after_writing ? true : false);
|
||||
}
|
||||
|
||||
/***********************************
|
||||
evma_report_connection_error_status
|
||||
***********************************/
|
||||
|
||||
extern "C" int evma_report_connection_error_status (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_report_connection_error_status");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->ReportErrorStatus();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/********************
|
||||
evma_stop_tcp_server
|
||||
********************/
|
||||
|
||||
extern "C" void evma_stop_tcp_server (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_stop_tcp_server");
|
||||
AcceptorDescriptor::StopAcceptor (binding);
|
||||
}
|
||||
|
||||
|
||||
/*****************
|
||||
evma_stop_machine
|
||||
*****************/
|
||||
|
||||
extern "C" void evma_stop_machine()
|
||||
{
|
||||
ensure_eventmachine("evma_stop_machine");
|
||||
EventMachine->ScheduleHalt();
|
||||
}
|
||||
|
||||
/*****************
|
||||
evma_stopping
|
||||
*****************/
|
||||
|
||||
extern "C" bool evma_stopping()
|
||||
{
|
||||
ensure_eventmachine("evma_stopping");
|
||||
return EventMachine->Stopping();
|
||||
}
|
||||
|
||||
/**************
|
||||
evma_start_tls
|
||||
**************/
|
||||
|
||||
extern "C" void evma_start_tls (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_start_tls");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
ed->StartTls();
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_set_tls_parms
|
||||
******************/
|
||||
|
||||
extern "C" void evma_set_tls_parms (const uintptr_t binding, const char *privatekey_filename, const char *certchain_filename, int verify_peer, int fail_if_no_peer_cert, const char *sni_hostname, const char *cipherlist, const char *ecdh_curve, const char *dhparam, int ssl_version)
|
||||
{
|
||||
ensure_eventmachine("evma_set_tls_parms");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
ed->SetTlsParms (privatekey_filename, certchain_filename, (verify_peer == 1 ? true : false), (fail_if_no_peer_cert == 1 ? true : false), sni_hostname, cipherlist, ecdh_curve, dhparam, ssl_version);
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_get_peer_cert
|
||||
******************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" X509 *evma_get_peer_cert (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_peer_cert");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetPeerCert();
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/******************
|
||||
evma_get_cipher_bits
|
||||
******************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" int evma_get_cipher_bits (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_cipher_bits");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetCipherBits();
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/******************
|
||||
evma_get_cipher_name
|
||||
******************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" const char *evma_get_cipher_name (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_cipher_name");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetCipherName();
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/******************
|
||||
evma_get_cipher_protocol
|
||||
******************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" const char *evma_get_cipher_protocol (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_cipher_protocol");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetCipherProtocol();
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/******************
|
||||
evma_get_sni_hostname
|
||||
******************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" const char *evma_get_sni_hostname (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_sni_hostname");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed)
|
||||
return ed->GetSNIHostname();
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/********************
|
||||
evma_accept_ssl_peer
|
||||
********************/
|
||||
|
||||
#ifdef WITH_SSL
|
||||
extern "C" void evma_accept_ssl_peer (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_accept_ssl_peer");
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (cd)
|
||||
cd->AcceptSslPeer();
|
||||
}
|
||||
#endif
|
||||
|
||||
/*****************
|
||||
evma_get_peername
|
||||
*****************/
|
||||
|
||||
extern "C" int evma_get_peername (const uintptr_t binding, struct sockaddr *sa, socklen_t *len)
|
||||
{
|
||||
ensure_eventmachine("evma_get_peername");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ed->GetPeername (sa, len) ? 1 : 0;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************
|
||||
evma_get_sockname
|
||||
*****************/
|
||||
|
||||
extern "C" int evma_get_sockname (const uintptr_t binding, struct sockaddr *sa, socklen_t *len)
|
||||
{
|
||||
ensure_eventmachine("evma_get_sockname");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ed->GetSockname (sa, len) ? 1 : 0;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/***********************
|
||||
evma_get_subprocess_pid
|
||||
***********************/
|
||||
|
||||
#ifdef OS_UNIX
|
||||
extern "C" int evma_get_subprocess_pid (const uintptr_t binding, pid_t *pid)
|
||||
{
|
||||
ensure_eventmachine("evma_get_subprocess_pid");
|
||||
PipeDescriptor *pd = dynamic_cast <PipeDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (pd) {
|
||||
return pd->GetSubprocessPid (pid) ? 1 : 0;
|
||||
}
|
||||
else if (pid && EventMachine->SubprocessPid) {
|
||||
*pid = EventMachine->SubprocessPid;
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
extern "C" int evma_get_subprocess_pid (const uintptr_t binding UNUSED, pid_t *pid UNUSED)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**************************
|
||||
evma_get_subprocess_status
|
||||
**************************/
|
||||
|
||||
extern "C" int evma_get_subprocess_status (const uintptr_t binding UNUSED, int *status)
|
||||
{
|
||||
ensure_eventmachine("evma_get_subprocess_status");
|
||||
if (status) {
|
||||
*status = EventMachine->SubprocessExitStatus;
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*************************
|
||||
evma_get_connection_count
|
||||
*************************/
|
||||
|
||||
extern "C" int evma_get_connection_count()
|
||||
{
|
||||
ensure_eventmachine("evma_get_connection_count");
|
||||
return EventMachine->GetConnectionCount();
|
||||
}
|
||||
|
||||
/*********************
|
||||
evma_signal_loopbreak
|
||||
*********************/
|
||||
|
||||
extern "C" void evma_signal_loopbreak()
|
||||
{
|
||||
ensure_eventmachine("evma_signal_loopbreak");
|
||||
EventMachine->SignalLoopBreaker();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************
|
||||
evma_get_comm_inactivity_timeout
|
||||
********************************/
|
||||
|
||||
extern "C" float evma_get_comm_inactivity_timeout (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_comm_inactivity_timeout");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ((float)ed->GetCommInactivityTimeout() / 1000);
|
||||
}
|
||||
else
|
||||
return 0.0; //Perhaps this should be an exception. Access to an unknown binding.
|
||||
}
|
||||
|
||||
/********************************
|
||||
evma_set_comm_inactivity_timeout
|
||||
********************************/
|
||||
|
||||
extern "C" int evma_set_comm_inactivity_timeout (const uintptr_t binding, float value)
|
||||
{
|
||||
ensure_eventmachine("evma_set_comm_inactivity_timeout");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ed->SetCommInactivityTimeout ((uint64_t)(value * 1000));
|
||||
}
|
||||
else
|
||||
return 0; //Perhaps this should be an exception. Access to an unknown binding.
|
||||
}
|
||||
|
||||
|
||||
/********************************
|
||||
evma_get_pending_connect_timeout
|
||||
********************************/
|
||||
|
||||
extern "C" float evma_get_pending_connect_timeout (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_pending_connect_timeout");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ((float)ed->GetPendingConnectTimeout() / 1000);
|
||||
}
|
||||
else
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
/********************************
|
||||
evma_set_pending_connect_timeout
|
||||
********************************/
|
||||
|
||||
extern "C" int evma_set_pending_connect_timeout (const uintptr_t binding, float value)
|
||||
{
|
||||
ensure_eventmachine("evma_set_pending_connect_timeout");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
if (ed) {
|
||||
return ed->SetPendingConnectTimeout ((uint64_t)(value * 1000));
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
evma_set_timer_quantum
|
||||
**********************/
|
||||
|
||||
extern "C" void evma_set_timer_quantum (int interval)
|
||||
{
|
||||
ensure_eventmachine("evma_set_timer_quantum");
|
||||
EventMachine->SetTimerQuantum (interval);
|
||||
}
|
||||
|
||||
|
||||
/************************
|
||||
evma_get_max_timer_count
|
||||
************************/
|
||||
|
||||
extern "C" int evma_get_max_timer_count()
|
||||
{
|
||||
return EventMachine_t::GetMaxTimerCount();
|
||||
}
|
||||
|
||||
/************************
|
||||
evma_set_max_timer_count
|
||||
************************/
|
||||
|
||||
extern "C" void evma_set_max_timer_count (int ct)
|
||||
{
|
||||
// This may only be called if the reactor is not running.
|
||||
|
||||
if (EventMachine)
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
rb_raise(rb_eRuntimeError, "eventmachine already initialized: evma_set_max_timer_count");
|
||||
#else
|
||||
throw std::runtime_error ("eventmachine already initialized: evma_set_max_timer_count");
|
||||
#endif
|
||||
EventMachine_t::SetMaxTimerCount (ct);
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_get/set_simultaneous_accept_count
|
||||
******************/
|
||||
|
||||
extern "C" void evma_set_simultaneous_accept_count (int count)
|
||||
{
|
||||
EventMachine_t::SetSimultaneousAcceptCount(count);
|
||||
}
|
||||
|
||||
extern "C" int evma_get_simultaneous_accept_count()
|
||||
{
|
||||
return EventMachine_t::GetSimultaneousAcceptCount();
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
evma_setuid_string
|
||||
******************/
|
||||
|
||||
extern "C" void evma_setuid_string (const char *username)
|
||||
{
|
||||
// We do NOT need to be running an EM instance because this method is static.
|
||||
EventMachine_t::SetuidString (username);
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
evma_popen
|
||||
**********/
|
||||
|
||||
extern "C" const uintptr_t evma_popen (char * const*cmd_strings)
|
||||
{
|
||||
ensure_eventmachine("evma_popen");
|
||||
return EventMachine->Socketpair (cmd_strings);
|
||||
}
|
||||
|
||||
|
||||
/***************************
|
||||
evma_get_outbound_data_size
|
||||
***************************/
|
||||
|
||||
extern "C" int evma_get_outbound_data_size (const uintptr_t binding)
|
||||
{
|
||||
ensure_eventmachine("evma_get_outbound_data_size");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
|
||||
return ed ? ed->GetOutboundDataSize() : 0;
|
||||
}
|
||||
|
||||
|
||||
/**************
|
||||
evma_set_epoll
|
||||
**************/
|
||||
|
||||
extern "C" void evma_set_epoll (int use)
|
||||
{
|
||||
if (use)
|
||||
Poller = Poller_Epoll;
|
||||
else
|
||||
Poller = Poller_Default;
|
||||
}
|
||||
|
||||
/***************
|
||||
evma_set_kqueue
|
||||
***************/
|
||||
|
||||
extern "C" void evma_set_kqueue (int use)
|
||||
{
|
||||
if (use)
|
||||
Poller = Poller_Kqueue;
|
||||
else
|
||||
Poller = Poller_Default;
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
evma_set_rlimit_nofile
|
||||
**********************/
|
||||
|
||||
extern "C" int evma_set_rlimit_nofile (int nofiles)
|
||||
{
|
||||
return EventMachine_t::SetRlimitNofile (nofiles);
|
||||
}
|
||||
|
||||
|
||||
/*********************************
|
||||
evma_send_file_data_to_connection
|
||||
*********************************/
|
||||
|
||||
extern "C" int evma_send_file_data_to_connection (const uintptr_t binding, const char *filename)
|
||||
{
|
||||
/* This is a sugaring over send_data_to_connection that reads a file into a
|
||||
* locally-allocated buffer, and sends the file data to the remote peer.
|
||||
* Return the number of bytes written to the caller.
|
||||
* TODO, needs to impose a limit on the file size. This is intended only for
|
||||
* small files. (I don't know, maybe 8K or less.) For larger files, use interleaved
|
||||
* I/O to avoid slowing the rest of the system down.
|
||||
* TODO: we should return a code rather than barf, in case of file-not-found.
|
||||
* TODO, does this compile on Windows?
|
||||
* TODO, given that we want this to work only with small files, how about allocating
|
||||
* the buffer on the stack rather than the heap?
|
||||
*
|
||||
* Modified 25Jul07. This now returns -1 on file-too-large; 0 for success, and a positive
|
||||
* errno in case of other errors.
|
||||
*
|
||||
* Contributed by Kirk Haines.
|
||||
*/
|
||||
|
||||
char data[32*1024];
|
||||
int r;
|
||||
|
||||
ensure_eventmachine("evma_send_file_data_to_connection");
|
||||
|
||||
#if defined(OS_WIN32)
|
||||
int Fd = open (filename, O_RDONLY|O_BINARY);
|
||||
#else
|
||||
int Fd = open (filename, O_RDONLY);
|
||||
#endif
|
||||
if (Fd < 0)
|
||||
return errno;
|
||||
// From here on, all early returns MUST close Fd.
|
||||
|
||||
struct stat st;
|
||||
if (fstat (Fd, &st)) {
|
||||
int e = errno;
|
||||
close (Fd);
|
||||
return e;
|
||||
}
|
||||
|
||||
off_t filesize = st.st_size;
|
||||
if (filesize <= 0) {
|
||||
close (Fd);
|
||||
return 0;
|
||||
}
|
||||
else if (filesize > (off_t) sizeof(data)) {
|
||||
close (Fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
r = read (Fd, data, filesize);
|
||||
if (r != filesize) {
|
||||
int e = errno;
|
||||
close (Fd);
|
||||
return e;
|
||||
}
|
||||
evma_send_data_to_connection (binding, data, r);
|
||||
close (Fd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/****************
|
||||
evma_start_proxy
|
||||
*****************/
|
||||
|
||||
extern "C" void evma_start_proxy (const uintptr_t from, const uintptr_t to, const unsigned long bufsize, const unsigned long length)
|
||||
{
|
||||
ensure_eventmachine("evma_start_proxy");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (from));
|
||||
if (ed)
|
||||
ed->StartProxy(to, bufsize, length);
|
||||
}
|
||||
|
||||
|
||||
/***************
|
||||
evma_stop_proxy
|
||||
****************/
|
||||
|
||||
extern "C" void evma_stop_proxy (const uintptr_t from)
|
||||
{
|
||||
ensure_eventmachine("evma_stop_proxy");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (from));
|
||||
if (ed)
|
||||
ed->StopProxy();
|
||||
}
|
||||
|
||||
/******************
|
||||
evma_proxied_bytes
|
||||
*******************/
|
||||
|
||||
extern "C" unsigned long evma_proxied_bytes (const uintptr_t from)
|
||||
{
|
||||
ensure_eventmachine("evma_proxied_bytes");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (from));
|
||||
if (ed)
|
||||
return ed->GetProxiedBytes();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/***************************
|
||||
evma_get_last_activity_time
|
||||
****************************/
|
||||
|
||||
extern "C" uint64_t evma_get_last_activity_time(const uintptr_t from)
|
||||
{
|
||||
ensure_eventmachine("evma_get_last_activity_time");
|
||||
EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (from));
|
||||
if (ed)
|
||||
return ed->GetLastActivity();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/***************************
|
||||
evma_get_heartbeat_interval
|
||||
****************************/
|
||||
|
||||
extern "C" float evma_get_heartbeat_interval()
|
||||
{
|
||||
ensure_eventmachine("evma_get_heartbeat_interval");
|
||||
return EventMachine->GetHeartbeatInterval();
|
||||
}
|
||||
|
||||
|
||||
/***************************
|
||||
evma_set_heartbeat_interval
|
||||
****************************/
|
||||
|
||||
extern "C" int evma_set_heartbeat_interval(float interval)
|
||||
{
|
||||
ensure_eventmachine("evma_set_heartbeat_interval");
|
||||
return EventMachine->SetHeartbeatInterval(interval);
|
||||
}
|
||||
|
||||
|
||||
/**************************
|
||||
evma_get_current_loop_time
|
||||
***************************/
|
||||
|
||||
extern "C" uint64_t evma_get_current_loop_time()
|
||||
{
|
||||
ensure_eventmachine("evma_get_current_loop_time");
|
||||
return EventMachine->GetCurrentLoopTime();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,457 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: ed.h
|
||||
Date: 06Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef __EventableDescriptor__H_
|
||||
#define __EventableDescriptor__H_
|
||||
|
||||
|
||||
class EventMachine_t; // forward reference
|
||||
#ifdef WITH_SSL
|
||||
class SslBox_t; // forward reference
|
||||
#endif
|
||||
|
||||
bool SetSocketNonblocking (SOCKET);
|
||||
bool SetFdCloexec (int);
|
||||
|
||||
/*************************
|
||||
class EventableDescriptor
|
||||
*************************/
|
||||
|
||||
class EventableDescriptor: public Bindable_t
|
||||
{
|
||||
public:
|
||||
EventableDescriptor (SOCKET, EventMachine_t*);
|
||||
virtual ~EventableDescriptor() NO_EXCEPT_FALSE;
|
||||
|
||||
SOCKET GetSocket() {return MySocket;}
|
||||
void SetSocketInvalid() { MySocket = INVALID_SOCKET; }
|
||||
void Close();
|
||||
|
||||
virtual void Read() = 0;
|
||||
virtual void Write() = 0;
|
||||
virtual void Heartbeat() = 0;
|
||||
|
||||
// These methods tell us whether the descriptor
|
||||
// should be selected or polled for read/write.
|
||||
virtual bool SelectForRead() = 0;
|
||||
virtual bool SelectForWrite() = 0;
|
||||
|
||||
// are we scheduled for a close, or in an error state, or already closed?
|
||||
bool ShouldDelete();
|
||||
// Do we have any data to write? This is used by ShouldDelete.
|
||||
virtual int GetOutboundDataSize() {return 0;}
|
||||
virtual bool IsWatchOnly(){ return bWatchOnly; }
|
||||
|
||||
virtual void ScheduleClose (bool after_writing);
|
||||
bool IsCloseScheduled();
|
||||
virtual void HandleError(){ ScheduleClose (false); }
|
||||
|
||||
void SetEventCallback (EMCallback);
|
||||
|
||||
virtual bool GetPeername (struct sockaddr*, socklen_t*) = 0;
|
||||
virtual bool GetSockname (struct sockaddr*, socklen_t*) = 0;
|
||||
virtual bool GetSubprocessPid (pid_t*) {return false;}
|
||||
|
||||
virtual void StartTls() {}
|
||||
virtual void SetTlsParms (const char *, const char *, bool, bool, const char *, const char *, const char *, const char *, int) {}
|
||||
|
||||
#ifdef WITH_SSL
|
||||
virtual X509 *GetPeerCert() {return NULL;}
|
||||
virtual int GetCipherBits() {return -1;}
|
||||
virtual const char *GetCipherName() {return NULL;}
|
||||
virtual const char *GetCipherProtocol() {return NULL;}
|
||||
virtual const char *GetSNIHostname() {return NULL;}
|
||||
#endif
|
||||
|
||||
virtual uint64_t GetCommInactivityTimeout() {return 0;}
|
||||
virtual int SetCommInactivityTimeout (uint64_t) {return 0;}
|
||||
uint64_t GetPendingConnectTimeout();
|
||||
int SetPendingConnectTimeout (uint64_t value);
|
||||
uint64_t GetLastActivity() { return LastActivity; }
|
||||
|
||||
#ifdef HAVE_EPOLL
|
||||
struct epoll_event *GetEpollEvent() { return &EpollEvent; }
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
bool GetKqueueArmWrite() { return bKqueueArmWrite; }
|
||||
#endif
|
||||
|
||||
virtual void StartProxy(const uintptr_t, const unsigned long, const unsigned long);
|
||||
virtual void StopProxy();
|
||||
virtual unsigned long GetProxiedBytes(){ return ProxiedBytes; };
|
||||
virtual void SetProxiedFrom(EventableDescriptor*, const unsigned long);
|
||||
virtual int SendOutboundData(const char*,unsigned long){ return -1; }
|
||||
virtual bool IsPaused(){ return bPaused; }
|
||||
virtual bool Pause(){ bPaused = true; return bPaused; }
|
||||
virtual bool Resume(){ bPaused = false; return bPaused; }
|
||||
|
||||
void SetUnbindReasonCode(int code){ UnbindReasonCode = code; }
|
||||
virtual int ReportErrorStatus(){ return 0; }
|
||||
virtual bool IsConnectPending(){ return false; }
|
||||
virtual uint64_t GetNextHeartbeat();
|
||||
|
||||
private:
|
||||
bool bCloseNow;
|
||||
bool bCloseAfterWriting;
|
||||
|
||||
protected:
|
||||
SOCKET MySocket;
|
||||
bool bAttached;
|
||||
bool bWatchOnly;
|
||||
|
||||
EMCallback EventCallback;
|
||||
void _GenericInboundDispatch (const char *buffer, unsigned long size);
|
||||
bool _GenericGetPeername (struct sockaddr*, socklen_t*);
|
||||
bool _GenericGetSockname (struct sockaddr*, socklen_t*);
|
||||
|
||||
uint64_t CreatedAt;
|
||||
bool bCallbackUnbind;
|
||||
int UnbindReasonCode;
|
||||
|
||||
unsigned long BytesToProxy;
|
||||
EventableDescriptor *ProxyTarget;
|
||||
EventableDescriptor *ProxiedFrom;
|
||||
unsigned long ProxiedBytes;
|
||||
|
||||
unsigned long MaxOutboundBufSize;
|
||||
|
||||
#ifdef HAVE_EPOLL
|
||||
struct epoll_event EpollEvent;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
bool bKqueueArmWrite;
|
||||
#endif
|
||||
|
||||
EventMachine_t *MyEventMachine;
|
||||
uint64_t PendingConnectTimeout;
|
||||
uint64_t InactivityTimeout;
|
||||
uint64_t LastActivity;
|
||||
uint64_t NextHeartbeat;
|
||||
bool bPaused;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*************************
|
||||
class LoopbreakDescriptor
|
||||
*************************/
|
||||
|
||||
class LoopbreakDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
LoopbreakDescriptor (SOCKET, EventMachine_t*);
|
||||
virtual ~LoopbreakDescriptor() {}
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat() {}
|
||||
|
||||
virtual bool SelectForRead() {return true;}
|
||||
virtual bool SelectForWrite() {return false;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return _GenericGetPeername (s, len); }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); }
|
||||
};
|
||||
|
||||
|
||||
/**************************
|
||||
class ConnectionDescriptor
|
||||
**************************/
|
||||
|
||||
class ConnectionDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
ConnectionDescriptor (SOCKET, EventMachine_t*);
|
||||
virtual ~ConnectionDescriptor();
|
||||
|
||||
int SendOutboundData (const char*, unsigned long);
|
||||
|
||||
void SetConnectPending (bool f);
|
||||
virtual void ScheduleClose (bool after_writing);
|
||||
virtual void HandleError();
|
||||
|
||||
void SetNotifyReadable (bool);
|
||||
void SetNotifyWritable (bool);
|
||||
void SetAttached (bool);
|
||||
void SetWatchOnly (bool);
|
||||
|
||||
bool Pause();
|
||||
bool Resume();
|
||||
|
||||
bool IsNotifyReadable(){ return bNotifyReadable; }
|
||||
bool IsNotifyWritable(){ return bNotifyWritable; }
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat();
|
||||
|
||||
virtual bool SelectForRead();
|
||||
virtual bool SelectForWrite();
|
||||
|
||||
// Do we have any data to write? This is used by ShouldDelete.
|
||||
virtual int GetOutboundDataSize() {return OutboundDataSize;}
|
||||
|
||||
virtual void StartTls();
|
||||
virtual void SetTlsParms (const char *, const char *, bool, bool, const char *, const char *, const char *, const char *, int);
|
||||
|
||||
#ifdef WITH_SSL
|
||||
virtual X509 *GetPeerCert();
|
||||
virtual int GetCipherBits();
|
||||
virtual const char *GetCipherName();
|
||||
virtual const char *GetCipherProtocol();
|
||||
virtual const char *GetSNIHostname();
|
||||
virtual bool VerifySslPeer(const char*);
|
||||
virtual void AcceptSslPeer();
|
||||
#endif
|
||||
|
||||
void SetServerMode() {bIsServer = true;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return _GenericGetPeername (s, len); }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); }
|
||||
|
||||
virtual uint64_t GetCommInactivityTimeout();
|
||||
virtual int SetCommInactivityTimeout (uint64_t value);
|
||||
|
||||
virtual int ReportErrorStatus();
|
||||
virtual bool IsConnectPending(){ return bConnectPending; }
|
||||
|
||||
protected:
|
||||
struct OutboundPage {
|
||||
OutboundPage (const char *b, int l, int o=0): Buffer(b), Length(l), Offset(o) {}
|
||||
void Free() {if (Buffer) free (const_cast<char*>(Buffer)); }
|
||||
const char *Buffer;
|
||||
int Length;
|
||||
int Offset;
|
||||
};
|
||||
|
||||
protected:
|
||||
bool bConnectPending;
|
||||
|
||||
bool bNotifyReadable;
|
||||
bool bNotifyWritable;
|
||||
|
||||
bool bReadAttemptedAfterClose;
|
||||
bool bWriteAttemptedAfterClose;
|
||||
|
||||
std::deque<OutboundPage> OutboundPages;
|
||||
int OutboundDataSize;
|
||||
|
||||
#ifdef WITH_SSL
|
||||
SslBox_t *SslBox;
|
||||
std::string CertChainFilename;
|
||||
std::string PrivateKeyFilename;
|
||||
std::string CipherList;
|
||||
std::string EcdhCurve;
|
||||
std::string DhParam;
|
||||
int Protocols;
|
||||
bool bHandshakeSignaled;
|
||||
bool bSslVerifyPeer;
|
||||
bool bSslFailIfNoPeerCert;
|
||||
std::string SniHostName;
|
||||
bool bSslPeerAccepted;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
bool bGotExtraKqueueEvent;
|
||||
#endif
|
||||
|
||||
bool bIsServer;
|
||||
|
||||
private:
|
||||
void _UpdateEvents();
|
||||
void _UpdateEvents(bool, bool);
|
||||
void _WriteOutboundData();
|
||||
void _DispatchInboundData (const char *buffer, unsigned long size);
|
||||
void _DispatchCiphertext();
|
||||
int _SendRawOutboundData (const char *buffer, unsigned long size);
|
||||
void _CheckHandshakeStatus();
|
||||
|
||||
};
|
||||
|
||||
|
||||
/************************
|
||||
class DatagramDescriptor
|
||||
************************/
|
||||
|
||||
class DatagramDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
DatagramDescriptor (SOCKET, EventMachine_t*);
|
||||
virtual ~DatagramDescriptor();
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat();
|
||||
|
||||
virtual bool SelectForRead() {return true;}
|
||||
virtual bool SelectForWrite();
|
||||
|
||||
int SendOutboundData (const char*, unsigned long);
|
||||
int SendOutboundDatagram (const char*, unsigned long, const char*, int);
|
||||
|
||||
// Do we have any data to write? This is used by ShouldDelete.
|
||||
virtual int GetOutboundDataSize() {return OutboundDataSize;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len);
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); };
|
||||
|
||||
virtual uint64_t GetCommInactivityTimeout();
|
||||
virtual int SetCommInactivityTimeout (uint64_t value);
|
||||
|
||||
protected:
|
||||
struct OutboundPage {
|
||||
OutboundPage (const char *b, int l, struct sockaddr_in6 f, int o=0): Buffer(b), Length(l), Offset(o), From(f) {}
|
||||
void Free() {if (Buffer) free (const_cast<char*>(Buffer)); }
|
||||
const char *Buffer;
|
||||
int Length;
|
||||
int Offset;
|
||||
struct sockaddr_in6 From;
|
||||
};
|
||||
|
||||
std::deque<OutboundPage> OutboundPages;
|
||||
int OutboundDataSize;
|
||||
|
||||
struct sockaddr_in6 ReturnAddress;
|
||||
};
|
||||
|
||||
|
||||
/************************
|
||||
class AcceptorDescriptor
|
||||
************************/
|
||||
|
||||
class AcceptorDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
AcceptorDescriptor (SOCKET, EventMachine_t*);
|
||||
virtual ~AcceptorDescriptor();
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat();
|
||||
|
||||
virtual bool SelectForRead() {return true;}
|
||||
virtual bool SelectForWrite() {return false;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return _GenericGetPeername (s, len); }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); };
|
||||
|
||||
static void StopAcceptor (const uintptr_t binding);
|
||||
};
|
||||
|
||||
/********************
|
||||
class PipeDescriptor
|
||||
********************/
|
||||
|
||||
#ifdef OS_UNIX
|
||||
class PipeDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
PipeDescriptor (SOCKET, pid_t, EventMachine_t*);
|
||||
virtual ~PipeDescriptor() NO_EXCEPT_FALSE;
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat();
|
||||
|
||||
virtual bool SelectForRead();
|
||||
virtual bool SelectForWrite();
|
||||
|
||||
int SendOutboundData (const char*, unsigned long);
|
||||
virtual int GetOutboundDataSize() {return OutboundDataSize;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return _GenericGetPeername (s, len); }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); }
|
||||
|
||||
virtual bool GetSubprocessPid (pid_t*);
|
||||
|
||||
protected:
|
||||
struct OutboundPage {
|
||||
OutboundPage (const char *b, int l, int o=0): Buffer(b), Length(l), Offset(o) {}
|
||||
void Free() {if (Buffer) free (const_cast<char*>(Buffer)); }
|
||||
const char *Buffer;
|
||||
int Length;
|
||||
int Offset;
|
||||
};
|
||||
|
||||
protected:
|
||||
bool bReadAttemptedAfterClose;
|
||||
|
||||
std::deque<OutboundPage> OutboundPages;
|
||||
int OutboundDataSize;
|
||||
|
||||
pid_t SubprocessPid;
|
||||
|
||||
private:
|
||||
void _DispatchInboundData (const char *buffer, int size);
|
||||
};
|
||||
#endif // OS_UNIX
|
||||
|
||||
|
||||
/************************
|
||||
class KeyboardDescriptor
|
||||
************************/
|
||||
|
||||
class KeyboardDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
KeyboardDescriptor (EventMachine_t*);
|
||||
virtual ~KeyboardDescriptor();
|
||||
|
||||
virtual void Read();
|
||||
virtual void Write();
|
||||
virtual void Heartbeat();
|
||||
|
||||
virtual bool SelectForRead() {return true;}
|
||||
virtual bool SelectForWrite() {return false;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return _GenericGetPeername (s, len); }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return _GenericGetSockname (s, len); }
|
||||
|
||||
protected:
|
||||
bool bReadAttemptedAfterClose;
|
||||
|
||||
private:
|
||||
void _DispatchInboundData (const char *buffer, int size);
|
||||
};
|
||||
|
||||
|
||||
/***********************
|
||||
class InotifyDescriptor
|
||||
************************/
|
||||
|
||||
class InotifyDescriptor: public EventableDescriptor
|
||||
{
|
||||
public:
|
||||
InotifyDescriptor (EventMachine_t*);
|
||||
virtual ~InotifyDescriptor();
|
||||
|
||||
void Read();
|
||||
void Write();
|
||||
|
||||
virtual void Heartbeat() {}
|
||||
virtual bool SelectForRead() {return true;}
|
||||
virtual bool SelectForWrite() {return false;}
|
||||
|
||||
virtual bool GetPeername (struct sockaddr* s, socklen_t* len) { return false; }
|
||||
virtual bool GetSockname (struct sockaddr* s, socklen_t* len) { return false; }
|
||||
};
|
||||
|
||||
#endif // __EventableDescriptor__H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: em.h
|
||||
Date: 06Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef __EventMachine__H_
|
||||
#define __EventMachine__H_
|
||||
|
||||
#ifdef BUILD_FOR_RUBY
|
||||
#include <ruby.h>
|
||||
#ifdef HAVE_RB_THREAD_FD_SELECT
|
||||
#define EmSelect rb_thread_fd_select
|
||||
#else
|
||||
// ruby 1.9.1 and below
|
||||
#define EmSelect rb_thread_select
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
|
||||
#include <ruby/thread.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_RB_WAIT_FOR_SINGLE_FD
|
||||
#include <ruby/io.h>
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_RB_TRAP_IMMEDIATE)
|
||||
#include <rubysig.h>
|
||||
#elif defined(HAVE_RB_ENABLE_INTERRUPT)
|
||||
extern "C" {
|
||||
void rb_enable_interrupt(void);
|
||||
void rb_disable_interrupt(void);
|
||||
}
|
||||
|
||||
#define TRAP_BEG rb_enable_interrupt()
|
||||
#define TRAP_END do { rb_disable_interrupt(); rb_thread_check_ints(); } while(0)
|
||||
#else
|
||||
#define TRAP_BEG
|
||||
#define TRAP_END
|
||||
#endif
|
||||
|
||||
// 1.9.0 compat
|
||||
#ifndef RUBY_UBF_IO
|
||||
#define RUBY_UBF_IO RB_UBF_DFL
|
||||
#endif
|
||||
#ifndef RSTRING_PTR
|
||||
#define RSTRING_PTR(str) RSTRING(str)->ptr
|
||||
#endif
|
||||
#ifndef RSTRING_LEN
|
||||
#define RSTRING_LEN(str) RSTRING(str)->len
|
||||
#endif
|
||||
#ifndef RSTRING_LENINT
|
||||
#define RSTRING_LENINT(str) RSTRING_LEN(str)
|
||||
#endif
|
||||
#else
|
||||
#define EmSelect select
|
||||
#endif
|
||||
|
||||
#if !defined(HAVE_TYPE_RB_FDSET_T)
|
||||
#define fd_check(n) (((n) < FD_SETSIZE) ? 1 : 0*fprintf(stderr, "fd %d too large for select\n", (n)))
|
||||
// These definitions are cribbed from include/ruby/intern.h in Ruby 1.9.3,
|
||||
// with this change: any macros that read or write the nth element of an
|
||||
// fdset first call fd_check to make sure n is in bounds.
|
||||
typedef fd_set rb_fdset_t;
|
||||
#define rb_fd_zero(f) FD_ZERO(f)
|
||||
#define rb_fd_set(n, f) do { if (fd_check(n)) FD_SET((n), (f)); } while(0)
|
||||
#define rb_fd_clr(n, f) do { if (fd_check(n)) FD_CLR((n), (f)); } while(0)
|
||||
#define rb_fd_isset(n, f) (fd_check(n) ? FD_ISSET((n), (f)) : 0)
|
||||
#define rb_fd_copy(d, s, n) (*(d) = *(s))
|
||||
#define rb_fd_dup(d, s) (*(d) = *(s))
|
||||
#define rb_fd_resize(n, f) ((void)(f))
|
||||
#define rb_fd_ptr(f) (f)
|
||||
#define rb_fd_init(f) FD_ZERO(f)
|
||||
#define rb_fd_init_copy(d, s) (*(d) = *(s))
|
||||
#define rb_fd_term(f) ((void)(f))
|
||||
#define rb_fd_max(f) FD_SETSIZE
|
||||
#define rb_fd_select(n, rfds, wfds, efds, timeout) \
|
||||
select(fd_check((n)-1) ? (n) : FD_SETSIZE, (rfds), (wfds), (efds), (timeout))
|
||||
#define rb_thread_fd_select(n, rfds, wfds, efds, timeout) \
|
||||
rb_thread_select(fd_check((n)-1) ? (n) : FD_SETSIZE, (rfds), (wfds), (efds), (timeout))
|
||||
#endif
|
||||
|
||||
|
||||
// This Solaris fix is adapted from eval_intern.h in Ruby 1.9.3:
|
||||
// Solaris sys/select.h switches select to select_large_fdset to support larger
|
||||
// file descriptors if FD_SETSIZE is larger than 1024 on 32bit environment.
|
||||
// But Ruby doesn't change FD_SETSIZE because fd_set is allocated dynamically.
|
||||
// So following definition is required to use select_large_fdset.
|
||||
#ifdef HAVE_SELECT_LARGE_FDSET
|
||||
#define select(n, r, w, e, t) select_large_fdset((n), (r), (w), (e), (t))
|
||||
extern "C" {
|
||||
int select_large_fdset(int, fd_set *, fd_set *, fd_set *, struct timeval *);
|
||||
}
|
||||
#endif
|
||||
|
||||
class EventableDescriptor;
|
||||
class InotifyDescriptor;
|
||||
struct SelectData_t;
|
||||
|
||||
/*************
|
||||
enum Poller_t
|
||||
*************/
|
||||
enum Poller_t {
|
||||
Poller_Default, // typically Select
|
||||
Poller_Epoll,
|
||||
Poller_Kqueue
|
||||
};
|
||||
|
||||
|
||||
/********************
|
||||
class EventMachine_t
|
||||
********************/
|
||||
|
||||
class EventMachine_t
|
||||
{
|
||||
public:
|
||||
static int GetMaxTimerCount();
|
||||
static void SetMaxTimerCount (int);
|
||||
|
||||
static int GetSimultaneousAcceptCount();
|
||||
static void SetSimultaneousAcceptCount (int);
|
||||
|
||||
public:
|
||||
EventMachine_t (EMCallback, Poller_t);
|
||||
virtual ~EventMachine_t();
|
||||
|
||||
bool RunOnce();
|
||||
void Run();
|
||||
void ScheduleHalt();
|
||||
bool Stopping();
|
||||
void SignalLoopBreaker();
|
||||
const uintptr_t InstallOneshotTimer (uint64_t);
|
||||
const uintptr_t ConnectToServer (const char *, int, const char *, int);
|
||||
const uintptr_t ConnectToUnixServer (const char *);
|
||||
|
||||
const uintptr_t CreateTcpServer (const char *, int);
|
||||
const uintptr_t OpenDatagramSocket (const char *, int);
|
||||
const uintptr_t CreateUnixDomainServer (const char*);
|
||||
const uintptr_t AttachSD (SOCKET);
|
||||
const uintptr_t OpenKeyboard();
|
||||
//const char *Popen (const char*, const char*);
|
||||
const uintptr_t Socketpair (char* const*);
|
||||
|
||||
void Add (EventableDescriptor*);
|
||||
void Modify (EventableDescriptor*);
|
||||
void Deregister (EventableDescriptor*);
|
||||
|
||||
const uintptr_t AttachFD (SOCKET, bool);
|
||||
int DetachFD (EventableDescriptor*);
|
||||
|
||||
void ArmKqueueWriter (EventableDescriptor*);
|
||||
void ArmKqueueReader (EventableDescriptor*);
|
||||
|
||||
void SetTimerQuantum (int);
|
||||
static void SetuidString (const char*);
|
||||
static int SetRlimitNofile (int);
|
||||
|
||||
pid_t SubprocessPid;
|
||||
int SubprocessExitStatus;
|
||||
|
||||
int GetConnectionCount();
|
||||
float GetHeartbeatInterval();
|
||||
int SetHeartbeatInterval(float);
|
||||
|
||||
const uintptr_t WatchFile (const char*);
|
||||
void UnwatchFile (int);
|
||||
void UnwatchFile (const uintptr_t);
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
void _HandleKqueueFileEvent (struct kevent*);
|
||||
void _RegisterKqueueFileEvent(int);
|
||||
#endif
|
||||
|
||||
const uintptr_t WatchPid (int);
|
||||
void UnwatchPid (int);
|
||||
void UnwatchPid (const uintptr_t);
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
void _HandleKqueuePidEvent (struct kevent*);
|
||||
#endif
|
||||
|
||||
uint64_t GetCurrentLoopTime() { return MyCurrentLoopTime; }
|
||||
|
||||
void QueueHeartbeat(EventableDescriptor*);
|
||||
void ClearHeartbeat(uint64_t, EventableDescriptor*);
|
||||
|
||||
uint64_t GetRealTime();
|
||||
|
||||
Poller_t GetPoller() { return Poller; }
|
||||
|
||||
static int name2address (const char *server, int port, int socktype, struct sockaddr *addr, size_t *addr_len);
|
||||
|
||||
private:
|
||||
void _RunTimers();
|
||||
void _UpdateTime();
|
||||
void _AddNewDescriptors();
|
||||
void _ModifyDescriptors();
|
||||
void _InitializeLoopBreaker();
|
||||
void _CleanupSockets();
|
||||
|
||||
void _RunSelectOnce();
|
||||
void _RunEpollOnce();
|
||||
void _RunKqueueOnce();
|
||||
|
||||
void _ModifyEpollEvent (EventableDescriptor*);
|
||||
void _DispatchHeartbeats();
|
||||
timeval _TimeTilNextEvent();
|
||||
void _CleanBadDescriptors();
|
||||
|
||||
public:
|
||||
void _ReadLoopBreaker();
|
||||
void _ReadInotifyEvents();
|
||||
int NumCloseScheduled;
|
||||
|
||||
private:
|
||||
enum {
|
||||
MaxEpollDescriptors = 64*1024,
|
||||
MaxEvents = 4096
|
||||
};
|
||||
int HeartbeatInterval;
|
||||
EMCallback EventCallback;
|
||||
|
||||
class Timer_t: public Bindable_t {
|
||||
};
|
||||
|
||||
std::multimap<uint64_t, Timer_t> Timers;
|
||||
std::multimap<uint64_t, EventableDescriptor*> Heartbeats;
|
||||
std::map<int, Bindable_t*> Files;
|
||||
std::map<int, Bindable_t*> Pids;
|
||||
std::vector<EventableDescriptor*> Descriptors;
|
||||
std::vector<EventableDescriptor*> NewDescriptors;
|
||||
std::set<EventableDescriptor*> ModifiedDescriptors;
|
||||
|
||||
SOCKET LoopBreakerReader;
|
||||
SOCKET LoopBreakerWriter;
|
||||
#ifdef OS_WIN32
|
||||
struct sockaddr_in LoopBreakerTarget;
|
||||
#endif
|
||||
|
||||
timeval Quantum;
|
||||
|
||||
uint64_t MyCurrentLoopTime;
|
||||
|
||||
#ifdef OS_WIN32
|
||||
unsigned TickCountTickover;
|
||||
unsigned LastTickCount;
|
||||
#endif
|
||||
|
||||
#ifdef OS_DARWIN
|
||||
mach_timebase_info_data_t mach_timebase;
|
||||
#endif
|
||||
|
||||
private:
|
||||
bool bTerminateSignalReceived;
|
||||
SelectData_t *SelectData;
|
||||
|
||||
Poller_t Poller;
|
||||
|
||||
int epfd; // Epoll file-descriptor
|
||||
#ifdef HAVE_EPOLL
|
||||
struct epoll_event epoll_events [MaxEvents];
|
||||
#endif
|
||||
|
||||
int kqfd; // Kqueue file-descriptor
|
||||
#ifdef HAVE_KQUEUE
|
||||
struct kevent Karray [MaxEvents];
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_INOTIFY
|
||||
InotifyDescriptor *inotify; // pollable descriptor for our inotify instance
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
/*******************
|
||||
struct SelectData_t
|
||||
*******************/
|
||||
|
||||
struct SelectData_t
|
||||
{
|
||||
SelectData_t();
|
||||
~SelectData_t();
|
||||
|
||||
int _Select();
|
||||
void _Clear();
|
||||
|
||||
SOCKET maxsocket;
|
||||
rb_fdset_t fdreads;
|
||||
rb_fdset_t fdwrites;
|
||||
rb_fdset_t fderrors;
|
||||
timeval tv;
|
||||
int nSockets;
|
||||
};
|
||||
|
||||
#endif // __EventMachine__H_
|
||||
@@ -0,0 +1,143 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: eventmachine.h
|
||||
Date: 15Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef __EVMA_EventMachine__H_
|
||||
#define __EVMA_EventMachine__H_
|
||||
|
||||
#if __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum { // Event names
|
||||
EM_TIMER_FIRED = 100,
|
||||
EM_CONNECTION_READ = 101,
|
||||
EM_CONNECTION_UNBOUND = 102,
|
||||
EM_CONNECTION_ACCEPTED = 103,
|
||||
EM_CONNECTION_COMPLETED = 104,
|
||||
EM_LOOPBREAK_SIGNAL = 105,
|
||||
EM_CONNECTION_NOTIFY_READABLE = 106,
|
||||
EM_CONNECTION_NOTIFY_WRITABLE = 107,
|
||||
EM_SSL_HANDSHAKE_COMPLETED = 108,
|
||||
EM_SSL_VERIFY = 109,
|
||||
EM_PROXY_TARGET_UNBOUND = 110,
|
||||
EM_PROXY_COMPLETED = 111
|
||||
};
|
||||
|
||||
enum { // SSL/TLS Protocols
|
||||
EM_PROTO_SSLv2 = 2,
|
||||
EM_PROTO_SSLv3 = 4,
|
||||
EM_PROTO_TLSv1 = 8,
|
||||
EM_PROTO_TLSv1_1 = 16,
|
||||
EM_PROTO_TLSv1_2 = 32
|
||||
};
|
||||
|
||||
void evma_initialize_library (EMCallback);
|
||||
bool evma_run_machine_once();
|
||||
void evma_run_machine();
|
||||
void evma_release_library();
|
||||
const uintptr_t evma_install_oneshot_timer (uint64_t milliseconds);
|
||||
const uintptr_t evma_connect_to_server (const char *bind_addr, int bind_port, const char *server, int port);
|
||||
const uintptr_t evma_connect_to_unix_server (const char *server);
|
||||
|
||||
const uintptr_t evma_attach_fd (int file_descriptor, int watch_mode);
|
||||
int evma_detach_fd (const uintptr_t binding);
|
||||
int evma_get_file_descriptor (const uintptr_t binding);
|
||||
int evma_is_notify_readable (const uintptr_t binding);
|
||||
void evma_set_notify_readable (const uintptr_t binding, int mode);
|
||||
int evma_is_notify_writable (const uintptr_t binding);
|
||||
void evma_set_notify_writable (const uintptr_t binding, int mode);
|
||||
|
||||
int evma_pause(const uintptr_t binding);
|
||||
int evma_is_paused(const uintptr_t binding);
|
||||
int evma_resume(const uintptr_t binding);
|
||||
|
||||
int evma_num_close_scheduled();
|
||||
|
||||
void evma_stop_tcp_server (const uintptr_t binding);
|
||||
const uintptr_t evma_create_tcp_server (const char *address, int port);
|
||||
const uintptr_t evma_create_unix_domain_server (const char *filename);
|
||||
const uintptr_t evma_attach_sd (int sd);
|
||||
const uintptr_t evma_open_datagram_socket (const char *server, int port);
|
||||
const uintptr_t evma_open_keyboard();
|
||||
void evma_set_tls_parms (const uintptr_t binding, const char *privatekey_filename, const char *certchain_filenane, int verify_peer, int fail_if_no_peer_cert, const char *sni_hostname, const char *cipherlist, const char *ecdh_curve, const char *dhparam, int protocols);
|
||||
void evma_start_tls (const uintptr_t binding);
|
||||
|
||||
#ifdef WITH_SSL
|
||||
X509 *evma_get_peer_cert (const uintptr_t binding);
|
||||
int evma_get_cipher_bits (const uintptr_t binding);
|
||||
const char *evma_get_cipher_name (const uintptr_t binding);
|
||||
const char *evma_get_cipher_protocol (const uintptr_t binding);
|
||||
const char *evma_get_sni_hostname (const uintptr_t binding);
|
||||
void evma_accept_ssl_peer (const uintptr_t binding);
|
||||
#endif
|
||||
|
||||
int evma_get_peername (const uintptr_t binding, struct sockaddr*, socklen_t*);
|
||||
int evma_get_sockname (const uintptr_t binding, struct sockaddr*, socklen_t*);
|
||||
int evma_get_subprocess_pid (const uintptr_t binding, pid_t*);
|
||||
int evma_get_subprocess_status (const uintptr_t binding, int*);
|
||||
int evma_get_connection_count();
|
||||
int evma_send_data_to_connection (const uintptr_t binding, const char *data, int data_length);
|
||||
int evma_send_datagram (const uintptr_t binding, const char *data, int data_length, const char *address, int port);
|
||||
float evma_get_comm_inactivity_timeout (const uintptr_t binding);
|
||||
int evma_set_comm_inactivity_timeout (const uintptr_t binding, float value);
|
||||
float evma_get_pending_connect_timeout (const uintptr_t binding);
|
||||
int evma_set_pending_connect_timeout (const uintptr_t binding, float value);
|
||||
int evma_get_outbound_data_size (const uintptr_t binding);
|
||||
uint64_t evma_get_last_activity_time (const uintptr_t binding);
|
||||
int evma_send_file_data_to_connection (const uintptr_t binding, const char *filename);
|
||||
|
||||
void evma_close_connection (const uintptr_t binding, int after_writing);
|
||||
int evma_report_connection_error_status (const uintptr_t binding);
|
||||
void evma_signal_loopbreak();
|
||||
void evma_set_timer_quantum (int);
|
||||
int evma_get_max_timer_count();
|
||||
void evma_set_max_timer_count (int);
|
||||
int evma_get_simultaneous_accept_count();
|
||||
void evma_set_simultaneous_accept_count (int);
|
||||
void evma_setuid_string (const char *username);
|
||||
void evma_stop_machine();
|
||||
bool evma_stopping();
|
||||
float evma_get_heartbeat_interval();
|
||||
int evma_set_heartbeat_interval(float);
|
||||
|
||||
const uintptr_t evma_popen (char * const*cmd_strings);
|
||||
|
||||
const uintptr_t evma_watch_filename (const char *fname);
|
||||
void evma_unwatch_filename (const uintptr_t binding);
|
||||
|
||||
const uintptr_t evma_watch_pid (int);
|
||||
void evma_unwatch_pid (const uintptr_t binding);
|
||||
|
||||
void evma_start_proxy(const uintptr_t from, const uintptr_t to, const unsigned long bufsize, const unsigned long length);
|
||||
void evma_stop_proxy(const uintptr_t from);
|
||||
unsigned long evma_proxied_bytes(const uintptr_t from);
|
||||
|
||||
int evma_set_rlimit_nofile (int n_files);
|
||||
|
||||
void evma_set_epoll (int use);
|
||||
void evma_set_kqueue (int use);
|
||||
|
||||
uint64_t evma_get_current_loop_time();
|
||||
#if __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif // __EventMachine__H_
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
require 'fileutils'
|
||||
require 'mkmf'
|
||||
|
||||
# Eager check devs tools
|
||||
have_devel? if respond_to?(:have_devel?)
|
||||
|
||||
def check_libs libs = [], fatal = false
|
||||
libs.all? { |lib| have_library(lib) || (abort("could not find library: #{lib}") if fatal) }
|
||||
end
|
||||
|
||||
def check_heads heads = [], fatal = false
|
||||
heads.all? { |head| have_header(head) || (abort("could not find header: #{head}") if fatal)}
|
||||
end
|
||||
|
||||
def add_define(name)
|
||||
$defs.push("-D#{name}")
|
||||
end
|
||||
|
||||
##
|
||||
# OpenSSL:
|
||||
|
||||
# override append_library, so it actually appends (instead of prepending)
|
||||
# this fixes issues with linking ssl, since libcrypto depends on symbols in libssl
|
||||
def append_library(libs, lib)
|
||||
libs + " " + format(LIBARG, lib)
|
||||
end
|
||||
|
||||
SSL_HEADS = %w(openssl/ssl.h openssl/err.h)
|
||||
SSL_LIBS = %w(crypto ssl)
|
||||
# OpenSSL 1.1.0 and above for Windows use the Unix library names
|
||||
# OpenSSL 0.9.8 and 1.0.x for Windows use the *eay32 library names
|
||||
SSL_LIBS_WIN = RUBY_PLATFORM =~ /mswin|mingw|bccwin/ ? %w(ssleay32 libeay32) : []
|
||||
|
||||
def dir_config_wrapper(pretty_name, name, idefault=nil, ldefault=nil)
|
||||
inc, lib = dir_config(name, idefault, ldefault)
|
||||
if inc && lib
|
||||
# TODO: Remove when 2.0.0 is the minimum supported version
|
||||
# Ruby versions not incorporating the mkmf fix at
|
||||
# https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/39717
|
||||
# do not properly search for lib directories, and must be corrected
|
||||
unless lib && lib[-3, 3] == 'lib'
|
||||
@libdir_basename = 'lib'
|
||||
inc, lib = dir_config(name, idefault, ldefault)
|
||||
end
|
||||
unless idefault && ldefault
|
||||
abort "-----\nCannot find #{pretty_name} include path #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
|
||||
abort "-----\nCannot find #{pretty_name} library path #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
|
||||
warn "-----\nUsing #{pretty_name} in path #{File.dirname inc}\n-----"
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def dir_config_search(pretty_name, name, paths, &b)
|
||||
paths.each do |p|
|
||||
if dir_config_wrapper('OpenSSL', 'ssl', p + '/include', p + '/lib') && yield
|
||||
warn "-----\nFound #{pretty_name} in path #{p}\n-----"
|
||||
return true
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def pkg_config_wrapper(pretty_name, name)
|
||||
cflags, ldflags, libs = pkg_config(name)
|
||||
unless [cflags, ldflags, libs].any?(&:nil?) || [cflags, ldflags, libs].any?(&:empty?)
|
||||
warn "-----\nUsing #{pretty_name} from pkg-config #{cflags} && #{ldflags} && #{libs}\n-----"
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
if ENV['CROSS_COMPILING']
|
||||
openssl_version = ENV.fetch("OPENSSL_VERSION", "1.0.2e")
|
||||
openssl_dir = File.expand_path("~/.rake-compiler/builds/openssl-#{openssl_version}/")
|
||||
if File.exist?(openssl_dir)
|
||||
FileUtils.mkdir_p Dir.pwd+"/openssl/"
|
||||
FileUtils.cp Dir[openssl_dir+"/include/openssl/*.h"], Dir.pwd+"/openssl/", :verbose => true
|
||||
FileUtils.cp Dir[openssl_dir+"/lib*.a"], Dir.pwd, :verbose => true
|
||||
$INCFLAGS << " -I#{Dir.pwd}" # for the openssl headers
|
||||
add_define "WITH_SSL"
|
||||
else
|
||||
STDERR.puts
|
||||
STDERR.puts "**************************************************************************************"
|
||||
STDERR.puts "**** Cross-compiled OpenSSL not found"
|
||||
STDERR.puts "**** Run: hg clone http://bitbucket.org/ged/ruby-pg && cd ruby-pg && rake openssl_libs"
|
||||
STDERR.puts "**************************************************************************************"
|
||||
STDERR.puts
|
||||
end
|
||||
elsif dir_config_wrapper('OpenSSL', 'ssl')
|
||||
# If the user has provided a --with-ssl-dir argument, we must respect it or fail.
|
||||
add_define 'WITH_SSL' if (check_libs(SSL_LIBS) || check_libs(SSL_LIBS_WIN)) && check_heads(SSL_HEADS)
|
||||
elsif pkg_config_wrapper('OpenSSL', 'openssl')
|
||||
# If we can detect OpenSSL by pkg-config, use it as the next-best option
|
||||
add_define 'WITH_SSL' if (check_libs(SSL_LIBS) || check_libs(SSL_LIBS_WIN)) && check_heads(SSL_HEADS)
|
||||
elsif (check_libs(SSL_LIBS) || check_libs(SSL_LIBS_WIN)) && check_heads(SSL_HEADS)
|
||||
# If we don't even need any options to find a usable OpenSSL, go with it
|
||||
add_define 'WITH_SSL'
|
||||
elsif dir_config_search('OpenSSL', 'ssl', ['/usr/local', '/opt/local', '/usr/local/opt/openssl']) do
|
||||
(check_libs(SSL_LIBS) || check_libs(SSL_LIBS_WIN)) && check_heads(SSL_HEADS)
|
||||
end
|
||||
# Finally, look for OpenSSL in alternate locations including MacPorts and HomeBrew
|
||||
add_define 'WITH_SSL'
|
||||
end
|
||||
|
||||
add_define 'BUILD_FOR_RUBY'
|
||||
|
||||
# Ruby features:
|
||||
|
||||
have_var('rb_trap_immediate', ['ruby.h', 'rubysig.h'])
|
||||
have_func('rb_thread_blocking_region')
|
||||
have_func('rb_thread_call_without_gvl', 'ruby/thread.h')
|
||||
have_func('rb_thread_fd_select')
|
||||
have_type('rb_fdset_t', 'ruby/intern.h')
|
||||
have_func('rb_wait_for_single_fd')
|
||||
have_func('rb_enable_interrupt')
|
||||
have_func('rb_time_new')
|
||||
|
||||
# System features:
|
||||
|
||||
add_define('HAVE_INOTIFY') if inotify = have_func('inotify_init', 'sys/inotify.h')
|
||||
add_define('HAVE_OLD_INOTIFY') if !inotify && have_macro('__NR_inotify_init', 'sys/syscall.h')
|
||||
have_func('writev', 'sys/uio.h')
|
||||
have_func('pipe2', 'unistd.h')
|
||||
have_func('accept4', 'sys/socket.h')
|
||||
have_const('SOCK_CLOEXEC', 'sys/socket.h')
|
||||
|
||||
# Minor platform details between *nix and Windows:
|
||||
|
||||
if RUBY_PLATFORM =~ /(mswin|mingw|bccwin)/
|
||||
GNU_CHAIN = ENV['CROSS_COMPILING'] || $1 == 'mingw'
|
||||
OS_WIN32 = true
|
||||
add_define "OS_WIN32"
|
||||
else
|
||||
GNU_CHAIN = true
|
||||
OS_UNIX = true
|
||||
add_define 'OS_UNIX'
|
||||
|
||||
add_define "HAVE_KQUEUE" if have_header("sys/event.h") && have_header("sys/queue.h")
|
||||
end
|
||||
|
||||
# Adjust number of file descriptors (FD) on Windows
|
||||
|
||||
if RbConfig::CONFIG["host_os"] =~ /mingw/
|
||||
found = RbConfig::CONFIG.values_at("CFLAGS", "CPPFLAGS").
|
||||
any? { |v| v.include?("FD_SETSIZE") }
|
||||
|
||||
add_define "FD_SETSIZE=32767" unless found
|
||||
end
|
||||
|
||||
# Main platform invariances:
|
||||
|
||||
case RUBY_PLATFORM
|
||||
when /mswin32/, /mingw32/, /bccwin32/
|
||||
check_heads(%w[windows.h winsock.h], true)
|
||||
check_libs(%w[kernel32 rpcrt4 gdi32], true)
|
||||
|
||||
if GNU_CHAIN
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -shared -static-libgcc -static-libstdc++"
|
||||
else
|
||||
$defs.push "-EHs"
|
||||
$defs.push "-GR"
|
||||
end
|
||||
|
||||
# Newer versions of Ruby already define _WIN32_WINNT, which is needed
|
||||
# to get access to newer POSIX networking functions (e.g. getaddrinfo)
|
||||
add_define '_WIN32_WINNT=0x0501' unless have_func('getaddrinfo')
|
||||
|
||||
when /solaris/
|
||||
add_define 'OS_SOLARIS8'
|
||||
check_libs(%w[nsl socket], true)
|
||||
|
||||
# If Ruby was compiled for 32-bits, then select() can only handle 1024 fds
|
||||
# There is an alternate function, select_large_fdset, that supports more.
|
||||
have_func('select_large_fdset', 'sys/select.h')
|
||||
|
||||
if CONFIG['CC'] == 'cc' && (
|
||||
`cc -flags 2>&1` =~ /Sun/ || # detect SUNWspro compiler
|
||||
`cc -V 2>&1` =~ /Sun/ # detect Solaris Studio compiler
|
||||
)
|
||||
# SUN CHAIN
|
||||
add_define 'CC_SUNWspro'
|
||||
$preload = ["\nCXX = CC"] # hack a CXX= line into the makefile
|
||||
$CFLAGS = CONFIG['CFLAGS'] = "-KPIC"
|
||||
CONFIG['CCDLFLAGS'] = "-KPIC"
|
||||
CONFIG['LDSHARED'] = "$(CXX) -G -KPIC -lCstd"
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -G -KPIC -lCstd"
|
||||
else
|
||||
# GNU CHAIN
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
end
|
||||
|
||||
when /openbsd/
|
||||
# OpenBSD branch contributed by Guillaume Sellier.
|
||||
|
||||
# on Unix we need a g++ link, not gcc. On OpenBSD, linking against libstdc++ have to be explicitly done for shared libs
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared -lstdc++ -fPIC"
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -shared -lstdc++ -fPIC"
|
||||
|
||||
when /darwin/
|
||||
add_define 'OS_DARWIN'
|
||||
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
# Ff line contributed by Daniel Harple.
|
||||
CONFIG['LDSHARED'] = "$(CXX) " + CONFIG['LDSHARED'].split[1..-1].join(' ')
|
||||
|
||||
when /linux/
|
||||
add_define 'HAVE_EPOLL' if have_func('epoll_create', 'sys/epoll.h')
|
||||
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
|
||||
when /aix/
|
||||
CONFIG['LDSHARED'] = "$(CXX) -Wl,-bstatic -Wl,-bdynamic -Wl,-G -Wl,-brtl"
|
||||
|
||||
when /cygwin/
|
||||
# For rubies built with Cygwin, CXX may be set to CC, which is just
|
||||
# a wrapper for gcc.
|
||||
# This will compile, but it will not link to the C++ std library.
|
||||
# Explicitly set CXX to use g++.
|
||||
CONFIG['CXX'] = "g++"
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
|
||||
else
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
end
|
||||
|
||||
# Platform-specific time functions
|
||||
if have_func('clock_gettime')
|
||||
# clock_gettime is POSIX, but the monotonic clocks are not
|
||||
have_const('CLOCK_MONOTONIC_RAW', 'time.h') # Linux
|
||||
have_const('CLOCK_MONOTONIC', 'time.h') # Linux, Solaris, BSDs
|
||||
else
|
||||
have_func('gethrtime') # Older Solaris and HP-UX
|
||||
end
|
||||
|
||||
# Hack so that try_link will test with a C++ compiler instead of a C compiler
|
||||
TRY_LINK.sub!('$(CC)', '$(CXX)')
|
||||
|
||||
# This is our wishlist. We use whichever flags work on the host.
|
||||
# In the future, add -Werror to make sure all warnings are resolved.
|
||||
# deprecated-declarations are used in OS X OpenSSL
|
||||
# ignored-qualifiers are used by the Bindings (would-be void *)
|
||||
# unused-result because GCC 4.6 no longer silences (void) ignore_this(function)
|
||||
# address because on Windows, rb_fd_select checks if &fds is non-NULL, which it cannot be
|
||||
%w(
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wno-deprecated-declarations
|
||||
-Wno-ignored-qualifiers
|
||||
-Wno-unused-result
|
||||
-Wno-address
|
||||
).select do |flag|
|
||||
try_link('int main() {return 0;}', flag)
|
||||
end.each do |flag|
|
||||
CONFIG['CXXFLAGS'] << ' ' << flag
|
||||
end
|
||||
puts "CXXFLAGS=#{CONFIG['CXXFLAGS']}"
|
||||
|
||||
# Solaris C++ compiler doesn't have make_pair()
|
||||
add_define 'HAVE_MAKE_PAIR' if try_link(<<SRC, '-lstdc++')
|
||||
#include <utility>
|
||||
using namespace std;
|
||||
int main(){ pair<const int,int> tuple = make_pair(1,2); }
|
||||
SRC
|
||||
TRY_LINK.sub!('$(CXX)', '$(CC)')
|
||||
|
||||
create_makefile "rubyeventmachine"
|
||||
@@ -0,0 +1,269 @@
|
||||
|
||||
SHELL = /bin/sh
|
||||
|
||||
# V=0 quiet, V=1 verbose. other values don't work.
|
||||
V = 0
|
||||
V0 = $(V:0=)
|
||||
Q1 = $(V:1=)
|
||||
Q = $(Q1:0=@)
|
||||
ECHO1 = $(V:1=@ :)
|
||||
ECHO = $(ECHO1:0=@ echo)
|
||||
NULLCMD = :
|
||||
|
||||
#### Start of system configuration section. ####
|
||||
|
||||
srcdir = .
|
||||
topdir = /usr/include/ruby-3.2.0
|
||||
hdrdir = $(topdir)
|
||||
arch_hdrdir = /usr/include/x86_64-linux-gnu/ruby-3.2.0
|
||||
PATH_SEPARATOR = :
|
||||
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
|
||||
prefix = $(DESTDIR)/usr
|
||||
rubysitearchprefix = $(sitearchlibdir)/$(RUBY_BASE_NAME)
|
||||
rubyarchprefix = $(archlibdir)/$(RUBY_BASE_NAME)
|
||||
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
|
||||
exec_prefix = $(prefix)
|
||||
vendorarchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/vendor_ruby
|
||||
sitearchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/site_ruby
|
||||
rubyarchhdrdir = $(archincludedir)/$(RUBY_VERSION_NAME)
|
||||
vendorhdrdir = $(rubyhdrdir)/vendor_ruby
|
||||
sitehdrdir = $(rubyhdrdir)/site_ruby
|
||||
rubyhdrdir = $(includedir)/$(RUBY_VERSION_NAME)
|
||||
vendorarchdir = $(rubysitearchprefix)/vendor_ruby/$(ruby_version)
|
||||
vendorlibdir = $(vendordir)/$(ruby_version)
|
||||
vendordir = $(rubylibprefix)/vendor_ruby
|
||||
sitearchdir = $(DESTDIR)/usr/local/lib/x86_64-linux-gnu/site_ruby
|
||||
sitelibdir = $(sitedir)/$(ruby_version)
|
||||
sitedir = $(DESTDIR)/usr/local/lib/site_ruby
|
||||
rubyarchdir = $(rubyarchprefix)/$(ruby_version)
|
||||
rubylibdir = $(rubylibprefix)/$(ruby_version)
|
||||
sitearchincludedir = $(includedir)/$(sitearch)
|
||||
archincludedir = $(includedir)/$(arch)
|
||||
sitearchlibdir = $(libdir)/$(sitearch)
|
||||
archlibdir = $(libdir)/$(arch)
|
||||
ridir = $(datarootdir)/$(RI_BASE_NAME)
|
||||
mandir = $(datarootdir)/man
|
||||
localedir = $(datarootdir)/locale
|
||||
libdir = $(exec_prefix)/lib
|
||||
psdir = $(docdir)
|
||||
pdfdir = $(docdir)
|
||||
dvidir = $(docdir)
|
||||
htmldir = $(docdir)
|
||||
infodir = $(datarootdir)/info
|
||||
docdir = $(datarootdir)/doc/$(PACKAGE)
|
||||
oldincludedir = $(DESTDIR)/usr/include
|
||||
includedir = $(prefix)/include
|
||||
runstatedir = $(DESTDIR)/var/run
|
||||
localstatedir = $(DESTDIR)/var
|
||||
sharedstatedir = $(prefix)/com
|
||||
sysconfdir = $(DESTDIR)/etc
|
||||
datadir = $(datarootdir)
|
||||
datarootdir = $(prefix)/share
|
||||
libexecdir = $(exec_prefix)/libexec
|
||||
sbindir = $(exec_prefix)/sbin
|
||||
bindir = $(exec_prefix)/bin
|
||||
archdir = $(rubyarchdir)
|
||||
|
||||
|
||||
CC_WRAPPER =
|
||||
CC = x86_64-linux-gnu-gcc
|
||||
CXX = x86_64-linux-gnu-g++
|
||||
LIBRUBY = $(LIBRUBY_SO)
|
||||
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
|
||||
LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
|
||||
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static $(MAINLIBS)
|
||||
empty =
|
||||
OUTFLAG = -o $(empty)
|
||||
COUTFLAG = -o $(empty)
|
||||
CSRCFLAG = $(empty)
|
||||
|
||||
RUBY_EXTCONF_H =
|
||||
cflags = $(optflags) $(debugflags) $(warnflags)
|
||||
cxxflags =
|
||||
optflags = -O3 -fno-fast-math
|
||||
debugflags = -ggdb3
|
||||
warnflags = -Wall -Wextra -Wdeprecated-declarations -Wdiv-by-zero -Wduplicated-cond -Wimplicit-function-declaration -Wimplicit-int -Wmisleading-indentation -Wpointer-arith -Wwrite-strings -Wold-style-definition -Wimplicit-fallthrough=0 -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-packed-bitfield-compat -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wunused-variable -Wundef
|
||||
cppflags =
|
||||
CCDLFLAGS = -fPIC
|
||||
CFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 -fPIC $(ARCH_FLAG)
|
||||
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
|
||||
DEFS =
|
||||
CPPFLAGS = -DBUILD_FOR_RUBY -DOS_UNIX -Wdate-time -D_FORTIFY_SOURCE=3 $(DEFS) $(cppflags)
|
||||
CXXFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 $(ARCH_FLAG)
|
||||
ldflags = -L. -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed
|
||||
dldflags = -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now
|
||||
ARCH_FLAG =
|
||||
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
|
||||
LDSHARED = $(CXX) -shared
|
||||
LDSHAREDXX = $(CXX) -shared
|
||||
AR = x86_64-linux-gnu-gcc-ar
|
||||
EXEEXT =
|
||||
|
||||
RUBY_INSTALL_NAME = $(RUBY_BASE_NAME)3.2
|
||||
RUBY_SO_NAME = ruby-3.2
|
||||
RUBYW_INSTALL_NAME =
|
||||
RUBY_VERSION_NAME = $(RUBY_BASE_NAME)-$(ruby_version)
|
||||
RUBYW_BASE_NAME = rubyw
|
||||
RUBY_BASE_NAME = ruby
|
||||
|
||||
arch = x86_64-linux-gnu
|
||||
sitearch = $(arch)
|
||||
ruby_version = 3.2.0
|
||||
ruby = $(bindir)/$(RUBY_BASE_NAME)3.2
|
||||
RUBY = $(ruby)
|
||||
BUILTRUBY = $(bindir)/$(RUBY_BASE_NAME)3.2
|
||||
ruby_headers = $(hdrdir)/ruby.h $(hdrdir)/ruby/backward.h $(hdrdir)/ruby/ruby.h $(hdrdir)/ruby/defines.h $(hdrdir)/ruby/missing.h $(hdrdir)/ruby/intern.h $(hdrdir)/ruby/st.h $(hdrdir)/ruby/subst.h $(arch_hdrdir)/ruby/config.h
|
||||
|
||||
RM = rm -f
|
||||
RM_RF = rm -fr
|
||||
RMDIRS = rmdir --ignore-fail-on-non-empty -p
|
||||
MAKEDIRS = /bin/mkdir -p
|
||||
INSTALL = /usr/bin/install -c
|
||||
INSTALL_PROG = $(INSTALL) -m 0755
|
||||
INSTALL_DATA = $(INSTALL) -m 644
|
||||
COPY = cp
|
||||
TOUCH = exit >
|
||||
|
||||
#### End of system configuration section. ####
|
||||
|
||||
preload =
|
||||
libpath = . $(archlibdir)
|
||||
LIBPATH = -L. -L$(archlibdir)
|
||||
DEFFILE =
|
||||
|
||||
CLEANFILES = mkmf.log
|
||||
DISTCLEANFILES =
|
||||
DISTCLEANDIRS =
|
||||
|
||||
extout =
|
||||
extout_prefix =
|
||||
target_prefix =
|
||||
LOCAL_LIBS =
|
||||
LIBS = $(LIBRUBYARG_SHARED) -lm -lpthread -lc
|
||||
ORIG_SRCS = mapper.cpp rubymain.cpp
|
||||
SRCS = $(ORIG_SRCS)
|
||||
OBJS = mapper.o rubymain.o
|
||||
HDRS = $(srcdir)/mapper.h
|
||||
LOCAL_HDRS =
|
||||
TARGET = fastfilereaderext
|
||||
TARGET_NAME = fastfilereaderext
|
||||
TARGET_ENTRY = Init_$(TARGET_NAME)
|
||||
DLLIB = $(TARGET).so
|
||||
EXTSTATIC =
|
||||
STATIC_LIB =
|
||||
|
||||
TIMESTAMP_DIR = .
|
||||
BINDIR = $(bindir)
|
||||
RUBYCOMMONDIR = $(sitedir)$(target_prefix)
|
||||
RUBYLIBDIR = $(sitelibdir)$(target_prefix)
|
||||
RUBYARCHDIR = $(sitearchdir)$(target_prefix)
|
||||
HDRDIR = $(sitehdrdir)$(target_prefix)
|
||||
ARCHHDRDIR = $(sitearchhdrdir)$(target_prefix)
|
||||
TARGET_SO_DIR =
|
||||
TARGET_SO = $(TARGET_SO_DIR)$(DLLIB)
|
||||
CLEANLIBS = $(TARGET_SO) false
|
||||
CLEANOBJS = $(OBJS) *.bak
|
||||
TARGET_SO_DIR_TIMESTAMP = $(TIMESTAMP_DIR)/.sitearchdir.time
|
||||
|
||||
all: $(DLLIB)
|
||||
static: $(STATIC_LIB)
|
||||
.PHONY: all install static install-so install-rb
|
||||
.PHONY: clean clean-so clean-static clean-rb
|
||||
|
||||
clean-static::
|
||||
clean-rb-default::
|
||||
clean-rb::
|
||||
clean-so::
|
||||
clean: clean-so clean-static clean-rb-default clean-rb
|
||||
-$(Q)$(RM_RF) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES) .*.time
|
||||
|
||||
distclean-rb-default::
|
||||
distclean-rb::
|
||||
distclean-so::
|
||||
distclean-static::
|
||||
distclean: clean distclean-so distclean-static distclean-rb-default distclean-rb
|
||||
-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
|
||||
-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
|
||||
-$(Q)$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true
|
||||
|
||||
realclean: distclean
|
||||
install: install-so install-rb
|
||||
|
||||
install-so: $(DLLIB) $(TARGET_SO_DIR_TIMESTAMP)
|
||||
$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
|
||||
clean-static::
|
||||
-$(Q)$(RM) $(STATIC_LIB)
|
||||
install-rb: pre-install-rb do-install-rb install-rb-default
|
||||
install-rb-default: pre-install-rb-default do-install-rb-default
|
||||
pre-install-rb: Makefile
|
||||
pre-install-rb-default: Makefile
|
||||
do-install-rb:
|
||||
do-install-rb-default:
|
||||
pre-install-rb-default:
|
||||
@$(NULLCMD)
|
||||
$(TARGET_SO_DIR_TIMESTAMP):
|
||||
$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)
|
||||
$(Q) $(TOUCH) $@
|
||||
|
||||
site-install: site-install-so site-install-rb
|
||||
site-install-so: install-so
|
||||
site-install-rb: install-rb
|
||||
|
||||
.SUFFIXES: .c .m .cc .mm .cxx .cpp .o .S
|
||||
|
||||
.cc.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cc.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.mm.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.mm.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.cxx.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cxx.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.cpp.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.cpp.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.c.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.c.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
.m.o:
|
||||
$(ECHO) compiling $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
||||
|
||||
.m.S:
|
||||
$(ECHO) translating $(<)
|
||||
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
||||
|
||||
$(TARGET_SO): $(OBJS) Makefile
|
||||
$(ECHO) linking shared-object $(DLLIB)
|
||||
-$(Q)$(RM) $(@)
|
||||
$(Q) $(LDSHAREDXX) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
|
||||
|
||||
|
||||
|
||||
$(OBJS): $(HDRS) $(ruby_headers)
|
||||
@@ -0,0 +1,109 @@
|
||||
require 'mkmf'
|
||||
|
||||
def check_libs libs = [], fatal = false
|
||||
libs.all? { |lib| have_library(lib) || (abort("could not find library: #{lib}") if fatal) }
|
||||
end
|
||||
|
||||
def check_heads heads = [], fatal = false
|
||||
heads.all? { |head| have_header(head) || (abort("could not find header: #{head}") if fatal)}
|
||||
end
|
||||
|
||||
def add_define(name)
|
||||
$defs.push("-D#{name}")
|
||||
end
|
||||
|
||||
# Eager check devs tools
|
||||
have_devel? if respond_to?(:have_devel?)
|
||||
|
||||
add_define 'BUILD_FOR_RUBY'
|
||||
|
||||
# Minor platform details between *nix and Windows:
|
||||
|
||||
if RUBY_PLATFORM =~ /(mswin|mingw|bccwin)/
|
||||
GNU_CHAIN = ENV['CROSS_COMPILING'] || $1 == 'mingw'
|
||||
OS_WIN32 = true
|
||||
add_define "OS_WIN32"
|
||||
else
|
||||
GNU_CHAIN = true
|
||||
OS_UNIX = true
|
||||
add_define 'OS_UNIX'
|
||||
end
|
||||
|
||||
# Adjust number of file descriptors (FD) on Windows
|
||||
|
||||
if RbConfig::CONFIG["host_os"] =~ /mingw/
|
||||
found = RbConfig::CONFIG.values_at("CFLAGS", "CPPFLAGS").
|
||||
any? { |v| v.include?("FD_SETSIZE") }
|
||||
|
||||
add_define "FD_SETSIZE=32767" unless found
|
||||
end
|
||||
|
||||
# Main platform invariances:
|
||||
|
||||
case RUBY_PLATFORM
|
||||
when /mswin32/, /mingw32/, /bccwin32/
|
||||
check_heads(%w[windows.h winsock.h], true)
|
||||
check_libs(%w[kernel32 rpcrt4 gdi32], true)
|
||||
|
||||
if GNU_CHAIN
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -shared -static-libgcc -static-libstdc++"
|
||||
else
|
||||
$defs.push "-EHs"
|
||||
$defs.push "-GR"
|
||||
end
|
||||
|
||||
when /solaris/
|
||||
add_define 'OS_SOLARIS8'
|
||||
check_libs(%w[nsl socket], true)
|
||||
|
||||
if CONFIG['CC'] == 'cc' && (
|
||||
`cc -flags 2>&1` =~ /Sun/ || # detect SUNWspro compiler
|
||||
`cc -V 2>&1` =~ /Sun/ # detect Solaris Studio compiler
|
||||
)
|
||||
# SUN CHAIN
|
||||
add_define 'CC_SUNWspro'
|
||||
$preload = ["\nCXX = CC"] # hack a CXX= line into the makefile
|
||||
$CFLAGS = CONFIG['CFLAGS'] = "-KPIC"
|
||||
CONFIG['CCDLFLAGS'] = "-KPIC"
|
||||
CONFIG['LDSHARED'] = "$(CXX) -G -KPIC -lCstd"
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -G -KPIC -lCstd"
|
||||
else
|
||||
# GNU CHAIN
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
end
|
||||
|
||||
when /openbsd/
|
||||
# OpenBSD branch contributed by Guillaume Sellier.
|
||||
|
||||
# on Unix we need a g++ link, not gcc. On OpenBSD, linking against libstdc++ have to be explicitly done for shared libs
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared -lstdc++ -fPIC"
|
||||
CONFIG['LDSHAREDXX'] = "$(CXX) -shared -lstdc++ -fPIC"
|
||||
|
||||
when /darwin/
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
# Ff line contributed by Daniel Harple.
|
||||
CONFIG['LDSHARED'] = "$(CXX) " + CONFIG['LDSHARED'].split[1..-1].join(' ')
|
||||
|
||||
when /linux/
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
|
||||
when /aix/
|
||||
CONFIG['LDSHARED'] = "$(CXX) -Wl,-bstatic -Wl,-bdynamic -Wl,-G -Wl,-brtl"
|
||||
|
||||
when /cygwin/
|
||||
# For rubies built with Cygwin, CXX may be set to CC, which is just
|
||||
# a wrapper for gcc.
|
||||
# This will compile, but it will not link to the C++ std library.
|
||||
# Explicitly set CXX to use g++.
|
||||
CONFIG['CXX'] = "g++"
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
|
||||
else
|
||||
# on Unix we need a g++ link, not gcc.
|
||||
CONFIG['LDSHARED'] = "$(CXX) -shared"
|
||||
end
|
||||
|
||||
create_makefile "fastfilereaderext"
|
||||
@@ -0,0 +1,214 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id: mapper.cpp 4527 2007-07-04 10:21:34Z francis $
|
||||
|
||||
File: mapper.cpp
|
||||
Date: 02Jul07
|
||||
|
||||
Copyright (C) 2007 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: garbagecat10
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// UNIX implementation
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#ifdef OS_UNIX
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "mapper.h"
|
||||
|
||||
/******************
|
||||
Mapper_t::Mapper_t
|
||||
******************/
|
||||
|
||||
Mapper_t::Mapper_t (const std::string &filename)
|
||||
{
|
||||
/* We ASSUME we can open the file.
|
||||
* (More precisely, we assume someone else checked before we got here.)
|
||||
*/
|
||||
|
||||
Fd = open (filename.c_str(), O_RDONLY);
|
||||
if (Fd < 0)
|
||||
throw std::runtime_error (strerror (errno));
|
||||
|
||||
struct stat st;
|
||||
if (fstat (Fd, &st))
|
||||
throw std::runtime_error (strerror (errno));
|
||||
FileSize = st.st_size;
|
||||
|
||||
#ifdef OS_WIN32
|
||||
MapPoint = (char*) mmap (0, FileSize, PROT_READ, MAP_SHARED, Fd, 0);
|
||||
#else
|
||||
MapPoint = (const char*) mmap (0, FileSize, PROT_READ, MAP_SHARED, Fd, 0);
|
||||
#endif
|
||||
if (MapPoint == MAP_FAILED)
|
||||
throw std::runtime_error (strerror (errno));
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
Mapper_t::~Mapper_t
|
||||
*******************/
|
||||
|
||||
Mapper_t::~Mapper_t()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
|
||||
/***************
|
||||
Mapper_t::Close
|
||||
***************/
|
||||
|
||||
void Mapper_t::Close()
|
||||
{
|
||||
// Can be called multiple times.
|
||||
// Calls to GetChunk are invalid after a call to Close.
|
||||
if (MapPoint) {
|
||||
#ifdef CC_SUNWspro
|
||||
// TODO: The void * cast works fine on Solaris 11, but
|
||||
// I don't know at what point that changed from older Solaris.
|
||||
munmap ((char*)MapPoint, FileSize);
|
||||
#else
|
||||
munmap ((void*)MapPoint, FileSize);
|
||||
#endif
|
||||
MapPoint = NULL;
|
||||
}
|
||||
if (Fd >= 0) {
|
||||
close (Fd);
|
||||
Fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/******************
|
||||
Mapper_t::GetChunk
|
||||
******************/
|
||||
|
||||
const char *Mapper_t::GetChunk (unsigned start)
|
||||
{
|
||||
return MapPoint + start;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif // OS_UNIX
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// WINDOWS implementation
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifdef OS_WIN32
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "mapper.h"
|
||||
|
||||
/******************
|
||||
Mapper_t::Mapper_t
|
||||
******************/
|
||||
|
||||
Mapper_t::Mapper_t (const std::string &filename)
|
||||
{
|
||||
/* We ASSUME we can open the file.
|
||||
* (More precisely, we assume someone else checked before we got here.)
|
||||
*/
|
||||
|
||||
hFile = INVALID_HANDLE_VALUE;
|
||||
hMapping = NULL;
|
||||
MapPoint = NULL;
|
||||
FileSize = 0;
|
||||
|
||||
hFile = CreateFile (filename.c_str(), GENERIC_READ|GENERIC_WRITE, FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
throw std::runtime_error ("File not found");
|
||||
|
||||
BY_HANDLE_FILE_INFORMATION i;
|
||||
if (GetFileInformationByHandle (hFile, &i))
|
||||
FileSize = i.nFileSizeLow;
|
||||
|
||||
hMapping = CreateFileMapping (hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
|
||||
if (!hMapping)
|
||||
throw std::runtime_error ("File not mapped");
|
||||
|
||||
#ifdef OS_WIN32
|
||||
MapPoint = (char*) MapViewOfFile (hMapping, FILE_MAP_WRITE, 0, 0, 0);
|
||||
#else
|
||||
MapPoint = (const char*) MapViewOfFile (hMapping, FILE_MAP_WRITE, 0, 0, 0);
|
||||
#endif
|
||||
if (!MapPoint)
|
||||
throw std::runtime_error ("Mappoint not read");
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
Mapper_t::~Mapper_t
|
||||
*******************/
|
||||
|
||||
Mapper_t::~Mapper_t()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/***************
|
||||
Mapper_t::Close
|
||||
***************/
|
||||
|
||||
void Mapper_t::Close()
|
||||
{
|
||||
// Can be called multiple times.
|
||||
// Calls to GetChunk are invalid after a call to Close.
|
||||
if (MapPoint) {
|
||||
UnmapViewOfFile (MapPoint);
|
||||
MapPoint = NULL;
|
||||
}
|
||||
if (hMapping != NULL) {
|
||||
CloseHandle (hMapping);
|
||||
hMapping = NULL;
|
||||
}
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle (hFile);
|
||||
hFile = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
Mapper_t::GetChunk
|
||||
******************/
|
||||
|
||||
const char *Mapper_t::GetChunk (unsigned start)
|
||||
{
|
||||
return MapPoint + start;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif // OS_WINDOWS
|
||||
@@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id: mapper.h 4529 2007-07-04 11:32:22Z francis $
|
||||
|
||||
File: mapper.h
|
||||
Date: 02Jul07
|
||||
|
||||
Copyright (C) 2007 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: garbagecat10
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#ifndef __Mapper__H_
|
||||
#define __Mapper__H_
|
||||
|
||||
|
||||
/**************
|
||||
class Mapper_t
|
||||
**************/
|
||||
|
||||
class Mapper_t
|
||||
{
|
||||
public:
|
||||
Mapper_t (const std::string&);
|
||||
virtual ~Mapper_t();
|
||||
|
||||
const char *GetChunk (unsigned);
|
||||
void Close();
|
||||
size_t GetFileSize() {return FileSize;}
|
||||
|
||||
private:
|
||||
size_t FileSize;
|
||||
|
||||
#ifdef OS_UNIX
|
||||
private:
|
||||
int Fd;
|
||||
const char *MapPoint;
|
||||
#endif // OS_UNIX
|
||||
|
||||
#ifdef OS_WIN32
|
||||
private:
|
||||
HANDLE hFile;
|
||||
HANDLE hMapping;
|
||||
char *MapPoint;
|
||||
#endif // OS_WIN32
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif // __Mapper__H_
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id: rubymain.cpp 4529 2007-07-04 11:32:22Z francis $
|
||||
|
||||
File: rubymain.cpp
|
||||
Date: 02Jul07
|
||||
|
||||
Copyright (C) 2007 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: garbagecat10
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <ruby.h>
|
||||
#include "mapper.h"
|
||||
|
||||
static VALUE EmModule;
|
||||
static VALUE FastFileReader;
|
||||
static VALUE Mapper;
|
||||
|
||||
|
||||
|
||||
/*********
|
||||
mapper_dt
|
||||
*********/
|
||||
|
||||
static void mapper_dt (void *ptr)
|
||||
{
|
||||
if (ptr)
|
||||
delete (Mapper_t*) ptr;
|
||||
}
|
||||
|
||||
/**********
|
||||
mapper_new
|
||||
**********/
|
||||
|
||||
static VALUE mapper_new (VALUE self, VALUE filename)
|
||||
{
|
||||
Mapper_t *m = new Mapper_t (StringValueCStr (filename));
|
||||
if (!m)
|
||||
rb_raise (rb_eStandardError, "No Mapper Object");
|
||||
VALUE v = Data_Wrap_Struct (Mapper, 0, mapper_dt, (void*)m);
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
/****************
|
||||
mapper_get_chunk
|
||||
****************/
|
||||
|
||||
static VALUE mapper_get_chunk (VALUE self, VALUE start, VALUE length)
|
||||
{
|
||||
Mapper_t *m = NULL;
|
||||
Data_Get_Struct (self, Mapper_t, m);
|
||||
if (!m)
|
||||
rb_raise (rb_eStandardError, "No Mapper Object");
|
||||
|
||||
// TODO, what if some moron sends us a negative start value?
|
||||
unsigned _start = NUM2INT (start);
|
||||
unsigned _length = NUM2INT (length);
|
||||
if ((_start + _length) > m->GetFileSize())
|
||||
rb_raise (rb_eStandardError, "Mapper Range Error");
|
||||
|
||||
const char *chunk = m->GetChunk (_start);
|
||||
if (!chunk)
|
||||
rb_raise (rb_eStandardError, "No Mapper Chunk");
|
||||
return rb_str_new (chunk, _length);
|
||||
}
|
||||
|
||||
/************
|
||||
mapper_close
|
||||
************/
|
||||
|
||||
static VALUE mapper_close (VALUE self)
|
||||
{
|
||||
Mapper_t *m = NULL;
|
||||
Data_Get_Struct (self, Mapper_t, m);
|
||||
if (!m)
|
||||
rb_raise (rb_eStandardError, "No Mapper Object");
|
||||
m->Close();
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
/***********
|
||||
mapper_size
|
||||
***********/
|
||||
|
||||
static VALUE mapper_size (VALUE self)
|
||||
{
|
||||
Mapper_t *m = NULL;
|
||||
Data_Get_Struct (self, Mapper_t, m);
|
||||
if (!m)
|
||||
rb_raise (rb_eStandardError, "No Mapper Object");
|
||||
return INT2NUM (m->GetFileSize());
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
Init_fastfilereaderext
|
||||
**********************/
|
||||
|
||||
extern "C" void Init_fastfilereaderext()
|
||||
{
|
||||
EmModule = rb_define_module ("EventMachine");
|
||||
FastFileReader = rb_define_class_under (EmModule, "FastFileReader", rb_cObject);
|
||||
Mapper = rb_define_class_under (FastFileReader, "Mapper", rb_cObject);
|
||||
|
||||
rb_define_module_function (Mapper, "new", (VALUE(*)(...))mapper_new, 1);
|
||||
rb_define_method (Mapper, "size", (VALUE(*)(...))mapper_size, 0);
|
||||
rb_define_method (Mapper, "close", (VALUE(*)(...))mapper_close, 0);
|
||||
rb_define_method (Mapper, "get_chunk", (VALUE(*)(...))mapper_get_chunk, 2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: kb.cpp
|
||||
Date: 24Aug07
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#include "project.h"
|
||||
|
||||
|
||||
/**************************************
|
||||
KeyboardDescriptor::KeyboardDescriptor
|
||||
**************************************/
|
||||
|
||||
KeyboardDescriptor::KeyboardDescriptor (EventMachine_t *parent_em):
|
||||
EventableDescriptor (0, parent_em),
|
||||
bReadAttemptedAfterClose (false)
|
||||
{
|
||||
#ifdef HAVE_EPOLL
|
||||
EpollEvent.events = EPOLLIN;
|
||||
#endif
|
||||
#ifdef HAVE_KQUEUE
|
||||
MyEventMachine->ArmKqueueReader (this);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/***************************************
|
||||
KeyboardDescriptor::~KeyboardDescriptor
|
||||
***************************************/
|
||||
|
||||
KeyboardDescriptor::~KeyboardDescriptor()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*************************
|
||||
KeyboardDescriptor::Write
|
||||
*************************/
|
||||
|
||||
void KeyboardDescriptor::Write()
|
||||
{
|
||||
// Why are we here?
|
||||
throw std::runtime_error ("bad code path in keyboard handler");
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
KeyboardDescriptor::Heartbeat
|
||||
*****************************/
|
||||
|
||||
void KeyboardDescriptor::Heartbeat()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
/************************
|
||||
KeyboardDescriptor::Read
|
||||
************************/
|
||||
|
||||
void KeyboardDescriptor::Read()
|
||||
{
|
||||
char c;
|
||||
(void)read (GetSocket(), &c, 1);
|
||||
_GenericInboundDispatch(&c, 1);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: page.cpp
|
||||
Date: 30Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "project.h"
|
||||
|
||||
|
||||
/******************
|
||||
PageList::PageList
|
||||
******************/
|
||||
|
||||
PageList::PageList()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
PageList::~PageList
|
||||
*******************/
|
||||
|
||||
PageList::~PageList()
|
||||
{
|
||||
while (HasPages())
|
||||
PopFront();
|
||||
}
|
||||
|
||||
|
||||
/***************
|
||||
PageList::Front
|
||||
***************/
|
||||
|
||||
void PageList::Front (const char **page, int *length)
|
||||
{
|
||||
assert (page && length);
|
||||
|
||||
if (HasPages()) {
|
||||
Page p = Pages.front();
|
||||
*page = p.Buffer;
|
||||
*length = p.Size;
|
||||
}
|
||||
else {
|
||||
*page = NULL;
|
||||
*length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
PageList::PopFront
|
||||
******************/
|
||||
|
||||
void PageList::PopFront()
|
||||
{
|
||||
if (HasPages()) {
|
||||
Page p = Pages.front();
|
||||
Pages.pop_front();
|
||||
if (p.Buffer)
|
||||
free ((void*)p.Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
PageList::HasPages
|
||||
******************/
|
||||
|
||||
bool PageList::HasPages()
|
||||
{
|
||||
return (Pages.size() > 0) ? true : false;
|
||||
}
|
||||
|
||||
|
||||
/**************
|
||||
PageList::Push
|
||||
**************/
|
||||
|
||||
void PageList::Push (const char *buf, int size)
|
||||
{
|
||||
if (buf && (size > 0)) {
|
||||
char *copy = (char*) malloc (size);
|
||||
if (!copy)
|
||||
throw std::runtime_error ("no memory in pagelist");
|
||||
memcpy (copy, buf, size);
|
||||
Pages.push_back (Page (copy, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: page.h
|
||||
Date: 30Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#ifndef __PageManager__H_
|
||||
#define __PageManager__H_
|
||||
|
||||
|
||||
/**************
|
||||
class PageList
|
||||
**************/
|
||||
|
||||
class PageList
|
||||
{
|
||||
struct Page {
|
||||
Page (const char *b, size_t s): Buffer(b), Size(s) {}
|
||||
const char *Buffer;
|
||||
size_t Size;
|
||||
};
|
||||
|
||||
public:
|
||||
PageList();
|
||||
virtual ~PageList();
|
||||
|
||||
void Push (const char*, int);
|
||||
bool HasPages();
|
||||
void Front (const char**, int*);
|
||||
void PopFront();
|
||||
|
||||
private:
|
||||
std::deque<Page> Pages;
|
||||
};
|
||||
|
||||
|
||||
#endif // __PageManager__H_
|
||||
@@ -0,0 +1,354 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: pipe.cpp
|
||||
Date: 30May07
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
#include "project.h"
|
||||
|
||||
|
||||
#ifdef OS_UNIX
|
||||
// THIS ENTIRE FILE IS ONLY COMPILED ON UNIX-LIKE SYSTEMS.
|
||||
|
||||
/******************************
|
||||
PipeDescriptor::PipeDescriptor
|
||||
******************************/
|
||||
|
||||
PipeDescriptor::PipeDescriptor (int fd, pid_t subpid, EventMachine_t *parent_em):
|
||||
EventableDescriptor (fd, parent_em),
|
||||
bReadAttemptedAfterClose (false),
|
||||
OutboundDataSize (0),
|
||||
SubprocessPid (subpid)
|
||||
{
|
||||
#ifdef HAVE_EPOLL
|
||||
EpollEvent.events = EPOLLIN;
|
||||
#endif
|
||||
#ifdef HAVE_KQUEUE
|
||||
MyEventMachine->ArmKqueueReader (this);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/*******************************
|
||||
PipeDescriptor::~PipeDescriptor
|
||||
*******************************/
|
||||
|
||||
PipeDescriptor::~PipeDescriptor() NO_EXCEPT_FALSE
|
||||
{
|
||||
// Run down any stranded outbound data.
|
||||
for (size_t i=0; i < OutboundPages.size(); i++)
|
||||
OutboundPages[i].Free();
|
||||
|
||||
/* As a virtual destructor, we come here before the base-class
|
||||
* destructor that closes our file-descriptor.
|
||||
* We have to make sure the subprocess goes down (if it's not
|
||||
* already down) and we have to reap the zombie.
|
||||
*
|
||||
* This implementation is PROVISIONAL and will surely be improved.
|
||||
* The intention here is that we never block, hence the highly
|
||||
* undesirable sleeps. But if we can't reap the subprocess even
|
||||
* after sending it SIGKILL, then something is wrong and we
|
||||
* throw a fatal exception, which is also not something we should
|
||||
* be doing.
|
||||
*
|
||||
* Eventually the right thing to do will be to have the reactor
|
||||
* core respond to SIGCHLD by chaining a handler on top of the
|
||||
* one Ruby may have installed, and dealing with a list of dead
|
||||
* children that are pending cleanup.
|
||||
*
|
||||
* Since we want to have a signal processor integrated into the
|
||||
* client-visible API, let's wait until that is done before cleaning
|
||||
* this up.
|
||||
*
|
||||
* Added a very ugly hack to support passing the subprocess's exit
|
||||
* status to the user. It only makes logical sense for user code to access
|
||||
* the subprocess exit status in the unbind callback. But unbind is called
|
||||
* back during the EventableDescriptor destructor. So by that time there's
|
||||
* no way to call back this object through an object binding, because it's
|
||||
* already been cleaned up. We might have added a parameter to the unbind
|
||||
* callback, but that would probably break a huge amount of existing code.
|
||||
* So the hack-solution is to define an instance variable in the EventMachine
|
||||
* object and stick the exit status in there, where it can easily be accessed
|
||||
* with an accessor visible to user code.
|
||||
* User code should ONLY access the exit status from within the unbind callback.
|
||||
* Otherwise there's no guarantee it'll be valid.
|
||||
* This hack won't make it impossible to run multiple EventMachines in a single
|
||||
* process, but it will make it impossible to reliably nest unbind calls
|
||||
* within other unbind calls. (Not sure if that's even possible.)
|
||||
*/
|
||||
|
||||
assert (MyEventMachine);
|
||||
|
||||
/* Another hack to make the SubprocessPid available to get_subprocess_status */
|
||||
MyEventMachine->SubprocessPid = SubprocessPid;
|
||||
|
||||
/* 01Mar09: Updated to use a small nanosleep in a loop. When nanosleep is interrupted by SIGCHLD,
|
||||
* it resumes the system call after processing the signal (resulting in unnecessary latency).
|
||||
* Calling nanosleep in a loop avoids this problem.
|
||||
*/
|
||||
struct timespec req = {0, 50000000}; // 0.05s
|
||||
int n;
|
||||
|
||||
// wait 0.5s for the process to die
|
||||
for (n=0; n<10; n++) {
|
||||
if (waitpid (SubprocessPid, &(MyEventMachine->SubprocessExitStatus), WNOHANG) != 0) return;
|
||||
nanosleep (&req, NULL);
|
||||
}
|
||||
|
||||
// send SIGTERM and wait another 1s
|
||||
kill (SubprocessPid, SIGTERM);
|
||||
for (n=0; n<20; n++) {
|
||||
nanosleep (&req, NULL);
|
||||
if (waitpid (SubprocessPid, &(MyEventMachine->SubprocessExitStatus), WNOHANG) != 0) return;
|
||||
}
|
||||
|
||||
// send SIGKILL and wait another 5s
|
||||
kill (SubprocessPid, SIGKILL);
|
||||
for (n=0; n<100; n++) {
|
||||
nanosleep (&req, NULL);
|
||||
if (waitpid (SubprocessPid, &(MyEventMachine->SubprocessExitStatus), WNOHANG) != 0) return;
|
||||
}
|
||||
|
||||
// still not dead, give up!
|
||||
throw std::runtime_error ("unable to reap subprocess");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************
|
||||
PipeDescriptor::Read
|
||||
********************/
|
||||
|
||||
void PipeDescriptor::Read()
|
||||
{
|
||||
int sd = GetSocket();
|
||||
if (sd == INVALID_SOCKET) {
|
||||
assert (!bReadAttemptedAfterClose);
|
||||
bReadAttemptedAfterClose = true;
|
||||
return;
|
||||
}
|
||||
|
||||
LastActivity = MyEventMachine->GetCurrentLoopTime();
|
||||
|
||||
int total_bytes_read = 0;
|
||||
char readbuffer [16 * 1024];
|
||||
|
||||
for (int i=0; i < 10; i++) {
|
||||
// Don't read just one buffer and then move on. This is faster
|
||||
// if there is a lot of incoming.
|
||||
// But don't read indefinitely. Give other sockets a chance to run.
|
||||
// NOTICE, we're reading one less than the buffer size.
|
||||
// That's so we can put a guard byte at the end of what we send
|
||||
// to user code.
|
||||
// Use read instead of recv, which on Linux gives a "socket operation
|
||||
// on nonsocket" error.
|
||||
|
||||
|
||||
int r = read (sd, readbuffer, sizeof(readbuffer) - 1);
|
||||
//cerr << "<R:" << r << ">";
|
||||
|
||||
if (r > 0) {
|
||||
total_bytes_read += r;
|
||||
|
||||
// Add a null-terminator at the the end of the buffer
|
||||
// that we will send to the callback.
|
||||
// DO NOT EVER CHANGE THIS. We want to explicitly allow users
|
||||
// to be able to depend on this behavior, so they will have
|
||||
// the option to do some things faster. Additionally it's
|
||||
// a security guard against buffer overflows.
|
||||
readbuffer [r] = 0;
|
||||
_GenericInboundDispatch(readbuffer, r);
|
||||
}
|
||||
else if (r == 0) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// Basically a would-block, meaning we've read everything there is to read.
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (total_bytes_read == 0) {
|
||||
// If we read no data on a socket that selected readable,
|
||||
// it generally means the other end closed the connection gracefully.
|
||||
ScheduleClose (false);
|
||||
//bCloseNow = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*********************
|
||||
PipeDescriptor::Write
|
||||
*********************/
|
||||
|
||||
void PipeDescriptor::Write()
|
||||
{
|
||||
int sd = GetSocket();
|
||||
assert (sd != INVALID_SOCKET);
|
||||
|
||||
LastActivity = MyEventMachine->GetCurrentLoopTime();
|
||||
char output_buffer [16 * 1024];
|
||||
size_t nbytes = 0;
|
||||
|
||||
while ((OutboundPages.size() > 0) && (nbytes < sizeof(output_buffer))) {
|
||||
OutboundPage *op = &(OutboundPages[0]);
|
||||
if ((nbytes + op->Length - op->Offset) < sizeof (output_buffer)) {
|
||||
memcpy (output_buffer + nbytes, op->Buffer + op->Offset, op->Length - op->Offset);
|
||||
nbytes += (op->Length - op->Offset);
|
||||
op->Free();
|
||||
OutboundPages.pop_front();
|
||||
}
|
||||
else {
|
||||
int len = sizeof(output_buffer) - nbytes;
|
||||
memcpy (output_buffer + nbytes, op->Buffer + op->Offset, len);
|
||||
op->Offset += len;
|
||||
nbytes += len;
|
||||
}
|
||||
}
|
||||
|
||||
// We should never have gotten here if there were no data to write,
|
||||
// so assert that as a sanity check.
|
||||
// Don't bother to make sure nbytes is less than output_buffer because
|
||||
// if it were we probably would have crashed already.
|
||||
assert (nbytes > 0);
|
||||
|
||||
assert (GetSocket() != INVALID_SOCKET);
|
||||
int bytes_written = write (GetSocket(), output_buffer, nbytes);
|
||||
#ifdef OS_WIN32
|
||||
int e = WSAGetLastError();
|
||||
#else
|
||||
int e = errno;
|
||||
#endif
|
||||
|
||||
if (bytes_written > 0) {
|
||||
OutboundDataSize -= bytes_written;
|
||||
if ((size_t)bytes_written < nbytes) {
|
||||
int len = nbytes - bytes_written;
|
||||
char *buffer = (char*) malloc (len + 1);
|
||||
if (!buffer)
|
||||
throw std::runtime_error ("bad alloc throwing back data");
|
||||
memcpy (buffer, output_buffer + bytes_written, len);
|
||||
buffer [len] = 0;
|
||||
OutboundPages.push_front (OutboundPage (buffer, len));
|
||||
}
|
||||
#ifdef HAVE_EPOLL
|
||||
EpollEvent.events = EPOLLIN;
|
||||
if (SelectForWrite())
|
||||
EpollEvent.events |= EPOLLOUT;
|
||||
assert (MyEventMachine);
|
||||
MyEventMachine->Modify (this);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#ifdef OS_UNIX
|
||||
if ((e != EINPROGRESS) && (e != EWOULDBLOCK) && (e != EINTR))
|
||||
#endif
|
||||
#ifdef OS_WIN32
|
||||
if ((e != WSAEINPROGRESS) && (e != WSAEWOULDBLOCK))
|
||||
#endif
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************
|
||||
PipeDescriptor::Heartbeat
|
||||
*************************/
|
||||
|
||||
void PipeDescriptor::Heartbeat()
|
||||
{
|
||||
// If an inactivity timeout is defined, then check for it.
|
||||
if (InactivityTimeout && ((MyEventMachine->GetCurrentLoopTime() - LastActivity) >= InactivityTimeout))
|
||||
ScheduleClose (false);
|
||||
//bCloseNow = true;
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
PipeDescriptor::SelectForRead
|
||||
*****************************/
|
||||
|
||||
bool PipeDescriptor::SelectForRead()
|
||||
{
|
||||
/* Pipe descriptors, being local by definition, don't have
|
||||
* a pending state, so this is simpler than for the
|
||||
* ConnectionDescriptor object.
|
||||
*/
|
||||
return bPaused ? false : true;
|
||||
}
|
||||
|
||||
/******************************
|
||||
PipeDescriptor::SelectForWrite
|
||||
******************************/
|
||||
|
||||
bool PipeDescriptor::SelectForWrite()
|
||||
{
|
||||
/* Pipe descriptors, being local by definition, don't have
|
||||
* a pending state, so this is simpler than for the
|
||||
* ConnectionDescriptor object.
|
||||
*/
|
||||
return (GetOutboundDataSize() > 0) && !bPaused ? true : false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/********************************
|
||||
PipeDescriptor::SendOutboundData
|
||||
********************************/
|
||||
|
||||
int PipeDescriptor::SendOutboundData (const char *data, unsigned long length)
|
||||
{
|
||||
//if (bCloseNow || bCloseAfterWriting)
|
||||
if (IsCloseScheduled())
|
||||
return 0;
|
||||
|
||||
if (!data && (length > 0))
|
||||
throw std::runtime_error ("bad outbound data");
|
||||
char *buffer = (char *) malloc (length + 1);
|
||||
if (!buffer)
|
||||
throw std::runtime_error ("no allocation for outbound data");
|
||||
memcpy (buffer, data, length);
|
||||
buffer [length] = 0;
|
||||
OutboundPages.push_back (OutboundPage (buffer, length));
|
||||
OutboundDataSize += length;
|
||||
#ifdef HAVE_EPOLL
|
||||
EpollEvent.events = (EPOLLIN | EPOLLOUT);
|
||||
assert (MyEventMachine);
|
||||
MyEventMachine->Modify (this);
|
||||
#endif
|
||||
return length;
|
||||
}
|
||||
|
||||
/********************************
|
||||
PipeDescriptor::GetSubprocessPid
|
||||
********************************/
|
||||
|
||||
bool PipeDescriptor::GetSubprocessPid (pid_t *pid)
|
||||
{
|
||||
bool ok = false;
|
||||
if (pid && (SubprocessPid > 0)) {
|
||||
*pid = SubprocessPid;
|
||||
ok = true;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
#endif // OS_UNIX
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: project.h
|
||||
Date: 06Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#ifndef __Project__H_
|
||||
#define __Project__H_
|
||||
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
|
||||
#ifdef OS_UNIX
|
||||
#include <signal.h>
|
||||
#include <netdb.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/wait.h>
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <pwd.h>
|
||||
#include <string.h>
|
||||
typedef int SOCKET;
|
||||
#define INVALID_SOCKET -1
|
||||
#define SOCKET_ERROR -1
|
||||
#ifdef OS_SOLARIS8
|
||||
#include <strings.h>
|
||||
#include <sys/un.h>
|
||||
#ifndef AF_LOCAL
|
||||
#define AF_LOCAL AF_UNIX
|
||||
#endif
|
||||
// INADDR_NONE is undefined on Solaris < 8. Thanks to Brett Eisenberg and Tim Pease.
|
||||
#ifndef INADDR_NONE
|
||||
#define INADDR_NONE ((unsigned long)-1)
|
||||
#endif
|
||||
#endif /* OS_SOLARIS8 */
|
||||
|
||||
#ifdef _AIX
|
||||
#include <strings.h>
|
||||
#ifndef AF_LOCAL
|
||||
#define AF_LOCAL AF_UNIX
|
||||
#endif
|
||||
#endif /* _AIX */
|
||||
|
||||
#ifdef OS_DARWIN
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
#endif /* OS_DARWIN */
|
||||
|
||||
#endif /* OS_UNIX */
|
||||
|
||||
#ifdef OS_WIN32
|
||||
// 21Sep09: windows limits select() to 64 sockets by default, we increase it to 1024 here (before including winsock2.h)
|
||||
// 18Jun12: fd_setsize must be changed in the ruby binary (not in this extension). redefining it also causes segvs, see eventmachine/eventmachine#333
|
||||
//#define FD_SETSIZE 1024
|
||||
|
||||
// WIN32_LEAN_AND_MEAN excludes APIs such as Cryptography, DDE, RPC, Shell, and Windows Sockets.
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <rpc.h>
|
||||
#include <fcntl.h>
|
||||
#include <assert.h>
|
||||
|
||||
// Older versions of MinGW in the Ruby Dev Kit do not provide the getaddrinfo hint flags
|
||||
#ifndef AI_ADDRCONFIG
|
||||
#define AI_ADDRCONFIG 0x0400
|
||||
#endif
|
||||
|
||||
#ifndef AI_NUMERICSERV
|
||||
#define AI_NUMERICSERV 0x0008
|
||||
#endif
|
||||
|
||||
// Use the Win32 wrapper library that Ruby owns to be able to close sockets with the close() function
|
||||
#define RUBY_EXPORT
|
||||
#include <ruby/defines.h>
|
||||
#include <ruby/win32.h>
|
||||
#endif /* OS_WIN32 */
|
||||
|
||||
#if !defined(_MSC_VER) || _MSC_VER > 1500
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#ifdef WITH_SSL
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_EPOLL
|
||||
#include <sys/epoll.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KQUEUE
|
||||
#include <sys/event.h>
|
||||
#include <sys/queue.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_INOTIFY
|
||||
#include <sys/inotify.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_OLD_INOTIFY
|
||||
#include <sys/syscall.h>
|
||||
#include <linux/inotify.h>
|
||||
static inline int inotify_init (void) { return syscall (__NR_inotify_init); }
|
||||
static inline int inotify_add_watch (int fd, const char *name, __u32 mask) { return syscall (__NR_inotify_add_watch, fd, name, mask); }
|
||||
static inline int inotify_rm_watch (int fd, __u32 wd) { return syscall (__NR_inotify_rm_watch, fd, wd); }
|
||||
#define HAVE_INOTIFY 1
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_INOTIFY
|
||||
#define INOTIFY_EVENT_SIZE (sizeof(struct inotify_event))
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_WRITEV
|
||||
#include <sys/uio.h>
|
||||
#endif
|
||||
|
||||
#if __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef void (*EMCallback)(const unsigned long, int, const char*, const unsigned long);
|
||||
#if __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__) && (__GNUC__ >= 3)
|
||||
#define UNUSED __attribute__ ((unused))
|
||||
#else
|
||||
#define UNUSED
|
||||
#endif
|
||||
|
||||
#include "binder.h"
|
||||
#include "em.h"
|
||||
#include "ed.h"
|
||||
#include "page.h"
|
||||
#include "ssl.h"
|
||||
#include "eventmachine.h"
|
||||
|
||||
#endif // __Project__H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,619 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: ssl.cpp
|
||||
Date: 30Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#ifdef WITH_SSL
|
||||
|
||||
#include "project.h"
|
||||
|
||||
|
||||
bool SslContext_t::bLibraryInitialized = false;
|
||||
|
||||
|
||||
|
||||
static void InitializeDefaultCredentials();
|
||||
static EVP_PKEY *DefaultPrivateKey = NULL;
|
||||
static X509 *DefaultCertificate = NULL;
|
||||
|
||||
static char PrivateMaterials[] = {
|
||||
"-----BEGIN RSA PRIVATE KEY-----\n"
|
||||
"MIICXAIBAAKBgQDCYYhcw6cGRbhBVShKmbWm7UVsEoBnUf0cCh8AX+MKhMxwVDWV\n"
|
||||
"Igdskntn3cSJjRtmgVJHIK0lpb/FYHQB93Ohpd9/Z18pDmovfFF9nDbFF0t39hJ/\n"
|
||||
"AqSzFB3GiVPoFFZJEE1vJqh+3jzsSF5K56bZ6azz38VlZgXeSozNW5bXkQIDAQAB\n"
|
||||
"AoGALA89gIFcr6BIBo8N5fL3aNHpZXjAICtGav+kTUpuxSiaym9cAeTHuAVv8Xgk\n"
|
||||
"H2Wbq11uz+6JMLpkQJH/WZ7EV59DPOicXrp0Imr73F3EXBfR7t2EQDYHPMthOA1D\n"
|
||||
"I9EtCzvV608Ze90hiJ7E3guGrGppZfJ+eUWCPgy8CZH1vRECQQDv67rwV/oU1aDo\n"
|
||||
"6/+d5nqjeW6mWkGqTnUU96jXap8EIw6B+0cUKskwx6mHJv+tEMM2748ZY7b0yBlg\n"
|
||||
"w4KDghbFAkEAz2h8PjSJG55LwqmXih1RONSgdN9hjB12LwXL1CaDh7/lkEhq0PlK\n"
|
||||
"PCAUwQSdM17Sl0Xxm2CZiekTSlwmHrtqXQJAF3+8QJwtV2sRJp8u2zVe37IeH1cJ\n"
|
||||
"xXeHyjTzqZ2803fnjN2iuZvzNr7noOA1/Kp+pFvUZUU5/0G2Ep8zolPUjQJAFA7k\n"
|
||||
"xRdLkzIx3XeNQjwnmLlncyYPRv+qaE3FMpUu7zftuZBnVCJnvXzUxP3vPgKTlzGa\n"
|
||||
"dg5XivDRfsV+okY5uQJBAMV4FesUuLQVEKb6lMs7rzZwpeGQhFDRfywJzfom2TLn\n"
|
||||
"2RdJQQ3dcgnhdVDgt5o1qkmsqQh8uJrJ9SdyLIaZQIc=\n"
|
||||
"-----END RSA PRIVATE KEY-----\n"
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIID6TCCA1KgAwIBAgIJANm4W/Tzs+s+MA0GCSqGSIb3DQEBBQUAMIGqMQswCQYD\n"
|
||||
"VQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRYw\n"
|
||||
"FAYDVQQKEw1TdGVhbWhlYXQubmV0MRQwEgYDVQQLEwtFbmdpbmVlcmluZzEdMBsG\n"
|
||||
"A1UEAxMUb3BlbmNhLnN0ZWFtaGVhdC5uZXQxKDAmBgkqhkiG9w0BCQEWGWVuZ2lu\n"
|
||||
"ZWVyaW5nQHN0ZWFtaGVhdC5uZXQwHhcNMDYwNTA1MTcwNjAzWhcNMjQwMjIwMTcw\n"
|
||||
"NjAzWjCBqjELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQH\n"
|
||||
"EwhOZXcgWW9yazEWMBQGA1UEChMNU3RlYW1oZWF0Lm5ldDEUMBIGA1UECxMLRW5n\n"
|
||||
"aW5lZXJpbmcxHTAbBgNVBAMTFG9wZW5jYS5zdGVhbWhlYXQubmV0MSgwJgYJKoZI\n"
|
||||
"hvcNAQkBFhllbmdpbmVlcmluZ0BzdGVhbWhlYXQubmV0MIGfMA0GCSqGSIb3DQEB\n"
|
||||
"AQUAA4GNADCBiQKBgQDCYYhcw6cGRbhBVShKmbWm7UVsEoBnUf0cCh8AX+MKhMxw\n"
|
||||
"VDWVIgdskntn3cSJjRtmgVJHIK0lpb/FYHQB93Ohpd9/Z18pDmovfFF9nDbFF0t3\n"
|
||||
"9hJ/AqSzFB3GiVPoFFZJEE1vJqh+3jzsSF5K56bZ6azz38VlZgXeSozNW5bXkQID\n"
|
||||
"AQABo4IBEzCCAQ8wHQYDVR0OBBYEFPJvPd1Fcmd8o/Tm88r+NjYPICCkMIHfBgNV\n"
|
||||
"HSMEgdcwgdSAFPJvPd1Fcmd8o/Tm88r+NjYPICCkoYGwpIGtMIGqMQswCQYDVQQG\n"
|
||||
"EwJVUzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRYwFAYD\n"
|
||||
"VQQKEw1TdGVhbWhlYXQubmV0MRQwEgYDVQQLEwtFbmdpbmVlcmluZzEdMBsGA1UE\n"
|
||||
"AxMUb3BlbmNhLnN0ZWFtaGVhdC5uZXQxKDAmBgkqhkiG9w0BCQEWGWVuZ2luZWVy\n"
|
||||
"aW5nQHN0ZWFtaGVhdC5uZXSCCQDZuFv087PrPjAMBgNVHRMEBTADAQH/MA0GCSqG\n"
|
||||
"SIb3DQEBBQUAA4GBAC1CXey/4UoLgJiwcEMDxOvW74plks23090iziFIlGgcIhk0\n"
|
||||
"Df6hTAs7H3MWww62ddvR8l07AWfSzSP5L6mDsbvq7EmQsmPODwb6C+i2aF3EDL8j\n"
|
||||
"uw73m4YIGI0Zw2XdBpiOGkx2H56Kya6mJJe/5XORZedh1wpI7zki01tHYbcy\n"
|
||||
"-----END CERTIFICATE-----\n"};
|
||||
|
||||
/* These private materials were made with:
|
||||
* openssl req -new -x509 -keyout cakey.pem -out cacert.pem -nodes -days 6500
|
||||
* TODO: We need a full-blown capability to work with user-supplied
|
||||
* keypairs and properly-signed certificates.
|
||||
*/
|
||||
|
||||
|
||||
/*****************
|
||||
builtin_passwd_cb
|
||||
*****************/
|
||||
|
||||
extern "C" int builtin_passwd_cb (char *buf UNUSED, int bufsize UNUSED, int rwflag UNUSED, void *userdata UNUSED)
|
||||
{
|
||||
strcpy (buf, "kittycat");
|
||||
return 8;
|
||||
}
|
||||
|
||||
/****************************
|
||||
InitializeDefaultCredentials
|
||||
****************************/
|
||||
|
||||
static void InitializeDefaultCredentials()
|
||||
{
|
||||
BIO *bio = BIO_new_mem_buf (PrivateMaterials, -1);
|
||||
assert (bio);
|
||||
|
||||
if (DefaultPrivateKey) {
|
||||
// we may come here in a restart.
|
||||
EVP_PKEY_free (DefaultPrivateKey);
|
||||
DefaultPrivateKey = NULL;
|
||||
}
|
||||
PEM_read_bio_PrivateKey (bio, &DefaultPrivateKey, builtin_passwd_cb, 0);
|
||||
|
||||
if (DefaultCertificate) {
|
||||
// we may come here in a restart.
|
||||
X509_free (DefaultCertificate);
|
||||
DefaultCertificate = NULL;
|
||||
}
|
||||
PEM_read_bio_X509 (bio, &DefaultCertificate, NULL, 0);
|
||||
|
||||
BIO_free (bio);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************
|
||||
SslContext_t::SslContext_t
|
||||
**************************/
|
||||
|
||||
SslContext_t::SslContext_t (bool is_server, const std::string &privkeyfile, const std::string &certchainfile, const std::string &cipherlist, const std::string &ecdh_curve, const std::string &dhparam, int ssl_version) :
|
||||
bIsServer (is_server),
|
||||
pCtx (NULL),
|
||||
PrivateKey (NULL),
|
||||
Certificate (NULL)
|
||||
{
|
||||
/* TODO: the usage of the specified private-key and cert-chain filenames only applies to
|
||||
* client-side connections at this point. Server connections currently use the default materials.
|
||||
* That needs to be fixed asap.
|
||||
* Also, in this implementation, server-side connections use statically defined X-509 defaults.
|
||||
* One thing I'm really not clear on is whether or not you have to explicitly free X509 and EVP_PKEY
|
||||
* objects when we call our destructor, or whether just calling SSL_CTX_free is enough.
|
||||
*/
|
||||
|
||||
if (!bLibraryInitialized) {
|
||||
bLibraryInitialized = true;
|
||||
SSL_library_init();
|
||||
OpenSSL_add_ssl_algorithms();
|
||||
OpenSSL_add_all_algorithms();
|
||||
SSL_load_error_strings();
|
||||
ERR_load_crypto_strings();
|
||||
|
||||
InitializeDefaultCredentials();
|
||||
}
|
||||
|
||||
pCtx = SSL_CTX_new (bIsServer ? SSLv23_server_method() : SSLv23_client_method());
|
||||
if (!pCtx)
|
||||
throw std::runtime_error ("no SSL context");
|
||||
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_ALL);
|
||||
|
||||
#ifdef SSL_CTRL_CLEAR_OPTIONS
|
||||
SSL_CTX_clear_options (pCtx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1);
|
||||
# ifdef SSL_OP_NO_TLSv1_1
|
||||
SSL_CTX_clear_options (pCtx, SSL_OP_NO_TLSv1_1);
|
||||
# endif
|
||||
# ifdef SSL_OP_NO_TLSv1_2
|
||||
SSL_CTX_clear_options (pCtx, SSL_OP_NO_TLSv1_2);
|
||||
# endif
|
||||
#endif
|
||||
|
||||
if (!(ssl_version & EM_PROTO_SSLv2))
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_NO_SSLv2);
|
||||
|
||||
if (!(ssl_version & EM_PROTO_SSLv3))
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_NO_SSLv3);
|
||||
|
||||
if (!(ssl_version & EM_PROTO_TLSv1))
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_NO_TLSv1);
|
||||
|
||||
#ifdef SSL_OP_NO_TLSv1_1
|
||||
if (!(ssl_version & EM_PROTO_TLSv1_1))
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_NO_TLSv1_1);
|
||||
#endif
|
||||
|
||||
#ifdef SSL_OP_NO_TLSv1_2
|
||||
if (!(ssl_version & EM_PROTO_TLSv1_2))
|
||||
SSL_CTX_set_options (pCtx, SSL_OP_NO_TLSv1_2);
|
||||
#endif
|
||||
|
||||
#ifdef SSL_MODE_RELEASE_BUFFERS
|
||||
SSL_CTX_set_mode (pCtx, SSL_MODE_RELEASE_BUFFERS);
|
||||
#endif
|
||||
|
||||
if (bIsServer) {
|
||||
|
||||
// The SSL_CTX calls here do NOT allocate memory.
|
||||
int e;
|
||||
if (privkeyfile.length() > 0)
|
||||
e = SSL_CTX_use_PrivateKey_file (pCtx, privkeyfile.c_str(), SSL_FILETYPE_PEM);
|
||||
else
|
||||
e = SSL_CTX_use_PrivateKey (pCtx, DefaultPrivateKey);
|
||||
if (e <= 0) ERR_print_errors_fp(stderr);
|
||||
assert (e > 0);
|
||||
|
||||
if (certchainfile.length() > 0)
|
||||
e = SSL_CTX_use_certificate_chain_file (pCtx, certchainfile.c_str());
|
||||
else
|
||||
e = SSL_CTX_use_certificate (pCtx, DefaultCertificate);
|
||||
if (e <= 0) ERR_print_errors_fp(stderr);
|
||||
assert (e > 0);
|
||||
|
||||
if (dhparam.length() > 0) {
|
||||
DH *dh;
|
||||
BIO *bio;
|
||||
|
||||
bio = BIO_new_file(dhparam.c_str(), "r");
|
||||
if (bio == NULL) {
|
||||
char buf [500];
|
||||
snprintf (buf, sizeof(buf)-1, "dhparam: BIO_new_file(%s) failed", dhparam.c_str());
|
||||
throw std::runtime_error (buf);
|
||||
}
|
||||
|
||||
dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL);
|
||||
|
||||
if (dh == NULL) {
|
||||
BIO_free(bio);
|
||||
char buf [500];
|
||||
snprintf (buf, sizeof(buf)-1, "dhparam: PEM_read_bio_DHparams(%s) failed", dhparam.c_str());
|
||||
throw std::runtime_error (buf);
|
||||
}
|
||||
|
||||
SSL_CTX_set_tmp_dh(pCtx, dh);
|
||||
|
||||
DH_free(dh);
|
||||
BIO_free(bio);
|
||||
}
|
||||
|
||||
if (ecdh_curve.length() > 0) {
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x0090800fL && !defined(OPENSSL_NO_ECDH)
|
||||
int nid;
|
||||
EC_KEY *ecdh;
|
||||
|
||||
nid = OBJ_sn2nid((const char *) ecdh_curve.c_str());
|
||||
if (nid == 0) {
|
||||
char buf [200];
|
||||
snprintf (buf, sizeof(buf)-1, "ecdh_curve: Unknown curve name: %s", ecdh_curve.c_str());
|
||||
throw std::runtime_error (buf);
|
||||
}
|
||||
|
||||
ecdh = EC_KEY_new_by_curve_name(nid);
|
||||
if (ecdh == NULL) {
|
||||
char buf [200];
|
||||
snprintf (buf, sizeof(buf)-1, "ecdh_curve: Unable to create: %s", ecdh_curve.c_str());
|
||||
throw std::runtime_error (buf);
|
||||
}
|
||||
|
||||
SSL_CTX_set_options(pCtx, SSL_OP_SINGLE_ECDH_USE);
|
||||
|
||||
SSL_CTX_set_tmp_ecdh(pCtx, ecdh);
|
||||
|
||||
EC_KEY_free(ecdh);
|
||||
#else
|
||||
throw std::runtime_error ("No openssl ECDH support");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (cipherlist.length() > 0)
|
||||
SSL_CTX_set_cipher_list (pCtx, cipherlist.c_str());
|
||||
else
|
||||
SSL_CTX_set_cipher_list (pCtx, "ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH");
|
||||
|
||||
if (bIsServer) {
|
||||
SSL_CTX_sess_set_cache_size (pCtx, 128);
|
||||
SSL_CTX_set_session_id_context (pCtx, (unsigned char*)"eventmachine", 12);
|
||||
}
|
||||
else {
|
||||
int e;
|
||||
if (privkeyfile.length() > 0) {
|
||||
e = SSL_CTX_use_PrivateKey_file (pCtx, privkeyfile.c_str(), SSL_FILETYPE_PEM);
|
||||
if (e <= 0) ERR_print_errors_fp(stderr);
|
||||
assert (e > 0);
|
||||
}
|
||||
if (certchainfile.length() > 0) {
|
||||
e = SSL_CTX_use_certificate_chain_file (pCtx, certchainfile.c_str());
|
||||
if (e <= 0) ERR_print_errors_fp(stderr);
|
||||
assert (e > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************
|
||||
SslContext_t::~SslContext_t
|
||||
***************************/
|
||||
|
||||
SslContext_t::~SslContext_t()
|
||||
{
|
||||
if (pCtx)
|
||||
SSL_CTX_free (pCtx);
|
||||
if (PrivateKey)
|
||||
EVP_PKEY_free (PrivateKey);
|
||||
if (Certificate)
|
||||
X509_free (Certificate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************
|
||||
SslBox_t::SslBox_t
|
||||
******************/
|
||||
|
||||
SslBox_t::SslBox_t (bool is_server, const std::string &privkeyfile, const std::string &certchainfile, bool verify_peer, bool fail_if_no_peer_cert, const std::string &snihostname, const std::string &cipherlist, const std::string &ecdh_curve, const std::string &dhparam, int ssl_version, const uintptr_t binding):
|
||||
bIsServer (is_server),
|
||||
bHandshakeCompleted (false),
|
||||
bVerifyPeer (verify_peer),
|
||||
bFailIfNoPeerCert (fail_if_no_peer_cert),
|
||||
pSSL (NULL),
|
||||
pbioRead (NULL),
|
||||
pbioWrite (NULL)
|
||||
{
|
||||
/* TODO someday: make it possible to re-use SSL contexts so we don't have to create
|
||||
* a new one every time we come here.
|
||||
*/
|
||||
|
||||
Context = new SslContext_t (bIsServer, privkeyfile, certchainfile, cipherlist, ecdh_curve, dhparam, ssl_version);
|
||||
assert (Context);
|
||||
|
||||
pbioRead = BIO_new (BIO_s_mem());
|
||||
assert (pbioRead);
|
||||
|
||||
pbioWrite = BIO_new (BIO_s_mem());
|
||||
assert (pbioWrite);
|
||||
|
||||
pSSL = SSL_new (Context->pCtx);
|
||||
assert (pSSL);
|
||||
|
||||
if (snihostname.length() > 0) {
|
||||
SSL_set_tlsext_host_name (pSSL, snihostname.c_str());
|
||||
}
|
||||
|
||||
SSL_set_bio (pSSL, pbioRead, pbioWrite);
|
||||
|
||||
// Store a pointer to the binding signature in the SSL object so we can retrieve it later
|
||||
SSL_set_ex_data(pSSL, 0, (void*) binding);
|
||||
|
||||
if (bVerifyPeer) {
|
||||
int mode = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
|
||||
if (bFailIfNoPeerCert)
|
||||
mode = mode | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
|
||||
SSL_set_verify(pSSL, mode, ssl_verify_wrapper);
|
||||
}
|
||||
|
||||
if (!bIsServer) {
|
||||
int e = SSL_connect (pSSL);
|
||||
if (e != 1)
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************
|
||||
SslBox_t::~SslBox_t
|
||||
*******************/
|
||||
|
||||
SslBox_t::~SslBox_t()
|
||||
{
|
||||
// Freeing pSSL will also free the associated BIOs, so DON'T free them separately.
|
||||
if (pSSL) {
|
||||
if (SSL_get_shutdown (pSSL) & SSL_RECEIVED_SHUTDOWN)
|
||||
SSL_shutdown (pSSL);
|
||||
else
|
||||
SSL_clear (pSSL);
|
||||
SSL_free (pSSL);
|
||||
}
|
||||
|
||||
delete Context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************
|
||||
SslBox_t::PutCiphertext
|
||||
***********************/
|
||||
|
||||
bool SslBox_t::PutCiphertext (const char *buf, int bufsize)
|
||||
{
|
||||
assert (buf && (bufsize > 0));
|
||||
|
||||
assert (pbioRead);
|
||||
int n = BIO_write (pbioRead, buf, bufsize);
|
||||
|
||||
return (n == bufsize) ? true : false;
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetPlaintext
|
||||
**********************/
|
||||
|
||||
int SslBox_t::GetPlaintext (char *buf, int bufsize)
|
||||
{
|
||||
if (!SSL_is_init_finished (pSSL)) {
|
||||
int e = bIsServer ? SSL_accept (pSSL) : SSL_connect (pSSL);
|
||||
if (e != 1) {
|
||||
int er = SSL_get_error (pSSL, e);
|
||||
if (er != SSL_ERROR_WANT_READ) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
// Return -1 for a nonfatal error, -2 for an error that should force the connection down.
|
||||
return (er == SSL_ERROR_SSL) ? (-2) : (-1);
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
bHandshakeCompleted = true;
|
||||
// If handshake finished, FALL THROUGH and return the available plaintext.
|
||||
}
|
||||
|
||||
if (!SSL_is_init_finished (pSSL)) {
|
||||
// We can get here if a browser abandons a handshake.
|
||||
// The user can see a warning dialog and abort the connection.
|
||||
//cerr << "<SSL_incomp>";
|
||||
return 0;
|
||||
}
|
||||
|
||||
//cerr << "CIPH: " << SSL_get_cipher (pSSL) << endl;
|
||||
|
||||
int n = SSL_read (pSSL, buf, bufsize);
|
||||
if (n >= 0) {
|
||||
return n;
|
||||
}
|
||||
else {
|
||||
if (SSL_get_error (pSSL, n) == SSL_ERROR_WANT_READ) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************
|
||||
SslBox_t::CanGetCiphertext
|
||||
**************************/
|
||||
|
||||
bool SslBox_t::CanGetCiphertext()
|
||||
{
|
||||
assert (pbioWrite);
|
||||
return BIO_pending (pbioWrite) ? true : false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************
|
||||
SslBox_t::GetCiphertext
|
||||
***********************/
|
||||
|
||||
int SslBox_t::GetCiphertext (char *buf, int bufsize)
|
||||
{
|
||||
assert (pbioWrite);
|
||||
assert (buf && (bufsize > 0));
|
||||
|
||||
return BIO_read (pbioWrite, buf, bufsize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************
|
||||
SslBox_t::PutPlaintext
|
||||
**********************/
|
||||
|
||||
int SslBox_t::PutPlaintext (const char *buf, int bufsize)
|
||||
{
|
||||
// The caller will interpret the return value as the number of bytes written.
|
||||
// WARNING WARNING WARNING, are there any situations in which a 0 or -1 return
|
||||
// from SSL_write means we should immediately retry? The socket-machine loop
|
||||
// will probably wait for a time-out cycle (perhaps a second) before re-trying.
|
||||
// THIS WOULD CAUSE A PERCEPTIBLE DELAY!
|
||||
|
||||
/* We internally queue any outbound plaintext that can't be dispatched
|
||||
* because we're in the middle of a handshake or something.
|
||||
* When we get called, try to send any queued data first, and then
|
||||
* send the caller's data (or queue it). We may get called with no outbound
|
||||
* data, which means we try to send the outbound queue and that's all.
|
||||
*
|
||||
* Return >0 if we wrote any data, 0 if we didn't, and <0 for a fatal error.
|
||||
* Note that if we return 0, the connection is still considered live
|
||||
* and we are signalling that we have accepted the outbound data (if any).
|
||||
*/
|
||||
|
||||
OutboundQ.Push (buf, bufsize);
|
||||
|
||||
if (!SSL_is_init_finished (pSSL))
|
||||
return 0;
|
||||
|
||||
bool fatal = false;
|
||||
bool did_work = false;
|
||||
int pending = BIO_pending(pbioWrite);
|
||||
|
||||
while (OutboundQ.HasPages() && pending < SSLBOX_WRITE_BUFFER_SIZE) {
|
||||
const char *page;
|
||||
int length;
|
||||
OutboundQ.Front (&page, &length);
|
||||
assert (page && (length > 0));
|
||||
int n = SSL_write (pSSL, page, length);
|
||||
pending = BIO_pending(pbioWrite);
|
||||
|
||||
if (n > 0) {
|
||||
did_work = true;
|
||||
OutboundQ.PopFront();
|
||||
}
|
||||
else {
|
||||
int er = SSL_get_error (pSSL, n);
|
||||
if ((er != SSL_ERROR_WANT_READ) && (er != SSL_ERROR_WANT_WRITE))
|
||||
fatal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (did_work)
|
||||
return 1;
|
||||
else if (fatal)
|
||||
return -1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetPeerCert
|
||||
**********************/
|
||||
|
||||
X509 *SslBox_t::GetPeerCert()
|
||||
{
|
||||
X509 *cert = NULL;
|
||||
|
||||
if (pSSL)
|
||||
cert = SSL_get_peer_certificate(pSSL);
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetCipherBits
|
||||
**********************/
|
||||
|
||||
int SslBox_t::GetCipherBits()
|
||||
{
|
||||
int bits = -1;
|
||||
if (pSSL)
|
||||
SSL_get_cipher_bits(pSSL, &bits);
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetCipherName
|
||||
**********************/
|
||||
|
||||
const char *SslBox_t::GetCipherName()
|
||||
{
|
||||
if (pSSL)
|
||||
return SSL_get_cipher_name(pSSL);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetCipherProtocol
|
||||
**********************/
|
||||
|
||||
const char *SslBox_t::GetCipherProtocol()
|
||||
{
|
||||
if (pSSL)
|
||||
return SSL_get_cipher_version(pSSL);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**********************
|
||||
SslBox_t::GetSNIHostname
|
||||
**********************/
|
||||
|
||||
const char *SslBox_t::GetSNIHostname()
|
||||
{
|
||||
#ifdef TLSEXT_NAMETYPE_host_name
|
||||
if (pSSL)
|
||||
return SSL_get_servername (pSSL, TLSEXT_NAMETYPE_host_name);
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/******************
|
||||
ssl_verify_wrapper
|
||||
*******************/
|
||||
|
||||
extern "C" int ssl_verify_wrapper(int preverify_ok UNUSED, X509_STORE_CTX *ctx)
|
||||
{
|
||||
uintptr_t binding;
|
||||
X509 *cert;
|
||||
SSL *ssl;
|
||||
BUF_MEM *buf;
|
||||
BIO *out;
|
||||
int result;
|
||||
|
||||
cert = X509_STORE_CTX_get_current_cert(ctx);
|
||||
ssl = (SSL*) X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
|
||||
binding = (uintptr_t) SSL_get_ex_data(ssl, 0);
|
||||
|
||||
out = BIO_new(BIO_s_mem());
|
||||
PEM_write_bio_X509(out, cert);
|
||||
BIO_write(out, "\0", 1);
|
||||
BIO_get_mem_ptr(out, &buf);
|
||||
|
||||
ConnectionDescriptor *cd = dynamic_cast <ConnectionDescriptor*> (Bindable_t::GetObject(binding));
|
||||
result = (cd->VerifySslPeer(buf->data) == true ? 1 : 0);
|
||||
BIO_free(out);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif // WITH_SSL
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*****************************************************************************
|
||||
|
||||
$Id$
|
||||
|
||||
File: ssl.h
|
||||
Date: 30Apr06
|
||||
|
||||
Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
Gmail: blackhedd
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of either: 1) the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of the
|
||||
License, or (at your option) any later version; or 2) Ruby's License.
|
||||
|
||||
See the file COPYING for complete licensing information.
|
||||
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#ifndef __SslBox__H_
|
||||
#define __SslBox__H_
|
||||
|
||||
|
||||
|
||||
|
||||
#ifdef WITH_SSL
|
||||
|
||||
/******************
|
||||
class SslContext_t
|
||||
******************/
|
||||
|
||||
class SslContext_t
|
||||
{
|
||||
public:
|
||||
SslContext_t (bool is_server, const std::string &privkeyfile, const std::string &certchainfile, const std::string &cipherlist, const std::string &ecdh_curve, const std::string &dhparam, int ssl_version);
|
||||
virtual ~SslContext_t();
|
||||
|
||||
private:
|
||||
static bool bLibraryInitialized;
|
||||
|
||||
private:
|
||||
bool bIsServer;
|
||||
SSL_CTX *pCtx;
|
||||
|
||||
EVP_PKEY *PrivateKey;
|
||||
X509 *Certificate;
|
||||
|
||||
friend class SslBox_t;
|
||||
};
|
||||
|
||||
|
||||
/**************
|
||||
class SslBox_t
|
||||
**************/
|
||||
|
||||
#define SSLBOX_INPUT_CHUNKSIZE 2019
|
||||
#define SSLBOX_OUTPUT_CHUNKSIZE 2048
|
||||
#define SSLBOX_WRITE_BUFFER_SIZE 8192 // (SSLBOX_OUTPUT_CHUNKSIZE * 4)
|
||||
|
||||
class SslBox_t
|
||||
{
|
||||
public:
|
||||
SslBox_t (bool is_server, const std::string &privkeyfile, const std::string &certchainfile, bool verify_peer, bool fail_if_no_peer_cert, const std::string &snihostname, const std::string &cipherlist, const std::string &ecdh_curve, const std::string &dhparam, int ssl_version, const uintptr_t binding);
|
||||
virtual ~SslBox_t();
|
||||
|
||||
int PutPlaintext (const char*, int);
|
||||
int GetPlaintext (char*, int);
|
||||
|
||||
bool PutCiphertext (const char*, int);
|
||||
bool CanGetCiphertext();
|
||||
int GetCiphertext (char*, int);
|
||||
bool IsHandshakeCompleted() {return bHandshakeCompleted;}
|
||||
|
||||
X509 *GetPeerCert();
|
||||
int GetCipherBits();
|
||||
const char *GetCipherName();
|
||||
const char *GetCipherProtocol();
|
||||
const char *GetSNIHostname();
|
||||
|
||||
void Shutdown();
|
||||
|
||||
protected:
|
||||
SslContext_t *Context;
|
||||
|
||||
bool bIsServer;
|
||||
bool bHandshakeCompleted;
|
||||
bool bVerifyPeer;
|
||||
bool bFailIfNoPeerCert;
|
||||
SSL *pSSL;
|
||||
BIO *pbioRead;
|
||||
BIO *pbioWrite;
|
||||
|
||||
PageList OutboundQ;
|
||||
};
|
||||
|
||||
extern "C" int ssl_verify_wrapper(int, X509_STORE_CTX*);
|
||||
|
||||
#endif // WITH_SSL
|
||||
|
||||
|
||||
#endif // __SslBox__H_
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry excluding="src/" kind="src" path=""/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
|
||||
<classpathentry kind="output" path="src"/>
|
||||
</classpath>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>em_reactor</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
@@ -0,0 +1,613 @@
|
||||
/**
|
||||
* $Id$
|
||||
*
|
||||
* Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
* Homepage:: http://rubyeventmachine.com
|
||||
* Date:: 15 Jul 2007
|
||||
*
|
||||
* See EventMachine and EventMachine::Connection for documentation and
|
||||
* usage examples.
|
||||
*
|
||||
*
|
||||
*----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
* Gmail: blackhedd
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of either: 1) the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version; or 2) Ruby's License.
|
||||
*
|
||||
* See the file COPYING for complete licensing information.
|
||||
*
|
||||
*---------------------------------------------------------------------------
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package com.rubyeventmachine;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.*;
|
||||
import java.nio.*;
|
||||
import java.net.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.security.*;
|
||||
|
||||
public class EmReactor {
|
||||
public final int EM_TIMER_FIRED = 100;
|
||||
public final int EM_CONNECTION_READ = 101;
|
||||
public final int EM_CONNECTION_UNBOUND = 102;
|
||||
public final int EM_CONNECTION_ACCEPTED = 103;
|
||||
public final int EM_CONNECTION_COMPLETED = 104;
|
||||
public final int EM_LOOPBREAK_SIGNAL = 105;
|
||||
public final int EM_CONNECTION_NOTIFY_READABLE = 106;
|
||||
public final int EM_CONNECTION_NOTIFY_WRITABLE = 107;
|
||||
public final int EM_SSL_HANDSHAKE_COMPLETED = 108;
|
||||
public final int EM_SSL_VERIFY = 109;
|
||||
public final int EM_PROXY_TARGET_UNBOUND = 110;
|
||||
public final int EM_PROXY_COMPLETED = 111;
|
||||
|
||||
public final int EM_PROTO_SSLv2 = 2;
|
||||
public final int EM_PROTO_SSLv3 = 4;
|
||||
public final int EM_PROTO_TLSv1 = 8;
|
||||
public final int EM_PROTO_TLSv1_1 = 16;
|
||||
public final int EM_PROTO_TLSv1_2 = 32;
|
||||
|
||||
private Selector mySelector;
|
||||
private TreeMap<Long, ArrayList<Long>> Timers;
|
||||
private HashMap<Long, EventableChannel> Connections;
|
||||
private HashMap<Long, ServerSocketChannel> Acceptors;
|
||||
private ArrayList<Long> NewConnections;
|
||||
private ArrayList<Long> UnboundConnections;
|
||||
private ArrayList<EventableSocketChannel> DetachedConnections;
|
||||
|
||||
private boolean bRunReactor;
|
||||
private long BindingIndex;
|
||||
private AtomicBoolean loopBreaker;
|
||||
private ByteBuffer myReadBuffer;
|
||||
private int timerQuantum;
|
||||
|
||||
public EmReactor() {
|
||||
Timers = new TreeMap<Long, ArrayList<Long>>();
|
||||
Connections = new HashMap<Long, EventableChannel>();
|
||||
Acceptors = new HashMap<Long, ServerSocketChannel>();
|
||||
NewConnections = new ArrayList<Long>();
|
||||
UnboundConnections = new ArrayList<Long>();
|
||||
DetachedConnections = new ArrayList<EventableSocketChannel>();
|
||||
|
||||
BindingIndex = 0;
|
||||
loopBreaker = new AtomicBoolean();
|
||||
loopBreaker.set(false);
|
||||
myReadBuffer = ByteBuffer.allocate(32*1024); // don't use a direct buffer. Ruby doesn't seem to like them.
|
||||
timerQuantum = 98;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a no-op stub, intended to be overridden in user code.
|
||||
*/
|
||||
public void eventCallback (long sig, int eventType, ByteBuffer data, long data2) {
|
||||
System.out.println ("Default callback: "+sig+" "+eventType+" "+data+" "+data2);
|
||||
}
|
||||
public void eventCallback (long sig, int eventType, ByteBuffer data) {
|
||||
eventCallback (sig, eventType, data, 0);
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
mySelector = Selector.open();
|
||||
bRunReactor = true;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException ("Could not open selector", e);
|
||||
}
|
||||
|
||||
while (bRunReactor) {
|
||||
runLoopbreaks();
|
||||
if (!bRunReactor) break;
|
||||
|
||||
runTimers();
|
||||
if (!bRunReactor) break;
|
||||
|
||||
removeUnboundConnections();
|
||||
checkIO();
|
||||
addNewConnections();
|
||||
processIO();
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
void addNewConnections() {
|
||||
ListIterator<EventableSocketChannel> iter = DetachedConnections.listIterator(0);
|
||||
while (iter.hasNext()) {
|
||||
EventableSocketChannel ec = iter.next();
|
||||
ec.cleanup();
|
||||
}
|
||||
DetachedConnections.clear();
|
||||
|
||||
ListIterator<Long> iter2 = NewConnections.listIterator(0);
|
||||
while (iter2.hasNext()) {
|
||||
long b = iter2.next();
|
||||
|
||||
EventableChannel ec = Connections.get(b);
|
||||
if (ec != null) {
|
||||
try {
|
||||
ec.register();
|
||||
} catch (ClosedChannelException e) {
|
||||
UnboundConnections.add (ec.getBinding());
|
||||
}
|
||||
}
|
||||
}
|
||||
NewConnections.clear();
|
||||
}
|
||||
|
||||
void removeUnboundConnections() {
|
||||
ListIterator<Long> iter = UnboundConnections.listIterator(0);
|
||||
while (iter.hasNext()) {
|
||||
long b = iter.next();
|
||||
|
||||
EventableChannel ec = Connections.remove(b);
|
||||
if (ec != null) {
|
||||
eventCallback (b, EM_CONNECTION_UNBOUND, null);
|
||||
ec.close();
|
||||
|
||||
EventableSocketChannel sc = (EventableSocketChannel) ec;
|
||||
if (sc != null && sc.isAttached())
|
||||
DetachedConnections.add (sc);
|
||||
}
|
||||
}
|
||||
UnboundConnections.clear();
|
||||
}
|
||||
|
||||
void checkIO() {
|
||||
long timeout;
|
||||
|
||||
if (NewConnections.size() > 0) {
|
||||
timeout = -1;
|
||||
} else if (!Timers.isEmpty()) {
|
||||
long now = new Date().getTime();
|
||||
long k = Timers.firstKey();
|
||||
long diff = k-now;
|
||||
|
||||
if (diff <= 0)
|
||||
timeout = -1; // don't wait, just poll once
|
||||
else
|
||||
timeout = diff;
|
||||
} else {
|
||||
timeout = 0; // wait indefinitely
|
||||
}
|
||||
|
||||
try {
|
||||
if (timeout == -1)
|
||||
mySelector.selectNow();
|
||||
else
|
||||
mySelector.select(timeout);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
void processIO() {
|
||||
Iterator<SelectionKey> it = mySelector.selectedKeys().iterator();
|
||||
while (it.hasNext()) {
|
||||
SelectionKey k = it.next();
|
||||
it.remove();
|
||||
|
||||
if (k.isConnectable())
|
||||
isConnectable(k);
|
||||
|
||||
else if (k.isAcceptable())
|
||||
isAcceptable(k);
|
||||
|
||||
else {
|
||||
if (k.isWritable())
|
||||
isWritable(k);
|
||||
|
||||
if (k.isReadable())
|
||||
isReadable(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void isAcceptable (SelectionKey k) {
|
||||
ServerSocketChannel ss = (ServerSocketChannel) k.channel();
|
||||
SocketChannel sn;
|
||||
long b;
|
||||
|
||||
for (int n = 0; n < 10; n++) {
|
||||
try {
|
||||
sn = ss.accept();
|
||||
if (sn == null)
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
k.cancel();
|
||||
|
||||
ServerSocketChannel server = Acceptors.remove(k.attachment());
|
||||
if (server != null)
|
||||
try{ server.close(); } catch (IOException ex) {};
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
sn.configureBlocking(false);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
b = createBinding();
|
||||
EventableSocketChannel ec = new EventableSocketChannel (sn, b, mySelector);
|
||||
Connections.put (b, ec);
|
||||
NewConnections.add (b);
|
||||
|
||||
eventCallback (((Long)k.attachment()).longValue(), EM_CONNECTION_ACCEPTED, null, b);
|
||||
}
|
||||
}
|
||||
|
||||
void isReadable (SelectionKey k) {
|
||||
EventableChannel ec = (EventableChannel) k.attachment();
|
||||
long b = ec.getBinding();
|
||||
|
||||
if (ec.isWatchOnly()) {
|
||||
if (ec.isNotifyReadable())
|
||||
eventCallback (b, EM_CONNECTION_NOTIFY_READABLE, null);
|
||||
} else {
|
||||
myReadBuffer.clear();
|
||||
|
||||
try {
|
||||
ec.readInboundData (myReadBuffer);
|
||||
myReadBuffer.flip();
|
||||
if (myReadBuffer.limit() > 0)
|
||||
eventCallback (b, EM_CONNECTION_READ, myReadBuffer);
|
||||
} catch (IOException e) {
|
||||
UnboundConnections.add (b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void isWritable (SelectionKey k) {
|
||||
EventableChannel ec = (EventableChannel) k.attachment();
|
||||
long b = ec.getBinding();
|
||||
|
||||
if (ec.isWatchOnly()) {
|
||||
if (ec.isNotifyWritable())
|
||||
eventCallback (b, EM_CONNECTION_NOTIFY_WRITABLE, null);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
if (!ec.writeOutboundData())
|
||||
UnboundConnections.add (b);
|
||||
} catch (IOException e) {
|
||||
UnboundConnections.add (b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void isConnectable (SelectionKey k) {
|
||||
EventableSocketChannel ec = (EventableSocketChannel) k.attachment();
|
||||
long b = ec.getBinding();
|
||||
|
||||
try {
|
||||
if (ec.finishConnecting())
|
||||
eventCallback (b, EM_CONNECTION_COMPLETED, null);
|
||||
else
|
||||
UnboundConnections.add (b);
|
||||
} catch (IOException e) {
|
||||
UnboundConnections.add (b);
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
try {
|
||||
if (mySelector != null)
|
||||
mySelector.close();
|
||||
} catch (IOException e) {}
|
||||
mySelector = null;
|
||||
|
||||
// run down open connections and sockets.
|
||||
Iterator<ServerSocketChannel> i = Acceptors.values().iterator();
|
||||
while (i.hasNext()) {
|
||||
try {
|
||||
i.next().close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
|
||||
// 29Sep09: We create an ArrayList of the existing connections, then iterate over
|
||||
// that to call unbind on them. This is because an unbind can trigger a reconnect,
|
||||
// which will add to the Connections HashMap, causing a ConcurrentModificationException.
|
||||
// XXX: The correct behavior here would be to latch the various reactor methods to return
|
||||
// immediately if the reactor is shutting down.
|
||||
ArrayList<EventableChannel> conns = new ArrayList<EventableChannel>();
|
||||
Iterator<EventableChannel> i2 = Connections.values().iterator();
|
||||
while (i2.hasNext()) {
|
||||
EventableChannel ec = i2.next();
|
||||
if (ec != null) {
|
||||
conns.add (ec);
|
||||
}
|
||||
}
|
||||
Connections.clear();
|
||||
|
||||
ListIterator<EventableChannel> i3 = conns.listIterator(0);
|
||||
while (i3.hasNext()) {
|
||||
EventableChannel ec = i3.next();
|
||||
eventCallback (ec.getBinding(), EM_CONNECTION_UNBOUND, null);
|
||||
ec.close();
|
||||
|
||||
EventableSocketChannel sc = (EventableSocketChannel) ec;
|
||||
if (sc != null && sc.isAttached())
|
||||
DetachedConnections.add (sc);
|
||||
}
|
||||
|
||||
ListIterator<EventableSocketChannel> i4 = DetachedConnections.listIterator(0);
|
||||
while (i4.hasNext()) {
|
||||
EventableSocketChannel ec = i4.next();
|
||||
ec.cleanup();
|
||||
}
|
||||
DetachedConnections.clear();
|
||||
}
|
||||
|
||||
void runLoopbreaks() {
|
||||
if (loopBreaker.getAndSet(false)) {
|
||||
eventCallback (0, EM_LOOPBREAK_SIGNAL, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
bRunReactor = false;
|
||||
signalLoopbreak();
|
||||
}
|
||||
|
||||
void runTimers() {
|
||||
long now = new Date().getTime();
|
||||
while (!Timers.isEmpty()) {
|
||||
long k = Timers.firstKey();
|
||||
if (k > now)
|
||||
break;
|
||||
|
||||
ArrayList<Long> callbacks = Timers.get(k);
|
||||
Timers.remove(k);
|
||||
|
||||
// Fire all timers at this timestamp
|
||||
ListIterator<Long> iter = callbacks.listIterator(0);
|
||||
while (iter.hasNext()) {
|
||||
eventCallback (0, EM_TIMER_FIRED, null, iter.next().longValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long installOneshotTimer (long milliseconds) {
|
||||
long s = createBinding();
|
||||
long deadline = new Date().getTime() + milliseconds;
|
||||
|
||||
if (Timers.containsKey(deadline)) {
|
||||
Timers.get(deadline).add(s);
|
||||
} else {
|
||||
ArrayList<Long> callbacks = new ArrayList<Long>();
|
||||
callbacks.add(s);
|
||||
Timers.put(deadline, callbacks);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public long startTcpServer (SocketAddress sa) throws EmReactorException {
|
||||
try {
|
||||
ServerSocketChannel server = ServerSocketChannel.open();
|
||||
server.configureBlocking(false);
|
||||
server.socket().bind (sa);
|
||||
long s = createBinding();
|
||||
Acceptors.put(s, server);
|
||||
server.register(mySelector, SelectionKey.OP_ACCEPT, s);
|
||||
return s;
|
||||
} catch (IOException e) {
|
||||
throw new EmReactorException ("unable to open socket acceptor: " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public long startTcpServer (String address, int port) throws EmReactorException {
|
||||
return startTcpServer (new InetSocketAddress (address, port));
|
||||
}
|
||||
|
||||
public void stopTcpServer (long signature) throws IOException {
|
||||
ServerSocketChannel server = Acceptors.remove(signature);
|
||||
if (server != null)
|
||||
server.close();
|
||||
else
|
||||
throw new RuntimeException ("failed to close unknown acceptor");
|
||||
}
|
||||
|
||||
public long openUdpSocket (InetSocketAddress address) throws IOException {
|
||||
// TODO, don't throw an exception out of here.
|
||||
DatagramChannel dg = DatagramChannel.open();
|
||||
dg.configureBlocking(false);
|
||||
dg.socket().bind(address);
|
||||
long b = createBinding();
|
||||
EventableChannel ec = new EventableDatagramChannel (dg, b, mySelector);
|
||||
dg.register(mySelector, SelectionKey.OP_READ, ec);
|
||||
Connections.put(b, ec);
|
||||
return b;
|
||||
}
|
||||
|
||||
public long openUdpSocket (String address, int port) throws IOException {
|
||||
return openUdpSocket (new InetSocketAddress (address, port));
|
||||
}
|
||||
|
||||
public void sendData (long sig, ByteBuffer bb) throws IOException {
|
||||
Connections.get(sig).scheduleOutboundData( bb );
|
||||
}
|
||||
|
||||
public void sendData (long sig, byte[] data) throws IOException {
|
||||
sendData (sig, ByteBuffer.wrap(data));
|
||||
}
|
||||
|
||||
public void setCommInactivityTimeout (long sig, long mills) {
|
||||
Connections.get(sig).setCommInactivityTimeout (mills);
|
||||
}
|
||||
|
||||
public void sendDatagram (long sig, byte[] data, int length, String recipAddress, int recipPort) {
|
||||
sendDatagram (sig, ByteBuffer.wrap(data), recipAddress, recipPort);
|
||||
}
|
||||
|
||||
public void sendDatagram (long sig, ByteBuffer bb, String recipAddress, int recipPort) {
|
||||
(Connections.get(sig)).scheduleOutboundDatagram( bb, recipAddress, recipPort);
|
||||
}
|
||||
|
||||
public long connectTcpServer (String address, int port) {
|
||||
return connectTcpServer(null, 0, address, port);
|
||||
}
|
||||
|
||||
public long connectTcpServer (String bindAddr, int bindPort, String address, int port) {
|
||||
long b = createBinding();
|
||||
|
||||
try {
|
||||
SocketChannel sc = SocketChannel.open();
|
||||
sc.configureBlocking(false);
|
||||
if (bindAddr != null)
|
||||
sc.socket().bind(new InetSocketAddress (bindAddr, bindPort));
|
||||
|
||||
EventableSocketChannel ec = new EventableSocketChannel (sc, b, mySelector);
|
||||
|
||||
if (sc.connect (new InetSocketAddress (address, port))) {
|
||||
// Connection returned immediately. Can happen with localhost connections.
|
||||
// WARNING, this code is untested due to lack of available test conditions.
|
||||
// Ought to be be able to come here from a localhost connection, but that
|
||||
// doesn't happen on Linux. (Maybe on FreeBSD?)
|
||||
// The reason for not handling this until we can test it is that we
|
||||
// really need to return from this function WITHOUT triggering any EM events.
|
||||
// That's because until the user code has seen the signature we generated here,
|
||||
// it won't be able to properly dispatch them. The C++ EM deals with this
|
||||
// by setting pending mode as a flag in ALL eventable descriptors and making
|
||||
// the descriptor select for writable. Then, it can send UNBOUND and
|
||||
// CONNECTION_COMPLETED on the next pass through the loop, because writable will
|
||||
// fire.
|
||||
throw new RuntimeException ("immediate-connect unimplemented");
|
||||
}
|
||||
else {
|
||||
ec.setConnectPending();
|
||||
Connections.put (b, ec);
|
||||
NewConnections.add (b);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Can theoretically come here if a connect failure can be determined immediately.
|
||||
// I don't know how to make that happen for testing purposes.
|
||||
throw new RuntimeException ("immediate-connect unimplemented: " + e.toString());
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
public void closeConnection (long sig, boolean afterWriting) {
|
||||
EventableChannel ec = Connections.get(sig);
|
||||
if (ec != null)
|
||||
if (ec.scheduleClose (afterWriting))
|
||||
UnboundConnections.add (sig);
|
||||
}
|
||||
|
||||
long createBinding() {
|
||||
return ++BindingIndex;
|
||||
}
|
||||
|
||||
public void signalLoopbreak() {
|
||||
loopBreaker.set(true);
|
||||
if (mySelector != null)
|
||||
mySelector.wakeup();
|
||||
}
|
||||
|
||||
public void startTls (long sig) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
Connections.get(sig).startTls();
|
||||
}
|
||||
|
||||
public void setTimerQuantum (int mills) {
|
||||
if (mills < 5 || mills > 2500)
|
||||
throw new RuntimeException ("attempt to set invalid timer-quantum value: "+mills);
|
||||
timerQuantum = mills;
|
||||
}
|
||||
|
||||
public Object[] getPeerName (long sig) {
|
||||
EventableChannel channel = Connections.get(sig);
|
||||
if (channel != null) {
|
||||
return Connections.get(sig).getPeerName();
|
||||
}
|
||||
else {
|
||||
ServerSocketChannel acceptor = Acceptors.get(sig);
|
||||
return new Object[] { acceptor.socket().getLocalPort(),
|
||||
acceptor.socket().getInetAddress().getHostAddress() };
|
||||
}
|
||||
}
|
||||
|
||||
public Object[] getSockName (long sig) {
|
||||
EventableChannel channel = Connections.get(sig);
|
||||
if (channel != null) {
|
||||
return Connections.get(sig).getSockName();
|
||||
}
|
||||
else {
|
||||
ServerSocketChannel acceptor = Acceptors.get(sig);
|
||||
return new Object[] { acceptor.socket().getLocalPort(),
|
||||
acceptor.socket().getInetAddress().getHostAddress() };
|
||||
}
|
||||
}
|
||||
|
||||
public long attachChannel (SocketChannel sc, boolean watch_mode) {
|
||||
long b = createBinding();
|
||||
|
||||
EventableSocketChannel ec = new EventableSocketChannel (sc, b, mySelector);
|
||||
|
||||
ec.setAttached();
|
||||
if (watch_mode)
|
||||
ec.setWatchOnly();
|
||||
|
||||
Connections.put (b, ec);
|
||||
NewConnections.add (b);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
public SocketChannel detachChannel (long sig) {
|
||||
EventableSocketChannel ec = (EventableSocketChannel) Connections.get (sig);
|
||||
if (ec != null) {
|
||||
UnboundConnections.add (sig);
|
||||
return ec.getChannel();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setNotifyReadable (long sig, boolean mode) {
|
||||
((EventableSocketChannel) Connections.get(sig)).setNotifyReadable(mode);
|
||||
}
|
||||
|
||||
public void setNotifyWritable (long sig, boolean mode) {
|
||||
((EventableSocketChannel) Connections.get(sig)).setNotifyWritable(mode);
|
||||
}
|
||||
|
||||
public boolean isNotifyReadable (long sig) {
|
||||
return Connections.get(sig).isNotifyReadable();
|
||||
}
|
||||
|
||||
public boolean isNotifyWritable (long sig) {
|
||||
return Connections.get(sig).isNotifyWritable();
|
||||
}
|
||||
|
||||
public boolean pauseConnection (long sig) {
|
||||
return ((EventableSocketChannel) Connections.get(sig)).pause();
|
||||
}
|
||||
|
||||
public boolean resumeConnection (long sig) {
|
||||
return ((EventableSocketChannel) Connections.get(sig)).resume();
|
||||
}
|
||||
|
||||
public boolean isConnectionPaused (long sig) {
|
||||
return ((EventableSocketChannel) Connections.get(sig)).isPaused();
|
||||
}
|
||||
|
||||
public long getOutboundDataSize (long sig) {
|
||||
return Connections.get(sig).getOutboundDataSize();
|
||||
}
|
||||
|
||||
public int getConnectionCount() {
|
||||
return Connections.size() + Acceptors.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* $Id$
|
||||
*
|
||||
* Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
* Homepage:: http://rubyeventmachine.com
|
||||
* Date:: 15 Jul 2007
|
||||
*
|
||||
* See EventMachine and EventMachine::Connection for documentation and
|
||||
* usage examples.
|
||||
*
|
||||
*
|
||||
*----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
* Gmail: blackhedd
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of either: 1) the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version; or 2) Ruby's License.
|
||||
*
|
||||
* See the file COPYING for complete licensing information.
|
||||
*
|
||||
*---------------------------------------------------------------------------
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package com.rubyeventmachine;
|
||||
|
||||
/**
|
||||
* @author francis
|
||||
*
|
||||
*/
|
||||
public class EmReactorException extends Exception {
|
||||
static final long serialVersionUID = 0;
|
||||
public EmReactorException (String msg) {
|
||||
super (msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* $Id$
|
||||
*
|
||||
* Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
* Homepage:: http://rubyeventmachine.com
|
||||
* Date:: 15 Jul 2007
|
||||
*
|
||||
* See EventMachine and EventMachine::Connection for documentation and
|
||||
* usage examples.
|
||||
*
|
||||
*
|
||||
*----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
* Gmail: blackhedd
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of either: 1) the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version; or 2) Ruby's License.
|
||||
*
|
||||
* See the file COPYING for complete licensing information.
|
||||
*
|
||||
*---------------------------------------------------------------------------
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package com.rubyeventmachine;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
|
||||
public interface EventableChannel {
|
||||
|
||||
public void scheduleOutboundData (ByteBuffer bb);
|
||||
|
||||
public void scheduleOutboundDatagram (ByteBuffer bb, String recipAddress, int recipPort);
|
||||
|
||||
public boolean scheduleClose (boolean afterWriting);
|
||||
|
||||
public void startTls();
|
||||
|
||||
public long getBinding();
|
||||
|
||||
public void readInboundData (ByteBuffer dst) throws IOException;
|
||||
|
||||
public void register() throws ClosedChannelException;
|
||||
|
||||
/**
|
||||
* This is called by the reactor after it finishes running.
|
||||
* The idea is to free network resources.
|
||||
*/
|
||||
public void close();
|
||||
|
||||
public boolean writeOutboundData() throws IOException;
|
||||
|
||||
public long getOutboundDataSize();
|
||||
|
||||
public void setCommInactivityTimeout (long seconds);
|
||||
|
||||
public Object[] getPeerName();
|
||||
public Object[] getSockName();
|
||||
|
||||
public boolean isWatchOnly();
|
||||
|
||||
public boolean isNotifyReadable();
|
||||
public boolean isNotifyWritable();
|
||||
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* $Id$
|
||||
*
|
||||
* Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
* Homepage:: http://rubyeventmachine.com
|
||||
* Date:: 15 Jul 2007
|
||||
*
|
||||
* See EventMachine and EventMachine::Connection for documentation and
|
||||
* usage examples.
|
||||
*
|
||||
*
|
||||
*----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
* Gmail: blackhedd
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of either: 1) the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version; or 2) Ruby's License.
|
||||
*
|
||||
* See the file COPYING for complete licensing information.
|
||||
*
|
||||
*---------------------------------------------------------------------------
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package com.rubyeventmachine;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.util.LinkedList;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
public class EventableDatagramChannel implements EventableChannel {
|
||||
|
||||
class Packet {
|
||||
public ByteBuffer bb;
|
||||
public SocketAddress recipient;
|
||||
public Packet (ByteBuffer _bb, SocketAddress _recipient) {
|
||||
bb = _bb;
|
||||
recipient = _recipient;
|
||||
}
|
||||
}
|
||||
|
||||
DatagramChannel channel;
|
||||
long binding;
|
||||
Selector selector;
|
||||
boolean bCloseScheduled;
|
||||
LinkedList<Packet> outboundQ;
|
||||
long outboundS;
|
||||
SocketAddress returnAddress;
|
||||
|
||||
|
||||
public EventableDatagramChannel (DatagramChannel dc, long _binding, Selector sel) throws ClosedChannelException {
|
||||
channel = dc;
|
||||
binding = _binding;
|
||||
selector = sel;
|
||||
bCloseScheduled = false;
|
||||
outboundQ = new LinkedList<Packet>();
|
||||
outboundS = 0;
|
||||
|
||||
dc.register(selector, SelectionKey.OP_READ, this);
|
||||
}
|
||||
|
||||
public void scheduleOutboundData (ByteBuffer bb) {
|
||||
try {
|
||||
if ((!bCloseScheduled) && (bb.remaining() > 0)) {
|
||||
outboundQ.addLast(new Packet(bb, returnAddress));
|
||||
outboundS += bb.remaining();
|
||||
channel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this);
|
||||
}
|
||||
} catch (ClosedChannelException e) {
|
||||
throw new RuntimeException ("no outbound data");
|
||||
}
|
||||
}
|
||||
|
||||
public void scheduleOutboundDatagram (ByteBuffer bb, String recipAddress, int recipPort) {
|
||||
try {
|
||||
if ((!bCloseScheduled) && (bb.remaining() > 0)) {
|
||||
outboundQ.addLast(new Packet (bb, new InetSocketAddress (recipAddress, recipPort)));
|
||||
outboundS += bb.remaining();
|
||||
channel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, this);
|
||||
}
|
||||
} catch (ClosedChannelException e) {
|
||||
throw new RuntimeException ("no outbound data");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean scheduleClose (boolean afterWriting) {
|
||||
System.out.println ("NOT SCHEDULING CLOSE ON DATAGRAM");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void startTls() {
|
||||
throw new RuntimeException ("TLS is unimplemented on this Channel");
|
||||
}
|
||||
|
||||
public long getBinding() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
public void register() throws ClosedChannelException {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate with extreme prejudice. Don't assume there will be another pass through
|
||||
* the reactor core.
|
||||
*/
|
||||
public void close() {
|
||||
try {
|
||||
channel.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
public void readInboundData (ByteBuffer dst) {
|
||||
returnAddress = null;
|
||||
try {
|
||||
// If there is no datagram available (we're nonblocking after all),
|
||||
// then channel.receive returns null.
|
||||
returnAddress = channel.receive(dst);
|
||||
} catch (IOException e) {
|
||||
// probably a no-op. The caller will see the empty (or even partial) buffer
|
||||
// and presumably do the right thing.
|
||||
}
|
||||
}
|
||||
|
||||
public boolean writeOutboundData() {
|
||||
while (!outboundQ.isEmpty()) {
|
||||
Packet p = outboundQ.getFirst();
|
||||
int written = 0;
|
||||
try {
|
||||
// With a datagram socket, it's ok to send an empty buffer.
|
||||
written = channel.send(p.bb, p.recipient);
|
||||
outboundS -= written;
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Did we consume the whole outbound buffer? If yes, pop it off and
|
||||
* keep looping. If no, the outbound network buffers are full, so break
|
||||
* out of here. There's a flaw that affects outbound buffers that are intentionally
|
||||
* empty. We can tell whether they got sent or not. So we assume they were.
|
||||
* TODO: As implemented, this ALWAYS discards packets if they were at least
|
||||
* partially written. This matches the behavior of the C++ EM. My judgment
|
||||
* is that this is less surprising than fragmenting the data and sending multiple
|
||||
* packets would be. I could be wrong, so this is subject to change.
|
||||
*/
|
||||
|
||||
if ((written > 0) || (p.bb.remaining() == 0))
|
||||
outboundQ.removeFirst();
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (outboundQ.isEmpty()) {
|
||||
try {
|
||||
channel.register(selector, SelectionKey.OP_READ, this);
|
||||
} catch (ClosedChannelException e) {}
|
||||
}
|
||||
|
||||
// ALWAYS drain the outbound queue before triggering a connection close.
|
||||
// If anyone wants to close immediately, they're responsible for clearing
|
||||
// the outbound queue.
|
||||
return (bCloseScheduled && outboundQ.isEmpty()) ? false : true;
|
||||
}
|
||||
|
||||
public void setCommInactivityTimeout (long seconds) {
|
||||
// TODO
|
||||
System.out.println ("DATAGRAM: SET COMM INACTIVITY UNIMPLEMENTED " + seconds);
|
||||
}
|
||||
|
||||
public Object[] getPeerName () {
|
||||
if (returnAddress != null) {
|
||||
InetSocketAddress inetAddr = (InetSocketAddress) returnAddress;
|
||||
return new Object[]{ inetAddr.getPort(), inetAddr.getHostName() };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Object[] getSockName () {
|
||||
DatagramSocket socket = channel.socket();
|
||||
return new Object[]{ socket.getLocalPort(),
|
||||
socket.getLocalAddress().getHostAddress() };
|
||||
}
|
||||
|
||||
public boolean isWatchOnly() { return false; }
|
||||
public boolean isNotifyReadable() { return false; }
|
||||
public boolean isNotifyWritable() { return false; }
|
||||
public long getOutboundDataSize() { return outboundS; }
|
||||
}
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* $Id$
|
||||
*
|
||||
* Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
* Homepage:: http://rubyeventmachine.com
|
||||
* Date:: 15 Jul 2007
|
||||
*
|
||||
* See EventMachine and EventMachine::Connection for documentation and
|
||||
* usage examples.
|
||||
*
|
||||
*
|
||||
*----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
* Gmail: blackhedd
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of either: 1) the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version; or 2) Ruby's License.
|
||||
*
|
||||
* See the file COPYING for complete licensing information.
|
||||
*
|
||||
*---------------------------------------------------------------------------
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
package com.rubyeventmachine;
|
||||
|
||||
/**
|
||||
* @author francis
|
||||
*
|
||||
*/
|
||||
|
||||
import java.nio.channels.*;
|
||||
import java.nio.*;
|
||||
import java.util.*;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import javax.net.ssl.*;
|
||||
import javax.net.ssl.SSLEngineResult.*;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import java.security.*;
|
||||
|
||||
public class EventableSocketChannel implements EventableChannel {
|
||||
Selector selector;
|
||||
SelectionKey channelKey;
|
||||
SocketChannel channel;
|
||||
|
||||
long binding;
|
||||
LinkedList<ByteBuffer> outboundQ;
|
||||
long outboundS;
|
||||
|
||||
boolean bCloseScheduled;
|
||||
boolean bConnectPending;
|
||||
boolean bWatchOnly;
|
||||
boolean bAttached;
|
||||
boolean bNotifyReadable;
|
||||
boolean bNotifyWritable;
|
||||
boolean bPaused;
|
||||
|
||||
SSLEngine sslEngine;
|
||||
SSLContext sslContext;
|
||||
|
||||
public EventableSocketChannel (SocketChannel sc, long _binding, Selector sel) {
|
||||
channel = sc;
|
||||
binding = _binding;
|
||||
selector = sel;
|
||||
bCloseScheduled = false;
|
||||
bConnectPending = false;
|
||||
bWatchOnly = false;
|
||||
bAttached = false;
|
||||
bNotifyReadable = false;
|
||||
bNotifyWritable = false;
|
||||
outboundQ = new LinkedList<ByteBuffer>();
|
||||
outboundS = 0;
|
||||
}
|
||||
|
||||
public long getBinding() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
public SocketChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void register() throws ClosedChannelException {
|
||||
if (channelKey == null) {
|
||||
int events = currentEvents();
|
||||
channelKey = channel.register(selector, events, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate with extreme prejudice. Don't assume there will be another pass through
|
||||
* the reactor core.
|
||||
*/
|
||||
public void close() {
|
||||
if (channelKey != null) {
|
||||
channelKey.cancel();
|
||||
channelKey = null;
|
||||
}
|
||||
|
||||
if (bAttached) {
|
||||
// attached channels are copies, so reset the file descriptor to prevent java from close()ing it
|
||||
Field f;
|
||||
FileDescriptor fd;
|
||||
|
||||
try {
|
||||
/* do _NOT_ clobber fdVal here, it will break epoll/kqueue on jdk6!
|
||||
* channelKey.cancel() above does not occur until the next call to select
|
||||
* and if fdVal is gone, we will continue to get events for this fd.
|
||||
*
|
||||
* instead, remove fdVal in cleanup(), which is processed via DetachedConnections,
|
||||
* after UnboundConnections but before NewConnections.
|
||||
*/
|
||||
|
||||
f = channel.getClass().getDeclaredField("fd");
|
||||
f.setAccessible(true);
|
||||
fd = (FileDescriptor) f.get(channel);
|
||||
|
||||
f = fd.getClass().getDeclaredField("fd");
|
||||
f.setAccessible(true);
|
||||
f.set(fd, -1);
|
||||
} catch (java.lang.NoSuchFieldException e) {
|
||||
e.printStackTrace();
|
||||
} catch (java.lang.IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
channel.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (bAttached) {
|
||||
Field f;
|
||||
try {
|
||||
f = channel.getClass().getDeclaredField("fdVal");
|
||||
f.setAccessible(true);
|
||||
f.set(channel, -1);
|
||||
} catch (java.lang.NoSuchFieldException e) {
|
||||
e.printStackTrace();
|
||||
} catch (java.lang.IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
channel = null;
|
||||
}
|
||||
|
||||
public void scheduleOutboundData (ByteBuffer bb) {
|
||||
if (!bCloseScheduled && bb.remaining() > 0) {
|
||||
if (sslEngine != null) {
|
||||
try {
|
||||
ByteBuffer b = ByteBuffer.allocate(32*1024); // TODO, preallocate this buffer.
|
||||
sslEngine.wrap(bb, b);
|
||||
b.flip();
|
||||
outboundQ.addLast(b);
|
||||
outboundS += b.remaining();
|
||||
} catch (SSLException e) {
|
||||
throw new RuntimeException ("ssl error");
|
||||
}
|
||||
}
|
||||
else {
|
||||
outboundQ.addLast(bb);
|
||||
outboundS += bb.remaining();
|
||||
}
|
||||
|
||||
updateEvents();
|
||||
}
|
||||
}
|
||||
|
||||
public void scheduleOutboundDatagram (ByteBuffer bb, String recipAddress, int recipPort) {
|
||||
throw new RuntimeException ("datagram sends not supported on this channel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the reactor when we have selected readable.
|
||||
*/
|
||||
public void readInboundData (ByteBuffer bb) throws IOException {
|
||||
if (channel.read(bb) == -1)
|
||||
throw new IOException ("eof");
|
||||
}
|
||||
|
||||
public long getOutboundDataSize() { return outboundS; }
|
||||
|
||||
/**
|
||||
* Called by the reactor when we have selected writable.
|
||||
* Return false to indicate an error that should cause the connection to close.
|
||||
* TODO, VERY IMPORTANT: we're here because we selected writable, but it's always
|
||||
* possible to become unwritable between the poll and when we get here. The way
|
||||
* this code is written, we're depending on a nonblocking write NOT TO CONSUME
|
||||
* the whole outbound buffer in this case, rather than firing an exception.
|
||||
* We should somehow verify that this is indeed Java's defined behavior.
|
||||
* @return
|
||||
*/
|
||||
public boolean writeOutboundData() throws IOException {
|
||||
ByteBuffer[] bufs = new ByteBuffer[64];
|
||||
int i;
|
||||
long written, toWrite;
|
||||
while (!outboundQ.isEmpty()) {
|
||||
i = 0;
|
||||
toWrite = 0;
|
||||
written = 0;
|
||||
while (i < 64 && !outboundQ.isEmpty()) {
|
||||
bufs[i] = outboundQ.removeFirst();
|
||||
toWrite += bufs[i].remaining();
|
||||
i++;
|
||||
}
|
||||
if (toWrite > 0)
|
||||
written = channel.write(bufs, 0, i);
|
||||
|
||||
outboundS -= written;
|
||||
// Did we consume the whole outbound buffer? If yes,
|
||||
// pop it off and keep looping. If no, the outbound network
|
||||
// buffers are full, so break out of here.
|
||||
if (written < toWrite) {
|
||||
while (i > 0 && bufs[i-1].remaining() > 0) {
|
||||
outboundQ.addFirst(bufs[i-1]);
|
||||
i--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (outboundQ.isEmpty() && !bCloseScheduled) {
|
||||
updateEvents();
|
||||
}
|
||||
|
||||
// ALWAYS drain the outbound queue before triggering a connection close.
|
||||
// If anyone wants to close immediately, they're responsible for clearing
|
||||
// the outbound queue.
|
||||
return (bCloseScheduled && outboundQ.isEmpty()) ? false : true;
|
||||
}
|
||||
|
||||
public void setConnectPending() {
|
||||
bConnectPending = true;
|
||||
updateEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the reactor when we have selected connectable.
|
||||
* Return false to indicate an error that should cause the connection to close.
|
||||
*/
|
||||
public boolean finishConnecting() throws IOException {
|
||||
channel.finishConnect();
|
||||
|
||||
bConnectPending = false;
|
||||
updateEvents();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean scheduleClose (boolean afterWriting) {
|
||||
// TODO: What the hell happens here if bConnectPending is set?
|
||||
if (!afterWriting) {
|
||||
outboundQ.clear();
|
||||
outboundS = 0;
|
||||
}
|
||||
|
||||
if (outboundQ.isEmpty())
|
||||
return true;
|
||||
else {
|
||||
updateEvents();
|
||||
bCloseScheduled = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void startTls() {
|
||||
if (sslEngine == null) {
|
||||
try {
|
||||
sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, null, null); // TODO, fill in the parameters.
|
||||
sslEngine = sslContext.createSSLEngine(); // TODO, should use the parameterized version, to get Kerb stuff and session re-use.
|
||||
sslEngine.setUseClientMode(false);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException ("unable to start TLS"); // TODO, get rid of this.
|
||||
} catch (KeyManagementException e) {
|
||||
throw new RuntimeException ("unable to start TLS"); // TODO, get rid of this.
|
||||
}
|
||||
}
|
||||
System.out.println ("Starting TLS");
|
||||
}
|
||||
|
||||
public ByteBuffer dispatchInboundData (ByteBuffer bb) throws SSLException {
|
||||
if (sslEngine != null) {
|
||||
if (true) throw new RuntimeException ("TLS currently unimplemented");
|
||||
System.setProperty("javax.net.debug", "all");
|
||||
ByteBuffer w = ByteBuffer.allocate(32*1024); // TODO, WRONG, preallocate this buffer.
|
||||
SSLEngineResult res = sslEngine.unwrap(bb, w);
|
||||
if (res.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
|
||||
Runnable r;
|
||||
while ((r = sslEngine.getDelegatedTask()) != null) {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
System.out.println (bb);
|
||||
w.flip();
|
||||
return w;
|
||||
}
|
||||
else
|
||||
return bb;
|
||||
}
|
||||
|
||||
public void setCommInactivityTimeout (long seconds) {
|
||||
// TODO
|
||||
System.out.println ("SOCKET: SET COMM INACTIVITY UNIMPLEMENTED " + seconds);
|
||||
}
|
||||
|
||||
public Object[] getPeerName () {
|
||||
Socket sock = channel.socket();
|
||||
return new Object[]{ sock.getPort(), sock.getInetAddress().getHostAddress() };
|
||||
}
|
||||
|
||||
public Object[] getSockName () {
|
||||
Socket sock = channel.socket();
|
||||
return new Object[]{ sock.getLocalPort(),
|
||||
sock.getLocalAddress().getHostAddress() };
|
||||
}
|
||||
|
||||
public void setWatchOnly() {
|
||||
bWatchOnly = true;
|
||||
updateEvents();
|
||||
}
|
||||
public boolean isWatchOnly() { return bWatchOnly; }
|
||||
|
||||
public void setAttached() {
|
||||
bAttached = true;
|
||||
}
|
||||
public boolean isAttached() { return bAttached; }
|
||||
|
||||
public void setNotifyReadable (boolean mode) {
|
||||
bNotifyReadable = mode;
|
||||
updateEvents();
|
||||
}
|
||||
public boolean isNotifyReadable() { return bNotifyReadable; }
|
||||
|
||||
public void setNotifyWritable (boolean mode) {
|
||||
bNotifyWritable = mode;
|
||||
updateEvents();
|
||||
}
|
||||
public boolean isNotifyWritable() { return bNotifyWritable; }
|
||||
|
||||
public boolean pause() {
|
||||
if (bWatchOnly) {
|
||||
throw new RuntimeException ("cannot pause/resume 'watch only' connections, set notify readable/writable instead");
|
||||
}
|
||||
boolean old = bPaused;
|
||||
bPaused = true;
|
||||
updateEvents();
|
||||
return !old;
|
||||
}
|
||||
|
||||
public boolean resume() {
|
||||
if (bWatchOnly) {
|
||||
throw new RuntimeException ("cannot pause/resume 'watch only' connections, set notify readable/writable instead");
|
||||
}
|
||||
boolean old = bPaused;
|
||||
bPaused = false;
|
||||
updateEvents();
|
||||
return old;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return bPaused;
|
||||
}
|
||||
|
||||
private void updateEvents() {
|
||||
if (channelKey == null)
|
||||
return;
|
||||
|
||||
int events = currentEvents();
|
||||
|
||||
if (channelKey.interestOps() != events) {
|
||||
channelKey.interestOps(events);
|
||||
}
|
||||
}
|
||||
|
||||
private int currentEvents() {
|
||||
int events = 0;
|
||||
|
||||
if (bWatchOnly)
|
||||
{
|
||||
if (bNotifyReadable)
|
||||
events |= SelectionKey.OP_READ;
|
||||
|
||||
if (bNotifyWritable)
|
||||
events |= SelectionKey.OP_WRITE;
|
||||
}
|
||||
else if (!bPaused)
|
||||
{
|
||||
if (bConnectPending)
|
||||
events |= SelectionKey.OP_CONNECT;
|
||||
else {
|
||||
events |= SelectionKey.OP_READ;
|
||||
|
||||
if (!outboundQ.isEmpty())
|
||||
events |= SelectionKey.OP_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# BufferedTokenizer takes a delimiter upon instantiation, or acts line-based
|
||||
# by default. It allows input to be spoon-fed from some outside source which
|
||||
# receives arbitrary length datagrams which may-or-may-not contain the token
|
||||
# by which entities are delimited. In this respect it's ideally paired with
|
||||
# something like EventMachine (http://rubyeventmachine.com/).
|
||||
class BufferedTokenizer
|
||||
# New BufferedTokenizers will operate on lines delimited by a delimiter,
|
||||
# which is by default the global input delimiter $/ ("\n").
|
||||
#
|
||||
# The input buffer is stored as an array. This is by far the most efficient
|
||||
# approach given language constraints (in C a linked list would be a more
|
||||
# appropriate data structure). Segments of input data are stored in a list
|
||||
# which is only joined when a token is reached, substantially reducing the
|
||||
# number of objects required for the operation.
|
||||
def initialize(delimiter = $/)
|
||||
@delimiter = delimiter
|
||||
@input = []
|
||||
@tail = ''
|
||||
@trim = @delimiter.length - 1
|
||||
end
|
||||
|
||||
# Extract takes an arbitrary string of input data and returns an array of
|
||||
# tokenized entities, provided there were any available to extract. This
|
||||
# makes for easy processing of datagrams using a pattern like:
|
||||
#
|
||||
# tokenizer.extract(data).map { |entity| Decode(entity) }.each do ...
|
||||
#
|
||||
# Using -1 makes split to return "" if the token is at the end of
|
||||
# the string, meaning the last element is the start of the next chunk.
|
||||
def extract(data)
|
||||
if @trim > 0
|
||||
tail_end = @tail.slice!(-@trim, @trim) # returns nil if string is too short
|
||||
data = tail_end + data if tail_end
|
||||
end
|
||||
|
||||
@input << @tail
|
||||
entities = data.split(@delimiter, -1)
|
||||
@tail = entities.shift
|
||||
|
||||
unless entities.empty?
|
||||
@input << @tail
|
||||
entities.unshift @input.join
|
||||
@input.clear
|
||||
@tail = entities.pop
|
||||
end
|
||||
|
||||
entities
|
||||
end
|
||||
|
||||
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
||||
# a token has not yet been encountered
|
||||
def flush
|
||||
@input << @tail
|
||||
buffer = @input.join
|
||||
@input.clear
|
||||
@tail = "" # @tail.clear is slightly faster, but not supported on 1.8.7
|
||||
buffer
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
module EventMachine
|
||||
# Utility method for coercing arguments to an object that responds to :call.
|
||||
# Accepts an object and a method name to send to, or a block, or an object
|
||||
# that responds to :call.
|
||||
#
|
||||
# @example EventMachine.Callback used with a block. Returns that block.
|
||||
#
|
||||
# cb = EventMachine.Callback do |msg|
|
||||
# puts(msg)
|
||||
# end
|
||||
# # returned object is a callable
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @example EventMachine.Callback used with an object (to be more specific, class object) and a method name, returns an object that responds to #call
|
||||
#
|
||||
# cb = EventMachine.Callback(Object, :puts)
|
||||
# # returned object is a callable that delegates to Kernel#puts (in this case Object.puts)
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @example EventMachine.Callback used with an object that responds to #call. Returns the argument.
|
||||
#
|
||||
# cb = EventMachine.Callback(proc{ |msg| puts(msg) })
|
||||
# # returned object is a callable
|
||||
# cb.call('hello world')
|
||||
#
|
||||
#
|
||||
# @overload Callback(object, method)
|
||||
# Wraps `method` invocation on `object` into an object that responds to #call that proxies all the arguments to that method
|
||||
# @param [Object] Object to invoke method on
|
||||
# @param [Symbol] Method name
|
||||
# @return [<#call>] An object that responds to #call that takes any number of arguments and invokes method on object with those arguments
|
||||
#
|
||||
# @overload Callback(object)
|
||||
# Returns callable object as is, without any coercion
|
||||
# @param [<#call>] An object that responds to #call
|
||||
# @return [<#call>] Its argument
|
||||
#
|
||||
# @overload Callback(&block)
|
||||
# Returns block passed to it without any coercion
|
||||
# @return [<#call>] Block passed to this method
|
||||
#
|
||||
# @raise [ArgumentError] When argument doesn't respond to #call, method name is missing or when invoked without arguments and block isn't given
|
||||
#
|
||||
# @return [<#call>]
|
||||
def self.Callback(object = nil, method = nil, &blk)
|
||||
if object && method
|
||||
lambda { |*args| object.__send__ method, *args }
|
||||
else
|
||||
if object.respond_to? :call
|
||||
object
|
||||
else
|
||||
blk || raise(ArgumentError)
|
||||
end # if
|
||||
end # if
|
||||
end # self.Callback
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,69 @@
|
||||
module EventMachine
|
||||
# Provides a simple thread-safe way to transfer data between (typically) long running
|
||||
# tasks in {EventMachine.defer} and event loop thread.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# channel = EventMachine::Channel.new
|
||||
# sid = channel.subscribe { |msg| p [:got, msg] }
|
||||
#
|
||||
# channel.push('hello world')
|
||||
# channel.unsubscribe(sid)
|
||||
#
|
||||
#
|
||||
class Channel
|
||||
def initialize
|
||||
@subs = {}
|
||||
@uid = 0
|
||||
end
|
||||
|
||||
# Return the number of current subscribers.
|
||||
def num_subscribers
|
||||
return @subs.size
|
||||
end
|
||||
|
||||
# Takes any arguments suitable for EM::Callback() and returns a subscriber
|
||||
# id for use when unsubscribing.
|
||||
#
|
||||
# @return [Integer] Subscribe identifier
|
||||
# @see #unsubscribe
|
||||
def subscribe(*a, &b)
|
||||
name = gen_id
|
||||
EM.schedule { @subs[name] = EM::Callback(*a, &b) }
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
# Removes subscriber from the list.
|
||||
#
|
||||
# @param [Integer] Subscriber identifier
|
||||
# @see #subscribe
|
||||
def unsubscribe(name)
|
||||
EM.schedule { @subs.delete name }
|
||||
end
|
||||
|
||||
# Add items to the channel, which are pushed out to all subscribers.
|
||||
def push(*items)
|
||||
items = items.dup
|
||||
EM.schedule { items.each { |i| @subs.values.each { |s| s.call i } } }
|
||||
end
|
||||
alias << push
|
||||
|
||||
# Fetches one message from the channel.
|
||||
def pop(*a, &b)
|
||||
EM.schedule {
|
||||
name = subscribe do |*args|
|
||||
unsubscribe(name)
|
||||
EM::Callback(*a, &b).call(*args)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @private
|
||||
def gen_id
|
||||
@uid += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,304 @@
|
||||
# = EM::Completion
|
||||
#
|
||||
# A completion is a callback container for various states of completion. In
|
||||
# its most basic form it has a start state and a finish state.
|
||||
#
|
||||
# This implementation includes some hold-back from the EM::Deferrable
|
||||
# interface in order to be compatible - but it has a much cleaner
|
||||
# implementation.
|
||||
#
|
||||
# In general it is preferred that this implementation be used as a state
|
||||
# callback container than EM::DefaultDeferrable or other classes including
|
||||
# EM::Deferrable. This is because it is generally more sane to keep this level
|
||||
# of state in a dedicated state-back container. This generally leads to more
|
||||
# malleable interfaces and software designs, as well as eradicating nasty bugs
|
||||
# that result from abstraction leakage.
|
||||
#
|
||||
# == Basic Usage
|
||||
#
|
||||
# As already mentioned, the basic usage of a Completion is simply for its two
|
||||
# final states, :succeeded and :failed.
|
||||
#
|
||||
# An asynchronous operation will complete at some future point in time, and
|
||||
# users often want to react to this event. API authors will want to expose
|
||||
# some common interface to react to these events.
|
||||
#
|
||||
# In the following example, the user wants to know when a short lived
|
||||
# connection has completed its exchange with the remote server. The simple
|
||||
# protocol just waits for an ack to its message.
|
||||
#
|
||||
# class Protocol < EM::Connection
|
||||
# include EM::P::LineText2
|
||||
#
|
||||
# def initialize(message, completion)
|
||||
# @message, @completion = message, completion
|
||||
# @completion.completion { close_connection }
|
||||
# @completion.timeout(1, :timeout)
|
||||
# end
|
||||
#
|
||||
# def post_init
|
||||
# send_data(@message)
|
||||
# end
|
||||
#
|
||||
# def receive_line(line)
|
||||
# case line
|
||||
# when /ACK/i
|
||||
# @completion.succeed line
|
||||
# when /ERR/i
|
||||
# @completion.fail :error, line
|
||||
# else
|
||||
# @completion.fail :unknown, line
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# @completion.fail :disconnected unless @completion.completed?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class API
|
||||
# attr_reader :host, :port
|
||||
#
|
||||
# def initialize(host = 'example.org', port = 8000)
|
||||
# @host, @port = host, port
|
||||
# end
|
||||
#
|
||||
# def request(message)
|
||||
# completion = EM::Deferrable::Completion.new
|
||||
# EM.connect(host, port, Protocol, message, completion)
|
||||
# completion
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# api = API.new
|
||||
# completion = api.request('stuff')
|
||||
# completion.callback do |line|
|
||||
# puts "API responded with: #{line}"
|
||||
# end
|
||||
# completion.errback do |type, line|
|
||||
# case type
|
||||
# when :error
|
||||
# puts "API error: #{line}"
|
||||
# when :unknown
|
||||
# puts "API returned unknown response: #{line}"
|
||||
# when :disconnected
|
||||
# puts "API server disconnected prematurely"
|
||||
# when :timeout
|
||||
# puts "API server did not respond in a timely fashion"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Advanced Usage
|
||||
#
|
||||
# This completion implementation also supports more state callbacks and
|
||||
# arbitrary states (unlike the original Deferrable API). This allows for basic
|
||||
# stateful process encapsulation. One might use this to setup state callbacks
|
||||
# for various states in an exchange like in the basic usage example, except
|
||||
# where the applicaiton could be made to react to "connected" and
|
||||
# "disconnected" states additionally.
|
||||
#
|
||||
# class Protocol < EM::Connection
|
||||
# def initialize(completion)
|
||||
# @response = []
|
||||
# @completion = completion
|
||||
# @completion.stateback(:disconnected) do
|
||||
# @completion.succeed @response.join
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def connection_completed
|
||||
# @host, @port = Socket.unpack_sockaddr_in get_peername
|
||||
# @completion.change_state(:connected, @host, @port)
|
||||
# send_data("GET http://example.org/ HTTP/1.0\r\n\r\n")
|
||||
# end
|
||||
#
|
||||
# def receive_data(data)
|
||||
# @response << data
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# @completion.change_state(:disconnected, @host, @port)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# completion = EM::Deferrable::Completion.new
|
||||
# completion.stateback(:connected) do |host, port|
|
||||
# puts "Connected to #{host}:#{port}"
|
||||
# end
|
||||
# completion.stateback(:disconnected) do |host, port|
|
||||
# puts "Disconnected from #{host}:#{port}"
|
||||
# end
|
||||
# completion.callback do |response|
|
||||
# puts response
|
||||
# end
|
||||
#
|
||||
# EM.connect('example.org', 80, Protocol, completion)
|
||||
#
|
||||
# == Timeout
|
||||
#
|
||||
# The Completion also has a timeout. The timeout is global and is not aware of
|
||||
# states apart from completion states. The timeout is only engaged if #timeout
|
||||
# is called, and it will call fail if it is reached.
|
||||
#
|
||||
# == Completion states
|
||||
#
|
||||
# By default there are two completion states, :succeeded and :failed. These
|
||||
# states can be modified by subclassing and overrding the #completion_states
|
||||
# method. Completion states are special, in that callbacks for all completion
|
||||
# states are explcitly cleared when a completion state is entered. This
|
||||
# prevents errors that could arise from accidental unterminated timeouts, and
|
||||
# other such user errors.
|
||||
#
|
||||
# == Other notes
|
||||
#
|
||||
# Several APIs have been carried over from EM::Deferrable for compatibility
|
||||
# reasons during a transitionary period. Specifically cancel_errback and
|
||||
# cancel_callback are implemented, but their usage is to be strongly
|
||||
# discouraged. Due to the already complex nature of reaction systems, dynamic
|
||||
# callback deletion only makes the problem much worse. It is always better to
|
||||
# add correct conditionals to the callback code, or use more states, than to
|
||||
# address such implementaiton issues with conditional callbacks.
|
||||
|
||||
module EventMachine
|
||||
|
||||
class Completion
|
||||
# This is totally not used (re-implemented), it's here in case people check
|
||||
# for kind_of?
|
||||
include EventMachine::Deferrable
|
||||
|
||||
attr_reader :state, :value
|
||||
|
||||
def initialize
|
||||
@state = :unknown
|
||||
@callbacks = Hash.new { |h,k| h[k] = [] }
|
||||
@value = []
|
||||
@timeout_timer = nil
|
||||
end
|
||||
|
||||
# Enter the :succeeded state, setting the result value if given.
|
||||
def succeed(*args)
|
||||
change_state(:succeeded, *args)
|
||||
end
|
||||
# The old EM method:
|
||||
alias set_deferred_success succeed
|
||||
|
||||
# Enter the :failed state, setting the result value if given.
|
||||
def fail(*args)
|
||||
change_state(:failed, *args)
|
||||
end
|
||||
# The old EM method:
|
||||
alias set_deferred_failure fail
|
||||
|
||||
# Statebacks are called when you enter (or are in) the named state.
|
||||
def stateback(state, *a, &b)
|
||||
# The following is quite unfortunate special casing for :completed
|
||||
# statebacks, but it's a necessary evil for latent completion
|
||||
# definitions.
|
||||
|
||||
if :completed == state || !completed? || @state == state
|
||||
@callbacks[state] << EM::Callback(*a, &b)
|
||||
end
|
||||
execute_callbacks
|
||||
self
|
||||
end
|
||||
|
||||
# Callbacks are called when you enter (or are in) a :succeeded state.
|
||||
def callback(*a, &b)
|
||||
stateback(:succeeded, *a, &b)
|
||||
end
|
||||
|
||||
# Errbacks are called when you enter (or are in) a :failed state.
|
||||
def errback(*a, &b)
|
||||
stateback(:failed, *a, &b)
|
||||
end
|
||||
|
||||
# Completions are called when you enter (or are in) either a :failed or a
|
||||
# :succeeded state. They are stored as a special (reserved) state called
|
||||
# :completed.
|
||||
def completion(*a, &b)
|
||||
stateback(:completed, *a, &b)
|
||||
end
|
||||
|
||||
# Enter a new state, setting the result value if given. If the state is one
|
||||
# of :succeeded or :failed, then :completed callbacks will also be called.
|
||||
def change_state(state, *args)
|
||||
@value = args
|
||||
@state = state
|
||||
|
||||
EM.schedule { execute_callbacks }
|
||||
end
|
||||
|
||||
# The old EM method:
|
||||
alias set_deferred_status change_state
|
||||
|
||||
# Indicates that we've reached some kind of completion state, by default
|
||||
# this is :succeeded or :failed. Due to these semantics, the :completed
|
||||
# state is reserved for internal use.
|
||||
def completed?
|
||||
completion_states.any? { |s| state == s }
|
||||
end
|
||||
|
||||
# Completion states simply returns a list of completion states, by default
|
||||
# this is :succeeded and :failed.
|
||||
def completion_states
|
||||
[:succeeded, :failed]
|
||||
end
|
||||
|
||||
# Schedule a time which if passes before we enter a completion state, this
|
||||
# deferrable will be failed with the given arguments.
|
||||
def timeout(time, *args)
|
||||
cancel_timeout
|
||||
@timeout_timer = EM::Timer.new(time) do
|
||||
fail(*args) unless completed?
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the timeout
|
||||
def cancel_timeout
|
||||
if @timeout_timer
|
||||
@timeout_timer.cancel
|
||||
@timeout_timer = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Remove an errback. N.B. Some errbacks cannot be deleted. Usage is NOT
|
||||
# recommended, this is an anti-pattern.
|
||||
def cancel_errback(*a, &b)
|
||||
@callbacks[:failed].delete(EM::Callback(*a, &b))
|
||||
end
|
||||
|
||||
# Remove a callback. N.B. Some callbacks cannot be deleted. Usage is NOT
|
||||
# recommended, this is an anti-pattern.
|
||||
def cancel_callback(*a, &b)
|
||||
@callbacks[:succeeded].delete(EM::Callback(*a, &b))
|
||||
end
|
||||
|
||||
private
|
||||
# Execute all callbacks for the current state. If in a completed state, then
|
||||
# call any statebacks associated with the completed state.
|
||||
def execute_callbacks
|
||||
execute_state_callbacks(state)
|
||||
if completed?
|
||||
execute_state_callbacks(:completed)
|
||||
clear_dead_callbacks
|
||||
cancel_timeout
|
||||
end
|
||||
end
|
||||
|
||||
# Iterate all callbacks for a given state, and remove then call them.
|
||||
def execute_state_callbacks(state)
|
||||
while callback = @callbacks[state].shift
|
||||
callback.call(*value)
|
||||
end
|
||||
end
|
||||
|
||||
# If we enter a completion state, clear other completion states after all
|
||||
# callback chains are completed. This means that operation specific
|
||||
# callbacks can't be dual-called, which is most common user error.
|
||||
def clear_dead_callbacks
|
||||
completion_states.each do |state|
|
||||
@callbacks[state].clear
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,770 @@
|
||||
module EventMachine
|
||||
class FileNotFoundException < Exception
|
||||
end
|
||||
|
||||
# EventMachine::Connection is a class that is instantiated
|
||||
# by EventMachine's processing loop whenever a new connection
|
||||
# is created. (New connections can be either initiated locally
|
||||
# to a remote server or accepted locally from a remote client.)
|
||||
# When a Connection object is instantiated, it <i>mixes in</i>
|
||||
# the functionality contained in the user-defined module
|
||||
# specified in calls to {EventMachine.connect} or {EventMachine.start_server}.
|
||||
# User-defined handler modules may redefine any or all of the standard
|
||||
# methods defined here, as well as add arbitrary additional code
|
||||
# that will also be mixed in.
|
||||
#
|
||||
# EventMachine manages one object inherited from EventMachine::Connection
|
||||
# (and containing the mixed-in user code) for every network connection
|
||||
# that is active at any given time.
|
||||
# The event loop will automatically call methods on EventMachine::Connection
|
||||
# objects whenever specific events occur on the corresponding connections,
|
||||
# as described below.
|
||||
#
|
||||
# This class is never instantiated by user code, and does not publish an
|
||||
# initialize method. The instance methods of EventMachine::Connection
|
||||
# which may be called by the event loop are:
|
||||
#
|
||||
# * {#post_init}
|
||||
# * {#connection_completed}
|
||||
# * {#receive_data}
|
||||
# * {#unbind}
|
||||
# * {#ssl_verify_peer} (if TLS is used)
|
||||
# * {#ssl_handshake_completed}
|
||||
#
|
||||
# All of the other instance methods defined here are called only by user code.
|
||||
#
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
class Connection
|
||||
# @private
|
||||
attr_accessor :signature
|
||||
|
||||
# @private
|
||||
alias original_method method
|
||||
|
||||
# Override .new so subclasses don't have to call super and can ignore
|
||||
# connection-specific arguments
|
||||
#
|
||||
# @private
|
||||
def self.new(sig, *args)
|
||||
allocate.instance_eval do
|
||||
# Store signature
|
||||
@signature = sig
|
||||
# associate_callback_target sig
|
||||
|
||||
# Call a superclass's #initialize if it has one
|
||||
initialize(*args)
|
||||
|
||||
# post initialize callback
|
||||
post_init
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# Stubbed initialize so legacy superclasses can safely call super
|
||||
#
|
||||
# @private
|
||||
def initialize(*args)
|
||||
end
|
||||
|
||||
# Called by the event loop immediately after the network connection has been established,
|
||||
# and before resumption of the network loop.
|
||||
# This method is generally not called by user code, but is called automatically
|
||||
# by the event loop. The base-class implementation is a no-op.
|
||||
# This is a very good place to initialize instance variables that will
|
||||
# be used throughout the lifetime of the network connection.
|
||||
#
|
||||
# @see #connection_completed
|
||||
# @see #unbind
|
||||
# @see #send_data
|
||||
# @see #receive_data
|
||||
def post_init
|
||||
end
|
||||
|
||||
# Called by the event loop whenever data has been received by the network connection.
|
||||
# It is never called by user code. {#receive_data} is called with a single parameter, a String containing
|
||||
# the network protocol data, which may of course be binary. You will
|
||||
# generally redefine this method to perform your own processing of the incoming data.
|
||||
#
|
||||
# Here's a key point which is essential to understanding the event-driven
|
||||
# programming model: <i>EventMachine knows absolutely nothing about the protocol
|
||||
# which your code implements.</i> You must not make any assumptions about
|
||||
# the size of the incoming data packets, or about their alignment on any
|
||||
# particular intra-message or PDU boundaries (such as line breaks).
|
||||
# receive_data can and will send you arbitrary chunks of data, with the
|
||||
# only guarantee being that the data is presented to your code in the order
|
||||
# it was collected from the network. Don't even assume that the chunks of
|
||||
# data will correspond to network packets, as EventMachine can and will coalesce
|
||||
# several incoming packets into one, to improve performance. The implication for your
|
||||
# code is that you generally will need to implement some kind of a state machine
|
||||
# in your redefined implementation of receive_data. For a better understanding
|
||||
# of this, read through the examples of specific protocol handlers in EventMachine::Protocols
|
||||
#
|
||||
# The base-class implementation (which will be invoked only if you didn't override it in your protocol handler)
|
||||
# simply prints incoming data packet size to stdout.
|
||||
#
|
||||
# @param [String] data Opaque incoming data.
|
||||
# @note Depending on the protocol, buffer sizes and OS networking stack configuration, incoming data may or may not be "a complete message".
|
||||
# It is up to this handler to detect content boundaries to determine whether all the content (for example, full HTTP request)
|
||||
# has been received and can be processed.
|
||||
#
|
||||
# @see #post_init
|
||||
# @see #connection_completed
|
||||
# @see #unbind
|
||||
# @see #send_data
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def receive_data data
|
||||
puts "............>>>#{data.length}"
|
||||
end
|
||||
|
||||
# Called by EventMachine when the SSL/TLS handshake has
|
||||
# been completed, as a result of calling #start_tls to initiate SSL/TLS on the connection.
|
||||
#
|
||||
# This callback exists because {#post_init} and {#connection_completed} are **not** reliable
|
||||
# for indicating when an SSL/TLS connection is ready to have its certificate queried for.
|
||||
#
|
||||
# @see #get_peer_cert
|
||||
def ssl_handshake_completed
|
||||
end
|
||||
|
||||
# Called by EventMachine when :verify_peer => true has been passed to {#start_tls}.
|
||||
# It will be called with each certificate in the certificate chain provided by the remote peer.
|
||||
#
|
||||
# The cert will be passed as a String in PEM format, the same as in {#get_peer_cert}. It is up to user defined
|
||||
# code to perform a check on the certificates. The return value from this callback is used to accept or deny the peer.
|
||||
# A return value that is not nil or false triggers acceptance. If the peer is not accepted, the connection
|
||||
# will be subsequently closed.
|
||||
#
|
||||
# @example This server always accepts all peers
|
||||
#
|
||||
# module AcceptServer
|
||||
# def post_init
|
||||
# start_tls(:verify_peer => true)
|
||||
# end
|
||||
#
|
||||
# def ssl_verify_peer(cert)
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# $server_handshake_completed = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# @example This server never accepts any peers
|
||||
#
|
||||
# module DenyServer
|
||||
# def post_init
|
||||
# start_tls(:verify_peer => true)
|
||||
# end
|
||||
#
|
||||
# def ssl_verify_peer(cert)
|
||||
# # Do not accept the peer. This should now cause the connection to shut down
|
||||
# # without the SSL handshake being completed.
|
||||
# false
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# $server_handshake_completed = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @see #start_tls
|
||||
def ssl_verify_peer(cert)
|
||||
end
|
||||
|
||||
# called by the framework whenever a connection (either a server or client connection) is closed.
|
||||
# The close can occur because your code intentionally closes it (using {#close_connection} and {#close_connection_after_writing}),
|
||||
# because the remote peer closed the connection, or because of a network error.
|
||||
# You may not assume that the network connection is still open and able to send or
|
||||
# receive data when the callback to unbind is made. This is intended only to give
|
||||
# you a chance to clean up associations your code may have made to the connection
|
||||
# object while it was open.
|
||||
#
|
||||
# If you want to detect which peer has closed the connection, you can override {#close_connection} in your protocol handler
|
||||
# and set an @ivar.
|
||||
#
|
||||
# @example Overriding Connection#close_connection to distinguish connections closed on our side
|
||||
#
|
||||
# class MyProtocolHandler < EventMachine::Connection
|
||||
#
|
||||
# # ...
|
||||
#
|
||||
# def close_connection(*args)
|
||||
# @intentionally_closed_connection = true
|
||||
# super(*args)
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# if @intentionally_closed_connection
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # ...
|
||||
#
|
||||
# end
|
||||
#
|
||||
# @see #post_init
|
||||
# @see #connection_completed
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def unbind
|
||||
end
|
||||
|
||||
# Called by the reactor after attempting to relay incoming data to a descriptor (set as a proxy target descriptor with
|
||||
# {EventMachine.enable_proxy}) that has already been closed.
|
||||
#
|
||||
# @see EventMachine.enable_proxy
|
||||
def proxy_target_unbound
|
||||
end
|
||||
|
||||
# called when the reactor finished proxying all
|
||||
# of the requested bytes.
|
||||
def proxy_completed
|
||||
end
|
||||
|
||||
# EventMachine::Connection#proxy_incoming_to is called only by user code. It sets up
|
||||
# a low-level proxy relay for all data inbound for this connection, to the connection given
|
||||
# as the argument. This is essentially just a helper method for enable_proxy.
|
||||
#
|
||||
# @see EventMachine.enable_proxy
|
||||
def proxy_incoming_to(conn,bufsize=0)
|
||||
EventMachine::enable_proxy(self, conn, bufsize)
|
||||
end
|
||||
|
||||
# A helper method for {EventMachine.disable_proxy}
|
||||
def stop_proxying
|
||||
EventMachine::disable_proxy(self)
|
||||
end
|
||||
|
||||
# The number of bytes proxied to another connection. Reset to zero when
|
||||
# EventMachine::Connection#proxy_incoming_to is called, and incremented whenever data is proxied.
|
||||
def get_proxied_bytes
|
||||
EventMachine::get_proxied_bytes(@signature)
|
||||
end
|
||||
|
||||
# EventMachine::Connection#close_connection is called only by user code, and never
|
||||
# by the event loop. You may call this method against a connection object in any
|
||||
# callback handler, whether or not the callback was made against the connection
|
||||
# you want to close. close_connection <i>schedules</i> the connection to be closed
|
||||
# at the next available opportunity within the event loop. You may not assume that
|
||||
# the connection is closed when close_connection returns. In particular, the framework
|
||||
# will callback the unbind method for the particular connection at a point shortly
|
||||
# after you call close_connection. You may assume that the unbind callback will
|
||||
# take place sometime after your call to close_connection completes. In other words,
|
||||
# the unbind callback will not re-enter your code "inside" of your call to close_connection.
|
||||
# However, it's not guaranteed that a future version of EventMachine will not change
|
||||
# this behavior.
|
||||
#
|
||||
# {#close_connection} will *silently discard* any outbound data which you have
|
||||
# sent to the connection using {EventMachine::Connection#send_data} but which has not
|
||||
# yet been sent across the network. If you want to avoid this behavior, use
|
||||
# {EventMachine::Connection#close_connection_after_writing}.
|
||||
#
|
||||
def close_connection after_writing = false
|
||||
EventMachine::close_connection @signature, after_writing
|
||||
end
|
||||
|
||||
# Removes given connection from the event loop.
|
||||
# The connection's socket remains open and its file descriptor number is returned.
|
||||
def detach
|
||||
EventMachine::detach_fd @signature
|
||||
end
|
||||
|
||||
def get_sock_opt level, option
|
||||
EventMachine::get_sock_opt @signature, level, option
|
||||
end
|
||||
|
||||
def set_sock_opt level, optname, optval
|
||||
EventMachine::set_sock_opt @signature, level, optname, optval
|
||||
end
|
||||
|
||||
# A variant of {#close_connection}.
|
||||
# All of the descriptive comments given for close_connection also apply to
|
||||
# close_connection_after_writing, *with one exception*: if the connection has
|
||||
# outbound data sent using send_dat but which has not yet been sent across the network,
|
||||
# close_connection_after_writing will schedule the connection to be closed *after*
|
||||
# all of the outbound data has been safely written to the remote peer.
|
||||
#
|
||||
# Depending on the amount of outgoing data and the speed of the network,
|
||||
# considerable time may elapse between your call to close_connection_after_writing
|
||||
# and the actual closing of the socket (at which time the unbind callback will be called
|
||||
# by the event loop). During this time, you *may not* call send_data to transmit
|
||||
# additional data (that is, the connection is closed for further writes). In very
|
||||
# rare cases, you may experience a receive_data callback after your call to {#close_connection_after_writing},
|
||||
# depending on whether incoming data was in the process of being received on the connection
|
||||
# at the moment when you called {#close_connection_after_writing}. Your protocol handler must
|
||||
# be prepared to properly deal with such data (probably by ignoring it).
|
||||
#
|
||||
# @see #close_connection
|
||||
# @see #send_data
|
||||
def close_connection_after_writing
|
||||
close_connection true
|
||||
end
|
||||
|
||||
# Call this method to send data to the remote end of the network connection. It takes a single String argument,
|
||||
# which may contain binary data. Data is buffered to be sent at the end of this event loop tick (cycle).
|
||||
#
|
||||
# When used in a method that is event handler (for example, {#post_init} or {#connection_completed}, it will send
|
||||
# data to the other end of the connection that generated the event.
|
||||
# You can also call {#send_data} to write to other connections. For more information see The Chat Server Example in the
|
||||
# {file:docs/GettingStarted.md EventMachine tutorial}.
|
||||
#
|
||||
# If you want to send some data and then immediately close the connection, make sure to use {#close_connection_after_writing}
|
||||
# instead of {#close_connection}.
|
||||
#
|
||||
#
|
||||
# @param [String] data Data to send asynchronously
|
||||
#
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
# @see Connection#receive_data
|
||||
# @see Connection#post_init
|
||||
# @see Connection#unbind
|
||||
def send_data data
|
||||
data = data.to_s
|
||||
size = data.bytesize if data.respond_to?(:bytesize)
|
||||
size ||= data.size
|
||||
EventMachine::send_data @signature, data, size
|
||||
end
|
||||
|
||||
# Returns true if the connection is in an error state, false otherwise.
|
||||
#
|
||||
# In general, you can detect the occurrence of communication errors or unexpected
|
||||
# disconnection by the remote peer by handing the {#unbind} method. In some cases, however,
|
||||
# it's useful to check the status of the connection using {#error?} before attempting to send data.
|
||||
# This function is synchronous but it will return immediately without blocking.
|
||||
#
|
||||
# @return [Boolean] true if the connection is in an error state, false otherwise
|
||||
def error?
|
||||
errno = EventMachine::report_connection_error_status(@signature)
|
||||
case errno
|
||||
when 0
|
||||
false
|
||||
when -1
|
||||
true
|
||||
else
|
||||
EventMachine::ERRNOS[errno]
|
||||
end
|
||||
end
|
||||
|
||||
# Called by the event loop when a remote TCP connection attempt completes successfully.
|
||||
# You can expect to get this notification after calls to {EventMachine.connect}. Remember that EventMachine makes remote connections
|
||||
# asynchronously, just as with any other kind of network event. This method
|
||||
# is intended primarily to assist with network diagnostics. For normal protocol
|
||||
# handling, use #post_init to perform initial work on a new connection (such as sending initial set of data).
|
||||
# {Connection#post_init} will always be called. This method will only be called in case of a successful completion.
|
||||
# A connection attempt which fails will result a call to {Connection#unbind} after the failure.
|
||||
#
|
||||
# @see Connection#post_init
|
||||
# @see Connection#unbind
|
||||
# @see file:docs/GettingStarted.md EventMachine tutorial
|
||||
def connection_completed
|
||||
end
|
||||
|
||||
# Call {#start_tls} at any point to initiate TLS encryption on connected streams.
|
||||
# The method is smart enough to know whether it should perform a server-side
|
||||
# or a client-side handshake. An appropriate place to call {#start_tls} is in
|
||||
# your redefined {#post_init} method, or in the {#connection_completed} handler for
|
||||
# an outbound connection.
|
||||
#
|
||||
#
|
||||
# @option args [String] :cert_chain_file (nil) local path of a readable file that contants a chain of X509 certificates in
|
||||
# the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
|
||||
# with the most-resolved certificate at the top of the file, successive intermediate
|
||||
# certs in the middle, and the root (or CA) cert at the bottom.
|
||||
#
|
||||
# @option args [String] :private_key_file (nil) local path of a readable file that must contain a private key in the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail).
|
||||
#
|
||||
# @option args [Boolean] :verify_peer (false) indicates whether a server should request a certificate from a peer, to be verified by user code.
|
||||
# If true, the {#ssl_verify_peer} callback on the {EventMachine::Connection} object is called with each certificate
|
||||
# in the certificate chain provided by the peer. See documentation on {#ssl_verify_peer} for how to use this.
|
||||
#
|
||||
# @option args [Boolean] :fail_if_no_peer_cert (false) Used in conjunction with verify_peer. If set the SSL handshake will be terminated if the peer does not provide a certificate.
|
||||
#
|
||||
#
|
||||
# @option args [String] :cipher_list ("ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH") indicates the available SSL cipher values. Default value is "ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH". Check the format of the OpenSSL cipher string at http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT.
|
||||
#
|
||||
# @option args [String] :ecdh_curve (nil) The curve for ECDHE ciphers. See available ciphers with 'openssl ecparam -list_curves'
|
||||
#
|
||||
# @option args [String] :dhparam (nil) The local path of a file containing DH parameters for EDH ciphers in [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail) See: 'openssl dhparam'
|
||||
#
|
||||
# @option args [Array] :ssl_version (TLSv1 TLSv1_1 TLSv1_2) indicates the allowed SSL/TLS versions. Possible values are: {SSLv2}, {SSLv3}, {TLSv1}, {TLSv1_1}, {TLSv1_2}.
|
||||
#
|
||||
# @example Using TLS with EventMachine
|
||||
#
|
||||
# require 'rubygems'
|
||||
# require 'eventmachine'
|
||||
#
|
||||
# module Handler
|
||||
# def post_init
|
||||
# start_tls(:private_key_file => '/tmp/server.key', :cert_chain_file => '/tmp/server.crt', :verify_peer => false)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EventMachine.run do
|
||||
# EventMachine.start_server("127.0.0.1", 9999, Handler)
|
||||
# end
|
||||
#
|
||||
# @param [Hash] args
|
||||
#
|
||||
# @todo support passing an encryption parameter, which can be string or Proc, to get a passphrase
|
||||
# for encrypted private keys.
|
||||
# @todo support passing key material via raw strings or Procs that return strings instead of
|
||||
# just filenames.
|
||||
#
|
||||
# @see #ssl_verify_peer
|
||||
def start_tls args={}
|
||||
priv_key = args[:private_key_file]
|
||||
cert_chain = args[:cert_chain_file]
|
||||
verify_peer = args[:verify_peer]
|
||||
sni_hostname = args[:sni_hostname]
|
||||
cipher_list = args[:cipher_list]
|
||||
ssl_version = args[:ssl_version]
|
||||
ecdh_curve = args[:ecdh_curve]
|
||||
dhparam = args[:dhparam]
|
||||
fail_if_no_peer_cert = args[:fail_if_no_peer_cert]
|
||||
|
||||
[priv_key, cert_chain].each do |file|
|
||||
next if file.nil? or file.empty?
|
||||
raise FileNotFoundException,
|
||||
"Could not find #{file} for start_tls" unless File.exist? file
|
||||
end
|
||||
|
||||
protocols_bitmask = 0
|
||||
if ssl_version.nil?
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
|
||||
else
|
||||
[ssl_version].flatten.each do |p|
|
||||
case p.to_s.downcase
|
||||
when 'sslv2'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_SSLv2
|
||||
when 'sslv3'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_SSLv3
|
||||
when 'tlsv1'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
|
||||
when 'tlsv1_1'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
|
||||
when 'tlsv1_2'
|
||||
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
|
||||
else
|
||||
raise("Unrecognized SSL/TLS Protocol: #{p}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EventMachine::set_tls_parms(@signature, priv_key || '', cert_chain || '', verify_peer, fail_if_no_peer_cert, sni_hostname || '', cipher_list || '', ecdh_curve || '', dhparam || '', protocols_bitmask)
|
||||
EventMachine::start_tls @signature
|
||||
end
|
||||
|
||||
# If [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is active on the connection, returns the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509)
|
||||
# as a string, in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail). This can then be used for arbitrary validation
|
||||
# of a peer's certificate in your code.
|
||||
#
|
||||
# This should be called in/after the {#ssl_handshake_completed} callback, which indicates
|
||||
# that SSL/TLS is active. Using this callback is important, because the certificate may not
|
||||
# be available until the time it is executed. Using #post_init or #connection_completed is
|
||||
# not adequate, because the SSL handshake may still be taking place.
|
||||
#
|
||||
# This method will return `nil` if:
|
||||
#
|
||||
# * EventMachine is not built with [OpenSSL](http://www.openssl.org) support
|
||||
# * [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is not active on the connection
|
||||
# * TLS handshake is not yet complete
|
||||
# * Remote peer for any other reason has not presented a certificate
|
||||
#
|
||||
#
|
||||
# @example Getting peer TLS certificate information in EventMachine
|
||||
#
|
||||
# module Handler
|
||||
# def post_init
|
||||
# puts "Starting TLS"
|
||||
# start_tls
|
||||
# end
|
||||
#
|
||||
# def ssl_handshake_completed
|
||||
# puts get_peer_cert
|
||||
# close_connection
|
||||
# end
|
||||
#
|
||||
# def unbind
|
||||
# EventMachine::stop_event_loop
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EventMachine.run do
|
||||
# EventMachine.connect "mail.google.com", 443, Handler
|
||||
# end
|
||||
#
|
||||
# # Will output:
|
||||
# # -----BEGIN CERTIFICATE-----
|
||||
# # MIIDIjCCAougAwIBAgIQbldpChBPqv+BdPg4iwgN8TANBgkqhkiG9w0BAQUFADBM
|
||||
# # MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
|
||||
# # THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wODA1MDIxNjMyNTRaFw0w
|
||||
# # OTA1MDIxNjMyNTRaMGkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
|
||||
# # MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMRgw
|
||||
# # FgYDVQQDEw9tYWlsLmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
|
||||
# # AoGBALlkxdh2QXegdElukCSOV2+8PKiONIS+8Tu9K7MQsYpqtLNC860zwOPQ2NLI
|
||||
# # 3Zp4jwuXVTrtzGuiqf5Jioh35Ig3CqDXtLyZoypjZUQcq4mlLzHlhIQ4EhSjDmA7
|
||||
# # Ffw9y3ckSOQgdBQWNLbquHh9AbEUjmhkrYxIqKXeCnRKhv6nAgMBAAGjgecwgeQw
|
||||
# # KAYDVR0lBCEwHwYIKwYBBQUHAwEGCCsGAQUFBwMCBglghkgBhvhCBAEwNgYDVR0f
|
||||
# # BC8wLTAroCmgJ4YlaHR0cDovL2NybC50aGF3dGUuY29tL1RoYXd0ZVNHQ0NBLmNy
|
||||
# # bDByBggrBgEFBQcBAQRmMGQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnRoYXd0
|
||||
# # ZS5jb20wPgYIKwYBBQUHMAKGMmh0dHA6Ly93d3cudGhhd3RlLmNvbS9yZXBvc2l0
|
||||
# # b3J5L1RoYXd0ZV9TR0NfQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEF
|
||||
# # BQADgYEAsRwpLg1dgCR1gYDK185MFGukXMeQFUvhGqF8eT/CjpdvezyKVuz84gSu
|
||||
# # 6ccMXgcPQZGQN/F4Xug+Q01eccJjRSVfdvR5qwpqCj+6BFl5oiKDBsveSkrmL5dz
|
||||
# # s2bn7TdTSYKcLeBkjXxDLHGBqLJ6TNCJ3c4/cbbG5JhGvoema94=
|
||||
# # -----END CERTIFICATE-----
|
||||
#
|
||||
# You can do whatever you want with the certificate String, such as load it
|
||||
# as a certificate object using the OpenSSL library, and check its fields.
|
||||
#
|
||||
# @return [String] the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509), in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
|
||||
# if TLS is active on the connection
|
||||
#
|
||||
# @see Connection#start_tls
|
||||
# @see Connection#ssl_handshake_completed
|
||||
def get_peer_cert
|
||||
EventMachine::get_peer_cert @signature
|
||||
end
|
||||
|
||||
def get_cipher_bits
|
||||
EventMachine::get_cipher_bits @signature
|
||||
end
|
||||
|
||||
def get_cipher_name
|
||||
EventMachine::get_cipher_name @signature
|
||||
end
|
||||
|
||||
def get_cipher_protocol
|
||||
EventMachine::get_cipher_protocol @signature
|
||||
end
|
||||
|
||||
def get_sni_hostname
|
||||
EventMachine::get_sni_hostname @signature
|
||||
end
|
||||
|
||||
# Sends UDP messages.
|
||||
#
|
||||
# This method may be called from any Connection object that refers
|
||||
# to an open datagram socket (see EventMachine#open_datagram_socket).
|
||||
# The method sends a UDP (datagram) packet containing the data you specify,
|
||||
# to a remote peer specified by the IP address and port that you give
|
||||
# as parameters to the method.
|
||||
# Observe that you may send a zero-length packet (empty string).
|
||||
# However, you may not send an arbitrarily-large data packet because
|
||||
# your operating system will enforce a platform-specific limit on
|
||||
# the size of the outbound packet. (Your kernel
|
||||
# will respond in a platform-specific way if you send an overlarge
|
||||
# packet: some will send a truncated packet, some will complain, and
|
||||
# some will silently drop your request).
|
||||
# On LANs, it's usually OK to send datagrams up to about 4000 bytes in length,
|
||||
# but to be really safe, send messages smaller than the Ethernet-packet
|
||||
# size (typically about 1400 bytes). Some very restrictive WANs
|
||||
# will either drop or truncate packets larger than about 500 bytes.
|
||||
#
|
||||
# @param [String] data Data to send asynchronously
|
||||
# @param [String] recipient_address IP address of the recipient
|
||||
# @param [String] recipient_port Port of the recipient
|
||||
def send_datagram data, recipient_address, recipient_port
|
||||
data = data.to_s
|
||||
size = data.bytesize if data.respond_to?(:bytesize)
|
||||
size ||= data.size
|
||||
EventMachine::send_datagram @signature, data, size, recipient_address, Integer(recipient_port)
|
||||
end
|
||||
|
||||
|
||||
# This method is used with stream-connections to obtain the identity
|
||||
# of the remotely-connected peer. If a peername is available, this method
|
||||
# returns a sockaddr structure. The method returns nil if no peername is available.
|
||||
# You can use Socket.unpack_sockaddr_in and its variants to obtain the
|
||||
# values contained in the peername structure returned from #get_peername.
|
||||
#
|
||||
# @example How to get peer IP address and port with EventMachine
|
||||
#
|
||||
# require 'socket'
|
||||
#
|
||||
# module Handler
|
||||
# def receive_data data
|
||||
# port, ip = Socket.unpack_sockaddr_in(get_peername)
|
||||
# puts "got #{data.inspect} from #{ip}:#{port}"
|
||||
# end
|
||||
# end
|
||||
def get_peername
|
||||
EventMachine::get_peername @signature
|
||||
end
|
||||
|
||||
# Used with stream-connections to obtain the identity
|
||||
# of the local side of the connection. If a local name is available, this method
|
||||
# returns a sockaddr structure. The method returns nil if no local name is available.
|
||||
# You can use {Socket.unpack_sockaddr_in} and its variants to obtain the
|
||||
# values contained in the local-name structure returned from this method.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# require 'socket'
|
||||
#
|
||||
# module Handler
|
||||
# def receive_data data
|
||||
# port, ip = Socket.unpack_sockaddr_in(get_sockname)
|
||||
# puts "got #{data.inspect}"
|
||||
# end
|
||||
# end
|
||||
def get_sockname
|
||||
EventMachine::get_sockname @signature
|
||||
end
|
||||
|
||||
# Returns the PID (kernel process identifier) of a subprocess
|
||||
# associated with this Connection object. For use with {EventMachine.popen}
|
||||
# and similar methods. Returns nil when there is no meaningful subprocess.
|
||||
#
|
||||
# @return [Integer]
|
||||
def get_pid
|
||||
EventMachine::get_subprocess_pid @signature
|
||||
end
|
||||
|
||||
# Returns a subprocess exit status. Only useful for {EventMachine.popen}. Call it in your
|
||||
# {#unbind} handler.
|
||||
#
|
||||
# @return [Integer]
|
||||
def get_status
|
||||
EventMachine::get_subprocess_status @signature
|
||||
end
|
||||
|
||||
# The number of seconds since the last send/receive activity on this connection.
|
||||
def get_idle_time
|
||||
EventMachine::get_idle_time @signature
|
||||
end
|
||||
|
||||
# comm_inactivity_timeout returns the current value (float in seconds) of the inactivity-timeout
|
||||
# property of network-connection and datagram-socket objects. A nonzero value
|
||||
# indicates that the connection or socket will automatically be closed if no read or write
|
||||
# activity takes place for at least that number of seconds.
|
||||
# A zero value (the default) specifies that no automatic timeout will take place.
|
||||
def comm_inactivity_timeout
|
||||
EventMachine::get_comm_inactivity_timeout @signature
|
||||
end
|
||||
|
||||
# Allows you to set the inactivity-timeout property for
|
||||
# a network connection or datagram socket. Specify a non-negative float value in seconds.
|
||||
# If the value is greater than zero, the connection or socket will automatically be closed
|
||||
# if no read or write activity takes place for at least that number of seconds.
|
||||
# Specify a value of zero to indicate that no automatic timeout should take place.
|
||||
# Zero is the default value.
|
||||
def comm_inactivity_timeout= value
|
||||
EventMachine::set_comm_inactivity_timeout @signature, value.to_f
|
||||
end
|
||||
alias set_comm_inactivity_timeout comm_inactivity_timeout=
|
||||
|
||||
# The duration after which a TCP connection in the connecting state will fail.
|
||||
# It is important to distinguish this value from {EventMachine::Connection#comm_inactivity_timeout},
|
||||
# which looks at how long since data was passed on an already established connection.
|
||||
# The value is a float in seconds.
|
||||
#
|
||||
# @return [Float] The duration after which a TCP connection in the connecting state will fail, in seconds.
|
||||
def pending_connect_timeout
|
||||
EventMachine::get_pending_connect_timeout @signature
|
||||
end
|
||||
|
||||
# Sets the duration after which a TCP connection in a
|
||||
# connecting state will fail.
|
||||
#
|
||||
# @param [Float, #to_f] value Connection timeout in seconds
|
||||
def pending_connect_timeout= value
|
||||
EventMachine::set_pending_connect_timeout @signature, value.to_f
|
||||
end
|
||||
alias set_pending_connect_timeout pending_connect_timeout=
|
||||
|
||||
# Reconnect to a given host/port with the current instance
|
||||
#
|
||||
# @param [String] server Hostname or IP address
|
||||
# @param [Integer] port Port to reconnect to
|
||||
def reconnect server, port
|
||||
EventMachine::reconnect server, port, self
|
||||
end
|
||||
|
||||
|
||||
# Like {EventMachine::Connection#send_data}, this sends data to the remote end of
|
||||
# the network connection. {EventMachine::Connection#send_file_data} takes a
|
||||
# filename as an argument, though, and sends the contents of the file, in one
|
||||
# chunk.
|
||||
#
|
||||
# @param [String] filename Local path of the file to send
|
||||
#
|
||||
# @see #send_data
|
||||
# @author Kirk Haines
|
||||
def send_file_data filename
|
||||
EventMachine::send_file_data @signature, filename
|
||||
end
|
||||
|
||||
# Open a file on the filesystem and send it to the remote peer. This returns an
|
||||
# object of type {EventMachine::Deferrable}. The object's callbacks will be executed
|
||||
# on the reactor main thread when the file has been completely scheduled for
|
||||
# transmission to the remote peer. Its errbacks will be called in case of an error (such as file-not-found).
|
||||
# This method employs various strategies to achieve the fastest possible performance,
|
||||
# balanced against minimum consumption of memory.
|
||||
#
|
||||
# Warning: this feature has an implicit dependency on an outboard extension,
|
||||
# evma_fastfilereader. You must install this extension in order to use {#stream_file_data}
|
||||
# with files larger than a certain size (currently 8192 bytes).
|
||||
#
|
||||
# @option args [Boolean] :http_chunks (false) If true, this method will stream the file data in a format
|
||||
# compatible with the HTTP chunked-transfer encoding
|
||||
#
|
||||
# @param [String] filename Local path of the file to stream
|
||||
# @param [Hash] args Options
|
||||
#
|
||||
# @return [EventMachine::Deferrable]
|
||||
def stream_file_data filename, args={}
|
||||
EventMachine::FileStreamer.new( self, filename, args )
|
||||
end
|
||||
|
||||
# Watches connection for readability. Only possible if the connection was created
|
||||
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
|
||||
#
|
||||
# @see #notify_readable?
|
||||
def notify_readable= mode
|
||||
EventMachine::set_notify_readable @signature, mode
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the connection is being watched for readability.
|
||||
def notify_readable?
|
||||
EventMachine::is_notify_readable @signature
|
||||
end
|
||||
|
||||
# Watches connection for writeability. Only possible if the connection was created
|
||||
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
|
||||
#
|
||||
# @see #notify_writable?
|
||||
def notify_writable= mode
|
||||
EventMachine::set_notify_writable @signature, mode
|
||||
end
|
||||
|
||||
# Returns true if the connection is being watched for writability.
|
||||
def notify_writable?
|
||||
EventMachine::is_notify_writable @signature
|
||||
end
|
||||
|
||||
# Pause a connection so that {#send_data} and {#receive_data} events are not fired until {#resume} is called.
|
||||
# @see #resume
|
||||
def pause
|
||||
EventMachine::pause_connection @signature
|
||||
end
|
||||
|
||||
# Resume a connection's {#send_data} and {#receive_data} events.
|
||||
# @see #pause
|
||||
def resume
|
||||
EventMachine::resume_connection @signature
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the connect was paused using {EventMachine::Connection#pause}.
|
||||
# @see #pause
|
||||
# @see #resume
|
||||
def paused?
|
||||
EventMachine::connection_paused? @signature
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,210 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Deferrable
|
||||
autoload :Pool, 'em/deferrable/pool'
|
||||
|
||||
# Specify a block to be executed if and when the Deferrable object receives
|
||||
# a status of :succeeded. See #set_deferred_status for more information.
|
||||
#
|
||||
# Calling this method on a Deferrable object whose status is not yet known
|
||||
# will cause the callback block to be stored on an internal list.
|
||||
# If you call this method on a Deferrable whose status is :succeeded, the
|
||||
# block will be executed immediately, receiving the parameters given to the
|
||||
# prior #set_deferred_status call.
|
||||
#
|
||||
#--
|
||||
# If there is no status, add a callback to an internal list.
|
||||
# If status is succeeded, execute the callback immediately.
|
||||
# If status is failed, do nothing.
|
||||
#
|
||||
def callback &block
|
||||
return unless block
|
||||
@deferred_status ||= :unknown
|
||||
if @deferred_status == :succeeded
|
||||
block.call(*@deferred_args)
|
||||
elsif @deferred_status != :failed
|
||||
@callbacks ||= []
|
||||
@callbacks.unshift block # << block
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding callback to &block if any. Undoes the action of #callback.
|
||||
#
|
||||
def cancel_callback block
|
||||
@callbacks ||= []
|
||||
@callbacks.delete block
|
||||
end
|
||||
|
||||
# Specify a block to be executed if and when the Deferrable object receives
|
||||
# a status of :failed. See #set_deferred_status for more information.
|
||||
#--
|
||||
# If there is no status, add an errback to an internal list.
|
||||
# If status is failed, execute the errback immediately.
|
||||
# If status is succeeded, do nothing.
|
||||
#
|
||||
def errback &block
|
||||
return unless block
|
||||
@deferred_status ||= :unknown
|
||||
if @deferred_status == :failed
|
||||
block.call(*@deferred_args)
|
||||
elsif @deferred_status != :succeeded
|
||||
@errbacks ||= []
|
||||
@errbacks.unshift block # << block
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding errback to &block if any. Undoes the action of #errback.
|
||||
#
|
||||
def cancel_errback block
|
||||
@errbacks ||= []
|
||||
@errbacks.delete block
|
||||
end
|
||||
|
||||
# Sets the "disposition" (status) of the Deferrable object. See also the large set of
|
||||
# sugarings for this method.
|
||||
# Note that if you call this method without arguments,
|
||||
# no arguments will be passed to the callback/errback.
|
||||
# If the user has coded these with arguments, then the
|
||||
# user code will throw an argument exception.
|
||||
# Implementors of deferrable classes <b>must</b>
|
||||
# document the arguments they will supply to user callbacks.
|
||||
#
|
||||
# OBSERVE SOMETHING VERY SPECIAL here: you may call this method even
|
||||
# on the INSIDE of a callback. This is very useful when a previously-registered
|
||||
# callback wants to change the parameters that will be passed to subsequently-registered
|
||||
# ones.
|
||||
#
|
||||
# You may give either :succeeded or :failed as the status argument.
|
||||
#
|
||||
# If you pass :succeeded, then all of the blocks passed to the object using the #callback
|
||||
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
|
||||
# passed to the object using #errback will be discarded.
|
||||
#
|
||||
# If you pass :failed, then all of the blocks passed to the object using the #errback
|
||||
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
|
||||
# passed to the object using # callback will be discarded.
|
||||
#
|
||||
# If you pass any arguments to #set_deferred_status in addition to the status argument,
|
||||
# they will be passed as arguments to any callbacks or errbacks that are executed.
|
||||
# It's your responsibility to ensure that the argument lists specified in your callbacks and
|
||||
# errbacks match the arguments given in calls to #set_deferred_status, otherwise Ruby will raise
|
||||
# an ArgumentError.
|
||||
#
|
||||
#--
|
||||
# We're shifting callbacks off and discarding them as we execute them.
|
||||
# This is valid because by definition callbacks are executed no more than
|
||||
# once. It also has the magic effect of permitting recursive calls, which
|
||||
# means that a callback can call #set_deferred_status and change the parameters
|
||||
# that will be sent to subsequent callbacks down the chain.
|
||||
#
|
||||
# Changed @callbacks and @errbacks from push/shift to unshift/pop, per suggestion
|
||||
# by Kirk Haines, to work around the memory leak bug that still exists in many Ruby
|
||||
# versions.
|
||||
#
|
||||
# Changed 15Sep07: after processing callbacks or errbacks, CLEAR the other set of
|
||||
# handlers. This gets us a little closer to the behavior of Twisted's "deferred,"
|
||||
# which only allows status to be set once. Prior to making this change, it was possible
|
||||
# to "succeed" a Deferrable (triggering its callbacks), and then immediately "fail" it,
|
||||
# triggering its errbacks! That is clearly undesirable, but it's just as undesirable
|
||||
# to raise an exception is status is set more than once on a Deferrable. The latter
|
||||
# behavior would invalidate the idiom of resetting arguments by setting status from
|
||||
# within a callback or errback, but more seriously it would cause spurious errors
|
||||
# if a Deferrable was timed out and then an attempt was made to succeed it. See the
|
||||
# comments under the new method #timeout.
|
||||
#
|
||||
def set_deferred_status status, *args
|
||||
cancel_timeout
|
||||
@errbacks ||= nil
|
||||
@callbacks ||= nil
|
||||
@deferred_status = status
|
||||
@deferred_args = args
|
||||
case @deferred_status
|
||||
when :succeeded
|
||||
if @callbacks
|
||||
while cb = @callbacks.pop
|
||||
cb.call(*@deferred_args)
|
||||
end
|
||||
end
|
||||
@errbacks.clear if @errbacks
|
||||
when :failed
|
||||
if @errbacks
|
||||
while eb = @errbacks.pop
|
||||
eb.call(*@deferred_args)
|
||||
end
|
||||
end
|
||||
@callbacks.clear if @callbacks
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Setting a timeout on a Deferrable causes it to go into the failed state after
|
||||
# the Timeout expires (passing no arguments to the object's errbacks).
|
||||
# Setting the status at any time prior to a call to the expiration of the timeout
|
||||
# will cause the timer to be cancelled.
|
||||
def timeout seconds, *args
|
||||
cancel_timeout
|
||||
me = self
|
||||
@deferred_timeout = EventMachine::Timer.new(seconds) {me.fail(*args)}
|
||||
self
|
||||
end
|
||||
|
||||
# Cancels an outstanding timeout if any. Undoes the action of #timeout.
|
||||
#
|
||||
def cancel_timeout
|
||||
@deferred_timeout ||= nil
|
||||
if @deferred_timeout
|
||||
@deferred_timeout.cancel
|
||||
@deferred_timeout = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Sugar for set_deferred_status(:succeeded, ...)
|
||||
#
|
||||
def succeed *args
|
||||
set_deferred_status :succeeded, *args
|
||||
end
|
||||
alias set_deferred_success succeed
|
||||
|
||||
# Sugar for set_deferred_status(:failed, ...)
|
||||
#
|
||||
def fail *args
|
||||
set_deferred_status :failed, *args
|
||||
end
|
||||
alias set_deferred_failure fail
|
||||
end
|
||||
|
||||
|
||||
# DefaultDeferrable is an otherwise empty class that includes Deferrable.
|
||||
# This is very useful when you just need to return a Deferrable object
|
||||
# as a way of communicating deferred status to some other part of a program.
|
||||
class DefaultDeferrable
|
||||
include Deferrable
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
warn "EM::Deferrable::Pool is deprecated, please use EM::Pool"
|
||||
EM::Deferrable::Pool = EM::Pool
|
||||
@@ -0,0 +1,73 @@
|
||||
module EventMachine
|
||||
# Utility class that is useful for file monitoring. Supported events are
|
||||
#
|
||||
# * File is modified
|
||||
# * File is deleted
|
||||
# * File is moved
|
||||
#
|
||||
# @note On Mac OS X, file watching only works when kqueue is enabled
|
||||
#
|
||||
# @see EventMachine.watch_file
|
||||
class FileWatch < Connection
|
||||
# @private
|
||||
Cmodified = 'modified'.freeze
|
||||
# @private
|
||||
Cdeleted = 'deleted'.freeze
|
||||
# @private
|
||||
Cmoved = 'moved'.freeze
|
||||
|
||||
|
||||
# @private
|
||||
def receive_data(data)
|
||||
case data
|
||||
when Cmodified
|
||||
file_modified
|
||||
when Cdeleted
|
||||
file_deleted
|
||||
when Cmoved
|
||||
file_moved
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the path that is being monitored.
|
||||
#
|
||||
# @note Current implementation does not pick up on the new filename after a rename occurs.
|
||||
#
|
||||
# @return [String]
|
||||
# @see EventMachine.watch_file
|
||||
def path
|
||||
@path
|
||||
end
|
||||
|
||||
# Will be called when the file is modified. Supposed to be redefined by subclasses.
|
||||
#
|
||||
# @abstract
|
||||
def file_modified
|
||||
end
|
||||
|
||||
# Will be called when the file is deleted. Supposed to be redefined by subclasses.
|
||||
# When the file is deleted, stop_watching will be called after this to make sure everything is
|
||||
# cleaned up correctly.
|
||||
#
|
||||
# @note On Linux (with {http://en.wikipedia.org/wiki/Inotify inotify}), this method will not be called until *all* open file descriptors to
|
||||
# the file have been closed.
|
||||
#
|
||||
# @abstract
|
||||
def file_deleted
|
||||
end
|
||||
|
||||
# Will be called when the file is moved or renamed. Supposed to be redefined by subclasses.
|
||||
#
|
||||
# @abstract
|
||||
def file_moved
|
||||
end
|
||||
|
||||
# Discontinue monitoring of the file.
|
||||
#
|
||||
# This involves cleaning up the underlying monitoring details with kqueue/inotify, and in turn firing {EventMachine::Connection#unbind}.
|
||||
# This will be called automatically when a file is deleted. User code may call it as well.
|
||||
def stop_watching
|
||||
EventMachine::unwatch_filename(@signature)
|
||||
end # stop_watching
|
||||
end # FileWatch
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,61 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
#--
|
||||
# This defines EventMachine::Deferrable#future, which requires
|
||||
# that the rest of EventMachine::Deferrable has already been seen.
|
||||
# (It's in deferrable.rb.)
|
||||
|
||||
module EventMachine
|
||||
module Deferrable
|
||||
|
||||
# A future is a sugaring of a typical deferrable usage.
|
||||
#--
|
||||
# Evaluate arg (which may be an expression or a block).
|
||||
# What's the class of arg?
|
||||
# If arg is an ordinary expression, then return it.
|
||||
# If arg is deferrable (responds to :set_deferred_status),
|
||||
# then look at the arguments. If either callback or errback
|
||||
# are defined, then use them. If neither are defined, then
|
||||
# use the supplied block (if any) as the callback.
|
||||
# Then return arg.
|
||||
def self.future arg, cb=nil, eb=nil, &blk
|
||||
arg = arg.call if arg.respond_to?(:call)
|
||||
|
||||
if arg.respond_to?(:set_deferred_status)
|
||||
if cb || eb
|
||||
arg.callback(&cb) if cb
|
||||
arg.errback(&eb) if eb
|
||||
else
|
||||
arg.callback(&blk) if blk
|
||||
end
|
||||
end
|
||||
|
||||
arg
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
module EventMachine
|
||||
# A simple iterator for concurrent asynchronous work.
|
||||
#
|
||||
# Unlike ruby's built-in iterators, the end of the current iteration cycle is signaled manually,
|
||||
# instead of happening automatically after the yielded block finishes executing. For example:
|
||||
#
|
||||
# (0..10).each{ |num| }
|
||||
#
|
||||
# becomes:
|
||||
#
|
||||
# EM::Iterator.new(0..10).each{ |num,iter| iter.next }
|
||||
#
|
||||
# This is especially useful when doing asynchronous work via reactor libraries and
|
||||
# functions. For example, given a sync and async http api:
|
||||
#
|
||||
# response = sync_http_get(url); ...
|
||||
# async_http_get(url){ |response| ... }
|
||||
#
|
||||
# a synchronous iterator such as:
|
||||
#
|
||||
# responses = urls.map{ |url| sync_http_get(url) }
|
||||
# ...
|
||||
# puts 'all done!'
|
||||
#
|
||||
# could be written as:
|
||||
#
|
||||
# EM::Iterator.new(urls).map(proc{ |url,iter|
|
||||
# async_http_get(url){ |res|
|
||||
# iter.return(res)
|
||||
# }
|
||||
# }, proc{ |responses|
|
||||
# ...
|
||||
# puts 'all done!'
|
||||
# })
|
||||
#
|
||||
# Now, you can take advantage of the asynchronous api to issue requests in parallel. For example,
|
||||
# to fetch 10 urls at a time, simply pass in a concurrency of 10:
|
||||
#
|
||||
# EM::Iterator.new(urls, 10).each do |url,iter|
|
||||
# async_http_get(url){ iter.next }
|
||||
# end
|
||||
#
|
||||
class Iterator
|
||||
Stop = "EM::Stop"
|
||||
# Create a new parallel async iterator with specified concurrency.
|
||||
#
|
||||
# i = EM::Iterator.new(1..100, 10)
|
||||
#
|
||||
# will create an iterator over the range that processes 10 items at a time. Iteration
|
||||
# is started via #each, #map or #inject
|
||||
#
|
||||
# The list may either be an array-like object, or a proc that returns a new object
|
||||
# to be processed each time it is called. If a proc is used, it must return
|
||||
# EventMachine::Iterator::Stop to signal the end of the iterations.
|
||||
#
|
||||
def initialize(list, concurrency = 1)
|
||||
raise ArgumentError, 'concurrency must be bigger than zero' unless (concurrency > 0)
|
||||
if list.respond_to?(:call)
|
||||
@list = nil
|
||||
@list_proc = list
|
||||
elsif list.respond_to?(:to_a)
|
||||
@list = list.to_a.dup
|
||||
@list_proc = nil
|
||||
else
|
||||
raise ArgumentError, 'argument must be a proc or an array'
|
||||
end
|
||||
@concurrency = concurrency
|
||||
|
||||
@started = false
|
||||
@ended = false
|
||||
end
|
||||
|
||||
# Change the concurrency of this iterator. Workers will automatically be spawned or destroyed
|
||||
# to accomodate the new concurrency level.
|
||||
#
|
||||
def concurrency=(val)
|
||||
old = @concurrency
|
||||
@concurrency = val
|
||||
|
||||
spawn_workers if val > old and @started and !@ended
|
||||
end
|
||||
attr_reader :concurrency
|
||||
|
||||
# Iterate over a set of items using the specified block or proc.
|
||||
#
|
||||
# EM::Iterator.new(1..100).each do |num, iter|
|
||||
# puts num
|
||||
# iter.next
|
||||
# end
|
||||
#
|
||||
# An optional second proc is invoked after the iteration is complete.
|
||||
#
|
||||
# EM::Iterator.new(1..100).each(
|
||||
# proc{ |num,iter| iter.next },
|
||||
# proc{ puts 'all done' }
|
||||
# )
|
||||
#
|
||||
def each(foreach=nil, after=nil, &blk)
|
||||
raise ArgumentError, 'proc or block required for iteration' unless foreach ||= blk
|
||||
raise RuntimeError, 'cannot iterate over an iterator more than once' if @started or @ended
|
||||
|
||||
@started = true
|
||||
@pending = 0
|
||||
@workers = 0
|
||||
|
||||
all_done = proc{
|
||||
after.call if after and @ended and @pending == 0
|
||||
}
|
||||
|
||||
@process_next = proc{
|
||||
# p [:process_next, :pending=, @pending, :workers=, @workers, :ended=, @ended, :concurrency=, @concurrency, :list=, @list]
|
||||
unless @ended or @workers > @concurrency
|
||||
item = next_item()
|
||||
if item.equal?(Stop)
|
||||
@ended = true
|
||||
@workers -= 1
|
||||
all_done.call
|
||||
else
|
||||
@pending += 1
|
||||
|
||||
is_done = false
|
||||
on_done = proc{
|
||||
raise RuntimeError, 'already completed this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
@pending -= 1
|
||||
|
||||
if @ended
|
||||
all_done.call
|
||||
else
|
||||
EM.next_tick(@process_next)
|
||||
end
|
||||
}
|
||||
class << on_done
|
||||
alias :next :call
|
||||
end
|
||||
|
||||
foreach.call(item, on_done)
|
||||
end
|
||||
else
|
||||
@workers -= 1
|
||||
end
|
||||
}
|
||||
|
||||
spawn_workers
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Collect the results of an asynchronous iteration into an array.
|
||||
#
|
||||
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).map(proc{ |cmd,iter|
|
||||
# EM.system(cmd){ |output,status|
|
||||
# iter.return(output)
|
||||
# }
|
||||
# }, proc{ |results|
|
||||
# p results
|
||||
# })
|
||||
#
|
||||
def map(foreach, after)
|
||||
index = 0
|
||||
|
||||
inject([], proc{ |results,item,iter|
|
||||
i = index
|
||||
index += 1
|
||||
|
||||
is_done = false
|
||||
on_done = proc{ |res|
|
||||
raise RuntimeError, 'already returned a value for this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
results[i] = res
|
||||
iter.return(results)
|
||||
}
|
||||
class << on_done
|
||||
alias :return :call
|
||||
def next
|
||||
raise NoMethodError, 'must call #return on a map iterator'
|
||||
end
|
||||
end
|
||||
|
||||
foreach.call(item, on_done)
|
||||
}, proc{ |results|
|
||||
after.call(results)
|
||||
})
|
||||
end
|
||||
|
||||
# Inject the results of an asynchronous iteration onto a given object.
|
||||
#
|
||||
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).inject({}, proc{ |hash,cmd,iter|
|
||||
# EM.system(cmd){ |output,status|
|
||||
# hash[cmd] = status.exitstatus == 0 ? output.strip : nil
|
||||
# iter.return(hash)
|
||||
# }
|
||||
# }, proc{ |results|
|
||||
# p results
|
||||
# })
|
||||
#
|
||||
def inject(obj, foreach, after)
|
||||
each(proc{ |item,iter|
|
||||
is_done = false
|
||||
on_done = proc{ |res|
|
||||
raise RuntimeError, 'already returned a value for this iteration' if is_done
|
||||
is_done = true
|
||||
|
||||
obj = res
|
||||
iter.next
|
||||
}
|
||||
class << on_done
|
||||
alias :return :call
|
||||
def next
|
||||
raise NoMethodError, 'must call #return on an inject iterator'
|
||||
end
|
||||
end
|
||||
|
||||
foreach.call(obj, item, on_done)
|
||||
}, proc{
|
||||
after.call(obj)
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Spawn workers to consume items from the iterator's enumerator based on the current concurrency level.
|
||||
#
|
||||
def spawn_workers
|
||||
EM.next_tick(start_worker = proc{
|
||||
if @workers < @concurrency and !@ended
|
||||
# p [:spawning_worker, :workers=, @workers, :concurrency=, @concurrency, :ended=, @ended]
|
||||
@workers += 1
|
||||
@process_next.call
|
||||
EM.next_tick(start_worker)
|
||||
end
|
||||
})
|
||||
nil
|
||||
end
|
||||
|
||||
# Return the next item from @list or @list_proc.
|
||||
# Once items have run out, will return EM::Iterator::Stop. Procs must supply this themselves
|
||||
def next_item
|
||||
if @list_proc
|
||||
@list_proc.call
|
||||
else
|
||||
@list.empty? ? Stop : @list.shift
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: pass in one object instead of two? .each{ |iter| puts iter.current; iter.next }
|
||||
# TODO: support iter.pause/resume/stop/break/continue?
|
||||
# TODO: create some exceptions instead of using RuntimeError
|
||||
@@ -0,0 +1,66 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 Jul 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
=begin
|
||||
|
||||
Message Routing in EventMachine.
|
||||
|
||||
The goal here is to enable "routing points," objects that can send and receive
|
||||
"messages," which are delimited streams of bytes. The boundaries of a message
|
||||
are preserved as it passes through the reactor system.
|
||||
|
||||
There will be several module methods defined in EventMachine to create route-point
|
||||
objects (which will probably have a base class of EventMachine::MessageRouter
|
||||
until someone suggests a better name).
|
||||
|
||||
As with I/O objects, routing objects will receive events by having the router
|
||||
core call methods on them. And of course user code can and will define handlers
|
||||
to deal with events of interest.
|
||||
|
||||
The message router base class only really needs a receive_message method. There will
|
||||
be an EM module-method to send messages, in addition to the module methods to create
|
||||
the various kinds of message receivers.
|
||||
|
||||
The simplest kind of message receiver object can receive messages by being named
|
||||
explicitly in a parameter to EM#send_message. More sophisticated receivers can define
|
||||
pub-sub selectors and message-queue names. And they can also define channels for
|
||||
route-points in other processes or even on other machines.
|
||||
|
||||
A message is NOT a marshallable entity. Rather, it's a chunk of flat content more like
|
||||
an Erlang message. Initially, all content submitted for transmission as a message will
|
||||
have the to_s method called on it. Eventually, we'll be able to transmit certain structured
|
||||
data types (XML and YAML documents, Structs within limits) and have them reconstructed
|
||||
on the other end.
|
||||
|
||||
A fundamental goal of the message-routing capability is to interoperate seamlessly with
|
||||
external systems, including non-Ruby systems like ActiveMQ. We will define various protocol
|
||||
handlers for things like Stomp and possibly AMQP, but these will be wrapped up and hidden
|
||||
from the users of the basic routing capability.
|
||||
|
||||
As with Erlang, a critical goal is for programs that are built to use message-passing to work
|
||||
WITHOUT CHANGE when the code is re-based on a multi-process system.
|
||||
|
||||
=end
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
module EventMachine
|
||||
# A simple async resource pool based on a resource and work queue. Resources
|
||||
# are enqueued and work waits for resources to become available.
|
||||
#
|
||||
# @example
|
||||
# require 'em-http-request'
|
||||
#
|
||||
# EM.run do
|
||||
# pool = EM::Pool.new
|
||||
# spawn = lambda { pool.add EM::HttpRequest.new('http://example.org') }
|
||||
# 10.times { spawn[] }
|
||||
# done, scheduled = 0, 0
|
||||
#
|
||||
# check = lambda do
|
||||
# done += 1
|
||||
# if done >= scheduled
|
||||
# EM.stop
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# pool.on_error { |conn| spawn[] }
|
||||
#
|
||||
# 100.times do |i|
|
||||
# scheduled += 1
|
||||
# pool.perform do |conn|
|
||||
# req = conn.get :path => '/', :keepalive => true
|
||||
#
|
||||
# req.callback do
|
||||
# p [:success, conn.object_id, i, req.response.size]
|
||||
# check[]
|
||||
# end
|
||||
#
|
||||
# req.errback { check[] }
|
||||
#
|
||||
# req
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Resources are expected to be controlled by an object responding to a
|
||||
# deferrable/completion style API with callback and errback blocks.
|
||||
#
|
||||
class Pool
|
||||
|
||||
def initialize
|
||||
@resources = EM::Queue.new
|
||||
@removed = []
|
||||
@contents = []
|
||||
@on_error = nil
|
||||
end
|
||||
|
||||
def add resource
|
||||
@contents << resource
|
||||
requeue resource
|
||||
end
|
||||
|
||||
def remove resource
|
||||
@contents.delete resource
|
||||
@removed << resource
|
||||
end
|
||||
|
||||
# Returns a list for introspection purposes only. You should *NEVER* call
|
||||
# modification or work oriented methods on objects in this list. A good
|
||||
# example use case is periodic statistics collection against a set of
|
||||
# connection resources.
|
||||
#
|
||||
# @example
|
||||
# pool.contents.inject(0) { |sum, connection| connection.num_bytes }
|
||||
def contents
|
||||
@contents.dup
|
||||
end
|
||||
|
||||
# Define a default catch-all for when the deferrables returned by work
|
||||
# blocks enter a failed state. By default all that happens is that the
|
||||
# resource is returned to the pool. If on_error is defined, this block is
|
||||
# responsible for re-adding the resource to the pool if it is still usable.
|
||||
# In other words, it is generally assumed that on_error blocks explicitly
|
||||
# handle the rest of the lifetime of the resource.
|
||||
def on_error *a, &b
|
||||
@on_error = EM::Callback(*a, &b)
|
||||
end
|
||||
|
||||
# Perform a given #call-able object or block. The callable object will be
|
||||
# called with a resource from the pool as soon as one is available, and is
|
||||
# expected to return a deferrable.
|
||||
#
|
||||
# The deferrable will have callback and errback added such that when the
|
||||
# deferrable enters a finished state, the object is returned to the pool.
|
||||
#
|
||||
# If on_error is defined, then objects are not automatically returned to the
|
||||
# pool.
|
||||
def perform(*a, &b)
|
||||
work = EM::Callback(*a, &b)
|
||||
|
||||
@resources.pop do |resource|
|
||||
if removed? resource
|
||||
@removed.delete resource
|
||||
reschedule work
|
||||
else
|
||||
process work, resource
|
||||
end
|
||||
end
|
||||
end
|
||||
alias reschedule perform
|
||||
|
||||
# A peek at the number of enqueued jobs waiting for resources
|
||||
def num_waiting
|
||||
@resources.num_waiting
|
||||
end
|
||||
|
||||
# Removed will show resources in a partial pruned state. Resources in the
|
||||
# removed list may not appear in the contents list if they are currently in
|
||||
# use.
|
||||
def removed? resource
|
||||
@removed.include? resource
|
||||
end
|
||||
|
||||
protected
|
||||
def requeue resource
|
||||
@resources.push resource
|
||||
end
|
||||
|
||||
def failure resource
|
||||
if @on_error
|
||||
@contents.delete resource
|
||||
@on_error.call resource
|
||||
# Prevent users from calling a leak.
|
||||
@removed.delete resource
|
||||
else
|
||||
requeue resource
|
||||
end
|
||||
end
|
||||
|
||||
def completion deferrable, resource
|
||||
deferrable.callback { requeue resource }
|
||||
deferrable.errback { failure resource }
|
||||
end
|
||||
|
||||
def process work, resource
|
||||
deferrable = work.call resource
|
||||
if deferrable.kind_of?(EM::Deferrable)
|
||||
completion deferrable, resource
|
||||
else
|
||||
raise ArgumentError, "deferrable expected from work"
|
||||
end
|
||||
rescue
|
||||
failure resource
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
module EventMachine
|
||||
|
||||
# This is subclassed from EventMachine::Connection for use with the process monitoring API. Read the
|
||||
# documentation on the instance methods of this class, and for a full explanation see EventMachine.watch_process.
|
||||
class ProcessWatch < Connection
|
||||
# @private
|
||||
Cfork = 'fork'.freeze
|
||||
# @private
|
||||
Cexit = 'exit'.freeze
|
||||
|
||||
# @private
|
||||
def receive_data(data)
|
||||
case data
|
||||
when Cfork
|
||||
process_forked
|
||||
when Cexit
|
||||
process_exited
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the pid that EventMachine::watch_process was originally called with.
|
||||
def pid
|
||||
@pid
|
||||
end
|
||||
|
||||
# Should be redefined with the user's custom callback that will be fired when the prcess is forked.
|
||||
#
|
||||
# There is currently not an easy way to get the pid of the forked child.
|
||||
def process_forked
|
||||
end
|
||||
|
||||
# Should be redefined with the user's custom callback that will be fired when the process exits.
|
||||
#
|
||||
# stop_watching is called automatically after this callback
|
||||
def process_exited
|
||||
end
|
||||
|
||||
# Discontinue monitoring of the process.
|
||||
# This will be called automatically when a process dies. User code may call it as well.
|
||||
def stop_watching
|
||||
EventMachine::unwatch_pid(@signature)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,123 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 13 Dec 07
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
module EventMachine
|
||||
|
||||
# EM::DeferrableChildProcess is a sugaring of a common use-case
|
||||
# involving EM::popen.
|
||||
# Call the #open method on EM::DeferrableChildProcess, passing
|
||||
# a command-string. #open immediately returns an EM::Deferrable
|
||||
# object. It also schedules the forking of a child process, which
|
||||
# will execute the command passed to #open.
|
||||
# When the forked child terminates, the Deferrable will be signalled
|
||||
# and execute its callbacks, passing the data that the child process
|
||||
# wrote to stdout.
|
||||
#
|
||||
class DeferrableChildProcess < EventMachine::Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
# @private
|
||||
def initialize
|
||||
super
|
||||
@data = []
|
||||
end
|
||||
|
||||
# Sugars a common use-case involving forked child processes.
|
||||
# #open takes a String argument containing an shell command
|
||||
# string (including arguments if desired). #open immediately
|
||||
# returns an EventMachine::Deferrable object, without blocking.
|
||||
#
|
||||
# It also invokes EventMachine#popen to run the passed-in
|
||||
# command in a forked child process.
|
||||
#
|
||||
# When the forked child terminates, the Deferrable that
|
||||
# #open calls its callbacks, passing the data returned
|
||||
# from the child process.
|
||||
#
|
||||
def self.open cmd
|
||||
EventMachine.popen( cmd, DeferrableChildProcess )
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_data data
|
||||
@data << data
|
||||
end
|
||||
|
||||
# @private
|
||||
def unbind
|
||||
succeed( @data.join )
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
class SystemCmd < EventMachine::Connection
|
||||
def initialize cb
|
||||
@cb = cb
|
||||
@output = []
|
||||
end
|
||||
def receive_data data
|
||||
@output << data
|
||||
end
|
||||
def unbind
|
||||
@cb.call @output.join(''), get_status if @cb
|
||||
end
|
||||
end
|
||||
|
||||
# EM::system is a simple wrapper for EM::popen. It is similar to Kernel::system, but requires a
|
||||
# single string argument for the command and performs no shell expansion.
|
||||
#
|
||||
# The block or proc passed to EM::system is called with two arguments: the output generated by the command,
|
||||
# and a Process::Status that contains information about the command's execution.
|
||||
#
|
||||
# EM.run{
|
||||
# EM.system('ls'){ |output,status| puts output if status.exitstatus == 0 }
|
||||
# }
|
||||
#
|
||||
# You can also supply an additional proc to send some data to the process:
|
||||
#
|
||||
# EM.run{
|
||||
# EM.system('sh', proc{ |process|
|
||||
# process.send_data("echo hello\n")
|
||||
# process.send_data("exit\n")
|
||||
# }, proc{ |out,status|
|
||||
# puts(out)
|
||||
# })
|
||||
# }
|
||||
#
|
||||
# Like EventMachine.popen, EventMachine.system currently does not work on windows.
|
||||
# It returns the pid of the spawned process.
|
||||
def EventMachine::system cmd, *args, &cb
|
||||
cb ||= args.pop if args.last.is_a? Proc
|
||||
init = args.pop if args.last.is_a? Proc
|
||||
|
||||
# merge remaining arguments into the command
|
||||
cmd = [cmd, *args] if args.any?
|
||||
|
||||
EM.get_subprocess_pid(EM.popen(cmd, SystemCmd, cb) do |c|
|
||||
init[c] if init
|
||||
end.signature)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
module EventMachine
|
||||
# This module contains various protocol implementations, including:
|
||||
# - HttpClient and HttpClient2
|
||||
# - Stomp
|
||||
# - Memcache
|
||||
# - SmtpClient and SmtpServer
|
||||
# - SASLauth and SASLauthclient
|
||||
# - LineProtocol, LineAndTextProtocol and LineText2
|
||||
# - HeaderAndContentProtocol
|
||||
# - Postgres3
|
||||
# - ObjectProtocol
|
||||
#
|
||||
# The protocol implementations live in separate files in the protocols/ subdirectory,
|
||||
# but are auto-loaded when they are first referenced in your application.
|
||||
#
|
||||
# EventMachine::Protocols is also aliased to EM::P for easier usage.
|
||||
#
|
||||
module Protocols
|
||||
# TODO : various autotools are completely useless with the lack of naming
|
||||
# convention, we need to correct that!
|
||||
autoload :TcpConnectTester, 'em/protocols/tcptest'
|
||||
autoload :HttpClient, 'em/protocols/httpclient'
|
||||
autoload :HttpClient2, 'em/protocols/httpclient2'
|
||||
autoload :LineAndTextProtocol, 'em/protocols/line_and_text'
|
||||
autoload :HeaderAndContentProtocol, 'em/protocols/header_and_content'
|
||||
autoload :LineText2, 'em/protocols/linetext2'
|
||||
autoload :Stomp, 'em/protocols/stomp'
|
||||
autoload :SmtpClient, 'em/protocols/smtpclient'
|
||||
autoload :SmtpServer, 'em/protocols/smtpserver'
|
||||
autoload :SASLauth, 'em/protocols/saslauth'
|
||||
autoload :Memcache, 'em/protocols/memcache'
|
||||
autoload :Postgres3, 'em/protocols/postgres3'
|
||||
autoload :ObjectProtocol, 'em/protocols/object_protocol'
|
||||
autoload :Socks4, 'em/protocols/socks4'
|
||||
autoload :LineProtocol, 'em/protocols/line_protocol'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 Nov 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# === Usage
|
||||
#
|
||||
# class RequestHandler < EM::P::HeaderAndContentProtocol
|
||||
# def receive_request headers, content
|
||||
# p [:request, headers, content]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{
|
||||
# EM.start_server 'localhost', 80, RequestHandler
|
||||
# }
|
||||
#
|
||||
#--
|
||||
# Originally, this subclassed LineAndTextProtocol, which in
|
||||
# turn relies on BufferedTokenizer, which doesn't gracefully
|
||||
# handle the transitions between lines and binary text.
|
||||
# Changed 13Sep08 by FCianfrocca.
|
||||
class HeaderAndContentProtocol < Connection
|
||||
include LineText2
|
||||
|
||||
ContentLengthPattern = /Content-length:\s*(\d+)/i
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
init_for_request
|
||||
end
|
||||
|
||||
def receive_line line
|
||||
case @hc_mode
|
||||
when :discard_blanks
|
||||
unless line == ""
|
||||
@hc_mode = :headers
|
||||
receive_line line
|
||||
end
|
||||
when :headers
|
||||
if line == ""
|
||||
raise "unrecognized state" unless @hc_headers.length > 0
|
||||
if respond_to?(:receive_headers)
|
||||
receive_headers @hc_headers
|
||||
end
|
||||
# @hc_content_length will be nil, not 0, if there was no content-length header.
|
||||
if @hc_content_length.to_i > 0
|
||||
set_binary_mode @hc_content_length
|
||||
else
|
||||
dispatch_request
|
||||
end
|
||||
else
|
||||
@hc_headers << line
|
||||
if ContentLengthPattern =~ line
|
||||
# There are some attacks that rely on sending multiple content-length
|
||||
# headers. This is a crude protection, but needs to become tunable.
|
||||
raise "extraneous content-length header" if @hc_content_length
|
||||
@hc_content_length = $1.to_i
|
||||
end
|
||||
if @hc_headers.length == 1 and respond_to?(:receive_first_header_line)
|
||||
receive_first_header_line line
|
||||
end
|
||||
end
|
||||
else
|
||||
raise "internal error, unsupported mode"
|
||||
end
|
||||
end
|
||||
|
||||
def receive_binary_data text
|
||||
@hc_content = text
|
||||
dispatch_request
|
||||
end
|
||||
|
||||
def dispatch_request
|
||||
if respond_to?(:receive_request)
|
||||
receive_request @hc_headers, @hc_content
|
||||
end
|
||||
init_for_request
|
||||
end
|
||||
private :dispatch_request
|
||||
|
||||
def init_for_request
|
||||
@hc_mode = :discard_blanks
|
||||
@hc_headers = []
|
||||
# originally was @hc_headers ||= []; @hc_headers.clear to get a performance
|
||||
# boost, but it's counterproductive because a subclassed handler will have to
|
||||
# call dup to use the header array we pass in receive_headers.
|
||||
|
||||
@hc_content_length = nil
|
||||
@hc_content = ""
|
||||
end
|
||||
private :init_for_request
|
||||
|
||||
# Basically a convenience method. We might create a subclass that does this
|
||||
# automatically. But it's such a performance killer.
|
||||
def headers_2_hash hdrs
|
||||
self.class.headers_2_hash hdrs
|
||||
end
|
||||
|
||||
class << self
|
||||
def headers_2_hash hdrs
|
||||
hash = {}
|
||||
hdrs.each {|h|
|
||||
if /\A([^\s:]+)\s*:\s*/ =~ h
|
||||
tail = $'.dup
|
||||
hash[ $1.downcase.gsub(/-/,"_").intern ] = tail
|
||||
end
|
||||
}
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,300 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
|
||||
#
|
||||
# @example
|
||||
# EventMachine.run {
|
||||
# http = EventMachine::Protocols::HttpClient.request(
|
||||
# :host => server,
|
||||
# :port => 80,
|
||||
# :request => "/index.html",
|
||||
# :query_string => "parm1=value1&parm2=value2"
|
||||
# )
|
||||
# http.callback {|response|
|
||||
# puts response[:status]
|
||||
# puts response[:headers]
|
||||
# puts response[:content]
|
||||
# }
|
||||
# }
|
||||
#--
|
||||
# TODO:
|
||||
# Add streaming so we can support enormous POSTs. Current max is 20meg.
|
||||
# Timeout for connections that run too long or hang somewhere in the middle.
|
||||
# Persistent connections (HTTP/1.1), may need a associated delegate object.
|
||||
# DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
|
||||
# DNS lookups are unbelievably slow.
|
||||
# HEAD requests.
|
||||
# Convenience methods for requests. get, post, url, etc.
|
||||
# SSL.
|
||||
# Handle status codes like 304, 100, etc.
|
||||
# Refactor this code so that protocol errors all get handled one way (an exception?),
|
||||
# instead of sprinkling set_deferred_status :failed calls everywhere.
|
||||
class HttpClient < Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
MaxPostContentLength = 20 * 1024 * 1024
|
||||
|
||||
def initialize
|
||||
warn "HttpClient is deprecated and will be removed. EM-Http-Request should be used instead."
|
||||
@connected = false
|
||||
end
|
||||
|
||||
# @param args [Hash] The request arguments
|
||||
# @option args [String] :host The host IP/DNS name
|
||||
# @option args [Integer] :port The port to connect too
|
||||
# @option args [String] :verb The request type [GET | POST | DELETE | PUT]
|
||||
# @option args [String] :request The request path
|
||||
# @option args [Hash] :basic_auth The basic auth credentials (:username and :password)
|
||||
# @option args [String] :content The request content
|
||||
# @option args [String] :contenttype The content type (e.g. text/plain)
|
||||
# @option args [String] :query_string The query string
|
||||
# @option args [String] :host_header The host header to set
|
||||
# @option args [String] :cookie Cookies to set
|
||||
def self.request( args = {} )
|
||||
args[:port] ||= 80
|
||||
EventMachine.connect( args[:host], args[:port], self ) {|c|
|
||||
# According to the docs, we will get here AFTER post_init is called.
|
||||
c.instance_eval {@args = args}
|
||||
}
|
||||
end
|
||||
|
||||
def post_init
|
||||
@start_time = Time.now
|
||||
@data = ""
|
||||
@read_state = :base
|
||||
end
|
||||
|
||||
# We send the request when we get a connection.
|
||||
# AND, we set an instance variable to indicate we passed through here.
|
||||
# That allows #unbind to know whether there was a successful connection.
|
||||
# NB: This naive technique won't work when we have to support multiple
|
||||
# requests on a single connection.
|
||||
def connection_completed
|
||||
@connected = true
|
||||
send_request @args
|
||||
end
|
||||
|
||||
def send_request args
|
||||
args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
|
||||
args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
|
||||
|
||||
verb = args[:verb].to_s.upcase
|
||||
unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
|
||||
set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
|
||||
return # NOTE THE EARLY RETURN, we're not sending any data.
|
||||
end
|
||||
|
||||
request = args[:request] || "/"
|
||||
unless request[0,1] == "/"
|
||||
request = "/" + request
|
||||
end
|
||||
|
||||
qs = args[:query_string] || ""
|
||||
if qs.length > 0 and qs[0,1] != '?'
|
||||
qs = "?" + qs
|
||||
end
|
||||
|
||||
version = args[:version] || "1.1"
|
||||
|
||||
# Allow an override for the host header if it's not the connect-string.
|
||||
host = args[:host_header] || args[:host] || "_"
|
||||
# For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
|
||||
port = args[:port].to_i != 80 ? ":#{args[:port]}" : ""
|
||||
|
||||
# POST items.
|
||||
postcontenttype = args[:contenttype] || "application/octet-stream"
|
||||
postcontent = args[:content] || ""
|
||||
raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
|
||||
|
||||
# ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
|
||||
# TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
|
||||
req = [
|
||||
"#{verb} #{request}#{qs} HTTP/#{version}",
|
||||
"Host: #{host}#{port}",
|
||||
"User-agent: Ruby EventMachine",
|
||||
]
|
||||
|
||||
if verb == "POST" || verb == "PUT"
|
||||
req << "Content-type: #{postcontenttype}"
|
||||
req << "Content-length: #{postcontent.length}"
|
||||
end
|
||||
|
||||
# TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
|
||||
# Eventually we will want to deal intelligently with arrays and hashes.
|
||||
if args[:cookie]
|
||||
req << "Cookie: #{args[:cookie]}"
|
||||
end
|
||||
|
||||
# Allow custom HTTP headers, e.g. SOAPAction
|
||||
args[:custom_headers].each do |k,v|
|
||||
req << "#{k}: #{v}"
|
||||
end if args[:custom_headers]
|
||||
|
||||
# Basic-auth stanza contributed by Matt Murphy.
|
||||
if args[:basic_auth]
|
||||
basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
|
||||
req << "Authorization: Basic #{basic_auth_string}"
|
||||
end
|
||||
|
||||
req << ""
|
||||
reqstring = req.map {|l| "#{l}\r\n"}.join
|
||||
send_data reqstring
|
||||
|
||||
if verb == "POST" || verb == "PUT"
|
||||
send_data postcontent
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def receive_data data
|
||||
while data and data.length > 0
|
||||
case @read_state
|
||||
when :base
|
||||
# Perform any per-request initialization here and don't consume any data.
|
||||
@data = ""
|
||||
@headers = []
|
||||
@content_length = nil # not zero
|
||||
@content = ""
|
||||
@status = nil
|
||||
@chunked = false
|
||||
@chunk_length = nil
|
||||
@read_state = :header
|
||||
@connection_close = nil
|
||||
when :header
|
||||
ary = data.split( /\r?\n/m, 2 )
|
||||
if ary.length == 2
|
||||
data = ary.last
|
||||
if ary.first == ""
|
||||
if (@content_length and @content_length > 0) || @chunked || @connection_close
|
||||
@read_state = :content
|
||||
else
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
else
|
||||
@headers << ary.first
|
||||
if @headers.length == 1
|
||||
parse_response_line
|
||||
elsif ary.first =~ /\Acontent-length:\s*/i
|
||||
# Only take the FIRST content-length header that appears,
|
||||
# which we can distinguish because @content_length is nil.
|
||||
# TODO, it's actually a fatal error if there is more than one
|
||||
# content-length header, because the caller is presumptively
|
||||
# a bad guy. (There is an exploit that depends on multiple
|
||||
# content-length headers.)
|
||||
@content_length ||= $'.to_i
|
||||
elsif ary.first =~ /\Aconnection:\s*close/i
|
||||
@connection_close = true
|
||||
elsif ary.first =~ /\Atransfer-encoding:\s*chunked/i
|
||||
@chunked = true
|
||||
end
|
||||
end
|
||||
else
|
||||
@data << data
|
||||
data = ""
|
||||
end
|
||||
when :content
|
||||
if @chunked && @chunk_length
|
||||
bytes_needed = @chunk_length - @chunk_read
|
||||
new_data = data[0, bytes_needed]
|
||||
@chunk_read += new_data.length
|
||||
@content += new_data
|
||||
data = data[bytes_needed..-1] || ""
|
||||
if @chunk_length == @chunk_read && data[0,2] == "\r\n"
|
||||
@chunk_length = nil
|
||||
data = data[2..-1]
|
||||
end
|
||||
elsif @chunked
|
||||
if (m = data.match(/\A(\S*)\r\n/m))
|
||||
data = data[m[0].length..-1]
|
||||
@chunk_length = m[1].to_i(16)
|
||||
@chunk_read = 0
|
||||
if @chunk_length == 0
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
end
|
||||
elsif @content_length
|
||||
# If there was no content-length header, we have to wait until the connection
|
||||
# closes. Everything we get until that point is content.
|
||||
# TODO: Must impose a content-size limit, and also must implement chunking.
|
||||
# Also, must support either temporary files for large content, or calling
|
||||
# a content-consumer block supplied by the user.
|
||||
bytes_needed = @content_length - @content.length
|
||||
@content += data[0, bytes_needed]
|
||||
data = data[bytes_needed..-1] || ""
|
||||
if @content_length == @content.length
|
||||
dispatch_response
|
||||
@read_state = :base
|
||||
end
|
||||
else
|
||||
@content << data
|
||||
data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# We get called here when we have received an HTTP response line.
|
||||
# It's an opportunity to throw an exception or trigger other exceptional
|
||||
# handling.
|
||||
def parse_response_line
|
||||
if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
|
||||
@status = $1.to_i
|
||||
else
|
||||
set_deferred_status :failed, {
|
||||
:status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
|
||||
}
|
||||
close_connection
|
||||
end
|
||||
end
|
||||
private :parse_response_line
|
||||
|
||||
def dispatch_response
|
||||
@read_state = :base
|
||||
set_deferred_status :succeeded, {
|
||||
:content => @content,
|
||||
:headers => @headers,
|
||||
:status => @status
|
||||
}
|
||||
# TODO, we close the connection for now, but this is wrong for persistent clients.
|
||||
close_connection
|
||||
end
|
||||
|
||||
def unbind
|
||||
if !@connected
|
||||
set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
|
||||
elsif (@read_state == :content and @content_length == nil)
|
||||
dispatch_response
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,600 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
|
||||
#
|
||||
# === Usage
|
||||
#
|
||||
# EM.run{
|
||||
# conn = EM::Protocols::HttpClient2.connect 'google.com', 80
|
||||
#
|
||||
# req = conn.get('/')
|
||||
# req.callback{ |response|
|
||||
# p(response.status)
|
||||
# p(response.headers)
|
||||
# p(response.content)
|
||||
# }
|
||||
# }
|
||||
class HttpClient2 < Connection
|
||||
include LineText2
|
||||
|
||||
def initialize
|
||||
warn "HttpClient2 is deprecated and will be removed. EM-Http-Request should be used instead."
|
||||
|
||||
@authorization = nil
|
||||
@closed = nil
|
||||
@requests = nil
|
||||
end
|
||||
|
||||
# @private
|
||||
class Request
|
||||
include Deferrable
|
||||
|
||||
attr_reader :version
|
||||
attr_reader :status
|
||||
attr_reader :header_lines
|
||||
attr_reader :headers
|
||||
attr_reader :content
|
||||
attr_reader :internal_error
|
||||
|
||||
def initialize conn, args
|
||||
@conn = conn
|
||||
@args = args
|
||||
@header_lines = []
|
||||
@headers = {}
|
||||
@blanks = 0
|
||||
@chunk_trailer = nil
|
||||
@chunking = nil
|
||||
end
|
||||
|
||||
def send_request
|
||||
az = @args[:authorization] and az = "Authorization: #{az}\r\n"
|
||||
|
||||
r = [
|
||||
"#{@args[:verb]} #{@args[:uri]} HTTP/#{@args[:version] || "1.1"}\r\n",
|
||||
"Host: #{@args[:host_header] || "_"}\r\n",
|
||||
az || "",
|
||||
"\r\n"
|
||||
]
|
||||
@conn.send_data r.join
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
#
|
||||
def receive_line ln
|
||||
if @chunk_trailer
|
||||
receive_chunk_trailer(ln)
|
||||
elsif @chunking
|
||||
receive_chunk_header(ln)
|
||||
else
|
||||
receive_header_line(ln)
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
#
|
||||
def receive_chunk_trailer ln
|
||||
if ln.length == 0
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
else
|
||||
p "Received chunk trailer line"
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Allow up to ten blank lines before we get a real response line.
|
||||
# Allow no more than 100 lines in the header.
|
||||
#
|
||||
def receive_header_line ln
|
||||
if ln.length == 0
|
||||
if @header_lines.length > 0
|
||||
process_header
|
||||
else
|
||||
@blanks += 1
|
||||
if @blanks > 10
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
else
|
||||
@header_lines << ln
|
||||
if @header_lines.length > 100
|
||||
@internal_error = :bad_header
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Cf RFC 2616 pgh 3.6.1 for the format of HTTP chunks.
|
||||
#
|
||||
def receive_chunk_header ln
|
||||
if ln.length > 0
|
||||
chunksize = ln.to_i(16)
|
||||
if chunksize > 0
|
||||
@conn.set_text_mode(ln.to_i(16))
|
||||
else
|
||||
@content = @content ? @content.join : ''
|
||||
@chunk_trailer = true
|
||||
end
|
||||
else
|
||||
# We correctly come here after each chunk gets read.
|
||||
# p "Got A BLANK chunk line"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# We get a single chunk. Append it to the incoming content and switch back to line mode.
|
||||
#
|
||||
def receive_chunked_text text
|
||||
# p "RECEIVED #{text.length} CHUNK"
|
||||
(@content ||= []) << text
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# TODO, inefficient how we're handling this. Part of it is done so as to
|
||||
# make sure we don't have problems in detecting chunked-encoding, content-length,
|
||||
# etc.
|
||||
#
|
||||
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
|
||||
ClenRE = /\AContent-length:\s*(\d+)/i
|
||||
ChunkedRE = /\ATransfer-encoding:\s*chunked/i
|
||||
ColonRE = /\:\s*/
|
||||
|
||||
def process_header
|
||||
unless @header_lines.first =~ HttpResponseRE
|
||||
@conn.close_connection
|
||||
@internal_error = :bad_request
|
||||
end
|
||||
@version = $1.dup
|
||||
@status = $2.dup.to_i
|
||||
|
||||
clen = nil
|
||||
chunks = nil
|
||||
@header_lines.each_with_index do |e,ix|
|
||||
if ix > 0
|
||||
hdr,val = e.split(ColonRE,2)
|
||||
(@headers[hdr.downcase] ||= []) << val
|
||||
end
|
||||
|
||||
if clen == nil and e =~ ClenRE
|
||||
clen = $1.dup.to_i
|
||||
end
|
||||
if e =~ ChunkedRE
|
||||
chunks = true
|
||||
end
|
||||
end
|
||||
|
||||
if clen
|
||||
# If the content length is zero we should not call set_text_mode,
|
||||
# because a value of zero will make it wait forever, hanging the
|
||||
# connection. Just return success instead, with empty content.
|
||||
if clen == 0 then
|
||||
@content = ""
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
else
|
||||
@conn.set_text_mode clen
|
||||
end
|
||||
elsif chunks
|
||||
@chunking = true
|
||||
else
|
||||
# Chunked transfer, multipart, or end-of-connection.
|
||||
# For end-of-connection, we need to go the unbind
|
||||
# method and suppress its desire to fail us.
|
||||
p "NO CLEN"
|
||||
p @args[:uri]
|
||||
p @header_lines
|
||||
@internal_error = :unsupported_clen
|
||||
@conn.close_connection
|
||||
end
|
||||
end
|
||||
private :process_header
|
||||
|
||||
|
||||
def receive_text text
|
||||
@chunking ? receive_chunked_text(text) : receive_sized_text(text)
|
||||
end
|
||||
|
||||
#--
|
||||
# At the present time, we only handle contents that have a length
|
||||
# specified by the content-length header.
|
||||
#
|
||||
def receive_sized_text text
|
||||
@content = text
|
||||
@conn.pop_request
|
||||
succeed(self)
|
||||
end
|
||||
end
|
||||
|
||||
# Make a connection to a remote HTTP server.
|
||||
# Can take either a pair of arguments (which will be interpreted as
|
||||
# a hostname/ip-address and a port), or a hash.
|
||||
# If the arguments are a hash, then supported values include:
|
||||
# :host => a hostname or ip-address
|
||||
# :port => a port number
|
||||
# :ssl => true to enable ssl
|
||||
def self.connect *args
|
||||
if args.length == 2
|
||||
args = {:host=>args[0], :port=>args[1]}
|
||||
else
|
||||
args = args.first
|
||||
end
|
||||
|
||||
h,prt,ssl = args[:host], Integer(args[:port]), (args[:tls] || args[:ssl])
|
||||
conn = EM.connect( h, prt, self )
|
||||
conn.start_tls if ssl
|
||||
conn.set_default_host_header( h, prt, ssl )
|
||||
conn
|
||||
end
|
||||
|
||||
# Get a url
|
||||
#
|
||||
# req = conn.get(:uri => '/')
|
||||
# req.callback{|response| puts response.content }
|
||||
#
|
||||
def get args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "GET"
|
||||
request args
|
||||
end
|
||||
|
||||
# Post to a url
|
||||
#
|
||||
# req = conn.post('/data')
|
||||
# req.callback{|response| puts response.content }
|
||||
#--
|
||||
# XXX there's no way to supply a POST body.. wtf?
|
||||
def post args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "POST"
|
||||
request args
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Compute and remember a string to be used as the host header in HTTP requests
|
||||
# unless the user overrides it with an argument to #request.
|
||||
#
|
||||
# @private
|
||||
def set_default_host_header host, port, ssl
|
||||
if (ssl and port != 443) or (!ssl and port != 80)
|
||||
@host_header = "#{host}:#{port}"
|
||||
else
|
||||
@host_header = host
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# @private
|
||||
def post_init
|
||||
super
|
||||
@connected = EM::DefaultDeferrable.new
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
super
|
||||
@connected.succeed
|
||||
end
|
||||
|
||||
#--
|
||||
# All pending requests, if any, must fail.
|
||||
# We might come here without ever passing through connection_completed
|
||||
# in case we can't connect to the server. We'll also get here when the
|
||||
# connection closes (either because the server closes it, or we close it
|
||||
# due to detecting an internal error or security violation).
|
||||
# In either case, run down all pending requests, if any, and signal failure
|
||||
# on them.
|
||||
#
|
||||
# Set and remember a flag (@closed) so we can immediately fail any
|
||||
# subsequent requests.
|
||||
#
|
||||
# @private
|
||||
def unbind
|
||||
super
|
||||
@closed = true
|
||||
(@requests || []).each {|r| r.fail}
|
||||
end
|
||||
|
||||
# @private
|
||||
def request args
|
||||
args[:host_header] = @host_header unless args.has_key?(:host_header)
|
||||
args[:authorization] = @authorization unless args.has_key?(:authorization)
|
||||
r = Request.new self, args
|
||||
if @closed
|
||||
r.fail
|
||||
else
|
||||
(@requests ||= []).unshift r
|
||||
@connected.callback {r.send_request}
|
||||
end
|
||||
r
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line ln
|
||||
if req = @requests.last
|
||||
req.receive_line ln
|
||||
else
|
||||
p "??????????"
|
||||
p ln
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_binary_data text
|
||||
@requests.last.receive_text text
|
||||
end
|
||||
|
||||
#--
|
||||
# Called by a Request object when it completes.
|
||||
#
|
||||
# @private
|
||||
def pop_request
|
||||
@requests.pop
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
=begin
|
||||
class HttpClient2x < Connection
|
||||
include LineText2
|
||||
|
||||
# TODO: Make this behave appropriate in case a #connect fails.
|
||||
# Currently, this produces no errors.
|
||||
|
||||
# Make a connection to a remote HTTP server.
|
||||
# Can take either a pair of arguments (which will be interpreted as
|
||||
# a hostname/ip-address and a port), or a hash.
|
||||
# If the arguments are a hash, then supported values include:
|
||||
# :host => a hostname or ip-address;
|
||||
# :port => a port number
|
||||
#--
|
||||
# TODO, support optional encryption arguments like :ssl
|
||||
def self.connect *args
|
||||
if args.length == 2
|
||||
args = {:host=>args[0], :port=>args[1]}
|
||||
else
|
||||
args = args.first
|
||||
end
|
||||
|
||||
h,prt = args[:host],Integer(args[:port])
|
||||
EM.connect( h, prt, self, h, prt )
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Sugars a connection that makes a single request and then
|
||||
# closes the connection. Matches the behavior and the arguments
|
||||
# of the original implementation of class HttpClient.
|
||||
#
|
||||
# Intended primarily for back compatibility, but the idiom
|
||||
# is probably useful so it's not deprecated.
|
||||
# We return a Deferrable, as did the original implementation.
|
||||
#
|
||||
# Because we're improving the way we deal with errors and exceptions
|
||||
# (specifically, HTTP response codes other than 2xx will trigger the
|
||||
# errback rather than the callback), this may break some existing code.
|
||||
#
|
||||
def self.request args
|
||||
c = connect args
|
||||
end
|
||||
|
||||
#--
|
||||
# Requests can be pipelined. When we get a request, add it to the
|
||||
# front of a queue as an array. The last element of the @requests
|
||||
# array is always the oldest request received. Each element of the
|
||||
# @requests array is a two-element array consisting of a hash with
|
||||
# the original caller's arguments, and an initially-empty Ostruct
|
||||
# containing the data we retrieve from the server's response.
|
||||
# Maintain the instance variable @current_response, which is the response
|
||||
# of the oldest pending request. That's just to make other code a little
|
||||
# easier. If the variable doesn't exist when we come here, we're
|
||||
# obviously the first request being made on the connection.
|
||||
#
|
||||
# The reason for keeping this method private (and requiring use of the
|
||||
# convenience methods #get, #post, #head, etc) is to avoid the small
|
||||
# performance penalty of canonicalizing the verb.
|
||||
#
|
||||
def request args
|
||||
d = EventMachine::DefaultDeferrable.new
|
||||
|
||||
if @closed
|
||||
d.fail
|
||||
return d
|
||||
end
|
||||
|
||||
o = OpenStruct.new
|
||||
o.deferrable = d
|
||||
(@requests ||= []).unshift [args, o]
|
||||
@current_response ||= @requests.last.last
|
||||
@connected.callback {
|
||||
az = args[:authorization] and az = "Authorization: #{az}\r\n"
|
||||
|
||||
r = [
|
||||
"#{args[:verb]} #{args[:uri]} HTTP/#{args[:version] || "1.1"}\r\n",
|
||||
"Host: #{args[:host_header] || @host_header}\r\n",
|
||||
az || "",
|
||||
"\r\n"
|
||||
]
|
||||
p r
|
||||
send_data r.join
|
||||
}
|
||||
o.deferrable
|
||||
end
|
||||
private :request
|
||||
|
||||
def get args
|
||||
if args.is_a?(String)
|
||||
args = {:uri=>args}
|
||||
end
|
||||
args[:verb] = "GET"
|
||||
request args
|
||||
end
|
||||
|
||||
def initialize host, port
|
||||
super
|
||||
@host_header = "#{host}:#{port}"
|
||||
end
|
||||
def post_init
|
||||
super
|
||||
@connected = EM::DefaultDeferrable.new
|
||||
end
|
||||
|
||||
|
||||
def connection_completed
|
||||
super
|
||||
@connected.succeed
|
||||
end
|
||||
|
||||
#--
|
||||
# Make sure to throw away any leftover incoming data if we've
|
||||
# been closed due to recognizing an error.
|
||||
#
|
||||
# Generate an internal error if we get an unreasonable number of
|
||||
# header lines. It could be malicious.
|
||||
#
|
||||
def receive_line ln
|
||||
p ln
|
||||
return if @closed
|
||||
|
||||
if ln.length > 0
|
||||
(@current_response.headers ||= []).push ln
|
||||
abort_connection if @current_response.headers.length > 100
|
||||
else
|
||||
process_received_headers
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# We come here when we've seen all the headers for a particular request.
|
||||
# What we do next depends on the response line (which should be the
|
||||
# first line in the header set), and whether there is content to read.
|
||||
# We may transition into a text-reading state to read content, or
|
||||
# we may abort the connection, or we may go right back into parsing
|
||||
# responses for the next response in the chain.
|
||||
#
|
||||
# We make an ASSUMPTION that the first line is an HTTP response.
|
||||
# Anything else produces an error that aborts the connection.
|
||||
# This may not be enough, because it may be that responses to pipelined
|
||||
# requests will come with a blank-line delimiter.
|
||||
#
|
||||
# Any non-2xx response will be treated as a fatal error, and abort the
|
||||
# connection. We will set up the status and other response parameters.
|
||||
# TODO: we will want to properly support 1xx responses, which some versions
|
||||
# of IIS copiously generate.
|
||||
# TODO: We need to give the option of not aborting the connection with certain
|
||||
# non-200 responses, in order to work with NTLM and other authentication
|
||||
# schemes that work at the level of individual connections.
|
||||
#
|
||||
# Some error responses will get sugarings. For example, we'll return the
|
||||
# Location header in the response in case of a 301/302 response.
|
||||
#
|
||||
# Possible dispositions here:
|
||||
# 1) No content to read (either content-length is zero or it's a HEAD request);
|
||||
# 2) Switch to text mode to read a specific number of bytes;
|
||||
# 3) Read a chunked or multipart response;
|
||||
# 4) Read till the server closes the connection.
|
||||
#
|
||||
# Our reponse to the client can be either to wait till all the content
|
||||
# has been read and then to signal caller's deferrable, or else to signal
|
||||
# it when we finish the processing the headers and then expect the caller
|
||||
# to have given us a block to call as the content comes in. And of course
|
||||
# the latter gets stickier with chunks and multiparts.
|
||||
#
|
||||
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
|
||||
ClenRE = /\AContent-length:\s*(\d+)/i
|
||||
def process_received_headers
|
||||
abort_connection unless @current_response.headers.first =~ HttpResponseRE
|
||||
@current_response.version = $1.dup
|
||||
st = $2.dup
|
||||
@current_response.status = st.to_i
|
||||
abort_connection unless st[0,1] == "2"
|
||||
|
||||
clen = nil
|
||||
@current_response.headers.each do |e|
|
||||
if clen == nil and e =~ ClenRE
|
||||
clen = $1.dup.to_i
|
||||
end
|
||||
end
|
||||
|
||||
if clen
|
||||
set_text_mode clen
|
||||
end
|
||||
end
|
||||
private :process_received_headers
|
||||
|
||||
|
||||
def receive_binary_data text
|
||||
@current_response.content = text
|
||||
@current_response.deferrable.succeed @current_response
|
||||
@requests.pop
|
||||
@current_response = (@requests.last || []).last
|
||||
set_line_mode
|
||||
end
|
||||
|
||||
|
||||
|
||||
# We've received either a server error or an internal error.
|
||||
# Close the connection and abort any pending requests.
|
||||
#--
|
||||
# When should we call close_connection? It will cause #unbind
|
||||
# to be fired. Should the user expect to see #unbind before
|
||||
# we call #receive_http_error, or the other way around?
|
||||
#
|
||||
# Set instance variable @closed. That's used to inhibit further
|
||||
# processing of any inbound data after an error has been recognized.
|
||||
#
|
||||
# We shouldn't have to worry about any leftover outbound data,
|
||||
# because we call close_connection (not close_connection_after_writing).
|
||||
# That ensures that any pipelined requests received after an error
|
||||
# DO NOT get streamed out to the server on this connection.
|
||||
# Very important. TODO, write a unit-test to establish that behavior.
|
||||
#
|
||||
def abort_connection
|
||||
close_connection
|
||||
@closed = true
|
||||
@current_response.deferrable.fail( @current_response )
|
||||
end
|
||||
|
||||
|
||||
#------------------------
|
||||
# Below here are user-overridable methods.
|
||||
|
||||
end
|
||||
=end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# A protocol that handles line-oriented data with interspersed binary text.
|
||||
#
|
||||
# This version is optimized for performance. See EventMachine::Protocols::LineText2
|
||||
# for a version which is optimized for correctness with regard to binary text blocks
|
||||
# that can switch back to line mode.
|
||||
class LineAndTextProtocol < Connection
|
||||
MaxBinaryLength = 32*1024*1024
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
lbp_init_line_state
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
if @lbp_mode == :lines
|
||||
begin
|
||||
@lpb_buffer.extract(data).each do |line|
|
||||
receive_line(line.chomp) if respond_to?(:receive_line)
|
||||
end
|
||||
rescue
|
||||
receive_error('overlength line') if respond_to?(:receive_error)
|
||||
close_connection
|
||||
return
|
||||
end
|
||||
else
|
||||
if @lbp_binary_limit > 0
|
||||
wanted = @lbp_binary_limit - @lbp_binary_bytes_received
|
||||
chunk = nil
|
||||
if data.length > wanted
|
||||
chunk = data.slice!(0...wanted)
|
||||
else
|
||||
chunk = data
|
||||
data = ""
|
||||
end
|
||||
@lbp_binary_buffer[@lbp_binary_bytes_received...(@lbp_binary_bytes_received+chunk.length)] = chunk
|
||||
@lbp_binary_bytes_received += chunk.length
|
||||
if @lbp_binary_bytes_received == @lbp_binary_limit
|
||||
receive_binary_data(@lbp_binary_buffer) if respond_to?(:receive_binary_data)
|
||||
lbp_init_line_state
|
||||
end
|
||||
receive_data(data) if data.length > 0
|
||||
else
|
||||
receive_binary_data(data) if respond_to?(:receive_binary_data)
|
||||
data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unbind
|
||||
if @lbp_mode == :binary and @lbp_binary_limit > 0
|
||||
if respond_to?(:receive_binary_data)
|
||||
receive_binary_data( @lbp_binary_buffer[0...@lbp_binary_bytes_received] )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Set up to read the supplied number of binary bytes.
|
||||
# This recycles all the data currently waiting in the line buffer, if any.
|
||||
# If the limit is nil, then ALL subsequent data will be treated as binary
|
||||
# data and passed to the upstream protocol handler as we receive it.
|
||||
# If a limit is given, we'll hold the incoming binary data and not
|
||||
# pass it upstream until we've seen it all, or until there is an unbind
|
||||
# (in which case we'll pass up a partial).
|
||||
# Specifying nil for the limit (the default) means there is no limit.
|
||||
# Specifiyng zero for the limit will cause an immediate transition back to line mode.
|
||||
#
|
||||
def set_binary_mode size = nil
|
||||
if @lbp_mode == :lines
|
||||
if size == 0
|
||||
receive_binary_data("") if respond_to?(:receive_binary_data)
|
||||
# Do no more work here. Stay in line mode and keep consuming data.
|
||||
else
|
||||
@lbp_binary_limit = size.to_i # (nil will be stored as zero)
|
||||
if @lbp_binary_limit > 0
|
||||
raise "Overlength" if @lbp_binary_limit > MaxBinaryLength # arbitrary sanity check
|
||||
@lbp_binary_buffer = "\0" * @lbp_binary_limit
|
||||
@lbp_binary_bytes_received = 0
|
||||
end
|
||||
|
||||
@lbp_mode = :binary
|
||||
receive_data @lpb_buffer.flush
|
||||
end
|
||||
else
|
||||
raise "invalid operation"
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# For internal use, establish protocol baseline for handling lines.
|
||||
def lbp_init_line_state
|
||||
@lpb_buffer = BufferedTokenizer.new("\n")
|
||||
@lbp_mode = :lines
|
||||
end
|
||||
private :lbp_init_line_state
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# LineProtocol will parse out newline terminated strings from a receive_data stream
|
||||
#
|
||||
# module Server
|
||||
# include EM::P::LineProtocol
|
||||
#
|
||||
# def receive_line(line)
|
||||
# send_data("you said: #{line}")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module LineProtocol
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buf ||= '') << data
|
||||
|
||||
while @buf.slice!(/(.*?)\r?\n/)
|
||||
receive_line($1)
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked with lines received over the network
|
||||
def receive_line(line)
|
||||
# stub
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,179 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# In the grand, time-honored tradition of re-inventing the wheel, we offer
|
||||
# here YET ANOTHER protocol that handles line-oriented data with interspersed
|
||||
# binary text. This one trades away some of the performance optimizations of
|
||||
# EventMachine::Protocols::LineAndTextProtocol in order to get better correctness
|
||||
# with regard to binary text blocks that can switch back to line mode. It also
|
||||
# permits the line-delimiter to change in midstream.
|
||||
# This was originally written to support Stomp.
|
||||
module LineText2
|
||||
# TODO! We're not enforcing the limits on header lengths and text-lengths.
|
||||
# When we get around to that, call #receive_error if the user defined it, otherwise
|
||||
# throw exceptions.
|
||||
|
||||
MaxBinaryLength = 32*1024*1024
|
||||
|
||||
#--
|
||||
# Will loop internally until there's no data left to read.
|
||||
# That way the user-defined handlers we call can modify the
|
||||
# handling characteristics on a per-token basis.
|
||||
#
|
||||
def receive_data data
|
||||
return unless (data and data.length > 0)
|
||||
|
||||
# Do this stuff in lieu of a constructor.
|
||||
@lt2_mode ||= :lines
|
||||
@lt2_delimiter ||= "\n"
|
||||
@lt2_linebuffer ||= []
|
||||
|
||||
remaining_data = data
|
||||
|
||||
while remaining_data.length > 0
|
||||
if @lt2_mode == :lines
|
||||
delimiter_string = case @lt2_delimiter
|
||||
when Regexp
|
||||
remaining_data.slice(@lt2_delimiter)
|
||||
else
|
||||
@lt2_delimiter
|
||||
end
|
||||
ix = remaining_data.index(delimiter_string) if delimiter_string
|
||||
if ix
|
||||
@lt2_linebuffer << remaining_data[0...ix]
|
||||
ln = @lt2_linebuffer.join
|
||||
@lt2_linebuffer.clear
|
||||
if @lt2_delimiter == "\n"
|
||||
ln.chomp!
|
||||
end
|
||||
receive_line ln
|
||||
remaining_data = remaining_data[(ix+delimiter_string.length)..-1]
|
||||
else
|
||||
@lt2_linebuffer << remaining_data
|
||||
remaining_data = ""
|
||||
end
|
||||
elsif @lt2_mode == :text
|
||||
if @lt2_textsize
|
||||
needed = @lt2_textsize - @lt2_textpos
|
||||
will_take = if remaining_data.length > needed
|
||||
needed
|
||||
else
|
||||
remaining_data.length
|
||||
end
|
||||
|
||||
@lt2_textbuffer << remaining_data[0...will_take]
|
||||
tail = remaining_data[will_take..-1]
|
||||
|
||||
@lt2_textpos += will_take
|
||||
if @lt2_textpos >= @lt2_textsize
|
||||
# Reset line mode (the default behavior) BEFORE calling the
|
||||
# receive_binary_data. This makes it possible for user code
|
||||
# to call set_text_mode, enabling chains of text blocks
|
||||
# (which can possibly be of different sizes).
|
||||
set_line_mode
|
||||
receive_binary_data @lt2_textbuffer.join
|
||||
receive_end_of_binary_data
|
||||
end
|
||||
|
||||
remaining_data = tail
|
||||
else
|
||||
receive_binary_data remaining_data
|
||||
remaining_data = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The line delimiter may be a regular expression or a string. Anything
|
||||
# passed to set_delimiter other than a regular expression will be
|
||||
# converted to a string.
|
||||
def set_delimiter delim
|
||||
@lt2_delimiter = case delim
|
||||
when Regexp
|
||||
delim
|
||||
else
|
||||
delim.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Called internally but also exposed to user code, for the case in which
|
||||
# processing of binary data creates a need to transition back to line mode.
|
||||
# We support an optional parameter to "throw back" some data, which might
|
||||
# be an umprocessed chunk of the transmitted binary data, or something else
|
||||
# entirely.
|
||||
def set_line_mode data=""
|
||||
@lt2_mode = :lines
|
||||
(@lt2_linebuffer ||= []).clear
|
||||
receive_data data.to_s
|
||||
end
|
||||
|
||||
def set_text_mode size=nil
|
||||
if size == 0
|
||||
set_line_mode
|
||||
else
|
||||
@lt2_mode = :text
|
||||
(@lt2_textbuffer ||= []).clear
|
||||
@lt2_textsize = size # which can be nil, signifying no limit
|
||||
@lt2_textpos = 0
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for #set_text_mode, added for back-compatibility with LineAndTextProtocol.
|
||||
def set_binary_mode size=nil
|
||||
set_text_mode size
|
||||
end
|
||||
|
||||
# In case of a dropped connection, we'll send a partial buffer to user code
|
||||
# when in sized text mode. User overrides of #receive_binary_data need to
|
||||
# be aware that they may get a short buffer.
|
||||
def unbind
|
||||
@lt2_mode ||= nil
|
||||
if @lt2_mode == :text and @lt2_textpos > 0
|
||||
receive_binary_data @lt2_textbuffer.join
|
||||
end
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
def receive_line ln
|
||||
# no-op
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
def receive_binary_data data
|
||||
# no-op
|
||||
end
|
||||
|
||||
# Stub. Should be subclassed by user code.
|
||||
# This is called when transitioning internally from text mode
|
||||
# back to line mode. Useful when client code doesn't want
|
||||
# to keep track of how much data it's received.
|
||||
def receive_end_of_binary_data
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,331 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# Implements the Memcache protocol (http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt).
|
||||
# Requires memcached >= 1.2.4 w/ noreply support
|
||||
#
|
||||
# == Usage example
|
||||
#
|
||||
# EM.run{
|
||||
# cache = EM::P::Memcache.connect 'localhost', 11211
|
||||
#
|
||||
# cache.set :a, 'hello'
|
||||
# cache.set :b, 'hi'
|
||||
# cache.set :c, 'how are you?'
|
||||
# cache.set :d, ''
|
||||
#
|
||||
# cache.get(:a){ |v| p v }
|
||||
# cache.get_hash(:a, :b, :c, :d){ |v| p v }
|
||||
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
|
||||
#
|
||||
# cache.get(:a,:z,:b,:y,:d){ |a,z,b,y,d| p [a,z,b,y,d] }
|
||||
#
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# cache.set(:missing, 'abc'){ p :stored }
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# cache.del(:missing){ p :deleted }
|
||||
# cache.get(:missing){ |m| p [:missing=, m] }
|
||||
# }
|
||||
#
|
||||
module Memcache
|
||||
include EM::Deferrable
|
||||
|
||||
##
|
||||
# constants
|
||||
|
||||
unless defined? Cempty
|
||||
# @private
|
||||
Cstored = 'STORED'.freeze
|
||||
# @private
|
||||
Cend = 'END'.freeze
|
||||
# @private
|
||||
Cdeleted = 'DELETED'.freeze
|
||||
# @private
|
||||
Cunknown = 'NOT_FOUND'.freeze
|
||||
# @private
|
||||
Cerror = 'ERROR'.freeze
|
||||
|
||||
# @private
|
||||
Cempty = ''.freeze
|
||||
# @private
|
||||
Cdelimiter = "\r\n".freeze
|
||||
end
|
||||
|
||||
##
|
||||
# commands
|
||||
|
||||
# Get the value associated with one or multiple keys
|
||||
#
|
||||
# cache.get(:a){ |v| p v }
|
||||
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
|
||||
#
|
||||
def get *keys
|
||||
raise ArgumentError unless block_given?
|
||||
|
||||
callback{
|
||||
keys = keys.map{|k| k.to_s.gsub(/\s/,'_') }
|
||||
send_data "get #{keys.join(' ')}\r\n"
|
||||
@get_cbs << [keys, proc{ |values|
|
||||
yield *keys.map{ |k| values[k] }
|
||||
}]
|
||||
}
|
||||
end
|
||||
|
||||
# Set the value for a given key
|
||||
#
|
||||
# cache.set :a, 'hello'
|
||||
# cache.set(:missing, 'abc'){ puts "stored the value!" }
|
||||
#
|
||||
def set key, val, exptime = 0, &cb
|
||||
callback{
|
||||
val = val.to_s
|
||||
send_cmd :set, key, 0, exptime, val.respond_to?(:bytesize) ? val.bytesize : val.size, !block_given?
|
||||
send_data val
|
||||
send_data Cdelimiter
|
||||
@set_cbs << cb if cb
|
||||
}
|
||||
end
|
||||
|
||||
# Gets multiple values as a hash
|
||||
#
|
||||
# cache.get_hash(:a, :b, :c, :d){ |h| puts h[:a] }
|
||||
#
|
||||
def get_hash *keys
|
||||
raise ArgumentError unless block_given?
|
||||
|
||||
get *keys do |*values|
|
||||
yield keys.inject({}){ |hash, k| hash.update k => values[keys.index(k)] }
|
||||
end
|
||||
end
|
||||
|
||||
# Delete the value associated with a key
|
||||
#
|
||||
# cache.del :a
|
||||
# cache.del(:b){ puts "deleted the value!" }
|
||||
#
|
||||
def delete key, expires = 0, &cb
|
||||
callback{
|
||||
send_data "delete #{key} #{expires}#{cb ? '' : ' noreply'}\r\n"
|
||||
@del_cbs << cb if cb
|
||||
}
|
||||
end
|
||||
alias del delete
|
||||
|
||||
# Connect to a memcached server (must support NOREPLY, memcached >= 1.2.4)
|
||||
def self.connect host = 'localhost', port = 11211
|
||||
EM.connect host, port, self, host, port
|
||||
end
|
||||
|
||||
def send_cmd cmd, key, flags = 0, exptime = 0, bytes = 0, noreply = false
|
||||
send_data "#{cmd} #{key} #{flags} #{exptime} #{bytes}#{noreply ? ' noreply' : ''}\r\n"
|
||||
end
|
||||
private :send_cmd
|
||||
|
||||
##
|
||||
# errors
|
||||
|
||||
# @private
|
||||
class ParserError < StandardError
|
||||
end
|
||||
|
||||
##
|
||||
# em hooks
|
||||
|
||||
# @private
|
||||
def initialize host, port = 11211
|
||||
@host, @port = host, port
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
@get_cbs = []
|
||||
@set_cbs = []
|
||||
@del_cbs = []
|
||||
|
||||
@values = {}
|
||||
|
||||
@reconnecting = false
|
||||
@connected = true
|
||||
succeed
|
||||
# set_delimiter "\r\n"
|
||||
# set_line_mode
|
||||
end
|
||||
|
||||
#--
|
||||
# 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
|
||||
# stack overflows when there is too much data.
|
||||
# include EM::P::LineText2
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buffer||='') << data
|
||||
|
||||
while index = @buffer.index(Cdelimiter)
|
||||
begin
|
||||
line = @buffer.slice!(0,index+2)
|
||||
process_cmd line
|
||||
rescue ParserError
|
||||
@buffer[0...0] = line
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# def receive_line line
|
||||
# @private
|
||||
def process_cmd line
|
||||
case line.strip
|
||||
when /^VALUE\s+(.+?)\s+(\d+)\s+(\d+)/ # VALUE <key> <flags> <bytes>
|
||||
bytes = Integer($3)
|
||||
# set_binary_mode bytes+2
|
||||
# @cur_key = $1
|
||||
if @buffer.size >= bytes + 2
|
||||
@values[$1] = @buffer.slice!(0,bytes)
|
||||
@buffer.slice!(0,2) # \r\n
|
||||
else
|
||||
raise ParserError
|
||||
end
|
||||
|
||||
when Cend # END
|
||||
if entry = @get_cbs.shift
|
||||
keys, cb = entry
|
||||
cb.call(@values)
|
||||
end
|
||||
@values = {}
|
||||
|
||||
when Cstored # STORED
|
||||
if cb = @set_cbs.shift
|
||||
cb.call(true)
|
||||
end
|
||||
|
||||
when Cdeleted # DELETED
|
||||
if cb = @del_cbs.shift
|
||||
cb.call(true)
|
||||
end
|
||||
|
||||
when Cunknown # NOT_FOUND
|
||||
if cb = @del_cbs.shift
|
||||
cb.call(false)
|
||||
end
|
||||
|
||||
else
|
||||
p [:MEMCACHE_UNKNOWN, line]
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# def receive_binary_data data
|
||||
# @values[@cur_key] = data[0..-3]
|
||||
# end
|
||||
|
||||
# @private
|
||||
def unbind
|
||||
if @connected or @reconnecting
|
||||
EM.add_timer(1){ reconnect @host, @port }
|
||||
@connected = false
|
||||
@reconnecting = true
|
||||
@deferred_status = nil
|
||||
else
|
||||
raise 'Unable to connect to memcached server'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
# ruby -I ext:lib -r eventmachine -rubygems lib/protocols/memcache.rb
|
||||
require 'em/spec'
|
||||
|
||||
# @private
|
||||
class TestConnection
|
||||
include EM::P::Memcache
|
||||
def send_data data
|
||||
sent_data << data
|
||||
end
|
||||
def sent_data
|
||||
@sent_data ||= ''
|
||||
end
|
||||
|
||||
def initialize
|
||||
connection_completed
|
||||
end
|
||||
end
|
||||
|
||||
EM.describe EM::Protocols::Memcache do
|
||||
|
||||
before{
|
||||
@c = TestConnection.new
|
||||
}
|
||||
|
||||
should 'send get requests' do
|
||||
@c.get('a'){}
|
||||
@c.sent_data.should == "get a\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'send set requests' do
|
||||
@c.set('a', 1){}
|
||||
@c.sent_data.should == "set a 0 0 1\r\n1\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'use noreply on set without block' do
|
||||
@c.set('a', 1)
|
||||
@c.sent_data.should == "set a 0 0 1 noreply\r\n1\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'send delete requests' do
|
||||
@c.del('a')
|
||||
@c.sent_data.should == "delete a 0 noreply\r\n"
|
||||
done
|
||||
end
|
||||
|
||||
should 'work when get returns no values' do
|
||||
@c.get('a'){ |a|
|
||||
a.should.be.nil
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "END\r\n"
|
||||
end
|
||||
|
||||
should 'invoke block on set' do
|
||||
@c.set('a', 1){
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "STORED\r\n"
|
||||
end
|
||||
|
||||
should 'invoke block on delete' do
|
||||
@c.delete('a'){ |found|
|
||||
found.should.be.false
|
||||
}
|
||||
@c.delete('b'){ |found|
|
||||
found.should.be.true
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "NOT_FOUND\r\n"
|
||||
@c.receive_data "DELETED\r\n"
|
||||
end
|
||||
|
||||
should 'parse split responses' do
|
||||
@c.get('a'){ |a|
|
||||
a.should == 'abc'
|
||||
done
|
||||
}
|
||||
|
||||
@c.receive_data "VAL"
|
||||
@c.receive_data "UE a 0 "
|
||||
@c.receive_data "3\r\n"
|
||||
@c.receive_data "ab"
|
||||
@c.receive_data "c"
|
||||
@c.receive_data "\r\n"
|
||||
@c.receive_data "EN"
|
||||
@c.receive_data "D\r\n"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# ObjectProtocol allows for easy communication using marshaled ruby objects
|
||||
#
|
||||
# module RubyServer
|
||||
# include EM::P::ObjectProtocol
|
||||
#
|
||||
# def receive_object obj
|
||||
# send_object({'you said' => obj})
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module ObjectProtocol
|
||||
# By default returns Marshal, override to return JSON or YAML, or any
|
||||
# other serializer/deserializer responding to #dump and #load.
|
||||
def serializer
|
||||
Marshal
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_data data
|
||||
(@buf ||= '') << data
|
||||
|
||||
while @buf.size >= 4
|
||||
if @buf.size >= 4+(size=@buf.unpack('N').first)
|
||||
@buf.slice!(0,4)
|
||||
receive_object serializer.load(@buf.slice!(0,size))
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked with ruby objects received over the network
|
||||
def receive_object obj
|
||||
# stub
|
||||
end
|
||||
|
||||
# Sends a ruby object over the network
|
||||
def send_object obj
|
||||
data = serializer.dump(obj)
|
||||
send_data [data.respond_to?(:bytesize) ? data.bytesize : data.size, data].pack('Na*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,246 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
require 'postgres-pr/message'
|
||||
require 'postgres-pr/connection'
|
||||
require 'stringio'
|
||||
|
||||
# @private
|
||||
class StringIO
|
||||
# Reads exactly +n+ bytes.
|
||||
#
|
||||
# If the data read is nil an EOFError is raised.
|
||||
#
|
||||
# If the data read is too short an IOError is raised
|
||||
def readbytes(n)
|
||||
str = read(n)
|
||||
if str == nil
|
||||
raise EOFError, "End of file reached"
|
||||
end
|
||||
if str.size < n
|
||||
raise IOError, "data truncated"
|
||||
end
|
||||
str
|
||||
end
|
||||
alias read_exactly_n_bytes readbytes
|
||||
end
|
||||
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# PROVISIONAL IMPLEMENTATION of an evented Postgres client.
|
||||
# This implements version 3 of the Postgres wire protocol, which will work
|
||||
# with any Postgres version from roughly 7.4 onward.
|
||||
#
|
||||
# Objective: we want to access Postgres databases without requiring threads.
|
||||
# Until now this has been a problem because the Postgres client implementations
|
||||
# have all made use of blocking I/O calls, which is incompatible with a
|
||||
# thread-free evented model.
|
||||
#
|
||||
# But rather than re-implement the Postgres Wire3 protocol, we're taking advantage
|
||||
# of the existing postgres-pr library, which was originally written by Michael
|
||||
# Neumann but (at this writing) appears to be no longer maintained. Still, it's
|
||||
# in basically a production-ready state, and the wire protocol isn't that complicated
|
||||
# anyway.
|
||||
#
|
||||
# We're tucking in a bunch of require statements that may not be present in garden-variety
|
||||
# EM installations. Until we find a good way to only require these if a program
|
||||
# requires postgres, this file will need to be required explicitly.
|
||||
#
|
||||
# We need to monkeypatch StringIO because it lacks the #readbytes method needed
|
||||
# by postgres-pr.
|
||||
# The StringIO monkeypatch is lifted from the standard library readbytes.rb,
|
||||
# which adds method #readbytes directly to class IO. But StringIO is not a subclass of IO.
|
||||
# It is modified to raise an IOError instead of TruncatedDataException since the exception is unused.
|
||||
#
|
||||
# We cloned the handling of postgres messages from lib/postgres-pr/connection.rb
|
||||
# in the postgres-pr library, and modified it for event-handling.
|
||||
#
|
||||
# TODO: The password handling in dispatch_conn_message is totally incomplete.
|
||||
#
|
||||
#
|
||||
# We return Deferrables from the user-level operations surfaced by this interface.
|
||||
# Experimentally, we're using the pattern of always returning a boolean value as the
|
||||
# first argument of a deferrable callback to indicate success or failure. This is
|
||||
# instead of the traditional pattern of calling Deferrable#succeed or #fail, and
|
||||
# requiring the user to define both a callback and an errback function.
|
||||
#
|
||||
# === Usage
|
||||
# EM.run {
|
||||
# db = EM.connect_unix_domain( "/tmp/.s.PGSQL.5432", EM::P::Postgres3 )
|
||||
# db.connect( dbname, username, psw ).callback do |status|
|
||||
# if status
|
||||
# db.query( "select * from some_table" ).callback do |status, result, errors|
|
||||
# if status
|
||||
# result.rows.each do |row|
|
||||
# p row
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# }
|
||||
class Postgres3 < EventMachine::Connection
|
||||
include PostgresPR
|
||||
|
||||
def initialize
|
||||
@data = ""
|
||||
@params = {}
|
||||
end
|
||||
|
||||
def connect db, user, psw=nil
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.timeout 15
|
||||
|
||||
if @pending_query || @pending_conn
|
||||
d.succeed false, "Operation already in progress"
|
||||
else
|
||||
@pending_conn = d
|
||||
prms = {"user"=>user, "database"=>db}
|
||||
@user = user
|
||||
if psw
|
||||
@password = psw
|
||||
#prms["password"] = psw
|
||||
end
|
||||
send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump
|
||||
end
|
||||
|
||||
d
|
||||
end
|
||||
|
||||
def query sql
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.timeout 15
|
||||
|
||||
if @pending_query || @pending_conn
|
||||
d.succeed false, "Operation already in progress"
|
||||
else
|
||||
@r = PostgresPR::Connection::Result.new
|
||||
@e = []
|
||||
@pending_query = d
|
||||
send_data PostgresPR::Query.dump(sql)
|
||||
end
|
||||
|
||||
d
|
||||
end
|
||||
|
||||
|
||||
def receive_data data
|
||||
@data << data
|
||||
while @data.length >= 5
|
||||
pktlen = @data[1...5].unpack("N").first
|
||||
if @data.length >= (1 + pktlen)
|
||||
pkt = @data.slice!(0...(1+pktlen))
|
||||
m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) }
|
||||
if @pending_conn
|
||||
dispatch_conn_message m
|
||||
elsif @pending_query
|
||||
dispatch_query_message m
|
||||
else
|
||||
raise "Unexpected message from database"
|
||||
end
|
||||
else
|
||||
break # very important, break out of the while
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def unbind
|
||||
if o = (@pending_query || @pending_conn)
|
||||
o.succeed false, "lost connection"
|
||||
end
|
||||
end
|
||||
|
||||
# Cloned and modified from the postgres-pr.
|
||||
def dispatch_conn_message msg
|
||||
case msg
|
||||
when AuthentificationClearTextPassword
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
send_data PasswordMessage.new(@password).dump
|
||||
|
||||
when AuthentificationCryptPassword
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
send_data PasswordMessage.new(@password.crypt(msg.salt)).dump
|
||||
|
||||
when AuthentificationMD5Password
|
||||
raise ArgumentError, "no password specified" if @password.nil?
|
||||
require 'digest/md5'
|
||||
|
||||
m = Digest::MD5.hexdigest(@password + @user)
|
||||
m = Digest::MD5.hexdigest(m + msg.salt)
|
||||
m = 'md5' + m
|
||||
send_data PasswordMessage.new(m).dump
|
||||
|
||||
when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
|
||||
raise "unsupported authentification"
|
||||
|
||||
when AuthentificationOk
|
||||
when ErrorResponse
|
||||
raise msg.field_values.join("\t")
|
||||
when NoticeResponse
|
||||
@notice_processor.call(msg) if @notice_processor
|
||||
when ParameterStatus
|
||||
@params[msg.key] = msg.value
|
||||
when BackendKeyData
|
||||
# TODO
|
||||
#p msg
|
||||
when ReadyForQuery
|
||||
# TODO: use transaction status
|
||||
pc,@pending_conn = @pending_conn,nil
|
||||
pc.succeed true
|
||||
else
|
||||
raise "unhandled message type"
|
||||
end
|
||||
end
|
||||
|
||||
# Cloned and modified from the postgres-pr.
|
||||
def dispatch_query_message msg
|
||||
case msg
|
||||
when DataRow
|
||||
@r.rows << msg.columns
|
||||
when CommandComplete
|
||||
@r.cmd_tag = msg.cmd_tag
|
||||
when ReadyForQuery
|
||||
pq,@pending_query = @pending_query,nil
|
||||
pq.succeed true, @r, @e
|
||||
when RowDescription
|
||||
@r.fields = msg.fields
|
||||
when CopyInResponse
|
||||
when CopyOutResponse
|
||||
when EmptyQueryResponse
|
||||
when ErrorResponse
|
||||
# TODO
|
||||
@e << msg
|
||||
when NoticeResponse
|
||||
@notice_processor.call(msg) if @notice_processor
|
||||
else
|
||||
# TODO
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,175 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Implements SASL authd.
|
||||
# This is a very, very simple protocol that mimics the one used
|
||||
# by saslauthd and pwcheck, two outboard daemons included in the
|
||||
# standard SASL library distro.
|
||||
# The only thing this is really suitable for is SASL PLAIN
|
||||
# (user+password) authentication, but the SASL libs that are
|
||||
# linked into standard servers (like imapd and sendmail) implement
|
||||
# the other ones.
|
||||
#
|
||||
# SASL-auth is intended for reasonably fast operation inside a
|
||||
# single machine, so it has no transport-security (although there
|
||||
# have been multi-machine extensions incorporating transport-layer
|
||||
# encryption).
|
||||
#
|
||||
# The standard saslauthd module generally runs privileged and does
|
||||
# its work by referring to the system-account files.
|
||||
#
|
||||
# This feature was added to EventMachine to enable the development
|
||||
# of custom authentication/authorization engines for standard servers.
|
||||
#
|
||||
# To use SASLauth, include it in a class that subclasses EM::Connection,
|
||||
# and reimplement the validate method.
|
||||
#
|
||||
# The typical way to incorporate this module into an authentication
|
||||
# daemon would be to set it as the handler for a UNIX-domain socket.
|
||||
# The code might look like this:
|
||||
#
|
||||
# EM.start_unix_domain_server( "/var/run/saslauthd/mux", MyHandler )
|
||||
# File.chmod( 0777, "/var/run/saslauthd/mux")
|
||||
#
|
||||
# The chmod is probably needed to ensure that unprivileged clients can
|
||||
# access the UNIX-domain socket.
|
||||
#
|
||||
# It's also a very good idea to drop superuser privileges (if any), after
|
||||
# the UNIX-domain socket has been opened.
|
||||
#--
|
||||
# Implementation details: assume the client can send us pipelined requests,
|
||||
# and that the client will close the connection.
|
||||
#
|
||||
# The client sends us four values, each encoded as a two-byte length field in
|
||||
# network order followed by the specified number of octets.
|
||||
# The fields specify the username, password, service name (such as imap),
|
||||
# and the "realm" name. We send back the barest minimum reply, a single
|
||||
# field also encoded as a two-octet length in network order, followed by
|
||||
# either "NO" or "OK" - simplicity itself.
|
||||
#
|
||||
# We enforce a maximum field size just as a sanity check.
|
||||
# We do NOT automatically time out the connection.
|
||||
#
|
||||
# The code we use to parse out the values is ugly and probably slow.
|
||||
# Improvements welcome.
|
||||
#
|
||||
module SASLauth
|
||||
|
||||
MaxFieldSize = 128*1024
|
||||
def post_init
|
||||
super
|
||||
@sasl_data = ""
|
||||
@sasl_values = []
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
@sasl_data << data
|
||||
while @sasl_data.length >= 2
|
||||
len = (@sasl_data[0,2].unpack("n")).first
|
||||
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
|
||||
if @sasl_data.length >= (len + 2)
|
||||
@sasl_values << @sasl_data[2,len]
|
||||
@sasl_data.slice!(0...(2+len))
|
||||
if @sasl_values.length == 4
|
||||
send_data( validate(*@sasl_values) ? "\0\002OK" : "\0\002NO" )
|
||||
@sasl_values.clear
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate username, psw, sysname, realm
|
||||
p username
|
||||
p psw
|
||||
p sysname
|
||||
p realm
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# Implements the SASL authd client protocol.
|
||||
# This is a very, very simple protocol that mimics the one used
|
||||
# by saslauthd and pwcheck, two outboard daemons included in the
|
||||
# standard SASL library distro.
|
||||
# The only thing this is really suitable for is SASL PLAIN
|
||||
# (user+password) authentication, but the SASL libs that are
|
||||
# linked into standard servers (like imapd and sendmail) implement
|
||||
# the other ones.
|
||||
#
|
||||
# You can use this module directly as a handler for EM Connections,
|
||||
# or include it in a module or handler class of your own.
|
||||
#
|
||||
# First connect to a SASL server (it's probably a TCP server, or more
|
||||
# likely a Unix-domain socket). Then call the #validate? method,
|
||||
# passing at least a username and a password. #validate? returns
|
||||
# a Deferrable which will either succeed or fail, depending
|
||||
# on the status of the authentication operation.
|
||||
#
|
||||
module SASLauthclient
|
||||
MaxFieldSize = 128*1024
|
||||
|
||||
def validate? username, psw, sysname=nil, realm=nil
|
||||
|
||||
str = [username, psw, sysname, realm].map {|m|
|
||||
[(m || "").length, (m || "")]
|
||||
}.flatten.pack( "nA*" * 4 )
|
||||
send_data str
|
||||
|
||||
d = EM::DefaultDeferrable.new
|
||||
@queries.unshift d
|
||||
d
|
||||
end
|
||||
|
||||
def post_init
|
||||
@sasl_data = ""
|
||||
@queries = []
|
||||
end
|
||||
|
||||
def receive_data data
|
||||
@sasl_data << data
|
||||
|
||||
while @sasl_data.length > 2
|
||||
len = (@sasl_data[0,2].unpack("n")).first
|
||||
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
|
||||
if @sasl_data.length >= (len + 2)
|
||||
val = @sasl_data[2,len]
|
||||
@sasl_data.slice!(0...(2+len))
|
||||
q = @queries.pop
|
||||
(val == "NO") ? q.fail : q.succeed
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,394 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
require 'ostruct'
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Simple SMTP client
|
||||
#
|
||||
# @example
|
||||
# email = EM::Protocols::SmtpClient.send(
|
||||
# :domain=>"example.com",
|
||||
# :host=>'localhost',
|
||||
# :port=>25, # optional, defaults 25
|
||||
# :starttls=>true, # use ssl
|
||||
# :from=>"sender@example.com",
|
||||
# :to=> ["to_1@example.com", "to_2@example.com"],
|
||||
# :header=> {"Subject" => "This is a subject line"},
|
||||
# :body=> "This is the body of the email"
|
||||
# )
|
||||
# email.callback{
|
||||
# puts 'Email sent!'
|
||||
# }
|
||||
# email.errback{ |e|
|
||||
# puts 'Email failed!'
|
||||
# }
|
||||
#
|
||||
# Sending generated emails (using Mail)
|
||||
#
|
||||
# mail = Mail.new do
|
||||
# from 'alice@example.com'
|
||||
# to 'bob@example.com'
|
||||
# subject 'This is a test email'
|
||||
# body 'Hello, world!'
|
||||
# end
|
||||
#
|
||||
# email = EM::P::SmtpClient.send(
|
||||
# :domain=>'example.com',
|
||||
# :from=>mail.from.first,
|
||||
# :to=>mail.to,
|
||||
# :message=>mail.to_s
|
||||
# )
|
||||
#
|
||||
class SmtpClient < Connection
|
||||
include EventMachine::Deferrable
|
||||
include EventMachine::Protocols::LineText2
|
||||
|
||||
def initialize
|
||||
@succeeded = nil
|
||||
@responder = nil
|
||||
@code = nil
|
||||
@msg = nil
|
||||
end
|
||||
|
||||
# :host => required String
|
||||
# a string containing the IP address or host name of the SMTP server to connect to.
|
||||
# :port => optional
|
||||
# defaults to 25.
|
||||
# :domain => required String
|
||||
# This is passed as the argument to the EHLO command.
|
||||
# :starttls => optional Boolean
|
||||
# If it evaluates true, then the client will initiate STARTTLS with
|
||||
# the server, and abort the connection if the negotiation doesn't succeed.
|
||||
# TODO, need to be able to pass certificate parameters with this option.
|
||||
# :auth => optional Hash of auth parameters
|
||||
# If not given, then no auth will be attempted.
|
||||
# (In that case, the connection will be aborted if the server requires auth.)
|
||||
# Specify the hash value :type to determine the auth type, along with additional parameters
|
||||
# depending on the type.
|
||||
# Currently only :type => :plain is supported. Pass additional parameters :username (String),
|
||||
# and :password (either a String or a Proc that will be called at auth-time).
|
||||
#
|
||||
# @example
|
||||
# :auth => {:type=>:plain, :username=>"mickey@disney.com", :password=>"mouse"}
|
||||
#
|
||||
# :from => required String
|
||||
# Specifies the sender of the message. Will be passed as the argument
|
||||
# to the MAIL FROM. Do NOT enclose the argument in angle-bracket (<>) characters.
|
||||
# The connection will abort if the server rejects the value.
|
||||
# :to => required String or Array of Strings
|
||||
# The recipient(s) of the message. Do NOT enclose
|
||||
# any of the values in angle-brackets (<>) characters. It's NOT a fatal error if one or more
|
||||
# recipients are rejected by the server. (Of course, if ALL of them are, the server will most
|
||||
# likely trigger an error when we try to send data.) An array of codes containing the status
|
||||
# of each requested recipient is available after the call completes. TODO, we should define
|
||||
# an overridable stub that will be called on rejection of a recipient or a sender, giving
|
||||
# user code the chance to try again or abort the connection.
|
||||
#
|
||||
# One of either :message, :content, or :header and :body is required:
|
||||
#
|
||||
# :message => String
|
||||
# A valid RFC2822 Internet Message.
|
||||
# :content => String
|
||||
# Raw data which MUST be in correct SMTP body format, with escaped leading dots and a trailing
|
||||
# dot line.
|
||||
# :header => String or Hash of values to be transmitted in the header of the message.
|
||||
# The hash keys are the names of the headers (do NOT append a trailing colon), and the values
|
||||
# are strings containing the header values. TODO, support Arrays of header values, which would
|
||||
# cause us to send that specific header line more than once.
|
||||
#
|
||||
# @example
|
||||
# :header => {"Subject" => "Bogus", "CC" => "myboss@example.com"}
|
||||
#
|
||||
# :body => Optional String or Array of Strings, defaults blank.
|
||||
# This will be passed as the body of the email message.
|
||||
# TODO, this needs to be significantly beefed up. As currently written, this requires the caller
|
||||
# to properly format the input into CRLF-delimited lines of 7-bit characters in the standard
|
||||
# SMTP transmission format. We need to be able to automatically convert binary data, and add
|
||||
# correct line-breaks to text data.
|
||||
#
|
||||
# :verbose => Optional.
|
||||
# If true, will cause a lot of information (including the server-side of the
|
||||
# conversation) to be dumped to $>.
|
||||
#
|
||||
def self.send args={}
|
||||
args[:port] ||= 25
|
||||
args[:body] ||= ""
|
||||
|
||||
=begin
|
||||
(I don't think it's possible for EM#connect to throw an exception under normal
|
||||
circumstances, so this original code is stubbed out. A connect-failure will result
|
||||
in the #unbind method being called without calling #connection_completed.)
|
||||
begin
|
||||
EventMachine.connect( args[:host], args[:port], self) {|c|
|
||||
# According to the EM docs, we will get here AFTER post_init is called.
|
||||
c.args = args
|
||||
c.set_comm_inactivity_timeout 60
|
||||
}
|
||||
rescue
|
||||
# We'll get here on a connect error. This code mimics the effect
|
||||
# of a call to invoke_internal_error. Would be great to DRY this up.
|
||||
# (Actually, it may be that we never get here, if EM#connect catches
|
||||
# its errors internally.)
|
||||
d = EM::DefaultDeferrable.new
|
||||
d.set_deferred_status(:failed, {:error=>[:connect, 500, "unable to connect to server"]})
|
||||
d
|
||||
end
|
||||
=end
|
||||
EventMachine.connect( args[:host], args[:port], self) {|c|
|
||||
# According to the EM docs, we will get here AFTER post_init is called.
|
||||
c.args = args
|
||||
c.set_comm_inactivity_timeout 60
|
||||
}
|
||||
end
|
||||
|
||||
attr_writer :args
|
||||
|
||||
# @private
|
||||
def post_init
|
||||
@return_values = OpenStruct.new
|
||||
@return_values.start_time = Time.now
|
||||
end
|
||||
|
||||
# @private
|
||||
def connection_completed
|
||||
@responder = :receive_signon
|
||||
@msg = []
|
||||
end
|
||||
|
||||
# We can get here in a variety of ways, all of them being failures unless
|
||||
# the @succeeded flag is set. If a protocol success was recorded, then don't
|
||||
# set a deferred success because the caller will already have done it
|
||||
# (no need to wait until the connection closes to invoke the callbacks).
|
||||
#
|
||||
# @private
|
||||
def unbind
|
||||
unless @succeeded
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status(:failed, @return_values)
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line ln
|
||||
$>.puts ln if @args[:verbose]
|
||||
@range = ln[0...1].to_i
|
||||
@code = ln[0...3].to_i
|
||||
@msg << ln[4..-1]
|
||||
unless ln[3...4] == '-'
|
||||
$>.puts @responder if @args[:verbose]
|
||||
send @responder
|
||||
@msg.clear
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# We encountered an error from the server and will close the connection.
|
||||
# Use the error and message the server returned.
|
||||
#
|
||||
def invoke_error
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status :failed, @return_values
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
# We encountered an error on our side of the protocol and will close the connection.
|
||||
# Use an extra-protocol error code (900) and use the message from the caller.
|
||||
#
|
||||
def invoke_internal_error msg = "???"
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = 900
|
||||
@return_values.message = msg
|
||||
set_deferred_status :failed, @return_values
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
def send_ehlo
|
||||
send_data "EHLO #{@args[:domain]}\r\n"
|
||||
end
|
||||
|
||||
def receive_signon
|
||||
return invoke_error unless @range == 2
|
||||
send_ehlo
|
||||
@responder = :receive_ehlo_response
|
||||
end
|
||||
def receive_ehlo_response
|
||||
return invoke_error unless @range == 2
|
||||
@server_caps = @msg
|
||||
invoke_starttls
|
||||
end
|
||||
|
||||
def invoke_starttls
|
||||
if @args[:starttls]
|
||||
# It would be more sociable to first ask if @server_caps contains
|
||||
# the string "STARTTLS" before we invoke it, but hey, life's too short.
|
||||
send_data "STARTTLS\r\n"
|
||||
@responder = :receive_starttls_response
|
||||
else
|
||||
invoke_auth
|
||||
end
|
||||
end
|
||||
def receive_starttls_response
|
||||
return invoke_error unless @range == 2
|
||||
start_tls
|
||||
invoke_ehlo_over_tls
|
||||
end
|
||||
|
||||
def invoke_ehlo_over_tls
|
||||
send_ehlo
|
||||
@responder = :receive_ehlo_over_tls_response
|
||||
end
|
||||
def receive_ehlo_over_tls_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_auth
|
||||
end
|
||||
|
||||
# Perform an authentication. If the caller didn't request one, then fall through
|
||||
# to the mail-from state.
|
||||
def invoke_auth
|
||||
if @args[:auth]
|
||||
if @args[:auth][:type] == :plain
|
||||
psw = @args[:auth][:password]
|
||||
if psw.respond_to?(:call)
|
||||
psw = psw.call
|
||||
end
|
||||
#str = Base64::encode64("\0#{@args[:auth][:username]}\0#{psw}").chomp
|
||||
str = ["\0#{@args[:auth][:username]}\0#{psw}"].pack("m").gsub(/\n/, '')
|
||||
send_data "AUTH PLAIN #{str}\r\n"
|
||||
@responder = :receive_auth_response
|
||||
else
|
||||
return invoke_internal_error("unsupported auth type")
|
||||
end
|
||||
else
|
||||
invoke_mail_from
|
||||
end
|
||||
end
|
||||
def receive_auth_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_mail_from
|
||||
end
|
||||
|
||||
def invoke_mail_from
|
||||
send_data "MAIL FROM: <#{@args[:from]}>\r\n"
|
||||
@responder = :receive_mail_from_response
|
||||
end
|
||||
def receive_mail_from_response
|
||||
return invoke_error unless @range == 2
|
||||
invoke_rcpt_to
|
||||
end
|
||||
|
||||
def invoke_rcpt_to
|
||||
@rcpt_responses ||= []
|
||||
l = @rcpt_responses.length
|
||||
to = @args[:to].is_a?(Array) ? @args[:to] : [@args[:to].to_s]
|
||||
if l < to.length
|
||||
send_data "RCPT TO: <#{to[l]}>\r\n"
|
||||
@responder = :receive_rcpt_to_response
|
||||
else
|
||||
e = @rcpt_responses.select {|rr| rr.last == 2}
|
||||
if e and e.length > 0
|
||||
invoke_data
|
||||
else
|
||||
invoke_error
|
||||
end
|
||||
end
|
||||
end
|
||||
def receive_rcpt_to_response
|
||||
@rcpt_responses << [@code, @msg, @range]
|
||||
invoke_rcpt_to
|
||||
end
|
||||
|
||||
def escape_leading_dots(s)
|
||||
s.gsub(/^\./, '..')
|
||||
end
|
||||
|
||||
def invoke_data
|
||||
send_data "DATA\r\n"
|
||||
@responder = :receive_data_response
|
||||
end
|
||||
def receive_data_response
|
||||
return invoke_error unless @range == 3
|
||||
|
||||
# The data to send can be given in either @args[:message], @args[:content], or the
|
||||
# combination of @args[:header] and @args[:body].
|
||||
#
|
||||
# - @args[:message] (String) MUST be a valid RFC2822 Internet Message
|
||||
#
|
||||
# - @args[:content] (String) MUST be in correct SMTP body format, with escaped
|
||||
# leading dots and a trailing dot line
|
||||
#
|
||||
# - @args[:header] (Hash or String)
|
||||
# - @args[:body] (Array or String)
|
||||
if @args[:message]
|
||||
send_data escape_leading_dots(@args[:message].to_s)
|
||||
send_data "\r\n.\r\n"
|
||||
elsif @args[:content]
|
||||
send_data @args[:content].to_s
|
||||
else
|
||||
# The header can be a hash or an array.
|
||||
if @args[:header].is_a?(Hash)
|
||||
(@args[:header] || {}).each {|k,v| send_data escape_leading_dots("#{k}: #{v}\r\n") }
|
||||
else
|
||||
send_data escape_leading_dots(@args[:header].to_s)
|
||||
end
|
||||
send_data "\r\n"
|
||||
|
||||
if @args[:body].is_a?(Array)
|
||||
@args[:body].each {|e| send_data escape_leading_dots(e)}
|
||||
else
|
||||
send_data escape_leading_dots(@args[:body].to_s)
|
||||
end
|
||||
|
||||
send_data "\r\n.\r\n"
|
||||
end
|
||||
|
||||
@responder = :receive_message_response
|
||||
end
|
||||
def receive_message_response
|
||||
return invoke_error unless @range == 2
|
||||
send_data "QUIT\r\n"
|
||||
close_connection_after_writing
|
||||
@succeeded = true
|
||||
@return_values.elapsed_time = Time.now - @return_values.start_time
|
||||
@return_values.responder = @responder
|
||||
@return_values.code = @code
|
||||
@return_values.message = @msg
|
||||
set_deferred_status :succeeded, @return_values
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,666 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# This is a protocol handler for the server side of SMTP.
|
||||
# It's NOT a complete SMTP server obeying all the semantics of servers conforming to
|
||||
# RFC2821. Rather, it uses overridable method stubs to communicate protocol states
|
||||
# and data to user code. User code is responsible for doing the right things with the
|
||||
# data in order to get complete and correct SMTP server behavior.
|
||||
#
|
||||
# Simple SMTP server example:
|
||||
#
|
||||
# class EmailServer < EM::P::SmtpServer
|
||||
# def receive_plain_auth(user, pass)
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def get_server_domain
|
||||
# "mock.smtp.server.local"
|
||||
# end
|
||||
#
|
||||
# def get_server_greeting
|
||||
# "mock smtp server greets you with impunity"
|
||||
# end
|
||||
#
|
||||
# def receive_sender(sender)
|
||||
# current.sender = sender
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_recipient(recipient)
|
||||
# current.recipient = recipient
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_message
|
||||
# current.received = true
|
||||
# current.completed_at = Time.now
|
||||
#
|
||||
# p [:received_email, current]
|
||||
# @current = OpenStruct.new
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_ehlo_domain(domain)
|
||||
# @ehlo_domain = domain
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_data_command
|
||||
# current.data = ""
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_data_chunk(data)
|
||||
# current.data << data.join("\n")
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def receive_transaction
|
||||
# if @ehlo_domain
|
||||
# current.ehlo_domain = @ehlo_domain
|
||||
# @ehlo_domain = nil
|
||||
# end
|
||||
# true
|
||||
# end
|
||||
#
|
||||
# def current
|
||||
# @current ||= OpenStruct.new
|
||||
# end
|
||||
#
|
||||
# def self.start(host = 'localhost', port = 1025)
|
||||
# require 'ostruct'
|
||||
# @server = EM.start_server host, port, self
|
||||
# end
|
||||
#
|
||||
# def self.stop
|
||||
# if @server
|
||||
# EM.stop_server @server
|
||||
# @server = nil
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.running?
|
||||
# !!@server
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{ EmailServer.start }
|
||||
#
|
||||
#--
|
||||
# Useful paragraphs in RFC-2821:
|
||||
# 4.3.2: Concise list of command-reply sequences, in essence a text representation
|
||||
# of the command state-machine.
|
||||
#
|
||||
# STARTTLS is defined in RFC2487.
|
||||
# Observe that there are important rules governing whether a publicly-referenced server
|
||||
# (meaning one whose Internet address appears in public MX records) may require the
|
||||
# non-optional use of TLS.
|
||||
# Non-optional TLS does not apply to EHLO, NOOP, QUIT or STARTTLS.
|
||||
class SmtpServer < EventMachine::Connection
|
||||
include Protocols::LineText2
|
||||
|
||||
HeloRegex = /\AHELO\s*/i
|
||||
EhloRegex = /\AEHLO\s*/i
|
||||
QuitRegex = /\AQUIT/i
|
||||
MailFromRegex = /\AMAIL FROM:\s*/i
|
||||
RcptToRegex = /\ARCPT TO:\s*/i
|
||||
DataRegex = /\ADATA/i
|
||||
NoopRegex = /\ANOOP/i
|
||||
RsetRegex = /\ARSET/i
|
||||
VrfyRegex = /\AVRFY\s+/i
|
||||
ExpnRegex = /\AEXPN\s+/i
|
||||
HelpRegex = /\AHELP/i
|
||||
StarttlsRegex = /\ASTARTTLS/i
|
||||
AuthRegex = /\AAUTH\s+/i
|
||||
|
||||
|
||||
# Class variable containing default parameters that can be overridden
|
||||
# in application code.
|
||||
# Individual objects of this class will make an instance-local copy of
|
||||
# the class variable, so that they can be reconfigured on a per-instance
|
||||
# basis.
|
||||
#
|
||||
# Chunksize is the number of data lines we'll buffer before
|
||||
# sending them to the application. TODO, make this user-configurable.
|
||||
#
|
||||
@@parms = {
|
||||
:chunksize => 4000,
|
||||
:verbose => false
|
||||
}
|
||||
def self.parms= parms={}
|
||||
@@parms.merge!(parms)
|
||||
end
|
||||
|
||||
|
||||
|
||||
def initialize *args
|
||||
super
|
||||
@parms = @@parms
|
||||
init_protocol_state
|
||||
end
|
||||
|
||||
def parms= parms={}
|
||||
@parms.merge!(parms)
|
||||
end
|
||||
|
||||
# In SMTP, the server talks first. But by a (perhaps flawed) axiom in EM,
|
||||
# #post_init will execute BEFORE the block passed to #start_server, for any
|
||||
# given accepted connection. Since in this class we'll probably be getting
|
||||
# a lot of initialization parameters, we want the guts of post_init to
|
||||
# run AFTER the application has initialized the connection object. So we
|
||||
# use a spawn to schedule the post_init to run later.
|
||||
# It's a little weird, I admit. A reasonable alternative would be to set
|
||||
# parameters as a class variable and to do that before accepting any connections.
|
||||
#
|
||||
# OBSOLETE, now we have @@parms. But the spawn is nice to keep as an illustration.
|
||||
#
|
||||
def post_init
|
||||
#send_data "220 #{get_server_greeting}\r\n" (ORIGINAL)
|
||||
#(EM.spawn {|x| x.send_data "220 #{x.get_server_greeting}\r\n"}).notify(self)
|
||||
(EM.spawn {|x| x.send_server_greeting}).notify(self)
|
||||
end
|
||||
|
||||
def send_server_greeting
|
||||
send_data "220 #{get_server_greeting}\r\n"
|
||||
end
|
||||
|
||||
def receive_line ln
|
||||
@@parms[:verbose] and $>.puts ">>> #{ln}"
|
||||
|
||||
return process_data_line(ln) if @state.include?(:data)
|
||||
return process_auth_line(ln) if @state.include?(:auth_incomplete)
|
||||
|
||||
case ln
|
||||
when EhloRegex
|
||||
process_ehlo $'.dup
|
||||
when HeloRegex
|
||||
process_helo $'.dup
|
||||
when MailFromRegex
|
||||
process_mail_from $'.dup
|
||||
when RcptToRegex
|
||||
process_rcpt_to $'.dup
|
||||
when DataRegex
|
||||
process_data
|
||||
when RsetRegex
|
||||
process_rset
|
||||
when VrfyRegex
|
||||
process_vrfy
|
||||
when ExpnRegex
|
||||
process_expn
|
||||
when HelpRegex
|
||||
process_help
|
||||
when NoopRegex
|
||||
process_noop
|
||||
when QuitRegex
|
||||
process_quit
|
||||
when StarttlsRegex
|
||||
process_starttls
|
||||
when AuthRegex
|
||||
process_auth $'.dup
|
||||
else
|
||||
process_unknown
|
||||
end
|
||||
end
|
||||
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_help
|
||||
send_data "250 Ok, but unimplemented\r\n"
|
||||
end
|
||||
|
||||
# RFC2821, 3.5.3 Meaning of VRFY or EXPN Success Response:
|
||||
# A server MUST NOT return a 250 code in response to a VRFY or EXPN
|
||||
# command unless it has actually verified the address. In particular,
|
||||
# a server MUST NOT return 250 if all it has done is to verify that the
|
||||
# syntax given is valid. In that case, 502 (Command not implemented)
|
||||
# or 500 (Syntax error, command unrecognized) SHOULD be returned.
|
||||
#
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_vrfy
|
||||
send_data "502 Command not implemented\r\n"
|
||||
end
|
||||
# TODO - implement this properly, the implementation is a stub!
|
||||
def process_expn
|
||||
send_data "502 Command not implemented\r\n"
|
||||
end
|
||||
|
||||
#--
|
||||
# This is called at several points to restore the protocol state
|
||||
# to a pre-transaction state. In essence, we "forget" having seen
|
||||
# any valid command except EHLO and STARTTLS.
|
||||
# We also have to callback user code, in case they're keeping track
|
||||
# of senders, recipients, and whatnot.
|
||||
#
|
||||
# We try to follow the convention of avoiding the verb "receive" for
|
||||
# internal method names except receive_line (which we inherit), and
|
||||
# using only receive_xxx for user-overridable stubs.
|
||||
#
|
||||
# init_protocol_state is called when we initialize the connection as
|
||||
# well as during reset_protocol_state. It does NOT call the user
|
||||
# override method. This enables us to promise the users that they
|
||||
# won't see the overridable fire except after EHLO and RSET, and
|
||||
# after a message has been received. Although the latter may be wrong.
|
||||
# The standard may allow multiple DATA segments with the same set of
|
||||
# senders and recipients.
|
||||
#
|
||||
def reset_protocol_state
|
||||
init_protocol_state
|
||||
s,@state = @state,[]
|
||||
@state << :starttls if s.include?(:starttls)
|
||||
@state << :ehlo if s.include?(:ehlo)
|
||||
receive_transaction
|
||||
end
|
||||
def init_protocol_state
|
||||
@state ||= []
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# EHLO/HELO is always legal, per the standard. On success
|
||||
# it always clears buffers and initiates a mail "transaction."
|
||||
# Which means that a MAIL FROM must follow.
|
||||
#
|
||||
# Per the standard, an EHLO/HELO or a RSET "initiates" an email
|
||||
# transaction. Thereafter, MAIL FROM must be received before
|
||||
# RCPT TO, before DATA. Not sure what this specific ordering
|
||||
# achieves semantically, but it does make it easier to
|
||||
# implement. We also support user-specified requirements for
|
||||
# STARTTLS and AUTH. We make it impossible to proceed to MAIL FROM
|
||||
# without fulfilling tls and/or auth, if the user specified either
|
||||
# or both as required. We need to check the extension standard
|
||||
# for auth to see if a credential is discarded after a RSET along
|
||||
# with all the rest of the state. We'll behave as if it is.
|
||||
# Now clearly, we can't discard tls after its been negotiated
|
||||
# without dropping the connection, so that flag doesn't get cleared.
|
||||
#
|
||||
def process_ehlo domain
|
||||
if receive_ehlo_domain domain
|
||||
send_data "250-#{get_server_domain}\r\n"
|
||||
if @@parms[:starttls]
|
||||
send_data "250-STARTTLS\r\n"
|
||||
end
|
||||
if @@parms[:auth]
|
||||
send_data "250-AUTH PLAIN\r\n"
|
||||
end
|
||||
send_data "250-NO-SOLICITING\r\n"
|
||||
# TODO, size needs to be configurable.
|
||||
send_data "250 SIZE 20000000\r\n"
|
||||
reset_protocol_state
|
||||
@state << :ehlo
|
||||
else
|
||||
send_data "550 Requested action not taken\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_helo domain
|
||||
if receive_ehlo_domain domain.dup
|
||||
send_data "250 #{get_server_domain}\r\n"
|
||||
reset_protocol_state
|
||||
@state << :ehlo
|
||||
else
|
||||
send_data "550 Requested action not taken\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_quit
|
||||
send_data "221 Ok\r\n"
|
||||
close_connection_after_writing
|
||||
end
|
||||
|
||||
def process_noop
|
||||
send_data "250 Ok\r\n"
|
||||
end
|
||||
|
||||
def process_unknown
|
||||
send_data "500 Unknown command\r\n"
|
||||
end
|
||||
|
||||
#--
|
||||
# So far, only AUTH PLAIN is supported but we should do at least LOGIN as well.
|
||||
# TODO, support clients that send AUTH PLAIN with no parameter, expecting a 3xx
|
||||
# response and a continuation of the auth conversation.
|
||||
#
|
||||
def process_auth str
|
||||
if @state.include?(:auth)
|
||||
send_data "503 auth already issued\r\n"
|
||||
elsif str =~ /\APLAIN\s?/i
|
||||
if $'.length == 0
|
||||
# we got a partial response, so let the client know to send the rest
|
||||
@state << :auth_incomplete
|
||||
send_data("334 \r\n")
|
||||
else
|
||||
# we got the initial response, so go ahead & process it
|
||||
process_auth_line($')
|
||||
end
|
||||
#elsif str =~ /\ALOGIN\s+/i
|
||||
else
|
||||
send_data "504 auth mechanism not available\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def process_auth_line(line)
|
||||
plain = line.unpack("m").first
|
||||
_,user,psw = plain.split("\000")
|
||||
|
||||
succeeded = proc {
|
||||
send_data "235 authentication ok\r\n"
|
||||
@state << :auth
|
||||
}
|
||||
failed = proc {
|
||||
send_data "535 invalid authentication\r\n"
|
||||
}
|
||||
auth = receive_plain_auth user,psw
|
||||
|
||||
if auth.respond_to?(:callback)
|
||||
auth.callback(&succeeded)
|
||||
auth.errback(&failed)
|
||||
else
|
||||
(auth ? succeeded : failed).call
|
||||
end
|
||||
|
||||
@state.delete :auth_incomplete
|
||||
end
|
||||
|
||||
#--
|
||||
# Unusually, we can deal with a Deferrable returned from the user application.
|
||||
# This was added to deal with a special case in a particular application, but
|
||||
# it would be a nice idea to add it to the other user-code callbacks.
|
||||
#
|
||||
def process_data
|
||||
unless @state.include?(:rcpt)
|
||||
send_data "503 Operation sequence error\r\n"
|
||||
else
|
||||
succeeded = proc {
|
||||
send_data "354 Send it\r\n"
|
||||
@state << :data
|
||||
@databuffer = []
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 Operation failed\r\n"
|
||||
}
|
||||
|
||||
d = receive_data_command
|
||||
|
||||
if d.respond_to?(:callback)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_rset
|
||||
reset_protocol_state
|
||||
receive_reset
|
||||
send_data "250 Ok\r\n"
|
||||
end
|
||||
|
||||
def unbind
|
||||
connection_ended
|
||||
end
|
||||
|
||||
#--
|
||||
# STARTTLS may not be issued before EHLO, or unless the user has chosen
|
||||
# to support it.
|
||||
#
|
||||
# If :starttls_options is present and :starttls is set in the parms
|
||||
# pass the options in :starttls_options to start_tls. Do this if you want to use
|
||||
# your own certificate
|
||||
# e.g. {:cert_chain_file => "/etc/ssl/cert.pem", :private_key_file => "/etc/ssl/private/cert.key"}
|
||||
|
||||
def process_starttls
|
||||
if @@parms[:starttls]
|
||||
if @state.include?(:starttls)
|
||||
send_data "503 TLS Already negotiated\r\n"
|
||||
elsif ! @state.include?(:ehlo)
|
||||
send_data "503 EHLO required before STARTTLS\r\n"
|
||||
else
|
||||
send_data "220 Start TLS negotiation\r\n"
|
||||
start_tls(@@parms[:starttls_options] || {})
|
||||
@state << :starttls
|
||||
end
|
||||
else
|
||||
process_unknown
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#--
|
||||
# Requiring TLS is touchy, cf RFC2784.
|
||||
# Requiring AUTH seems to be much more reasonable.
|
||||
# We don't currently support any notion of deriving an authentication from the TLS
|
||||
# negotiation, although that would certainly be reasonable.
|
||||
# We DON'T allow MAIL FROM to be given twice.
|
||||
# We DON'T enforce all the various rules for validating the sender or
|
||||
# the reverse-path (like whether it should be null), and notifying the reverse
|
||||
# path in case of delivery problems. All of that is left to the calling application.
|
||||
#
|
||||
def process_mail_from sender
|
||||
if (@@parms[:starttls]==:required and !@state.include?(:starttls))
|
||||
send_data "550 This server requires STARTTLS before MAIL FROM\r\n"
|
||||
elsif (@@parms[:auth]==:required and !@state.include?(:auth))
|
||||
send_data "550 This server requires authentication before MAIL FROM\r\n"
|
||||
elsif @state.include?(:mail_from)
|
||||
send_data "503 MAIL already given\r\n"
|
||||
else
|
||||
unless receive_sender sender
|
||||
send_data "550 sender is unacceptable\r\n"
|
||||
else
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :mail_from
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Since we require :mail_from to have been seen before we process RCPT TO,
|
||||
# we don't need to repeat the tests for TLS and AUTH.
|
||||
# Note that we don't remember or do anything else with the recipients.
|
||||
# All of that is on the user code.
|
||||
# TODO: we should enforce user-definable limits on the total number of
|
||||
# recipients per transaction.
|
||||
# We might want to make sure that a given recipient is only seen once, but
|
||||
# for now we'll let that be the user's problem.
|
||||
#
|
||||
# User-written code can return a deferrable from receive_recipient.
|
||||
#
|
||||
def process_rcpt_to rcpt
|
||||
unless @state.include?(:mail_from)
|
||||
send_data "503 MAIL is required before RCPT\r\n"
|
||||
else
|
||||
succeeded = proc {
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :rcpt unless @state.include?(:rcpt)
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 recipient is unacceptable\r\n"
|
||||
}
|
||||
|
||||
d = receive_recipient rcpt
|
||||
|
||||
if d.respond_to?(:set_deferred_status)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
|
||||
=begin
|
||||
unless receive_recipient rcpt
|
||||
send_data "550 recipient is unacceptable\r\n"
|
||||
else
|
||||
send_data "250 Ok\r\n"
|
||||
@state << :rcpt unless @state.include?(:rcpt)
|
||||
end
|
||||
=end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Send the incoming data to the application one chunk at a time, rather than
|
||||
# one line at a time. That lets the application be a little more flexible about
|
||||
# storing to disk, etc.
|
||||
# Since we clear the chunk array every time we submit it, the caller needs to be
|
||||
# aware to do things like dup it if he wants to keep it around across calls.
|
||||
#
|
||||
# Resets the transaction upon disposition of the incoming message.
|
||||
# RFC5321 says this about the MAIL FROM command:
|
||||
# "This command tells the SMTP-receiver that a new mail transaction is
|
||||
# starting and to reset all its state tables and buffers, including any
|
||||
# recipients or mail data."
|
||||
#
|
||||
# Equivalent behaviour is implemented by resetting after a completed transaction.
|
||||
#
|
||||
# User-written code can return a Deferrable as a response from receive_message.
|
||||
#
|
||||
def process_data_line ln
|
||||
if ln == "."
|
||||
if @databuffer.length > 0
|
||||
receive_data_chunk @databuffer
|
||||
@databuffer.clear
|
||||
end
|
||||
|
||||
|
||||
succeeded = proc {
|
||||
send_data "250 Message accepted\r\n"
|
||||
reset_protocol_state
|
||||
}
|
||||
failed = proc {
|
||||
send_data "550 Message rejected\r\n"
|
||||
reset_protocol_state
|
||||
}
|
||||
d = receive_message
|
||||
|
||||
if d.respond_to?(:set_deferred_status)
|
||||
d.callback(&succeeded)
|
||||
d.errback(&failed)
|
||||
else
|
||||
(d ? succeeded : failed).call
|
||||
end
|
||||
|
||||
@state.delete :data
|
||||
else
|
||||
# slice off leading . if any
|
||||
ln.slice!(0...1) if ln[0] == ?.
|
||||
@databuffer << ln
|
||||
if @databuffer.length > @@parms[:chunksize]
|
||||
receive_data_chunk @databuffer
|
||||
@databuffer.clear
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#------------------------------------------
|
||||
# Everything from here on can be overridden in user code.
|
||||
|
||||
# The greeting returned in the initial connection message to the client.
|
||||
def get_server_greeting
|
||||
"EventMachine SMTP Server"
|
||||
end
|
||||
# The domain name returned in the first line of the response to a
|
||||
# successful EHLO or HELO command.
|
||||
def get_server_domain
|
||||
"Ok EventMachine SMTP Server"
|
||||
end
|
||||
|
||||
# A false response from this user-overridable method will cause a
|
||||
# 550 error to be returned to the remote client.
|
||||
#
|
||||
def receive_ehlo_domain domain
|
||||
true
|
||||
end
|
||||
|
||||
# Return true or false to indicate that the authentication is acceptable.
|
||||
def receive_plain_auth user, password
|
||||
true
|
||||
end
|
||||
|
||||
# Receives the argument of the MAIL FROM command. Return false to
|
||||
# indicate to the remote client that the sender is not accepted.
|
||||
# This can only be successfully called once per transaction.
|
||||
#
|
||||
def receive_sender sender
|
||||
true
|
||||
end
|
||||
|
||||
# Receives the argument of a RCPT TO command. Can be given multiple
|
||||
# times per transaction. Return false to reject the recipient.
|
||||
#
|
||||
def receive_recipient rcpt
|
||||
true
|
||||
end
|
||||
|
||||
# Sent when the remote peer issues the RSET command.
|
||||
# Since RSET is not allowed to fail (according to the protocol),
|
||||
# we ignore any return value from user overrides of this method.
|
||||
#
|
||||
def receive_reset
|
||||
end
|
||||
|
||||
# Sent when the remote peer has ended the connection.
|
||||
#
|
||||
def connection_ended
|
||||
end
|
||||
|
||||
# Called when the remote peer sends the DATA command.
|
||||
# Returning false will cause us to send a 550 error to the peer.
|
||||
# This can be useful for dealing with problems that arise from processing
|
||||
# the whole set of sender and recipients.
|
||||
#
|
||||
def receive_data_command
|
||||
true
|
||||
end
|
||||
|
||||
# Sent when data from the remote peer is available. The size can be controlled
|
||||
# by setting the :chunksize parameter. This call can be made multiple times.
|
||||
# The goal is to strike a balance between sending the data to the application one
|
||||
# line at a time, and holding all of a very large message in memory.
|
||||
#
|
||||
def receive_data_chunk data
|
||||
@smtps_msg_size ||= 0
|
||||
@smtps_msg_size += data.join.length
|
||||
STDERR.write "<#{@smtps_msg_size}>"
|
||||
end
|
||||
|
||||
# Sent after a message has been completely received. User code
|
||||
# must return true or false to indicate whether the message has
|
||||
# been accepted for delivery.
|
||||
def receive_message
|
||||
@@parms[:verbose] and $>.puts "Received complete message"
|
||||
true
|
||||
end
|
||||
|
||||
# This is called when the protocol state is reset. It happens
|
||||
# when the remote client calls EHLO/HELO or RSET.
|
||||
def receive_transaction
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
module EventMachine
|
||||
module Protocols
|
||||
# Basic SOCKS v4 client implementation
|
||||
#
|
||||
# Use as you would any regular connection:
|
||||
#
|
||||
# class MyConn < EM::P::Socks4
|
||||
# def post_init
|
||||
# send_data("sup")
|
||||
# end
|
||||
#
|
||||
# def receive_data(data)
|
||||
# send_data("you said: #{data}")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.connect socks_host, socks_port, MyConn, host, port
|
||||
#
|
||||
class Socks4 < Connection
|
||||
def initialize(host, port)
|
||||
@host = Socket.gethostbyname(host).last
|
||||
@port = port
|
||||
@socks_error_code = nil
|
||||
@buffer = ''
|
||||
setup_methods
|
||||
end
|
||||
|
||||
def setup_methods
|
||||
class << self
|
||||
def post_init; socks_post_init; end
|
||||
def receive_data(*a); socks_receive_data(*a); end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_methods
|
||||
class << self
|
||||
remove_method :post_init
|
||||
remove_method :receive_data
|
||||
end
|
||||
end
|
||||
|
||||
def socks_post_init
|
||||
header = [4, 1, @port, @host, 0].flatten.pack("CCnA4C")
|
||||
send_data(header)
|
||||
end
|
||||
|
||||
def socks_receive_data(data)
|
||||
@buffer << data
|
||||
return if @buffer.size < 8
|
||||
|
||||
header_resp = @buffer.slice! 0, 8
|
||||
_, r = header_resp.unpack("cc")
|
||||
if r != 90
|
||||
@socks_error_code = r
|
||||
close_connection
|
||||
return
|
||||
end
|
||||
|
||||
restore_methods
|
||||
|
||||
post_init
|
||||
receive_data(@buffer) unless @buffer.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,205 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 15 November 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# Implements Stomp (http://docs.codehaus.org/display/STOMP/Protocol).
|
||||
#
|
||||
# == Usage example
|
||||
#
|
||||
# module StompClient
|
||||
# include EM::Protocols::Stomp
|
||||
#
|
||||
# def connection_completed
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
# end
|
||||
#
|
||||
# def receive_msg msg
|
||||
# if msg.command == "CONNECTED"
|
||||
# subscribe '/some/topic'
|
||||
# else
|
||||
# p ['got a message', msg]
|
||||
# puts msg.body
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EM.run{
|
||||
# EM.connect 'localhost', 61613, StompClient
|
||||
# }
|
||||
#
|
||||
module Stomp
|
||||
include LineText2
|
||||
|
||||
class Message
|
||||
# The command associated with the message, usually 'CONNECTED' or 'MESSAGE'
|
||||
attr_accessor :command
|
||||
# Hash containing headers such as destination and message-id
|
||||
attr_accessor :header
|
||||
alias :headers :header
|
||||
# Body of the message
|
||||
attr_accessor :body
|
||||
|
||||
# @private
|
||||
def initialize
|
||||
@header = {}
|
||||
@state = :precommand
|
||||
@content_length = nil
|
||||
end
|
||||
# @private
|
||||
def consume_line line
|
||||
if @state == :precommand
|
||||
unless line =~ /\A\s*\Z/
|
||||
@command = line
|
||||
@state = :headers
|
||||
end
|
||||
elsif @state == :headers
|
||||
if line == ""
|
||||
if @content_length
|
||||
yield( [:sized_text, @content_length+1] )
|
||||
else
|
||||
@state = :body
|
||||
yield( [:unsized_text] )
|
||||
end
|
||||
elsif line =~ /\A([^:]+):(.+)\Z/
|
||||
k = $1.dup.strip
|
||||
v = $2.dup.strip
|
||||
@header[k] = v
|
||||
if k == "content-length"
|
||||
@content_length = v.to_i
|
||||
end
|
||||
else
|
||||
# This is a protocol error. How to signal it?
|
||||
end
|
||||
elsif @state == :body
|
||||
@body = line
|
||||
yield( [:dispatch] )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def send_frame verb, headers={}, body=""
|
||||
body = body.to_s
|
||||
ary = [verb, "\n"]
|
||||
body_bytesize = body.bytesize if body.respond_to? :bytesize
|
||||
body_bytesize ||= body.size
|
||||
headers.each {|k,v| ary << "#{k}:#{v}\n" }
|
||||
ary << "content-length: #{body_bytesize}\n"
|
||||
ary << "content-type: text/plain; charset=UTF-8\n" unless headers.has_key? 'content-type'
|
||||
ary << "\n"
|
||||
ary << body
|
||||
ary << "\0"
|
||||
send_data ary.join
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_line line
|
||||
@stomp_initialized || init_message_reader
|
||||
@stomp_message.consume_line(line) {|outcome|
|
||||
if outcome.first == :sized_text
|
||||
set_text_mode outcome[1]
|
||||
elsif outcome.first == :unsized_text
|
||||
set_delimiter "\0"
|
||||
elsif outcome.first == :dispatch
|
||||
receive_msg(@stomp_message) if respond_to?(:receive_msg)
|
||||
init_message_reader
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @private
|
||||
def receive_binary_data data
|
||||
@stomp_message.body = data[0..-2]
|
||||
receive_msg(@stomp_message) if respond_to?(:receive_msg)
|
||||
init_message_reader
|
||||
end
|
||||
|
||||
# @private
|
||||
def init_message_reader
|
||||
@stomp_initialized = true
|
||||
set_delimiter "\n"
|
||||
set_line_mode
|
||||
@stomp_message = Message.new
|
||||
end
|
||||
|
||||
# Invoked with an incoming Stomp::Message received from the STOMP server
|
||||
def receive_msg msg
|
||||
# stub, overwrite this in your handler
|
||||
end
|
||||
|
||||
# CONNECT command, for authentication
|
||||
#
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
#
|
||||
def connect parms={}
|
||||
send_frame "CONNECT", parms
|
||||
end
|
||||
|
||||
# SEND command, for publishing messages to a topic
|
||||
#
|
||||
# send '/topic/name', 'some message here'
|
||||
#
|
||||
def send destination, body, parms={}
|
||||
send_frame "SEND", parms.merge( :destination=>destination ), body.to_s
|
||||
end
|
||||
|
||||
# SUBSCRIBE command, for subscribing to topics
|
||||
#
|
||||
# subscribe '/topic/name', false
|
||||
#
|
||||
def subscribe dest, ack=false
|
||||
send_frame "SUBSCRIBE", {:destination=>dest, :ack=>(ack ? "client" : "auto")}
|
||||
end
|
||||
|
||||
# ACK command, for acknowledging receipt of messages
|
||||
#
|
||||
# module StompClient
|
||||
# include EM::P::Stomp
|
||||
#
|
||||
# def connection_completed
|
||||
# connect :login => 'guest', :passcode => 'guest'
|
||||
# # subscribe with ack mode
|
||||
# subscribe '/some/topic', true
|
||||
# end
|
||||
#
|
||||
# def receive_msg msg
|
||||
# if msg.command == "MESSAGE"
|
||||
# ack msg.headers['message-id']
|
||||
# puts msg.body
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def ack msgid
|
||||
send_frame "ACK", 'message-id'=> msgid
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 16 July 2006
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
module Protocols
|
||||
|
||||
# @private
|
||||
class TcpConnectTester < Connection
|
||||
include EventMachine::Deferrable
|
||||
|
||||
def self.test( host, port )
|
||||
EventMachine.connect( host, port, self )
|
||||
end
|
||||
|
||||
def post_init
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def connection_completed
|
||||
@completed = true
|
||||
set_deferred_status :succeeded, (Time.now - @start_time)
|
||||
close_connection
|
||||
end
|
||||
|
||||
def unbind
|
||||
set_deferred_status :failed, (Time.now - @start_time) unless @completed
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
module EventMachine
|
||||
# A cross thread, reactor scheduled, linear queue.
|
||||
#
|
||||
# This class provides a simple queue abstraction on top of the reactor
|
||||
# scheduler. It services two primary purposes:
|
||||
#
|
||||
# * API sugar for stateful protocols
|
||||
# * Pushing processing onto the reactor thread
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# q = EM::Queue.new
|
||||
# q.push('one', 'two', 'three')
|
||||
# 3.times do
|
||||
# q.pop { |msg| puts(msg) }
|
||||
# end
|
||||
#
|
||||
class Queue
|
||||
def initialize
|
||||
@sink = []
|
||||
@drain = []
|
||||
@popq = []
|
||||
end
|
||||
|
||||
# Pop items off the queue, running the block on the reactor thread. The pop
|
||||
# will not happen immediately, but at some point in the future, either in
|
||||
# the next tick, if the queue has data, or when the queue is populated.
|
||||
#
|
||||
# @return [NilClass] nil
|
||||
def pop(*a, &b)
|
||||
cb = EM::Callback(*a, &b)
|
||||
EM.schedule do
|
||||
if @drain.empty?
|
||||
@drain = @sink
|
||||
@sink = []
|
||||
end
|
||||
if @drain.empty?
|
||||
@popq << cb
|
||||
else
|
||||
cb.call @drain.shift
|
||||
end
|
||||
end
|
||||
nil # Always returns nil
|
||||
end
|
||||
|
||||
# Push items onto the queue in the reactor thread. The items will not appear
|
||||
# in the queue immediately, but will be scheduled for addition during the
|
||||
# next reactor tick.
|
||||
def push(*items)
|
||||
EM.schedule do
|
||||
@sink.push(*items)
|
||||
unless @popq.empty?
|
||||
@drain = @sink
|
||||
@sink = []
|
||||
@popq.shift.call @drain.shift until @drain.empty? || @popq.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :<< :push
|
||||
|
||||
# @return [Boolean]
|
||||
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
|
||||
def empty?
|
||||
@drain.empty? && @sink.empty?
|
||||
end
|
||||
|
||||
# @return [Integer] Queue size
|
||||
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
|
||||
def size
|
||||
@drain.size + @sink.size
|
||||
end
|
||||
|
||||
# @return [Integer] Waiting size
|
||||
# @note This is a peek at the number of jobs that are currently waiting on the Queue
|
||||
def num_waiting
|
||||
@popq.size
|
||||
end
|
||||
|
||||
end # Queue
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,232 @@
|
||||
module EventMachine
|
||||
module DNS
|
||||
class Resolver
|
||||
|
||||
def self.windows?
|
||||
if RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
|
||||
require 'win32/resolv'
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
HOSTS_FILE = windows? ? Win32::Resolv.get_hosts_path : '/etc/hosts'
|
||||
|
||||
@hosts = nil
|
||||
@nameservers = nil
|
||||
@socket = nil
|
||||
|
||||
def self.resolve(hostname)
|
||||
Request.new(socket, hostname)
|
||||
end
|
||||
|
||||
def self.socket
|
||||
if @socket && @socket.error?
|
||||
@socket = Socket.open
|
||||
else
|
||||
@socket ||= Socket.open
|
||||
end
|
||||
end
|
||||
|
||||
def self.nameservers=(ns)
|
||||
@nameservers = ns
|
||||
end
|
||||
|
||||
def self.nameservers
|
||||
return @nameservers if @nameservers
|
||||
|
||||
if windows?
|
||||
_, ns = Win32::Resolv.get_resolv_info
|
||||
return @nameservers = ns || []
|
||||
end
|
||||
|
||||
@nameservers = []
|
||||
IO.readlines('/etc/resolv.conf').each do |line|
|
||||
if line =~ /^nameserver (.+)$/
|
||||
@nameservers << $1.split(/\s+/).first
|
||||
end
|
||||
end
|
||||
|
||||
@nameservers
|
||||
rescue
|
||||
@nameservers = []
|
||||
end
|
||||
|
||||
def self.nameserver
|
||||
nameservers.shuffle.first
|
||||
end
|
||||
|
||||
def self.hosts
|
||||
return @hosts if @hosts
|
||||
|
||||
@hosts = {}
|
||||
IO.readlines(HOSTS_FILE).each do |line|
|
||||
next if line =~ /^#/
|
||||
addr, host = line.split(/\s+/)
|
||||
|
||||
next unless addr && host
|
||||
@hosts[host] ||= []
|
||||
@hosts[host] << addr
|
||||
end
|
||||
|
||||
@hosts
|
||||
rescue
|
||||
@hosts = {}
|
||||
end
|
||||
end
|
||||
|
||||
class RequestIdAlreadyUsed < RuntimeError; end
|
||||
|
||||
class Socket < EventMachine::Connection
|
||||
def self.open
|
||||
EventMachine::open_datagram_socket('0.0.0.0', 0, self)
|
||||
end
|
||||
|
||||
def initialize
|
||||
@nameserver = nil
|
||||
end
|
||||
|
||||
def post_init
|
||||
@requests = {}
|
||||
end
|
||||
|
||||
def start_timer
|
||||
@timer ||= EM.add_periodic_timer(0.1, &method(:tick))
|
||||
end
|
||||
|
||||
def stop_timer
|
||||
EM.cancel_timer(@timer)
|
||||
@timer = nil
|
||||
end
|
||||
|
||||
def unbind
|
||||
end
|
||||
|
||||
def tick
|
||||
@requests.each do |id,req|
|
||||
req.tick
|
||||
end
|
||||
end
|
||||
|
||||
def register_request(id, req)
|
||||
if @requests.has_key?(id)
|
||||
raise RequestIdAlreadyUsed
|
||||
else
|
||||
@requests[id] = req
|
||||
end
|
||||
|
||||
start_timer
|
||||
end
|
||||
|
||||
def deregister_request(id, req)
|
||||
@requests.delete(id)
|
||||
stop_timer if @requests.length == 0
|
||||
end
|
||||
|
||||
def send_packet(pkt)
|
||||
send_datagram(pkt, nameserver, 53)
|
||||
end
|
||||
|
||||
def nameserver=(ns)
|
||||
@nameserver = ns
|
||||
end
|
||||
|
||||
def nameserver
|
||||
@nameserver || Resolver.nameserver
|
||||
end
|
||||
|
||||
# Decodes the packet, looks for the request and passes the
|
||||
# response over to the requester
|
||||
def receive_data(data)
|
||||
msg = nil
|
||||
begin
|
||||
msg = Resolv::DNS::Message.decode data
|
||||
rescue
|
||||
else
|
||||
req = @requests[msg.id]
|
||||
if req
|
||||
@requests.delete(msg.id)
|
||||
stop_timer if @requests.length == 0
|
||||
req.receive_answer(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
include Deferrable
|
||||
attr_accessor :retry_interval, :max_tries
|
||||
|
||||
def initialize(socket, hostname)
|
||||
@socket = socket
|
||||
@hostname = hostname
|
||||
@tries = 0
|
||||
@last_send = Time.at(0)
|
||||
@retry_interval = 3
|
||||
@max_tries = 5
|
||||
|
||||
if addrs = Resolver.hosts[hostname]
|
||||
succeed addrs
|
||||
else
|
||||
EM.next_tick { tick }
|
||||
end
|
||||
end
|
||||
|
||||
def tick
|
||||
# Break early if nothing to do
|
||||
return if @last_send + @retry_interval > Time.now
|
||||
if @tries < @max_tries
|
||||
send
|
||||
else
|
||||
@socket.deregister_request(@id, self)
|
||||
fail 'retries exceeded'
|
||||
end
|
||||
end
|
||||
|
||||
def receive_answer(msg)
|
||||
addrs = []
|
||||
msg.each_answer do |name,ttl,data|
|
||||
if data.kind_of?(Resolv::DNS::Resource::IN::A) ||
|
||||
data.kind_of?(Resolv::DNS::Resource::IN::AAAA)
|
||||
addrs << data.address.to_s
|
||||
end
|
||||
end
|
||||
|
||||
if addrs.empty?
|
||||
fail "rcode=#{msg.rcode}"
|
||||
else
|
||||
succeed addrs
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send
|
||||
@tries += 1
|
||||
@last_send = Time.now
|
||||
@socket.send_packet(packet.encode)
|
||||
end
|
||||
|
||||
def id
|
||||
begin
|
||||
@id = rand(65535)
|
||||
@socket.register_request(@id, self)
|
||||
rescue RequestIdAlreadyUsed
|
||||
retry
|
||||
end unless defined?(@id)
|
||||
|
||||
@id
|
||||
end
|
||||
|
||||
def packet
|
||||
msg = Resolv::DNS::Message.new
|
||||
msg.id = id
|
||||
msg.rd = 1
|
||||
msg.add_question @hostname, Resolv::DNS::Resource::IN::A
|
||||
msg
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
#--
|
||||
#
|
||||
# Author:: Francis Cianfrocca (gmail: blackhedd)
|
||||
# Homepage:: http://rubyeventmachine.com
|
||||
# Date:: 25 Aug 2007
|
||||
#
|
||||
# See EventMachine and EventMachine::Connection for documentation and
|
||||
# usage examples.
|
||||
#
|
||||
#----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
|
||||
# Gmail: blackhedd
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of either: 1) the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2 of the
|
||||
# License, or (at your option) any later version; or 2) Ruby's License.
|
||||
#
|
||||
# See the file COPYING for complete licensing information.
|
||||
#
|
||||
#---------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
|
||||
module EventMachine
|
||||
# Support for Erlang-style processes.
|
||||
#
|
||||
class SpawnedProcess
|
||||
# Send a message to the spawned process
|
||||
def notify *x
|
||||
me = self
|
||||
EM.next_tick {
|
||||
# A notification executes in the context of this
|
||||
# SpawnedProcess object. That makes self and notify
|
||||
# work as one would expect.
|
||||
#
|
||||
y = me.call(*x)
|
||||
if y and y.respond_to?(:pull_out_yield_block)
|
||||
a,b = y.pull_out_yield_block
|
||||
set_receiver a
|
||||
self.notify if b
|
||||
end
|
||||
}
|
||||
end
|
||||
alias_method :resume, :notify
|
||||
alias_method :run, :notify # for formulations like (EM.spawn {xxx}).run
|
||||
|
||||
def set_receiver blk
|
||||
(class << self ; self ; end).class_eval do
|
||||
remove_method :call if method_defined? :call
|
||||
define_method :call, blk
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# @private
|
||||
class YieldBlockFromSpawnedProcess
|
||||
def initialize block, notify
|
||||
@block = [block,notify]
|
||||
end
|
||||
def pull_out_yield_block
|
||||
@block
|
||||
end
|
||||
end
|
||||
|
||||
# Spawn an erlang-style process
|
||||
def self.spawn &block
|
||||
s = SpawnedProcess.new
|
||||
s.set_receiver block
|
||||
s
|
||||
end
|
||||
|
||||
# @private
|
||||
def self.yield &block
|
||||
return YieldBlockFromSpawnedProcess.new( block, false )
|
||||
end
|
||||
|
||||
# @private
|
||||
def self.yield_and_notify &block
|
||||
return YieldBlockFromSpawnedProcess.new( block, true )
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
module EventMachine
|
||||
# Streams a file over a given connection. Streaming begins once the object is
|
||||
# instantiated. Typically FileStreamer instances are not reused.
|
||||
#
|
||||
# Streaming uses buffering for files larger than 16K and uses so-called fast file reader (a C++ extension)
|
||||
# if available (it is part of eventmachine gem itself).
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# module FileSender
|
||||
# def post_init
|
||||
# streamer = EventMachine::FileStreamer.new(self, '/tmp/bigfile.tar')
|
||||
# streamer.callback{
|
||||
# # file was sent successfully
|
||||
# close_connection_after_writing
|
||||
# }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# @author Francis Cianfrocca
|
||||
class FileStreamer
|
||||
include Deferrable
|
||||
|
||||
# Use mapped streamer for files bigger than 16k
|
||||
MappingThreshold = 16384
|
||||
# Wait until next tick to send more data when 50k is still in the outgoing buffer
|
||||
BackpressureLevel = 50000
|
||||
# Send 16k chunks at a time
|
||||
ChunkSize = 16384
|
||||
|
||||
# @param [EventMachine::Connection] connection
|
||||
# @param [String] filename File path
|
||||
#
|
||||
# @option args [Boolean] :http_chunks (false) Use HTTP 1.1 style chunked-encoding semantics.
|
||||
def initialize connection, filename, args = {}
|
||||
@connection = connection
|
||||
@http_chunks = args[:http_chunks]
|
||||
|
||||
if File.exist?(filename)
|
||||
@size = File.size(filename)
|
||||
if @size <= MappingThreshold
|
||||
stream_without_mapping filename
|
||||
else
|
||||
stream_with_mapping filename
|
||||
end
|
||||
else
|
||||
fail "file not found"
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def stream_without_mapping filename
|
||||
if @http_chunks
|
||||
@connection.send_data "#{@size.to_s(16)}\r\n"
|
||||
@connection.send_file_data filename
|
||||
@connection.send_data "\r\n0\r\n\r\n"
|
||||
else
|
||||
@connection.send_file_data filename
|
||||
end
|
||||
succeed
|
||||
end
|
||||
private :stream_without_mapping
|
||||
|
||||
# @private
|
||||
def stream_with_mapping filename
|
||||
ensure_mapping_extension_is_present
|
||||
|
||||
@position = 0
|
||||
@mapping = EventMachine::FastFileReader::Mapper.new filename
|
||||
stream_one_chunk
|
||||
end
|
||||
private :stream_with_mapping
|
||||
|
||||
# Used internally to stream one chunk at a time over multiple reactor ticks
|
||||
# @private
|
||||
def stream_one_chunk
|
||||
loop {
|
||||
if @position < @size
|
||||
if @connection.get_outbound_data_size > BackpressureLevel
|
||||
EventMachine::next_tick {stream_one_chunk}
|
||||
break
|
||||
else
|
||||
len = @size - @position
|
||||
len = ChunkSize if (len > ChunkSize)
|
||||
|
||||
@connection.send_data( "#{len.to_s(16)}\r\n" ) if @http_chunks
|
||||
@connection.send_data( @mapping.get_chunk( @position, len ))
|
||||
@connection.send_data("\r\n") if @http_chunks
|
||||
|
||||
@position += len
|
||||
end
|
||||
else
|
||||
@connection.send_data "0\r\n\r\n" if @http_chunks
|
||||
@mapping.close
|
||||
succeed
|
||||
break
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# We use an outboard extension class to get memory-mapped files.
|
||||
# It's outboard to avoid polluting the core distro, but that means
|
||||
# there's a "hidden" dependency on it. The first time we get here in
|
||||
# any run, try to load up the dependency extension. User code will see
|
||||
# a LoadError if it's not available, but code that doesn't require
|
||||
# mapped files will work fine without it. This is a somewhat difficult
|
||||
# compromise between usability and proper modularization.
|
||||
#
|
||||
# @private
|
||||
def ensure_mapping_extension_is_present
|
||||
@@fastfilereader ||= (require 'fastfilereaderext')
|
||||
end
|
||||
private :ensure_mapping_extension_is_present
|
||||
|
||||
end # FileStreamer
|
||||
end # EventMachine
|
||||
@@ -0,0 +1,90 @@
|
||||
module EventMachine
|
||||
# = EventMachine::ThreadedResource
|
||||
#
|
||||
# A threaded resource is a "quick and dirty" wrapper around the concept of
|
||||
# wiring up synchronous code into a standard EM::Pool. This is useful to keep
|
||||
# interfaces coherent and provide a simple approach at "making an interface
|
||||
# async-ish".
|
||||
#
|
||||
# General usage is to wrap libraries that do not support EventMachine, or to
|
||||
# have a specific number of dedicated high-cpu worker resources.
|
||||
#
|
||||
# == Basic Usage example
|
||||
#
|
||||
# This example requires the cassandra gem. The cassandra gem contains an
|
||||
# EventMachine interface, but it's sadly Fiber based and thus only works on
|
||||
# 1.9. It also requires (potentially) complex stack switching logic to reach
|
||||
# completion of nested operations. By contrast this approach provides a block
|
||||
# in which normal synchronous code can occur, but makes no attempt to wire the
|
||||
# IO into EventMachines C++ IO implementations, instead relying on the reactor
|
||||
# pattern in rb_thread_select.
|
||||
#
|
||||
# cassandra_dispatcher = ThreadedResource.new do
|
||||
# Cassandra.new('allthethings', '127.0.0.1:9160')
|
||||
# end
|
||||
#
|
||||
# pool = EM::Pool.new
|
||||
#
|
||||
# pool.add cassandra_dispatcher
|
||||
#
|
||||
# # If we don't care about the result:
|
||||
# pool.perform do |dispatcher|
|
||||
# # The following block executes inside a dedicated thread, and should not
|
||||
# # access EventMachine things:
|
||||
# dispatcher.dispatch do |cassandra|
|
||||
# cassandra.insert(:Things, '10', 'stuff' => 'things')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # Example where we care about the result:
|
||||
# pool.perform do |dispatcher|
|
||||
# # The dispatch block is executed in the resources thread.
|
||||
# completion = dispatcher.dispatch do |cassandra|
|
||||
# cassandra.get(:Things, '10', 'stuff')
|
||||
# end
|
||||
#
|
||||
# # This block will be yielded on the EM thread:
|
||||
# completion.callback do |result|
|
||||
# EM.do_something_with(result)
|
||||
# end
|
||||
#
|
||||
# completion
|
||||
# end
|
||||
class ThreadedResource
|
||||
|
||||
# The block should return the resource that will be yielded in a dispatch.
|
||||
def initialize
|
||||
@resource = yield
|
||||
|
||||
@running = true
|
||||
@queue = ::Queue.new
|
||||
@thread = Thread.new do
|
||||
@queue.pop.call while @running
|
||||
end
|
||||
end
|
||||
|
||||
# Called on the EM thread, generally in a perform block to return a
|
||||
# completion for the work.
|
||||
def dispatch
|
||||
completion = EM::Completion.new
|
||||
@queue << lambda do
|
||||
begin
|
||||
result = yield @resource
|
||||
completion.succeed result
|
||||
rescue => e
|
||||
completion.fail e
|
||||
end
|
||||
end
|
||||
completion
|
||||
end
|
||||
|
||||
# Kill the internal thread. should only be used to cleanup - generally
|
||||
# only required for tests.
|
||||
def shutdown
|
||||
@running = false
|
||||
@queue << lambda {}
|
||||
@thread.join
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
module EventMachine
|
||||
# Creates and immediately starts an EventMachine::TickLoop
|
||||
def self.tick_loop(*a, &b)
|
||||
TickLoop.new(*a, &b).start
|
||||
end
|
||||
|
||||
# A TickLoop is useful when one needs to distribute amounts of work
|
||||
# throughout ticks in order to maintain response times. It is also useful for
|
||||
# simple repeated checks and metrics.
|
||||
# @example
|
||||
# # Here we run through an array one item per tick until it is empty,
|
||||
# # printing each element.
|
||||
# # When the array is empty, we return :stop from the callback, and the
|
||||
# # loop will terminate.
|
||||
# # When the loop terminates, the on_stop callbacks will be called.
|
||||
# EM.run do
|
||||
# array = (1..100).to_a
|
||||
#
|
||||
# tickloop = EM.tick_loop do
|
||||
# if array.empty?
|
||||
# :stop
|
||||
# else
|
||||
# puts array.shift
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# tickloop.on_stop { EM.stop }
|
||||
# end
|
||||
#
|
||||
class TickLoop
|
||||
|
||||
# Arguments: A callback (EM::Callback) to call each tick. If the call
|
||||
# returns +:stop+ then the loop will be stopped. Any other value is
|
||||
# ignored.
|
||||
def initialize(*a, &b)
|
||||
@work = EM::Callback(*a, &b)
|
||||
@stops = []
|
||||
@stopped = true
|
||||
end
|
||||
|
||||
# Arguments: A callback (EM::Callback) to call once on the next stop (or
|
||||
# immediately if already stopped).
|
||||
def on_stop(*a, &b)
|
||||
if @stopped
|
||||
EM::Callback(*a, &b).call
|
||||
else
|
||||
@stops << EM::Callback(*a, &b)
|
||||
end
|
||||
end
|
||||
|
||||
# Stop the tick loop immediately, and call it's on_stop callbacks.
|
||||
def stop
|
||||
@stopped = true
|
||||
until @stops.empty?
|
||||
@stops.shift.call
|
||||
end
|
||||
end
|
||||
|
||||
# Query if the loop is stopped.
|
||||
def stopped?
|
||||
@stopped
|
||||
end
|
||||
|
||||
# Start the tick loop, will raise argument error if the loop is already
|
||||
# running.
|
||||
def start
|
||||
raise ArgumentError, "double start" unless @stopped
|
||||
@stopped = false
|
||||
schedule
|
||||
end
|
||||
|
||||
private
|
||||
def schedule
|
||||
EM.next_tick do
|
||||
next if @stopped
|
||||
if @work.call == :stop
|
||||
stop
|
||||
else
|
||||
schedule
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
module EventMachine
|
||||
# Creates a one-time timer
|
||||
#
|
||||
# timer = EventMachine::Timer.new(5) do
|
||||
# # this will never fire because we cancel it
|
||||
# end
|
||||
# timer.cancel
|
||||
#
|
||||
class Timer
|
||||
# Create a new timer that fires after a given number of seconds
|
||||
def initialize interval, callback=nil, &block
|
||||
@signature = EventMachine::add_timer(interval, callback || block)
|
||||
end
|
||||
|
||||
# Cancel the timer
|
||||
def cancel
|
||||
EventMachine.send :cancel_timer, @signature
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a periodic timer
|
||||
#
|
||||
# @example
|
||||
# n = 0
|
||||
# timer = EventMachine::PeriodicTimer.new(5) do
|
||||
# puts "the time is #{Time.now}"
|
||||
# timer.cancel if (n+=1) > 5
|
||||
# end
|
||||
#
|
||||
class PeriodicTimer
|
||||
# Create a new periodic timer that executes every interval seconds
|
||||
def initialize interval, callback=nil, &block
|
||||
@interval = interval
|
||||
@code = callback || block
|
||||
@cancelled = false
|
||||
@work = method(:fire)
|
||||
schedule
|
||||
end
|
||||
|
||||
# Cancel the periodic timer
|
||||
def cancel
|
||||
@cancelled = true
|
||||
end
|
||||
|
||||
# Fire the timer every interval seconds
|
||||
attr_accessor :interval
|
||||
|
||||
# @private
|
||||
def schedule
|
||||
EventMachine::add_timer @interval, @work
|
||||
end
|
||||
|
||||
# @private
|
||||
def fire
|
||||
unless @cancelled
|
||||
@code.call
|
||||
schedule
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module EventMachine
|
||||
VERSION = "1.2.7"
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user