diff options
Diffstat (limited to 'indra/llcommon')
93 files changed, 7767 insertions, 3872 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 1f7b3163c6..a0a196d3e8 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -20,7 +20,6 @@ set(llcommon_SOURCE_FILES llapr.cpp llassettype.cpp llatomic.cpp - llbase32.cpp llbase64.cpp llbitpack.cpp llcallbacklist.cpp @@ -67,7 +66,7 @@ set(llcommon_SOURCE_FILES llmortician.cpp llmutex.cpp llpointer.cpp - llptrto.cpp + llptrto.cpp llpredicate.cpp llprocess.cpp llprocessor.cpp @@ -84,7 +83,6 @@ set(llcommon_SOURCE_FILES llsdutil.cpp llsingleton.cpp llstacktrace.cpp - llstreamqueue.cpp llstreamtools.cpp llstring.cpp llstringtable.cpp @@ -108,7 +106,7 @@ set(llcommon_SOURCE_FILES workqueue.cpp StackWalker.cpp ) - + set(llcommon_HEADER_FILES CMakeLists.txt @@ -117,8 +115,6 @@ set(llcommon_HEADER_FILES chrono.h classic_callback.h commoncontrol.h - ctype_workaround.h - fix_macros.h fsyspath.h function_types.h indra_constants.h @@ -129,7 +125,6 @@ set(llcommon_HEADER_FILES llapr.h llassettype.h llatomic.h - llbase32.h llbase64.h llbitpack.h llboost.h @@ -219,7 +214,6 @@ set(llcommon_HEADER_FILES llsingleton.h llstacktrace.h llstl.h - llstreamqueue.h llstreamtools.h llstrider.h llstring.h @@ -248,26 +242,38 @@ set(llcommon_HEADER_FILES hbxxh.h is_approx_equal_fraction.h lockstatic.h - mutex.h stdtypes.h stringize.h threadpool.h threadpool_fwd.h threadsafeschedule.h - timer.h tuple.h u64.h workqueue.h StackWalker.h ) - + if (DARWIN) list(APPEND llcommon_HEADER_FILES llsys_objc.h) list(APPEND llcommon_SOURCE_FILES llsys_objc.mm) + + set_source_files_properties( + llsys_objc.mm + PROPERTIES + SKIP_PRECOMPILE_HEADERS TRUE + ) endif (DARWIN) if (USE_TRACY) list(APPEND llcommon_SOURCE_FILES llprofiler.cpp) + + # Skip precompiled headers on these files due to special compilation needs + set_source_files_properties( + llfasttimer.cpp + llprofiler.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS TRUE + ) endif () list(APPEND llcommon_SOURCE_FILES ${llcommon_HEADER_FILES}) @@ -284,8 +290,12 @@ target_link_libraries( ll::tracy ) -target_include_directories(llcommon INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(llcommon PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(llcommon PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(llcommon PRIVATE ${CMAKE_SOURCE_DIR}/llmath) + +if (USE_PRECOMPILED_HEADERS) + target_precompile_headers(llcommon REUSE_FROM llprecompiled) +endif () if (CMAKE_OSX_ARCHITECTURES MATCHES arm64 OR CMAKE_SYSTEM_PROCESSOR MATCHES aarch64) if (${PREBUILD_TRACKING_DIR}/sentinel_installed IS_NEWER_THAN ${PREBUILD_TRACKING_DIR}/sse2neon_installed OR NOT ${sse2neon_installed} EQUAL 0) @@ -320,6 +330,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES GNU) llevent.cpp llfasttimer.cpp PROPERTIES COMPILE_FLAGS -Wno-nonnull) + set_source_files_properties( + llprocessor.cpp + PROPERTIES COMPILE_FLAGS -Wno-unused-function) elseif (LINUX AND CMAKE_CXX_COMPILER_ID MATCHES Clang) set_source_files_properties(llsys.cpp PROPERTIES COMPILE_FLAGS -Wno-unused-but-set-variable) @@ -341,28 +354,35 @@ if (LL_TESTS) LL_ADD_INTEGRATION_TEST(classic_callback "" "${test_libs}") LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lazyeventapi "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llapp "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llbase64 "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llcond "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lldate "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lldeadmantimer "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lldependencies "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(lldoubledispatch "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llerror "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lleventcoro "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lleventfilter "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llevents "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llfile "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llframetimer "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llhttpdate "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llheteromap "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llinstancetracker "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llleap "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llmainthreadtask "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llpounceable "" "${test_libs}") - LL_ADD_INTEGRATION_TEST(llprocess "" "${test_libs}") + #LL_ADD_INTEGRATION_TEST(llprocess "" "${test_libs}") RIDER: Test is a known failure. Disabling for the moment. LL_ADD_INTEGRATION_TEST(llprocessor "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llprocinfo "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llrand "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llsd "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llsdserialize "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llsdutil "" "llcommon;llmath") LL_ADD_INTEGRATION_TEST(llsingleton "" "${test_libs}") - LL_ADD_INTEGRATION_TEST(llstreamqueue "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(llstreamtools "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llstring "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lltrace "" "${test_libs}") LL_ADD_INTEGRATION_TEST(lltreeiterators "" "${test_libs}") @@ -378,4 +398,11 @@ if (LL_TESTS) ## throwing and catching exceptions. ##LL_ADD_INTEGRATION_TEST(llexception "" "${test_libs}") + # Skip precompiled header for tests that need special handling + set_source_files_properties( + tests/llsd_test.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS TRUE + ) + endif (LL_TESTS) diff --git a/indra/llcommon/StackWalker.cpp b/indra/llcommon/StackWalker.cpp index 027df80df5..02cf48693b 100644 --- a/indra/llcommon/StackWalker.cpp +++ b/indra/llcommon/StackWalker.cpp @@ -785,6 +785,8 @@ private: case 8: //SymVirtual: szSymType = "Virtual"; break; + case NumSymTypes: + break; } } LPCSTR pdbName = Module.LoadedImageName; @@ -902,7 +904,7 @@ bool StackWalker::LoadModules() SetLastError(ERROR_DLL_INIT_FAILED); return false; } - if (m_modulesLoaded != FALSE) + if (m_modulesLoaded) return true; // Build the sym-path: diff --git a/indra/llcommon/classic_callback.h b/indra/llcommon/classic_callback.h index 009c25d67c..8a6ac7f7ff 100644 --- a/indra/llcommon/classic_callback.h +++ b/indra/llcommon/classic_callback.h @@ -184,8 +184,8 @@ protected: template <typename SIGNATURE, typename USERDATA=void*, typename CALLABLE=void(*)()> auto makeClassicCallback(CALLABLE&& callable) { - return std::move(ClassicCallback<SIGNATURE, USERDATA, CALLABLE> - (std::forward<CALLABLE>(callable))); + return ClassicCallback<SIGNATURE, USERDATA, CALLABLE> + (std::forward<CALLABLE>(callable)); } /***************************************************************************** diff --git a/indra/llcommon/ctype_workaround.h b/indra/llcommon/ctype_workaround.h deleted file mode 100644 index 89a47fe3db..0000000000 --- a/indra/llcommon/ctype_workaround.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @file ctype_workaround.h - * @brief The workaround is to create some legacy symbols that point - * to the correct symbols, which avoids link errors. - * - * $LicenseInfo:firstyear=2006&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#ifndef _CTYPE_WORKAROUND_H_ -#define _CTYPE_WORKAROUND_H_ - -/** - * the CTYPE_WORKAROUND is needed for linux dev stations that don't - * have the broken libc6 packages needed by our out-of-date static - * libs (such as libcrypto and libcurl). - * - * -- Leviathan 20060113 -*/ - -#include <cctype> - -__const unsigned short int *__ctype_b; -__const __int32_t *__ctype_tolower; -__const __int32_t *__ctype_toupper; - -// call this function at the beginning of main() -void ctype_workaround() -{ - __ctype_b = *(__ctype_b_loc()); - __ctype_toupper = *(__ctype_toupper_loc()); - __ctype_tolower = *(__ctype_tolower_loc()); -} - -#endif - diff --git a/indra/llcommon/fix_macros.h b/indra/llcommon/fix_macros.h deleted file mode 100644 index ed6c26a371..0000000000 --- a/indra/llcommon/fix_macros.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file fix_macros.h - * @author Nat Goodspeed - * @date 2012-11-16 - * @brief The Mac system headers seem to #define macros with obnoxiously - * generic names, preventing any library from using those names. We've - * had to fix these in so many places that it's worth making a header - * file to handle it. - * - * $LicenseInfo:firstyear=2012&license=viewerlgpl$ - * Copyright (c) 2012, Linden Research, Inc. - * $/LicenseInfo$ - */ - -// DON'T use an #include guard: every time we encounter this header, #undef -// these macros all over again. - -// who injects MACROS with such generic names?! Grr. -#ifdef check -#undef check -#endif diff --git a/indra/llcommon/fsyspath.h b/indra/llcommon/fsyspath.h index 2c900c02a7..237d99ae86 100644 --- a/indra/llcommon/fsyspath.h +++ b/indra/llcommon/fsyspath.h @@ -57,7 +57,7 @@ class fsyspath: public std::filesystem::path public: // default - fsyspath() {} + fsyspath() = default; // construct from UTF-8 encoded string fsyspath(const std::string& path): fsyspath(std::string_view(path)) {} fsyspath(const char* path): fsyspath(std::string_view(path)) {} @@ -66,8 +66,10 @@ public: {} // construct from existing path fsyspath(const super& path): super(path) {} + fsyspath(super&& path) : super(std::move(path)) {} fsyspath& operator=(const super& p) { super::operator=(p); return *this; } + fsyspath& operator=(super&& p) { super::operator=(std::move(p)); return *this; } fsyspath& operator=(const std::string& p) { return (*this) = std::string_view(p); } fsyspath& operator=(const char* p) { return (*this) = std::string_view(p); } fsyspath& operator=(std::string_view p) diff --git a/indra/llcommon/hexdump.h b/indra/llcommon/hexdump.h index ab5ba2b16d..ab5ba2b16d 100755..100644 --- a/indra/llcommon/hexdump.h +++ b/indra/llcommon/hexdump.h diff --git a/indra/llcommon/llapp.cpp b/indra/llcommon/llapp.cpp index f92bb98ba6..02d6da989a 100644 --- a/indra/llcommon/llapp.cpp +++ b/indra/llcommon/llapp.cpp @@ -37,7 +37,7 @@ #endif #include "llcommon.h" -#include "llapr.h" + #include "llerrorcontrol.h" #include "llframetimer.h" #include "lllivefile.h" @@ -53,19 +53,13 @@ // // Signal handling #ifndef LL_WINDOWS +#include "apr_signal.h" + # include <signal.h> # include <unistd.h> // for fork() void setup_signals(); void default_unix_signal_handler(int signum, siginfo_t *info, void *); -#if LL_LINUX -#else -// Called by breakpad exception handler after the minidump has been generated. -bool unix_post_minidump_callback(const char *dump_dir, - const char *minidump_id, - void *context, bool succeeded); -#endif - # if LL_DARWIN /* OSX doesn't support SIGRT* */ S32 LL_SMACKDOWN_SIGNAL = SIGUSR1; @@ -721,47 +715,4 @@ void default_unix_signal_handler(int signum, siginfo_t *info, void *) } } -bool unix_post_minidump_callback(const char *dump_dir, - const char *minidump_id, - void *context, bool succeeded) -{ - // Copy minidump file path into fixed buffer in the app instance to avoid - // heap allocations in a crash handler. - - // path format: <dump_dir>/<minidump_id>.dmp - auto dirPathLength = strlen(dump_dir); - auto idLength = strlen(minidump_id); - - // The path must not be truncated. - llassert((dirPathLength + idLength + 5) <= LLApp::MAX_MINDUMP_PATH_LENGTH); - - char * path = LLApp::instance()->getMiniDumpFilename(); - auto remaining = LLApp::MAX_MINDUMP_PATH_LENGTH; - strncpy(path, dump_dir, remaining); - remaining -= dirPathLength; - path += dirPathLength; - if (remaining > 0 && dirPathLength > 0 && path[-1] != '/') - { - *path++ = '/'; - --remaining; - } - if (remaining > 0) - { - strncpy(path, minidump_id, remaining); - remaining -= idLength; - path += idLength; - strncpy(path, ".dmp", remaining); - } - - LL_INFOS("CRASHREPORT") << "generated minidump: " << LLApp::instance()->getMiniDumpFilename() << LL_ENDL; - LLApp::runErrorHandler(); - -#ifndef LL_RELEASE_FOR_DOWNLOAD - clear_signals(); - return false; -#else - return true; -#endif -} #endif // !WINDOWS - diff --git a/indra/llcommon/llapr.cpp b/indra/llcommon/llapr.cpp index 51c814a387..3baaa412ef 100644 --- a/indra/llcommon/llapr.cpp +++ b/indra/llcommon/llapr.cpp @@ -231,9 +231,35 @@ bool _ll_apr_warn_status(apr_status_t status, const char* file, int line) { if(APR_SUCCESS == status) return false; #if !LL_LINUX && !__FreeBSD__ - char buf[MAX_STRING]; /* Flawfinder: ignore */ + char buf[MAX_STRING]; apr_strerror(status, buf, sizeof(buf)); - LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << LL_ENDL; + +#ifdef LL_WINDOWS + // On Windows, APR error strings may be in the system's ANSI code page (e.g., Cyrillic) + // Convert to UTF-8 for proper logging + std::string error_msg = buf; + int wlen = MultiByteToWideChar(CP_ACP, 0, buf, -1, nullptr, 0); + if (wlen > 0) + { + std::wstring wbuf(wlen, L'\0'); + MultiByteToWideChar(CP_ACP, 0, buf, -1, &wbuf[0], wlen); + + int utf8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (utf8len > 0) + { + std::string utf8buf(utf8len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), -1, &utf8buf[0], utf8len, nullptr, nullptr); + error_msg = utf8buf.c_str(); // Remove null terminator + } + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << error_msg << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; + } + else + { + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; + } +#else + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; +#endif #endif return true; } @@ -526,226 +552,6 @@ S32 LLAPRFile::seek(apr_file_t* file_handle, apr_seek_where_t where, S32 offset) } } -//static -S32 LLAPRFile::readEx(const std::string& filename, void *buf, S32 offset, S32 nbytes, LLVolatileAPRPool* pool) -{ - LL_PROFILE_ZONE_SCOPED; - //***************************************** - LLAPRFilePoolScope scope(pool); - apr_file_t* file_handle = open(filename, scope.getVolatileAPRPool(), APR_READ|APR_BINARY); - //***************************************** - if (!file_handle) - { - return 0; - } - - llassert(offset >= 0); - - if (offset > 0) - offset = LLAPRFile::seek(file_handle, APR_SET, offset); - - apr_size_t bytes_read; - if (offset < 0) - { - bytes_read = 0; - } - else - { - bytes_read = nbytes ; - apr_status_t s = apr_file_read(file_handle, buf, &bytes_read); - if (s != APR_SUCCESS) - { - LL_WARNS("APR") << " Attempting to read filename: " << filename << LL_ENDL; - ll_apr_warn_status(s); - bytes_read = 0; - } - else - { - llassert_always(bytes_read <= 0x7fffffff); - } - } - - //***************************************** - close(file_handle) ; - //***************************************** - return (S32)bytes_read; -} - -//static -S32 LLAPRFile::writeEx(const std::string& filename, const void *buf, S32 offset, S32 nbytes, LLVolatileAPRPool* pool) -{ - LL_PROFILE_ZONE_SCOPED; - apr_int32_t flags = APR_CREATE|APR_WRITE|APR_BINARY; - if (offset < 0) - { - flags |= APR_APPEND; - offset = 0; - } - - //***************************************** - LLAPRFilePoolScope scope(pool); - apr_file_t* file_handle = open(filename, scope.getVolatileAPRPool(), flags); - //***************************************** - if (!file_handle) - { - return 0; - } - - if (offset > 0) - { - offset = LLAPRFile::seek(file_handle, APR_SET, offset); - } - - apr_size_t bytes_written; - if (offset < 0) - { - bytes_written = 0; - } - else - { - bytes_written = nbytes ; - apr_status_t s = apr_file_write(file_handle, buf, &bytes_written); - if (s != APR_SUCCESS) - { - LL_WARNS("APR") << "Attempting to write filename: " << filename << LL_ENDL; - if (APR_STATUS_IS_ENOSPC(s)) - { - LLApp::notifyOutOfDiskSpace(); - } - ll_apr_warn_status(s); - bytes_written = 0; - } - else - { - llassert_always(bytes_written <= 0x7fffffff); - } - } - - //***************************************** - LLAPRFile::close(file_handle); - //***************************************** - - return (S32)bytes_written; -} - -//static -bool LLAPRFile::remove(const std::string& filename, LLVolatileAPRPool* pool) -{ - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_file_remove(filename.c_str(), scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS) - { - ll_apr_warn_status(s); - LL_WARNS("APR") << " Attempting to remove filename: " << filename << LL_ENDL; - return false; - } - return true; -} - -//static -bool LLAPRFile::rename(const std::string& filename, const std::string& newname, LLVolatileAPRPool* pool) -{ - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_file_rename(filename.c_str(), newname.c_str(), scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS) - { - ll_apr_warn_status(s); - LL_WARNS("APR") << " Attempting to rename filename: " << filename << LL_ENDL; - return false; - } - return true; -} - -//static -bool LLAPRFile::isExist(const std::string& filename, LLVolatileAPRPool* pool, apr_int32_t flags) -{ - apr_file_t* apr_file; - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_file_open(&apr_file, filename.c_str(), flags, APR_OS_DEFAULT, scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS || !apr_file) - { - return false; - } - else - { - apr_file_close(apr_file) ; - return true; - } -} - -//static -S32 LLAPRFile::size(const std::string& filename, LLVolatileAPRPool* pool) -{ - apr_file_t* apr_file; - apr_finfo_t info; - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_file_open(&apr_file, filename.c_str(), APR_READ, APR_OS_DEFAULT, scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS || !apr_file) - { - return 0; - } - else - { - apr_status_t s = apr_file_info_get(&info, APR_FINFO_SIZE, apr_file); - - apr_file_close(apr_file) ; - - if (s == APR_SUCCESS) - { - return (S32)info.size; - } - else - { - return 0; - } - } -} - -//static -bool LLAPRFile::makeDir(const std::string& dirname, LLVolatileAPRPool* pool) -{ - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_dir_make(dirname.c_str(), APR_FPROT_OS_DEFAULT, scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS) - { - ll_apr_warn_status(s); - LL_WARNS("APR") << " Attempting to make directory: " << dirname << LL_ENDL; - return false; - } - return true; -} - -//static -bool LLAPRFile::removeDir(const std::string& dirname, LLVolatileAPRPool* pool) -{ - apr_status_t s; - - LLAPRFilePoolScope scope(pool); - s = apr_file_remove(dirname.c_str(), scope.getVolatileAPRPool()); - - if (s != APR_SUCCESS) - { - ll_apr_warn_status(s); - LL_WARNS("APR") << " Attempting to remove directory: " << dirname << LL_ENDL; - return false; - } - return true; -} // //end of static components of LLAPRFile //******************************************************************************************************************************* diff --git a/indra/llcommon/llapr.h b/indra/llcommon/llapr.h index d9f1a31575..8795a0f2c4 100644 --- a/indra/llcommon/llapr.h +++ b/indra/llcommon/llapr.h @@ -35,13 +35,8 @@ #include "llwin32headers.h" #include "apr_thread_proc.h" -#include "apr_getopt.h" -#include "apr_signal.h" -#include "llstring.h" - -#include "mutex.h" -#include <memory> +#include <mutex> struct apr_dso_handle_t; /** @@ -185,20 +180,7 @@ private: static apr_file_t* open(const std::string& filename, apr_pool_t* apr_pool, apr_int32_t flags); static apr_status_t close(apr_file_t* file) ; static S32 seek(apr_file_t* file, apr_seek_where_t where, S32 offset); -public: - // returns false if failure: - static bool remove(const std::string& filename, LLVolatileAPRPool* pool = NULL); - static bool rename(const std::string& filename, const std::string& newname, LLVolatileAPRPool* pool = NULL); - static bool isExist(const std::string& filename, LLVolatileAPRPool* pool = NULL, apr_int32_t flags = APR_READ); - static S32 size(const std::string& filename, LLVolatileAPRPool* pool = NULL); - static bool makeDir(const std::string& dirname, LLVolatileAPRPool* pool = NULL); - static bool removeDir(const std::string& dirname, LLVolatileAPRPool* pool = NULL); - - // Returns bytes read/written, 0 if read/write fails: - static S32 readEx(const std::string& filename, void *buf, S32 offset, S32 nbytes, LLVolatileAPRPool* pool = NULL); - static S32 writeEx(const std::string& filename, const void *buf, S32 offset, S32 nbytes, LLVolatileAPRPool* pool = NULL); // offset<0 means append //******************************************************************************************************************************* }; - #endif // LL_LLAPR_H diff --git a/indra/llcommon/llatomic.cpp b/indra/llcommon/llatomic.cpp index d200bb0406..cff5e01225 100644 --- a/indra/llcommon/llatomic.cpp +++ b/indra/llcommon/llatomic.cpp @@ -23,6 +23,8 @@ * $/LicenseInfo$ */ +#include "linden_common.h" + #include "llatomic.h" //============================================================================ diff --git a/indra/llcommon/llbase32.cpp b/indra/llcommon/llbase32.cpp deleted file mode 100644 index 979114fe86..0000000000 --- a/indra/llcommon/llbase32.cpp +++ /dev/null @@ -1,238 +0,0 @@ -/** - * @file llbase32.cpp - * @brief base32 encoding that returns a std::string - * @author James Cook - * - * Based on code from bitter - * http://ghostwhitecrab.com/bitter/ - * - * Some parts of this file are: - * $LicenseInfo:firstyear=2007&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -/** - * Copyright (c) 2006 Christian Biere <christianbiere@gmx.de> - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the authors nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -#include "linden_common.h" - -#include "llbase32.h" - -#include <string> - -// bitter - base32.c starts here - -/* - * See RFC 3548 for details about Base 32 encoding: - * http://www.faqs.org/rfcs/rfc3548.html - */ - -static const char base32_alphabet[32] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', '2', '3', '4', '5', '6', '7' -}; - -size_t -base32_encode(char *dst, size_t size, const void *data, size_t len) -{ - size_t i = 0; - const U8 *p = (const U8*)data; - const char *end = &dst[size]; - char *q = dst; - - do { - size_t j, k; - U8 x[5]; - char s[8]; - - switch (len - i) { - case 4: k = 7; break; - case 3: k = 5; break; - case 2: k = 3; break; - case 1: k = 2; break; - default: - k = 8; - } - - for (j = 0; j < 5; j++) - x[j] = i < len ? p[i++] : 0; - -/* - +-------+-----------+--------+ - | target| source | source | - | byte | bits | byte | - +-------+-----------+--------+ - | 0 | 7 6 5 4 3 | 0 | - | 1 | 2 1 0 7 6 | 0-1 | - | 2 | 5 4 3 2 1 | 1 | - | 3 | 0 7 6 5 4 | 1-2 | - | 4 | 3 2 1 0 7 | 2-3 | - | 5 | 6 5 4 3 2 | 3 | - | 6 | 1 0 7 6 5 | 3-4 | - | 7 | 4 3 2 1 0 | 4 | - +-------+-----------+--------+ - -*/ - - s[0] = (x[0] >> 3); - s[1] = ((x[0] & 0x07) << 2) | (x[1] >> 6); - s[2] = (x[1] >> 1) & 0x1f; - s[3] = ((x[1] & 0x01) << 4) | (x[2] >> 4); - s[4] = ((x[2] & 0x0f) << 1) | (x[3] >> 7); - s[5] = (x[3] >> 2) & 0x1f; - s[6] = ((x[3] & 0x03) << 3) | (x[4] >> 5); - s[7] = x[4] & 0x1f; - - for (j = 0; j < k && q != end; j++) - *q++ = base32_alphabet[(U8) s[j]]; - - } while (i < len); - - return q - dst; -} - -/* *TODO: Implement base32 encode. - -static inline int -ascii_toupper(int c) -{ - return c >= 97 && c <= 122 ? c - 32 : c; -} - -static inline int -ascii_tolower(int c) -{ - return c >= 65 && c <= 90 ? c + 32 : c; -} - - -static char base32_map[(unsigned char) -1]; - -size_t -base32_decode(char *dst, size_t size, const void *data, size_t len) -{ - const char *end = &dst[size]; - const unsigned char *p = data; - char *q = dst; - size_t i; - unsigned max_pad = 3; - - if (0 == base32_map[0]) { - for (i = 0; i < LL_ARRAY_SIZE(base32_map); i++) { - const char *x; - - x = memchr(base32_alphabet, ascii_toupper(i), sizeof base32_alphabet); - base32_map[i] = x ? (x - base32_alphabet) : (unsigned char) -1; - } - } - - for (i = 0; i < len && max_pad > 0; i++) { - unsigned char c; - char s[8]; - size_t j; - - c = p[i]; - if ('=' == c) { - max_pad--; - c = 0; - } else { - c = base32_map[c]; - if ((unsigned char) -1 == c) { - return -1; - } - } - - j = i % LL_ARRAY_SIZE(s); - s[j] = c; - - if (7 == j) { - char b[5]; - - b[0] = ((s[0] << 3) & 0xf8) | ((s[1] >> 2) & 0x07); - b[1] = ((s[1] & 0x03) << 6) | ((s[2] & 0x1f) << 1) | ((s[3] >> 4) & 1); - b[2] = ((s[3] & 0x0f) << 4) | ((s[4] >> 1) & 0x0f); - b[3] = ((s[4] & 1) << 7) | ((s[5] & 0x1f) << 2) | ((s[6] >> 3) & 0x03); - b[4] = ((s[6] & 0x07) << 5) | (s[7] & 0x1f); - - for (j = 0; j < LL_ARRAY_SIZE(b); j++) { - if (q != end) - *q = b[j]; - q++; - } - } - } - - return q - dst; -} -*/ - - -// The following is -// Copyright (c) 2007-$CurrentYear$, Linden Research, Inc. -// static -std::string LLBase32::encode(const U8* input, size_t input_size) -{ - std::string output; - if (input) - { - // Each 5 byte chunk of input is represented by an - // 8 byte chunk of output. - size_t input_chunks = (input_size + 4) / 5; - size_t output_size = input_chunks * 8; - - output.resize(output_size); - - size_t encoded = base32_encode(&output[0], output_size, input, input_size); - - LL_INFOS() << "encoded " << encoded << " into buffer of size " - << output_size << LL_ENDL; - } - return output; -} diff --git a/indra/llcommon/llbase32.h b/indra/llcommon/llbase32.h deleted file mode 100644 index 8cfbd7ef53..0000000000 --- a/indra/llcommon/llbase32.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file llbase32.h - * @brief base32 encoding that returns a std::string - * @author James Cook - * - * $LicenseInfo:firstyear=2007&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#ifndef LLBASE32_H -#define LLBASE32_H - -class LL_COMMON_API LLBase32 -{ -public: - static std::string encode(const U8* input, size_t input_size); -}; - -#endif diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index 3d5d30bd90..8fb7823f04 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -24,6 +24,8 @@ * $/LicenseInfo$ */ +#include "linden_common.h" + #include "llcallbacklist.h" #include "lleventtimer.h" #include "llerrorlegacy.h" diff --git a/indra/llcommon/llcommon.cpp b/indra/llcommon/llcommon.cpp index 7a22eaf203..bf8801c46b 100644 --- a/indra/llcommon/llcommon.cpp +++ b/indra/llcommon/llcommon.cpp @@ -47,30 +47,13 @@ void* ll_tracy_new(size_t size) return ptr; } -void* ll_tracy_aligned_new(size_t size, size_t alignment) -{ - void* ptr = ll_aligned_malloc_fallback(size, alignment); - if (!ptr) - { - throw std::bad_alloc(); - } - LL_PROFILE_ALLOC(ptr, size); - return ptr; -} - void ll_tracy_delete(void* ptr) { LL_PROFILE_FREE(ptr); (free)(ptr); } -void ll_tracy_aligned_delete(void* ptr) -{ - LL_PROFILE_FREE(ptr); - ll_aligned_free_fallback(ptr); -} - -void* operator new(size_t size) +void* operator new(std::size_t size) { return ll_tracy_new(size); } @@ -80,24 +63,41 @@ void* operator new[](std::size_t count) return ll_tracy_new(count); } -void* operator new(size_t size, std::align_val_t align) +void operator delete(void *ptr) noexcept { - return ll_tracy_aligned_new(size, (size_t)align); + ll_tracy_delete(ptr); } -void* operator new[](std::size_t count, std::align_val_t align) +void operator delete[](void* ptr) noexcept { - return ll_tracy_aligned_new(count, (size_t)align); + ll_tracy_delete(ptr); } -void operator delete(void *ptr) noexcept +#if !defined(LL_LINUX) + +void* ll_tracy_aligned_new(size_t size, size_t alignment) { - ll_tracy_delete(ptr); + void* ptr = ll_aligned_malloc_fallback(size, alignment); + if (!ptr) + { + throw std::bad_alloc(); + } + return ptr; } -void operator delete[](void* ptr) noexcept +void ll_tracy_aligned_delete(void* ptr) { - ll_tracy_delete(ptr); + ll_aligned_free_fallback(ptr); +} + +void* operator new(size_t size, std::align_val_t align) +{ + return ll_tracy_aligned_new(size, (size_t)align); +} + +void* operator new[](std::size_t count, std::align_val_t align) +{ + return ll_tracy_aligned_new(count, (size_t)align); } void operator delete(void *ptr, std::align_val_t align) noexcept @@ -110,6 +110,8 @@ void operator delete[](void* ptr, std::align_val_t align) noexcept ll_tracy_aligned_delete(ptr); } +#endif + #endif // TRACY_ENABLE && !LL_PROFILER_ENABLE_TRACY_OPENGL //static diff --git a/indra/llcommon/llcond.h b/indra/llcommon/llcond.h index 2df1719941..b72ea33dea 100644 --- a/indra/llcommon/llcond.h +++ b/indra/llcommon/llcond.h @@ -17,7 +17,7 @@ #include "llunits.h" #include "llcoros.h" #include LLCOROS_MUTEX_HEADER -#include "mutex.h" +#include <mutex> #include <chrono> /** diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index 9e95d9c85f..ae7c50e9a3 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -252,6 +252,7 @@ void LLCoros::setStackSize(S32 stacksize) void LLCoros::printActiveCoroutines(const std::string& when) { + LL_PROFILE_ZONE_SCOPED; LL_INFOS("LLCoros") << "Number of active coroutines " << when << ": " << CoroData::instanceCount() << LL_ENDL; if (CoroData::instanceCount() > 0) diff --git a/indra/llcommon/llcoros.h b/indra/llcommon/llcoros.h index 9df52b6ed5..602c65b9ba 100644 --- a/indra/llcommon/llcoros.h +++ b/indra/llcommon/llcoros.h @@ -34,10 +34,10 @@ #include <boost/fiber/future/future.hpp> #include <boost/fiber/future/promise.hpp> #include <boost/fiber/recursive_mutex.hpp> -#include "mutex.h" #include "llsingleton.h" #include "llinstancetracker.h" #include <functional> +#include <mutex> #include <string> #include <exception> #include <queue> diff --git a/indra/llcommon/llcrc.cpp b/indra/llcommon/llcrc.cpp index d79d06e2a2..33bf01a29b 100644 --- a/indra/llcommon/llcrc.cpp +++ b/indra/llcommon/llcrc.cpp @@ -166,7 +166,7 @@ void LLCRC::update(const std::string& filename) return; } - FILE* fp = LLFile::fopen(filename, "rb"); /* Flawfinder: ignore */ + FILE* fp = LLFile::fopen(filename, LLFILE_MODE("rb")); /* Flawfinder: ignore */ if (fp) { diff --git a/indra/llcommon/llcrc.h b/indra/llcommon/llcrc.h index a3bde47780..2ea5fbc3b0 100644 --- a/indra/llcommon/llcrc.h +++ b/indra/llcommon/llcrc.h @@ -36,7 +36,7 @@ // example (don't try this at work kids): // // LLCRC crc; -// FILE* fp = LLFile::fopen(filename,"rb"); +// FILE* fp = LLFile::fopen(filename,LLFILE_MODE("rb")); // while(!feof(fp)) { // crc.update(fgetc(fp)); // } diff --git a/indra/llcommon/lldate.cpp b/indra/llcommon/lldate.cpp index 5545bb71af..7f65c0d715 100644 --- a/indra/llcommon/lldate.cpp +++ b/indra/llcommon/lldate.cpp @@ -134,11 +134,7 @@ void LLDate::toStream(std::ostream& s) const } s << std::dec << std::setfill('0'); -#if( LL_WINDOWS || __GNUC__ > 2) s << std::right; -#else - s.setf(ios::right); -#endif s << std::setw(4) << (exp_time.tm_year + 1900) << '-' << std::setw(2) << (exp_time.tm_mon + 1) << '-' << std::setw(2) << (exp_time.tm_mday) diff --git a/indra/llcommon/lldeadmantimer.cpp b/indra/llcommon/lldeadmantimer.cpp index f9c14d7c24..45c01eb84a 100644 --- a/indra/llcommon/lldeadmantimer.cpp +++ b/indra/llcommon/lldeadmantimer.cpp @@ -25,6 +25,7 @@ * $/LicenseInfo$ */ +#include "linden_common.h" #include "lldeadmantimer.h" diff --git a/indra/llcommon/lldefs.h b/indra/llcommon/lldefs.h index 232987da14..0481e45351 100644 --- a/indra/llcommon/lldefs.h +++ b/indra/llcommon/lldefs.h @@ -28,6 +28,7 @@ #define LL_LLDEFS_H #include "stdtypes.h" +#include <cassert> #include <type_traits> // Often used array indices @@ -169,6 +170,38 @@ constexpr U32 MAXADDRSTR = 17; // 123.567.901.345 = 15 chars + \0 + // llclampb(a) // clamps a to [0 .. 255] // +// llless(d0, d1) safely compares d0 < d1 even if one is signed and the other +// is unsigned. A simple (d0 < d1) expression converts the signed operand to +// unsigned before comparing. If the signed operand is negative, that flips +// the negative value to a huge positive value, producing the wrong answer! +// llless() specifically addresses that case. +template <typename T0, typename T1> +constexpr bool llless(T0 d0, T1 d1) +{ + if constexpr (std::is_signed_v<T0> && ! std::is_signed_v<T1>) + { + // T0 signed, T1 unsigned: negative d0 is less than any unsigned d1 + if (d0 < 0) + return true; + // both are non-negative: explicitly cast to avoid C4018 + return std::make_unsigned_t<T0>(d0) < d1; + } + else if constexpr (! std::is_signed_v<T0> && std::is_signed_v<T1>) + { + // T0 unsigned, T1 signed: any unsigned d0 is greater than negative d1 + if (d1 < 0) + return false; + // both are non-negative: explicitly cast to avoid C4018 + return d0 < std::make_unsigned_t<T1>(d1); + } + else + { + // both T0 and T1 are signed, or both are unsigned: + // straightforward comparison works + return d0 < d1; + } +} + // recursion tail template <typename T> constexpr auto llmax(T data) @@ -180,7 +213,7 @@ template <typename T0, typename T1, typename... Ts> constexpr auto llmax(T0 d0, T1 d1, Ts... rest) { auto maxrest = llmax(d1, rest...); - return (d0 > maxrest)? d0 : maxrest; + return llless(maxrest, d0)? d0 : maxrest; } // recursion tail @@ -194,12 +227,28 @@ template <typename T0, typename T1, typename... Ts> constexpr auto llmin(T0 d0, T1 d1, Ts... rest) { auto minrest = llmin(d1, rest...); - return (d0 < minrest) ? d0 : minrest; + return llless(d0, minrest) ? d0 : minrest; } template <typename A, typename MIN, typename MAX> constexpr A llclamp(A a, MIN minval, MAX maxval) { + // The only troublesome case is if A is unsigned and either minval or + // maxval is both signed and negative. Casting a negative number to + // unsigned flips it to a huge positive number, making this llclamp() call + // ineffective. + if constexpr (! std::is_signed_v<A>) + { + if constexpr (std::is_signed_v<MIN>) + { + assert(minval >= 0); + } + if constexpr (std::is_signed_v<MAX>) + { + assert(maxval >= 0); + } + } + A aminval{ static_cast<A>(minval) }, amaxval{ static_cast<A>(maxval) }; if ( a < aminval ) { @@ -225,7 +274,7 @@ constexpr LLDATATYPE llclampb(LLDATATYPE a) } template <class LLDATATYPE> -inline void llswap(LLDATATYPE& lhs, LLDATATYPE& rhs) +constexpr void llswap(LLDATATYPE& lhs, LLDATATYPE& rhs) { std::swap(lhs, rhs); } diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp index 6f5e57c3de..78101e21af 100644 --- a/indra/llcommon/llerror.cpp +++ b/indra/llcommon/llerror.cpp @@ -133,6 +133,7 @@ namespace { RecordToFile(const std::string& filename): mName(filename) { + showMultiline(true); mFile.open(filename.c_str(), std::ios_base::out | std::ios_base::app); if (!mFile) { @@ -344,6 +345,51 @@ namespace { } }; #endif + +#if LL_PROFILER_CONFIGURATION >= LL_PROFILER_CONFIG_TRACY + class RecordToTracy : public LLError::Recorder + { + public: + RecordToTracy() + { + this->showMultiline(true); + this->showTags(false); + this->showLocation(false); + } + + virtual bool enabled() override { return LLError::getEnabledLogTypesMask() & 0x12; } + + virtual void recordMessage(LLError::ELevel level, const std::string& message) override + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_LOGGING; + switch (level) + { + case LLError::LEVEL_DEBUG: + { + TracyMessageC(message.c_str(), message.size(), tracy::Color::Turquoise); + break; + } + default: + case LLError::LEVEL_NONE: + case LLError::LEVEL_INFO: + { + TracyMessageC(message.c_str(), message.size(), tracy::Color::White); + break; + } + case LLError::LEVEL_WARN: + { + TracyMessageC(message.c_str(), message.size(), tracy::Color::Yellow); + break; + } + case LLError::LEVEL_ERROR: + { + TracyMessageC(message.c_str(), message.size(), tracy::Color::Red); + break; + } + } + } + }; +#endif } @@ -435,13 +481,9 @@ namespace std::string file = user_dir + "/logcontrol-dev.xml"; - llstat stat_info; - if (LLFile::stat(file, &stat_info)) { - // NB: stat returns non-zero if it can't read the file, for example - // if it doesn't exist. LLFile has no better abstraction for - // testing for file existence. - - file = app_dir + "/logcontrol.xml"; + if (!LLFile::isfile(file)) + { + file = app_dir + "/logcontrol.xml"; } return * new LogControlFile(file); // NB: This instance is never freed @@ -759,6 +801,11 @@ namespace LLError::addRecorder(recordToWinDebug); #endif +#if LL_PROFILER_CONFIGURATION >= LL_PROFILER_CONFIG_TRACY + LLError::RecorderPtr recordToTracy(new RecordToTracy()); + LLError::addRecorder(recordToTracy); +#endif + LogControlFile& e = LogControlFile::fromDirectory(user_dir, app_dir); // NOTE: We want to explicitly load the file before we add it to the event timer diff --git a/indra/llcommon/llerror.h b/indra/llcommon/llerror.h index 41893a35e5..a06c299c8d 100644 --- a/indra/llcommon/llerror.h +++ b/indra/llcommon/llerror.h @@ -91,13 +91,8 @@ const int LL_ERR_NOERR = 0; #define llverify(func) do {if (func) {}} while(0) #endif -#ifdef LL_WINDOWS #define LL_STATIC_ASSERT(func, msg) static_assert(func, msg) -#define LL_BAD_TEMPLATE_INSTANTIATION(type, msg) static_assert(false, msg) -#else -#define LL_STATIC_ASSERT(func, msg) BOOST_STATIC_ASSERT(func) -#define LL_BAD_TEMPLATE_INSTANTIATION(type, msg) BOOST_STATIC_ASSERT(sizeof(type) != 0 && false); -#endif +#define LL_BAD_TEMPLATE_INSTANTIATION(type, msg) static_assert(sizeof(type) != 0 && false, msg) /** Error Logging Facility @@ -314,6 +309,7 @@ namespace LLError ERROR_OTHER = 0, ERROR_BAD_ALLOC = 1, ERROR_MISSING_FILES = 2, + ERROR_INIT_FAILED = 3, } eLastExecEvent; // tittle, message and error code to include in error marker file diff --git a/indra/llcommon/llfasttimer.cpp b/indra/llcommon/llfasttimer.cpp index be0cab36e3..6784570a1c 100644 --- a/indra/llcommon/llfasttimer.cpp +++ b/indra/llcommon/llfasttimer.cpp @@ -387,6 +387,11 @@ void BlockTimer::logStats() } +#if defined(LL_GNUC) && GCC_VERSION >= 130000 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wnonnull" +#endif + //static void BlockTimer::dumpCurTimes() { @@ -418,8 +423,12 @@ void BlockTimer::dumpCurTimes() << num_calls << " calls"; LL_INFOS() << out_str.str() << LL_ENDL; + } } -} + +#if defined(LL_GNUC) && GCC_VERSION >= 130000 +# pragma GCC diagnostic push +#endif //static void BlockTimer::writeLog(std::ostream& os) diff --git a/indra/llcommon/llfile.cpp b/indra/llcommon/llfile.cpp index f752e31563..114df29f6e 100644 --- a/indra/llcommon/llfile.cpp +++ b/indra/llcommon/llfile.cpp @@ -29,22 +29,17 @@ #include "linden_common.h" #include "llfile.h" -#include "llstring.h" #include "llerror.h" #include "stringize.h" #if LL_WINDOWS -#include "llwin32headers.h" -#include <vector> +#include <fcntl.h> #else #include <errno.h> +#include <sys/file.h> #endif -using namespace std; - -static std::string empty; - -// Many of the methods below use OS-level functions that mess with errno. Wrap +// Some of the methods below use OS-level functions that mess with errno. Wrap // variants of strerror() to report errors. #if LL_WINDOWS @@ -79,6 +74,7 @@ static errentry const errtable[] { ERROR_CURRENT_DIRECTORY, EACCES }, // 16 { ERROR_NOT_SAME_DEVICE, EXDEV }, // 17 { ERROR_NO_MORE_FILES, ENOENT }, // 18 + { ERROR_SHARING_VIOLATION, EACCES }, // 32 { ERROR_LOCK_VIOLATION, EACCES }, // 33 { ERROR_BAD_NETPATH, ENOENT }, // 53 { ERROR_NETWORK_ACCESS_DENIED, EACCES }, // 65 @@ -109,22 +105,25 @@ static errentry const errtable[] { ERROR_NOT_ENOUGH_QUOTA, ENOMEM } // 1816 }; -static int set_errno_from_oserror(unsigned long oserr) +static int get_errno_from_oserror(int oserr) { if (!oserr) return 0; // Check the table for the Windows OS error code - for (const struct errentry &entry : errtable) + for (const struct errentry& entry : errtable) { if (oserr == entry.oserr) { - _set_errno(entry.errcode); - return -1; + return entry.errcode; } } + return EINVAL; +} - _set_errno(EINVAL); +static int set_errno_from_oserror(unsigned long oserr) +{ + _set_errno(get_errno_from_oserror(oserr)); return -1; } @@ -136,90 +135,8 @@ std::string strerr(int errn) return buffer; } -inline bool is_slash(wchar_t const c) -{ - return c == L'\\' || c == L'/'; -} - -static std::wstring utf8path_to_wstring(const std::string& utf8path) -{ - if (utf8path.size() >= MAX_PATH) - { - // By prepending "\\?\" to a path, Windows widechar file APIs will not fail on long path names - std::wstring utf16path = L"\\\\?\\" + ll_convert<std::wstring>(utf8path); - // We need to make sure that the path does not contain forward slashes as above - // prefix does bypass the path normalization that replaces slashes with backslashes - // before passing the path to kernel mode APIs - std::replace(utf16path.begin(), utf16path.end(), L'/', L'\\'); - return utf16path; - } - return ll_convert<std::wstring>(utf8path); -} - -static unsigned short get_fileattr(const std::wstring& utf16path, bool dontFollowSymLink = false) -{ - unsigned long flags = FILE_FLAG_BACKUP_SEMANTICS; - if (dontFollowSymLink) - { - flags |= FILE_FLAG_OPEN_REPARSE_POINT; - } - HANDLE file_handle = CreateFileW(utf16path.c_str(), FILE_READ_ATTRIBUTES, - FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, flags, nullptr); - if (file_handle != INVALID_HANDLE_VALUE) - { - FILE_ATTRIBUTE_TAG_INFO attribute_info; - if (GetFileInformationByHandleEx(file_handle, FileAttributeTagInfo, &attribute_info, sizeof(attribute_info))) - { - // A volume path alone (only drive letter) is not recognized as directory while it technically is - bool is_directory = (attribute_info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) || - (iswalpha(utf16path[0]) && utf16path[1] == ':' && - (!utf16path[2] || (is_slash(utf16path[2]) && !utf16path[3]))); - unsigned short st_mode = is_directory ? S_IFDIR : - (attribute_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT ? S_IFLNK : S_IFREG); - st_mode |= (attribute_info.FileAttributes & FILE_ATTRIBUTE_READONLY) ? S_IREAD : S_IREAD | S_IWRITE; - // we do not try to guess executable flag - - // propagate user bits to group/other fields: - st_mode |= (st_mode & 0700) >> 3; - st_mode |= (st_mode & 0700) >> 6; - - CloseHandle(file_handle); - return st_mode; - } - // Retrieve last error before calling CloseHandle() - DWORD last_error = GetLastError(); - CloseHandle(file_handle); - set_errno_from_oserror(last_error); - } - else - { - set_errno_from_oserror(GetLastError()); - } - - // If CreateFileW approach failed (e.g., exFAT), try the simpler GetFileAttributesW() - // GetFileAttributesW() always follows symlinks, so we skip this fallback when dontFollowSymLink is true. - if (!dontFollowSymLink) - { - DWORD attributes = GetFileAttributesW(utf16path.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES) - { - bool is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; - unsigned short st_mode = is_directory ? S_IFDIR : S_IFREG; - st_mode |= (attributes & FILE_ATTRIBUTE_READONLY) ? S_IREAD : S_IREAD | S_IWRITE; - - // propagate user bits to group/other fields: - st_mode |= (st_mode & 0700) >> 3; - st_mode |= (st_mode & 0700) >> 6; - - return st_mode; - } - set_errno_from_oserror(GetLastError()); - } - return 0; -} - #else + // On Posix we want to call strerror_r(), but alarmingly, there are two // different variants. The one that returns int always populates the passed // buffer (except in case of error), whereas the other one always returns a @@ -266,9 +183,50 @@ std::string strerr(int errn) return message_from(errn, buffer, sizeof(buffer), strerror_r(errn, buffer, sizeof(buffer))); } + #endif // ! LL_WINDOWS -static int warnif(const std::string& desc, const std::string& filename, int rc, int accept = 0) +#if LL_WINDOWS && 0 // turn on to debug file-locking problems +#define PROCESS_LOCKING_CHECK 1 +static void find_locking_process(const std::string& filename) +{ + // Only do any of this stuff (before LL_ENDL) if it will be logged. + LL_DEBUGS("LLFile") << ""; + // wrong way + std::string TEMP = LLFile::tmpdir(); + if (TEMP.empty()) + { + LL_CONT << "No $TEMP, not running 'handle'"; + } + else + { + std::string tf(TEMP); + tf += "handle.tmp"; + // http://technet.microsoft.com/en-us/sysinternals/bb896655 + std::string cmd(STRINGIZE("handle \"" << filename + // "openfiles /query /v | fgrep -i \"" << filename + << "\" > \"" << tf << '"')); + LL_CONT << cmd; + if (system(cmd.c_str()) != 0) + { + LL_CONT << "\nDownload 'handle.exe' from http://technet.microsoft.com/en-us/sysinternals/bb896655"; + } + else + { + std::ifstream inf(tf); + std::string line; + while (std::getline(inf, line)) + { + LL_CONT << '\n' << line; + } + } + LLFile::remove(tf); + } + LL_CONT << LL_ENDL; +} +#endif // LL_WINDOWS hack to identify processes holding file open + +static int warnif(std::string_view desc, const std::filesystem::path& filename, int rc, int suppress_warning = 0) { if (rc < 0) { @@ -277,322 +235,840 @@ static int warnif(const std::string& desc, const std::string& filename, int rc, // For certain operations, a particular errno value might be // acceptable -- e.g. stat() could permit ENOENT, mkdir() could permit - // EEXIST. Don't warn if caller explicitly says this errno is okay. - if (errn != accept) + // EEXIST. Don't log a warning if caller explicitly says this errno is okay. + if (errn != suppress_warning) { - LL_WARNS("LLFile") << "Couldn't " << desc << " '" << filename - << "' (errno " << errn << "): " << strerr(errn) << LL_ENDL; + LL_WARNS("LLFile") << "Couldn't " << desc << " '" << filename << "' (errno " << errn << "): " << strerr(errn) << LL_ENDL; } -#if 0 && LL_WINDOWS // turn on to debug file-locking problems +#if PROCESS_LOCKING_CHECK // If the problem is "Permission denied," maybe it's because another // process has the file open. Try to find out. - if (errn == EACCES) // *not* EPERM + if (errn == EACCES) // *not* EPERM { - // Only do any of this stuff (before LL_ENDL) if it will be logged. - LL_DEBUGS("LLFile") << empty; - // would be nice to use LLDir for this, but dependency goes the - // wrong way - const char* TEMP = LLFile::tmpdir(); - if (! (TEMP && *TEMP)) - { - LL_CONT << "No $TEMP, not running 'handle'"; - } - else - { - std::string tf(TEMP); - tf += "\\handle.tmp"; - // http://technet.microsoft.com/en-us/sysinternals/bb896655 - std::string cmd(STRINGIZE("handle \"" << filename - // "openfiles /query /v | fgrep -i \"" << filename - << "\" > \"" << tf << '"')); - LL_CONT << cmd; - if (system(cmd.c_str()) != 0) - { - LL_CONT << "\nDownload 'handle.exe' from http://technet.microsoft.com/en-us/sysinternals/bb896655"; - } - else - { - std::ifstream inf(tf); - std::string line; - while (std::getline(inf, line)) - { - LL_CONT << '\n' << line; - } - } - LLFile::remove(tf); - } - LL_CONT << LL_ENDL; + find_locking_process(filename); } -#endif // LL_WINDOWS hack to identify processes holding file open +#endif } return rc; } -// static -int LLFile::mkdir(const std::string& dirname, int perms) +static int warnif(std::string_view desc, const std::filesystem::path& filename, const std::error_code& ec, int suppress_warning = 0) { - // We often use mkdir() to ensure the existence of a directory that might - // already exist. There is no known case in which we want to call out as - // an error the requested directory already existing. + if (ec) + { + // get Posix errno from the std::error_code so we can compare it to the suppress_warning parameter + // to see when a caller wants us to not generate a warning for a particular error code +#if LL_WINDOWS + int errn = get_errno_from_oserror(ec.value()); +#else + int errn = ec.value(); +#endif + // For certain operations, a particular errno value might be acceptable + // Don't warn if caller explicitly says this errno is okay. + if (errn != suppress_warning) + { + LL_WARNS("LLFile") << "Couldn't " << desc << " '" << filename << "' (errno " << errn << "): " << ec.message() << LL_ENDL; + } +#if PROCESS_LOCKING_CHECK + // Try to detect locked files by other processes + if (ec.value() == ERROR_SHARING_VIOLATION || ec.value() == ERROR_LOCK_VIOLATION) + { + find_locking_process(filename); + } +#endif + return -1; + } + return 0; +} + #if LL_WINDOWS - // permissions are ignored on Windows - int rc = 0; - std::wstring utf16dirname = utf8path_to_wstring(dirname); - if (!CreateDirectoryW(utf16dirname.c_str(), nullptr)) + +inline int set_ec_from_system_error(std::error_code& ec, DWORD error) +{ + ec.assign(error, std::system_category()); + return -1; +} + +static int set_ec_from_system_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, GetLastError()); +} + +inline int set_ec_to_parameter_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, ERROR_INVALID_PARAMETER); +} + +inline int set_ec_to_outofmemory_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, ERROR_NOT_ENOUGH_MEMORY); +} + +inline DWORD decode_access_mode(std::ios_base::openmode omode) +{ + switch (omode & (LLFile::in | LLFile::out)) + { + case LLFile::in: + return GENERIC_READ; + case LLFile::out: + return GENERIC_WRITE; + case static_cast<std::ios_base::openmode>(LLFile::in | LLFile::out): + return GENERIC_READ | GENERIC_WRITE; + } + if (omode & LLFile::app) + { + return GENERIC_WRITE; + } + return 0; +} + +inline DWORD decode_open_create_flags(std::ios_base::openmode omode) +{ + if (omode & LLFile::noreplace) + { + return CREATE_NEW; // create if it does not exist, otherwise fail + } + if (omode & LLFile::trunc) { - // Only treat other errors than an already existing file as a real error - unsigned long oserr = GetLastError(); - if (oserr != ERROR_ALREADY_EXISTS) + if (!(omode & LLFile::out)) { - rc = set_errno_from_oserror(oserr); + return TRUNCATE_EXISTING; // open and truncate if it exists, otherwise fail } + return CREATE_ALWAYS; // open and truncate if it exists, otherwise create it + } + if (!(omode & LLFile::out)) + { + return OPEN_EXISTING; // open if it exists, otherwise fail + } + // LLFile::app or (LLFile::out and (!LLFile::trunc or !LLFile::noreplace)) + return OPEN_ALWAYS; // open if it exists, otherwise create it +} + +inline DWORD decode_share_mode(int omode) +{ + if (omode & LLFile::exclusive) + { + return 0; // allow no other access + } + if (omode & LLFile::shared) + { + return FILE_SHARE_READ; // allow read access + } + return FILE_SHARE_READ | FILE_SHARE_WRITE; // allow read and write access to others +} + +inline DWORD decode_attributes(std::ios_base::openmode omode, int perm) +{ + return (perm & S_IWRITE) ? FILE_ATTRIBUTE_NORMAL : FILE_ATTRIBUTE_READONLY; +} + +// Under Windows the values for the std::ios_base::seekdir constants match the according FILE_BEGIN +// and other constants but we do a programmatic translation for now to be sure +static DWORD seek_mode_from_dir(std::ios_base::seekdir seekdir) +{ + switch (seekdir) + { + case LLFile::beg: + return FILE_BEGIN; + case LLFile::cur: + return FILE_CURRENT; + case LLFile::end: + return FILE_END; } + return FILE_BEGIN; +} + #else - int rc = ::mkdir(dirname.c_str(), (mode_t)perms); - if (rc < 0 && errno == EEXIST) + +inline int set_ec_from_system_error(std::error_code& ec, int error) +{ + ec.assign(error, std::system_category()); + return -1; +} + +static int set_ec_from_system_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, errno); +} + +inline int set_ec_to_parameter_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, EINVAL); +} + +inline int set_ec_to_outofmemory_error(std::error_code& ec) +{ + return set_ec_from_system_error(ec, ENOMEM); +} + +inline int decode_access_mode(std::ios_base::openmode omode) +{ + if (omode & LLFile::out) { - // this is not the error you want, move along - return 0; + if (omode & LLFile::in) + { + return O_RDWR; + } + return O_WRONLY; + } + return O_RDONLY; + + /*switch (omode & (LLFile::in | LLFile::out)) + { + case LLFile::out: + return O_WRONLY; + case static_cast<std::ios_base::openmode>(LLFile::in | LLFile::out): + return O_RDWR; + } + return O_RDONLY;*/ +} + +inline int decode_open_mode(std::ios_base::openmode omode) +{ + int flags = O_CREAT | decode_access_mode(omode); + if (omode & LLFile::app) + { + flags |= O_APPEND; } + if (omode & LLFile::trunc) + { + flags |= O_TRUNC; + } + if (omode & LLFile::binary) + { + // Not a thing under *nix + } + if (omode & LLFile::noreplace) + { + flags |= O_EXCL; + } + return flags; +} + +inline int decode_lock_mode(std::ios_base::openmode omode) +{ + int lmode = omode & LLFile::noblock ? LOCK_NB : 0; + if (omode & LLFile::lock_mask) + { + if (omode & LLFile::exclusive) + { + return lmode | LOCK_EX; + } + return lmode | LOCK_SH; + } + return lmode | LOCK_UN; +} + +// Under Linux and Mac the values for the std::ios_base::seekdir constants match the according SEEK_SET +// and other constants but we do a programmatic translation for now to be sure +inline int seek_mode_from_dir(std::ios_base::seekdir seekdir) +{ + switch (seekdir) + { + case LLFile::beg: + return SEEK_SET; + case LLFile::cur: + return SEEK_CUR; + case LLFile::end: + return SEEK_END; + } + return SEEK_SET; +} + #endif - // anything else might be a problem - return warnif("mkdir", dirname, rc); + +inline int clear_error(std::error_code& ec) +{ + ec.clear(); + return 0; } -// static -int LLFile::rmdir(const std::string& dirname, int suppress_error) +inline bool are_open_mode_flags_invalid(std::ios_base::openmode omode) +{ + // at least one of input or output needs to be specified + if (!(omode & (LLFile::in | LLFile::out))) + { + return true; + } + // output must be possible for any of the extra options + if (!(omode & LLFile::out) && (omode & (LLFile::trunc | LLFile::app | LLFile::noreplace))) + { + return true; + } + // invalid combination, mutually exclusive + if ((omode & LLFile::app) && (omode & (LLFile::trunc | LLFile::noreplace))) + { + return true; + } + return false; +} + +//---------------------------------------------------------------------------------------- +// class member functions +//---------------------------------------------------------------------------------------- +int LLFile::open(const std::filesystem::path& file_path, std::ios_base::openmode omode, std::error_code& ec, int perm) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + close(ec); + if (are_open_mode_flags_invalid(omode)) + { + return set_ec_to_parameter_error(ec); + } #if LL_WINDOWS - std::wstring utf16dirname = utf8path_to_wstring(dirname); - int rc = _wrmdir(utf16dirname.c_str()); + DWORD access = decode_access_mode(omode), + share = decode_share_mode(omode), + create = decode_open_create_flags(omode), + attributes = decode_attributes(omode, perm); + + mHandle = CreateFileW(file_path.native().c_str(), access, share, nullptr, create, attributes, nullptr); + // The dwShareMode = share parameter takes care of locking the file for other processes if indicated, + // no need to do anything else for file locking here #else - int rc = ::rmdir(dirname.c_str()); + int oflags = decode_open_mode(omode); + int lmode = omode & LLFile::lock_mask; + mHandle = ::open(file_path.native().c_str(), oflags, perm); + if (mHandle != InvalidHandle && lmode && lock(lmode | LLFile::noblock, ec) != 0) + { + close(); + return -1; + } #endif - return warnif("rmdir", dirname, rc, suppress_error); + if (mHandle == InvalidHandle) + { + return set_ec_from_system_error(ec); + } + + if (omode & LLFile::ate && seek(0, LLFile::end, ec) != 0) + { + close(); + return -1; + } + mOpen = omode; + return clear_error(ec); } -// static -LLFILE* LLFile::fopen(const std::string& filename, const char* mode) +S64 LLFile::size(std::error_code& ec) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; #if LL_WINDOWS - std::wstring utf16filename = utf8path_to_wstring(filename); - std::wstring utf16mode = ll_convert<std::wstring>(std::string(mode)); - return _wfopen(utf16filename.c_str(), utf16mode.c_str()); + LARGE_INTEGER value = { 0 }; + if (GetFileSizeEx(mHandle, &value)) + { + clear_error(ec); + return value.QuadPart; + } #else - return ::fopen(filename.c_str(),mode); + struct stat statval; + if (fstat(mHandle, &statval) == 0) + { + clear_error(ec); + return statval.st_size; + } #endif + set_ec_from_system_error(ec); + return 0; } -// static -int LLFile::close(LLFILE * file) +S64 LLFile::tell(std::error_code& ec) { - int ret_value = 0; - if (file) + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; +#if LL_WINDOWS + LARGE_INTEGER value = { 0 }; + if (SetFilePointerEx(mHandle, value, &value, FILE_CURRENT)) { - ret_value = fclose(file); + clear_error(ec); + return value.QuadPart; } - return ret_value; +#else + off_t offset = lseek(mHandle, 0, SEEK_CUR); + if (offset != -1) + { + clear_error(ec); + return offset; + } +#endif + return set_ec_from_system_error(ec); } -// static -std::string LLFile::getContents(const std::string& filename) +int LLFile::seek(S64 pos, std::error_code& ec) { - LLFILE* fp = LLFile::fopen(filename, "rb"); - if (fp) + return seek(pos, LLFile::beg, ec); +} + +int LLFile::seek(S64 offset, std::ios_base::seekdir dir, std::error_code& ec) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + S64 newOffset = 0; +#if LL_WINDOWS + DWORD seekdir = seek_mode_from_dir(dir); + LARGE_INTEGER value; + value.QuadPart = offset; + if (SetFilePointerEx(mHandle, value, (PLARGE_INTEGER)&newOffset, seekdir)) +#else + newOffset = lseek(mHandle, offset, seek_mode_from_dir(dir)); + if (newOffset != -1) +#endif { - fseek(fp, 0, SEEK_END); - U32 length = ftell(fp); - fseek(fp, 0, SEEK_SET); + return clear_error(ec); + } + return set_ec_from_system_error(ec); +} - std::vector<char> buffer(length); - size_t nread = fread(buffer.data(), 1, length, fp); - fclose(fp); +#if LL_WINDOWS +inline DWORD next_buffer_size(S64 nbytes) +{ + return nbytes > 0x80000000 ? 0x80000000 : (DWORD)nbytes; +} +#endif - return std::string(buffer.data(), nread); +S64 LLFile::read(void* buffer, S64 nbytes, std::error_code& ec) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + if (nbytes == 0) + { + // Nothing to do + return clear_error(ec); } + else if (!buffer || nbytes < 0) + { + return set_ec_to_parameter_error(ec); + } +#if LL_WINDOWS + S64 totalBytes = 0; + char *ptr = (char*)buffer; + DWORD bytesRead, bytesToRead = next_buffer_size(nbytes); - return LLStringUtil::null; + // Read in chunks to support >4GB which the S64 nbytes value makes possible + while (ReadFile(mHandle, ptr, bytesToRead, &bytesRead, nullptr)) + { + totalBytes += bytesRead; + if (nbytes <= totalBytes || // requested amount read + bytesRead < bytesToRead) // ReadFile encountered eof + { + clear_error(ec); + return totalBytes; + } + ptr += bytesRead; + bytesToRead = next_buffer_size(nbytes - totalBytes); + } +#else + ssize_t bytesRead = ::read(mHandle, buffer, nbytes); + if (bytesRead != -1) + { + clear_error(ec); + return bytesRead; + } +#endif + return set_ec_from_system_error(ec); } -// static -int LLFile::remove(const std::string& filename, int suppress_error) +S64 LLFile::write(const void* buffer, S64 nbytes, std::error_code& ec) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + if (nbytes == 0) + { + // Nothing to do here + return clear_error(ec); + } + else if (!buffer || nbytes < 0) + { + return set_ec_to_parameter_error(ec); + } #if LL_WINDOWS - // Posix remove() works on both files and directories although on Windows - // remove() and its wide char variant _wremove() only removes files just - // as its siblings unlink() and _wunlink(). - // If we really only want to support files we should instead use - // unlink() in the non-Windows part below too - int rc = -1; - std::wstring utf16filename = utf8path_to_wstring(filename); - unsigned short st_mode = get_fileattr(utf16filename); - if (S_ISDIR(st_mode)) + // If this was opened in append mode, we emulate it on Windows + if (mOpen & LLFile::app && seek(0, LLFile::end, ec) != 0) { - rc = _wrmdir(utf16filename.c_str()); + return -1; } - else if (S_ISREG(st_mode)) + + S64 totalBytes = 0; + char* ptr = (char*)buffer; + DWORD bytesWritten, bytesToWrite = next_buffer_size(nbytes); + + // Write in chunks to support >4GB which the S64 nbytes value makes possible + while (WriteFile(mHandle, ptr, bytesToWrite, &bytesWritten, nullptr)) { - rc = _wunlink(utf16filename.c_str()); + totalBytes += bytesWritten; + if (nbytes <= totalBytes) + { + clear_error(ec); + return totalBytes; + } + ptr += bytesWritten; + bytesToWrite = next_buffer_size(nbytes - totalBytes); + } +#else + ssize_t bytesWritten = ::write(mHandle, buffer, nbytes); + if (bytesWritten != -1) + { + clear_error(ec); + return bytesWritten; + } +#endif + return set_ec_from_system_error(ec); +} + +S64 LLFile::printf(const char* fmt, ...) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + va_list args1; + va_start(args1, fmt); + va_list args2; + va_copy(args2, args1); + int length = vsnprintf(nullptr, 0, fmt, args1); + va_end(args1); + if (length < 0) + { + va_end(args2); + return -1; } - else if (st_mode) + void* buffer = malloc(length + 1); + if (!buffer) { - // it is something else than a file or directory - // this should not really happen as long as we do not allow for symlink - // detection in the optional parameter to get_fileattr() - rc = set_errno_from_oserror(ERROR_INVALID_PARAMETER); + va_end(args2); + return -1; + } + length = vsnprintf((char*)buffer, length + 1, fmt, args2); + va_end(args2); + std::error_code ec; + S64 written = write(buffer, length, ec); + free(buffer); + return written; +} + +int LLFile::lock(int mode, std::error_code& ec) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; +#if LL_WINDOWS + if (!(mode & LLFile::lock_mask)) + { + if (UnlockFile(mHandle, 0, 0, MAXDWORD, MAXDWORD)) + { + return clear_error(ec); + } } else { - // get_fileattr() failed and already set errno, preserve it for correct error reporting + OVERLAPPED overlapped = { 0 }; + DWORD flags = (mode & LLFile::noblock) ? LOCKFILE_FAIL_IMMEDIATELY : 0; + if (mode & LLFile::exclusive) + { + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + // We lock the maximum range, since flock only supports locking the entire file too + if (LockFileEx(mHandle, flags, 0, MAXDWORD, MAXDWORD, &overlapped)) + { + return clear_error(ec); + } } #else - int rc = ::remove(filename.c_str()); + if (flock(mHandle, decode_lock_mode(static_cast<std::ios_base::openmode>(mode))) == 0) + { + return clear_error(ec); + } #endif - return warnif("remove", filename, rc, suppress_error); + return set_ec_from_system_error(ec); +} + +int LLFile::close(std::error_code& ec) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + if (mHandle != InvalidHandle) + { + llfile_handle_t handle = InvalidHandle; + std::swap(handle, mHandle); +#if LL_WINDOWS + if (!CloseHandle(handle)) +#else + if (::close(handle)) +#endif + { + return set_ec_from_system_error(ec); + } + } + return clear_error(ec); +} + +int LLFile::close() +{ + std::error_code ec; + return close(ec); } +//---------------------------------------------------------------------------------------- +// static member functions +//---------------------------------------------------------------------------------------- + // static -int LLFile::rename(const std::string& filename, const std::string& newname, int suppress_error) +LLFILE* LLFile::fopen(const std::filesystem::path& file_path, const fopen_flags_t* mode, int lmode) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + LLFILE* file; #if LL_WINDOWS - // Posix rename() will gladly overwrite a file at newname if it exists, the Windows - // rename(), respectively _wrename(), will bark on that. Instead call directly the Windows - // API MoveFileEx() and use its flags to specify that overwrite is allowed. - std::wstring utf16filename = utf8path_to_wstring(filename); - std::wstring utf16newname = utf8path_to_wstring(newname); - int rc = 0; - if (!MoveFileExW(utf16filename.c_str(), utf16newname.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) + int shflag = _SH_DENYNO; + switch (lmode) { - rc = set_errno_from_oserror(GetLastError()); + case LLFile::exclusive: + shflag = _SH_DENYRW; + break; + case LLFile::shared: + shflag = _SH_DENYWR; + break; } + file = _wfsopen(file_path.native().c_str(), mode, shflag); #else - int rc = ::rename(filename.c_str(),newname.c_str()); + file = ::fopen(file_path.native().c_str(), mode); + if (file && (lmode & (LLFile::lock_mask))) + { + // Rather fail on a sharing conflict than block + if (flock(fileno(file), decode_lock_mode(static_cast<std::ios_base::openmode>(lmode | LLFile::noblock)))) + { + ::fclose(file); + file = nullptr; + } + } #endif - return warnif(STRINGIZE("rename to '" << newname << "' from"), filename, rc, suppress_error); + return file; } -// Make this a define rather than using magic numbers multiple times in the code -#define LLFILE_COPY_BUFFER_SIZE 16384 +// static +int LLFile::close(LLFILE* file) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + int ret_value = 0; + if (file) + { + ret_value = ::fclose(file); + } + return ret_value; +} // static -bool LLFile::copy(const std::string& from, const std::string& to) +std::string LLFile::getContents(const std::filesystem::path& file_path, std::error_code& ec) { - bool copied = false; - LLFILE* in = LLFile::fopen(from, "rb"); - if (in) + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::string buffer; + LLFile file(file_path, LLFile::in | LLFile::binary, ec); + if (file) { - LLFILE* out = LLFile::fopen(to, "wb"); - if (out) + S64 length = file.size(ec); + if (!ec && length > 0) { - char buf[LLFILE_COPY_BUFFER_SIZE]; - size_t readbytes; - bool write_ok = true; - while (write_ok && (readbytes = fread(buf, 1, LLFILE_COPY_BUFFER_SIZE, in))) - { - if (fwrite(buf, 1, readbytes, out) != readbytes) - { - LL_WARNS("LLFile") << "Short write" << LL_ENDL; - write_ok = false; - } - } - if ( write_ok ) + buffer = std::string(length, 0); + file.read(&buffer[0], length, ec); + if (ec) { - copied = true; + buffer.clear(); } - fclose(out); } - fclose(in); } - return copied; + return buffer; } // static -int LLFile::stat(const std::string& filename, llstat* filestatus, int suppress_error) +int LLFile::mkdir(const std::filesystem::path& file_path) { -#if LL_WINDOWS - std::wstring utf16filename = utf8path_to_wstring(filename); - int rc = _wstat64(utf16filename.c_str(), filestatus); -#else - int rc = ::stat(filename.c_str(), filestatus); -#endif - return warnif("stat", filename, rc, suppress_error); + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::error_code ec; + // We often use mkdir() to ensure the existence of a directory that might + // already exist. There is no known case in which we want to call out as + // an error the requested directory already existing. + std::filesystem::create_directory(file_path, ec); + // The return value is only true if the directory was actually created. + // But if it already existed, ec still indicates success. + return warnif("mkdir", file_path, ec); } // static -unsigned short LLFile::getattr(const std::string& filename, bool dontFollowSymLink, int suppress_error) +int LLFile::remove(const std::filesystem::path& file_path, int suppress_warning) { -#if LL_WINDOWS - // _wstat64() is a bit heavyweight on Windows, use a more lightweight API - // to just get the attributes - int rc = -1; - std::wstring utf16filename = utf8path_to_wstring(filename); - unsigned short st_mode = get_fileattr(utf16filename, dontFollowSymLink); - if (st_mode) + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::error_code ec; + std::filesystem::remove(file_path, ec); + return warnif("remove", file_path, ec, suppress_warning); +} + +// static +int LLFile::rename(const std::filesystem::path& file_path, const std::filesystem::path& new_path, int suppress_warning) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::error_code ec; + std::filesystem::rename(file_path, new_path, ec); + return warnif("rename", file_path, ec, suppress_warning); +} + +// static +S64 LLFile::read(const std::filesystem::path& file_path, void* buf, S64 offset, S64 nbytes, std::error_code& ec) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + // if number of bytes is 0 or less there is nothing to do here + if (nbytes <= 0) { - return st_mode; + clear_error(ec); + return 0; } -#else - llstat filestatus; - int rc = dontFollowSymLink ? ::lstat(filename.c_str(), &filestatus) : ::stat(filename.c_str(), &filestatus); - if (rc == 0) + + if (!buf || offset < 0) { - return filestatus.st_mode; + set_ec_to_parameter_error(ec); } -#endif - warnif("getattr", filename, rc, suppress_error); - return 0; + else + { + std::ios_base::openmode omode = LLFile::in | LLFile::binary; + + LLFile file(file_path, omode, ec); + if (!ec && (bool)file) + { + if (offset > 0) + { + file.seek(offset, ec); + } + // else (offset == 0) file was just opened and should already be at 0. + if (!ec) + { + S64 bytes_read = file.read(buf, nbytes, ec); + if (!ec) + { + return bytes_read; + } + } + } + } + return warnif("read from file failed", file_path, ec); } // static -bool LLFile::isdir(const std::string& filename) +S64 LLFile::write(const std::filesystem::path& file_path, const void* buf, S64 offset, S64 nbytes, std::error_code& ec) { - return S_ISDIR(getattr(filename)); + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + // if number of bytes is 0 or less there is nothing to do here + if (nbytes <= 0) + { + clear_error(ec); + return 0; + } + + if (!buf) + { + set_ec_to_parameter_error(ec); + } + else + { + std::ios_base::openmode omode = LLFile::out | LLFile::binary; + if (offset < 0) + { + omode |= LLFile::app; + } + + LLFile file(file_path, omode, ec); + if (!ec && (bool)file) + { + if (offset > 0) + { + file.seek(offset, ec); + } + // else (offset == 0) we are not appending, file was just opened and should already be at 0. + if (!ec) + { + S64 bytes_written = file.write(buf, nbytes, ec); + if (!ec) + { + return bytes_written; + } + } + } + } + return warnif("write to file failed", file_path, ec); } // static -bool LLFile::isfile(const std::string& filename) +bool LLFile::copy(const std::filesystem::path& source_path, const std::filesystem::path& target_path, std::filesystem::copy_options options, std::error_code& ec) { - return S_ISREG(getattr(filename)); + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + bool copied = std::filesystem::copy_file(source_path, target_path, options, ec); + if (!copied) + { + warnif(STRINGIZE("copy failed, to '" << target_path << "' from"), source_path, ec); + } + return copied; } // static -bool LLFile::islink(const std::string& filename) +int LLFile::stat(const std::filesystem::path& file_path, llstat* filestatus, const char *fname, int suppress_warning) { - return S_ISLNK(getattr(filename, true)); + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; +#if LL_WINDOWS + int rc = _wstat64(file_path.native().c_str(), filestatus); +#else + int rc = ::stat(file_path.native().c_str(), filestatus); +#endif + return warnif(fname ? fname : "stat", file_path, rc, suppress_warning); } // static -const char *LLFile::tmpdir() +S64 LLFile::size(const std::filesystem::path& file_path, int suppress_warning) { - static std::string utf8path; + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::error_code ec; + std::intmax_t size = static_cast<std::intmax_t>(std::filesystem::file_size(file_path, ec)); + if (ec) + { + warnif("size", file_path, ec, suppress_warning); + return 0; + } + return size; +} - if (utf8path.empty()) +// static +std::filesystem::file_status LLFile::getStatus(const std::filesystem::path& file_path, bool dontFollowSymLink, int suppress_warning) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + std::error_code ec; + std::filesystem::file_status status; + if (dontFollowSymLink) { - char sep; -#if LL_WINDOWS - sep = '\\'; + status = std::filesystem::symlink_status(file_path, ec); + } + else + { + status = std::filesystem::status(file_path, ec); + } + warnif("getStatus()", file_path, ec, suppress_warning); + return status; +} - std::vector<wchar_t> utf16path(MAX_PATH + 1); - GetTempPathW(static_cast<DWORD>(utf16path.size()), &utf16path[0]); - utf8path = ll_convert_wide_to_string(&utf16path[0]); +// static +const std::string& LLFile::tmpdir() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE; + static std::string temppath; + if (temppath.empty()) + { +#if LL_WINDOWS + temppath = ll_convert<std::string>(std::filesystem::temp_directory_path().native()); + char sep = '\\'; #else - sep = '/'; - - utf8path = LLStringUtil::getenv("TMPDIR", "/tmp/"); + temppath = std::filesystem::temp_directory_path().string(); + char sep = '/'; #endif - if (utf8path[utf8path.size() - 1] != sep) + if (temppath[temppath.size() - 1] != sep) { - utf8path += sep; + temppath += sep; } } - return utf8path.c_str(); + + return temppath; } #if LL_WINDOWS /************** input file stream ********************************/ -llifstream::llifstream() {} +// explicit +llifstream::llifstream(const char* _Filename, ios_base::openmode _Mode) : + std::ifstream(ll_convert<std::wstring>(_Filename).c_str(), _Mode | ios_base::in) +{ +} // explicit llifstream::llifstream(const std::string& _Filename, ios_base::openmode _Mode): @@ -601,17 +1077,37 @@ llifstream::llifstream(const std::string& _Filename, ios_base::openmode _Mode): { } +// explicit +llifstream::llifstream(const std::filesystem::path& _Filepath, ios_base::openmode _Mode) : + std::ifstream(_Filepath, _Mode | ios_base::in) +{ +} + +void llifstream::open(const char* _Filename, ios_base::openmode _Mode) +{ + std::ifstream::open(ll_convert<std::wstring>(_Filename).c_str(), + _Mode | ios_base::in); +} + void llifstream::open(const std::string& _Filename, ios_base::openmode _Mode) { std::ifstream::open(ll_convert<std::wstring>(_Filename).c_str(), _Mode | ios_base::in); } +void llifstream::open(const std::filesystem::path& _Filepath, ios_base::openmode _Mode) +{ + std::ifstream::open(_Filepath, + _Mode | ios_base::in); +} /************** output file stream ********************************/ - -llofstream::llofstream() {} +// explicit +llofstream::llofstream(const char* _Filename, ios_base::openmode _Mode) : + std::ofstream(ll_convert<std::wstring>(_Filename).c_str(), _Mode | ios_base::out) +{ +} // explicit llofstream::llofstream(const std::string& _Filename, ios_base::openmode _Mode): @@ -620,36 +1116,27 @@ llofstream::llofstream(const std::string& _Filename, ios_base::openmode _Mode): { } -void llofstream::open(const std::string& _Filename, ios_base::openmode _Mode) +// explicit +llofstream::llofstream(const std::filesystem::path& _Filepath, ios_base::openmode _Mode) : + std::ofstream(_Filepath, _Mode | ios_base::out) { - std::ofstream::open(ll_convert<std::wstring>( _Filename ).c_str(), - _Mode | ios_base::out); } -/************** helper functions ********************************/ +void llofstream::open(const char* _Filename, ios_base::openmode _Mode) +{ + std::ofstream::open(ll_convert<std::wstring>(_Filename).c_str(), _Mode | ios_base::out); +} -std::streamsize llifstream_size(llifstream& ifstr) +void llofstream::open(const std::string& _Filename, ios_base::openmode _Mode) { - if(!ifstr.is_open()) return 0; - std::streampos pos_old = ifstr.tellg(); - ifstr.seekg(0, ios_base::beg); - std::streampos pos_beg = ifstr.tellg(); - ifstr.seekg(0, ios_base::end); - std::streampos pos_end = ifstr.tellg(); - ifstr.seekg(pos_old, ios_base::beg); - return pos_end - pos_beg; + std::ofstream::open(ll_convert<std::wstring>( _Filename ).c_str(), + _Mode | ios_base::out); } -std::streamsize llofstream_size(llofstream& ofstr) +void llofstream::open(const std::filesystem::path& _Filepath, ios_base::openmode _Mode) { - if(!ofstr.is_open()) return 0; - std::streampos pos_old = ofstr.tellp(); - ofstr.seekp(0, ios_base::beg); - std::streampos pos_beg = ofstr.tellp(); - ofstr.seekp(0, ios_base::end); - std::streampos pos_end = ofstr.tellp(); - ofstr.seekp(pos_old, ios_base::beg); - return pos_end - pos_beg; + std::ofstream::open(_Filepath, + _Mode | ios_base::out); } #endif // LL_WINDOWS diff --git a/indra/llcommon/llfile.h b/indra/llcommon/llfile.h index 04a2946ac4..8de3f5c9b4 100644 --- a/indra/llcommon/llfile.h +++ b/indra/llcommon/llfile.h @@ -35,133 +35,712 @@ * Attempts to mostly mirror the POSIX style IO functions. */ -typedef FILE LLFILE; +#include "stdtypes.h" #include <fstream> +#include <filesystem> #include <sys/stat.h> #if LL_WINDOWS +#include <windows.h> // The Windows version of stat function and stat data structure are called _stat64 // We use _stat64 here to support 64-bit st_size and time_t values -typedef struct _stat64 llstat; +typedef struct _stat64 llstat; #else -typedef struct stat llstat; #include <sys/types.h> +typedef struct stat llstat; #endif -#ifndef S_ISREG -# define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#endif - -#ifndef S_ISDIR -# define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#endif +typedef FILE LLFILE; -// Windows C runtime library does not define this and does not support symlink detection in the -// stat functions but we do in our getattr() function -#ifndef S_IFLNK -#define S_IFLNK 0xA000 /* symlink */ -#endif +#include "fsyspath.h" +#include "llstring.h" // safe char* -> std::string conversion -#ifndef S_ISLNK -#define S_ISLNK(x) (((x) & S_IFMT) == S_IFLNK) +#if LL_WINDOWS +#define LLFILE_MODE(flags) L##flags +#else +#define LLFILE_MODE(flags) flags #endif -#include "llstring.h" // safe char* -> std::string conversion - -/// LLFile is a class of static functions operating on paths +/// This class provides a selection of functions to operate on files through names and +/// a class implementation to represent a file for reading and writing to it /// All the functions with a path string input take UTF8 path/filenames -class LL_COMMON_API LLFile +/// +/// @nosubgrouping +/// +class LLFile { public: + // ================================================================================ + /// @name Constants + /// + ///@{ + /** These can be passed to the omode parameter of LLFile::open() and its constructor + + This is similar to the openmode flags for std:fstream but not exactly the same + std::fstream open() does not allow to open a file for writing without either + forcing the file to be truncated on open or all write operations being always + appended to the end of the file or failing to open when the file does not exist. + But to allow implementing the LLAPRFile::writeEx() functionality we need to be + able to write at a random position to the file without truncating it on open. + + any other combinations than listed here are not allowed and will cause an error + + bin in out trunc app noreplace File exists File doesn't exist + ---------------------------------------------------------------------------------- + - + - - - - Open at begin Failure to open + + + - - - - " " + - - + - - - " Create new + + - + - - - " " + - + + - - - " " + + + + - - - " " + ---------------------------------------------------------------------------------- + - - + + - - Destroy contents Create new + + - + + - - " " + - + + + - - " " + + + + + - - " " + ---------------------------------------------------------------------------------- + - - + x - + Failure to open Create new + + - + x - + " " + - + + x - + " " + + + + x - + " " + ---------------------------------------------------------------------------------- + - - + - + - Write to end Create new + + - + - + - " " + - + + - + - " " + + + + - + - " " + ---------------------------------------------------------------------------------- + */ + static const std::ios_base::openmode app = static_cast<std::ios_base::openmode>(1 << 1); // append to end + static const std::ios_base::openmode ate = static_cast<std::ios_base::openmode>(1 << 2); // initialize to end + static const std::ios_base::openmode binary = static_cast<std::ios_base::openmode>(1 << 3); // binary mode + static const std::ios_base::openmode in = static_cast<std::ios_base::openmode>(1 << 4); // for reading + static const std::ios_base::openmode out = static_cast<std::ios_base::openmode>(1 << 5); // for writing // for writing and reading + static const std::ios_base::openmode trunc = static_cast<std::ios_base::openmode>(1 << 6); // truncate on open + static const std::ios_base::openmode noreplace = static_cast<std::ios_base::openmode>(1 << 7); // no replace if it exists + + /// Additional optional flags to omode in open() and lmode in fopen() or lock() + /// to indicate which sort of lock if any to attempt to get + /// + /// NOTE: there is a fundamental difference between platforms. + /// On Windows this lock is mandatory as it is part of the API to open a file handle and other + /// processes can not avoid it. If a file was opened denying other processes read and/or write + /// access, trying to open the same file in another process with that access will fail. + /// On Mac and Linux it is only an advisory lock implemented through the flock() system call. + /// This means that any other application needs to also attempt to at least acquire a shared + /// lock on the file in order to notice that the file is actually already locked. It can + /// therefore not be used to prevent random other applications from accessing the file, but it + /// works for other viewer processes when they use either the LLFile::open() or LLFile::fopen() + /// functions with the appropriate lock flags to open a file. + static const std::ios_base::openmode exclusive = static_cast<std::ios_base::openmode>(1 << 16); + static const std::ios_base::openmode shared = static_cast<std::ios_base::openmode>(1 << 17); + + /// Additional lmode flag to indicate to rather fail instead of blocking when trying + /// to acquire a lock with LLFile::lock() + static const std::ios_base::openmode noblock = static_cast<std::ios_base::openmode>(1 << 18); + + /// The mask value for the lock mask bits + static const std::ios_base::openmode lock_mask = static_cast<std::ios_base::openmode>(exclusive | shared); + + /// One of these can be passed to the dir parameter of LLFile::seek() + static const std::ios_base::seekdir beg = std::ios_base::beg; + static const std::ios_base::seekdir cur = std::ios_base::cur; + static const std::ios_base::seekdir end = std::ios_base::end; + ///@} + + // ================================================================================ + /// @name constructor/deconstructor + /// + ///@{ + /// default constructor + LLFile() : mHandle(InvalidHandle) {} + + /// no copy constructor + LLFile(const LLFile&) = delete; + + /// move constructor + LLFile(LLFile&& other) noexcept + { + mHandle = other.mHandle; + other.mHandle = InvalidHandle; + } + + /// constructor opening the file + explicit LLFile(const char* filename, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666) : + mHandle(InvalidHandle) + { + open(filename, omode, ec, perm); + } + + explicit LLFile(const std::string& filename, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666) : + mHandle(InvalidHandle) + { + open(filename, omode, ec, perm); + } + + explicit LLFile(const std::filesystem::path& file_path, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666) : + mHandle(InvalidHandle) + { + open(file_path, omode, ec, perm); + } + + /// destructor always attempts to close the file + ~LLFile() { close(); } + ///@} + + // ================================================================================ + /// @name operators + /// + ///@{ + /// copy assignment deleted + LLFile& operator=(const LLFile&) = delete; + + /// move assignment + LLFile& operator=(LLFile&& other) noexcept + { + close(); + std::swap(mHandle, other.mHandle); + return *this; + } + + // detect whether the wrapped file descriptor/handle is open or not + explicit operator bool() const { return (mHandle != InvalidHandle); } + bool operator!() { return (mHandle == InvalidHandle); } + ///@} + + /// ================================================================================ + /// @name class member methods + /// + /// These methods provide read and write support as well as additional functionality to query the size of + /// the file, change the position of the current file pointer or query it. + /// + /// Most of these functions take as one of their parameters a std::error_code object which can be used to + /// determine in more detail what error occurred if required + ///@{ + + /// Open a file with the specific open mode flags + inline int open(const char* filename, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666) + { + std::filesystem::path file_path = fsyspath(filename); + return open(file_path, omode, ec, perm); + } + inline int open(const std::string& filename, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666) + { + std::filesystem::path file_path = fsyspath(filename); + return open(file_path, omode, ec, perm); + } + int open(const std::filesystem::path& filename, std::ios_base::openmode omode, std::error_code& ec, int perm = 0666); + ///< @returns 0 on success, -1 on failure + + /// Determine the size of the opened file + S64 size(std::error_code& ec); + ///< @returns the number of bytes in the file or 0 on failure + + /// Query the position of the current file pointer in the file + S64 tell(std::error_code& ec); + ///< @returns the absolute offset of the file pointer in bytes relative to the start of the file or -1 on failure + + /// Move the file pointer to the specified absolute position relative to the start of the file + int seek(S64 pos, std::error_code& ec); + ///< @returns 0 on success, -1 on failure + + /// Move the file pointer to the specified position relative to dir + int seek(S64 offset, std::ios_base::seekdir dir, std::error_code& ec); + ///< @returns 0 on success, -1 on failure + + /// Read the specified number of bytes into the buffer starting at the current file pointer + S64 read(void* buffer, S64 nbytes, std::error_code& ec); + ///< If the file ends before the requested amount of bytes could be read, the function succeeds and + /// returns the bytes up to the end of the file. The return value indicates the number of actually + /// read bytes and can be therefore smaller than the requested amount. + /// @returns the number of bytes read from the file or -1 on failure + + /// Write the specified number of bytes to the file starting at the current file pointer + S64 write(const void* buffer, S64 nbytes, std::error_code& ec); + ///< @returns the number of bytes written to the file or -1 on failure + + /// Write into the file starting at the current file pointer using printf style format and + /// additional optional parameters as specified in the fmt string + S64 printf(const char* fmt, ...); + ///< @returns the number of bytes written to the file or -1 on failure + + /// Attempt to acquire or release a lock on the file + int lock(int lmode, std::error_code& ec); + ///< lmode can be one of LLFile::exclusive or LLFile::shared to acquire the according lock + /// or 0 to give up an earlier acquired lock. Adding LLFile::noblock together with one of + /// the lock requests will cause the function to fail if the lock can not be acquired, + /// otherwise the function will block until the lock can be acquired. + /// @returns 0 on success, -1 on failure + + /// close the file explicitly + int close(std::error_code& ec); + ///< @returns 0 on success, -1 on failure + + /// Convenience function to close the file without additional parameters + int close(); + ///< @returns 0 on success, -1 on failure + ///@} + + /// ================================================================================ + /// @name static member functions + /// + /// These functions are static and operate with UTF8 filenames as one of their parameters. + /// + ///@{ /// open a file with the specified access mode - static LLFILE* fopen(const std::string& filename, const char* accessmode); /* Flawfinder: ignore */ + /// +#if LL_WINDOWS + using fopen_flags_t = wchar_t; +#else + using fopen_flags_t = char; +#endif + inline static LLFILE* fopen(const char* filename, const fopen_flags_t* accessmode, int lmode = 0) + { + std::filesystem::path file_path = fsyspath(filename); + return LLFile::fopen(file_path, accessmode, lmode); + } + inline static LLFILE* fopen(const std::string& filename, const fopen_flags_t* accessmode, int lmode = 0) + { + std::filesystem::path file_path = fsyspath(filename); + return LLFile::fopen(file_path, accessmode, lmode); + } + static LLFILE* fopen(const std::filesystem::path& file_path, const fopen_flags_t* accessmode, int lmode = 0); ///< 'accessmode' follows the rules of the Posix fopen() mode parameter - /// "r" open the file for reading only and positions the stream at the beginning - /// "r+" open the file for reading and writing and positions the stream at the beginning - /// "w" open the file for reading and writing and truncate it to zero length - /// "w+" open or create the file for reading and writing and truncate to zero length if it existed - /// "a" open the file for reading and writing and position the stream at the end of the file - /// "a+" open or create the file for reading and writing and position the stream at the end of the file + /// "r" open the file for reading only and positions the stream at the beginning + /// "r+" open the file for reading and writing and positions the stream at the beginning + /// "w" open the file for reading and writing and truncate it to zero length + /// "w+" open or create the file for reading and writing and truncate to zero length if it existed + /// "a" open the file for reading and writing and before every write position the stream at the end of the file + /// "a+" open or create the file for reading and writing and before every write position the stream at the end of the file + /// + /// in addition to these values, "b" can be appended to indicate binary stream access, but on Linux and Mac + /// this is strictly for compatibility and has no effect. On Windows this makes the file functions not + /// try to translate line endings. Windows also allows to append "t" to indicate text mode. If neither + /// "b" or "t" is defined, Windows uses the value set by _fmode which by default is _O_TEXT. + /// This means that it is always a good idea to append "b" specifically for binary file access to + /// avoid corruption of the binary consistency of the data stream when reading or writing + /// Other characters in 'accessmode', while possible on some platforms (Windows), will usually + /// cause an error on other platforms as fopen will verify this parameter /// - /// in addition to these values, "b" can be appended to indicate binary stream access, but on Linux and Mac - /// this is strictly for compatibility and has no effect. On Windows this makes the file functions not - /// try to translate line endings. Windows also allows to append "t" to indicate text mode. If neither - /// "b" or "t" is defined, Windows uses the value set by _fmode which by default is _O_TEXT. - /// This means that it is always a good idea to append "b" specifically for binary file access to - /// avoid corruption of the binary consistency of the data stream when reading or writing - /// Other characters in 'accessmode' will usually cause an error as fopen will verify this parameter - /// @returns a valid LLFILE* pointer on success or NULL on failure + /// lmode is optional and allows to lock the file for other processes either as a shared lock or an + /// exclusive lock. If the requested lock conflicts with an already existing lock, the open fails. + /// Pass either LLFIle::exclusive or LLFile::shared to this parameter if you want to prevent other + /// processes from reading (exclusive lock) or writing (shared lock) to the file. It will always use + /// LLFile::noblock, meaning the open will immediately fail if it conflicts with an existing lock on the + /// file. + /// + /// @returns a valid LLFILE* pointer on success that can be passed to the fread() and fwrite() functions + /// and some other f<something> functions in the Standard C library that accept a FILE* as parameter + /// or NULL on failure + /// Close a file handle opened with fopen() above static int close(LLFILE * file); - - /// retrieve the content of a file into a string - static std::string getContents(const std::string& filename); - ///< @returns the content of the file or an empty string on failure + ///< @returns 0 on success and -1 on failure. /// create a directory - static int mkdir(const std::string& filename, int perms = 0700); - ///< perms is a permissions mask like 0777 or 0700. In most cases it will be - /// overridden by the user's umask. It is ignored on Windows. - /// mkdir() considers "directory already exists" to be not an error. - /// @returns 0 on success and -1 on failure. - - //// remove a directory - static int rmdir(const std::string& filename, int suppress_error = 0); - ///< pass ENOENT in the optional 'suppress_error' parameter - /// if you don't want a warning in the log when the directory does not exist + inline static int mkdir(const char* dirname) + { + std::filesystem::path dir_path = fsyspath(dirname); + return mkdir(dir_path); + } + inline static int mkdir(const std::string& dirname) + { + std::filesystem::path dir_path = fsyspath(dirname); + return mkdir(dir_path); + } + static int mkdir(const std::filesystem::path& dirname); + ///< mkdir() considers "directory already exists" to be not an error. /// @returns 0 on success and -1 on failure. /// remove a file or directory - static int remove(const std::string& filename, int suppress_error = 0); - ///< pass ENOENT in the optional 'suppress_error' parameter - /// if you don't want a warning in the log when the directory does not exist + inline static int remove(const char* filename, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + return remove(file_path, suppress_warning); + } + inline static int remove(const std::string& filename, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + return remove(file_path, suppress_warning); + } + static int remove(const std::filesystem::path& file_path, int suppress_warning = 0); + ///< pass an errno value (e.g., ENOENT) in the optional 'suppress_warning' parameter if you want to + /// suppress a warning in the log when the failure matches that errno (e.g., suppress warning if + /// the file or directory does not exist) /// @returns 0 on success and -1 on failure. /// rename a file - static int rename(const std::string& filename, const std::string& newname, int suppress_error = 0); + inline static int rename(const char* filename, const char* newname, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + std::filesystem::path new_path = fsyspath(newname); + return rename(file_path, new_path, suppress_warning); + } + inline static int rename(const std::string& filename, const char* newname, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + std::filesystem::path new_path = fsyspath(newname); + return rename(file_path, new_path, suppress_warning); + } + inline static int rename(const char* filename, const std::string& newname, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + std::filesystem::path new_path = fsyspath(newname); + return rename(file_path, new_path, suppress_warning); + } + inline static int rename(const std::string& filename, const std::string& newname, int suppress_warning = 0) + { + std::filesystem::path file_path = fsyspath(filename); + std::filesystem::path new_path = fsyspath(newname); + return rename(file_path, new_path, suppress_warning); + } + static int rename(const std::filesystem::path& file_path, const std::filesystem::path& new_path, int suppress_warning = 0); ///< it will silently overwrite newname if it exists without returning an error /// Posix guarantees that if newname already exists, then there will be no moment /// in which for other processes newname does not exist. There is no such guarantee - /// under Windows at this time. It may do it in the same way but the used Windows API - /// does not make such guarantees. + /// under Windows at this time. It may do it in the same way but the used Windows + /// APIs do not make such guarantees. /// @returns 0 on success and -1 on failure. + /// copy the contents of the file from 'source' to 'target' + inline static bool copy(const char* source, const char* target) + { + std::error_code ec; + return copy(source, target, std::filesystem::copy_options::overwrite_existing, ec); + } + inline static bool copy(const char* source, const std::string& target) + { + std::error_code ec; + return copy(source, target, std::filesystem::copy_options::overwrite_existing, ec); + } + inline static bool copy(const std::string& source, const char* target) + { + std::error_code ec; + return copy(source, target, std::filesystem::copy_options::overwrite_existing, ec); + } + inline static bool copy(const std::string& source, const std::string& target) + { + std::error_code ec; + return copy(source, target, std::filesystem::copy_options::overwrite_existing, ec); + } + inline static bool copy(const std::filesystem::path& source_path, const std::filesystem::path& target_path) + { + std::error_code ec; + return copy(source_path, target_path, std::filesystem::copy_options::overwrite_existing, ec); + } + ///< Copies the contents of the file 'source' to the file 'target', overwriting 'target' if it already + /// existed. + /// This is a convenience function that implements the previous behavior of silently overwriting an + /// already existing target file. Consider using the function below if you desire a different + /// behavior when the target file already exists + /// @returns true on success and false on failure. + + /// copy the contents of the file from 'from' to 'to' + inline static bool copy(const char* source, const char* target, std::filesystem::copy_options options, std::error_code& ec) + { + std::filesystem::path source_path = fsyspath(source); + std::filesystem::path target_path = fsyspath(target); + return copy(source_path, target_path, options, ec); + } + inline static bool copy(const char* source, const std::string& target, std::filesystem::copy_options options, std::error_code& ec) + { + std::filesystem::path source_path = fsyspath(source); + std::filesystem::path target_path = fsyspath(target); + return copy(source_path, target_path, options, ec); + } + inline static bool copy(const std::string& source, const char* target, std::filesystem::copy_options options, std::error_code& ec) + { + std::filesystem::path source_path = fsyspath(source); + std::filesystem::path target_path = fsyspath(target); + return copy(source_path, target_path, options, ec); + } + inline static bool copy(const std::string& source, const std::string& target, std::filesystem::copy_options options, std::error_code& ec) + { + std::filesystem::path source_path = fsyspath(source); + std::filesystem::path target_path = fsyspath(target); + return copy(source_path, target_path, options, ec); + } + static bool copy(const std::filesystem::path& source_path, const std::filesystem::path& target_path, std::filesystem::copy_options options, std::error_code& ec); + ///< Copies the contents of the file 'source' to the file 'target'. The options parameter allows to + /// specify what should happen if the "target" file already exists: + /// std::filesystem::copy_options::none - return an error in ec and fail + /// std::filesystem::copy_options::skip_existing - skip the operation and do not overwrite file + /// std::filesystem::copy_options::overwrite_existing - overwrite the file + /// std::filesystem::copy_options::update_existing - overwrite the file only if it is older than the file being copied + /// @returns true on success and false on failure. + + /// retrieve the content of a file into a string + inline static std::string getContents(const char* filename) + { + std::error_code ec; + return getContents(filename, ec); + } + inline static std::string getContents(const std::string& filename) + { + std::error_code ec; + return getContents(filename, ec); + } + inline static std::string getContents(const std::filesystem::path& file_path) + { + std::error_code ec; + return getContents(file_path, ec); + } + inline static std::string getContents(const char* filename, std::error_code& ec) + { + std::filesystem::path file_path = fsyspath(filename); + return getContents(file_path, ec); + } + inline static std::string getContents(const std::string& filename, std::error_code& ec) + { + std::filesystem::path file_path = fsyspath(filename); + return getContents(file_path, ec); + } + static std::string getContents(const std::filesystem::path& file_path, std::error_code& ec); + ///< @returns the entire content of the file as std::string or an empty string on failure + + /// read nBytes from the file into the buffer, starting at offset in the file + inline static S64 read(const char* filename, void* buf, S64 offset, S64 nbytes) + { + std::error_code ec; + return read(filename, buf, offset, nbytes, ec); + } + inline static S64 read(const std::string& filename, void* buf, S64 offset, S64 nbytes) + { + std::error_code ec; + return read(filename, buf, offset, nbytes, ec); + } + inline static S64 read(const std::filesystem::path& file_path, void* buf, S64 offset, S64 nbytes) + { + std::error_code ec; + return read(file_path, buf, offset, nbytes, ec); + } + inline static S64 read(const char* filename, void* buf, S64 offset, S64 nbytes, std::error_code& ec) + { + std::filesystem::path file_path = fsyspath(filename); + return read(file_path, buf, offset, nbytes, ec); + } + inline static S64 read(const std::string& filename, void* buf, S64 offset, S64 nbytes, std::error_code& ec) + { + std::filesystem::path file_path = fsyspath(filename); + return read(file_path, buf, offset, nbytes, ec); + } + static S64 read(const std::filesystem::path& filename, void* buf, S64 offset, S64 nbytes, std::error_code& ec); + ///< @returns bytes read on success, or -1 on failure - /// copy the contents of file from 'from' to 'to' filename - static bool copy(const std::string& from, const std::string& to); - ///< @returns true on success and false on failure. + /// write nBytes from the buffer into the file, starting at offset in the file + inline static S64 write(const std::string& filename, const void* buf, S64 offset, S64 nbytes) + { + std::error_code ec; + std::filesystem::path file_path = fsyspath(filename); + return write(file_path, buf, offset, nbytes, ec); + } + inline static S64 write(const std::filesystem::path& filename, const void* buf, S64 offset, S64 nbytes) + { + std::error_code ec; + std::filesystem::path file_path = fsyspath(filename); + return write(file_path, buf, offset, nbytes, ec); + } + inline static S64 write(const std::string& filename, const void* buf, S64 offset, S64 nbytes, std::error_code& ec) + { + std::filesystem::path file_path = fsyspath(filename); + return write(file_path, buf, offset, nbytes, ec); + } + static S64 write(const std::filesystem::path& filename, const void* buf, S64 offset, S64 nbytes, std::error_code& ec); + ///< If a negative offset is provided, the file is opened in append mode and the + /// write will be appended to the end of the file. + /// @returns bytes written on success, or -1 on failure /// return the file stat structure for filename - static int stat(const std::string& filename, llstat* file_status, int suppress_error = ENOENT); + inline static int stat(const char* filename, llstat* file_status, const char* operation = nullptr, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return stat(file_path, file_status, operation, suppress_warning); + } + inline static int stat(const std::string& filename, llstat* file_status, const char* operation = nullptr, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return stat(file_path, file_status, operation, suppress_warning); + } + static int stat(const std::filesystem::path& file_path, llstat* file_status, const char *operation = nullptr, int suppress_warning = ENOENT); ///< for compatibility with existing uses of LL_File::stat() we use ENOENT as default in the - /// optional 'suppress_error' parameter to avoid spamming the log with warnings when the API + /// optional 'suppress_warning' parameter to avoid spamming the log with warnings when the API /// is used to detect if a file exists /// @returns 0 on success and -1 on failure. - /// get the file or directory attributes for filename - static unsigned short getattr(const std::string& filename, bool dontFollowSymLink = false, int suppress_error = ENOENT); - ///< a more lightweight function on Windows to stat, that just returns the file attribute flags - /// dontFollowSymLinks set to true returns the attributes of the symlink if it is one, rather than resolving it - /// we pass by default ENOENT in the optional 'suppress_error' parameter to not spam the log with - /// warnings when the file or directory does not exist - /// @returns 0 on failure and a st_mode value with either S_IFDIR or S_IFREG set otherwise - /// together with the three access bits which under Windows only the write bit is relevant. + /// get the std::filesystem::file_status for filename + inline static std::filesystem::file_status getStatus(const char* filename, bool dontFollowSymLink = false, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return getStatus(file_path, dontFollowSymLink, suppress_warning); + } + inline static std::filesystem::file_status getStatus(const std::string& filename, bool dontFollowSymLink = false, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return getStatus(file_path, dontFollowSymLink, suppress_warning); + } + static std::filesystem::file_status getStatus(const std::filesystem::path& file_path, bool dontFollowSymLink = false, int suppress_warning = ENOENT); + ///< dontFollowSymLinks set to true returns the std::filesystem::file_status of the symlink if it + /// is one, rather than resolving it. We pass by default ENOENT in the optional 'suppress_warning' + /// parameter to not spam the log with warnings when the file or directory does not exist + /// @returns a std::filesystem::file_status value that can be passed to the appropriate std::filesystem::exists() + /// and other APIs accepting a file_status. + + /// get the size of a file in bytes + inline static S64 size(const char* filename, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return size(file_path, suppress_warning); + } + inline static S64 size(const std::string& filename, int suppress_warning = ENOENT) + { + std::filesystem::path file_path = fsyspath(filename); + return size(file_path, suppress_warning); + } + static S64 size(const std::filesystem::path& file_path, int suppress_warning = ENOENT); + ///< we pass by default ENOENT in the optional 'suppress_warning' parameter to not spam + /// the log with warnings when the file does not exist + /// @returns the file size on success or 0 on failure. + + /// check if filename is an existing file or directory + inline static bool exists(const char* filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::exists(status); + } + inline static bool exists(const std::string& filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::exists(status); + } + inline static bool exists(const std::filesystem::path& file_path) + { + std::filesystem::file_status status = getStatus(file_path); + return std::filesystem::exists(status); + } + ///< @returns true if the path is for an existing file or directory /// check if filename is an existing directory - static bool isdir(const std::string& filename); + inline static bool isdir(const char* filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::is_directory(status); + } + inline static bool isdir(const std::string& filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::is_directory(status); + } + inline static bool isdir(const std::filesystem::path& file_path) + { + std::filesystem::file_status status = getStatus(file_path); + return std::filesystem::is_directory(status); + } ///< @returns true if the path is for an existing directory /// check if filename is an existing file - static bool isfile(const std::string& filename); + inline static bool isfile(const char* filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::is_regular_file(status); + } + inline static bool isfile(const std::string& filename) + { + std::filesystem::file_status status = getStatus(filename); + return std::filesystem::is_regular_file(status); + } + inline static bool isfile(const std::filesystem::path& file_path) + { + std::filesystem::file_status status = getStatus(file_path); + return std::filesystem::is_regular_file(status); + } ///< @returns true if the path is for an existing file /// check if filename is a symlink - static bool islink(const std::string& filename); + inline static bool islink(const char* filename) + { + std::filesystem::file_status status = getStatus(filename, true); + return std::filesystem::is_symlink(status); + } + inline static bool islink(const std::string& filename) + { + std::filesystem::file_status status = getStatus(filename, true); + return std::filesystem::is_symlink(status); + } + inline static bool islink(const std::filesystem::path& file_path) + { + std::filesystem::file_status status = getStatus(file_path, true); + return std::filesystem::is_symlink(status); + } ///< @returns true if the path is pointing at a symlink /// return a path to the temporary directory on the system - static const char * tmpdir(); + static const std::string& tmpdir(); + +private: +#if LL_WINDOWS + typedef HANDLE llfile_handle_t; + const llfile_handle_t InvalidHandle = INVALID_HANDLE_VALUE; + llfile_handle_t mHandle = INVALID_HANDLE_VALUE; // The file handle/descriptor +#else + typedef int llfile_handle_t; + const llfile_handle_t InvalidHandle = -1; + llfile_handle_t mHandle = -1; // The file handle/descriptor +#endif + + std::ios_base::openmode mOpen{}; // Used to emulate std::ios_base::app under Windows +}; + +#if LL_WINDOWS +/** + * @brief Controlling input for files. + * + * This class supports reading from named files, using the inherited + * functions from std::ifstream. The only added value is that our constructor + * Does The Right Thing when passed a non-ASCII pathname. Sadly, that isn't + * true of Microsoft's std::ifstream. + */ +class llifstream : public std::ifstream +{ + // input stream associated with a C stream + public: + // Constructors: + /** + * @brief Default constructor. + * + * Initializes @c sb using its default constructor, and passes + * @c &sb to the base class initializer. Does not open any files + * (you haven't given it a filename to open). + */ + llifstream() = default; + + /** + * @brief Create an input file stream. + * @param Filename String specifying the filename. + * @param Mode Open file in specified mode (see std::ios_base). + * + * @c ios_base::in is automatically included in @a mode. + */ + explicit llifstream(const char* _Filename, + ios_base::openmode _Mode = ios_base::in); + explicit llifstream(const std::string& _Filename, + ios_base::openmode _Mode = ios_base::in); + explicit llifstream(const std::filesystem::path& _Filepath, + ios_base::openmode _Mode = ios_base::in); + + /** + * @brief Opens an external file. + * @param Filename The name of the file. + * @param Node The open mode flags. + * + * Calls @c llstdio_filebuf::open(s,mode|in). If that function + * fails, @c failbit is set in the stream's error state. + */ + void open(const char* _Filename, + ios_base::openmode _Mode = ios_base::in); + void open(const std::string& _Filename, + ios_base::openmode _Mode = ios_base::in); + void open(const std::filesystem::path& _Filepath, + ios_base::openmode _Mode = ios_base::in); }; /// RAII class @@ -169,22 +748,19 @@ class LLUniqueFile { public: // empty - LLUniqueFile(): mFileHandle(nullptr) {} + LLUniqueFile() = default; // wrap (e.g.) result of LLFile::fopen() - LLUniqueFile(LLFILE* f): mFileHandle(f) {} + LLUniqueFile(LLFILE* f) : mFileHandle(f) {} // no copy LLUniqueFile(const LLUniqueFile&) = delete; // move construction LLUniqueFile(LLUniqueFile&& other) noexcept { - mFileHandle = other.mFileHandle; + mFileHandle = other.mFileHandle; other.mFileHandle = nullptr; } // The point of LLUniqueFile is to close on destruction. - ~LLUniqueFile() - { - close(); - } + ~LLUniqueFile() { close(); } // simple assignment LLUniqueFile& operator=(LLFILE* f) @@ -209,7 +785,7 @@ public: if (mFileHandle) { // in case close() throws, set mFileHandle null FIRST - LLFILE* h{nullptr}; + LLFILE* h{ nullptr }; std::swap(h, mFileHandle); LLFile::close(h); } @@ -217,62 +793,16 @@ public: // detect whether the wrapped LLFILE is open or not explicit operator bool() const { return bool(mFileHandle); } - bool operator!() { return ! mFileHandle; } + bool operator!() { return !mFileHandle; } // LLUniqueFile should be usable for any operation that accepts LLFILE* // (or FILE* for that matter) operator LLFILE*() const { return mFileHandle; } private: - LLFILE* mFileHandle; -}; - -#if LL_WINDOWS -/** - * @brief Controlling input for files. - * - * This class supports reading from named files, using the inherited - * functions from std::ifstream. The only added value is that our constructor - * Does The Right Thing when passed a non-ASCII pathname. Sadly, that isn't - * true of Microsoft's std::ifstream. - */ -class LL_COMMON_API llifstream : public std::ifstream -{ - // input stream associated with a C stream - public: - // Constructors: - /** - * @brief Default constructor. - * - * Initializes @c sb using its default constructor, and passes - * @c &sb to the base class initializer. Does not open any files - * (you haven't given it a filename to open). - */ - llifstream(); - - /** - * @brief Create an input file stream. - * @param Filename String specifying the filename. - * @param Mode Open file in specified mode (see std::ios_base). - * - * @c ios_base::in is automatically included in @a mode. - */ - explicit llifstream(const std::string& _Filename, - ios_base::openmode _Mode = ios_base::in); - - /** - * @brief Opens an external file. - * @param Filename The name of the file. - * @param Node The open mode flags. - * - * Calls @c llstdio_filebuf::open(s,mode|in). If that function - * fails, @c failbit is set in the stream's error state. - */ - void open(const std::string& _Filename, - ios_base::openmode _Mode = ios_base::in); + LLFILE* mFileHandle = nullptr; }; - /** * @brief Controlling output for files. * @@ -281,7 +811,7 @@ class LL_COMMON_API llifstream : public std::ifstream * Right Thing when passed a non-ASCII pathname. Sadly, that isn't true of * Microsoft's std::ofstream. */ -class LL_COMMON_API llofstream : public std::ofstream +class llofstream : public std::ofstream { public: // Constructors: @@ -292,7 +822,7 @@ class LL_COMMON_API llofstream : public std::ofstream * @c &sb to the base class initializer. Does not open any files * (you haven't given it a filename to open). */ - llofstream(); + llofstream() = default; /** * @brief Create an output file stream. @@ -301,8 +831,12 @@ class LL_COMMON_API llofstream : public std::ofstream * * @c ios_base::out is automatically included in @a mode. */ + explicit llofstream(const char* _Filename, + ios_base::openmode _Mode = ios_base::out|ios_base::trunc); explicit llofstream(const std::string& _Filename, ios_base::openmode _Mode = ios_base::out|ios_base::trunc); + explicit llofstream(const std::filesystem::path& _Filepath, + ios_base::openmode _Mode = ios_base::out|ios_base::trunc); /** * @brief Opens an external file. @@ -311,21 +845,14 @@ class LL_COMMON_API llofstream : public std::ofstream * * @c ios_base::out is automatically included in @a mode. */ + void open(const char* _Filename, + ios_base::openmode _Mode = ios_base::out|ios_base::trunc); void open(const std::string& _Filename, ios_base::openmode _Mode = ios_base::out|ios_base::trunc); + void open(const std::filesystem::path& _Filepath, + ios_base::openmode _Mode = ios_base::out|ios_base::trunc); }; - -/** - * @brief filesize helpers. - * - * The file size helpers are not considered particularly efficient, - * and should only be used for config files and the like -- not in a - * loop. - */ -std::streamsize LL_COMMON_API llifstream_size(llifstream& fstr); -std::streamsize LL_COMMON_API llofstream_size(llofstream& fstr); - #else // ! LL_WINDOWS // on non-windows, llifstream and llofstream are just mapped directly to the std:: equivalents diff --git a/indra/llcommon/llfindlocale.cpp b/indra/llcommon/llfindlocale.cpp index b4bcc80ac4..d606a6fdb9 100644 --- a/indra/llcommon/llfindlocale.cpp +++ b/indra/llcommon/llfindlocale.cpp @@ -134,7 +134,7 @@ accumulate_locstring(const char *str, FL_Locale *l) { return 0; } - +#ifndef LL_WINDOWS static int accumulate_env(const char *name, FL_Locale *l) { char *env; @@ -148,7 +148,7 @@ accumulate_env(const char *name, FL_Locale *l) { free(lang); free(country); free(variant); return 0; } - +#endif static void canonise_fl(FL_Locale *l) { diff --git a/indra/llcommon/llfixedbuffer.h b/indra/llcommon/llfixedbuffer.h index 1234d2014f..d2cd660c18 100644 --- a/indra/llcommon/llfixedbuffer.h +++ b/indra/llcommon/llfixedbuffer.h @@ -27,7 +27,7 @@ #ifndef LL_LLFIXEDBUFFER_H #define LL_LLFIXEDBUFFER_H -#include "timer.h" +#include "lltimer.h" #include <deque> #include <string> #include "llstring.h" diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h index 92b26354a1..734365767d 100644 --- a/indra/llcommon/llinstancetracker.h +++ b/indra/llcommon/llinstancetracker.h @@ -28,6 +28,8 @@ #ifndef LL_LLINSTANCETRACKER_H #define LL_LLINSTANCETRACKER_H +#include "llpreprocessor.h" + #include <map> #include <set> #include <vector> @@ -35,7 +37,7 @@ #include <memory> #include <type_traits> -#include "mutex.h" +#include <mutex> #include <boost/iterator/transform_iterator.hpp> #include <boost/iterator/indirect_iterator.hpp> @@ -244,6 +246,7 @@ public: } protected: + LL_UBSAN_SUPRESS_VPTR LLInstanceTracker(const KEY& key) { // We do not intend to manage the lifespan of this object with @@ -482,6 +485,7 @@ public: using key_snapshot_of = instance_snapshot_of<SUBCLASS>; protected: + LL_UBSAN_SUPRESS_VPTR LLInstanceTracker() { // Since we do not intend for this shared_ptr to manage lifespan, give diff --git a/indra/llcommon/llkeybind.cpp b/indra/llcommon/llkeybind.cpp index e36c1d0a4c..abb0df0b87 100644 --- a/indra/llcommon/llkeybind.cpp +++ b/indra/llcommon/llkeybind.cpp @@ -114,15 +114,6 @@ void LLKeyData::reset() mIgnoreMasks = false; } -LLKeyData& LLKeyData::operator=(const LLKeyData& rhs) -{ - mMouse = rhs.mMouse; - mKey = rhs.mKey; - mMask = rhs.mMask; - mIgnoreMasks = rhs.mIgnoreMasks; - return *this; -} - bool LLKeyData::operator==(const LLKeyData& rhs) { if (mMouse != rhs.mMouse) return false; diff --git a/indra/llcommon/llkeybind.h b/indra/llcommon/llkeybind.h index 1bbb2fadb5..0a9d4600cc 100644 --- a/indra/llcommon/llkeybind.h +++ b/indra/llcommon/llkeybind.h @@ -43,7 +43,6 @@ public: bool isEmpty() const; bool empty() const { return isEmpty(); }; void reset(); - LLKeyData& operator=(const LLKeyData& rhs); bool operator==(const LLKeyData& rhs); bool operator!=(const LLKeyData& rhs); diff --git a/indra/llcommon/llmd5.cpp b/indra/llcommon/llmd5.cpp index c8ca586e7f..b05fd2e21b 100644 --- a/indra/llcommon/llmd5.cpp +++ b/indra/llcommon/llmd5.cpp @@ -255,13 +255,15 @@ void LLMD5::raw_digest(unsigned char* s) const memcpy(s, digest, 16); /* Flawfinder: ignore */ } -#if LL_DARWIN +#if LL_CLANG #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif void LLMD5::hex_digest(char* s) const { + if(!s) return; + if (!finalized) { std::cerr << "LLMD5::hex_digest: Can't get digest if you haven't " @@ -278,7 +280,7 @@ void LLMD5::hex_digest(char* s) const s[32] = '\0'; } -#if LL_DARWIN +#if LL_CLANG #pragma clang diagnostic pop #endif diff --git a/indra/llcommon/llmemory.cpp b/indra/llcommon/llmemory.cpp index 4e44b9a56a..6e6597431d 100644 --- a/indra/llcommon/llmemory.cpp +++ b/indra/llcommon/llmemory.cpp @@ -146,9 +146,6 @@ void LLMemory::updateMemoryInfo() LL_WARNS() << "task_info failed" << LL_ENDL; } #elif defined(LL_LINUX) - // Use sysinfo() to get the total physical memory. - struct sysinfo info; - sysinfo(&info); sAllocatedMemInKB = U32Kilobytes::convert(U64Bytes(LLMemory::getCurrentRSS())); // represents the RAM allocated by this process only (in line with the windows implementation) #else //not valid for other systems for now. diff --git a/indra/llcommon/llmemory.h b/indra/llcommon/llmemory.h index 5ac114cac9..b02458d8d2 100644 --- a/indra/llcommon/llmemory.h +++ b/indra/llcommon/llmemory.h @@ -49,6 +49,9 @@ class LLMutex ; #define LL_ALIGN_OF __align_of__ #endif +#if defined(LL_X86_64) || defined(LL_ARM64) +#define LL_DEFAULT_HEAP_ALIGN 16 +#else #if LL_WINDOWS #define LL_DEFAULT_HEAP_ALIGN 8 #elif LL_DARWIN @@ -56,7 +59,7 @@ class LLMutex ; #elif LL_LINUX || __FreeBSD__ #define LL_DEFAULT_HEAP_ALIGN 8 #endif - +#endif LL_COMMON_API void ll_assert_aligned_func(uintptr_t ptr,U32 alignment); @@ -90,22 +93,6 @@ template <typename T> T* LL_NEXT_ALIGNED_ADDRESS_64(T* address) (uintptr_t(address) + 0x3F) & ~0x3F); } -#if LL_LINUX || LL_DARWIN || __FreeBSD__ - -#define LL_ALIGN_PREFIX(x) -#define LL_ALIGN_POSTFIX(x) __attribute__((aligned(x))) - -#elif LL_WINDOWS - -#define LL_ALIGN_PREFIX(x) __declspec(align(x)) -#define LL_ALIGN_POSTFIX(x) - -#else -#error "LL_ALIGN_PREFIX and LL_ALIGN_POSTFIX undefined" -#endif - -#define LL_ALIGN_16(var) LL_ALIGN_PREFIX(16) var LL_ALIGN_POSTFIX(16) - #define LL_ALIGN_NEW \ public: \ void* operator new(size_t size) \ @@ -128,7 +115,6 @@ public: \ ll_aligned_free_16(ptr); \ } - //------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------ // for enable buffer overrun detection predefine LL_DEBUG_BUFFER_OVERRUN in current library @@ -144,6 +130,10 @@ public: \ LL_PROFILE_ZONE_SCOPED_CATEGORY_MEMORY; #if defined(LL_WINDOWS) void* ret = _aligned_malloc(size, align); + #elif defined(LL_LINUX) + void *ret; + if (0 != posix_memalign(&ret, align, size)) + return nullptr; #else char* aligned = NULL; void* mem = malloc( size + (align - 1) + sizeof(void*) ); @@ -166,6 +156,8 @@ public: \ LL_PROFILE_FREE(ptr); #if defined(LL_WINDOWS) _aligned_free(ptr); + #elif defined(LL_LINUX) + free(ptr); #else if (ptr) { @@ -180,10 +172,10 @@ public: \ inline void* ll_aligned_malloc_16(size_t size) // returned hunk MUST be freed with ll_aligned_free_16(). { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEMORY; -#if defined(LL_WINDOWS) +#if LL_DEFAULT_HEAP_ALIGN == 16 + void* ret = malloc(size); // default osx and 64-bit malloc is 16 byte aligned. +#elif defined(LL_WINDOWS) void* ret = _aligned_malloc(size, 16); -#elif defined(LL_DARWIN) - void* ret = malloc(size); // default osx malloc is 16 byte aligned. #else void *ret; if (0 != posix_memalign(&ret, 16, size)) @@ -197,10 +189,10 @@ inline void ll_aligned_free_16(void *p) { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEMORY; LL_PROFILE_FREE(p); -#if defined(LL_WINDOWS) +#if LL_DEFAULT_HEAP_ALIGN == 16 + free(p); +#elif defined(LL_WINDOWS) _aligned_free(p); -#elif defined(LL_DARWIN) - return free(p); #else free(p); // posix_memalign() is compatible with heap deallocator #endif @@ -210,10 +202,10 @@ inline void* ll_aligned_realloc_16(void* ptr, size_t size, size_t old_size) // r { LL_PROFILE_ZONE_SCOPED_CATEGORY_MEMORY; LL_PROFILE_FREE(ptr); -#if defined(LL_WINDOWS) +#if LL_DEFAULT_HEAP_ALIGN == 16 + void* ret = realloc(ptr,size); // default osx and 64bit malloc is 16 byte aligned. +#elif defined(LL_WINDOWS) void* ret = _aligned_realloc(ptr, size, 16); -#elif defined(LL_DARWIN) - void* ret = realloc(ptr,size); // default osx malloc is 16 byte aligned. #else //FIXME: memcpy is SLOW void* ret = ll_aligned_malloc_16(size); diff --git a/indra/llcommon/llmutex.h b/indra/llcommon/llmutex.h index f3615a1270..bc3d8fed06 100644 --- a/indra/llcommon/llmutex.h +++ b/indra/llcommon/llmutex.h @@ -30,7 +30,7 @@ #include "stdtypes.h" #include "llthread.h" -#include "mutex.h" +#include <mutex> #include <shared_mutex> #include <unordered_map> #include <condition_variable> diff --git a/indra/llcommon/llpreprocessor.h b/indra/llcommon/llpreprocessor.h index 7e2d0b47f7..e2944fd1a4 100644 --- a/indra/llcommon/llpreprocessor.h +++ b/indra/llcommon/llpreprocessor.h @@ -63,35 +63,48 @@ # define LL_UNLIKELY(EXPR) (EXPR) #endif - // Figure out differences between compilers -#if defined(__GNUC__) +#if defined(__MSVC_VER__) || defined(_MSC_VER) + #ifndef LL_MSVC + #define LL_MSVC 1 + #endif + + // Clang CL is MSVC-like but also supports its own macros + #if defined(__clang__) + #ifndef LL_CLANG + #define LL_CLANG 1 + #endif + #endif +#elif defined(__clang__) + #define CLANG_VERSION (__clang_major__ * 10000 \ + + __clang_minor__ * 100 \ + + __clang_patchlevel__) + #ifndef LL_CLANG + #define LL_CLANG 1 + #endif +#elif defined(__GNUC__) #define GCC_VERSION (__GNUC__ * 10000 \ + __GNUC_MINOR__ * 100 \ + __GNUC_PATCHLEVEL__) #ifndef LL_GNUC #define LL_GNUC 1 #endif -#elif defined(__MSVC_VER__) || defined(_MSC_VER) - #ifndef LL_MSVC - #define LL_MSVC 1 - #endif #endif // Set up CPU architecture defines #if LL_MSVC && defined(_M_ARM64) # define LL_ARM64 1 -#elif LL_GNUC && (defined(__arm64__) || defined(__aarch64__)) +#elif (LL_GNUC || LL_CLANG) && (defined(__arm64__) || defined(__aarch64__)) # define LL_ARM64 1 #elif LL_MSVC && _M_X64 # define LL_X86_64 1 # define LL_X86 1 #elif LL_MSVC && _M_IX86 # define LL_X86 1 -#elif LL_GNUC && ( defined(__amd64__) || defined(__x86_64__) ) +#elif (LL_GNUC || LL_CLANG) && ( defined(__amd64__) || defined(__x86_64__) ) # define LL_X86_64 1 # define LL_X86 1 -#elif LL_GNUC && ( defined(__i386__) ) +#elif (LL_GNUC || LL_CLANG) && ( defined(__i386__) ) # define LL_X86 1 #endif @@ -108,16 +121,6 @@ #endif -// Although thread_local is now a standard storage class, we can't just -// #define LL_THREAD_LOCAL as thread_local because the *usage* is different. -// We'll have to take the time to change LL_THREAD_LOCAL declarations by hand. -#if LL_WINDOWS -# define LL_THREAD_LOCAL __declspec(thread) -#else -# define LL_THREAD_LOCAL __thread -#endif - - #if defined(LL_WINDOWS) #define BOOST_REGEX_NO_LIB 1 #define CURL_STATICLIB 1 @@ -142,7 +145,7 @@ #define LL_DLLIMPORT #endif // LL_WINDOWS -#if __clang__ || ! defined(LL_WINDOWS) +#if LL_CLANG || ! defined(LL_WINDOWS) // Only on Windows, and only with the Microsoft compiler (vs. clang) is // wchar_t potentially not a distinct type. #define LL_WCHAR_T_NATIVE 1 @@ -203,6 +206,13 @@ #define LL_PRETTY_FUNCTION __PRETTY_FUNCTION__ #endif +// vptr warning supression funtionality for undefined behavior sanitizer +#if LL_CLANG || LL_GNUC +# define LL_UBSAN_SUPRESS_VPTR __attribute__((no_sanitize("vptr"))) +#else +# define LL_UBSAN_SUPRESS_VPTR +#endif + #ifndef _M_ARM64 #if LL_ARM64 #define GLM_FORCE_NEON 1 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; +} diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h index 773a3e97f4..ec83350476 100644 --- a/indra/llcommon/llprocess.h +++ b/indra/llcommon/llprocess.h @@ -30,8 +30,16 @@ #include "llinitparam.h" #include "llsdparam.h" #include "llexception.h" -#include "apr_thread_proc.h" -#include <boost/optional.hpp> +#include <memory> +#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/signals2.hpp> +#include <boost/asio.hpp> +#include <boost/asio/streambuf.hpp> +#include <boost/asio/readable_pipe.hpp> +#include <boost/asio/writable_pipe.hpp> #include <iosfwd> // std::ostream #if LL_WINDOWS @@ -65,93 +73,31 @@ typedef std::shared_ptr<LLProcess> LLProcessPtr; * indra/llcommon/tests/llprocess_test.cpp for an example of waiting for * child-process termination in a standalone test context. */ -class LL_COMMON_API LLProcess + +class LL_COMMON_API LLProcess : public std::enable_shared_from_this<LLProcess> { LOG_CLASS(LLProcess); public: /** * Specify what to pass for each of child stdin, stdout, stderr. - * @see LLProcess::Params::files. */ - struct FileParam: public LLInitParam::Block<FileParam> + struct FileParam : public LLInitParam::Block<FileParam> { /** * type of file handle to pass to child process - * - * - "" (default): let the child inherit the same file handle used by - * this process. For instance, if passed as stdout, child stdout - * will be interleaved with stdout from this process. In this case, - * @a name is moot and should be left "". - * - * - "file": open an OS filesystem file with the specified @a name. - * <i>Not yet implemented.</i> - * - * - "pipe" or "tpipe" or "npipe": depends on @a name - * - * - @a name.empty(): construct an OS pipe used only for this slot - * of the forthcoming child process. - * - * - ! @a name.empty(): in a global registry, find or create (using - * the specified @a name) an OS pipe. The point of the (purely - * internal) @a name is that passing the same @a name in more than - * one slot for a given LLProcess -- or for slots in different - * LLProcess instances -- means the same pipe. For example, you - * might pass the same @a name value as both stdout and stderr to - * make the child process produce both on the same actual pipe. Or - * you might pass the same @a name as the stdout for one LLProcess - * and the stdin for another to connect the two child processes. - * Use LLProcess::getPipeName() to generate a unique name - * guaranteed not to already exist in the registry. <i>Not yet - * implemented.</i> - * - * The difference between "pipe", "tpipe" and "npipe" is as follows. - * - * - "pipe": direct LLProcess to monitor the parent end of the pipe, - * pumping nonblocking I/O every frame. The expectation (at least - * for stdout or stderr) is that the caller will listen for - * incoming data and consume it as it arrives. It's important not - * to neglect such a pipe, because it's buffered in memory. If you - * suspect the child may produce a great volume of output between - * frames, consider directing the child to write to a filesystem - * file instead, then read the file later. - * - * - "tpipe": do not engage LLProcess machinery to monitor the - * parent end of the pipe. A "tpipe" is used only to connect - * different child processes. As such, it makes little sense to - * pass an empty @a name. <i>Not yet implemented.</i> - * - * - "npipe": like "tpipe", but use an OS named pipe with a - * generated name. Note that @a name is the @em internal name of - * the pipe in our global registry -- it doesn't necessarily have - * anything to do with the pipe's name in the OS filesystem. Use - * LLProcess::getPipeName() to obtain the named pipe's OS - * filesystem name, e.g. to pass it as the @a name to another - * LLProcess instance using @a type "file". This supports usage - * like bash's <(subcommand...) or >(subcommand...) - * constructs. <i>Not yet implemented.</i> - * - * In all cases the open mode (read, write) is determined by the child - * slot you're filling. Child stdin means select the "read" end of a - * pipe, or open a filesystem file for reading; child stdout or stderr - * means select the "write" end of a pipe, or open a filesystem file - * for writing. - * - * Confusion such as passing the same pipe as the stdin of two - * processes (rather than stdout for one and stdin for the other) is - * explicitly permitted: it's up to the caller to construct meaningful - * LLProcess pipe graphs. + * - "" (default): inherit from parent + * - "pipe": create a pipe for I/O + * - "file": open a filesystem file (future enhancement) */ Optional<std::string> type; Optional<std::string> name; - FileParam(const std::string& tp="", const std::string& nm=""): + FileParam(const std::string& tp = "", const std::string& nm = "") : type("type"), name("name") { - // If caller wants to specify values, use explicit assignment to - // set them rather than initialization. - if (! tp.empty()) type = tp; - if (! nm.empty()) name = nm; + if (!tp.empty()) type = tp; + if (!nm.empty()) name = nm; } }; @@ -168,7 +114,8 @@ public: files("files"), postend("postend"), desc("desc") - {} + { + } /// pathname of executable Mandatory<std::string> executable; @@ -254,18 +201,11 @@ public: }; typedef LLSDParamAdapter<Params> LLSDOrParams; - /** - * Factory accepting either plain LLSD::Map or Params block. - * MAY RETURN DEFAULT-CONSTRUCTED LLProcessPtr if params invalid! - */ static LLProcessPtr create(const LLSDOrParams& params); virtual ~LLProcess(); /// Is child process still running? bool isRunning() const; - // static isRunning(LLProcessPtr), getStatus(LLProcessPtr), - // getStatusString(LLProcessPtr), kill(LLProcessPtr) handle the case in - // which the passed LLProcessPtr might be NULL (default-constructed). static bool isRunning(const LLProcessPtr&); /** @@ -284,296 +224,136 @@ public: */ struct Status { - Status(): - mState(UNSTARTED), - mData(0) - {} - - state mState; ///< @see state - /** - * - for mState == EXITED: mData is exit() code - * - for mState == KILLED: mData is signal number (Posix) - * - otherwise: mData is undefined - */ - int mData; + Status() : mState(UNSTARTED), mData(0) {} + state mState; + int mData; // exit code or signal number }; - /// Status query Status getStatus() const; static Status getStatus(const LLProcessPtr&); - /// English Status string query, for logging etc. std::string getStatusString() const; static std::string getStatusString(const std::string& desc, const LLProcessPtr&); - /// English Status string query for previously-captured Status std::string getStatusString(const Status& status) const; - /// static English Status string query static std::string getStatusString(const std::string& desc, const Status& status); - // Attempt to kill the process -- returns true if the process is no longer running when it returns. - // Note that even if this returns false, the process may exit some time after it's called. - bool kill(const std::string& who=""); - static bool kill(const LLProcessPtr& p, const std::string& who=""); + bool kill(const std::string& who = ""); + static bool kill(const LLProcessPtr& p, const std::string& who = ""); + + /// Manually drive pending I/O and check process state. + /// Use this when the mainloop is not yet running or was terminated. + void pump(); #if LL_WINDOWS - typedef int id; ///< as returned by getProcessID() - typedef HANDLE handle; ///< as returned by getProcessHandle() + typedef int id; + typedef HANDLE handle; #else typedef pid_t id; typedef pid_t handle; #endif - /** - * Get an int-like id value. This is primarily intended for a human reader - * to differentiate processes. - */ + id getProcessID() const; - /** - * Get a "handle" of a kind that you might pass to platform-specific API - * functions to engage features not directly supported by LLProcess. - */ handle getProcessHandle() const; + static handle isRunning(handle, const std::string& desc = ""); - /** - * Test if a process (@c handle obtained from getProcessHandle()) is still - * running. Return same nonzero @c handle value if still running, else - * zero, so you can test it like a bool. But if you want to update a - * stored variable as a side effect, you can write code like this: - * @code - * hchild = LLProcess::isRunning(hchild); - * @endcode - * @note This method is intended as a unit-test hook, not as the first of - * a whole set of operations supported on freestanding @c handle values. - * New functionality should be added as nonstatic members operating on - * the same data as getProcessHandle(). - * - * In particular, if child termination is detected by this static isRunning() - * rather than by nonstatic isRunning(), the LLProcess object won't be - * aware of the child's changed status and may encounter OS errors trying - * to obtain it. This static isRunning() is only intended for after the - * launching LLProcess object has been destroyed. - */ - static handle isRunning(handle, const std::string& desc=""); + enum FILESLOT { STDIN = 0, STDOUT = 1, STDERR = 2, NSLOTS = 3 }; - /// Provide symbolic access to child's file slots - enum FILESLOT { STDIN=0, STDOUT=1, STDERR=2, NSLOTS=3 }; + /// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to + /// create a pipe at the corresponding FILESLOT. + struct NoPipe : public LLException + { + NoPipe(const std::string& what) : LLException(what) {} + }; - /** - * For a pipe constructed with @a type "npipe", obtain the generated OS - * filesystem name for the specified pipe. Otherwise returns the empty - * string. @see LLProcess::FileParam::type - */ std::string getPipeName(FILESLOT) const; - /// base of ReadPipe, WritePipe + /// Base class for pipes class LL_COMMON_API BasePipe { public: - virtual ~BasePipe() = 0; - + virtual ~BasePipe() = default; typedef std::size_t size_type; static const size_type npos; - - /** - * Get accumulated buffer length. - * - * For WritePipe, is there still pending data to send to child? - * - * For ReadPipe, we often need to refrain from actually reading the - * std::istream returned by get_istream() until we've accumulated - * enough data to make it worthwhile. For instance, if we're expecting - * a number from the child, but the child happens to flush "12" before - * emitting "3\n", get_istream() >> myint could return 12 rather than - * 123! - */ virtual size_type size() const = 0; }; - /// As returned by getWritePipe() or getOptWritePipe() - class WritePipe: public BasePipe + /// Write pipe for stdin + class WritePipe : public BasePipe { public: - /** - * Get ostream& on which to write to child's stdin. - * - * @usage - * @code - * myProcess->getWritePipe().get_ostream() << "Hello, child!" << std::endl; - * @endcode - */ virtual std::ostream& get_ostream() = 0; + // Called each mainloop tick to initiate any pending async writes. + virtual void tick() {} }; - /// As returned by getReadPipe() or getOptReadPipe() - class ReadPipe: public BasePipe + /// Read pipe for stdout/stderr + class ReadPipe : public BasePipe { public: - /** - * Get istream& on which to read from child's stdout or stderr. - * - * @usage - * @code - * std::string stuff; - * myProcess->getReadPipe().get_istream() >> stuff; - * @endcode - * - * You should be sure in advance that the ReadPipe in question can - * fill the request. @see getPump() - */ virtual std::istream& get_istream() = 0; - - /** - * Like std::getline(get_istream(), line), but trims off trailing '\r' - * to make calling code less platform-sensitive. - */ virtual std::string getline() = 0; - - /** - * Like get_istream().read(buffer, n), but returns std::string rather - * than requiring caller to construct a buffer, etc. - */ virtual std::string read(size_type len) = 0; + virtual std::string peek(size_type offset = 0, size_type len = npos) const = 0; - /** - * Peek at accumulated buffer data without consuming it. Optional - * parameters give you substr() functionality. - * - * @note You can discard buffer data using get_istream().ignore(n). - */ - virtual std::string peek(size_type offset=0, size_type len=npos) const = 0; - - /** - * Detect presence of a substring (or char) in accumulated buffer data - * without retrieving it. Optional offset allows you to search from - * specified position. - */ template <typename SEEK> - bool contains(SEEK seek, size_type offset=0) const - { return find(seek, offset) != npos; } - - /** - * Search for a substring in accumulated buffer data without - * retrieving it. Returns size_type position at which found, or npos - * meaning not found. Optional offset allows you to search from - * specified position. - */ - virtual size_type find(const std::string& seek, size_type offset=0) const = 0; + bool contains(SEEK seek, size_type offset = 0) const + { + return find(seek, offset) != npos; + } - /** - * Search for a char in accumulated buffer data without retrieving it. - * Returns size_type position at which found, or npos meaning not - * found. Optional offset allows you to search from specified - * position. - */ - virtual size_type find(char seek, size_type offset=0) const = 0; + virtual size_type find(const std::string& seek, size_type offset = 0) const = 0; + virtual size_type find(char seek, size_type offset = 0) const = 0; - /** - * Get LLEventPump& on which to listen for incoming data. The posted - * LLSD::Map event will contain: - * - * - "data" part of pending data; see setLimit() - * - "len" entire length of pending data, regardless of setLimit() - * - "slot" this ReadPipe's FILESLOT, e.g. LLProcess::STDOUT - * - "name" e.g. "stdout" - * - "desc" e.g. "SLPlugin (pid) stdout" - * - "eof" @c true means there no more data will arrive on this pipe, - * therefore no more events on this pump - * - * If the child sends "abc", and this ReadPipe posts "data"="abc", but - * you don't consume it by reading the std::istream returned by - * get_istream(), and the child next sends "def", ReadPipe will post - * "data"="abcdef". - */ virtual LLEventPump& getPump() = 0; - - /** - * Set maximum length of buffer data that will be posted in the LLSD - * announcing arrival of new data from the child. If you call - * setLimit(5), and the child sends "abcdef", the LLSD event will - * contain "data"="abcde". However, you may still read the entire - * "abcdef" from get_istream(): this limit affects only the size of - * the data posted with the LLSD event. If you don't call this method, - * @em no data will be posted: the default is 0 bytes. - */ virtual void setLimit(size_type limit) = 0; - - /** - * Query the current setLimit() limit. - */ virtual size_type getLimit() const = 0; + // True once the pipe has observed child-process EOF. + virtual bool atEOF() const = 0; }; - /// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to - /// create a pipe at the corresponding FILESLOT. - struct NoPipe: public LLException - { - NoPipe(const std::string& what): LLException(what) {} - }; - - /** - * Get a reference to the (only) WritePipe for this LLProcess. @a slot, if - * specified, must be STDIN. Throws NoPipe if you did not request a "pipe" - * for child stdin. Use this method when you know how you created the - * LLProcess in hand. - */ - WritePipe& getWritePipe(FILESLOT slot=STDIN); + WritePipe& getWritePipe(FILESLOT slot = STDIN); + ReadPipe& getReadPipe(FILESLOT index); + WritePipe* getOptWritePipe(FILESLOT slot = STDIN); + ReadPipe* getOptReadPipe(FILESLOT index); - /** - * Get a boost::optional<WritePipe&> to the (only) WritePipe for this - * LLProcess. @a slot, if specified, must be STDIN. The return value is - * empty if you did not request a "pipe" for child stdin. Use this method - * for inspecting an LLProcess you did not create. - */ - boost::optional<WritePipe&> getOptWritePipe(FILESLOT slot=STDIN); + static std::string basename(const std::string& path); + static std::string getline(std::istream& in); - /** - * Get a reference to one of the ReadPipes for this LLProcess. @a slot, if - * specified, must be STDOUT or STDERR. Throws NoPipe if you did not - * request a "pipe" for child stdout or stderr. Use this method when you - * know how you created the LLProcess in hand. - */ - ReadPipe& getReadPipe(FILESLOT slot); + // Constructor is public for the sake of make_shared + // but create() should be used instead for proper initialization. + LLProcess(const Params& params); - /** - * Get a boost::optional<ReadPipe&> to one of the ReadPipes for this - * LLProcess. @a slot, if specified, must be STDOUT or STDERR. The return - * value is empty if you did not request a "pipe" for child stdout or - * stderr. Use this method for inspecting an LLProcess you did not create. - */ - boost::optional<ReadPipe&> getOptReadPipe(FILESLOT slot); +private: + void launch(const LLSDOrParams& params); + void connectMainloop(); + void tick(); + void handleExit(Status exitStatus); - /// little utilities that really should already be somewhere else in the - /// code base - static std::string basename(const std::string& path); - static std::string getline(std::istream&); + // Boost.Process v2 components + boost::asio::io_context mIOContext; + std::unique_ptr<boost::process::v2::process> mChild; - // Non-copyable - LLProcess(const LLProcess&) = delete; - LLProcess& operator=(const LLProcess&) = delete; + // Pipes - using Boost.Asio pipes directly (v2 no longer has async_pipe) + // From parent's perspective: write to stdin (writable_pipe), read from stdout/stderr (readable_pipe) + // std::shared_ptr so WritePipeImpl/ReadPipeImpl keep the pipe alive as + // long as async operations are in flight + std::shared_ptr<boost::asio::writable_pipe> mStdinPipe; + std::shared_ptr<boost::asio::readable_pipe> mStdoutPipe; + std::shared_ptr<boost::asio::readable_pipe> mStderrPipe; -private: - /// constructor is private: use create() instead - LLProcess(const LLSDOrParams& params); - void autokill(); - // Classic-C-style APR callback - static void status_callback(int reason, void* data, int status); - // Object-oriented callback - void handle_status(int reason, int status); - // implementation for get[Opt][Read|Write]Pipe() - template <class PIPETYPE> - PIPETYPE& getPipe(FILESLOT slot); - template <class PIPETYPE> - boost::optional<PIPETYPE&> getOptPipe(FILESLOT slot); - template <class PIPETYPE> - PIPETYPE* getPipePtr(std::string& error, FILESLOT slot); + // Our pipe wrapper implementations + std::unique_ptr<WritePipe> mWritePipe; + std::unique_ptr<ReadPipe> mStdoutReadPipe; + std::unique_ptr<ReadPipe> mStderrReadPipe; + Status mStatus; std::string mDesc; std::string mPostend; - apr_proc_t mProcess; - bool mAutokill, mAttached; - Status mStatus; - // explicitly want this ptr_vector to be able to store NULLs - typedef std::vector<std::unique_ptr<BasePipe>> PipeVector; - PipeVector mPipes; - apr_pool_t* mPool; + bool mAutokill; + bool mAttached; + bool mKillCalled; + + // For integrating with LLEventPump mainloop + boost::signals2::scoped_connection mMainloopConnection; }; /// for logging diff --git a/indra/llcommon/llprocessor.cpp b/indra/llcommon/llprocessor.cpp index bb3a35a1e3..a83aeb8477 100644 --- a/indra/llcommon/llprocessor.cpp +++ b/indra/llcommon/llprocessor.cpp @@ -123,6 +123,9 @@ namespace eSSE4_1_Features = 38, eSSE4_2_Features = 39, eSSE4a_Features = 40, + eAVX_Features = 41, + eAVX2_Features = 42, + eAVX512F_Features = 43, }; const char* cpu_feature_names[] = @@ -170,6 +173,9 @@ namespace "SSE4.1 Instructions", "SSE4.2 Instructions", "SSE4a Instructions", + "AVX Instructions", // 41 + "AVX2 Instructions", // 42 + "AVX-512F Instructions", // 43 }; std::string intel_CPUFamilyName(int composed_family) @@ -283,6 +289,21 @@ public: return hasExtension(cpu_feature_names[eSSE4a_Features]); } + bool hasAVX() const + { + return hasExtension(cpu_feature_names[eAVX_Features]); + } + + bool hasAVX2() const + { + return hasExtension(cpu_feature_names[eAVX2_Features]); + } + + bool hasAVX512F() const + { + return hasExtension(cpu_feature_names[eAVX512F_Features]); + } + bool hasAltivec() const { return hasExtension("Altivec"); @@ -602,6 +623,12 @@ private: setExtension(cpu_feature_names[eSSE4_2_Features]); } + // AVX: CPUID leaf 1, ECX bit 28, OSXSAVE + XCR0[1:2] enabled + if ((cpu_info[2] & 0x18000000) == 0x18000000 && ((_xgetbv(0) & 0x6) == 0x6)) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + unsigned int feature_info = (unsigned int) cpu_info[3]; for(unsigned int index = 0, bit = 1; index < eSSE3_Features; ++index, bit <<= 1) { @@ -611,6 +638,27 @@ private: } } } + else if (i == 7) + { + // AVX2/AVX-512* require AVX to be usable (OSXSAVE + XCR0 state) + if (hasExtension(cpu_feature_names[eAVX_Features])) + { + int cpu_info7[4] = { -1 }; + __cpuidex(cpu_info7, 7, 0); + // AVX2: EBX bit 5 + if (cpu_info7[1] & 0x20) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + // AVX-512F: EBX bit 16; ZMM state in XCR0 (bits 5-7) + const unsigned long long xcr0 = _xgetbv(0); + if ((cpu_info7[1] & 0x10000) && ((xcr0 & 0xE6) == 0xE6)) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + } + } } // Calling __cpuid with 0x80000000 as the InfoType argument @@ -804,6 +852,29 @@ private: // Not supposed to happen? setExtension(cpu_feature_names[eSSE4a_Features]); } + if (cpu_features_str.find(" AVX1.0 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + // AVX2 and AVX-512F are reported in machdep.cpu.leaf7_features on macOS + char cpu_leaf7_features[1024]; + len = sizeof(cpu_leaf7_features); + memset(cpu_leaf7_features, 0, len); + sysctlbyname("machdep.cpu.leaf7_features", (void*)cpu_leaf7_features, &len, NULL, 0); + + std::string cpu_leaf7_str(cpu_leaf7_features); + cpu_leaf7_str = " " + cpu_leaf7_str + " "; + + if (cpu_leaf7_str.find(" AVX2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (cpu_leaf7_str.find(" AVX512F ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } } }; @@ -918,9 +989,11 @@ public: const char CPUINFO_FILE[] = "/proc/cpuinfo"; -class LLProcessorInfoLinuxImpl : public LLProcessorInfoImpl { +class LLProcessorInfoLinuxImpl : public LLProcessorInfoImpl +{ public: - LLProcessorInfoLinuxImpl() { + LLProcessorInfoLinuxImpl() + { get_proc_cpuinfo(); } @@ -933,7 +1006,7 @@ private: // Nicky: We just look into cpu0. In theory we could iterate over all cores // "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq" // But those should not fluctuate that much? - std::ifstream fIn { "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" }; + llifstream fIn{ "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" }; if( !fIn.is_open() ) return 0.0; @@ -954,7 +1027,7 @@ private: void get_proc_cpuinfo() { std::map< std::string, std::string > cpuinfo; - LLFILE* cpuinfo_fp = LLFile::fopen(CPUINFO_FILE, "rb"); + LLFILE* cpuinfo_fp = LLFile::fopen(CPUINFO_FILE, LLFILE_MODE("rb")); if(cpuinfo_fp) { char line[MAX_STRING]; @@ -979,7 +1052,7 @@ private: std::string llinename(linename); LLStringUtil::toLower(llinename); std::string lineval( spacespot + 1, nlspot ); - cpuinfo[ llinename ] = lineval; + cpuinfo[ llinename ] = lineval; } fclose(cpuinfo_fp); } @@ -1063,6 +1136,21 @@ private: setExtension(cpu_feature_names[eSSE4a_Features]); } + if (flags.find(" avx ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + if (flags.find(" avx2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (flags.find(" avx512f ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + # endif // LL_X86 } @@ -1071,7 +1159,7 @@ private: std::ostringstream s; // *NOTE:Mani - This is for linux only. - LLFILE* cpuinfo = LLFile::fopen(CPUINFO_FILE, "rb"); + LLFILE* cpuinfo = LLFile::fopen(CPUINFO_FILE, LLFILE_MODE("rb")); if(cpuinfo) { char line[MAX_STRING]; @@ -1130,6 +1218,9 @@ bool LLProcessorInfo::hasSSE3S() const { return mImpl->hasSSE3S(); } bool LLProcessorInfo::hasSSE41() const { return mImpl->hasSSE41(); } bool LLProcessorInfo::hasSSE42() const { return mImpl->hasSSE42(); } bool LLProcessorInfo::hasSSE4a() const { return mImpl->hasSSE4a(); } +bool LLProcessorInfo::hasAVX() const { return mImpl->hasAVX(); } +bool LLProcessorInfo::hasAVX2() const { return mImpl->hasAVX2(); } +bool LLProcessorInfo::hasAVX512F() const { return mImpl->hasAVX512F(); } bool LLProcessorInfo::hasAltivec() const { return mImpl->hasAltivec(); } std::string LLProcessorInfo::getCPUFamilyName() const { return mImpl->getCPUFamilyName(); } std::string LLProcessorInfo::getCPUBrandName() const { return mImpl->getCPUBrandName(); } diff --git a/indra/llcommon/llprocessor.h b/indra/llcommon/llprocessor.h index b955f7f55c..da93a531f4 100644 --- a/indra/llcommon/llprocessor.h +++ b/indra/llcommon/llprocessor.h @@ -46,6 +46,9 @@ public: bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; bool hasAltivec() const; std::string getCPUFamilyName() const; std::string getCPUBrandName() const; diff --git a/indra/llcommon/llprocinfo.cpp b/indra/llcommon/llprocinfo.cpp index 69be00e14a..9fe4af2103 100644 --- a/indra/llcommon/llprocinfo.cpp +++ b/indra/llcommon/llprocinfo.cpp @@ -25,6 +25,7 @@ * $/LicenseInfo$ */ +#include "linden_common.h" #include "llprocinfo.h" diff --git a/indra/llcommon/llprofiler.cpp b/indra/llcommon/llprofiler.cpp index bdddabf977..b2ac7723d4 100644 --- a/indra/llcommon/llprofiler.cpp +++ b/indra/llcommon/llprofiler.cpp @@ -25,6 +25,6 @@ * $/LicenseInfo$ */ -#include "linden_common.h" +#include "llpreprocessor.h" #include "TracyClient.cpp" diff --git a/indra/llcommon/llprofiler.h b/indra/llcommon/llprofiler.h index fa94f78019..7df8f7b302 100644 --- a/indra/llcommon/llprofiler.h +++ b/indra/llcommon/llprofiler.h @@ -76,7 +76,16 @@ #if defined(LL_PROFILER_CONFIGURATION) && (LL_PROFILER_CONFIGURATION > LL_PROFILER_CONFIG_NONE) #if LL_PROFILER_CONFIGURATION == LL_PROFILER_CONFIG_TRACY || LL_PROFILER_CONFIGURATION == LL_PROFILER_CONFIG_TRACY_FAST_TIMER + #include "llpreprocessor.h" + +#if defined(LL_GNUC) && GCC_VERSION >= 130000 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wnonnull" +#endif #include "tracy/Tracy.hpp" +#if defined(LL_GNUC) && GCC_VERSION >= 130000 +# pragma GCC diagnostic push +#endif // Enable RenderDoc labeling //#define LL_PROFILER_ENABLE_RENDER_DOC 0 diff --git a/indra/llcommon/llprofilercategories.h b/indra/llcommon/llprofilercategories.h index 1c4f0f5624..4fc1d7f159 100644 --- a/indra/llcommon/llprofilercategories.h +++ b/indra/llcommon/llprofilercategories.h @@ -48,6 +48,7 @@ #define LL_PROFILER_CATEGORY_ENABLE_DRAWPOOL 1 #define LL_PROFILER_CATEGORY_ENABLE_ENVIRONMENT 1 #define LL_PROFILER_CATEGORY_ENABLE_FACE 1 +//#define LL_PROFILER_CATEGORY_ENABLE_FILE 1 #define LL_PROFILER_CATEGORY_ENABLE_LLSD 1 #define LL_PROFILER_CATEGORY_ENABLE_LOGGING 1 #define LL_PROFILER_CATEGORY_ENABLE_MATERIAL 1 @@ -69,6 +70,8 @@ #define LL_PROFILER_CATEGORY_ENABLE_WIN32 1 #define LL_PROFILER_CATEGORY_ENABLE_GLTF 1 #define LL_PROFILER_CATEGORY_ENABLE_VOICE 1 +#define LL_PROFILER_CATEGORY_ENABLE_WEBSOCKET 1 +#define LL_PROFILER_CATEGORY_ENABLE_SCRIPTDEV 1 #if LL_PROFILER_CATEGORY_ENABLE_APP #define LL_PROFILE_ZONE_NAMED_CATEGORY_APP LL_PROFILE_ZONE_NAMED @@ -126,6 +129,14 @@ #define LL_PROFILE_ZONE_SCOPED_CATEGORY_FACE #endif +#if LL_PROFILER_CATEGORY_ENABLE_FILE +#define LL_PROFILE_ZONE_NAMED_CATEGORY_FILE LL_PROFILE_ZONE_NAMED +#define LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE LL_PROFILE_ZONE_SCOPED +#else +#define LL_PROFILE_ZONE_NAMED_CATEGORY_FILE(name) +#define LL_PROFILE_ZONE_SCOPED_CATEGORY_FILE +#endif + #if LL_PROFILER_CATEGORY_ENABLE_LLSD #define LL_PROFILE_ZONE_NAMED_CATEGORY_LLSD LL_PROFILE_ZONE_NAMED #define LL_PROFILE_ZONE_SCOPED_CATEGORY_LLSD LL_PROFILE_ZONE_SCOPED @@ -293,5 +304,22 @@ #define LL_PROFILE_ZONE_SCOPED_CATEGORY_VOICE #endif +#if LL_PROFILER_CATEGORY_ENABLE_WEBSOCKET + #define LL_PROFILE_ZONE_NAMED_CATEGORY_WEBSOCKET LL_PROFILE_ZONE_NAMED + #define LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET LL_PROFILE_ZONE_SCOPED +#else + #define LL_PROFILE_ZONE_NAMED_CATEGORY_WEBSOCKET(name) + #define LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET +#endif + +#if LL_PROFILER_CATEGORY_ENABLE_SCRIPTDEV + #define LL_PROFILE_ZONE_NAMED_CATEGORY_SCRIPTDEV LL_PROFILE_ZONE_NAMED + #define LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV LL_PROFILE_ZONE_SCOPED +#else + #define LL_PROFILE_ZONE_NAMED_CATEGORY_SCRIPTDEV(name) + #define LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV +#endif + + #endif // LL_PROFILER_CATEGORIES_H diff --git a/indra/llcommon/llqueuedthread.cpp b/indra/llcommon/llqueuedthread.cpp index efeeb1340e..8c740956b3 100644 --- a/indra/llcommon/llqueuedthread.cpp +++ b/indra/llcommon/llqueuedthread.cpp @@ -222,6 +222,33 @@ void LLQueuedThread::waitOnPending() return; } +void LLQueuedThread::waitOnPending(F32 max_time_sec) +{ + LLTimer wait_timer; + wait_timer.reset(); + while (1) + { + update(0); + + if (mIdleThread) + { + break; + } + + if (wait_timer.getElapsedTimeF64() >= max_time_sec) + { + LL_WARNS() << "waitOnPending() timed out after " << max_time_sec + << " seconds in thread: " << mName << LL_ENDL; + break; + } + + if (mThreaded) + { + yield(); + } + } +} + // MAIN thread void LLQueuedThread::printQueueStats() { diff --git a/indra/llcommon/llqueuedthread.h b/indra/llcommon/llqueuedthread.h index de50b8ae95..95f0e58fa8 100644 --- a/indra/llcommon/llqueuedthread.h +++ b/indra/llcommon/llqueuedthread.h @@ -141,6 +141,7 @@ public: size_t updateQueue(F32 max_time_ms); void waitOnPending(); + void waitOnPending(F32 max_time_sec); void printQueueStats(); virtual size_t getPending(); diff --git a/indra/llcommon/llrand.cpp b/indra/llcommon/llrand.cpp index 2c51e6f07f..513613f543 100644 --- a/indra/llcommon/llrand.cpp +++ b/indra/llcommon/llrand.cpp @@ -58,9 +58,26 @@ * to restore uniform distribution. */ -// gRandomGenerator is a stateful static object, which is therefore not +// pRandomGenerator is a stateful static object, which is therefore not // inherently thread-safe. -static thread_local LLRandLagFib2281 gRandomGenerator(LLUUID::getRandomSeed()); +//We use a pointer to not construct a huge object in the TLS space, sadly this is necessary +// due to libcef.so on Linux being compiled with TLS model initial-exec (resulting in +// FLAG STATIC_TLS, see readelf libcef.so). CEFs own TLS objects + LLRandLagFib2281 then will exhaust the +// available TLS space, causing media failure. + +static thread_local std::unique_ptr< LLRandLagFib2281 > pRandomGenerator = nullptr; + +namespace { + F64 ll_internal_get_rand() + { + if( !pRandomGenerator ) + { + pRandomGenerator.reset(new LLRandLagFib2281(LLUUID::getRandomSeed( ) )); + } + + return(*pRandomGenerator)(); + } +} // no default implementation, only specific F64 and F32 specializations template <typename REAL> @@ -73,7 +90,7 @@ inline F64 ll_internal_random<F64>() // CPUs (or at least multi-threaded processes) seem to // occasionally give an obviously incorrect random number -- like // 5^15 or something. Sooooo, clamp it as described above. - F64 rv{ gRandomGenerator() }; + F64 rv{ ll_internal_get_rand() }; if(!((rv >= 0.0) && (rv < 1.0))) return fmod(rv, 1.0); return rv; } @@ -85,7 +102,7 @@ inline F32 ll_internal_random<F32>() // Per Monty, it's important to clamp using the correct fmodf() rather // than expanding to F64 for fmod() and then truncating back to F32. Prior // to this change, we were getting sporadic ll_frand() == 1.0 results. - F32 rv{ narrow<F64>(gRandomGenerator()) }; + F32 rv{ narrow<F64>(ll_internal_get_rand()) }; if(!((rv >= 0.0f) && (rv < 1.0f))) return fmodf(rv, 1.0f); return rv; } diff --git a/indra/llcommon/llsd.cpp b/indra/llcommon/llsd.cpp index ce39bc7af3..ad2ce231fa 100644 --- a/indra/llcommon/llsd.cpp +++ b/indra/llcommon/llsd.cpp @@ -32,7 +32,6 @@ #include "llbase64.h" #include "llerror.h" -#include "../llmath/llmath.h" #include "llformat.h" #include "llsdserialize.h" #include "stringize.h" @@ -297,10 +296,10 @@ namespace }; LLSD::Boolean ImplReal::asBoolean() const - { return !llisnan(mValue) && mValue != 0.0; } + { return !std::isnan(mValue) && mValue != 0.0; } LLSD::Integer ImplReal::asInteger() const - { return !llisnan(mValue) ? (LLSD::Integer)mValue : 0; } + { return !std::isnan(mValue) ? (LLSD::Integer)mValue : 0; } LLSD::String ImplReal::asString() const { return llformat("%lg", mValue); } diff --git a/indra/llcommon/llsdjson.cpp b/indra/llcommon/llsdjson.cpp index 71f35802c5..e42bac4c70 100644 --- a/indra/llcommon/llsdjson.cpp +++ b/indra/llcommon/llsdjson.cpp @@ -26,14 +26,12 @@ // Must turn on conditional declarations in header file so definitions end up // with proper linkage. -#define LLSD_DEBUG_INFO #include "linden_common.h" #include "llsdjson.h" #include "llsdutil.h" #include "llerror.h" -#include "../llmath/llmath.h" #if LL_WINDOWS #include <boost/json.hpp> diff --git a/indra/llcommon/llsdserialize.cpp b/indra/llcommon/llsdserialize.cpp index f850238dff..323b3a0c52 100644 --- a/indra/llcommon/llsdserialize.cpp +++ b/indra/llcommon/llsdserialize.cpp @@ -1421,11 +1421,7 @@ S32 LLSDNotationFormatter::format_impl(const LLSD& data, std::ostream& ostr, case LLSD::TypeBoolean: if(mBoolAlpha || -#if( LL_WINDOWS || __GNUC__ > 2) (ostr.flags() & std::ios::boolalpha) -#else - (ostr.flags() & 0x0100) -#endif ) { ostr << (data.asBoolean() @@ -2391,6 +2387,7 @@ U8* unzip_llsdNavMesh( bool& valid, size_t& outsize, std::istream& is, S32 size { case Z_NEED_DICT: ret = Z_DATA_ERROR; + [[fallthrough]]; case Z_DATA_ERROR: case Z_MEM_ERROR: inflateEnd(&strm); diff --git a/indra/llcommon/llsdserialize_xml.cpp b/indra/llcommon/llsdserialize_xml.cpp index afe4fd63a6..704e433139 100644 --- a/indra/llcommon/llsdserialize_xml.cpp +++ b/indra/llcommon/llsdserialize_xml.cpp @@ -63,15 +63,80 @@ S32 LLSDXMLFormatter::format(const LLSD& data, std::ostream& ostr, EFormatterOptions options) const { std::streamsize old_precision = ostr.precision(25); + std::ios_base::iostate old_exceptions = ostr.exceptions(); + // Merged exception mask: preserve the caller's bits and add failbit|badbit + // for I/O error detection, so we never drop bits the caller already enabled. + std::ios_base::iostate new_exceptions = + old_exceptions | std::ios_base::badbit | std::ios_base::failbit; + // Bits we are newly adding (not already in the caller's mask). + std::ios_base::iostate added_bits = new_exceptions & ~old_exceptions; - std::string post; - if (options & LLSDFormatter::OPTIONS_PRETTY) + // If the stream already has error-state bits that we would newly add to the + // exception mask, enabling those bits would throw immediately; bail out early. + if (added_bits && (ostr.rdstate() & added_bits)) { - post = "\n"; + LL_WARNS() << "LLSDXMLFormatter::format: Stream already in error state" << LL_ENDL; + ostr.precision(old_precision); + return -1; + } + + S32 rv = 0; + + try + { + // Enable the merged exception mask to detect I/O errors during formatting. + if (added_bits) + { + ostr.exceptions(new_exceptions); + } + + std::string post; + if (options & LLSDFormatter::OPTIONS_PRETTY) + { + post = "\n"; + } + ostr << "<llsd>" << post; + rv = format_impl(data, ostr, options, 1); + ostr << "</llsd>\n"; + } + catch (const std::ios_base::failure& e) + { + LL_WARNS() << "LLSDXMLFormatter::format: Stream I/O exception: " << e.what() + << " - Stream state: good=" << ostr.good() + << " eof=" << ostr.eof() + << " fail=" << ostr.fail() + << " bad=" << ostr.bad() << LL_ENDL; + rv = -1; + } + catch (const std::bad_alloc&) + { + // we might be saving something massive, don't error or crash + LL_WARNS() << "LLSDXMLFormatter::format: Memory allocation failed during formatting" << LL_ENDL; + rv = -1; + } + catch (const std::exception& e) + { + LL_WARNS() << "LLSDXMLFormatter::format: Standard exception: " << e.what() << LL_ENDL; + rv = -1; + } + catch (...) + { + LL_WARNS() << "LLSDXMLFormatter::format: Unknown exception during formatting" << LL_ENDL; + rv = -1; + } + + // Restore original exception mask. First set to goodbit (never throws) so + // the subsequent restore call won't immediately throw if the stream is in + // error state for bits in old_exceptions. + try + { + ostr.exceptions(std::ios_base::goodbit); + ostr.exceptions(old_exceptions); + } + catch (...) + { + LL_WARNS() << "LLSDXMLFormatter::format: failed to restore exceptions" << LL_ENDL; } - ostr << "<llsd>" << post; - S32 rv = format_impl(data, ostr, options, 1); - ostr << "</llsd>\n"; ostr.precision(old_precision); return rv; diff --git a/indra/llcommon/llsdutil.cpp b/indra/llcommon/llsdutil.cpp index 9fa27cee78..e23d274724 100644 --- a/indra/llcommon/llsdutil.cpp +++ b/indra/llcommon/llsdutil.cpp @@ -503,7 +503,7 @@ bool filter_llsd_with_template( * Helpers for llsd_matches() *****************************************************************************/ // raw data used for LLSD::Type lookup -struct Data +struct LLSDData { LLSD::Type type; const char* name; @@ -534,7 +534,7 @@ public: { LL_PROFILE_ZONE_SCOPED; - for (const Data *di(boost::begin(typedata)), *dend(boost::end(typedata)); di != dend; ++di) + for (const LLSDData*di(boost::begin(typedata)), *dend(boost::end(typedata)); di != dend; ++di) { mMap[di->type] = di->name; } diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index e6989211ae..ebef2489ce 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -30,7 +30,7 @@ #include <typeinfo> #include <unordered_set> #include <vector> -#include "mutex.h" +#include <mutex> #include "lockstatic.h" #include "llthread.h" // on_main_thread() #include "llmainthreadtask.h" diff --git a/indra/llcommon/llstl.h b/indra/llcommon/llstl.h index 7a1c7caf82..bb8055ed46 100644 --- a/indra/llcommon/llstl.h +++ b/indra/llcommon/llstl.h @@ -36,6 +36,7 @@ #include <set> #include <typeindex> #include <typeinfo> +#include <boost/iterator/filter_iterator.hpp> #ifdef LL_LINUX // <ND> For strcmp @@ -519,18 +520,8 @@ llbind2nd(const _Operation& __oper, const _Tp& __x) inline bool before(const std::type_info* lhs, const std::type_info* rhs) { -#if LL_LINUX && defined(__GNUC__) && ((__GNUC__ < 4) || (__GNUC__ == 4 && __GNUC_MINOR__ < 4)) - // If we're building on Linux with gcc, and it's either gcc 3.x or - // 4.{0,1,2,3}, then we have to use a workaround. Note that we use gcc on - // Mac too, and some people build with gcc on Windows (cygwin or mingw). - // On Linux, different load modules may produce different type_info* - // pointers for the same type. Have to compare name strings to get good - // results. - return strcmp(lhs->name(), rhs->name()) < 0; -#else // not Linux, or gcc 4.4+ // Just use before(), as we normally would return lhs->before(*rhs); -#endif } /** @@ -706,6 +697,210 @@ struct ll_template_cast_impl<DEST, SOURCE> \ } \ } +//----------------------------------------------- +namespace LL +{ + /** + * @brief A range adapter that provides filtered iteration over a container. + * + * filter_range creates a filtered view of an iterator range using a predicate function. + * Only elements that satisfy the predicate will be accessible when iterating through + * the range. This is useful for processing subsets of containers without copying data. + * + * The class uses boost::filter_iterator internally to provide the filtering functionality. + * + * @tparam Predicate A callable object (function, functor, lambda) that takes an element + * from the iterator range and returns true if the element should be + * included in the filtered range. + * @tparam Iterator The iterator type for the underlying container/range. + * + * Example usage: + * @code + * std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + * + * // Create a predicate to filter even numbers + * auto is_even = [](int n) { return n % 2 == 0; }; + * + * // Create filtered range using make_filter helper + * auto even_range = LL::make_filter(is_even, numbers.begin(), numbers.end()); + * + * // Iterate through only even numbers + * for (auto value : even_range) { + * std::cout << value << " "; // Prints: 2 4 6 8 10 + * } + * + * // Or manually construct the filter_range + * LL::filter_range<decltype(is_even), std::vector<int>::iterator> + * manual_range(is_even, numbers.begin(), numbers.end()); + * @endcode + * + * @note This class provides a lightweight view over the original data. + * No copying of elements occurs, making it efficient for large containers. + * @note The predicate is applied during iteration, so complex predicates may + * impact performance for frequently-accessed ranges. + * @note The underlying container must remain valid for the lifetime of the filter_range. + * + * @see make_filter() for a convenient factory function + * @see boost::filter_iterator for the underlying implementation details + */ + template<typename Predicate, typename Iterator> + class filter_range + { + public: + /// The filtered iterator type - combines predicate with base iterator + using filter_iter = boost::filter_iterator<Predicate, Iterator>; + + /// Value type of the filtered elements + using value_type = typename std::iterator_traits<Iterator>::value_type; + + /// Iterator type for range-based for loops and STL algorithms + using iterator = filter_iter; + using const_iterator = filter_iter; + + /** + * @brief Constructs a filter_range with the given predicate and iterator range. + * + * @param pred The predicate function/functor to filter elements. + * Must be callable with signature: bool(const value_type&) + * @param begin Iterator to the beginning of the range to filter + * @param end Iterator to the end of the range to filter + * + * @pre begin and end must form a valid iterator range + * @pre pred must be a valid callable that can be invoked with elements from [begin, end) + */ + filter_range(Predicate pred, Iterator begin, Iterator end) + : begin_(pred, begin, end), end_(pred, end, end) {} + + /** + * @brief Returns an iterator to the first element that satisfies the predicate. + * + * @return filter_iter Iterator pointing to the first filtered element, + * or equal to end() if no elements satisfy the predicate. + */ + filter_iter begin() const { return begin_; } + + /** + * @brief Returns an iterator representing the end of the filtered range. + * + * @return filter_iter Past-the-end iterator for the filtered range. + */ + filter_iter end() const { return end_; } + + /** + * @brief Checks if the filtered range is empty. + * + * @return true if no elements in the range satisfy the predicate, false otherwise. + * + * @note This operation has O(1) complexity as it only compares iterators. + */ + bool empty() const { return begin_ == end_; } + + private: + filter_iter begin_; ///< Iterator to first element satisfying predicate + filter_iter end_; ///< Past-the-end iterator for the filtered range + }; + + /** + * @brief Factory function to create a filter_range with automatic template deduction. + * + * This convenience function eliminates the need to explicitly specify template parameters + * when creating a filter_range. The template parameters are automatically deduced from + * the function arguments. + * + * @tparam Predicate Automatically deduced predicate type + * @tparam Iterator Automatically deduced iterator type + * + * @param pred Predicate function/functor for filtering elements + * @param begin Iterator to the beginning of the range + * @param end Iterator to the end of the range + * + * @return filter_range<Predicate, Iterator> A filter_range object configured with + * the provided predicate and range + * + * Example usage: + * @code + * std::vector<std::string> words = {"hello", "world", "test", "example"}; + * + * // Filter strings longer than 4 characters + * auto long_words = LL::make_filter( + * [](const std::string& s) { return s.length() > 4; }, + * words.begin(), + * words.end() + * ); + * + * // Use with range-based for loop + * for (const auto& word : long_words) { + * std::cout << word << std::endl; // Prints: hello, world, example + * } + * + * // Use with STL algorithms + * auto count = std::distance(long_words.begin(), long_words.end()); + * std::cout << "Found " << count << " long words." << std::endl; + * @endcode + * + * @note This function is preferred over direct construction of filter_range + * for most use cases due to automatic template parameter deduction. + */ + template<typename Predicate, typename Iterator> + filter_range<Predicate, Iterator> make_filter(Predicate pred, Iterator begin, Iterator end) + { + return filter_range<Predicate, Iterator>(pred, begin, end); + } + + /** + * @brief Create a filter_range over an entire container with automatic template deduction. + * + * This convenience function creates a filtered view over an entire container without + * requiring explicit begin() and end() calls. It automatically handles both const and + * non-const containers, preserving constness in the resulting iterator types. + * + * @tparam Predicate Automatically deduced predicate type + * @tparam Container Automatically deduced container type (const or non-const) + * + * @param pred Predicate function/functor for filtering elements + * @param container The container to filter (can be const or non-const) + * + * @return filter_range with appropriate iterator type for the container + * + * Example usage: + * @code + * // Non-const container + * std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + * auto evens = LL::make_filter([](int n) { return n % 2 == 0; }, numbers); + * + * // Const container + * const std::list<std::string> words = {"cat", "elephant", "dog", "hippopotamus"}; + * auto long_words = LL::make_filter([](const std::string& s) { return s.size() > 3; }, words); + * + * // Works with any container that supports begin()/end() + * std::set<double> values = {1.1, 2.2, 3.3, 4.4, 5.5}; + * auto large_values = LL::make_filter([](double d) { return d > 3.0; }, values); + * + * // Use with range-based for loops + * for (const auto& word : long_words) { + * std::cout << word << " "; // Prints: elephant hippopotamus + * } + * + * // Chain with STL algorithms + * auto even_count = std::distance(evens.begin(), evens.end()); + * std::cout << "Found " << even_count << " even numbers." << std::endl; + * @endcode + * + * @note This overload automatically calls begin() and end() on the container, + * making it more convenient than the iterator-based version. + * @note The container must remain valid for the lifetime of the returned filter_range. + * @note Constness of the container is preserved in the iterator type. + */ + template<typename Predicate, typename Container> + filter_range<Predicate, decltype(std::begin(std::declval<Container>()))> + make_filter(Predicate pred, Container&& container) + { + return filter_range<Predicate, decltype(std::begin(std::declval<Container>()))>( + pred, std::begin(container), std::end(container)); + } + +} // namespace LL + // Transparent string hashing helper for use with std::unordered_* // std::unordered_map<std::string, val, ll::string_hash, std::equal_to<>> namespace ll diff --git a/indra/llcommon/llstreamqueue.cpp b/indra/llcommon/llstreamqueue.cpp deleted file mode 100644 index 981d913749..0000000000 --- a/indra/llcommon/llstreamqueue.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @file llstreamqueue.cpp - * @author Nat Goodspeed - * @date 2012-01-05 - * @brief Implementation for llstreamqueue. - * - * $LicenseInfo:firstyear=2012&license=viewerlgpl$ - * Copyright (c) 2012, Linden Research, Inc. - * $/LicenseInfo$ - */ - -// Precompiled header -#include "linden_common.h" -// associated header -#include "llstreamqueue.h" -// STL headers -// std headers -// external library headers -// other Linden headers - -// As of this writing, llstreamqueue.h is entirely template-based, therefore -// we don't strictly need a corresponding .cpp file. However, our CMake test -// macro assumes one. Here it is. -bool llstreamqueue_cpp_ignored = true; diff --git a/indra/llcommon/llstreamqueue.h b/indra/llcommon/llstreamqueue.h deleted file mode 100644 index 01689457dd..0000000000 --- a/indra/llcommon/llstreamqueue.h +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @file llstreamqueue.h - * @author Nat Goodspeed - * @date 2012-01-04 - * @brief Definition of LLStreamQueue - * - * $LicenseInfo:firstyear=2012&license=viewerlgpl$ - * Copyright (c) 2012, Linden Research, Inc. - * $/LicenseInfo$ - */ - -#if ! defined(LL_LLSTREAMQUEUE_H) -#define LL_LLSTREAMQUEUE_H - -#include <string> -#include <list> -#include <iosfwd> // std::streamsize -#include <boost/iostreams/categories.hpp> - -/** - * This class is a growable buffer between a producer and consumer. It serves - * as a queue usable with Boost.Iostreams -- hence, a "stream queue." - * - * This is especially useful for buffering nonblocking I/O. For instance, we - * want application logic to be able to serialize LLSD to a std::ostream. We - * may write more data than the destination pipe can handle all at once, but - * it's imperative NOT to block the application-level serialization call. So - * we buffer it instead. Successive frames can try nonblocking writes to the - * destination pipe until all buffered data has been sent. - * - * Similarly, we want application logic be able to deserialize LLSD from a - * std::istream. Again, we must not block that deserialize call waiting for - * more data to arrive from the input pipe! Instead we build up a buffer over - * a number of frames, using successive nonblocking reads, until we have - * "enough" data to be able to present it through a std::istream. - * - * @note The use cases for this class overlap somewhat with those for the - * LLIOPipe/LLPumpIO hierarchies, and indeed we considered using those. This - * class has two virtues over the older machinery: - * - * # It's vastly simpler -- way fewer concepts. It's not clear to me whether - * there were ever LLIOPipe/etc. use cases that demanded all the fanciness - * rolled in, or whether they were simply overdesigned. In any case, no - * remaining Lindens will admit to familiarity with those classes -- and - * they're sufficiently obtuse that it would take considerable learning - * curve to figure out how to use them properly. The bottom line is that - * current management is not keen on any more engineers climbing that curve. - * # This class is designed around available components such as std::string, - * std::list, Boost.Iostreams. There's less proprietary code. - */ -template <typename Ch> -class LLGenericStreamQueue -{ -public: - LLGenericStreamQueue(): - mSize(0), - mClosed(false) - {} - - /** - * Boost.Iostreams Source Device facade for use with other Boost.Iostreams - * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost - * 1.48 Iostreams concepts; instead it behaves as both a Sink and a - * Source. This is its Source facade. - */ - struct Source - { - typedef Ch char_type; - typedef boost::iostreams::source_tag category; - - /// Bind the underlying LLGenericStreamQueue - Source(LLGenericStreamQueue& sq): - mStreamQueue(sq) - {} - - // Read up to n characters from the underlying data source into the - // buffer s, returning the number of characters read; return -1 to - // indicate EOF - std::streamsize read(Ch* s, std::streamsize n) - { - return mStreamQueue.read(s, n); - } - - LLGenericStreamQueue& mStreamQueue; - }; - - /** - * Boost.Iostreams Sink Device facade for use with other Boost.Iostreams - * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost - * 1.48 Iostreams concepts; instead it behaves as both a Sink and a - * Source. This is its Sink facade. - */ - struct Sink - { - typedef Ch char_type; - typedef boost::iostreams::sink_tag category; - - /// Bind the underlying LLGenericStreamQueue - Sink(LLGenericStreamQueue& sq): - mStreamQueue(sq) - {} - - /// Write up to n characters from the buffer s to the output sequence, - /// returning the number of characters written - std::streamsize write(const Ch* s, std::streamsize n) - { - return mStreamQueue.write(s, n); - } - - /// Send EOF to consumer - void close() - { - mStreamQueue.close(); - } - - LLGenericStreamQueue& mStreamQueue; - }; - - /// Present Boost.Iostreams Source facade - Source asSource() { return Source(*this); } - /// Present Boost.Iostreams Sink facade - Sink asSink() { return Sink(*this); } - - /// append data to buffer - std::streamsize write(const Ch* s, std::streamsize n) - { - // Unclear how often we might be asked to write 0 bytes -- perhaps a - // naive caller responding to an unready nonblocking read. But if we - // do get such a call, don't add a completely empty BufferList entry. - if (n == 0) - return n; - // We could implement this using a single std::string object, a la - // ostringstream. But the trouble with appending to a string is that - // you might have to recopy all previous contents to grow its size. If - // we want this to scale to large data volumes, better to allocate - // individual pieces. - mBuffer.push_back(string(s, n)); - mSize += n; - return n; - } - - /** - * Inform this LLGenericStreamQueue that no further data are forthcoming. - * For our purposes, close() is strictly a producer-side operation; - * there's little point in closing the consumer side. - */ - void close() - { - mClosed = true; - } - - /// consume data from buffer - std::streamsize read(Ch* s, std::streamsize n) - { - // read() is actually a convenience method for peek() followed by - // skip(). - std::streamsize got(peek(s, n)); - // We can only skip() as many characters as we can peek(); ignore - // skip() return here. - skip(n); - return got; - } - - /// Retrieve data from buffer without consuming. Like read(), return -1 on - /// EOF. - std::streamsize peek(Ch* s, std::streamsize n) const; - - /// Consume data from buffer without retrieving. Unlike read() and peek(), - /// at EOF we simply skip 0 characters. - std::streamsize skip(std::streamsize n); - - /// How many characters do we currently have buffered? - std::streamsize size() const - { - return mSize; - } - -private: - typedef std::basic_string<Ch> string; - typedef std::list<string> BufferList; - BufferList mBuffer; - std::streamsize mSize; - bool mClosed; -}; - -template <typename Ch> -std::streamsize LLGenericStreamQueue<Ch>::peek(Ch* s, std::streamsize n) const -{ - // Here we may have to build up 'n' characters from an arbitrary - // number of individual BufferList entries. - typename BufferList::const_iterator bli(mBuffer.begin()), blend(mBuffer.end()); - // Indicate EOF if producer has closed the pipe AND we've exhausted - // all previously-buffered data. - if (mClosed && bli == blend) - { - return -1; - } - // Here either producer hasn't yet closed, or we haven't yet exhausted - // remaining data. - std::streamsize needed(n), got(0); - // Loop until either we run out of BufferList entries or we've - // completely satisfied the request. - for ( ; bli != blend && needed; ++bli) - { - std::streamsize chunk(std::min(needed, std::streamsize(bli->length()))); - std::copy(bli->begin(), bli->begin() + chunk, s); - needed -= chunk; - s += chunk; - got += chunk; - } - return got; -} - -template <typename Ch> -std::streamsize LLGenericStreamQueue<Ch>::skip(std::streamsize n) -{ - typename BufferList::iterator bli(mBuffer.begin()), blend(mBuffer.end()); - std::streamsize toskip(n), skipped(0); - while (bli != blend && (size_t)toskip >= bli->length()) - { - std::streamsize chunk(bli->length()); - typename BufferList::iterator zap(bli++); - mBuffer.erase(zap); - mSize -= chunk; - toskip -= chunk; - skipped += chunk; - } - if (bli != blend && toskip) - { - bli->erase(bli->begin(), bli->begin() + toskip); - mSize -= toskip; - skipped += toskip; - } - return skipped; -} - -typedef LLGenericStreamQueue<char> LLStreamQueue; -typedef LLGenericStreamQueue<wchar_t> LLWStreamQueue; - -#endif /* ! defined(LL_LLSTREAMQUEUE_H) */ diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index 452acf4322..d6283c0d32 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -531,7 +531,7 @@ namespace { #if LL_LINUX constexpr U32 STATUS_SIZE = 2048; - LLFILE* status_filep = LLFile::fopen("/proc/self/status", "rb"); + LLFILE* status_filep = LLFile::fopen("/proc/self/status", LLFILE_MODE("rb")); if (status_filep) { char buff[STATUS_SIZE]; /* Flawfinder: ignore */ @@ -596,6 +596,9 @@ LLCPUInfo::LLCPUInfo() mHasSSE41 = proc.hasSSE41(); mHasSSE42 = proc.hasSSE42(); mHasSSE4a = proc.hasSSE4a(); + mHasAVX = proc.hasAVX(); + mHasAVX2 = proc.hasAVX2(); + mHasAVX512F = proc.hasAVX512F(); mHasAltivec = proc.hasAltivec(); mCPUMHz = (F64)proc.getCPUFrequency(); mFamily = proc.getCPUFamilyName(); @@ -637,6 +640,18 @@ LLCPUInfo::LLCPUInfo() { mSSEVersions.append("4a"); } + if (mHasAVX) + { + mSIMDVersions.append("AVX"); + } + if (mHasAVX2) + { + mSIMDVersions.append("AVX2"); + } + if (mHasAVX512F) + { + mSIMDVersions.append("AVX-512F"); + } } bool LLCPUInfo::hasAltivec() const @@ -679,6 +694,21 @@ bool LLCPUInfo::hasSSE4a() const return mHasSSE4a; } +bool LLCPUInfo::hasAVX() const +{ + return mHasAVX; +} + +bool LLCPUInfo::hasAVX2() const +{ + return mHasAVX2; +} + +bool LLCPUInfo::hasAVX512F() const +{ + return mHasAVX512F; +} + F64 LLCPUInfo::getMHz() const { return mCPUMHz; @@ -694,6 +724,11 @@ const LLSD& LLCPUInfo::getSSEVersions() const return mSSEVersions; } +const LLSD& LLCPUInfo::getSIMDVersions() const +{ + return mSIMDVersions; +} + void LLCPUInfo::stream(std::ostream& s) const { // gather machine information. @@ -727,7 +762,7 @@ public: void add(const LLSD::String& name, const T& value, typename std::enable_if_t<std::is_integral_v<T> >* = 0) { - mStats[name] = LLSD::Integer(value); + mStats[name] = LLSD::Integer(llmin<T>(value, S32_MAX)); } // Store every floating-point type as LLSD::Real. @@ -838,57 +873,8 @@ void LLMemoryInfo::updateAvailableMemory() } #elif LL_LINUX - // mStatsMap is derived from MEMINFO_FILE: - // $ cat /proc/meminfo - // MemTotal: 4108424 kB - // MemFree: 1244064 kB - // Buffers: 85164 kB - // Cached: 1990264 kB - // SwapCached: 0 kB - // Active: 1176648 kB - // Inactive: 1427532 kB - // Active(anon): 529152 kB - // Inactive(anon): 15924 kB - // Active(file): 647496 kB - // Inactive(file): 1411608 kB - // Unevictable: 16 kB - // Mlocked: 16 kB - // HighTotal: 3266316 kB - // HighFree: 721308 kB - // LowTotal: 842108 kB - // LowFree: 522756 kB - // SwapTotal: 6384632 kB - // SwapFree: 6384632 kB - // Dirty: 28 kB - // Writeback: 0 kB - // AnonPages: 528820 kB - // Mapped: 89472 kB - // Shmem: 16324 kB - // Slab: 159624 kB - // SReclaimable: 145168 kB - // SUnreclaim: 14456 kB - // KernelStack: 2560 kB - // PageTables: 5560 kB - // NFS_Unstable: 0 kB - // Bounce: 0 kB - // WritebackTmp: 0 kB - // CommitLimit: 8438844 kB - // Committed_AS: 1271596 kB - // VmallocTotal: 122880 kB - // VmallocUsed: 65252 kB - // VmallocChunk: 52356 kB - // HardwareCorrupted: 0 kB - // HugePages_Total: 0 - // HugePages_Free: 0 - // HugePages_Rsvd: 0 - // HugePages_Surp: 0 - // Hugepagesize: 2048 kB - // DirectMap4k: 434168 kB - // DirectMap2M: 477184 kB - // (could also run 'free', but easier to read a file than run a program) - LLSD statsMap(loadStatsMap()); - - LLMemory::sAvailPhysicalMemInKB = (U32Kilobytes)statsMap["MemFree"].asInteger(); + U64 phys = U64(getpagesize()) * U64(get_avphys_pages()); + LLMemory::sAvailPhysicalMemInKB = U64Bytes(phys); #else //do not know how to collect available memory info for other systems. //leave it blank here for now. @@ -1117,7 +1103,7 @@ LLSD LLMemoryInfo::loadStatsMap() } #elif LL_LINUX - std::ifstream meminfo(MEMINFO_FILE); + llifstream meminfo(MEMINFO_FILE); if (meminfo.is_open()) { // MemTotal: 4108424 kB @@ -1157,9 +1143,10 @@ LLSD LLMemoryInfo::loadStatsMap() LLSD::String key(matched[1].first, matched[1].second); LLSD::String value_str(matched[2].first, matched[2].second); LLSD::Integer value(0); + S64 intval = 0; try { - value = boost::lexical_cast<LLSD::Integer>(value_str); + intval = llclamp(boost::lexical_cast<S64>(value_str), S32_MIN, S32_MAX); } catch (const boost::bad_lexical_cast&) { @@ -1168,6 +1155,7 @@ LLSD LLMemoryInfo::loadStatsMap() << line << LL_ENDL; continue; } + value = LLSD::Integer(intval); // Store this statistic. stats.add(key, value); } @@ -1368,7 +1356,7 @@ bool gunzip_file(const std::string& srcfile, const std::string& dstfile) src = gzopen(srcfile.c_str(), "rb"); #endif if (! src) goto err; - dst = LLFile::fopen(tmpfile, "wb"); /* Flawfinder: ignore */ + dst = LLFile::fopen(tmpfile, LLFILE_MODE("wb")); /* Flawfinder: ignore */ if (! dst) goto err; do { @@ -1409,7 +1397,7 @@ bool gzip_file(const std::string& srcfile, const std::string& dstfile) #endif if (! dst) goto err; - src = LLFile::fopen(srcfile, "rb"); /* Flawfinder: ignore */ + src = LLFile::fopen(srcfile, LLFILE_MODE("rb")); /* Flawfinder: ignore */ if (! src) goto err; while ((bytes = (S32)fread(buffer, sizeof(U8), COMPRESS_BUFFER_SIZE, src)) > 0) diff --git a/indra/llcommon/llsys.h b/indra/llcommon/llsys.h index 91fa2754ab..1facd64469 100644 --- a/indra/llcommon/llsys.h +++ b/indra/llcommon/llsys.h @@ -81,6 +81,7 @@ public: std::string getCPUString() const; const LLSD& getSSEVersions() const; + const LLSD& getSIMDVersions() const; bool hasAltivec() const; bool hasSSE() const; @@ -90,6 +91,9 @@ public: bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; F64 getMHz() const; // Family is "AMD Duron" or "Intel Pentium Pro" @@ -103,11 +107,15 @@ private: bool mHasSSE41; bool mHasSSE42; bool mHasSSE4a; + bool mHasAVX; + bool mHasAVX2; + bool mHasAVX512F; bool mHasAltivec; F64 mCPUMHz; std::string mFamily; std::string mCPUString; LLSD mSSEVersions; + LLSD mSIMDVersions; }; //============================================================================= diff --git a/indra/llcommon/llthread.cpp b/indra/llcommon/llthread.cpp index 8c12ee7f12..277edcb6a2 100644 --- a/indra/llcommon/llthread.cpp +++ b/indra/llcommon/llthread.cpp @@ -42,6 +42,10 @@ #include <sched.h> #endif +#if LL_DARWIN || LL_LINUX +#include <pthread.h> +#endif + #ifdef LL_WINDOWS @@ -56,25 +60,32 @@ typedef struct tagTHREADNAME_INFO DWORD dwFlags; // Reserved for future use, must be zero. } THREADNAME_INFO; #pragma pack(pop) +#endif -void set_thread_name( DWORD dwThreadID, const char* threadName) +void set_thread_name(const char* threadName) { +#if LL_WINDOWS THREADNAME_INFO info; - info.dwType = 0x1000; - info.szName = threadName; - info.dwThreadID = dwThreadID; - info.dwFlags = 0; + info.dwType = 0x1000; + info.szName = threadName; + info.dwThreadID = GetCurrentThreadId(); + info.dwFlags = 0; __try { - ::RaiseException( MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(DWORD), (ULONG_PTR*)&info ); + ::RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(DWORD), (ULONG_PTR*)&info); } - __except(EXCEPTION_CONTINUE_EXECUTION) + __except (EXCEPTION_CONTINUE_EXECUTION) { } -} +#elif LL_DARWIN + std::string truncated_name(std::string_view(threadName).substr(0, 15)); + pthread_setname_np(truncated_name.c_str()); +#elif LL_LINUX + std::string truncated_name(std::string_view(threadName).substr(0, 15)); + pthread_setname_np(pthread_self(), truncated_name.c_str()); #endif - +} //---------------------------------------------------------------------------- // Usage: @@ -148,27 +159,12 @@ LL_COMMON_API bool assert_main_thread() return false; } -// this function has become moot -void LLThread::registerThreadID() {} - // // Handed to the APR thread creation function // void LLThread::threadRun() { -#ifdef LL_WINDOWS - set_thread_name(-1, mName.c_str()); - -#if 0 // probably a bad idea, see usage of SetThreadIdealProcessor in LLWindowWin32) - HANDLE hThread = GetCurrentThread(); - if (hThread) - { - SetThreadAffinityMask(hThread, (DWORD_PTR) 0xFFFFFFFFFFFFFFFE); - } -#endif - -#endif - + set_thread_name(mName.c_str()); LL_PROFILER_SET_THREAD_NAME( mName.c_str() ); // this is the first point at which we're actually running in the new thread @@ -314,11 +310,13 @@ void LLThread::shutdown() // The thread isn't already stopped // First, set the flag that indicates that we're ready to die setQuitting(); + if (!isStopped()) + { + // Give the thread a chance to update status. + yield(); + } //LL_INFOS() << "LLThread::~LLThread() Killing thread " << mName << " Status: " << mStatus << LL_ENDL; - // Now wait a bit for the thread to exit - // It's unclear whether I should even bother doing this - this destructor - // should never get called unless we're already stopped, really... S32 counter = 0; const S32 MAX_WAIT = 600; while (counter < MAX_WAIT) @@ -327,9 +325,9 @@ void LLThread::shutdown() { break; } - // Sleep for a tenth of a second - ms_sleep(100); - yield(); + // Sleep for 10ms, 6 seconds total, then give up and kill the thread + // Warning: This can be called from the main thread + ms_sleep(10); counter++; } } diff --git a/indra/llcommon/llthread.h b/indra/llcommon/llthread.h index 8794ac93aa..b97d479abc 100644 --- a/indra/llcommon/llthread.h +++ b/indra/llcommon/llthread.h @@ -28,10 +28,11 @@ #define LL_LLTHREAD_H #include "llapr.h" -#include "boost/intrusive_ptr.hpp" #include "llrefcount.h" #include <thread> +extern void set_thread_name(const char* threadName); + namespace LLTrace { class ThreadRecorder; @@ -86,11 +87,6 @@ public: id_t getID() const { return mID; } - // Called by threads *not* created via LLThread to register some - // internal state used by LLMutex. You must call this once early - // in the running thread to prevent collisions with the main thread. - static void registerThreadID(); - private: bool mPaused; std::thread::native_handle_type mNativeHandle; // for termination in case of issues diff --git a/indra/llcommon/llthreadsafequeue.h b/indra/llcommon/llthreadsafequeue.h index 6532439dd9..46ec074424 100644 --- a/indra/llcommon/llthreadsafequeue.h +++ b/indra/llcommon/llthreadsafequeue.h @@ -32,7 +32,7 @@ #include <boost/fiber/timed_mutex.hpp> #include LLCOROS_CONDVAR_HEADER #include "llexception.h" -#include "mutex.h" +#include <mutex> #include <chrono> #include <queue> #include <string> diff --git a/indra/llcommon/lltracerecording.cpp b/indra/llcommon/lltracerecording.cpp index c23adca7e8..f010f0e961 100644 --- a/indra/llcommon/lltracerecording.cpp +++ b/indra/llcommon/lltracerecording.cpp @@ -32,11 +32,6 @@ #include "lltracethreadrecorder.h" #include "llthread.h" -inline F64 lerp(F64 a, F64 b, F64 u) -{ - return a + ((b - a) * u); -} - namespace LLTrace { @@ -302,7 +297,7 @@ F64 Recording::getMean( const StatType<SampleAccumulator>& stat ) { t = (F64)active_accumulator->getSampleCount() / (F64)div; } - return lerp(accumulator.getMean(), active_accumulator->getMean(), t); + return std::lerp(accumulator.getMean(), active_accumulator->getMean(), t); } else { @@ -388,7 +383,7 @@ F64 Recording::getMean( const StatType<EventAccumulator>& stat ) { t = (F64)active_accumulator->getSampleCount() / (F64)div; } - return lerp(accumulator.getMean(), active_accumulator->getMean(), t); + return std::lerp(accumulator.getMean(), active_accumulator->getMean(), t); } else { diff --git a/indra/llcommon/lluuid.cpp b/indra/llcommon/lluuid.cpp index 3fbc45baaf..9f14461d6b 100644 --- a/indra/llcommon/lluuid.cpp +++ b/indra/llcommon/lluuid.cpp @@ -151,6 +151,48 @@ U32 janky_fast_random_seeded_bytes(U32 seed, U32 val) } #endif +void LLUUID::to_chars(char* out) const +{ + snprintf(out, UUID_STR_LENGTH, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + (U8)(mData[0]), + (U8)(mData[1]), + (U8)(mData[2]), + (U8)(mData[3]), + (U8)(mData[4]), + (U8)(mData[5]), + (U8)(mData[6]), + (U8)(mData[7]), + (U8)(mData[8]), + (U8)(mData[9]), + (U8)(mData[10]), + (U8)(mData[11]), + (U8)(mData[12]), + (U8)(mData[13]), + (U8)(mData[14]), + (U8)(mData[15])); +} + +void LLUUID::to_wchars(wchar_t* out) const +{ + swprintf(out, UUID_STR_LENGTH, L"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + (U8)(mData[0]), + (U8)(mData[1]), + (U8)(mData[2]), + (U8)(mData[3]), + (U8)(mData[4]), + (U8)(mData[5]), + (U8)(mData[6]), + (U8)(mData[7]), + (U8)(mData[8]), + (U8)(mData[9]), + (U8)(mData[10]), + (U8)(mData[11]), + (U8)(mData[12]), + (U8)(mData[13]), + (U8)(mData[14]), + (U8)(mData[15])); +} + // Common to all UUID implementations void LLUUID::toString(std::string& out) const { @@ -405,8 +447,8 @@ LLUUID LLUUID::combine(const LLUUID& other) const std::ostream& operator<<(std::ostream& s, const LLUUID& uuid) { - std::string uuid_str; - uuid.toString(uuid_str); + char uuid_str[UUID_STR_LENGTH]; + uuid.to_chars(uuid_str); s << uuid_str; return s; } diff --git a/indra/llcommon/lluuid.h b/indra/llcommon/lluuid.h index f91aadccc0..122cf414ca 100644 --- a/indra/llcommon/lluuid.h +++ b/indra/llcommon/lluuid.h @@ -104,6 +104,8 @@ public: friend LL_COMMON_API std::ostream& operator<<(std::ostream& s, const LLUUID &uuid); friend LL_COMMON_API std::istream& operator>>(std::istream& s, LLUUID &uuid); + void to_chars(char* out) const; + void to_wchars(wchar_t* out) const; void toString(std::string& out) const; void toCompressedString(std::string& out) const; diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 886f19366c..ad7c2db62e 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -51,6 +51,7 @@ public: { mStopping = true; mSleepMsecs = 1; + setQuitting(); } void run() override @@ -116,6 +117,11 @@ bool LLWatchdogTimeout::isAlive() const return (mTimer.getStarted() && !mTimer.hasExpired()); } +bool LLWatchdogTimeout::hasExpired() const +{ + return mTimer.hasExpired(); +} + bool LLWatchdogTimeout::started() const { return mTimer.getStarted(); @@ -225,11 +231,21 @@ void LLWatchdog::init( mCrashOnFreeze = crash_on_freeze; } +void LLWatchdog::shutdown() +{ + if (mTimer) + { + mTimer->stop(); + } +} + void LLWatchdog::cleanup() { + LL_PROFILE_ZONE_SCOPED; if (mTimer) { mTimer->stop(); + mTimer->shutdown(); delete mTimer; mTimer = nullptr; } diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index d55bf434f3..9386bd6bb3 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -47,6 +47,7 @@ public: // This may mean that resources used by // isAlive and other method may need synchronization. virtual bool isAlive() const = 0; + virtual bool hasExpired() const = 0; virtual bool started() const = 0; virtual void reset() = 0; virtual void start(); @@ -67,6 +68,7 @@ public: virtual ~LLWatchdogTimeout(); bool isAlive() const override; + bool hasExpired() const override; bool started() const override; void reset() override; void start() override { start(""); } @@ -91,6 +93,11 @@ public: LLWatchdog(); ~LLWatchdog(); + LLWatchdog(const LLWatchdog&) = delete; + LLWatchdog(LLWatchdog&&) = delete; + LLWatchdog& operator=(const LLWatchdog&) = delete; + LLWatchdog& operator=(LLWatchdog&&) = delete; + // Add an entry to the watchdog. void add(LLWatchdogEntry* e); void remove(LLWatchdogEntry* e); @@ -107,6 +114,7 @@ public: bool crash_on_freeze); void run(); void cleanup(); + void shutdown(); private: diff --git a/indra/llcommon/lockstatic.h b/indra/llcommon/lockstatic.h index 7cc9b7eec0..db4c7af0ec 100644 --- a/indra/llcommon/lockstatic.h +++ b/indra/llcommon/lockstatic.h @@ -13,7 +13,7 @@ #if ! defined(LL_LOCKSTATIC_H) #define LL_LOCKSTATIC_H -#include "mutex.h" // std::unique_lock +#include <mutex> // std::unique_lock namespace llthread { diff --git a/indra/llcommon/mutex.h b/indra/llcommon/mutex.h deleted file mode 100644 index 82e46315e2..0000000000 --- a/indra/llcommon/mutex.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @file mutex.h - * @author Nat Goodspeed - * @date 2019-12-03 - * @brief Wrap <mutex> in odious boilerplate - * - * $LicenseInfo:firstyear=2019&license=viewerlgpl$ - * Copyright (c) 2019, Linden Research, Inc. - * $/LicenseInfo$ - */ - -#if LL_WINDOWS -#pragma warning (push) -#pragma warning (disable:4265) -#endif -// warning C4265: 'std::_Pad' : class has virtual functions, but destructor is not virtual - -#include <mutex> - -#if LL_WINDOWS -#pragma warning (pop) -#endif diff --git a/indra/llcommon/stdtypes.h b/indra/llcommon/stdtypes.h index 78d5e50e4b..46310da463 100644 --- a/indra/llcommon/stdtypes.h +++ b/indra/llcommon/stdtypes.h @@ -39,6 +39,11 @@ typedef signed short S16; typedef unsigned short U16; typedef signed int S32; typedef unsigned int U32; +typedef long long int S64; +typedef long long unsigned int U64; + +#define S64L(a) (a##LL) +#define U64L(a) (a##ULL) // to express an index that might go negative // (ssize_t is provided by SOME compilers, don't collide) @@ -63,21 +68,6 @@ typedef wchar_t llwchar; #define LLWCHAR_IS_WCHAR_T 1 #endif -#if LL_WINDOWS -typedef signed __int64 S64; -// probably should be 'hyper' or similiar -#define S64L(a) (a) -typedef unsigned __int64 U64; -#define U64L(a) (a) -#else -typedef long long int S64; -typedef long long unsigned int U64; -#if LL_DARWIN || LL_LINUX || __FreeBSD__ -#define S64L(a) (a##LL) -#define U64L(a) (a##ULL) -#endif -#endif - typedef float F32; typedef double F64; @@ -126,10 +116,6 @@ typedef U8 LLPCode; #define LL_ARRAY_SIZE( _kArray ) ( sizeof( (_kArray) ) / sizeof( _kArray[0] ) ) -#if LL_LINUX && __GNUC__ <= 2 -typedef int intptr_t; -#endif - /***************************************************************************** * Narrowing *****************************************************************************/ diff --git a/indra/llcommon/tests/commonmisc_test.cpp b/indra/llcommon/tests/commonmisc_test.cpp index b1a284225e..80be26dfeb 100644 --- a/indra/llcommon/tests/commonmisc_test.cpp +++ b/indra/llcommon/tests/commonmisc_test.cpp @@ -503,7 +503,7 @@ namespace tut result_str = (const char*) result; ensure_equals("U64_to_str converted 1.4", val_str, result_str); - val = U64L(-1); // 0xFFFFFFFFFFFFFFFF == 18446744073709551615 + val = U64(-1); // 0xFFFFFFFFFFFFFFFF == 18446744073709551615 val_str = "18446744073709551615"; U64_to_str(val, result, sizeof(result)); result_str = (const char*) result; @@ -619,7 +619,7 @@ namespace tut val = U64_to_F64(U64L(2)); ensure_equals("U64_to_F64 converted 3.6", val, result); - result = U64L(0x7FFFFFFFFFFFFFFF) * 1.0L; // 0x7FFFFFFFFFFFFFFF + result = F64(0x7FFFFFFFFFFFFFFF) * 1.0; // 0x7FFFFFFFFFFFFFFF val = U64_to_F64(U64L(0x7FFFFFFFFFFFFFFF)); ensure_equals("U64_to_F64 converted 3.7", val, result); } diff --git a/indra/llcommon/tests/llallocator_heap_profile_test.cpp b/indra/llcommon/tests/llallocator_heap_profile_test.cpp deleted file mode 100644 index da8b54ffc0..0000000000 --- a/indra/llcommon/tests/llallocator_heap_profile_test.cpp +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @file llallocator_heap_profile_test.cpp - * @author Brad Kittenbrink - * @date 2008-02- - * @brief Test for llallocator_heap_profile.cpp. - * - * $LicenseInfo:firstyear=2009&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "../llallocator_heap_profile.h" -#include "../test/lltut.h" - -namespace tut -{ - struct llallocator_heap_profile_data - { - LLAllocatorHeapProfile prof; - - static char const * const sample_win_profile; - // *TODO - get test output from mac/linux tcmalloc - static char const * const sample_mac_profile; - static char const * const sample_lin_profile; - - static char const * const crash_testcase; - }; - typedef test_group<llallocator_heap_profile_data> factory; - typedef factory::object object; -} -namespace -{ - tut::factory llallocator_heap_profile_test_factory("LLAllocatorHeapProfile"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() - { - prof.parse(sample_win_profile); - - ensure_equals("count lines", prof.mLines.size() , 5); - ensure_equals("alloc counts", prof.mLines[0].mLiveCount, 2131854U); - ensure_equals("alloc counts", prof.mLines[0].mLiveSize, 2245710106ULL); - ensure_equals("alloc counts", prof.mLines[0].mTotalCount, 14069198U); - ensure_equals("alloc counts", prof.mLines[0].mTotalSize, 4295177308ULL); - ensure_equals("count markers", prof.mLines[0].mTrace.size(), 0); - ensure_equals("count markers", prof.mLines[1].mTrace.size(), 0); - ensure_equals("count markers", prof.mLines[2].mTrace.size(), 4); - ensure_equals("count markers", prof.mLines[3].mTrace.size(), 6); - ensure_equals("count markers", prof.mLines[4].mTrace.size(), 7); - - //prof.dump(std::cout); - } - - template<> template<> - void object::test<2>() - { - prof.parse(crash_testcase); - - ensure_equals("count lines", prof.mLines.size(), 2); - ensure_equals("alloc counts", prof.mLines[0].mLiveCount, 3U); - ensure_equals("alloc counts", prof.mLines[0].mLiveSize, 1049652ULL); - ensure_equals("alloc counts", prof.mLines[0].mTotalCount, 8U); - ensure_equals("alloc counts", prof.mLines[0].mTotalSize, 1049748ULL); - ensure_equals("count markers", prof.mLines[0].mTrace.size(), 0); - ensure_equals("count markers", prof.mLines[1].mTrace.size(), 0); - - //prof.dump(std::cout); - } - - template<> template<> - void object::test<3>() - { - // test that we don't crash on edge case data - prof.parse(""); - ensure("emtpy on error", prof.mLines.empty()); - - prof.parse("heap profile:"); - ensure("emtpy on error", prof.mLines.empty()); - } - -char const * const llallocator_heap_profile_data::sample_win_profile = -"heap profile: 2131854: 2245710106 [14069198: 4295177308] @\n" -"308592: 1073398388 [966564: 1280998739] @\n" -"462651: 375969538 [1177377: 753561247] @ 2 3 6 1\n" -"314744: 206611283 [2008722: 570934755] @ 2 3 3 7 21 32\n" -"277152: 82862770 [621961: 168503640] @ 2 3 3 7 21 32 87\n" -"\n" -"MAPPED_LIBRARIES:\n" -"00400000-02681000 r-xp 00000000 00:00 0 c:\\proj\\tcmalloc-eval-9\\indra\\build-vc80\\newview\\RelWithDebInfo\\secondlife-bin.exe\n" -"77280000-773a7000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\ntdll.dll\n" -"76df0000-76ecb000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\kernel32.dll\n" -"76000000-76073000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\comdlg32.dll\n" -"75ee0000-75f8a000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\msvcrt.dll\n" -"76c30000-76c88000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\SHLWAPI.dll\n" -"75f90000-75fdb000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\GDI32.dll\n" -"77420000-774bd000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\USER32.dll\n" -"75e10000-75ed6000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\ADVAPI32.dll\n" -"75b00000-75bc2000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\RPCRT4.dll\n" -"72ca0000-72d25000 r-xp 00000000 00:00 0 C:\\Windows\\WinSxS\\x86_microsoft.windows.common-controls_6595b64144ccf1df_5.82.6001.18000_none_886786f450a74a05\\COMCTL32.dll\n" -"76120000-76c30000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\SHELL32.dll\n" -"71ce0000-71d13000 r-xp 00000000 00:00 0 C:\\Windows\\system32\\DINPUT8.dll\n"; - - -char const * const llallocator_heap_profile_data::crash_testcase = -"heap profile: 3: 1049652 [ 8: 1049748] @\n" -" 3: 1049652 [ 8: 1049748] @\n" -"\n" -"MAPPED_LIBRARIES:\n" -"00400000-004d5000 r-xp 00000000 00:00 0 c:\\code\\linden\\tcmalloc\\indra\\build-vc80\\llcommon\\RelWithDebInfo\\llallocator_test.exe\n" -"7c900000-7c9af000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\ntdll.dll\n" -"7c800000-7c8f6000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\kernel32.dll\n" -"77dd0000-77e6b000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\ADVAPI32.dll\n" -"77e70000-77f02000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\RPCRT4.dll\n" -"77fe0000-77ff1000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\Secur32.dll\n" -"71ab0000-71ac7000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\WS2_32.dll\n" -"77c10000-77c68000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\msvcrt.dll\n" -"71aa0000-71aa8000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\WS2HELP.dll\n" -"76bf0000-76bfb000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\PSAPI.DLL\n" -"5b860000-5b8b5000 r-xp 00000000 00:00 0 C:\\WINDOWS\\system32\\NETAPI32.dll\n" -"10000000-10041000 r-xp 00000000 00:00 0 c:\\code\\linden\\tcmalloc\\indra\\build-vc80\\llcommon\\RelWithDebInfo\\libtcmalloc_minimal.dll\n" -"7c420000-7c4a7000 r-xp 00000000 00:00 0 C:\\WINDOWS\\WinSxS\\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.1433_x-ww_5cf844d2\\MSVCP80.dll\n" -"78130000-781cb000 r-xp 00000000 00:00 0 C:\\WINDOWS\\WinSxS\\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.1433_x-ww_5cf844d2\\MSVCR80.dll\n"; - -} diff --git a/indra/llcommon/tests/llallocator_test.cpp b/indra/llcommon/tests/llallocator_test.cpp deleted file mode 100644 index a7573641c7..0000000000 --- a/indra/llcommon/tests/llallocator_test.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file llallocator_test.cpp - * @author Brad Kittenbrink - * @date 2008-02- - * @brief Test for llallocator.cpp. - * - * $LicenseInfo:firstyear=2009&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "../llallocator.h" -#include "../test/lltut.h" - -namespace tut -{ - struct llallocator_data - { - LLAllocator llallocator; - }; - typedef test_group<llallocator_data> factory; - typedef factory::object object; -} -namespace -{ - tut::factory llallocator_test_factory("LLAllocator"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() - { - llallocator.setProfilingEnabled(false); - ensure("Profiler disable", !llallocator.isProfiling()); - } - -#if LL_USE_TCMALLOC - template<> template<> - void object::test<2>() - { - llallocator.setProfilingEnabled(true); - ensure("Profiler enable", llallocator.isProfiling()); - } - - template <> template <> - void object::test<3>() - { - llallocator.setProfilingEnabled(true); - - char * test_alloc = new char[1024]; - - llallocator.getProfile(); - - delete [] test_alloc; - - llallocator.getProfile(); - - // *NOTE - this test isn't ensuring anything right now other than no - // exceptions are thrown. - } -#endif // LL_USE_TCMALLOC -}; diff --git a/indra/llcommon/tests/llapp_test.cpp b/indra/llcommon/tests/llapp_test.cpp new file mode 100644 index 0000000000..114af6edbf --- /dev/null +++ b/indra/llcommon/tests/llapp_test.cpp @@ -0,0 +1,162 @@ +/** + * @file llapp_tut.cpp + * @author Phoenix + * @date 2006-09-12 + * + * $LicenseInfo:firstyear=2006&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2006-2011, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include <tut/tut.hpp> + +#include "linden_common.h" +#include "llapp.h" +#include "lltut.h" + + +namespace tut +{ + struct application + { + class LLTestApp : public LLApp + { + public: + virtual bool init() { return true; } + virtual bool cleanup() { return true; } + virtual bool frame() { return true; } + }; + LLTestApp* mApp; + application() + { + mApp = new LLTestApp; + } + ~application() + { + delete mApp; + } + }; + + typedef test_group<application> application_t; + typedef application_t::object application_object_t; + tut::application_t tut_application("application"); + + template<> template<> + void application_object_t::test<1>() + { + LLSD defaults; + defaults["template"] = "../../../scripts/messages/message_template.msg"; + defaults["configdir"] = "."; + defaults["datadir"] = "data"; + mApp->setOptionData(LLApp::PRIORITY_DEFAULT, defaults); + + LLSD datadir_sd = mApp->getOption("datadir"); + ensure_equals("data type", datadir_sd.type(), LLSD::TypeString); + ensure_equals( + "data value", datadir_sd.asString(), std::string("data")); + } + + template<> template<> + void application_object_t::test<2>() + { + const int ARGC = 13; + const char* ARGV[ARGC] = + { + "", // argv[0] is usually the application name + "-crashcount", + "2", + "-space", + "spaceserver.grid.lindenlab.com", + "-db_host", + "localhost", + "--allowlslhttprequests", + "-asset-uri", + "http://test.lindenlab.com/assets", + "-data", + "127.0.0.1", + "--smtp" + }; + bool ok = mApp->parseCommandOptions(ARGC, const_cast<char**>(ARGV)); + ensure("command line parsed", ok); + ensure_equals( + "crashcount", mApp->getOption("crashcount").asInteger(), 2); + ensure_equals( + "space", + mApp->getOption("space").asString(), + std::string("spaceserver.grid.lindenlab.com")); + ensure_equals( + "db_host", + mApp->getOption("db_host").asString(), + std::string("localhost")); + ensure("allowlshlttprequests", mApp->getOption("smtp")); + ensure_equals( + "asset-uri", + mApp->getOption("asset-uri").asString(), + std::string("http://test.lindenlab.com/assets")); + ensure_equals( + "data", + mApp->getOption("data").asString(), + std::string("127.0.0.1")); + ensure("smtp", mApp->getOption("smtp")); + } + + template<> template<> + void application_object_t::test<3>() + { + const int ARGC = 4; + const char* ARGV[ARGC] = + { + "", // argv[0] is usually the application name + "crashcount", + "2", + "--space" + }; + bool ok = mApp->parseCommandOptions(ARGC, const_cast<char**>(ARGV)); + ensure("command line parse failure", !ok); + } + + template<> template<> + void application_object_t::test<4>() + { + const int ARGC = 4; + const char* ARGV[ARGC] = + { + "", // argv[0] is usually the application name + "--crashcount", + "2", + "space" + }; + bool ok = mApp->parseCommandOptions(ARGC, const_cast<char**>(ARGV)); + ensure("command line parse failure", !ok); + } + + + template<> template<> + void application_object_t::test<5>() + { + LLSD options; + options["boolean-test"] = true; + mApp->setOptionData(LLApp::PRIORITY_GENERAL_CONFIGURATION, options); + ensure("bool set", mApp->getOption("boolean-test").asBoolean()); + options["boolean-test"] = false; + mApp->setOptionData(LLApp::PRIORITY_RUNTIME_OVERRIDE, options); + ensure("bool unset", !mApp->getOption("boolean-test").asBoolean()); + } +} diff --git a/indra/llcommon/tests/lldependencies_test.cpp b/indra/llcommon/tests/lldependencies_test.cpp index 2fea92e3b7..fd554005a3 100644 --- a/indra/llcommon/tests/lldependencies_test.cpp +++ b/indra/llcommon/tests/lldependencies_test.cpp @@ -228,19 +228,7 @@ namespace tut StringList keys(StringList{ "The", "brown", "dog.", "fox", "jumps", "lazy", "over", "quick", "the", "yellow" }); ensure_equals(instance_from_range<StringList>(deps.get_key_range()), keys); -#if (! defined(__GNUC__)) || (__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ > 3) - // This is the succinct way, works on modern compilers ensure_equals(instance_from_range<StringList>(make_transform_range(deps.get_range(), extract_key)), keys); -#else // gcc 3.3 - StringDeps::range got_range(deps.get_range()); - StringDeps::iterator kni = got_range.begin(), knend = got_range.end(); - StringList::iterator ki = keys.begin(), kend = keys.end(); - for ( ; kni != knend && ki != kend; ++kni, ++ki) - { - ensure_equals(kni->first, *ki); - } - ensure("get_range() returns proper length", kni == knend && ki == kend); -#endif // gcc 3.3 // blow off get_node_range() because they're all LLDependenciesEmpty instances } diff --git a/indra/llcommon/tests/lldoubledispatch_test.cpp b/indra/llcommon/tests/lldoubledispatch_test.cpp new file mode 100644 index 0000000000..79c4edc058 --- /dev/null +++ b/indra/llcommon/tests/lldoubledispatch_test.cpp @@ -0,0 +1,240 @@ +/** + * @file lldoubledispatch_tut.cpp + * @author Nat Goodspeed + * @date 2008-11-13 + * @brief Test for lldoubledispatch.h + * + * This program tests the DoubleDispatch class, using a variation on the example + * from Scott Meyers' "More Effective C++", Item 31. + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lldoubledispatch.h" +// STL headers +// std headers +#include <iostream> +#include <memory> +#include <string> +#include <typeinfo> +// external library headers +// other Linden headers +#include "lltut.h" + + +/*---------------------------- Class hierarchy -----------------------------*/ +// All objects are GameObjects. +class GameObject +{ +public: + GameObject(const std::string& name): mName(name) {} + virtual ~GameObject() {} + virtual std::string stringize() { return std::string(typeid(*this).name()) + " " + mName; } + +protected: + std::string mName; +}; + +// SpaceStation, Asteroid and SpaceShip are peer GameObjects. +struct SpaceStation: public GameObject +{ + SpaceStation(const std::string& name): GameObject(name) {} + // Only a dummy SpaceStation is constructed without a name + SpaceStation(): GameObject("dummy") {} +}; + +struct Asteroid: public GameObject +{ + Asteroid(const std::string& name): GameObject(name) {} + Asteroid(): GameObject("dummy") {} +}; + +struct SpaceShip: public GameObject +{ + SpaceShip(const std::string& name): GameObject(name) {} + SpaceShip(): GameObject("dummy") {} +}; + +// SpaceShip is specialized further into CommercialShip and MilitaryShip. +struct CommercialShip: public SpaceShip +{ + CommercialShip(const std::string& name): SpaceShip(name) {} + CommercialShip(): SpaceShip("dummy") {} +}; + +struct MilitaryShip: public SpaceShip +{ + MilitaryShip(const std::string& name): SpaceShip(name) {} + MilitaryShip(): SpaceShip("dummy") {} +}; + +/*-------------------------- Collision functions ---------------------------*/ +// This mechanism permits us to overcome a limitation of Meyers' approach: we +// can declare the parameter types exactly as we want, rather than having to +// make them all GameObject& parameters. +std::string shipAsteroid(SpaceShip& ship, Asteroid& rock) +{ +// std::cout << rock.stringize() << " has pulverized " << ship.stringize() << std::endl; + return "shipAsteroid"; +} + +std::string militaryShipAsteroid(MilitaryShip& ship, Asteroid& rock) +{ +// std::cout << rock.stringize() << " has severely damaged " << ship.stringize() << std::endl; + return "militaryShipAsteroid"; +} + +std::string shipStation(SpaceShip& ship, SpaceStation& dock) +{ +// std::cout << ship.stringize() << " has docked at " << dock.stringize() << std::endl; + return "shipStation"; +} + +std::string asteroidStation(Asteroid& rock, SpaceStation& dock) +{ +// std::cout << rock.stringize() << " has damaged " << dock.stringize() << std::endl; + return "asteroidStation"; +} + +/*------------------------------- Test code --------------------------------*/ +namespace tut +{ + struct dispatch_data + { + dispatch_data(): + home(new SpaceStation("Terra Station")), + obstacle(new Asteroid("Ganymede")), + tug(new CommercialShip("Pilotfish")), + patrol(new MilitaryShip("Enterprise")) + {} + + // Instantiate and populate the DoubleDispatch object. + typedef LLDoubleDispatch<std::string, GameObject> DD; + DD dispatcher; + + // Instantiate a few GameObjects. Make sure we refer to them + // polymorphically, and don't let them leak. + std::unique_ptr<GameObject> home; + std::unique_ptr<GameObject> obstacle; + std::unique_ptr<GameObject> tug; + std::unique_ptr<GameObject> patrol; + + // prototype objects + Asteroid dummyAsteroid; + SpaceShip dummyShip; + MilitaryShip dummyMilitary; + CommercialShip dummyCommercial; + SpaceStation dummyStation; + }; + typedef test_group<dispatch_data> dispatch_group; + typedef dispatch_group::object dispatch_object; + tut::dispatch_group ddgr("double dispatch"); + + template<> template<> + void dispatch_object::test<1>() + { + // Describe param types using explicit DD::Type objects + // (order-sensitive add() variant) + dispatcher.add(DD::Type<SpaceShip>(), DD::Type<Asteroid>(), shipAsteroid, true); + // naive adding, won't work + dispatcher.add(DD::Type<MilitaryShip>(), DD::Type<Asteroid>(), militaryShipAsteroid, true); + dispatcher.add(DD::Type<SpaceShip>(), DD::Type<SpaceStation>(), shipStation, true); + dispatcher.add(DD::Type<Asteroid>(), DD::Type<SpaceStation>(), asteroidStation, true); + + // Try colliding them. + ensure_equals(dispatcher(*home, *tug), // reverse params, SpaceShip subclass + "shipStation"); + ensure_equals(dispatcher(*patrol, *home), // forward params, SpaceShip subclass + "shipStation"); + ensure_equals(dispatcher(*obstacle, *home), // forward params + "asteroidStation"); + ensure_equals(dispatcher(*home, *obstacle), // reverse params + "asteroidStation"); + ensure_equals(dispatcher(*tug, *obstacle), // forward params, SpaceShip subclass + "shipAsteroid"); + ensure_equals(dispatcher(*obstacle, *patrol), // reverse params, SpaceShip subclass + // won't use militaryShipAsteroid() because it was added + // in wrong order + "shipAsteroid"); + } + + template<> template<> + void dispatch_object::test<2>() + { + // Describe param types using explicit DD::Type objects + // (order-sensitive add() variant) + // adding in correct order + dispatcher.add(DD::Type<MilitaryShip>(), DD::Type<Asteroid>(), militaryShipAsteroid, true); + dispatcher.add(DD::Type<SpaceShip>(), DD::Type<Asteroid>(), shipAsteroid, true); + dispatcher.add(DD::Type<SpaceShip>(), DD::Type<SpaceStation>(), shipStation, true); + dispatcher.add(DD::Type<Asteroid>(), DD::Type<SpaceStation>(), asteroidStation, true); + + ensure_equals(dispatcher(*patrol, *obstacle), "militaryShipAsteroid"); + ensure_equals(dispatcher(*tug, *obstacle), "shipAsteroid"); + } + + template<> template<> + void dispatch_object::test<3>() + { + // Describe param types with actual prototype instances + // (order-insensitive add() variant) + dispatcher.add(dummyMilitary, dummyAsteroid, militaryShipAsteroid); + dispatcher.add(dummyShip, dummyAsteroid, shipAsteroid); + dispatcher.add(dummyShip, dummyStation, shipStation); + dispatcher.add(dummyAsteroid, dummyStation, asteroidStation); + + ensure_equals(dispatcher(*patrol, *obstacle), "militaryShipAsteroid"); + ensure_equals(dispatcher(*tug, *obstacle), "shipAsteroid"); + ensure_equals(dispatcher(*obstacle, *patrol), ""); + } + + template<> template<> + void dispatch_object::test<4>() + { + // Describe param types with actual prototype instances + // (order-insensitive add() variant) + dispatcher.add(dummyShip, dummyAsteroid, shipAsteroid); + // Even if we add the militaryShipAsteroid in the wrong order, it + // should still work. + dispatcher.add(dummyMilitary, dummyAsteroid, militaryShipAsteroid); + dispatcher.add(dummyShip, dummyStation, shipStation); + dispatcher.add(dummyAsteroid, dummyStation, asteroidStation); + + ensure_equals(dispatcher(*patrol, *obstacle), "militaryShipAsteroid"); + ensure_equals(dispatcher(*tug, *obstacle), "shipAsteroid"); + } + + template<> template<> + void dispatch_object::test<5>() + { + dispatcher.add<SpaceShip, Asteroid>(shipAsteroid); + dispatcher.add<MilitaryShip, Asteroid>(militaryShipAsteroid); + dispatcher.add<SpaceShip, SpaceStation>(shipStation); + dispatcher.add<Asteroid, SpaceStation>(asteroidStation); + + ensure_equals(dispatcher(*patrol, *obstacle), "militaryShipAsteroid"); + ensure_equals(dispatcher(*tug, *obstacle), "shipAsteroid"); + } +} // namespace tut diff --git a/indra/llcommon/tests/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp index 3ec429530c..653f64f1b6 100644 --- a/indra/llcommon/tests/llerror_test.cpp +++ b/indra/llcommon/tests/llerror_test.cpp @@ -59,8 +59,10 @@ static const char* FieldName[] = namespace { -#ifdef __clang__ +#if defined(LL_CLANG) # pragma clang diagnostic ignored "-Wunused-function" +#elif defined(LL_GNUC) +# pragma GCC diagnostic ignored "-Wunused-function" #endif void test_that_error_h_includes_enough_things_to_compile_a_message() { diff --git a/indra/llcommon/tests/llevents_test.cpp b/indra/llcommon/tests/llevents_test.cpp new file mode 100644 index 0000000000..c5d5dcb9d4 --- /dev/null +++ b/indra/llcommon/tests/llevents_test.cpp @@ -0,0 +1,506 @@ +/** + * @file llevents_tut.cpp + * @author Nat Goodspeed + * @date 2008-09-12 + * @brief Test of llevents.h + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#if LL_WINDOWS +#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want! +#endif + +// Precompiled header +#include "linden_common.h" +// associated header +// UGLY HACK! We want to verify state internal to the classes without +// providing public accessors. +#define testable public +#include "llevents.h" +#undef testable +// STL headers +// std headers +#include <iostream> +#include <typeinfo> +// external library headers +#include <boost/bind.hpp> +// other Linden headers +#include "tests/listener.h" // must PRECEDE lltut.h +#include "lltut.h" +#include "catch_and_store_what_in.h" +#include "stringize.h" + +template<typename T> +T make(const T& value) +{ + return value; +} + +/***************************************************************************** + * tut test group + *****************************************************************************/ +namespace tut +{ +struct events_data +{ + events_data() : + pumps(LLEventPumps::instance()), + listener0("first"), + listener1("second") + { + } + LLEventPumps& pumps; + Listener listener0; + Listener listener1; + + void check_listener(const std::string& desc, const Listener& listener, LLSD::Integer got) + { + ensure_equals(STRINGIZE(listener << ' ' << desc), + listener.getLastEvent().asInteger(), got); + } +}; +typedef test_group<events_data> events_group; +typedef events_group::object events_object; +tut::events_group evgr("events"); + +template<> template<> +void events_object::test<1>() +{ + set_test_name("basic operations"); + // Having to modify this to track the statically- + // constructed pumps in other TUT modules in this giant monolithic test + // executable isn't such a hot idea. + // ensure_equals("initial pump", pumps.mPumpMap.size(), 1); + size_t initial_pumps(pumps.mPumpMap.size()); + LLEventPump& per_frame(pumps.obtain("per-frame")); + ensure_equals("first explicit pump", pumps.mPumpMap.size(), initial_pumps + 1); + // Verify that per_frame was instantiated as an LLEventStream. + ensure("LLEventStream leaf class", dynamic_cast<LLEventStream*> (&per_frame)); + ensure("enabled", per_frame.enabled()); + // Trivial test, but posting an event to an EventPump with no + // listeners should not blow up. The test is relevant because defining + // a boost::signal with a non-void return signature, using the default + // combiner, blows up if there are no listeners. This is because the + // default combiner is defined to return the value returned by the + // last listener, which is meaningless if there were no listeners. + per_frame.post(0); + LLBoundListener connection = listener0.listenTo(per_frame); + ensure("connected", connection.connected()); + ensure("not blocked", !connection.blocked()); + per_frame.post(1); + check_listener("received", listener0, 1); + { // block the connection + LLEventPump::Blocker block(connection); + ensure("blocked", connection.blocked()); + per_frame.post(2); + check_listener("not updated", listener0, 1); + } // unblock + ensure("unblocked", !connection.blocked()); + per_frame.post(3); + check_listener("unblocked", listener0, 3); + LLBoundListener sameConnection = per_frame.getListener(listener0.getName()); + ensure("still connected", sameConnection.connected()); + ensure("still not blocked", !sameConnection.blocked()); + { // block it again + LLEventPump::Blocker block(sameConnection); + ensure("re-blocked", sameConnection.blocked()); + per_frame.post(4); + check_listener("re-blocked", listener0, 3); + } // unblock + std::string threw = catch_what<LLEventPump::DupListenerName>( + [&per_frame, this](){ + // NOTE: boost::bind() saves its arguments by VALUE! If you pass + // an object instance rather than a pointer, you'll end up binding + // to an internal copy of that instance! Use boost::ref() to + // capture a reference instead. + per_frame.listen(listener0.getName(), // note bug, dup name + boost::bind(&Listener::call, boost::ref(listener1), _1)); + }); + ensure_equals(threw, + std::string("DupListenerName: " + "Attempt to register duplicate listener name '") + + listener0.getName() + "' on " + typeid(per_frame).name() + + " '" + per_frame.getName() + "'"); + // do it right this time + listener1.listenTo(per_frame); + per_frame.post(5); + check_listener("got", listener0, 5); + check_listener("got", listener1, 5); + per_frame.enable(false); + per_frame.post(6); + check_listener("didn't get", listener0, 5); + check_listener("didn't get", listener1, 5); + per_frame.enable(); + per_frame.post(7); + check_listener("got", listener0, 7); + check_listener("got", listener1, 7); + per_frame.stopListening(listener0.getName()); + ensure("disconnected 0", ! connection.connected()); + ensure("disconnected 1", ! sameConnection.connected()); + per_frame.post(8); + check_listener("disconnected", listener0, 7); + check_listener("still connected", listener1, 8); + per_frame.stopListening(listener1.getName()); + per_frame.post(9); + check_listener("disconnected", listener1, 8); +} + +template<> template<> +void events_object::test<2>() +{ + set_test_name("callstop() returning true"); + LLEventPump& per_frame(pumps.obtain("per-frame")); + listener0.reset(0); + listener1.reset(0); + LLBoundListener bound0 = listener0.listenTo(per_frame, &Listener::callstop); + LLBoundListener bound1 = listener1.listenTo(per_frame, &Listener::call, + // after listener0 + make<LLEventPump::NameList>(LLEventPump::NameList{ listener0.getName() })); + ensure("enabled", per_frame.enabled()); + ensure("connected 0", bound0.connected()); + ensure("unblocked 0", !bound0.blocked()); + ensure("connected 1", bound1.connected()); + ensure("unblocked 1", !bound1.blocked()); + per_frame.post(1); + check_listener("got", listener0, 1); + // Because listener0.callstop() returns true, control never reaches listener1.call(). + check_listener("got", listener1, 0); +} + +bool chainEvents(Listener& someListener, const LLSD& event) +{ + // Make this call so we can watch for side effects for test purposes. + someListener.call(event); + // This function represents a recursive event chain -- or some other + // scenario in which an event handler raises additional events. + int value = event.asInteger(); + if (value) + { + LLEventPumps::instance().obtain("login").post(value - 1); + } + return false; +} + +template<> template<> +void events_object::test<3>() +{ + set_test_name("explicitly-instantiated LLEventStream"); + // Explicitly instantiate an LLEventStream, and verify that it + // self-registers with LLEventPumps + size_t registered = pumps.mPumpMap.size(); + size_t owned = pumps.mOurPumps.size(); + LLEventPump* localInstance; + { + LLEventStream myEventStream("stream"); + localInstance = &myEventStream; + LLEventPump& stream(pumps.obtain("stream")); + ensure("found named LLEventStream instance", &stream == localInstance); + ensure_equals("registered new instance", pumps.mPumpMap.size(), registered + 1); + ensure_equals("explicit instance not owned", pumps.mOurPumps.size(), owned); + } // destroy myEventStream -- should unregister + ensure_equals("destroyed instance unregistered", pumps.mPumpMap.size(), registered); + ensure_equals("destroyed instance not owned", pumps.mOurPumps.size(), owned); + LLEventPump& stream(pumps.obtain("stream")); + ensure("new LLEventStream instance", &stream != localInstance); + ensure_equals("obtain()ed instance registered", pumps.mPumpMap.size(), registered + 1); + ensure_equals("obtain()ed instance owned", pumps.mOurPumps.size(), owned + 1); +} + +template<> template<> +void events_object::test<4>() +{ + set_test_name("stopListening()"); + LLEventPump& login(pumps.obtain("login")); + listener0.listenTo(login); + login.stopListening(listener0.getName()); + // should not throw because stopListening() should have removed name + listener0.listenTo(login, &Listener::callstop); + LLBoundListener wrong = login.getListener("bogus"); + ensure("bogus connection disconnected", !wrong.connected()); + ensure("bogus connection blocked", wrong.blocked()); +} + +template<> template<> +void events_object::test<5>() +{ + set_test_name("chaining LLEventPump instances"); + LLEventPump& upstream(pumps.obtain("upstream")); + // One potentially-useful construct is to chain LLEventPumps together. + // Among other things, this allows you to turn subsets of listeners on + // and off in groups. + LLEventPump& filter0(pumps.obtain("filter0")); + LLEventPump& filter1(pumps.obtain("filter1")); + upstream.listen(filter0.getName(), boost::bind(&LLEventPump::post, boost::ref(filter0), _1)); + upstream.listen(filter1.getName(), boost::bind(&LLEventPump::post, boost::ref(filter1), _1)); + listener0.listenTo(filter0); + listener1.listenTo(filter1); + listener0.reset(0); + listener1.reset(0); + upstream.post(1); + check_listener("got unfiltered", listener0, 1); + check_listener("got unfiltered", listener1, 1); + filter0.enable(false); + upstream.post(2); + check_listener("didn't get filtered", listener0, 1); + check_listener("got filtered", listener1, 2); +} + +template<> template<> +void events_object::test<6>() +{ + set_test_name("listener dependency order"); + typedef LLEventPump::NameList NameList; + LLEventPump& button(pumps.obtain("button")); + Collect collector; + button.listen("Mary", + boost::bind(&Collect::add, boost::ref(collector), "Mary", _1), + // state that "Mary" must come after "checked" + make<NameList>(NameList{ "checked" })); + button.listen("checked", + boost::bind(&Collect::add, boost::ref(collector), "checked", _1), + // "checked" must come after "spot" + make<NameList>(NameList{ "spot" })); + button.listen("spot", + boost::bind(&Collect::add, boost::ref(collector), "spot", _1)); + button.post(1); + ensure_equals(collector.result, make<StringVec>(StringVec{ "spot", "checked", "Mary" })); + collector.clear(); + button.stopListening("Mary"); + button.listen("Mary", + boost::bind(&Collect::add, boost::ref(collector), "Mary", _1), + LLEventPump::empty, // no after dependencies + // now "Mary" must come before "spot" + make<NameList>(NameList{ "spot" })); + button.post(2); + ensure_equals(collector.result, make<StringVec>(StringVec{ "Mary", "spot", "checked" })); + collector.clear(); + button.stopListening("spot"); + std::string threw = catch_what<LLEventPump::Cycle>( + [&button, &collector](){ + button.listen("spot", + boost::bind(&Collect::add, boost::ref(collector), "spot", _1), + // after "Mary" and "checked" -- whoops! + make<NameList>(NameList{ "Mary", "checked" })); + }); + // Obviously the specific wording of the exception text can + // change; go ahead and change the test to match. + // Establish that it contains: + // - the name and runtime type of the LLEventPump + ensure_contains("LLEventPump type", threw, typeid(button).name()); + ensure_contains("LLEventPump name", threw, "'button'"); + // - the name of the new listener that caused the problem + ensure_contains("new listener name", threw, "'spot'"); + // - a synopsis of the problematic dependencies. + ensure_contains("cyclic dependencies", threw, + "\"Mary\" -> before (\"spot\")"); + ensure_contains("cyclic dependencies", threw, + "after (\"spot\") -> \"checked\""); + ensure_contains("cyclic dependencies", threw, + "after (\"Mary\", \"checked\") -> \"spot\""); + button.listen("yellow", + boost::bind(&Collect::add, boost::ref(collector), "yellow", _1), make<NameList>(NameList{ "checked" })); + button.listen("shoelaces", + boost::bind(&Collect::add, boost::ref(collector), "shoelaces", _1), make<NameList>(NameList{ "checked" })); + button.post(3); + ensure_equals(collector.result, make<StringVec>(StringVec{ "Mary", "checked", "yellow", "shoelaces" })); + collector.clear(); + threw = catch_what<LLEventPump::OrderChange>( + [&button, &collector](){ + button.listen("of", + boost::bind(&Collect::add, boost::ref(collector), "of", _1), + make<NameList>(NameList{ "shoelaces" }), + make<NameList>(NameList{ "yellow" })); + }); + // Same remarks about the specific wording of the exception. Just + // ensure that it contains enough information to clarify the + // problem and what must be done to resolve it. + ensure_contains("LLEventPump type", threw, typeid(button).name()); + ensure_contains("LLEventPump name", threw, "'button'"); + ensure_contains("new listener name", threw, "'of'"); + ensure_contains("prev listener name", threw, "'yellow'"); + // std::cout << "Thrown Exception: " << threw << std::endl; + ensure_contains("old order", threw, "was: Mary, checked, yellow, shoelaces"); + ensure_contains("new order", threw, "now: Mary, checked, shoelaces, of, yellow"); + button.post(4); + ensure_equals(collector.result, make<StringVec>(StringVec{ "Mary", "checked", "yellow", "shoelaces" })); +} + +template<> template<> +void events_object::test<7>() +{ + set_test_name("tweaked and untweaked LLEventPump instance names"); + { // nested scope + // Hand-instantiate an LLEventStream... + LLEventStream bob("bob"); + std::string threw = catch_what<LLEventPump::DupPumpName>( + [](){ + // then another with a duplicate name. + LLEventStream bob2("bob"); + }); + ensure("Caught DupPumpName", !threw.empty()); + } // delete first 'bob' + LLEventStream bob("bob"); // should work, previous one unregistered + LLEventStream bob1("bob", true);// allowed to tweak name + ensure_equals("tweaked LLEventStream name", bob1.getName(), "bob1"); + std::vector<std::shared_ptr<LLEventStream> > streams; + for (int i = 2; i <= 10; ++i) + { + streams.push_back(std::shared_ptr<LLEventStream>(new LLEventStream("bob", true))); + } + ensure_equals("last tweaked LLEventStream name", streams.back()->getName(), "bob10"); +} + +// Define a function that accepts an LLListenerOrPumpName +void eventSource(const LLListenerOrPumpName& listener) +{ + // Pretend that some time has elapsed. Call listener immediately. + listener(17); +} + +template<> template<> +void events_object::test<8>() +{ + set_test_name("LLListenerOrPumpName"); + // Passing a boost::bind() expression to LLListenerOrPumpName + listener0.reset(0); + eventSource(boost::bind(&Listener::call, boost::ref(listener0), _1)); + check_listener("got by listener", listener0, 17); + // Passing a string LLEventPump name to LLListenerOrPumpName + listener0.reset(0); + LLEventStream random("random"); + listener0.listenTo(random); + eventSource("random"); + check_listener("got by pump name", listener0, 17); + std::string threw = catch_what<LLListenerOrPumpName::Empty>( + [](){ + LLListenerOrPumpName empty; + empty(17); + }); + + ensure("threw Empty", !threw.empty()); +} + +class TempListener: public Listener +{ +public: + TempListener(const std::string& name, bool& liveFlag) : + Listener(name), mLiveFlag(liveFlag) + { + mLiveFlag = true; + } + + virtual ~TempListener() + { + mLiveFlag = false; + } + +private: + bool& mLiveFlag; +}; + +template<> template<> +void events_object::test<9>() +{ + set_test_name("listen(boost::bind(...TempListener...))"); + // listen() can't do anything about a plain TempListener instance: + // it's not managed with shared_ptr, nor is it an LLEventTrackable subclass + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempListener tempListener("temp", live); + ensure("TempListener constructed", live); + connection = heaptest.listen(tempListener.getName(), + boost::bind(&Listener::call, + boost::ref(tempListener), + _1)); + heaptest.post(1); + check_listener("received", tempListener, 1); + } // presumably this will make newListener go away? + // verify that + ensure("TempListener destroyed", !live); + // This is the case against which we can't defend. Don't even try to + // post to heaptest -- that would engage Undefined Behavior. + // Cautiously inspect connection... + ensure("misleadingly connected", connection.connected()); + // then disconnect by hand. + heaptest.stopListening("temp"); +} + +class TempTrackableListener: public TempListener, public LLEventTrackable +{ +public: + TempTrackableListener(const std::string& name, bool& liveFlag): + TempListener(name, liveFlag) + {} +}; + +template<> template<> +void events_object::test<10>() +{ + set_test_name("listen(boost::bind(...TempTrackableListener ref...))"); + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempTrackableListener tempListener("temp", live); + ensure("TempTrackableListener constructed", live); + connection = heaptest.listen(tempListener.getName(), + boost::bind(&TempTrackableListener::call, + boost::ref(tempListener), _1)); + heaptest.post(1); + check_listener("received", tempListener, 1); + } // presumably this will make tempListener go away? + // verify that + ensure("TempTrackableListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); +} + +template<> template<> +void events_object::test<11>() +{ + set_test_name("listen(boost::bind(...TempTrackableListener pointer...))"); + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempTrackableListener* newListener(new TempTrackableListener("temp", live)); + ensure("TempTrackableListener constructed", live); + connection = heaptest.listen(newListener->getName(), + boost::bind(&TempTrackableListener::call, + newListener, _1)); + heaptest.post(1); + check_listener("received", *newListener, 1); + // explicitly destroy newListener + delete newListener; + } + // verify that + ensure("TempTrackableListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); +} + +} // namespace tut diff --git a/indra/llcommon/tests/llfile_test.cpp b/indra/llcommon/tests/llfile_test.cpp new file mode 100644 index 0000000000..ab4616cfb2 --- /dev/null +++ b/indra/llcommon/tests/llfile_test.cpp @@ -0,0 +1,299 @@ +/** + * @file llfile_test.cpp + * @author Frederick Martian + * @date 2025-11 + * @brief LLFile test cases. + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include <tut/tut.hpp> +#include "lltut.h" +#include "linden_common.h" +#include "llfile.h" + +namespace tut +{ + static void clear_entire_dir(const std::filesystem::path& dir_path) + { + std::error_code ec; + std::filesystem::remove_all(dir_path, ec); + } + + static std::filesystem::path append_filename(const std::filesystem::path& dir, const std::u8string& element) + { + std::filesystem::path path = dir; + return path.append(element); + } + + static std::filesystem::path get_testdir(const std::filesystem::path& tempdir) + { + return append_filename(tempdir, std::u8string(u8"test_dir")); + } + + static std::filesystem::path get_testdir_unicode(const std::filesystem::path& tempdir) + { + // Example Unicode directory name: "test_ユニコード_dir" + return append_filename(tempdir, std::u8string(u8"test_\xE3\x83\xA6\xE3\x83\x8B\xE3\x82\xB3\xE3\x83\xBC\xE3\x83\x89_dir")); + } + + struct llfile_test + { + std::filesystem::path tempdir = LLFile::tmpdir(); + std::filesystem::path testdir = get_testdir(tempdir); + }; + typedef test_group<llfile_test> llfile_test_t; + typedef llfile_test_t::object llfile_test_object_t; + tut::llfile_test_t tut_llfile_test("llfile_test"); + + template<> template<> + void llfile_test_object_t::test<1>() + { + // Test creating directories and files and deleting them and checking if the + // relevant status functions work as expected + ensure("LLFile::tmpdir() empty", !tempdir.empty()); + ensure("LLFile::tmpdir() doesn't exist", LLFile::exists(tempdir)); + ensure("LLFile::tmpdir() is not a directory", LLFile::isdir(tempdir)); + ensure("LLFile::tmpdir() should not be a file", !LLFile::isfile(tempdir)); + + // Make sure there is nothing left from a previous test run + clear_entire_dir(testdir); + ensure("llfile_test should not exist anymore", !LLFile::exists(testdir)); + + int rc = LLFile::mkdir(testdir); + ensure("LLFile::mkdir() failed", rc == 0); + ensure("llfile_test should be a directory", LLFile::isdir(testdir)); + rc = LLFile::mkdir(testdir); + ensure("LLFile::mkdir() should not fail when the directory already exists", rc == 0); + + std::filesystem::path testfile1 = testdir; + testfile1.append("llfile_test.dat"); + ensure("llfile_test1.dat should not yet exist", !LLFile::exists(testfile1)); + + const char* testdata = "testdata"; + S64 bytes = LLFile::write(testfile1, testdata, 0, sizeof(testdata)); + ensure("LLFile::write() did not write correctly", bytes == sizeof(testdata)); + + rc = LLFile::remove(testfile1); + ensure("LLFile::remove() for file test_file.dat", rc == 0); + ensure("llfile_test.dat should not exist anymore", !LLFile::exists(testfile1)); + ensure("llfile_test.dat should not be a file", !LLFile::isfile(testfile1)); + ensure("llfile_test.dat should not be a directory", !LLFile::isdir(testfile1)); + ensure("llfile_test.dat should not be a symlink", !LLFile::islink(testfile1)); + + rc = LLFile::remove(testdir); + ensure("LLFile::remove() for directory llfile_test failed", rc == 0); + ensure("llfile_test should not exist anymore", !LLFile::exists(testdir)); + } + + template<> template<> + void llfile_test_object_t::test<2>() + { + // High level static file IO functions to read and write data files + LLFile::mkdir(testdir); + ensure("llfile_test should exist", LLFile::isdir(testdir)); + + std::filesystem::path testfile1 = testdir; + testfile1.append("llfile_test.dat"); + + std::string testdata1("testdata"); + std::string testdata2("datateststuff"); + std::time_t current = time(nullptr); + S64 bytes = LLFile::write(testfile1, testdata1.c_str(), 0, testdata1.length()); + ensure("LLFile::write() did not write correctly", bytes == testdata1.length()); + ensure("llfile_test.dat should exist", LLFile::exists(testfile1)); + ensure("llfile_test.dat should be a file", LLFile::isfile(testfile1)); + ensure("llfile_test.dat should not be a directory", !LLFile::isdir(testfile1)); + + bytes = LLFile::size(testfile1); + ensure("LLFile::size() did not return the correct size", bytes == testdata1.length()); + + std::string data = LLFile::getContents(testfile1); + ensure("LLFile::getContents() did not return the correct size data", data.length() == testdata1.length()); + ensure_memory_matches("LLFile::getContents() did not read correct data", testdata1.c_str(), (U32)testdata1.length(), data.c_str(), (U32)data.length()); + + char buffer[1024]; + bytes = LLFile::read(testfile1, buffer, 0, testdata1.length()); + ensure("LLFile:read() did not return the correct size", bytes == testdata1.length()); + ensure_memory_matches("LLFile::read() did not read correct data", testdata1.c_str(), (U32)bytes, buffer, (U32)bytes); + + // What if we try to read more data than there is in the file? + bytes = LLFile::read(testfile1, buffer, 0, bytes + 10); + ensure("LLFile:read() did not correctly stop on eof", bytes == testdata1.length()); + ensure_memory_matches("LLFile::read() did not read correct data", testdata1.c_str(), (U32)bytes, buffer, (U32)bytes); + + // Let's append more data + bytes = LLFile::write(testfile1, testdata2.c_str(), -1, testdata2.length()); + ensure("LLFile::write() did not write correctly", bytes == testdata2.length()); + + bytes = LLFile::size(testfile1); + ensure("LLFile::size() did not return the correct size", bytes == testdata1.length() + testdata2.length()); + bytes = LLFile::read(testfile1, buffer, 0, bytes); + ensure("LLFile:read() did not read correct number of bytes", bytes == testdata1.length() + testdata2.length()); + ensure_memory_matches("LLFile:read() did not read correct testdata1", testdata1.c_str(), (U32)testdata1.length(), buffer, (U32)testdata1.length()); + ensure_memory_matches("LLFile:read() did not read correct testdata2", testdata2.c_str(), (U32)testdata2.length(), buffer + testdata1.length(), (U32)testdata2.length()); + } + + template<> template<> + void llfile_test_object_t::test<3>() + { + const size_t numints = 1024; + + // Testing the LLFile class implementation + std::filesystem::path testfile = testdir; + testfile.append("llfile_test.bin"); + + int data[numints]; + for (int &t : data) + { + t = rand(); + } + + std::error_code ec; + LLFile fileout(testfile, LLFile::out, ec); + ensure("LLFile constructor did not open correctly", (bool)fileout); + ensure("error_code from LLFile constructor should not indicate an error", !ec); + if (fileout) + { + S64 length = fileout.size(ec); + ensure("freshly created file should be empty", length == 0); + ensure("error_code from LLFile::size() should not indicate an error", !ec); + S64 bytes = fileout.write(data, sizeof(data), ec); + ensure("LLFile::write() did not write correctly", bytes == sizeof(data)); + ensure("error_code from LLFile::write() should not indicate an error", !ec); + bytes = fileout.write(data, sizeof(data), ec); + ensure("LLFile::write() did not write correctly", bytes == sizeof(data)); + ensure("error_code from LLFile::write() should not indicate an error", !ec); + bytes = fileout.size(ec); + ensure("LLFile::size() returned wrong size", bytes == 2 * sizeof(data)); + ensure("error_code from LLFile::size() should not indicate an error", !ec); + fileout.close(); + } + + LLFile filein(testfile, LLFile::in, ec); + ensure("LLFile constructor did not open correctly", (bool)filein); + ensure("error_code from LLFile constructor should not indicate an error", !ec); + if (filein) + { + S64 length = filein.size(ec); + ensure("LLFile::size() returned wrong size", length == 2 * sizeof(data)); + ensure("error_code from LLFile::size() should not indicate an error", !ec); + char* buffer = (char*)malloc(length); + S64 bytes = filein.read(buffer, length, ec); + ensure("LLFile::read() did not read correctly", bytes == length); + ensure("error_code from LLFile::read() should not indicate an error", !ec); + ensure_memory_matches("LLFile:read() did not read correct data1", data, (U32)sizeof(data), buffer, (U32)sizeof(data)); + ensure_memory_matches("LLFile:read() did not read correct data2", data, (U32)sizeof(data), buffer + sizeof(data), (U32)sizeof(data)); + S64 offset = filein.tell(ec); + ensure("LLFile::tell() returned a bad offset", offset == length); + ensure("error_code from LLFile::read() should not indicate an error", !ec); + offset = sizeof(data) / 2; + int rc = filein.seek(offset, ec); + ensure("LLFile::seek() indicated an error", rc == 0); + ensure("error_code from LLFile::seek() should not indicate an error", !ec); + bytes = filein.read(buffer, 2 * sizeof(data), ec); + ensure("LLFile::read() did not read correctly", bytes == sizeof(data) + offset); + ensure("error_code from LLFile::read() should not indicate an error", !ec); + ensure_memory_matches("LLFile:read() did not read correct data3", (char*)data + offset, (U32)offset, buffer, (U32)offset); + ensure_memory_matches("LLFile:read() did not read correct data4", (char*)data, (U32)sizeof(data), buffer + offset, (U32)sizeof(data)); + filein.close(); + + free(buffer); + } + } + + template<> template<> + void llfile_test_object_t::test<4>() + { + // Testing the LLFile class implementation with wrong paths and parameters + std::filesystem::path testfile = testdir; + testfile.append("llfile_test.bin"); + + std::error_code ec; + LLFile file(testfile, LLFile::out | LLFile::noreplace, ec); + ensure("LLFile constructor should not have opened the already existing file", !file); + ensure("error_code from LLFile constructor should indicate an error", (bool)ec); + + LLFile::remove(testfile); + file = LLFile(testfile, LLFile::out | LLFile::app | LLFile::trunc, ec); + ensure("LLFile constructor should not have opened the file with conflicting flags", !file); + ensure("error_code from LLFile constructor should indicate an error", (bool)ec); + + file = LLFile(testfile, LLFile::out | LLFile::app | LLFile::noreplace, ec); + ensure("LLFile constructor should not have opened the file with conflicting flags", !file); + ensure("error_code from LLFile constructor should indicate an error", (bool)ec); + + testfile = testdir; + testfile.append("llfile_test"); + testfile.append("llfile_test.bin"); + + file = LLFile(testfile, LLFile::in, ec); + ensure("LLFile constructor should not have been able to open the file in the non-existing directory", !file); + ensure("error_code from LLFile constructor should indicate an error", (bool)ec); + } + + template<> template<> + void llfile_test_object_t::test<5>() + { + // Test file and directory operations with Unicode paths and filenames + std::filesystem::path testdir_unicode = get_testdir_unicode(tempdir); + + // Unicode filename: "ファイル_テスト.bin" (means "file_test.bin" in Japanese) + std::string unicode_filename = "\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB_\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88.bin"; + std::filesystem::path testfile_unicode = testdir_unicode; + testfile_unicode.append(unicode_filename); + + // Clean up any previous test artifacts + clear_entire_dir(testdir_unicode); + ensure("Unicode test directory should not exist", !LLFile::exists(testdir_unicode)); + + // Create the Unicode directory + int rc = LLFile::mkdir(testdir_unicode); + ensure("LLFile::mkdir() failed for Unicode directory", rc == 0); + ensure("Unicode test directory should exist", LLFile::isdir(testdir_unicode)); + + + ensure("Unicode test file should not exist", !LLFile::exists(testfile_unicode)); + + // Write to the Unicode file + const char* testdata = "unicode_testdata"; + S64 bytes = LLFile::write(testfile_unicode, testdata, 0, strlen(testdata)); + ensure("Unicode test file should exist", LLFile::exists(testfile_unicode)); + ensure("Unicode test file should be a file", LLFile::isfile(testfile_unicode)); + ensure("LLFile::write() did not write correctly to Unicode file", bytes == (S64)strlen(testdata)); + + // Read back the data + char buffer[64] = {}; + bytes = LLFile::read(testfile_unicode, buffer, 0, sizeof(buffer)); + ensure("LLFile::read() did not read correctly from Unicode file", bytes == (S64)strlen(testdata)); + ensure_memory_matches("LLFile::read() did not read correct Unicode data", testdata, (U32)bytes, buffer, (U32)bytes); + + // Remove the file and directory + rc = LLFile::remove(testfile_unicode); + ensure("LLFile::remove() failed for Unicode file", rc == 0); + ensure("Unicode test file should not exist after removal", !LLFile::exists(testfile_unicode)); + + rc = LLFile::remove(testdir_unicode); + ensure("LLFile::remove() failed for Unicode directory", rc == 0); + ensure("Unicode test directory should not exist after removal", !LLFile::exists(testdir_unicode)); + } +} // namespace tut diff --git a/indra/llcommon/tests/llhttpdate_test.cpp b/indra/llcommon/tests/llhttpdate_test.cpp new file mode 100644 index 0000000000..b580b09a9f --- /dev/null +++ b/indra/llcommon/tests/llhttpdate_test.cpp @@ -0,0 +1,162 @@ +/** + * @file llhttpdate_tut.cpp + * @author Kartic Krishnamurthy + * @date Wednesday, 18 Jul 2007 17:00:00 GMT :) + * + * $LicenseInfo:firstyear=2007&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "lltut.h" + +#include "lldate.h" +#include "llcommon.h" +#include "llframetimer.h" + +#include <time.h> +#include <locale.h> + +namespace tut +{ + struct httpdate_data + { + httpdate_data() + { + } + ~httpdate_data() + { + } + LLDate some_date; + }; + typedef test_group<httpdate_data> httpdate_test; + typedef httpdate_test::object httpdate_object; + tut::httpdate_test httpdate("httpdate"); + + template<> template<> + void httpdate_object::test<1>() + { + + static std::string epoch_expected = "Thursday, 01 Jan 1970 00:00:00 GMT" ; + ensure("Check Epoch in RFC 1123", ( epoch_expected == some_date.asRFC1123())); + } + + template<> template<> + void httpdate_object::test<2>() + { + + static std::string expected = "Wednesday, 18 Jul 2007 22:17:24 GMT" ; + some_date = LLDate(1184797044.037586); + ensure("Check some timestamp in RFC 1123", ( expected == some_date.asRFC1123())); + } + + // This test of course most generic.. runs off current time + template<> template<> + void httpdate_object::test<3>() + { + + //F64 sometime = LLFrameTimer::getTotalSeconds(); + time_t sometime; + time(&sometime); + some_date = LLDate((F64) sometime); + struct tm *result; + char expected[255]; + std::string actual; + + result = gmtime(&sometime); + /* + std::cout << " seconds: "<< result->tm_sec + << ", minutes: " << result->tm_min + << ", hours: " << result->tm_hour + << ", day of the month: " << result->tm_mday + << ", month: " << result->tm_mon + << ", year: " << result->tm_year + << ", day of the week: " << result->tm_wday + << ", day in the year: " << result->tm_yday + << ", DST: " << result->tm_isdst << std::endl; + */ + strftime(expected, 255, "%A, %d %b %Y %H:%M:%S GMT", result); + actual = some_date.asRFC1123(); + // probably not a good idea to use strcmp but this is just a unit test + ensure("Current time in RFC 1123", (strcmp(expected, actual.c_str()) == 0)); + } + + void test_date_string(const std::string &locale, struct tm *t, + const std::string &fmt, const std::string &expected) + { + std::string result = LLDate::toHTTPDateString(t, fmt); + LLStringUtil::toLower(result); + std::string label = std::string("toHTTPDateString - ") + locale; + ensure_equals(label.c_str(), result, expected); + } + + template<> template<> + void httpdate_object::test<4>() + { + // test localization of http dates + const char *en_locale = "en_US.UTF-8"; + const char *fr_locale = "fr_FR.UTF-8"; + + std::string prev_locale = LLStringUtil::getLocale(); + std::string prev_clocale = std::string(setlocale(LC_TIME, NULL)); + time_t test_time = 1252374030; // 8 Sep 2009 01:40:01 + struct tm *t = gmtime(&test_time); + + setlocale(LC_TIME, en_locale); + if (strcmp(setlocale(LC_TIME, NULL), en_locale) != 0) + { + setlocale(LC_TIME, prev_clocale.c_str()); + skip("Cannot set English locale"); + } + + LLStringUtil::setLocale(en_locale); + test_date_string(en_locale, t, "%d %B %Y - %H:%M", "08 september 2009 - 01:40"); + test_date_string(en_locale, t, "%H", "01"); + test_date_string(en_locale, t, "%M", "40"); + test_date_string(en_locale, t, "%I", "01"); + test_date_string(en_locale, t, "%d", "08"); + test_date_string(en_locale, t, "%Y", "2009"); + test_date_string(en_locale, t, "%p", "am"); + test_date_string(en_locale, t, "%A", "tuesday"); + test_date_string(en_locale, t, "%B", "september"); + + setlocale(LC_TIME, fr_locale); + if (strcmp(setlocale(LC_TIME, NULL), fr_locale) != 0) + { + LLStringUtil::setLocale(prev_locale); + setlocale(LC_TIME, prev_clocale.c_str()); + skip("Cannot set French locale"); + } + + LLStringUtil::setLocale(fr_locale); + test_date_string(fr_locale, t, "%d %B %Y - %H:%M", "08 septembre 2009 - 01:40"); + test_date_string(fr_locale, t, "%H", "01"); + test_date_string(fr_locale, t, "%M", "40"); + test_date_string(fr_locale, t, "%I", "01"); + test_date_string(fr_locale, t, "%d", "08"); + test_date_string(fr_locale, t, "%Y", "2009"); + test_date_string(fr_locale, t, "%A", "mardi"); + test_date_string(fr_locale, t, "%B", "septembre"); + + LLStringUtil::setLocale(prev_locale); + setlocale(LC_TIME, prev_clocale.c_str()); + } +} diff --git a/indra/llcommon/tests/lllazy_test.cpp b/indra/llcommon/tests/lllazy_test.cpp deleted file mode 100644 index 923fe952a9..0000000000 --- a/indra/llcommon/tests/lllazy_test.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/** - * @file lllazy_test.cpp - * @author Nat Goodspeed - * @date 2009-01-28 - * @brief Tests of lllazy.h. - * - * $LicenseInfo:firstyear=2009&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -// Precompiled header -#include "linden_common.h" -// associated header -#include "lllazy.h" -// STL headers -#include <iostream> -// std headers -// external library headers -#include <boost/lambda/construct.hpp> -#include <boost/lambda/bind.hpp> -// other Linden headers -#include "../test/lltut.h" -#include "../test/catch_and_store_what_in.h" - -namespace bll = boost::lambda; - -/***************************************************************************** -* Test classes -*****************************************************************************/ - -// Let's say that because of its many external dependencies, YuckyFoo is very -// hard to instantiate in a test harness. -class YuckyFoo -{ -public: - virtual ~YuckyFoo() {} - virtual std::string whoami() const { return "YuckyFoo"; } -}; - -// Let's further suppose that YuckyBar is another hard-to-instantiate class. -class YuckyBar -{ -public: - YuckyBar(const std::string& which): - mWhich(which) - {} - virtual ~YuckyBar() {} - - virtual std::string identity() const { return std::string("YuckyBar(") + mWhich + ")"; } - -private: - const std::string mWhich; -}; - -// Pretend that this class would be tough to test because, up until we started -// trying to test it, it contained instances of both YuckyFoo and YuckyBar. -// Now we've refactored so it contains LLLazy<YuckyFoo> and LLLazy<YuckyBar>. -// More than that, it contains them by virtue of deriving from -// LLLazyBase<YuckyFoo> and LLLazyBase<YuckyBar>. -// We postulate two different LLLazyBases because, with only one, you need not -// specify *which* get()/set() method you're talking about. That's a simpler -// case. -class NeedsTesting: public LLLazyBase<YuckyFoo>, public LLLazyBase<YuckyBar> -{ -public: - NeedsTesting(): - // mYuckyBar("RealYuckyBar") - LLLazyBase<YuckyBar>(bll::bind(bll::new_ptr<YuckyBar>(), "RealYuckyBar")) - {} - virtual ~NeedsTesting() {} - - virtual std::string describe() const - { - return std::string("NeedsTesting(") + getLazy<YuckyFoo>(this).whoami() + ", " + - getLazy<YuckyBar>(this).identity() + ")"; - } - -private: - // These instance members were moved to LLLazyBases: - // YuckyFoo mYuckyFoo; - // YuckyBar mYuckyBar; -}; - -// Fake up a test YuckyFoo class -class TestFoo: public YuckyFoo -{ -public: - virtual std::string whoami() const { return "TestFoo"; } -}; - -// and a test YuckyBar -class TestBar: public YuckyBar -{ -public: - TestBar(const std::string& which): YuckyBar(which) {} - virtual std::string identity() const - { - return std::string("TestBar(") + YuckyBar::identity() + ")"; - } -}; - -// So here's a test subclass of NeedsTesting that uses TestFoo and TestBar -// instead of YuckyFoo and YuckyBar. -class TestNeedsTesting: public NeedsTesting -{ -public: - TestNeedsTesting() - { - // Exercise setLazy(T*) - setLazy<YuckyFoo>(this, new TestFoo()); - // Exercise setLazy(Factory) - setLazy<YuckyBar>(this, bll::bind(bll::new_ptr<TestBar>(), "TestYuckyBar")); - } - - virtual std::string describe() const - { - return std::string("TestNeedsTesting(") + NeedsTesting::describe() + ")"; - } - - void toolate() - { - setLazy<YuckyFoo>(this, new TestFoo()); - } -}; - -// This class tests having an explicit LLLazy<T> instance as a named member, -// rather than deriving from LLLazyBase<T>. -class LazyMember -{ -public: - YuckyFoo& getYuckyFoo() { return *mYuckyFoo; } - std::string whoisit() const { return mYuckyFoo->whoami(); } - -protected: - LLLazy<YuckyFoo> mYuckyFoo; -}; - -// This is a test subclass of the above, dynamically replacing the -// LLLazy<YuckyFoo> member. -class TestLazyMember: public LazyMember -{ -public: - // use factory setter - TestLazyMember() - { - mYuckyFoo.set(bll::new_ptr<TestFoo>()); - } - - // use instance setter - TestLazyMember(YuckyFoo* instance) - { - mYuckyFoo.set(instance); - } -}; - -/***************************************************************************** -* TUT -*****************************************************************************/ -namespace tut -{ - struct lllazy_data - { - }; - typedef test_group<lllazy_data> lllazy_group; - typedef lllazy_group::object lllazy_object; - lllazy_group lllazygrp("lllazy"); - - template<> template<> - void lllazy_object::test<1>() - { - // Instantiate an official one, just because we can - NeedsTesting nt; - // and a test one - TestNeedsTesting tnt; -// std::cout << nt.describe() << '\n'; - ensure_equals(nt.describe(), "NeedsTesting(YuckyFoo, YuckyBar(RealYuckyBar))"); -// std::cout << tnt.describe() << '\n'; - ensure_equals(tnt.describe(), - "TestNeedsTesting(NeedsTesting(TestFoo, TestBar(YuckyBar(TestYuckyBar))))"); - } - - template<> template<> - void lllazy_object::test<2>() - { - TestNeedsTesting tnt; - std::string threw = catch_what<LLLazyCommon::InstanceChange>([&tnt](){ - tnt.toolate(); - }); - ensure_contains("InstanceChange exception", threw, "replace LLLazy instance"); - } - - template<> template<> - void lllazy_object::test<3>() - { - { - LazyMember lm; - // operator*() on-demand instantiation - ensure_equals(lm.getYuckyFoo().whoami(), "YuckyFoo"); - } - { - LazyMember lm; - // operator->() on-demand instantiation - ensure_equals(lm.whoisit(), "YuckyFoo"); - } - } - - template<> template<> - void lllazy_object::test<4>() - { - { - // factory setter - TestLazyMember tlm; - ensure_equals(tlm.whoisit(), "TestFoo"); - } - { - // instance setter - TestLazyMember tlm(new TestFoo()); - ensure_equals(tlm.whoisit(), "TestFoo"); - } - } -} // namespace tut diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp index 3fb25b4cef..807b30a60a 100644 --- a/indra/llcommon/tests/llleap_test.cpp +++ b/indra/llcommon/tests/llleap_test.cpp @@ -190,7 +190,7 @@ namespace tut // computation, so I don't mind calling it twice.) Then take the // basename. reader_module(LLProcess::basename( - reader.getName().substr(0, reader.getName().length()-3))), + reader.getPath().string().substr(0, reader.getPath().string().length()-3))), PYTHON(LLStringUtil::getenv("PYTHON")) { ensure("Set PYTHON to interpreter pathname", !PYTHON.empty()); @@ -212,9 +212,9 @@ namespace tut "time.sleep(1)\n"); LLLeapVector instances; instances.push_back(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})->getWeak()); + StringVec{PYTHON, script.getPath().string()})->getWeak()); instances.push_back(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})->getWeak()); + StringVec{PYTHON, script.getPath().string()})->getWeak()); // In this case we're simply establishing that two LLLeap instances // can coexist without throwing exceptions or bombing in any other // way. Wait for them to terminate. @@ -229,7 +229,7 @@ namespace tut "import sys\n" "sys.stderr.write('''Hello from Python!\n" "note partial line''')\n"); - StringVec vcommand{ PYTHON, script.getName() }; + StringVec vcommand{ PYTHON, script.getPath().string() }; CaptureLog log(LLError::LEVEL_INFO); waitfor(LLLeap::create(get_test_name(), vcommand)); log.messageWith("Hello from Python!"); @@ -244,7 +244,7 @@ namespace tut "print('Hello from Python!')\n"); CaptureLog log(LLError::LEVEL_WARN); waitfor(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})); + StringVec{PYTHON, script.getPath().string()})); ensure_contains("error log line", log.messageWith("invalid protocol"), "Hello from Python!"); } @@ -259,7 +259,7 @@ namespace tut "sys.stdout.write('Hello from Python!')\n"); CaptureLog log(LLError::LEVEL_WARN); waitfor(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})); + StringVec{PYTHON, script.getPath().string()})); ensure_contains("error log line", log.messageWith("Discarding"), "Hello from Python!"); } @@ -273,7 +273,7 @@ namespace tut "sys.stdout.write('5a2:something')\n"); CaptureLog log(LLError::LEVEL_WARN); waitfor(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})); + StringVec{PYTHON, script.getPath().string()})); ensure_contains("error log line", log.messageWith("invalid protocol"), "5a2:"); } @@ -386,7 +386,7 @@ namespace tut " else 'bad: ' + str(resp)\n" "send(pump='" << result.getName() << "', data=result)\n";}); waitfor(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})); + StringVec{PYTHON, script.getPath().string()})); result.ensure(); } @@ -445,7 +445,7 @@ namespace tut " result = 'expected reqid=%s in %s' % (i, resp)\n" " break\n" "send(pump='" << result.getName() << "', data=result)\n";}); - waitfor(LLLeap::create(get_test_name(), StringVec{PYTHON, script.getName()}), + waitfor(LLLeap::create(get_test_name(), StringVec{PYTHON, script.getPath().string()}), 300); // needs more realtime than most tests result.ensure(); } @@ -512,7 +512,7 @@ namespace tut " (start, large[start:end], echoed[start:end]))\n" "sys.exit(1)\n";}); waitfor(LLLeap::create(test_name, - StringVec{PYTHON, script.getName(), stringize(size)}), + StringVec{PYTHON, script.getPath().string(), stringize(size)}), 180); // try a longer timeout result.ensure(); } diff --git a/indra/llcommon/tests/llmemtype_test.cpp b/indra/llcommon/tests/llmemtype_test.cpp deleted file mode 100644 index 2d64d342ae..0000000000 --- a/indra/llcommon/tests/llmemtype_test.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @file llmemtype_test.cpp - * @author Palmer Truelson - * @date 2008-03- - * @brief Test for llmemtype.cpp. - * - * $LicenseInfo:firstyear=2009&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "../llmemtype.h" -#include "../test/lltut.h" -#include "../llallocator.h" - - -#include <stack> - -std::stack<S32> memTypeStack; - -void LLAllocator::pushMemType(S32 i) -{ - memTypeStack.push(i); -} - -S32 LLAllocator::popMemType(void) -{ - S32 ret = memTypeStack.top(); - memTypeStack.pop(); - return ret; -} - -namespace tut -{ - struct llmemtype_data - { - }; - - typedef test_group<llmemtype_data> factory; - typedef factory::object object; -} -namespace -{ - tut::factory llmemtype_test_factory("LLMemType"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() - { - ensure("Simplest test ever", true); - } - - // test with no scripts - template<> template<> - void object::test<2>() - { - { - LLMemType m1(LLMemType::MTYPE_INIT); - } - ensure("Test that you can construct and destruct the mem type"); - } - - // test creation and stack testing - template<> template<> - void object::test<3>() - { - { - ensure("Test that creation and destruction properly inc/dec the stack"); - ensure_equals(memTypeStack.size(), 0); - { - LLMemType m1(LLMemType::MTYPE_INIT); - ensure_equals(memTypeStack.size(), 1); - LLMemType m2(LLMemType::MTYPE_STARTUP); - ensure_equals(memTypeStack.size(), 2); - } - ensure_equals(memTypeStack.size(), 0); - } - } - - // test with no scripts - template<> template<> - void object::test<4>() - { - // catch the begining and end - std::string test_name = LLMemType::getNameFromID(LLMemType::MTYPE_INIT.mID); - ensure_equals("Init name", test_name, "Init"); - - std::string test_name2 = LLMemType::getNameFromID(LLMemType::MTYPE_VOLUME.mID); - ensure_equals("Volume name", test_name2, "Volume"); - - std::string test_name3 = LLMemType::getNameFromID(LLMemType::MTYPE_OTHER.mID); - ensure_equals("Other name", test_name3, "Other"); - - std::string test_name4 = LLMemType::getNameFromID(-1); - ensure_equals("Invalid name", test_name4, "INVALID"); - } - -}; diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index d3d8e54d45..dfc34373da 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -17,7 +17,9 @@ #include <vector> #include <list> // std headers +#include <chrono> #include <fstream> +#include <thread> // external library headers #include "llapr.h" #include "apr_thread_proc.h" @@ -87,14 +89,14 @@ static void aprchk_(const char* call, apr_status_t rv, apr_status_t expected=APR * @param desc Optional description of the file for error message; * defaults to "in <pathname>" */ -static std::string readfile(const std::string& pathname, const std::string& desc="") +static std::string readfile(const std::filesystem::path& pathname, const std::string& desc="") { std::string use_desc(desc); if (use_desc.empty()) { - use_desc = "in " + pathname; + use_desc = "in " + pathname.string(); } - std::ifstream inf(pathname.c_str()); + llifstream inf(pathname.c_str()); std::string output; if (!std::getline(inf, output)) { @@ -108,6 +110,19 @@ static std::string readfile(const std::string& pathname, const std::string& desc return output; } +#if LL_WINDOWS +static std::string readfile_if_present(const std::string& pathname) +{ + std::ifstream inf(pathname.c_str()); + if (!inf.is_open()) + { + return ""; + } + return std::string((std::istreambuf_iterator<char>(inf)), + std::istreambuf_iterator<char>()); +} +#endif + /// Looping on LLProcess::isRunning() must now be accompanied by pumping /// "mainloop" -- otherwise the status won't update and you get an infinite /// loop. @@ -118,6 +133,9 @@ void yield(int seconds=1) LLEventPumps::instance().obtain("mainloop").post(LLSD()); } +constexpr int EOF_EVENT_RETRY_COUNT = 20; +constexpr auto EOF_EVENT_RETRY_DELAY = std::chrono::milliseconds(50); + void waitfor(LLProcess& proc, int timeout=60) { int i = 0; @@ -165,7 +183,7 @@ struct PythonProcessLauncher mParams.desc = desc + " script"; mParams.executable = PYTHON; - mParams.args.add(mScript.getName()); + mParams.args.add(mScript.getPath().string()); } /// Launch Python script; verify that it launched @@ -185,7 +203,7 @@ struct PythonProcessLauncher const char* APR_LOG = getenv("APR_LOG"); if (APR_LOG && *APR_LOG) { - std::ifstream inf(APR_LOG); + llifstream inf(APR_LOG); if (! inf.is_open()) { LL_WARNS() << "Couldn't open '" << APR_LOG << "'" << LL_ENDL; @@ -233,11 +251,11 @@ struct PythonProcessLauncher { NamedTempFile out("out", ""); // placeholder // pass name of this temporary file to the script - mParams.args.add(out.getName()); + mParams.args.add(out.getPath().string()); run(); // assuming the script wrote to that file, read it std::string desc = "from " + mDesc + " script"; - return readfile(out.getName(), desc); + return readfile(out.getPath(), desc); } LLProcess::Params mParams; @@ -246,6 +264,215 @@ struct PythonProcessLauncher NamedExtTempFile mScript; }; +#if LL_WINDOWS +namespace +{ + static constexpr const char* AUTOKILL_HELPER_SCRIPT_ENV = "LLPROCESS_AUTOKILL_HELPER_SCRIPT"; + static constexpr const char* AUTOKILL_HELPER_PIDFILE_ENV = "LLPROCESS_AUTOKILL_HELPER_PIDFILE"; + static constexpr const char* AUTOKILL_HELPER_RELEASE_ENV = "LLPROCESS_AUTOKILL_HELPER_RELEASE"; + static constexpr const char* AUTOKILL_HELPER_COUNT_ENV = "LLPROCESS_AUTOKILL_HELPER_COUNT"; + static constexpr int AUTOKILL_HELPER_RELEASE_TIMEOUT_SECONDS = 60; + static constexpr int AUTOKILL_HELPER_PID_TIMEOUT_SECONDS = 15; + static constexpr DWORD AUTOKILL_HELPER_POLL_INTERVAL_MS = 100; + static constexpr DWORD AUTOKILL_CHILD_TERMINATION_TIMEOUT_MS = 5000; + static constexpr int AUTOKILL_HELPER_INVALID_ENV_EXIT = 2; + static constexpr int AUTOKILL_HELPER_INVALID_COUNT_EXIT = 3; + static constexpr int AUTOKILL_HELPER_LAUNCH_FAILURE_EXIT = 4; + + struct ScopedEnvironmentVariable + { + ScopedEnvironmentVariable(const char* name, const std::string& value): + mName(name), + mHadValue(false) + { + DWORD size = GetEnvironmentVariableA(name, nullptr, 0); + if (size > 0) + { + std::vector<char> buffer(size); + DWORD copied = GetEnvironmentVariableA(name, buffer.data(), size); + if (copied > 0) + { + mHadValue = true; + mOldValue.assign(buffer.data(), copied); + } + } + SetEnvironmentVariableA(name, value.c_str()); + } + + ~ScopedEnvironmentVariable() + { + SetEnvironmentVariableA(mName.c_str(), mHadValue ? mOldValue.c_str() : nullptr); + } + + std::string mName; + std::string mOldValue; + bool mHadValue; + }; + + std::string get_current_executable_path() + { + std::vector<char> buffer(MAX_PATH); + for (;;) + { + DWORD length = GetModuleFileNameA(nullptr, buffer.data(), static_cast<DWORD>(buffer.size())); + tut::ensure("GetModuleFileNameA() failed", length > 0); + if (length < buffer.size() && + (length < buffer.size() - 1 || buffer[length] == '\0')) + { + return std::string(buffer.data(), length); + } + buffer.resize(buffer.size() * 2); + } + } + + void run_autokill_helper_from_environment() + { + const std::string script = LLStringUtil::getenv(AUTOKILL_HELPER_SCRIPT_ENV); + if (script.empty()) + { + return; + } + + const std::string pidfile = LLStringUtil::getenv(AUTOKILL_HELPER_PIDFILE_ENV); + const std::string releasefile = LLStringUtil::getenv(AUTOKILL_HELPER_RELEASE_ENV); + const std::string countstr = LLStringUtil::getenv(AUTOKILL_HELPER_COUNT_ENV); + const std::string python = LLStringUtil::getenv("PYTHON"); + int child_count = 1; + if (!countstr.empty()) + { + try + { + child_count = std::stoi(countstr); + } + catch (const std::exception& err) + { + LL_WARNS("LLProcess") << "Invalid autokill helper child count '" + << countstr << "': " << err.what() << LL_ENDL; + std::exit(AUTOKILL_HELPER_INVALID_COUNT_EXIT); + } + } + + if (pidfile.empty() || releasefile.empty() || python.empty() || child_count < 1) + { + std::exit(AUTOKILL_HELPER_INVALID_ENV_EXIT); + } + + std::vector<LLProcessPtr> children; + children.reserve(child_count); + std::ofstream out(pidfile.c_str(), std::ios::trunc); + for (int i = 0; i < child_count; ++i) + { + LLProcess::Params params; + params.executable = python; + params.args.add(script); + params.autokill = true; + params.attached = false; + LLProcessPtr child = LLProcess::create(params); + if (!child) + { + std::exit(AUTOKILL_HELPER_LAUNCH_FAILURE_EXIT); + } + children.push_back(child); + out << child->getProcessID() << '\n'; + } + out.flush(); + out.close(); + + for (DWORD elapsed_ms = 0; + elapsed_ms < AUTOKILL_HELPER_RELEASE_TIMEOUT_SECONDS * 1000; + elapsed_ms += AUTOKILL_HELPER_POLL_INTERVAL_MS) + { + if (readfile_if_present(releasefile) == "exit") + { + break; + } + Sleep(AUTOKILL_HELPER_POLL_INTERVAL_MS); + } + + // Exit the helper process itself so the job handle closes and Windows + // terminates the autokilled children. + std::exit(0); + } + + std::vector<DWORD> wait_for_helper_pids( + const std::string& pidfile, + int expected_count, + int timeout = AUTOKILL_HELPER_PID_TIMEOUT_SECONDS) + { + for (int i = 0; + i < (timeout * 1000) / static_cast<int>(AUTOKILL_HELPER_POLL_INTERVAL_MS); + ++i) + { + std::ifstream inf(pidfile.c_str()); + std::vector<DWORD> pids; + DWORD pid = 0; + while (inf >> pid) + { + pids.push_back(pid); + } + if (static_cast<int>(pids.size()) == expected_count) + { + return pids; + } + Sleep(AUTOKILL_HELPER_POLL_INTERVAL_MS); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } + tut::ensure(STRINGIZE("expected " << expected_count + << " child pids within " << timeout + << " seconds"), false); + return {}; + } + + void verify_autokill_on_helper_exit(const std::string& desc, int child_count) + { + NamedExtTempFile child_script("py", + "import time\n" + "time.sleep(30)\n"); + NamedTempFile pidfile("pid", ""); + NamedTempFile releasefile("release", ""); + + ScopedEnvironmentVariable helper_script(AUTOKILL_HELPER_SCRIPT_ENV, child_script.getPath().string()); + ScopedEnvironmentVariable helper_pidfile(AUTOKILL_HELPER_PIDFILE_ENV, pidfile.getPath().string()); + ScopedEnvironmentVariable helper_release(AUTOKILL_HELPER_RELEASE_ENV, releasefile.getPath().string()); + ScopedEnvironmentVariable helper_count(AUTOKILL_HELPER_COUNT_ENV, std::to_string(child_count)); + + LLProcess::Params params; + params.executable = get_current_executable_path(); + params.desc = desc + " helper"; + LLProcessPtr helper = LLProcess::create(params); + tut::ensure("helper launched", bool(helper)); + + std::vector<DWORD> pids = wait_for_helper_pids(pidfile.getPath().string(), child_count); + std::vector<HANDLE> handles; + handles.reserve(pids.size()); + for (DWORD pid : pids) + { + // SYNCHRONIZE lets the test wait for the child to terminate, while + // PROCESS_QUERY_LIMITED_INFORMATION keeps the requested access minimal. + HANDLE handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + tut::ensure(STRINGIZE("opened child process handle for pid " << pid), handle != nullptr); + handles.push_back(handle); + } + + { + std::ofstream out(releasefile.getPath(), std::ios::trunc); + out << "exit"; + } + + waitfor(*helper); + tut::ensure_equals("helper exited", helper->getStatus().mState, LLProcess::EXITED); + + for (HANDLE handle : handles) + { + tut::ensure_equals("autokilled child exited", + WaitForSingleObject(handle, AUTOKILL_CHILD_TERMINATION_TIMEOUT_MS), + WAIT_OBJECT_0); + CloseHandle(handle); + } + } +} +#endif + /// convenience function for PythonProcessLauncher::run() template <typename CONTENT> static void python(const std::string& desc, const CONTENT& script) @@ -271,23 +498,23 @@ public: NamedTempDir(): mPath(NamedTempFile::temp_path()), - mCreated(boost::filesystem::create_directories(mPath)) + mCreated(std::filesystem::create_directories(mPath)) { - mPath = boost::filesystem::canonical(mPath); + mPath = std::filesystem::canonical(mPath); } ~NamedTempDir() { if (mCreated) { - boost::filesystem::remove_all(mPath); + std::filesystem::remove_all(mPath); } } std::string getName() const { return mPath.string(); } private: - boost::filesystem::path mPath; + std::filesystem::path mPath; bool mCreated; }; @@ -298,6 +525,13 @@ namespace tut { struct llprocess_data { + llprocess_data() + { +#if LL_WINDOWS + run_autokill_helper_from_environment(); +#endif + } + LLAPRPool pool; }; typedef test_group<llprocess_data> llprocess_group; @@ -335,7 +569,7 @@ namespace tut WaitInfo(apr_proc_t* child_): child(child_), rv(-1), // we haven't yet called apr_proc_wait() - rc(0), + rc(0), // child's exit code why(apr_exit_why_e(0)) {} apr_proc_t* child; // which subprocess @@ -440,7 +674,7 @@ namespace tut #endif // Have to have a named copy of this std::string so its c_str() value // will persist. - std::string scriptname(script.getName()); + std::string scriptname(script.getPath().string()); argv.push_back(scriptname.c_str()); argv.push_back(NULL); @@ -719,14 +953,14 @@ namespace tut "with open(sys.argv[1], 'w') as f:\n" " f.write('bad')\n"); NamedTempFile out("out", "not started"); - py.mParams.args.add(out.getName()); + py.mParams.args.add(out.getPath().string()); py.launch(); // Wait for the script to wake up and do its first write int i = 0, timeout = 60; for ( ; i < timeout; ++i) { yield(); - if (readfile(out.getName(), "from kill() script") == "ok") + if (readfile(out.getPath(), "from kill() script") == "ok") break; } // If we broke this loop because of the counter, something's wrong @@ -745,7 +979,7 @@ namespace tut // If kill() failed, the script would have woken up on its own and // overwritten the file with 'bad'. But if kill() succeeded, it should // not have had that chance. - ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); + ensure_equals(get_test_name() + " script output", readfile(out.getPath()), "ok"); } template<> template<> @@ -765,7 +999,7 @@ namespace tut "# if caller hasn't managed to kill by now, bad\n" "with open(sys.argv[1], 'w') as f:\n" " f.write('bad')\n"); - py.mParams.args.add(out.getName()); + py.mParams.args.add(out.getPath().string()); py.launch(); // Capture handle for later phandle = py.mPy->getProcessHandle(); @@ -774,7 +1008,7 @@ namespace tut for ( ; i < timeout; ++i) { yield(); - if (readfile(out.getName(), "from kill() script") == "ok") + if (readfile(out.getPath(), "from kill() script") == "ok") break; } // If we broke this loop because of the counter, something's wrong @@ -787,7 +1021,7 @@ namespace tut // If kill() failed, the script would have woken up on its own and // overwritten the file with 'bad'. But if kill() succeeded, it should // not have had that chance. - ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); + ensure_equals(get_test_name() + " script output", readfile(out.getPath()), "ok"); } template<> template<> @@ -817,8 +1051,8 @@ namespace tut "# okay, saw 'go', write 'ack'\n" "with open(sys.argv[1], 'w') as f:\n" " f.write('ack')\n"); - py.mParams.args.add(from.getName()); - py.mParams.args.add(to.getName()); + py.mParams.args.add(from.getPath().string()); + py.mParams.args.add(to.getPath().string()); py.mParams.autokill = false; py.launch(); // Capture handle for later @@ -828,7 +1062,7 @@ namespace tut for ( ; i < timeout; ++i) { yield(); - if (readfile(from.getName(), "from autokill script") == "ok") + if (readfile(from.getPath(), "from autokill script") == "ok") break; } // If we broke this loop because of the counter, something's wrong @@ -840,14 +1074,14 @@ namespace tut // How do we know it's not terminated? By making it respond to // a specific stimulus in a specific way. { - std::ofstream outf(to.getName().c_str()); + llofstream outf(to.getPath()); outf << "go"; } // flush and close. // now wait for the script to terminate... one way or another. waitfor(phandle, "autokill script"); // If the LLProcess destructor implicitly called kill(), the // script could not have written 'ack' as we expect. - ensure_equals(get_test_name() + " script output", readfile(from.getName()), "ack"); + ensure_equals(get_test_name() + " script output", readfile(from.getPath()), "ack"); } template<> template<> @@ -879,8 +1113,8 @@ namespace tut "# okay, saw 'go', write 'ack'\n" "with open(sys.argv[1], 'w') as f:\n" " f.write('ack')\n"); - py.mParams.args.add(from.getName()); - py.mParams.args.add(to.getName()); + py.mParams.args.add(from.getPath().string()); + py.mParams.args.add(to.getPath().string()); py.mParams.autokill = true; py.mParams.attached = false; py.launch(); @@ -891,7 +1125,7 @@ namespace tut for ( ; i < timeout; ++i) { yield(); - if (readfile(from.getName(), "from autokill script") == "ok") + if (readfile(from.getPath(), "from autokill script") == "ok") break; } // If we broke this loop because of the counter, something's wrong @@ -903,14 +1137,14 @@ namespace tut // How do we know it's not terminated? By making it respond to // a specific stimulus in a specific way. { - std::ofstream outf(to.getName().c_str()); + llofstream outf(to.getPath()); outf << "go"; } // flush and close. // now wait for the script to terminate... one way or another. waitfor(phandle, "autokill script"); // If the LLProcess destructor implicitly called kill(), the // script could not have written 'ack' as we expect. - ensure_equals(get_test_name() + " script output", readfile(from.getName()), "ack"); + ensure_equals(get_test_name() + " script output", readfile(from.getPath()), "ack"); } template<> template<> @@ -1220,6 +1454,14 @@ namespace tut LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); EventListener listener(childout.getPump()); waitfor(*py.mPy); + // On Windows the pipe-close EOF notification can trail the process exit + // status by a short interval, so keep pumping for up to 1 second + // (20 * 50 ms) until it arrives. + for (int i = 0; i < EOF_EVENT_RETRY_COUNT && listener.mHistory.empty(); ++i) + { + std::this_thread::sleep_for(EOF_EVENT_RETRY_DELAY); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } // We can't be positive there will only be a single event, if the OS // (or any other intervening layer) does crazy buffering. What we want // to ensure is that there was exactly ONE event with "eof" true, and @@ -1431,4 +1673,396 @@ namespace tut waitfor(*py.mPy); ensure("postend never triggered", listener.mTriggered); } + + template<> template<> + void object::test<25>() + { + set_test_name("large stdin write"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "expected = int(sys.argv[1])\n" + "data = sys.stdin.buffer.read(expected)\n" + "if len(data) != expected:\n" + " print('short read %s' % len(data))\n" + "elif data != (b'x' * expected):\n" + " print('payload mismatch')\n" + "else:\n" + " print('ok')\n"); + const std::size_t payload_size = 1024 * 1024; + const std::string payload(payload_size, 'x'); + py.mParams.args.add(stringize(payload_size)); + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); + std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); + childin.write(payload.data(), payload.size()); + childin.flush(); + + int i, timeout = 20; + for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) + { + yield(); + } + ensure("large stdin write timed out", i < timeout); + ensure("child never replied", childout.contains("\n")); + ensure_equals("large stdin ack", childout.getline(), "ok"); + waitfor(*py.mPy); + ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("bad child exit code", py.mPy->getStatus().mData, 0); + } + + template<> template<> + void object::test<26>() + { + set_test_name("all three pipes active"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "sys.stdout.write('stdout message\\n')\n" + "sys.stderr.write('stderr message\\n')\n" + "sys.stdout.flush()\n" + "sys.stderr.flush()\n" + "input_data = sys.stdin.readline()\n" + "sys.stdout.write('received: ' + input_data)\n"); + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.mParams.files.add(LLProcess::FileParam("pipe")); // stderr + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + LLProcess::ReadPipe& childerr = py.mPy->getReadPipe(LLProcess::STDERR); + + // Wait for initial output + int i, timeout = 60; + for (i = 0; i < timeout && (!childout.contains("\n") || !childerr.contains("\n")); ++i) + { + yield(); + } + ensure("initial output timeout", i < timeout); + + ensure_equals("stdout message", childout.getline(), "stdout message"); + ensure_equals("stderr message", childerr.getline(), "stderr message"); + + py.mPy->getWritePipe().get_ostream() << "test input" << std::endl; + + waitfor(*py.mPy); + ensure("script never replied", childout.contains("\n")); + ensure_equals("echo response", childout.getline(), "received: test input"); + } + + template<> template<> + void object::test<27>() + { + set_test_name("process state transitions"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n"); + + py.launch(); + + // Immediately after launch + LLProcess::Status status = py.mPy->getStatus(); + ensure_equals("post-launch state", status.mState, LLProcess::RUNNING); + ensure("process is running", py.mPy->isRunning()); + + // After completion + waitfor(*py.mPy); + status = py.mPy->getStatus(); + ensure_equals("post-completion state", status.mState, LLProcess::EXITED); + ensure("process is not running", !py.mPy->isRunning()); + } + + template<> template<> + void object::test<28>() + { + set_test_name("ReadPipe limit with rapid data"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "for i in range(100):\n" + " sys.stdout.write('line %d\\n' % i)\n" + " sys.stdout.flush()\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + childout.setLimit(50); // Small limit + + EventListener listener(childout.getPump()); + waitfor(*py.mPy); + + // Verify that events respect the limit + listener.checkHistory( + [](const EventListener::Listory& history) + { + bool saw_data = false; + for (const LLSD& event : history) + { + const std::string data = event["data"].asString(); + ensure("event data within limit", data.length() <= 50); + saw_data = saw_data || !data.empty(); + } + ensure("saw at least one data event", saw_data); + }); + } + + template<> template<> + void object::test<29>() + { + set_test_name("nonexistent executable"); + LLProcess::Params params; + params.executable = "/path/to/nonexistent/executable"; + std::string pumpname("postend_invalid"); + params.postend = pumpname; + + EventListener listener(LLEventPumps::instance().obtain(pumpname)); + + LLProcessPtr child = LLProcess::create(params); + ensure("should not create invalid process", !child); + + listener.checkHistory( + [](const EventListener::Listory& history) + { + ensure_equals("got failure event", history.size(), 1); + LLSD event = history.front(); + ensure_equals("state is UNSTARTED", + event["state"].asInteger(), LLProcess::UNSTARTED); + ensure("has error string", !event["string"].asString().empty()); + }); + } + + template<> template<> + void object::test<30>() + { + set_test_name("process ID and handle validity"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n"); + py.launch(); + + LLProcess::id pid = py.mPy->getProcessID(); + LLProcess::handle handle = py.mPy->getProcessHandle(); + + ensure("PID is valid", pid != 0); + ensure("handle is valid", handle != 0); + +#if LL_WINDOWS + // On Windows, verify handle is a valid process handle + DWORD exitCode; + ensure("GetExitCodeProcess succeeds", + GetExitCodeProcess(handle, &exitCode) != 0); + ensure_equals("process still running", exitCode, STILL_ACTIVE); +#else + // On POSIX, PID and handle should be the same + ensure_equals("PID equals handle", pid, handle); + ensure("process still running", py.mPy->isRunning()); +#endif + + waitfor(*py.mPy); + } + + template<> template<> + void object::test<31>() + { + set_test_name("ReadPipe search at boundaries"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "sys.stdout.write('abcdefghijklmnopqrstuvwxyz')\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + waitfor(*py.mPy); + + // Test find at exact end + ensure("find at end succeeds", + childout.find("xyz", 23) != LLProcess::ReadPipe::npos); + ensure("find past end returns npos", + childout.find("a", 30) == LLProcess::ReadPipe::npos); + + // Test empty string search + ensure("contains empty string", childout.contains("")); + + // Test single char at boundaries + ensure_equals("find 'a' at 0", childout.find('a', 0), 0); + ensure_equals("find 'z' at end", childout.find('z'), 25); + + // Test peek at boundaries + ensure_equals("peek at exact size", childout.peek(26), ""); + ensure_equals("peek past end", childout.peek(30, 10), ""); + } + + template<> template<> + void object::test<32>() + { + set_test_name("rapid process lifecycle"); + const int ITERATIONS = 10; + + for (int i = 0; i < ITERATIONS; ++i) + { + PythonProcessLauncher py(STRINGIZE(get_test_name() << " " << i), + "import sys\n" + "sys.exit(0)\n"); + py.run(); + ensure_equals("quick exit status", + py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("quick exit code", + py.mPy->getStatus().mData, 0); + // Let the process object destroy + } + + // Give time for cleanup + yield(0); + } + + template<> template<> + void object::test<33>() + { + set_test_name("process with no output"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n" + "# Produce no output\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.mParams.files.add(LLProcess::FileParam("pipe")); // stderr + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + LLProcess::ReadPipe& childerr = py.mPy->getReadPipe(LLProcess::STDERR); + + EventListener outListener(childout.getPump()); + EventListener errListener(childerr.getPump()); + + waitfor(*py.mPy); + // On Windows the pipe-close EOF notification can trail the process exit + // status by a short interval, so keep pumping for up to 1 second + // (20 * 50 ms) until both pipes report it. + for (int i = 0; + i < EOF_EVENT_RETRY_COUNT && + (outListener.mHistory.empty() || errListener.mHistory.empty()); + ++i) + { + std::this_thread::sleep_for(EOF_EVENT_RETRY_DELAY); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } + + ensure_equals("stdout size", childout.size(), 0); + ensure_equals("stderr size", childerr.size(), 0); + ensure_equals("process exited", py.mPy->getStatus().mState, LLProcess::EXITED); + + auto check_eof = [](const EventListener::Listory& history, const std::string& which) + { + ensure_equals(STRINGIZE(which << " events"), history.size(), 1); + const LLSD& event = history.front(); + ensure(STRINGIZE(which << " eof event"), event["eof"].asBoolean()); + ensure_equals(STRINGIZE(which << " len"), event["len"].asInteger(), 0); + }; + + outListener.checkHistory( + [&](const EventListener::Listory& history) + { + check_eof(history, "stdout"); + }); + + errListener.checkHistory( + [&](const EventListener::Listory& history) + { + check_eof(history, "stderr"); + }); + } + + template<> template<> + void object::test<34>() + { + set_test_name("tick() completes quickly after kill()"); + // Regression test: tick() must not block after kill() is called. + // The old Windows code called WaitForSingleObject(..., 100) in tick(), + // causing a 100 ms stall on the main thread. This test ensures that a + // mainloop tick completes well under that threshold even when the child + // process has been killed and may still be exiting. + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(120)\n"); + py.launch(); + + // Wait for the process to start up + yield(); + ensure("process started", py.mPy->isRunning()); + + // Send the kill signal + py.mPy->kill(); + + // Sleep longer than the tick threshold to ensure the child has had + // time to exit at the OS level, so the next tick is likely to enter + // the "process just exited" code path that used to block for 100 ms. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Time a single mainloop tick: it must not block + auto start = std::chrono::steady_clock::now(); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - start).count(); + + ensure(STRINGIZE("tick() took " << elapsed_ms << " ms, expected < 75 ms"), + elapsed_ms < 75); + + // Let the process fully exit so cleanup is orderly + waitfor(*py.mPy); + } + + template<> template<> + void object::test<35>() + { + set_test_name("LLProcess destructor completes quickly after kill()"); + // Regression test: after an explicit kill() call the destructor must + // not perform a blocking wait. The old Windows code called + // WaitForSingleObject(..., 100) in the destructor, causing a 100 ms + // stall on the main thread. With mKillCalled set to true, the + // destructor skips the termination/wait block entirely. + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(120)\n"); + py.launch(); + + // Wait for the process to start up + yield(); + ensure("process started", py.mPy->isRunning()); + + // Kill the process (sets mKillCalled = true) + py.mPy->kill(); + + // Time how long the destructor takes + auto start = std::chrono::steady_clock::now(); + py.mPy.reset(); // explicit destruction + auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - start).count(); + + ensure(STRINGIZE("destructor took " << elapsed_ms << " ms, expected < 75 ms"), + elapsed_ms < 75); + } + + template<> template<> + void object::test<36>() + { + set_test_name("autokill ensures child termination on parent exit"); +#if !LL_WINDOWS + skip("Windows-specific test"); +#else + verify_autokill_on_helper_exit(get_test_name(), 1); +#endif + } + + template<> template<> + void object::test<37>() + { + set_test_name("multiple processes with autokill"); +#if !LL_WINDOWS + skip("Windows-specific test"); +#else + verify_autokill_on_helper_exit(get_test_name(), 2); +#endif + } } // namespace tut diff --git a/indra/llcommon/tests/llsd_test.cpp b/indra/llcommon/tests/llsd_test.cpp new file mode 100644 index 0000000000..9cc93bcd14 --- /dev/null +++ b/indra/llcommon/tests/llsd_test.cpp @@ -0,0 +1,830 @@ +/** + * @file llsd_new_tut.cpp + * @date February 2006 + * @brief LLSD unit tests + * + * $LicenseInfo:firstyear=2006&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2006-2011, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#define LLSD_DEBUG_INFO +#include "linden_common.h" +#include "lltut.h" +#include "llsd.h" + +#include "llsdtraits.h" +#include "llstring.h" + +using std::fpclassify; + +namespace tut +{ + class SDCleanupCheck + { + private: + U32 mOutstandingAtStart; + public: + SDCleanupCheck() : mOutstandingAtStart(llsd::outstandingCount()) { } + ~SDCleanupCheck() + { + ensure_equals("SDCleanupCheck", + llsd::outstandingCount(), mOutstandingAtStart); + } + }; + + class SDAllocationCheck : public SDCleanupCheck + { + private: + std::string mMessage; + U32 mExpectedAllocations; + U32 mAllocationAtStart; + public: + SDAllocationCheck(const std::string& message, int expectedAllocations) + : mMessage(message), + mExpectedAllocations(expectedAllocations), + mAllocationAtStart(llsd::allocationCount()) + { } + ~SDAllocationCheck() + { + ensure_equals(mMessage + " SDAllocationCheck", + llsd::allocationCount() - mAllocationAtStart, + mExpectedAllocations); + } + }; + + struct SDTestData { + template<class T> + static void ensureTypeAndValue(const char* msg, const LLSD& actual, + T expectedValue) + { + LLSDTraits<T> traits; + + std::string s(msg); + + ensure( s + " type", traits.checkType(actual)); + ensure_equals( s + " value", traits.get(actual), expectedValue); + } + + template<class T> + static void ensureTypeAndRefValue(const char* msg, const LLSD& actual, + const T& expectedValue) + { + LLSDTraits<const T&> traits; + + std::string s(msg); + + ensure( s + " type", traits.checkType(actual)); + ensure_equals( s + " value", traits.get(actual), expectedValue); + } + }; + + typedef test_group<SDTestData> SDTestGroup; + typedef SDTestGroup::object SDTestObject; + + SDTestGroup sdTestGroup("LLSD(new)"); + + // template<> template<> + // void SDTestObject::test<1>() + // // construction and test of undefined + // { + // SDCleanupCheck check; + + // LLSD u; + // ensure("is undefined", u.isUndefined()); + // } + + template<> template<> + void SDTestObject::test<2>() + // setting and fetching scalar types + { + SDCleanupCheck check; + + LLSD v; + + v = true; ensureTypeAndValue("set true", v, true); + v = false; ensureTypeAndValue("set false", v, false); + v = true; ensureTypeAndValue("set true again", v, true); + + v = 42; ensureTypeAndValue("set to 42", v, 42); + v = 0; ensureTypeAndValue("set to zero", v, 0); + v = -12345; ensureTypeAndValue("set to neg", v, -12345); + v = 2000000000; ensureTypeAndValue("set to big", v, 2000000000); + + v = 3.14159265359; + ensureTypeAndValue("set to pi", v, 3.14159265359); + ensure_not_equals("isn't float", v.asReal(), + (float)3.14159265359); + v = 6.7e256; ensureTypeAndValue("set to big", v, 6.7e256); + + LLUUID nullUUID; + LLUUID newUUID; + newUUID.generate(); + + v = nullUUID; ensureTypeAndValue("set to null UUID", v, nullUUID); + v = newUUID; ensureTypeAndValue("set to new UUID", v, newUUID); + v = nullUUID; ensureTypeAndValue("set to null again", v, nullUUID); + + // strings must be tested with two types of string objects + std::string s = "now is the time"; + const char* cs = "for all good zorks"; + + v = s; ensureTypeAndValue("set to std::string", v, s); + v = cs; ensureTypeAndValue("set to const char*", v, cs); + + LLDate epoch; + LLDate aDay("2001-10-22T10:11:12.00Z"); + + v = epoch; ensureTypeAndValue("set to epoch", v, epoch); + v = aDay; ensureTypeAndValue("set to a day", v, aDay); + + LLURI path("http://slurl.com/secondlife/Ambleside/57/104/26/"); + + v = path; ensureTypeAndValue("set to a uri", v, path); + + const char source[] = "once in a blue moon"; + std::vector<U8> data; + copy(&source[0], &source[sizeof(source)], back_inserter(data)); + + v = data; ensureTypeAndRefValue("set to data", v, data); + + v.clear(); + ensure("reset to undefined", v.type() == LLSD::TypeUndefined); + } + + template<> template<> + void SDTestObject::test<3>() + // construction via scalar values + // tests both constructor and initialize forms + { + SDCleanupCheck check; + + LLSD b1(true); ensureTypeAndValue("construct boolean", b1, true); + LLSD b2 = true; ensureTypeAndValue("initialize boolean", b2, true); + LLSD i1(42); ensureTypeAndValue("construct int", i1, 42); + LLSD i2 =42; ensureTypeAndValue("initialize int", i2, 42); + LLSD d1(1.2); ensureTypeAndValue("construct double", d1, 1.2); + LLSD d2 = 1.2; ensureTypeAndValue("initialize double", d2, 1.2); + + LLUUID newUUID; + newUUID.generate(); + LLSD u1(newUUID); + ensureTypeAndValue("construct UUID", u1, newUUID); + LLSD u2 = newUUID; + ensureTypeAndValue("initialize UUID", u2, newUUID); + + LLSD ss1(std::string("abc")); + ensureTypeAndValue("construct std::string", ss1, "abc"); + LLSD ss2 = std::string("abc"); + ensureTypeAndValue("initialize std::string",ss2, "abc"); + LLSD sl1(std::string("def")); + ensureTypeAndValue("construct std::string", sl1, "def"); + LLSD sl2 = std::string("def"); + ensureTypeAndValue("initialize std::string", sl2, "def"); + LLSD sc1("ghi"); + ensureTypeAndValue("construct const char*", sc1, "ghi"); + LLSD sc2 = "ghi"; + ensureTypeAndValue("initialize const char*",sc2, "ghi"); + + LLDate aDay("2001-10-22T10:11:12.00Z"); + LLSD t1(aDay); ensureTypeAndValue("construct LLDate", t1, aDay); + LLSD t2 = aDay; ensureTypeAndValue("initialize LLDate", t2, aDay); + + LLURI path("http://slurl.com/secondlife/Ambleside/57/104/26/"); + LLSD p1(path); ensureTypeAndValue("construct LLURI", p1, path); + LLSD p2 = path; ensureTypeAndValue("initialize LLURI", p2, path); + + const char source[] = "once in a blue moon"; + std::vector<U8> data; + copy(&source[0], &source[sizeof(source)], back_inserter(data)); + LLSD x1(data); ensureTypeAndRefValue("construct vector<U8>", x1, data); + LLSD x2 = data; ensureTypeAndRefValue("initialize vector<U8>", x2, data); + } + + void checkConversions(const char* msg, const LLSD& v, + LLSD::Boolean eBoolean, LLSD::Integer eInteger, + LLSD::Real eReal, const LLSD::String& eString) + { + std::string s(msg); + + ensure_equals(s+" to bool", v.asBoolean(), eBoolean); + ensure_equals(s+" to int", v.asInteger(), eInteger); + if (eReal == eReal) + { + ensure_equals(s+" to real", v.asReal(), eReal); + ensure_equals(s+" to string", v.asString(), eString); + } + else + { + int left = fpclassify(v.asReal()); + int right = fpclassify(eReal); + + ensure_equals(s+" to real", left, right); + // ensure_equals(s+" to string", v.asString(), eString); + // I've commented this check out, since there doesn't + // seem to be uniform string representation for NaN on + // all platforms. For example, on my Ubuntu 8.10 laptop + // with libc 2.11.1, sqrt(-1.0) will return '-nan', not + // 'nan'. + } + } + + template<> template<> + void SDTestObject::test<4>() + // conversion between undefined and basic scalar types: + // boolean, integer, real and string + { + SDCleanupCheck check; + + LLSD v; checkConversions("untitled", v, false, 0, 0.0, ""); + + v = false; checkConversions("false", v, false, 0, 0.0, ""); + v = true; checkConversions("true", v, true, 1, 1.0, "true"); + + v = 0; checkConversions("zero", v, false, 0, 0.0, "0"); + v = 1; checkConversions("one", v, true, 1, 1.0, "1"); + v = -33; checkConversions("neg33", v, true, -33, -33.0, "-33"); + + v = 0.0; checkConversions("0.0", v, false, 0, 0.0, "0"); + v = 0.5; checkConversions("point5", v, true, 0, 0.5, "0.5"); + v = 0.9; checkConversions("point9", v, true, 0, 0.9, "0.9"); + v = -3.9; checkConversions("neg3dot9", v, true, -3, -3.9, "-3.9"); + // Get rid of NaN test. First, some libraries don't reliably return + // NaN for sqrt(-1.0) -- meaning that I don't even know how to + // portably, reliably produce a NaN value. Second, we observe failures + // on different platforms in the asString() test. But LLSD's + // ImplReal::asString() does not itself recognize NaN! It merely + // passes the value through to llformat(), which passes it through to + // the library vsnprintf(). That is, even when we do produce NaN, + // we're not testing any LLSD code: we're testing the local library's + // vsnprintf() function, which (empirically) produces idiosyncratic + // results. This is just not a good test case. +// v = sqrt(-1.0); checkConversions("NaN", v, false, 0, sqrt(-1.0), "nan"); + + v = ""; checkConversions("empty", v, false, 0, 0.0, ""); + v = "0"; checkConversions("digit0", v, true, 0, 0.0, "0"); + v = "10"; checkConversions("digit10", v, true, 10, 10.0, "10"); + v = "-2.345"; checkConversions("decdigits", v, + true, -2, -2.345, "-2.345"); + v = "apple"; checkConversions("apple", v, true, 0, 0.0, "apple"); + v = "33bob"; checkConversions("digialpha", v, true, 0, 0.0, "33bob"); + v = " "; checkConversions("space", v, true, 0, 0.0, " "); + v = "\n"; checkConversions("newline", v, true, 0, 0.0, "\n"); + } + + template<class T> + void checkRoundTrip(const std::string& msg, const LLSD& actual, + const char* sExpected, T vExpected) + { + std::string str = actual.asString(); + + if (sExpected) { + ensure_equals(msg + " string", str, sExpected); + } + + LLSD u(str); + LLSDTraits<T> traits; + + ensure_equals(msg + " value", traits.get(u), vExpected); + } + + + template<> template<> + void SDTestObject::test<5>() + // conversion of String to and from UUID, Date and URI. + { + SDCleanupCheck check; + + LLSD v; + + LLUUID nullUUID; + LLUUID someUUID; + someUUID.generate(); + + v = nullUUID; checkRoundTrip("null uuid", v, + "00000000-0000-0000-0000-000000000000", nullUUID); + v = someUUID; checkRoundTrip("random uuid", v, 0, someUUID); + + LLDate epoch; + LLDate beta("2003-04-30T04:00:00Z"); + LLDate oneOh("2003-06-23T04:00:00Z"); + + v = epoch; checkRoundTrip("epoch date", v, 0, epoch); + v = beta; checkRoundTrip("beta date", v, + "2003-04-30T04:00:00Z", beta); + v = oneOh; checkRoundTrip("1.0 date", v, + "2003-06-23T04:00:00Z", oneOh); + + LLURI empty; + LLURI path("http://slurl.com/secondlife/Ambleside/57/104/26/"); + LLURI mail("mailto:zero.linden@secondlife.com"); + + v = empty; checkRoundTrip("empty URI", v, 0, empty); + v = path; checkRoundTrip("path URI", v, + "http://slurl.com/secondlife/Ambleside/57/104/26/", + path); + v = mail; checkRoundTrip("mail URI", v, + "mailto:zero.linden@secondlife.com", mail); + } + + template<> template<> + void SDTestObject::test<6>() + // copy construction and assignment + // checking for shared values after constr. or assignment + // checking in both the same type and change of type case + { + SDCleanupCheck check; + + { + LLSD v = 42; + + LLSD w0(v); + ensureTypeAndValue("int constr.", w0, 42); + + LLSD w1(v); + w1 = 13; + ensureTypeAndValue("int constr. change case 1", w1, 13); + ensureTypeAndValue("int constr. change case 2", v, 42); + + LLSD w2(v); + v = 7; + ensureTypeAndValue("int constr. change case 3", w2, 42); + ensureTypeAndValue("int constr. change case 4", v, 7); + } + + { + LLSD v = 42; + + LLSD w1(v); + w1 = "bob"; + ensureTypeAndValue("string constr. change case 1", w1, "bob"); + ensureTypeAndValue("string constr. change case 2", v, 42); + + LLSD w2(v); + v = "amy"; + ensureTypeAndValue("string constr. change case 3", w2, 42); + ensureTypeAndValue("string constr. change case 4", v, "amy"); + } + + { + LLSD v = 42; + + LLSD w0; + w0 = v; + ensureTypeAndValue("int assign", w0, 42); + + LLSD w1; + w1 = v; + w1 = 13; + ensureTypeAndValue("int assign change case 1", w1, 13); + ensureTypeAndValue("int assign change case 2", v, 42); + + LLSD w2; + w2 = v; + v = 7; + ensureTypeAndValue("int assign change case 3", w2, 42); + ensureTypeAndValue("int assign change case 4", v, 7); + } + + { + LLSD v = 42; + + LLSD w1; + w1 = v; + w1 = "bob"; + ensureTypeAndValue("string assign change case 1", w1, "bob"); + ensureTypeAndValue("string assign change case 2", v, 42); + + LLSD w2; + w2 = v; + v = "amy"; + ensureTypeAndValue("string assign change case 3", w2, 42); + ensureTypeAndValue("string assign change case 4", v, "amy"); + } + } + + + template<> template<> + void SDTestObject::test<7>() + // Test assignment and casting to various scalar types. These + // assignments should invoke the right conversion without it being + // mentioned explicitly. The few exceptions are marked SAD. + { + SDCleanupCheck check; + + LLSD v(" 42.375"); + + bool b = false; + b = v; ensure_equals("assign to bool", b, true); + b = (bool)v; ensure_equals("cast to bool", b, true); + + int i = 99; + i = v; ensure_equals("assign to int", i, 42); + i = (int)v; ensure_equals("cast to int", i, 42); + + double d = 3.14159; + d = v; ensure_equals("assign to double", d, 42.375); + d = (double)v; ensure_equals("cast to double", d, 42.375); + + std::string s = "yo"; +// SAD s = v; ensure_equals("assign to string", s, " 42.375"); + s = (std::string)v; ensure_equals("cast to string", s, " 42.375"); + + std::string uuidStr = "b1e50c2b-b627-4d23-8a86-a65d97b6319b"; + v = uuidStr; + LLUUID u; + u = v; + ensure_equals("assign to LLUUID", u, LLUUID(uuidStr)); +// SAD u = (LLUUID)v; +// ensure_equals("cast to LLUUID", u, LLUUID(uuidStr)); + + std::string dateStr = "2005-10-24T15:00:00Z"; + v = dateStr; + LLDate date; + date = v; + ensure_equals("assign to LLDate", date.asString(), dateStr); +// SAD date = (LLDate)v; +// ensure_equals("cast to LLDate", date.asString(), dateStr); + + std::string uriStr = "http://secondlife.com"; + v = uriStr; + LLURI uri; + uri = v; + ensure_equals("assign to LLURI", uri.asString(), uriStr); +// SAD uri = (LLURI)v; +// ensure_equals("cast to LLURI", uri.asString(), uriStr); + } + + template<> template<> + void SDTestObject::test<8>() + // Test construction of various scalar types from LLSD. + // Test both construction and initialization forms. + // These should invoke the right conversion without it being + // mentioned explicitly. The few exceptions are marked SAD. + { + SDCleanupCheck check; + + LLSD v(" 42.375"); + + bool b1(v); ensure_equals("contruct bool", b1, true); + bool b2 = v; ensure_equals("initialize bool", b2, true); + + int i1(v); ensure_equals("contruct int", i1, 42); + int i2 = v; ensure_equals("initialize int", i2, 42); + + double d1(v); ensure_equals("contruct double", d1, 42.375); + double d2 = v; ensure_equals("initialize double", d2, 42.375); + + std::string s1(v); + std::string s2 = v; + ensure_equals("contruct string", s1, " 42.375"); + ensure_equals("initialize string", s2, " 42.375"); + + std::string t1(v); + std::string t2 = v.asString(); // SAD + ensure_equals("contruct std::string", t1, " 42.375"); + ensure_equals("initialize std::string", t2, " 42.375"); + + std::string uuidStr = "b1e50c2b-b627-4d23-8a86-a65d97b6319b"; + v = uuidStr; + LLUUID uuid1(v.asUUID()); // SAD + LLUUID uuid2 = v; + ensure_equals("contruct LLUUID", uuid1, LLUUID(uuidStr)); + ensure_equals("initialize LLUUID", uuid2, LLUUID(uuidStr)); + + std::string dateStr = "2005-10-24T15:00:00Z"; + v = dateStr; + LLDate date1(v.asDate()); // SAD + LLDate date2 = v; + ensure_equals("contruct LLDate", date1.asString(), dateStr); + ensure_equals("initialize LLDate", date2.asString(), dateStr); + + std::string uriStr = "http://secondlife.com"; + v = uriStr; + LLURI uri1(v.asURI()); // SAD + LLURI uri2 = v; + ensure_equals("contruct LLURI", uri1.asString(), uriStr); + ensure_equals("initialize LLURI", uri2.asString(), uriStr); + } + + + template<> template<> + void SDTestObject::test<9>() + // test to make sure v is interpreted as a bool in a various + // scenarios. + { + SDCleanupCheck check; + + LLSD v = "0"; + // magic value that is interpreted as boolean true, but integer false! + + ensure_equals("trinary operator bool", (v ? true : false), true); + ensure_equals("convert to int, then bool", + ((int)v ? true : false), false); + + if(v) + { + ensure("if converted to bool", true); + } + else + { + fail("bool did not convert to a bool in if statement."); + } + + if(!v) + { + fail("bool did not convert to a bool in negated if statement."); + } + } + + template<> template<> + void SDTestObject::test<10>() + // map operations + { + SDCleanupCheck check; + + LLSD v; + ensure("undefined has no members", !v.has("amy")); + ensure("undefined get() is undefined", v.get("bob").isUndefined()); + + v = LLSD::emptyMap(); + ensure("empty map is a map", v.isMap()); + ensure("empty map has no members", !v.has("cam")); + ensure("empty map get() is undefined", v.get("don").isUndefined()); + + v.clear(); + v.insert("eli", 43); + ensure("insert converts to map", v.isMap()); + ensure("inserted key is present", v.has("eli")); + ensureTypeAndValue("inserted value", v.get("eli"), 43); + + v.insert("fra", false); + ensure("first key still present", v.has("eli")); + ensure("second key is present", v.has("fra")); + ensureTypeAndValue("first value", v.get("eli"), 43); + ensureTypeAndValue("second value", v.get("fra"), false); + + v.erase("eli"); + ensure("first key now gone", !v.has("eli")); + ensure("second key still present", v.has("fra")); + ensure("first value gone", v.get("eli").isUndefined()); + ensureTypeAndValue("second value sill there", v.get("fra"), false); + + v.erase("fra"); + ensure("second key now gone", !v.has("fra")); + ensure("second value gone", v.get("fra").isUndefined()); + + v["gil"] = (std::string)"good morning"; + ensure("third key present", v.has("gil")); + ensureTypeAndValue("third key value", v.get("gil"), "good morning"); + + const LLSD& cv = v; // FIX ME IF POSSIBLE + ensure("missing key", cv["ham"].isUndefined()); + ensure("key not present", !v.has("ham")); + + LLSD w = 43; + const LLSD& cw = w; // FIX ME IF POSSIBLE + int i = cw["ian"]; + ensureTypeAndValue("other missing value", i, 0); + ensure("other missing key", !w.has("ian")); + ensure("no conversion", w.isInteger()); + + LLSD x; + x = v; + ensure("copy map type", x.isMap()); + ensureTypeAndValue("copy map value gil", x.get("gil"), "good morning"); + } + + + template<> template<> + void SDTestObject::test<11>() + // array operations + { + SDCleanupCheck check; + + LLSD v; + ensure_equals("undefined has no size", v.size(), 0); + ensure("undefined get() is undefined", v.get(0).isUndefined()); + + v = LLSD::emptyArray(); + ensure("empty array is an array", v.isArray()); + ensure_equals("empty array has no size", v.size(), 0); + ensure("empty map get() is undefined", v.get(0).isUndefined()); + + v.clear(); + v.append(88); + v.append("noodle"); + v.append(true); + ensure_equals("appened array size", v.size(), 3); + ensure("append array is an array", v.isArray()); + ensureTypeAndValue("append 0", v[0], 88); + ensureTypeAndValue("append 1", v[1], "noodle"); + ensureTypeAndValue("append 2", v[2], true); + + v.insert(0, 77); + v.insert(2, "soba"); + v.insert(4, false); + ensure_equals("inserted array size", v.size(), 6); + ensureTypeAndValue("post insert 0", v[0], 77); + ensureTypeAndValue("post insert 1", v[1], 88); + ensureTypeAndValue("post insert 2", v[2], "soba"); + ensureTypeAndValue("post insert 3", v[3], "noodle"); + ensureTypeAndValue("post insert 4", v[4], false); + ensureTypeAndValue("post insert 5", v[5], true); + + ensureTypeAndValue("get 1", v.get(1), 88); + v.set(1, "hot"); + ensureTypeAndValue("set 1", v.get(1), "hot"); + + v.erase(3); + ensure_equals("post erase array size", v.size(), 5); + ensureTypeAndValue("post erase 0", v[0], 77); + ensureTypeAndValue("post erase 1", v[1], "hot"); + ensureTypeAndValue("post erase 2", v[2], "soba"); + ensureTypeAndValue("post erase 3", v[3], false); + ensureTypeAndValue("post erase 4", v[4], true); + + v.append(34); + ensure_equals("size after append", v.size(), 6); + ensureTypeAndValue("post append 5", v[5], 34); + + LLSD w; + w = v; + ensure("copy array type", w.isArray()); + ensure_equals("copy array size", w.size(), 6); + ensureTypeAndValue("copy array 0", w[0], 77); + ensureTypeAndValue("copy array 1", w[1], "hot"); + ensureTypeAndValue("copy array 2", w[2], "soba"); + ensureTypeAndValue("copy array 3", w[3], false); + ensureTypeAndValue("copy array 4", w[4], true); + ensureTypeAndValue("copy array 5", w[5], 34); + } + + + template<> template<> + void SDTestObject::test<12>() + // no sharing + { + SDCleanupCheck check; + + LLSD a = 99; + LLSD b = a; + a = 34; + ensureTypeAndValue("top level original changed", a, 34); + ensureTypeAndValue("top level copy unaltered", b, 99); + b = a; + b = 66; + ensureTypeAndValue("top level original unaltered", a, 34); + ensureTypeAndValue("top level copy changed", b, 66); + + a[0] = "uno"; + a[1] = 99; + a[2] = 1.414; + b = a; + a[1] = 34; + ensureTypeAndValue("array member original changed", a[1], 34); + ensureTypeAndValue("array member copy unaltered", b[1], 99); + b = a; + b[1] = 66; + ensureTypeAndValue("array member original unaltered", a[1], 34); + ensureTypeAndValue("array member copy changed", b[1], 66); + + a["alpha"] = "uno"; + a["beta"] = 99; + a["gamma"] = 1.414; + b = a; + a["beta"] = 34; + ensureTypeAndValue("map member original changed", a["beta"], 34); + ensureTypeAndValue("map member copy unaltered", b["beta"], 99); + b = a; + b["beta"] = 66; + ensureTypeAndValue("map member original unaltered", a["beta"], 34); + ensureTypeAndValue("map member copy changed", b["beta"], 66); + } + + template<> template<> + void SDTestObject::test<13>() + // sharing implementation + { + SDCleanupCheck check; + + { + SDAllocationCheck check("copy construct undefinded", 0); + LLSD v; + LLSD w = v; + } + + { + SDAllocationCheck check("assign undefined", 0); + LLSD v; + LLSD w; + w = v; + } + + { + SDAllocationCheck check("assign integer value", 1); + LLSD v = 45; + v = 33; + v = 0; + } + + { + SDAllocationCheck check("copy construct integer", 1); + LLSD v = 45; + LLSD w = v; + } + + { + SDAllocationCheck check("assign integer", 1); + LLSD v = 45; + LLSD w; + w = v; + } + + { + SDAllocationCheck check("avoids extra clone", 2); + LLSD v = 45; + LLSD w = v; + w = "nice day"; + } + + { + SDAllocationCheck check("shared values test for threaded work", 9); + + //U32 start_llsd_count = LLSD::outstandingCount(); + + LLSD m = LLSD::emptyMap(); + + m["one"] = 1; + m["two"] = 2; + m["one_copy"] = m["one"]; // 3 (m, "one" and "two") + + m["undef_one"] = LLSD(); + m["undef_two"] = LLSD(); + m["undef_one_copy"] = m["undef_one"]; + + { // Ensure first_array gets freed to avoid counting it + LLSD first_array = LLSD::emptyArray(); + first_array.append(1.0f); + first_array.append(2.0f); + first_array.append(3.0f); // 7 + + m["array"] = first_array; + m["array_clone"] = first_array; + m["array_copy"] = m["array"]; // 7 + } + + m["string_one"] = "string one value"; + m["string_two"] = "string two value"; + m["string_one_copy"] = m["string_one"]; // 9 + + //U32 llsd_object_count = LLSD::outstandingCount(); + //std::cout << "Using " << (llsd_object_count - start_llsd_count) << " LLSD objects" << std::endl; + + //m.dumpStats(); + } + } + + template<> template<> + void SDTestObject::test<14>() + // make sure that assignment of char* NULL in a string does not crash. + { + LLSD v; + v = (const char*)NULL; + ensure("type is a string", v.isString()); + } + + /* TO DO: + conversion of undefined to UUID, Date, URI and Binary + conversion of undefined to map and array + test map operations + test array operations + test array extension + + test copying and assign maps and arrays (clone) + test iteration over map + test iteration over array + test iteration over scalar + + test empty map and empty array are indeed shared + test serializations + */ +} + diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index fae9f7023f..71096523a9 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -52,7 +52,7 @@ typedef U32 uint32_t; #include "llformat.h" #include "llmemorystream.h" -#include "../test/hexdump.h" +#include "hexdump.h" #include "../test/lltut.h" #include "../test/namedtempfile.h" #include "stringize.h" @@ -243,6 +243,58 @@ namespace tut xml_test("binary", expected); } + template<> template<> + void sd_xml_object::test<7>() + { + // Test that stream state (precision and exceptions) is correctly restored after format() + { + std::ostringstream ostr; + + // Set custom precision + ostr.precision(10); + + // Set some exception bits + ostr.exceptions(std::ios_base::badbit); + std::ios_base::iostate original_exceptions = ostr.exceptions(); + + // Format some LLSD data + mSD = 3.141592653589793; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify precision was restored + ensure_equals("precision should be restored", + ostr.precision(), 10); + + // Verify exceptions were restored + ensure_equals("exception bits should be restored", + ostr.exceptions(), original_exceptions); + } + + // Test with no bits set + { + std::ostringstream ostr; + ostr.precision(5); + std::ios_base::iostate original_exceptions = ostr.exceptions(); // 0 + + mSD = "test"; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify exceptions were removed + ensure_equals("exception bits should remain unchanged", + ostr.exceptions(), original_exceptions); + + // Verify precision was still restored even on failure + ensure_equals("precision should be restored even on stream failure", + ostr.precision(), (std::streamsize)5); + } + } + class TestLLSDSerializeData { public: @@ -1808,7 +1860,7 @@ namespace tut #if LL_WINDOWS std::string q("\""); std::string qPYTHON(q + PYTHON + q); - std::string qscript(q + scriptfile.getName() + q); + std::string qscript(q + scriptfile.getPath().string() + q); int rc = (int)_spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(), std::forward<ARGS>(args)..., NULL); if (rc == -1) @@ -1825,7 +1877,7 @@ namespace tut #else // LL_DARWIN, LL_LINUX LLProcess::Params params; params.executable = PYTHON; - params.args.add(scriptfile.getName()); + params.args.add(scriptfile.getPath().string()); for (const std::string& arg : StringVec{ std::forward<ARGS>(args)... }) { params.args.add(arg); @@ -1921,12 +1973,12 @@ namespace tut int bufflen{ static_cast<int>(buffstr.length()) }; out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen)); LL_DEBUGS() << "Wrote length: " - << hexdump(reinterpret_cast<const char*>(&bufflen), + << LL::hexdump(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen)) << LL_ENDL; out.write(buffstr.c_str(), buffstr.length()); LL_DEBUGS() << "Wrote data: " - << hexmix(buffstr.c_str(), buffstr.length()) + << LL::hexmix(buffstr.c_str(), buffstr.length()) << LL_ENDL; } } @@ -2002,8 +2054,8 @@ namespace tut " yield frombytes\n" << pydata << // Don't forget raw-string syntax for Windows pathnames. - "debug = open(r'" << debug.getName() << "', 'w')\n" - "verify(parse_each(open(r'" << file.getName() << "', 'rb')))\n";}); + "debug = open(r'" << debug.getPath().string() << "', 'w')\n" + "verify(parse_each(open(r'" << file.getPath().string() << "', 'rb')))\n";}); } catch (const failure&) { @@ -2111,13 +2163,13 @@ namespace tut "]\n" // Don't forget raw-string syntax for Windows pathnames. // N.B. Using 'print' implicitly adds newlines. - "with open(r'" << file.getName() << "', 'wb') as f:\n" + "with open(r'" << (const char*)file.getPath().u8string().c_str() << "', 'wb') as f:\n" " for item in DATA:\n" " serialized = llsd." << pyformatter << "(item)\n" " f.write(lenformat.pack(len(serialized)))\n" " f.write(serialized)\n";}); - std::ifstream inf(file.getName().c_str()); + llifstream inf(file.getPath()); LLSD item; try { diff --git a/indra/llcommon/tests/llsdutil_test.cpp b/indra/llcommon/tests/llsdutil_test.cpp new file mode 100644 index 0000000000..4b25a02f30 --- /dev/null +++ b/indra/llcommon/tests/llsdutil_test.cpp @@ -0,0 +1,434 @@ +/** + * @file llsdutil_tut.cpp + * @author Adroit + * @date 2007-02 + * @brief LLSD conversion routines test cases. + * + * $LicenseInfo:firstyear=2007&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "lltut.h" +#include "m4math.h" +#include "v2math.h" +#include "v2math.h" +#include "v3color.h" +#include "v3math.h" +#include "v3dmath.h" +#include "v4coloru.h" +#include "v4math.h" +#include "llquaternion.h" +#include "llsdutil.h" +#include "llsdutil_math.h" +#include "stringize.h" +#include <set> +#include <boost/range.hpp> + +namespace tut +{ + struct llsdutil_data + { + void test_matches(const std::string& proto_key, const LLSD& possibles, + const char** begin, const char** end) + { + std::set<std::string> succeed(begin, end); + LLSD prototype(possibles[proto_key]); + for (LLSD::map_const_iterator pi(possibles.beginMap()), pend(possibles.endMap()); + pi != pend; ++pi) + { + std::string match(llsd_matches(prototype, pi->second)); + std::set<std::string>::const_iterator found = succeed.find(pi->first); + if (found != succeed.end()) + { + // This test is supposed to succeed. Comparing to the + // empty string ensures that if the test fails, it will + // display the string received so we can tell what failed. + ensure_equals("match", match, ""); + } + else + { + // This test is supposed to fail. If we get a false match, + // the string 'match' will be empty, which doesn't tell us + // much about which case went awry. So construct a more + // detailed description string. + ensure(proto_key + " shouldn't match " + pi->first, ! match.empty()); + } + } + } + }; + typedef test_group<llsdutil_data> llsdutil_test;; + typedef llsdutil_test::object llsdutil_object; + tut::llsdutil_test tutil("llsdutil"); + + template<> template<> + void llsdutil_object::test<1>() + { + LLSD sd; + U64 valueIn , valueOut; + valueIn = U64L(0xFEDCBA9876543210); + sd = ll_sd_from_U64(valueIn); + valueOut = ll_U64_from_sd(sd); + ensure_equals("U64 valueIn->sd->valueOut", valueIn, valueOut); + } + + template<> template<> + void llsdutil_object::test<2>() + { + LLSD sd; + U32 valueIn, valueOut; + valueIn = 0x87654321; + sd = ll_sd_from_U32(valueIn); + valueOut = ll_U32_from_sd(sd); + ensure_equals("U32 valueIn->sd->valueOut", valueIn, valueOut); + } + + template<> template<> + void llsdutil_object::test<3>() + { + U32 valueIn, valueOut; + valueIn = 0x87654321; + LLSD sd; + sd = ll_sd_from_ipaddr(valueIn); + valueOut = ll_ipaddr_from_sd(sd); + ensure_equals("valueIn->sd->valueOut", valueIn, valueOut); + } + + template<> template<> + void llsdutil_object::test<4>() + { + LLSD sd; + LLVector3 vec1(-1.0, 2.0, -3.0); + sd = ll_sd_from_vector3(vec1); + LLVector3 vec2 = ll_vector3_from_sd(sd); + ensure_equals("vector3 -> sd -> vector3: 1", vec1, vec2); + + LLVector3 vec3(sd); + ensure_equals("vector3 -> sd -> vector3: 2", vec1, vec3); + + sd.clear(); + vec1.setVec(0., 0., 0.); + sd = ll_sd_from_vector3(vec1); + vec2 = ll_vector3_from_sd(sd); + ensure_equals("vector3 -> sd -> vector3: 3", vec1, vec2); + sd.clear(); + } + + template<> template<> + void llsdutil_object::test<5>() + { + LLSD sd; + LLVector3d vec1((F64)(U64L(0xFEDCBA9876543210) << 2), -1., 0); + sd = ll_sd_from_vector3d(vec1); + LLVector3d vec2 = ll_vector3d_from_sd(sd); + ensure_equals("vector3d -> sd -> vector3d: 1", vec1, vec2); + + LLVector3d vec3(sd); + ensure_equals("vector3d -> sd -> vector3d : 2", vec1, vec3); + } + + template<> template<> + void llsdutil_object::test<6>() + { + LLSD sd; + LLVector2 vec((F32) -3., (F32) 4.2); + sd = ll_sd_from_vector2(vec); + LLVector2 vec1 = ll_vector2_from_sd(sd); + ensure_equals("vector2 -> sd -> vector2", vec, vec1); + + LLSD sd2 = ll_sd_from_vector2(vec1); + ensure_equals("sd -> vector2 -> sd: 2", sd, sd2); + } + + template<> template<> + void llsdutil_object::test<7>() + { + LLSD sd; + LLQuaternion quat((F32) 1., (F32) -0.98, (F32) 2.3, (F32) 0xffff); + sd = ll_sd_from_quaternion(quat); + LLQuaternion quat1 = ll_quaternion_from_sd(sd); + ensure_equals("LLQuaternion -> sd -> LLQuaternion", quat, quat1); + + LLSD sd2 = ll_sd_from_quaternion(quat1); + ensure_equals("sd -> LLQuaternion -> sd ", sd, sd2); + } + + template<> template<> + void llsdutil_object::test<8>() + { + LLSD sd; + LLColor4 c(1.0f, 2.2f, 4.0f, 7.f); + sd = ll_sd_from_color4(c); + LLColor4 c1 =ll_color4_from_sd(sd); + ensure_equals("LLColor4 -> sd -> LLColor4", c, c1); + + LLSD sd1 = ll_sd_from_color4(c1); + ensure_equals("sd -> LLColor4 -> sd", sd, sd1); + } + + template<> template<> + void llsdutil_object::test<9>() + { + set_test_name("llsd_matches"); + + // for this test, construct a map of all possible LLSD types + LLSD map; + map.insert("empty", LLSD()); + map.insert("Boolean", LLSD::Boolean()); + map.insert("Integer", LLSD::Integer(0)); + map.insert("Real", LLSD::Real(0.0)); + map.insert("String", LLSD::String("bah")); + map.insert("NumString", LLSD::String("1")); + map.insert("UUID", LLSD::UUID()); + map.insert("Date", LLSD::Date()); + map.insert("URI", LLSD::URI()); + map.insert("Binary", LLSD::Binary()); + map.insert("Map", LLSD().with("foo", LLSD())); + // Only an empty array can be constructed on the fly + LLSD array; + array.append(LLSD()); + map.insert("Array", array); + + // These iterators are declared outside our various for loops to avoid + // fatal MSVC warning: "I used to be broken, but I'm all better now!" + LLSD::map_const_iterator mi, mend(map.endMap()); + + /*-------------------------- llsd_matches --------------------------*/ + + // empty prototype matches anything + for (mi = map.beginMap(); mi != mend; ++mi) + { + ensure_equals(std::string("empty matches ") + mi->first, llsd_matches(LLSD(), mi->second), ""); + } + + LLSD proto_array, data_array; + for (int i = 0; i < 3; ++i) + { + proto_array.append(LLSD()); + data_array.append(LLSD()); + } + + // prototype array matches only array + for (mi = map.beginMap(); mi != mend; ++mi) + { + ensure(std::string("array doesn't match ") + mi->first, + ! llsd_matches(proto_array, mi->second).empty()); + } + + // data array must be at least as long as prototype array + proto_array.append(LLSD()); + ensure_equals("data array too short", llsd_matches(proto_array, data_array), + "Array size 4 required instead of Array size 3"); + data_array.append(LLSD()); + ensure_equals("data array just right", llsd_matches(proto_array, data_array), ""); + data_array.append(LLSD()); + ensure_equals("data array longer", llsd_matches(proto_array, data_array), ""); + + // array element matching + data_array[0] = LLSD::String(); + ensure_equals("undefined prototype array entry", llsd_matches(proto_array, data_array), ""); + proto_array[0] = LLSD::Binary(); + ensure_equals("scalar prototype array entry", llsd_matches(proto_array, data_array), + "[0]: Binary required instead of String"); + data_array[0] = LLSD::Binary(); + ensure_equals("matching prototype array entry", llsd_matches(proto_array, data_array), ""); + + // build a coupla maps + LLSD proto_map, data_map; + data_map["got"] = LLSD(); + data_map["found"] = LLSD(); + for (LLSD::map_const_iterator dmi(data_map.beginMap()), dmend(data_map.endMap()); + dmi != dmend; ++dmi) + { + proto_map[dmi->first] = dmi->second; + } + proto_map["foo"] = LLSD(); + proto_map["bar"] = LLSD(); + + // prototype map matches only map + for (mi = map.beginMap(); mi != mend; ++mi) + { + ensure(std::string("map doesn't match ") + mi->first, + ! llsd_matches(proto_map, mi->second).empty()); + } + + // data map must contain all keys in prototype map + std::string error(llsd_matches(proto_map, data_map)); + ensure_contains("missing keys", error, "missing keys"); + ensure_contains("missing foo", error, "foo"); + ensure_contains("missing bar", error, "bar"); + ensure_does_not_contain("found found", error, "found"); + ensure_does_not_contain("got got", error, "got"); + data_map["bar"] = LLSD(); + error = llsd_matches(proto_map, data_map); + ensure_contains("missing foo", error, "foo"); + ensure_does_not_contain("got bar", error, "bar"); + data_map["foo"] = LLSD(); + ensure_equals("data map just right", llsd_matches(proto_map, data_map), ""); + data_map["extra"] = LLSD(); + ensure_equals("data map with extra", llsd_matches(proto_map, data_map), ""); + + // map element matching + data_map["foo"] = LLSD::String(); + ensure_equals("undefined prototype map entry", llsd_matches(proto_map, data_map), ""); + proto_map["foo"] = LLSD::Binary(); + ensure_equals("scalar prototype map entry", llsd_matches(proto_map, data_map), + "['foo']: Binary required instead of String"); + data_map["foo"] = LLSD::Binary(); + ensure_equals("matching prototype map entry", llsd_matches(proto_map, data_map), ""); + + // String + { + static const char* matches[] = { "String", "NumString", "Boolean", "Integer", + "Real", "UUID", "Date", "URI" }; + test_matches("String", map, boost::begin(matches), boost::end(matches)); + } + + // Boolean, Integer, Real + static const char* numerics[] = { "Boolean", "Integer", "Real" }; + for (const char **ni = boost::begin(numerics), **nend = boost::end(numerics); + ni != nend; ++ni) + { + static const char* matches[] = { "Boolean", "Integer", "Real", "String", "NumString" }; + test_matches(*ni, map, boost::begin(matches), boost::end(matches)); + } + + // UUID + { + static const char* matches[] = { "UUID", "String", "NumString" }; + test_matches("UUID", map, boost::begin(matches), boost::end(matches)); + } + + // Date + { + static const char* matches[] = { "Date", "String", "NumString" }; + test_matches("Date", map, boost::begin(matches), boost::end(matches)); + } + + // URI + { + static const char* matches[] = { "URI", "String", "NumString" }; + test_matches("URI", map, boost::begin(matches), boost::end(matches)); + } + + // Binary + { + static const char* matches[] = { "Binary" }; + test_matches("Binary", map, boost::begin(matches), boost::end(matches)); + } + + /*-------------------------- llsd_equals ---------------------------*/ + + // Cross-product of each LLSD type with every other + for (LLSD::map_const_iterator lmi(map.beginMap()), lmend(map.endMap()); + lmi != lmend; ++lmi) + { + for (LLSD::map_const_iterator rmi(map.beginMap()), rmend(map.endMap()); + rmi != rmend; ++rmi) + { + // Name this test based on the map keys naming the types of + // interest, e.g "String::Integer". + // We expect the values (xmi->second) to be equal if and only + // if the type names (xmi->first) are equal. + ensure(STRINGIZE(lmi->first << "::" << rmi->first), + bool(lmi->first == rmi->first) == + bool(llsd_equals(lmi->second, rmi->second))); + } + } + + // Array cases + LLSD rarray; + rarray.append(1.0); + rarray.append(2); + rarray.append("3"); + LLSD larray(rarray); + ensure("llsd_equals(equal arrays)", llsd_equals(larray, rarray)); + rarray[2] = "4"; + ensure("llsd_equals(different [2])", ! llsd_equals(larray, rarray)); + rarray = larray; + rarray.append(LLSD::Date()); + ensure("llsd_equals(longer right array)", ! llsd_equals(larray, rarray)); + rarray = larray; + rarray.erase(2); + ensure("llsd_equals(shorter right array)", ! llsd_equals(larray, rarray)); + + // Map cases + LLSD rmap; + rmap["San Francisco"] = 65; + rmap["Phoenix"] = 92; + rmap["Boston"] = 77; + LLSD lmap(rmap); + ensure("llsd_equals(equal maps)", llsd_equals(lmap, rmap)); + rmap["Boston"] = 80; + ensure("llsd_equals(different [\"Boston\"])", ! llsd_equals(lmap, rmap)); + rmap = lmap; + rmap["Atlanta"] = 95; + ensure("llsd_equals(superset right map)", ! llsd_equals(lmap, rmap)); + rmap = lmap; + lmap["Seattle"] = 72; + ensure("llsd_equals(superset left map)", ! llsd_equals(lmap, rmap)); + } + + template<> template<> + void llsdutil_object::test<10>() + { + set_test_name("llsd_hashing"); + + { + LLSD data_s1 = LLSD::String("The quick brown aardvark jumped over the lazy lemming."); + LLSD data_s2 = LLSD::String("The quick brown aardvark jumped over the lazy lemming."); + + ensure("hash: Identical string hashes match.", boost::hash<LLSD>{}(data_s1) == boost::hash<LLSD>{}(data_s2)); + } + { + LLSD data_r1 = LLSD::Real(3.0f); + LLSD data_i1 = LLSD::Integer(3); + ensure("hash: equivalent values but different types do not match.", boost::hash<LLSD>{}(data_r1) != boost::hash<LLSD>{}(data_i1)); + } + { + LLSD data_a1 = llsd::array("A", "B", "C"); + LLSD data_a2 = llsd::array("A", "B", "C"); + + ensure("hash: identical arrays produce identical results", boost::hash<LLSD>{}(data_a1) == boost::hash<LLSD>{}(data_a2)); + + data_a2.append(llsd::array(1, 2)); + + ensure("hash: changing the array changes the hash.", boost::hash<LLSD>{}(data_a1) != boost::hash<LLSD>{}(data_a2)); + + data_a1.append(llsd::array(1, 2)); + ensure("hash: identical arrays produce identical results with nested arrays", boost::hash<LLSD>{}(data_a1) == boost::hash<LLSD>{}(data_a2)); + } + { + LLSD data_m1 = LLSDMap("key1", LLSD::Real(3.0))("key2", "value2")("key3", llsd::array(1, 2, 3)); + LLSD data_m2 = LLSDMap("key1", LLSD::Real(3.0))("key2", "value2")("key3", llsd::array(1, 2, 3)); + + ensure("hash: identical maps produce identical results", boost::hash<LLSD>{}(data_m1) == boost::hash<LLSD>{}(data_m2)); + + LLSD data_m3 = LLSDMap("key1", LLSD::Real(5.0))("key2", "value2")("key3", llsd::array(1, 2, 3)); + ensure("hash: Different values in the map produce different hashes.", boost::hash<LLSD>{}(data_m1) != boost::hash<LLSD>{}(data_m3)); + + LLSD data_m4 = LLSDMap("keyA", LLSD::Real(3.0))("key2", "value2")("key3", llsd::array(1, 2, 3)); + ensure("hash: Different keys in the map produce different hashes.", boost::hash<LLSD>{}(data_m1) != boost::hash<LLSD>{}(data_m4)); + + } + } + +} diff --git a/indra/llcommon/tests/llstreamqueue_test.cpp b/indra/llcommon/tests/llstreamqueue_test.cpp deleted file mode 100644 index 82b451119e..0000000000 --- a/indra/llcommon/tests/llstreamqueue_test.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @file llstreamqueue_test.cpp - * @author Nat Goodspeed - * @date 2012-01-05 - * @brief Test for llstreamqueue. - * - * $LicenseInfo:firstyear=2012&license=viewerlgpl$ - * Copyright (c) 2012, Linden Research, Inc. - * $/LicenseInfo$ - */ - -// Precompiled header -#include "linden_common.h" -// associated header -#include "llstreamqueue.h" -// STL headers -#include <vector> -// other Linden headers -#include "../test/lltut.h" -#include "stringize.h" - -/***************************************************************************** -* TUT -*****************************************************************************/ -namespace tut -{ - struct llstreamqueue_data - { - llstreamqueue_data(): - // we want a buffer with actual bytes in it, not an empty vector - buffer(10) - {} - // As LLStreamQueue is merely a typedef for - // LLGenericStreamQueue<char>, and no logic in LLGenericStreamQueue is - // specific to the <char> instantiation, we're comfortable for now - // testing only the narrow-char version. - LLStreamQueue strq; - // buffer for use in multiple tests - std::vector<char> buffer; - }; - typedef test_group<llstreamqueue_data> llstreamqueue_group; - typedef llstreamqueue_group::object object; - llstreamqueue_group llstreamqueuegrp("llstreamqueue"); - - template<> template<> - void object::test<1>() - { - set_test_name("empty LLStreamQueue"); - ensure_equals("brand-new LLStreamQueue isn't empty", - strq.size(), 0); - ensure_equals("brand-new LLStreamQueue returns data", - strq.asSource().read(&buffer[0], buffer.size()), 0); - strq.asSink().close(); - ensure_equals("closed empty LLStreamQueue not at EOF", - strq.asSource().read(&buffer[0], buffer.size()), -1); - } - - template<> template<> - void object::test<2>() - { - set_test_name("one internal block, one buffer"); - LLStreamQueue::Sink sink(strq.asSink()); - ensure_equals("write(\"\")", sink.write("", 0), 0); - ensure_equals("0 write should leave LLStreamQueue empty (size())", - strq.size(), 0); - ensure_equals("0 write should leave LLStreamQueue empty (peek())", - strq.peek(&buffer[0], buffer.size()), 0); - // The meaning of "atomic" is that it must be smaller than our buffer. - std::string atomic("atomic"); - ensure("test data exceeds buffer", atomic.length() < buffer.size()); - ensure_equals(STRINGIZE("write(\"" << atomic << "\")"), - sink.write(&atomic[0], atomic.length()), atomic.length()); - ensure_equals("size() after write()", strq.size(), atomic.length()); - size_t peeklen(strq.peek(&buffer[0], buffer.size())); - ensure_equals(STRINGIZE("peek(\"" << atomic << "\")"), - peeklen, atomic.length()); - ensure_equals(STRINGIZE("peek(\"" << atomic << "\") result"), - std::string(buffer.begin(), buffer.begin() + peeklen), atomic); - ensure_equals("size() after peek()", strq.size(), atomic.length()); - // peek() should not consume. Use a different buffer to prove it isn't - // just leftover data from the first peek(). - std::vector<char> again(buffer.size()); - peeklen = size_t(strq.peek(&again[0], again.size())); - ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again"), - peeklen, atomic.length()); - ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again result"), - std::string(again.begin(), again.begin() + peeklen), atomic); - // now consume. - std::vector<char> third(buffer.size()); - size_t readlen(strq.read(&third[0], third.size())); - ensure_equals(STRINGIZE("read(\"" << atomic << "\")"), - readlen, atomic.length()); - ensure_equals(STRINGIZE("read(\"" << atomic << "\") result"), - std::string(third.begin(), third.begin() + readlen), atomic); - ensure_equals("peek() after read()", strq.peek(&buffer[0], buffer.size()), 0); - ensure_equals("size() after read()", strq.size(), 0); - } - - template<> template<> - void object::test<3>() - { - set_test_name("basic skip()"); - std::string lovecraft("lovecraft"); - ensure("test data exceeds buffer", lovecraft.length() < buffer.size()); - ensure_equals(STRINGIZE("write(\"" << lovecraft << "\")"), - strq.write(&lovecraft[0], lovecraft.length()), lovecraft.length()); - size_t peeklen(strq.peek(&buffer[0], buffer.size())); - ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\")"), - peeklen, lovecraft.length()); - ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\") result"), - std::string(buffer.begin(), buffer.begin() + peeklen), lovecraft); - std::streamsize skip1(4); - ensure_equals(STRINGIZE("skip(" << skip1 << ")"), strq.skip(skip1), skip1); - ensure_equals("size() after skip()", strq.size(), lovecraft.length() - skip1); - size_t readlen(strq.read(&buffer[0], buffer.size())); - ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\")"), - readlen, lovecraft.length() - skip1); - ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\") result"), - std::string(buffer.begin(), buffer.begin() + readlen), - lovecraft.substr(skip1)); - ensure_equals("unconsumed", strq.read(&buffer[0], buffer.size()), 0); - } - - template<> template<> - void object::test<4>() - { - set_test_name("skip() multiple blocks"); - std::string blocks[] = { "books of ", "H.P. ", "Lovecraft" }; - std::streamsize total(blocks[0].length() + blocks[1].length() + blocks[2].length()); - std::streamsize leave(5); // len("craft") above - std::streamsize skip(total - leave); - std::streamsize written(0); - for (const std::string& block : blocks) - { - written += strq.write(&block[0], block.length()); - ensure_equals("size() after write()", strq.size(), written); - } - std::streamsize skiplen(strq.skip(skip)); - ensure_equals(STRINGIZE("skip(" << skip << ")"), skiplen, skip); - ensure_equals("size() after skip()", strq.size(), leave); - size_t readlen(strq.read(&buffer[0], buffer.size())); - ensure_equals("read(\"craft\")", readlen, leave); - ensure_equals("read(\"craft\") result", - std::string(buffer.begin(), buffer.begin() + readlen), "craft"); - } - - template<> template<> - void object::test<5>() - { - set_test_name("concatenate blocks"); - std::string blocks[] = { "abcd", "efghij", "klmnopqrs" }; - for (const std::string& block : blocks) - { - strq.write(&block[0], block.length()); - } - std::vector<char> longbuffer(30); - std::streamsize readlen(strq.read(&longbuffer[0], longbuffer.size())); - ensure_equals("read() multiple blocks", - readlen, blocks[0].length() + blocks[1].length() + blocks[2].length()); - ensure_equals("read() multiple blocks result", - std::string(longbuffer.begin(), longbuffer.begin() + readlen), - blocks[0] + blocks[1] + blocks[2]); - } - - template<> template<> - void object::test<6>() - { - set_test_name("split blocks"); - std::string blocks[] = { "abcdefghijklm", "nopqrstuvwxyz" }; - for (const std::string& block : blocks) - { - strq.write(&block[0], block.length()); - } - strq.close(); - // We've already verified what strq.size() should be at this point; - // see above test named "skip() multiple blocks" - std::streamsize chksize(strq.size()); - std::streamsize readlen(strq.read(&buffer[0], buffer.size())); - ensure_equals("read() 0", readlen, buffer.size()); - ensure_equals("read() 0 result", std::string(buffer.begin(), buffer.end()), "abcdefghij"); - chksize -= readlen; - ensure_equals("size() after read() 0", strq.size(), chksize); - readlen = strq.read(&buffer[0], buffer.size()); - ensure_equals("read() 1", readlen, buffer.size()); - ensure_equals("read() 1 result", std::string(buffer.begin(), buffer.end()), "klmnopqrst"); - chksize -= readlen; - ensure_equals("size() after read() 1", strq.size(), chksize); - readlen = strq.read(&buffer[0], buffer.size()); - ensure_equals("read() 2", readlen, chksize); - ensure_equals("read() 2 result", - std::string(buffer.begin(), buffer.begin() + readlen), "uvwxyz"); - ensure_equals("read() 3", strq.read(&buffer[0], buffer.size()), -1); - } -} // namespace tut diff --git a/indra/llcommon/tests/llstreamtools_test.cpp b/indra/llcommon/tests/llstreamtools_test.cpp new file mode 100644 index 0000000000..970afb3b02 --- /dev/null +++ b/indra/llcommon/tests/llstreamtools_test.cpp @@ -0,0 +1,883 @@ +/** + * @file llstreamtools_tut.cpp + * @author Adroit + * @date February 2007 + * @brief llstreamtools test cases. + * + * $LicenseInfo:firstyear=2007&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include <tut/tut.hpp> + +#include "linden_common.h" +#include "llstreamtools.h" +#include "lltut.h" + + +namespace tut +{ + struct streamtools_data + { + }; + typedef test_group<streamtools_data> streamtools_test; + typedef streamtools_test::object streamtools_object; + tut::streamtools_test streamtools_testcase("streamtools"); + + //test cases for skip_whitespace() + template<> template<> + void streamtools_object::test<1>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + is.str(str = ""); + ensure("skip_whitespace: empty string", (false == skip_whitespace(is))); + + is.clear(); + is.str(str = " SecondLife is a 3D World"); + skip_whitespace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World"; + ensure_equals("skip_whitespace: space", arr, expected_result); + + is.clear(); + is.str(str = "\t \tSecondLife is a 3D World"); + skip_whitespace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World"; + ensure_equals("skip_whitespace: space and tabs", arr, expected_result); + + is.clear(); + is.str(str = "\t \tSecondLife is a 3D World "); + skip_whitespace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World "; + ensure_equals("skip_whitespace: space at end", arr, expected_result); + + is.clear(); + is.str(str = "\t \r\nSecondLife is a 3D World"); + skip_whitespace(is); + is.get(arr, 255, '\0'); + expected_result = "\r\nSecondLife is a 3D World"; + ensure_equals("skip_whitespace: space at end", arr, expected_result); + } + + //testcases for skip_emptyspaces() + template<> template<> + void streamtools_object::test<2>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = " \tSecondLife is a 3D World.\n"); + skip_emptyspace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World.\n"; + ensure_equals("skip_emptyspace: space and tabs", arr, expected_result); + + is.clear(); + is.str(str = " \t\r\n \r SecondLife is a 3D World.\n"); + skip_emptyspace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World.\n"; + ensure_equals("skip_emptyspace: space, tabs, carriage return, newline", arr, expected_result); + + is.clear(); + is.str(str = ""); + ret = skip_emptyspace(is); + is.get(arr, 255, '\0'); + ensure("skip_emptyspace: empty string", ret == false); + + is.clear(); + is.str(str = " \r\n \t "); + ret = skip_emptyspace(is); + is.get(arr, 255, '\0'); + ensure("skip_emptyspace: space newline empty", ret == false); + } + + //testcases for skip_comments_and_emptyspace() + template<> template<> + void streamtools_object::test<3>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = " \t\r\n \r SecondLife is a 3D World.\n"); + skip_comments_and_emptyspace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World.\n"; + ensure_equals("skip_comments_and_emptyspace: space, tabs, carriage return, newline", arr, expected_result); + + is.clear(); + is.str(str = "# \r\n SecondLife is a 3D World."); + skip_comments_and_emptyspace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World."; + ensure_equals("skip_comments_and_emptyspace: skip comment - 1", arr, expected_result); + + is.clear(); + is.str(str = "# \r\n # SecondLife is a 3D World. ##"); + ensure("should not be good()", ! skip_comments_and_emptyspace(is)); + ensure("should be at eof()", is.eof()); + // don't get(): given bad state, we can't rely on results + + is.clear(); + is.str(str = " \r\n SecondLife is a 3D World. ##"); + skip_comments_and_emptyspace(is); + is.get(arr, 255, '\0'); + expected_result = "SecondLife is a 3D World. ##"; + ensure_equals("skip_comments_and_emptyspace: skip comment - 3", arr, expected_result); + + is.clear(); + is.str(str = ""); + ret = skip_comments_and_emptyspace(is); + ensure("skip_comments_and_emptyspace: empty string", ! ret); + + is.clear(); + is.str(str = " \r\n \t # SecondLife is a 3D World"); + ret = skip_comments_and_emptyspace(is); + ensure("skip_comments_and_emptyspace: space newline comment empty", ! ret); + } + + //testcases for skip_line() + template<> template<> + void streamtools_object::test<4>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = "SecondLife is a 3D World.\n\n It provides an opportunity to the site \nuser to perform real life activities in virtual world."); + skip_line(is); + is.get(arr, 255, '\0'); + expected_result = "\n It provides an opportunity to the site \nuser to perform real life activities in virtual world."; + ensure_equals("skip_line: 1 newline", arr, expected_result); + + is.clear(); + is.str(expected_result); + skip_line(is); + is.get(arr, 255, '\0'); + expected_result = " It provides an opportunity to the site \nuser to perform real life activities in virtual world."; + ensure_equals("skip_line: 2 newline", arr, expected_result); + + is.clear(); + is.str(expected_result); + skip_line(is); + is.get(arr, 255, '\0'); + expected_result = "user to perform real life activities in virtual world."; + ensure_equals("skip_line: 3 newline", arr, expected_result); + + is.clear(); + is.str(str = ""); + ret = skip_line(is); + ensure("skip_line: empty string", ret == false); + } + + + // testcases for skip_to_next_word() + template<> template<> + void streamtools_object::test<5>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = "SecondLife is a 3D_World.\n\n It-provides an opportunity to the site \nuser to perform real life activities in virtual world."); + skip_to_next_word(is); // get past SecondLife + is.get(arr, 255, '\0'); + expected_result = "is a 3D_World.\n\n It-provides an opportunity to the site \nuser to perform real life activities in virtual world."; + ensure_equals("skip_to_next_word: 1", arr, expected_result); + + is.clear(); + is.str(expected_result); + skip_to_next_word(is); // get past is + skip_to_next_word(is); // get past a + skip_to_next_word(is); // get past 3D_World.\n\n + is.get(arr, 255, '\0'); + expected_result = "It-provides an opportunity to the site \nuser to perform real life activities in virtual world."; + ensure_equals("skip_to_next_word: get past .\n\n 2", arr, expected_result); + + is.clear(); + is.str(expected_result); + skip_to_next_word(is); // get past It- + expected_result = "provides an opportunity to the site \nuser to perform real life activities in virtual world."; + is.get(arr, 255, '\0'); + ensure_equals("skip_to_next_word: get past -", arr, expected_result); + + is.clear(); + is.str(str = ""); + ret = skip_to_next_word(is); + ensure("skip_line: empty string", ret == false); + + is.clear(); + is.str(str = " \r\n\r\n"); + ret = skip_to_next_word(is); + ensure("skip_line: space new lines", ret == false); + } + + + //testcases for skip_to_end_of_next_keyword() + template<> template<> + void streamtools_object::test<6>() + { + char arr[255]; + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = "FIRSTKEY followed by second delimiter\nSECONDKEY\t SecondValue followed by third delimiter \nSECONDKEY\nFOURTHKEY FOURTHVALUEis a 3DWorld."); + ret = skip_to_end_of_next_keyword("FIRSTKEY", is); + is.get(arr, 255, '\0'); + expected_result = " followed by second delimiter\nSECONDKEY\t SecondValue followed by third delimiter \nSECONDKEY\nFOURTHKEY FOURTHVALUEis a 3DWorld."; + ensure_equals("skip_to_end_of_next_keyword: 1", arr, expected_result); + + is.clear(); + is.str(expected_result); + ret = skip_to_end_of_next_keyword("SECONDKEY", is); + is.get(arr, 255, '\0'); + expected_result = "\t SecondValue followed by third delimiter \nSECONDKEY\nFOURTHKEY FOURTHVALUEis a 3DWorld."; + ensure_equals("skip_to_end_of_next_keyword: 2", arr, expected_result); + + is.clear(); + is.str(expected_result); + ret = skip_to_end_of_next_keyword("SECONDKEY", is); + is.get(arr, 255, '\0'); + expected_result = "\nFOURTHKEY FOURTHVALUEis a 3DWorld."; + ensure_equals("skip_to_end_of_next_keyword: 3", arr, expected_result); + + is.clear(); + is.str(expected_result); + ret = skip_to_end_of_next_keyword("FOURTHKEY", is); + is.get(arr, 255, '\0'); + expected_result = " FOURTHVALUEis a 3DWorld."; + ensure_equals("skip_to_end_of_next_keyword: 4", arr, expected_result); + + is.clear(); + is.str(str = "{should be skipped as newline/space/tab does not follow but this one should be picked\n { Does it?\n"); + ret = skip_to_end_of_next_keyword("{", is); + is.get(arr, 255, '\0'); + expected_result = " Does it?\n"; + ensure_equals("skip_to_end_of_next_keyword: multiple delim matches on same line", arr, expected_result); + + is.clear(); + is.str(str = "Delim { could not be found at start"); + ret = skip_to_end_of_next_keyword("{", is); + ensure("skip_to_end_of_next_keyword: delim should not be present", ret == false); + + is.clear(); + is.str(str = "Empty Delim"); + ret = skip_to_end_of_next_keyword("", is); + ensure("skip_to_end_of_next_keyword: empty delim should not be valid", ret == false); + + is.clear(); + is.str(str = ""); + ret = skip_to_end_of_next_keyword("}", is); + ensure("skip_to_end_of_next_keyword: empty string", ret == false); + } + + //testcases for get_word(std::string& output_string, std::istream& input_stream) + template<> template<> + void streamtools_object::test<7>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = " First Second \t \r \n Third Fourth-ShouldThisBePartOfFourth Fifth\n"); + actual_result = ""; + ret = get_word(actual_result, is); + expected_result = "First"; + ensure_equals("get_word: 1", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is); + expected_result = "Second"; + ensure_equals("get_word: 2", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is); + expected_result = "Third"; + ensure_equals("get_word: 3", actual_result, expected_result); + + // the current implementation of get_word seems inconsistent with + // skip_to_next_word. skip_to_next_word treats any character other + // than alhpa-numeric and '_' as a delimter, while get_word() + // treats only isspace() (i.e. space, form-feed('\f'), newline ('\n'), + // carriage return ('\r'), horizontal tab ('\t'), and vertical tab ('\v') + // as delimiters + actual_result = ""; + ret = get_word(actual_result, is); // will copy Fourth-ShouldThisBePartOfFourth + expected_result = "Fourth-ShouldThisBePartOfFourth"; // as per current impl. + ensure_equals("get_word: 4", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is); // will copy Fifth + expected_result = "Fifth"; // as per current impl. + ensure_equals("get_word: 5", actual_result, expected_result); + + is.clear(); + is.str(str = " \t \r \n "); + actual_result = ""; + ret = get_word(actual_result, is); + ensure("get_word: empty all spaces, newline tabs", ret == false); + + is.clear(); + is.str(str = ""); + actual_result = ""; + ret = get_word(actual_result, is); + ensure("get_word: empty string", ret == false); + } + + // testcase for get_word and skip_to_next_word compatibility + template<> template<> + void streamtools_object::test<8>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + is.clear(); + is.str(str = " First Second \t \r \n Third Fourth-ShouldThisBePartOfFourth Fifth\n"); + actual_result = ""; + get_word(actual_result, is); // First + actual_result = ""; + get_word(actual_result, is); // Second + actual_result = ""; + get_word(actual_result, is); // Third + + // the current implementation of get_word seems inconsistent with + // skip_to_next_word. skip_to_next_word treats any character other + // than alhpa-numeric and '_' as a delimter, while get_word() + // treats only isspace() (i.e. space, form-feed('\f'), newline ('\n'), + // carriage return ('\r'), horizontal tab ('\t'), and vertical tab ('\v') + // as delimiters + actual_result = ""; + get_word(actual_result, is); // will copy Fourth-ShouldThisBePartOfFourth + + actual_result = ""; + get_word(actual_result, is); // will copy Fifth + + is.clear(); + is.str(str = " First Second \t \r \n Third Fourth_ShouldThisBePartOfFourth Fifth\n"); + skip_to_next_word(is); // should now point to First + skip_to_next_word(is); // should now point to Second + skip_to_next_word(is); // should now point to Third + skip_to_next_word(is); // should now point to Fourth + skip_to_next_word(is); // should now point to ShouldThisBePartOfFourth + expected_result = ""; + // will copy ShouldThisBePartOfFourth, the fifth word, + // while using get_word above five times result in getting "Fifth" + get_word(expected_result, is); + ensure_equals("get_word: skip_to_next_word compatibility", actual_result, expected_result); + } + + //testcases for get_word(std::string& output_string, std::istream& input_stream, int n) + template<> template<> + void streamtools_object::test<9>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = " First Second \t \r \n Third Fourth-ShouldThisBePartOfFourth Fifth\n"); + actual_result = ""; + ret = get_word(actual_result, is, 255); + expected_result = "First"; + ensure_equals("get_word: 1", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is, 4); + expected_result = "Seco"; // should be cut short + ensure_equals("get_word: 2", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is, 255); + expected_result = "nd"; // get remainder of Second + ensure_equals("get_word: 3", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is, 0); // 0 size string + expected_result = ""; // get remainder of Second + ensure_equals("get_word: 0 sized output", actual_result, expected_result); + + actual_result = ""; + ret = get_word(actual_result, is, 255); + expected_result = "Third"; + ensure_equals("get_word: 4", actual_result, expected_result); + + is.clear(); + is.str(str = " \t \r \n "); + actual_result = ""; + ret = get_word(actual_result, is, 255); + ensure("get_word: empty all spaces, newline tabs", ret == false); + + is.clear(); + is.str(str = ""); + actual_result = ""; + ret = get_word(actual_result, is, 255); + ensure("get_word: empty string", ret == false); + } + + //test cases for get_line(std::string& output_string, std::istream& input_stream) + template<> template<> + void streamtools_object::test<10>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + is.clear(); + is.str(str = "First Second \t \r\n Third Fourth-ShouldThisBePartOfFourth IsThisFifth\n"); + actual_result = ""; + get_line(actual_result, is); + expected_result = "First Second \t \r\n"; + ensure_equals("get_line: 1", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is); + expected_result = " Third Fourth-ShouldThisBePartOfFourth IsThisFifth\n"; + ensure_equals("get_line: 2", actual_result, expected_result); + + is.clear(); + is.str(str = "\nFirst Line.\n\nSecond Line.\n"); + actual_result = ""; + get_line(actual_result, is); + expected_result = "\n"; + ensure_equals("get_line: First char as newline", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is); + expected_result = "First Line.\n"; + ensure_equals("get_line: 3", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is); + expected_result = "\n"; + ensure_equals("get_line: 4", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is); + expected_result = "Second Line.\n"; + ensure_equals("get_line: 5", actual_result, expected_result); + } + + //test cases for get_line(std::string& output_string, std::istream& input_stream) + template<> template<> + void streamtools_object::test<11>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = "One Line only with no newline"); + actual_result = ""; + ret = get_line(actual_result, is); + expected_result = "One Line only with no newline"; + ensure_equals("get_line: No newline", actual_result, expected_result); + ensure_equals("return value is good state of stream", ret, is.good()); + } + + //test cases for get_line(std::string& output_string, std::istream& input_stream) + template<> template<> + void streamtools_object::test<12>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + // need to be check if this test case is wrong or the implementation is wrong. + is.clear(); + is.str(str = "Should not skip lone \r.\r\n"); + actual_result = ""; + get_line(actual_result, is); + expected_result = "Should not skip lone \r.\r\n"; + ensure_equals("get_line: carriage return skipped even though not followed by newline", actual_result, expected_result); + } + + //test cases for get_line(std::string& output_string, std::istream& input_stream) + template<> template<> + void streamtools_object::test<13>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + is.clear(); + is.str(str = "\n"); + actual_result = ""; + get_line(actual_result, is); + expected_result = "\n"; + ensure_equals("get_line: Just newline", actual_result, expected_result); + } + + + //testcases for get_line(std::string& output_string, std::istream& input_stream, int n) + template<> template<> + void streamtools_object::test<14>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + + is.clear(); + is.str(str = "First Line.\nSecond Line.\n"); + actual_result = ""; + get_line(actual_result, is, 255); + expected_result = "First Line.\n"; + ensure_equals("get_line: Basic Operation", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is, sizeof("Second")-1); + expected_result = "Second\n"; + ensure_equals("get_line: Insufficient length 1", actual_result, expected_result); + + actual_result = ""; + get_line(actual_result, is, 255); + expected_result = " Line.\n"; + ensure_equals("get_line: Remainder after earlier insufficient length", actual_result, expected_result); + + is.clear(); + is.str(str = "One Line only with no newline with limited length"); + actual_result = ""; + get_line(actual_result, is, sizeof("One Line only with no newline with limited length")-1); + expected_result = "One Line only with no newline with limited length\n"; + ensure_equals("get_line: No newline with limited length", actual_result, expected_result); + + is.clear(); + is.str(str = "One Line only with no newline"); + actual_result = ""; + get_line(actual_result, is, 255); + expected_result = "One Line only with no newline"; + ensure_equals("get_line: No newline", actual_result, expected_result); + } + + //testcases for get_line(std::string& output_string, std::istream& input_stream, int n) + template<> template<> + void streamtools_object::test<15>() + { + std::string str; + std::string expected_result; + std::string actual_result; + std::istringstream is; + bool ret; + + is.clear(); + is.str(str = "One Line only with no newline"); + actual_result = ""; + ret = get_line(actual_result, is, 255); + expected_result = "One Line only with no newline"; + ensure_equals("get_line: No newline", actual_result, expected_result); + ensure_equals("return value is good state of stream", ret, is.good()); + } + + //testcases for remove_last_char() + template<> template<> + void streamtools_object::test<16>() + { + std::string str; + std::string expected_result; + bool ret; + + str = "SecondLife is a 3D World"; + + ret = remove_last_char('d',str); + expected_result = "SecondLife is a 3D Worl"; + ensure_equals("remove_last_char: should remove last char", str, expected_result); + + str = "SecondLife is a 3D World"; + ret = remove_last_char('W',str); + expected_result = "SecondLife is a 3D World"; + ensure_equals("remove_last_char: should not remove as it is not last char", str, expected_result); + ensure("remove_last_char: should return false", ret == false); + + str = "SecondLife is a 3D World\n"; + ret = remove_last_char('\n',str); + expected_result = "SecondLife is a 3D World"; + ensure_equals("remove_last_char: should remove last newline", str, expected_result); + ensure("remove_last_char: should remove newline and return true", ret == true); + } + + + //testcases for unescaped_string() + template<> template<> + void streamtools_object::test<17>() + { + std::string str; + std::string expected_result; + + str = "SecondLife is a 3D world \\n"; + unescape_string(str); + expected_result = "SecondLife is a 3D world \n"; + ensure_equals("unescape_string: newline", str, expected_result); + + str = "SecondLife is a 3D world \\\\t \\n"; + unescape_string(str); + expected_result = "SecondLife is a 3D world \\t \n"; + ensure_equals("unescape_string: backslash and newline", str, expected_result); + + str = "SecondLife is a 3D world \\ "; + unescape_string(str); + expected_result = "SecondLife is a 3D world \\ "; + ensure_equals("unescape_string: insufficient to unescape", str, expected_result); + + str = "SecondLife is a 3D world \\n \\n \\n \\\\\\n"; + unescape_string(str); + expected_result = "SecondLife is a 3D world \n \n \n \\\n"; + ensure_equals("unescape_string: multipel newline and backslash", str, expected_result); + + str = "SecondLife is a 3D world \\t"; + unescape_string(str); + expected_result = "SecondLife is a 3D world \\t"; + ensure_equals("unescape_string: leaves tab as is", str, expected_result); + + str = "\\n"; + unescape_string(str); + expected_result = "\n"; + ensure_equals("unescape_string: only a newline", str, expected_result); + } + + //testcases for escape_string() + template<> template<> + void streamtools_object::test<18>() + { + std::string str; + std::string expected_result; + + str = "SecondLife is a 3D world \n"; + escape_string(str); + expected_result = "SecondLife is a 3D world \\n"; + ensure_equals("escape_string: newline", str, expected_result); + + str = "SecondLife is a 3D world \\t \n"; + escape_string(str); + expected_result = "SecondLife is a 3D world \\\\t \\n"; + ensure_equals("escape_string: backslash and newline", str, expected_result); + + str = "SecondLife is a 3D world \n \n \n \\\n"; + escape_string(str); + expected_result = "SecondLife is a 3D world \\n \\n \\n \\\\\\n"; + ensure_equals("escape_string: multipel newline and backslash", str, expected_result); + + str = "SecondLife is a 3D world \t"; + escape_string(str); + expected_result = "SecondLife is a 3D world \t"; + ensure_equals("unescape_string: leaves tab as is", str, expected_result); + + str = "\n"; + escape_string(str); + expected_result = "\\n"; + ensure_equals("unescape_string: only a newline", str, expected_result); + + // serialization/deserialization escape->unescape + str = "SecondLife is a 3D world \n \n \n \\\n"; + escape_string(str); + unescape_string(str); + expected_result = "SecondLife is a 3D world \n \n \n \\\n"; + ensure_equals("escape_string: should preserve with escape/unescape", str, expected_result); + + // serialization/deserialization unescape->escape + str = "SecondLife is a 3D world \\n \\n \\n \\\\"; + unescape_string(str); + escape_string(str); + expected_result = "SecondLife is a 3D world \\n \\n \\n \\\\"; + ensure_equals("escape_string: should preserve with unescape/escape", str, expected_result); + } + + // testcases for replace_newlines_with_whitespace() + template<> template<> + void streamtools_object::test<19>() + { + std::string str; + std::string expected_result; + + str = "SecondLife is a 3D \n\nworld\n"; + replace_newlines_with_whitespace(str); + expected_result = "SecondLife is a 3D world "; + ensure_equals("replace_newlines_with_whitespace: replace all newline", str, expected_result); + + str = "\nSecondLife is a 3D world\n"; + replace_newlines_with_whitespace(str); + expected_result = " SecondLife is a 3D world "; + ensure_equals("replace_newlines_with_whitespace: begin and newline", str, expected_result); + + str = "SecondLife is a 3D world\r\t"; + replace_newlines_with_whitespace(str); + expected_result = "SecondLife is a 3D world\r\t"; + ensure_equals("replace_newlines_with_whitespace: should only replace newline", str, expected_result); + + str = ""; + replace_newlines_with_whitespace(str); + expected_result = ""; + ensure_equals("replace_newlines_with_whitespace: empty string", str, expected_result); + } + + //testcases for remove_double_quotes() + template<> template<> + void streamtools_object::test<20>() + { + std::string str; + std::string expected_result; + + str = "SecondLife is a \"\"3D world"; + remove_double_quotes(str); + expected_result = "SecondLife is a 3D world"; + ensure_equals("remove_double_quotes: replace empty doube quotes", str, expected_result); + + str = "SecondLife is a \"3D world"; + remove_double_quotes(str); + expected_result = "SecondLife is a 3D world"; + ensure_equals("remove_double_quotes: keep as is it matching quote not found", str, expected_result); + } + + // testcases for get_brace_count() + template<> template<> + void streamtools_object::test<21>() + { + } + + //testcases for get_keyword_and_value() + template<> template<> + void streamtools_object::test<22>() + { + std::string s = "SecondLife is a 3D World"; + std::string keyword; + std::string value; + get_keyword_and_value(keyword, value, s); + ensure("get_keyword_and_value: Unable to get Keyword and Value", ((keyword == "SecondLife") && (value == "is a 3D World"))); + + s = "SecondLife"; + get_keyword_and_value(keyword, value, s); + ensure("get_keyword_and_value: value should be empty", ((keyword == "SecondLife") && (value == ""))); + + s = "SecondLife \t is cool! \n"; + get_keyword_and_value(keyword, value, s); + ensure("get_keyword_and_value: remove space before value but not after", ((keyword == "SecondLife") && (value == "is cool! "))); + } + + //testcases for get_keyword_and_value() + template<> template<> + void streamtools_object::test<23>() + { + std::string s; + std::string keyword = "SOME PRIOR KEYWORD"; + std::string value = "SOME PRIOR VALUE"; + + s = "SecondLife\n"; + get_keyword_and_value(keyword, value, s); + ensure("get_keyword_and_value: terminated with newline. value should be empty", ((keyword == "SecondLife") && (value == ""))); + } + + //testcases for get_keyword_and_value() + template<> template<> + void streamtools_object::test<24>() + { + std::string s; + std::string keyword = "SOME PRIOR KEYWORD"; + std::string value = "SOME PRIOR VALUE"; + + s = ""; + get_keyword_and_value(keyword, value, s); + ensure("get_keyword_and_value: empty string. keyword value should empty", ((keyword == "") && (value == ""))); + } + + //testcase for fullread() + template<> template<> + void streamtools_object::test<25>() + { + std::string str = "First Line.\nSecond Line\n"; + std::istringstream is(str); + char buf[255] = {0}; + + fullread(is, buf, 255); + ensure_memory_matches("fullread: read with newlines", (void*) buf, static_cast<U32>(str.size())-1, (void*) str.c_str(), static_cast<U32>(str.size())-1); + + is.clear(); + is.str(str = "First Line.\nSecond Line\n"); + memset(buf, 0, 255); + + char expected_string[] = "First Line.\nSecond"; + int len = sizeof(expected_string)-1; + fullread(is, buf, len); + ensure_memory_matches("fullread: read with newlines", (void*) buf, len, (void*) &expected_string, len); + } + + +// testcases for operator >> + + template<> template<> + void streamtools_object::test<26>() + { + char arr[255]; + std::string str; + std::string toCheck = "SecondLife" ; + std::string expected_result; + std::istringstream stream("SecondLife is a 3D World"); + stream >> toCheck.c_str(); + stream.get(arr, 255, '\0'); + expected_result = " is a 3D World"; + ensure_equals("istream << operator", arr, expected_result); + + stream.clear(); + stream.str(str = "SecondLife is a 3D World"); + toCheck = "is"; + stream >> toCheck.c_str(); + ensure("istream << operator should have failed", stream.good() == false); + } +} diff --git a/indra/llcommon/tests/llstring_test.cpp b/indra/llcommon/tests/llstring_test.cpp index 6a2c967363..3c9d8071ac 100644 --- a/indra/llcommon/tests/llstring_test.cpp +++ b/indra/llcommon/tests/llstring_test.cpp @@ -396,7 +396,7 @@ namespace tut { F64 value; std::string str_val("9223372036854775807"); //0x7FFFFFFFFFFFFFFF - ensure("1: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 9223372036854775807.); + ensure("1: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 9223372036854775807.0); str_val = "0"; ensure("2: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 0.0F); diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp index 451e60c083..6adbdffba8 100644 --- a/indra/llcommon/threadpool.cpp +++ b/indra/llcommon/threadpool.cpp @@ -78,6 +78,7 @@ void LL::ThreadPoolBase::start() std::string tname{ stringize(mName, ':', (i+1), '/', mThreadCount) }; mThreads.emplace_back(tname, [this, tname]() { + set_thread_name(tname.c_str()); LL_PROFILER_SET_THREAD_NAME(tname.c_str()); run(tname); }); diff --git a/indra/llcommon/timer.h b/indra/llcommon/timer.h deleted file mode 100644 index aaa0cf0775..0000000000 --- a/indra/llcommon/timer.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @file timer.h - * @brief Legacy wrapper header. - * - * $LicenseInfo:firstyear=2000&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ -#include "lltimer.h" |
