diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
index d6a5a18565101082cd7f984b1b48a41c8b576815..9799ed19383117333ded67f6a76381b994d181d7 100644
--- a/indra/llcommon/llprocess.cpp
+++ b/indra/llcommon/llprocess.cpp
@@ -125,7 +125,7 @@ const LLProcess::BasePipe::size_type
 
 class WritePipeImpl: public LLProcess::WritePipe
 {
-    LOG_CLASS(WritePipeImpl);
+	LOG_CLASS(WritePipeImpl);
 public:
 	WritePipeImpl(const std::string& desc, apr_file_t* pipe):
 		mDesc(desc),
@@ -202,7 +202,7 @@ class WritePipeImpl: public LLProcess::WritePipe
 
 class ReadPipeImpl: public LLProcess::ReadPipe
 {
-    LOG_CLASS(ReadPipeImpl);
+	LOG_CLASS(ReadPipeImpl);
 public:
 	ReadPipeImpl(const std::string& desc, apr_file_t* pipe):
 		mDesc(desc),
@@ -394,6 +394,23 @@ LLProcessPtr LLProcess::create(const LLSDOrParams& params)
 	catch (const LLProcessError& e)
 	{
 		LL_WARNS("LLProcess") << e.what() << LL_ENDL;
+
+		// If caller is requesting an event on process termination, send one
+		// indicating bad launch. This may prevent someone waiting forever for
+		// a termination post that can't arrive because the child never
+		// started.
+		if (! std::string(params.postend).empty())
+		{
+			LLEventPumps::instance().obtain(params.postend)
+				.post(LLSDMap
+					  // no "id"
+					  ("desc", std::string(params.executable))
+					  ("state", LLProcess::UNSTARTED)
+					  // no "data"
+					  ("string", e.what())
+					 );
+		}
+
 		return LLProcessPtr();
 	}
 }
@@ -425,6 +442,8 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 									   << LLSDNotationStreamer(params)));
 	}
 
+	mPostend = params.postend;
+
 	apr_procattr_t *procattr = NULL;
 	chkapr(apr_procattr_create(&procattr, gAPRPoolp));
 
@@ -744,6 +763,19 @@ void LLProcess::handle_status(int reason, int status)
 	// hand.
 	mStatus = interpret_status(status);
 	LL_INFOS("LLProcess") << getStatusString() << LL_ENDL;
+
+	// If caller requested notification on child termination, send it.
+	if (! mPostend.empty())
+	{
+		LLEventPumps::instance().obtain(mPostend)
+			.post(LLSDMap
+				  ("id",     getProcessID())
+				  ("desc",   mDesc)
+				  ("state",  mStatus.mState)
+				  ("data",   mStatus.mData)
+				  ("string", getStatusString())
+				 );
+	}
 }
 
 LLProcess::id LLProcess::getProcessID() const
@@ -769,72 +801,72 @@ std::string LLProcess::getPipeName(FILESLOT)
 template<class PIPETYPE>
 PIPETYPE* LLProcess::getPipePtr(std::string& error, FILESLOT slot)
 {
-    if (slot >= NSLOTS)
-    {
-        error = STRINGIZE(mDesc << " has no slot " << slot);
-        return NULL;
-    }
-    if (mPipes.is_null(slot))
-    {
-        error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a monitored pipe");
-        return NULL;
-    }
-    // Make sure we dynamic_cast in pointer domain so we can test, rather than
-    // accepting runtime's exception.
-    PIPETYPE* ppipe = dynamic_cast<PIPETYPE*>(&mPipes[slot]);
-    if (! ppipe)
-    {
-        error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a " << typeid(PIPETYPE).name());
-        return NULL;
-    }
-
-    error.clear();
-    return ppipe;
+	if (slot >= NSLOTS)
+	{
+		error = STRINGIZE(mDesc << " has no slot " << slot);
+		return NULL;
+	}
+	if (mPipes.is_null(slot))
+	{
+		error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a monitored pipe");
+		return NULL;
+	}
+	// Make sure we dynamic_cast in pointer domain so we can test, rather than
+	// accepting runtime's exception.
+	PIPETYPE* ppipe = dynamic_cast<PIPETYPE*>(&mPipes[slot]);
+	if (! ppipe)
+	{
+		error = STRINGIZE(mDesc << ' ' << whichfile[slot] << " not a " << typeid(PIPETYPE).name());
+		return NULL;
+	}
+
+	error.clear();
+	return ppipe;
 }
 
 template <class PIPETYPE>
 PIPETYPE& LLProcess::getPipe(FILESLOT slot)
 {
-    std::string error;
-    PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
-    if (! wp)
-    {
-        throw NoPipe(error);
-    }
-    return *wp;
+	std::string error;
+	PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
+	if (! wp)
+	{
+		throw NoPipe(error);
+	}
+	return *wp;
 }
 
 template <class PIPETYPE>
 boost::optional<PIPETYPE&> LLProcess::getOptPipe(FILESLOT slot)
 {
-    std::string error;
-    PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
-    if (! wp)
-    {
-        LL_DEBUGS("LLProcess") << error << LL_ENDL;
-        return boost::optional<PIPETYPE&>();
-    }
-    return *wp;
+	std::string error;
+	PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
+	if (! wp)
+	{
+		LL_DEBUGS("LLProcess") << error << LL_ENDL;
+		return boost::optional<PIPETYPE&>();
+	}
+	return *wp;
 }
 
 LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot)
 {
-    return getPipe<WritePipe>(slot);
+	return getPipe<WritePipe>(slot);
 }
 
 boost::optional<LLProcess::WritePipe&> LLProcess::getOptWritePipe(FILESLOT slot)
 {
-    return getOptPipe<WritePipe>(slot);
+	return getOptPipe<WritePipe>(slot);
 }
 
 LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot)
 {
-    return getPipe<ReadPipe>(slot);
+	return getPipe<ReadPipe>(slot);
 }
 
 boost::optional<LLProcess::ReadPipe&> LLProcess::getOptReadPipe(FILESLOT slot)
 {
-    return getOptPipe<ReadPipe>(slot);
+	return getOptPipe<ReadPipe>(slot);
 }
 
 std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params)
@@ -932,7 +964,7 @@ static std::string WindowsErrorString(const std::string& operation)
 					   NULL)
 		!= 0) 
 	{
-        // convert from wide-char string to multi-byte string
+		// convert from wide-char string to multi-byte string
 		char message[256];
 		wcstombs(message, error_str, sizeof(message));
 		message[sizeof(message)-1] = 0;
diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h
index 06be0954c061ab0d121d31a4e6df52734a338659..96a3dce5b30ca6998ce8850bee1d72e7f870f059 100644
--- a/indra/llcommon/llprocess.h
+++ b/indra/llcommon/llprocess.h
@@ -158,7 +158,8 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 			args("args"),
 			cwd("cwd"),
 			autokill("autokill", true),
-			files("files")
+			files("files"),
+			postend("postend")
 		{}
 
 		/// pathname of executable
@@ -184,6 +185,20 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 		 * underlying implementation library doesn't support that.
 		 */
 		Multiple<FileParam> files;
+		/**
+		 * On child-process termination, if this LLProcess object still
+		 * exists, post LLSD event to LLEventPump with specified name (default
+		 * no event). Event contains at least:
+		 *
+		 * - "id" as obtained from getProcessID()
+		 * - "desc" short string description of child (executable + pid)
+		 * - "state" @c state enum value, from Status.mState
+		 * - "data"	 if "state" is EXITED, exit code; if KILLED, on Posix,
+		 *   signal number
+		 * - "string" English text describing "state" and "data" (e.g. "exited
+		 *   with code 0")
+		 */
+		Optional<std::string> postend;
 	};
 	typedef LLSDParamAdapter<Params> LLSDOrParams;
 
@@ -462,6 +477,7 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 	PIPETYPE* getPipePtr(std::string& error, FILESLOT slot);
 
 	std::string mDesc;
+	std::string mPostend;
 	apr_proc_t mProcess;
 	bool mAutokill;
 	Status mStatus;
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
index c67605cc0b4be758514f79dd7bb0b3c25c1c2864..1a755c283c4bbb1a2f66f7626642427296b15924 100644
--- a/indra/llcommon/tests/llprocess_test.cpp
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -1206,6 +1206,65 @@ namespace tut
         ensure("find(\"ghi\", 27)", childout.find("ghi", 27) == LLProcess::ReadPipe::npos);
     }
 
+    template<> template<>
+    void object::test<20>()
+    {
+        set_test_name("good postend");
+        PythonProcessLauncher py("postend",
+                                 "import sys\n"
+                                 "sys.exit(35)\n");
+        std::string pumpname("postend");
+        EventListener listener(LLEventPumps::instance().obtain(pumpname));
+        py.mParams.postend = pumpname;
+        py.launch();
+        LLProcess::id childid(py.mPy->getProcessID());
+        // Don't use waitfor(), which calls isRunning(); instead wait for an
+        // event on pumpname.
+        int i, timeout = 60;
+        for (i = 0; i < timeout && listener.mHistory.empty(); ++i)
+        {
+            yield();
+        }
+        ensure("no postend event", i < timeout);
+        ensure_equals("number of postend events", listener.mHistory.size(), 1);
+        LLSD postend(listener.mHistory.front());
+        ensure_equals("id",    postend["id"].asInteger(), childid);
+        ensure("desc empty", ! postend["desc"].asString().empty());
+        ensure_equals("state", postend["state"].asInteger(), LLProcess::EXITED);
+        ensure_equals("data",  postend["data"].asInteger(),  35);
+        std::string str(postend["string"]);
+        ensure_contains("string", str, "exited");
+        ensure_contains("string", str, "35");
+    }
+
+    template<> template<>
+    void object::test<21>()
+    {
+        set_test_name("bad postend");
+        std::string pumpname("postend");
+        EventListener listener(LLEventPumps::instance().obtain(pumpname));
+        LLProcess::Params params;
+        params.postend = pumpname;
+        LLProcessPtr child = LLProcess::create(params);
+        ensure("shouldn't have launched", ! child);
+        ensure_equals("number of postend events", listener.mHistory.size(), 1);
+        LLSD postend(listener.mHistory.front());
+        ensure("has id", ! postend.has("id"));
+        // Ha ha, in this case the implementation normally sets "desc" to
+        // params.executable. But as the nature of the problem is that
+        // params.executable is empty, expecting "desc" to be nonempty is a
+        // bit unreasonable!
+        //ensure("desc empty", ! postend["desc"].asString().empty());
+        ensure_equals("state", postend["state"].asInteger(), LLProcess::UNSTARTED);
+        ensure("has data", ! postend.has("data"));
+        std::string error(postend["string"]);
+        // All we get from canned parameter validation is a bool, so the
+        // "validation failed" message we ourselves generate can't mention
+        // "executable" by name. Just check that it's nonempty.
+        //ensure_contains("error", error, "executable");
+        ensure("string", ! error.empty());
+    }
+
     // TODO:
     // test EOF -- check logging