[DRE-commits] r3365 - in tools/ruby-support: . examples examples/libtorrent-ruby-0.3 examples/libtorrent-ruby-0.3/debian examples/libtorrent-ruby-0.3/doc examples/libtorrent-ruby-0.3/rubytorrent
lucas at alioth.debian.org
lucas at alioth.debian.org
Sat Apr 4 15:26:31 UTC 2009
Author: lucas
Date: 2009-04-04 15:26:31 +0000 (Sat, 04 Apr 2009)
New Revision: 3365
Added:
tools/ruby-support/examples/
tools/ruby-support/examples/libtorrent-ruby-0.3/
tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING
tools/ruby-support/examples/libtorrent-ruby-0.3/README
tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch
tools/ruby-support/examples/libtorrent-ruby-0.3/doc/
tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt
tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt
tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb
tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb
tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
Log:
added an example lib ported to ruby-support
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/COPYING 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library 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.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/README
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/README (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/README 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,21 @@
+Try it out
+----------
+
+RubyTorrent is primarily a library. See doc/api.txt for an example of how to
+use it in your Ruby applications. There are also a few executable scripts for
+you to play around with.
+
+rtpeer.rb: downloads a BitTorrent package.
+
+rtpeer-ncurses.rb: a nicer, ncurses version of the same. (The standard Ruby
+curses library appears not to play nicely with Threads, so we can't use it.)
+
+dump-metainfo.rb: takes a .torrent metainfo file and spits out everything about
+it.
+
+make-metainfo.rb: creates a .torrent file.
+
+dump-peers.rb: takes a .torrent metainfo file, connects to the tracker, and
+displays all the peers. (hack.)
+
+-- William <wmorgan-rubytorrent at masanjin.net>
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/ReleaseNotes.txt 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,25 @@
+Release notes for 0.3:
+
+Many more bug fixes. Speed is now basically comparable to Bram's client---at
+least in my limited experiments.
+
+The following are known issues with this release:
+
+- Ruby threads don't play well with curses. Non-blocking getch hangs.
+ See [ruby-talk:130620]. So we use ncurses.
+
+- Ruby threads don't play well with TCP sockets on Windows. There is a
+ 20-second *global* freeze every time an outgoing connection is made to a
+ non-responsive host. See [ruby-talk:129578], [ruby-core:04364]. As you can
+ imagine, this can be quite a performance hit in a program that can make
+ potentially hundreds of such connections. In fact, it renders RubyTorrent
+ almost useless on Windows. A patch exists (indeed, has existed for many
+ months), and if I bug Matz maybe it'll get in to 1.8.3. :)
+
+- Ruby threads don't play well with writing data over TCP sockets. At least,
+ that's what I glean from [ruby-talk:130480], and it might explain the
+ occasional freezing behavior I see (3 to 30 seconds, sporadic) under heavy
+ loads in Linux.
+
+Other than that :) everything works. I think.
+
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/changelog 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,12 @@
+libtorrent-ruby (0.3-2) unstable; urgency=low
+
+ * Fix architecture.
+ * Do not use the uploaders rule anymore.
+
+ -- Arnaud Cornet <arnaud.cornet at gmail.com> Sun, 15 Apr 2007 11:24:25 +0200
+
+libtorrent-ruby (0.3-1) unstable; urgency=low
+
+ * Initial release.
+
+ -- Arnaud Cornet <arnaud.cornet at gmail.com> Sat, 04 Nov 2006 17:53:07 +0100
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/compat 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1 @@
+5
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/control 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,14 @@
+Source: libtorrent-ruby
+Section: libs
+Priority: optional
+Maintainer: Arnaud Cornet <arnaud.cornet at gmail.com>
+Uploaders: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers at lists.alioth.debian.org>
+Standards-Version: 3.7.2
+Build-Depends: ruby-pkg-tools (>= 0.8), cdbs, debhelper (>= 5.0.37.2), ruby, ruby-support
+
+Package: ruby-torrent
+Architecture: all
+Depends: ${misc:Depends}
+Description: BitTorrent library in Ruby
+ RubyTorrent is a pure-Ruby BitTorrent peer library and toolset. You can use
+ it to download or serve files over BitTorrent from any Ruby program.
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/copyright 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,437 @@
+This package was debianized by Arnaud Cornet <arnaud.cornet at gmail.com>
+on Sat, 04 Nov 2006 17:53:07 +0100
+
+The current Debian maintainer is Arnaud Cornet <arnaud.cornet at gmail.com>.
+
+It was downloaded from http://rubytorrent.rubyforge.org/
+
+Upstream Author: William Morgan <ruby at cs.stanford.edu>
+
+Copyright: 2003-2005 William Morgan
+
+ RubyTorrent is free software; you can redistribute it and/or modify it
+ under the terms of version 2 of the GNU General Public License as
+ published by the Free Software Foundation.
+
+ RubyTorrent is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License (in the file COPYING) for more details.
+
+On Debian systems, the complete text of the GNU General Public
+License, version 2, can be found in /usr/share/common-licenses/GPL-2.
+
+The file doc/api.txt is Copyright 2005 William Morgan
+
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.2; with
+ no Invariant Sections, no Front-Cover Texts, and no Back-Covers.
+
+The GNU Free Documentation License, Version 1.2, is appended at the end of
+this file.
+
+The Debian packaging is Copyright 2006 Arnaud Cornet <arnaud.cornet at gmail.com>
+and is licensed under the GPL, see above.
+
+----
+
+GNU Free Documentation License
+******************************
+
+ GNU Free Documentation License
+ Version 1.2, November 2002
+
+
+ Copyright (C) 2000,2001,2002 Free Software Foundation, Inc.
+ 51 Franklin St, 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.
+
+
+0. PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document "free" in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of "copyleft", which means that derivative
+works of the document must themselves be free in the same sense. It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does. But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book. We recommend this License
+principally for works whose purpose is instruction or reference.
+
+
+1. APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License. Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein. The "Document", below,
+refers to any such manual or work. Any member of the public is a
+licensee, and is addressed as "you". You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A "Modified Version" of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A "Secondary Section" is a named appendix or a front-matter section of
+the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall subject
+(or to related matters) and contains nothing that could fall directly
+within that overall subject. (Thus, if the Document is in part a
+textbook of mathematics, a Secondary Section may not explain any
+mathematics.) The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The "Invariant Sections" are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License. If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant. The Document may contain zero
+Invariant Sections. If the Document does not identify any Invariant
+Sections then there are none.
+
+The "Cover Texts" are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License. A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A "Transparent" copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters. A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text. A copy that is not "Transparent" is called "Opaque".
+
+Examples of suitable formats for Transparent copies include plain
+ASCII without markup, Texinfo input format, LaTeX input format, SGML
+or XML using a publicly available DTD, and standard-conforming simple
+HTML, PostScript or PDF designed for human modification. Examples of
+transparent image formats include PNG, XCF and JPG. Opaque formats
+include proprietary formats that can be read and edited only by
+proprietary word processors, SGML or XML for which the DTD and/or
+processing tools are not generally available, and the
+machine-generated HTML, PostScript or PDF produced by some word
+processors for output purposes only.
+
+The "Title Page" means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page. For works in
+formats which do not have any title page as such, "Title Page" means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section "Entitled XYZ" means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language. (Here XYZ stands for a
+specific section name mentioned below, such as "Acknowledgements",
+"Dedications", "Endorsements", or "History".) To "Preserve the Title"
+of such a section when you modify the Document means that it remains a
+section "Entitled XYZ" according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document. These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+
+2. VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License. You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute. However, you may accept
+compensation in exchange for copies. If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+
+3. COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover. Both covers must also clearly and legibly identify
+you as the publisher of these copies. The front cover must present
+the full title with all words of the title equally prominent and
+visible. You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+
+4. MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it. In addition, you must do these things in the Modified Version:
+
+A. Use in the Title Page (and on the covers, if any) a title distinct
+ from that of the Document, and from those of previous versions
+ (which should, if there were any, be listed in the History section
+ of the Document). You may use the same title as a previous version
+ if the original publisher of that version gives permission.
+B. List on the Title Page, as authors, one or more persons or entities
+ responsible for authorship of the modifications in the Modified
+ Version, together with at least five of the principal authors of the
+ Document (all of its principal authors, if it has fewer than five),
+ unless they release you from this requirement.
+C. State on the Title page the name of the publisher of the
+ Modified Version, as the publisher.
+D. Preserve all the copyright notices of the Document.
+E. Add an appropriate copyright notice for your modifications
+ adjacent to the other copyright notices.
+F. Include, immediately after the copyright notices, a license notice
+ giving the public permission to use the Modified Version under the
+ terms of this License, in the form shown in the Addendum below.
+G. Preserve in that license notice the full lists of Invariant Sections
+ and required Cover Texts given in the Document's license notice.
+H. Include an unaltered copy of this License.
+I. Preserve the section Entitled "History", Preserve its Title, and add
+ to it an item stating at least the title, year, new authors, and
+ publisher of the Modified Version as given on the Title Page. If
+ there is no section Entitled "History" in the Document, create one
+ stating the title, year, authors, and publisher of the Document as
+ given on its Title Page, then add an item describing the Modified
+ Version as stated in the previous sentence.
+J. Preserve the network location, if any, given in the Document for
+ public access to a Transparent copy of the Document, and likewise
+ the network locations given in the Document for previous versions
+ it was based on. These may be placed in the "History" section.
+ You may omit a network location for a work that was published at
+ least four years before the Document itself, or if the original
+ publisher of the version it refers to gives permission.
+K. For any section Entitled "Acknowledgements" or "Dedications",
+ Preserve the Title of the section, and preserve in the section all
+ the substance and tone of each of the contributor acknowledgements
+ and/or dedications given therein.
+L. Preserve all the Invariant Sections of the Document,
+ unaltered in their text and in their titles. Section numbers
+ or the equivalent are not considered part of the section titles.
+M. Delete any section Entitled "Endorsements". Such a section
+ may not be included in the Modified Version.
+N. Do not retitle any existing section to be Entitled "Endorsements"
+ or to conflict in title with any Invariant Section.
+O. Preserve any Warranty Disclaimers.
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant. To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled "Endorsements", provided it contains
+nothing but endorsements of your Modified Version by various
+parties--for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version. Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity. If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+
+5. COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy. If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled "History"
+in the various original documents, forming one section Entitled
+"History"; likewise combine any sections Entitled "Acknowledgements",
+and any sections Entitled "Dedications". You must delete all sections
+Entitled "Endorsements".
+
+
+6. COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+
+7. AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an "aggregate" if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+
+8. TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections. You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers. In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled "Acknowledgements",
+"Dedications", or "History", the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+
+9. TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License. Any other attempt to
+copy, modify, sublicense or distribute the Document 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.
+
+
+10. FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation 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. See
+http://www.gnu.org/copyleft/.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation. If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+
+
+ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+ Copyright (c) YEAR YOUR NAME.
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.2
+ or any later version published by the Free Software Foundation;
+ with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
+ A copy of the license is included in the section entitled "GNU
+ Free Documentation License".
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the "with...Texts." line with this:
+
+ with the Invariant Sections being LIST THEIR TITLES, with the
+ Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.docs 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,2 @@
+doc/*
+ReleaseNotes.txt
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.examples 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,5 @@
+dump-metainfo.rb
+dump-peers.rb
+make-metainfo.rb
+rtpeer-ncurses.rb
+rtpeer.rb
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/ruby-torrent.install 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1 @@
+rubytorrent* /usr/lib/ruby/vendor_ruby/1.8
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,6 @@
+#!/usr/bin/make -f
+
+include /usr/share/cdbs/1/rules/debhelper.mk
+
+$(patsubst %,binary-post-install/%,$(DEB_ALL_PACKAGES))::
+ dh_ruby -p$(cdbs_curpkg)
Property changes on: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/rules
___________________________________________________________________
Name: svn:executable
+ *
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/debian/watch 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,2 @@
+version=3
+http://rubyforge.org/frs/?group_id=498 .*rubytorrent-(.*)\.t.*
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/doc/api.txt 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,289 @@
+RubyTorrent Documentation
+
+Introduction
+------------
+
+RubyTorrent is a pure-Ruby BitTorrent library. You can use RubyTorrent
+in your Ruby applications to download and serve files via the
+BitTorrent protocol. More information about BitTorrent can be found
+at http://bittorrent.com/.
+
+There's a lot going behind the scenes, but using this library is
+pretty simple: on the surface, RubyTorrent simply lets you download a
+file or set of files, given an initial .torrent filename or URL.
+
+I recommend you take a look at rtpeer.rb for an example Ruby BitTorrent
+peer that uses all this stuff.
+
+Synopsis
+--------
+
+require "rubytorrent"
+
+# simple
+bt = RubyTorrent::BitTorrent.new(filename)
+bt.on_event(self, :complete) { puts "done!" }
+
+# more complex
+mi = RubyTorrent::MetaInfo.from_location(url)
+package = RubyTorrent::Package.new(mi, dest)
+bt = RubyTorrent::BitTorrent.new(mi, package)
+thread = Thread.new do
+ until bt.complete?
+ puts "#{bt.percent_completed}% done"
+ sleep 5
+ end
+end
+bt.on_event(self, :complete) { puts "done!" }
+thread.join
+
+Overview
+--------
+
+There are three top-level classes you should be familiar with in the
+RubyTorrent module: BitTorrent, MetaInfo and Package. BitTorrent is
+the main interface; MetaInfo and Package classes allow you more
+control over the details, but they're completely options and the
+BitTorrent class will do reasonable things if you don't use them.
+
+RubyTorrent has a very event-driven interface; all methods are
+non-blocking and the BitTorrent class generates notifications of all
+interesting events, which you can subscribe to. See the documentation
+on BitTorrent#on_event() below for how to subscribe to events.
+
+RubyTorrent::MetaInfo
+---------------------
+
+This class represents the contents of the .torrent file or URL.
+
+CLASS METHODS
+
+from_location(location, http_proxy=ENV["http_proxy"])
+ Creates a MetaInfo object from a filename or URL.
+
+ Arguments:
+ location: a filename or a URL of a .torrent file.
+ http_proxy: is the HTTP proxy to be used in the case that
+ "location" is a URL (nil for none).
+
+ Returns:
+ A MetaInfo object.
+
+ Throws:
+ RubyTorrent::MetaInfoFormatError,
+ RubyTorrent::BEncodingError,
+ RubyTorrent::TypedStructError
+ if the contents of the file/url are not a BitTorrent metainfo file.
+ IOError, SystemCallError
+ if reading the contents of the file/url failed for system-level
+ issues.
+
+from_stream(stream)
+ Creates a MetaInfo object from a readable IO stream.
+
+ Arguments:
+ stream: a readable IO stream, e.g. an opened File.
+
+ Returns:
+ A MetaInfo object.
+
+ Throws:
+ same as RubyTorrent::Metainfo#from_location
+
+INSTANCE METHODS
+
+single?
+ Returns true if this .torrent contains a single file, false otherwise.
+
+multiple?
+ The opposite of single?
+
+RubyTorrent::Package
+--------------------
+
+This class represents the target file or files on disk.
+
+CLASS METHODS
+
+new(info, out=nil, validity_assumption=nil, path_sep="/") # optional block
+ Creates a Package.
+
+ Arguments:
+ info: a MetaInfo object.
+ out: if info.single?, this should be a File object corresponding to
+ the target file on disk. If info.multiple?, this should be a Dir
+ object corresponding to the target directory on disk. If nil, the
+ original filename (for single-file .torrents) or the current
+ directory (for multi-file .torrents) will be used.
+ validity_assumption: if nil, make no assumptions about the validity of
+ any files on disk. If true, assume all files on disk are complete
+ and valid. If false, assume all files on disk are incomplete and
+ invalid. This can be used to speed up start time by skipping all
+ examination of current disk contents: if you're just starting a
+ download, you can use false; if you're serving a complete file or
+ set of files, you can use true.
+ path_sep: how to join path-name components to make paths. "/"
+ should work on both Windows and Unix worlds; I'm not sure about
+ other OSs.
+
+ Block:
+ If given, yields a "Piece" object when checking the files on
+ disk. This object has complete?() and valid?() methods. This is
+ really only useful for updating the user on the status of the
+ Package creation, which can take a long time for large files (I/O
+ time and SHA1 calculations).
+
+ Throws:
+ IOError, if the file access fails.
+
+RubyTorrent::BitTorrent
+-----------------------
+
+The main BitTorrent peer protocol interface.
+
+CLASS METHODS
+
+new(metainfo, package=nil, :host, :port, :dlratelim, :ulratelim, :http_proxy)
+ Creates a BitTorrent peer.
+
+ Arguments (all symbol arguments are optional hash pseudo-keyword arguments):
+ metainfo: a String, IO or MetaInfo object corresponding to a .torrent file.
+ In the case of a String or IO object, a MetaInfo object will be implictly
+ created with default arguments.
+ package: a Package, or nil. In the case on nil, a new Package will be
+ implicitly created with default arguments.
+ :host: the host to report to the tracker, if the source IP address of the
+ HTTP request is not correct (for weird IP masquerading issues, I suppose).
+ :port: the port to report to the tracker, if the port the BitTorrent peer is
+ listening on is not correct (likewise).
+ :dlratelim: the download rate limit in bytes/sec. This limit right now is
+ applied on a per-peer basis to the average download rate. In the future
+ this might change to something stricter/more useful.
+ :ulratelim: likewise, for the upload rate limit.
+ :http_proxy: the http_proxy used for connecting to the tracker, or nil
+ or unspecified for ENV["http_proxy"].
+
+ Throws:
+ All of the exceptions thrown by MetaInfo.new and Package.new.
+
+INSTANCE METHODS
+
+running?
+ Returns whether this client is running or not.
+
+ip
+ Returns the IP address the client is bound to, as a String (possibly "0.0.0.0")
+
+port
+ Returns the port the client is bound to.
+
+complete?
+ Returns whether the file on disk is complete or not.
+
+bytes_completed
+ Returns the number of bytes completed.
+
+total_bytes
+ Returns the total number of bytes in the target file/fileset.
+
+percent_completed
+ Returns the percent of bytes completed.
+
+pieces_completed
+ Returns the number of BitTorrent "pieces" completed.
+
+num_pieces
+ Returns the total number of BitTorrent pieces.
+
+tracker
+ Returns the URL of the tracker being used, or nil if no tracker can be reached.
+
+num_possible_peers
+ Returns the number of peers we've read from the tracker, or nil if no tracker
+ can be reached. This is typically capped at 50.
+
+peer_info
+ Returns an array of hashes, one per current peer, with the following symbols
+ as keys:
+ :name: the peer name (probably "ip address/port")
+ :seed: true if the peer is a seed, false if it's a leecher
+ :dlamt, :ulamt: the number of bytes downloaded from /uploaded to this peer
+ :dlrate, :ulrate: the bytes/sec downloaded from/uploaded to this peer
+ :pending_send, :pending_recv: the number of blocks pending for send/receive
+ :interested, :peer_interested: who's interested in the other's pieces
+ :choking, :peer_choking: who's choking whom
+ :snubbing: whether we're snubbing this peer
+ :we_desire, :they_desire: number of pieces one has that the other wants
+ A lot of this stuff has to do with the internals of the BitTorrent wire
+ protocol, so it's mainly useful for debugging.
+
+shutdown
+ Shuts down this particular client.
+
+shutdown_all
+ Shuts down all clients.
+
+on_event(who, *events) # mandatory block
+ Registers a notification for one or more events. When one of the
+ events occurs, the block will be called. The first argument to the
+ block will be the source of the event (in this case a
+ RubyTorrent::BitTorrent object); the other arguments are dependent
+ on the block itself.
+
+ Arguments:
+ who: should be "self"
+ events: one or more event symbols (see EVENTS below)
+
+unregister_events(who, *events)
+ Unregisters event notifications. All blocks added with on_event(who, ...)
+ will be removed if they have an event in "events". If "events" is nil,
+ all blocks registered with on_event(who, ...) will be removed.
+
+ Arguments:
+ who: the same argument as was passed to on_event()
+ events: one or more event symbols.
+
+EVENTS
+
+:trying_peer |source, peer|
+ We're trying to connect to the peer "peer" (a String: "ip addr/port").
+
+:forgetting_peer |source, peer|
+ We're couldn't connect to the peer.
+
+:added_peer |source, peer|
+ We successfully connected to the peer.
+
+:removed_peer |source, peer|
+ We dropped our connection to the peer.
+
+:received_block |source, block, peer|
+ We received a block "block" from peer.
+
+:sent_block |source, block, peer|
+ We sent a block "block" to peer.
+
+:have_piece |source, piece|
+ We've successfully downloaded a complete piece "piece".
+
+:discarded_piece |source, piece|
+ We had to discard piece "piece" because of checksum errors
+
+:complete |source|
+ We've downloaded the entire file! Hooray!
+
+:tracker_connected |source, url|
+ We connected to tracker "url".
+
+:tracker_lost
+ We couldn't connect to tracker "url" after previously having connected to it.
+
+COPYRIGHT
+---------
+
+Copyright 2005 William Morgan
+Permission is granted to copy, distribute and/or modify this document under the
+terms of the GNU Free Documentation License, Version 1.2; with no Invariant
+Sections, no Front-Cover Texts, and no Back-Covers. A copy of the license may
+be found at http://www.gnu.org/licenses/fdl.html.
+
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/doc/design.txt 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,59 @@
+RubyTorrent Design
+------------------
+
+This is pretty sketchy at the moment but it might help if you want to
+do some hacking.
+
+
+ +---------+ +----------------+
+disk <::::::::>| Package | /-| PeerConnection |<=== network ===> peer
+ +---------+ | +----------------+
+ | |
+ +---------+ +------------+ / +----------------+
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
+ +---------+ +------------+ \ +----------------+
+ / | .
+ +--------+ / | .
+ | Server |- | .
+ +--------+ \
+ \ +---------+
+ | | Package |<:::::::> disk
+ | +---------+
+ | |
+ +---------+ \+------------+ +----------------+
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
+ +---------+ +------------+ \ +----------------+
+ |
+ | +----------------+
+ |--| PeerConnection |<=== network ===> peer
+ | +----------------+
+ .
+ .
+ .
+
+
+Each .torrent download is associated with a Package. A Package is
+composed of several Pieces, each corresponding to a BitTorrent
+piece. A Package provides simple aggregate operations over all the
+Pieces. Each Piece handles writing to and reading from disk (across
+potentially multiple file pointers), as well as dividing its data into
+one or more Blocks. Each Block is an in-memory section of a Piece and
+corresponds to the BitTorrent piece, transferrable across the network.
+
+One Server coordinates all BitTorrent downloads. It maintains several
+Controllers, one per .torrent download. The server handles all
+handshaking. It accepts incoming connections, shunting them to the
+appropriate Controller, and creates outgoing ones at the Controllers'
+behest. Each connection to a peer is maintained by a PeerConnection,
+which keeps track of the peer's state and the connection state.
+PeerConnections get empty Blocks from their Controller and send
+requests for them across the wire, and, upon receiving requests from
+the peer, get full Blocks from the Package and transmit them back.
+
+The Controller also keeps a Tracker object, which it uses to
+communicate with the tracker.
+
+PeerConnections are completely reactive, and are tightly integrated
+with their Controller. They rely on the Controller's heartbeat thread
+to trigger any time-dependent events, and also for propagating any
+messages to other peers.
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/dump-metainfo.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,55 @@
+## dump-metainfo.rb -- command-line .torrent dumper
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'rubytorrent'
+
+def dump_metainfoinfo(mii)
+ if mii.single?
+ <<EOS
+ length: #{mii.length / 1024}kb
+ filename: #{mii.name}
+EOS
+ else
+ mii.files.map do |f|
+ <<EOS
+ - filename: #{File.join(mii.name, f.path)}
+ length: #{f.length}
+EOS
+ end.join + "\n"
+ end + <<EOS
+ piece length: #{mii.piece_length / 1024}kb
+ pieces: #{mii.pieces.length / 20}
+EOS
+end
+
+def dump_metainfo(mi)
+ <<EOS
+#{dump_metainfoinfo(mi.info).chomp}
+ announce: #{mi.announce}
+announce-list: #{(mi.announce_list.nil? ? "<not specified>" : mi.announce_list.map { |x| x.join(', ') }.join('; '))}
+creation date: #{mi.creation_date || "<not specified>"}
+ created by: #{mi.created_by || "<not specified>"}
+ comment: #{mi.comment || "<not specified>"}
+EOS
+end
+
+if ARGV.length == 1
+ fn = ARGV[0]
+ begin
+ puts dump_metainfo(RubyTorrent::MetaInfo.from_location(fn))
+ rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+ puts "Can't parse #{fn}: maybe not a .torrent file?"
+ end
+else
+ puts "Usage: dump-metainfo <filename>"
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/dump-peers.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,45 @@
+## dump-peers.rb -- command-line peer lister
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def dump_peer(p)
+ "#{(p.peer_id.nil? ? '<not specified>' : p.peer_id.inspect)} on #{p.ip}:#{p.port}"
+end
+
+fn = ARGV.shift or raise "first argument must be .torrent file"
+
+mi = nil
+begin
+ mi = RubyTorrent::MetaInfo.from_location(fn)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+ die "error parsing metainfo file #{fn}---maybe not a .torrent?"
+end
+
+# complete abuse
+mi.trackers.each do |track|
+ puts "#{track}:"
+
+ tc = RubyTorrent::TrackerConnection.new(track, mi.info.sha1, mi.info.total_length, 9999, "rubytorrent.dumppeer") # complete abuse, i know
+ begin
+ tc.force_refresh
+ puts "<no peers>" if tc.peers.length == 0
+ tc.peers.each do |p|
+ puts dump_peer(p)
+ end
+ rescue RubyTorrent::TrackerError => e
+ puts "error connecting to tracker: #{e.message}"
+ end
+end
+
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/make-metainfo.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,211 @@
+## make-metainfo.rb -- interactive .torrent creater
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'digest/sha1'
+require "rubytorrent"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def syntax
+ %{
+ Syntax: make-metainfo.rb [<file or directory>]+
+
+ make-metainfo is an interactive program for creating .torrent files from a set
+ of files or directories. any directories specified will be scanned recursively.
+ }
+end
+
+def find_files(f)
+ if FileTest.directory? f
+ Dir.new(f).entries.map { |x| find_files(File.join(f, x)) unless x =~ /^\.[\.\/]*$/}.compact
+ else
+ f
+ end
+end
+
+class Numeric
+ def to_size_s
+ if self < 1024
+ "#{self.round}b"
+ elsif self < 1024**2
+ "#{(self / 1024.0).round}kb"
+ elsif self < 1024**3
+ "#{(self / (1024.0**2)).round}mb"
+ else
+ "#{(self / (1024.0**3)).round}gb"
+ end
+ end
+end
+
+def read_pieces(files, length)
+ buf = ""
+ files.each do |f|
+ File.open(f) do |fh|
+ begin
+ read = fh.read(length - buf.length)
+ if (buf.length + read.length) == length
+ yield(buf + read)
+ buf = ""
+ else
+ buf += read
+ end
+ end until fh.eof?
+ end
+ end
+
+ yield buf
+end
+
+die syntax if ARGV.length == 0
+
+puts "Scanning..."
+files = ARGV.map { |f| find_files f }.flatten
+single = files.length == 1
+
+puts "Building #{(single ? 'single' : 'multi')}-file .torrent for #{files.length} file#{(single ? '' : 's')}."
+
+mi = RubyTorrent::MetaInfo.new
+mii = RubyTorrent::MetaInfoInfo.new
+
+maybe_name = if single
+ ARGV[0]
+ else
+ (File.directory?(ARGV[0]) ? File.basename(ARGV[0]) : File.basename(File.dirname(ARGV[0])))
+ end
+puts
+print %{Default output file/directory name (enter for "#{maybe_name}"): }
+name = $stdin.gets.chomp
+mii.name = (name == "" ? maybe_name : name)
+puts %{We'll use "#{mii.name}".}
+
+puts
+puts "Measuring..."
+length = nil
+if single
+ length = mii.length = files.inject(0) { |s, f| s + File.size(f) }
+else
+ mii.files = []
+ length = files.inject(0) do |s, f|
+ miif = RubyTorrent::MetaInfoInfoFile.new
+ miif.length = File.size f
+ miif.path = f.split File::SEPARATOR
+ miif.path = miif.path[1, miif.path.length - 1] if miif.path[0] == mii.name
+ mii.files << miif
+ s + miif.length
+ end
+end
+
+puts <<EOS
+
+The file is #{length.to_size_s}. What piece size would you like? A smaller piece size
+will result in a larger .torrent file; a larger piece size may cause
+transfer inefficiency. Common sizes are 256, 512, and 1024kb.
+
+Hint: for this .torrent,
+EOS
+
+size = nil
+[64, 128, 256, 512, 1024, 2048, 4096].each do |size|
+ num_pieces = (length.to_f / size / 1024.0).ceil
+ tsize = num_pieces.to_f * 20.0 + 100
+ puts " - piece size of #{size}kb => #{num_pieces} pieces and .torrent size of approx. #{tsize.to_size_s}."
+ break if tsize < 10240
+end
+
+maybe_plen = [size, 256].min
+begin
+ print "Piece size in kb (enter for #{maybe_plen}k): "
+ plen = $stdin.gets.chomp
+end while plen !~ /^\d*$/
+
+plen = (plen == "" ? maybe_plen : plen.to_i)
+
+mii.piece_length = plen * 1024
+num_pieces = (length.to_f / mii.piece_length.to_f).ceil
+puts "Using piece size of #{plen}kb => .torrent size of approx. #{(num_pieces * 20.0).to_size_s}."
+
+print "Calculating #{num_pieces} piece SHA1s... " ; $stdout.flush
+
+mii.pieces = ""
+i = 0
+read_pieces(files, mii.piece_length) do |piece|
+ mii.pieces += Digest::SHA1.digest(piece)
+ i += 1
+ if (i % 100) == 0
+ print "#{(i.to_f / num_pieces * 100.0).round}%... "; $stdout.flush
+ end
+end
+puts "done"
+
+mi.info = mii
+puts <<EOS
+
+Enter the tracker URL or URLs that will be hosting the .torrent
+file. These are typically of the form:
+
+ http://tracker.example.com:6969/announce
+
+Multiple trackers may be partitioned into tiers; clients will try all
+servers (in random order) from an earlier tier before trying those of
+a later tier. See http://home.elp.rr.com/tur/multitracker-spec.txt
+for details.
+
+Enter the tracker URL(s) now. Separate multiple tracker URLs on the
+same tier with spaces. Enter a blank line when you're done.
+
+(Note that if you have multiple trackers, some clients may only use
+the first one, so that should be the one capable of handling the most
+traffic.)
+EOS
+
+tier = 0
+trackers = []
+begin
+ print "Tier #{tier} tracker(s): "
+ these = $stdin.gets.chomp.split(/\s+/)
+ trackers.push these unless these.length == 0
+ tier += 1 unless these.length == 0
+end while (these.length != 0) || (tier == 0)
+
+mi.announce = URI.parse(trackers[0][0])
+mi.announce_list = trackers.map do |tier|
+ tier.map { |x| URI.parse(x) }
+end unless (trackers.length == 1) && (trackers[0].length == 1)
+
+puts <<EOS
+
+Enter any comments. No one will probably ever see these. End with a blank line.
+EOS
+comm = ""
+while true
+ s = $stdin.gets.chomp
+ break if s == ""
+ comm += s + "\n"
+end
+mi.comment = comm.chomp unless comm == ""
+
+mi.created_by = "RubyTorrent make-metainfo (http://rubytorrent.rubyforge.org)"
+mi.creation_date = Time.now
+
+maybe_name = "#{mii.name}.torrent"
+begin
+ print "Output filename (enter for #{maybe_name}): "
+ name = $stdin.gets.chomp
+end while name.length == ""
+
+name = (name == "" ? maybe_name : name)
+File.open(name, "w") do |f|
+ f.write mi.to_bencoding
+end
+
+puts "Succesfully created #{name}"
+
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer-ncurses.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,339 @@
+## rtpeer-ncurses.rb -- RubyTorrent ncurses BitTorrent peer.
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+require "ncurses"
+require "optparse"
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+
+dlratelim = nil
+ulratelim = nil
+
+opts = OptionParser.new do |opts|
+ opts.banner =
+%{Usage: rtpeer-ncurses [options] <torrent> [<target>]
+
+rtpeer-ncurses is a very simple ncurses-based BitTorrent peer. You can use it
+to download .torrents or to seed them.
+
+<torrent> is a .torrent filename or URL.
+<target> is a file or directory on disk. If not specified, the default value
+ from <torrent> will be used.
+[options] are:
+}
+
+ opts.on("-l", "--log FILENAME",
+ "Log events to FILENAME (for debugging)") do |fn|
+ RubyTorrent::log_output_to(fn)
+ end
+
+ opts.on("-d", "--downlimit LIMIT", Integer,
+ "Limit download rate to LIMIT kb/s") do |x|
+ dlratelim = x * 1024
+ end
+
+ opts.on("-u", "--uplimit LIMIT", Integer,
+ "Limit upload rate to LIMIT kb/s") do |x|
+ ulratelim = x * 1024
+ end
+
+ opts.on_tail("-h", "--help", "Show this message") do
+ puts opts
+ exit
+ end
+end
+
+opts.parse!(ARGV)
+proxy = ENV["http_proxy"]
+torrent = ARGV.shift or (puts opts; exit)
+dest = ARGV.shift
+
+class Numeric
+ def to_sz
+ if self < 1024
+ "#{self.round}b"
+ elsif self < 1024 ** 2
+ "#{(self / 1024 ).round}k"
+ elsif self < 1024 ** 3
+ sprintf("%.1fm", self.to_f / (1024 ** 2))
+ else
+ sprintf("%.2fg", self.to_f / (1024 ** 3))
+ end
+ end
+
+ MIN = 60
+ HOUR = 60 * MIN
+ DAY = 24 * HOUR
+
+ def to_time
+ if self < MIN
+ sprintf("0:%02d", self)
+ elsif self < HOUR
+ sprintf("%d:%02d", self / MIN, self % MIN)
+ elsif self < DAY
+ sprintf("%d:%02d:%02d", self / HOUR, (self % HOUR) / MIN, (self % HOUR) % MIN)
+ else
+ sprintf("%dd %d:%02d:%02d", self / DAY, (self % DAY) / HOUR, ((self % DAY) % HOUR) / MIN, ((self % DAY) % HOUR) % MIN)
+ end
+ end
+end
+
+class NilClass
+ def to_time; "--:--"; end
+ def to_sz; "-"; end
+end
+
+class Display
+ STALL_SECS = 15
+
+ attr_accessor :fn, :dest, :status,:dlamt, :ulamt, :dlrate, :ulrate,
+ :conn_peers, :fail_peers, :untried_peers, :tracker,
+ :errcount, :completed, :total, :rate, :use_rate
+
+ def initialize(window)
+ @window = window
+ @need_update = true
+
+ @fn = ""
+ @dest = ""
+ @status = ""
+ @completed = 0
+ @total = 0
+ @dlamt = 0
+ @dlrate = 0
+ @ulamt = 0
+ @ulrate = 0
+ @rate = 0
+ @conn_peers = 0
+ @fail_peers = 0
+ @untried_peers = 0
+ @tracker = "not connected"
+ @errcount = 0
+ @dlblocks = 0
+ @ulblocks = 0
+
+ @got_blocks = 0
+ @sent_blocks = 0
+ @last_got_block = nil
+ @last_sent_block = nil
+ @start_time = nil
+
+ @use_rate = false
+ end
+
+ def got_block
+ @got_blocks += 1
+ @last_got_block = Time.now
+ end
+
+ def sent_block
+ @sent_blocks += 1
+ @last_sent_block = Time.now
+ end
+
+ def sigwinch_handler(sig = nil)
+ @need_update = true
+ end
+
+ def start_timer
+ @start_time = Time.now
+ end
+
+ def draw
+ if @need_update
+ update_size
+ @window.erase
+ end
+
+ complete_width = [@cols - 23, 0].max
+ complete_ticks = ((@completed.to_f / @total) * complete_width)
+
+ elapsed = (@start_time ? Time.now - @start_time : nil)
+ rate = (use_rate ? @rate : @dlrate)
+ remaining = rate && (rate > 0 ? (@total - @completed).to_f / rate : nil)
+
+ dlstall = @last_got_block && ((Time.now - @last_got_block) > STALL_SECS)
+ ulstall = @last_sent_block && ((Time.now - @last_sent_block) > STALL_SECS)
+
+ line = 1
+ [
+ "Contents: #@fn",
+ " Dest: #@dest",
+ "",
+ " Status: #@status",
+ "Progress: [" + ("#" * complete_ticks),
+ " Time: elapsed #{elapsed.to_time}, remaining #{remaining.to_time}",
+ "Download: #{@dlamt.to_sz} at #{dlstall ? '(stalled)' : @dlrate.to_sz + '/s'}",
+ " Upload: #{@ulamt.to_sz} at #{ulstall ? '(stalled)' : @ulrate.to_sz + '/s'}",
+ " Peers: connected to #@conn_peers (#@fail_peers failed, #@untried_peers untried)",
+ " Tracker: #@tracker",
+ " Errors: #@errcount",
+ ].each do |s|
+ break if line > @rows
+ @window.mvaddnstr(line, 2, s + (" " * @cols), @cols - 4)
+ line += 1
+ end
+
+ ## progress bar tail
+ @window.mvaddstr(5, @cols - 11, sprintf("] %.2f%% ", (@completed.to_f / @total) * 100.0))
+ @window.mvaddnstr(7, 31, "|" + ("#" * (@dlrate / 1024)) + (" " * @cols), @cols - 31 - 2)
+ @window.mvaddnstr(8, 31, "|" + ('#' * (@ulrate / 1024)) + (" " * @cols), @cols - 31 - 2)
+
+ @window.box(0,0)
+
+# @got_blocks -= 1 unless @got_blocks == 0
+# @sent_blocks -= 1 unless @sent_blocks == 0
+ end
+
+ private
+
+ def update_size
+ rows = []
+ cols = []
+ ## jesus CHRIST this is a shitty interface.
+ @window.getmaxyx(rows, cols)
+ @rows = rows[0]
+ @cols = cols[0]
+ @need_update = false
+ end
+end
+
+begin
+ mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+ die %{Error: can\'t parse metainfo file "#{torrent}"---maybe not a .torrent?}
+rescue RubyTorrent::TypedStructError => e
+ $stderr << <<EOS
+error parsing metainfo file, and it's likely something I should know about.
+please email the torrent file to wmorgan-rubytorrent-bug at masanjin.net,
+along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
+EOS
+
+ raise e
+rescue IOError, SystemCallError => e
+ $stderr.puts %{Error: can't read file "#{torrent}": #{e.message}}
+ exit
+end
+
+unless dest.nil?
+ if FileTest.directory?(dest) && mi.info.single?
+ dest = File.join(dest, mi.info.name)
+ elsif FileTest.file?(dest) && mi.info.multiple?
+ die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
+ end
+end
+
+def handle_any_input(display)
+ case(Ncurses.getch())
+ when ?q, ?Q
+ Ncurses.curs_set(1)
+ Ncurses.endwin()
+ exit
+ when Ncurses::KEY_RESIZE
+ display.sigwinch_handler
+ end
+end
+
+Ncurses.initscr
+
+begin
+ Ncurses.nl()
+ Ncurses.noecho()
+ Ncurses.curs_set(0)
+ Ncurses.stdscr.nodelay(true)
+ Ncurses.timeout(0)
+
+ display = Display.new Ncurses.stdscr
+ display.status = "checking file on disk..."
+ display.dest = File.expand_path(dest || mi.info.name) + (mi.single? ? "" : "/")
+ if mi.single?
+ display.fn = "#{mi.info.name} (#{mi.info.length.to_sz} in one file)"
+ else
+ display.fn = "#{mi.info.name}/ (#{mi.info.total_length.to_sz} in #{mi.info.files.length} files)"
+ end
+ display.total = mi.info.num_pieces * mi.info.piece_length
+ display.completed = 0
+ display.draw; Ncurses.refresh
+
+ display.use_rate = true
+ display.start_timer
+ num_pieces = 0
+ start = Time.now
+ every = 10
+ package = RubyTorrent::Package.new(mi, dest) do |piece|
+ num_pieces += 1
+ if (num_pieces % every) == 0
+ display.completed = (num_pieces * mi.info.piece_length)
+ display.rate = display.completed.to_f / (Time.now - start)
+ handle_any_input display
+ display.draw; Ncurses.refresh
+ end
+ end
+
+ display.status = "starting peer..."
+ display.use_rate = false
+ display.draw; Ncurses.refresh
+ bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy, :dlratelim => dlratelim, :ulratelim => ulratelim)
+
+ connecting = true
+ bt.on_event(self, :received_block) do |s, b, peer|
+ display.got_block
+ connecting = false
+ end
+ bt.on_event(self, :sent_block) do |s, b, peer|
+ display.sent_block
+ connecting = false
+ end
+ bt.on_event(self, :discarded_piece) { |s, p| display.errcount += 1 }
+ bt.on_event(self, :tracker_connected) do |s, url|
+ display.tracker = url
+ display.untried_peers = bt.num_possible_peers
+ end
+ bt.on_event(self, :tracker_lost) { |s, url| display.tracker = "can't connect to #{url}" }
+ bt.on_event(self, :forgetting_peer) { |s, p| display.fail_peers += 1 }
+ bt.on_event(self, :removed_peer, :added_peer) do |s, p|
+ if (display.conn_peers = bt.num_active_peers) == 0
+ connecting = true
+ end
+ end
+ bt.on_event(self, :added_peer) { |s, p| display.conn_peers += 1 }
+ bt.on_event(self, :trying_peer) { |s, p| display.untried_peers -= 1 unless display.untried_peers == 0 }
+
+ display.total = bt.total_bytes
+ display.start_timer
+
+ while true
+ handle_any_input(display)
+
+ display.status = if bt.complete?
+ "seeding (download complete)"
+ elsif connecting
+ "connecting to peers"
+ else
+ "downloading"
+ end
+ display.draw; Ncurses.refresh
+
+ display.dlamt = bt.dlamt
+ display.dlrate = bt.dlrate
+ display.ulamt = bt.ulamt
+ display.ulrate = bt.ulrate
+ display.completed = bt.bytes_completed
+ display.draw; Ncurses.refresh
+ sleep(0.5)
+ end
+ensure
+ Ncurses.curs_set(1)
+ Ncurses.endwin()
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rtpeer.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,125 @@
+## rtpeer.rb -- RubyTorrent line-mode BitTorrent peer.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent"
+
+Thread.abort_on_exception = true # make debugging easier
+
+def die(x); $stderr << "#{x}\n" && exit(-1); end
+def syntax
+ %{
+ Syntax: rtpeer <.torrent filename or URL> [<destination file or directory>]
+
+ rtpeer is a very simple line-based BitTorrent peer. You can use it
+ to download .torrents or to seed them, but it's mainly good for debugging.
+ }
+end
+
+class Numeric
+ def k; (self.to_f / 1024.0); end
+ def f(format="0.0")
+ sprintf("%#{format.to_s}f", self)
+ end
+end
+
+proxy = ENV["http_proxy"]
+torrent = ARGV.shift or die syntax
+dest = ARGV.shift
+
+puts "reading torrent..."
+begin
+ mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
+rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
+ die %{Error: can't parse metainfo file "#{torrent}"---maybe not a .torrent?}
+rescue RubyTorrent::TypedStructError => e
+ $stderr << <<EOS
+error parsing metainfo file, and it's likely something I should know about.
+please email the torrent file to wmorgan-rubytorrent-bug at masanjin.net,
+along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
+EOS
+
+ raise e
+rescue IOError, SystemCallError => e
+ die %{Error: can't read file "#{torrent}": #{e.message}}
+end
+
+unless dest.nil?
+ if FileTest.directory?(dest) && mi.info.single?
+ dest = File.join(dest, mi.info.name)
+ elsif FileTest.file?(dest) && mi.info.multiple?
+ die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
+ end
+end
+print "checking file status: " ; $stdout.flush
+package = RubyTorrent::Package.new(mi, dest) do |piece|
+ print(piece.complete? && piece.valid? ? "#" : ".")
+ $stdout.flush
+end
+puts " done"
+
+puts "starting peer..."
+bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy) #, :dlratelim => 20*1024, :ulratelim => 10*1024)
+
+unless $DEBUG # these are duplicated by debugging information
+ bt.on_event(self, :trying_peer) { |s, p| puts "trying peer #{p}" }
+ bt.on_event(self, :forgetting_peer) { |s, p| puts "couldn't connect to peer #{p}" }
+ bt.on_event(self, :removed_peer) { |s, p| puts "disconnected from peer #{p}" }
+end
+bt.on_event(self, :added_peer) { |s, p| puts "connected to peer #{p}" }
+bt.on_event(self, :received_block) { |s, b, peer| puts "<- got block #{b} from peer #{peer}, now #{package.pieces[b.pindex].percent_done.f}% done and #{package.pieces[b.pindex].percent_claimed.f}% claimed" }
+bt.on_event(self, :sent_block) { |s, b, peer| puts "-> sent block #{b} to peer #{peer}" }
+bt.on_event(self, :requested_block) { |s, b, peer| puts "-- requested block #{b} from #{peer}" }
+bt.on_event(self, :have_piece) { |s, p| puts "***** got complete and valid piece #{p}" }
+bt.on_event(self, :discarded_piece) { |s, p| puts "XXXXX checksum error on piece #{p}, discarded" }
+bt.on_event(self, :tracker_connected) { |s, url| puts "[tracker] connected to tracker #{url}" }
+bt.on_event(self, :tracker_lost) { |s, url| puts "[tracker] couldn't connect to tracker #{url}" }
+bt.on_event(self, :complete) do
+ puts <<EOS
+*********************
+* download complete *
+*********************
+EOS
+end
+
+puts "listening on #{bt.ip} port #{bt.port}"
+
+thread = nil
+## not sure if this works on windows, but it's just a nicety anyways.
+Signal.trap("INT") do
+ Signal.trap("INT", "DEFAULT") # second ^C will really kill us
+ thread.kill unless thread.nil?
+ bt.shutdown_all
+end unless $DEBUG
+
+thread = Thread.new do
+ while true
+ puts "-" * 78
+ ps = bt.peer_info.sort_by { |h| h[:start_time] }.reverse
+ puts <<EOS
+downloaded #{bt.dlamt.k.f}k @ #{bt.dlrate.k.f}kb/s, uploaded #{bt.ulamt.k.f}k @ #{bt.ulrate.k.f}kb/s
+completed: #{bt.pieces_completed} / #{bt.num_pieces} pieces = #{bt.bytes_completed.k.f}kb / #{bt.total_bytes.k.f}kb = #{bt.percent_completed.f}%
+tracker: #{bt.tracker || "not connected"}
+connected to #{ps.length} / #{bt.num_possible_peers} possible peers:
+EOS
+ ps.each do |p|
+ puts <<EOS
+dl #{p[:dlamt].k.f(4.0)}kb @#{p[:dlrate].k.f(3.0)}kb/s, ul #{p[:ulamt].k.f(4.0)}kb @#{p[:ulrate].k.f(3.0)}kb/s, i: #{(p[:interested] ? 'y' : 'n')}/#{(p[:peer_interested] ? 'y' : 'n')} (#{p[:we_desire].f(4.0)}/#{p[:they_desire].f(4.0)}) c: #{(p[:choking] ? 'y' : 'n')}/#{(p[:peer_choking] ? 'y' : 'n')} p: #{p[:pending_send]}/#{p[:pending_recv]}#{(p[:snubbing] ? ', snub' : '')} #{p[:name]}
+EOS
+ end
+ puts "-" * 78
+ sleep 30
+ end
+end
+
+thread.join
+puts "done"
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/bencoding.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,174 @@
+## bencoding.rb -- parse and generate bencoded values.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'uri'
+require 'digest/sha1'
+
+module RubyTorrent
+
+## we mess in the users' namespaces in this file. there's no good way
+## around it. i don't think it's too egregious though.
+
+class BEncodingError < StandardError; end
+
+class BStream
+ include Enumerable
+
+ @@classes = []
+
+ def initialize(s)
+ @s = s
+ end
+
+ def self.register_bencoded_class(c)
+ @@classes.push c
+ end
+
+ def each
+ happy = true
+ begin
+ happy = false
+ c = @s.getc
+ @@classes.each do |klass|
+ if klass.bencoded? c
+ o = klass.parse_bencoding(c, @s)
+ happy = true
+ yield o
+ break
+ end
+ end unless c.nil?
+ unless happy
+ @s.ungetc c unless c.nil?
+ end
+ end while happy
+ self
+ end
+end
+
+end
+
+class String
+ def to_bencoding
+ self.length.to_s + ":" + self.to_s
+ end
+
+ def self.bencoded?(c)
+ (?0 .. ?9).include? c
+ end
+
+ def self.parse_bencoding(c, s)
+ lens = c.chr
+ while ((x = s.getc) != ?:)
+ unless (?0 .. ?9).include? x
+ s.ungetc x
+ raise RubyTorrent::BEncodingError, "invalid bencoded string length #{lens} + #{x}"
+ end
+ lens += x.chr
+ end
+ raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless lens.length <= 20
+ len = lens.to_i
+ raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless len >= 0
+ (len > 0 ? s.read(len) : "")
+ end
+
+ RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Integer
+ def to_bencoding
+ "i" + self.to_s + "e"
+ end
+
+ def self.bencoded?(c)
+ c == ?i
+ end
+
+ def self.parse_bencoding(c, s)
+ ints = ""
+ while ((x = s.getc.chr) != 'e')
+ raise RubyTorrent::BEncodingError, "invalid bencoded integer #{x.inspect}" unless x =~ /\d|-/
+ ints += x
+ end
+ raise RubyTorrent::BEncodingError, "invalid integer #{ints} (too long)" unless ints.length <= 20
+ int = ints.to_i
+ raise RubyTorrent::BEncodingError, %{can't parse bencoded integer "#{ints}"} if (int == 0) && (ints !~ /^0$/) #'
+ int
+ end
+
+ RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Time
+ def to_bencoding
+ self.to_i.to_bencoding
+ end
+end
+
+module URI
+ def to_bencoding
+ self.to_s.to_bencoding
+ end
+end
+
+class Array
+ def to_bencoding
+ "l" + self.map { |e| e.to_bencoding }.join + "e"
+ end
+
+ def self.bencoded?(c)
+ c == ?l
+ end
+
+ def self.parse_bencoding(c, s)
+ ret = RubyTorrent::BStream.new(s).map { |x| x }
+ raise RubyTorrent::BEncodingError, "missing list terminator" unless s.getc == ?e
+ ret
+ end
+
+ RubyTorrent::BStream.register_bencoded_class self
+end
+
+class Hash
+ def to_bencoding
+ "d" + keys.sort.map do |k|
+ v = self[k]
+ if v.nil?
+ nil
+ else
+ [k.to_bencoding, v.to_bencoding].join
+ end
+ end.compact.join + "e"
+ end
+
+ def self.bencoded?(c)
+ c == ?d
+ end
+
+ def self.parse_bencoding(c, s)
+ ret = {}
+ key = nil
+ RubyTorrent::BStream.new(s).each do |x|
+ if key == nil
+ key = x
+ else
+ ret[key] = x
+ key = nil
+ end
+ end
+
+ raise RubyTorrent::BEncodingError, "no dictionary terminator" unless s.getc == ?e
+ ret
+ end
+
+ RubyTorrent::BStream.register_bencoded_class self
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/controller.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,610 @@
+## controller.rb -- cross-peer logic.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require 'thread'
+
+module RubyTorrent
+
+## keeps pieces in order
+class PieceOrder
+ POP_RECALC_THRESH = 20 # popularity of all pieces is recalculated
+ # (expensive sort) when this number of pieces
+ # have arrived at any of the peers, OR:
+ POP_RECALC_LIMIT = 30 # ... when this many seconds have passed when
+ # at least one piece has changed in
+ # popularity, or if we're in fuseki mode.
+ def initialize(package)
+ @package = package
+ @order = nil
+ @num_changed = 0
+ @pop = Array.new(@package.pieces.length, 0)
+ @jitter = Array.new(@package.pieces.length) { rand }
+ @m = Mutex.new
+ @last_recalc = nil
+ end
+
+ ## increment the popularity of a piece
+ def inc(i)
+ @m.synchronize do
+ @pop[i.to_i] += 1
+ @num_changed += 1
+ end
+ end
+
+ ## increment the popularity of multiple pieces
+ def inc_all(bitfield, inc=1)
+ @m.synchronize do
+ bitfield.each_index do |i|
+ if bitfield[i]
+ @pop[i] += inc
+ @num_changed += 1
+ end
+ end
+ end
+ end
+
+ def dec_all(bitfield)
+ inc_all(bitfield, -1)
+ end
+
+ def each(in_fuseki, num_peers)
+ if (@num_changed > POP_RECALC_THRESH) || @last_recalc.nil? || (((@num_changed > 0) || in_fuseki) && ((Time.now - @last_recalc) > POP_RECALC_LIMIT))
+ rt_debug "* reordering pieces: (#@num_changed changed, last recalc #{(@last_recalc.nil? ? '(never)' : (Time.now - @last_recalc).round)}s ago)..."
+ recalc_order(in_fuseki, num_peers)
+ end
+
+ @order.each { |i| yield i }
+ end
+
+ private
+
+ def recalc_order(in_fuseki, num_peers)
+ @m.synchronize do
+ @num_changed = 0
+ @order = (0 ... @pop.length).sort_by do |i|
+ p = @package.pieces[i]
+ @jitter[i] +
+ if p.started? && !p.complete? # always try to complete a started piece
+ pri = -1 + p.unclaimed_bytes.to_f / p.length
+ rt_debug " piece #{i} is started but not completed => priority #{pri} (#{p.percent_claimed.round}% claimed, #{p.percent_done.round}% done)"
+ pri
+ elsif p.complete? # don't need these
+# puts " piece #{i} is completed => #{@pop.length}"
+ @pop.length # a big number
+ elsif in_fuseki # distance from (# peers) / 2
+# puts " piece #{i} has fuseki score #{(@pop[i] - (num_peers / 2)).abs}"
+ (@pop[i] - (num_peers / 2)).abs
+ else
+# puts " piece #{i} has popularity #{@pop[i]}"
+ @pop[i]
+ end
+ end
+ end
+ @last_recalc = Time.now
+ rt_debug "* new piece priority: " + @order[0...15].map { |x| x.to_s }.join(', ') + " ..."
+ end
+end
+
+## The Controller manages all PeerConnections for a single Package. It
+## instructs them to request blocks, and tells them whether to choke
+## their connections or not. It also reports progress to the tracker.
+##
+## Incoming, post-handshake peer connections are added by the Server
+## via calling add_connection; deciding to accept these is the
+## Controller's responsibility, as is connecting to any new peers.
+class Controller
+ include EventSource
+ extend AttrReaderQ, MinIntervalMethods
+
+ ## general behavior parameters
+ HEARTBEAT = 5 # seconds between iterations of the heartbeat
+ MAX_PEERS = 15 # hard limit on the number of peers
+ ENDGAME_PIECE_THRESH = 5 # (wild guess) number of pieces remaining
+ # before we trigger end-game mode
+ FUSEKI_PIECE_THRESH = 2 # number of pieces we must have before
+ # getting out of fuseki mode. in fuseki
+ # ("opening", if you're not a weiqi/go fan)
+ # mode, rather than ranking pieces by
+ # rarity, we rank them by how distant their
+ # popularity is from (# peers) / 2, and we're
+ # also stingly in handing out requests.
+ SPAWN_NEW_PEER_THRESH = 0.75 # portion of the download rate above
+ # which we'll stop making new peer
+ # connections
+ RATE_WINDOW = 20 # window size (in seconds) of the rate calculation.
+ # presumably this should be the same as the window
+ # used in the RateMeter class.
+
+ ## tracker parameters. when we can't access a tracker, we retry at
+ ## DEAD_TRACKER_INITIAL_DELAY seconds and double that after every
+ ## failure, capping at DEAD_TRACKER_MAX_DELAY.
+ DEAD_TRACKER_INITIAL_INTERVAL = 5
+ DEAD_TRACKER_MAX_INTERVAL = 3600
+
+ ## single peer parameters
+ KEEPALIVE_INTERVAL = 120 # seconds of silence before sending a keepalive
+ SILENT_DEATH_INTERVAL = 240 # seconds of silence before we drop a peer
+ BOREDOM_DEATH_INTERVAL = 120 # seconds of existence with no downloaded data
+ # at which we drop a peer in favor of
+ # an incoming peer (unless the package
+ # is complete)
+
+ BLOCK_SIZE = 2**15 # send this size blocks. need to find out more
+ # about this parameter: how does it affect
+ # transfer rates?
+
+ ## antisnubbing
+ ANTISNUB_RATE_THRESH = 1024 # if the total bytes/second across all
+ # peers falls below this threshold, we
+ # trigger anti-snubbing mode
+ ANTISNUB_INTERVAL = 60 # seconds of no blocks from a peer before we
+ # add an optimistic unchoke slot when in
+ # anti-snubbing mode.
+
+ ## choking and optimistic unchoking parameters
+ NUM_FRIENDS = 4 # number of peers unchoked due to high download rates
+ CALC_FRIENDS_INTERVAL = 10 # seconds between recalculating choked
+ # status for each peer
+ CALC_OPTUNCHOKES_INTERVAL = 30 # seconds between reassigning
+ # optimistic unchoked status
+ NUM_OPTUNCHOKES = 1 # number of optimistic unchoke slots
+ # (not including any temporary ones
+ # generated in anti-snubbing mode.
+ NEW_OPTUNCHOKE_PROB = 0.5 # peers are ranked by the age of
+ # their connection, and optimistic
+ # unchoking slots are given with
+ # probability p*(1-p)^r, where r
+ # is the rank and p is this number.
+
+ attr_accessor :package, :info_hash, :tracker, :ulratelim, :dlratelim,
+ :http_proxy
+ attr_reader_q :running
+ event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
+ :received_block, :sent_block, :have_piece, :discarded_piece,
+ :tracker_connected, :tracker_lost, :requested_block
+
+ def initialize(server, package, info_hash, trackers, dlratelim=nil, ulratelim=nil, http_proxy=ENV["http_proxy"])
+ @server = server
+ @info_hash = info_hash
+ @package = package
+ @trackers = trackers
+ @http_proxy = http_proxy
+
+ @dlratelim = dlratelim
+ @ulratelim = ulratelim
+
+ @peers = [].extend(ArrayShuffle)
+ @peers_m = Mutex.new
+ @thread = nil
+
+ @tracker = nil
+ @last_tracker_attempt = nil
+ @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
+
+ ## friends
+ @num_friends = 0
+ @num_optunchokes = 0
+ @num_snubbed = 0
+
+ ## keep track of the popularity of the pieces so as to assign
+ ## blocks optimally to peers.
+ @piece_order = PieceOrder.new @package
+
+ @running = false
+ end
+
+ def dlrate; @peers.inject(0) { |s, p| s + p.dlrate }; end
+ def ulrate; @peers.inject(0) { |s, p| s + p.ulrate }; end
+ def dlamt; @peers.inject(0) { |s, p| s + p.dlamt }; end
+ def ulamt; @peers.inject(0) { |s, p| s + p.ulamt }; end
+ def num_peers; @peers.length; end
+
+ def start
+ raise "already" if @running
+
+ find_tracker
+
+ @in_endgame = false
+ @in_antisnub = false
+ @in_fuseki = false
+ @running = true
+ @thread = Thread.new do
+ while @running
+ step
+ sleep HEARTBEAT
+ end
+ end
+
+ @peers.each { |p| p.start unless p.running? }
+
+ self
+ end
+
+ def shutdown
+ @running = false
+ @tracker.stopped unless @tracker.nil? rescue TrackerError
+ @thread.join(0.2)
+ @peers.each { |c| c.shutdown }
+ self
+ end
+
+ def to_s
+ "<#{self.class}: package #{@package}>"
+ end
+
+ ## this could be called at any point by the Server, if it receives
+ ## incoming peer connections.
+ def add_peer(p)
+ accept = true
+
+ if @peers.length >= MAX_PEERS && !@package.complete?
+ oldp = @peers.find { |x| !x.running? || ((x.dlamt == 0) && ((Time.now - x.start_time) > BOREDOM_DEATH_INTERVAL)) }
+
+ if oldp
+ rt_debug "killing peer for being boring: #{oldp}"
+ oldp.shutdown
+ else
+ rt_debug "too many peers, ignoring #{p}"
+ p.shutdown
+ accept = false
+ end
+ end
+
+ if accept
+ p.on_event(self, :received_block) { |peer, block| received_block(block, peer) }
+ p.on_event(self, :peer_has_piece) { |peer, piece| peer_has_piece(piece, peer) }
+ p.on_event(self, :peer_has_pieces) { |peer, bitfield| peer_has_pieces(bitfield, peer) }
+ p.on_event(self, :sent_block) { |peer, block| send_event(:sent_block, block, peer.name) }
+ p.on_event(self, :requested_block) { |peer, block| send_event(:requested_block, block, peer.name) }
+
+ @peers_m.synchronize do
+ @peers.push p
+ ## it's important not to call p.start (which triggers the
+ ## bitfield message) until it's been added to @peer, such that
+ ## any :have messages that might happen from other peers in
+ ## the mean time are propagated to it.
+ ##
+ ## of course that means we need to call p.start within the
+ ## mutex context so that the reaper section of the heartbeat
+ ## doesn't kill it between push and start.
+ ##
+ ## ah, the joys of threaded programming.
+ p.start if @running
+ end
+
+ send_event(:added_peer, p.name)
+ end
+ end
+
+ def received_block(block, peer)
+ if @in_endgame
+ @peers_m.synchronize { @peers.each { |p| p.cancel block if p.running? && (p != peer)} }
+ end
+ send_event(:received_block, block, peer.name)
+
+ piece = @package.pieces[block.pindex] # find corresponding piece
+ if piece.complete?
+ if piece.valid?
+ @peers_m.synchronize { @peers.each { |peer| peer.have_piece piece } }
+ send_event(:have_piece, piece)
+ else
+ rt_warning "#{self}: received data for #{piece} does not match SHA1 hash, discarding"
+ send_event(:discarded_piece, piece)
+ piece.discard
+ end
+ end
+ end
+
+ def peer_has_piece(piece, peer)
+ @piece_order.inc piece.index
+ end
+
+ def peer_has_pieces(bitfield, peer)
+ @piece_order.inc_all bitfield
+ end
+
+ ## yield all desired blocks, in order of desire. called by peers to
+ ## refill their queues.
+ def claim_blocks
+ @piece_order.each(@in_fuseki, @peers.length) do |i|
+ p = @package.pieces[i]
+ next if p.complete?
+# rt_debug "+ considering piece #{p}"
+ if @in_endgame
+ p.each_empty_block(BLOCK_SIZE) { |b| yield b }
+ else
+ p.each_unclaimed_block(BLOCK_SIZE) do |b|
+ if yield b
+ p.claim_block b
+ return if @in_fuseki # fuseki shortcut
+ end
+ end
+ end
+ end
+ end
+
+ def forget_blocks(blocks)
+# rt_debug "#{self}: forgetting blocks #{blocks.join(', ')}"
+ blocks.each { |b| @package.pieces[b.pindex].unclaim_block b }
+ end
+
+ def peer_info
+ @peers.map do |p|
+ next nil unless p.running?
+ {:name => p.name, :seed => p.peer_complete?,
+ :dlamt => p.dlamt, :ulamt => p.ulamt,
+ :dlrate => p.dlrate, :ulrate => p.ulrate,
+ :pending_send => p.pending_send, :pending_recv => p.pending_recv,
+ :interested => p.interested?, :peer_interested => p.peer_interested?,
+ :choking => p.choking?, :peer_choking => p.peer_choking?,
+ :snubbing => p.snubbing?,
+ :we_desire => @package.pieces.inject(0) do |s, piece|
+ s + (!piece.complete? && p.piece_available?(piece.index) ? 1 : 0)
+ end,
+ :they_desire => @package.pieces.inject(0) do |s, piece|
+ s + (piece.complete? && !p.piece_available?(piece.index) ? 1 : 0)
+ end,
+ :start_time => p.start_time
+ }
+ end.compact
+ end
+
+ private
+
+ def find_tracker
+ return if @tracker || (@last_tracker_attempt && (Time.now - @last_tracker_attempt) < @tracker_delay)
+
+ @last_tracker_attempt = Time.now
+ Thread.new do
+ @trackers.each do |tracker|
+ break if @tracker
+ rt_debug "trying tracker #{tracker}"
+ tc = TrackerConnection.new(tracker, @info_hash, @package.size, @server.port, @server.id, nil, 50, @http_proxy)
+ begin
+ @tracker = tc.started
+ tc.already_completed if @package.complete?
+ @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
+ send_event(:tracker_connected, tc.url)
+ rescue TrackerError => e
+ rt_debug "couldn't connect: #{e.message}"
+ end
+ end
+ end
+
+ @tracker_delay = [@tracker_delay * 2, DEAD_TRACKER_MAX_INTERVAL].min if @tracker.nil?
+ rt_warning "couldn't connect to tracker, next try in #@tracker_delay seconds" if @tracker.nil?
+ end
+
+ def add_a_peer
+ return false if @tracker.nil? || (@peers.length >= MAX_PEERS) || @package.complete? || (@num_friends >= NUM_FRIENDS) || (@dlratelim && (dlrate > (@dlratelim * SPAWN_NEW_PEER_THRESH)))
+
+ @tracker.peers.shuffle.each do |peer|
+# rt_debug "]] comparing: #{peer.ip} vs #{@server.ip} and #{peer.port} vs #{@server.port} (tried? #{peer.tried?})"
+ next if peer.tried? || ((peer.ip == @server.ip) && (peer.port == @server.port)) rescue next
+
+ peername = "#{peer.ip}:#{peer.port}"
+ send_event(:trying_peer, peername)
+
+ Thread.new do # this may ultimately result in a call to add_peer
+ sleep rand(10)
+ rt_debug "=> making outgoing connection to #{peername}"
+ begin
+ peer.tried = true
+ socket = TCPSocket.new(peer.ip, peer.port)
+ @server.add_connection(peername, self, socket)
+ rescue SocketError, SystemCallError, Timeout::Error => e
+ rt_debug "couldn't connect to #{peername}: #{e}"
+ send_event(:forgetting_peer, peername)
+ end
+ end
+ break
+ end
+ true
+ end
+
+ def refresh_tracker
+ return if @tracker.nil?
+
+ @tracker.downloaded = dlamt
+ @tracker.uploaded = ulamt
+ @tracker.left = @package.size - @package.bytes_completed
+ begin
+ @tracker.refresh
+ rescue TrackerError
+ send_event(:tracker_lost, @tracker.url)
+ @tracker = nil
+ find_tracker # find a new one
+ end
+ end
+
+ def calc_friends
+ @num_friends = 0
+
+ if @package.complete?
+ @peers.sort_by { |p| -p.ulrate }.each do |p|
+ next if p.snubbing? || !p.running?
+ p.choke = (@num_friends >= NUM_FRIENDS)
+ @num_friends += 1 if p.peer_interested?
+ end
+ else
+ @peers.sort_by { |p| -p.dlrate }.each do |p|
+ next if p.snubbing? || !p.running?
+ p.choke = (@num_friends >= NUM_FRIENDS)
+ @num_friends += 1 if p.peer_interested?
+ end
+ end
+ end
+ min_interval :calc_friends, CALC_FRIENDS_INTERVAL
+
+ def calc_optunchokes
+ rt_debug "* calculating optimistic unchokes..."
+ @num_optunchokes = 0
+
+ if @in_antisnub
+ ## count up the number of our fair weather friends: peers who
+ ## are interested and whom we're not choking, but who haven't
+ ## sent us a block for ANTISNUB_INTERVAL seconds. for each of
+ ## these, we add an extra optimistic unchoking slot to our usual
+ ## NUM_OPTUNCHOKES slots. in actuality that's the number of
+ ## friends PLUS the number of optimistic unchokes who are
+ ## snubbing us, but that's not a big deal, as long as we cap the
+ ## number of extra slots at NUM_FRIENDS.
+ @num_optunchokes -= @peers.inject(0) { |s, p| s + (p.running? && p.peer_interested? && !p.choking? && (Time.now - (p.last_recv_block_time || p.start_time) > ANTISNUB_INTERVAL) ? 1 : 0) }
+ @num_optunchokes = [-NUM_FRIENDS, @num_optunchokes].max
+ rt_debug "* anti-snubbing mode, #{- at num_optunchokes} extra optimistic unchoke slots"
+ end
+
+ ## i love ruby
+ @peers.find_all { |p| p.running? }.sort_by { |p| p.start_time }.reverse.each do |p|
+ break if @num_optunchokes >= NUM_OPTUNCHOKES
+ next if p.snubbing?
+# rt_debug "* considering #{p}: #{p.peer_interested?} and #{@num_optunchokes < NUM_OPTUNCHOKES} and #{rand(0.999) < NEW_OPTUNCHOKE_PROB}"
+ if p.peer_interested? && (rand < NEW_OPTUNCHOKE_PROB)
+ rt_debug " #{p}: awarded optimistic unchoke"
+ p.choke = false
+ @num_optunchokes += 1
+ end
+ end
+ end
+ min_interval :calc_optunchokes, CALC_OPTUNCHOKES_INTERVAL
+
+ ## the "heartbeat". all time-based actions are triggered here.
+ def step
+ ## see if we should be in antisnubbing mode
+ if !@package.complete? && (dlrate < ANTISNUB_RATE_THRESH)
+ rt_debug "= dl rate #{dlrate} < #{ANTISNUB_RATE_THRESH}, in antisnub mode" if !@in_antisnub
+ @in_antisnub = true
+ else
+ rt_debug "= dl rate #{dlrate} >= #{ANTISNUB_RATE_THRESH}, out of antisnub mode" if @in_antisnub
+ @in_antisnub = false
+ end
+
+ ## see if we should be in fuseki mode
+ if !@package.complete? && (@package.pieces_completed < FUSEKI_PIECE_THRESH)
+ rt_debug "= num pieces #{@package.pieces_completed} < #{FUSEKI_PIECE_THRESH}, in fuseki mode" if !@in_fuseki
+ @in_fuseki = true
+ else
+ rt_debug "= num pieces #{@package.pieces_completed} >= #{FUSEKI_PIECE_THRESH}, out of fuseki mode" if @in_fuseki
+ @in_fuseki = false
+ end
+
+ ## see if we should be in endgame mode
+ if @package.complete?
+ rt_debug "= left endgame mode" if @in_endgame
+ @in_endgame = false
+ elsif (@package.pieces.length - @package.pieces_completed) <= ENDGAME_PIECE_THRESH
+ rt_debug "= have #{@package.pieces_completed} pieces, in endgame mode"
+ @in_endgame = true
+ end
+
+# puts " heartbeat: dlrate #{(dlrate / 1024.0).round}kb/s (lim #{(@dlratelim ? (@dlratelim / 1024.0).round : 'none')}) ulrate #{(ulrate / 1024.0).round}kb/s (lim #{(@ulratelim ? (@ulratelim / 1024.0).round : 'none')}) endgame? #@in_endgame antisnubbing? #@in_antisnub fuseki? #@in_fuseki"
+# @package.pieces.each do |p|
+# next if p.complete? || !p.started?
+# l1 = 0
+# p.each_unclaimed_block(9999999) { |b| l1 += b.length }
+# l2 = 0
+# p.each_empty_block(9999999) { |b| l2 += b.length }
+# puts " heartbeat: #{p.index}: #{l1} unclaimed bytes, #{l2} unfilled bytes"
+# end
+
+ ## find a tracker if we aren't already connected to one
+ find_tracker if @tracker.nil?
+
+ if @package.complete? # if package is complete...
+ ## kill all peers who are complete as well, as per bram's client
+ @peers.each { |p| p.shutdown if p.peer_complete? }
+ @tracker.completed unless @tracker.nil? || @tracker.sent_completed?
+ ## reopen all files as readonly (dunno why, just seems like a
+ ## good idea)
+ @package.reopen_ro unless @package.ro?
+ end
+
+ ## kill any silent connections, and anyone who hasn't sent or
+ ## received data in a long time.
+ @peers_m.synchronize do
+ @peers.each do |p|
+ next unless p.running?
+ if ((Time.now - (p.last_send_time || p.start_time)) > SILENT_DEATH_INTERVAL)
+ rt_warning "shutting down peer #{p} for silence/boredom"
+ p.shutdown
+ end
+ end
+ end
+
+ ## discard any dead connections
+ @peers_m.synchronize do
+ @peers.delete_if do |p|
+ !p.running? && begin
+ p.unregister_events self
+ @piece_order.dec_all p.peer_pieces
+ rt_debug "burying corpse of #{p}"
+ send_event(:removed_peer, p)
+ true
+ end
+ end
+ end
+
+ ## get more peers from the tracker, if all of the following are true:
+ ## a) the package is incomplete (i.e. we're downloading, not uploading)
+ ## b) we're connected to a tracker
+ ## c) we've tried all the peers we've gotten so far
+ ## d) the tracker hasn't already reported the maximum number of peers
+ if !@package.complete? && @tracker && (@tracker.peers.inject(0) { |s, p| s + (p.tried? ? 0 : 1) } == 0) && (@tracker.numwant <= @tracker.peers.length)
+ rt_debug "* getting more peers from the tracker"
+ @tracker.numwant += 50
+ unless @tracker.in_force_refresh
+ Thread.new do
+ begin
+ @tracker.force_refresh
+ rescue TrackerError
+ end
+ end
+ end
+ end
+
+ ## add peer if necessary
+ 3.times { add_a_peer } # there's no place like home
+
+
+ ## iterate choking policy
+ calc_friends
+ calc_optunchokes
+
+ ## this is needed. sigh.
+ break unless @running
+
+ ## send keepalives
+ @peers_m.synchronize { @peers.each { |p| p.send_keepalive if p.running? && p.last_send_time && ((Time.now - p.last_send_time) > KEEPALIVE_INTERVAL) } }
+
+ ## now we apportion our bandwidth amongst all the peers. we'll go
+ ## through them at random, dump everything we can, and move on iff
+ ## we don't expect to hit our bandwidth cap.
+ dllim = @dlratelim.nil? ? nil : (@dlratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (dlrate.to_f * RATE_WINDOW)
+ ullim = @ulratelim.nil? ? nil : (@ulratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (ulrate.to_f * RATE_WINDOW)
+ dl = ul = 0
+ @peers.shuffle.each do |p|
+ break if (dllim && (dl >= dllim)) || (ullim && (ul >= ullim))
+ if p.running?
+ pdl, pul = p.send_blocks_and_reqs(dllim && (dllim - dl), ullim && (ullim - ul))
+ dl += pdl
+ ul += pul
+ end
+ end
+
+ ## refresh tracker stats
+ refresh_tracker if @tracker
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/message.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,128 @@
+## message.rb -- peer wire protocol message parsing/composition
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+## we violate the users' namespaces here. but it's not in too
+## egregious of a way, and it's a royal pita to remove, so i'm keeping
+## it in for the time being.
+class String
+ def from_fbbe # four-byte big-endian integer
+ raise "fbbe must be four-byte string (got #{self.inspect})" unless length == 4
+ (self[0] << 24) + (self[1] << 16) + (self[2] << 8) + self[3]
+ end
+end
+
+class Integer
+ def to_fbbe # four-byte big-endian integer
+ raise "fbbe must be < 2^32" unless self <= 2**32
+ raise "fbbe must be >= 0" unless self >= 0
+ s = " "
+ s[0] = (self >> 24) % 256
+ s[1] = (self >> 16) % 256
+ s[2] = (self >> 8) % 256
+ s[3] = (self ) % 256
+ s
+ end
+end
+
+module RubyTorrent
+
+module StringExpandBits
+ include StringMapBytes
+
+ def expand_bits # just for debugging purposes
+ self.map_bytes do |b|
+ (0 .. 7).map { |i| ((b & (1 << (7 - i))) == 0 ? "0" : "1") }
+ end.flatten.join
+ end
+end
+
+class Message
+ WIRE_IDS = [:choke, :unchoke, :interested, :uninterested, :have, :bitfield,
+ :request, :piece, :cancel]
+
+ attr_accessor :id
+
+ def initialize(id, args=nil)
+ @id = id
+ @args = args
+ end
+
+ def method_missing(meth)
+ if @args.has_key? meth
+ @args[meth]
+ else
+ raise %{no such argument "#{meth}" to message #{self.to_s}}
+ end
+ end
+
+ def to_wire_form
+ case @id
+ when :keepalive
+ 0.to_fbbe
+ when :choke, :unchoke, :interested, :uninterested
+ 1.to_fbbe + WIRE_IDS.index(@id).chr
+ when :have
+ 5.to_fbbe + 4.chr + @args[:index].to_fbbe
+ when :bitfield
+ (@args[:bitfield].length + 1).to_fbbe + 5.chr + @args[:bitfield]
+ when :request, :cancel
+ 13.to_fbbe + WIRE_IDS.index(@id).chr + @args[:index].to_fbbe +
+ @args[:begin].to_fbbe + @args[:length].to_fbbe
+ when :piece
+ (@args[:length] + 9).to_fbbe + 7.chr + @args[:index].to_fbbe +
+ @args[:begin].to_fbbe
+ else
+ raise "unknown message type #{id}"
+ end
+ end
+
+ def self.from_wire_form(idnum, argstr)
+ type = WIRE_IDS[idnum]
+
+ case type
+ when :choke, :unchoke, :interested, :uninterested
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.nil? or (argstr.length == 0)
+ Message.new(type)
+
+ when :have
+ raise ProtocolError, "invalid length #{str.length} for #{type} message" unless argstr.length == 4
+ Message.new(type, {:index => argstr[0,4].from_fbbe})
+
+ when :bitfield
+ Message.new(type, {:bitfield => argstr})
+
+ when :request, :cancel
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 12
+ Message.new(type, {:index => argstr[0,4].from_fbbe,
+ :begin => argstr[4,4].from_fbbe,
+ :length => argstr[8,4].from_fbbe})
+ when :piece
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 8
+ Message.new(type, {:index => argstr[0,4].from_fbbe,
+ :begin => argstr[4,4].from_fbbe})
+ else
+ raise "unknown message #{type.inspect}"
+ end
+ end
+
+ def to_s
+ case @id
+ when :bitfield
+ %{bitfield <#{@args[:bitfield].extend(StringExpandBits).expand_bits}>}
+ else
+ %{#@id#{@args.nil? ? "" : "(" + @args.map { |k, v| "#{k}=#{v.to_s.inspect}" }.join(", ") + ")"}}
+ end
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/metainfo.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,214 @@
+## metainfo.rb -- parsed .torrent file
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent/typedstruct"
+require 'uri'
+require 'open-uri'
+require 'digest/sha1'
+
+## MetaInfo file is the parsed form of the .torrent file that people
+## send around. It contains a MetaInfoInfo and possibly some
+## MetaInfoInfoFile objects.
+module RubyTorrent
+
+class MetaInfoFormatError < StandardError; end
+
+class MetaInfoInfoFile
+ def initialize(dict=nil)
+ @s = TypedStruct.new do |s|
+ s.field :length => Integer, :md5sum => String, :sha1 => String,
+ :path => String
+ s.array :path
+ s.required :length, :path
+ end
+
+ @dict = dict
+ unless dict.nil?
+ @s.parse dict
+ check
+ end
+ end
+
+ def method_missing(meth, *args)
+ @s.send(meth, *args)
+ end
+
+ def check
+ raise MetaInfoFormatError, "invalid file length" unless @s.length >= 0
+ end
+
+ def to_bencoding
+ check
+ (@dict || @s).to_bencoding
+ end
+end
+
+class MetaInfoInfo
+ def initialize(dict=nil)
+ @s = TypedStruct.new do |s|
+ s.field :length => Integer, :md5sum => String, :name => String,
+ :piece_length => Integer, :pieces => String,
+ :files => MetaInfoInfoFile, :sha1 => String
+ s.label :piece_length => "piece length"
+ s.required :name, :piece_length, :pieces
+ s.array :files
+ s.coerce :files => lambda { |x| x.map { |y| MetaInfoInfoFile.new(y) } }
+ end
+
+ @dict = dict
+ unless dict.nil?
+ @s.parse dict
+ check
+ if dict["sha1"]
+ ## this seems to always be off. don't know how it's supposed
+ ## to be calculated, so fuck it.
+# puts "we have #{sha1.inspect}, they have #{dict['sha1'].inspect}"
+# rt_warning "info hash SHA1 mismatch" unless dict["sha1"] == sha1
+# raise MetaInfoFormatError, "info hash SHA1 mismatch" unless dict["sha1"] == sha1
+ end
+ end
+ end
+
+ def check
+ raise MetaInfoFormatError, "invalid file length" unless @s.length.nil? || @s.length >= 0
+ raise MetaInfoFormatError, "one (and only one) of 'length' (single-file torrent) or 'files' (multi-file torrent) must be specified" if (@s.length.nil? && @s.files.nil?) || (!@s.length.nil? && !@s.files.nil?)
+ if single?
+ length = @s.length
+ else
+ length = @s.files.inject(0) { |s, x| s + x.length }
+ end
+ raise MetaInfoFormatError, "invalid metainfo file: length #{length} > (#{@s.pieces.length / 20} pieces * #{@s.piece_length})" unless length <= (@s.pieces.length / 20) * @s.piece_length
+ raise MetaInfoFormatError, "invalid metainfo file: pieces length = #{@s.pieces.length} not a multiple of 20" unless (@s.pieces.length % 20) == 0
+ end
+
+ def to_bencoding
+ check
+ (@dict || @s).to_bencoding
+ end
+
+ def sha1
+ if @s.dirty
+ @sha1 = Digest::SHA1.digest(self.to_bencoding)
+ @s.dirty = false
+ end
+ @sha1
+ end
+
+ def single?
+ !length.nil?
+ end
+
+ def multiple?
+ length.nil?
+ end
+
+ def total_length
+ if single?
+ length
+ else
+ files.inject(0) { |a, f| a + f.length }
+ end
+ end
+
+ def num_pieces
+ pieces.length / 20
+ end
+
+ def method_missing(meth, *args)
+ @s.send(meth, *args)
+ end
+end
+
+class MetaInfo
+ def initialize(dict=nil)
+ raise TypeError, "argument must be a Hash (maybe see MetaInfo.from_location)" unless dict.is_a? Hash
+ @s = TypedStruct.new do |s|
+ s.field :info => MetaInfoInfo, :announce => URI::HTTP,
+ :announce_list => Array, :creation_date => Time,
+ :comment => String, :created_by => String, :encoding => String
+ s.label :announce_list => "announce-list", :creation_date => "creation date",
+ :created_by => "created by"
+ s.array :announce_list
+ s.coerce :info => lambda { |x| MetaInfoInfo.new(x) },
+ :creation_date => lambda { |x| Time.at(x) },
+ :announce => lambda { |x| URI.parse(x) },
+ :announce_list => lambda { |x| x.map { |y| y.map { |z| URI.parse(z) } } }
+ end
+
+ @dict = dict
+ unless dict.nil?
+ @s.parse dict
+ check
+ end
+ end
+
+ def single?; info.single?; end
+ def multiple?; info.multiple?; end
+
+ def check
+ if @s.announce_list
+ @s.announce_list.each do |tier|
+ tier.each { |track| raise MetaInfoFormatError, "expecting HTTP URL in announce-list, got #{track} instead" unless track.is_a? URI::HTTP }
+ end
+ end
+ end
+
+ def self.from_bstream(bs)
+ dict = nil
+ bs.each do |e|
+ if dict == nil
+ dict = e
+ else
+ raise MetaInfoFormatError, "too many bencoded elements for metainfo file (just need one)"
+ end
+ end
+
+ raise MetaInfoFormatError, "bencoded element must be a dictionary, got a #{dict.class}" unless dict.kind_of? ::Hash
+
+ MetaInfo.new(dict)
+ end
+
+ ## either a filename or a URL
+ def self.from_location(fn, http_proxy=ENV["http_proxy"])
+ if http_proxy # lame!
+ open(fn, "rb", :proxy => http_proxy) { |f| from_bstream(BStream.new(f)) }
+ else
+ open(fn, "rb") { |f| from_bstream(BStream.new(f)) }
+ end
+ end
+
+ def self.from_stream(s)
+ from_bstream(BStream.new(s))
+ end
+
+ def method_missing(meth, *args)
+ @s.send(meth, *args)
+ end
+
+ def to_bencoding
+ check
+ (@dict || @s).to_bencoding
+ end
+
+ def trackers
+ if announce_list && (announce_list.length > 0)
+ announce_list.map do |tier|
+ tier.extend(ArrayShuffle).shuffle
+ end.flatten
+ else
+ [announce]
+ end
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/package.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,595 @@
+## package.rb -- RubyTorrent <=> filesystem interface.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'thread'
+require 'digest/sha1'
+
+## A Package is the connection between the network and the
+## filesystem. There is one Package per torrent. Each Package is
+## composed of one or more Pieces, as determined by the MetaInfoInfo
+## object, and each Piece is composed of one or more Blocks, which are
+## transmitted over the PeerConnection with :piece comments.
+
+module RubyTorrent
+
+## Range plus a lot of utility methods
+class AwesomeRange < Range
+ def initialize(start, endd=nil, exclude_end=false)
+ case start
+ when Integer
+ raise ArgumentError, "both start and endd must be specified" if endd.nil?
+ super(start, endd, exclude_end)
+ when Range
+ super(start.first, start.last, start.exclude_end?)
+ else
+ raise ArgumentError, "start should be an Integer or a Range, is a #{start.class}"
+ end
+ end
+
+ ## range super-set: does this range encompass 'o'?
+ def rss?(o)
+ (first <= o.first) &&
+ ((last > o.last) || (o.exclude_end? && (last == o.last)))
+ end
+
+ ## range intersection
+ def rint(o)
+ ## three cases. either:
+ ## a) our left endpoint is within o
+ if ((first >= o.first) &&
+ ((first < o.last) || (!o.exclude_end? && (first == o.last))))
+ if last < o.last
+ AwesomeRange.new(first, last, exclude_end?)
+ elsif last > o.last
+ AwesomeRange.new(first, o.last, o.exclude_end?)
+ else # ==
+ AwesomeRange.new(first, last, exclude_end? || o.exclude_end?)
+ end
+ ## b) our right endpoint is within o
+ elsif (((last > o.first) || (!exclude_end? && (last == o.first))) && ((last < o.last) || (!o.exclude_end? && (last == o.last))))
+ AwesomeRange.new([first, o.first].max, last, exclude_end?)
+ ## c) we encompass o
+ elsif rss?(o)
+ o
+ else
+ nil
+ end
+ end
+
+ ## range continuity
+ def rcont?(o)
+ (first == o.last) || (last == o.first) || (rint(o) != nil)
+ end
+
+ ## range union: only valid for continuous ranges
+ def runion(o)
+ if last > o.last
+ AwesomeRange.new([first, o.first].min, last, exclude_end?)
+ elsif o.last > last
+ AwesomeRange.new([first, o.first].min, o.last, o.exclude_end?)
+ else # equal
+ AwesomeRange.new([first, o.first].min, last, (exclude_end? && o.exclude_end?))
+ end
+ end
+
+ ## range difference. returns an array of 0, 1 or 2 ranges.
+ def rdiff(o)
+ return [] if o == self
+ ret = []
+ int = rint o
+ return [] if int == self
+ return [self] if int == nil
+ raise RangeError, "can't subtract a range that doesn't have an exclusive end" unless int.exclude_end?
+ if int.first > first
+ ret << AwesomeRange.new(first, int.first, true)
+ end
+ ret + [AwesomeRange.new(int.last, last, exclude_end?)]
+ end
+end
+
+## a Covering is a set of non-overlapping ranges within a given start
+## point and endpoint.
+class Covering
+ attr_accessor :domain, :ranges
+
+ ## 'domain' should be an AwesomeRange determining the start and end
+ ## point. 'ranges' should be an array of non-overlapping
+ ## AwesomeRanges sorted by start point.
+ def initialize(domain, ranges=[])
+ @domain = domain
+ @ranges = ranges
+ end
+
+ def complete!; @ranges = [@domain]; self; end
+ def complete?; @ranges == [@domain]; end
+ def empty!; @ranges = []; self; end
+ def empty?; @ranges == []; end
+
+ ## given a covering of size N and a new range 'r', returns a
+ ## covering of size 0 <= s <= N + 1 that doesn't cover the range
+ ## given by 'r'.
+ def poke(r)
+ raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
+ Covering.new(@domain, @ranges.inject([]) do |set, x|
+ if x.rint(r) != nil
+ set + x.rdiff(r)
+ else
+ set + [x]
+ end
+ end)
+ end
+
+ ## given a covering of size N and a new range 'r', returns a
+ ## covering of size 0 < s <= N + 1 that also covers the range 'r'.
+ def fill(r)
+ raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
+ Covering.new(@domain, @ranges.inject([]) do |set, x|
+ ## r contains the result of the continuing merge. if r is nil,
+ ## then we've already added it, so we just copy x.
+ if r.nil? then set + [x] else
+ ## otoh, if r is there, we try and merge in the current
+ ## element.
+ if r.rcont? x
+ ## if we can merge, keep the union in r and don't add
+ ## anything
+ r = r.runion x
+ set
+ ## if we can't merge it, we'll see if it's time to add it. we
+ ## know that r and x don't overlap because r.mergable?(x) was
+ ## false, so we can simply compare the start points to see
+ ## whether it should come before x.
+ elsif r.first < x.first
+ s = set + [r, x] # add both
+ r = nil
+ s
+ else set + [x] ## no merging or adding, so we just copy x.
+ end
+ end
+ ## if 'r' still hasn't been added, it should be the last element,
+ ## we add it here.
+ end.push(r).compact)
+ end
+
+ ## given an array of non-overlapping ranges sorted by start point,
+ ## and a range 'domain', returns the first range from 'domain' not
+ ## covered by any range in the array.
+ def first_gap(domain=@domain)
+ start = domain.first
+ endd = nil
+ excl = nil
+ @ranges.each do |r|
+ next if r.last < start
+
+ if r.first > start # found a gap
+ if r.first < domain.last
+ return AwesomeRange.new(start, r.first, false)
+ else # r.first >= domain.last, so use domain's exclusion
+ return AwesomeRange.new(start, domain.last, domain.exclude_end?)
+ end
+ else # r.first <= start
+ start = r.last unless r.last < start
+ break if start > domain.last
+ end
+ end
+
+ if (start >= domain.last)
+ ## entire domain was covered
+ nil
+ else
+ ## tail end of the domain uncovered
+ AwesomeRange.new(start, domain.last, domain.exclude_end?)
+ end
+ end
+
+ def ==(o); o.domain == self.domain && o.ranges == self.ranges; end
+end
+
+## Blocks are very simple chunks of data which exist solely in
+## memory. they are the basic currency of the bittorrent protocol. a
+## Block can be divided into "chunks" (no intelligence there; it's
+## solely for the purposes of buffered reading/writing) and one or
+## more Blocks comprises a Piece.
+class Block
+ attr_accessor :pindex, :begin, :length, :data, :requested
+
+ def initialize(pindex, beginn, length)
+ @pindex = pindex
+ @begin = beginn
+ @length = length
+ @data = nil
+ @requested = false
+ @time = nil
+ end
+
+ def requested?; @requested; end
+ def have_length; @data.length; end
+ def complete?; @data && (@data.length == @length); end
+ def mark_time; @time = Time.now; end
+ def time_elapsed; Time.now - @time; end
+
+ def to_s
+ "<block: p[#{@pindex}], #@begin + #@length #{(data.nil? || (data.length == 0) ? 'emp' : (complete? ? 'cmp' : 'inc'))}>"
+ end
+
+ ## chunk can only be added to blocks in order
+ def add_chunk(chunk)
+ @data = "" if @data.nil?
+ raise "adding chunk would result in too much data (#{@data.length} + #{chunk.length} > #@length)" if (@data.length + chunk.length) > @length
+ @data += chunk
+ self
+ end
+
+ def each_chunk(blocksize)
+ raise "each_chunk called on incomplete block" unless complete?
+ start = 0
+ while(start < @length)
+ yield data[start, [blocksize, @length - start].min]
+ start += blocksize
+ end
+ end
+
+ def ==(o)
+ o.is_a?(Block) && (o.pindex == self.pindex) && (o.begin == self.begin) &&
+ (o.length == self.length)
+ end
+end
+
+## a Piece is the basic unit of the .torrent metainfo file (though not
+## of the bittorrent protocol). Pieces store their data directly on
+## disk, so many operations here will be slow. each Piece stores data
+## in one or more file pointers.
+##
+## unlike Blocks and Packages, which are either complete or
+## incomplete, a Piece can be complete but not valid, if the SHA1
+## check fails. thus, a call to piece.complete? is not sufficient to
+## determine whether the data is ok to use or not.
+##
+## Pieces handle all the trickiness involved with Blocks: taking in
+## Blocks from arbitrary locations, writing them out to the correct
+## set of file pointers, keeping track of which sections of the data
+## have been filled, claimed but not filled, etc.
+class Piece
+ include EventSource
+
+ attr_reader :index, :start, :length
+ event :complete
+
+ def initialize(index, sha1, start, length, files, validity_assumption=nil)
+ @index = index
+ @sha1 = sha1
+ @start = start
+ @length = length
+ @files = files # array of [file pointer, mutex, file length]
+ @valid = nil
+
+ ## calculate where we start and end in terms of the file pointers.
+ @start_index = 0
+ sum = 0
+ while(sum + @files[@start_index][2] <= @start)
+ sum += @files[@start_index][2]
+ @start_index += 1
+ end
+ ## now sum + @files[@start_index][2] > start, and sum <= start
+ @start_offset = @start - sum
+
+ ## sections of the data we have
+ @have = Covering.new(AwesomeRange.new(0 ... @length)).complete!
+ @valid = validity_assumption
+ @have.empty! unless valid?
+
+ ## sections of the data someone has laid claim to but hasn't yet
+ ## provided. a super-set of @have.
+ @claimed = Covering.new(AwesomeRange.new(0 ... @length))
+
+ ## protects @claimed, @have
+ @state_m = Mutex.new
+ end
+
+ def to_s
+ "<piece #@index: #@start + #@length #{(complete? ? 'cmp' : 'inc')}>"
+ end
+
+ def complete?; @have.complete?; end
+ def started?; !@claimed.empty? || !@have.empty?; end
+
+ def discard # discard all data
+ @state_m.synchronize do
+ @have.empty!
+ @claimed.empty!
+ end
+ @valid = false
+ end
+
+ def valid?
+ return @valid unless @valid.nil?
+ return (@valid = false) unless complete?
+
+ data = read_bytes(0, @length)
+ if (data.length != @length)
+ @valid = false
+ else
+ @valid = (Digest::SHA1.digest(data) == @sha1)
+ end
+ end
+
+ def unclaimed_bytes
+ r = 0
+ each_gap(@claimed) { |start, len| r += len }
+ r
+ end
+
+ def empty_bytes
+ r = 0
+ each_gap(@have) { |start, len| r += len }
+ r
+ end
+
+ def percent_claimed; 100.0 * (@length.to_f - unclaimed_bytes) / @length; end
+ def percent_done; 100.0 * (@length.to_f - empty_bytes) / @length; end
+
+ def each_unclaimed_block(max_length)
+ raise "no unclaimed blocks in a complete piece" if complete?
+
+ each_gap(@claimed, max_length) do |start, len|
+ yield Block.new(@index, start, len)
+ end
+ end
+
+ def each_empty_block(max_length)
+ raise "no empty blocks in a complete piece" if complete?
+
+ each_gap(@have, max_length) do |start, len|
+ yield Block.new(@index, start, len)
+ end
+ end
+
+ def claim_block(b)
+ @state_m.synchronize do
+ @claimed = @claimed.fill AwesomeRange.new(b.begin ... (b.begin + b.length))
+ end
+ end
+
+ def unclaim_block(b)
+ @state_m.synchronize do
+ @claimed = @claimed.poke AwesomeRange.new(b.begin ... (b.begin + b.length))
+ end
+ end
+
+ ## for a complete Piece, returns a complete Block of specified size
+ ## and location.
+ def get_complete_block(beginn, length)
+ raise "can't make block from incomplete piece" unless complete?
+ raise "invalid parameters #{beginn}, #{length}" unless (length > 0) && (beginn + length) <= @length
+
+ b = Block.new(@index, beginn, length)
+ b.add_chunk read_bytes(beginn, length) # returns b
+ end
+
+ ## we don't do any checking that this block has been claimed or not.
+ def add_block(b)
+ @valid = nil
+ write = false
+ new_have = @state_m.synchronize { @have.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) }
+ if new_have != @have
+ @have = new_have
+ write = true
+ end
+
+ write_bytes(b.begin, b.data) if write
+ send_event(:complete) if complete?
+ end
+
+ private
+
+ ## yields successive gaps from 'array' between 0 and @length
+ def each_gap(covering, max_length=nil)
+ return if covering.complete?
+
+ range_first = 0
+ while true
+ range = covering.first_gap(range_first ... @length)
+ break if range.nil? || (range.first == range.last)
+ start = range.first
+
+ while start < range.last
+ len = range.last - start
+ len = max_length if max_length && (max_length < len)
+ yield start, len
+ start += len
+ end
+
+ range_first = range.last
+ end
+ end
+
+ def write_bytes(start, data); do_bytes(start, 0, data); end
+ def read_bytes(start, length); do_bytes(start, length, nil); end
+
+ ## do the dirty work of splitting the read/writes across multiple
+ ## file pointers to possibly incomplete, possibly overcomplete files
+ def do_bytes(start, length, data)
+ raise ArgumentError, "invalid start" if (start < 0) || (start > @length)
+# raise "invalid length" if (length < 0) || (start + length > @length)
+
+ start += @start_offset
+ index = @start_index
+ sum = 0
+ while(sum + @files[index][2] <= start)
+ sum += @files[index][2]
+ index += 1
+ end
+ offset = start - sum
+
+ done = 0
+ abort = false
+ if data.nil?
+ want = length
+ ret = ""
+ else
+ want = data.length
+ ret = 0
+ end
+ while (done < want) && !abort
+ break if index > @files.length
+ fp, mutex, size = @files[index]
+ mutex.synchronize do
+ fp.seek offset
+ here = [want - done, size - offset].min
+ if data.nil?
+# puts "> reading #{here} bytes from #{index} at #{offset}"
+ s = fp.read here
+# puts "> got #{(s.nil? ? s.inspect : s.length)} bytes"
+ if s.nil?
+ abort = true
+ else
+ ret += s
+ abort = true if s.length < here
+# puts "fp.tell is #{fp.tell}, size is #{size}, eof #{fp.eof?}"
+ if (fp.tell == size) && !fp.eof?
+ rt_warning "file #{index}: not at eof after #{size} bytes, truncating"
+ fp.truncate(size - 1)
+ end
+ end
+ else
+# puts "> writing #{here} bytes to #{index} at #{offset}"
+ x = fp.write data[done, here]
+ ret += here
+# @files[index][0].flush
+ end
+ done += here
+ end
+ index += 1
+ offset = 0
+ end
+
+ ret
+ end
+end
+
+## finally, the Package. one Package per Controller so we don't do any
+## thread safety stuff in here.
+class Package
+ include EventSource
+
+ attr_reader :pieces, :size
+ event :complete
+
+ def initialize(metainfo, out=nil, validity_assumption=nil)
+ info = metainfo.info
+
+ created = false
+ out ||= info.name
+ case out
+ when File
+ raise ArgumentError, "'out' cannot be a File for a multi-file .torrent" if info.multiple?
+ fstream = out
+ when Dir
+ raise ArgumentError, "'out' cannot be a Dir for a single-file .torrent" if info.single?
+ fstream = out
+ when String
+ if info.single?
+ rt_debug "output file is #{out}"
+ begin
+ fstream = File.open(out, "rb+")
+ rescue Errno::ENOENT
+ created = true
+ fstream = File.open(out, "wb+")
+ end
+ else
+ rt_debug "output directory is #{out}"
+ unless File.exists? out
+ Dir.mkdir(out)
+ created = true
+ end
+ fstream = Dir.open(out)
+ end
+ else
+ raise ArgumentError, "'out' should be a File, Dir or String object, is #{out.class}"
+ end
+
+ @ro = false
+ @size = info.total_length
+ if info.single?
+ @files = [[fstream, Mutex.new, info.length]]
+ else
+ @files = info.files.map do |finfo|
+ path = File.join(finfo.path[0, finfo.path.length - 1].inject(fstream.path) do |path, el|
+ dir = File.join(path, el)
+ unless File.exist? dir
+ rt_debug "making directory #{dir}"
+ Dir.mkdir dir
+ end
+ dir
+ end, finfo.path[finfo.path.length - 1])
+ rt_debug "opening #{path}..."
+ [open_file(path), Mutex.new, finfo.length]
+ end
+ end
+
+ i = 0
+ @pieces = info.pieces.unpack("a20" * (info.pieces.length / 20)).map do |hash|
+ start = (info.piece_length * i)
+ len = [info.piece_length, @size - start].min
+ p = Piece.new(i, hash, start, len, @files, (created ? false : validity_assumption))
+ p.on_event(self, :complete) { send_event(:complete) if complete? }
+ yield p if block_given?
+ (i += 1) && p
+ end
+
+ reopen_ro if complete?
+ end
+
+ def ro?; @ro; end
+ def reopen_ro
+ raise "called on incomplete package" unless complete?
+ return if @ro
+
+ rt_debug "reopening all files with mode r"
+ @files = @files.map do |fp, mutex, size|
+ [fp.reopen(fp.path, "rb"), mutex, size]
+ end
+ @ro = true
+ end
+
+ def complete?; @pieces.detect { |p| !p.complete? || !p.valid? } == nil; end
+
+ def bytes_completed
+ @pieces.inject(0) { |s, p| s + (p.complete? ? p.length : 0) }
+ end
+
+ def pieces_completed
+ @pieces.inject(0) { |s, p| s + (p.complete? ? 1 : 0) }
+ end
+
+ def percent_completed
+ 100.0 * pieces_completed.to_f / @pieces.length.to_f
+ end
+
+ def num_pieces; @pieces.length; end
+
+ def to_s
+ "<#{self.class} size #@size>"
+ end
+
+ private
+
+ def open_file(path)
+ begin
+ File.open(path, "rb+")
+ rescue Errno::ENOENT
+ File.open(path, "wb+")
+ end
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/peer.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,536 @@
+## peer.rb -- bitttorrent peer ("wire") protocol.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require 'thread'
+require "rubytorrent/message"
+
+module RubyTorrent
+
+module ArrayToBitstring
+ def to_bitstring
+ ret = "\0"
+ bit = 7
+ map do |b|
+ if bit == -1
+ ret += "\0"
+ bit = 7
+ end
+ ret[ret.length - 1] |= (1 << bit) if b
+ bit -= 1
+ end
+ ret
+ end
+end
+
+module ArrayDelete2
+ ## just like delete but returns the *array* element deleted rather
+ ## than the argument. someone should file an rcr.
+ def delete2(el)
+ i = index el
+ unless i.nil?
+ ret = self[i]
+ delete_at i
+ ret
+ else
+ nil
+ end
+ end
+end
+
+module StringToBarray
+ include StringMapBytes
+ def to_barray
+ self.map_bytes do |b|
+ (0 .. 7).map { |i| (b & (1 << (7 - i))) != 0 }
+ end.flatten
+ end
+end
+
+## estimate a rate. basically copied from bram's code.
+class RateMeter
+ attr_reader :amt
+
+ def initialize(window=20)
+ @window = window.to_f
+ @amt = 0
+ @rate = 0
+ @last = @since = Time.now - 1
+ @m = Mutex.new
+ end
+
+ def add(new_amt)
+ now = Time.now
+ @m.synchronize do
+ @amt += new_amt
+ @rate = ((@rate * (@last - @since)) + new_amt).to_f / (now - @since)
+ @last = now
+ @since = [@since, now - @window].max
+ end
+ end
+
+ def rate
+ (@rate * (@last - @since)).to_f / (Time.now - @since)
+ end
+
+ def bytes_until(new_rate)
+ [(new_rate.to_f * (Time.now - @since)) - (@rate * (@last - @since)), 0].max
+ end
+end
+
+class ProtocolError < StandardError; end
+
+## The PeerConnection object deals with all the protocol issues. It
+## keeps state information as to the connection and the peer. It is
+## tightly integrated with the Controller object.
+##
+## Remember to be "strict in what you send, lenient in what you
+## accept".
+class PeerConnection
+ extend AttrReaderQ
+ include EventSource
+
+ attr_reader :peer_pieces, :name
+ attr_reader_q :running, :choking, :interested, :peer_choking,
+ :peer_interested, :snubbing
+ event :peer_has_piece, :peer_has_pieces, :received_block, :sent_block,
+ :requested_block
+
+ BUFSIZE = 8192
+ MAX_PEER_REQUESTS = 5 # how many peer requests to keep queued
+ MAX_REQUESTS = 5 # how many requests for blocks to keep current
+ MIN_REQUESTS = 1 # get more blocks from controller when this limit is reached
+ REQUEST_TIMEOUT = 60 # number of seconds after sending a request before we
+ # decide it's been forgotten
+
+ def initialize(name, controller, socket, package)
+ @name = name
+ @controller = controller
+ @socket = socket
+ @package = package
+ @running = false
+
+ ## my state
+ @want_blocks = [].extend(ArrayDelete2) # blocks i want
+ @want_blocks_m = Mutex.new
+ @choking = true
+ @interested = false
+ @snubbing = false
+
+ ## peer's state
+ @peer_want_blocks = [].extend(ArrayDelete2)
+ @peer_choking = true # assumption of initial condition
+ @peer_interested = false # ditto
+ @peer_pieces = Array.new(@package.num_pieces, false) # ditto
+ @peer_virgin = true # does the peer have any pieces at all?
+
+ ## connection stats
+ @dlmeter = RateMeter.new
+ @ulmeter = RateMeter.new
+
+ @send_q = Queue.new # output thread takes messages from here and
+ # puts them on the wire
+ end
+
+ def pending_recv; @want_blocks.find_all { |b| b.requested? }.length; end
+ def pending_send; @peer_want_blocks.length; end
+
+ def start
+ @running = true
+ @time = {:start => Time.now}
+
+ Thread.new do # start input thread
+ begin
+ while @running; input_thread_step; end
+ rescue SystemCallError, IOError, ProtocolError => e
+ rt_debug "#{self} (input): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying"
+# rt_debug e.backtrace.join("\n")
+ @running = false
+ @controller.forget_blocks @want_blocks
+ end
+ end
+
+ Thread.new do # start output thread
+ begin
+ while @running; output_thread_step; end
+ rescue SystemCallError, IOError, ProtocolError => e
+ rt_debug "#{self} (output): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying"
+# rt_debug e.backtrace.join("\n")
+ @running = false
+ @controller.forget_blocks @want_blocks
+ end
+ end
+
+ ## queue the initial messages
+ queue_message(:bitfield, {:bitfield => @package.pieces.map { |p| p.complete? }.extend(ArrayToBitstring).to_bitstring})
+
+ ## and that's it. if peer sends a bitfield, we'll send an
+ ## interested and start requesting blocks at that point. if they
+ ## don't, it means they don't have any pieces, so we can just sit
+ ## tight.
+ self
+ end
+
+ ## the Controller calls this from heartbeat thread to tell us
+ ## whether to choke or not.
+ def choke=(now_choke)
+ queue_message(now_choke ? :choke : :unchoke) unless @choking == now_choke
+ @choking = now_choke
+ end
+
+ ## the Controller calls this from heartbeat thread to tell us
+ ## whether to snub or not.
+ def snub=(now_snub)
+ unless @snubbing = now_snub
+ @snubbing = now_snub
+ choke = true if @snubbing
+ end
+ end
+
+ def peer_complete?; @peer_pieces.all?; end
+ def last_send_time; @time[:send]; end
+ def last_recv_time; @time[:recv]; end
+ def last_send_block_time; @time[:send_block]; end
+ def last_recv_block_time; @time[:recv_block]; end
+ def start_time; @time[:start]; end
+ def dlrate; @dlmeter.rate; end
+ def ulrate; @ulmeter.rate; end
+ def dlamt; @dlmeter.amt; end
+ def ulamt; @ulmeter.amt; end
+ def piece_available?(index); @peer_pieces[index]; end
+ def to_s; "<peer: #@name>"; end
+
+ ## called by Controller in the event that a request needs to be
+ ## rescinded.
+ def cancel(block)
+ wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block }
+ unless wblock.nil? || !wblock.requested?
+ rt_debug "#{self}: sending cancel for #{wblock}"
+ queue_message(:cancel, {:index => wblock.pindex, :begin => wblock.begin,
+ :length => wblock.length})
+ end
+ get_want_blocks unless wblock.nil?
+ end
+
+ def shutdown
+ rt_debug "#{self.to_s}: shutting down"
+ @running = false
+ @socket.close rescue nil
+ end
+
+ ## Controller calls this to tell us that a complete piece has been
+ ## received.
+ def have_piece(piece)
+ queue_message(:have, {:index => piece.index})
+ end
+
+ ## Controller calls this to tell us to send a keepalive
+ def send_keepalive
+# rt_debug "* sending keepalive!"
+ queue_message(:keepalive)
+ end
+
+ ## this is called both by input_thread_step and by the controller's
+ ## heartbeat thread. it sends as many pending blocks as it can while
+ ## keeping the amount below 'ullim', and sends as many requests as
+ ## it can while keeping the amount below 'dllim'.
+ ##
+ ## returns the number of bytes requested and sent
+ def send_blocks_and_reqs(dllim=nil, ullim=nil)
+ sent_bytes = 0
+ reqd_bytes = 0
+
+ @want_blocks_m.synchronize do
+ @want_blocks.each do |b|
+# puts "[][] #{self}: #{b} is #{b.requested? ? 'requested' : 'NOT requested'} and has time_elapsed of #{b.requested? ? b.time_elapsed.round : 'n/a'}s"
+ if b.requested? && (b.time_elapsed > REQUEST_TIMEOUT)
+ rt_warning "#{self}: for block #{b}, time elapsed since request is #{b.time_elapsed} > #{REQUEST_TIMEOUT}, assuming peer forgot about it"
+ @want_blocks.delete b
+ @controller.forget_blocks [b]
+ end
+ end
+ end
+
+ ## send :requests
+ unless @peer_choking || !@interested
+ @want_blocks_m.synchronize do
+ @want_blocks.each do |b|
+ break if dllim && (reqd_bytes >= dllim)
+ next if b.requested?
+
+ if @package.pieces[b.pindex].complete?
+ # not sure that this will ever happen, but...
+ rt_warning "#{self}: deleting scheduled block for already-complete piece #{b}"
+ @want_blocks.delete b
+ next
+ end
+
+ queue_message(:request, {:index => b.pindex, :begin => b.begin,
+ :length => b.length})
+ reqd_bytes += b.length
+ b.requested = true
+ b.mark_time
+ send_event(:requested_block, b)
+ end
+ end
+ end
+
+ ## send blocks
+# rt_debug "sending blocks. choking? #@choking, choked? #@peer_choking, ul rate #{ulrate}b/s, limit #@ulmeterlim" unless @peer_want_blocks.empty?
+ unless @choking || !@peer_interested
+ while !@peer_want_blocks.empty?
+ break if ullim && (sent_bytes >= ullim)
+ if (b = @peer_want_blocks.shift)
+ sent_bytes += b.length
+ @send_q.push b
+ @time[:send_block] = Time.now
+ send_event(:sent_block, b)
+ end
+ end
+ end
+
+ get_want_blocks
+
+ [reqd_bytes, sent_bytes]
+ end
+
+ private
+
+ ## re-calculate whether we're interested or not. triggered by
+ ## received :have and :bitfield messages.
+ def recalc_interested
+ show_interest = !@peer_virgin || (@package.pieces.detect do |p|
+ !p.complete? && @peer_pieces[p.index]
+ end) != nil
+
+ queue_message(show_interest ? :interested : :uninterested) unless show_interest == @interested
+ if ((@interested = show_interest) == false)
+ @want_blocks_m.synchronize do
+ @controller.forget_blocks @want_blocks
+ @want_blocks.clear
+ end
+ end
+ end
+
+ ## take a message/block from the send_q and place it on the wire. blocking.
+ def output_thread_step
+ obj = @send_q.deq
+ case obj
+ when Message
+# rt_debug "output: sending message #{obj}" + (obj.id == :request ? " (request queue size #{@want_blocks.length})" : "")
+ send_bytes obj.to_wire_form
+ @time[:send] = Time.now
+ when Block
+# rt_debug "output: sending block #{obj}"
+ send_bytes Message.new(:piece, {:length => obj.length, :index => obj.pindex, :begin => obj.begin}).to_wire_form
+ obj.each_chunk(BUFSIZE) { |c| send_bytes c }
+ @time[:send] = Time.now
+ @ulmeter.add obj.length
+# rt_debug "sent block #{obj} ul rate now #{(ulrate / 1024.0).round}kb/s"
+ else
+ raise "don't know what to do with #{obj}"
+ end
+ end
+
+ ## take bits from the wire and respond to them. blocking.
+ def input_thread_step
+ case (obj = read_from_wire)
+ when Block
+ handle_block obj
+ when Message
+ handle_message obj
+ else
+ raise "don't know what to do with #{obj.inspect}"
+ end
+
+ ## to enable immediate response, if there are no rate limits,
+ ## we'll send the blocks and reqs right here. otherwise, the
+ ## controller will call this at intervals.
+ send_blocks_and_reqs if @controller.dlratelim.nil? && @controller.ulratelim.nil?
+ end
+
+ ## take bits from the wire and make a message/block out of them. blocking.
+ def read_from_wire
+ len = nil
+ while (len = recv_bytes(4).from_fbbe) == 0
+ @time[:recv] = Time.now
+# rt_debug "* hey, a keepalive!"
+ end
+
+ id = recv_bytes(1)[0]
+
+ if Message::WIRE_IDS[id] == :piece # add a block
+ len -= 9
+ m = Message.from_wire_form(id, recv_bytes(8))
+ b = Block.new(m.index, m.begin, len)
+ while len > 0
+ thislen = [BUFSIZE, len].min
+ b.add_chunk recv_bytes(thislen)
+ len -= thislen
+ end
+ @time[:recv] = @time[:recv_block] = Time.now
+ b
+ else # add a message
+ m = Message.from_wire_form(id, recv_bytes(len - 1))
+# rt_debug "input: read message #{m}"
+ @time[:recv] = Time.now
+ m
+ end
+ end
+
+ def handle_block(block)
+ wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block }
+
+ return rt_warning("#{self}: peer sent unrequested (possibly cancelled) block #{block}") if wblock.nil? || !wblock.requested?
+
+ @dlmeter.add block.have_length
+# rt_debug "received block #{block}, dl rate now #{(dlrate / 1024.0).round}kb/s"
+
+ piece = @package.pieces[block.pindex] # find corresponding piece
+ piece.add_block block
+ send_event(:received_block, block)
+ get_want_blocks
+ end
+
+ def send_bytes(s)
+ if s.nil?
+ raise "can't send nil"
+ elsif s.length > 0
+ @socket.send(s, 0)
+ end
+ end
+
+ def recv_bytes(len)
+ if len < 0
+ raise "can't recv negative bytes"
+ elsif len == 0
+ ""
+ elsif len > 512 * 1024 # 512k
+ raise ProtocolError, "read size too big."
+ else
+ r = ""
+ zeros = 0
+ while r.length < len
+ x = @socket.recv(len - r.length)
+ raise IOError, "zero bytes received" if x.length == 0
+ r += x
+ end
+ r
+ end
+ end
+
+ def handle_message(m)
+ case m.id
+ when :choke
+# rt_debug "#{self}: peer choking (was #{@peer_choking})"
+ @peer_choking = true
+ @want_blocks_m.synchronize do
+ @controller.forget_blocks @want_blocks
+ @want_blocks.clear
+ end
+
+ when :unchoke
+# rt_debug "#{self}: peer not choking (was #{@peer_choking})"
+ @peer_choking = false
+
+ when :interested
+# rt_debug "peer interested (was #{@peer_interested})"
+ @peer_interested = true
+
+ when :uninterested
+# rt_debug "peer not interested (was #{@peer_interested})"
+ @peer_interested = false
+
+ when :have
+# rt_debug "peer has piece #{m.index}"
+ rt_warning "#{self}: peer already has piece #{m.index}" if @peer_pieces[m.index]
+ @peer_pieces[m.index] = true
+ @peer_virgin = false
+ send_event(:peer_has_piece, m)
+ recalc_interested
+
+ when :bitfield
+# rt_debug "peer reports bitfield #{m.bitfield.inspect}"
+ barray = m.bitfield.extend(StringToBarray).to_barray
+
+ expected_pieces = @package.num_pieces - (@package.num_pieces % 8) + ((@package.num_pieces % 8) == 0 ? 0 : 8)
+ raise ProtocolError, "invalid length in bitfield message (package has #{@package.num_pieces} pieces; bitfield should be size #{expected_pieces} but is #{barray.length} pieces)" unless barray.length == expected_pieces
+
+ @peer_pieces.each_index { |i| @peer_pieces[i] = barray[i] }
+ @peer_virgin = false
+ send_event(:peer_has_pieces, barray)
+ recalc_interested
+ get_want_blocks
+
+ when :request
+ return rt_warning("#{self}: peer requests invalid piece #{m.index}") unless m.index < @package.num_pieces
+ return rt_warning("#{self}: peer requests a block but we're choking") if @choking
+ return rt_warning("#{self}: peer requests a block but isn't interested") unless @peer_interested
+ return rt_warning("#{self}: peer requested too many blocks, ignoring") if @peer_want_blocks.length > MAX_PEER_REQUESTS
+
+ piece = @package.pieces[m.index]
+ return rt_warning("#{self}: peer requests unavailable block from piece #{piece}") unless piece.complete?
+
+ @peer_want_blocks.push piece.get_complete_block(m.begin, m.length)
+
+ when :piece
+ raise "can't handle piece here"
+
+ when :cancel
+ b = Block.new(m.index, m.begin, m.length)
+# rt_debug "peer cancels #{b}"
+ if @peer_want_blocks.delete2(b) == nil
+ rt_warning "#{self}: peer wants to cancel unrequested block #{b}"
+ end
+
+ else
+ raise "unknown message #{type}"
+ end
+ end
+
+ ## queues a message for delivery. (for :piece messages, this
+ ## transmits everything but the piece itself)
+ def queue_message(id, args=nil)
+ @send_q.push Message.new(id, args)
+ end
+
+ ## talks to Controller and get some new blocks to request. could be
+ ## slow. this is presumably called whenever the queue of requests is
+ ## too small.
+ def get_want_blocks
+ return if (@want_blocks.length >= MIN_REQUESTS) || @peer_virgin || @peer_choking || !@interested
+
+ rej_count = 0
+ acc_count = 0
+ @controller.claim_blocks do |b|
+ break if @want_blocks.length >= MAX_REQUESTS
+ if @peer_pieces[b.pindex] && !@want_blocks.member?(b)
+ rt_debug "! #{self}: starting new piece #{@package.pieces[b.pindex]}" unless @package.pieces[b.pindex].started?
+
+# rt_debug "#{self}: added to queue block #{b}"
+# puts "#{self}: claimed block #{b}"
+ @want_blocks.push b
+ acc_count += 1
+ true
+ else
+# puts "#{self}: cont offers block #{b} but peer has? #{@peer_pieces[b.pindex]} i already want? #{@want_blocks.member? b}" if rej_count < 10
+ rej_count += 1
+ false
+ end
+ end
+ # puts "#{self}: ... and #{rej_count} more (peer has #{@peer_pieces.inject(0) { |s, p| s + (p ? 1 : 0) }} pieces)... " if rej_count >= 10
+# puts "#{self}: accepted #{acc_count} blocks, rejected #{rej_count} blocks"
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/server.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,166 @@
+## server.rb -- make/receive and handshake all new peer connections.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'socket'
+require "rubytorrent/tracker"
+require "rubytorrent/controller"
+require "rubytorrent/peer"
+
+module RubyTorrent
+
+## The Server coordinates all Packages available on the machine. It
+## instantiates one Controller for each Package. It's also responsible
+## for the creation of all TCP connections---it sets up the TCP
+## socket, receives incoming connections, validates handshakes, and
+## hands them off to the appropriate Controller; it also creates
+## outgoing connections (typically at Controllers' requests) and sends
+## the handshake.
+class Server
+ attr_reader :port, :id, :http_proxy
+
+ VERSION = 0
+ PORT_RANGE=(6881 .. 6889)
+
+ def initialize(hostname=nil, port=nil, http_proxy=ENV["http_proxy"])
+ @http_proxy = http_proxy
+ @server = nil
+ if port.nil?
+ @port = PORT_RANGE.detect do |p|
+ begin
+ @server = TCPServer.new(hostname, p)
+ @port = p
+ rescue Errno::EADDRINUSE
+ @server = nil
+ end
+ !@server.nil?
+ end
+ raise Errno::EADDRINUSE, "ports #{PORT_RANGE}" unless @port
+ else
+ @server = TCPServer.new(hostname, port)
+ @port = port
+ end
+
+ @id = "rubytor" + VERSION.chr + (1 .. 12).map { |x| rand(256).chr }.join
+ @controllers = {}
+ end
+
+ def ip; @server.addr[3]; end
+
+ def add_torrent(mi, package, dlratelim=nil, ulratelim=nil)
+ @controllers[mi.info.sha1] = Controller.new(self, package, mi.info.sha1, mi.trackers, dlratelim, ulratelim, @http_proxy)
+ @controllers[mi.info.sha1].start
+ end
+
+ def add_connection(name, cont, socket)
+ begin
+ shake_hands(socket, cont.info_hash)
+ peer = PeerConnection.new(name, cont, socket, cont.package)
+ cont.add_peer peer
+ rescue ProtocolError => e
+ socket.close rescue nil
+ end
+ end
+
+ def start
+ @shutdown = false
+ @thread = Thread.new do
+ begin
+ while !@shutdown; receive; end
+ rescue IOError, StandardError
+ rt_warning "**** socket receive error, retrying"
+ sleep 5
+ retry
+ end
+ end
+ self
+ end
+
+ def shutdown
+ return if @shutdown
+ @shutdown = true
+ @server.close rescue nil
+
+ @thread.join(0.2)
+ @controllers.each { |hash, cont| cont.shutdown }
+ self
+ end
+
+ def to_s
+ "<#{self.class}: port #{port}, peer_id #{@id.inspect}>"
+ end
+
+ private
+
+ def receive # blocking
+ ssocket = @server.accept
+ Thread.new do
+ socket = ssocket
+
+ begin
+ rt_debug "<= incoming connection from #{socket.peeraddr[2]}:#{socket.peeraddr[1]}"
+ hash, peer_id = shake_hands(socket, nil)
+ cont = @controllers[hash]
+ peer = PeerConnection.new("#{socket.peeraddr[2]}:#{socket.peeraddr[1]}", cont, socket, cont.package)
+ cont.add_peer peer
+ rescue SystemCallError, ProtocolError => e
+ rt_debug "killing incoming connection: #{e}"
+ socket.close rescue nil
+ end
+ end
+ end
+
+ ## if info_hash is nil here, the socket is treated as an incoming
+ ## connection---it will wait for the peer's info_hash and respond
+ ## with the same if it corresponds to a current download, otherwise
+ ## it will raise a ProtocolError.
+ ##
+ ## if info_hash is not nil, the socket is treated as an outgoing
+ ## connection, and it will send the info_hash immediately.
+ def shake_hands(sock, info_hash)
+# rt_debug "initiating #{(info_hash.nil? ? 'incoming' : 'outgoing')} handshake..."
+ sock.send("\023BitTorrent protocol\0\0\0\0\0\0\0\0", 0);
+ sock.send("#{info_hash}#{@id}", 0) unless info_hash.nil?
+
+ len = sock.recv(1)[0]
+# rt_debug "length #{len.inspect}"
+ raise ProtocolError, "invalid handshake length byte #{len.inspect}" unless len == 19
+
+ name = sock.recv(19)
+# rt_debug "name #{name.inspect}"
+ raise ProtocolError, "invalid handshake protocol string #{name.inspect}" unless name == "BitTorrent protocol"
+
+ reserved = sock.recv(8)
+# rt_debug "reserved: #{reserved.inspect}"
+ # ignore for now
+
+ their_hash = sock.recv(20)
+# rt_debug "their info hash: #{their_hash.inspect}"
+
+ if info_hash.nil?
+ raise ProtocolError, "client requests package we don't have: hash=#{their_hash.inspect}" unless @controllers.has_key? their_hash
+ info_hash = their_hash
+ sock.send("#{info_hash}#{@id}", 0)
+ else
+ raise ProtocolError, "mismatched info hashes: us=#{info_hash.inspect}, them=#{their_hash.inspect}" unless info_hash == their_hash
+ end
+
+ peerid = sock.recv(20)
+# rt_debug "peer id: #{peerid.inspect}"
+ raise ProtocolError, "connected to self" if peerid == @id
+
+# rt_debug "== handshake complete =="
+ [info_hash, peerid]
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/tracker.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,225 @@
+## tracker.rb -- bittorrent tracker protocol.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'open-uri'
+require 'timeout'
+require "rubytorrent"
+
+module RubyTorrent
+
+module HashAddition
+ def +(o)
+ ret = self.dup
+ o.each { |k, v| ret[k] = v }
+ ret
+ end
+end
+
+## am i insane or does 'uniq' not use == or === for some insane
+## reason? wtf is that about?
+module ArrayUniq2
+ def uniq2
+ ret = []
+ each { |x| ret.push x unless ret.member? x }
+ ret
+ end
+end
+
+class TrackerResponsePeer
+ attr_writer :tried
+
+ def initialize(dict=nil)
+ @s = TypedStruct.new do |s|
+ s.field :peer_id => String, :ip => String, :port => Integer
+ s.required :ip, :port
+ s.label :peer_id => "peer id"
+ end
+
+ @s.parse(dict) unless dict.nil?
+ @connected = false
+ @tried = false
+ end
+
+ def tried?; @tried; end
+
+ def method_missing(meth, *args)
+ @s.send(meth, *args)
+ end
+
+ def ==(o); (self.ip == o.ip) && (self.port == o.port); end
+
+ def to_s
+ %{<#{self.class}: ip=#{self.ip}, port=#{self.port}>}
+ end
+end
+
+class TrackerResponse
+ def initialize(dict=nil)
+ @s = TypedStruct.new do |s|
+ s.field :interval => Integer, :complete => Integer,
+ :incomplete => Integer, :peers => TrackerResponsePeer
+ s.array :peers
+ s.required :peers #:interval, :complete, :incomplete, :peers
+ s.coerce :peers => lambda { |x| make_peers x }
+ end
+
+ @s.parse(dict) unless dict.nil?
+
+ peers.extend ArrayShuffle
+ end
+
+ def method_missing(meth, *args)
+ @s.send(meth, *args)
+ end
+
+ private
+
+ def make_peers(x)
+ case x
+ when Array
+ x.map { |e| TrackerResponsePeer.new e }.extend(ArrayUniq2).uniq2
+ when String
+ x.unpack("a6" * (x.length / 6)).map do |y|
+ TrackerResponsePeer.new({"ip" => (0..3).map { |i| y[i] }.join('.'),
+ "port" => (y[4] << 8) + y[5] })
+ end.extend(ArrayUniq2).uniq2
+ else
+ raise "don't know how to make peers array from #{x.class}"
+ end
+ end
+end
+
+class TrackerError < StandardError; end
+
+class TrackerConnection
+ attr_reader :port, :left, :peer_id, :last_conn_time, :url, :in_force_refresh
+ attr_accessor :uploaded, :downloaded, :left, :numwant
+
+ def initialize(url, info_hash, length, port, peer_id, ip=nil, numwant=50, http_proxy=ENV["http_proxy"])
+ @url = url
+ @hash = info_hash
+ @length = length
+ @port = port
+ @uploaded = @downloaded = @left = 0
+ @ip = ip
+ @numwant = numwant
+ @peer_id = peer_id
+ @http_proxy = http_proxy
+ @state = :stopped
+ @sent_completed = false
+ @last_conn_time = nil
+ @tracker_data = nil
+ @compact = true
+ @in_force_refresh = false
+ end
+
+ def already_completed; @sent_completed = true; end
+ def sent_completed?; @sent_completed; end
+
+ def started
+ return unless @state == :stopped
+ @state = :started
+ @tracker_data = send_tracker "started"
+ self
+ end
+
+ def stopped
+ return unless @state == :started
+ @state = :stopped
+ @tracker_data = send_tracker "stopped"
+ self
+ end
+
+ def completed
+ return if @sent_completed
+ @tracker_data = send_tracker "completed"
+ @sent_completed = true
+ self
+ end
+
+ def refresh
+ return unless (Time.now - @last_conn_time) >= (interval || 0)
+ @tracker_data = send_tracker nil
+ end
+
+ def force_refresh
+ return if @in_force_refresh
+ @in_force_refresh = true
+ @tracker_data = send_tracker nil
+ @in_force_refresh = false
+ end
+
+ [:interval, :seeders, :leechers, :peers].each do |m|
+ class_eval %{
+ def #{m}
+ if @tracker_data then @tracker_data.#{m} else nil end
+ end
+ }
+ end
+
+ private
+
+ def send_tracker(event)
+ resp = nil
+ if @compact
+ resp = get_tracker_response({ :event => event, :compact => 1 })
+ if resp["failure reason"]
+ @compact = false
+ end
+ end
+
+ resp = get_tracker_response({ :event => event }) unless resp
+ raise TrackerError, "tracker reports error: #{resp['failure reason']}" if resp["failure reason"]
+
+ TrackerResponse.new(resp)
+ end
+
+ def get_tracker_response(opts)
+ target = @url.dup
+ opts.extend HashAddition
+ opts += {:info_hash => @hash, :peer_id => @peer_id,
+ :port => @port, :uploaded => @uploaded, :downloaded => @downloaded,
+ :left => @left, :numwant => @numwant, :ip => @ip}
+ target.query = opts.map do |k, v|
+ unless v.nil?
+ ek = URI.escape(k.to_s) # sigh
+ ev = URI.escape(v.to_s, /[^a-zA-Z0-9]/)
+ "#{ek}=#{ev}"
+ end
+ end.compact.join "&"
+
+ rt_debug "connecting to #{target.to_s} ..."
+
+ ret = nil
+ begin
+ target.open(:proxy => @http_proxy) do |resp|
+ BStream.new(resp).each do |e|
+ if ret.nil?
+ ret = e
+ else
+ raise TrackerError, "don't understand tracker response (too many objects)"
+ end
+ end
+ end
+ rescue SocketError, EOFError, OpenURI::HTTPError, RubyTorrent::TrackerError, Timeout::Error, SystemCallError, NoMethodError => e
+ raise TrackerError, e.message
+ end
+ @last_conn_time = Time.now
+
+ raise TrackerError, "empty tracker response" if ret.nil?
+ raise TrackerError, "don't understand tracker response (not a dict)" unless ret.kind_of? ::Hash
+ ret
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/typedstruct.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,132 @@
+## typedstruct.rb -- type-checking struct, for bencoded objects.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require "rubytorrent/bencoding"
+
+module RubyTorrent
+
+module ArrayToH
+ def to_h
+ inject({}) { |h, (k, v)| h[k] = v; h } # found this neat trick on the internet
+ end
+end
+
+module HashMapHash
+ def map_hash
+ a = map { |k, v| yield k, v }.extend(ArrayToH).to_h
+ end
+end
+
+class TypedStructError < StandardError; end
+
+## type-checking struct meant for easy translation from and to
+## bencoded dicts.
+class TypedStruct
+ attr_accessor :dirty
+ attr_reader :fields # writer below
+
+ def initialize
+ @required = {}
+ @label = {}
+ @coerce = {}
+ @field = {}
+ @array = {}
+ @dirty = false
+ @values = {}
+
+ yield self if block_given?
+
+ @field.each do |f, type|
+ @required[f] ||= false
+ @label[f] ||= f.to_s
+ @array[f] ||= false
+ end
+ end
+
+ def method_missing(meth, *args)
+ if meth.to_s =~ /^(.*?)=$/
+# p [meth, args]
+
+ f = $1.intern
+ raise ArgumentError, "no such value #{f}" unless @field.has_key? f
+
+ type = @field[f]
+ o = args[0]
+ if @array[f]
+ raise TypeError, "for #{f}, expecting Array, got #{o.class}" unless o.kind_of? ::Array
+ o.each { |e| raise TypeError, "for elements of #{f}, expecting #{type}, got #{e.class}" unless e.kind_of? type }
+ @values[f] = o
+ @dirty = true
+ else
+ raise TypeError, "for #{f}, expecting #{type}, got #{o.class}" unless o.kind_of? type
+ @values[f] = o
+ @dirty = true
+ end
+ else
+ raise ArgumentError, "no such value #{meth}" unless @field.has_key? meth
+# p [meth, @values[meth]]
+
+ @values[meth]
+ end
+ end
+
+ [:required, :array].each do |f|
+ class_eval %{
+ def #{f}(*args)
+ args.each do |x|
+ raise %q{unknown field "\#{x}" in #{f} list} unless @field[x]
+ @#{f}[x] = true
+ end
+ end
+ }
+ end
+
+ [:field , :label, :coerce].each do |f|
+ class_eval %{
+ def #{f}(hash)
+ hash.each { |k, v| @#{f}[k] = v }
+ end
+ }
+ end
+
+ ## given a Hash from a bencoded dict, parses it according to the
+ ## rules you've set up with field, required, label, etc.
+ def parse(dict)
+ @required.each do |f, reqd|
+ flabel = @label[f]
+ raise TypedStructError, "missing required parameter #{flabel} (dict has #{dict.keys.join(', ')})" if reqd && !(dict.member? flabel)
+
+ if dict.member? flabel
+ v = dict[flabel]
+ if @coerce.member? f
+ v = @coerce[f][v]
+ end
+ if @array[f]
+ raise TypeError, "for #{flabel}, expecting Array, got #{v.class} instead" unless v.kind_of? ::Array
+ end
+ self.send("#{f}=", v)
+ end
+ end
+
+ ## disabled the following line as applications seem to put tons of
+ ## weird fields in their .torrent files.
+ # dict.each { |k, v| raise TypedStructError, %{unknown field "#{k}"} unless @field.member?(k.to_sym) || @label.values.member?(k) }
+ end
+
+ def to_bencoding
+ @required.each { |f, reqd| raise ArgumentError, "missing required parameter #{f}" if reqd && self.send(f).nil? }
+ @field.extend(HashMapHash).map_hash { |f, type| [@label[f], self.send(f)] }.to_bencoding
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent/util.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,186 @@
+## util.rb -- miscellaneous RubyTorrent utility modules.
+## Copyright 2005 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+def rt_debug(*args)
+ if $DEBUG || RubyTorrent.log
+ stream = RubyTorrent.log || $stdout
+ stream << args.join << "\n"
+ stream.flush
+ end
+end
+
+def rt_warning(*args)
+ if $DEBUG || RubyTorrent.log
+ stream = RubyTorrent.log || $stderr
+ stream << "warning: " << args.join << "\n"
+ stream.flush
+ end
+end
+
+module RubyTorrent
+
+ at log = nil
+def log_output_to(fn)
+ @log = File.open(fn, "w")
+end
+attr_reader :log
+module_function :log_output_to, :log
+
+
+## parse final hash of pseudo-keyword arguments
+def get_args(rest, *names)
+ hash = rest.find { |x| x.is_a? Hash }
+ if hash
+ rest.delete hash
+ hash.each { |k, v| raise ArgumentError, %{unknown argument "#{k}"} unless names.include?(k) }
+ end
+
+ [hash || {}, rest]
+end
+module_function :get_args
+
+## "events": very similar to Observable, but cleaner, IMO. events are
+## listened to and sent in instance space, but registered in class
+## space. example:
+##
+## class C
+## include EventSource
+## event :goat, :boat
+##
+## def send_events
+## send_event :goat
+## send_event(:boat, 3)
+## end
+## end
+##
+## c = C.new
+## c.on_event(:goat) { puts "got goat!" }
+## c.on_event(:boat) { |x| puts "got boat: #{x}" }
+##
+## Defining them in class space is not really necessary, except as an
+## error-checking mechanism.
+module EventSource
+ def on_event(who, *events, &b)
+ @event_handlers ||= Hash.new { [] }
+ events.each do |e|
+ raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+ @event_handlers[e] <<= [who, b]
+ end
+ nil
+ end
+
+ def send_event(e, *args)
+ raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+ @event_handlers ||= Hash.new { [] }
+ @event_handlers[e].each { |who, proc| proc[self, *args] }
+ nil
+ end
+
+ def unregister_events(who, *events)
+ @event_handlers.each do |event, handlers|
+ handlers.each do |ewho, proc|
+ if (ewho == who) && (events.empty? || events.member?(event))
+ @event_handlers[event].delete [who, proc]
+ end
+ end
+ end
+ nil
+ end
+
+ def relay_event(who, *events)
+ @event_handlers ||= Hash.new { [] }
+ events.each do |e|
+ raise "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
+ raise "unknown event #{e} for #{who.class}" unless (who.class.class_eval "@@event_has")[e]
+ @event_handlers[e] <<= [who, lambda { |s, *a| who.send_event e, *a }]
+ end
+ nil
+ end
+
+ def self.append_features(mod)
+ super(mod)
+ mod.class_eval %q{
+ @@event_has ||= Hash.new(false)
+ def self.event(*args)
+ args.each { |a| @@event_has[a] = true }
+ end
+ }
+ end
+end
+
+## ensure that a method doesn't execute more frequently than some
+## number of seconds. e.g.:
+##
+## def meth
+## ...
+## end
+## min_iterval :meth, 10
+##
+## ensures that "meth" won't be executed more than once every 10
+## seconds.
+module MinIntervalMethods
+ def min_interval(meth, int)
+ class_eval %{
+ @@min_interval ||= {}
+ @@min_interval[:#{meth}] = [nil, #{int.to_i}]
+ alias :min_interval_#{meth} :#{meth}
+ def #{meth}(*a, &b)
+ last, int = @@min_interval[:#{meth}]
+ unless last && ((Time.now - last) < int)
+ min_interval_#{meth}(*a, &b)
+ @@min_interval[:#{meth}][0] = Time.now
+ end
+ end
+ }
+ end
+end
+
+## boolean attributes now get question marks in their accessors
+## don't forget to 'extend' rather than 'include' this one
+module AttrReaderQ
+ def attr_reader_q(*args)
+ args.each { |v| class_eval "def #{v}?; @#{v}; end" }
+ end
+
+ def attr_writer_q(*args)
+ args.each { |v| attr_writer v }
+ end
+
+ def attr_accessor_q(*args)
+ attr_reader_q args
+ attr_writer_q args
+ end
+end
+
+module ArrayShuffle
+ def shuffle!
+ each_index do |i|
+ j = i + rand(self.size - i);
+ self[i], self[j] = self[j], self[i]
+ end
+ end
+
+ def shuffle
+ self.clone.shuffle! # dup doesn't preserve shuffle! method
+ end
+end
+
+module StringMapBytes
+ def map_bytes
+ ret = []
+ each_byte { |x| ret.push(yield(x)) }
+ ret
+ end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb
===================================================================
--- tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb (rev 0)
+++ tools/ruby-support/examples/libtorrent-ruby-0.3/rubytorrent.rb 2009-04-04 15:26:31 UTC (rev 3365)
@@ -0,0 +1,94 @@
+## rubytorrent.rb -- top-level RubyTorrent file.
+## Copyright 2004 William Morgan.
+##
+## This file is part of RubyTorrent. RubyTorrent is free software;
+## you can redistribute it and/or modify it under the terms of version
+## 2 of the GNU General Public License as published by the Free
+## Software Foundation.
+##
+## RubyTorrent is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## General Public License (in the file COPYING) for more details.
+
+require 'rubytorrent/util'
+require 'rubytorrent/bencoding'
+require 'rubytorrent/metainfo'
+require 'rubytorrent/tracker'
+require 'rubytorrent/package'
+require 'rubytorrent/server'
+
+require "socket"
+Socket.do_not_reverse_lookup = true
+
+module RubyTorrent
+ VERSION = 0.3
+
+## the top-level class for RubyTorrent.
+class BitTorrent
+ include EventSource
+ event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
+ :received_block, :sent_block, :have_piece, :discarded_piece, :complete,
+ :tracker_connected, :tracker_lost, :requested_block
+
+ @@server = nil
+
+ ## hash arguments: host, port, dlratelim, ulratelim
+ def initialize(metainfo, *rest)
+ args, rest = RubyTorrent::get_args(rest, :host, :port, :dlratelim, :ulratelim, :http_proxy)
+ out = rest.shift
+ raise ArgumentError, "wrong number of arguments (expected 0/1, got #{rest.length})" unless rest.empty?
+
+ case metainfo
+ when MetaInfo
+ @metainfo = metainfo
+ when String
+ @metainfo = MetaInfo.from_location(metainfo)
+ when IO
+ @metainfo = MetaInfo.from_stream(metainfo)
+ else
+ raise ArgumentError, "'metainfo' should be a String, IO or RubyTorrent::MetaInfo object"
+ end
+
+ case out
+ when Package
+ @package = out
+ else
+ @package = Package.new(@metainfo, out)
+ end
+
+ unless @@server
+ @@server = RubyTorrent::Server.new(args[:host], args[:port], args[:http_proxy])
+ @@server.start
+ end
+
+ @cont = @@server.add_torrent(@metainfo, @package, args[:dlratelim], args[:ulratelim])
+
+ @cont.relay_event self, :trying_peer, :forgetting_peer, :added_peer,
+ :removed_peer, :received_block, :sent_block,
+ :have_piece, :discarded_piece, :tracker_connected,
+ :tracker_lost, :requested_block
+ @package.relay_event self, :complete
+ end
+
+ def ip; @@server.ip; end
+ def port; @@server.port; end
+ def peer_info; @cont.peer_info; end
+ def shutdown; @cont.shutdown; end
+ def shutdown_all; @@server.shutdown; end
+ def complete?; @package.complete?; end
+ def bytes_completed; @package.bytes_completed; end
+ def percent_completed; @package.percent_completed; end
+ def pieces_completed; @package.pieces_completed; end
+ def dlrate; @cont.dlrate; end
+ def ulrate; @cont.ulrate; end
+ def dlamt; @cont.dlamt; end
+ def ulamt; @cont.ulamt; end
+ def num_pieces; @package.num_pieces; end
+ def tracker; (@cont.tracker ? @cont.tracker.url : nil); end
+ def num_possible_peers; (@cont.tracker ? @cont.tracker.peers.length : 0); end
+ def num_active_peers; @cont.num_peers; end
+ def total_bytes; @package.size; end
+end
+
+end
Added: tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
===================================================================
(Binary files differ)
Property changes on: tools/ruby-support/examples/libtorrent-ruby_0.3.orig.tar.gz
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
More information about the Pkg-ruby-extras-commits
mailing list