Initial checkin
authorMichael Granger <ged@FaerieMUD.org>
Fri, 15 Aug 2008 15:43:38 +0000
changeset 0 92d00ff32c56
child 1 09d0d209d06d
Initial checkin
LICENSE
Rakefile
Rakefile.local
docs/example.rb
docs/jail.h
docs/jexec.c
docs/jls.c
examples/jexec.rb
ext/bsdjail.c
lib/jparallel.rb
misc/monkeypatches.rb
project.yml
utils.rb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,27 @@
+Copyright (c) 2008, Michae Granger and Mahlon Smith
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+    this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+
+  * Neither the name of the author/s, nor the names of the project's
+    contributors may be used to endorse or promote products derived from this
+    software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Rakefile	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,249 @@
+#!rake
+#
+# jparallel rakefile
+#
+# Based on various other Rakefiles, especially one by Ben Bleything
+#
+# Copyright (c) 2008 The FaerieMUD Consortium
+#
+# Authors:
+#  * Michae Granger and Mahlon Smith <ged@FaerieMUD.org, mahlon@martini.nu>
+#
+
+BEGIN {
+	require 'pathname'
+	basedir = Pathname.new( __FILE__ ).dirname
+
+	libdir = basedir + "lib"
+	extdir = basedir + "ext"
+
+	$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
+	$LOAD_PATH.unshift( extdir.to_s ) unless $LOAD_PATH.include?( extdir.to_s )
+}
+
+
+require 'rbconfig'
+require 'rubygems'
+require 'rake'
+require 'rake/rdoctask'
+require 'rake/testtask'
+require 'rake/packagetask'
+require 'rake/clean'
+
+$dryrun = false
+
+### Config constants
+BASEDIR       = Pathname.new( __FILE__ ).dirname.relative_path_from( Pathname.getwd )
+LIBDIR        = BASEDIR + 'lib'
+EXTDIR        = BASEDIR + 'ext'
+DOCSDIR       = BASEDIR + 'docs'
+PKGDIR        = BASEDIR + 'pkg'
+
+PKG_NAME      = 'jparallel'
+PKG_SUMMARY   = 'A "parallel jail shell" written in Ruby'
+VERSION_FILE  = LIBDIR + 'jparallel.rb'
+PKG_VERSION   = VERSION_FILE.read[ /VERSION = '(\d+\.\d+\.\d+)'/, 1 ]
+PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
+GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
+
+ARTIFACTS_DIR = Pathname.new( ENV['CC_BUILD_ARTIFACTS'] || 'artifacts' )
+
+TEXT_FILES    = %w( Rakefile ChangeLog README LICENSE ).collect {|filename| BASEDIR + filename }
+LIB_FILES     = Pathname.glob( LIBDIR + '**/*.rb' ).delete_if {|item| item =~ /\.svn/ }
+EXT_FILES     = Pathname.glob( EXTDIR + '**/*.{c,h,rb}' ).delete_if {|item| item =~ /\.svn/ }
+
+SPECDIR       = BASEDIR + 'spec'
+SPEC_FILES    = Pathname.glob( SPECDIR + '**/*_spec.rb' ).delete_if {|item| item =~ /\.svn/ }
+
+TESTDIR       = BASEDIR + 'tests'
+TEST_FILES    = Pathname.glob( TESTDIR + '**/*.tests.rb' ).delete_if {|item| item =~ /\.svn/ }
+
+RAKE_TASKDIR  = BASEDIR + 'rake'
+RAKE_TASKLIBS = Pathname.glob( RAKE_TASKDIR + '*.rb' )
+
+LOCAL_RAKEFILE = BASEDIR + 'Rakefile.local'
+
+EXTRA_PKGFILES = []
+EXTRA_PKGFILES += Pathname.glob( BASEDIR + 'examples/*.{c,rb}' ).delete_if {|item| item =~ /\.svn/ } 
+
+RELEASE_FILES = TEXT_FILES + 
+	SPEC_FILES + 
+	TEST_FILES + 
+	LIB_FILES + 
+	EXT_FILES + 
+	RAKE_TASKLIBS +
+	EXTRA_PKGFILES
+
+RELEASE_FILES << LOCAL_RAKEFILE if LOCAL_RAKEFILE.exist?
+
+COVERAGE_MINIMUM = ENV['COVERAGE_MINIMUM'] ? Float( ENV['COVERAGE_MINIMUM'] ) : 85.0
+RCOV_EXCLUDES = 'spec,tests,/Library/Ruby,/var/lib,/usr/local/lib'
+RCOV_OPTS = [
+	'--exclude', RCOV_EXCLUDES,
+	'--xrefs',
+	'--save',
+	'--callsites',
+	#'--aggregate', 'coverage.data' # <- doesn't work as of 0.8.1.2.0
+  ]
+
+
+# Subversion constants -- directory names for releases and tags
+SVN_TRUNK_DIR    = 'trunk'
+SVN_RELEASES_DIR = 'releases'
+SVN_BRANCHES_DIR = 'branches'
+SVN_TAGS_DIR     = 'tags'
+
+SVN_DOTDIR       = BASEDIR + '.svn'
+SVN_ENTRIES      = SVN_DOTDIR + 'entries'
+
+
+### Load some task libraries that need to be loaded early
+require RAKE_TASKDIR + 'helpers.rb'
+require RAKE_TASKDIR + 'svn.rb'
+require RAKE_TASKDIR + 'verifytask.rb'
+
+# Define some constants that depend on the 'svn' tasklib
+PKG_BUILD = get_svn_rev( BASEDIR ) || 0
+SNAPSHOT_PKG_NAME = "#{PKG_FILE_NAME}.#{PKG_BUILD}"
+SNAPSHOT_GEM_NAME = "#{SNAPSHOT_PKG_NAME}.gem"
+
+# Documentation constants
+RDOC_OPTIONS = [
+	'-w', '4',
+	'-SHN',
+	'-i', '.',
+	'-m', 'README',
+	'-W', 'http://deveiate.org/projects/Jparallel//browser/trunk/'
+  ]
+
+# Release constants
+SMTP_HOST = 'mail.faeriemud.org'
+SMTP_PORT = 465 # SMTP + SSL
+
+# Project constants
+PROJECT_HOST = 'deveiate.org'
+PROJECT_PUBDIR = "/usr/local/www/public/code"
+PROJECT_DOCDIR = "#{PROJECT_PUBDIR}/#{PKG_NAME}"
+PROJECT_SCPURL = "#{PROJECT_HOST}:#{PROJECT_DOCDIR}"
+
+# Rubyforge stuff
+RUBYFORGE_GROUP = 'deveiate'
+RUBYFORGE_PROJECT = 'jparallel'
+
+# Gem dependencies: gemname => version
+DEPENDENCIES = {
+}
+
+# Non-gem requirements: packagename => version
+REQUIREMENTS = {
+	'FreeBSD' => '>= 7.0',
+}
+
+# RubyGem specification
+GEMSPEC   = Gem::Specification.new do |gem|
+	gem.name              = PKG_NAME.downcase
+	gem.version           = PKG_VERSION
+
+	gem.summary           = PKG_SUMMARY
+	gem.description       = <<-EOD
+	This is shell that can be used to interact with multiple FreeBSD jail instances
+
+	simultaneously. It includes a Ruby binding to the FreeBSD jail(2) functions.
+	EOD
+
+	gem.authors           = 'Michae Granger and Mahlon Smith'
+	gem.email             = 'ged@FaerieMUD.org, mahlon@martini.nu'
+	gem.homepage          = 'http://deveiate.org/projects/Jparallel/'
+	gem.rubyforge_project = RUBYFORGE_PROJECT
+
+	gem.has_rdoc          = true
+	gem.rdoc_options      = RDOC_OPTIONS
+
+	gem.files             = RELEASE_FILES.
+		collect {|f| f.relative_path_from(BASEDIR).to_s }
+	gem.test_files        = SPEC_FILES.
+		collect {|f| f.relative_path_from(BASEDIR).to_s }
+		
+	DEPENDENCIES.each do |name, version|
+		version = '>= 0' if version.length.zero?
+		gem.add_dependency( name, version )
+	end
+	
+	REQUIREMENTS.each do |name, version|
+		gem.requirements << [ name, version ].compact.join(' ')
+	end
+end
+
+$trace = Rake.application.options.trace ? true : false
+$dryrun = Rake.application.options.dryrun ? true : false
+
+
+# Load any remaining task libraries
+RAKE_TASKLIBS.each do |tasklib|
+	next if tasklib =~ %r{/(helpers|svn|verifytask)\.rb$}
+	begin
+		require tasklib
+	rescue ScriptError => err
+		fail "Task library '%s' failed to load: %s: %s" %
+			[ tasklib, err.class.name, err.message ]
+		trace "Backtrace: \n  " + err.backtrace.join( "\n  " )
+	rescue => err
+		log "Task library '%s' failed to load: %s: %s. Some tasks may not be available." %
+			[ tasklib, err.class.name, err.message ]
+		trace "Backtrace: \n  " + err.backtrace.join( "\n  " )
+	end
+end
+
+# Load any project-specific rules defined in 'Rakefile.local' if it exists
+import LOCAL_RAKEFILE if LOCAL_RAKEFILE.exist?
+
+
+#####################################################################
+###	T A S K S 	
+#####################################################################
+
+### Default task
+task :default  => [:clean, :spec, :rdoc, :package]
+
+
+### Task: clean
+CLEAN.include 'coverage'
+CLOBBER.include 'artifacts', 'coverage.info', PKGDIR
+
+# Target to hinge on ChangeLog updates
+file SVN_ENTRIES
+
+### Task: changelog
+file 'ChangeLog' => SVN_ENTRIES.to_s do |task|
+	log "Updating #{task.name}"
+
+	changelog = make_svn_changelog()
+	File.open( task.name, 'w' ) do |fh|
+		fh.print( changelog )
+	end
+end
+
+
+### Task: cruise (Cruisecontrol task)
+desc "Cruisecontrol build"
+task :cruise => [:clean, :spec, :package] do |task|
+	raise "Artifacts dir not set." if ARTIFACTS_DIR.to_s.empty?
+	artifact_dir = ARTIFACTS_DIR.cleanpath
+	artifact_dir.mkpath
+	
+	$stderr.puts "Copying coverage stats..."
+	FileUtils.cp_r( 'coverage', artifact_dir )
+	
+	$stderr.puts "Copying packages..."
+	FileUtils.cp_r( FileList['pkg/*'].to_a, artifact_dir )
+end
+
+
+desc "Update the build system to the latest version"
+task :update_build do
+	log "Updating the build system"
+	sh 'svn', 'up', RAKE_TASKDIR
+	log "Updating the Rakefile"
+	sh 'rake', '-f', RAKE_TASKDIR + 'Metarakefile'
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Rakefile.local	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,89 @@
+#!rake
+
+begin
+	require 'mkrf'
+rescue LoadError => err
+	unless Object.const_defined?( :Gem )
+		require 'rubygems'
+		retry
+	end
+	
+	fail "You need to have the mkrf library installed to build this."
+end
+
+
+# C extension constants
+EXT_RAKEFILE  = EXTDIR + 'Rakefile'
+EXT_SO        = EXTDIR + "redleaf_ext.#{CONFIG['DLEXT']}"
+
+ADDITIONAL_INCLUDE_DIRS = %w[
+	/usr/local/include
+	/opt/include
+	/opt/local/include
+]
+
+# Additional (auto-generated) spec constants
+SPEC_RAKEFILE = SPECDIR + 'Rakefile'
+
+
+#####################################################################
+###	T A S K S
+#####################################################################
+
+# task :local => [ :build, :build_specs ]
+task :local => [ :build ]
+task :spec => [ :build ]
+
+desc "Make the Rakefile for the C extension"
+file EXT_RAKEFILE.to_s => FileList[ 'Rakefile', EXTDIR + '*.c' ] do
+	require 'misc/monkeypatches' # Fix Mkrf's output
+	
+	log "Configuring bsdjail C extension"
+	Dir.chdir( EXTDIR ) do
+		Mkrf::Generator.new( 'bsdjail', FileList['*.c'] ) do |gen|
+			trace "Setting CFLAGS"
+			gen.cflags << ' -Wall'
+			gen.cflags << ' -DDEBUG'
+
+			# Make sure we have the ODE library and header available
+			trace "checking for sys/param.h"
+			gen.include_header( "sys/param.h" ) or
+				fail "Can't find the sys/param.h header."
+
+			trace "checking for sys/jail.h"
+			gen.include_header( "sys/jail.h" ) or
+				fail "Can't find the sys/jail.h header."
+
+			gen.include_library( "c", "jail_attach" ) or
+				fail "Can't find jail_attach in the stdlib."
+		end
+	end
+end
+CLOBBER.include( EXTDIR + 'mkrf.log' )
+
+
+desc "Build the C extension"
+task :build => EXT_RAKEFILE.to_s do
+	Dir.chdir( EXTDIR ) do
+		sh 'rake'
+	end
+end
+
+
+task :clean do
+	if EXT_RAKEFILE.exist?
+		Dir.chdir( EXTDIR ) do
+			sh 'rake', 'clean'
+		end
+	end
+end	
+
+task :clobber do
+	if EXT_RAKEFILE.exist?
+		Dir.chdir( EXTDIR ) do
+			sh 'rake', 'clobber'
+		end
+	end
+end	
+CLOBBER.include( EXT_RAKEFILE )
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/example.rb	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,36 @@
+#!/usr/bin/ruby
+# 
+# Example jail_attach usage
+# 
+# == Synopsis
+# 
+#   
+# 
+# == Authors
+# 
+# * Michael Granger <ged@FaerieMUD.org>
+# 
+# == Copyright
+#
+# Copyright (c) 2006 The FaerieMUD Consortium. Some rights reserved.
+# 
+# This work is licensed under the Creative Commons Attribution License. To view
+# a copy of this license, visit http://creativecommons.org/licenses/by/1.0/ or
+# send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California
+# 94305, USA.
+# 
+# == Version
+#
+#  $Id$
+# 
+
+require 'bsdjail'
+
+# With a block, does a fork() then a jail_attach() from the child
+pid = BSDJail.attach( jid ) do
+	puts "I'm a child process in jail!"
+	
+	
+end
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/jail.h	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,22 @@
+/*-
+ * ----------------------------------------------------------------------------
+ * "THE BEER-WARE LICENSE" (Revision 42):
+ * <phk@FreeBSD.org> wrote this file.  As long as you retain this notice you
+ * can do whatever you want with this stuff. If we meet some day, and you think
+ * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
+ * ----------------------------------------------------------------------------
+ *
+ * $FreeBSD: src/sys/sys/jail.h,v 1.26 2005/06/09 18:49:19 pjd Exp $
+ *
+ */
+
+struct jail {
+        u_int32_t       version;
+        char            *path;
+        char            *hostname;
+        u_int32_t       ip_number;
+};
+
+int jail(struct jail *);
+int jail_attach(int);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/jexec.c	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,82 @@
+/*-
+ * Copyright (c) 2004 Joerg Sonnenberger <joerg@bec.de>
+ * Copyright (c) 2003 Mike Barcroft <mike@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD: src/usr.sbin/jexec/jexec.c,v 1.2 2003/07/04 19:14:27 bmilekic Exp $
+ * $DragonFly: src/usr.sbin/jexec/jexec.c,v 1.1 2005/01/31 22:29:59 joerg Exp $
+ */
+
+#include <sys/param.h>
+#include <sys/jail.h>
+
+#include <err.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static int	getjailid(const char *str);
+static void	usage(void);
+
+int
+main(int argc, char **argv)
+{
+	int jid;
+
+	if (argc < 3)
+		usage();
+	jid = getjailid(argv[1]);
+	if (jail_attach(jid) == -1)
+		err(1, "jail_attach(%d) failed", jid);
+	if (chdir("/") == -1)
+		err(1, "chdir(\"/\") failed");
+	if (execvp(argv[2], argv + 2) == -1)
+		err(1, "execvp(%s) failed", argv[2]);
+	exit(0);
+}
+
+static void
+usage(void)
+{
+	fprintf(stderr, "usage: jexec jid command [...]\n");
+	exit(1); 
+}
+
+static int
+getjailid(const char *str)
+{
+	long v;
+	char *ep;
+
+	errno = 0;
+	v = strtol(str, &ep, 10);
+	if (v < INT_MIN || v > INT_MAX || errno == ERANGE)
+		errc(1, ERANGE, "invalid jail id", str);
+	if (ep == str || *ep != '\0')
+		errx(1, "cannot parse jail id: %s.", str);
+
+	return((int)(v));
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/jls.c	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,82 @@
+/*-
+ * Copyright (c) 2003 Mike Barcroft <mike@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD: src/usr.sbin/jls/jls.c,v 1.3 2003/04/22 13:24:56 mike Exp $
+ * $DragonFly: src/usr.sbin/jls/jls.c,v 1.1 2005/01/31 22:29:59 joerg Exp $
+ */
+
+#include <sys/param.h>
+#include <sys/kinfo.h>
+#include <sys/sysctl.h>
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+int
+main(void)
+{ 
+	struct kinfo_prison *sxp, *xp;
+	struct in_addr in;
+	size_t i, len;
+
+	if (sysctlbyname("jail.list", NULL, &len, NULL, 0) == -1)
+		err(1, "sysctlbyname(): jail.list");
+retry:
+	if (len == 0)
+		exit(0);	
+
+	sxp = xp = malloc(len);
+	if (sxp == NULL)
+		err(1, "malloc failed");
+
+	if (sysctlbyname("jail.list", xp, &len, NULL, 0) == -1) {
+		if (errno == ENOMEM) {
+			free(sxp);
+			goto retry;
+		}
+		err(1, "sysctlbyname(): jail.list");
+	}
+	if (len < sizeof(*xp) || len % sizeof(*xp) ||
+	    xp->pr_version != KINFO_PRISON_VERSION)
+		errx(1, "Kernel and userland out of sync");
+
+	len /= sizeof(*xp);
+	printf("   JID  IP Address      Hostname                      Path\n");
+	for (i = 0; i < len; i++) {
+		in.s_addr = ntohl(xp->pr_ip);
+		printf("%6d  %-15.15s %-29.29s %.74s\n",
+		    xp->pr_id, inet_ntoa(in), xp->pr_host, xp->pr_path);
+		xp++;
+	}
+	free(sxp);
+	exit(0);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/jexec.rb	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,29 @@
+#!/usr/bin/ruby
+
+BEGIN {
+	require 'pathname'
+	basedir = Pathname.new( __FILE__ ).dirname.parent
+	
+	$LOAD_PATH.unshift basedir + "ext" unless 
+		$LOAD_PATH.include? basedir + "ext"
+}
+
+
+require 'bsdjail'
+
+jid = Integer( ARGV.shift )
+args = ARGV
+
+$deferr.puts "In process #{Process.pid}, about to jail_attach() with a block"
+
+childpid = BSD::Jail.attach( jid ) do
+	Dir.chdir( "/" )
+	$deferr.puts "Child #{Process.pid} exec()ing:", "  " + args.join(" ")
+	exec( *args )
+end
+
+$deferr.puts "Parent: waiting on imprisoned child #{childpid}"
+Process.waitpid( childpid )
+
+$deferr.puts "Child exited with exit code: %d" % [ $?.exitstatus ]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/bsdjail.c	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,233 @@
+/*
+ *  dict.c - Ruby LinkParser - Dict Class
+ *  $Id$
+ *  
+ *  Authors:
+ *    * Michael Granger <ged@FaerieMUD.org>
+ *  
+ *  Copyright (c) 2006 The FaerieMUD Consortium.
+ *  
+ *  This work is licensed under the Creative Commons Attribution License. To
+ *  view a copy of this license, visit
+ *  http://creativecommons.org/licenses/by/1.0 or send a letter to Creative
+ *  Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.
+ *  
+ */
+
+#include <ruby.h>
+#include <intern.h>
+
+#include <stdio.h>
+#include <sys/param.h>
+#include <sys/jail.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+
+VALUE rbjail_mBSD;
+VALUE rbjail_cBSDJail;
+
+
+/*
+struct jail {
+	u_int32_t       version;
+	char            *path;
+	char            *hostname;
+	u_int32_t       ip_number;
+};
+*/
+
+/*
+ * Allocation function
+ */
+static jail *
+rbjail_jail_alloc()
+{
+	jail *ptr = ALLOC( jail );
+	
+	ptr->version	= 0;
+	ptr->path		= NULL;
+	ptr->hostname	= NULL;
+	ptr->ip_number	= 0;
+	
+	debugMsg(( "Initialized a jail pointer <%p>", ptr ));
+	return ptr;
+}
+
+
+/*
+ * GC Free function
+ */
+static void
+rbjail_jail_gc_free( ptr )
+	jail *ptr;
+{
+	if ( ptr ) {
+		ptr->path		= NULL;
+		ptr->hostname	= NULL;
+		xfree( ptr );
+	}
+	
+	else {
+		debugMsg(( "Not freeing an uninitialized rlink_SENTENCE" ));
+	}
+}
+
+
+/*
+ * Object validity checker. Returns the data pointer.
+ */
+static rlink_SENTENCE *
+check_sentence( self )
+	 VALUE	self;
+{
+	debugMsg(( "Checking a LinkParser::Sentence object (%d).", self ));
+	Check_Type( self, T_DATA );
+
+    if ( !IsSentence(self) ) {
+		rb_raise( rb_eTypeError, "wrong argument type %s (expected LinkParser::Sentence)",
+				  rb_class2name(CLASS_OF( self )) );
+    }
+	
+	return DATA_PTR( self );
+}
+
+
+/*
+ * Fetch the data pointer and check it for sanity.
+ */
+static rlink_SENTENCE *
+get_sentence( self )
+	 VALUE self;
+{
+	rlink_SENTENCE *ptr = check_sentence( self );
+
+	debugMsg(( "Fetching a Sentence (%p).", ptr ));
+	if ( !ptr )
+		rb_raise( rb_eRuntimeError, "uninitialized Sentence" );
+
+	return ptr;
+}
+
+
+/*
+ * Publicly-usable sentence-fetcher
+ */
+rlink_SENTENCE *
+rlink_get_sentence( self )
+	VALUE self;
+{
+	return get_sentence( self );
+}
+
+
+
+
+
+
+static void
+rbjail_do_jail_attach( int jid )
+{
+	if ( jail_attach(jid) == -1 )
+		rb_sys_fail( "jail_attach" );
+}
+
+/* Mostly ripped off from Ruby's process.c */
+static VALUE
+rbjail_attach_block( int jid )
+{
+    int pid;
+
+    rb_secure(2);
+
+    fflush(stdout);
+    fflush(stderr);
+
+	switch ( pid = fork() ) {
+		case 0:
+			rb_thread_atfork();
+			if ( rb_block_given_p() ) {
+				int status;
+
+				rbjail_do_jail_attach( jid );
+				rb_protect( rb_yield, Qundef, &status );
+				ruby_stop( status );
+			}
+			return Qnil;
+
+		case -1:
+			rb_sys_fail( "fork(2)" );
+			return Qnil;
+
+		default:
+			return INT2FIX( pid );
+	}
+}
+
+static VALUE
+rbjail_attach( int argc, VALUE *argv, VALUE self )
+{
+	VALUE jidnum, rval;
+	int jid;
+	
+	rb_scan_args( argc, argv, "1", &jidnum );
+	jid = NUM2INT( jidnum );
+
+	if ( rb_block_given_p() ) {
+		rval = rbjail_attach_block( jid );
+	}
+	
+	else {
+		rbjail_do_jail_attach( jid );
+		rval = Qtrue;
+	}
+	
+	return rval;
+}
+
+static VALUE
+	rbjail_list( VALUE self )
+{
+	struct kinfo_prison *sxp, *xp;
+	struct in_addr in;
+	size_t i, len;
+
+	if (sysctlbyname("jail.list", NULL, &len, NULL, 0) == -1)
+		rb_sys_fail("sysctlbyname(): jail.list");
+
+	xp = ALLOCA_N( kinfo_prison, 1 );
+
+	if (sysctlbyname("jail.list", xp, &len, NULL, 0) == -1) {
+		rb_sys_fail("sysctlbyname(): jail.list");
+	}
+
+	if (len < sizeof(*xp) || len % sizeof(*xp) ||
+		xp->pr_version != KINFO_PRISON_VERSION)
+		rb_fatal("Kernel and userland out of sync");
+
+	len /= sizeof(*xp);
+	printf("   JID  IP Address      Hostname                      Path\n");
+	for (i = 0; i < len; i++) {
+		in.s_addr = ntohl(xp->pr_ip);
+		printf("%6d  %-15.15s %-29.29s %.74s\n",
+			xp->pr_id, inet_ntoa(in), xp->pr_host, xp->pr_path);
+		xp++;
+	}
+	free(sxp);
+	exit(0);
+
+}
+
+void
+Init_bsdjail( void )
+{
+	rbjail_mBSD = rb_define_module( "BSD" );
+	rbjail_cBSDJail = rb_define_class_under( rbjail_mBSD, "Jail" );
+
+	rb_define_singleton_method( rbjail_cBSDJail, "list", rbjail_list, 0 );
+	rb_define_alloc_function( rbjail_cBSDJail, )
+	
+	rb_define_method( rbjail_cBSDJail, "attach", rbjail_attach, -1 );
+	
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/jparallel.rb	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+
+
+
+# This is mostly here to appease the build system for now, but will be used
+# later as the toplevel namespace for the shell classes.
+module Jparallel
+	VERSION = '0.0.1'
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/monkeypatches.rb	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,71 @@
+### Monkeypatch to work around the broken Rakefile generated by Mkrf <= 0.2.3
+
+# This fixes:
+# * Some weird unnecessary string interpolation
+# * The :install task doesn't work unless you have RUBYARCHDIR in your environment, instead of
+#   falling back to CONFIG['sitearchdir'].
+# * The :install task created the target install directory every time instead of just
+#   declaring a dependency on a directory task
+
+require 'mkrf/generator'
+
+class Mkrf::Generator
+
+    def rakefile_contents # :nodoc:
+      objext = CONFIG['OBJEXT']
+
+      <<-END_RAKEFILE
+# Generated by mkrf
+require 'rake/clean'
+require 'rbconfig'
+
+include Config
+
+SRC = FileList[#{sources.join(',')}]
+OBJ = SRC.ext('#{objext}')
+CC = '#{@cc}'
+
+ADDITIONAL_OBJECTS = '#{objects}'
+
+LDSHARED = "#{@available.ldshared_string} #{ldshared}"
+
+LIBPATH =  "#{library_path(CONFIG['libdir'])} #{@available.library_paths_compile_string}"
+
+INCLUDES = "#{@available.includes_compile_string}"
+
+LIBS = "#{@available.library_compile_string}"
+
+CFLAGS = "#{cflags} #{defines_compile_string}"
+
+RUBYARCHDIR = ENV["RUBYARCHDIR"] || CONFIG['sitearchdir']
+LIBRUBYARG_SHARED = "#{CONFIG['LIBRUBYARG_SHARED']}"
+EXT = '#{@extension_name}'
+
+CLEAN.include( EXT, '*.#{objext}' )
+CLOBBER.include( 'mkrf.log' )
+
+task :default => EXT
+
+rule '.#{objext}' => '.#{@source_extension}' do |t|
+  sh "\#{CC} \#{CFLAGS} \#{INCLUDES} -c \#{t.source}"
+end
+
+desc "Build this extension"
+file EXT => OBJ do
+  sh "\#{LDSHARED} \#{LIBPATH} #{@available.ld_outfile(@extension_name)} \#{OBJ} \#{ADDITIONAL_OBJECTS} \#{LIBS} \#{LIBRUBYARG_SHARED}"
+end
+
+
+directory RUBYARCHDIR
+
+desc "Install this extension"
+task :install => [EXT, RUBYARCHDIR] do
+  install EXT, RUBYARCHDIR, :verbose => true
+end
+
+#{additional_code}
+      END_RAKEFILE
+    end
+
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/project.yml	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,17 @@
+--- 
+rubyforge_project: jparallel
+project_requirements: 
+  FreeBSD: ">= 5.0"
+project_description: |-
+  This is shell that can be used to interact with multiple FreeBSD jail instances
+  simultaneously. It includes a Ruby binding to the FreeBSD jail(2) functions.
+rubyforge_group: deveiate
+author_name: Michael Granger and Mahlon Smith
+project_homepage: http://deveiate.org/projects/Jparallel/
+project_dependencies: {}
+
+project_summary: A "parallel jail shell" written in Ruby
+project_name: jparallel
+additional_pkgfiles: 
+- examples/*.{c,rb}
+author_email: ged@FaerieMUD.org, mahlon@martini.nu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/utils.rb	Fri Aug 15 15:43:38 2008 +0000
@@ -0,0 +1,738 @@
+#
+#	Install/distribution utility functions
+#	$Id$
+#
+#	Copyright (c) 2001-2005, The FaerieMUD Consortium.
+#
+#	This is free software. You may use, modify, and/or redistribute this
+#	software under the terms of the Perl Artistic License. (See
+#	http://language.perl.com/misc/Artistic.html)
+#
+
+BEGIN {
+	require 'rbconfig'
+	require 'uri'
+	require 'find'
+	require 'pp'
+
+	begin
+		require 'readline'
+		include Readline
+	rescue LoadError => e
+		$stderr.puts "Faking readline..."
+		def readline( prompt )
+			$stderr.print prompt.chomp
+			return $stdin.gets.chomp
+		end
+	end
+
+	begin
+		require 'yaml'
+		$yaml = true
+	rescue LoadError => e
+		$stderr.puts "No YAML; try() will use PrettyPrint instead."
+		$yaml = false
+	end
+}
+
+
+module UtilityFunctions
+	include Config
+
+	# The list of regexen that eliminate files from the MANIFEST
+	ANTIMANIFEST = [
+		/makedist\.rb/,
+		/\bCVS\b/,
+		/~$/,
+		/^#/,
+		%r{docs/html},
+		%r{docs/man},
+		/\bTEMPLATE\.\w+\.tpl\b/,
+		/\.cvsignore/,
+		/\.s?o$/,
+	]
+
+	# Set some ANSI escape code constants (Shamelessly stolen from Perl's
+	# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
+	AnsiAttributes = {
+		'clear'      => 0,
+		'reset'      => 0,
+		'bold'       => 1,
+		'dark'       => 2,
+		'underline'  => 4,
+		'underscore' => 4,
+		'blink'      => 5,
+		'reverse'    => 7,
+		'concealed'  => 8,
+
+		'black'      => 30,   'on_black'   => 40, 
+		'red'        => 31,   'on_red'     => 41, 
+		'green'      => 32,   'on_green'   => 42, 
+		'yellow'     => 33,   'on_yellow'  => 43, 
+		'blue'       => 34,   'on_blue'    => 44, 
+		'magenta'    => 35,   'on_magenta' => 45, 
+		'cyan'       => 36,   'on_cyan'    => 46, 
+		'white'      => 37,   'on_white'   => 47
+	}
+
+	ErasePreviousLine = "\033[A\033[K"
+
+	ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' )
+		#
+		# Distribution Manifest
+		# Created: #{Time::now.to_s}
+		# 
+
+	EOF
+
+	###############
+	module_function
+	###############
+
+	# Create a string that contains the ANSI codes specified and return it
+	def ansiCode( *attributes )
+		return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
+		attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';')
+		if attr.empty? 
+			return ''
+		else
+			return "\e[%sm" % attr
+		end
+	end
+
+	# Test for the presence of the specified <tt>library</tt>, and output a
+	# message describing the test using <tt>nicename</tt>. If <tt>nicename</tt>
+	# is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default.
+	def testForLibrary( library, nicename=nil, progress=false )
+		nicename ||= library
+		message( "Testing for the #{nicename} library..." ) if progress
+		if $LOAD_PATH.detect {|dir|
+				File.exists?(File.join(dir,"#{library}.rb")) ||
+				File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}"))
+			}
+			message( "found.\n" ) if progress
+			return true
+		else
+			message( "not found.\n" ) if progress
+			return false
+		end
+	end
+
+	# Test for the presence of the specified <tt>library</tt>, and output a
+	# message describing the problem using <tt>nicename</tt>. If
+	# <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used
+	# to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are
+	# specified, they are also use to build a message describing how to find the
+	# required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library
+	# will cause the program to abort.
+	def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true )
+		nicename ||= library
+		unless testForLibrary( library, nicename )
+			msgs = [ "You are missing the required #{nicename} library.\n" ]
+			msgs << "RAA: #{raaUrl}\n" if raaUrl
+			msgs << "Download: #{downloadUrl}\n" if downloadUrl
+			if fatal
+				abort msgs.join('')
+			else
+				errorMessage msgs.join('')
+			end
+		end
+		return true
+	end
+
+	### Output <tt>msg</tt> as a ANSI-colored program/section header (white on
+	### blue).
+	def header( msg )
+		msg.chomp!
+		$stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' )
+		$stderr.flush
+	end
+
+	### Output <tt>msg</tt> to STDERR and flush it.
+	def message( *msgs )
+		$stderr.print( msgs.join("\n") )
+		$stderr.flush
+	end
+
+	### Output +msg+ to STDERR and flush it if $VERBOSE is true.
+	def verboseMsg( msg )
+		msg.chomp!
+		message( msg + "\n" ) if $VERBOSE
+	end
+
+	### Output the specified <tt>msg</tt> as an ANSI-colored error message
+	### (white on red).
+	def errorMessage( msg )
+		message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' )
+	end
+
+	### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
+	### (yellow on blue).
+	def debugMsg( msg )
+		return unless $DEBUG
+		msg.chomp!
+		$stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' )
+		$stderr.flush
+	end
+
+	### Erase the previous line (if supported by your terminal) and output the
+	### specified <tt>msg</tt> instead.
+	def replaceMessage( msg )
+		$stderr.print ErasePreviousLine
+		message( msg )
+	end
+
+	### Output a divider made up of <tt>length</tt> hyphen characters.
+	def divider( length=75 )
+		$stderr.puts "\r" + ("-" * length )
+	end
+	alias :writeLine :divider
+
+
+	### Output the specified <tt>msg</tt> colored in ANSI red and exit with a
+	### status of 1.
+	def abort( msg )
+		print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n"
+		Kernel.exit!( 1 )
+	end
+
+
+	### Output the specified <tt>promptString</tt> as a prompt (in green) and
+	### return the user's input with leading and trailing spaces removed.  If a
+	### test is provided, the prompt will repeat until the test returns true.
+	### An optional failure message can also be passed in.
+	def prompt( promptString, failure_msg="Try again." ) # :yields: response
+		promptString.chomp!
+		response = nil
+
+		begin
+			response = readline( ansiCode('bold', 'green') +
+				"#{promptString}: " + ansiCode('reset') ).strip
+			if block_given? && ! yield( response ) 
+				errorMessage( failure_msg + "\n\n" )
+				response = nil
+			end
+		end until response
+
+		return response
+	end
+
+
+	### Prompt the user with the given <tt>promptString</tt> via #prompt,
+	### substituting the given <tt>default</tt> if the user doesn't input
+	### anything.  If a test is provided, the prompt will repeat until the test
+	### returns true.  An optional failure message can also be passed in.
+	def promptWithDefault( promptString, default, failure_msg="Try again." )
+		response = nil
+		
+		begin
+			response = prompt( "%s [%s]" % [ promptString, default ] )
+			response = default if response.empty?
+
+			if block_given? && ! yield( response ) 
+				errorMessage( failure_msg + "\n\n" )
+				response = nil
+			end
+		end until response
+
+		return response
+	end
+
+
+	$programs = {}
+
+	### Search for the program specified by the given <tt>progname</tt> in the
+	### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if
+	### no such program is in the path.
+	def findProgram( progname )
+		unless $programs.key?( progname )
+			ENV['PATH'].split(File::PATH_SEPARATOR).each {|d|
+				file = File.join( d, progname )
+				if File.executable?( file )
+					$programs[ progname ] = file 
+					break
+				end
+			}
+		end
+
+		return $programs[ progname ]
+	end
+
+
+	### Search for the release version for the project in the specified
+	### +directory+.
+	def extractVersion( directory='.' )
+		release = nil
+
+		Dir::chdir( directory ) do
+			if File::directory?( "CVS" )
+				verboseMsg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." )
+
+				if (( cvs = findProgram('cvs') ))
+					revs = []
+					output = %x{cvs log}
+					output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match|
+						rev = $1.split(/_/).collect {|s| Integer(s) rescue 0}
+						verboseMsg( "Found %s...\n" % rev.join('.') )
+						revs << rev
+					}
+
+					release = revs.sort.last
+				end
+
+			elsif File::directory?( '.svn' )
+				verboseMsg( "Project is versioned via Subversion" )
+
+				if (( svn = findProgram('svn') ))
+					output = %x{svn pg project-version}.chomp
+					unless output.empty?
+						verboseMsg( "Using 'project-version' property: %p" % output )
+						release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0}
+					end
+				end
+			end
+		end
+
+		return release
+	end
+
+
+	### Find the current release version for the project in the specified
+	### +directory+ and return its successor.
+	def extractNextVersion( directory='.' )
+		version = extractVersion( directory ) || [0,0,0]
+		version.compact!
+		version[-1] += 1
+
+		return version
+	end
+
+
+	# Pattern for extracting the name of the project from a Subversion URL
+	SVNUrlPath = %r{
+		.*/						# Skip all but the last bit
+		([^/]+)					# $1 = project name
+		/						# Followed by / +
+		(?:
+			trunk |				# 'trunk'
+			(
+				branches |		# ...or branches/branch-name
+				tags			# ...or tags/tag-name
+			)/\w	
+		)
+		$						# bound to the end
+	}ix
+
+	### Extract the project name (CVS Repository name) for the given +directory+.
+	def extractProjectName( directory='.' )
+		name = nil
+
+		Dir::chdir( directory ) do
+
+			# CVS-controlled
+			if File::directory?( "CVS" )
+				verboseMsg( "Project is versioned via CVS. Using repository name." )
+				name = File.open( "CVS/Repository", "r").readline.chomp
+				name.sub!( %r{.*/}, '' )
+
+			# Subversion-controlled
+			elsif File::directory?( '.svn' )
+				verboseMsg( "Project is versioned via Subversion" )
+
+				# If the machine has the svn tool, try to get the project name
+				if (( svn = findProgram( 'svn' ) ))
+
+					# First try an explicit property
+					output = shellCommand( svn, 'pg', 'project-name' )
+					if !output.empty?
+						verboseMsg( "Using 'project-name' property: %p" % output )
+						name = output.first.chomp
+
+					# If that doesn't work, try to figure it out from the URL
+					elsif (( uri = getSvnUri() ))
+						name = uri.path.sub( SVNUrlPath ) { $1 }
+					end
+				end
+			end
+
+			# Fall back to guessing based on the directory name
+			unless name
+				name = File::basename(File::dirname( File::expand_path(__FILE__) ))
+			end
+		end
+
+		return name
+	end
+
+
+	### Extract the Subversion URL from the specified directory and return it as
+	### a URI object.
+	def getSvnUri( directory='.' )
+		uri = nil
+
+		Dir::chdir( directory ) do
+			output = %x{svn info}
+			debugMsg( "Using info: %p" % output )
+
+			if /^URL: \s* ( .* )/xi.match( output )
+				uri = URI::parse( $1 )
+			end
+		end
+
+		return uri
+	end
+
+
+	### (Re)make a manifest file in the specified +path+.
+	def makeManifest( path="MANIFEST" )
+		if File::exists?( path )
+			reply = promptWithDefault( "Replace current '#{path}'? [yN]", "n" )
+			return false unless /^y/i.match( reply )
+
+			verboseMsg "Replacing manifest at '#{path}'"
+		else
+			verboseMsg "Creating new manifest at '#{path}'"
+		end
+
+		files = []
+		verboseMsg( "Finding files...\n" )
+		Find::find( Dir::pwd ) do |f|
+			Find::prune if File::directory?( f ) &&
+				/^\./.match( File::basename(f) )
+			verboseMsg( "  found: #{f}\n" )
+			files << f.sub( %r{^#{Dir::pwd}/?}, '' )
+		end
+		files = vetManifest( files )
+		
+		verboseMsg( "Writing new manifest to #{path}..." )
+		File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
+			ofh.puts( ManifestHeader )
+			ofh.puts( files )
+		end
+		verboseMsg( "done." )
+	end
+
+
+	### Read the specified <tt>manifestFile</tt>, which is a text file
+	### describing which files to package up for a distribution. The manifest
+	### should consist of one or more lines, each containing one filename or
+	### shell glob pattern.
+	def readManifest( manifestFile="MANIFEST" )
+		verboseMsg "Building manifest..."
+		raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile
+
+		manifest = IO::readlines( manifestFile ).collect {|line|
+			line.chomp
+		}.select {|line|
+			line !~ /^(\s*(#.*)?)?$/
+		}
+
+		filelist = []
+		for pat in manifest
+			verboseMsg "Adding files that match '#{pat}' to the file list"
+			filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)}
+		end
+
+		verboseMsg "found #{filelist.length} files.\n"
+		return filelist
+	end
+
+
+	### Given a <tt>filelist</tt> like that returned by #readManifest, remove
+	### the entries therein which match the Regexp objects in the given
+	### <tt>antimanifest</tt> and return the resultant Array.
+	def vetManifest( filelist, antimanifest=ANTIMANIFEST )
+		origLength = filelist.length
+		verboseMsg "Vetting manifest..."
+
+		for regex in antimanifest
+			verboseMsg "\n\tPattern /#{regex.source}/ removed: " +
+				filelist.find_all {|file| regex.match(file)}.join(', ')
+			filelist.delete_if {|file| regex.match(file)}
+		end
+
+		verboseMsg "removed #{origLength - filelist.length} files from the list.\n"
+		return filelist
+	end
+
+
+	### Combine a call to #readManifest with one to #vetManifest.
+	def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST )
+		vetManifest( readManifest(manifestFile), antimanifest )
+	end
+
+
+	### Given a documentation <tt>catalogFile</tt>, extract the title, if
+	### available, and return it. Otherwise generate a title from the name of
+	### the CVS module.
+	def findRdocTitle( catalogFile="docs/CATALOG" )
+
+		# Try extracting it from the CATALOG file from a line that looks like:
+		# Title: Foo Bar Module
+		title = findCatalogKeyword( 'title', catalogFile )
+
+		# If that doesn't work for some reason, use the name of the project.
+		title = extractProjectName()
+
+		return title
+	end
+
+
+	### Given a documentation <tt>catalogFile</tt>, extract the name of the file
+	### to use as the initally displayed page. If extraction fails, the
+	### +default+ will be used if it exists. Returns +nil+ if there is no main
+	### file to be found.
+	def findRdocMain( catalogFile="docs/CATALOG", default="README" )
+
+		# Try extracting it from the CATALOG file from a line that looks like:
+		# Main: Foo Bar Module
+		main = findCatalogKeyword( 'main', catalogFile )
+
+		# Try to make some educated guesses if that doesn't work
+		if main.nil?
+			basedir = File::dirname( __FILE__ )
+			basedir = File::dirname( basedir ) if /docs$/ =~ basedir
+			
+			if File::exists?( File::join(basedir, default) )
+				main = default
+			end
+		end
+
+		return main
+	end
+
+
+	### Given a documentation <tt>catalogFile</tt>, extract an upload URL for
+	### RDoc.
+	def findRdocUpload( catalogFile="docs/CATALOG" )
+		findCatalogKeyword( 'upload', catalogFile )
+	end
+
+
+	### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend
+	### URL for RDoc.
+	def findRdocCvsURL( catalogFile="docs/CATALOG" )
+		findCatalogKeyword( 'webcvs', catalogFile )
+	end
+
+
+	### Find one or more 'accessor' directives in the catalog if they exist and
+	### return an Array of them.
+	def findRdocAccessors( catalogFile="docs/CATALOG" )
+		accessors = []
+		in_attr_section = false
+		indent = ''
+
+		if File::exists?( catalogFile )
+			verboseMsg "Extracting accessors from CATALOG file (%s).\n" % catalogFile
+
+			# Read lines from the catalog
+			File::foreach( catalogFile ) do |line|
+				debugMsg( "  Examining line #{line.inspect}..." )
+
+				# Multi-line accessors
+				if in_attr_section
+					if /^#\s+([a-z0-9_]+(?:\s*=\s*.*)?)$/i.match( line )
+						debugMsg( "    Found accessor: #$1" )
+						accessors << $1
+						next
+					end
+
+					debugMsg( "  End of accessors section." )
+					in_attr_section = false
+
+				# Single-line accessor
+				elsif /^#\s*Accessors:\s*(\S+)$/i.match( line )
+					debugMsg( "  Found single accessors line: #$1" )
+					vals = $1.split(/,/).collect {|val| val.strip }
+					accessors.replace( vals )
+
+				# Multi-line accessor header
+				elsif /^#\s*Accessors:\s*$/i.match( line )
+					debugMsg( "  Start of accessors section." )
+					in_attr_section = true
+				end
+
+			end
+		end
+
+		debugMsg( "Found accessors: %s" % accessors.join(",") )
+		return accessors
+	end
+
+		
+	### Given a documentation <tt>catalogFile</tt>, try extracting the given
+	### +keyword+'s value from it. Keywords are lines that look like:
+	###   # <keyword>: <value>
+	### Returns +nil+ if the catalog file was unreadable or didn't contain the
+	### specified +keyword+.
+	def findCatalogKeyword( keyword, catalogFile="docs/CATALOG" )
+		val = nil
+
+		if File::exists? catalogFile
+			verboseMsg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile
+			File::foreach( catalogFile ) do |line|
+				debugMsg( "Examining line #{line.inspect}..." )
+				val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i.match( line )
+			end
+		end
+
+		return val
+	end
+
+
+	### Given a documentation <tt>catalogFile</tt>, which is in the same format
+	### as that described by #readManifest, read and expand it, and then return
+	### a list of those files which appear to have RDoc documentation in
+	### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST
+	### file is used instead.
+	def findRdocableFiles( catalogFile="docs/CATALOG" )
+		startlist = []
+		if File.exists? catalogFile
+			verboseMsg "Using CATALOG file (%s).\n" % catalogFile
+			startlist = getVettedManifest( catalogFile )
+		else
+			verboseMsg "Using default MANIFEST\n"
+			startlist = getVettedManifest()
+		end
+
+		verboseMsg "Looking for RDoc comments in:\n"
+		startlist.select {|fn|
+			verboseMsg "  #{fn}: "
+			found = false
+			File::open( fn, "r" ) {|fh|
+				fh.each {|line|
+					if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*}
+						found = true
+						break
+					end
+				}
+			}
+
+			verboseMsg( (found ? "yes" : "no") + "\n" )
+			found
+		}
+	end
+
+
+	### Open a file and filter each of its lines through the given block a
+	### <tt>line</tt> at a time. The return value of the block is used as the
+	### new line, or omitted if the block returns <tt>nil</tt> or
+	### <tt>false</tt>.
+	def editInPlace( file, testMode=false ) # :yields: line
+		raise "No block specified for editing operation" unless block_given?
+
+		tempName = "#{file}.#{$$}"
+		File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile|
+			File::open( file, File::RDONLY ) {|fh|
+				fh.each {|line|
+					newline = yield( line ) or next
+					tempfile.print( newline )
+					$deferr.puts "%p -> %p" % [ line, newline ] if
+						line != newline
+				}
+			}
+		}
+
+		if testMode
+			File::unlink( tempName )
+		else
+			File::rename( tempName, file )
+		end
+	end
+
+
+	### Execute the specified shell <tt>command</tt>, read the results, and
+	### return them. Like a %x{} that returns an Array instead of a String.
+	def shellCommand( *command )
+		raise "Empty command" if command.empty?
+
+		cmdpipe = IO::popen( command.join(' '), 'r' )
+		return cmdpipe.readlines
+	end
+
+
+	### Execute a block with $VERBOSE set to +false+, restoring it to its
+	### previous value before returning.
+	def verboseOff
+		raise LocalJumpError, "No block given" unless block_given?
+
+		thrcrit = Thread.critical
+		oldverbose = $VERBOSE
+		begin
+			Thread.critical = true
+			$VERBOSE = false
+			yield
+		ensure
+			$VERBOSE = oldverbose
+			Thread.critical = false
+		end
+	end
+
+
+	### Try the specified code block, printing the given 
+	def try( msg, bind=TOPLEVEL_BINDING )
+		result = ''
+		if msg =~ /^to\s/
+			message "Trying #{msg}...\n"
+		else
+			message msg + "\n"
+		end
+			
+		begin
+			rval = nil
+			if block_given?
+				rval = yield
+			else
+				file, line = caller(1)[0].split(/:/,2)
+				rval = eval( msg, bind, file, line.to_i )
+			end
+
+			if $yaml
+				result = rval.to_yaml
+			else
+				PP.pp( rval, result )
+			end
+
+		rescue Exception => err
+			if err.backtrace
+				nicetrace = err.backtrace.delete_if {|frame|
+					/in `(try|eval)'/ =~ frame
+				}.join("\n\t")
+			else
+				nicetrace = "Exception had no backtrace"
+			end
+
+			result = err.message + "\n\t" + nicetrace
+		ensure
+			divider
+			message result.chomp + "\n"
+			divider
+			$deferr.puts
+		end
+	end
+end
+
+
+if __FILE__ == $0
+	# $DEBUG = true
+	include UtilityFunctions
+
+	projname = extractProjectName()
+	header "Project: #{projname}"
+
+	ver = extractVersion() || [0,0,1]
+	puts "Version: %s\n" % ver.join('.')
+
+	if File::directory?( "docs" )
+		puts "Rdoc:",
+			"  Title: " + findRdocTitle(),
+			"  Main: " + findRdocMain(),
+			"  Upload: " + findRdocUpload(),
+			"  SCCS URL: " + findRdocCvsURL(),
+			"  Accessors: " + findRdocAccessors().join(",")
+	end
+
+	puts "Manifest:",
+		"  " + getVettedManifest().join("\n  ")
+end