From 522d4d1472c216bd95a16ca5b118bc14693aad64 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 2 Jan 2010 02:33:23 -0800 Subject: [PATCH] initial commit --- .document | 7 + .gitignore | 14 + COPYING | 165 ++++ Documentation/.gitignore | 5 + Documentation/GNUmakefile | 30 + Documentation/posix-mq.rb.1.txt | 153 ++++ GIT-VERSION-GEN | 40 + GNUmakefile | 179 +++++ LICENSE | 16 + README | 70 ++ Rakefile | 159 ++++ bin/posix-mq.rb | 138 ++++ ext/posix_mq/extconf.rb | 9 + ext/posix_mq/posix_mq.c | 813 ++++++++++++++++++++ lib/posix_mq.rb | 34 + local.mk.sample | 70 ++ posix_mq.gemspec | 39 + setup.rb | 1586 +++++++++++++++++++++++++++++++++++++++ test/test_posix_mq.rb | 225 ++++++ 19 files changed, 3752 insertions(+) create mode 100644 .document create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 Documentation/.gitignore create mode 100644 Documentation/GNUmakefile create mode 100644 Documentation/posix-mq.rb.1.txt create mode 100755 GIT-VERSION-GEN create mode 100644 GNUmakefile create mode 100644 LICENSE create mode 100644 README create mode 100644 Rakefile create mode 100755 bin/posix-mq.rb create mode 100644 ext/posix_mq/extconf.rb create mode 100644 ext/posix_mq/posix_mq.c create mode 100644 lib/posix_mq.rb create mode 100644 local.mk.sample create mode 100644 posix_mq.gemspec create mode 100644 setup.rb create mode 100644 test/test_posix_mq.rb diff --git a/.document b/.document new file mode 100644 index 0000000..6f5c676 --- /dev/null +++ b/.document @@ -0,0 +1,7 @@ +README +LICENSE +NEWS +ChangeLog +lib +ext/posix_mq/posix_mq.c +posix-mq.rb.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a81a2c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.so +*.o +*.log +*.rbc +Makefile +/GIT-VERSION-FILE +/local.mk +/NEWS +/ChangeLog +/.manifest +/GIT-VERSION-FILE +/man +/pkg +/doc diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..cca7fc2 --- /dev/null +++ b/COPYING @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + 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. diff --git a/Documentation/.gitignore b/Documentation/.gitignore new file mode 100644 index 0000000..46679d6 --- /dev/null +++ b/Documentation/.gitignore @@ -0,0 +1,5 @@ +*.1 +*.5 +*.7 +*.gz +*.html diff --git a/Documentation/GNUmakefile b/Documentation/GNUmakefile new file mode 100644 index 0000000..8aea6ae --- /dev/null +++ b/Documentation/GNUmakefile @@ -0,0 +1,30 @@ +all:: + +PANDOC = pandoc +PANDOC_OPTS = -f markdown --email-obfuscation=none --sanitize-html +pandoc = $(PANDOC) $(PANDOC_OPTS) +pandoc_html = $(pandoc) --toc -t html --no-wrap + +man1 := $(addsuffix .1,posix-mq.rb) +html1 := $(addsuffix .html,$(man1)) + +all:: html man + +html: $(html1) +man: $(man1) + +install-html: html + mkdir -p ../doc/man1 + install -m 644 $(html1) ../doc/man1 + +install-man: man + mkdir -p ../man/man1 + install -m 644 $(man1) ../man/man1 + +%.1: %.1.txt + $(pandoc) -s -t man < $< > $@+ && mv $@+ $@ +%.1.html: %.1.txt + $(pandoc_html) < $< > $@+ && mv $@+ $@ + +clean:: + $(RM) $(man1) $(html1) diff --git a/Documentation/posix-mq.rb.1.txt b/Documentation/posix-mq.rb.1.txt new file mode 100644 index 0000000..e9005ee --- /dev/null +++ b/Documentation/posix-mq.rb.1.txt @@ -0,0 +1,153 @@ +% posix-mq.rb(1) posix-mq.rb User Manual +% Ruby POSIX MQ hackers +% Jan 1, 2010 + +# NAME + +posix-mq.rb - command-line interface for POSIX message queues + +# SYNOPSIS + +MQUEUE=/name posix-mq.rb COMMAND [*OPTIONS*] [*ARGUMENTS*] + +# DESCRIPTION + +A command-line interface for manipulating POSIX message queues. It is +useful for testing and debugging applications using POSIX message +queues. + +# COMMANDS + +*create* - create a new message queue + +*attr* - output attributes of the message queue + +*send* - insert a message into the queue from stdin or the command-line + +*receive* - take a message from the queue and outputs it to stdout + +*wait* - sleep until a message is available in the queue + +*unlink* - unlink the message queue + +# CREATE USAGE + +The *create* command accepts the following options: + +-x, \--exclusive +: This causes queue creation to fail if the queue exists. + +-m, \--mode MODE +: The MODE to open the file under, the actual mode of the queue + will be AND-ed with the current umask (like open(2)). + +-c, \--maxmsg COUNT +: The maximum messages in the queue. The default and limit of this + value is system-dependent. This must be specified if \--msgsize is + also specified. + +-s, \--msgsize BYTES +: The maximum size of an individual message. The default and limit of + this value is system-dependent. This must be specified if \--maxmsg + is also specified. + +# ATTR USAGE + +The *attr* command takes no special options nor command-line arguments. +The output format of this command is suitable for "eval" in +shell scripts. Sample output is below: + + flags=0 + maxmsg=10 + msgsize=8192 + curmsgs=3 + +See mq_getattr(3) for information on the meaning of the fields. + +# SEND USAGE + +The *send* command will read a message from standard input if no +command-line arguments are given. If command-line arguments are +given, each argument is considered its own message and will be +inserted into the queue separately. + +The following command-line arguments are accepted: + +-n, \--nonblock +: Exit immediately with error if the message queue is full. + Normally posix-mq.rb(1) will block until the queue is writable or + interrupted. This may not be used in conjunction with \--timeout . +-t, \--timeout SECONDS +: Timeout and exit with error after SECONDS if the message queue is full. + This may not be used in conjunction with \--nonblock. +-p, \--priority PRIORITY +: Specify an integer PRIORITY, this value should be 0 through 31 + (inclusive) for portability across POSIX-compliant systems. + The default priority is 0. + +# RECEIVE USAGE + +The *receive* command will output message to standard output. It will +read a message from standard input if no command-line arguments are +given. If command-line arguments are given, each argument is considered +its own message and will be inserted into the queue separately. + +The following command-line arguments are accepted: + +-n, \--nonblock +: Exit immediately with error if the message queue is empty. + Normally posix-mq.rb(1) will block until the queue is readable or + interrupted. This may not be used in conjunction with \--timeout . +-t, \--timeout SECONDS +: Timeout and exit with error after SECONDS if the message queue is empty. + This may not be used in conjunction with \--nonblock. +-p, \--priority +: Output the priority of the received message to stderr in the following + format: + + priority=3 + + The priority is an unsigned integer. + +# WAIT USAGE + +The *wait* command will cause posix-mq.rb(1) to sleep until a message is +available in the queue. Only one process may wait on an empty queue, +posix-mq.rb(1) will exit with an error if there is another waiting process. + +It takes no arguments and accepts the following options: + +-t, \--timeout SECONDS +: Timeout and exit with error after SECONDS if the message queue is empty. + +# UNLINK USAGE + +The *unlink* command prevents further opening and use of the current +queue. Existing processes with the queue open may continue to operate +on the queue indefinitely. If a new queue is created with the same +name, the created queue is a different queue from the unlinked queue. +See mq_unlink(3) for more information. + +# GENERAL OPTIONS +-q +: Do not show warning/error messages, suitable for scripting. + +\-h, \--help +: Show summary usage + +# ENVIRONMENT + +All commands rely on the MQUEUE environment variable. The value +of MQUEUE should always be prefixed with a slash ("/") for +portability. + +# DIAGNOSTICS + +Exit status is normally 0. Exit status is 2 if a timeout occurs, 1 for +all other errors. + +# SEE ALSO + +* [mq_overview(7)][1] + +[1]: http://kernel.org/doc/man-pages/online/pages/man7/mq_overview.7.html diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN new file mode 100755 index 0000000..4e7f0e3 --- /dev/null +++ b/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 +} diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..10e81e9 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,179 @@ +# use GNU Make to run tests in parallel, and without depending on RubyGems +all:: +RUBY = ruby +RAKE = rake +GIT_URL = git://git.bogomips.org/ruby_posix_mq.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 + +base_bins := posix-mq.rb +bins := $(addprefix bin/, $(base_bins)) +man1_bins := $(addsuffix .1, $(base_bins)) +man1_paths := $(addprefix man/man1/, $(man1_bins)) + +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/posix_mq clean + $(RM) $(setup_rb_files) ext/posix_mq/Makefile + +man: + $(MAKE) -C Documentation install-man + +pkg_extra := GIT-VERSION-FILE NEWS ChangeLog +manifest: $(pkg_extra) man + $(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://bogomips.org/ruby_posix_mq/NEWS.atom.xml +cgit_atom := http://git.bogomips.org/cgit/ruby_posix_mq.git/atom/?h=master +atom = + +# 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/ + $(MAKE) -C Documentation install-html install-man + install -m644 $(man1_paths) 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!("",%q{\&$(call atom,$(cgit_atom))})' \ + doc/ChangeLog.html + $(RUBY) -i -p -e \ + '$$_.gsub!("",%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 := qrp +rfpackage := posix_mq +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/posix_mq/posix_mq_ext.$(DLEXT) +ext/posix_mq/Makefile: ext/posix_mq/extconf.rb + cd $(@D) && $(RUBY) extconf.rb +$(ext): $(wildcard $(addprefix ext/posix_mq/,*.c *.h)) ext/posix_mq/Makefile + $(MAKE) -C $(@D) + +all:: test + +build: $(ext) +test: test-unit +test-unit: build + $(RUBY) -I lib:ext/posix_mq test/test_posix_mq.rb + +.PHONY: .FORCE-GIT-VERSION-FILE doc manifest man test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ae5ee8f --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +posix_mq 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 diff --git a/README b/README new file mode 100644 index 0000000..91279db --- /dev/null +++ b/README @@ -0,0 +1,70 @@ += posix_mq - POSIX Message Queues for Ruby + +POSIX message queues allow local processes to exchange data in the form +of messages. This API is distinct from that provided by System V +message queues, but provides similar functionality. + +POSIX message queues may be implemented in the kernel for fast, +low-latency communication between processes on the same machine. +POSIX message queues are not intended to replace userspace, +network-aware message queue implementations. + +== Features + +* Supports message notifications via signals. + +* Supports portable non-blocking operation. Under Linux 2.6.6+ only, + POSIX_MQ objects may even be used with event notification mechanisms + such as IO.select. + +* Optional timeouts may be applied to send and receive operations. + +* Thread-safe under Ruby 1.9, releases GVL before blocking operations. + +* Documented library API + +* Includes a generic "posix-mq.rb" command-line tool with manpage. + +== Install + +Operating system support (or library emulation) for POSIX message queues +is required. Most modern GNU/Linux distributions support this +out-of-the-box. + +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 plan on using the command-line client, a tarball installation +starts up faster and is recommended. Just grab the tarball from: + +http://bogomips.org/ruby_posix_mq/files/ +Unpack it, and run "ruby setup.rb" + +Otherwise, via RubyGems: gem install posix_mq + +== Development + +You can get the latest source via git from the following locations: + + git://git.bogomips.org/ruby_posix_mq.git + git://repo.or.cz/ruby_posix_mq.git (mirror) + +You may browse the code from the web and download the latest snapshot +tarballs here: + +* http://git.bogomips.org/cgit/ruby_posix_mq.git (cgit) +* http://repo.or.cz/w/ruby_posix_mq.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 posix_mq patches. + +== Contact + +All feedback (bug reports, user/development discussion, patches, pull +requests) go to the mailing list: mailto:ruby.posix.mq@librelist.com diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..97082f9 --- /dev/null +++ b/Rakefile @@ -0,0 +1,159 @@ +# -*- 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/ruby_posix_mq.git" +git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/ruby_posix_mq.git' +web_url = "http://bogomips.org/ruby_posix_mq/" + +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 "Ruby posix_mq news" + subtitle "POSIX Message Queues for Ruby" + 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 + require 'rubygems' + + spec = Gem::Specification.load('posix_mq.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('qrp', subject, body) +end + +desc "post to RAA" +task :raa_update do + require 'rubygems' + require 'net/http' + require 'net/netrc' + rc = Net::Netrc.locate('ruby_posix_mq-raa') or abort "~/.netrc not found" + password = rc.password + + s = Gem::Specification.load('posix_mq.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 => 'System', + :url => s.homepage, + :download => 'http://rubyforge.org/frs/?group_id=5626', + :license => 'LGPLv3', + :description_style => 'Plain', + :description => desc, + :pass => password, + :submit => 'Update', + } + res = Net::HTTP.post_form(uri, form) + p res + puts res.body +end diff --git a/bin/posix-mq.rb b/bin/posix-mq.rb new file mode 100755 index 0000000..fa44b94 --- /dev/null +++ b/bin/posix-mq.rb @@ -0,0 +1,138 @@ +#!/usr/bin/ruby +# -*- encoding: binary -*- +Encoding.default_external = Encoding::BINARY if defined?(Encoding) +$stderr.sync = $stdout.sync = true + +require 'posix_mq' +require 'optparse' + +commands = %w(create attr send receive wait unlink) +usage = "Usage: MQUEUE=/name #{File.basename($0)} COMMAND " \ + "[options] []\n" \ + "COMMAND may be one of: #{commands.join(', ')}" + +mqueue = ENV["MQUEUE"] or abort usage +command = ARGV.shift or abort usage +commands.include?(command) or abort usage + +priority = nil +timeout = nil +mode = 0666 +oflags = IO::RDONLY +mq_attr = nil +nonblock = false +command = command.to_sym + +ARGV.options do |x| + x.banner = usage.split(/\n/).first.gsub(/COMMAND/, command.to_s) + x.separator '' + + case command + when :create + oflags |= IO::CREAT + x.on('-x', '--exclusive', "exclusive create") { + oflags |= IO::EXCL + } + x.on('-m', '--mode=MODE', "octal file mode") { |i| + mode = i.to_i(8) + } + x.on('-c', '--maxmsg=COUNT', Integer, "maximum number of messages") { |i| + mq_attr ||= POSIX_MQ::Attr.new + mq_attr.maxmsg = i + } + x.on('-s', '--msgsize=BYTES', Integer, "maximum size of message") { |i| + mq_attr ||= POSIX_MQ::Attr.new + mq_attr.msgsize = i + } + when :wait + x.on('-t', '--timeout=SECONDS', Float, "timeout in seconds") { |f| + timeout = f + } + when :send, :receive + conflict = "timeout and nonblock are exclusive" + x.on('-t', '--timeout=SECONDS', Float, "timeout in seconds") { |f| + abort conflict if nonblock + timeout = f + } + x.on('-n', '--nonblock', "nonblocking operation") { + abort conflict if timeout + nonblock = true + oflags |= IO::NONBLOCK + } + if command == :send + oflags = IO::WRONLY + x.on('-p', '--priority=PRIO', Integer, "priority of message") { |i| + priority = i + } + else + x.on('-p', '--priority', "output priority of message to stderr") { + priority = $stderr + } + end + end + x.on('-q', "quiet warnings and errors") { $stderr.reopen("/dev/null", "w") } + x.on('-h', '--help', 'Show this help message.') { puts x; exit } + x.parse! +end + +trap(:INT) { exit 130 } + +unless command == :send || ARGV.empty? + abort "#{command} accepts no arguments" +end + +begin + if command == :create && mq_attr + mq_attr.flags = mq_attr.curmsgs = 0 + mq_attr.msgsize && ! mq_attr.maxmsg and + abort "--maxmsg must be set with --msgsize" + mq_attr.maxmsg && ! mq_attr.msgsize and + abort "--msgsize must be set with --maxmsg" + elsif command == :unlink + POSIX_MQ.unlink(mqueue) + exit + end + + mq = POSIX_MQ.open(mqueue, oflags, mode, mq_attr) + case command + when :create + exit + when :receive + buf, prio = mq.receive("", timeout) + $stderr.syswrite("priority=#{prio}\n") if priority + $stdout.syswrite(buf) + when :send + ARGV << $stdin.read if ARGV.empty? + ARGV.each { |msg| mq.send(msg, priority, timeout) } + when :attr + mq_attr = mq.attr + $stdout.syswrite( + "flags=#{mq_attr.flags}\n" \ + "maxmsg=#{mq_attr.maxmsg}\n" \ + "msgsize=#{mq_attr.msgsize}\n" \ + "curmsgs=#{mq_attr.curmsgs}\n") + when :wait + trap(:USR1) { exit } + + # we wouldn't get a notification if there were already messages + exit if mq.attr.curmsgs > 0 + mq.notify = :USR1 + exit if mq.attr.curmsgs > 0 # avoid race condition + + timeout.nil? ? sleep : sleep(timeout) + exit 2 # timed out + end +rescue Errno::EEXIST + abort "Queue exists" +rescue Errno::ENOENT + abort "Queue does not exist" +rescue Errno::EMSGSIZE + abort "Message too long" +rescue Errno::EAGAIN + abort(command == :send ? "Queue full" : "No messages available") +rescue Errno::ETIMEDOUT + warn "Operation timed out" + exit 2 +rescue => e + abort e.message +end diff --git a/ext/posix_mq/extconf.rb b/ext/posix_mq/extconf.rb new file mode 100644 index 0000000..b45cff6 --- /dev/null +++ b/ext/posix_mq/extconf.rb @@ -0,0 +1,9 @@ +require "mkmf" + +have_header("mqueue.h") or abort "mqueue.h header missing" +have_func("rb_str_set_len") +have_func("rb_struct_alloc_noinit") +have_func('rb_thread_blocking_region') +have_library("rt") +dir_config("posix_mq") +create_makefile("posix_mq_ext") diff --git a/ext/posix_mq/posix_mq.c b/ext/posix_mq/posix_mq.c new file mode 100644 index 0000000..e64cac3 --- /dev/null +++ b/ext/posix_mq/posix_mq.c @@ -0,0 +1,813 @@ +#define _XOPEN_SOURCE 600 +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +# define MQD_IS_FD 1 +# define MQ_IO_MARK(mq) rb_gc_mark((mq)->io) +# define MQ_IO_SET(mq,val) do { (mq)->io = (val); } while (0) +#else +# warning mqd_t is not select()-able on your OS +# define MQD_IS_FD 0 +# define MQ_IO_MARK(mq) ((void)(0)) +# define MQ_IO_SET(mq,val) ((void)(0)) +#endif /* non-Linux */ + +struct posix_mq { + mqd_t des; + long msgsize; + VALUE name; +#if MQD_IS_FD + VALUE io; +#endif +}; + +static VALUE cPOSIX_MQ, cAttr; +static ID id_new; +static ID sym_r, sym_w, sym_rw; +static const mqd_t MQD_INVALID = (mqd_t)-1; + +/* 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_STR_SET_LEN +# ifdef RUBINIUS +# define rb_str_set_len(str,len) rb_str_resize(str,len) +# else /* 1.8.6 optimized version */ +/* this is taken from Ruby 1.8.7, 1.8.6 may not have it */ +static void rb_18_str_set_len(VALUE str, long len) +{ + RSTRING(str)->len = len; + RSTRING(str)->ptr[len] = '\0'; + rb_str_flush(str); +} +# define rb_str_set_len(str,len) rb_18_str_set_len(str,len) +# endif /* ! RUBINIUS */ +#endif /* !defined(HAVE_RB_STR_SET_LEN) */ + +#ifndef HAVE_RB_STRUCT_ALLOC_NOINIT +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 +# 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; + + assert(RUBY_UBF_IO == ubf && "RUBY_UBF_IO required for emulation"); + + TRAP_BEG; + rv = func(data1); + TRAP_END; + + return rv; +} +#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */ + +/* used to pass arguments to mq_open inside blocking region */ +struct open_args { + int argc; + const char *name; + int oflags; + mode_t mode; + struct mq_attr attr; +}; + +/* used to pass arguments to mq_send/mq_receive inside blocking region */ +struct rw_args { + mqd_t des; + char *msg_ptr; + size_t msg_len; + unsigned msg_prio; + struct timespec *timeout; +}; + +/* hope it's there..., TODO: a better version that works in rbx */ +struct timeval rb_time_interval(VALUE); + +static struct timespec *convert_timeout(struct timespec *dest, VALUE time) +{ + struct timeval tv, now; + + if (NIL_P(time)) + return NULL; + + tv = rb_time_interval(time); /* aggregate return :( */ + gettimeofday(&now, NULL); + dest->tv_sec = now.tv_sec + tv.tv_sec; + dest->tv_nsec = (now.tv_usec + tv.tv_usec) * 1000; + + if (dest->tv_nsec > 1000000000) { + dest->tv_nsec -= 1000000000; + dest->tv_sec++; + } + + return dest; +} + +/* runs without GVL */ +static VALUE xopen(void *ptr) +{ + struct open_args *x = ptr; + mqd_t rv; + + switch (x->argc) { + case 2: rv = mq_open(x->name, x->oflags); break; + case 3: rv = mq_open(x->name, x->oflags, x->mode, NULL); break; + case 4: rv = mq_open(x->name, x->oflags, x->mode, &x->attr); break; + default: rv = MQD_INVALID; + } + + return (VALUE)rv; +} + +/* runs without GVL */ +static VALUE xsend(void *ptr) +{ + struct rw_args *x = ptr; + + if (x->timeout) + return (VALUE)mq_timedsend(x->des, x->msg_ptr, x->msg_len, + x->msg_prio, x->timeout); + + return (VALUE)mq_send(x->des, x->msg_ptr, x->msg_len, x->msg_prio); +} + +/* runs without GVL */ +static VALUE xrecv(void *ptr) +{ + struct rw_args *x = ptr; + + if (x->timeout) + return (VALUE)mq_timedreceive(x->des, x->msg_ptr, x->msg_len, + &x->msg_prio, x->timeout); + + return (VALUE)mq_receive(x->des, x->msg_ptr, x->msg_len, &x->msg_prio); +} + +/* runs without GVL, path resolution may be slow */ +static VALUE xunlink(void *ptr) +{ + VALUE name = (VALUE)ptr; + + return (VALUE)mq_unlink(RSTRING_PTR(name)); +} + +/* called by GC */ +static void mark(void *ptr) +{ + struct posix_mq *mq = ptr; + + rb_gc_mark(mq->name); + MQ_IO_MARK(mq); +} + +/* called by GC */ +static void _free(void *ptr) +{ + struct posix_mq *mq = ptr; + + if (mq->des != MQD_INVALID) { + /* we ignore errors when gc-ing */ + int saved_errno = errno; + + mq_close(mq->des); + errno = saved_errno; + mq->des = MQD_INVALID; + } +} + +/* automatically called at creation (before initialize) */ +static VALUE alloc(VALUE klass) +{ + struct posix_mq *mq; + VALUE rv = Data_Make_Struct(klass, struct posix_mq, mark, _free, mq); + + mq->des = MQD_INVALID; + mq->msgsize = -1; + mq->name = Qnil; + MQ_IO_SET(mq, Qnil); + + return rv; +} + +/* unwraps the posix_mq struct from self */ +static struct posix_mq *get(VALUE self, int need_valid) +{ + struct posix_mq *mq; + + Data_Get_Struct(self, struct posix_mq, mq); + + if (need_valid && mq->des == MQD_INVALID) + rb_raise(rb_eIOError, "closed queue descriptor"); + + return mq; +} + +/* converts the POSIX_MQ::Attr astruct into a struct mq_attr attr */ +static void attr_from_struct(struct mq_attr *attr, VALUE astruct, int all) +{ + VALUE *ptr; + + if (CLASS_OF(astruct) != cAttr) + rb_raise(rb_eArgError, "not a POSIX_MQ::Attr: %s", + RSTRING_PTR(rb_inspect(astruct))); + + ptr = RSTRUCT_PTR(astruct); + + attr->mq_flags = NUM2LONG(ptr[0]); + + if (all || !NIL_P(ptr[1])) + attr->mq_maxmsg = NUM2LONG(ptr[1]); + if (all || !NIL_P(ptr[2])) + attr->mq_msgsize = NUM2LONG(ptr[2]); + if (!NIL_P(ptr[3])) + attr->mq_curmsgs = NUM2LONG(ptr[3]); +} + +/* + * call-seq: + * POSIX_MQ.new(name [, flags [, mode [, mq_attr]]) => mq + * + * Opens a POSIX message queue given by +name+. +name+ should start + * with a slash ("/") for portable applications. + * + * If a Symbol is given in place of integer +flags+, then: + * + * * +:r+ is equivalent to IO::RDONLY + * * +:w+ is equivalent to IO::CREAT|IO::WRONLY + * * +:rw+ is equivalent to IO::CREAT|IO::RDWR + * + * +mode+ is an integer and only used when IO::CREAT is used. + * +mq_attr+ is a POSIX_MQ::Attr and only used if IO::CREAT is used. + * If +mq_attr+ is not specified when creating a queue, then the + * system defaults will be used. + * + * See the manpage for mq_open(3) for more details on this function. + */ +static VALUE init(int argc, VALUE *argv, VALUE self) +{ + struct posix_mq *mq = get(self, 0); + struct open_args x; + VALUE name, oflags, mode, attr; + + rb_scan_args(argc, argv, "13", &name, &oflags, &mode, &attr); + + if (TYPE(name) != T_STRING) + rb_raise(rb_eArgError, "name must be a string"); + + switch (TYPE(oflags)) { + case T_NIL: + x.oflags = O_RDONLY; + break; + case T_SYMBOL: + if (oflags == sym_r) + x.oflags = O_RDONLY; + else if (oflags == sym_w) + x.oflags = O_CREAT|O_WRONLY; + else if (oflags == sym_rw) + x.oflags = O_CREAT|O_RDWR; + else + rb_raise(rb_eArgError, + "symbol must be :r, :w, or :rw: %s", + RSTRING_PTR(rb_inspect(oflags))); + break; + case T_BIGNUM: + case T_FIXNUM: + x.oflags = NUM2INT(oflags); + break; + default: + rb_raise(rb_eArgError, "flags must be an int, :r, :w, or :wr"); + } + + x.name = RSTRING_PTR(name); + x.argc = 2; + + switch (TYPE(mode)) { + case T_FIXNUM: + x.argc = 3; + x.mode = NUM2INT(mode); + break; + case T_NIL: + if (x.oflags & O_CREAT) { + x.argc = 3; + x.mode = 0666; + } + break; + default: + rb_raise(rb_eArgError, "mode not an integer"); + } + + switch (TYPE(attr)) { + case T_STRUCT: + x.argc = 4; + attr_from_struct(&x.attr, attr, 1); + + /* principle of least surprise */ + if (x.attr.mq_flags & O_NONBLOCK) + x.oflags |= O_NONBLOCK; + break; + case T_NIL: + break; + default: + rb_raise(rb_eArgError, "attr must be a POSIX_MQ::Attr: %s", + RSTRING_PTR(rb_inspect(attr))); + } + + mq->des = (mqd_t)rb_thread_blocking_region(xopen, &x, RUBY_UBF_IO, 0); + if (mq->des == MQD_INVALID) + rb_sys_fail("mq_open"); + + mq->name = rb_str_dup(name); + + return self; +} + +/* + * call-seq: + * POSIX_MQ.unlink(name) => 1 + * + * Unlinks the message queue given by +name+. The queue will be destroyed + * when the last process with the queue open closes its queue descriptors. + */ +static VALUE s_unlink(VALUE self, VALUE name) +{ + mqd_t rv; + void *ptr = (void *)name; + + if (TYPE(name) != T_STRING) + rb_raise(rb_eArgError, "argument must be a string"); + + rv = (mqd_t)rb_thread_blocking_region(xunlink, ptr, RUBY_UBF_IO, 0); + if (rv == MQD_INVALID) + rb_sys_fail("mq_unlink"); + + return INT2NUM(1); +} + +/* + * call-seq: + * mq.unlink => mq + * + * Unlinks the message queue to prevent other processes from accessing it. + * All existing queue descriptors to this queue including those opened by + * other processes are unaffected. The queue will only be destroyed + * when the last process with open descriptors to this queue closes + * the descriptors. + */ +static VALUE _unlink(VALUE self) +{ + struct posix_mq *mq = get(self, 0); + mqd_t rv; + void *ptr = (void *)mq->name; + + assert(TYPE(mq->name) == T_STRING && "mq->name is not a string"); + + rv = (mqd_t)rb_thread_blocking_region(xunlink, ptr, RUBY_UBF_IO, 0); + if (rv == MQD_INVALID) + rb_sys_fail("mq_unlink"); + + return self; +} + +static void setup_send_buffer(struct rw_args *x, VALUE buffer) +{ + buffer = rb_obj_as_string(buffer); + x->msg_ptr = RSTRING_PTR(buffer); + x->msg_len = (size_t)RSTRING_LEN(buffer); +} + +/* + * call-seq: + * mq.send(string [,priority[, timeout]]) => nil + * + * Inserts the given +string+ into the message queue with an optional, + * unsigned integer +priority+. If the optional +timeout+ is specified, + * then Errno::ETIMEDOUT will be raised if the operation cannot complete + * before +timeout+ seconds has elapsed. Without +timeout+, this method + * may block until the queue is writable. + */ +static VALUE _send(int argc, VALUE *argv, VALUE self) +{ + struct posix_mq *mq = get(self, 1); + struct rw_args x; + VALUE buffer, prio, timeout; + mqd_t rv; + struct timespec expire; + + rb_scan_args(argc, argv, "12", &buffer, &prio, &timeout); + + setup_send_buffer(&x, buffer); + x.des = mq->des; + x.timeout = convert_timeout(&expire, timeout); + x.msg_prio = NIL_P(prio) ? 0 : NUM2UINT(prio); + + rv = (mqd_t)rb_thread_blocking_region(xsend, &x, RUBY_UBF_IO, 0); + if (rv == MQD_INVALID) + rb_sys_fail("mq_send"); + + return Qnil; +} + +/* + * call-seq: + * mq << string => mq + * + * Inserts the given +string+ into the message queue with a + * default priority of 0 and no timeout. + */ +static VALUE send0(VALUE self, VALUE buffer) +{ + struct posix_mq *mq = get(self, 1); + struct rw_args x; + mqd_t rv; + + setup_send_buffer(&x, buffer); + x.des = mq->des; + x.timeout = NULL; + x.msg_prio = 0; + + rv = (mqd_t)rb_thread_blocking_region(xsend, &x, RUBY_UBF_IO, 0); + if (rv == MQD_INVALID) + rb_sys_fail("mq_send"); + + return self; +} + +#if MQD_IS_FD +/* + * call-seq: + * mq.to_io => IO + * + * Returns an IO.select-able +IO+ object. This method is only available + * under Linux and is not intended to be portable. + */ +static VALUE to_io(VALUE self) +{ + struct posix_mq *mq = get(self, 1); + + if (NIL_P(mq->io)) + mq->io = rb_funcall(rb_cIO, id_new, 1, INT2NUM(mq->des)); + + return mq->io; +} +#endif + +static void get_msgsize(struct posix_mq *mq) +{ + struct mq_attr attr; + + if (mq_getattr(mq->des, &attr) == MQD_INVALID) + rb_sys_fail("mq_getattr"); + + mq->msgsize = attr.mq_msgsize; +} + +/* + * call-seq: + * mq.receive([buffer, [timeout]]) => [ message, priority ] + * + * Takes the highest priority message off the queue and returns + * an array containing the message as a String and the Integer + * priority of the message. + * + * If the optional +buffer+ is present, then it must be a String + * which will receive the data. + * + * If the optional +timeout+ is present, then it may be a Float + * or Integer specifying the timeout in seconds. Errno::ETIMEDOUT + * will be raised if +timeout+ has elapsed and there are no messages + * in the queue. + */ +static VALUE receive(int argc, VALUE *argv, VALUE self) +{ + struct posix_mq *mq = get(self, 1); + struct rw_args x; + VALUE buffer, timeout; + ssize_t r; + struct timespec expire; + + if (mq->msgsize < 0) + get_msgsize(mq); + + rb_scan_args(argc, argv, "02", &buffer, &timeout); + x.timeout = convert_timeout(&expire, timeout); + + if (NIL_P(buffer)) { + buffer = rb_str_new(0, mq->msgsize); + } else { + StringValue(buffer); + rb_str_modify(buffer); + rb_str_resize(buffer, mq->msgsize); + } + OBJ_TAINT(buffer); + x.msg_ptr = RSTRING_PTR(buffer); + x.msg_len = (size_t)mq->msgsize; + x.des = mq->des; + + r = (ssize_t)rb_thread_blocking_region(xrecv, &x, RUBY_UBF_IO, 0); + if (r < 0) + rb_sys_fail("mq_receive"); + + rb_str_set_len(buffer, r); + + return rb_ary_new3(2, buffer, UINT2NUM(x.msg_prio)); +} + +/* + * call-seq: + * mq.attr => mq_attr + * + * Returns a POSIX_MQ::Attr struct containing the attributes + * of the message queue. See the mq_getattr(3) manpage for + * more details. + */ +static VALUE getattr(VALUE self) +{ + struct posix_mq *mq = get(self, 1); + struct mq_attr attr; + VALUE astruct; + VALUE *ptr; + + if (mq_getattr(mq->des, &attr) == MQD_INVALID) + rb_sys_fail("mq_getattr"); + + astruct = rb_struct_alloc_noinit(cAttr); + ptr = RSTRUCT_PTR(astruct); + ptr[0] = LONG2NUM(attr.mq_flags); + ptr[1] = LONG2NUM(attr.mq_maxmsg); + ptr[2] = LONG2NUM(attr.mq_msgsize); + ptr[3] = LONG2NUM(attr.mq_curmsgs); + + return astruct; +} + +/* + * call-seq: + * mq.attr = POSIX_MQ::Attr(IO::NONBLOCK) => mq_attr + * + * Only the IO::NONBLOCK flag may be set or unset (zero) in this manner. + * See the mq_setattr(3) manpage for more details. + * + * Consider using the POSIX_MQ#nonblock= method as it is easier and + * more natural to use. + */ +static VALUE setattr(VALUE self, VALUE astruct) +{ + struct posix_mq *mq = get(self, 1); + struct mq_attr newattr; + + attr_from_struct(&newattr, astruct, 0); + + if (mq_setattr(mq->des, &newattr, NULL) == MQD_INVALID) + rb_sys_fail("mq_setattr"); + + return astruct; +} + +/* + * call-seq: + * mq.close => nil + * + * Closes the underlying message queue descriptor. + * If this descriptor had a registered notification request, the request + * will be removed so another descriptor or process may register a + * notification request. Message queue descriptors are automatically + * closed by garbage collection. + */ +static VALUE _close(VALUE self) +{ + struct posix_mq *mq = get(self, 1); + + if (mq_close(mq->des) == MQD_INVALID) + rb_sys_fail("mq_close"); + + mq->des = MQD_INVALID; + MQ_IO_SET(mq, Qnil); + + return Qnil; +} + +/* + * call-seq: + * mq.closed? => true or false + * + * Returns +true+ if the message queue descriptor is closed and therefore + * unusable, otherwise +false+ + */ +static VALUE closed(VALUE self) +{ + struct posix_mq *mq = get(self, 0); + + return mq->des == MQD_INVALID ? Qtrue : Qfalse; +} + +/* + * call-seq: + * mq.name => string + * + * Returns the string name of message queue associated with +mq+ + */ +static VALUE name(VALUE self) +{ + struct posix_mq *mq = get(self, 0); + + return mq->name; +} + +static int lookup_sig(VALUE sig) +{ + static VALUE list; + const char *ptr; + long len; + + sig = rb_obj_as_string(sig); + len = RSTRING_LEN(sig); + ptr = RSTRING_PTR(sig); + + if (len > 3 && !memcmp("SIG", ptr, 3)) + sig = rb_str_new(ptr + 3, len - 3); + + if (!list) { + VALUE mSignal = rb_define_module("Signal"""); /* avoid RDoc */ + + list = rb_funcall(mSignal, rb_intern("list"), 0, 0); + rb_global_variable(&list); + } + + sig = rb_hash_aref(list, sig); + if (NIL_P(sig)) + rb_raise(rb_eArgError, "invalid signal: %s\n", + RSTRING_PTR(rb_inspect(sig))); + + return NUM2INT(sig); +} + +/* + * call-seq: + * mq.notify = signal => signal + * + * Registers the notification request to deliver a given +signal+ + * to the current process when message is received. + * If +signal+ is +nil+, it will unregister and disable the notification + * request to allow other processes to register a request. + * Only one process may have a notification request for a queue + * at a time, Errno::EBUSY will be raised if there is already + * a notification request registration for the queue. + */ +static VALUE setnotify(VALUE self, VALUE arg) +{ + struct posix_mq *mq = get(self, 1); + struct sigevent not; + VALUE rv = arg; + + not.sigev_notify = SIGEV_SIGNAL; + + switch (TYPE(arg)) { + case T_FIXNUM: + not.sigev_signo = NUM2INT(arg); + break; + case T_SYMBOL: + case T_STRING: + not.sigev_signo = lookup_sig(arg); + rv = INT2NUM(not.sigev_signo); + break; + case T_NIL: + not.sigev_notify = SIGEV_NONE; + break; + default: + /* maybe support Proc+thread via sigev_notify_function.. */ + rb_raise(rb_eArgError, "must be a signal or nil"); + } + + if (mq_notify(mq->des, ¬) == MQD_INVALID) + rb_sys_fail("mq_notify"); + + return rv; +} + +/* + * call-seq: + * mq.nonblock? => true or false + * + * Returns the current non-blocking state of the message queue descriptor. + */ +static VALUE getnonblock(VALUE self) +{ + struct mq_attr attr; + struct posix_mq *mq = get(self, 1); + + if (mq_getattr(mq->des, &attr) == MQD_INVALID) + rb_sys_fail("mq_getattr"); + + mq->msgsize = attr.mq_msgsize; /* optimization */ + + return attr.mq_flags & O_NONBLOCK ? Qtrue : Qfalse; +} + +/* + * call-seq: + * mq.nonblock = boolean => boolean + * + * Enables or disables non-blocking operation for the message queue + * descriptor. Errno::EAGAIN will be raised in situations where + * the queue would block. This is not compatible with +timeout+ + * arguments to POSIX_MQ#send and POSIX_MQ#receive. + */ +static VALUE setnonblock(VALUE self, VALUE nb) +{ + struct mq_attr newattr, oldattr; + struct posix_mq *mq = get(self, 1); + + if (nb == Qtrue) + newattr.mq_flags = O_NONBLOCK; + else if (nb == Qfalse) + newattr.mq_flags = 0; + else + rb_raise(rb_eArgError, "must be true or false"); + + if (mq_setattr(mq->des, &newattr, &oldattr) == MQD_INVALID) + rb_sys_fail("mq_setattr"); + + mq->msgsize = oldattr.mq_msgsize; /* optimization */ + + return nb; +} + +void Init_posix_mq_ext(void) +{ + cPOSIX_MQ = rb_define_class("POSIX_MQ", rb_cObject); + rb_define_alloc_func(cPOSIX_MQ, alloc); + cAttr = rb_const_get(cPOSIX_MQ, rb_intern("Attr")); + + /* + * The maximum number of open message descriptors supported + * by the system. This may be -1, in which case it is dynamically + * set at runtime. Consult your operating system documentation + * for system-specific information about this. + */ + rb_define_const(cPOSIX_MQ, "OPEN_MAX", + LONG2NUM(sysconf(_SC_MQ_OPEN_MAX))); + + /* + * The maximum priority that may be specified for POSIX_MQ#send + * On POSIX-compliant systems, this is at least 31, but some + * systems allow higher limits. + * The minimum priority is always zero. + */ + rb_define_const(cPOSIX_MQ, "PRIO_MAX", + LONG2NUM(sysconf(_SC_MQ_PRIO_MAX))); + + rb_define_singleton_method(cPOSIX_MQ, "unlink", s_unlink, 1); + + rb_define_method(cPOSIX_MQ, "initialize", init, -1); + rb_define_method(cPOSIX_MQ, "send", _send, -1); + rb_define_method(cPOSIX_MQ, "<<", send0, 1); + rb_define_method(cPOSIX_MQ, "receive", receive, -1); + rb_define_method(cPOSIX_MQ, "attr", getattr, 0); + rb_define_method(cPOSIX_MQ, "attr=", setattr, 1); + rb_define_method(cPOSIX_MQ, "close", _close, 0); + rb_define_method(cPOSIX_MQ, "closed?", closed, 0); + rb_define_method(cPOSIX_MQ, "unlink", _unlink, 0); + rb_define_method(cPOSIX_MQ, "name", name, 0); + rb_define_method(cPOSIX_MQ, "notify=", setnotify, 1); + rb_define_method(cPOSIX_MQ, "nonblock=", setnonblock, 1); + rb_define_method(cPOSIX_MQ, "nonblock?", getnonblock, 0); +#if MQD_IS_FD + rb_define_method(cPOSIX_MQ, "to_io", to_io, 0); +#endif + + id_new = rb_intern("new"); + sym_r = ID2SYM(rb_intern("r")); + sym_w = ID2SYM(rb_intern("w")); + sym_rw = ID2SYM(rb_intern("rw")); +} diff --git a/lib/posix_mq.rb b/lib/posix_mq.rb new file mode 100644 index 0000000..a2a85ee --- /dev/null +++ b/lib/posix_mq.rb @@ -0,0 +1,34 @@ +# -*- encoding: binary -*- +class POSIX_MQ + + # version of POSIX_MQ, currently 0.1.0 + VERSION = '0.1.0' + + # An analogous Struct to "struct mq_attr" in C. + # This may be used in arguments for POSIX_MQ.new and + # POSIX_MQ#attr=. POSIX_MQ#attr returns an instance + # of this class. + # + # See the mq_getattr(3) manpage for more information on the values. + Attr = Struct.new(:flags, :maxmsg, :msgsize, :curmsgs) + + class << self + + # Opens a POSIX message queue and performs operations on the + # given block, closing the message queue at exit. + # All all arguments are passed to POSIX_MQ.new. + def open(*args) + mq = new(*args) + block_given? or return mq + begin + yield mq + ensure + mq.close unless mq.closed? + end + end + + end + +end + +require 'posix_mq_ext' diff --git a/local.mk.sample b/local.mk.sample new file mode 100644 index 0000000..4ce6cf8 --- /dev/null +++ b/local.mk.sample @@ -0,0 +1,70 @@ +# this is a sample local.mk file, feel free to modify it for your needs +# GNUmakefile will source local.mk in the top-level source tree +# if it is present. +# +# This is depends on a bunch of GNU-isms from bash, touch. + +RSYNC = rsync +DLEXT := so +gems := + +# Avoid loading rubygems to speed up tests because gmake is +# fork+exec heavy with Ruby. +prefix = $(HOME) +ifeq ($(r19),) + RUBY := $(prefix)/bin/ruby + gem_paths := $(addprefix $(prefix)/lib/ruby/gems/1.8/gems/,$(gems)) +else + prefix := $(prefix)/ruby-1.9 + export PATH := $(prefix)/bin:$(PATH) + RUBY := $(prefix)/bin/ruby --disable-gems + gem_paths := $(addprefix $(prefix)/lib/ruby/gems/1.9.1/gems/,$(gems)) +endif + +ifdef gem_paths + sp := + sp += + export RUBYLIB := $(subst $(sp),:,$(addsuffix /lib,$(gem_paths))) +endif + +# pipefail is THE reason to use bash (v3+) or never revisions of ksh93 +# SHELL := /bin/bash -e -o pipefail +SHELL := /bin/ksh93 -e -o pipefail + +# trace execution of tests +# TRACER = strace -f -o $(t_pfx).strace -s 100000 +TRACER = /usr/bin/time -v -o $(t_pfx).time + +full-test: test-18 test-19 +test-18: + $(MAKE) test 2>&1 | sed -e 's!^!1.8 !' +test-19: + $(MAKE) test r19=t 2>&1 | sed -e 's!^!1.9 !' + +latest: NEWS + @awk 'BEGIN{RS="=== ";ORS=""}NR==2{sub(/\n$$/,"");print RS""$$0 }' $< + +# publishes docs to http://bogomips.org/ruby_posix_mq/ +publish_doc: + -git set-file-times + $(RM) -r doc ChangeLog NEWS + $(MAKE) doc LOG_VERSION=$(shell git tag -l | tail -1) + $(MAKE) -s latest > doc/LATEST + find doc/images doc/js -type f | \ + TZ=UTC xargs touch -d '1970-01-01 00:00:00' doc/rdoc.css + $(MAKE) doc_gz + chmod 644 $$(find doc -type f) + $(RSYNC) -av doc/ dcvr:/srv/bogomips/ruby_posix_mq/ + git ls-files | xargs touch + +# Create gzip variants of the same timestamp as the original so nginx +# "gzip_static on" can serve the gzipped versions directly. +doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.\(gif\|jpg\|png\|gz\)$$') +doc_gz: + touch doc/NEWS.atom.xml -d "$$(awk 'NR==1{print $$4,$$5,$$6}' NEWS)" + for i in $(docs); do \ + gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done + +# launches any of the following shells with RUBYLIB set +irb sh bash ksh: + $@ diff --git a/posix_mq.gemspec b/posix_mq.gemspec new file mode 100644 index 0000000..73c0a53 --- /dev/null +++ b/posix_mq.gemspec @@ -0,0 +1,39 @@ +# -*- 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{posix_mq} + s.version = ENV["VERSION"] + + s.authors = ["Ruby POSIX MQ hackers"] + s.date = Time.now.utc.strftime('%Y-%m-%d') + s.description = File.read("README").split(/\n\n/)[1] + s.email = %q{ruby.posix.mq@librelist.com} + s.executables = %w(posix-mq.rb) + s.extensions = %w(ext/posix_mq/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://bogomips.org/ruby_posix_mq/} + s.summary = %q{POSIX Message Queues for Ruby} + s.rdoc_options = [ "-Na", "-t", "posix_mq - #{s.summary}" ] + s.require_paths = %w(lib) + s.rubyforge_project = %q{qrp} + + s.test_files = test_files + + # s.licenses = %w(LGPLv3) # accessor not compatible with older RubyGems +end diff --git a/setup.rb b/setup.rb new file mode 100644 index 0000000..5eb5006 --- /dev/null +++ b/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 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/test/test_posix_mq.rb b/test/test_posix_mq.rb new file mode 100644 index 0000000..00b0f5a --- /dev/null +++ b/test/test_posix_mq.rb @@ -0,0 +1,225 @@ +# -*- encoding: binary -*- +require 'test/unit' +require 'posix_mq' +require 'fcntl' +$stderr.sync = $stdout.sync = true + +class Test_POSIX_MQ < Test::Unit::TestCase + + HAVE_TO_IO = if POSIX_MQ.instance_methods.grep(/\Ato_io\z/).empty? + warn "POSIX_MQ#to_io not supported on this platform: #{RUBY_PLATFORM}" + false + else + true + end + + def setup + @mq = nil + @path = "/posix_mq.rb.#{Time.now.to_i}.#$$.#{rand}" + end + + def teardown + @mq or return + assert_equal @mq, @mq.unlink + assert ! @mq.closed? + assert_nil @mq.close + assert @mq.closed? + end + + def test_timed_receive + interval = 0.01 + @mq = POSIX_MQ.new(@path, :rw) + assert ! @mq.nonblock? + t0 = Time.now + assert_raises(Errno::ETIMEDOUT) { @mq.receive "", interval } + elapsed = Time.now - t0 + assert elapsed > interval + end + + def test_timed_send + interval = 0.01 + @mq = POSIX_MQ.new(@path, :rw, 0666, POSIX_MQ::Attr[0, 1, 1, 0]) + assert ! @mq.nonblock? + assert_nothing_raised { @mq.send "A", 1, interval } + t0 = Time.now + assert_raises(Errno::ETIMEDOUT) { @mq.send "B", 1, interval } + elapsed = Time.now - t0 + assert elapsed > interval + end + + def test_open + POSIX_MQ.open(@path, IO::CREAT|IO::WRONLY, 0666) do |mq| + @mq = mq + assert mq.kind_of?(POSIX_MQ) + assert_equal @path, mq.name + assert_nil mq.send("HI", 0) + assert_equal 1, mq.attr.curmsgs + assert_nil mq.close + assert_raises(IOError) { mq.close } + end + assert @mq.closed? + @mq = nil + POSIX_MQ.unlink(@path) + end + + def test_name + path = "" << @path.dup + path.freeze + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + assert_equal path, @mq.name + end + + def test_new_readonly + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + rd = POSIX_MQ.new @path, IO::RDONLY + assert_equal @mq.name, rd.name + assert_nil rd.close + end + + def test_send0_receive + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert_equal(@mq, @mq << "hello") + assert_equal [ "hello", 0 ], @mq.receive + end + + def test_send0_chain + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + @mq << "hello" << "world" + assert_equal [ "hello", 0 ], @mq.receive + assert_equal [ "world", 0 ], @mq.receive + end + + def test_send_receive + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert_nil @mq.send("hello", 0) + assert_equal [ "hello", 0 ], @mq.receive + end + + def test_send_receive_buf + buf = "" + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert_nil @mq.send("hello", 0) + assert_equal [ "hello", 0 ], @mq.receive(buf) + assert_equal "hello", buf + end + + def test_send_receive_prio + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert_nil @mq.send("hello", 2) + assert_equal [ "hello", 2 ], @mq.receive + end + + def test_getattr + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + mq_attr = @mq.attr + assert_equal POSIX_MQ::Attr, mq_attr.class + assert mq_attr.flags.kind_of?(Integer) + assert mq_attr.maxmsg.kind_of?(Integer) + assert mq_attr.msgsize.kind_of?(Integer) + assert mq_attr.curmsgs.kind_of?(Integer) + end + + def test_to_io + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert @mq.to_io.kind_of?(IO) + assert_nothing_raised { IO.select([@mq], nil, nil, 0) } + end if HAVE_TO_IO + + def test_notify + rd, wr = IO.pipe + orig = trap(:USR1) { wr.syswrite('.') } + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666 + assert_nothing_raised { @mq.notify = :SIGUSR1 } + assert_nothing_raised { @mq.send("hello", 0) } + assert_equal [[rd], [], []], IO.select([rd], nil, nil, 10) + assert_equal '.', rd.sysread(1) + assert_nil(@mq.notify = nil) + assert_nothing_raised { @mq.send("hello", 0) } + assert_nil IO.select([rd], nil, nil, 0.1) + assert_raises(Errno::EBUSY) { @mq.notify = :USR1 } + ensure + trap(:USR1, orig) + end + + def test_setattr + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + mq_attr = POSIX_MQ::Attr.new(IO::NONBLOCK) + @mq.attr = mq_attr + assert_equal IO::NONBLOCK, @mq.attr.flags + assert mq_attr.flags.kind_of?(Integer) + + mq_attr.flags = 0 + @mq.attr = mq_attr + assert_equal 0, @mq.attr.flags + end + + def test_new_nonblocking + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY|IO::NONBLOCK, 0666 + assert @mq.nonblock? + end + + def test_new_blocking + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + assert ! @mq.nonblock? + end + + def test_nonblock_toggle + @mq = POSIX_MQ.new @path, IO::CREAT|IO::WRONLY, 0666 + assert ! @mq.nonblock? + @mq.nonblock = true + assert @mq.nonblock? + @mq.nonblock = false + assert ! @mq.nonblock? + assert_raises(ArgumentError) { @mq.nonblock = nil } + end + + def test_new_sym_w + @mq = POSIX_MQ.new @path, :w + assert_equal IO::WRONLY, @mq.to_io.fcntl(Fcntl::F_GETFL) + end if HAVE_TO_IO + + def test_new_sym_r + @mq = POSIX_MQ.new @path, :w + mq = nil + assert_nothing_raised { mq = POSIX_MQ.new @path, :r } + assert_equal IO::RDONLY, mq.to_io.fcntl(Fcntl::F_GETFL) + assert_nil mq.close + end if HAVE_TO_IO + + def test_new_path_only + @mq = POSIX_MQ.new @path, :w + mq = nil + assert_nothing_raised { mq = POSIX_MQ.new @path } + assert_equal IO::RDONLY, mq.to_io.fcntl(Fcntl::F_GETFL) + assert_nil mq.close + end if HAVE_TO_IO + + def test_new_sym_wr + @mq = POSIX_MQ.new @path, :rw + assert_equal IO::RDWR, @mq.to_io.fcntl(Fcntl::F_GETFL) + end if HAVE_TO_IO + + def test_new_attr + mq_attr = POSIX_MQ::Attr.new(IO::NONBLOCK, 1, 1, 0) + @mq = POSIX_MQ.new @path, IO::CREAT|IO::RDWR, 0666, mq_attr + assert @mq.nonblock? + assert_equal mq_attr, @mq.attr + + assert_raises(Errno::EAGAIN) { @mq.receive } + assert_raises(Errno::EMSGSIZE) { @mq << '..' } + assert_nothing_raised { @mq << '.' } + assert_equal [ '.', 0 ], @mq.receive + assert_nothing_raised { @mq << '.' } + assert_raises(Errno::EAGAIN) { @mq << '.' } + end + + def test_prio_max + min_posix_mq_prio_max = 31 # defined by POSIX + assert POSIX_MQ::PRIO_MAX >= min_posix_mq_prio_max + end + + def test_open_max + assert POSIX_MQ::OPEN_MAX.kind_of?(Integer) + end + +end -- 2.11.4.GIT