diff options
| -rw-r--r-- | indra/llwindow/llwindowwin32.cpp | 144 | ||||
| -rw-r--r-- | indra/llwindow/llwindowwin32.h | 6 | ||||
| -rw-r--r-- | indra/newview/llappviewerwin32.cpp | 222 | ||||
| -rw-r--r-- | indra/newview/llappviewerwin32.h | 3 | ||||
| -rw-r--r-- | indra/newview/llvelopack.cpp | 7 |
5 files changed, 379 insertions, 3 deletions
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<DWORD>(l_param); + if (message_type == WM_POST_UNINSTALL_MSG_SHUTDOWN || message_type == WM_POST_UNINSTALL_MSG_UPDATE) + { + DWORD sender_process_id = static_cast<DWORD>(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<std::wstring>(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<HWND> 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<EnumData*>(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<LPARAM>(&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<WPARAM>(our_process_id), + static_cast<LPARAM>(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 <windows.h> #include <shlobj.h> #include <shobjidl.h> @@ -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) |
