diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
index b13e8eb8e0ad2bfe0118f088091bb62d5c327d0b..55eb7e69d3c48cedce150f3588f5e4b140689902 100644
--- a/indra/llcommon/llprocess.cpp
+++ b/indra/llcommon/llprocess.cpp
@@ -26,6 +26,7 @@
 
 #include "linden_common.h"
 #include "llprocess.h"
+#include "llsdutil.h"
 #include "llsdserialize.h"
 #include "llsingleton.h"
 #include "llstring.h"
@@ -36,19 +37,20 @@
 
 #include <boost/foreach.hpp>
 #include <boost/bind.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/buffers_iterator.hpp>
 #include <iostream>
 #include <stdexcept>
+#include <limits>
+#include <algorithm>
+#include <vector>
+#include <typeinfo>
+#include <utility>
 
+static const char* whichfile[] = { "stdin", "stdout", "stderr" };
 static std::string empty;
 static LLProcess::Status interpret_status(int status);
 
-/// Need an exception to avoid constructing an invalid LLProcess object, but
-/// internal use only
-struct LLProcessError: public std::runtime_error
-{
-	LLProcessError(const std::string& msg): std::runtime_error(msg) {}
-};
-
 /**
  * Ref-counted "mainloop" listener. As long as there are still outstanding
  * LLProcess objects, keep listening on "mainloop" so we can keep polling APR
@@ -117,6 +119,154 @@ class LLProcessListener
 };
 static LLProcessListener sProcessListener;
 
+LLProcess::BasePipe::~BasePipe() {}
+
+class WritePipeImpl: public LLProcess::WritePipe
+{
+public:
+	WritePipeImpl(const std::string& desc, apr_file_t* pipe):
+		mDesc(desc),
+		mPipe(pipe),
+		// Essential to initialize our std::ostream with our special streambuf!
+		mStream(&mStreambuf)
+	{
+		mConnection = LLEventPumps::instance().obtain("mainloop")
+			.listen(LLEventPump::inventName("WritePipe"),
+					boost::bind(&WritePipeImpl::tick, this, _1));
+	}
+
+	virtual std::ostream& get_ostream() { return mStream; }
+
+	bool tick(const LLSD&)
+	{
+		// If there's anything to send, try to send it.
+		if (mStreambuf.size())
+		{
+			// Copy data out from mStreambuf to a flat, contiguous buffer to
+			// write -- but only up to a certain size.
+			std::streamsize total(mStreambuf.size());
+			std::streamsize bufsize((std::min)(4096, total));
+			boost::asio::streambuf::const_buffers_type bufs = mStreambuf.data();
+			std::vector<char> buffer(
+				boost::asio::buffers_begin(bufs),
+				boost::asio::buffers_begin(bufs) + bufsize);
+			apr_size_t written(bufsize);
+			ll_apr_warn_status(apr_file_write(mPipe, &buffer[0], &written));
+			// 'written' is modified to reflect the number of bytes actually
+			// written. Since they've been sent, remove them from the
+			// streambuf so we don't keep trying to send them. This could be
+			// anywhere from 0 up to mStreambuf.size(); anything we haven't
+			// yet sent, we'll try again next tick() call.
+			mStreambuf.consume(written);
+			LL_DEBUGS("LLProcess") << "wrote " << written << " of " << bufsize
+								   << " bytes to " << mDesc
+								   << " (original " << total << "), "
+								   << mStreambuf.size() << " remaining" << LL_ENDL;
+		}
+		return false;
+	}
+
+private:
+	std::string mDesc;
+	apr_file_t* mPipe;
+	LLTempBoundListener mConnection;
+	boost::asio::streambuf mStreambuf;
+	std::ostream mStream;
+};
+
+class ReadPipeImpl: public LLProcess::ReadPipe
+{
+public:
+	ReadPipeImpl(const std::string& desc, apr_file_t* pipe):
+		mDesc(desc),
+		mPipe(pipe),
+		// Essential to initialize our std::istream with our special streambuf!
+		mStream(&mStreambuf),
+		mPump("ReadPipe"),
+		// use funky syntax to call max() to avoid blighted max() macros
+		mLimit((std::numeric_limits<size_t>::max)())
+	{
+		mConnection = LLEventPumps::instance().obtain("mainloop")
+			.listen(LLEventPump::inventName("ReadPipe"),
+					boost::bind(&ReadPipeImpl::tick, this, _1));
+	}
+
+	// Much of the implementation is simply connecting the abstract virtual
+	// methods with implementation data concealed from the base class.
+	virtual std::istream& get_istream() { return mStream; }
+	virtual LLEventPump& getPump() { return mPump; }
+	virtual void setLimit(size_t limit) { mLimit = limit; }
+	virtual size_t getLimit() const { return mLimit; }
+
+private:
+	bool tick(const LLSD&)
+	{
+		// Allocate a buffer and try, every time, to read into it.
+		std::vector<char> buffer(4096);
+		apr_size_t gotten(buffer.size());
+		apr_status_t err = apr_file_read(mPipe, &buffer[0], &gotten);
+		if (err == APR_EOF)
+		{
+			// Handle EOF specially: it's part of normal-case processing.
+			LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL;
+			// We won't need any more tick() calls.
+			mConnection.disconnect();
+		}
+		else if (! ll_apr_warn_status(err)) // validate anything but EOF
+		{
+			// 'gotten' was modified to reflect the number of bytes actually
+			// received. If nonzero, add them to the streambuf and notify
+			// interested parties.
+			if (gotten)
+			{
+				boost::asio::streambuf::mutable_buffers_type mbufs = mStreambuf.prepare(gotten);
+				std::copy(buffer.begin(), buffer.begin() + gotten,
+						  boost::asio::buffers_begin(mbufs));
+				// Don't forget to "commit" the data! The sequence (prepare(),
+				// commit()) is obviously intended to allow us to allocate
+				// buffer space, then read directly into some portion of it,
+				// then commit only as much as we managed to obtain. But the
+				// only official (documented) way I can find to populate a
+				// mutable_buffers_type is to use buffers_begin(). It Would Be
+				// Nice if we were permitted to directly read into
+				// mutable_buffers_type (not to mention writing directly from
+				// const_buffers_type in WritePipeImpl; APR even supports an
+				// apr_file_writev() function for writing from discontiguous
+				// buffers) -- but as of 2012-02-14, this copying appears to
+				// be the safest tactic.
+				mStreambuf.commit(gotten);
+				LL_DEBUGS("LLProcess") << "read " << gotten << " of " << buffer.size()
+									   << " bytes from " << mDesc << ", new total "
+									   << mStreambuf.size() << LL_ENDL;
+
+				// Now that we've received new data, publish it on our
+				// LLEventPump as advertised. Constrain it by mLimit.
+				std::streamsize datasize((std::min)(mLimit, mStreambuf.size()));
+				boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data();
+				mPump.post(LLSDMap("data", LLSD::String(
+									   boost::asio::buffers_begin(cbufs),
+									   boost::asio::buffers_begin(cbufs) + datasize)));
+			}
+		}
+		return false;
+	}
+
+	std::string mDesc;
+	apr_file_t* mPipe;
+	LLTempBoundListener mConnection;
+	boost::asio::streambuf mStreambuf;
+	std::istream mStream;
+	LLEventStream mPump;
+	size_t mLimit;
+};
+
+/// Need an exception to avoid constructing an invalid LLProcess object, but
+/// internal use only
+struct LLProcessError: public std::runtime_error
+{
+	LLProcessError(const std::string& msg): std::runtime_error(msg) {}
+};
+
 LLProcessPtr LLProcess::create(const LLSDOrParams& params)
 {
 	try
@@ -134,12 +284,23 @@ LLProcessPtr LLProcess::create(const LLSDOrParams& params)
 /// throw LLProcessError mentioning the function call that produced that
 /// result.
 #define chkapr(func)                            \
-    if (ll_apr_warn_status(func))               \
-        throw LLProcessError(#func " failed")
+	if (ll_apr_warn_status(func))				\
+		throw LLProcessError(#func " failed")
 
 LLProcess::LLProcess(const LLSDOrParams& params):
-	mAutokill(params.autokill)
+	mAutokill(params.autokill),
+	mPipes(NSLOTS)
 {
+	// Hmm, when you construct a ptr_vector with a size, it merely reserves
+	// space, it doesn't actually make it that big. Explicitly make it bigger.
+	// Because of ptr_vector's odd semantics, have to push_back(0) the right
+	// number of times! resize() wants to default-construct new BasePipe
+	// instances, which fails because it's pure virtual. But because of the
+	// constructor call, these push_back() calls should require no new
+	// allocation.
+	for (size_t i = 0; i < mPipes.capacity(); ++i)
+		mPipes.push_back(0);
+
 	if (! params.validateBlock(true))
 	{
 		throw LLProcessError(STRINGIZE("not launched: failed parameter validation\n"
@@ -154,16 +315,46 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 	// apr_procattr_io_set() alternatives: inherit the viewer's own stdxxx
 	// handle (APR_NO_PIPE, e.g. for stdout, stderr), or create a pipe that's
 	// blocking on the child end but nonblocking at the viewer end
-	// (APR_CHILD_BLOCK). The viewer can't block for anything: the parent end
-	// MUST be nonblocking. As the APR documentation itself points out, it
-	// makes very little sense to set nonblocking I/O for the child end of a
-	// pipe: only a specially-written child could deal with that.
+	// (APR_CHILD_BLOCK).
 	// Other major options could include explicitly creating a single APR pipe
 	// and passing it as both stdout and stderr (apr_procattr_child_out_set(),
 	// apr_procattr_child_err_set()), or accepting a filename, opening it and
 	// passing that apr_file_t (simple <, >, 2> redirect emulation).
-//	chkapr(apr_procattr_io_set(procattr, APR_CHILD_BLOCK, APR_CHILD_BLOCK, APR_CHILD_BLOCK));
-	chkapr(apr_procattr_io_set(procattr, APR_NO_PIPE, APR_NO_PIPE, APR_NO_PIPE));
+	std::vector<FileParam> fparams(params.files.begin(), params.files.end());
+	// By default, pass APR_NO_PIPE for each slot.
+	std::vector<apr_int32_t> select(LL_ARRAY_SIZE(whichfile), APR_NO_PIPE);
+	for (size_t i = 0; i < (std::min)(LL_ARRAY_SIZE(whichfile), fparams.size()); ++i)
+	{
+		if (std::string(fparams[i].type).empty()) // inherit our file descriptor
+		{
+			select[i] = APR_NO_PIPE;
+		}
+		else if (std::string(fparams[i].type) == "pipe") // anonymous pipe
+		{
+			if (! std::string(fparams[i].name).empty())
+			{
+				LL_WARNS("LLProcess") << "For " << std::string(params.executable)
+									  << ": internal names for reusing pipes ('"
+									  << std::string(fparams[i].name) << "' for " << whichfile[i]
+									  << ") are not yet supported -- creating distinct pipe"
+									  << LL_ENDL;
+			}
+			// The viewer can't block for anything: the parent end MUST be
+			// nonblocking. As the APR documentation itself points out, it
+			// makes very little sense to set nonblocking I/O for the child
+			// end of a pipe: only a specially-written child could deal with
+			// that.
+			select[i] = APR_CHILD_BLOCK;
+		}
+		else
+		{
+			throw LLProcessError(STRINGIZE("For " << std::string(params.executable)
+										   << ": unsupported FileParam for " << whichfile[i]
+										   << ": type='" << std::string(fparams[i].type)
+										   << "', name='" << std::string(fparams[i].name) << "'"));
+		}
+	}
+	chkapr(apr_procattr_io_set(procattr, select[STDIN], select[STDOUT], select[STDERR]));
 
 	// Thumbs down on implicitly invoking the shell to invoke the child. From
 	// our point of view, the other major alternative to APR_PROGRAM_PATH
@@ -251,6 +442,27 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 		// On Windows, associate the new child process with our Job Object.
 		autokill();
 	}
+
+	// Instantiate the proper pipe I/O machinery
+	// want to be able to point to apr_proc_t::in, out, err by index
+	typedef apr_file_t* apr_proc_t::*apr_proc_file_ptr;
+	static apr_proc_file_ptr members[] =
+		{ &apr_proc_t::in, &apr_proc_t::out, &apr_proc_t::err };
+	for (size_t i = 0; i < NSLOTS; ++i)
+	{
+		if (select[i] != APR_CHILD_BLOCK)
+			continue;
+		if (i == STDIN)
+		{
+			mPipes.replace(i, new WritePipeImpl(whichfile[i], mProcess.*(members[i])));
+		}
+		else
+		{
+			mPipes.replace(i, new ReadPipeImpl(whichfile[i], mProcess.*(members[i])));
+		}
+		LL_DEBUGS("LLProcess") << "Instantiating " << typeid(mPipes[i]).name()
+							   << "('" << whichfile[i] << "')" << LL_ENDL;
+	}
 }
 
 LLProcess::~LLProcess()
@@ -428,6 +640,83 @@ LLProcess::handle LLProcess::getProcessHandle() const
 #endif
 }
 
+std::string LLProcess::getPipeName(FILESLOT)
+{
+	// LLProcess::FileParam::type "npipe" is not yet implemented
+	return "";
+}
+
+template<class PIPETYPE>
+PIPETYPE* LLProcess::getPipePtr(std::string& error, FILESLOT slot)
+{
+    if (slot >= NSLOTS)
+    {
+        error = STRINGIZE(mDesc << " has no slot " << slot);
+        return NULL;
+    }
+    if (mPipes.is_null(slot))
+    {
+        error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a monitored pipe");
+        return NULL;
+    }
+    // Make sure we dynamic_cast in pointer domain so we can test, rather than
+    // accepting runtime's exception.
+    PIPETYPE* ppipe = dynamic_cast<PIPETYPE*>(&mPipes[slot]);
+    if (! ppipe)
+    {
+        error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a " << typeid(PIPETYPE).name());
+        return NULL;
+    }
+
+    error.clear();
+    return ppipe;
+}
+
+template <class PIPETYPE>
+PIPETYPE& LLProcess::getPipe(FILESLOT slot)
+{
+    std::string error;
+    PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
+    if (! wp)
+    {
+        throw NoPipe(error);
+    }
+    return *wp;
+}
+
+template <class PIPETYPE>
+boost::optional<PIPETYPE&> LLProcess::getOptPipe(FILESLOT slot)
+{
+    std::string error;
+    PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
+    if (! wp)
+    {
+        LL_DEBUGS("LLProcess") << error << LL_ENDL;
+        return boost::optional<PIPETYPE&>();
+    }
+    return *wp;
+}
+
+LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot)
+{
+    return getPipe<WritePipe>(slot);
+}
+
+boost::optional<LLProcess::WritePipe&> LLProcess::getOptWritePipe(FILESLOT slot)
+{
+    return getOptPipe<WritePipe>(slot);
+}
+
+LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot)
+{
+    return getPipe<ReadPipe>(slot);
+}
+
+boost::optional<LLProcess::ReadPipe&> LLProcess::getOptReadPipe(FILESLOT slot)
+{
+    return getOptPipe<ReadPipe>(slot);
+}
+
 std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params)
 {
 	std::string cwd(params.cwd);
diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h
index b95ae55701e764121421892a2c5083db69dfe14a..448a88f4c0297afe26d454c4c0ddbbd621f31847 100644
--- a/indra/llcommon/llprocess.h
+++ b/indra/llcommon/llprocess.h
@@ -31,8 +31,11 @@
 #include "llsdparam.h"
 #include "apr_thread_proc.h"
 #include <boost/shared_ptr.hpp>
+#include <boost/ptr_container/ptr_vector.hpp>
+#include <boost/optional.hpp>
 #include <boost/noncopyable.hpp>
 #include <iosfwd>                   // std::ostream
+#include <stdexcept>
 
 #if LL_WINDOWS
 #define WIN32_LEAN_AND_MEAN
@@ -43,6 +46,8 @@
 #endif
 #endif
 
+class LLEventPump;
+
 class LLProcess;
 /// LLProcess instances are created on the heap by static factory methods and
 /// managed by ref-counted pointers.
@@ -64,6 +69,87 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 {
 	LOG_CLASS(LLProcess);
 public:
+	/**
+	 * Specify what to pass for each of child stdin, stdout, stderr.
+	 * @see LLProcess::Params::files.
+	 */
+	struct FileParam: public LLInitParam::Block<FileParam>
+	{
+		/**
+		 * type of file handle to pass to child process
+		 *
+		 * - "" (default): let the child inherit the same file handle used by
+		 *   this process. For instance, if passed as stdout, child stdout
+		 *   will be interleaved with stdout from this process. In this case,
+		 *   @a name is moot and should be left "".
+		 *
+		 * - "file": open an OS filesystem file with the specified @a name.
+		 *   <i>Not yet implemented.</i>
+		 *
+		 * - "pipe" or "tpipe" or "npipe": depends on @a name
+		 *
+		 *   - @a name.empty(): construct an OS pipe used only for this slot
+		 *     of the forthcoming child process.
+		 *
+		 *   - ! @a name.empty(): in a global registry, find or create (using
+		 *     the specified @a name) an OS pipe. The point of the (purely
+		 *     internal) @a name is that passing the same @a name in more than
+		 *     one slot for a given LLProcess -- or for slots in different
+		 *     LLProcess instances -- means the same pipe. For example, you
+		 *     might pass the same @a name value as both stdout and stderr to
+		 *     make the child process produce both on the same actual pipe. Or
+		 *     you might pass the same @a name as the stdout for one LLProcess
+		 *     and the stdin for another to connect the two child processes.
+		 *     Use LLProcess::getPipeName() to generate a unique name
+		 *     guaranteed not to already exist in the registry. <i>Not yet
+		 *     implemented.</i>
+		 *
+		 *   The difference between "pipe", "tpipe" and "npipe" is as follows.
+		 *
+		 *   - "pipe": direct LLProcess to monitor the parent end of the pipe,
+		 *     pumping nonblocking I/O every frame. The expectation (at least
+		 *     for stdout or stderr) is that the caller will listen for
+		 *     incoming data and consume it as it arrives. It's important not
+		 *     to neglect such a pipe, because it's buffered in viewer memory.
+		 *     If you suspect the child may produce a great volume of output
+		 *     between viewer frames, consider directing the child to write to
+		 *     a filesystem file instead, then read the file later.
+		 *
+		 *   - "tpipe": do not engage LLProcess machinery to monitor the
+		 *     parent end of the pipe. A "tpipe" is used only to connect
+		 *     different child processes. As such, it makes little sense to
+		 *     pass an empty @a name. <i>Not yet implemented.</i>
+		 *
+		 *   - "npipe": like "tpipe", but use an OS named pipe with a
+		 *     generated name. Note that @a name is the @em internal name of
+		 *     the pipe in our global registry -- it doesn't necessarily have
+		 *     anything to do with the pipe's name in the OS filesystem. Use
+		 *     LLProcess::getPipeName() to obtain the named pipe's OS
+		 *     filesystem name, e.g. to pass it as the @a name to another
+		 *     LLProcess instance using @a type "file". This supports usage
+		 *     like bash's &lt;(subcommand...) or &gt;(subcommand...)
+		 *     constructs. <i>Not yet implemented.</i>
+		 *
+		 * In all cases the open mode (read, write) is determined by the child
+		 * slot you're filling. Child stdin means select the "read" end of a
+		 * pipe, or open a filesystem file for reading; child stdout or stderr
+		 * means select the "write" end of a pipe, or open a filesystem file
+		 * for writing.
+		 *
+		 * Confusion such as passing the same pipe as the stdin of two
+		 * processes (rather than stdout for one and stdin for the other) is
+		 * explicitly permitted: it's up to the caller to construct meaningful
+		 * LLProcess pipe graphs.
+		 */
+		Optional<std::string> type;
+		Optional<std::string> name;
+
+		FileParam(const std::string& tp="", const std::string& nm=""):
+			type("type", tp),
+			name("name", nm)
+		{}
+	};
+
 	/// Param block definition
 	struct Params: public LLInitParam::Block<Params>
 	{
@@ -71,7 +157,8 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 			executable("executable"),
 			args("args"),
 			cwd("cwd"),
-			autokill("autokill", true)
+			autokill("autokill", true),
+			files("files")
 		{}
 
 		/// pathname of executable
@@ -87,19 +174,22 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 		Optional<std::string> cwd;
 		/// implicitly kill process on destruction of LLProcess object
 		Optional<bool> autokill;
+		/**
+		 * Up to three FileParam items: for child stdin, stdout, stderr.
+		 * Passing two FileParam entries means default treatment for stderr,
+		 * and so forth.
+		 *
+		 * @note While it's theoretically plausible to pass additional open
+		 * file handles to a child specifically written to expect them, our
+		 * underlying implementation library doesn't support that.
+		 */
+		Multiple<FileParam> files;
 	};
 	typedef LLSDParamAdapter<Params> LLSDOrParams;
 
 	/**
 	 * Factory accepting either plain LLSD::Map or Params block.
 	 * MAY RETURN DEFAULT-CONSTRUCTED LLProcessPtr if params invalid!
-	 *
-	 * Redundant with Params definition above?
-	 *
-	 * executable (required, string):				executable pathname
-	 * args		  (optional, string array):			extra command-line arguments
-	 * cwd		  (optional, string, dft no chdir): change to this directory before executing
-	 * autokill	  (optional, bool, dft true):		implicit kill() on ~LLProcess
 	 */
 	static LLProcessPtr create(const LLSDOrParams& params);
 	virtual ~LLProcess();
@@ -190,6 +280,125 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 	 */
 	static handle isRunning(handle, const std::string& desc="");
 
+	/// Provide symbolic access to child's file slots
+	enum FILESLOT { STDIN=0, STDOUT=1, STDERR=2, NSLOTS=3 };
+
+	/**
+	 * For a pipe constructed with @a type "npipe", obtain the generated OS
+	 * filesystem name for the specified pipe. Otherwise returns the empty
+	 * string. @see LLProcess::FileParam::type
+	 */
+	std::string getPipeName(FILESLOT);
+
+	/// base of ReadPipe, WritePipe
+	class BasePipe
+	{
+	public:
+		virtual ~BasePipe() = 0;
+	};
+
+	/// As returned by getWritePipe() or getOptWritePipe()
+	class WritePipe: public BasePipe
+	{
+	public:
+		/**
+		 * Get ostream& on which to write to child's stdin.
+		 *
+		 * @usage
+		 * @code
+		 * myProcess->getWritePipe().get_ostream() << "Hello, child!" << std::endl;
+		 * @endcode
+		 */
+		virtual std::ostream& get_ostream() = 0;
+	};
+
+	/// As returned by getReadPipe() or getOptReadPipe()
+	class ReadPipe: public BasePipe
+	{
+	public:
+		/**
+		 * Get istream& on which to read from child's stdout or stderr.
+		 *
+		 * @usage
+		 * @code
+		 * std::string stuff;
+		 * myProcess->getReadPipe().get_istream() >> stuff;
+		 * @endcode
+		 *
+		 * You should be sure in advance that the ReadPipe in question can
+		 * fill the request. @see getPump()
+		 */
+		virtual std::istream& get_istream() = 0;
+
+		/**
+		 * Get LLEventPump& on which to listen for incoming data. The posted
+		 * LLSD::Map event will contain a key "data" whose value is an
+		 * LLSD::String containing (part of) the data accumulated in the
+		 * buffer.
+		 *
+		 * If the child sends "abc", and this ReadPipe posts "data"="abc", but
+		 * you don't consume it by reading the std::istream returned by
+		 * get_istream(), and the child next sends "def", ReadPipe will post
+		 * "data"="abcdef".
+		 */
+		virtual LLEventPump& getPump() = 0;
+
+		/**
+		 * Set maximum length of buffer data that will be posted in the LLSD
+		 * announcing arrival of new data from the child. If you call
+		 * setLimit(5), and the child sends "abcdef", the LLSD event will
+		 * contain "data"="abcde". However, you may still read the entire
+		 * "abcdef" from get_istream(): this limit affects only the size of
+		 * the data posted with the LLSD event. If you don't call this method,
+		 * all pending data will be posted.
+		 */
+		virtual void setLimit(size_t limit) = 0;
+
+		/**
+		 * Query the current setLimit() limit.
+		 */
+		virtual size_t getLimit() const = 0;
+	};
+
+	/// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to
+	/// create a pipe at the corresponding FILESLOT.
+	struct NoPipe: public std::runtime_error
+	{
+		NoPipe(const std::string& what): std::runtime_error(what) {}
+	};
+
+	/**
+	 * Get a reference to the (only) WritePipe for this LLProcess. @a slot, if
+	 * specified, must be STDIN. Throws NoPipe if you did not request a "pipe"
+	 * for child stdin. Use this method when you know how you created the
+	 * LLProcess in hand.
+	 */
+	WritePipe& getWritePipe(FILESLOT slot=STDIN);
+
+	/**
+	 * Get a boost::optional<WritePipe&> to the (only) WritePipe for this
+	 * LLProcess. @a slot, if specified, must be STDIN. The return value is
+	 * empty if you did not request a "pipe" for child stdin. Use this method
+	 * for inspecting an LLProcess you did not create.
+	 */
+	boost::optional<WritePipe&> getOptWritePipe(FILESLOT slot=STDIN);
+
+	/**
+	 * Get a reference to one of the ReadPipes for this LLProcess. @a slot, if
+	 * specified, must be STDOUT or STDERR. Throws NoPipe if you did not
+	 * request a "pipe" for child stdout or stderr. Use this method when you
+	 * know how you created the LLProcess in hand.
+	 */
+	ReadPipe& getReadPipe(FILESLOT slot);
+
+	/**
+	 * Get a boost::optional<ReadPipe&> to one of the ReadPipes for this
+	 * LLProcess. @a slot, if specified, must be STDOUT or STDERR. The return
+	 * value is empty if you did not request a "pipe" for child stdout or
+	 * stderr. Use this method for inspecting an LLProcess you did not create.
+	 */
+	boost::optional<ReadPipe&> getOptReadPipe(FILESLOT slot);
+
 private:
 	/// constructor is private: use create() instead
 	LLProcess(const LLSDOrParams& params);
@@ -198,11 +407,21 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 	static void status_callback(int reason, void* data, int status);
 	// Object-oriented callback
 	void handle_status(int reason, int status);
+	// implementation for get[Opt][Read|Write]Pipe()
+	template <class PIPETYPE>
+	PIPETYPE& getPipe(FILESLOT slot);
+	template <class PIPETYPE>
+	boost::optional<PIPETYPE&> getOptPipe(FILESLOT slot);
+	template <class PIPETYPE>
+	PIPETYPE* getPipePtr(std::string& error, FILESLOT slot);
 
 	std::string mDesc;
 	apr_proc_t mProcess;
 	bool mAutokill;
 	Status mStatus;
+	// explicitly want this ptr_vector to be able to store NULLs
+	typedef boost::ptr_vector< boost::nullable<BasePipe> > PipeVector;
+	PipeVector mPipes;
 };
 
 /// for logging
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
index 8c21be196bb2df39b3d63334874f43d9d60edc68..d4e9977e639ba85205bbb9d0467bef0b7d561780 100644
--- a/indra/llcommon/tests/llprocess_test.cpp
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -34,6 +34,7 @@
 #include "stringize.h"
 #include "llsdutil.h"
 #include "llevents.h"
+#include "llerrorcontrol.h"
 
 #if defined(LL_WINDOWS)
 #define sleep(secs) _sleep((secs) * 1000)
@@ -92,12 +93,18 @@ static std::string readfile(const std::string& pathname, const std::string& desc
 /// Looping on LLProcess::isRunning() must now be accompanied by pumping
 /// "mainloop" -- otherwise the status won't update and you get an infinite
 /// loop.
+void yield(int seconds=1)
+{
+    // This function simulates waiting for another viewer frame
+    sleep(seconds);
+    LLEventPumps::instance().obtain("mainloop").post(LLSD());
+}
+
 void waitfor(LLProcess& proc)
 {
     while (proc.isRunning())
     {
-        sleep(1);
-        LLEventPumps::instance().obtain("mainloop").post(LLSD());
+        yield();
     }
 }
 
@@ -105,8 +112,7 @@ void waitfor(LLProcess::handle h, const std::string& desc)
 {
     while (LLProcess::isRunning(h, desc))
     {
-        sleep(1);
-        LLEventPumps::instance().obtain("mainloop").post(LLSD());
+        yield();
     }
 }
 
@@ -219,6 +225,68 @@ class NamedTempDir: public boost::noncopyable
     std::string mPath;
 };
 
+// statically reference the function in test.cpp... it's short, we could
+// replicate, but better to reuse
+extern void wouldHaveCrashed(const std::string& message);
+
+/**
+ * Capture log messages. This is adapted (simplified) from the one in
+ * llerror_test.cpp. Sigh, should've broken that out into a separate header
+ * file, but time for this project is short...
+ */
+class TestRecorder : public LLError::Recorder
+{
+public:
+    TestRecorder():
+        // Mostly what we're trying to accomplish by saving and resetting
+        // LLError::Settings is to bypass the default RecordToStderr and
+        // RecordToWinDebug Recorders. As these are visible only inside
+        // llerror.cpp, we can't just call LLError::removeRecorder() with
+        // each. For certain tests we need to produce, capture and examine
+        // DEBUG log messages -- but we don't want to spam the user's console
+        // with that output. If it turns out that saveAndResetSettings() has
+        // some bad effect, give up and just let the DEBUG level log messages
+        // display.
+        mOldSettings(LLError::saveAndResetSettings())
+    {
+        LLError::setFatalFunction(wouldHaveCrashed);
+        LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
+        LLError::addRecorder(this);
+    }
+
+    ~TestRecorder()
+    {
+        LLError::removeRecorder(this);
+        LLError::restoreSettings(mOldSettings);
+    }
+
+    void recordMessage(LLError::ELevel level,
+                       const std::string& message)
+    {
+        mMessages.push_back(message);
+    }
+
+    /// Don't assume the message we want is necessarily the LAST log message
+    /// emitted by the underlying code; search backwards through all messages
+    /// for the sought string.
+    std::string messageWith(const std::string& search)
+    {
+        for (std::list<std::string>::const_reverse_iterator rmi(mMessages.rbegin()),
+                 rmend(mMessages.rend());
+             rmi != rmend; ++rmi)
+        {
+            if (rmi->find(search) != std::string::npos)
+                return *rmi;
+        }
+        // failed to find any such message
+        return std::string();
+    }
+
+    typedef std::list<std::string> MessageList;
+    MessageList mMessages;
+    LLError::Settings* mOldSettings;
+};
+
 /*****************************************************************************
 *   TUT
 *****************************************************************************/
@@ -602,9 +670,19 @@ namespace tut
         set_test_name("syntax_error:");
         PythonProcessLauncher py("syntax_error:",
                                  "syntax_error:\n");
+        py.mParams.files.add(LLProcess::FileParam()); // inherit stdin
+        py.mParams.files.add(LLProcess::FileParam()); // inherit stdout
+        py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for stderr
         py.run();
         ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED);
         ensure_equals("Status.mData",  py.mPy->getStatus().mData,  1);
+        std::istream& rpipe(py.mPy->getReadPipe(LLProcess::STDERR).get_istream());
+        std::vector<char> buffer(4096);
+        rpipe.read(&buffer[0], buffer.size());
+        std::streamsize got(rpipe.gcount());
+        ensure("Nothing read from stderr pipe", got);
+        std::string data(&buffer[0], got);
+        ensure("Didn't find 'SyntaxError:'", data.find("\nSyntaxError:") != std::string::npos);
     }
 
     template<> template<>
@@ -629,7 +707,7 @@ namespace tut
         int i = 0, timeout = 60;
         for ( ; i < timeout; ++i)
         {
-            sleep(1);
+            yield();
             if (readfile(out.getName(), "from kill() script") == "ok")
                 break;
         }
@@ -678,7 +756,7 @@ namespace tut
             int i = 0, timeout = 60;
             for ( ; i < timeout; ++i)
             {
-                sleep(1);
+                yield();
                 if (readfile(out.getName(), "from kill() script") == "ok")
                     break;
             }
@@ -733,7 +811,7 @@ namespace tut
             int i = 0, timeout = 60;
             for ( ; i < timeout; ++i)
             {
-                sleep(1);
+                yield();
                 if (readfile(from.getName(), "from autokill script") == "ok")
                     break;
             }
@@ -742,7 +820,7 @@ namespace tut
             // Now destroy the LLProcess, which should NOT kill the child!
         }
         // If the destructor killed the child anyway, give it time to die
-        sleep(2);
+        yield(2);
         // How do we know it's not terminated? By making it respond to
         // a specific stimulus in a specific way.
         {
@@ -755,4 +833,81 @@ namespace tut
         // script could not have written 'ack' as we expect.
         ensure_equals("autokill script output", readfile(from.getName()), "ack");
     }
+
+    template<> template<>
+    void object::test<10>()
+    {
+        set_test_name("'bogus' test");
+        TestRecorder recorder;
+        PythonProcessLauncher py("'bogus' test",
+                                 "print 'Hello world'\n");
+        py.mParams.files.add(LLProcess::FileParam("bogus"));
+        py.mPy = LLProcess::create(py.mParams);
+        ensure("should have rejected 'bogus'", ! py.mPy);
+        std::string message(recorder.messageWith("bogus"));
+        ensure("did not log 'bogus' type", ! message.empty());
+        ensure_contains("did not name 'stdin'", message, "stdin");
+    }
+
+    template<> template<>
+    void object::test<11>()
+    {
+        set_test_name("'file' test");
+        // Replace this test with one or more real 'file' tests when we
+        // implement 'file' support
+        PythonProcessLauncher py("'file' test",
+                                 "print 'Hello world'\n");
+        py.mParams.files.add(LLProcess::FileParam());
+        py.mParams.files.add(LLProcess::FileParam("file"));
+        py.mPy = LLProcess::create(py.mParams);
+        ensure("should have rejected 'file'", ! py.mPy);
+    }
+
+    template<> template<>
+    void object::test<12>()
+    {
+        set_test_name("'tpipe' test");
+        // Replace this test with one or more real 'tpipe' tests when we
+        // implement 'tpipe' support
+        TestRecorder recorder;
+        PythonProcessLauncher py("'tpipe' test",
+                                 "print 'Hello world'\n");
+        py.mParams.files.add(LLProcess::FileParam());
+        py.mParams.files.add(LLProcess::FileParam("tpipe"));
+        py.mPy = LLProcess::create(py.mParams);
+        ensure("should have rejected 'tpipe'", ! py.mPy);
+        std::string message(recorder.messageWith("tpipe"));
+        ensure("did not log 'tpipe' type", ! message.empty());
+        ensure_contains("did not name 'stdout'", message, "stdout");
+    }
+
+    template<> template<>
+    void object::test<13>()
+    {
+        set_test_name("'npipe' test");
+        // Replace this test with one or more real 'npipe' tests when we
+        // implement 'npipe' support
+        TestRecorder recorder;
+        PythonProcessLauncher py("'npipe' test",
+                                 "print 'Hello world'\n");
+        py.mParams.files.add(LLProcess::FileParam());
+        py.mParams.files.add(LLProcess::FileParam());
+        py.mParams.files.add(LLProcess::FileParam("npipe"));
+        py.mPy = LLProcess::create(py.mParams);
+        ensure("should have rejected 'npipe'", ! py.mPy);
+        std::string message(recorder.messageWith("npipe"));
+        ensure("did not log 'npipe' type", ! message.empty());
+        ensure_contains("did not name 'stderr'", message, "stderr");
+    }
+
+    // TODO:
+    // test "pipe" with nonempty name (should log & continue)
+    // test pipe for just stderr (tests for get[Opt]ReadPipe(), get[Opt]WritePipe())
+    // test pipe for stdin, stdout (etc.)
+    // test getWritePipe().get_ostream(), getReadPipe().get_istream()
+    // test listening on getReadPipe().getPump(), disconnecting
+    // test setLimit(), getLimit()
+    // test EOF -- check logging
+    // test get(Read|Write)Pipe(3), unmonitored slot, getReadPipe(1), getWritePipe(0)
+
 } // namespace tut