summaryrefslogtreecommitdiff
path: root/indra/llcommon/llprocess.cpp
diff options
context:
space:
mode:
authorAndrey Kleshchev <117672381+akleshchev@users.noreply.github.com>2026-07-14 23:24:48 +0300
committerGitHub <noreply@github.com>2026-07-14 23:24:48 +0300
commit8b909b6e4397efac013bebed965b91bc28b04679 (patch)
tree3aa13992564cfd07db1a196cacc858a0178f50f3 /indra/llcommon/llprocess.cpp
parentd9f67028b515dfdc7d8f3b8bf73b1f23a8ed1273 (diff)
#5994 Replace apr with boost in LLProcess
Replace apr's process with boost::process::v2 in LLProcess Note: this is mostly copilot-made
Diffstat (limited to 'indra/llcommon/llprocess.cpp')
-rw-r--r--indra/llcommon/llprocess.cpp1854
1 files changed, 853 insertions, 1001 deletions
diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
index 4a01ec567e..99abbfabbb 100644
--- a/indra/llcommon/llprocess.cpp
+++ b/indra/llcommon/llprocess.cpp
@@ -30,30 +30,48 @@
#include "llsdserialize.h"
#include "llsingleton.h"
#include "llstring.h"
-#include "stringize.h"
-#include "llapr.h"
-#include "apr_signal.h"
#include "llevents.h"
#include "llexception.h"
+#include "stringize.h"
-#include <boost/bind.hpp>
+#include <boost/process/v2/process.hpp>
+#include <boost/process/v2/environment.hpp>
+#include <boost/process/v2/start_dir.hpp>
+#include <boost/process/v2/stdio.hpp>
+#include <boost/asio.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/asio/buffers_iterator.hpp>
+#include <boost/filesystem/path.hpp>
#include <iostream>
#include <stdexcept>
#include <limits>
#include <algorithm>
+#include <chrono>
+#include <cstring>
+#include <errno.h>
+#include <thread>
#include <vector>
#include <typeinfo>
+#include <signal.h>
#include <utility>
+#if !LL_WINDOWS
+ // not necessarily available on random SDL platforms
+ // for waitpid()
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#endif
+
+
+namespace bp = boost::process::v2;
+namespace asio = boost::asio;
+
/*****************************************************************************
* Helpers
*****************************************************************************/
+
static const char* whichfile_[] = { "stdin", "stdout", "stderr" };
-static std::string empty;
-static LLProcess::Status interpret_status(int status);
-static std::string getDesc(const LLProcess::Params& params);
static std::string whichfile(LLProcess::FILESLOT index)
{
@@ -62,799 +80,781 @@ static std::string whichfile(LLProcess::FILESLOT index)
return STRINGIZE("file slot " << index);
}
-/**
- * Ref-counted "mainloop" listener. As long as there are still outstanding
- * LLProcess objects, keep listening on "mainloop" so we can keep polling APR
- * for process status.
- */
-class LLProcessListener
+std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params)
{
- LOG_CLASS(LLProcessListener);
-public:
- LLProcessListener():
- mCount(0)
- {}
-
- void addPoll(const LLProcess&)
- {
- // Unconditionally increment mCount. If it was zero before
- // incrementing, listen on "mainloop".
- if (mCount++ == 0)
- {
- LL_DEBUGS("LLProcess") << "listening on \"mainloop\"" << LL_ENDL;
- mConnection = LLEventPumps::instance().obtain("mainloop")
- .listen("LLProcessListener", boost::bind(&LLProcessListener::tick, this, _1));
- }
- }
-
- void dropPoll(const LLProcess&)
+ if (params.cwd.isProvided())
{
- // Unconditionally decrement mCount. If it's zero after decrementing,
- // stop listening on "mainloop".
- if (--mCount == 0)
- {
- LL_DEBUGS("LLProcess") << "disconnecting from \"mainloop\"" << LL_ENDL;
- mConnection.disconnect();
- }
+ out << "cd " << LLStringUtil::quote(params.cwd) << ": ";
}
-
-private:
- /// called once per frame by the "mainloop" LLEventPump
- bool tick(const LLSD&)
+ out << LLStringUtil::quote(params.executable);
+ for (const std::string& arg : params.args)
{
- // Tell APR to sense whether each registered LLProcess is still
- // running and call handle_status() appropriately. We should be able
- // to get the same info from an apr_proc_wait(APR_NOWAIT) call; but at
- // least in APR 1.4.2, testing suggests that even with APR_NOWAIT,
- // apr_proc_wait() blocks the caller. We can't have that in the
- // viewer. Hence the callback rigmarole. (Once we update APR, it's
- // probably worth testing again.) Also -- although there's an
- // apr_proc_other_child_refresh() call, i.e. get that information for
- // one specific child, it accepts an 'apr_other_child_rec_t*' that's
- // mentioned NOWHERE else in the documentation or header files! I
- // would use the specific call in LLProcess::getStatus() if I knew
- // how. As it is, each call to apr_proc_other_child_refresh_all() will
- // call callbacks for ALL still-running child processes. That's why we
- // centralize such calls, using "mainloop" to ensure it happens once
- // per frame, and refcounting running LLProcess objects to remain
- // registered only while needed.
- LL_DEBUGS("LLProcess") << "calling apr_proc_other_child_refresh_all()" << LL_ENDL;
- apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING);
- return false;
+ out << ' ' << LLStringUtil::quote(arg);
}
-
- /// If this object is destroyed before mCount goes to zero, stop
- /// listening on "mainloop" anyway.
- LLTempBoundListener mConnection;
- unsigned mCount;
-};
-static LLProcessListener sProcessListener;
-
+ return out;
+}
/*****************************************************************************
-* WritePipe and ReadPipe
+* Helper classes for pipe I/O
*****************************************************************************/
-LLProcess::BasePipe::~BasePipe() {}
-const LLProcess::BasePipe::size_type
- // use funky syntax to call max() to avoid blighted max() macros
- LLProcess::BasePipe::npos((std::numeric_limits<LLProcess::BasePipe::size_type>::max)());
-class WritePipeImpl: public LLProcess::WritePipe
+class WritePipeImpl : public LLProcess::WritePipe
{
LOG_CLASS(WritePipeImpl);
public:
- WritePipeImpl(const std::string& desc, apr_file_t* pipe):
+ WritePipeImpl(const std::string& desc,
+ std::shared_ptr<asio::writable_pipe> 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));
+ mStream(&mStreambuf),
+ mWritePending(false)
+ {}
-#if ! LL_WINDOWS
- // We can't count on every child process reading everything we try to
- // write to it. And if the child terminates with WritePipe data still
- // pending, unless we explicitly suppress it, Posix will hit us with
- // SIGPIPE. That would terminate the viewer, boom. "Ignoring" it means
- // APR gets the correct errno, passes it back to us, we log it, etc.
- signal(SIGPIPE, SIG_IGN);
-#endif
+ virtual ~WritePipeImpl() = default;
+
+ virtual std::ostream& get_ostream() override { return mStream; }
+
+ virtual size_type size() const override
+ {
+ return mStreambuf.size();
}
- virtual std::ostream& get_ostream() { return mStream; }
- virtual size_type size() const { return mStreambuf.size(); }
+ // Called from LLProcess::tick() to initiate writing buffered data.
+ // Self-chains: each completed write immediately starts the next one if
+ // more data is waiting, so large transfers don't stall between ticks.
+ void tick() override
+ {
+ startAsyncWrite();
+ }
- bool tick(const LLSD&)
+private:
+ void startAsyncWrite()
{
- typedef boost::asio::streambuf::const_buffers_type const_buffer_sequence;
- // If there's anything to send, try to send it.
- std::size_t total(mStreambuf.size()), consumed(0);
- if (total)
- {
- const_buffer_sequence bufs = mStreambuf.data();
- // In general, our streambuf might contain a number of different
- // physical buffers; iterate over those.
- bool keepwriting = true;
- for (auto bufi(boost::asio::buffer_sequence_begin(bufs)), bufend(boost::asio::buffer_sequence_end(bufs));
- bufi != bufend && keepwriting; ++bufi)
- {
- // http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents
- // Although apr_file_write() accepts const void*, we
- // manipulate const char* so we can increment the pointer.
- const char* remainptr = static_cast<const char*>(bufi->data());
- std::size_t remainlen = boost::asio::buffer_size(*bufi);
- while (remainlen)
- {
- // Tackle the current buffer in discrete chunks. On
- // Windows, we've observed strange failures when trying to
- // write big lengths (~1 MB) in a single operation. Even a
- // 32K chunk seems too large. At some point along the way
- // apr_file_write() returns 11 (Resource temporarily
- // unavailable, i.e. EAGAIN) and says it wrote 0 bytes --
- // even though it did write the chunk! Our next write
- // attempt retries with the same chunk, resulting in the
- // chunk being duplicated at the child end. Using smaller
- // chunks is empirically more reliable.
- std::size_t towrite((std::min)(remainlen, std::size_t(4*1024)));
- apr_size_t written(towrite);
- apr_status_t err = apr_file_write(mPipe, remainptr, &written);
- // EAGAIN is exactly what we want from a nonblocking pipe.
- // Rather than waiting for data, it should return immediately.
- if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err)))
- {
- LL_WARNS("LLProcess") << "apr_file_write(" << towrite << ") on " << mDesc
- << " got " << err << ":" << LL_ENDL;
- ll_apr_warn_status(err);
- }
+ if (mWritePending || !mPipe || !mPipe->is_open() || mStreambuf.size() == 0)
+ return;
- // 'written' is modified to reflect the number of bytes actually
- // written. Make sure we consume those later. (Don't consume them
- // now, that would invalidate the buffer iterator sequence!)
- consumed += written;
- // don't forget to advance to next chunk of current buffer
- remainptr += written;
- remainlen -= written;
+ mWritePending = true;
- char msgbuf[512];
- LL_DEBUGS("LLProcess") << "wrote " << written << " of " << towrite
- << " bytes to " << mDesc
- << " (original " << total << "),"
- << " code " << err << ": "
- << apr_strerror(err, msgbuf, sizeof(msgbuf))
- << LL_ENDL;
+ // Snapshot the number of bytes currently buffered so the async_write
+ // operates on a fixed-size, stable view. Any data written to
+ // get_ostream() while this write is in flight lands in the streambuf's
+ // put area and is excluded from the current operation; it will be sent
+ // on the next tick(). Without this snapshot, a concurrent write to
+ // get_ostream() could reallocate/invalidate the buffer sequence.
+ std::size_t writeSize = mStreambuf.size();
- // The parent end of this pipe is nonblocking. If we weren't able
- // to write everything we wanted, don't keep banging on it -- that
- // won't change until the child reads some. Wait for next tick().
- if (written < towrite)
- {
- keepwriting = false; // break outer loop over buffers too
- break;
- }
- } // next chunk of current buffer
- } // next buffer
- // In all, we managed to write 'consumed' bytes. 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 later.
- mStreambuf.consume(consumed);
- }
+ // Write all buffered data asynchronously. Do NOT self-chain in the
+ // completion handler: sending the next buffer immediately can cause
+ // the child to respond within the same tick(), which posts a read
+ // event before the test listener has a chance to disconnect -- that
+ // was the root cause of test 18 "more than 3 events" and test 9
+ // "many small messages" failures. tick() calls mWritePipe->tick() on
+ // every mainloop frame, so any newly-queued data will be sent then.
+ asio::async_write(*mPipe, asio::buffer(mStreambuf.data(), writeSize),
+ [this](const boost::system::error_code& ec, std::size_t bytes_transferred)
+ {
+ mWritePending = false;
- return false;
+ if (!ec)
+ {
+ mStreambuf.consume(bytes_transferred);
+ LL_DEBUGS("LLProcess") << "Wrote " << bytes_transferred
+ << " bytes to " << mDesc << LL_ENDL;
+ }
+ else if (ec != asio::error::operation_aborted)
+ {
+ LL_WARNS("LLProcess") << "Write error on " << mDesc
+ << ": " << ec.message() << LL_ENDL;
+ }
+ });
}
-private:
std::string mDesc;
- apr_file_t* mPipe;
- LLTempBoundListener mConnection;
- boost::asio::streambuf mStreambuf;
+ std::shared_ptr<asio::writable_pipe> mPipe;
+ asio::streambuf mStreambuf;
std::ostream mStream;
+ bool mWritePending;
};
-class ReadPipeImpl: public LLProcess::ReadPipe
+class ReadPipeImpl : public LLProcess::ReadPipe
{
LOG_CLASS(ReadPipeImpl);
public:
- ReadPipeImpl(const std::string& desc, apr_file_t* pipe, LLProcess::FILESLOT index):
+ ReadPipeImpl(const std::string& desc,
+ std::shared_ptr<asio::readable_pipe> pipe,
+ LLProcess::FILESLOT slot) :
mDesc(desc),
mPipe(pipe),
- mIndex(index),
- // Essential to initialize our std::istream with our special streambuf!
+ mSlot(slot),
mStream(&mStreambuf),
- mPump("ReadPipe", true), // tweak name as needed to avoid collisions
+ mPump("ReadPipe", true),
mLimit(0),
mEOF(false)
{
- mConnection = LLEventPumps::instance().obtain("mainloop")
- .listen(LLEventPump::inventName("ReadPipe"),
- boost::bind(&ReadPipeImpl::tick, this, _1));
+ // Start async read
+ startAsyncRead();
}
- ~ReadPipeImpl()
+ virtual ~ReadPipeImpl()
{
- if (mConnection.connected())
+ if (mPipe && mPipe->is_open())
{
- mConnection.disconnect();
+ boost::system::error_code ec;
+ mPipe->close(ec);
}
}
- // 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 std::string getline() { return LLProcess::getline(mStream); }
- virtual LLEventPump& getPump() { return mPump; }
- virtual void setLimit(size_type limit) { mLimit = limit; }
- virtual size_type getLimit() const { return mLimit; }
- virtual size_type size() const { return mStreambuf.size(); }
+ virtual std::istream& get_istream() override { return mStream; }
+
+ virtual std::string getline() override
+ {
+ return LLProcess::getline(mStream);
+ }
+
+ virtual LLEventPump& getPump() override { return mPump; }
- virtual std::string read(size_type len)
+ virtual void setLimit(size_type limit) override { mLimit = limit; }
+
+ virtual size_type getLimit() const override { return mLimit; }
+
+ virtual bool atEOF() const override { return mEOF; }
+
+ virtual size_type size() const override { return mStreambuf.size(); }
+
+ virtual std::string read(size_type len) override
{
- // Read specified number of bytes into a buffer.
- size_type readlen((std::min)(size(), len));
- // Formally, &buffer[0] is invalid for a vector of size() 0. Exit
- // early in that situation.
- if (! readlen)
+ size_type readlen = (std::min)(size(), len);
+ if (!readlen)
return "";
- // Make a buffer big enough.
+
std::vector<char> buffer(readlen);
mStream.read(&buffer[0], readlen);
- // Since we've already clamped 'readlen', we can think of no reason
- // why mStream.read() should read fewer than 'readlen' bytes.
- // Nonetheless, use the actual retrieved length.
return std::string(&buffer[0], mStream.gcount());
}
- virtual std::string peek(size_type offset=0, size_type len=npos) const
+ virtual std::string peek(size_type offset = 0, size_type len = npos) const override
{
- // Constrain caller's offset and len to overlap actual buffer content.
std::size_t real_offset = (std::min)(mStreambuf.size(), std::size_t(offset));
- size_type want_end = (len == npos)? npos : (real_offset + len);
- std::size_t real_end = (std::min)(mStreambuf.size(), std::size_t(want_end));
- boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data();
- return std::string(boost::asio::buffers_begin(cbufs) + real_offset,
- boost::asio::buffers_begin(cbufs) + real_end);
+ size_type want_end = (len == npos) ? npos : (real_offset + len);
+ std::size_t real_end = (std::min)(mStreambuf.size(), std::size_t(want_end));
+
+ auto cbufs = mStreambuf.data();
+ return std::string(asio::buffers_begin(cbufs) + real_offset,
+ asio::buffers_begin(cbufs) + real_end);
}
- virtual size_type find(const std::string& seek, size_type offset=0) const
+ virtual size_type find(const std::string& seek, size_type offset = 0) const override
{
- // If we're passing a string of length 1, use find(char), which can
- // use an O(n) std::find() rather than the O(n^2) std::search().
if (seek.length() == 1)
- {
return find(seek[0], offset);
- }
- // If offset is beyond the whole buffer, can't even construct a valid
- // iterator range; can't possibly find the string we seek.
if (offset > mStreambuf.size())
- {
return npos;
- }
- boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data();
- boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type>
- begin(boost::asio::buffers_begin(cbufs)),
- end (boost::asio::buffers_end(cbufs)),
- found(std::search(begin + offset, end, seek.begin(), seek.end()));
- return (found == end)? npos : (found - begin);
+ auto cbufs = mStreambuf.data();
+ auto begin = asio::buffers_begin(cbufs);
+ auto end = asio::buffers_end(cbufs);
+ auto found = std::search(begin + offset, end, seek.begin(), seek.end());
+ return (found == end) ? npos : (found - begin);
}
- virtual size_type find(char seek, size_type offset=0) const
+ virtual size_type find(char seek, size_type offset = 0) const override
{
- // If offset is beyond the whole buffer, can't even construct a valid
- // iterator range; can't possibly find the char we seek.
if (offset > mStreambuf.size())
- {
return npos;
- }
- boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data();
- boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type>
- begin(boost::asio::buffers_begin(cbufs)),
- end (boost::asio::buffers_end(cbufs)),
- found(std::find(begin + offset, end, seek));
- return (found == end)? npos : (found - begin);
+ auto cbufs = mStreambuf.data();
+ auto begin = asio::buffers_begin(cbufs);
+ auto end = asio::buffers_end(cbufs);
+ auto found = std::find(begin + offset, end, seek);
+ return (found == end) ? npos : (found - begin);
}
- bool tick(const LLSD&)
+private:
+ void startAsyncRead()
{
- // Once we've hit EOF, skip all the rest of this.
- if (mEOF)
- return false;
+ if (!mPipe || !mPipe->is_open() || mEOF)
+ return;
+
+ // Always read data regardless of mLimit to prevent INTEGRATION_TEST_llleap
+ // deadlock: stopping reads fills the OS pipe buffer and blocks the child,
+ // which prevents large (~1 MB) messages from being fully received.
+ // mLimit only controls how many bytes appear in event notifications.
+ auto bufs = mStreambuf.prepare(4096);
- typedef boost::asio::streambuf::mutable_buffers_type mutable_buffer_sequence;
- // Try, every time, to read into our streambuf. In fact, we have no
- // idea how much data the child might be trying to send: keep trying
- // until we're convinced we've temporarily exhausted the pipe.
- enum PipeState { RETRY, EXHAUSTED, CLOSED };
- PipeState state = RETRY;
- std::size_t committed(0);
- do
+ mPipe->async_read_some(bufs,
+ [this](const boost::system::error_code& ec, std::size_t bytes_transferred)
{
- // attempt to read an arbitrary size
- mutable_buffer_sequence bufs = mStreambuf.prepare(4096);
- // In general, the mutable_buffer_sequence returned by prepare() might
- // contain a number of different physical buffers; iterate over those.
- std::size_t tocommit(0);
- for (auto bufi(boost::asio::buffer_sequence_begin(bufs)), bufend(boost::asio::buffer_sequence_end(bufs));
- bufi != bufend; ++bufi)
+ if (!ec)
{
- // http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents
- std::size_t toread(boost::asio::buffer_size(*bufi));
- apr_size_t gotten(toread);
- apr_status_t err = apr_file_read(mPipe,
- bufi->data(),
- &gotten);
- // EAGAIN is exactly what we want from a nonblocking pipe.
- // Rather than waiting for data, it should return immediately.
- if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err)))
- {
- // Handle EOF specially: it's part of normal-case processing.
- if (err == APR_EOF)
- {
- LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL;
- }
- else
- {
- LL_WARNS("LLProcess") << "apr_file_read(" << toread << ") on " << mDesc
- << " got " << err << ":" << LL_ENDL;
- ll_apr_warn_status(err);
- }
- // Either way, though, we won't need any more tick() calls.
- mConnection.disconnect();
- // Ignore any subsequent calls we might get anyway.
- mEOF = true;
- state = CLOSED; // also break outer retry loop
- break;
- }
+ mStreambuf.commit(bytes_transferred);
+ LL_DEBUGS("LLProcess") << "Read " << bytes_transferred
+ << " bytes from " << mDesc << LL_ENDL;
- // 'gotten' was modified to reflect the number of bytes actually
- // received. Make sure we commit those later. (Don't commit them
- // now, that would invalidate the buffer iterator sequence!)
- tocommit += gotten;
- LL_DEBUGS("LLProcess") << "filled " << gotten << " of " << toread
- << " bytes from " << mDesc << LL_ENDL;
+ // Restore original 7-field event contract:
+ // data, len, slot, name, desc, eof, exhst
+ // A successful async read corresponds to the original EXHAUSTED
+ // state: we got data, pipe is not yet closed.
+ size_type data_len = (std::min)(mStreambuf.size(), mLimit);
+ LLSD event;
+ event["data"] = peek(0, data_len);
+ event["len"] = LLSD::Integer(mStreambuf.size());
+ event["slot"] = LLSD::Integer(mSlot);
+ event["name"] = whichfile(mSlot);
+ event["desc"] = mDesc;
+ event["eof"] = false;
+ event["exhst"] = true;
- // The parent end of this pipe is nonblocking. If we weren't even
- // able to fill this buffer, don't loop to try to fill the next --
- // that won't change until the child writes more. Wait for next
- // tick().
- if (gotten < toread)
+ // Arm the next read before posting: LLEventStream::post() is synchronous
+ // and listeners may destroy this object.
+ startAsyncRead();
+ mPump.post(event);
+ }
+ else if (ec == asio::error::eof
+#if LL_WINDOWS
+ // On Windows anonymous pipes, the write end closing
+ // delivers ERROR_BROKEN_PIPE (109), not asio::error::eof.
+ || ec.value() == ERROR_BROKEN_PIPE
+#endif
+ )
+ {
+ if (bytes_transferred > 0)
{
- // break outer retry loop too
- state = EXHAUSTED;
- break;
+ mStreambuf.commit(bytes_transferred);
}
- }
- // Don't forget to "commit" the data!
- mStreambuf.commit(tocommit);
- committed += tocommit;
+ mEOF = true;
+ LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL;
- // state is changed from RETRY when we can't fill any one buffer
- // of the mutable_buffer_sequence established by the current
- // prepare() call -- whether due to error or not enough bytes.
- // That is, if state is still RETRY, we've filled every physical
- // buffer in the mutable_buffer_sequence. In that case, for all we
- // know, the child might have still more data pending -- go for it!
- } while (state == RETRY);
+ // Match the original behavior: pack all 7 fields into the
+ // single eof event so consumers can "use it or lose it" --
+ // this is the last chance to see any remaining buffered data.
+ // EOF corresponds to the original CLOSED state.
+ size_type data_len = (std::min)(mStreambuf.size(), mLimit);
+ LLSD eof_event;
+ eof_event["data"] = peek(0, data_len);
+ eof_event["len"] = LLSD::Integer(mStreambuf.size());
+ eof_event["slot"] = LLSD::Integer(mSlot);
+ eof_event["name"] = whichfile(mSlot);
+ eof_event["desc"] = mDesc;
+ eof_event["eof"] = true;
+ eof_event["exhst"] = false;
- // Once we recognize that the pipe is closed, make one more call to
- // listener. The listener might be waiting for a particular substring
- // to arrive, or a particular length of data or something. The event
- // with "eof" == true announces that nothing further will arrive, so
- // use it or lose it.
- if (committed || state == CLOSED)
- {
- // If we actually received new data, publish it on our LLEventPump
- // as advertised. Constrain it by mLimit. But show listener the
- // actual accumulated buffer size, regardless of mLimit.
- size_type datasize((std::min)(mLimit, size_type(mStreambuf.size())));
- mPump.post(LLSDMap
- ("data", peek(0, datasize))
- ("len", LLSD::Integer(mStreambuf.size()))
- ("slot", LLSD::Integer(mIndex))
- ("name", whichfile(mIndex))
- ("desc", mDesc)
- ("eof", state == CLOSED)
- ("exhst", state == EXHAUSTED));
- }
-
- return false;
+ mPump.post(eof_event);
+ }
+ else if (ec != asio::error::operation_aborted)
+ {
+ LL_WARNS("LLProcess") << "Read error on " << mDesc
+ << ": " << ec.message() << LL_ENDL;
+ }
+ });
}
-private:
std::string mDesc;
- apr_file_t* mPipe;
- LLProcess::FILESLOT mIndex;
- LLTempBoundListener mConnection;
- boost::asio::streambuf mStreambuf;
+ std::shared_ptr<asio::readable_pipe> mPipe;
+ LLProcess::FILESLOT mSlot;
+ mutable asio::streambuf mStreambuf;
std::istream mStream;
- LLEventStream mPump;
+ LLEventStream mPump; // pump specific to this pipe
size_type mLimit;
bool mEOF;
};
/*****************************************************************************
-* LLProcess itself
+* LLProcess implementation
*****************************************************************************/
-/// Need an exception to avoid constructing an invalid LLProcess object, but
-/// internal use only
-struct LLProcessError: public LLException
+
+const LLProcess::BasePipe::size_type LLProcess::BasePipe::npos =
+static_cast<LLProcess::BasePipe::size_type>(-1);
+
+LLProcess::LLProcess(const Params& params) :
+ mStatus(),
+ mDesc(params.desc.isProvided() ? params.desc() : basename(params.executable())),
+ mPostend(params.postend.isProvided() ? params.postend() : ""),
+ mAutokill(params.autokill),
+ mAttached(params.attached.isProvided() ? params.attached() : bool(params.autokill))
{
- LLProcessError(const std::string& msg): LLException(msg) {}
-};
+ launch(params);
+}
+LLProcess::~LLProcess()
+{
+ if (mChild && mStatus.mState == RUNNING)
+ {
+ if (mAttached && mAutokill)
+ {
+ LL_INFOS("LLProcess") << "Terminating child process " << mDesc << LL_ENDL;
+ boost::system::error_code ec;
+ mChild->terminate(ec);
+
+#if !LL_WINDOWS
+ // On POSIX, terminate() sends SIGTERM which allows graceful shutdown.
+ // Poll with waitpid(WNOHANG) rather than mChild->running() to avoid
+ // competing with tick()'s own waitpid call.
+ pid_t pid = mChild->id();
+ for (int i = 0; i < 30; ++i)
+ {
+ int child_status = 0;
+ pid_t w = ::waitpid(pid, &child_status, WNOHANG);
+ if (w == pid || (w == -1 && errno == ECHILD))
+ break; // child exited or was already reaped
+ if (w == -1 && errno != EINTR)
+ {
+ LL_WARNS("LLProcess") << "waitpid(" << pid << ") failed while terminating "
+ << mDesc << ": " << strerror(errno) << LL_ENDL;
+ break;
+ }
+ // Sleep before next poll: applies to both EINTR and
+ // still-running (w == 0) cases.
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ }
+
+ // Force kill if still running
+ {
+ int child_status;
+ if (::waitpid(pid, &child_status, WNOHANG) == 0)
+ {
+ LL_WARNS("LLProcess") << "Force killing " << mDesc << LL_ENDL;
+ (void)::kill(pid, SIGKILL);
+ }
+ }
+#else
+ // On Windows, terminate() already does an immediate hard kill via TerminateProcess()
+ // Wait briefly to ensure termination completes
+ WaitForSingleObject(mChild->native_handle(), 100);
+#endif
+ }
+ else
+ {
+ LL_INFOS("LLProcess") << "Not terminating " << mDesc
+ << " (attached=" << mAttached
+ << ", autokill=" << mAutokill << ")" << LL_ENDL;
+ // Detach the bp::process so its destructor does not send SIGKILL
+ // to the still-running process (boost::process v2 terminates the
+ // child in bp::process::~process() if the handle is still valid).
+ mChild->detach();
+ }
+ }
+
+ if (mMainloopConnection.connected())
+ {
+ mMainloopConnection.disconnect();
+ }
+}
+
+//static
LLProcessPtr LLProcess::create(const LLSDOrParams& params)
{
try
{
- return LLProcessPtr(new LLProcess(params));
+ // Construct and then register the mainloop connection separately.
+ // connectMainloop() calls shared_from_this(), which requires the
+ // shared_ptr control block to be fully initialized — this is only true
+ // after make_shared returns, not during the constructor.
+ LLProcessPtr ptr = std::make_shared<LLProcess>(params);
+ ptr->connectMainloop();
+ return ptr;
}
- catch (const LLProcessError& e)
+ catch (const std::exception& e)
{
- LL_WARNS("LLProcess") << e.what() << LL_ENDL;
+ LL_WARNS("LLProcess") << "Failed to create process: " << 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 (params.postend.isProvided())
+ // Even on failure, fire the postend event if requested, so callers
+ // that listen for it can detect the launch failure.
+ if (params.postend.isProvided() && !params.postend().empty())
{
- LLEventPumps::instance().obtain(params.postend)
- .post(LLSDMap
- // no "id"
- ("desc", getDesc(params))
- ("state", LLProcess::UNSTARTED)
- // no "data"
- ("string", e.what())
- );
+ std::string desc = params.desc.isProvided() ? params.desc() :
+ (params.executable.isProvided() ? LLProcess::basename(params.executable()) : "");
+ LLSD event;
+ event["desc"] = desc;
+ event["state"] = LLProcess::UNSTARTED;
+ event["string"] = e.what();
+ LLEventPumps::instance().obtain(params.postend()).post(event);
}
return LLProcessPtr();
}
}
-/// Call an apr function returning apr_status_t. On failure, log warning and
-/// throw LLProcessError mentioning the function call that produced that
-/// result.
-#define chkapr(func) \
- if (ll_apr_warn_status(func)) \
- throw LLProcessError(#func " failed")
-
-LLProcess::LLProcess(const LLSDOrParams& params):
- mAutokill(params.autokill),
- // Because 'autokill' originally meant both 'autokill' and 'attached', to
- // preserve existing semantics, we promise that mAttached defaults to the
- // same setting as mAutokill.
- mAttached(params.attached.isProvided()? params.attached : params.autokill),
- mPool(NULL)
+void LLProcess::launch(const LLSDOrParams& params)
{
- mPipes.resize(NSLOTS);
-
- if (! params.validateBlock(true))
+ if (!params.validateBlock(true))
{
- LLTHROW(LLProcessError(STRINGIZE("not launched: failed parameter validation\n"
- << LLSDNotationStreamer(params))));
+ LL_WARNS("LLProcess") << "Failed parameter validation " << LLSDNotationStreamer(params) << LL_ENDL;
+ throw std::runtime_error("not launched: failed parameter validation\n");
}
- mPostend = params.postend;
-
- apr_pool_create(&mPool, gAPRPoolp);
- if (!mPool)
+ // Validate FileParam types before attempting to launch
+ int file_idx = 0;
+ for (const auto& fparam : params.files)
{
- LLTHROW(LLProcessError(STRINGIZE("failed to create apr pool")));
- }
+ if (fparam.type.isProvided())
+ {
+ const std::string& type = fparam.type();
+ // Only "" (inherit) and "pipe" are supported
+ if (!type.empty() && type != "pipe")
+ {
+ std::string slotname;
+ switch (file_idx)
+ {
+ case STDIN: slotname = "stdin"; break;
+ case STDOUT: slotname = "stdout"; break;
+ case STDERR: slotname = "stderr"; break;
+ default: slotname = STRINGIZE("file slot " << file_idx); break;
+ }
- apr_procattr_t *procattr = NULL;
- chkapr(apr_procattr_create(&procattr, mPool));
+ LL_WARNS("LLProcess") << "For " << params.executable()
+ << ": unsupported FileParam for " << slotname
+ << ": type='" << type << "'";
- // IQA-490, CHOP-900: On Windows, ask APR to jump through hoops to
- // constrain the set of handles passed to the child process. Before we
- // changed to APR, the Windows implementation of LLProcessLauncher called
- // CreateProcess(bInheritHandles=false), meaning to pass NO open handles
- // to the child process. Now that we support pipes, though, we must allow
- // apr_proc_create() to pass bInheritHandles=true. But without taking
- // special pains, that causes trouble in a number of ways, due to the fact
- // that the viewer is constantly opening and closing files -- most of
- // which CreateProcess() passes to every child process!
-#if ! defined(APR_HAS_PROCATTR_CONSTRAIN_HANDLE_SET)
- // Our special preprocessor symbol isn't even defined -- wrong APR
- LL_WARNS("LLProcess") << "This version of APR lacks Linden "
- << "apr_procattr_constrain_handle_set() extension" << LL_ENDL;
-#else
- chkapr(apr_procattr_constrain_handle_set(procattr, 1));
-#endif
+ if (fparam.name.isProvided())
+ {
+ LL_CONT << ", name='" << fparam.name() << "'";
+ }
- // For which of stdin, stdout, stderr should we create a pipe to the
- // child? In the viewer, there are only a couple viable
- // 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).
- // 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).
- std::vector<apr_int32_t> select;
- for (const FileParam& fparam : params.files)
- {
- // Every iteration, we're going to append an item to 'select'. At the
- // top of the loop, its size() is, in effect, an index. Use that to
- // pick a string description for messages.
- std::string which(whichfile(FILESLOT(select.size())));
- if (fparam.type().empty()) // inherit our file descriptor
- {
- select.push_back(APR_NO_PIPE);
- }
- else if (fparam.type() == "pipe") // anonymous pipe
- {
- if (! fparam.name().empty())
+ LL_CONT << LL_ENDL;
+
+ throw std::runtime_error(
+ STRINGIZE("unsupported FileParam type '" << type
+ << "' for " << slotname));
+ }
+
+ // Warn about internal pipe names (not yet supported)
+ if (type == "pipe" && fparam.name.isProvided() && !fparam.name().empty())
{
- LL_WARNS("LLProcess") << "For " << params.executable()
- << ": internal names for reusing pipes ('"
- << fparam.name() << "' for " << which
- << ") are not yet supported -- creating distinct pipe"
- << LL_ENDL;
+ LL_WARNS("LLProcess") << "Internal pipe name '" << fparam.name()
+ << "' not yet supported; ignoring" << 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.push_back(APR_CHILD_BLOCK);
- }
- else
- {
- LLTHROW(LLProcessError(STRINGIZE("For " << params.executable()
- << ": unsupported FileParam for " << which
- << ": type='" << fparam.type()
- << "', name='" << fparam.name() << "'")));
}
+ file_idx++;
}
- // By default, pass APR_NO_PIPE for unspecified slots.
- while (select.size() < NSLOTS)
+
+ // Build arguments vector
+ std::vector<std::string> args;
+ for (const auto& arg : params.args)
{
- select.push_back(APR_NO_PIPE);
+ args.push_back(arg);
}
- 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
- // would be APR_PROGRAM_ENV: still copy environment, but require full
- // executable pathname. I don't see a downside to searching the PATH,
- // though: if our caller wants (e.g.) a specific Python interpreter, s/he
- // can still pass the full pathname.
- chkapr(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH));
- // YES, do extra work if necessary to report child exec() failures back to
- // parent process.
- chkapr(apr_procattr_error_check_set(procattr, 1));
- // Do not start a non-autokill child in detached state. On Posix
- // platforms, this setting attempts to daemonize the new child, closing
- // std handles and the like, and that's a bit more detachment than we
- // want. autokill=false just means not to implicitly kill the child when
- // the parent terminates!
-// chkapr(apr_procattr_detach_set(procattr, mAutokill? 0 : 1));
+ // Determine pipe configuration
+ bool use_stdin_pipe = false;
+ bool use_stdout_pipe = false;
+ bool use_stderr_pipe = false;
- if (mAutokill)
+ file_idx = 0;
+ for (const auto& fparam : params.files)
{
-#if ! defined(APR_HAS_PROCATTR_AUTOKILL_SET)
- // Our special preprocessor symbol isn't even defined -- wrong APR
- LL_WARNS("LLProcess") << "This version of APR lacks Linden apr_procattr_autokill_set() extension" << LL_ENDL;
-#elif ! APR_HAS_PROCATTR_AUTOKILL_SET
- // Symbol is defined, but to 0: expect apr_procattr_autokill_set() to
- // return APR_ENOTIMPL.
-#else // APR_HAS_PROCATTR_AUTOKILL_SET nonzero
- ll_apr_warn_status(apr_procattr_autokill_set(procattr, 1));
-#endif
+ if (fparam.type.isProvided() && fparam.type() == "pipe")
+ {
+ switch (file_idx)
+ {
+ case STDIN: use_stdin_pipe = true; break;
+ case STDOUT: use_stdout_pipe = true; break;
+ case STDERR: use_stderr_pipe = true; break;
+ }
+ }
+ file_idx++;
}
- // In preparation for calling apr_proc_create(), we collect a number of
- // const char* pointers obtained from std::string::c_str(). Turns out
- // LLInitParam::Block's helpers Optional, Mandatory, Multiple et al.
- // guarantee that converting to the wrapped type (std::string in our
- // case), e.g. by calling operator(), returns a reference to *the same
- // instance* of the wrapped type that's stored in our Block subclass.
- // That's important! We know 'params' persists throughout this method
- // call; but without that guarantee, when you see params.cwd().c_str(),
- // grit your teeth and smile and carry on.
-
- if (params.cwd.isProvided())
+ // Create pipes if needed - v2 uses asio pipes directly
+ // NOTE: From parent's perspective: write to stdin, read from stdout/stderr
+ if (use_stdin_pipe)
{
- chkapr(apr_procattr_dir_set(procattr, params.cwd().c_str()));
+ mStdinPipe = std::make_shared<asio::writable_pipe>(mIOContext);
+ LL_DEBUGS("LLProcess") << "Created stdin pipe for " << mDesc << LL_ENDL;
}
-
- // create an argv vector for the child process
- std::vector<const char*> argv;
-
- // Add the executable path. See above remarks about c_str().
- argv.push_back(params.executable().c_str());
-
- // Add arguments. See above remarks about c_str().
- for (const std::string& arg : params.args)
+ if (use_stdout_pipe)
{
- argv.push_back(arg.c_str());
+ mStdoutPipe = std::make_shared<asio::readable_pipe>(mIOContext);
+ LL_DEBUGS("LLProcess") << "Created stdout pipe for " << mDesc << LL_ENDL;
}
-
- // terminate with a null pointer
- argv.push_back(NULL);
-
- // Launch! The NULL would be the environment block, if we were passing
- // one. Hand-expand chkapr() macro so we can fill in the actual command
- // string instead of the variable names.
- if (ll_apr_warn_status(apr_proc_create(&mProcess, argv[0], &argv[0], NULL, procattr,
- mPool)))
+ if (use_stderr_pipe)
{
- LLTHROW(LLProcessError(STRINGIZE(params << " failed")));
+ mStderrPipe = std::make_shared<asio::readable_pipe>(mIOContext);
+ LL_DEBUGS("LLProcess") << "Created stderr pipe for " << mDesc << LL_ENDL;
}
- // arrange to call status_callback()
- apr_proc_other_child_register(&mProcess, &LLProcess::status_callback, this, mProcess.in,
- mPool);
- // and make sure we poll it once per "mainloop" tick
- sProcessListener.addPoll(*this);
- mStatus.mState = RUNNING;
+ // Build the process
+ try
+ {
+#if !LL_WINDOWS
+ // Ignore SIGPIPE so that writing to a child's closed stdin doesn't
+ // terminate the viewer process. The write will fail with EPIPE instead.
+ signal(SIGPIPE, SIG_IGN);
+#endif
- mDesc = STRINGIZE(getDesc(params) << " (" << mProcess.pid << ')');
- LL_INFOS("LLProcess") << mDesc << ": launched " << params << LL_ENDL;
+ // Create child process with appropriate redirections.
+ // v2 uses process_stdio for I/O redirection
+ bp::process_stdio stdio;
- // Unless caller explicitly turned off autokill (child should persist),
- // take steps to terminate the child. This is all suspenders-and-belt: in
- // theory our destructor should kill an autokill child, but in practice
- // that doesn't always work (e.g. VWR-21538).
- if (mAutokill)
- {
-/*==========================================================================*|
- // NO: There may be an APR bug, not sure -- but at least on Mac, when
- // gAPRPoolp is destroyed, OUR process receives SIGTERM! Apparently
- // either our own PID is getting into the list of processes to kill()
- // (unlikely), or somehow one of those PIDs is getting zeroed first,
- // so that kill() sends SIGTERM to the whole process group -- this
- // process included. I'd have to build and link with a debug version
- // of APR to know for sure. It's too bad: this mechanism would be just
- // right for dealing with static autokill LLProcessPtr variables,
- // which aren't destroyed until after APR is no longer available.
+ if (use_stdin_pipe)
+ stdio.in = *mStdinPipe;
+ else
+ stdio.in = nullptr; // inherit
+
+ if (use_stdout_pipe)
+ stdio.out = *mStdoutPipe;
+ else
+ stdio.out = nullptr; // inherit
- // Tie the lifespan of this child process to the lifespan of our APR
- // pool: on destruction of the pool, forcibly kill the process. Tell
- // APR to try SIGTERM and suspend 3 seconds. If that didn't work, use
- // SIGKILL.
- apr_pool_note_subprocess(gAPRPoolp, &mProcess, APR_KILL_AFTER_TIMEOUT);
-|*==========================================================================*/
+ if (use_stderr_pipe)
+ stdio.err = *mStderrPipe;
+ else
+ stdio.err = nullptr; // inherit
- // On Windows, associate the new child process with our Job Object.
- autokill();
- }
+ // Build executable path
+ boost::filesystem::path executable_path(params.executable());
- // 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;
- std::string desc(STRINGIZE(mDesc << ' ' << whichfile(FILESLOT(i))));
- apr_file_t* pipe(mProcess.*(members[i]));
- if (i == STDIN)
+ // In Boost.Process v2, error_code cannot be passed as an initializer.
+ // Use the throwing overload and catch the exception instead.
+ try
{
- mPipes[i] = std::make_unique<WritePipeImpl>(desc, pipe);
+ if (params.cwd.isProvided())
+ {
+ mChild = std::make_unique<bp::process>(
+ mIOContext,
+ executable_path,
+ args,
+ bp::process_start_dir(params.cwd()),
+ stdio
+ );
+ }
+ else
+ {
+ mChild = std::make_unique<bp::process>(
+ mIOContext,
+ executable_path,
+ args,
+ stdio
+ );
+ }
}
- else
+ catch (const boost::system::system_error& ex)
{
- mPipes[i] = std::make_unique<ReadPipeImpl>(desc, pipe, FILESLOT(i));
+ throw std::runtime_error(STRINGIZE("failed to launch " << params.executable()
+ << ": " << ex.what()));
}
- // Removed temporaily for Xcode 7 build tests: error was:
- // "error: expression with side effects will be evaluated despite
- // being used as an operand to 'typeid' [-Werror,-Wpotentially-evaluated-expression]""
- //LL_DEBUGS("LLProcess") << "Instantiating " << typeid(mPipes[i]).name()
- // << "('" << desc << "')" << LL_ENDL;
- }
-}
-// Helper to obtain a description string, given a Params block
-static std::string getDesc(const LLProcess::Params& params)
-{
- // If caller specified a description string, by all means use it.
- if (params.desc.isProvided())
- return params.desc;
+#if !LL_WINDOWS
+ // boost::process v2 may install a SIGCHLD handler via boost::asio
+ // without SA_RESTART when mIOContext is passed to bp::process.
+ // Without SA_RESTART, blocking waitpid() calls elsewhere in the
+ // process return EINTR when a child exits. Ensure SA_RESTART is set.
+ {
+ struct sigaction sa_chld;
+ if (sigaction(SIGCHLD, nullptr, &sa_chld) != 0)
+ {
+ LL_WARNS("LLProcess") << "Failed to read SIGCHLD disposition: "
+ << strerror(errno) << LL_ENDL;
+ }
+ else if (!(sa_chld.sa_flags & SA_RESTART))
+ {
+ sa_chld.sa_flags |= SA_RESTART;
+ if (sigaction(SIGCHLD, &sa_chld, nullptr) != 0)
+ {
+ LL_WARNS("LLProcess") << "Failed to set SA_RESTART on SIGCHLD: "
+ << strerror(errno) << LL_ENDL;
+ }
+ }
+ }
+#endif
+
+ mStatus.mState = RUNNING;
+
+ // Create pipe wrappers
+ if (mStdinPipe)
+ {
+ mWritePipe = std::make_unique<WritePipeImpl>(
+ STRINGIZE(mDesc << " stdin"),
+ mStdinPipe
+ );
+ }
+ if (mStdoutPipe)
+ {
+ mStdoutReadPipe = std::make_unique<ReadPipeImpl>(
+ STRINGIZE(mDesc << " stdout"),
+ mStdoutPipe,
+ STDOUT
+ );
+ }
+ if (mStderrPipe)
+ {
+ mStderrReadPipe = std::make_unique<ReadPipeImpl>(
+ STRINGIZE(mDesc << " stderr"),
+ mStderrPipe,
+ STDERR
+ );
+ }
- // Caller didn't say. Use the executable name -- but use just the filename
- // part. On Mac, for instance, full pathnames get cumbersome.
- return LLProcess::basename(params.executable);
+ LL_INFOS("LLProcess") << "Launched " << mDesc
+ << " (PID: " << mChild->id() << ")" << LL_ENDL;
+ }
+ catch (const std::exception& e)
+ {
+ throw std::runtime_error(STRINGIZE("failed to create process: " << e.what()));
+ }
}
-//static
-std::string LLProcess::basename(const std::string& path)
+void LLProcess::connectMainloop()
{
- // If there are Linden utility functions to manipulate pathnames, I
- // haven't found them -- and for this usage, Boost.Filesystem seems kind
- // of heavyweight.
- std::string::size_type delim = path.find_last_of("\\/");
- // If path contains no pathname delimiters, return the whole thing.
- if (delim == std::string::npos)
- return path;
-
- // Return just the part beyond the last delimiter.
- return path.substr(delim + 1);
+ // Capture a weak_ptr to prevent use-after-free: a listener responding to
+ // a synchronous event post inside tick() may drop the last LLProcessPtr,
+ // destroying this object while tick() is on the call stack. Locking the
+ // weak_ptr before calling tick() keeps *this alive for the duration of
+ // the callback.
+ // NOTE: shared_from_this() is only valid after the shared_ptr owning this
+ // object has been fully constructed, so this must be called from create()
+ // rather than from the constructor.
+ std::weak_ptr<LLProcess> weak = shared_from_this();
+ mMainloopConnection = LLEventPumps::instance().obtain("mainloop")
+ .listen(LLEventPump::inventName("LLProcess"),
+ [weak](const LLSD&)
+ {
+ // Lock weak_ptr to keep *this alive during tick(), preventing
+ // destruction mid-callback if a listener drops the last LLProcessPtr.
+ auto self = weak.lock();
+ if (self) self->tick();
+ return false;
+ });
}
-LLProcess::~LLProcess()
+void LLProcess::tick()
{
- // In the Linden viewer, there's at least one static LLProcessPtr. Its
- // destructor will be called *after* ll_cleanup_apr(). In such a case,
- // unregistering is pointless (and fatal!) -- and kill(), which also
- // relies on APR, is impossible.
- if (! gAPRPoolp)
- return;
+ // Initiate pending stdin writes before draining the I/O context so that
+ // self-chained async writes can keep advancing within the same tick
+ // instead of waiting for the next mainloop frame.
+ if (mWritePipe)
+ mWritePipe->tick();
- // Only in state RUNNING are we registered for callback. In UNSTARTED we
- // haven't yet registered. And since receiving the callback is the only
- // way we detect child termination, we only change from state RUNNING at
- // the same time we unregister.
- if (mStatus.mState == RUNNING)
+ mIOContext.restart();
+ while (mIOContext.poll_one() > 0)
{
- // We're still registered for a callback: unregister. Do it before
- // we even issue the kill(): even if kill() somehow prompted an
- // instantaneous callback (unlikely), this object is going away! Any
- // information updated in this object by such a callback is no longer
- // available to any consumer anyway.
- apr_proc_other_child_unregister(this);
- // One less LLProcess to poll for
- sProcessListener.dropPoll(*this);
+ // Keep polling until no more handlers are ready
}
- if (mAttached)
+#if LL_WINDOWS
+ // Check process status
+ if (mChild && mStatus.mState == RUNNING && !mChild->running())
{
- kill("destructor");
+ WaitForSingleObject(mChild->native_handle(), 100);
+ Status exitStatus;
+ exitStatus.mState = EXITED;
+ exitStatus.mData = mChild->exit_code();
+ handleExit(exitStatus);
}
+#else
+ // Check process status using WNOHANG to avoid blocking or generating
+ // signals that interfere with other waitpid() callers.
+ if (mChild && mStatus.mState == RUNNING)
+ {
+ int status = 0;
+ pid_t result;
+ // Retry on EINTR: SA_RESTART only applies to blocking calls, not
+ // WNOHANG waitpid(), so manual retry is still needed.
+ do { // manual EINTR retry (SA_RESTART does not apply to WNOHANG)
+ result = ::waitpid(mChild->id(), &status, WNOHANG);
+ } while (result == -1 && errno == EINTR);
- if (mPool)
+ if (result == mChild->id())
+ {
+ // Child has exited; decode exit status now (before handleExit,
+ // since the child has already been reaped by this waitpid call).
+ Status exitStatus;
+ if (WIFEXITED(status))
+ {
+ exitStatus.mState = EXITED;
+ exitStatus.mData = WEXITSTATUS(status);
+ }
+ else if (WIFSIGNALED(status))
+ {
+ exitStatus.mState = KILLED;
+ exitStatus.mData = WTERMSIG(status);
+ }
+ else
+ {
+ exitStatus.mState = EXITED;
+ exitStatus.mData = 0;
+ }
+ handleExit(exitStatus);
+ }
+ else if (result == -1 && errno == ECHILD)
+ {
+ // The zombie was already reaped by someone else (e.g. a SIGCHLD
+ // handler from APR or another library). We can't determine the
+ // real exit code; synthesize EXITED/0 so the process is no longer
+ // considered "running" and waitfor() doesn't spin for 60 seconds.
+ Status exitStatus;
+ exitStatus.mState = EXITED;
+ exitStatus.mData = 0;
+ handleExit(exitStatus);
+ }
+ // result == 0 means still running
+ }
+#endif
+
+ // Keep pumping after process exit until all ReadPipes report EOF, then
+ // disconnect from mainloop to avoid losing trailing EOF notifications.
+ if (mStatus.mState != RUNNING && mMainloopConnection.connected())
{
- apr_pool_destroy(mPool);
- mPool = NULL;
+ bool stdout_eof = (!mStdoutReadPipe || mStdoutReadPipe->atEOF());
+ bool stderr_eof = (!mStderrReadPipe || mStderrReadPipe->atEOF());
+ if (stdout_eof && stderr_eof)
+ {
+ mMainloopConnection.disconnect();
+ }
}
}
-bool LLProcess::kill(const std::string& who)
+void LLProcess::handleExit(Status exitStatus)
{
- if (isRunning())
- {
- LL_INFOS("LLProcess") << who << " killing " << mDesc << LL_ENDL;
+ if (mStatus.mState != RUNNING)
+ return; // Already handled
-#if LL_WINDOWS
- int sig = -1;
-#else // Posix
- int sig = SIGTERM;
-#endif
+ mStatus = exitStatus;
- ll_apr_warn_status(apr_proc_kill(&mProcess, sig));
+ // Drain any remaining pipe handlers before notifying callers. LLLeap
+ // consumes child stdout/stderr from these callbacks, and some tests write
+ // enough data to require far more than a handful of async-read completions
+ // after the child has already exited.
+ if (mIOContext.stopped())
+ {
+ // If the io_context has reached the stopped state,
+ // the following loop will never run and trailing stdout/stderr
+ // handlers (and EOF notifications) may be missed.
+ LL_INFOS("LLProcess") << "mIOContext was stopped on exit, restarting" << LL_ENDL;
+ mIOContext.restart();
+ }
+ while (mIOContext.poll_one() > 0)
+ {
+ // Keep polling until no more handlers are immediately ready.
}
- return ! isRunning();
-}
+ LL_INFOS("LLProcess") << getStatusString(mStatus) << LL_ENDL;
-//static
-bool LLProcess::kill(const LLProcessPtr& p, const std::string& who)
-{
- if (! p)
- return true; // process dead! (was never running)
- return p->kill(who);
+ // Post to event pump if configured
+ if (!mPostend.empty())
+ {
+ LLSD event;
+ event["id"] = static_cast<int>(getProcessID());
+ event["desc"] = mDesc;
+ event["state"] = mStatus.mState;
+ event["data"] = mStatus.mData;
+ event["string"] = getStatusString(mStatus);
+
+ LLEventPumps::instance().obtain(mPostend).post(event);
+ }
+
+ // Leave mainloop connected until tick() observes EOF on all ReadPipes.
}
bool LLProcess::isRunning() const
{
- return getStatus().mState == RUNNING;
+ return mStatus.mState == RUNNING;
}
//static
-bool LLProcess::isRunning(const LLProcessPtr& p)
+bool LLProcess::isRunning(const LLProcessPtr& ptr)
{
- if (! p)
- return false;
- return p->isRunning();
+ return ptr && ptr->isRunning();
}
LLProcess::Status LLProcess::getStatus() const
@@ -863,19 +863,26 @@ LLProcess::Status LLProcess::getStatus() const
}
//static
-LLProcess::Status LLProcess::getStatus(const LLProcessPtr& p)
+LLProcess::Status LLProcess::getStatus(const LLProcessPtr& ptr)
{
- if (! p)
+ if (!ptr)
{
- // default-constructed Status has mState == UNSTARTED
- return Status();
+ Status status;
+ status.mState = UNSTARTED;
+ return status;
}
- return p->getStatus();
+ return ptr->getStatus();
}
std::string LLProcess::getStatusString() const
{
- return getStatusString(getStatus());
+ return getStatusString(mDesc, mStatus);
+}
+
+//static
+std::string LLProcess::getStatusString(const std::string& desc, const LLProcessPtr& ptr)
+{
+ return getStatusString(desc, getStatus(ptr));
}
std::string LLProcess::getStatusString(const Status& status) const
@@ -884,464 +891,309 @@ std::string LLProcess::getStatusString(const Status& status) const
}
//static
-std::string LLProcess::getStatusString(const std::string& desc, const LLProcessPtr& p)
+std::string LLProcess::getStatusString(const std::string& desc, const Status& status)
{
- if (! p)
+ std::string result = desc + ": ";
+ switch (status.mState)
{
- // default-constructed Status has mState == UNSTARTED
- return getStatusString(desc, Status());
+ case UNSTARTED: return result + "not started";
+ case RUNNING: return result + "running";
+ case EXITED: return result + STRINGIZE("exited with code " << status.mData);
+ case KILLED: return result + STRINGIZE("killed by signal " << status.mData);
+ default: return result + "unknown state";
}
- return desc + " " + p->getStatusString();
}
-//static
-std::string LLProcess::getStatusString(const std::string& desc, const Status& status)
+bool LLProcess::kill(const std::string& who)
{
- if (status.mState == UNSTARTED)
- return desc + " was never launched";
-
- if (status.mState == RUNNING)
- return desc + " running";
+ if (!mChild || mStatus.mState != RUNNING)
+ return true;
- if (status.mState == EXITED)
- return STRINGIZE(desc << " exited with code " << status.mData);
+ LL_INFOS("LLProcess") << who << " killing " << mDesc << LL_ENDL;
- if (status.mState == KILLED)
#if LL_WINDOWS
- return STRINGIZE(desc << " killed with exception " << std::hex << status.mData);
+ // Call TerminateProcess directly with exit code (UINT)-1 so that
+ // tick()'s GetExitCodeProcess reads back -1 as a signed int, matching
+ // the original APR-based behavior expected by tests and callers.
+ // Do NOT use mChild->terminate(): boost::process v2 may use a different
+ // exit code (e.g. EXIT_FAILURE=1) or invalidate the handle internally,
+ // which would break the exit-code check in tick().
+ if (!::TerminateProcess(mChild->native_handle(), (UINT)-1))
+ {
+ LL_WARNS("LLProcess") << "Failed to terminate " << mDesc
+ << ": error " << ::GetLastError() << LL_ENDL;
+ return false;
+ }
#else
- return STRINGIZE(desc << " killed by signal " << status.mData
- << " (" << apr_signal_description_get(status.mData) << ")");
+ // Send SIGTERM so the child can clean up gracefully, and so tick()'s
+ // waitpid() reports WIFSIGNALED/WTERMSIG == SIGTERM rather than SIGKILL.
+ // Do NOT call mChild->terminate(): in boost::process v2, that function
+ // sends SIGKILL and may set the internal pid to -1, which would cause
+ // tick()'s waitpid(mChild->id(), ...) to wait for any child (-1) and
+ // never match the result against the stored pid.
+ pid_t pid = mChild->id();
+ if (::kill(pid, SIGTERM) != 0 && errno != ESRCH)
+ {
+ LL_WARNS("LLProcess") << "Failed to send SIGTERM to " << mDesc
+ << " (pid " << pid << "): " << strerror(errno) << LL_ENDL;
+ return false;
+ }
#endif
- return STRINGIZE(desc << " in unknown state " << status.mState << " (" << status.mData << ")");
+ // Don't set status here - let handleExit() do it when the process actually terminates
+ return !isRunning();
}
-// Classic-C-style APR callback
-void LLProcess::status_callback(int reason, void* data, int status)
+//static
+bool LLProcess::kill(const LLProcessPtr& ptr, const std::string& who)
{
- // Our only role is to bounce this static method call back into object
- // space.
- static_cast<LLProcess*>(data)->handle_status(reason, status);
+ return !ptr || ptr->kill(who);
}
-#define tabent(symbol) { symbol, #symbol }
-static struct ReasonCode
-{
- int code;
- const char* name;
-} reasons[] =
+LLProcess::id LLProcess::getProcessID() const
{
- 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
+ if (!mChild)
+ return 0;
-// Object-oriented callback
-void LLProcess::handle_status(int reason, int status)
+#if LL_WINDOWS
+ return static_cast<int>(mChild->id());
+#else
+ return mChild->id();
+#endif
+}
+
+LLProcess::handle LLProcess::getProcessHandle() const
{
- {
- // This odd appearance of LL_DEBUGS is just to bracket a lookup that will
- // only be performed if in fact we're going to produce the log message.
- LL_DEBUGS("LLProcess") << empty;
- std::string reason_str;
- for (const ReasonCode& rcp : reasons)
- {
- if (reason == rcp.code)
- {
- reason_str = rcp.name;
- break;
- }
- }
- if (reason_str.empty())
- {
- reason_str = STRINGIZE("unknown reason " << reason);
- }
- LL_CONT << mDesc << ": handle_status(" << reason_str << ", " << status << ")" << LL_ENDL;
- }
+ if (!mChild)
+ return 0;
- if (! (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST))
+#if LL_WINDOWS
+ // Duplicate the process handle so the caller owns an independent copy.
+ // boost::process v2's process destructor always closes the handle
+ // (even after process::detach()), so the raw native_handle() becomes invalid
+ // once ~process() runs. DuplicateHandle gives the caller a handle whose
+ // lifetime is not tied to boost's internal process cleanup.
+ HANDLE source = mChild->native_handle();
+ if (!source || source == INVALID_HANDLE_VALUE)
+ return 0;
+ HANDLE dup = nullptr;
+ if (!::DuplicateHandle(
+ ::GetCurrentProcess(), source,
+ ::GetCurrentProcess(), &dup,
+ PROCESS_QUERY_INFORMATION | SYNCHRONIZE,
+ FALSE, 0))
{
- // We're only interested in the call when the child terminates.
- return;
+ LL_WARNS("LLProcess") << "DuplicateHandle failed for " << mDesc
+ << ": error " << ::GetLastError() << LL_ENDL;
+ return 0;
}
+ return dup;
+#else
+ return mChild->id();
+#endif
+}
- // Somewhat oddly, APR requires that you explicitly unregister even when
- // it already knows the child has terminated. We must pass the same 'data'
- // pointer as for the register() call, which was our 'this'.
- apr_proc_other_child_unregister(this);
- // don't keep polling for a terminated process
- sProcessListener.dropPoll(*this);
- // We overload mStatus.mState to indicate whether the child is registered
- // for APR callback: only RUNNING means registered. Track that we've
- // unregistered. We know the child has terminated; might be EXITED or
- // KILLED; refine below.
- mStatus.mState = EXITED;
+//static
+LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc)
+{
+#if LL_WINDOWS
+ if (h == 0 || h == INVALID_HANDLE_VALUE)
+ return 0;
- // Make last-gasp calls for each of the ReadPipes we have on hand. Since
- // they're listening on "mainloop", we can be sure they'll eventually
- // collect all pending data from the child. But we want to be able to
- // guarantee to our consumer that by the time we post on the "postend"
- // LLEventPump, our ReadPipes are already buffering all the data there
- // will ever be from the child. That lets the "postend" listener decide
- // what to do with that final data.
- for (size_t i = 0; i < mPipes.size(); ++i)
+ DWORD exit_code;
+ if (GetExitCodeProcess(h, &exit_code))
{
- std::string error;
- ReadPipeImpl* ppipe = getPipePtr<ReadPipeImpl>(error, FILESLOT(i));
- if (ppipe)
- {
- static LLSD trivial;
- ppipe->tick(trivial);
- }
+ if (exit_code == STILL_ACTIVE)
+ return h; // process still running
+ // Process has exited: close the duplicated handle (see getProcessHandle()).
}
+ // Either process exited or GetExitCodeProcess failed; either way we're done.
+ CloseHandle(h);
+ return 0;
+#else
+ if (h == 0)
+ return 0;
-// wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT);
- // 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, which means we have to decode it by
- // hand.
- mStatus = interpret_status(status);
- LL_INFOS("LLProcess") << getStatusString() << LL_ENDL;
+ // Use waitpid with WNOHANG to check if process is still running
+ // This is more reliable than kill(pid, 0) and properly reaps zombies
+ int status;
+ pid_t result;
- // If caller requested notification on child termination, send it.
- if (! mPostend.empty())
+ // Retry on EINTR (interrupted system call)
+ do
{
- LLEventPumps::instance().obtain(mPostend)
- .post(LLSDMap
- ("id", getProcessID())
- ("desc", mDesc)
- ("state", mStatus.mState)
- ("data", mStatus.mData)
- ("string", getStatusString())
- );
- }
-}
-
-LLProcess::id LLProcess::getProcessID() const
-{
- return mProcess.pid;
-}
-
-LLProcess::handle LLProcess::getProcessHandle() const
-{
-#if LL_WINDOWS
- return mProcess.hproc;
-#else
- return mProcess.pid;
-#endif
-}
+ result = waitpid(h, &status, WNOHANG);
+ } while (result == -1 && errno == EINTR);
-std::string LLProcess::getPipeName(FILESLOT) const
-{
- // LLProcess::FileParam::type "npipe" is not yet implemented
- return "";
-}
-
-template<class PIPETYPE>
-PIPETYPE* LLProcess::getPipePtr(std::string& error, FILESLOT slot)
-{
- if (slot >= NSLOTS)
+ if (result == 0)
{
- error = STRINGIZE(mDesc << " has no slot " << slot);
- return NULL;
+ // Process still running
+ return h;
}
- if (!mPipes[slot])
+ else if (result == h)
{
- error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a monitored pipe");
- return NULL;
+ // Process has terminated (and we've reaped it)
+ return 0;
}
- // 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].get());
- if (! ppipe)
+ else if (result == -1)
{
- error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a " << typeid(PIPETYPE).name());
- return NULL;
+ // Error occurred
+ if (errno == ECHILD)
+ {
+ // Process doesn't exist or was already reaped
+ return 0;
+ }
+ // For other errors, assume process is gone
+ LL_WARNS("LLProcess") << "waitpid(" << h << ") failed: "
+ << strerror(errno) << LL_ENDL;
+ return 0;
}
- error.clear();
- return ppipe;
+ // Shouldn't get here, but if we do, assume process is gone
+ return 0;
+#endif
}
-template <class PIPETYPE>
-PIPETYPE& LLProcess::getPipe(FILESLOT slot)
+std::string LLProcess::getPipeName(FILESLOT slot) const
{
- std::string error;
- PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
- if (! wp)
- {
- LLTHROW(NoPipe(error));
- }
- return *wp;
+ // Named pipes not yet implemented in this PoC
+ return "";
}
-template <class PIPETYPE>
-boost::optional<PIPETYPE&> LLProcess::getOptPipe(FILESLOT slot)
+LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot)
{
- std::string error;
- PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot);
- if (! wp)
+ if (slot >= NSLOTS)
+ throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot));
+
+ if (slot == STDIN)
{
- LL_DEBUGS("LLProcess") << error << LL_ENDL;
- return boost::optional<PIPETYPE&>();
+ if (!mWritePipe)
+ throw NoPipe(STRINGIZE(mDesc << ": stdin is not a monitored pipe"));
+ return *mWritePipe;
}
- return *wp;
-}
-LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot)
-{
- return getPipe<WritePipe>(slot);
-}
-
-boost::optional<LLProcess::WritePipe&> LLProcess::getOptWritePipe(FILESLOT slot)
-{
- return getOptPipe<WritePipe>(slot);
+ // STDOUT or STDERR slots: neither has a WritePipe.
+ // Distinguish "not piped at all" from "piped but wrong direction".
+ const char* slotname = (slot == STDOUT) ? "stdout" : "stderr";
+ bool is_piped = (slot == STDOUT) ? bool(mStdoutReadPipe) : bool(mStderrReadPipe);
+ if (!is_piped)
+ throw NoPipe(STRINGIZE(mDesc << ": " << slotname << " is not a monitored pipe"));
+ else
+ throw NoPipe(STRINGIZE(mDesc << ": " << slotname << " is a ReadPipe, not a WritePipe"));
}
LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot)
{
- return getPipe<ReadPipe>(slot);
-}
+ if (slot >= NSLOTS)
+ throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot));
-boost::optional<LLProcess::ReadPipe&> LLProcess::getOptReadPipe(FILESLOT slot)
-{
- return getOptPipe<ReadPipe>(slot);
-}
+ if (slot == STDIN)
+ throw NoPipe(STRINGIZE(mDesc << ": ReadPipe is invalid for stdin"));
-//static
-std::string LLProcess::getline(std::istream& in)
-{
- std::string line;
- std::getline(in, line);
- // Blur the distinction between "\r\n" and plain "\n". std::getline() will
- // have eaten the "\n", but we could still end up with a trailing "\r".
- std::string::size_type lastpos = line.find_last_not_of("\r");
- if (lastpos != std::string::npos)
+ // At this point, slot must be STDOUT or STDERR
+ // Check if a pipe was configured for this slot
+ if (slot == STDOUT)
{
- // Found at least one character that's not a trailing '\r'. SKIP OVER
- // IT and erase the rest of the line.
- line.erase(lastpos+1);
+ if (!mStdoutReadPipe)
+ throw NoPipe(STRINGIZE(mDesc << ": stdout is not a monitored pipe"));
+ return *mStdoutReadPipe;
}
- return line;
-}
-
-std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params)
-{
- if (params.cwd.isProvided())
+ else if (slot == STDERR)
{
- out << "cd " << LLStringUtil::quote(params.cwd) << ": ";
+ if (!mStderrReadPipe)
+ throw NoPipe(STRINGIZE(mDesc << ": stderr is not a monitored pipe"));
+ return *mStderrReadPipe;
}
- out << LLStringUtil::quote(params.executable);
- for (const std::string& arg : params.args)
+ else
{
- out << ' ' << LLStringUtil::quote(arg);
+ // This should never happen given the checks above, but handle it anyway
+ throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot));
}
- return out;
}
-/*****************************************************************************
-* Windows specific
-*****************************************************************************/
-#if LL_WINDOWS
-
-static std::string WindowsErrorString(const std::string& operation);
-
-void LLProcess::autokill()
-{
- // hopefully now handled by apr_procattr_autokill_set()
-}
-
-LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc)
+LLProcess::WritePipe* LLProcess::getOptWritePipe(FILESLOT slot)
{
- // This direct Windows implementation is because we have no access to the
- // apr_proc_t struct: we expect it's been destroyed.
- if (! h)
- return 0;
+ if (slot >= NSLOTS)
+ {
+ LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL;
+ return nullptr;
+ }
- DWORD waitresult = WaitForSingleObject(h, 0);
- if(waitresult == WAIT_OBJECT_0)
+ if (slot == STDIN)
{
- // the process has completed.
- if (! desc.empty())
+ if (!mWritePipe)
{
- DWORD status = 0;
- if (! GetExitCodeProcess(h, &status))
- {
- LL_WARNS("LLProcess") << desc << " terminated, but "
- << WindowsErrorString("GetExitCodeProcess()") << LL_ENDL;
- }
- {
- LL_INFOS("LLProcess") << getStatusString(desc, interpret_status(status))
- << LL_ENDL;
- }
+ LL_WARNS("LLProcess") << mDesc << ": stdin is not a monitored pipe" << LL_ENDL;
+ return nullptr;
}
- CloseHandle(h);
- return 0;
+ return mWritePipe.get();
}
- return h;
+ // STDOUT or STDERR slots: neither has a WritePipe.
+ const char* slotname = (slot == STDOUT) ? "stdout" : "stderr";
+ bool is_piped = (slot == STDOUT) ? bool(mStdoutReadPipe) : bool(mStderrReadPipe);
+ if (!is_piped)
+ LL_WARNS("LLProcess") << mDesc << ": " << slotname << " is not a monitored pipe" << LL_ENDL;
+ else
+ LL_WARNS("LLProcess") << mDesc << ": " << slotname << " is a ReadPipe, not a WritePipe" << LL_ENDL;
+ return nullptr;
}
-static LLProcess::Status interpret_status(int status)
+LLProcess::ReadPipe* LLProcess::getOptReadPipe(FILESLOT slot)
{
- LLProcess::Status result;
-
- // This bit of code is cribbed from apr/threadproc/win32/proc.c, a
- // function (unfortunately static) called why_from_exit_code():
- /* See WinNT.h STATUS_ACCESS_VIOLATION and family for how
- * this class of failures was determined
- */
- if ((status & 0xFFFF0000) == 0xC0000000)
- {
- result.mState = LLProcess::KILLED;
- }
- else
+ if (slot >= NSLOTS)
{
- result.mState = LLProcess::EXITED;
+ LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL;
+ return nullptr;
}
- result.mData = status;
-
- return result;
-}
-/// GetLastError()/FormatMessage() boilerplate
-static std::string WindowsErrorString(const std::string& operation)
-{
- auto result = GetLastError();
- return STRINGIZE(operation << " failed (" << result << "): "
- << windows_message<std::string>(result));
-}
-
-/*****************************************************************************
-* Posix specific
-*****************************************************************************/
-#else // Mac and linux
-
-#include <signal.h>
-#include <fcntl.h>
-#include <errno.h>
-#include <sys/wait.h>
-
-void LLProcess::autokill()
-{
- // What we ought to do here is to:
- // 1. create a unique process group and run all autokill children in that
- // group (see https://jira.secondlife.com/browse/SWAT-563);
- // 2. figure out a way to intercept control when the viewer exits --
- // gracefully or not;
- // 3. when the viewer exits, kill off the aforementioned process group.
-
- // It's point 2 that's troublesome. Although I've seen some signal-
- // handling logic in the Posix viewer code, I haven't yet found any bit of
- // code that's run no matter how the viewer exits (a try/finally for the
- // whole process, as it were).
-}
-
-// 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, LLProcess::Status* pstatus=NULL)
-{
- LLProcess::Status dummy;
- if (! pstatus)
+ if (slot == STDIN)
{
- // If caller doesn't want to see Status, give us a target anyway so we
- // don't have to have a bunch of conditionals.
- pstatus = &dummy;
+ LL_WARNS("LLProcess") << mDesc << ": ReadPipe is invalid for stdin" << LL_ENDL;
+ return nullptr;
}
- int status = 0;
- pid_t wait_result = ::waitpid(pid, &status, WNOHANG);
- if (wait_result == pid)
+ if (slot == STDOUT)
{
- *pstatus = interpret_status(status);
- return true;
+ if (!mStdoutReadPipe)
+ {
+ LL_WARNS("LLProcess") << mDesc << ": stdout is not a monitored pipe" << LL_ENDL;
+ return nullptr;
+ }
+ return mStdoutReadPipe.get();
}
- if (wait_result == 0)
+ else if (slot == STDERR)
{
- pstatus->mState = LLProcess::RUNNING;
- pstatus->mData = 0;
- return false;
+ if (!mStderrReadPipe)
+ {
+ LL_WARNS("LLProcess") << mDesc << ": stderr is not a monitored pipe" << LL_ENDL;
+ return nullptr;
+ }
+ return mStderrReadPipe.get();
}
-
- // Clear caller's Status block; caller must interpret UNSTARTED to mean
- // "if this PID was ever valid, it no longer is."
- *pstatus = LLProcess::Status();
-
- // We've dealt with the success cases: we were able to reap the child
- // (wait_result == pid) or it's still running (wait_result == 0). It may
- // be that the child terminated but didn't hang around long enough for us
- // to reap. In that case we still have no Status to report, but we can at
- // least state that it's not running.
- if (wait_result == -1 && errno == ECHILD)
+ else
{
- // No such process -- this may mean we're ignoring SIGCHILD.
- return true;
+ LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL;
+ return nullptr;
}
-
- // Uh, should never happen?!
- LL_WARNS("LLProcess") << "LLProcess::reap_pid(): waitpid(" << pid << ") returned "
- << wait_result << "; not meaningful?" << LL_ENDL;
- // If caller is looping until this pid terminates, and if we can't find
- // out, better to break the loop than to claim it's still running.
- return true;
}
-LLProcess::id LLProcess::isRunning(id pid, const std::string& desc)
+//static
+std::string LLProcess::basename(const std::string& path)
{
- // This direct Posix implementation is because we have no access to the
- // apr_proc_t struct: we expect it's been destroyed.
- if (! pid)
- return 0;
-
- // Check whether the process has exited, and reap it if it has.
- LLProcess::Status status;
- if(reap_pid(pid, &status))
- {
- // the process has exited.
- if (! desc.empty())
- {
- std::string statstr(desc + " apparently terminated: no status available");
- // We don't just pass UNSTARTED to getStatusString() because, in
- // the context of reap_pid(), that state has special meaning.
- if (status.mState != UNSTARTED)
- {
- statstr = getStatusString(desc, status);
- }
- LL_INFOS("LLProcess") << statstr << LL_ENDL;
- }
- return 0;
- }
-
- return pid;
+ std::string::size_type delim = path.find_last_of("\\/");
+ if (delim == std::string::npos)
+ return path;
+ return path.substr(delim + 1);
}
-static LLProcess::Status interpret_status(int status)
+//static
+std::string LLProcess::getline(std::istream& in)
{
- LLProcess::Status result;
-
- if (WIFEXITED(status))
- {
- result.mState = LLProcess::EXITED;
- result.mData = WEXITSTATUS(status);
- }
- else if (WIFSIGNALED(status))
- {
- result.mState = LLProcess::KILLED;
- result.mData = WTERMSIG(status);
- }
- else // uh, shouldn't happen?
- {
- result.mState = LLProcess::EXITED;
- result.mData = status; // someone else will have to decode
- }
-
- return result;
+ std::string line;
+ std::getline(in, line);
+ // Trim trailing \r for cross-platform compatibility
+ if (!line.empty() && line.back() == '\r')
+ line.pop_back();
+ return line;
}
-
-#endif // Posix