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