Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial

  • Loading branch information...
commit c3e9f5ba6fc10397f55941f36da29808a105d248 0 parents
Eric Wong authored
7 .document
@@ -0,0 +1,7 @@
+README
+LICENSE
+NEWS
+ChangeLog
+lib
+ext/raindrops/raindrops_ext.c
+ext/raindrops/linux_inet_diag.c
14 .gitignore
@@ -0,0 +1,14 @@
+*.o
+*.so
+*.log
+*.rbc
+Makefile
+/GIT-VERSION-FILE
+/local.mk
+/NEWS
+/ChangeLog
+/.manifest
+/GIT-VERSION-FILE
+/man
+/pkg
+/doc
165 COPYING
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser 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
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
40 GIT-VERSION-GEN
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+GVF=GIT-VERSION-FILE
+DEF_VER=v0.1.0.GIT
+
+LF='
+'
+
+# First see if there is a version file (included in release tarballs),
+# then try git-describe, then default.
+if test -f version
+then
+ VN=$(cat version) || VN="$DEF_VER"
+elif test -d .git -o -f .git &&
+ VN=$(git describe --abbrev=4 HEAD 2>/dev/null) &&
+ case "$VN" in
+ *$LF*) (exit 1) ;;
+ v[0-9]*)
+ git update-index -q --refresh
+ test -z "$(git diff-index --name-only HEAD --)" ||
+ VN="$VN-dirty" ;;
+ esac
+then
+ VN=$(echo "$VN" | sed -e 's/-/./g');
+else
+ VN="$DEF_VER"
+fi
+
+VN=$(expr "$VN" : v*'\(.*\)')
+
+if test -r $GVF
+then
+ VC=$(sed -e 's/^GIT_VERSION = //' <$GVF)
+else
+ VC=unset
+fi
+test "$VN" = "$VC" || {
+ echo >&2 "GIT_VERSION = $VN"
+ echo "GIT_VERSION = $VN" >$GVF
+}
172 GNUmakefile
@@ -0,0 +1,172 @@
+# use GNU Make to run tests in parallel, and without depending on RubyGems
+all::
+RUBY = ruby
+RAKE = rake
+GIT_URL = git://git.bogomips.org/raindrops.git
+
+GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
+ @./GIT-VERSION-GEN
+-include GIT-VERSION-FILE
+-include local.mk
+ifeq ($(DLEXT),) # "so" for Linux
+ DLEXT := $(shell $(RUBY) -rrbconfig -e 'puts Config::CONFIG["DLEXT"]')
+endif
+ifeq ($(RUBY_VERSION),)
+ RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
+endif
+
+install: $(bins)
+ $(prep_setup_rb)
+ $(RM) -r .install-tmp
+ mkdir .install-tmp
+ cp -p bin/* .install-tmp
+ $(RUBY) setup.rb all
+ $(RM) $^
+ mv .install-tmp/* bin/
+ $(RM) -r .install-tmp
+ $(prep_setup_rb)
+
+setup_rb_files := .config InstalledFiles
+prep_setup_rb := @-$(RM) $(setup_rb_files);$(MAKE) -C $(ext) clean
+
+clean:
+ -$(MAKE) -C ext/raindrops clean
+ $(RM) $(setup_rb_files) ext/raindrops/Makefile
+
+pkg_extra := GIT-VERSION-FILE NEWS ChangeLog
+manifest: $(pkg_extra)
+ $(RM) .manifest
+ $(MAKE) .manifest
+
+.manifest:
+ (git ls-files && \
+ for i in $@ $(pkg_extra) $(man1_paths); \
+ do echo $$i; done) | LC_ALL=C sort > $@+
+ cmp $@+ $@ || mv $@+ $@
+ $(RM) $@+
+
+NEWS: GIT-VERSION-FILE
+ $(RAKE) -s news_rdoc > $@+
+ mv $@+ $@
+
+SINCE =
+ChangeLog: LOG_VERSION = \
+ $(shell git rev-parse -q "$(GIT_VERSION)" >/dev/null 2>&1 && \
+ echo $(GIT_VERSION) || git describe)
+ifneq ($(SINCE),)
+ChangeLog: log_range = v$(SINCE)..$(LOG_VERSION)
+endif
+ChangeLog: GIT-VERSION-FILE
+ @echo "ChangeLog from $(GIT_URL) ($(log_range))" > $@+
+ @echo >> $@+
+ git log $(log_range) | sed -e 's/^/ /' >> $@+
+ mv $@+ $@
+
+news_atom := http://raindrops.bogomips.org/NEWS.atom.xml
+cgit_atom := http://git.bogomips.org/cgit/raindrops.git/atom/?h=master
+atom = <link rel="alternate" title="Atom feed" href="$(1)" \
+ type="application/atom+xml"/>
+
+# using rdoc 2.4.1+
+doc: .document NEWS ChangeLog
+ for i in $(man1_bins); do > $$i; done
+ rdoc -Na -t "$(shell sed -ne '1s/^= //p' README)"
+ install -m644 COPYING doc/COPYING
+ install -m644 $(shell grep '^[A-Z]' .document) doc/
+ cd doc && for i in $(base_bins); do \
+ html=$$(echo $$i | sed 's/\.rb/_rb/')_1.html; \
+ sed -e '/"documentation">/r man1/'$$i'.1.html' \
+ < $$html > tmp && mv tmp $$html; done
+ $(RUBY) -i -p -e \
+ '$$_.gsub!("</title>",%q{\&$(call atom,$(cgit_atom))})' \
+ doc/ChangeLog.html
+ $(RUBY) -i -p -e \
+ '$$_.gsub!("</title>",%q{\&$(call atom,$(news_atom))})' \
+ doc/NEWS.html doc/README.html
+ $(RAKE) -s news_atom > doc/NEWS.atom.xml
+ cd doc && ln README.html tmp && mv tmp index.html
+ $(RM) $(man1_bins)
+
+ifneq ($(VERSION),)
+rfproject := rainbows
+rfpackage := raindrops
+pkggem := pkg/$(rfpackage)-$(VERSION).gem
+pkgtgz := pkg/$(rfpackage)-$(VERSION).tgz
+release_notes := release_notes-$(VERSION)
+release_changes := release_changes-$(VERSION)
+
+release-notes: $(release_notes)
+release-changes: $(release_changes)
+$(release_changes):
+ $(RAKE) -s release_changes > $@+
+ $(VISUAL) $@+ && test -s $@+ && mv $@+ $@
+$(release_notes):
+ GIT_URL=$(GIT_URL) $(RAKE) -s release_notes > $@+
+ $(VISUAL) $@+ && test -s $@+ && mv $@+ $@
+
+# ensures we're actually on the tagged $(VERSION), only used for release
+verify:
+ test x"$(shell umask)" = x0022
+ git rev-parse --verify refs/tags/v$(VERSION)^{}
+ git diff-index --quiet HEAD^0
+ test `git rev-parse --verify HEAD^0` = \
+ `git rev-parse --verify refs/tags/v$(VERSION)^{}`
+
+fix-perms:
+ -git ls-tree -r HEAD | awk '/^100644 / {print $$NF}' | xargs chmod 644
+ -git ls-tree -r HEAD | awk '/^100755 / {print $$NF}' | xargs chmod 755
+
+gem: $(pkggem)
+
+install-gem: $(pkggem)
+ gem install $(CURDIR)/$<
+
+$(pkggem): manifest fix-perms
+ gem build $(rfpackage).gemspec
+ mkdir -p pkg
+ mv $(@F) $@
+
+$(pkgtgz): distdir = $(basename $@)
+$(pkgtgz): HEAD = v$(VERSION)
+$(pkgtgz): manifest fix-perms
+ @test -n "$(distdir)"
+ $(RM) -r $(distdir)
+ mkdir -p $(distdir)
+ tar c `cat .manifest` | (cd $(distdir) && tar x)
+ cd pkg && tar c $(basename $(@F)) | gzip -9 > $(@F)+
+ mv $@+ $@
+
+package: $(pkgtgz) $(pkggem)
+
+test-release: verify package $(release_notes) $(release_changes)
+release: verify package $(release_notes) $(release_changes)
+ # make tgz release on RubyForge
+ rubyforge add_release -f -n $(release_notes) -a $(release_changes) \
+ $(rfproject) $(rfpackage) $(VERSION) $(pkgtgz)
+ # push gem to Gemcutter
+ gem push $(pkggem)
+ # in case of gem downloads from RubyForge releases page
+ -rubyforge add_file \
+ $(rfproject) $(rfpackage) $(VERSION) $(pkggem)
+else
+gem install-gem: GIT-VERSION-FILE
+ $(MAKE) $@ VERSION=$(GIT_VERSION)
+endif
+
+ext := ext/raindrops/raindrops_ext.$(DLEXT)
+ext/raindrops/Makefile: ext/raindrops/extconf.rb
+ cd $(@D) && $(RUBY) extconf.rb
+$(ext): $(wildcard $(addprefix ext/raindrops/,*.c *.h)) ext/raindrops/Makefile
+ $(MAKE) -C $(@D)
+
+all:: test
+
+export STRESS BENCHMARK
+build: $(ext)
+test_units := $(wildcard test/test_*.rb)
+test: test-unit
+test-unit: $(test_units)
+$(test_units): build
+ $(RUBY) -I lib:ext/raindrops $@
+
+.PHONY: .FORCE-GIT-VERSION-FILE doc manifest man test $(test_units)
16 LICENSE
@@ -0,0 +1,16 @@
+raindrops is copyrighted Free Software by all contributors, see logs in
+revision control for names and email addresses of all of them.
+
+You can redistribute it and/or modify it under either the terms of the GNU
+Lesser General Public License as published by the Free Software Foundation,
+version 3.0 {LGPLv3}[http://www.gnu.org/licenses/lgpl-3.0.txt] (see
+link:COPYING).
+
+posix_mq 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 Lesser General Public
+License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with the GNU C Library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
117 README
@@ -0,0 +1,117 @@
+= raindrops - real-time stats for preforking Rack servers
+
+Raindrops is a real time stats package to show statistics for Rack HTTP
+servers. It is designed for preforking servers such as Rainbows! and
+Unicorn, but should support any Rack HTTP server under Ruby 1.9, 1.8 and
+possibly Rubinius (untested) on platforms supporting POSIX shared memory
+and compiled with GCC (for atomic builtins).
+
+Raindrops includes a Struct-like Raindrops::Struct class that may be used
+standalone to create atomic counters shared across any number of forked
+processes under SMP.
+
+== Features
+
+* counters are shared across all forked children and lock-free
+
+* counters are kept on separate cache lines to reduce contention under SMP
+
+* may expose server statistics as a Rack Middleware endpoint
+ (default: "/_raindrops")
+
+* middleware displays the number of actively processing and writing
+ clients from a single request regardless of which worker process
+ it hits.
+
+== Linux-only Extra Features!
+
+* Middleware response includes extra stats for bound TCP and
+ Unix domain sockets (configurable, it can include stats from
+ other TCP or UNIX domain socket servers).
+
+* TCP socket stats use efficient inet_diag facilities via netlink
+ instead of parsing /proc/net/tcp to minimize overhead.
+ This was fun to discover and write.
+
+== Install
+
+raindrops requires GCC 4.x (or compatible) or later to support the
+atomic builtins (__sync_{add,sub}_and_fetch()). Atomic operations on
+other compilers may be supported if there is demand.
+
+If you're using a packaged Ruby distribution, make sure you have a C
+compiler and the matching Ruby development libraries and headers.
+
+If you use RubyGems:
+
+ gem install raindrops
+
+Otherwise grab the latest tarball from:
+
+http://raindrops.bogomips.org/files/
+
+Unpack it, and run "ruby setup.rb"
+
+== Usage (Rainbows!/Unicorn preload_app=false)
+
+If you're using preload_app=false (the default) in your Rainbows!/Unicorn
+config file, you'll need to create the global Stats object before
+forking.
+
+ require 'raindrops'
+ $stats ||= Raindrops::Middleware::Stats.new
+
+In your Rack config.ru:
+
+ use Raindrops::Middleware, :stats => $stats
+
+== Usage (Rainbows!/Unicorn preload_app=true)
+
+If you're using preload_app=true in your Rainbows!/Unicorn
+config file, just add the middleware to your stack:
+
+In your Rack config.ru:
+
+ use Raindrops::Middleware
+
+== Usage (Linux-extras)
+
+To get bound listener statistics under Linux, you need to specify the
+listener names for your server. You can even include listen sockets for
+*other* servers on the same machine. This can be handy for monitoring
+your nginx proxy as well.
+
+In your Rack config.ru, just pass the :listeners argument as an array of
+strings (along with any other arguments). You can specify any
+combination of TCP or Unix domain socket names:
+
+ use Raindrops::Middleware, :listeners => %w(0.0.0.0:80 /tmp/.sock)
+
+See the tests/ and examples/ directory for more examples
+
+== Development
+
+You can get the latest source via git from the following locations:
+
+ git://git.bogomips.org/raindrops.git
+ git://repo.or.cz/raindrops.git (mirror)
+
+You may browse the code from the web and download the latest snapshot
+tarballs here:
+
+* http://git.bogomips.org/cgit/raindrops.git (cgit)
+* http://repo.or.cz/w/raindrops.git (gitweb)
+
+Inline patches (from "git format-patch") to the mailing list are
+preferred because they allow code review and comments in the reply to
+the patch.
+
+We will adhere to mostly the same conventions for patch submissions as
+git itself. See the Documentation/SubmittingPatches document
+distributed with git on on patch submission guidelines to follow. Just
+don't email the git mailing list or maintainer with raindrops patches.
+
+== Contact
+
+All feedback (bug reports, user/development discussion, patches, pull
+requests) go to the mailing list: mailto:raindrops@librelist.com
156 Rakefile
@@ -0,0 +1,156 @@
+# -*- encoding: binary -*-
+
+# most tasks are in the GNUmakefile which offers better parallelism
+
+def tags
+ timefmt = '%Y-%m-%dT%H:%M:%SZ'
+ @tags ||= `git tag -l`.split(/\n/).map do |tag|
+ if %r{\Av[\d\.]+\z} =~ tag
+ header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3)
+ header = header.split(/\n/)
+ tagger = header.grep(/\Atagger /).first
+ body ||= "initial"
+ {
+ :time => Time.at(tagger.split(/ /)[-2].to_i).utc.strftime(timefmt),
+ :tagger_name => %r{^tagger ([^<]+)}.match(tagger)[1].strip,
+ :tagger_email => %r{<([^>]+)>}.match(tagger)[1].strip,
+ :id => `git rev-parse refs/tags/#{tag}`.chomp!,
+ :tag => tag,
+ :subject => subject,
+ :body => body,
+ }
+ end
+ end.compact.sort { |a,b| b[:time] <=> a[:time] }
+end
+
+cgit_url = "http://git.bogomips.org/cgit/raindrops.git"
+git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/raindrops.git'
+web_url = "http://rainbows.bogomips.org/raindrops/"
+
+desc 'prints news as an Atom feed'
+task :news_atom do
+ require 'nokogiri'
+ new_tags = tags[0,10]
+ puts(Nokogiri::XML::Builder.new do
+ feed :xmlns => "http://www.w3.org/2005/Atom" do
+ id! "#{web_url}NEWS.atom.xml"
+ title "Raindrops news"
+ subtitle "real-time stats for Rack servers"
+ link! :rel => "alternate", :type => "text/html",
+ :href => "#{web_url}NEWS.html"
+ updated(new_tags.empty? ? "1970-01-01T00:00:00Z" : new_tags.first[:time])
+ new_tags.each do |tag|
+ entry do
+ title tag[:subject]
+ updated tag[:time]
+ published tag[:time]
+ author {
+ name tag[:tagger_name]
+ email tag[:tagger_email]
+ }
+ url = "#{cgit_url}/tag/?id=#{tag[:tag]}"
+ link! :rel => "alternate", :type => "text/html", :href =>url
+ id! url
+ message_only = tag[:body].split(/\n.+\(\d+\):\n {6}/s).first.strip
+ content({:type =>:text}, message_only)
+ content(:type =>:xhtml) { pre tag[:body] }
+ end
+ end
+ end
+ end.to_xml)
+end
+
+desc 'prints RDoc-formatted news'
+task :news_rdoc do
+ tags.each do |tag|
+ time = tag[:time].tr!('T', ' ').gsub!(/:\d\dZ/, ' UTC')
+ puts "=== #{tag[:tag].sub(/^v/, '')} / #{time}"
+ puts ""
+
+ body = tag[:body]
+ puts tag[:body].gsub(/^/sm, " ").gsub(/[ \t]+$/sm, "")
+ puts ""
+ end
+end
+
+desc "print release changelog for Rubyforge"
+task :release_changes do
+ version = ENV['VERSION'] or abort "VERSION= needed"
+ version = "v#{version}"
+ vtags = tags.map { |tag| tag[:tag] =~ /\Av/ and tag[:tag] }.sort
+ prev = vtags[vtags.index(version) - 1]
+ if prev
+ system('git', 'diff', '--stat', prev, version) or abort $?
+ puts ""
+ system('git', 'log', "#{prev}..#{version}") or abort $?
+ else
+ system('git', 'log', version) or abort $?
+ end
+end
+
+desc "print release notes for Rubyforge"
+task :release_notes do
+ spec = Gem::Specification.load('raindrops.gemspec')
+ puts spec.description.strip
+ puts ""
+ puts "* #{spec.homepage}"
+ puts "* #{spec.email}"
+ puts "* #{git_url}"
+
+ _, _, body = `git cat-file tag v#{spec.version}`.split(/\n\n/, 3)
+ print "\nChanges:\n\n"
+ puts body
+end
+
+desc "read news article from STDIN and post to rubyforge"
+task :publish_news do
+ require 'rubyforge'
+ IO.select([STDIN], nil, nil, 1) or abort "E: news must be read from stdin"
+ msg = STDIN.readlines
+ subject = msg.shift
+ blank = msg.shift
+ blank == "\n" or abort "no newline after subject!"
+ subject.strip!
+ body = msg.join("").strip!
+
+ rf = RubyForge.new.configure
+ rf.login
+ rf.post_news('rainbows', subject, body)
+end
+
+desc "post to RAA"
+task :raa_update do
+ require 'net/http'
+ require 'net/netrc'
+ rc = Net::Netrc.locate('raindrops-raa') or abort "~/.netrc not found"
+ password = rc.password
+
+ s = Gem::Specification.load('raindrops.gemspec')
+ desc = [ s.description.strip ]
+ desc << ""
+ desc << "* #{s.email}"
+ desc << "* #{git_url}"
+ desc << "* #{cgit_url}"
+ desc = desc.join("\n")
+ uri = URI.parse('http://raa.ruby-lang.org/regist.rhtml')
+ form = {
+ :name => s.name,
+ :short_description => s.summary,
+ :version => s.version.to_s,
+ :status => 'experimental',
+ :owner => s.authors.first,
+ :email => s.email,
+ :category_major => 'Library',
+ :category_minor => 'Rack',
+ :url => s.homepage,
+ :download => 'http://rubyforge.org/frs/?group_id=8977',
+ :license => 'LGPL', # LGPLv3, actually, but RAA is ancient...
+ :description_style => 'Plain',
+ :description => desc,
+ :pass => password,
+ :submit => 'Update',
+ }
+ res = Net::HTTP.post_form(uri, form)
+ p res
+ puts res.body
+end
2  TODO
@@ -0,0 +1,2 @@
+* more portable atomics for non-GCC systems (libatomicops?)
+* pure Ruby version for non-forking servers
28 examples/linux-tcp-listener-stats.rb
@@ -0,0 +1,28 @@
+#!/usr/bin/ruby
+
+# this is used to show or watch the number of active and queued
+# connections on any listener socket from the command line
+
+require 'raindrops'
+require 'optparse'
+usage = "Usage: #$0 [--loop] ADDR..."
+ARGV.size > 0 or abort usage
+delay = false
+
+# "normal" exits when driven on the command-line
+trap(:INT) { exit 130 }
+trap(:PIPE) { exit 0 }
+
+opts = OptionParser.new('', 24, ' ') do |opts|
+ opts.banner = usage
+ opts.on('-d', '--delay=delay') { |nr| delay = nr.to_i }
+ opts.parse! ARGV
+end
+
+fmt = "% 19s % 10u % 10u\n"
+printf fmt.tr('u','s'), *%w(address active queued)
+
+begin
+ stats = Raindrops::Linux.tcp_listener_stats(ARGV)
+ stats.each { |addr,stats| printf fmt, addr, stats.active, stats.queued }
+end while delay && sleep(delay)
11 ext/raindrops/extconf.rb
@@ -0,0 +1,11 @@
+require 'mkmf'
+
+# FIXME: test for GCC __sync_XXX builtins here, somehow...
+have_func('mmap', 'sys/mman.h') or abort 'mmap() not found'
+have_func('munmap', 'sys/mman.h') or abort 'munmap() not found'
+
+have_func("rb_struct_alloc_noinit")
+have_func('rb_thread_blocking_region')
+
+dir_config('raindrops')
+create_makefile('raindrops_ext')
342 ext/raindrops/linux_inet_diag.c
@@ -0,0 +1,342 @@
+#include <ruby.h>
+
+/* Ruby 1.8.6+ macros (for compatibility with Ruby 1.9) */
+#ifndef RSTRING_PTR
+# define RSTRING_PTR(s) (RSTRING(s)->ptr)
+#endif
+#ifndef RSTRING_LEN
+# define RSTRING_LEN(s) (RSTRING(s)->len)
+#endif
+#ifndef RSTRUCT_PTR
+# define RSTRUCT_PTR(s) (RSTRUCT(s)->ptr)
+#endif
+#ifndef RSTRUCT_LEN
+# define RSTRUCT_LEN(s) (RSTRUCT(s)->len)
+#endif
+
+#ifndef HAVE_RB_STRUCT_ALLOC_NOINIT
+static ID id_new;
+static VALUE rb_struct_alloc_noinit(VALUE class)
+{
+ return rb_funcall(class, id_new, 0, 0);
+}
+#endif /* !defined(HAVE_RB_STRUCT_ALLOC_NOINIT) */
+
+/* partial emulation of the 1.9 rb_thread_blocking_region under 1.8 */
+#ifndef HAVE_RB_THREAD_BLOCKING_REGION
+# include <rubysig.h>
+# define RUBY_UBF_IO ((rb_unblock_function_t *)-1)
+typedef void rb_unblock_function_t(void *);
+typedef VALUE rb_blocking_function_t(void *);
+static VALUE
+rb_thread_blocking_region(
+ rb_blocking_function_t *func, void *data1,
+ rb_unblock_function_t *ubf, void *data2)
+{
+ VALUE rv;
+
+ TRAP_BEG;
+ rv = func(data1);
+ TRAP_END;
+
+ return rv;
+}
+#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */
+
+#include <assert.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <string.h>
+#include <asm/types.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netinet/tcp.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <linux/inet_diag.h>
+
+static size_t page_size;
+static unsigned g_seq;
+static VALUE cListenStats;
+
+struct my_addr {
+ in_addr_t addr;
+ uint16_t port;
+};
+
+struct listen_stats {
+ long active;
+ long queued;
+};
+
+#define OPLEN (sizeof(struct inet_diag_bc_op) + \
+ sizeof(struct inet_diag_hostcond) + \
+ sizeof(in_addr_t))
+
+struct nogvl_args {
+ struct iovec iov[3]; /* last iov holds inet_diag bytecode */
+ struct my_addr addrs;
+ struct listen_stats stats;
+};
+
+/* creates a Ruby ListenStats Struct based on our internal listen_stats */
+static VALUE rb_listen_stats(struct listen_stats *stats)
+{
+ VALUE rv = rb_struct_alloc_noinit(cListenStats);
+ VALUE *ptr = RSTRUCT_PTR(rv);
+
+ ptr[0] = LONG2NUM(stats->active);
+ ptr[1] = LONG2NUM(stats->queued);
+
+ return rv;
+}
+
+/*
+ * converts a base 10 string representing a port number into
+ * an unsigned 16 bit integer. Raises ArgumentError on failure
+ */
+static uint16_t my_inet_port(const char *port)
+{
+ char *err;
+ unsigned long tmp = strtoul(port, &err, 10);
+
+ if (*err != 0 || tmp > 0xffff)
+ rb_raise(rb_eArgError, "port not parsable: `%s'\n", port);
+
+ return (uint16_t)tmp;
+}
+
+/* inner loop of inet_diag, called for every socket returned by netlink */
+static inline void r_acc(struct nogvl_args *args, struct inet_diag_msg *r)
+{
+ /*
+ * inode == 0 means the connection is still in the listen queue
+ * and has not yet been accept()-ed by the server. The
+ * inet_diag bytecode cannot filter this for us.
+ */
+ if (r->idiag_inode == 0)
+ return;
+ if (r->idiag_state == TCP_ESTABLISHED)
+ args->stats.active++;
+ else /* if (r->idiag_state == TCP_LISTEN) */
+ args->stats.queued = r->idiag_rqueue;
+ /*
+ * we wont get anything else because of the idiag_states filter
+ */
+}
+
+static const char err_socket[] = "socket";
+static const char err_sendmsg[] = "sendmsg";
+static const char err_recvmsg[] = "recvmsg";
+static const char err_nlmsg[] = "nlmsg";
+
+/* does the inet_diag stuff with netlink(), this is called w/o GVL */
+static VALUE diag(void *ptr)
+{
+ struct nogvl_args *args = ptr;
+ struct sockaddr_nl nladdr;
+ struct rtattr rta;
+ struct {
+ struct nlmsghdr nlh;
+ struct inet_diag_req r;
+ } req;
+ struct msghdr msg;
+ const char *err = NULL;
+ unsigned seq = __sync_add_and_fetch(&g_seq, 1);
+ int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_INET_DIAG);
+
+ if (fd < 0)
+ return (VALUE)err_socket;
+
+ memset(&args->stats, 0, sizeof(struct listen_stats));
+
+ memset(&nladdr, 0, sizeof(nladdr));
+ nladdr.nl_family = AF_NETLINK;
+
+ memset(&req, 0, sizeof(req));
+ req.nlh.nlmsg_len = sizeof(req) + RTA_LENGTH(args->iov[2].iov_len);
+ req.nlh.nlmsg_type = TCPDIAG_GETSOCK;
+ req.nlh.nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;
+ req.nlh.nlmsg_pid = getpid();
+ req.nlh.nlmsg_seq = seq;
+ req.r.idiag_family = AF_INET;
+ req.r.idiag_states = (1<<TCP_ESTABLISHED) | (1<<TCP_LISTEN);
+ rta.rta_type = INET_DIAG_REQ_BYTECODE;
+ rta.rta_len = RTA_LENGTH(args->iov[2].iov_len);
+
+ args->iov[0].iov_base = &req;
+ args->iov[0].iov_len = sizeof(req);
+ args->iov[1].iov_base = &rta;
+ args->iov[1].iov_len = sizeof(rta);
+
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_name = (void *)&nladdr;
+ msg.msg_namelen = sizeof(nladdr);
+ msg.msg_iov = args->iov;
+ msg.msg_iovlen = 3;
+
+ if (sendmsg(fd, &msg, 0) < 0) {
+ err = err_sendmsg;
+ goto out;
+ }
+
+ /* reuse buffer that was allocated for bytecode */
+ args->iov[0].iov_len = page_size;
+ args->iov[0].iov_base = args->iov[2].iov_base;
+
+ while (1) {
+ ssize_t readed;
+ struct nlmsghdr *h = (struct nlmsghdr *)args->iov[0].iov_base;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_name = (void *)&nladdr;
+ msg.msg_namelen = sizeof(nladdr);
+ msg.msg_iov = args->iov;
+ msg.msg_iovlen = 1;
+
+ readed = recvmsg(fd, &msg, 0);
+ if (readed < 0) {
+ if (errno == EINTR)
+ continue;
+ err = err_recvmsg;
+ goto out;
+ }
+ if (readed == 0)
+ goto out;
+
+ for ( ; NLMSG_OK(h, readed); h = NLMSG_NEXT(h, readed)) {
+ if (h->nlmsg_seq != seq)
+ continue;
+ if (h->nlmsg_type == NLMSG_DONE)
+ goto out;
+ if (h->nlmsg_type == NLMSG_ERROR) {
+ err = err_nlmsg;
+ goto out;
+ }
+ r_acc(args, NLMSG_DATA(h));
+ }
+ }
+out:
+ {
+ int save_errno = errno;
+ close(fd);
+ errno = save_errno;
+ }
+ return (VALUE)err;
+}
+
+/* populates inet my_addr struct by parsing +addr+ */
+static void parse_addr(struct my_addr *inet, VALUE addr)
+{
+ char *host_port, *colon;
+
+ if (TYPE(addr) != T_STRING)
+ rb_raise(rb_eArgError, "addrs must be an Array of Strings");
+
+ host_port = RSTRING_PTR(addr);
+ colon = memchr(host_port, ':', RSTRING_LEN(addr));
+ if (!colon)
+ rb_raise(rb_eArgError, "port not found in: `%s'", host_port);
+
+ *colon = 0;
+ inet->addr = inet_addr(host_port);
+ *colon = ':';
+ inet->port = htons(my_inet_port(colon + 1));
+}
+
+/* generates inet_diag bytecode to match a single addr */
+static void gen_bytecode(struct iovec *iov, struct my_addr *inet)
+{
+ struct inet_diag_bc_op *op;
+ struct inet_diag_hostcond *cond;
+
+ /* iov_len was already set and base allocated in a parent function */
+ assert(iov->iov_len == OPLEN && iov->iov_base && "iov invalid");
+ op = iov->iov_base;
+ op->code = INET_DIAG_BC_S_COND;
+ op->yes = OPLEN;
+ op->no = sizeof(struct inet_diag_bc_op) + OPLEN;
+
+ cond = (struct inet_diag_hostcond *)(op + 1);
+ cond->family = AF_INET;
+ cond->port = ntohs(inet->port);
+ cond->prefix_len = inet->addr == 0 ? 0 : sizeof(in_addr_t) * CHAR_BIT;
+ *cond->addr = inet->addr;
+}
+
+static VALUE tcp_stats(struct nogvl_args *args, VALUE addr)
+{
+ const char *err;
+ VALUE verr;
+
+ parse_addr(&args->addrs, addr);
+ gen_bytecode(&args->iov[2], &args->addrs);
+
+ verr = rb_thread_blocking_region(diag, args, RUBY_UBF_IO, 0);
+ err = (const char *)verr;
+ if (err) {
+ if (err == err_nlmsg)
+ rb_raise(rb_eRuntimeError, "NLMSG_ERROR");
+ else
+ rb_sys_fail(err);
+ }
+
+ return rb_listen_stats(&args->stats);
+}
+
+/*
+ * call-seq:
+ * addrs = %w(0.0.0.0:80 127.0.0.1:8080)
+ * Raindrops::Linux.tcp_listener_stats(addrs) => hash
+ *
+ * Takes an array of strings representing listen addresses to filter for.
+ * Returns a hash with given addresses as keys and ListenStats
+ * objects as the values.
+ */
+static VALUE tcp_listener_stats(VALUE obj, VALUE addrs)
+{
+ VALUE *ary;
+ long i;
+ VALUE rv;
+ struct nogvl_args args;
+
+ /*
+ * allocating page_size instead of OP_LEN since we'll reuse the
+ * buffer for recvmsg() later, we already checked for
+ * OPLEN <= page_size at initialization
+ */
+ args.iov[2].iov_len = OPLEN;
+ args.iov[2].iov_base = alloca(page_size);
+
+ if (TYPE(addrs) != T_ARRAY)
+ rb_raise(rb_eArgError, "addrs must be an Array or String");
+
+ rv = rb_hash_new();
+ ary = RARRAY_PTR(addrs);
+ for (i = RARRAY_LEN(addrs); --i >= 0; ary++)
+ rb_hash_aset(rv, *ary, tcp_stats(&args, *ary));
+
+ return rv;
+}
+
+void Init_raindrops_linux_inet_diag(void)
+{
+ VALUE cRaindrops = rb_const_get(rb_cObject, rb_intern("Raindrops"));
+ VALUE mLinux = rb_define_module_under(cRaindrops, "Linux");
+
+ cListenStats = rb_const_get(cRaindrops, rb_intern("ListenStats"));
+
+ rb_define_module_function(mLinux, "tcp_listener_stats",
+ tcp_listener_stats, 1);
+
+#ifndef HAVE_RB_STRUCT_ALLOC_NOINIT
+ id_new = rb_intern("new");
+#endif
+ rb_require("raindrops/linux");
+
+ page_size = getpagesize();
+
+ assert(OPLEN <= page_size && "bytecode OPLEN is no <= PAGE_SIZE");
+}
192 ext/raindrops/raindrops.c
@@ -0,0 +1,192 @@
+#include <ruby.h>
+#include <sys/mman.h>
+#include <assert.h>
+#include <errno.h>
+#include <stddef.h>
+
+/*
+ * most modern CPUs have a cache-line size of 64 or 128.
+ * We choose a bigger one by default since our structure is not
+ * heavily used
+ */
+#ifndef CACHE_LINE_SIZE
+# define CACHE_LINE_SIZE 128
+#endif
+
+/* each raindrop is a counter */
+struct raindrop {
+ union {
+ unsigned long counter;
+ unsigned char padding[CACHE_LINE_SIZE];
+ } as;
+} __attribute__((packed));
+
+/* allow mmap-ed regions can store more than one raindrop */
+struct raindrops {
+ long size;
+ struct raindrop *drops;
+};
+
+/* called by GC */
+static void evaporate(void *ptr)
+{
+ struct raindrops *r = ptr;
+
+ if (r->drops) {
+ int rv = munmap(r->drops, sizeof(struct raindrop) * r->size);
+ if (rv != 0)
+ rb_bug("munmap failed in gc: %s", strerror(errno));
+ }
+
+ xfree(ptr);
+}
+
+/* automatically called at creation (before initialize) */
+static VALUE alloc(VALUE klass)
+{
+ struct raindrops *r;
+
+ return Data_Make_Struct(klass, struct raindrops, NULL, evaporate, r);
+}
+
+static struct raindrops *get(VALUE self)
+{
+ struct raindrops *r;
+
+ Data_Get_Struct(self, struct raindrops, r);
+
+ return r;
+}
+
+/* initializes a Raindrops object to hold +size+ elements */
+static VALUE init(VALUE self, VALUE size)
+{
+ struct raindrops *r = get(self);
+ int tries = 1;
+
+ if (r->drops)
+ rb_raise(rb_eRuntimeError, "already initialized");
+
+ r->size = NUM2LONG(size);
+ if (r->size < 1)
+ rb_raise(rb_eArgError, "size must be >= 1");
+
+retry:
+ r->drops = mmap(NULL, sizeof(struct raindrop) * r->size,
+ PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
+ if (r->drops == MAP_FAILED) {
+ if ((errno == EAGAIN || errno == ENOMEM) && tries-- > 0) {
+ rb_gc();
+ goto retry;
+ }
+ rb_sys_fail("mmap");
+ }
+
+ return self;
+}
+
+/* :nodoc */
+static VALUE init_copy(VALUE dest, VALUE source)
+{
+ struct raindrops *dst = get(dest);
+ struct raindrops *src = get(source);
+
+ init(dest, LONG2NUM(src->size));
+ memcpy(dst->drops, src->drops, sizeof(struct raindrop) * src->size);
+
+ return dest;
+}
+
+static unsigned long *addr_of(VALUE self, VALUE index)
+{
+ struct raindrops *r = get(self);
+ unsigned long off = FIX2ULONG(index) * sizeof(struct raindrop);
+
+ if (off >= sizeof(struct raindrop) * r->size)
+ rb_raise(rb_eArgError, "offset overrun");
+
+ return (unsigned long *)((unsigned long)r->drops + off);
+}
+
+static unsigned long incr_decr_arg(int argc, const VALUE *argv)
+{
+ if (argc > 2 || argc < 1)
+ rb_raise(rb_eArgError,
+ "wrong number of arguments (%d for 1+)", argc);
+
+ return argc == 2 ? NUM2ULONG(argv[1]) : 1;
+}
+
+/* increments the value referred to by the +index+ constant by 1 */
+static VALUE incr(int argc, VALUE *argv, VALUE self)
+{
+ unsigned long nr = incr_decr_arg(argc, argv);
+
+ return ULONG2NUM(__sync_add_and_fetch(addr_of(self, argv[0]), nr));
+}
+
+/* decrements the value referred to by the +index+ constant by 1 */
+static VALUE decr(int argc, VALUE *argv, VALUE self)
+{
+ unsigned long nr = incr_decr_arg(argc, argv);
+
+ return ULONG2NUM(__sync_sub_and_fetch(addr_of(self, argv[0]), nr));
+}
+
+/* converts the raindrops structure to an Array */
+static VALUE to_ary(VALUE self)
+{
+ struct raindrops *r = get(self);
+ VALUE rv = rb_ary_new2(r->size);
+ long i;
+ unsigned long base = (unsigned long)r->drops;
+
+ for (i = 0; i < r->size; i++) {
+ rb_ary_push(rv, ULONG2NUM(*((unsigned long *)base)));
+ base += sizeof(struct raindrop);
+ }
+
+ return rv;
+}
+
+static VALUE size(VALUE self)
+{
+ return LONG2NUM(get(self)->size);
+}
+
+static VALUE aset(VALUE self, VALUE index, VALUE value)
+{
+ unsigned long *addr = addr_of(self, index);
+
+ *addr = NUM2ULONG(value);
+
+ return value;
+}
+
+static VALUE aref(VALUE self, VALUE index)
+{
+ return ULONG2NUM(*addr_of(self, index));
+}
+
+#ifdef __linux__
+void Init_raindrops_linux_inet_diag(void);
+#endif
+
+void Init_raindrops_ext(void)
+{
+ VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
+ rb_define_alloc_func(cRaindrops, alloc);
+
+ rb_define_method(cRaindrops, "initialize", init, 1);
+ rb_define_method(cRaindrops, "incr", incr, -1);
+ rb_define_method(cRaindrops, "decr", decr, -1);
+ rb_define_method(cRaindrops, "to_ary", to_ary, 0);
+ rb_define_method(cRaindrops, "[]", aref, 1);
+ rb_define_method(cRaindrops, "[]=", aset, 2);
+ rb_define_method(cRaindrops, "size", size, 0);
+ rb_define_method(cRaindrops, "initialize_copy", init_copy, 1);
+
+#ifdef __linux__
+ Init_raindrops_linux_inet_diag();
+#endif
+}
32 lib/raindrops.rb
@@ -0,0 +1,32 @@
+# -*- encoding: binary -*-
+class Raindrops
+
+ # Used to represent the number of +active+ and +queued+ sockets for
+ # a single listen socket across all threads and processes on a
+ # machine.
+ #
+ # For TCP listeners, only sockets in the TCP_ESTABLISHED state are
+ # accounted for. For Unix domain listeners, only CONNECTING and
+ # CONNECTED Unix domain sockets are accounted for.
+ #
+ # +active+ connections is the number of accept()-ed but not-yet-closed
+ # sockets in all threads/processes sharing the given listener.
+ #
+ # +queued+ connections is the number of un-accept()-ed sockets in the
+ # queue of a given listen socket.
+ #
+ # These stats are currently only available under Linux
+ class ListenStats < Struct.new(:active, :queued)
+
+ # the sum of +active+ and +queued+ sockets
+ def total
+ active + queued
+ end
+ end
+
+ # TODO: pure Ruby version for single processes
+ require 'raindrops_ext'
+
+ autoload :Struct, 'raindrops/struct'
+ autoload :Middleware, 'raindrops/middleware'
+end
55 lib/raindrops/linux.rb
@@ -0,0 +1,55 @@
+# -*- encoding: binary -*-
+class Raindrops
+module Linux
+
+ # The standard proc path for active UNIX domain sockets, feel free to call
+ # String#replace on this if your /proc is mounted in a non-standard location
+ # for whatever reason
+ PROC_NET_UNIX = "/proc/net/unix"
+
+ # Get ListenStats from an array of +paths+
+ #
+ # Socket state mapping from integer => symbol, based on socket_state
+ # enum from include/linux/net.h in the Linux kernel:
+ # typedef enum {
+ # SS_FREE = 0, /* not allocated */
+ # SS_UNCONNECTED, /* unconnected to any socket */
+ # SS_CONNECTING, /* in process of connecting */
+ # SS_CONNECTED, /* connected to socket */
+ # SS_DISCONNECTING /* in process of disconnecting */
+ # } socket_state;
+ # * SS_CONNECTING maps to ListenStats#active
+ # * SS_CONNECTED maps to ListenStats#queued
+ #
+ # This method may be significantly slower than its tcp_listener_stats
+ # counterpart due to the latter being able to use inet_diag via netlink.
+ # This parses /proc/net/unix as there is no other (known) way
+ # to expose Unix domain socket statistics over netlink.
+ def unix_listener_stats(paths)
+ rv = Hash.new { |h,k| h[k.freeze] = ListenStats.new(0, 0) }
+ paths = paths.map do |path|
+ path = path.dup
+ path.force_encoding(Encoding::BINARY) if defined?(Encoding)
+ rv[path]
+ Regexp.escape(path)
+ end
+ paths = / 00000000 \d+ (\d+)\s+\d+ (#{paths.join('|')})$/n
+
+ # no point in pread since we can't stat for size on this file
+ File.open(PROC_NET_UNIX, "rb") do |fp|
+ fp.read.scan(paths).each do |s|
+ path = s.last
+ case s.first.to_i
+ when 2 then rv[path].queued += 1
+ when 3 then rv[path].active += 1
+ end
+ end
+ end
+
+ rv
+ end
+
+ module_function :unix_listener_stats
+
+end # Linux
+end # Raindrops
75 lib/raindrops/middleware.rb
@@ -0,0 +1,75 @@
+# -*- encoding: binary -*-
+require 'raindrops'
+
+# Raindrops middleware should be loaded at the top of Rack
+# middleware stack before other middlewares for maximum accuracy.
+class Raindrops
+class Middleware < ::Struct.new(:app, :stats, :path, :tcp, :unix)
+
+ # :stopdoc:
+ Stats = Raindrops::Struct.new(:calling, :writing)
+ PATH_INFO = "PATH_INFO"
+ # :startdoc:
+
+ def initialize(app, opts = {})
+ super(app, opts[:stats] || Stats.new, opts[:path] || "/_raindrops")
+ if tmp = opts[:listeners]
+ self.tcp = tmp.grep(/\A[^:]+:\d+\z/)
+ self.unix = tmp.grep(%r{\A/})
+ self.tcp = nil if tcp.empty?
+ self.unix = nil if unix.empty?
+ end
+ end
+
+ # standard Rack endpoing
+ def call(env)
+ env[PATH_INFO] == path ? stats_response : dup._call(env)
+ end
+
+ def _call(env)
+ stats.incr_calling
+ status, headers, self.app = app.call(env)
+
+ # the Rack server will start writing headers soon after this method
+ stats.incr_writing
+ [ status, headers, self ]
+ ensure
+ stats.decr_calling
+ end
+
+ # yield to the Rack server here for writing
+ def each(&block)
+ app.each(&block)
+ end
+
+ # the Rack server should call this after #each (usually ensure-d)
+ def close
+ stats.decr_writing
+ ensure
+ app.close if app.respond_to?(:close)
+ end
+
+ def stats_response
+ body = "calling: #{stats.calling}\n" \
+ "writing: #{stats.writing}\n"
+
+ if defined?(Linux)
+ Linux.tcp_listener_stats(tcp).each do |addr,stats|
+ body << "#{addr} active: #{stats.active}\n" \
+ "#{addr} queued: #{stats.queued}\n"
+ end if tcp
+ Linux.unix_listener_stats(unix).each do |addr,stats|
+ body << "#{addr} active: #{stats.active}\n" \
+ "#{addr} queued: #{stats.queued}\n"
+ end if unix
+ end
+
+ headers = {
+ "Content-Type" => "text/plain",
+ "Content-Length" => body.size.to_s,
+ }
+ [ 200, headers, [ body ] ]
+ end
+
+end
+end
47 lib/raindrops/struct.rb
@@ -0,0 +1,47 @@
+# -*- encoding: binary -*-
+
+class Raindrops::Struct
+
+ def self.new(*members)
+ members = members.map { |x| x.to_sym }.freeze
+ str = <<EOS
+def initialize(*values)
+ (MEMBERS.size >= values.size) or raise ArgumentError, "too many arguments"
+ @raindrops = Raindrops.new(MEMBERS.size)
+ values.each_with_index { |val,i| @raindrops[i] = values[i] }
+end
+
+def initialize_copy(src)
+ @raindrops = src.instance_variable_get(:@raindrops).dup
+end
+
+def []=(index, value)
+ @raindrops[index] = value
+end
+
+def [](index)
+ @raindrops[index]
+end
+
+def to_hash
+ ary = @raindrops.to_ary
+ rv = {}
+ MEMBERS.each_with_index { |member, i| rv[member] = ary[i] }
+ rv
+end
+EOS
+
+ members.each_with_index do |member, i|
+ str << "def incr_#{member}; @raindrops.incr(#{i}); end; " \
+ "def decr_#{member}; @raindrops.decr(#{i}); end; " \
+ "def #{member}; @raindrops[#{i}]; end; " \
+ "def #{member}=(val); @raindrops[#{i}] = val; end; "
+ end
+
+ klass = Class.new
+ klass.const_set(:MEMBERS, members)
+ klass.class_eval(str)
+ klass
+ end
+
+end
38 raindrops.gemspec
@@ -0,0 +1,38 @@
+# -*- encoding: binary -*-
+
+ENV["VERSION"] or abort "VERSION= must be specified"
+manifest = File.readlines('.manifest').map! { |x| x.chomp! }
+test_files = manifest.grep(%r{\Atest/test_.*\.rb\z})
+
+Gem::Specification.new do |s|
+ s.name = %q{raindrops}
+ s.version = ENV["VERSION"]
+
+ s.authors = ["raindrops hackers"]
+ s.date = Time.now.utc.strftime('%Y-%m-%d')
+ s.description = File.read("README").split(/\n\n/)[1]
+ s.email = %q{raindrops@librelist.com}
+ s.extensions = %w(ext/raindrops/extconf.rb)
+
+ s.extra_rdoc_files = File.readlines('.document').map! do |x|
+ x.chomp!
+ if File.directory?(x)
+ manifest.grep(%r{\A#{x}/})
+ elsif File.file?(x)
+ x
+ else
+ nil
+ end
+ end.flatten.compact
+
+ s.files = manifest
+ s.homepage = %q{http://raindrops.bogomips.org/}
+ s.summary = %q{real-time stats for Rack servers}
+ s.rdoc_options = [ "-Na", "-t", "raindrops - #{s.summary}" ]
+ s.require_paths = %w(lib)
+ s.rubyforge_project = %q{raindrops}
+
+ s.test_files = test_files
+
+ # s.licenses = %w(LGPLv3) # accessor not compatible with older RubyGems
+end
1,586 setup.rb
@@ -0,0 +1,1586 @@
+# -*- encoding: binary -*-
+#
+# setup.rb
+#
+# Copyright (c) 2000-2005 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU LGPL, Lesser General Public License version 2.1.
+#
+
+unless Enumerable.method_defined?(:map) # Ruby 1.4.6
+ module Enumerable
+ alias map collect
+ end
+end
+
+unless File.respond_to?(:read) # Ruby 1.6
+ def File.read(fname)
+ open(fname) {|f|
+ return f.read
+ }
+ end
+end
+
+unless Errno.const_defined?(:ENOTEMPTY) # Windows?
+ module Errno
+ class ENOTEMPTY
+ # We do not raise this exception, implementation is not needed.
+ end
+ end
+end
+
+def File.binread(fname)
+ open(fname, 'rb') {|f|
+ return f.read
+ }
+end
+
+# for corrupted Windows' stat(2)
+def File.dir?(path)
+ File.directory?((path[-1,1] == '/') ? path : path + '/')
+end
+
+
+class ConfigTable
+
+ include Enumerable
+
+ def initialize(rbconfig)
+ @rbconfig = rbconfig
+ @items = []
+ @table = {}
+ # options
+ @install_prefix = nil
+ @config_opt = nil
+ @verbose = true
+ @no_harm = false
+ end
+
+ attr_accessor :install_prefix
+ attr_accessor :config_opt
+
+ attr_writer :verbose
+
+ def verbose?
+ @verbose
+ end
+
+ attr_writer :no_harm
+
+ def no_harm?
+ @no_harm
+ end
+
+ def [](key)
+ lookup(key).resolve(self)
+ end
+
+ def []=(key, val)
+ lookup(key).set val
+ end
+
+ def names
+ @items.map {|i| i.name }
+ end
+
+ def each(&block)
+ @items.each(&block)
+ end
+
+ def key?(name)
+ @table.key?(name)
+ end
+
+ def lookup(name)
+ @table[name] or setup_rb_error "no such config item: #{name}"
+ end
+
+ def add(item)
+ @items.push item
+ @table[item.name] = item
+ end
+
+ def remove(name)
+ item = lookup(name)
+ @items.delete_if {|i| i.name == name }
+ @table.delete_if {|name, i| i.name == name }
+ item
+ end
+
+ def load_script(path, inst = nil)
+ if File.file?(path)
+ MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
+ end
+ end
+
+ def savefile
+ '.config'
+ end
+
+ def load_savefile
+ begin
+ File.foreach(savefile()) do |line|
+ k, v = *line.split(/=/, 2)
+ self[k] = v.strip
+ end
+ rescue Errno::ENOENT
+ setup_rb_error $!.message + "\n#{File.basename($0)} config first"
+ end
+ end
+
+ def save
+ @items.each {|i| i.value }
+ File.open(savefile(), 'w') {|f|
+ @items.each do |i|
+ f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
+ end
+ }
+ end
+
+ def load_standard_entries
+ standard_entries(@rbconfig).each do |ent|
+ add ent
+ end
+ end
+
+ def standard_entries(rbconfig)
+ c = rbconfig
+
+ rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
+
+ major = c['MAJOR'].to_i
+ minor = c['MINOR'].to_i
+ teeny = c['TEENY'].to_i
+ version = "#{major}.#{minor}"
+
+ # ruby ver. >= 1.4.4?
+ newpath_p = ((major >= 2) or
+ ((major == 1) and
+ ((minor >= 5) or
+ ((minor == 4) and (teeny >= 4)))))
+
+ if c['rubylibdir']
+ # V > 1.6.3
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = c['rubylibdir']
+ librubyverarch = c['archdir']
+ siteruby = c['sitedir']
+ siterubyver = c['sitelibdir']
+ siterubyverarch = c['sitearchdir']
+ elsif newpath_p
+ # 1.4.4 <= V <= 1.6.3
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+ siteruby = c['sitedir']
+ siterubyver = "$siteruby/#{version}"
+ siterubyverarch = "$siterubyver/#{c['arch']}"
+ else
+ # V < 1.4.4
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+ siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
+ siterubyver = siteruby
+ siterubyverarch = "$siterubyver/#{c['arch']}"
+ end
+ parameterize = lambda {|path|
+ path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
+ }
+
+ if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
+ makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
+ else
+ makeprog = 'make'
+ end
+
+ [
+ ExecItem.new('installdirs', 'std/site/home',
+ 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
+ {|val, table|
+ case val
+ when 'std'
+ table['rbdir'] = '$librubyver'
+ table['sodir'] = '$librubyverarch'
+ when 'site'
+ table['rbdir'] = '$siterubyver'
+ table['sodir'] = '$siterubyverarch'
+ when 'home'
+ setup_rb_error '$HOME was not set' unless ENV['HOME']
+ table['prefix'] = ENV['HOME']
+ table['rbdir'] = '$libdir/ruby'
+ table['sodir'] = '$libdir/ruby'
+ end
+ },
+ PathItem.new('prefix', 'path', c['prefix'],
+ 'path prefix of target environment'),
+ PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
+ 'the directory for commands'),
+ PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
+ 'the directory for libraries'),
+ PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
+ 'the directory for shared data'),
+ PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
+ 'the directory for man pages'),
+ PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
+ 'the directory for system configuration files'),
+ PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
+ 'the directory for local state data'),
+ PathItem.new('libruby', 'path', libruby,
+ 'the directory for ruby libraries'),
+ PathItem.new('librubyver', 'path', librubyver,
+ 'the directory for standard ruby libraries'),
+ PathItem.new('librubyverarch', 'path', librubyverarch,
+ 'the directory for standard ruby extensions'),
+ PathItem.new('siteruby', 'path', siteruby,
+ 'the directory for version-independent aux ruby libraries'),
+ PathItem.new('siterubyver', 'path', siterubyver,
+ 'the directory for aux ruby libraries'),
+ PathItem.new('siterubyverarch', 'path', siterubyverarch,
+ 'the directory for aux ruby binaries'),
+ PathItem.new('rbdir', 'path', '$siterubyver',
+ 'the directory for ruby scripts'),
+ PathItem.new('sodir', 'path', '$siterubyverarch',
+ 'the directory for ruby extentions'),
+ PathItem.new('rubypath', 'path', rubypath,
+ 'the path to set to #! line'),
+ ProgramItem.new('rubyprog', 'name', rubypath,
+ 'the ruby program using for installation'),
+ ProgramItem.new('makeprog', 'name', makeprog,
+ 'the make program to compile ruby extentions'),
+ SelectItem.new('shebang', 'all/ruby/never', 'ruby',
+ 'shebang line (#!) editing mode'),
+ BoolItem.new('without-ext', 'yes/no', 'no',
+ 'does not compile/install ruby extentions')
+ ]
+ end
+ private :standard_entries
+
+ def load_multipackage_entries
+ multipackage_entries().each do |ent|
+ add ent
+ end
+ end
+
+ def multipackage_entries
+ [
+ PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
+ 'package names that you want to install'),
+ PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
+ 'package names that you do not want to install')
+ ]
+ end
+ private :multipackage_entries
+
+ ALIASES = {
+ 'std-ruby' => 'librubyver',
+ 'stdruby' => 'librubyver',
+ 'rubylibdir' => 'librubyver',
+ 'archdir' => 'librubyverarch',
+ 'site-ruby-common' => 'siteruby', # For backward compatibility
+ 'site-ruby' => 'siterubyver', # For backward compatibility
+ 'bin-dir' => 'bindir',
+ 'bin-dir' => 'bindir',
+ 'rb-dir' => 'rbdir',
+ 'so-dir' => 'sodir',
+ 'data-dir' => 'datadir',
+ 'ruby-path' => 'rubypath',
+ 'ruby-prog' => 'rubyprog',
+ 'ruby' => 'rubyprog',
+ 'make-prog' => 'makeprog',
+ 'make' => 'makeprog'
+ }
+
+ def fixup
+ ALIASES.each do |ali, name|
+ @table[ali] = @table[name]
+ end
+ @items.freeze
+ @table.freeze
+ @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
+ end
+
+ def parse_opt(opt)
+ m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
+ m.to_a[1,2]
+ end
+
+ def dllext
+ @rbconfig['DLEXT']
+ end
+
+ def value_config?(name)
+ lookup(name).value?
+ end
+
+ class Item
+ def initialize(name, template, default, desc)
+ @name = name.freeze
+ @template = template
+ @value = default
+ @default = default
+ @description = desc
+ end
+
+ attr_reader :name
+ attr_reader :description
+
+ attr_accessor :default
+ alias help_default default
+
+ def help_opt
+ "--#{@name}=#{@template}"
+ end
+
+ def value?
+ true
+ end
+
+ def value
+ @value
+ end
+
+ def resolve(table)
+ @value.gsub(%r<\$([^/]+)>) { table[$1] }
+ end
+
+ def set(val)
+ @value = check(val)
+ end
+
+ private
+
+ def check(val)
+ setup_rb_error "config: --#{name} requires argument" unless val
+ val
+ end
+ end
+
+ class BoolItem < Item
+ def config_type
+ 'bool'
+ end
+
+ def help_opt
+ "--#{@name}"
+ end
+
+ private
+
+ def check(val)
+ return 'yes' unless val
+ case val
+ when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
+ when /\An(o)?\z/i, /\Af(alse)\z/i then 'no'
+ else
+ setup_rb_error "config: --#{@name} accepts only yes/no for argument"
+ end
+ end
+ end
+
+ class PathItem < Item
+ def config_type
+ 'path'
+ end
+
+ private
+
+ def check(path)
+ setup_rb_error "config: --#{@name} requires argument" unless path
+ path[0,1] == '$' ? path : File.expand_path(path)
+ end
+ end
+
+ class ProgramItem < Item
+ def config_type
+ 'program'
+ end
+ end
+
+ class SelectItem < Item
+ def initialize(name, selection, default, desc)
+ super
+ @ok = selection.split('/')
+ end
+
+ def config_type
+ 'select'
+ end
+
+ private
+
+ def check(val)
+ unless @ok.include?(val.strip)
+ setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
+ end
+ val.strip
+ end
+ end
+
+ class ExecItem < Item
+ def initialize(name, selection, desc, &block)
+ super name, selection, nil, desc
+ @ok = selection.split('/')
+ @action = block
+ end
+
+ def config_type
+ 'exec'
+ end
+
+ def value?
+ false
+ end
+
+ def resolve(table)
+ setup_rb_error "$#{name()} wrongly used as option value"
+ end
+
+ undef set
+
+ def evaluate(val, table)
+ v = val.strip.downcase
+ unless @ok.include?(v)
+ setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
+ end
+ @action.call v, table
+ end
+ end
+
+ class PackageSelectionItem < Item
+ def initialize(name, template, default, help_default, desc)
+ super name, template, default, desc
+ @help_default = help_default
+ end
+
+ attr_reader :help_default
+
+ def config_type
+ 'package'
+ end
+
+ private
+
+ def check(val)
+ unless File.dir?("packages/#{val}")
+ setup_rb_error "config: no such package: #{val}"
+ end
+ val
+ end
+ end
+
+ class MetaConfigEnvironment
+ def initialize(config, installer)
+ @config = config
+ @installer = installer
+ end
+
+ def config_names
+ @config.names
+ end
+
+ def config?(name)
+ @config.key?(name)
+ end
+
+ def bool_config?(name)
+ @config.lookup(name).config_type == 'bool'
+ end
+
+ def path_config?(name)
+ @config.lookup(name).config_type == 'path'
+ end
+
+ def value_config?(name)
+ @config.lookup(name).config_type != 'exec'
+ end
+
+ def add_config(item)
+ @config.add item
+ end
+
+ def add_bool_config(name, default, desc)
+ @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
+ end
+
+ def add_path_config(name, default, desc)
+ @config.add PathItem.new(name, 'path', default, desc)
+ end
+
+ def set_config_default(name, default)
+ @config.lookup(name).default = default
+ end
+
+ def remove_config(name)
+ @config.remove(name)
+ end
+
+ # For only multipackage
+ def packages
+ raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
+ @installer.packages
+ end
+
+ # For only multipackage
+ def declare_packages(list)
+ raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
+ @installer.packages = list
+ end
+ end
+
+end # class ConfigTable
+
+
+# This module requires: #verbose?, #no_harm?
+module FileOperations
+
+ def mkdir_p(dirname, prefix = nil)
+ dirname = prefix + File.expand_path(dirname) if prefix
+ $stderr.puts "mkdir -p #{dirname}" if verbose?
+ return if no_harm?
+
+ # Does not check '/', it's too abnormal.
+ dirs = File.expand_path(dirname).split(%r<(?=/)>)
+ if /\A[a-z]:\z/i =~ dirs[0]
+ disk = dirs.shift
+ dirs[0] = disk + dirs[0]
+ end
+ dirs.each_index do |idx|
+ path = dirs[0..idx].join('')
+ Dir.mkdir path unless File.dir?(path)
+ end
+ end
+
+ def rm_f(path)
+ $stderr.puts "rm -f #{path}" if verbose?
+ return if no_harm?
+ force_remove_file path
+ end
+
+ def rm_rf(path)
+ $stderr.puts "rm -rf #{path}" if verbose?
+ return if no_harm?
+ remove_tree path
+ end
+
+ def remove_tree(path)
+ if File.symlink?(path)
+ remove_file path
+ elsif File.dir?(path)
+ remove_tree0 path
+ else
+ force_remove_file path
+ end
+ end
+
+ def remove_tree0(path)
+ Dir.foreach(path) do |ent|
+ next if ent == '.'
+ next if ent == '..'
+ entpath = "#{path}/#{ent}"
+ if File.symlink?(entpath)
+ remove_file entpath
+ elsif File.dir?(entpath)
+ remove_tree0 entpath
+ else
+ force_remove_file entpath
+ end
+ end
+ begin
+ Dir.rmdir path
+ rescue Errno::ENOTEMPTY
+ # directory may not be empty
+ end
+ end
+
+ def move_file(src, dest)
+ force_remove_file dest
+ begin
+ File.rename src, dest
+ rescue
+ File.open(dest, 'wb') {|f|
+ f.write File.binread(src)
+ }
+ File.chmod File.stat(src).mode, dest
+ File.unlink src
+ end
+ end
+
+ def force_remove_file(path)
+ begin
+ remove_file path
+ rescue
+ end
+ end
+
+ def remove_file(path)
+ File.chmod 0777, path
+ File.unlink path
+ end
+
+ def install(from, dest, mode, prefix = nil)
+ $stderr.puts "install #{from} #{dest}" if verbose?
+ return if no_harm?
+
+ realdest = prefix ? prefix + File.expand_path(dest) : dest
+ realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
+ str = File.binread(from)
+ if diff?(str, realdest)
+ verbose_off {
+ rm_f realdest if File.exist?(realdest)
+ }
+ File.open(realdest, 'wb') {|f|
+ f.write str
+ }
+ File.chmod mode, realdest
+
+ File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
+ if prefix
+ f.puts realdest.sub(prefix, '')
+ else
+ f.puts realdest
+ end
+ }
+ end
+ end
+
+ def diff?(new_content, path)
+ return true unless File.exist?(path)
+ new_content != File.binread(path)
+ end
+
+ def command(*args)
+ $stderr.puts args.join(' ') if verbose?
+ system(*args) or raise RuntimeError,
+ "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
+ end
+
+ def ruby(*args)
+ command config('rubyprog'), *args
+ end
+
+ def make(task = nil)
+ command(*[config('makeprog'), task].compact)
+ end
+
+ def extdir?(dir)
+ File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
+ end
+
+ def files_of(dir)
+ Dir.open(dir) {|d|
+ return d.select {|ent| File.file?("#{dir}/#{ent}") }
+ }
+ end
+
+ DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
+
+ def directories_of(dir)
+ Dir.open(dir) {|d|
+ return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
+ }
+ end
+
+end
+
+
+# This module requires: #srcdir_root, #objdir_root, #relpath
+module HookScriptAPI
+
+ def get_config(key)
+ @config[key]
+ end
+
+ alias config get_config
+
+ # obsolete: use metaconfig to change configuration
+ def set_config(key, val)
+ @config[key] = val
+ end
+
+ #
+ # srcdir/objdir (works only in the package directory)
+ #
+
+ def curr_srcdir
+ "#{srcdir_root()}/#{relpath()}"
+ end
+
+ def curr_objdir
+ "#{objdir_root()}/#{relpath()}"
+ end
+
+ def srcfile(path)
+ "#{curr_srcdir()}/#{path}"
+ end
+
+ def srcexist?(path)
+ File.exist?(srcfile(path))
+ end
+
+ def srcdirectory?(path)
+ File.dir?(srcfile(path))
+ end
+
+ def srcfile?(path)
+ File.file?(srcfile(path))
+ end
+
+ def srcentries(path = '.')
+ Dir.open("#{curr_srcdir()}/#{path}") {|d|
+ return d.to_a - %w(. ..)
+ }
+ end
+
+ def srcfiles(path = '.')
+ srcentries(path).select {|fname|
+ File.file?(File.join(curr_srcdir(), path, fname))
+ }
+ end
+
+ def srcdirectories(path = '.')
+ srcentries(path).select {|fname|
+ File.dir?(File.join(curr_srcdir(), path, fname))
+ }
+ end
+
+end
+
+
+class ToplevelInstaller
+
+ Version = '3.4.1'
+ Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
+
+ TASKS = [
+ [ 'all', 'do config, setup, then install' ],
+ [ 'config', 'saves your configurations' ],
+ [ 'show', 'shows current configuration' ],
+ [ 'setup', 'compiles ruby extentions and others' ],
+ [ 'install', 'installs files' ],
+ [ 'test', 'run all tests in test/' ],
+ [ 'clean', "does `make clean' for each extention" ],
+ [ 'distclean',"does `make distclean' for each extention" ]
+ ]
+
+ def ToplevelInstaller.invoke
+ config = ConfigTable.new(load_rbconfig())
+ config.load_standard_entries
+ config.load_multipackage_entries if multipackage?
+ config.fixup
+ klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
+ klass.new(File.dirname($0), config).invoke
+ end
+
+ def ToplevelInstaller.multipackage?
+ File.dir?(File.dirname($0) + '/packages')
+ end
+
+ def ToplevelInstaller.load_rbconfig
+ if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
+ ARGV.delete(arg)
+ load File.expand_path(arg.split(/=/, 2)[1])
+ $".push 'rbconfig.rb'
+ else
+ require 'rbconfig'
+ end
+ ::Config::CONFIG
+ end
+
+ def initialize(ardir_root, config)
+ @ardir = File.expand_path(ardir_root)
+ @config = config
+ # cache
+ @valid_task_re = nil
+ end
+
+ def config(key)
+ @config[key]
+ end
+
+ def inspect
+ "#<#{self.class} #{__id__()}>"
+ end
+
+ def invoke
+ run_metaconfigs
+ case task = parsearg_global()
+ when nil, 'all'
+ parsearg_config
+ init_installers
+ exec_config
+ exec_setup
+ exec_install
+ else
+ case task
+ when 'config', 'test'
+ ;
+ when 'clean', 'distclean'
+ @config.load_savefile if File.exist?(@config.savefile)
+ else
+ @config.load_savefile
+ end
+ __send__ "parsearg_#{task}"
+ init_installers
+ __send__ "exec_#{task}"
+ end
+ end
+
+ def run_metaconfigs
+ @config.load_script "#{@ardir}/metaconfig"
+ end
+
+ def init_installers
+ @installer = Installer.new(@config, @ardir, File.expand_path('.'))
+ end
+
+ #
+ # Hook Script API bases
+ #
+
+ def srcdir_root
+ @ardir
+ end
+
+ def objdir_root
+ '.'
+ end
+
+ def relpath
+ '.'
+ end
+
+ #
+ # Option Parsing
+ #
+
+ def parsearg_global
+ while arg = ARGV.shift
+ case arg
+ when /\A\w+\z/
+ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
+ return arg
+ when '-q', '--quiet'
+ @config.verbose = false
+ when '--verbose'
+ @config.verbose = true
+ when '--help'
+ print_usage $stdout
+ exit 0