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