/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "PluginHangUI.h" #include "PluginHangUIParent.h" #include "base/command_line.h" #include "mozilla/Telemetry.h" #include "mozilla/ipc/ProtocolUtils.h" #include "mozilla/plugins/PluginModuleParent.h" #include "nsContentUtils.h" #include "nsDirectoryServiceDefs.h" #include "nsIFile.h" #include "nsIProperties.h" #include "nsIWindowMediator.h" #include "nsIWinTaskbar.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "WidgetUtils.h" #define NS_TASKBAR_CONTRACTID "@mozilla.org/windows-taskbar;1" using base::ProcessHandle; using mozilla::widget::WidgetUtils; using std::string; using std::vector; namespace { class nsPluginHangUITelemetry : public mozilla::Runnable { public: nsPluginHangUITelemetry(int aResponseCode, int aDontAskCode, uint32_t aResponseTimeMs, uint32_t aTimeoutMs) : Runnable("nsPluginHangUITelemetry"), mResponseCode(aResponseCode), mDontAskCode(aDontAskCode), mResponseTimeMs(aResponseTimeMs), mTimeoutMs(aTimeoutMs) {} NS_IMETHOD Run() override { mozilla::Telemetry::Accumulate( mozilla::Telemetry::PLUGIN_HANG_UI_USER_RESPONSE, mResponseCode); mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLUGIN_HANG_UI_DONT_ASK, mDontAskCode); mozilla::Telemetry::Accumulate( mozilla::Telemetry::PLUGIN_HANG_UI_RESPONSE_TIME, mResponseTimeMs); mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLUGIN_HANG_TIME, mTimeoutMs + mResponseTimeMs); return NS_OK; } private: int mResponseCode; int mDontAskCode; uint32_t mResponseTimeMs; uint32_t mTimeoutMs; }; } // namespace namespace mozilla { namespace plugins { PluginHangUIParent::PluginHangUIParent(PluginModuleChromeParent* aModule, const int32_t aHangUITimeoutPref, const int32_t aChildTimeoutPref) : mMutex("mozilla::plugins::PluginHangUIParent::mMutex"), mModule(aModule), mTimeoutPrefMs(static_cast(aHangUITimeoutPref) * 1000U), mIPCTimeoutMs(static_cast(aChildTimeoutPref) * 1000U), mMainThreadMessageLoop(MessageLoop::current()), mIsShowing(false), mLastUserResponse(0), mHangUIProcessHandle(nullptr), mMainWindowHandle(nullptr), mRegWait(nullptr), mShowEvent(nullptr), mShowTicks(0), mResponseTicks(0) {} PluginHangUIParent::~PluginHangUIParent() { { // Scope for lock MutexAutoLock lock(mMutex); UnwatchHangUIChildProcess(true); } if (mShowEvent) { ::CloseHandle(mShowEvent); } if (mHangUIProcessHandle) { ::CloseHandle(mHangUIProcessHandle); } } bool PluginHangUIParent::DontShowAgain() const { return (mLastUserResponse & HANGUI_USER_RESPONSE_DONT_SHOW_AGAIN); } bool PluginHangUIParent::WasLastHangStopped() const { return (mLastUserResponse & HANGUI_USER_RESPONSE_STOP); } unsigned int PluginHangUIParent::LastShowDurationMs() const { // We only return something if there was a user response if (!mLastUserResponse) { return 0; } return static_cast(mResponseTicks - mShowTicks); } bool PluginHangUIParent::Init(const nsString& aPluginName) { if (mHangUIProcessHandle) { return false; } nsresult rv; rv = mMiniShm.Init(this, ::IsDebuggerPresent() ? INFINITE : mIPCTimeoutMs); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr directoryService( do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID)); if (!directoryService) { return false; } nsCOMPtr greDir; rv = directoryService->Get(NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(greDir)); if (NS_FAILED(rv)) { return false; } nsAutoString path; greDir->GetPath(path); FilePath exePath(path.get()); exePath = exePath.AppendASCII(MOZ_HANGUI_PROCESS_NAME); CommandLine commandLine(exePath.value()); nsAutoString localizedStr; rv = nsContentUtils::FormatLocalizedString( localizedStr, nsContentUtils::eDOM_PROPERTIES, "PluginHangUIMessage", aPluginName); if (NS_FAILED(rv)) { return false; } commandLine.AppendLooseValue(localizedStr.get()); const char* keys[] = {"PluginHangUITitle", "PluginHangUIWaitButton", "PluginHangUIStopButton", "DontAskAgain"}; for (unsigned int i = 0; i < ArrayLength(keys); ++i) { rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, keys[i], localizedStr); if (NS_FAILED(rv)) { return false; } commandLine.AppendLooseValue(localizedStr.get()); } rv = GetHangUIOwnerWindowHandle(mMainWindowHandle); if (NS_FAILED(rv)) { return false; } nsAutoString hwndStr; hwndStr.AppendPrintf("%p", mMainWindowHandle); commandLine.AppendLooseValue(hwndStr.get()); ScopedHandle procHandle( ::OpenProcess(SYNCHRONIZE, TRUE, GetCurrentProcessId())); if (!procHandle.IsValid()) { return false; } nsAutoString procHandleStr; procHandleStr.AppendPrintf("%p", procHandle.Get()); commandLine.AppendLooseValue(procHandleStr.get()); // On Win7+, pass the application user model to the child, so it can // register with it. This insures windows created by the Hang UI // properly group with the parent app on the Win7 taskbar. nsCOMPtr taskbarInfo = do_GetService(NS_TASKBAR_CONTRACTID); if (taskbarInfo) { bool isSupported = false; taskbarInfo->GetAvailable(&isSupported); nsAutoString appId; if (isSupported && NS_SUCCEEDED(taskbarInfo->GetDefaultGroupId(appId))) { commandLine.AppendLooseValue(appId.get()); } else { commandLine.AppendLooseValue(L"-"); } } else { commandLine.AppendLooseValue(L"-"); } nsAutoString ipcTimeoutStr; ipcTimeoutStr.AppendInt(mIPCTimeoutMs); commandLine.AppendLooseValue(ipcTimeoutStr.get()); std::wstring ipcCookie; rv = mMiniShm.GetCookie(ipcCookie); if (NS_FAILED(rv)) { return false; } commandLine.AppendLooseValue(ipcCookie); ScopedHandle showEvent(::CreateEventW(nullptr, FALSE, FALSE, nullptr)); if (!showEvent.IsValid()) { return false; } mShowEvent = showEvent.Get(); MutexAutoLock lock(mMutex); STARTUPINFO startupInfo = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION processInfo = {nullptr}; BOOL isProcessCreated = ::CreateProcess( exePath.value().c_str(), const_cast(commandLine.command_line_string().c_str()), nullptr, nullptr, TRUE, DETACHED_PROCESS, nullptr, nullptr, &startupInfo, &processInfo); if (isProcessCreated) { ::CloseHandle(processInfo.hThread); mHangUIProcessHandle = processInfo.hProcess; ::RegisterWaitForSingleObject(&mRegWait, processInfo.hProcess, &SOnHangUIProcessExit, this, INFINITE, WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE); ::WaitForSingleObject(mShowEvent, ::IsDebuggerPresent() ? INFINITE : mIPCTimeoutMs); // Setting this to true even if we time out on mShowEvent. This timeout // typically occurs when the machine is thrashing so badly that // plugin-hang-ui.exe is taking a while to start. If we didn't set // this to true, Firefox would keep spawning additional plugin-hang-ui // processes, which is not what we want. mIsShowing = true; } mShowEvent = nullptr; return !(!isProcessCreated); } // static VOID CALLBACK PluginHangUIParent::SOnHangUIProcessExit(PVOID aContext, BOOLEAN aIsTimer) { PluginHangUIParent* object = static_cast(aContext); MutexAutoLock lock(object->mMutex); // If the Hang UI child process died unexpectedly, act as if the UI cancelled if (object->IsShowing()) { object->RecvUserResponse(HANGUI_USER_RESPONSE_CANCEL); // Firefox window was disabled automatically when the Hang UI was shown. // If plugin-hang-ui.exe was unexpectedly terminated, we need to re-enable. ::EnableWindow(object->mMainWindowHandle, TRUE); } } // A precondition for this function is that the caller has locked mMutex bool PluginHangUIParent::UnwatchHangUIChildProcess(bool aWait) { mMutex.AssertCurrentThreadOwns(); if (mRegWait) { // If aWait is false then we want to pass a nullptr (i.e. default // constructor) completionEvent ScopedHandle completionEvent; if (aWait) { completionEvent.Set(::CreateEventW(nullptr, FALSE, FALSE, nullptr)); if (!completionEvent.IsValid()) { return false; } } // if aWait == false and UnregisterWaitEx fails with ERROR_IO_PENDING, // it is okay to clear mRegWait; Windows is telling us that the wait's // callback is running but will be cleaned up once the callback returns. if (::UnregisterWaitEx(mRegWait, completionEvent) || (!aWait && ::GetLastError() == ERROR_IO_PENDING)) { mRegWait = nullptr; if (aWait) { // We must temporarily unlock mMutex while waiting for the registered // wait callback to complete, or else we could deadlock. MutexAutoUnlock unlock(mMutex); ::WaitForSingleObject(completionEvent, INFINITE); } return true; } } return false; } bool PluginHangUIParent::Cancel() { MutexAutoLock lock(mMutex); bool result = mIsShowing && SendCancel(); if (result) { mIsShowing = false; } return result; } bool PluginHangUIParent::SendCancel() { PluginHangUICommand* cmd = nullptr; nsresult rv = mMiniShm.GetWritePtr(cmd); if (NS_FAILED(rv)) { return false; } cmd->mCode = PluginHangUICommand::HANGUI_CMD_CANCEL; return NS_SUCCEEDED(mMiniShm.Send()); } // A precondition for this function is that the caller has locked mMutex bool PluginHangUIParent::RecvUserResponse(const unsigned int& aResponse) { mMutex.AssertCurrentThreadOwns(); if (!mIsShowing && !(aResponse & HANGUI_USER_RESPONSE_CANCEL)) { // Don't process a user response if a cancellation is already pending return true; } mLastUserResponse = aResponse; mResponseTicks = ::GetTickCount(); mIsShowing = false; // responseCode: 1 = Stop, 2 = Continue, 3 = Cancel int responseCode; if (aResponse & HANGUI_USER_RESPONSE_STOP) { // User clicked Stop mModule->TerminateChildProcess(mMainThreadMessageLoop, mozilla::ipc::kInvalidProcessId, "ModalHangUI"_ns, u""_ns); responseCode = 1; } else if (aResponse & HANGUI_USER_RESPONSE_CONTINUE) { mModule->OnHangUIContinue(); // User clicked Continue responseCode = 2; } else { // Dialog was cancelled responseCode = 3; } int dontAskCode = (aResponse & HANGUI_USER_RESPONSE_DONT_SHOW_AGAIN) ? 1 : 0; nsCOMPtr workItem = new nsPluginHangUITelemetry( responseCode, dontAskCode, LastShowDurationMs(), mTimeoutPrefMs); NS_DispatchToMainThread(workItem); return true; } nsresult PluginHangUIParent::GetHangUIOwnerWindowHandle( NativeWindowHandle& windowHandle) { windowHandle = nullptr; nsresult rv; nsCOMPtr winMediator( do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr navWin; rv = winMediator->GetMostRecentWindow(u"navigator:browser", getter_AddRefs(navWin)); NS_ENSURE_SUCCESS(rv, rv); if (!navWin) { return NS_ERROR_FAILURE; } nsPIDOMWindowOuter* win = nsPIDOMWindowOuter::From(navWin); nsCOMPtr widget = WidgetUtils::DOMWindowToWidget(win); if (!widget) { return NS_ERROR_FAILURE; } windowHandle = reinterpret_cast( widget->GetNativeData(NS_NATIVE_WINDOW)); if (!windowHandle) { return NS_ERROR_FAILURE; } return NS_OK; } void PluginHangUIParent::OnMiniShmEvent(MiniShmBase* aMiniShmObj) { const PluginHangUIResponse* response = nullptr; nsresult rv = aMiniShmObj->GetReadPtr(response); NS_ASSERTION(NS_SUCCEEDED(rv), "Couldn't obtain read pointer OnMiniShmEvent"); if (NS_SUCCEEDED(rv)) { // The child process has returned a response so we shouldn't worry about // its state anymore. MutexAutoLock lock(mMutex); UnwatchHangUIChildProcess(false); RecvUserResponse(response->mResponseBits); } } void PluginHangUIParent::OnMiniShmConnect(MiniShmBase* aMiniShmObj) { PluginHangUICommand* cmd = nullptr; nsresult rv = aMiniShmObj->GetWritePtr(cmd); NS_ASSERTION(NS_SUCCEEDED(rv), "Couldn't obtain write pointer OnMiniShmConnect"); if (NS_FAILED(rv)) { return; } cmd->mCode = PluginHangUICommand::HANGUI_CMD_SHOW; if (NS_SUCCEEDED(aMiniShmObj->Send())) { mShowTicks = ::GetTickCount(); } ::SetEvent(mShowEvent); } } // namespace plugins } // namespace mozilla