зеркало из https://github.com/mozilla/gecko-dev.git
400 строки
13 KiB
C++
400 строки
13 KiB
C++
/* -*- 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 "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<uint32_t>(aHangUITimeoutPref) * 1000U),
|
|
mIPCTimeoutMs(static_cast<uint32_t>(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<unsigned int>(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<nsIProperties> directoryService(
|
|
do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
|
|
if (!directoryService) {
|
|
return false;
|
|
}
|
|
nsCOMPtr<nsIFile> 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<nsIWinTaskbar> 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<wchar_t*>(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<PluginHangUIParent*>(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,
|
|
NS_LITERAL_CSTRING("ModalHangUI"), EmptyString());
|
|
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<nsIRunnable> workItem = new nsPluginHangUITelemetry(
|
|
responseCode, dontAskCode, LastShowDurationMs(), mTimeoutPrefMs);
|
|
NS_DispatchToMainThread(workItem);
|
|
return true;
|
|
}
|
|
|
|
nsresult PluginHangUIParent::GetHangUIOwnerWindowHandle(
|
|
NativeWindowHandle& windowHandle) {
|
|
windowHandle = nullptr;
|
|
|
|
nsresult rv;
|
|
nsCOMPtr<nsIWindowMediator> winMediator(
|
|
do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<mozIDOMWindowProxy> 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<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(win);
|
|
if (!widget) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
windowHandle = reinterpret_cast<NativeWindowHandle>(
|
|
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
|