diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index a1aa887d3af49953bfb0b94f29000c185f303f61..3255e28e8edf56b34b8aa8b7f22f8eae67554df9 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -75,7 +75,7 @@ set(llcommon_SOURCE_FILES
     llmortician.cpp
     lloptioninterface.cpp
     llptrto.cpp 
-    llprocesslauncher.cpp
+    llprocess.cpp
     llprocessor.cpp
     llqueuedthread.cpp
     llrand.cpp
@@ -90,6 +90,7 @@ set(llcommon_SOURCE_FILES
     llsingleton.cpp
     llstat.cpp
     llstacktrace.cpp
+    llstreamqueue.cpp
     llstreamtools.cpp
     llstring.cpp
     llstringtable.cpp
@@ -199,7 +200,7 @@ set(llcommon_HEADER_FILES
     llpointer.h
     llpreprocessor.h
     llpriqueuemap.h
-    llprocesslauncher.h
+    llprocess.h
     llprocessor.h
     llptrskiplist.h
     llptrskipmap.h
@@ -226,6 +227,7 @@ set(llcommon_HEADER_FILES
     llstat.h
     llstatenums.h
     llstl.h
+    llstreamqueue.h
     llstreamtools.h
     llstrider.h
     llstring.h
@@ -331,6 +333,9 @@ if (LL_TESTS)
   LL_ADD_INTEGRATION_TEST(reflection "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(llprocess "" "${test_libs}"
+                          "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/setpython.py")
+  LL_ADD_INTEGRATION_TEST(llstreamqueue "" "${test_libs}")
 
   # *TODO - reenable these once tcmalloc libs no longer break the build.
   #ADD_BUILD_TEST(llallocator llcommon)
diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c0caca68045b54ddf1838b51d878092f8899cee
--- /dev/null
+++ b/indra/llcommon/llprocess.cpp
@@ -0,0 +1,338 @@
+/** 
+ * @file llprocess.cpp
+ * @brief Utility class for launching, terminating, and tracking the state of processes.
+ *
+ * $LicenseInfo:firstyear=2008&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ * 
+ * This library 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 this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+#include "llprocess.h"
+#include "llsd.h"
+#include "llsdserialize.h"
+#include "stringize.h"
+
+#include <boost/foreach.hpp>
+#include <iostream>
+#include <stdexcept>
+
+/// 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 LLSD& params)
+{
+	try
+	{
+		return LLProcessPtr(new LLProcess(params));
+	}
+	catch (const LLProcessError& e)
+	{
+		LL_WARNS("LLProcess") << e.what() << LL_ENDL;
+		return LLProcessPtr();
+	}
+}
+
+LLProcess::LLProcess(const LLSD& params):
+	mProcessID(0),
+	mAutokill(params["autokill"].asBoolean())
+{
+	// nonstandard default bool value
+	if (! params.has("autokill"))
+		mAutokill = true;
+	if (! params.has("executable"))
+	{
+		throw LLProcessError(STRINGIZE("not launched: missing 'executable'\n"
+									   << LLSDNotationStreamer(params)));
+	}
+
+	launch(params);
+}
+
+LLProcess::~LLProcess()
+{
+	if (mAutokill)
+	{
+		kill();
+	}
+}
+
+bool LLProcess::isRunning(void)
+{
+	mProcessID = isRunning(mProcessID);
+	return (mProcessID != 0);
+}
+
+#if LL_WINDOWS
+
+static std::string quote(const std::string& str)
+{
+	std::string::size_type len(str.length());
+	// If the string is already quoted, assume user knows what s/he's doing.
+	if (len >= 2 && str[0] == '"' && str[len-1] == '"')
+	{
+		return str;
+	}
+
+	// Not already quoted: do it.
+	std::string result("\"");
+	for (std::string::const_iterator ci(str.begin()), cend(str.end()); ci != cend; ++ci)
+	{
+		if (*ci == '"')
+		{
+			result.append("\\");
+		}
+		result.push_back(*ci);
+	}
+	return result + "\"";
+}
+
+void LLProcess::launch(const LLSD& params)
+{
+	PROCESS_INFORMATION pinfo;
+	STARTUPINFOA sinfo;
+	memset(&sinfo, 0, sizeof(sinfo));
+	
+	std::string args = quote(params["executable"]);
+	BOOST_FOREACH(const std::string& arg, llsd::inArray(params["args"]))
+	{
+		args += " ";
+		args += quote(arg);
+	}
+	
+	// So retarded.  Windows requires that the second parameter to
+	// CreateProcessA be a writable (non-const) string...
+	std::vector<char> args2(args.begin(), args.end());
+	args2.push_back('\0');
+
+	// Convert wrapper to a real std::string so we can use c_str(); but use a
+	// named variable instead of a temporary so c_str() pointer remains valid.
+	std::string cwd(params["cwd"]);
+	const char * working_directory = 0;
+	if (! cwd.empty())
+		working_directory = cwd.c_str();
+	if( ! CreateProcessA( NULL, &args2[0], NULL, NULL, FALSE, 0, NULL, working_directory, &sinfo, &pinfo ) )
+	{
+		int result = GetLastError();
+
+		LPTSTR error_str = 0;
+		if(
+			FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+				NULL,
+				result,
+				0,
+				(LPTSTR)&error_str,
+				0,
+				NULL) 
+			!= 0) 
+		{
+			char message[256];
+			wcstombs(message, error_str, sizeof(message));
+			message[sizeof(message)-1] = 0;
+			LocalFree(error_str);
+			throw LLProcessError(STRINGIZE("CreateProcessA failed (" << result << "): "
+										   << message));
+		}
+		throw LLProcessError(STRINGIZE("CreateProcessA failed (" << result
+									   << "), but FormatMessage() did not explain"));
+	}
+
+	// foo = pinfo.dwProcessId; // get your pid here if you want to use it later on
+	// CloseHandle(pinfo.hProcess); // stops leaks - nothing else
+	mProcessID = pinfo.hProcess;
+	CloseHandle(pinfo.hThread); // stops leaks - nothing else
+}
+
+LLProcess::id LLProcess::isRunning(id handle)
+{
+	if (! handle)
+		return 0;
+
+	DWORD waitresult = WaitForSingleObject(handle, 0);
+	if(waitresult == WAIT_OBJECT_0)
+	{
+		// the process has completed.
+		return 0;
+	}
+
+	return handle;
+}
+
+bool LLProcess::kill(void)
+{
+	if (! mProcessID)
+		return false;
+
+	TerminateProcess(mProcessID, 0);
+	return ! isRunning();
+}
+
+#else // Mac and linux
+
+#include <signal.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+// Attempt to reap a process ID -- returns true if the process has exited and been reaped, false otherwise.
+static bool reap_pid(pid_t pid)
+{
+	pid_t wait_result = ::waitpid(pid, NULL, WNOHANG);
+	if (wait_result == pid)
+	{
+		return true;
+	}
+	if (wait_result == -1 && errno == ECHILD)
+	{
+		// No such process -- this may mean we're ignoring SIGCHILD.
+		return true;
+	}
+	
+	return false;
+}
+
+void LLProcess::launch(const LLSD& params)
+{
+	// flush all buffers before the child inherits them
+	::fflush(NULL);
+
+	pid_t child = vfork();
+	if (child == 0)
+	{
+		// child process
+
+		std::string cwd(params["cwd"]);
+		if (! cwd.empty())
+		{
+			// change to the desired child working directory
+			if (::chdir(cwd.c_str()))
+			{
+				// chdir failed
+				LL_WARNS("LLProcess") << "could not chdir(\"" << cwd << "\")" << LL_ENDL;
+				// pointless to throw; this is child process...
+				_exit(248);
+			}
+		}
+
+		// create an argv vector for the child process
+		std::vector<const char*> fake_argv;
+
+		// add the executable path
+		std::string executable(params["executable"]);
+		fake_argv.push_back(executable.c_str());
+
+		// and any arguments
+		const LLSD& params_args(params["args"]);
+		std::vector<std::string> args(params_args.beginArray(), params_args.endArray());
+		BOOST_FOREACH(const std::string& arg, args)
+		{
+			fake_argv.push_back(arg.c_str());
+		}
+
+		// terminate with a null pointer
+		fake_argv.push_back(NULL);
+
+		::execv(executable.c_str(), const_cast<char* const*>(&fake_argv[0]));
+
+		// If we reach this point, the exec failed.
+		LL_WARNS("LLProcess") << "failed to launch: ";
+		BOOST_FOREACH(const char* arg, fake_argv)
+		{
+			LL_CONT << arg << ' ';
+		}
+		LL_CONT << LL_ENDL;
+		// Use _exit() instead of exit() per the vfork man page. Exit with a
+		// distinctive rc: someday soon we'll be able to retrieve it, and it
+		// would be nice to be able to tell that the child process failed!
+		_exit(249);
+	}
+
+	// parent process
+	mProcessID = child;
+}
+
+LLProcess::id LLProcess::isRunning(id pid)
+{
+	if (! pid)
+		return 0;
+
+	// Check whether the process has exited, and reap it if it has.
+	if(reap_pid(pid))
+	{
+		// the process has exited.
+		return 0;
+	}
+
+	return pid;
+}
+
+bool LLProcess::kill(void)
+{
+	if (! mProcessID)
+		return false;
+
+	// Try to kill the process. We'll do approximately the same thing whether
+	// the kill returns an error or not, so we ignore the result.
+	(void)::kill(mProcessID, SIGTERM);
+
+	// This will have the side-effect of reaping the zombie if the process has exited.
+	return ! isRunning();
+}
+
+/*==========================================================================*|
+static std::list<pid_t> sZombies;
+
+void LLProcess::orphan(void)
+{
+	// Disassociate the process from this object
+	if(mProcessID != 0)
+	{	
+		// We may still need to reap the process's zombie eventually
+		sZombies.push_back(mProcessID);
+	
+		mProcessID = 0;
+	}
+}
+
+// static 
+void LLProcess::reap(void)
+{
+	// Attempt to real all saved process ID's.
+	
+	std::list<pid_t>::iterator iter = sZombies.begin();
+	while(iter != sZombies.end())
+	{
+		if(reap_pid(*iter))
+		{
+			iter = sZombies.erase(iter);
+		}
+		else
+		{
+			iter++;
+		}
+	}
+}
+|*==========================================================================*/
+
+#endif
diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h
new file mode 100644
index 0000000000000000000000000000000000000000..9a74cfe8291afae70a547aa2f305c67ee4ab5f1e
--- /dev/null
+++ b/indra/llcommon/llprocess.h
@@ -0,0 +1,106 @@
+/** 
+ * @file llprocess.h
+ * @brief Utility class for launching, terminating, and tracking child processes.
+ *
+ * $LicenseInfo:firstyear=2008&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ * 
+ * This library 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 this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+ 
+#ifndef LL_LLPROCESS_H
+#define LL_LLPROCESS_H
+
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+
+#if LL_WINDOWS
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+class LLSD;
+
+class LLProcess;
+/// LLProcess instances are created on the heap by static factory methods and
+/// managed by ref-counted pointers.
+typedef boost::shared_ptr<LLProcess> LLProcessPtr;
+
+/**
+ *	LLProcess handles launching external processes with specified command line arguments.
+ *	It also keeps track of whether the process is still running, and can kill it if required.
+*/
+class LL_COMMON_API LLProcess: public boost::noncopyable
+{
+	LOG_CLASS(LLProcess);
+public:
+
+	/**
+	 * Factory accepting LLSD::Map.
+	 * MAY RETURN DEFAULT-CONSTRUCTED LLProcessPtr if params invalid!
+	 *
+	 * 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 LLSD& params);
+	virtual ~LLProcess();
+
+	// isRunning isn't const because, if child isn't running, it clears stored
+	// process ID
+	bool isRunning(void);
+	
+	// Attempt to kill the process -- returns true if the process is no longer running when it returns.
+	// Note that even if this returns false, the process may exit some time after it's called.
+	bool kill(void);
+
+#if LL_WINDOWS
+	typedef HANDLE id;
+#else
+	typedef pid_t  id;
+#endif	
+	/// Get platform-specific process ID
+	id getProcessID() const { return mProcessID; };
+
+	/**
+	 * Test if a process (id obtained from getProcessID()) is still
+	 * running. Return is same nonzero id value if still running, else
+	 * zero, so you can test it like a bool. But if you want to update a
+	 * stored variable as a side effect, you can write code like this:
+	 * @code
+	 * childpid = LLProcess::isRunning(childpid);
+	 * @endcode
+	 * @note This method is intended as a unit-test hook, not as the first of
+	 * a whole set of operations supported on freestanding @c id values. New
+	 * functionality should be added as nonstatic members operating on
+	 * mProcessID.
+	 */
+	static id isRunning(id);
+	
+private:
+	/// constructor is private: use create() instead
+	LLProcess(const LLSD& params);
+	void launch(const LLSD& params);
+
+	id mProcessID;
+	bool mAutokill;
+};
+
+#endif // LL_LLPROCESS_H
diff --git a/indra/llcommon/llprocesslauncher.cpp b/indra/llcommon/llprocesslauncher.cpp
deleted file mode 100644
index 10950181fd30bf0f182d19e5954a3847eb8ec1a7..0000000000000000000000000000000000000000
--- a/indra/llcommon/llprocesslauncher.cpp
+++ /dev/null
@@ -1,357 +0,0 @@
-/** 
- * @file llprocesslauncher.cpp
- * @brief Utility class for launching, terminating, and tracking the state of processes.
- *
- * $LicenseInfo:firstyear=2008&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2010, Linden Research, Inc.
- * 
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation;
- * version 2.1 of the License only.
- * 
- * This library 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 this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- * 
- * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
- * $/LicenseInfo$
- */
- 
-#include "linden_common.h"
-
-#include "llprocesslauncher.h"
-
-#include <iostream>
-#if LL_DARWIN || LL_LINUX
-// not required or present on Win32
-#include <sys/wait.h>
-#endif
-
-LLProcessLauncher::LLProcessLauncher()
-{
-#if LL_WINDOWS
-	mProcessHandle = 0;
-#else
-	mProcessID = 0;
-#endif
-}
-
-LLProcessLauncher::~LLProcessLauncher()
-{
-	kill();
-}
-
-void LLProcessLauncher::setExecutable(const std::string &executable)
-{
-	mExecutable = executable;
-}
-
-void LLProcessLauncher::setWorkingDirectory(const std::string &dir)
-{
-	mWorkingDir = dir;
-}
-
-const std::string& LLProcessLauncher::getExecutable() const
-{
-	return mExecutable;
-}
-
-void LLProcessLauncher::clearArguments()
-{
-	mLaunchArguments.clear();
-}
-
-void LLProcessLauncher::addArgument(const std::string &arg)
-{
-	mLaunchArguments.push_back(arg);
-}
-
-void LLProcessLauncher::addArgument(const char *arg)
-{
-	mLaunchArguments.push_back(std::string(arg));
-}
-
-#if LL_WINDOWS
-
-int LLProcessLauncher::launch(void)
-{
-	// If there was already a process associated with this object, kill it.
-	kill();
-	orphan();
-
-	int result = 0;
-	
-	PROCESS_INFORMATION pinfo;
-	STARTUPINFOA sinfo;
-	memset(&sinfo, 0, sizeof(sinfo));
-	
-	std::string args = mExecutable;
-	for(int i = 0; i < (int)mLaunchArguments.size(); i++)
-	{
-		args += " ";
-		args += mLaunchArguments[i];
-	}
-	
-	// So retarded.  Windows requires that the second parameter to CreateProcessA be a writable (non-const) string...
-	char *args2 = new char[args.size() + 1];
-	strcpy(args2, args.c_str());
-
-	const char * working_directory = 0;
-	if(!mWorkingDir.empty()) working_directory = mWorkingDir.c_str();
-	if( ! CreateProcessA( NULL, args2, NULL, NULL, FALSE, 0, NULL, working_directory, &sinfo, &pinfo ) )
-	{
-		result = GetLastError();
-
-		LPTSTR error_str = 0;
-		if(
-			FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
-				NULL,
-				result,
-				0,
-				(LPTSTR)&error_str,
-				0,
-				NULL) 
-			!= 0) 
-		{
-			char message[256];
-			wcstombs(message, error_str, 256);
-			message[255] = 0;
-			llwarns << "CreateProcessA failed: " << message << llendl;
-			LocalFree(error_str);
-		}
-
-		if(result == 0)
-		{
-			// Make absolutely certain we return a non-zero value on failure.
-			result = -1;
-		}
-	}
-	else
-	{
-		// foo = pinfo.dwProcessId; // get your pid here if you want to use it later on
-		// CloseHandle(pinfo.hProcess); // stops leaks - nothing else
-		mProcessHandle = pinfo.hProcess;
-		CloseHandle(pinfo.hThread); // stops leaks - nothing else
-	}		
-	
-	delete[] args2;
-	
-	return result;
-}
-
-bool LLProcessLauncher::isRunning(void)
-{
-	if(mProcessHandle != 0)		
-	{
-		DWORD waitresult = WaitForSingleObject(mProcessHandle, 0);
-		if(waitresult == WAIT_OBJECT_0)
-		{
-			// the process has completed.
-			mProcessHandle = 0;
-		}			
-	}
-
-	return (mProcessHandle != 0);
-}
-bool LLProcessLauncher::kill(void)
-{
-	bool result = true;
-	
-	if(mProcessHandle != 0)
-	{
-		TerminateProcess(mProcessHandle,0);
-
-		if(isRunning())
-		{
-			result = false;
-		}
-	}
-	
-	return result;
-}
-
-void LLProcessLauncher::orphan(void)
-{
-	// Forget about the process
-	mProcessHandle = 0;
-}
-
-// static 
-void LLProcessLauncher::reap(void)
-{
-	// No actions necessary on Windows.
-}
-
-#else // Mac and linux
-
-#include <signal.h>
-#include <fcntl.h>
-#include <errno.h>
-
-static std::list<pid_t> sZombies;
-
-// Attempt to reap a process ID -- returns true if the process has exited and been reaped, false otherwise.
-static bool reap_pid(pid_t pid)
-{
-	bool result = false;
-	
-	pid_t wait_result = ::waitpid(pid, NULL, WNOHANG);
-	if(wait_result == pid)
-	{
-		result = true;
-	}
-	else if(wait_result == -1)
-	{
-		if(errno == ECHILD)
-		{
-			// No such process -- this may mean we're ignoring SIGCHILD.
-			result = true;
-		}
-	}
-	
-	return result;
-}
-
-int LLProcessLauncher::launch(void)
-{
-	// If there was already a process associated with this object, kill it.
-	kill();
-	orphan();
-	
-	int result = 0;
-	int current_wd = -1;
-	
-	// create an argv vector for the child process
-	const char ** fake_argv = new const char *[mLaunchArguments.size() + 2];  // 1 for the executable path, 1 for the NULL terminator
-
-	int i = 0;
-	
-	// add the executable path
-	fake_argv[i++] = mExecutable.c_str();
-	
-	// and any arguments
-	for(int j=0; j < mLaunchArguments.size(); j++)
-		fake_argv[i++] = mLaunchArguments[j].c_str();
-	
-	// terminate with a null pointer
-	fake_argv[i] = NULL;
-	
-	if(!mWorkingDir.empty())
-	{
-		// save the current working directory
-		current_wd = ::open(".", O_RDONLY);
-	
-		// and change to the one the child will be executed in
-		if (::chdir(mWorkingDir.c_str()))
-		{
-			// chdir failed
-		}
-	}
-		
- 	// flush all buffers before the child inherits them
- 	::fflush(NULL);
-
-	pid_t id = vfork();
-	if(id == 0)
-	{
-		// child process
-		
-		::execv(mExecutable.c_str(), (char * const *)fake_argv);
-		
-		// If we reach this point, the exec failed.
-		// Use _exit() instead of exit() per the vfork man page.
-		_exit(0);
-	}
-
-	// parent process
-	
-	if(current_wd >= 0)
-	{
-		// restore the previous working directory
-		if (::fchdir(current_wd))
-		{
-			// chdir failed
-		}
-		::close(current_wd);
-	}
-	
-	delete[] fake_argv;
-	
-	mProcessID = id;
-
-	return result;
-}
-
-bool LLProcessLauncher::isRunning(void)
-{
-	if(mProcessID != 0)
-	{
-		// Check whether the process has exited, and reap it if it has.
-		if(reap_pid(mProcessID))
-		{
-			// the process has exited.
-			mProcessID = 0;
-		}
-	}
-	
-	return (mProcessID != 0);
-}
-
-bool LLProcessLauncher::kill(void)
-{
-	bool result = true;
-	
-	if(mProcessID != 0)
-	{
-		// Try to kill the process.  We'll do approximately the same thing whether the kill returns an error or not, so we ignore the result.
-		(void)::kill(mProcessID, SIGTERM);
-		
-		// This will have the side-effect of reaping the zombie if the process has exited.
-		if(isRunning())
-		{
-			result = false;
-		}
-	}
-	
-	return result;
-}
-
-void LLProcessLauncher::orphan(void)
-{
-	// Disassociate the process from this object
-	if(mProcessID != 0)
-	{	
-		// We may still need to reap the process's zombie eventually
-		sZombies.push_back(mProcessID);
-	
-		mProcessID = 0;
-	}
-}
-
-// static 
-void LLProcessLauncher::reap(void)
-{
-	// Attempt to real all saved process ID's.
-	
-	std::list<pid_t>::iterator iter = sZombies.begin();
-	while(iter != sZombies.end())
-	{
-		if(reap_pid(*iter))
-		{
-			iter = sZombies.erase(iter);
-		}
-		else
-		{
-			iter++;
-		}
-	}
-}
-
-#endif
diff --git a/indra/llcommon/llprocesslauncher.h b/indra/llcommon/llprocesslauncher.h
deleted file mode 100644
index 954c2491472eb2e17b11122fe7d95eeb53348fff..0000000000000000000000000000000000000000
--- a/indra/llcommon/llprocesslauncher.h
+++ /dev/null
@@ -1,90 +0,0 @@
-/** 
- * @file llprocesslauncher.h
- * @brief Utility class for launching, terminating, and tracking the state of processes.
- *
- * $LicenseInfo:firstyear=2008&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2010, Linden Research, Inc.
- * 
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation;
- * version 2.1 of the License only.
- * 
- * This library 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 this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- * 
- * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
- * $/LicenseInfo$
- */
- 
-#ifndef LL_LLPROCESSLAUNCHER_H
-#define LL_LLPROCESSLAUNCHER_H
-
-#if LL_WINDOWS
-#include <windows.h>
-#endif
-
-
-/*
-	LLProcessLauncher handles launching external processes with specified command line arguments.
-	It also keeps track of whether the process is still running, and can kill it if required.
-*/
-
-class LL_COMMON_API LLProcessLauncher
-{
-	LOG_CLASS(LLProcessLauncher);
-public:
-	LLProcessLauncher();
-	virtual ~LLProcessLauncher();
-	
-	void setExecutable(const std::string &executable);
-	void setWorkingDirectory(const std::string &dir);
-
-	const std::string& getExecutable() const;
-
-	void clearArguments();
-	void addArgument(const std::string &arg);
-	void addArgument(const char *arg);
-		
-	int launch(void);
-	bool isRunning(void);
-	
-	// Attempt to kill the process -- returns true if the process is no longer running when it returns.
-	// Note that even if this returns false, the process may exit some time after it's called.
-	bool kill(void);
-	
-	// Use this if you want the external process to continue execution after the LLProcessLauncher instance controlling it is deleted.
-	// Normally, the destructor will attempt to kill the process and wait for termination.
-	// This should only be used if the viewer is about to exit -- otherwise, the child process will become a zombie after it exits.
-	void orphan(void);	
-	
-	// This needs to be called periodically on Mac/Linux to clean up zombie processes.
-	static void reap(void);
-	
-	// Accessors for platform-specific process ID
-#if LL_WINDOWS
-	HANDLE getProcessHandle() { return mProcessHandle; };
-#else
-	pid_t getProcessID() { return mProcessID; };
-#endif	
-	
-private:
-	std::string mExecutable;
-	std::string mWorkingDir;
-	std::vector<std::string> mLaunchArguments;
-	
-#if LL_WINDOWS
-	HANDLE mProcessHandle;
-#else
-	pid_t mProcessID;
-#endif
-};
-
-#endif // LL_LLPROCESSLAUNCHER_H
diff --git a/indra/llcommon/llstreamqueue.cpp b/indra/llcommon/llstreamqueue.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1116a2b6a2cacfd76e7f563606cdfaf76b822a54
--- /dev/null
+++ b/indra/llcommon/llstreamqueue.cpp
@@ -0,0 +1,24 @@
+/**
+ * @file   llstreamqueue.cpp
+ * @author Nat Goodspeed
+ * @date   2012-01-05
+ * @brief  Implementation for llstreamqueue.
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llstreamqueue.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+
+// As of this writing, llstreamqueue.h is entirely template-based, therefore
+// we don't strictly need a corresponding .cpp file. However, our CMake test
+// macro assumes one. Here it is.
+bool llstreamqueue_cpp_ignored = true;
diff --git a/indra/llcommon/llstreamqueue.h b/indra/llcommon/llstreamqueue.h
new file mode 100644
index 0000000000000000000000000000000000000000..0726bad1757679f14f5099e27459799d6ccb3dfc
--- /dev/null
+++ b/indra/llcommon/llstreamqueue.h
@@ -0,0 +1,240 @@
+/**
+ * @file   llstreamqueue.h
+ * @author Nat Goodspeed
+ * @date   2012-01-04
+ * @brief  Definition of LLStreamQueue
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLSTREAMQUEUE_H)
+#define LL_LLSTREAMQUEUE_H
+
+#include <string>
+#include <list>
+#include <iosfwd>                   // std::streamsize
+#include <boost/iostreams/categories.hpp>
+
+/**
+ * This class is a growable buffer between a producer and consumer. It serves
+ * as a queue usable with Boost.Iostreams -- hence, a "stream queue."
+ *
+ * This is especially useful for buffering nonblocking I/O. For instance, we
+ * want application logic to be able to serialize LLSD to a std::ostream. We
+ * may write more data than the destination pipe can handle all at once, but
+ * it's imperative NOT to block the application-level serialization call. So
+ * we buffer it instead. Successive frames can try nonblocking writes to the
+ * destination pipe until all buffered data has been sent.
+ *
+ * Similarly, we want application logic be able to deserialize LLSD from a
+ * std::istream. Again, we must not block that deserialize call waiting for
+ * more data to arrive from the input pipe! Instead we build up a buffer over
+ * a number of frames, using successive nonblocking reads, until we have
+ * "enough" data to be able to present it through a std::istream.
+ *
+ * @note The use cases for this class overlap somewhat with those for the
+ * LLIOPipe/LLPumpIO hierarchies, and indeed we considered using those. This
+ * class has two virtues over the older machinery:
+ *
+ * # It's vastly simpler -- way fewer concepts. It's not clear to me whether
+ *   there were ever LLIOPipe/etc. use cases that demanded all the fanciness
+ *   rolled in, or whether they were simply overdesigned. In any case, no
+ *   remaining Lindens will admit to familiarity with those classes -- and
+ *   they're sufficiently obtuse that it would take considerable learning
+ *   curve to figure out how to use them properly. The bottom line is that
+ *   current management is not keen on any more engineers climbing that curve.
+ * # This class is designed around available components such as std::string,
+ *   std::list, Boost.Iostreams. There's less proprietary code.
+ */
+template <typename Ch>
+class LLGenericStreamQueue
+{
+public:
+    LLGenericStreamQueue():
+        mSize(0),
+        mClosed(false)
+    {}
+
+    /**
+     * Boost.Iostreams Source Device facade for use with other Boost.Iostreams
+     * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost
+     * 1.48 Iostreams concepts; instead it behaves as both a Sink and a
+     * Source. This is its Source facade.
+     */
+    struct Source
+    {
+        typedef Ch char_type;
+        typedef boost::iostreams::source_tag category;
+
+        /// Bind the underlying LLGenericStreamQueue
+        Source(LLGenericStreamQueue& sq):
+            mStreamQueue(sq)
+        {}
+
+        // Read up to n characters from the underlying data source into the
+        // buffer s, returning the number of characters read; return -1 to
+        // indicate EOF
+        std::streamsize read(Ch* s, std::streamsize n)
+        {
+            return mStreamQueue.read(s, n);
+        }
+
+        LLGenericStreamQueue& mStreamQueue;
+    };
+
+    /**
+     * Boost.Iostreams Sink Device facade for use with other Boost.Iostreams
+     * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost
+     * 1.48 Iostreams concepts; instead it behaves as both a Sink and a
+     * Source. This is its Sink facade.
+     */
+    struct Sink
+    {
+        typedef Ch char_type;
+        typedef boost::iostreams::sink_tag category;
+
+        /// Bind the underlying LLGenericStreamQueue
+        Sink(LLGenericStreamQueue& sq):
+            mStreamQueue(sq)
+        {}
+
+        /// Write up to n characters from the buffer s to the output sequence,
+        /// returning the number of characters written
+        std::streamsize write(const Ch* s, std::streamsize n)
+        {
+            return mStreamQueue.write(s, n);
+        }
+
+        /// Send EOF to consumer
+        void close()
+        {
+            mStreamQueue.close();
+        }
+
+        LLGenericStreamQueue& mStreamQueue;
+    };
+
+    /// Present Boost.Iostreams Source facade
+    Source asSource() { return Source(*this); }
+    /// Present Boost.Iostreams Sink facade
+    Sink   asSink()   { return Sink(*this); }
+
+    /// append data to buffer
+    std::streamsize write(const Ch* s, std::streamsize n)
+    {
+        // Unclear how often we might be asked to write 0 bytes -- perhaps a
+        // naive caller responding to an unready nonblocking read. But if we
+        // do get such a call, don't add a completely empty BufferList entry.
+        if (n == 0)
+            return n;
+        // We could implement this using a single std::string object, a la
+        // ostringstream. But the trouble with appending to a string is that
+        // you might have to recopy all previous contents to grow its size. If
+        // we want this to scale to large data volumes, better to allocate
+        // individual pieces.
+        mBuffer.push_back(string(s, n));
+        mSize += n;
+        return n;
+    }
+
+    /**
+     * Inform this LLGenericStreamQueue that no further data are forthcoming.
+     * For our purposes, close() is strictly a producer-side operation;
+     * there's little point in closing the consumer side.
+     */
+    void close()
+    {
+        mClosed = true;
+    }
+
+    /// consume data from buffer
+    std::streamsize read(Ch* s, std::streamsize n)
+    {
+        // read() is actually a convenience method for peek() followed by
+        // skip().
+        std::streamsize got(peek(s, n));
+        // We can only skip() as many characters as we can peek(); ignore
+        // skip() return here.
+        skip(n);
+        return got;
+    }
+
+    /// Retrieve data from buffer without consuming. Like read(), return -1 on
+    /// EOF.
+    std::streamsize peek(Ch* s, std::streamsize n) const;
+
+    /// Consume data from buffer without retrieving. Unlike read() and peek(),
+    /// at EOF we simply skip 0 characters.
+    std::streamsize skip(std::streamsize n);
+
+    /// How many characters do we currently have buffered?
+    std::streamsize size() const
+    {
+        return mSize;
+    }
+
+private:
+    typedef std::basic_string<Ch> string;
+    typedef std::list<string> BufferList;
+    BufferList mBuffer;
+    std::streamsize mSize;
+    bool mClosed;
+};
+
+template <typename Ch>
+std::streamsize LLGenericStreamQueue<Ch>::peek(Ch* s, std::streamsize n) const
+{
+    // Here we may have to build up 'n' characters from an arbitrary
+    // number of individual BufferList entries.
+    typename BufferList::const_iterator bli(mBuffer.begin()), blend(mBuffer.end());
+    // Indicate EOF if producer has closed the pipe AND we've exhausted
+    // all previously-buffered data.
+    if (mClosed && bli == blend)
+    {
+        return -1;
+    }
+    // Here either producer hasn't yet closed, or we haven't yet exhausted
+    // remaining data.
+    std::streamsize needed(n), got(0);
+    // Loop until either we run out of BufferList entries or we've
+    // completely satisfied the request.
+    for ( ; bli != blend && needed; ++bli)
+    {
+        std::streamsize chunk(std::min(needed, std::streamsize(bli->length())));
+        std::copy(bli->begin(), bli->begin() + chunk, s);
+        needed -= chunk;
+        s      += chunk;
+        got    += chunk;
+    }
+    return got;
+}
+
+template <typename Ch>
+std::streamsize LLGenericStreamQueue<Ch>::skip(std::streamsize n)
+{
+    typename BufferList::iterator bli(mBuffer.begin()), blend(mBuffer.end());
+    std::streamsize toskip(n), skipped(0);
+    while (bli != blend && toskip >= bli->length())
+    {
+        std::streamsize chunk(bli->length());
+        typename BufferList::iterator zap(bli++);
+        mBuffer.erase(zap);
+        mSize   -= chunk;
+        toskip  -= chunk;
+        skipped += chunk;
+    }
+    if (bli != blend && toskip)
+    {
+        bli->erase(bli->begin(), bli->begin() + toskip);
+        mSize   -= toskip;
+        skipped += toskip;
+    }
+    return skipped;
+}
+
+typedef LLGenericStreamQueue<char>    LLStreamQueue;
+typedef LLGenericStreamQueue<wchar_t> LLWStreamQueue;
+
+#endif /* ! defined(LL_LLSTREAMQUEUE_H) */
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..55e22abd819239d06819203d5f80d321e8818f39
--- /dev/null
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -0,0 +1,706 @@
+/**
+ * @file   llprocess_test.cpp
+ * @author Nat Goodspeed
+ * @date   2011-12-19
+ * @brief  Test for llprocess.
+ * 
+ * $LicenseInfo:firstyear=2011&license=viewerlgpl$
+ * Copyright (c) 2011, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llprocess.h"
+// STL headers
+#include <vector>
+#include <list>
+// std headers
+#include <fstream>
+// external library headers
+#include "llapr.h"
+#include "apr_thread_proc.h"
+#include <boost/foreach.hpp>
+#include <boost/function.hpp>
+#include <boost/algorithm/string/find_iterator.hpp>
+#include <boost/algorithm/string/finder.hpp>
+//#include <boost/lambda/lambda.hpp>
+//#include <boost/lambda/bind.hpp>
+// other Linden headers
+#include "../test/lltut.h"
+#include "../test/manageapr.h"
+#include "../test/namedtempfile.h"
+#include "stringize.h"
+#include "llsdutil.h"
+
+#if defined(LL_WINDOWS)
+#define sleep(secs) _sleep((secs) * 1000)
+#define EOL "\r\n"
+#else
+#define EOL "\n"
+#include <sys/wait.h>
+#endif
+
+//namespace lambda = boost::lambda;
+
+// static instance of this manages APR init/cleanup
+static ManageAPR manager;
+
+/*****************************************************************************
+*   Helpers
+*****************************************************************************/
+
+#define ensure_equals_(left, right) \
+        ensure_equals(STRINGIZE(#left << " != " << #right), (left), (right))
+
+#define aprchk(expr) aprchk_(#expr, (expr))
+static void aprchk_(const char* call, apr_status_t rv, apr_status_t expected=APR_SUCCESS)
+{
+    tut::ensure_equals(STRINGIZE(call << " => " << rv << ": " << manager.strerror(rv)),
+                       rv, expected);
+}
+
+/**
+ * Read specified file using std::getline(). It is assumed to be an error if
+ * the file is empty: don't use this function if that's an acceptable case.
+ * Last line will not end with '\n'; this is to facilitate the usual case of
+ * string compares with a single line of output.
+ * @param pathname The file to read.
+ * @param desc Optional description of the file for error message;
+ * defaults to "in <pathname>"
+ */
+static std::string readfile(const std::string& pathname, const std::string& desc="")
+{
+    std::string use_desc(desc);
+    if (use_desc.empty())
+    {
+        use_desc = STRINGIZE("in " << pathname);
+    }
+    std::ifstream inf(pathname.c_str());
+    std::string output;
+    tut::ensure(STRINGIZE("No output " << use_desc), std::getline(inf, output));
+    std::string more;
+    while (std::getline(inf, more))
+    {
+        output += '\n' + more;
+    }
+    return output;
+}
+
+/**
+ * Construct an LLProcess to run a Python script.
+ */
+struct PythonProcessLauncher
+{
+    /**
+     * @param desc Arbitrary description for error messages
+     * @param script Python script, any form acceptable to NamedTempFile,
+     * typically either a std::string or an expression of the form
+     * (lambda::_1 << "script content with " << variable_data)
+     */
+    template <typename CONTENT>
+    PythonProcessLauncher(const std::string& desc, const CONTENT& script):
+        mDesc(desc),
+        mScript("py", script)
+    {
+        const char* PYTHON(getenv("PYTHON"));
+        tut::ensure("Set $PYTHON to the Python interpreter", PYTHON);
+
+        mParams["executable"] = PYTHON;
+        mParams["args"].append(mScript.getName());
+    }
+
+    /// Run Python script and wait for it to complete.
+    void run()
+    {
+        mPy = LLProcess::create(mParams);
+        tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), mPy);
+        // One of the irritating things about LLProcess is that
+        // there's no API to wait for the child to terminate -- but given
+        // its use in our graphics-intensive interactive viewer, it's
+        // understandable.
+        while (mPy->isRunning())
+        {
+            sleep(1);
+        }
+    }
+
+    /**
+     * Run a Python script using LLProcess, expecting that it will
+     * write to the file passed as its sys.argv[1]. Retrieve that output.
+     *
+     * Until January 2012, LLProcess provided distressingly few
+     * mechanisms for a child process to communicate back to its caller --
+     * not even its return code. We've introduced a convention by which we
+     * create an empty temp file, pass the name of that file to our child
+     * as sys.argv[1] and expect the script to write its output to that
+     * file. This function implements the C++ (parent process) side of
+     * that convention.
+     */
+    std::string run_read()
+    {
+        NamedTempFile out("out", ""); // placeholder
+        // pass name of this temporary file to the script
+        mParams["args"].append(out.getName());
+        run();
+        // assuming the script wrote to that file, read it
+        return readfile(out.getName(), STRINGIZE("from " << mDesc << " script"));
+    }
+
+    LLSD mParams;
+    LLProcessPtr mPy;
+    std::string mDesc;
+    NamedTempFile mScript;
+};
+
+/// convenience function for PythonProcessLauncher::run()
+template <typename CONTENT>
+static void python(const std::string& desc, const CONTENT& script)
+{
+    PythonProcessLauncher py(desc, script);
+    py.run();
+}
+
+/// convenience function for PythonProcessLauncher::run_read()
+template <typename CONTENT>
+static std::string python_out(const std::string& desc, const CONTENT& script)
+{
+    PythonProcessLauncher py(desc, script);
+    return py.run_read();
+}
+
+/// Create a temporary directory and clean it up later.
+class NamedTempDir: public boost::noncopyable
+{
+public:
+    // Use python() function to create a temp directory: I've found
+    // nothing in either Boost.Filesystem or APR quite like Python's
+    // tempfile.mkdtemp().
+    // Special extra bonus: on Mac, mkdtemp() reports a pathname
+    // starting with /var/folders/something, whereas that's really a
+    // symlink to /private/var/folders/something. Have to use
+    // realpath() to compare properly.
+    NamedTempDir():
+        mPath(python_out("mkdtemp()",
+                         "from __future__ import with_statement\n"
+                         "import os.path, sys, tempfile\n"
+                         "with open(sys.argv[1], 'w') as f:\n"
+                         "    f.write(os.path.realpath(tempfile.mkdtemp()))\n"))
+    {}
+
+    ~NamedTempDir()
+    {
+        aprchk(apr_dir_remove(mPath.c_str(), gAPRPoolp));
+    }
+
+    std::string getName() const { return mPath; }
+
+private:
+    std::string mPath;
+};
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct llprocess_data
+    {
+        LLAPRPool pool;
+    };
+    typedef test_group<llprocess_data> llprocess_group;
+    typedef llprocess_group::object object;
+    llprocess_group llprocessgrp("llprocess");
+
+    struct Item
+    {
+        Item(): tries(0) {}
+        unsigned    tries;
+        std::string which;
+        std::string what;
+    };
+
+/*==========================================================================*|
+#define tabent(symbol) { symbol, #symbol }
+    static struct ReasonCode
+    {
+        int code;
+        const char* name;
+    } reasons[] =
+    {
+        tabent(APR_OC_REASON_DEATH),
+        tabent(APR_OC_REASON_UNWRITABLE),
+        tabent(APR_OC_REASON_RESTART),
+        tabent(APR_OC_REASON_UNREGISTER),
+        tabent(APR_OC_REASON_LOST),
+        tabent(APR_OC_REASON_RUNNING)
+    };
+#undef tabent
+|*==========================================================================*/
+
+    struct WaitInfo
+    {
+        WaitInfo(apr_proc_t* child_):
+            child(child_),
+            rv(-1),                 // we haven't yet called apr_proc_wait()
+            rc(0),
+            why(apr_exit_why_e(0))
+        {}
+        apr_proc_t* child;          // which subprocess
+        apr_status_t rv;            // return from apr_proc_wait()
+        int rc;                     // child's exit code
+        apr_exit_why_e why;         // APR_PROC_EXIT, APR_PROC_SIGNAL, APR_PROC_SIGNAL_CORE
+    };
+
+    void child_status_callback(int reason, void* data, int status)
+    {
+/*==========================================================================*|
+        std::string reason_str;
+        BOOST_FOREACH(const ReasonCode& rcp, reasons)
+        {
+            if (reason == rcp.code)
+            {
+                reason_str = rcp.name;
+                break;
+            }
+        }
+        if (reason_str.empty())
+        {
+            reason_str = STRINGIZE("unknown reason " << reason);
+        }
+        std::cout << "child_status_callback(" << reason_str << ")\n";
+|*==========================================================================*/
+
+        if (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST)
+        {
+            // Somewhat oddly, APR requires that you explicitly unregister
+            // even when it already knows the child has terminated.
+            apr_proc_other_child_unregister(data);
+
+            WaitInfo* wi(static_cast<WaitInfo*>(data));
+            // It's just wrong to call apr_proc_wait() here. The only way APR
+            // knows to call us with APR_OC_REASON_DEATH is that it's already
+            // reaped this child process, so calling wait() will only produce
+            // "huh?" from the OS. We must rely on the status param passed in,
+            // which unfortunately comes straight from the OS wait() call.
+//          wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT);
+            wi->rv = APR_CHILD_DONE; // fake apr_proc_wait() results
+#if defined(LL_WINDOWS)
+            wi->why = APR_PROC_EXIT;
+            wi->rc  = status;         // no encoding on Windows (no signals)
+#else  // Posix
+            if (WIFEXITED(status))
+            {
+                wi->why = APR_PROC_EXIT;
+                wi->rc  = WEXITSTATUS(status);
+            }
+            else if (WIFSIGNALED(status))
+            {
+                wi->why = APR_PROC_SIGNAL;
+                wi->rc  = WTERMSIG(status);
+            }
+            else                    // uh, shouldn't happen?
+            {
+                wi->why = APR_PROC_EXIT;
+                wi->rc  = status;   // someone else will have to decode
+            }
+#endif // Posix
+        }
+    }
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("raw APR nonblocking I/O");
+
+        // Create a script file in a temporary place.
+        NamedTempFile script("py",
+            "import sys" EOL
+            "import time" EOL
+            EOL
+            "time.sleep(2)" EOL
+            "print >>sys.stdout, 'stdout after wait'" EOL
+            "sys.stdout.flush()" EOL
+            "time.sleep(2)" EOL
+            "print >>sys.stderr, 'stderr after wait'" EOL
+            "sys.stderr.flush()" EOL
+            );
+
+        // Arrange to track the history of our interaction with child: what we
+        // fetched, which pipe it came from, how many tries it took before we
+        // got it.
+        std::vector<Item> history;
+        history.push_back(Item());
+
+        // Run the child process.
+        apr_procattr_t *procattr = NULL;
+        aprchk(apr_procattr_create(&procattr, pool.getAPRPool()));
+        aprchk(apr_procattr_io_set(procattr, APR_CHILD_BLOCK, APR_CHILD_BLOCK, APR_CHILD_BLOCK));
+        aprchk(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH));
+
+        std::vector<const char*> argv;
+        apr_proc_t child;
+        argv.push_back("python");
+        // Have to have a named copy of this std::string so its c_str() value
+        // will persist.
+        std::string scriptname(script.getName());
+        argv.push_back(scriptname.c_str());
+        argv.push_back(NULL);
+
+        aprchk(apr_proc_create(&child, argv[0],
+                               &argv[0],
+                               NULL, // if we wanted to pass explicit environment
+                               procattr,
+                               pool.getAPRPool()));
+
+        // We do not want this child process to outlive our APR pool. On
+        // destruction of the pool, forcibly kill the process. Tell APR to try
+        // SIGTERM and wait 3 seconds. If that didn't work, use SIGKILL.
+        apr_pool_note_subprocess(pool.getAPRPool(), &child, APR_KILL_AFTER_TIMEOUT);
+
+        // arrange to call child_status_callback()
+        WaitInfo wi(&child);
+        apr_proc_other_child_register(&child, child_status_callback, &wi, child.in, pool.getAPRPool());
+
+        // TODO:
+        // Stuff child.in until it (would) block to verify EWOULDBLOCK/EAGAIN.
+        // Have child script clear it later, then write one more line to prove
+        // that it gets through.
+
+        // Monitor two different output pipes. Because one will be closed
+        // before the other, keep them in a list so we can drop whichever of
+        // them is closed first.
+        typedef std::pair<std::string, apr_file_t*> DescFile;
+        typedef std::list<DescFile> DescFileList;
+        DescFileList outfiles;
+        outfiles.push_back(DescFile("out", child.out));
+        outfiles.push_back(DescFile("err", child.err));
+
+        while (! outfiles.empty())
+        {
+            // This peculiar for loop is designed to let us erase(dfli). With
+            // a list, that invalidates only dfli itself -- but even so, we
+            // lose the ability to increment it for the next item. So at the
+            // top of every loop, while dfli is still valid, increment
+            // dflnext. Then before the next iteration, set dfli to dflnext.
+            for (DescFileList::iterator
+                     dfli(outfiles.begin()), dflnext(outfiles.begin()), dflend(outfiles.end());
+                 dfli != dflend; dfli = dflnext)
+            {
+                // Only valid to increment dflnext once we're sure it's not
+                // already at dflend.
+                ++dflnext;
+
+                char buf[4096];
+
+                apr_status_t rv = apr_file_gets(buf, sizeof(buf), dfli->second);
+                if (APR_STATUS_IS_EOF(rv))
+                {
+//                  std::cout << "(EOF on " << dfli->first << ")\n";
+//                  history.back().which = dfli->first;
+//                  history.back().what  = "*eof*";
+//                  history.push_back(Item());
+                    outfiles.erase(dfli);
+                    continue;
+                }
+                if (rv == EWOULDBLOCK || rv == EAGAIN)
+                {
+//                  std::cout << "(waiting; apr_file_gets(" << dfli->first << ") => " << rv << ": " << manager.strerror(rv) << ")\n";
+                    ++history.back().tries;
+                    continue;
+                }
+                aprchk_("apr_file_gets(buf, sizeof(buf), dfli->second)", rv);
+                // Is it even possible to get APR_SUCCESS but read 0 bytes?
+                // Hope not, but defend against that anyway.
+                if (buf[0])
+                {
+//                  std::cout << dfli->first << ": " << buf;
+                    history.back().which = dfli->first;
+                    history.back().what.append(buf);
+                    if (buf[strlen(buf) - 1] == '\n')
+                        history.push_back(Item());
+                    else
+                    {
+                        // Just for pretty output... if we only read a partial
+                        // line, terminate it.
+//                      std::cout << "...\n";
+                    }
+                }
+            }
+            // Do this once per tick, as we expect the viewer will
+            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING);
+            sleep(1);
+        }
+        apr_file_close(child.in);
+        apr_file_close(child.out);
+        apr_file_close(child.err);
+
+        // Okay, we've broken the loop because our pipes are all closed. If we
+        // haven't yet called wait, give the callback one more chance. This
+        // models the fact that unlike this small test program, the viewer
+        // will still be running.
+        if (wi.rv == -1)
+        {
+            std::cout << "last gasp apr_proc_other_child_refresh_all()\n";
+            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING);
+        }
+
+        if (wi.rv == -1)
+        {
+            std::cout << "child_status_callback(APR_OC_REASON_DEATH) wasn't called" << std::endl;
+            wi.rv = apr_proc_wait(wi.child, &wi.rc, &wi.why, APR_NOWAIT);
+        }
+//      std::cout << "child done: rv = " << rv << " (" << manager.strerror(rv) << "), why = " << why << ", rc = " << rc << '\n';
+        aprchk_("apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT)", wi.rv, APR_CHILD_DONE);
+        ensure_equals_(wi.why, APR_PROC_EXIT);
+        ensure_equals_(wi.rc, 0);
+
+        // Beyond merely executing all the above successfully, verify that we
+        // obtained expected output -- and that we duly got control while
+        // waiting, proving the non-blocking nature of these pipes.
+        try
+        {
+            unsigned i = 0;
+            ensure("blocking I/O on child pipe (0)", history[i].tries);
+            ensure_equals_(history[i].which, "out");
+            ensure_equals_(history[i].what,  "stdout after wait" EOL);
+//          ++i;
+//          ensure_equals_(history[i].which, "out");
+//          ensure_equals_(history[i].what,  "*eof*");
+            ++i;
+            ensure("blocking I/O on child pipe (1)", history[i].tries);
+            ensure_equals_(history[i].which, "err");
+            ensure_equals_(history[i].what,  "stderr after wait" EOL);
+//          ++i;
+//          ensure_equals_(history[i].which, "err");
+//          ensure_equals_(history[i].what,  "*eof*");
+        }
+        catch (const failure&)
+        {
+            std::cout << "History:\n";
+            BOOST_FOREACH(const Item& item, history)
+            {
+                std::string what(item.what);
+                if ((! what.empty()) && what[what.length() - 1] == '\n')
+                {
+                    what.erase(what.length() - 1);
+                    if ((! what.empty()) && what[what.length() - 1] == '\r')
+                    {
+                        what.erase(what.length() - 1);
+                        what.append("\\r");
+                    }
+                    what.append("\\n");
+                }
+                std::cout << "  " << item.which << ": '" << what << "' ("
+                          << item.tries << " tries)\n";
+            }
+            std::cout << std::flush;
+            // re-raise same error; just want to enrich the output
+            throw;
+        }
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("setWorkingDirectory()");
+        // We want to test setWorkingDirectory(). But what directory is
+        // guaranteed to exist on every machine, under every OS? Have to
+        // create one. Naturally, ensure we clean it up when done.
+        NamedTempDir tempdir;
+        PythonProcessLauncher py("getcwd()",
+                                 "from __future__ import with_statement\n"
+                                 "import os, sys\n"
+                                 "with open(sys.argv[1], 'w') as f:\n"
+                                 "    f.write(os.getcwd())\n");
+        // Before running, call setWorkingDirectory()
+        py.mParams["cwd"] = tempdir.getName();
+        ensure_equals("os.getcwd()", py.run_read(), tempdir.getName());
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("arguments");
+        PythonProcessLauncher py("args",
+                                 "from __future__ import with_statement\n"
+                                 "import sys\n"
+                                 // note nonstandard output-file arg!
+                                 "with open(sys.argv[3], 'w') as f:\n"
+                                 "    for arg in sys.argv[1:]:\n"
+                                 "        print >>f, arg\n");
+        // We expect that PythonProcessLauncher has already appended
+        // its own NamedTempFile to mParams["args"] (sys.argv[0]).
+        py.mParams["args"].append("first arg");          // sys.argv[1]
+        py.mParams["args"].append("second arg");         // sys.argv[2]
+        // run_read() appends() one more argument, hence [3]
+        std::string output(py.run_read());
+        boost::split_iterator<std::string::const_iterator>
+            li(output, boost::first_finder("\n")), lend;
+        ensure("didn't get first arg", li != lend);
+        std::string arg(li->begin(), li->end());
+        ensure_equals(arg, "first arg");
+        ++li;
+        ensure("didn't get second arg", li != lend);
+        arg.assign(li->begin(), li->end());
+        ensure_equals(arg, "second arg");
+        ++li;
+        ensure("didn't get output filename?!", li != lend);
+        arg.assign(li->begin(), li->end());
+        ensure("output filename empty?!", ! arg.empty());
+        ++li;
+        ensure("too many args", li == lend);
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("explicit kill()");
+        PythonProcessLauncher py("kill()",
+                                 "from __future__ import with_statement\n"
+                                 "import sys, time\n"
+                                 "with open(sys.argv[1], 'w') as f:\n"
+                                 "    f.write('ok')\n"
+                                 "# now sleep; expect caller to kill\n"
+                                 "time.sleep(120)\n"
+                                 "# if caller hasn't managed to kill by now, bad\n"
+                                 "with open(sys.argv[1], 'w') as f:\n"
+                                 "    f.write('bad')\n");
+        NamedTempFile out("out", "not started");
+        py.mParams["args"].append(out.getName());
+        py.mPy = LLProcess::create(py.mParams);
+        ensure("couldn't launch kill() script", py.mPy);
+        // Wait for the script to wake up and do its first write
+        int i = 0, timeout = 60;
+        for ( ; i < timeout; ++i)
+        {
+            sleep(1);
+            if (readfile(out.getName(), "from kill() script") == "ok")
+                break;
+        }
+        // If we broke this loop because of the counter, something's wrong
+        ensure("script never started", i < timeout);
+        // script has performed its first write and should now be sleeping.
+        py.mPy->kill();
+        // wait for the script to terminate... one way or another.
+        while (py.mPy->isRunning())
+        {
+            sleep(1);
+        }
+        // If kill() failed, the script would have woken up on its own and
+        // overwritten the file with 'bad'. But if kill() succeeded, it should
+        // not have had that chance.
+        ensure_equals("kill() script output", readfile(out.getName()), "ok");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("implicit kill()");
+        NamedTempFile out("out", "not started");
+        LLProcess::id pid(0);
+        {
+            PythonProcessLauncher py("kill()",
+                                     "from __future__ import with_statement\n"
+                                     "import sys, time\n"
+                                     "with open(sys.argv[1], 'w') as f:\n"
+                                     "    f.write('ok')\n"
+                                     "# now sleep; expect caller to kill\n"
+                                     "time.sleep(120)\n"
+                                     "# if caller hasn't managed to kill by now, bad\n"
+                                     "with open(sys.argv[1], 'w') as f:\n"
+                                     "    f.write('bad')\n");
+            py.mParams["args"].append(out.getName());
+            py.mPy = LLProcess::create(py.mParams);
+            ensure("couldn't launch kill() script", py.mPy);
+            // Capture id for later
+            pid = py.mPy->getProcessID();
+            // Wait for the script to wake up and do its first write
+            int i = 0, timeout = 60;
+            for ( ; i < timeout; ++i)
+            {
+                sleep(1);
+                if (readfile(out.getName(), "from kill() script") == "ok")
+                    break;
+            }
+            // If we broke this loop because of the counter, something's wrong
+            ensure("script never started", i < timeout);
+            // Script has performed its first write and should now be sleeping.
+            // Destroy the LLProcess, which should kill the child.
+        }
+        // wait for the script to terminate... one way or another.
+        while (LLProcess::isRunning(pid))
+        {
+            sleep(1);
+        }
+        // If kill() failed, the script would have woken up on its own and
+        // overwritten the file with 'bad'. But if kill() succeeded, it should
+        // not have had that chance.
+        ensure_equals("kill() script output", readfile(out.getName()), "ok");
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("autokill");
+        NamedTempFile from("from", "not started");
+        NamedTempFile to("to", "");
+        LLProcess::id pid(0);
+        {
+            PythonProcessLauncher py("autokill",
+                                     "from __future__ import with_statement\n"
+                                     "import sys, time\n"
+                                     "with open(sys.argv[1], 'w') as f:\n"
+                                     "    f.write('ok')\n"
+                                     "# wait for 'go' from test program\n"
+                                     "for i in xrange(60):\n"
+                                     "    time.sleep(1)\n"
+                                     "    with open(sys.argv[2]) as f:\n"
+                                     "        go = f.read()\n"
+                                     "    if go == 'go':\n"
+                                     "        break\n"
+                                     "else:\n"
+                                     "    with open(sys.argv[1], 'w') as f:\n"
+                                     "        f.write('never saw go')\n"
+                                     "    sys.exit(1)\n"
+                                     "# okay, saw 'go', write 'ack'\n"
+                                     "with open(sys.argv[1], 'w') as f:\n"
+                                     "    f.write('ack')\n");
+            py.mParams["args"].append(from.getName());
+            py.mParams["args"].append(to.getName());
+            py.mParams["autokill"] = false;
+            py.mPy = LLProcess::create(py.mParams);
+            ensure("couldn't launch kill() script", py.mPy);
+            // Capture id for later
+            pid = py.mPy->getProcessID();
+            // Wait for the script to wake up and do its first write
+            int i = 0, timeout = 60;
+            for ( ; i < timeout; ++i)
+            {
+                sleep(1);
+                if (readfile(from.getName(), "from autokill script") == "ok")
+                    break;
+            }
+            // If we broke this loop because of the counter, something's wrong
+            ensure("script never started", i < timeout);
+            // Now destroy the LLProcess, which should NOT kill the child!
+        }
+        // If the destructor killed the child anyway, give it time to die
+        sleep(2);
+        // How do we know it's not terminated? By making it respond to
+        // a specific stimulus in a specific way.
+        {
+            std::ofstream outf(to.getName().c_str());
+            outf << "go";
+        } // flush and close.
+        // now wait for the script to terminate... one way or another.
+        while (LLProcess::isRunning(pid))
+        {
+            sleep(1);
+        }
+        // If the LLProcess destructor implicitly called kill(), the
+        // script could not have written 'ack' as we expect.
+        ensure_equals("autokill script output", readfile(from.getName()), "ack");
+    }
+} // namespace tut
diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index 72322c3b727a46b9f24a9527badfa76657eb2229..7756ba622603c66bb3c79b36a9769f65d77f459f 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -40,41 +40,15 @@ typedef U32 uint32_t;
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <sys/wait.h>
-#include "llprocesslauncher.h"
+#include "llprocess.h"
 #endif
 
-#include <sstream>
-
-/*==========================================================================*|
-// Whoops, seems Linden's Boost package and the viewer are built with
-// different settings of VC's /Zc:wchar_t switch! Using Boost.Filesystem
-// pathname operations produces Windows link errors:
-// unresolved external symbol "private: static class std::codecvt<unsigned short,
-// char,int> const * & __cdecl boost::filesystem3::path::wchar_t_codecvt_facet()"
-// unresolved external symbol "void __cdecl boost::filesystem3::path_traits::convert()"
-// See:
-// http://boost.2283326.n4.nabble.com/filesystem-v3-unicode-and-std-codecvt-linker-error-td3455549.html
-// which points to:
-// http://msdn.microsoft.com/en-us/library/dh8che7s%28v=VS.100%29.aspx
-
-// As we're not trying to preserve compatibility with old Boost.Filesystem
-// code, but rather writing brand-new code, use the newest available
-// Filesystem API.
-#define BOOST_FILESYSTEM_VERSION 3
-#include "boost/filesystem.hpp"
-#include "boost/filesystem/v3/fstream.hpp"
-|*==========================================================================*/
 #include "boost/range.hpp"
 #include "boost/foreach.hpp"
 #include "boost/function.hpp"
 #include "boost/lambda/lambda.hpp"
 #include "boost/lambda/bind.hpp"
 namespace lambda = boost::lambda;
-/*==========================================================================*|
-// Aaaarrgh, Linden's Boost package doesn't even include Boost.Iostreams!
-#include "boost/iostreams/stream.hpp"
-#include "boost/iostreams/device/file_descriptor.hpp"
-|*==========================================================================*/
 
 #include "../llsd.h"
 #include "../llsdserialize.h"
@@ -82,236 +56,17 @@ namespace lambda = boost::lambda;
 #include "../llformat.h"
 
 #include "../test/lltut.h"
+#include "../test/manageapr.h"
+#include "../test/namedtempfile.h"
 #include "stringize.h"
 
+static ManageAPR manager;
+
 std::vector<U8> string_to_vector(const std::string& str)
 {
 	return std::vector<U8>(str.begin(), str.end());
 }
 
-#if ! LL_WINDOWS
-// We want to call strerror_r(), but alarmingly, there are two different
-// variants. The one that returns int always populates the passed buffer
-// (except in case of error), whereas the other one always returns a valid
-// char* but might or might not populate the passed buffer. How do we know
-// which one we're getting? Define adapters for each and let the compiler
-// select the applicable adapter.
-
-// strerror_r() returns char*
-std::string message_from(int /*orig_errno*/, const char* /*buffer*/, const char* strerror_ret)
-{
-    return strerror_ret;
-}
-
-// strerror_r() returns int
-std::string message_from(int orig_errno, const char* buffer, int strerror_ret)
-{
-    if (strerror_ret == 0)
-    {
-        return buffer;
-    }
-    // Here strerror_r() has set errno. Since strerror_r() has already failed,
-    // seems like a poor bet to call it again to diagnose its own error...
-    int stre_errno = errno;
-    if (stre_errno == ERANGE)
-    {
-        return STRINGIZE("strerror_r() can't explain errno " << orig_errno
-                         << " (buffer too small)");
-    }
-    if (stre_errno == EINVAL)
-    {
-        return STRINGIZE("unknown errno " << orig_errno);
-    }
-    // Here we don't even understand the errno from strerror_r()!
-    return STRINGIZE("strerror_r() can't explain errno " << orig_errno
-                     << " (error " << stre_errno << ')');
-}
-#endif  // ! LL_WINDOWS
-
-// boost::filesystem::temp_directory_path() isn't yet in Boost 1.45! :-(
-std::string temp_directory_path()
-{
-#if LL_WINDOWS
-    char buffer[4096];
-    GetTempPathA(sizeof(buffer), buffer);
-    return buffer;
-
-#else  // LL_DARWIN, LL_LINUX
-    static const char* vars[] = { "TMPDIR", "TMP", "TEMP", "TEMPDIR" };
-    BOOST_FOREACH(const char* var, vars)
-    {
-        const char* found = getenv(var);
-        if (found)
-            return found;
-    }
-    return "/tmp";
-#endif // LL_DARWIN, LL_LINUX
-}
-
-// Windows presents a kinda sorta compatibility layer. Code to the yucky
-// Windows names because they're less likely than the Posix names to collide
-// with any other names in this source.
-#if LL_WINDOWS
-#define _remove   DeleteFileA
-#else  // ! LL_WINDOWS
-#define _open     open
-#define _write    write
-#define _close    close
-#define _remove   remove
-#endif  // ! LL_WINDOWS
-
-// Create a text file with specified content "somewhere in the
-// filesystem," cleaning up when it goes out of scope.
-class NamedTempFile
-{
-public:
-    // Function that accepts an ostream ref and (presumably) writes stuff to
-    // it, e.g.:
-    // (lambda::_1 << "the value is " << 17 << '\n')
-    typedef boost::function<void(std::ostream&)> Streamer;
-
-    NamedTempFile(const std::string& ext, const std::string& content):
-        mPath(temp_directory_path())
-    {
-        createFile(ext, lambda::_1 << content);
-    }
-
-    // Disambiguate when passing string literal
-    NamedTempFile(const std::string& ext, const char* content):
-        mPath(temp_directory_path())
-    {
-        createFile(ext, lambda::_1 << content);
-    }
-
-    NamedTempFile(const std::string& ext, const Streamer& func):
-        mPath(temp_directory_path())
-    {
-        createFile(ext, func);
-    }
-
-    ~NamedTempFile()
-    {
-        _remove(mPath.c_str());
-    }
-
-    std::string getName() const { return mPath; }
-
-private:
-    void createFile(const std::string& ext, const Streamer& func)
-    {
-        // Silly maybe, but use 'ext' as the name prefix. Strip off a leading
-        // '.' if present.
-        int pfx_offset = ((! ext.empty()) && ext[0] == '.')? 1 : 0;
-
-#if ! LL_WINDOWS
-        // Make sure mPath ends with a directory separator, if it doesn't already.
-        if (mPath.empty() ||
-            ! (mPath[mPath.length() - 1] == '\\' || mPath[mPath.length() - 1] == '/'))
-        {
-            mPath.append("/");
-        }
-
-        // mkstemp() accepts and modifies a char* template string. Generate
-        // the template string, then copy to modifiable storage.
-        // mkstemp() requires its template string to end in six X's.
-        mPath += ext.substr(pfx_offset) + "XXXXXX";
-        // Copy to vector<char>
-        std::vector<char> pathtemplate(mPath.begin(), mPath.end());
-        // append a nul byte for classic-C semantics
-        pathtemplate.push_back('\0');
-        // std::vector promises that a pointer to the 0th element is the same
-        // as a pointer to a contiguous classic-C array
-        int fd(mkstemp(&pathtemplate[0]));
-        if (fd == -1)
-        {
-            // The documented errno values (http://linux.die.net/man/3/mkstemp)
-            // are used in a somewhat unusual way, so provide context-specific
-            // errors.
-            if (errno == EEXIST)
-            {
-                LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath
-                                         << "\") could not create unique file " << LL_ENDL;
-            }
-            if (errno == EINVAL)
-            {
-                LL_ERRS("NamedTempFile") << "bad mkstemp() file path template '"
-                                         << mPath << "'" << LL_ENDL;
-            }
-            // Shrug, something else
-            int mkst_errno = errno;
-            char buffer[256];
-            LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath << "\") failed: "
-                                     << message_from(mkst_errno, buffer,
-                                                     strerror_r(mkst_errno, buffer, sizeof(buffer)))
-                                     << LL_ENDL;
-        }
-        // mkstemp() seems to have worked! Capture the modified filename.
-        // Avoid the nul byte we appended.
-        mPath.assign(pathtemplate.begin(), (pathtemplate.end()-1));
-
-/*==========================================================================*|
-        // Define an ostream on the open fd. Tell it to close fd on destruction.
-        boost::iostreams::stream<boost::iostreams::file_descriptor_sink>
-            out(fd, boost::iostreams::close_handle);
-|*==========================================================================*/
-
-        // Write desired content.
-        std::ostringstream out;
-        // Stream stuff to it.
-        func(out);
-
-        std::string data(out.str());
-        int written(_write(fd, data.c_str(), data.length()));
-        int closed(_close(fd));
-        llassert_always(written == data.length() && closed == 0);
-
-#else // LL_WINDOWS
-        // GetTempFileName() is documented to require a MAX_PATH buffer.
-        char tempname[MAX_PATH];
-        // Use 'ext' as filename prefix, but skip leading '.' if any.
-        // The 0 param is very important: requests iterating until we get a
-        // unique name.
-        if (0 == GetTempFileNameA(mPath.c_str(), ext.c_str() + pfx_offset, 0, tempname))
-        {
-            // I always have to look up this call...  :-P
-            LPSTR msgptr;
-            FormatMessageA(
-                FORMAT_MESSAGE_ALLOCATE_BUFFER | 
-                FORMAT_MESSAGE_FROM_SYSTEM |
-                FORMAT_MESSAGE_IGNORE_INSERTS,
-                NULL,
-                GetLastError(),
-                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
-                LPSTR(&msgptr),     // have to cast (char**) to (char*)
-                0, NULL );
-            LL_ERRS("NamedTempFile") << "GetTempFileName(\"" << mPath << "\", \""
-                                     << (ext.c_str() + pfx_offset) << "\") failed: "
-                                     << msgptr << LL_ENDL;
-            LocalFree(msgptr);
-        }
-        // GetTempFileName() appears to have worked! Capture the actual
-        // filename.
-        mPath = tempname;
-        // Open the file and stream content to it. Destructor will close.
-        std::ofstream out(tempname);
-        func(out);
-
-#endif  // LL_WINDOWS
-    }
-
-    void peep()
-    {
-        std::cout << "File '" << mPath << "' contains:\n";
-        std::ifstream reader(mPath.c_str());
-        std::string line;
-        while (std::getline(reader, line))
-            std::cout << line << '\n';
-        std::cout << "---\n";
-    }
-
-    std::string mPath;
-};
-
 namespace tut
 {
 	struct sd_xml_data
@@ -1783,7 +1538,7 @@ namespace tut
             const char* PYTHON(getenv("PYTHON"));
             ensure("Set $PYTHON to the Python interpreter", PYTHON);
 
-            NamedTempFile scriptfile(".py", script);
+            NamedTempFile scriptfile("py", script);
 
 #if LL_WINDOWS
             std::string q("\"");
@@ -1802,14 +1557,15 @@ namespace tut
             }
 
 #else  // LL_DARWIN, LL_LINUX
-            LLProcessLauncher py;
-            py.setExecutable(PYTHON);
-            py.addArgument(scriptfile.getName());
-            ensure_equals(STRINGIZE("Couldn't launch " << desc << " script"), py.launch(), 0);
+            LLSD params;
+            params["executable"] = PYTHON;
+            params["args"].append(scriptfile.getName());
+            LLProcessPtr py(LLProcess::create(params));
+            ensure(STRINGIZE("Couldn't launch " << desc << " script"), py);
             // Implementing timeout would mean messing with alarm() and
             // catching SIGALRM... later maybe...
             int status(0);
-            if (waitpid(py.getProcessID(), &status, 0) == -1)
+            if (waitpid(py->getProcessID(), &status, 0) == -1)
             {
                 int waitpid_errno(errno);
                 ensure_equals(STRINGIZE("Couldn't retrieve rc from " << desc << " script: "
@@ -1888,12 +1644,12 @@ namespace tut
             "    else:\n"
             "        assert False, 'Too many data items'\n";
 
-        // Create a something.llsd file containing 'data' serialized to
+        // Create an llsdXXXXXX file containing 'data' serialized to
         // notation. It's important to separate with newlines because Python's
         // llsd module doesn't support parsing from a file stream, only from a
         // string, so we have to know how much of the file to read into a
         // string.
-        NamedTempFile file(".llsd",
+        NamedTempFile file("llsd",
                            // NamedTempFile's boost::function constructor
                            // takes a callable. To this callable it passes the
                            // std::ostream with which it's writing the
@@ -1926,7 +1682,7 @@ namespace tut
         // Create an empty data file. This is just a placeholder for our
         // script to write into. Create it to establish a unique name that
         // we know.
-        NamedTempFile file(".llsd", "");
+        NamedTempFile file("llsd", "");
 
         python("write Python notation",
                lambda::_1 <<
diff --git a/indra/llcommon/tests/llstreamqueue_test.cpp b/indra/llcommon/tests/llstreamqueue_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..050ad5c5bff287ddc7bdad0f7ed7f59997c9f123
--- /dev/null
+++ b/indra/llcommon/tests/llstreamqueue_test.cpp
@@ -0,0 +1,197 @@
+/**
+ * @file   llstreamqueue_test.cpp
+ * @author Nat Goodspeed
+ * @date   2012-01-05
+ * @brief  Test for llstreamqueue.
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llstreamqueue.h"
+// STL headers
+#include <vector>
+// std headers
+// external library headers
+#include <boost/foreach.hpp>
+// other Linden headers
+#include "../test/lltut.h"
+#include "stringize.h"
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct llstreamqueue_data
+    {
+        llstreamqueue_data():
+            // we want a buffer with actual bytes in it, not an empty vector
+            buffer(10)
+        {}
+        // As LLStreamQueue is merely a typedef for
+        // LLGenericStreamQueue<char>, and no logic in LLGenericStreamQueue is
+        // specific to the <char> instantiation, we're comfortable for now
+        // testing only the narrow-char version.
+        LLStreamQueue strq;
+        // buffer for use in multiple tests
+        std::vector<char> buffer;
+    };
+    typedef test_group<llstreamqueue_data> llstreamqueue_group;
+    typedef llstreamqueue_group::object object;
+    llstreamqueue_group llstreamqueuegrp("llstreamqueue");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("empty LLStreamQueue");
+        ensure_equals("brand-new LLStreamQueue isn't empty",
+                      strq.size(), 0);
+        ensure_equals("brand-new LLStreamQueue returns data",
+                      strq.asSource().read(&buffer[0], buffer.size()), 0);
+        strq.asSink().close();
+        ensure_equals("closed empty LLStreamQueue not at EOF",
+                      strq.asSource().read(&buffer[0], buffer.size()), -1);
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("one internal block, one buffer");
+        LLStreamQueue::Sink sink(strq.asSink());
+        ensure_equals("write(\"\")", sink.write("", 0), 0);
+        ensure_equals("0 write should leave LLStreamQueue empty (size())",
+                      strq.size(), 0);
+        ensure_equals("0 write should leave LLStreamQueue empty (peek())",
+                      strq.peek(&buffer[0], buffer.size()), 0);
+        // The meaning of "atomic" is that it must be smaller than our buffer.
+        std::string atomic("atomic");
+        ensure("test data exceeds buffer", atomic.length() < buffer.size());
+        ensure_equals(STRINGIZE("write(\"" << atomic << "\")"),
+                      sink.write(&atomic[0], atomic.length()), atomic.length());
+        ensure_equals("size() after write()", strq.size(), atomic.length());
+        size_t peeklen(strq.peek(&buffer[0], buffer.size()));
+        ensure_equals(STRINGIZE("peek(\"" << atomic << "\")"),
+                      peeklen, atomic.length());
+        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") result"),
+                      std::string(buffer.begin(), buffer.begin() + peeklen), atomic);
+        ensure_equals("size() after peek()", strq.size(), atomic.length());
+        // peek() should not consume. Use a different buffer to prove it isn't
+        // just leftover data from the first peek().
+        std::vector<char> again(buffer.size());
+        peeklen = size_t(strq.peek(&again[0], again.size()));
+        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again"),
+                      peeklen, atomic.length());
+        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again result"),
+                      std::string(again.begin(), again.begin() + peeklen), atomic);
+        // now consume.
+        std::vector<char> third(buffer.size());
+        size_t readlen(strq.read(&third[0], third.size()));
+        ensure_equals(STRINGIZE("read(\"" << atomic << "\")"),
+                      readlen, atomic.length());
+        ensure_equals(STRINGIZE("read(\"" << atomic << "\") result"),
+                      std::string(third.begin(), third.begin() + readlen), atomic);
+        ensure_equals("peek() after read()", strq.peek(&buffer[0], buffer.size()), 0);
+        ensure_equals("size() after read()", strq.size(), 0);
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("basic skip()");
+        std::string lovecraft("lovecraft");
+        ensure("test data exceeds buffer", lovecraft.length() < buffer.size());
+        ensure_equals(STRINGIZE("write(\"" << lovecraft << "\")"),
+                      strq.write(&lovecraft[0], lovecraft.length()), lovecraft.length());
+        size_t peeklen(strq.peek(&buffer[0], buffer.size()));
+        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\")"),
+                      peeklen, lovecraft.length());
+        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\") result"),
+                      std::string(buffer.begin(), buffer.begin() + peeklen), lovecraft);
+        std::streamsize skip1(4);
+        ensure_equals(STRINGIZE("skip(" << skip1 << ")"), strq.skip(skip1), skip1);
+        ensure_equals("size() after skip()", strq.size(), lovecraft.length() - skip1);
+        size_t readlen(strq.read(&buffer[0], buffer.size()));
+        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\")"),
+                      readlen, lovecraft.length() - skip1);
+        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\") result"),
+                      std::string(buffer.begin(), buffer.begin() + readlen),
+                      lovecraft.substr(skip1));
+        ensure_equals("unconsumed", strq.read(&buffer[0], buffer.size()), 0);
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("skip() multiple blocks");
+        std::string blocks[] = { "books of ", "H.P. ", "Lovecraft" };
+        std::streamsize total(blocks[0].length() + blocks[1].length() + blocks[2].length());
+        std::streamsize leave(5);   // len("craft") above
+        std::streamsize skip(total - leave);
+        std::streamsize written(0);
+        BOOST_FOREACH(const std::string& block, blocks)
+        {
+            written += strq.write(&block[0], block.length());
+            ensure_equals("size() after write()", strq.size(), written);
+        }
+        std::streamsize skiplen(strq.skip(skip));
+        ensure_equals(STRINGIZE("skip(" << skip << ")"), skiplen, skip);
+        ensure_equals("size() after skip()", strq.size(), leave);
+        size_t readlen(strq.read(&buffer[0], buffer.size()));
+        ensure_equals("read(\"craft\")", readlen, leave);
+        ensure_equals("read(\"craft\") result",
+                      std::string(buffer.begin(), buffer.begin() + readlen), "craft");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("concatenate blocks");
+        std::string blocks[] = { "abcd", "efghij", "klmnopqrs" };
+        BOOST_FOREACH(const std::string& block, blocks)
+        {
+            strq.write(&block[0], block.length());
+        }
+        std::vector<char> longbuffer(30);
+        std::streamsize readlen(strq.read(&longbuffer[0], longbuffer.size()));
+        ensure_equals("read() multiple blocks",
+                      readlen, blocks[0].length() + blocks[1].length() + blocks[2].length());
+        ensure_equals("read() multiple blocks result",
+                      std::string(longbuffer.begin(), longbuffer.begin() + readlen),
+                      blocks[0] + blocks[1] + blocks[2]);
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("split blocks");
+        std::string blocks[] = { "abcdefghijklm", "nopqrstuvwxyz" };
+        BOOST_FOREACH(const std::string& block, blocks)
+        {
+            strq.write(&block[0], block.length());
+        }
+        strq.close();
+        // We've already verified what strq.size() should be at this point;
+        // see above test named "skip() multiple blocks"
+        std::streamsize chksize(strq.size());
+        std::streamsize readlen(strq.read(&buffer[0], buffer.size()));
+        ensure_equals("read() 0", readlen, buffer.size());
+        ensure_equals("read() 0 result", std::string(buffer.begin(), buffer.end()), "abcdefghij");
+        chksize -= readlen;
+        ensure_equals("size() after read() 0", strq.size(), chksize);
+        readlen = strq.read(&buffer[0], buffer.size());
+        ensure_equals("read() 1", readlen, buffer.size());
+        ensure_equals("read() 1 result", std::string(buffer.begin(), buffer.end()), "klmnopqrst");
+        chksize -= readlen;
+        ensure_equals("size() after read() 1", strq.size(), chksize);
+        readlen = strq.read(&buffer[0], buffer.size());
+        ensure_equals("read() 2", readlen, chksize);
+        ensure_equals("read() 2 result",
+                      std::string(buffer.begin(), buffer.begin() + readlen), "uvwxyz");
+        ensure_equals("read() 3", strq.read(&buffer[0], buffer.size()), -1);
+    }
+} // namespace tut
diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp
index 110fac0f2388abe4a39bb009cd660fe899e89e52..9b225cabb8fd3e0e68cefbb7c50859d4e7597e44 100644
--- a/indra/llplugin/llpluginprocessparent.cpp
+++ b/indra/llplugin/llpluginprocessparent.cpp
@@ -31,6 +31,7 @@
 #include "llpluginprocessparent.h"
 #include "llpluginmessagepipe.h"
 #include "llpluginmessageclasses.h"
+#include "stringize.h"
 
 #include "llapr.h"
 
@@ -134,7 +135,10 @@ LLPluginProcessParent::~LLPluginProcessParent()
 		mSharedMemoryRegions.erase(iter);
 	}
 	
-	mProcess.kill();
+	if (mProcess)
+	{
+		mProcess->kill();
+	}
 	killSockets();
 }
 
@@ -159,8 +163,8 @@ void LLPluginProcessParent::errorState(void)
 
 void LLPluginProcessParent::init(const std::string &launcher_filename, const std::string &plugin_dir, const std::string &plugin_filename, bool debug)
 {	
-	mProcess.setExecutable(launcher_filename);
-	mProcess.setWorkingDirectory(plugin_dir);
+	mProcessParams["executable"] = launcher_filename;
+	mProcessParams["cwd"] = plugin_dir;
 	mPluginFile = plugin_filename;
 	mPluginDir = plugin_dir;
 	mCPUUsage = 0.0f;
@@ -371,10 +375,8 @@ void LLPluginProcessParent::idle(void)
 				// Launch the plugin process.
 				
 				// Only argument to the launcher is the port number we're listening on
-				std::stringstream stream;
-				stream << mBoundPort;
-				mProcess.addArgument(stream.str());
-				if(mProcess.launch() != 0)
+				mProcessParams["args"].append(stringize(mBoundPort));
+				if (! (mProcess = LLProcess::create(mProcessParams)))
 				{
 					errorState();
 				}
@@ -388,19 +390,18 @@ void LLPluginProcessParent::idle(void)
 						// The command we're constructing would look like this on the command line:
 						// osascript -e 'tell application "Terminal"' -e 'set win to do script "gdb -pid 12345"' -e 'do script "continue" in win' -e 'end tell'
 
-						std::stringstream cmd;
-						
-						mDebugger.setExecutable("/usr/bin/osascript");
-						mDebugger.addArgument("-e");
-						mDebugger.addArgument("tell application \"Terminal\"");
-						mDebugger.addArgument("-e");
-						cmd << "set win to do script \"gdb -pid " << mProcess.getProcessID() << "\"";
-						mDebugger.addArgument(cmd.str());
-						mDebugger.addArgument("-e");
-						mDebugger.addArgument("do script \"continue\" in win");
-						mDebugger.addArgument("-e");
-						mDebugger.addArgument("end tell");
-						mDebugger.launch();
+						LLSD params;
+						params["executable"] = "/usr/bin/osascript";
+						params["args"].append("-e");
+						params["args"].append("tell application \"Terminal\"");
+						params["args"].append("-e");
+						params["args"].append(STRINGIZE("set win to do script \"gdb -pid "
+														<< mProcess->getProcessID() << "\""));
+						params["args"].append("-e");
+						params["args"].append("do script \"continue\" in win");
+						params["args"].append("-e");
+						params["args"].append("end tell");
+						mDebugger = LLProcess::create(params);
 
 						#endif
 					}
@@ -470,7 +471,7 @@ void LLPluginProcessParent::idle(void)
 			break;
 			
 			case STATE_EXITING:
-				if(!mProcess.isRunning())
+				if (! mProcess->isRunning())
 				{
 					setState(STATE_CLEANUP);
 				}
@@ -498,7 +499,7 @@ void LLPluginProcessParent::idle(void)
 			break;
 			
 			case STATE_CLEANUP:
-				mProcess.kill();
+				mProcess->kill();
 				killSockets();
 				setState(STATE_DONE);
 			break;
@@ -1077,7 +1078,7 @@ bool LLPluginProcessParent::pluginLockedUpOrQuit()
 {
 	bool result = false;
 	
-	if(!mProcess.isRunning())
+	if (! mProcess->isRunning())
 	{
 		LL_WARNS("Plugin") << "child exited" << LL_ENDL;
 		result = true;
diff --git a/indra/llplugin/llpluginprocessparent.h b/indra/llplugin/llpluginprocessparent.h
index c66723f1753c7a7046e21b57a3a8c2789f7ccaa1..e8bcba75e07b80f0c7b4347ad8348a84100f04c2 100644
--- a/indra/llplugin/llpluginprocessparent.h
+++ b/indra/llplugin/llpluginprocessparent.h
@@ -30,13 +30,14 @@
 #define LL_LLPLUGINPROCESSPARENT_H
 
 #include "llapr.h"
-#include "llprocesslauncher.h"
+#include "llprocess.h"
 #include "llpluginmessage.h"
 #include "llpluginmessagepipe.h"
 #include "llpluginsharedmemory.h"
 
 #include "lliosocket.h"
 #include "llthread.h"
+#include "llsd.h"
 
 class LLPluginProcessParentOwner
 {
@@ -148,8 +149,9 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner
 	LLSocket::ptr_t mListenSocket;
 	LLSocket::ptr_t mSocket;
 	U32 mBoundPort;
-	
-	LLProcessLauncher mProcess;
+
+	LLSD mProcessParams;
+	LLProcessPtr mProcess;
 	
 	std::string mPluginFile;
 	std::string mPluginDir;
@@ -171,7 +173,7 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner
 	bool mBlocked;
 	bool mPolledInput;
 
-	LLProcessLauncher mDebugger;
+	LLProcessPtr mDebugger;
 	
 	F32 mPluginLaunchTimeout;		// Somewhat longer timeout for initial launch.
 	F32 mPluginLockupTimeout;		// If we don't receive a heartbeat in this many seconds, we declare the plugin locked up.
diff --git a/indra/newview/llexternaleditor.cpp b/indra/newview/llexternaleditor.cpp
index ed1d7e860a0cfa672fe78857a6b8861059c5169f..ba58cd8067822c18b949df2db9947eb18cb83910 100644
--- a/indra/newview/llexternaleditor.cpp
+++ b/indra/newview/llexternaleditor.cpp
@@ -29,6 +29,9 @@
 
 #include "lltrans.h"
 #include "llui.h"
+#include "llprocess.h"
+#include "llsdutil.h"
+#include <boost/foreach.hpp>
 
 // static
 const std::string LLExternalEditor::sFilenameMarker = "%s";
@@ -45,19 +48,8 @@ LLExternalEditor::EErrorCode LLExternalEditor::setCommand(const std::string& env
 		return EC_NOT_SPECIFIED;
 	}
 
-	// Add the filename marker if missing.
-	if (cmd.find(sFilenameMarker) == std::string::npos)
-	{
-		cmd += " \"" + sFilenameMarker + "\"";
-		llinfos << "Adding the filename marker (" << sFilenameMarker << ")" << llendl;
-	}
-
 	string_vec_t tokens;
-	if (tokenize(tokens, cmd) < 2) // 2 = bin + at least one arg (%s)
-	{
-		llwarns << "Error parsing editor command" << llendl;
-		return EC_PARSE_ERROR;
-	}
+	tokenize(tokens, cmd);
 
 	// Check executable for existence.
 	std::string bin_path = tokens[0];
@@ -68,51 +60,60 @@ LLExternalEditor::EErrorCode LLExternalEditor::setCommand(const std::string& env
 	}
 
 	// Save command.
-	mProcess.setExecutable(bin_path);
-	mArgs.clear();
+	mProcessParams["executable"] = bin_path;
+	mProcessParams["args"].clear();
 	for (size_t i = 1; i < tokens.size(); ++i)
 	{
-		if (i > 1) mArgs += " ";
-		mArgs += "\"" + tokens[i] + "\"";
+		mProcessParams["args"].append(tokens[i]);
+	}
+
+	// Add the filename marker if missing.
+	if (cmd.find(sFilenameMarker) == std::string::npos)
+	{
+		mProcessParams["args"].append(sFilenameMarker);
+		llinfos << "Adding the filename marker (" << sFilenameMarker << ")" << llendl;
+	}
+
+	llinfos << "Setting command [" << bin_path;
+	BOOST_FOREACH(const std::string& arg, llsd::inArray(mProcessParams["args"]))
+	{
+		llcont << " \"" << arg << "\"";
 	}
-	llinfos << "Setting command [" << bin_path << " " << mArgs << "]" << llendl;
+	llcont << "]" << llendl;
 
 	return EC_SUCCESS;
 }
 
 LLExternalEditor::EErrorCode LLExternalEditor::run(const std::string& file_path)
 {
-	std::string args = mArgs;
-	if (mProcess.getExecutable().empty() || args.empty())
+	if (mProcessParams["executable"].asString().empty() || ! mProcessParams["args"].size())
 	{
 		llwarns << "Editor command not set" << llendl;
 		return EC_NOT_SPECIFIED;
 	}
 
-	// Substitute the filename marker in the command with the actual passed file name.
-	LLStringUtil::replaceString(args, sFilenameMarker, file_path);
-
-	// Split command into separate tokens.
-	string_vec_t tokens;
-	tokenize(tokens, args);
+	// Copy params block so we can replace sFilenameMarker
+	LLSD params(mProcessParams);
 
-	// Set process arguments taken from the command.
-	mProcess.clearArguments();
-	for (string_vec_t::const_iterator arg_it = tokens.begin(); arg_it != tokens.end(); ++arg_it)
+	// Substitute the filename marker in the command with the actual passed file name.
+	LLSD& args(params["args"]);
+	for (LLSD::array_iterator ai(args.beginArray()), aend(args.endArray()); ai != aend; ++ai)
 	{
-		mProcess.addArgument(*arg_it);
+		std::string sarg(*ai);
+		LLStringUtil::replaceString(sarg, sFilenameMarker, file_path);
+		*ai = sarg;
 	}
 
 	// Run the editor.
-	llinfos << "Running editor command [" << mProcess.getExecutable() + " " + args << "]" << llendl;
-	int result = mProcess.launch();
-	if (result == 0)
+	llinfos << "Running editor command [" << params["executable"];
+	BOOST_FOREACH(const std::string& arg, llsd::inArray(params["args"]))
 	{
-		// Prevent killing the process in destructor (will add it to the zombies list).
-		mProcess.orphan();
+		llcont << " \"" << arg << "\"";
 	}
-
-	return result == 0 ? EC_SUCCESS : EC_FAILED_TO_RUN;
+	llcont << "]" << llendl;
+	// Prevent killing the process in destructor.
+	params["autokill"] = false;
+	return LLProcess::create(params) ? EC_SUCCESS : EC_FAILED_TO_RUN;
 }
 
 // static
diff --git a/indra/newview/llexternaleditor.h b/indra/newview/llexternaleditor.h
index ef5db56c6ee056e6ed1ee560c20ae2e1abe51e92..e81c360c24b47dc160411a82e13998af949c1ca3 100644
--- a/indra/newview/llexternaleditor.h
+++ b/indra/newview/llexternaleditor.h
@@ -27,7 +27,7 @@
 #ifndef LL_LLEXTERNALEDITOR_H
 #define LL_LLEXTERNALEDITOR_H
 
-#include <llprocesslauncher.h>
+#include "llsd.h"
 
 /**
  * Usage:
@@ -98,8 +98,7 @@ class LLExternalEditor
 	static const std::string sSetting;
 
 
-	std::string			mArgs;
-	LLProcessLauncher	mProcess;
+	LLSD				mProcessParams;
 };
 
 #endif // LL_LLEXTERNALEDITOR_H
diff --git a/indra/test/manageapr.h b/indra/test/manageapr.h
new file mode 100644
index 0000000000000000000000000000000000000000..0c1ca7b7be14162a42f23d9316c6af44749036c9
--- /dev/null
+++ b/indra/test/manageapr.h
@@ -0,0 +1,45 @@
+/**
+ * @file   manageapr.h
+ * @author Nat Goodspeed
+ * @date   2012-01-13
+ * @brief  ManageAPR class for simple test programs
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_MANAGEAPR_H)
+#define LL_MANAGEAPR_H
+
+#include "llapr.h"
+
+/**
+ * Declare a static instance of this class for dead-simple ll_init_apr() at
+ * program startup, ll_cleanup_apr() at termination. This is recommended for
+ * use only with simple test programs. Once you start introducing static
+ * instances of other classes that depend on APR already being initialized,
+ * the indeterminate static-constructor-order problem rears its ugly head.
+ */
+class ManageAPR
+{
+public:
+    ManageAPR()
+    {
+        ll_init_apr();
+    }
+
+    ~ManageAPR()
+    {
+        ll_cleanup_apr();
+    }
+
+    static std::string strerror(apr_status_t rv)
+    {
+        char errbuf[256];
+        apr_strerror(rv, errbuf, sizeof(errbuf));
+        return errbuf;
+    }
+};
+
+#endif /* ! defined(LL_MANAGEAPR_H) */
diff --git a/indra/test/namedtempfile.h b/indra/test/namedtempfile.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa7058b1117cc73f967b2e7b6c49572aefe58e73
--- /dev/null
+++ b/indra/test/namedtempfile.h
@@ -0,0 +1,114 @@
+/**
+ * @file   namedtempfile.h
+ * @author Nat Goodspeed
+ * @date   2012-01-13
+ * @brief  NamedTempFile class for tests that need disk files as fixtures.
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_NAMEDTEMPFILE_H)
+#define LL_NAMEDTEMPFILE_H
+
+#include "llapr.h"
+#include "apr_file_io.h"
+#include <string>
+#include <boost/function.hpp>
+#include <boost/lambda/lambda.hpp>
+#include <boost/lambda/bind.hpp>
+#include <boost/noncopyable.hpp>
+#include <iostream>
+#include <sstream>
+
+/**
+ * Create a text file with specified content "somewhere in the
+ * filesystem," cleaning up when it goes out of scope.
+ */
+class NamedTempFile: public boost::noncopyable
+{
+public:
+    NamedTempFile(const std::string& pfx, const std::string& content, apr_pool_t* pool=gAPRPoolp):
+        mPool(pool)
+    {
+        createFile(pfx, boost::lambda::_1 << content);
+    }
+
+    // Disambiguate when passing string literal
+    NamedTempFile(const std::string& pfx, const char* content, apr_pool_t* pool=gAPRPoolp):
+        mPool(pool)
+    {
+        createFile(pfx, boost::lambda::_1 << content);
+    }
+
+    // Function that accepts an ostream ref and (presumably) writes stuff to
+    // it, e.g.:
+    // (boost::lambda::_1 << "the value is " << 17 << '\n')
+    typedef boost::function<void(std::ostream&)> Streamer;
+
+    NamedTempFile(const std::string& pfx, const Streamer& func, apr_pool_t* pool=gAPRPoolp):
+        mPool(pool)
+    {
+        createFile(pfx, func);
+    }
+
+    ~NamedTempFile()
+    {
+        ll_apr_assert_status(apr_file_remove(mPath.c_str(), mPool));
+    }
+
+    std::string getName() const { return mPath; }
+
+    void peep()
+    {
+        std::cout << "File '" << mPath << "' contains:\n";
+        std::ifstream reader(mPath.c_str());
+        std::string line;
+        while (std::getline(reader, line))
+            std::cout << line << '\n';
+        std::cout << "---\n";
+    }
+
+private:
+    void createFile(const std::string& pfx, const Streamer& func)
+    {
+        // Create file in a temporary place.
+        const char* tempdir = NULL;
+        ll_apr_assert_status(apr_temp_dir_get(&tempdir, mPool));
+
+        // Construct a temp filename template in that directory.
+        char *tempname = NULL;
+        ll_apr_assert_status(apr_filepath_merge(&tempname,
+                                                tempdir,
+                                                (pfx + "XXXXXX").c_str(),
+                                                0,
+                                                mPool));
+
+        // Create a temp file from that template.
+        apr_file_t* fp = NULL;
+        ll_apr_assert_status(apr_file_mktemp(&fp,
+                                             tempname,
+                                             APR_CREATE | APR_WRITE | APR_EXCL,
+                                             mPool));
+        // apr_file_mktemp() alters tempname with the actual name. Not until
+        // now is it valid to capture as our mPath.
+        mPath = tempname;
+
+        // Write desired content.
+        std::ostringstream out;
+        // Stream stuff to it.
+        func(out);
+
+        std::string data(out.str());
+        apr_size_t writelen(data.length());
+        ll_apr_assert_status(apr_file_write(fp, data.c_str(), &writelen));
+        ll_apr_assert_status(apr_file_close(fp));
+        llassert_always(writelen == data.length());
+    }
+
+    std::string mPath;
+    apr_pool_t* mPool;
+};
+
+#endif /* ! defined(LL_NAMEDTEMPFILE_H) */
diff --git a/indra/viewer_components/updater/llupdateinstaller.cpp b/indra/viewer_components/updater/llupdateinstaller.cpp
index c7b70c2de8e91fcd8deec2075c3a66f9df8f8675..e99fd0af7ee715040f6bc1737953439bb5ba2c30 100644
--- a/indra/viewer_components/updater/llupdateinstaller.cpp
+++ b/indra/viewer_components/updater/llupdateinstaller.cpp
@@ -26,10 +26,10 @@
 #include "linden_common.h"
 #include <apr_file_io.h>
 #include "llapr.h"
-#include "llprocesslauncher.h"
+#include "llprocess.h"
 #include "llupdateinstaller.h"
 #include "lldir.h" 
-
+#include "llsd.h"
 
 #if defined(LL_WINDOWS)
 #pragma warning(disable: 4702)      // disable 'unreachable code' so we can use lexical_cast (really!).
@@ -78,15 +78,13 @@ int ll_install_update(std::string const & script,
 	llinfos << "UpdateInstaller: installing " << updatePath << " using " <<
 		actualScriptPath << LL_ENDL;
 	
-	LLProcessLauncher launcher;
-	launcher.setExecutable(actualScriptPath);
-	launcher.addArgument(updatePath);
-	launcher.addArgument(ll_install_failed_marker_path().c_str());
-	launcher.addArgument(boost::lexical_cast<std::string>(required));
-	int result = launcher.launch();
-	launcher.orphan();
-	
-	return result;
+	LLSD params;
+	params["executable"] = actualScriptPath;
+	params["args"].append(updatePath);
+	params["args"].append(ll_install_failed_marker_path());
+	params["args"].append(boost::lexical_cast<std::string>(required));
+	params["autokill"] = false;
+	return LLProcess::create(params)? 0 : -1;
 }