diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-09-01 18:35:23 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-09-04 21:58:53 +0800 |
| commit | 00bb3bb6f07660bd914d29a1389342bb3060d254 (patch) | |
| tree | 431f574922494d2201718f13085f17bc6bd7fb42 /indra/llcommon/llprocess.cpp | |
| parent | a8636720129952c10cf49ab80da21bf8b340b96c (diff) | |
| parent | 4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff) | |
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llcommon/llprocess.cpp')
| -rw-r--r-- | indra/llcommon/llprocess.cpp | 2022 |
1 files changed, 1013 insertions, 1009 deletions
diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp index cbba2ceb87..86e1f48377 100644 --- a/indra/llcommon/llprocess.cpp +++ b/indra/llcommon/llprocess.cpp @@ -30,30 +30,129 @@ #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 + +#if LL_WINDOWS +#include <windows.h> +#include "llwin32headers.h" +#include <mutex> + +namespace { + // Global job object that will kill all child processes when parent terminates + HANDLE g_jobObject = NULL; + bool g_jobObjectInitialized = false; + std::mutex g_jobObjectMutex; + + void InitializeJobObject() + { + if (g_jobObjectInitialized) + return; + std::lock_guard<std::mutex> lock(g_jobObjectMutex); + if (g_jobObjectInitialized) + return; + + g_jobObjectInitialized = true; + + // Create a job object + g_jobObject = ::CreateJobObjectW(NULL, NULL); + if (g_jobObject == NULL) + { + LL_WARNS("LLProcess") << "Failed to create job object: " << ::GetLastError() << LL_ENDL; + return; + } + + // Configure the job to kill all processes when the last handle closes + // (i.e., when the parent process exits) + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 }; + jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + // Windows 8+ supports nested jobs (for VS studio) + jeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK; + + if (!::SetInformationJobObject(g_jobObject, JobObjectExtendedLimitInformation, + &jeli, sizeof(jeli))) + { + LL_WARNS("LLProcess") << "Failed to set job object limits: " << ::GetLastError() << LL_ENDL; + ::CloseHandle(g_jobObject); + g_jobObject = NULL; + return; + } + + LL_INFOS("LLProcess") << "Job object created - child processes will terminate with parent" << LL_ENDL; + } + + void AssignProcessToJob(HANDLE hProcess, const std::string& desc) + { + if (!g_jobObjectInitialized) + InitializeJobObject(); + + if (g_jobObject != NULL) + { + if (!::AssignProcessToJobObject(g_jobObject, hProcess)) + { + DWORD error = ::GetLastError(); + // ERROR_ACCESS_DENIED (5) means the process is already in a job + // This can happen if the parent viewer is itself in a job + if (error == ERROR_ACCESS_DENIED) + { + LL_WARNS("LLProcess") << "Autokill requested but process " << desc + << " is already in a job object (ERROR_ACCESS_DENIED)" + << LL_ENDL; + } + else + { + LL_WARNS("LLProcess") << "Failed to assign process " << desc + << " to job object: error " << error << LL_ENDL; + } + } + else + { + LL_DEBUGS("LLProcess") << "Process " << desc << " assigned to job object" << LL_ENDL; + } + } + } +} +#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,811 +161,853 @@ 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)), + mKillCalled(false) { - LLProcessError(const std::string& msg): LLException(msg) {} -}; + launch(params); +} + +LLProcess::~LLProcess() +{ + if (mChild && mStatus.mState == RUNNING) + { + if (mAttached && mAutokill && !mKillCalled) + { + 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() + // Only wait if terminate succeeded (process was running) + + if (!ec) + { + DWORD exit_code = 0; + bool still_running = (::GetExitCodeProcess(mChild->native_handle(), &exit_code) + && exit_code == STILL_ACTIVE); + if (still_running) + { + // We are likely on the main thread. Don't wait long! + // Maybe shouldn't wait at all. + WaitForSingleObject(mChild->native_handle(), 10); + } + } + else + { + LL_WARNS("LLProcess") << "Process " << mDesc + << " terminate failed with error " << ec.value() + << " (" << ec.message() << "), skipping wait" << LL_ENDL; + } +#endif + } + else if (!mKillCalled) + { + 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); - - // create an env vector for the child process - std::vector<const char*> envv; - - // Add environment value assignments. See above remarks about c_str(). - for (const std::string& env : params.envs) + if (use_stderr_pipe) { - envv.push_back(env.c_str()); + mStderrPipe = std::make_shared<asio::readable_pipe>(mIOContext); + LL_DEBUGS("LLProcess") << "Created stderr pipe for " << mDesc << LL_ENDL; } - // terminate with a null pointer - envv.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], &envv[0], procattr, - mPool))) + // Build the process + try { - LLTHROW(LLProcessError(STRINGIZE(params << " failed"))); - } +#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 - // 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; + // Create child process with appropriate redirections. + // v2 uses process_stdio for I/O redirection + bp::process_stdio stdio; - mDesc = STRINGIZE(getDesc(params) << " (" << mProcess.pid << ')'); - LL_INFOS("LLProcess") << mDesc << ": launched " << params << LL_ENDL; + if (use_stdin_pipe) + stdio.in = *mStdinPipe; + else + stdio.in = nullptr; // inherit - // 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_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. + // Resolve bare executable names (i.e. "outleap-agent") through the + // environment's PATH + boost::filesystem::path executable_path(params.executable()); + if (!executable_path.has_parent_path()) + { + boost::filesystem::path resolved = + bp::environment::find_executable(executable_path); + if (!resolved.empty()) + { + executable_path = resolved; + } + else + { + LL_WARNS("LLProcess") << "Could not locate '" << params.executable() + << "' on PATH -- launch will likely fail" << LL_ENDL; + // Let bp::process try to produce an error. + } + } - // 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 + // Add the process to the job object so it terminates when parent dies. + // This is done for all processes with autokill=true (the default). + // Job objects are the Windows-recommended way to ensure child processes + // don't become orphaned if the parent crashes or is killed. + if (mAutokill && mChild) + { + AssignProcessToJob(mChild->native_handle(), mDesc); + } +#else + // 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; - // 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); + // 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 + ); + } + + 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"); + // Process has exited. + // We are on the main thread, so get exit code without blocking + DWORD exit_code = STILL_ACTIVE; + if (::GetExitCodeProcess(mChild->native_handle(), &exit_code)) + { + if (exit_code != STILL_ACTIVE) + { + // Exit code is available + Status exitStatus; + exitStatus.mState = EXITED; + exitStatus.mData = static_cast<int>(exit_code); + handleExit(exitStatus); + } + else + { + // Exit code not ready yet, will retry next tick + // This shouldn't happen if mChild->running() returned false, + // but handle it gracefully + LL_DEBUGS("LLProcess") << "Process " << mDesc + << " exited but exit code not ready yet" << LL_ENDL; + } + } + else + { + LL_WARNS("LLProcess") << "GetExitCodeProcess failed for " << mDesc + << ": error " << ::GetLastError() << LL_ENDL; + // Synthesize exit status + Status exitStatus; + exitStatus.mState = EXITED; + exitStatus.mData = -1; + 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 (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 - if (mPool) + // 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 @@ -875,19 +1016,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 @@ -896,464 +1044,320 @@ 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 << ")"); + // Mark as killed so the destructor doesn't repeat the termination attempt, + // but a better idea might be to modify mState. + mKillCalled = true; + // Don't set status here - let handleExit() do it when the process actually terminates + // so that it will be able to post EOF event. + // At this point in time mStatus.mState is still RUNNING, so this is basically + // retuning false. + 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[] = -{ - 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 - -// Object-oriented callback -void LLProcess::handle_status(int reason, int status) +void LLProcess::pump() { - { - // 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 (! (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST)) - { - // We're only interested in the call when the child terminates. - return; - } - - // 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; - - // 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) - { - std::string error; - ReadPipeImpl* ppipe = getPipePtr<ReadPipeImpl>(error, FILESLOT(i)); - if (ppipe) - { - static LLSD trivial; - ppipe->tick(trivial); - } - } - -// 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; - - // If caller requested notification on child termination, send it. - if (! mPostend.empty()) - { - LLEventPumps::instance().obtain(mPostend) - .post(LLSDMap - ("id", getProcessID()) - ("desc", mDesc) - ("state", mStatus.mState) - ("data", mStatus.mData) - ("string", getStatusString()) - ); - } + tick(); } LLProcess::id LLProcess::getProcessID() const { - return mProcess.pid; -} + if (!mChild) + return 0; -LLProcess::handle LLProcess::getProcessHandle() const -{ #if LL_WINDOWS - return mProcess.hproc; + return static_cast<int>(mChild->id()); #else - return mProcess.pid; + return mChild->id(); #endif } -std::string LLProcess::getPipeName(FILESLOT) const +LLProcess::handle LLProcess::getProcessHandle() const { - // LLProcess::FileParam::type "npipe" is not yet implemented - return ""; -} + if (!mChild) + return 0; -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[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].get()); - if (! ppipe) +#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)) { - error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a " << typeid(PIPETYPE).name()); - return NULL; + LL_WARNS("LLProcess") << "DuplicateHandle failed for " << mDesc + << ": error " << ::GetLastError() << LL_ENDL; + return 0; } - - error.clear(); - return ppipe; + return dup; +#else + return mChild->id(); +#endif } -template <class PIPETYPE> -PIPETYPE& LLProcess::getPipe(FILESLOT slot) +//static +LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc) { - std::string error; - PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot); - if (! wp) - { - LLTHROW(NoPipe(error)); - } - return *wp; -} +#if LL_WINDOWS + if (h == 0 || h == INVALID_HANDLE_VALUE) + return 0; -template <class PIPETYPE> -boost::optional<PIPETYPE&> LLProcess::getOptPipe(FILESLOT slot) -{ - std::string error; - PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot); - if (! wp) + DWORD exit_code; + if (GetExitCodeProcess(h, &exit_code)) { - LL_DEBUGS("LLProcess") << error << LL_ENDL; - return boost::optional<PIPETYPE&>(); + if (exit_code == STILL_ACTIVE) + return h; // process still running + // Process has exited: close the duplicated handle (see getProcessHandle()). } - 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); -} + // Either process exited or GetExitCodeProcess failed; either way we're done. + CloseHandle(h); + return 0; +#else + if (h == 0) + return 0; -boost::optional<LLProcess::ReadPipe&> LLProcess::getOptReadPipe(FILESLOT slot) -{ - return getOptPipe<ReadPipe>(slot); -} + // 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; -//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) + // Retry on EINTR (interrupted system call) + do { - // 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); - } - return line; -} + result = waitpid(h, &status, WNOHANG); + } while (result == -1 && errno == EINTR); -std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params) -{ - if (params.cwd.isProvided()) + if (result == 0) { - out << "cd " << LLStringUtil::quote(params.cwd) << ": "; + // Process still running + return h; } - out << LLStringUtil::quote(params.executable); - for (const std::string& arg : params.args) + else if (result == h) { - out << ' ' << LLStringUtil::quote(arg); - } - 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) -{ - // This direct Windows implementation is because we have no access to the - // apr_proc_t struct: we expect it's been destroyed. - if (! h) + // Process has terminated (and we've reaped it) return 0; - - DWORD waitresult = WaitForSingleObject(h, 0); - if(waitresult == WAIT_OBJECT_0) + } + else if (result == -1) { - // the process has completed. - if (! desc.empty()) + // Error occurred + if (errno == ECHILD) { - 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; - } + // Process doesn't exist or was already reaped + return 0; } - CloseHandle(h); + // For other errors, assume process is gone + LL_WARNS("LLProcess") << "waitpid(" << h << ") failed: " + << strerror(errno) << LL_ENDL; return 0; } - return h; + // Shouldn't get here, but if we do, assume process is gone + return 0; +#endif } -static LLProcess::Status interpret_status(int status) +std::string LLProcess::getPipeName(FILESLOT slot) const { - 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 - { - result.mState = LLProcess::EXITED; - } - result.mData = status; - - return result; + // Named pipes not yet implemented in this PoC + return ""; } -/// GetLastError()/FormatMessage() boilerplate -static std::string WindowsErrorString(const std::string& operation) +LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot) { - 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> + if (slot >= NSLOTS) + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); -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. + if (slot == STDIN) + { + if (!mWritePipe) + throw NoPipe(STRINGIZE(mDesc << ": stdin is not a monitored pipe")); + return *mWritePipe; + } - // 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). + // 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")); } -// 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::ReadPipe& LLProcess::getReadPipe(FILESLOT slot) { - LLProcess::Status dummy; - if (! pstatus) - { - // 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; - } + if (slot >= NSLOTS) + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); + + if (slot == STDIN) + throw NoPipe(STRINGIZE(mDesc << ": ReadPipe is invalid for stdin")); - int status = 0; - pid_t wait_result = ::waitpid(pid, &status, WNOHANG); - if (wait_result == pid) + // At this point, slot must be STDOUT or STDERR + // Check if a pipe was configured for this slot + if (slot == STDOUT) { - *pstatus = interpret_status(status); - return true; + if (!mStdoutReadPipe) + throw NoPipe(STRINGIZE(mDesc << ": stdout is not a monitored pipe")); + return *mStdoutReadPipe; } - if (wait_result == 0) + else if (slot == STDERR) { - pstatus->mState = LLProcess::RUNNING; - pstatus->mData = 0; - return false; + if (!mStderrReadPipe) + throw NoPipe(STRINGIZE(mDesc << ": stderr is not a monitored pipe")); + return *mStderrReadPipe; } - - // 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; + // This should never happen given the checks above, but handle it anyway + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); } - - // 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) +LLProcess::WritePipe* LLProcess::getOptWritePipe(FILESLOT slot) { - // 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; + if (slot >= NSLOTS) + { + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; + } - // Check whether the process has exited, and reap it if it has. - LLProcess::Status status; - if(reap_pid(pid, &status)) + if (slot == STDIN) { - // the process has exited. - if (! desc.empty()) + if (!mWritePipe) { - 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; + LL_WARNS("LLProcess") << mDesc << ": stdin is not a monitored pipe" << LL_ENDL; + return nullptr; } - return 0; + return mWritePipe.get(); } - return pid; + // 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; + if (slot >= NSLOTS) + { + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; + } - if (WIFEXITED(status)) + if (slot == STDIN) { - result.mState = LLProcess::EXITED; - result.mData = WEXITSTATUS(status); + LL_WARNS("LLProcess") << mDesc << ": ReadPipe is invalid for stdin" << LL_ENDL; + return nullptr; } - else if (WIFSIGNALED(status)) + + if (slot == STDOUT) { - result.mState = LLProcess::KILLED; - result.mData = WTERMSIG(status); + if (!mStdoutReadPipe) + { + LL_WARNS("LLProcess") << mDesc << ": stdout is not a monitored pipe" << LL_ENDL; + return nullptr; + } + return mStdoutReadPipe.get(); } - else // uh, shouldn't happen? + else if (slot == STDERR) + { + if (!mStderrReadPipe) + { + LL_WARNS("LLProcess") << mDesc << ": stderr is not a monitored pipe" << LL_ENDL; + return nullptr; + } + return mStderrReadPipe.get(); + } + else { - result.mState = LLProcess::EXITED; - result.mData = status; // someone else will have to decode + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; } +} - return result; +//static +std::string LLProcess::basename(const std::string& path) +{ + std::string::size_type delim = path.find_last_of("\\/"); + if (delim == std::string::npos) + return path; + return path.substr(delim + 1); } -#endif // Posix +//static +std::string LLProcess::getline(std::istream& in) +{ + 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; +} |
