From 24b2343e83099257dc43bf4f296d58f307adba39 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 23 Oct 2025 04:48:17 -0400 Subject: Fix makefile and ninja generators invalidating secondlife-bin target every time cmake is run Signed-off-by: Rye --- indra/newview/llappviewerwin32.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index a951338138..e55836fd02 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -72,6 +72,8 @@ #include #include +#include "llversioninfovars.h" + // Bugsplat (http://bugsplat.com) crash reporting tool #ifdef LL_BUGSPLAT #include "BugSplat.h" -- cgit v1.3 From 638c7f7bea1a1d345980fa385a36201f5b6cc64e Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:50:55 +0300 Subject: #5629 Velopack allows uninstall while the viewer is running (#5662) Velopack's uninstall can't be canceled, so instead it now checks for presense of a running window, makes sure window matches velopack's path, then sends a shutdown message. Window gets the message, verifies path, initiates shutdown. --- indra/llwindow/llwindowwin32.cpp | 144 ++++++++++++++++++++++++ indra/llwindow/llwindowwin32.h | 6 + indra/newview/llappviewerwin32.cpp | 222 ++++++++++++++++++++++++++++++++++++- indra/newview/llappviewerwin32.h | 3 + indra/newview/llvelopack.cpp | 7 +- 5 files changed, 379 insertions(+), 3 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index c185fc6c4a..3cb2911f9a 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -2605,6 +2605,150 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ // if session is ending OS is going to take care of it. return 0; } + case WM_POST_UNINSTALL_: + { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POST_UNINSTALL_"); + // Other instance, likely velopack, requested we quit. + // Don't trust PID alone (can be spoofed), verify the + // path for security purposes before processing. + // Verifying path isn't a strong varranty, if this turns + // up to be a risk, we will want something more secure. + // See sendShutdownToOtherInstances for the sender. + + // LPARAM contains message type. + DWORD message_type = static_cast(l_param); + if (message_type == WM_POST_UNINSTALL_MSG_SHUTDOWN || message_type == WM_POST_UNINSTALL_MSG_UPDATE) + { + DWORD sender_process_id = static_cast(w_param); + + // Make sure something didn't just send us our own process + DWORD our_process_id = GetCurrentProcessId(); + if (our_process_id == sender_process_id) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ from our own process, ignoring" << LL_ENDL; + break; + } + + if (sender_process_id == 0) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't get sender process ID" << LL_ENDL; + break; + } + + // Open the existing sender process to verify its executable path + HANDLE hSenderProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, sender_process_id); + if (!hSenderProcess) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't open sender process" << LL_ENDL; + break; + } + + // Get the actual executable path of the sender + wchar_t sender_exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + bool got_sender_path = QueryFullProcessImageNameW(hSenderProcess, 0, sender_exe_path, &size) != 0; + CloseHandle(hSenderProcess); + + if (!got_sender_path) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't query sender executable path" << LL_ENDL; + break; + } + + // Extract directory from sender's executable path + wchar_t sender_dir[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(sender_exe_path, MAX_PATH, sender_dir, &file_part); + + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize sender executable path" << LL_ENDL; + break; + } + + // Remove the filename to get just directory + if (file_part) + { + *file_part = L'\0'; + } + + // Remove trailing backslash + size_t sender_dir_len = wcslen(sender_dir); + if (sender_dir_len > 0 && sender_dir[sender_dir_len - 1] == L'\\') + { + sender_dir[sender_dir_len - 1] = L'\0'; + sender_dir_len--; + } + + // Remove "\current" suffix from sender's path if present + const std::wstring current_suffix = L"\\current"; + std::wstring sender_normalized_str(sender_dir); + if (sender_normalized_str.length() >= current_suffix.length() && + _wcsicmp(sender_normalized_str.c_str() + sender_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + sender_normalized_str.resize(sender_normalized_str.length() - current_suffix.length()); + } + + // Get our executable directory for comparison + std::wstring our_wide = ll_convert(gDirUtilp->getExecutableDir()); + + // Normalize our path + wchar_t our_normalized[MAX_PATH]; + file_part = nullptr; + + DWORD result2 = GetFullPathNameW(our_wide.c_str(), MAX_PATH, our_normalized, &file_part); + + if (result2 == 0 || result2 >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize our executable path" << LL_ENDL; + break; + } + + // Remove trailing backslash + size_t our_len = wcslen(our_normalized); + if (our_len > 0 && our_normalized[our_len - 1] == L'\\') + { + our_normalized[our_len - 1] = L'\0'; + our_len--; + } + + // Remove "\current" suffix from our path if present + std::wstring our_normalized_str(our_normalized); + if (our_normalized_str.length() >= current_suffix.length() && + _wcsicmp(our_normalized_str.c_str() + our_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + our_normalized_str.resize(our_normalized_str.length() - current_suffix.length()); + } + + // Compare the normalized base installation paths (case-insensitive) + if (_wcsicmp(sender_normalized_str.c_str(), our_normalized_str.c_str()) == 0) + { + window_imp->post([=]() + { + LL_INFOS("Window") << "Received valid shutdown request from verified same installation directory" << LL_ENDL; + // Check if app needs cleanup or can be closed immediately. + if (window_imp->mCallbacks->handleCloseRequest(window_imp, false)) + { + // Get the app to initiate cleanup. + window_imp->mCallbacks->handleQuit(window_imp); + } + }); + } + else + { + LL_WARNS("Window") << "Rejected shutdown request - sender not from our installation directory. " + << "Sender: " << ll_convert_wide_to_string(sender_normalized_str) + << " Our: " << ll_convert_wide_to_string(our_normalized_str) << LL_ENDL; + } + } + else + { + LL_WARNS("Window") << "Received invalid WM_POST_UNINSTALL_ message" << LL_ENDL; + } + break; + } case WM_COMMAND: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_COMMAND"); diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index aab2635a34..afff3d5cb6 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -40,6 +40,12 @@ // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) +// For requesting shutdown on uninstall, +// make sure it does not conflict with messages like WM_DUMMY_ +inline constexpr UINT WM_POST_UNINSTALL_ = WM_USER + 0x0019; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_SHUTDOWN = 1; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_UPDATE = 2; + typedef void (*LLW32MsgCallback)(const MSG &msg); class LLWindowWin32 : public LLWindow diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 94a5f7951e..0dee17010c 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -969,7 +969,7 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) if (other_window != NULL) { - LL_DEBUGS() << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; + LL_DEBUGS("AppInit") << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; COPYDATASTRUCT cds; const S32 SLURL_MESSAGE_TYPE = 0; cds.dwData = SLURL_MESSAGE_TYPE; @@ -977,13 +977,231 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) cds.lpData = (void*)url.c_str(); LRESULT msg_result = SendMessage(other_window, WM_COPYDATA, NULL, (LPARAM)&cds); - LL_DEBUGS() << "SendMessage(WM_COPYDATA) to other window '" + LL_DEBUGS("AppInit") << "SendMessage(WM_COPYDATA) to other window '" << getWindowTitle() << "' returned " << msg_result << LL_ENDL; return true; } return false; } +bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir) +{ + // Velopack installs viewer like this: + // %appdata%\Local\ChannelNameViewer\Update.exe // which is our uninstaller + // %appdata%\Local\ChannelNameViewer\SecondLifeViewer.exe // wrapper, redirects to main executable + // %appdata%\Local\ChannelNameViewer\current\SecondLifeViewer.exe // main executable + // For reliability don't expect install_dir to be actually in the base path, strip 'current' + + const std::wstring current_suffix = L"\\current"; + std::wstring normalized_path(install_dir); + if (normalized_path.length() >= current_suffix.length() && + _wcsicmp(normalized_path.c_str() + normalized_path.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + normalized_path.resize(normalized_path.length() - current_suffix.length()); + } + + wchar_t window_class[256]; // Assume max length < 255 chars. + mbstowcs(window_class, sWindowClass, 255); + window_class[255] = 0; + + // Normalize the directory path + wchar_t our_dir_normalized[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(normalized_path.c_str(), MAX_PATH, our_dir_normalized, &file_part); + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS() << "Failed to normalize our executable path" << LL_ENDL; + return false; + } + + // Remove trailing backslash if present + size_t dir_len = wcslen(our_dir_normalized); + if (dir_len > 0 && our_dir_normalized[dir_len - 1] == L'\\') + { + our_dir_normalized[dir_len - 1] = L'\0'; + dir_len--; + } + + // This message is meant for velopack, so we don't expect to have + // a window of our own, store any matching windows. + struct EnumData + { + const wchar_t* target_class; + const wchar_t* our_dir_normalized; + std::vector found_windows; + }; + + EnumData enum_data; + enum_data.target_class = window_class; + enum_data.our_dir_normalized = our_dir_normalized; + + // Callback function to find all matching windows + auto find_windows_callback = [](HWND hwnd, LPARAM lParam) -> BOOL + { + EnumData* data = reinterpret_cast(lParam); + wchar_t class_name[256]; + + if (GetClassName(hwnd, class_name, 256) > 0) + { + if (wcscmp(class_name, data->target_class) == 0) + { + // Get the process ID for this window + DWORD process_id = 0; + GetWindowThreadProcessId(hwnd, &process_id); + + // Open the process to query its executable path + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id); + if (hProcess) + { + wchar_t exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + if (QueryFullProcessImageNameW(hProcess, 0, exe_path, &size)) + { + // Normalize the other process's path + wchar_t other_dir_normalized[MAX_PATH]; + wchar_t* other_file_part = nullptr; + DWORD result = GetFullPathNameW(exe_path, MAX_PATH, other_dir_normalized, &other_file_part); + + if (result > 0 && result < MAX_PATH) + { + // Remove the filename part to get just the directory + // We are doing this to avoid incidents, like having + // multiple viewer version exes in the same folder. + if (other_file_part) + { + *other_file_part = L'\0'; + } + + // Remove trailing backslash if present + size_t other_dir_len = wcslen(other_dir_normalized); + if (other_dir_len > 0 && other_dir_normalized[other_dir_len - 1] == L'\\') + { + other_dir_normalized[other_dir_len - 1] = L'\0'; + other_dir_len--; + } + + // Strip "\current" suffix if present to normalize comparison + // This handles both release (with \current) and debug builds (without) + const std::wstring current_suffix = L"\\current"; + if (other_dir_len >= current_suffix.length()) + { + size_t offset = other_dir_len - current_suffix.length(); + if (_wcsicmp(other_dir_normalized + offset, current_suffix.c_str()) == 0) + { + other_dir_normalized[offset] = L'\0'; + } + } + + // Compare directories (case-insensitive) + if (_wcsicmp(other_dir_normalized, data->our_dir_normalized) == 0) + { + data->found_windows.push_back(hwnd); + } + } + } + CloseHandle(hProcess); + } + } + } + + return TRUE; // Continue enumeration + }; + + // Find all matching windows and send shutdown messages + EnumWindows(find_windows_callback, reinterpret_cast(&enum_data)); + + if (enum_data.found_windows.empty()) + { + LL_DEBUGS("AppInit") << "No other instances found" << LL_ENDL; + return false; + } + + LL_INFOS("AppInit") << "Found " << (S32)(enum_data.found_windows.size()) << " other instance(s), sending shutdown messages" << LL_ENDL; + + // Get our own process ID to include in the message + DWORD our_process_id = GetCurrentProcessId(); + + constexpr UINT timeout_ms = 2000; // 2s. Viewer's message thread is supposed to be fast. + for (HWND other_window : enum_data.found_windows) + { + if (IsWindow(other_window)) + { + DWORD_PTR result = 0; + LRESULT send_result = SendMessageTimeout( + other_window, + WM_POST_UNINSTALL_, + static_cast(our_process_id), + static_cast(WM_POST_UNINSTALL_MSG_SHUTDOWN), + SMTO_ABORTIFHUNG | SMTO_BLOCK, + timeout_ms, + &result + ); + + if (send_result == 0) + { + DWORD error = GetLastError(); + if (error == ERROR_TIMEOUT) + { + LL_WARNS("AppInit") << "Shutdown message timed out for window " << std::hex << other_window << std::dec << LL_ENDL; + } + else + { + LL_WARNS("AppInit") << "Failed to send shutdown message to window " << std::hex << other_window + << ", error: " << error << std::dec << LL_ENDL; + } + + PostMessage(other_window, WM_CLOSE, 0, 0); + } + else + { + LL_DEBUGS("AppInit") << "Shutdown message sent successfully to window " << std::hex << other_window << std::dec << LL_ENDL; + } + } + } + + // Poll for up to 30 seconds, checking every 5 seconds + const S32 MAX_WAIT_TIME_MS = 60000; // 30 seconds + const S32 POLL_INTERVAL_MS = 5000; // 5 seconds + S32 elapsed_time_ms = 0; + size_t still_open_count = enum_data.found_windows.size(); + + while (elapsed_time_ms < MAX_WAIT_TIME_MS) + { + LL_INFOS("AppInit") << "Waiting for " << (S32)still_open_count << " instance(s) to close... (" + << (S32)(elapsed_time_ms / 1000) << "s elapsed)" << LL_ENDL; + + ms_sleep(POLL_INTERVAL_MS); + elapsed_time_ms += POLL_INTERVAL_MS; + + // Check if the specific windows we found still exist + // Don't enumerate all windows for new ones, assume that + // no instances were reused and assume user won't open + // the app again. For now just check our list. + still_open_count = 0; + for (HWND hwnd : enum_data.found_windows) + { + if (IsWindow(hwnd)) + { + still_open_count++; + } + } + + if (still_open_count == 0) + { + LL_INFOS("AppInit") << "All other instances have closed after " << (S32)(elapsed_time_ms / 1000) << " seconds" << LL_ENDL; + return false; + } + } + + if (still_open_count != 0) + { + LL_WARNS("AppInit") << "Proceeding with uninstall with " << (S32)still_open_count << " instance(s) still open." << LL_ENDL; + } + + return true; +} + std::string LLAppViewerWin32::generateSerialNumber() { diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 0741758a0c..d0a3f2f5ce 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -45,6 +45,9 @@ public: bool reportCrashToBugsplat(void* pExcepInfo) override; + // returns true if other windows were found and are still running. + static bool sendShutdownToOtherInstances(const std::wstring& install_dir); + protected: bool initWindow() override; // Override to initialize the viewer's window. void initLoggingAndGetLastDuration() override; // Override to clean stack_trace info. diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index 8975f7d369..d34d43cc48 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -43,6 +43,7 @@ #include "Velopack.h" #if LL_WINDOWS +#include "llappviewerwin32.h" #include #include #include @@ -666,8 +667,12 @@ static void on_before_uninstall(void* user_data, const char* app_version) unregister_protocol_handler(PROTOCOL_SECONDLIFE); unregister_protocol_handler(PROTOCOL_GRID_INFO); - unregister_uninstall_info(); remove_shortcuts(app_name); + + std::wstring install_dir = get_install_dir(); + LLAppViewerWin32::sendShutdownToOtherInstances(install_dir); + + unregister_uninstall_info(); } static void on_log_message(void* user_data, const char* level, const char* message) -- cgit v1.3 From 998c2bc8d924fe9179f4ee914ef2b65c6ae78302 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:20:55 +0200 Subject: #5084 Improve watchdog's behavior --- indra/llcommon/llapp.h | 1 + indra/llcommon/llwatchdog.cpp | 68 +++++++++--- indra/llcommon/llwatchdog.h | 19 +++- indra/newview/llappviewer.cpp | 142 ++++++++++++++++++++----- indra/newview/llappviewer.h | 3 + indra/newview/llappviewerwin32.cpp | 50 ++++++++- indra/newview/llappviewerwin32.h | 1 + indra/newview/skins/default/xui/en/strings.xml | 3 + 8 files changed, 246 insertions(+), 41 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/llcommon/llapp.h b/indra/llcommon/llapp.h index ce09c566a9..fef7dc80b3 100644 --- a/indra/llcommon/llapp.h +++ b/indra/llcommon/llapp.h @@ -285,6 +285,7 @@ public: #ifdef LL_WINDOWS virtual bool reportCrashToBugsplat(void* pExcepInfo /*EXCEPTION_POINTERS*/) { return false; } + virtual bool reportCustomToBugsplat(const std::string& desription) { return false; } #endif public: diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index d3242a6c96..1622aeb180 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -173,6 +173,17 @@ void LLWatchdog::add(LLWatchdogEntry* e) { lockThread(); mSuspects.insert(e); + + if (!mFrozeList.empty()) + { + mFrozeList.erase(e); + if (mFrozeList.empty()) + { + // Clear error marker file if there is no frozen threads, + // viewer is responsive again. + mClearMarkerFnc(); + } + } unlockThread(); } @@ -183,7 +194,12 @@ void LLWatchdog::remove(LLWatchdogEntry* e) unlockThread(); } -void LLWatchdog::init(func_t set_error_state_callback) +void LLWatchdog::init( + create_marker_func_t error_state_callback, + clear_marker_func_t clear_marker_callback, + report_func_t report_callback, + notify_func_t notify_callback, + bool crash_on_freeze) { if (!mSuspectsAccessMutex && !mTimer) { @@ -196,7 +212,11 @@ void LLWatchdog::init(func_t set_error_state_callback) // start needs to use the mSuspectsAccessMutex mTimer->start(); } - mCreateMarkerFnc = set_error_state_callback; + mCreateMarkerFnc = error_state_callback; + mClearMarkerFnc = clear_marker_callback; + mCrashReportFnc = report_callback; + mNotifyFnc = notify_callback; + mCrashOnFreeze = crash_on_freeze; } void LLWatchdog::cleanup() @@ -251,21 +271,45 @@ void LLWatchdog::run() mTimer->stop(); } - // Sets error marker file - mCreateMarkerFnc(); - // Todo1: Warn user? - // Todo2: We probably want to report even if 5 seconds passed, just not error 'yet'. std::string last_state = (*result)->getLastState(); - if (last_state.empty()) + std::string description = "Watchdog timer for thread " + (*result)->getThreadName() + " expired"; + if (!last_state.empty()) { - LL_ERRS() << "Watchdog timer for thread " << (*result)->getThreadName() - << " expired; assuming viewer is hung and crashing" << LL_ENDL; + description += " with state: " + last_state; + } + description += "; assuming viewer is hung and crashing"; + + if (!mCrashOnFreeze) + { + // Sets watchdog marker file + mCreateMarkerFnc(false); + // If it's mainloop and it somehow recovers, it will re-add itself + mSuspects.erase(*result); + mFrozeList.insert(*result); + LL_WARNS() << description << LL_ENDL; } else { - LL_ERRS() << "Watchdog timer for thread " << (*result)->getThreadName() - << " expired with state: " << last_state - << "; assuming viewer is hung and crashing" << LL_ENDL; + + if (!mCrashReportFnc(description)) + { + // Sets error marker file + mCreateMarkerFnc(true); + // If false is returned, then we failed to report the issue to bugsplat, + // instead, Notify user, then crash viewer. + // Todo: ask user if viewer should quit or wait? + mNotifyFnc(); + LL_ERRS() << description << LL_ENDL; + } + else + { + // Sets watchdog marker file + mCreateMarkerFnc(false); + // Already reported, don't report again. + // If it's mainloop and it somehow recovers, it will re-add itself + mSuspects.erase(result); + mFrozeList.insert(*result); + } } } } diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index 2100a90879..f138fbccb0 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -93,8 +93,16 @@ public: void add(LLWatchdogEntry* e); void remove(LLWatchdogEntry* e); - typedef std::function func_t; - void init(func_t set_error_state_callback); + typedef std::function create_marker_func_t; + typedef std::function clear_marker_func_t; + typedef std::function report_func_t; + typedef std::function notify_func_t; + void init( + create_marker_func_t error_state_callback, + clear_marker_func_t clear_marker_callback, + report_func_t report_callback, + notify_func_t notify_callback, + bool crash_on_freeze); void run(); void cleanup(); @@ -105,14 +113,19 @@ private: typedef std::set SuspectsRegistry; SuspectsRegistry mSuspects; + SuspectsRegistry mFrozeList; LLMutex* mSuspectsAccessMutex; LLWatchdogTimerThread* mTimer; U64 mLastClockCount; + bool mCrashOnFreeze; // At the moment watchdog expects app to set markers in mCreateMarkerFnc, // but technically can be used to set any error states or do some cleanup // or show warnings. - func_t mCreateMarkerFnc; + create_marker_func_t mCreateMarkerFnc; + clear_marker_func_t mClearMarkerFnc; + report_func_t mCrashReportFnc; + notify_func_t mNotifyFnc; }; #endif // LL_LLTHREADWATCHDOG_H diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 0b6ea72df4..4aaf8411cc 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -383,6 +383,7 @@ const std::string MARKER_FILE_NAME("SecondLife.exec_marker"); const std::string START_MARKER_FILE_NAME("SecondLife.start_marker"); const std::string ERROR_MARKER_FILE_NAME("SecondLife.error_marker"); const std::string LOGOUT_MARKER_FILE_NAME("SecondLife.logout_marker"); +const std::string WATCHDOG_MARKER_FILE_NAME("SecondLife.watchdog_marker"); static std::string gLaunchFileOnQuit; //---------------------------------------------------------------------------- @@ -3235,20 +3236,60 @@ bool LLAppViewer::initWindow() << " (setting = " << watchdog_enabled_setting << ")" << LL_ENDL; - if (use_watchdog) + // Watchdog reports to statistics via marker files, that is + // pointless without ability to write (!mSecondInstance) those files. + // If use_watchdog is set, watchdog also reports to bugspat. + if (use_watchdog || !mSecondInstance) { - LLWatchdog::getInstance()->init([]() - { - LLAppViewer* app = LLAppViewer::instance(); - if (app->logoutRequestSent()) + LLWatchdog::getInstance()->init( + [](bool final_marker) { - app->createErrorMarker(LAST_EXEC_LOGOUT_FROZE); - } - else + LLAppViewer* app = LLAppViewer::instance(); + // Without watchdog everything will be counted as + // either 'unknown' (no crash marker) or based of present crash marker + if (final_marker) + { + // watchdog is going to crash viewer, so crate a 'crash' marker + if (app->logoutRequestSent()) + { + app->createErrorMarker(LAST_EXEC_LOGOUT_FROZE); + } + else + { + app->createErrorMarker(LAST_EXEC_FROZE); + } + } + else + { + // not going to crash, just create a 'watchdog' marker + app->createWatchdogMarker(); + } + }, + []() { - app->createErrorMarker(LAST_EXEC_FROZE); - } - }); + LLAppViewer* app = LLAppViewer::instance(); + // in case process recovered from freeze, remove watchdog marker. + app->removeWatchdogMarker(); + }, + [](std::string &desc) + { +#if LL_WINDOWS && LL_BUGSPLAT + LLAppViewer* app = LLAppViewer::instance(); + app->writeDebugInfo(); + return app->reportCustomToBugsplat(desc); +#else + return false; +#endif + }, + []() + { + LLAppViewer* app = LLAppViewer::instance(); + app->sendLogoutRequest(); + // Might be better to ask user if user wants to terminate the app or wait. + OSMessageBox(LLTrans::getString("MBFreezeDetected"), LLTrans::getString("MBFatalError"), OSMB_OK); + }, + use_watchdog); + } LLNotificationsUI::LLNotificationManager::getInstance(); @@ -4021,13 +4062,8 @@ void LLAppViewer::processMarkerFiles() { // the file existed, is ours, and matched our version, so we can report on what it says LL_INFOS("MarkerFile") << "Exec marker '"<< mMarkerFileName << "' found; last exec crashed or froze" << LL_ENDL; -#if LL_WINDOWS && LL_BUGSPLAT - // bugsplat will set correct state in bugsplatSendLog - // Might be more accurate to rename this one into 'unknown' + // App terminated unexpectedly or froze, we don't know the cause yet. gLastExecEvent = LAST_EXEC_UNKNOWN; -#else - gLastExecEvent = LAST_EXEC_OTHER_CRASH; -#endif // LL_WINDOWS } else @@ -4080,23 +4116,29 @@ void LLAppViewer::processMarkerFiles() } LLAPRFile::remove(logout_marker_file); } - // and last refine based on whether or not a marker created during a non-llerr crash is found + // Refine based on whether or not a marker created during + // a crash is found or if wathdog caught a freeze. + // Bugsplat will set correct state in bugsplatSendLog. std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ERROR_MARKER_FILE_NAME); + std::string watchdog_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); if(LLAPRFile::isExist(error_marker_file, NULL, LL_APR_RB)) { S32 marker_code = getMarkerErrorCode(error_marker_file); if (marker_code >= 0) { - if (gLastExecEvent == LAST_EXEC_LOGOUT_FROZE) - { - gLastExecEvent = LAST_EXEC_LOGOUT_CRASH; - LL_INFOS("MarkerFile") << "Error marker '"<< error_marker_file << "' crashed, setting LastExecEvent to LOGOUT_CRASH" << LL_ENDL; - } - else if (marker_code > 0 && marker_code < (S32)LAST_EXEC_COUNT) + if (marker_code > 0 && marker_code < (S32)LAST_EXEC_COUNT) { + // If we have a code, it takes precendence gLastExecEvent = (eLastExecEvent)marker_code; LL_INFOS("MarkerFile") << "Error marker '"<< error_marker_file << "' crashed, setting LastExecEvent to " << gLastExecEvent << LL_ENDL; } + // if we have the marker, even without a code, it's a crash. + else if (gLastExecEvent == LAST_EXEC_LOGOUT_UNKNOWN + || gLastExecEvent == LAST_EXEC_LOGOUT_FROZE) + { + gLastExecEvent = LAST_EXEC_LOGOUT_CRASH; + LL_INFOS("MarkerFile") << "Error marker '" << error_marker_file << "' crashed, setting LastExecEvent to LOGOUT_CRASH" << LL_ENDL; + } else { gLastExecEvent = LAST_EXEC_OTHER_CRASH; @@ -4108,6 +4150,33 @@ void LLAppViewer::processMarkerFiles() LL_INFOS("MarkerFile") << "Error marker '"<< error_marker_file << "' marker found, but versions did not match" << LL_ENDL; } LLAPRFile::remove(error_marker_file); + if (LLAPRFile::isExist(watchdog_marker_file, NULL, LL_APR_RB)) + { + // If viewer crashed after a freeze was detected, + // crash still takes precendence. Just clear watchdog. + removeWatchdogMarker(); + } + } + else + { + // so only check watchdog marker if there is no error marker. + if (LLAPRFile::isExist(watchdog_marker_file, NULL, LL_APR_RB)) + { + if (LAST_EXEC_UNKNOWN == gLastExecEvent + || LAST_EXEC_LOGOUT_UNKNOWN == gLastExecEvent) + { + // watchdog marker gets created if we detect a freeze, + // so if viwer did not stop gracefully, and we know it wasn't a crash, + // we have no other info, check watchdog. + if (markerIsSameVersion(watchdog_marker_file)) + { + gLastExecEvent = LAST_EXEC_UNKNOWN == gLastExecEvent ? LAST_EXEC_FROZE : LAST_EXEC_LOGOUT_FROZE; + LL_INFOS("MarkerFile") << "Watchdog marker '" << watchdog_marker_file << "' found, setting LastExecEvent to FROZE" + << LL_ENDL; + } + } + removeWatchdogMarker(); + } } #if LL_DARWIN @@ -4152,6 +4221,7 @@ void LLAppViewer::removeMarkerFiles() { LL_WARNS("MarkerFile") << "logout marker '"<getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); + + LLAPRFile file; + file.open(error_marker, LL_APR_WB); + if (file.getFileHandle()) + { + recordMarkerVersion(file); + file.close(); + } + } +} +void LLAppViewer::removeWatchdogMarker() const +{ + if (!mSecondInstance) + { + std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); + LLFile::remove(error_marker_file); + } +} + void LLAppViewer::outOfMemorySoftQuit() { if (!mQuitRequested) diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index c977757e48..d76e5015e9 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -254,6 +254,9 @@ public: void createErrorMarker(eLastExecEvent error_code) const; bool errorMarkerExists() const; + void createWatchdogMarker() const; + void removeWatchdogMarker() const; + // Attempt a 'soft' quit with disconnect and saving of settings/cache. // Intended to be thread safe. // Good chance of viewer crashing either way, but better than alternatives. diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 0dee17010c..a78a7fb95f 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -119,6 +119,7 @@ namespace // MiniDmpSender pointer. As things stand, though, we must define an // actual function and store the pointer statically. static MiniDmpSender *sBugSplatSender = nullptr; + static std::string sBugsplatDesriptionField; bool bugsplatSendLog(UINT nCode, LPVOID lpVal1, LPVOID lpVal2) { @@ -155,8 +156,21 @@ namespace WCSTR(gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, "settings_per_account.xml"))); } - // LL_ERRS message, when there is one - sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); + if (!sBugsplatDesriptionField.empty()) + { + // Can be set by watchdog or other code that detects a problem + // and wants to add some context to the crash report. + // Will be visible in the BugSplat web UI. + sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); + // This type of crash is not nessesarily a crash, or final. + // Prepare for the next one. + sBugsplatDesriptionField.clear(); + } + else + { + // LL_ERRS message, when there is one + sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); + } sBugSplatSender->setAttribute(WCSTR(L"OS"), WCSTR(LLOSInfo::instance().getOSStringSimple())); // In case we ever stop using email for this sBugSplatSender->setAttribute(WCSTR(L"AppState"), WCSTR(LLStartUp::getStartupStateString())); @@ -862,6 +876,38 @@ bool LLAppViewerWin32::reportCrashToBugsplat(void* pExcepInfo) return false; } +#if defined(LL_BUGSPLAT) +static int reportCustomToBugsplatFilter(EXCEPTION_POINTERS* pExcepInfo) +{ + if (sBugSplatSender) + { + sBugSplatSender->createReport(pExcepInfo); + } + return EXCEPTION_EXECUTE_HANDLER; +} +#endif + +bool LLAppViewerWin32::reportCustomToBugsplat(const std::string &description) +{ +#if defined(LL_BUGSPLAT) + if (sBugSplatSender) + { + sBugsplatDesriptionField = description; + + __try + { + // Generate a custom exception code + RaiseException(0xE0000001, 0, 0, NULL); + } + __except (reportCustomToBugsplatFilter(GetExceptionInformation())) + { + } + return true; + } +#endif // LL_BUGSPLAT + return false; +} + bool LLAppViewerWin32::initWindow() { // This is a workaround/hotfix for a change in Windows 11 24H2 (and possibly later) diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index d0a3f2f5ce..ece4fef6fd 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -44,6 +44,7 @@ public: bool cleanup() override; bool reportCrashToBugsplat(void* pExcepInfo) override; + bool reportCustomToBugsplat(const std::string& desription) override; // returns true if other windows were found and are still running. static bool sendShutdownToOtherInstances(const std::wstring& install_dir); diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 6a123d2a2a..a4ff883c2b 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -3008,6 +3008,9 @@ If this message persists, restart your computer. [APP_NAME] appears to have frozen or crashed on the previous run. Would you like to send a crash report? + + [APP_NAME] appears to have frozen. If this issue occurs regularly, please contact support at https://support.secondlife.com. + Notification [APP_NAME] is unable to detect DirectX 9.0b or greater. -- cgit v1.3 From 6fb576a95cb813a39b48aa51dbf7935db2c7ce11 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 30 Apr 2026 01:39:30 +0300 Subject: #5084 Improve watchdog's behavior #2 --- indra/llcommon/llapp.h | 2 +- indra/llcommon/llwatchdog.cpp | 9 ++++++--- indra/newview/llappviewerwin32.cpp | 12 ++++++------ indra/newview/llappviewerwin32.h | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/llcommon/llapp.h b/indra/llcommon/llapp.h index fef7dc80b3..3a855bc480 100644 --- a/indra/llcommon/llapp.h +++ b/indra/llcommon/llapp.h @@ -285,7 +285,7 @@ public: #ifdef LL_WINDOWS virtual bool reportCrashToBugsplat(void* pExcepInfo /*EXCEPTION_POINTERS*/) { return false; } - virtual bool reportCustomToBugsplat(const std::string& desription) { return false; } + virtual bool reportCustomToBugsplat(const std::string& description) { return false; } #endif public: diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 1622aeb180..66b565c763 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -191,6 +191,7 @@ void LLWatchdog::remove(LLWatchdogEntry* e) { lockThread(); mSuspects.erase(e); + mFrozeList.erase(e); unlockThread(); } @@ -284,8 +285,9 @@ void LLWatchdog::run() // Sets watchdog marker file mCreateMarkerFnc(false); // If it's mainloop and it somehow recovers, it will re-add itself - mSuspects.erase(*result); - mFrozeList.insert(*result); + LLWatchdogEntry* froze_entry = *result; + mSuspects.erase(result); + mFrozeList.insert(froze_entry); LL_WARNS() << description << LL_ENDL; } else @@ -307,8 +309,9 @@ void LLWatchdog::run() mCreateMarkerFnc(false); // Already reported, don't report again. // If it's mainloop and it somehow recovers, it will re-add itself + LLWatchdogEntry* froze_entry = *result; mSuspects.erase(result); - mFrozeList.insert(*result); + mFrozeList.insert(froze_entry); } } } diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index a78a7fb95f..3dc98b83c4 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -119,7 +119,7 @@ namespace // MiniDmpSender pointer. As things stand, though, we must define an // actual function and store the pointer statically. static MiniDmpSender *sBugSplatSender = nullptr; - static std::string sBugsplatDesriptionField; + static std::string sBugsplatDescriptionField; bool bugsplatSendLog(UINT nCode, LPVOID lpVal1, LPVOID lpVal2) { @@ -156,15 +156,15 @@ namespace WCSTR(gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, "settings_per_account.xml"))); } - if (!sBugsplatDesriptionField.empty()) + if (!sBugsplatDescriptionField.empty()) { // Can be set by watchdog or other code that detects a problem // and wants to add some context to the crash report. // Will be visible in the BugSplat web UI. - sBugSplatSender->setDefaultUserDescription(WCSTR(LLError::getFatalMessage())); - // This type of crash is not nessesarily a crash, or final. + sBugSplatSender->setDefaultUserDescription(WCSTR(sBugsplatDescriptionField)); + // This type of crash is not necessarily a crash, or final. // Prepare for the next one. - sBugsplatDesriptionField.clear(); + sBugsplatDescriptionField.clear(); } else { @@ -892,7 +892,7 @@ bool LLAppViewerWin32::reportCustomToBugsplat(const std::string &description) #if defined(LL_BUGSPLAT) if (sBugSplatSender) { - sBugsplatDesriptionField = description; + sBugsplatDescriptionField = description; __try { diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index ece4fef6fd..ad61ae68d6 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -44,7 +44,7 @@ public: bool cleanup() override; bool reportCrashToBugsplat(void* pExcepInfo) override; - bool reportCustomToBugsplat(const std::string& desription) override; + bool reportCustomToBugsplat(const std::string& description) override; // returns true if other windows were found and are still running. static bool sendShutdownToOtherInstances(const std::wstring& install_dir); -- cgit v1.3 From 0422a389ad0289880dc5cead1c8d0788fa881e73 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 13 May 2026 23:29:23 +0300 Subject: #5810 Improve early exit's cleanup --- indra/newview/llappviewer.cpp | 39 ++++++++++++++++++++++++++++++++++---- indra/newview/llappviewer.h | 3 ++- indra/newview/llappviewerwin32.cpp | 18 +++++++++++------- indra/newview/llappviewerwin32.h | 4 +++- 4 files changed, 51 insertions(+), 13 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 2e42855079..cb140bc523 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2012, Linden Research, Inc. + * Copyright (C) 2026, 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 @@ -940,6 +940,16 @@ bool LLAppViewer::init() LL_WARNS("InitInfo") << "initHardwareTest() failed." << LL_ENDL; // quit immediately LL_PROFILER_FRAME_END; + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Stats from previous session will likely be lost, but this should + // be fine as this is likely first run for this version. + // Todo: Might be smarter to have an exit code for a cleaner shutdown + removeMarkerFiles(); + } return false; } LL_INFOS("InitInfo") << "Hardware test initialization done." << LL_ENDL ; @@ -3008,7 +3018,23 @@ bool LLAppViewer::initConfiguration() { if (sendURLToOtherInstance(start_slurl.getSLURLString())) { - // successfully handed off URL to existing instance, exit + // Successfully handed off URL to existing instance. + // Returning 'false' gets treated as a failure to init, + // without cleanup, so instead clear markers and app here. + // Do not save settings. + // Might be smarter to have an exit code for a more reliable + // "early exit, needs cleanup" case. + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Todo: Unfortunately, if we are doing this, stats and + // markers from previous session were already processed, + // cleared yet haven't been reported and will be lost. + // Consider a way to save those. + removeMarkerFiles(); + } return false; } } @@ -3055,6 +3081,11 @@ bool LLAppViewer::initConfiguration() LLTrans::getString("MBAlreadyRunning"), LLStringUtil::null, OSMB_OK); + + // Since returning 'false' is basically an error without cleanup, + // do cleanup here. No need to worry about marker files here. + LLSingletonBase::deleteAll(); + cleanupConsole(); return false; } @@ -5661,7 +5692,7 @@ void LLAppViewer::removeCloseRequestMarker() const if (!mSecondInstance) { std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); - LLFile::remove(error_marker_file); + LLFile::remove(error_marker_file, ENOENT); } } @@ -5686,7 +5717,7 @@ void LLAppViewer::removeWatchdogMarker() const if (!mSecondInstance) { std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); - LLFile::remove(error_marker_file); + LLFile::remove(error_marker_file, ENOENT); } } diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 87c885e4be..13c4c5f6c0 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -17,7 +17,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, 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 @@ -276,6 +276,7 @@ protected: virtual bool initWindow(); // Initialize the viewer's window. virtual void initLoggingAndGetLastDuration(); // Initialize log files, logging system virtual void initConsole() {}; // Initialize OS level debugging console. + virtual void cleanupConsole() {}; // Cleanup OS level debugging console. virtual bool initHardwareTest() { return true; } // A false result indicates the app should quit. virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 3dc98b83c4..4e443ee189 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, 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 @@ -854,12 +854,7 @@ bool LLAppViewerWin32::cleanup() bool result = LLAppViewer::cleanup(); gDXHardware.cleanup(); - - if (mIsConsoleAllocated) - { - FreeConsole(); - mIsConsoleAllocated = false; - } + cleanupConsole(); return result; } @@ -943,6 +938,15 @@ void LLAppViewerWin32::initConsole() return LLAppViewer::initConsole(); } +void LLAppViewerWin32::cleanupConsole() +{ + if (mIsConsoleAllocated) + { + FreeConsole(); + mIsConsoleAllocated = false; + } +} + void write_debug_dx(const char* str) { std::string value = gDebugInfo["DXInfo"].asString(); diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index ad61ae68d6..971907694a 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, 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 @@ -53,6 +53,8 @@ protected: bool initWindow() override; // Override to initialize the viewer's window. void initLoggingAndGetLastDuration() override; // Override to clean stack_trace info. void initConsole() override; // Initialize OS level debugging console. + void cleanupConsole() override; + bool initHardwareTest() override; // Win32 uses DX9 to test hardware. bool initParseCommandLine(LLCommandLineParser& clp) override; -- cgit v1.3 From 1832026b5896968bac100e67235ad242a40f2157 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sat, 30 May 2026 01:35:57 +0300 Subject: #5856 An option to prevent hibernation --- indra/newview/app_settings/settings.xml | 11 + indra/newview/llagent.cpp | 9 + indra/newview/llappviewer.cpp | 48 +++- indra/newview/llappviewer.h | 15 +- indra/newview/llappviewerlinux.cpp | 302 +++++++++++++++++++++ indra/newview/llappviewerlinux.h | 10 + indra/newview/llappviewermacosx-objc.h | 1 + indra/newview/llappviewermacosx-objc.mm | 66 +++++ indra/newview/llappviewermacosx.cpp | 7 + indra/newview/llappviewermacosx.h | 1 + indra/newview/llappviewerwin32.cpp | 50 ++++ indra/newview/llappviewerwin32.h | 1 + indra/newview/llviewerwindow.cpp | 30 +- .../default/xui/en/panel_preferences_setup.xml | 36 +++ 14 files changed, 578 insertions(+), 9 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 8086afb865..b1e161182e 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -14429,6 +14429,17 @@ Value 180 + OSHibernationMode + + Comment + Whether to prevent OS from hibernating. 0 - can hibernate; 1 - can't hibernate, can turn screen off; 2 - can't hibernate, can't turn screen off + Persist + 1 + Type + S32 + Value + 0 + HeightUnits Comment diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 31814d8f4f..35d60e6595 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -1552,6 +1552,8 @@ void LLAgent::setAFK() setControlFlags(AGENT_CONTROL_AWAY | AGENT_CONTROL_STOP); gAwayTimer.start(); } + + LLAppViewer::instance()->setPermitOSHibernation(true); } //----------------------------------------------------------------------------- @@ -1570,6 +1572,13 @@ void LLAgent::clearAFK() sendAnimationRequest(ANIM_AGENT_AWAY, ANIM_REQUEST_STOP); clearControlFlags(AGENT_CONTROL_AWAY); } + + if (isAgentAvatarValid()) + { + // Only set this if agent is inworld, login screen + // shouldn't prevent hibernation. + LLAppViewer::instance()->setPermitOSHibernation(false); + } } //----------------------------------------------------------------------------- diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 6b5c0ea6b0..4a6739bb40 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -633,6 +633,12 @@ bool LLAppViewer::sendURLToOtherInstance(const std::string& url) return false; } +//virtual +void LLAppViewer::setOSHibernationMode(eHibernationMode mode) +{ + // See OS specific files +} + //---------------------------------------------------------------------------- // LLAppViewer definition @@ -4474,9 +4480,12 @@ void LLAppViewer::abortQuit() mClosingFloaters = false; } -void LLAppViewer::sendViewerStatistics() +void LLAppViewer::sendViewerStatistics(bool include_preferences) { - send_viewer_stats(false); + if (!gDisconnected) + { + send_viewer_stats(include_preferences); + } } void LLAppViewer::migrateCacheDirectory() @@ -5857,6 +5866,29 @@ void LLAppViewer::outOfMemorySoftQuit() } } +void LLAppViewer::setPermitOSHibernation(bool permit) +{ + if (permit) + { + if (mCurrentHibernationMode != LL_HIBERNATE_MODE_DEFAULT) + { + // Will call OS specific code to let OS hibernate when idle + setOSHibernationMode(LL_HIBERNATE_MODE_DEFAULT); + mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + } + } + else + { + static LLCachedControl os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); + eHibernationMode mode = static_cast(os_hibernation_mode()); + if (mode != LL_HIBERNATE_MODE_DEFAULT && mCurrentHibernationMode != mode) + { + setOSHibernationMode(mode); + mCurrentHibernationMode = mode; + } + } +} + void LLAppViewer::idleNameCache() { // Neither old nor new name cache can function before agent has a region @@ -6064,6 +6096,9 @@ void LLAppViewer::disconnectViewer() // Pass the connection state to LLUrlEntryParcel not to attempt // parcel info requests while disconnected. LLUrlEntryParcel::setDisconnected(gDisconnected); + + // Restore default OS hibernation mode + setPermitOSHibernation(true); } void LLAppViewer::forceErrorLLError() @@ -6329,6 +6364,15 @@ void LLAppViewer::handleLoginComplete() // we logged in successfully, so save settings on logout LL_INFOS() << "Login successful, per account settings will be saved on log out." << LL_ENDL; mSavePerAccountSettings=true; + + // Don't allow hibernation while we're running + setPermitOSHibernation(false); + // Track 'hibernation' mode changes + mOSHibernationModeChangeConnection = gSavedSettings.getControl("OSHibernationMode")->getSignal()->connect([](LLControlVariable* control, const LLSD& new_val, const LLSD& old_val) + { + // setPermitOSHibernation will sort itself out based on new mode. + LLAppViewer::instance()->setPermitOSHibernation(false); + }); } //virtual diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index cde58b0850..6ecafae036 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -114,7 +114,7 @@ public: const LLSD& substitutions = LLSD()); // Display an error dialog and forcibly quit. void earlyExitNoNotify(); // Do not display error dialog then forcibly quit. void abortQuit(); // Called to abort a quit request. - void sendViewerStatistics(); + void sendViewerStatistics(bool include_preferences); bool quitRequested() { return mQuitRequested; } bool logoutRequestSent() { return mLogoutRequestSent; } @@ -269,6 +269,8 @@ public: // Note: mQuitRequested can be aborted by user. void outOfMemorySoftQuit(); + virtual void setPermitOSHibernation(bool permit); + #ifdef LL_DISCORD static void initDiscordSocial(); static void updateDiscordActivity(); @@ -285,6 +287,14 @@ protected: virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + typedef enum + { + LL_HIBERNATE_MODE_DEFAULT = 0, // Use the platform's default behavior. + LL_HIBERNATE_MODE_PREVENT = 1, + LL_HIBERNATE_MODE_PREVENT_SCREEN = 2, + } eHibernationMode; + virtual void setOSHibernationMode(eHibernationMode mode); + virtual bool initParseCommandLine(LLCommandLineParser& clp) { return true; } // Allow platforms to specify the command line args. @@ -391,6 +401,9 @@ private: LLAppCoreHttp mAppCoreHttp; bool mIsFirstRun; + + eHibernationMode mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + boost::signals2::scoped_connection mOSHibernationModeChangeConnection; }; // Globals with external linkage. From viewer.h diff --git a/indra/newview/llappviewerlinux.cpp b/indra/newview/llappviewerlinux.cpp index 89d19d180b..fe0d6f6e7d 100644 --- a/indra/newview/llappviewerlinux.cpp +++ b/indra/newview/llappviewerlinux.cpp @@ -58,6 +58,10 @@ namespace void (*gOldTerminateHandler)() = NULL; } +// Initialize static members +guint32 LLAppViewerLinux::sPowerInhibitCookie = 0; +bool LLAppViewerLinux::sPowerInhibitActive = false; + static void exceptionTerminateHandler() { @@ -117,6 +121,11 @@ LLAppViewerLinux::LLAppViewerLinux() LLAppViewerLinux::~LLAppViewerLinux() { + // Clean up any power management inhibition on exit + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } } bool LLAppViewerLinux::init() @@ -329,6 +338,299 @@ bool LLAppViewerLinux::sendURLToOtherInstance(const std::string& url) } #endif // LL_DBUS_ENABLED + +void LLAppViewerLinux::setOSHibernationMode(eHibernationMode mode) +{ + if (mode == LL_HIBERNATE_MODE_DEFAULT) + { + // Allow OS to sleep/hibernate - remove any inhibition + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT) + { + // Prevent system sleep, but allow display to turn off + // Release any existing inhibition first to allow mode switching + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } + + if (inhibitPowerManagement(false)) + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep, display sleep allowed" << LL_ENDL; + } + else + { + LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Prevent both system and display sleep + // Release any existing inhibition first to allow mode switching + if (sPowerInhibitActive) + { + uninhibitPowerManagement(); + } + + if (inhibitPowerManagement(true)) + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep and display sleep" << LL_ENDL; + } + else + { + LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep and display sleep" << LL_ENDL; + } + } +} + +// TODO: This is AI Generated!!!, needs review and testing. +bool LLAppViewerLinux::inhibitPowerManagement(bool inhibit_display) +{ +#if LL_DBUS_ENABLED + // Try to use D-Bus to inhibit power management via various desktop environment APIs + // This works with GNOME, KDE, XFCE, and most modern Linux desktop environments + + if (!grab_dbus_syms(DBUSGLIB_DYLIB_DEFAULT_NAME)) + { + LL_WARNS("OS") << "Failed to load D-Bus symbols for power management" << LL_ENDL; + return false; + } + + GError* error = nullptr; + DBusGConnection* bus = lldbus_g_bus_get(DBUS_BUS_SESSION, &error); + + if (!bus) + { + LL_WARNS("OS") << "Failed to connect to D-Bus session bus: " + << (error ? error->message : "unknown error") << LL_ENDL; + if (error) + g_error_free(error); + return false; + } + + // Try multiple power management services in order of preference + // 1. org.freedesktop.PowerManagement (older standard) + // 2. org.gnome.SessionManager (GNOME) + // 3. org.kde.Solid.PowerManagement (KDE) + + const char* services[] = { + "org.freedesktop.PowerManagement", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* paths[] = { + "/org/freedesktop/PowerManagement/Inhibit", + "/org/gnome/SessionManager", + "/org/kde/Solid/PowerManagement" + }; + + const char* interfaces[] = { + "org.freedesktop.PowerManagement.Inhibit", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* methods[] = { + "Inhibit", + "Inhibit", + "inhibit" + }; + + bool success = false; + + for (int i = 0; i < 3 && !success; ++i) + { + DBusGProxy* proxy = lldbus_g_proxy_new_for_name( + bus, + services[i], + paths[i], + interfaces[i] + ); + + if (!proxy) + continue; + + error = nullptr; + guint32 cookie = 0; + + if (i == 0) // freedesktop.PowerManagement + { + // Inhibit(application_name: s, reason: s) -> cookie: u + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_STRING, "Second Life Viewer", + G_TYPE_STRING, inhibit_display ? + "Viewer active - preventing system and display sleep" : + "Viewer active - preventing system sleep", + G_TYPE_INVALID, + G_TYPE_UINT, &cookie, + G_TYPE_INVALID + ); + } + else if (i == 1) // GNOME SessionManager + { + // Inhibit(app_id: s, toplevel_xid: u, reason: s, flags: u) -> cookie: u + // flags: 4 = suspend, 8 = idle (display), 12 = both + guint32 flags = inhibit_display ? 12 : 4; + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_STRING, "SecondLifeViewer", + G_TYPE_UINT, 0, // toplevel_xid (0 = none) + G_TYPE_STRING, inhibit_display ? + "Viewer active - preventing system and display sleep" : + "Viewer active - preventing system sleep", + G_TYPE_UINT, flags, + G_TYPE_INVALID, + G_TYPE_UINT, &cookie, + G_TYPE_INVALID + ); + } + else if (i == 2) // KDE Solid + { + // Different method signature for KDE + success = lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_INVALID, + G_TYPE_INT, &cookie, + G_TYPE_INVALID + ); + } + + if (success) + { + sPowerInhibitCookie = cookie; + sPowerInhibitActive = true; + LL_INFOS("OS") << "Successfully inhibited power management using " + << services[i] << LL_ENDL; + } + else if (error) + { + LL_DEBUGS("OS") << "Failed to inhibit via " << services[i] + << ": " << error->message << LL_ENDL; + g_error_free(error); + error = nullptr; + } + + g_object_unref(proxy); + } + + return success; + +#else // !LL_DBUS_ENABLED + LL_WARNS("OS") << "Power management control not available - D-Bus support not enabled" << LL_ENDL; + return false; +#endif +} + +void LLAppViewerLinux::uninhibitPowerManagement() +{ +#if LL_DBUS_ENABLED + if (!sPowerInhibitActive || sPowerInhibitCookie == 0) + { + return; + } + + if (!grab_dbus_syms(DBUSGLIB_DYLIB_DEFAULT_NAME)) + { + LL_WARNS("OS") << "Failed to load D-Bus symbols for power management uninhibit" << LL_ENDL; + return; + } + + GError* error = nullptr; + DBusGConnection* bus = lldbus_g_bus_get(DBUS_BUS_SESSION, &error); + + if (!bus) + { + if (error) + g_error_free(error); + return; + } + + // Try to uninhibit using all services that might have been used + const char* services[] = { + "org.freedesktop.PowerManagement", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* paths[] = { + "/org/freedesktop/PowerManagement/Inhibit", + "/org/gnome/SessionManager", + "/org/kde/Solid/PowerManagement" + }; + + const char* interfaces[] = { + "org.freedesktop.PowerManagement.Inhibit", + "org.gnome.SessionManager", + "org.kde.Solid.PowerManagement" + }; + + const char* methods[] = { + "UnInhibit", + "Uninhibit", + "uninhibit" + }; + + bool success = false; + + for (int i = 0; i < 3; ++i) + { + DBusGProxy* proxy = lldbus_g_proxy_new_for_name( + bus, + services[i], + paths[i], + interfaces[i] + ); + + if (!proxy) + continue; + + error = nullptr; + + if (lldbus_g_proxy_call( + proxy, + methods[i], + &error, + G_TYPE_UINT, sPowerInhibitCookie, + G_TYPE_INVALID, + G_TYPE_INVALID)) + { + success = true; + LL_INFOS("OS") << "Successfully uninhibited power management using " + << services[i] << LL_ENDL; + } + else if (error) + { + LL_DEBUGS("OS") << "Failed to uninhibit via " << services[i] + << ": " << error->message << LL_ENDL; + g_error_free(error); + error = nullptr; + } + + g_object_unref(proxy); + + if (success) + break; + } + + sPowerInhibitCookie = 0; + sPowerInhibitActive = false; + +#endif // LL_DBUS_ENABLED +} + void LLAppViewerLinux::initCrashReporting(bool reportFreeze) { std::string cmd =gDirUtilp->getExecutableDir(); diff --git a/indra/newview/llappviewerlinux.h b/indra/newview/llappviewerlinux.h index dde223878d..6ab3682515 100644 --- a/indra/newview/llappviewerlinux.h +++ b/indra/newview/llappviewerlinux.h @@ -68,6 +68,16 @@ protected: virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + virtual void setOSHibernationMode(eHibernationMode mode); + +private: + // Power management state tracking + static guint32 sPowerInhibitCookie; + static bool sPowerInhibitActive; + + // Helper methods for power management + bool inhibitPowerManagement(bool inhibit_display); + void uninhibitPowerManagement(); }; #if LL_DBUS_ENABLED diff --git a/indra/newview/llappviewermacosx-objc.h b/indra/newview/llappviewermacosx-objc.h index bfbb48dadb..13b8056193 100644 --- a/indra/newview/llappviewermacosx-objc.h +++ b/indra/newview/llappviewermacosx-objc.h @@ -32,5 +32,6 @@ void force_ns_sxeption(); void register_url_schemes(); +void set_os_hibernation_mode(int mode); #endif // LL_LLAPPVIEWERMACOSX_OBJC_H diff --git a/indra/newview/llappviewermacosx-objc.mm b/indra/newview/llappviewermacosx-objc.mm index 75d6b56e3e..ab4ae428b9 100644 --- a/indra/newview/llappviewermacosx-objc.mm +++ b/indra/newview/llappviewermacosx-objc.mm @@ -29,6 +29,7 @@ #endif #import +#import #include #include "llappviewermacosx-objc.h" @@ -63,3 +64,68 @@ void register_url_schemes() } } } + +// Add these as static variables at file scope +static IOPMAssertionID gPowerAssertionID = kIOPMNullAssertionID; + +void set_os_hibernation_mode(int mode) +{ + // Release existing assertion + if (gPowerAssertionID != kIOPMNullAssertionID) + { + IOReturn result = IOPMAssertionRelease(gPowerAssertionID); + if (result == kIOReturnSuccess) + { + gPowerAssertionID = kIOPMNullAssertionID; + NSLog(@"Permitted OS hibernation/sleep"); + } + else + { + NSLog(@"Failed to release power assertion: %d", result); + } + } + + if (mode == 1) + { + // Prevent OS from sleeping/hibernating + CFStringRef assertionName = CFSTR("Second Life Viewer"); + // kIOPMAssertionTypeNoIdleSleep prevents idle sleep + IOReturn result = IOPMAssertionCreateWithName( + kIOPMAssertionTypeNoIdleSleep, + kIOPMAssertionLevelOn, + assertionName, + &gPowerAssertionID + ); + + if (result == kIOReturnSuccess) + { + NSLog(@"Prevented OS hibernation/sleep, allow display sleep"); + } + else + { + NSLog(@"Failed to create power assertion: %d", result); + } + } + else if (mode == 2) + { + // Prevent OS from sleeping/hibernating, prevent screen from going off + CFStringRef assertionName = CFSTR("Second Life Viewer"); + // kIOPMAssertionTypeNoIdleSleep prevents idle sleep + // kIOPMAssertionTypeNoDisplaySleep prevents display sleep + IOReturn result = IOPMAssertionCreateWithName( + kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, + assertionName, + &gPowerAssertionID + ); + + if (result == kIOReturnSuccess) + { + NSLog(@"Prevented OS hibernation/sleep or screen from turning off"); + } + else + { + NSLog(@"Failed to create power assertion: %d", result); + } + } +} diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp index 830e2d4473..1c01d06852 100644 --- a/indra/newview/llappviewermacosx.cpp +++ b/indra/newview/llappviewermacosx.cpp @@ -125,6 +125,7 @@ bool pumpMainLoop() void cleanupViewer() { + set_os_hibernation_mode(0); // restore default OS hibernation behavior if(!LLApp::isError()) { if (gViewerAppPtr) @@ -427,6 +428,12 @@ bool LLAppViewerMacOSX::initSLURLHandler() return true; } +void LLAppViewerMacOSX::setOSHibernationMode(eHibernationMode mode) +{ + // pass to objective-c++ + set_os_hibernation_mode((int)mode); +} + std::string LLAppViewerMacOSX::generateSerialNumber() { char serial_md5[MD5HEX_STR_SIZE]; // Flawfinder: ignore diff --git a/indra/newview/llappviewermacosx.h b/indra/newview/llappviewermacosx.h index 35fc99dbd9..1153bd4191 100644 --- a/indra/newview/llappviewermacosx.h +++ b/indra/newview/llappviewermacosx.h @@ -47,6 +47,7 @@ public: protected: virtual bool restoreErrorTrap(); virtual bool initSLURLHandler(); + virtual void setOSHibernationMode(eHibernationMode mode); std::string generateSerialNumber(); virtual bool initParseCommandLine(LLCommandLineParser& clp); diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 5184b0f025..25aeb2a26e 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -1020,6 +1020,56 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) return false; } +void LLAppViewerWin32::setOSHibernationMode(eHibernationMode mode) +{ + // ES_CONTINUOUS tells Windows to reset the idle timer + // and restore normal operation + // ES_SYSTEM_REQUIRED prevents system sleep/hibernation + // ES_DISPLAY_REQUIRED prevents display sleep + + if (mode == LL_HIBERNATE_MODE_DEFAULT) + { + // Allow OS to hibernate - clear the previous execution state flags + // ES_CONTINUOUS without other flags allows the system to idle normally + SetThreadExecutionState(ES_CONTINUOUS); + LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; + } + else if (mode == LL_HIBERNATE_MODE_PREVENT) + { + // Prevent OS from hibernating while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED + ); + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation, but allowed display sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Prevent OS from hibernating or turning screen off while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + // ES_DISPLAY_REQUIRED keeps the display on + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED + ); + + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation and display sleep, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep" << LL_ENDL; + } + } +} + bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir) { // Velopack installs viewer like this: diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 971907694a..7c3c7a475a 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -62,6 +62,7 @@ protected: bool restoreErrorTrap() override; bool sendURLToOtherInstance(const std::string& url) override; + void setOSHibernationMode(eHibernationMode mode) override; std::string generateSerialNumber(); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index abd7096f50..4229ffcfb5 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -1487,12 +1487,30 @@ void LLViewerWindow::handleCloseRequestCanceled() void LLViewerWindow::handleSuspendRequest() { - LLAppViewer::instance()->sendViewerStatistics(); - // Todo: this should send a disconnect request as viewer - // can't keep heartbeat up while suspended and will get - // disconnected within a minute. - // Add a disconnect here once 'prevent OS from sleeping' - // feature is ready. + static LLCachedControl os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); + if (os_hibernation_mode == 0) + { + LL_INFOS() << "Got a 'suspend' event from OS" << LL_ENDL; + // Viewer doesn't handle hibernation. + // Just send statistics. + LLAppViewer::instance()->sendViewerStatistics(false); + } + else + { + LL_INFOS() << "Got a 'suspend' event from OS, disconnecting" << LL_ENDL; + // Viewer is set to prevent hibernation if agent isn't away. + // If we got here, likely Agent 'went' away then viewer got + // a hibernation message. + // We have a limited timeframe. Sends stats then disconnect. + LLViewerRegion* region = gAgent.getRegion(); + if (region) + { + LLAppViewer::instance()->sendViewerStatistics(true); + LLAppViewer::instance()->metricsSend(!gDisconnected); + // Make sure to show a message. + LLAppViewer::instance()->forceDisconnect(LLTrans::getString("YouHaveBeenDisconnected")); + } + } } bool LLViewerWindow::handleCloseRequest(LLWindow *window, bool from_user) diff --git a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml index 2036ed75ca..d5f2f69df7 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml @@ -246,5 +246,41 @@ + + Prevent OS from hibernating if not Away: + + + + + + -- cgit v1.3 From 6d9293fe7393be0f17836c7e5512e8a2d8bb53f4 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:32:32 +0300 Subject: #5084 Include watchdog's state in crash report to help diagnose crashes --- indra/llcommon/llwatchdog.cpp | 5 +++++ indra/llcommon/llwatchdog.h | 2 ++ indra/newview/llappviewer.cpp | 26 ++++++++++++++++++++++++++ indra/newview/llappviewer.h | 1 + indra/newview/llappviewerwin32.cpp | 8 ++++++++ 5 files changed, 42 insertions(+) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index 66b565c763..fd0702733c 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -116,6 +116,11 @@ bool LLWatchdogTimeout::isAlive() const return (mTimer.getStarted() && !mTimer.hasExpired()); } +bool LLWatchdogTimeout::hasExpired() const +{ + return mTimer.hasExpired(); +} + void LLWatchdogTimeout::reset() { mTimer.setTimerExpirySec(mTimeout); diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index f138fbccb0..b286dd179d 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 void reset() = 0; virtual void start(); virtual void stop(); @@ -66,6 +67,7 @@ public: virtual ~LLWatchdogTimeout(); bool isAlive() const override; + bool hasExpired() const override; void reset() override; void start() override { start(""); } void stop() override; diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 263c1f2054..58e3aea9b7 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -6321,6 +6321,32 @@ F32 LLAppViewer::getMainloopTimeoutSec() const } } +std::string LLAppViewer::getMainloopWatchdogState() const +{ + if (!mMainloopTimeout) + { + return std::string(); + } + std::string state = mMainloopTimeout->getState(); + + if (mMainloopTimeout->hasExpired()) + { + return "Expired at " + state; + } + + // Check if the watchdog is currently active (timer started) + if (!mMainloopTimeout->isAlive()) + { + // Timer is not running, meaning watchdog is paused/stopped + if (state.empty()) + { + return "Paused"; + } + return "Paused at " + state; + } + return state; +} + void LLAppViewer::handleLoginComplete() { gLoggedInTime.start(); diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 6ecafae036..1b1a89d756 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -212,6 +212,7 @@ public: void pingMainloopTimeout(std::string_view state); F32 getMainloopTimeoutSec() const; + std::string getMainloopWatchdogState() const; // Handle the 'login completed' event. // *NOTE:Mani Fix this for login abstraction!! diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 25aeb2a26e..416efa9354 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -193,6 +193,14 @@ namespace } LLAppViewer* app = LLAppViewer::instance(); + + // Include mainloop watchdog state if available + std::string watchdog_state = app->getMainloopWatchdogState(); + if (!watchdog_state.empty()) + { + sBugSplatSender->setAttribute(WCSTR(L"WatchdogState"), WCSTR(watchdog_state)); + } + if (!app->isSecondInstance() && !app->errorMarkerExists()) { // If marker doesn't exist, create a marker with 'other' or 'logout' code for next launch -- cgit v1.3 From 60b7e3b78cc401318e4f895bb64b5688645b3462 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:18:58 +0300 Subject: #6153 Make hardware test more robust --- indra/llcommon/llprocess.cpp | 5 ++ indra/llcommon/llprocess.h | 4 ++ indra/newview/app_settings/cmd_line.xml | 6 ++ indra/newview/llappviewer.cpp | 43 +++++++++--- indra/newview/llappviewerwin32.cpp | 18 ++++- indra/newview/llcommandlineparser.cpp | 2 +- indra/newview/llfeaturemanager.cpp | 114 +++++++++++++++++++++++++++++++- 7 files changed, 178 insertions(+), 14 deletions(-) (limited to 'indra/newview/llappviewerwin32.cpp') diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp index e43f386af2..86e1f48377 100644 --- a/indra/llcommon/llprocess.cpp +++ b/indra/llcommon/llprocess.cpp @@ -1109,6 +1109,11 @@ bool LLProcess::kill(const LLProcessPtr& ptr, const std::string& who) return !ptr || ptr->kill(who); } +void LLProcess::pump() +{ + tick(); +} + LLProcess::id LLProcess::getProcessID() const { if (!mChild) diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h index 26f9a27837..a016b6a258 100644 --- a/indra/llcommon/llprocess.h +++ b/indra/llcommon/llprocess.h @@ -234,6 +234,10 @@ public: 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; typedef HANDLE handle; diff --git a/indra/newview/app_settings/cmd_line.xml b/indra/newview/app_settings/cmd_line.xml index e16a5c7e76..bf7f9bab1f 100644 --- a/indra/newview/app_settings/cmd_line.xml +++ b/indra/newview/app_settings/cmd_line.xml @@ -90,6 +90,12 @@ ConnectAsGod + gpubenchmark + + desc + Run GPU memory bandwidth benchmark, print result to stdout, and exit. Used internally by the viewer to isolate benchmark from the main process. + + graphicslevel desc diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 35a2cc1852..d155e7340b 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -300,6 +300,13 @@ extern bool gDebugGL; extern bool gHiDPISupport; #endif +#if LL_WINDOWS +extern bool gGPUBenchmarkMode; +#else +static constexpr bool gGPUBenchmarkMode = false; +#endif // LL_WINDOWS + + //////////////////////////////////////////////////////////// // All from the last globals push... @@ -2373,11 +2380,17 @@ void LLAppViewer::initLoggingAndGetLastDuration() if (mSecondInstance) { - LLFile::mkdir(gDirUtilp->getDumpLogsDirPath()); + if (!gGPUBenchmarkMode) + { + LLFile::mkdir(gDirUtilp->getDumpLogsDirPath()); - LLUUID uid; - uid.generate(); - LLError::logToFile(gDirUtilp->getDumpLogsDirPath(uid.asString() + ".log")); + LLUUID uid; + uid.generate(); + // Is this even useful? + // Originally this wa used to store states, but I don't think it's practical with bugsplat attributes. + // So it just spams files now. + LLError::logToFile(gDirUtilp->getDumpLogsDirPath(uid.asString() + ".log")); + } } else { @@ -3081,12 +3094,15 @@ bool LLAppViewer::initConfiguration() // Display splash screen. Must be after above check for previous // crash as this dialog is always frontmost. - std::string splash_msg; - LLStringUtil::format_map_t args; - args["[APP_NAME]"] = LLTrans::getString("SECOND_LIFE"); - splash_msg = LLTrans::getString("StartupLoading", args); - LLSplashScreen::show(); - LLSplashScreen::update(splash_msg); + if (!gGPUBenchmarkMode) + { + std::string splash_msg; + LLStringUtil::format_map_t args; + args["[APP_NAME]"] = LLTrans::getString("SECOND_LIFE"); + splash_msg = LLTrans::getString("StartupLoading", args); + LLSplashScreen::show(); + LLSplashScreen::update(splash_msg); + } //LLVolumeMgr::initClass(); LLVolumeMgr* volume_manager = new LLVolumeMgr(); @@ -4139,6 +4155,13 @@ bool LLAppViewer::getMarkerData(const std::string& marker_name, std::string& dat void LLAppViewer::processMarkerFiles() { + if (gGPUBenchmarkMode) + { + // Skipping marker file processing in GPU benchmark mode + mSecondInstance = true; + initLoggingAndGetLastDuration(); + return; + } //We've got 4 things to test for here // - Other Process Running (SecondLife.exec_marker present, locked) // - Freeze (SecondLife.exec_marker present, not locked) diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 159c3af8a0..2080d0b7bf 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -234,6 +234,8 @@ namespace } #endif // LL_BUGSPLAT +extern bool gGPUBenchmarkMode; + namespace { void (*gOldTerminateHandler)() = NULL; @@ -515,12 +517,21 @@ int APIENTRY WINMAIN(HINSTANCE hInstance, gIconResource = MAKEINTRESOURCE(IDI_LL_ICON); gIconSmallResource = MAKEINTRESOURCE(IDI_LL_ICON_SMALL); + // Benchmark subprocess mode before full init for LLFeatureManager::loadGPUClass(). + { + std::wstring cmdLineStr(pCmdLine ? pCmdLine : L""); + if (cmdLineStr.find(L"--gpubenchmark") != std::wstring::npos) + { + gGPUBenchmarkMode = true; + } + } + LLAppViewerWin32* viewer_app_ptr = new LLAppViewerWin32(ll_convert_wide_to_string(pCmdLine).c_str()); gOldTerminateHandler = std::set_terminate(exceptionTerminateHandler); // Set a debug info flag to indicate if multiple instances are running. - bool found_other_instance = !create_app_mutex(); + bool found_other_instance = gGPUBenchmarkMode || !create_app_mutex(); gDebugInfo["FoundOtherInstanceAtStartup"] = LLSD::Boolean(found_other_instance); bool ok = viewer_app_ptr->init(); @@ -940,7 +951,10 @@ void LLAppViewerWin32::initLoggingAndGetLastDuration() void LLAppViewerWin32::initConsole() { // pop up debug console - mIsConsoleAllocated = create_console(); + if (!gGPUBenchmarkMode) + { + mIsConsoleAllocated = create_console(); + } return LLAppViewer::initConsole(); } diff --git a/indra/newview/llcommandlineparser.cpp b/indra/newview/llcommandlineparser.cpp index 84d3ff90d5..7e7d528199 100644 --- a/indra/newview/llcommandlineparser.cpp +++ b/indra/newview/llcommandlineparser.cpp @@ -60,7 +60,7 @@ namespace // List of command-line switches that can't map-to settings variables. // Going forward, we want every new command-line switch to map-to some // settings variable. This list is used to validate that. - const std::set unmapped_options = { "help", "set", "setdefault", "settings", "sessionsettings", "usersessionsettings" }; + const std::set unmapped_options = { "help", "set", "setdefault", "settings", "sessionsettings", "usersessionsettings", "gpubenchmark" }; po::options_description gOptionsDesc; po::positional_options_description gPositionalOptions; diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp index 8140aaab22..3dbb93c453 100644 --- a/indra/newview/llfeaturemanager.cpp +++ b/indra/newview/llfeaturemanager.cpp @@ -377,6 +377,83 @@ F32 gpu_benchmark(); #if LL_WINDOWS +bool gGPUBenchmarkMode = false; + +// Runs gpu_benchmark() in a subprocess (exe with --gpubenchmark). +static F32 subprocess_gpu_benchmark() +{ + LLProcess::Params params; + params.executable = gDirUtilp->getExecutablePathAndName(); + params.args.add("--gpubenchmark"); + params.desc = "GPU benchmark"; + params.autokill = true; // killed via job object if parent crashes + params.attached = true; // killed on LLProcessPtr destruction (timeout) + params.files.add(LLProcess::FileParam()); // stdin: default + params.files.add(LLProcess::FileParam().type("pipe")); // stdout: pipe + params.files.add(LLProcess::FileParam()); // stderr: default + + LLProcessPtr child; + try + { + child = LLProcess::create(params); + } + catch (const std::exception& e) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: failed to launch: " + << e.what() << LL_ENDL; + return -1.f; + } + + if (!child) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: LLProcess::create returned null." << LL_ENDL; + return -1.f; + } + + LLProcess::ReadPipe& out = child->getReadPipe(LLProcess::STDOUT); + + const F32 POLL_INTERVAL_S = 0.25f; + const F32 TOTAL_TIMEOUT_S = 120.f; // covers full viewer init + benchmark + LLTimer timer; + timer.start(); + + while (timer.getElapsedTimeF32() < TOTAL_TIMEOUT_S) + { + child->pump(); + + // Result is a single float followed by '\n' + if (out.contains('\n')) + { + std::string line = out.getline(); + float parsed = 0.f; + if (sscanf_s(line.c_str(), "%f", &parsed) == 1 && parsed > 0.f) + { + LL_INFOS("RenderInit") << "subprocess_gpu_benchmark: result = " + << parsed << " GB/sec" << LL_ENDL; + // Let LLProcessPtr destructor handle cleanup + return parsed; + } + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: unparseable output: '" + << line << "'" << LL_ENDL; + return -1.f; + } + + // If child already exited without writing anything, bail + if (!child->isRunning()) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: process exited without result." << LL_ENDL; + return -1.f; + } + + ms_sleep((U32)(POLL_INTERVAL_S * 1000)); + } + + // Timeout: attached=true means LLProcessPtr destructor kills it + LL_WARNS("RenderInit") << "GPU benchmark subprocess timed out after " + << (int)TOTAL_TIMEOUT_S << " seconds; killing." << LL_ENDL; + return -1.f; +} + F32 logExceptionBenchmark() { // FIXME: gpu_benchmark uses many C++ classes on the stack to control state. @@ -457,7 +534,27 @@ bool LLFeatureManager::loadGPUClass() try { #if LL_WINDOWS - gbps = logExceptionBenchmark(); + if (gGPUBenchmarkMode) + { + // We ARE the benchmark subprocess; run directly in-process. + // logExceptionBenchmark wraps with SEH so structured exceptions + // (e.g. access violations inside the driver) are still caught. + gbps = logExceptionBenchmark(); + } + else + { + // Normal path: run benchmark in an isolated subprocess so a + // driver hang can be killed without freezing the main viewer. + gbps = subprocess_gpu_benchmark(); + if (gbps == -1.f + && gGLManager.getRawGLString().find("Radeon") != std::string::npos + && checkRDNA35()) + { + // Certain AMD GPUs have known issues with shader profiling and occlusion queries that can lead to hangs. + // If test returned -1, we are likely on a bad driver. + gSavedSettings.setBOOL("UseOcclusion", false); + } + } #else gbps = gpu_benchmark(); #endif @@ -468,6 +565,21 @@ bool LLFeatureManager::loadGPUClass() LL_WARNS("RenderInit") << "GPU benchmark failed: " << e.what() << LL_ENDL; } +#if LL_WINDOWS + // If we are the benchmark subprocess, write the raw result to stdout + // so the parent process can read it, then exit immediately. + if (gGPUBenchmarkMode) + { + LL_WARNS("RenderInit") << "Passing " << gbps << " to parent" << LL_ENDL; + char buf[64]; + int len = snprintf(buf, sizeof(buf), "%.6f\n", gbps); + DWORD written = 0; + WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buf, (DWORD)len, &written, NULL); + FlushFileBuffers(GetStdHandle(STD_OUTPUT_HANDLE)); + ExitProcess(0); + } +#endif + mGPUMemoryBandwidth = gbps; // bias by CPU speed -- cgit v1.3