Bug 805591 - Win32 implementation of the Plugin Hang UI. r=bsmedberg,bbondy

This commit is contained in:
Aaron Klotz 2013-01-03 22:24:07 -05:00
parent 40dbc4f2f4
commit ea9d8f367c
23 changed files with 2217 additions and 11 deletions

View File

@ -78,6 +78,7 @@
@BINPATH@/@MOZ_CHILD_PROCESS_NAME@
#endif
#ifdef XP_WIN32
@BINPATH@/plugin-hang-ui@BIN_SUFFIX@
#ifndef MOZ_DEBUG
#if MOZ_MSVC_REDIST == 1400
@BINPATH@/Microsoft.VC80.CRT.manifest

View File

@ -78,6 +78,12 @@ PARALLEL_DIRS += \
audiochannel \
$(NULL)
ifeq (WINNT,$(OS_ARCH))
PARALLEL_DIRS += \
plugins/ipc/hangui \
$(NULL)
endif
ifdef MOZ_B2G_RIL
PARALLEL_DIRS += \
telephony \

View File

@ -114,5 +114,9 @@ MutationEventWarning=Use of Mutation Events is deprecated. Use MutationObserver
MozSliceWarning=Use of mozSlice on the Blob object is deprecated. Use slice instead.
# LOCALIZATION NOTE: Do not translate "Components"
ComponentsWarning=The Components object is deprecated. It will soon be removed.
PluginHangUITitle=Warning: Unresponsive plugin
PluginHangUIMessage=%S may be busy, or it may have stopped responding. You can stop the plugin now, or you can continue to see if the plugin will complete.
PluginHangUIWaitButton=Continue
PluginHangUIStopButton=Stop plugin
# LOCALIZATION NOTE: Do not translate "mozHidden", "mozVisibilityState", "hidden", or "visibilityState"
PrefixedVisibilityApiWarning='mozHidden' and 'mozVisibilityState' are deprecated. Please use the unprefixed 'hidden' and 'visibilityState' instead.

View File

@ -94,6 +94,12 @@ ifeq (WINNT,$(OS_ARCH))
CPPSRCS += \
COMMessageFilter.cpp \
PluginSurfaceParent.cpp \
MiniShmParent.cpp \
PluginHangUIParent.cpp \
$(NULL)
DEFINES += \
-DMOZ_HANGUI_PROCESS_NAME=\"plugin-hang-ui$(BIN_SUFFIX)\" \
$(NULL)
EXPORTS_mozilla/plugins += \
@ -121,6 +127,13 @@ LOCAL_INCLUDES = \
-I$(topsrcdir)/xpcom/base/ \
$(NULL)
ifeq (WINNT,$(OS_ARCH))
LOCAL_INCLUDES += \
-I$(srcdir)/hangui \
-I$(topsrcdir)/widget/shared \
$(NULL)
endif
include $(topsrcdir)/config/config.mk
include $(topsrcdir)/ipc/chromium/chromium-config.mk

View File

@ -0,0 +1,219 @@
/* -*- 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 "MiniShmParent.h"
#include "base/scoped_handle.h"
#include <sstream>
namespace mozilla {
namespace plugins {
// static
const unsigned int MiniShmParent::kDefaultMiniShmSectionSize = 0x1000;
MiniShmParent::MiniShmParent()
: mSectionSize(0),
mParentEvent(NULL),
mParentGuard(NULL),
mChildEvent(NULL),
mChildGuard(NULL),
mRegWait(NULL),
mFileMapping(NULL),
mView(nullptr),
mIsConnected(false),
mTimeout(INFINITE)
{
}
MiniShmParent::~MiniShmParent()
{
CleanUp();
}
void
MiniShmParent::CleanUp()
{
if (mRegWait) {
::UnregisterWaitEx(mRegWait, INVALID_HANDLE_VALUE);
mRegWait = NULL;
}
if (mParentEvent) {
::CloseHandle(mParentEvent);
mParentEvent = NULL;
}
if (mParentGuard) {
::CloseHandle(mParentGuard);
mParentGuard = NULL;
}
if (mChildEvent) {
::CloseHandle(mChildEvent);
mChildEvent = NULL;
}
if (mChildGuard) {
::CloseHandle(mChildGuard);
mChildGuard = NULL;
}
if (mView) {
::UnmapViewOfFile(mView);
mView = nullptr;
}
if (mFileMapping) {
::CloseHandle(mFileMapping);
mFileMapping = NULL;
}
}
nsresult
MiniShmParent::Init(MiniShmObserver* aObserver, const DWORD aTimeout,
const unsigned int aSectionSize)
{
if (!aObserver || !aSectionSize || (aSectionSize % 0x1000) || !aTimeout) {
return NS_ERROR_ILLEGAL_VALUE;
}
if (mFileMapping) {
return NS_ERROR_ALREADY_INITIALIZED;
}
SECURITY_ATTRIBUTES securityAttributes = {sizeof(securityAttributes),
nullptr,
TRUE};
ScopedHandle parentEvent(::CreateEvent(&securityAttributes,
FALSE,
FALSE,
nullptr));
if (!parentEvent.IsValid()) {
return NS_ERROR_FAILURE;
}
ScopedHandle parentGuard(::CreateEvent(&securityAttributes,
FALSE,
TRUE,
nullptr));
if (!parentGuard.IsValid()) {
return NS_ERROR_FAILURE;
}
ScopedHandle childEvent(::CreateEvent(&securityAttributes,
FALSE,
FALSE,
nullptr));
if (!childEvent.IsValid()) {
return NS_ERROR_FAILURE;
}
ScopedHandle childGuard(::CreateEvent(&securityAttributes,
FALSE,
TRUE,
nullptr));
if (!childGuard.IsValid()) {
return NS_ERROR_FAILURE;
}
ScopedHandle mapping(::CreateFileMapping(INVALID_HANDLE_VALUE,
&securityAttributes,
PAGE_READWRITE,
0,
aSectionSize,
nullptr));
if (!mapping.IsValid()) {
return NS_ERROR_FAILURE;
}
ScopedMappedFileView view(::MapViewOfFile(mapping,
FILE_MAP_WRITE,
0, 0, 0));
if (!view.IsValid()) {
return NS_ERROR_FAILURE;
}
nsresult rv = SetView(view, aSectionSize, false);
NS_ENSURE_SUCCESS(rv, rv);
MiniShmInit* initStruct = nullptr;
rv = GetWritePtrInternal(initStruct);
NS_ENSURE_SUCCESS(rv, rv);
initStruct->mParentEvent = parentEvent;
initStruct->mParentGuard = parentGuard;
initStruct->mChildEvent = childEvent;
initStruct->mChildGuard = childGuard;
if (!::RegisterWaitForSingleObject(&mRegWait,
parentEvent,
&SOnEvent,
this,
INFINITE,
WT_EXECUTEDEFAULT)) {
return NS_ERROR_FAILURE;
}
mParentEvent = parentEvent.Take();
mParentGuard = parentGuard.Take();
mChildEvent = childEvent.Take();
mChildGuard = childGuard.Take();
mFileMapping = mapping.Take();
mView = view.Take();
mSectionSize = aSectionSize;
SetObserver(aObserver);
mTimeout = aTimeout;
return NS_OK;
}
nsresult
MiniShmParent::GetCookie(std::wstring& cookie)
{
if (!mFileMapping) {
return NS_ERROR_NOT_INITIALIZED;
}
std::wostringstream oss;
oss << mFileMapping;
if (!oss) {
return NS_ERROR_FAILURE;
}
cookie = oss.str();
return NS_OK;
}
nsresult
MiniShmParent::Send()
{
if (!mChildEvent || !mChildGuard) {
return NS_ERROR_NOT_INITIALIZED;
}
if (::WaitForSingleObject(mChildGuard, mTimeout) != WAIT_OBJECT_0) {
return NS_ERROR_FAILURE;
}
if (!::SetEvent(mChildEvent)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
bool
MiniShmParent::IsConnected() const
{
return mIsConnected;
}
void
MiniShmParent::OnEvent()
{
if (mIsConnected) {
MiniShmBase::OnEvent();
} else {
FinalizeConnection();
}
::SetEvent(mParentGuard);
}
void
MiniShmParent::FinalizeConnection()
{
const MiniShmInitComplete* initCompleteStruct = nullptr;
nsresult rv = GetReadPtr(initCompleteStruct);
mIsConnected = NS_SUCCEEDED(rv) && initCompleteStruct->mSucceeded;
if (mIsConnected) {
OnConnect();
}
}
} // namespace plugins
} // namespace mozilla

View File

@ -0,0 +1,96 @@
/* -*- 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/. */
#ifndef mozilla_plugins_MiniShmParent_h
#define mozilla_plugins_MiniShmParent_h
#include "MiniShmBase.h"
#include <string>
namespace mozilla {
namespace plugins {
/**
* This class provides a lightweight shared memory interface for a parent
* process in Win32.
* This code assumes that there is a parent-child relationship between
* processes, as it creates inheritable handles.
* Note that this class is *not* an IPDL actor.
*
* @see MiniShmChild
*/
class MiniShmParent : public MiniShmBase
{
public:
MiniShmParent();
virtual ~MiniShmParent();
static const unsigned int kDefaultMiniShmSectionSize;
/**
* Initialize shared memory on the parent side.
*
* @param aObserver A MiniShmObserver object to receive event notifications.
* @param aTimeout Timeout in milliseconds.
* @param aSectionSize Desired size of the shared memory section. This is
* expected to be a multiple of 0x1000 (4KiB).
* @return nsresult error code
*/
nsresult
Init(MiniShmObserver* aObserver, const DWORD aTimeout,
const unsigned int aSectionSize = kDefaultMiniShmSectionSize);
/**
* Destroys the shared memory section. Useful to explicitly release
* resources if it is known that they won't be needed again.
*/
void
CleanUp();
/**
* Provides a cookie string that should be passed to MiniShmChild
* during its initialization.
*
* @param aCookie A std::wstring variable to receive the cookie.
* @return nsresult error code
*/
nsresult
GetCookie(std::wstring& aCookie);
virtual nsresult
Send() MOZ_OVERRIDE;
bool
IsConnected() const;
protected:
void
OnEvent() MOZ_OVERRIDE;
private:
void
FinalizeConnection();
unsigned int mSectionSize;
HANDLE mParentEvent;
HANDLE mParentGuard;
HANDLE mChildEvent;
HANDLE mChildGuard;
HANDLE mRegWait;
HANDLE mFileMapping;
LPVOID mView;
bool mIsConnected;
DWORD mTimeout;
DISALLOW_COPY_AND_ASSIGN(MiniShmParent);
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_MiniShmParent_h

View File

@ -0,0 +1,348 @@
/* -*- 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/plugins/PluginModuleParent.h"
#include "nsContentUtils.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIFile.h"
#include "nsIProperties.h"
#include "nsIWindowMediator.h"
#include "nsServiceManagerUtils.h"
#include "WidgetUtils.h"
using base::ProcessHandle;
using mozilla::widget::WidgetUtils;
using std::string;
using std::vector;
namespace {
class nsPluginHangUITelemetry : public nsRunnable
{
public:
nsPluginHangUITelemetry(int aResponseCode, int aDontAskCode)
: mResponseCode(aResponseCode),
mDontAskCode(aDontAskCode)
{
}
NS_IMETHOD
Run()
{
mozilla::Telemetry::Accumulate(
mozilla::Telemetry::PLUGIN_HANG_UI_USER_RESPONSE, mResponseCode);
mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLUGIN_HANG_UI_DONT_ASK,
mDontAskCode);
return NS_OK;
}
private:
int mResponseCode;
int mDontAskCode;
};
} // anonymous namespace
namespace mozilla {
namespace plugins {
const DWORD PluginHangUIParent::kTimeout = 5000U;
PluginHangUIParent::PluginHangUIParent(PluginModuleParent* aModule)
: mModule(aModule),
mMainThreadMessageLoop(MessageLoop::current()),
mIsShowing(false),
mLastUserResponse(0),
mHangUIProcessHandle(NULL),
mMainWindowHandle(NULL),
mRegWait(NULL),
mShowEvent(NULL),
mShowTicks(0),
mResponseTicks(0)
{
}
PluginHangUIParent::~PluginHangUIParent()
{
if (mRegWait) {
::UnregisterWaitEx(mRegWait, INVALID_HANDLE_VALUE);
}
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 : kTimeout);
NS_ENSURE_SUCCESS(rv, rv);
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());
nsXPIDLString localizedStr;
const PRUnichar* formatParams[] = { aPluginName.get() };
rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
"PluginHangUIMessage",
formatParams,
localizedStr);
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);
commandLine.AppendLooseValue(procHandleStr.get());
std::wstring ipcCookie;
rv = mMiniShm.GetCookie(ipcCookie);
if (NS_FAILED(rv)) {
return false;
}
commandLine.AppendLooseValue(ipcCookie);
mShowEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
ScopedHandle showEvent(::CreateEvent(NULL, FALSE, FALSE, NULL));
if (!showEvent.IsValid()) {
return false;
}
mShowEvent = showEvent.Get();
STARTUPINFO startupInfo = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION processInfo = { NULL };
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, kTimeout);
}
mShowEvent = NULL;
return !(!isProcessCreated);
}
// static
VOID CALLBACK PluginHangUIParent::SOnHangUIProcessExit(PVOID aContext,
BOOLEAN aIsTimer)
{
PluginHangUIParent* object = static_cast<PluginHangUIParent*>(aContext);
// 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);
}
object->mMiniShm.CleanUp();
}
bool
PluginHangUIParent::Cancel()
{
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());
}
bool
PluginHangUIParent::RecvUserResponse(const unsigned int& aResponse)
{
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);
responseCode = 1;
} else if(aResponse & HANGUI_USER_RESPONSE_CONTINUE) {
// 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);
NS_DispatchToMainThread(workItem, NS_DISPATCH_NORMAL);
return true;
}
nsresult
PluginHangUIParent::GetHangUIOwnerWindowHandle(NativeWindowHandle& windowHandle)
{
windowHandle = NULL;
nsresult rv;
nsCOMPtr<nsIWindowMediator> winMediator(do_GetService(NS_WINDOWMEDIATOR_CONTRACTID,
&rv));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIDOMWindow> navWin;
rv = winMediator->GetMostRecentWindow(NS_LITERAL_STRING("navigator:browser").get(),
getter_AddRefs(navWin));
NS_ENSURE_SUCCESS(rv, rv);
if (!navWin) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(navWin);
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)) {
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;
mIsShowing = NS_SUCCEEDED(aMiniShmObj->Send());
if (mIsShowing) {
mShowTicks = GetTickCount();
}
::SetEvent(mShowEvent);
}
} // namespace plugins
} // namespace mozilla

View File

@ -0,0 +1,152 @@
/* -*- 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/. */
#ifndef mozilla_plugins_PluginHangUIParent_h
#define mozilla_plugins_PluginHangUIParent_h
#include "nsString.h"
#include "base/process.h"
#include "base/process_util.h"
#include "mozilla/plugins/PluginMessageUtils.h"
#include "MiniShmParent.h"
namespace mozilla {
namespace plugins {
class PluginModuleParent;
/**
* This class is responsible for launching and communicating with the
* plugin-hang-ui process.
*
* NOTE: PluginHangUIParent is *not* an IPDL actor! In this case, "Parent"
* is describing the fact that firefox is the parent process to the
* plugin-hang-ui process, which is the PluginHangUIChild.
* PluginHangUIParent and PluginHangUIChild are a matched pair.
* @see PluginHangUIChild
*/
class PluginHangUIParent : public MiniShmObserver
{
public:
PluginHangUIParent(PluginModuleParent* aModule);
virtual ~PluginHangUIParent();
/**
* Spawn the plugin-hang-ui.exe child process and terminate the given
* plugin container process if the user elects to stop the hung plugin.
*
* @param aPluginName Human-readable name of the affected plugin.
* @return true if the plugin hang ui process was successfully launched,
* otherwise false.
*/
bool
Init(const nsString& aPluginName);
/**
* If the Plugin Hang UI is being shown, send a cancel notification to the
* Plugin Hang UI child process.
*
* @return true if the UI was shown and the cancel command was successfully
* sent to the child process, otherwise false.
*/
bool
Cancel();
/**
* Returns whether the Plugin Hang UI is currently being displayed.
*
* @return true if the Plugin Hang UI is showing, otherwise false.
*/
bool
IsShowing() const { return mIsShowing; }
/**
* Returns whether this Plugin Hang UI instance has been shown. Note
* that this does not necessarily mean that the UI is showing right now.
*
* @return true if the Plugin Hang UI has shown, otherwise false.
*/
bool
WasShown() const { return mIsShowing || mLastUserResponse != 0; }
/**
* Returns whether the user checked the "Don't ask me again" checkbox.
*
* @return true if the user does not want to see the Hang UI again.
*/
bool
DontShowAgain() const;
/**
* Returns whether the user clicked stop during the last time that the
* Plugin Hang UI was displayed, if applicable.
*
* @return true if the UI was shown and the user chose to stop the
* plugin, otherwise false
*/
bool
WasLastHangStopped() const;
/**
* @return unsigned int containing the response bits from the last
* time the Plugin Hang UI ran.
*/
unsigned int
LastUserResponse() const { return mLastUserResponse; }
/**
* @return unsigned int containing the number of milliseconds that
* the Plugin Hang UI was displayed before the user responded.
* Returns 0 if the Plugin Hang UI has not been shown or was cancelled.
*/
unsigned int
LastShowDurationMs() const;
virtual void
OnMiniShmEvent(MiniShmBase* aMiniShmObj) MOZ_OVERRIDE;
virtual void
OnMiniShmConnect(MiniShmBase* aMiniShmObj) MOZ_OVERRIDE;
private:
nsresult
GetHangUIOwnerWindowHandle(NativeWindowHandle& windowHandle);
bool
SendCancel();
bool
RecvUserResponse(const unsigned int& aResponse);
static
VOID CALLBACK SOnHangUIProcessExit(PVOID aContext, BOOLEAN aIsTimer);
private:
PluginModuleParent* mModule;
MessageLoop* mMainThreadMessageLoop;
volatile bool mIsShowing;
unsigned int mLastUserResponse;
base::ProcessHandle mHangUIProcessHandle;
NativeWindowHandle mMainWindowHandle;
HANDLE mRegWait;
HANDLE mShowEvent;
DWORD mShowTicks;
DWORD mResponseTicks;
MiniShmParent mMiniShm;
static const DWORD kTimeout;
DISALLOW_COPY_AND_ASSIGN(PluginHangUIParent);
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_PluginHangUIParent_h

View File

@ -35,6 +35,7 @@
#include "prsystem.h"
#ifdef XP_WIN
#include "PluginHangUIParent.h"
#include "mozilla/widget/AudioSession.h"
#endif
#include "sampler.h"
@ -59,6 +60,13 @@ using namespace CrashReporter;
static const char kChildTimeoutPref[] = "dom.ipc.plugins.timeoutSecs";
static const char kParentTimeoutPref[] = "dom.ipc.plugins.parentTimeoutSecs";
static const char kLaunchTimeoutPref[] = "dom.ipc.plugins.processLaunchTimeoutSecs";
#ifdef XP_WIN
static const char kHangUITimeoutPref[] = "dom.ipc.plugins.hangUITimeoutSecs";
static const char kHangUIMinDisplayPref[] = "dom.ipc.plugins.hangUIMinDisplaySecs";
#define CHILD_TIMEOUT_PREF kHangUITimeoutPref
#else
#define CHILD_TIMEOUT_PREF kChildTimeoutPref
#endif
template<>
struct RunnableMethodTraits<mozilla::plugins::PluginModuleParent>
@ -87,7 +95,7 @@ PluginModuleParent::LoadModule(const char* aFilePath)
parent->Open(parent->mSubprocess->GetChannel(),
parent->mSubprocess->GetChildProcessHandle());
TimeoutChanged(kChildTimeoutPref, parent);
TimeoutChanged(CHILD_TIMEOUT_PREF, parent);
#ifdef MOZ_CRASHREPORTER
// If this fails, we're having IPC troubles, and we're doomed anyways.
@ -111,6 +119,9 @@ PluginModuleParent::PluginModuleParent(const char* aFilePath)
, mTaskFactory(this)
#ifdef XP_WIN
, mPluginCpuUsageOnHang()
, mHangUIParent(nullptr)
, mHangUIEnabled(true)
, mIsTimerReset(true)
#endif
#ifdef MOZ_CRASHREPORTER_INJECTOR
, mFlashProcess1(0)
@ -123,6 +134,10 @@ PluginModuleParent::PluginModuleParent(const char* aFilePath)
Preferences::RegisterCallback(TimeoutChanged, kChildTimeoutPref, this);
Preferences::RegisterCallback(TimeoutChanged, kParentTimeoutPref, this);
#ifdef XP_WIN
Preferences::RegisterCallback(TimeoutChanged, kHangUITimeoutPref, this);
Preferences::RegisterCallback(TimeoutChanged, kHangUIMinDisplayPref, this);
#endif
}
PluginModuleParent::~PluginModuleParent()
@ -150,6 +165,15 @@ PluginModuleParent::~PluginModuleParent()
Preferences::UnregisterCallback(TimeoutChanged, kChildTimeoutPref, this);
Preferences::UnregisterCallback(TimeoutChanged, kParentTimeoutPref, this);
#ifdef XP_WIN
Preferences::UnregisterCallback(TimeoutChanged, kHangUITimeoutPref, this);
Preferences::UnregisterCallback(TimeoutChanged, kHangUIMinDisplayPref, this);
if (mHangUIParent) {
delete mHangUIParent;
mHangUIParent = nullptr;
}
#endif
}
#ifdef MOZ_CRASHREPORTER
@ -206,16 +230,29 @@ PluginModuleParent::WriteExtraDataForMinidump(AnnotationTable& notes)
}
#endif // MOZ_CRASHREPORTER
void
PluginModuleParent::SetChildTimeout(const int32_t aChildTimeout)
{
int32_t timeoutMs = (aChildTimeout > 0) ? (1000 * aChildTimeout) :
SyncChannel::kNoTimeout;
SetReplyTimeoutMs(timeoutMs);
}
int
PluginModuleParent::TimeoutChanged(const char* aPref, void* aModule)
{
NS_ASSERTION(NS_IsMainThread(), "Wrong thead!");
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
#ifndef XP_WIN
if (!strcmp(aPref, kChildTimeoutPref)) {
// The timeout value used by the parent for children
int32_t timeoutSecs = Preferences::GetInt(kChildTimeoutPref, 0);
int32_t timeoutMs = (timeoutSecs > 0) ? (1000 * timeoutSecs) :
SyncChannel::kNoTimeout;
static_cast<PluginModuleParent*>(aModule)->SetReplyTimeoutMs(timeoutMs);
static_cast<PluginModuleParent*>(aModule)->SetChildTimeout(timeoutSecs);
#else
if (!strcmp(aPref, kChildTimeoutPref) ||
!strcmp(aPref, kHangUIMinDisplayPref) ||
!strcmp(aPref, kHangUITimeoutPref)) {
static_cast<PluginModuleParent*>(aModule)->EvaluateHangUIState(true);
#endif // XP_WIN
} else if (!strcmp(aPref, kParentTimeoutPref)) {
// The timeout value used by the child for its parent
int32_t timeoutSecs = Preferences::GetInt(kParentTimeoutPref, 0);
@ -308,6 +345,13 @@ GetProcessCpuUsage(const InfallibleTArray<base::ProcessHandle>& processHandles,
}
} // anonymous namespace
void
PluginModuleParent::ExitedCxxStack()
{
FinishHangUI();
}
#endif // #ifdef XP_WIN
#ifdef MOZ_CRASHREPORTER_INJECTOR
@ -334,10 +378,36 @@ CreateFlashMinidump(DWORD processId, ThreadId childThread,
bool
PluginModuleParent::ShouldContinueFromReplyTimeout()
{
#ifdef XP_WIN
if (LaunchHangUI()) {
return true;
}
// If LaunchHangUI returned false then we should proceed with the
// original plugin hang behaviour and kill the plugin container.
FinishHangUI();
#endif // XP_WIN
TerminateChildProcess(MessageLoop::current());
return false;
}
void
PluginModuleParent::TerminateChildProcess(MessageLoop* aMsgLoop)
{
#ifdef MOZ_CRASHREPORTER
CrashReporterParent* crashReporter = CrashReporter();
crashReporter->AnnotateCrashReport(NS_LITERAL_CSTRING("PluginHang"),
NS_LITERAL_CSTRING("1"));
#ifdef XP_WIN
if (mHangUIParent) {
unsigned int hangUIDuration = mHangUIParent->LastShowDurationMs();
if (hangUIDuration) {
nsPrintfCString strHangUIDuration("%u", hangUIDuration);
crashReporter->AnnotateCrashReport(
NS_LITERAL_CSTRING("PluginHangUIDuration"),
strHangUIDuration);
}
}
#endif // XP_WIN
if (crashReporter->GeneratePairedMinidump(this)) {
mPluginDumpID = crashReporter->ChildDumpID();
PLUGIN_LOG_DEBUG(
@ -395,17 +465,128 @@ PluginModuleParent::ShouldContinueFromReplyTimeout()
// this must run before the error notification from the channel,
// or not at all
MessageLoop::current()->PostTask(
FROM_HERE,
mTaskFactory.NewRunnableMethod(
&PluginModuleParent::CleanupFromTimeout));
if (aMsgLoop == MessageLoop::current()) {
aMsgLoop->PostTask(
FROM_HERE,
mTaskFactory.NewRunnableMethod(
&PluginModuleParent::CleanupFromTimeout));
} else {
// If we're posting from a different thread we can't create
// the task via mTaskFactory
aMsgLoop->PostTask(FROM_HERE,
NewRunnableMethod(this,
&PluginModuleParent::CleanupFromTimeout));
}
if (!KillProcess(OtherProcess(), 1, false))
NS_WARNING("failed to kill subprocess!");
return false;
}
#ifdef XP_WIN
void
PluginModuleParent::EvaluateHangUIState(const bool aReset)
{
int32_t minDispSecs = Preferences::GetInt(kHangUIMinDisplayPref, 10);
int32_t autoStopSecs = Preferences::GetInt(kChildTimeoutPref, 0);
int32_t timeoutSecs = 0;
if (autoStopSecs > 0 && autoStopSecs < minDispSecs) {
/* If we're going to automatically terminate the plugin within a
time frame shorter than minDispSecs, there's no point in
showing the hang UI; it would just flash briefly on the screen. */
mHangUIEnabled = false;
} else {
timeoutSecs = Preferences::GetInt(kHangUITimeoutPref, 0);
mHangUIEnabled = timeoutSecs > 0;
}
if (mHangUIEnabled) {
if (aReset) {
mIsTimerReset = true;
SetChildTimeout(timeoutSecs);
return;
} else if (mIsTimerReset) {
/* The Hang UI is being shown, so now we're setting the
timeout to kChildTimeoutPref while we wait for a user
response. ShouldContinueFromReplyTimeout will fire
after (reply timeout / 2) seconds, which is not what
we want. Doubling the timeout value here so that we get
the right result. */
autoStopSecs *= 2;
}
}
mIsTimerReset = false;
SetChildTimeout(autoStopSecs);
}
bool
PluginModuleParent::GetPluginName(nsAString& aPluginName)
{
nsPluginHost* host = nsPluginHost::GetInst();
if (!host) {
return false;
}
nsPluginTag* pluginTag = host->TagForPlugin(mPlugin);
if (!pluginTag) {
return false;
}
CopyUTF8toUTF16(pluginTag->mName, aPluginName);
return true;
}
bool
PluginModuleParent::LaunchHangUI()
{
if (!mHangUIEnabled) {
return false;
}
if (mHangUIParent) {
if (mHangUIParent->IsShowing()) {
// We've already shown the UI but the timeout has expired again.
return false;
}
if (mHangUIParent->DontShowAgain()) {
return !mHangUIParent->WasLastHangStopped();
}
delete mHangUIParent;
mHangUIParent = nullptr;
}
mHangUIParent = new PluginHangUIParent(this);
nsAutoString pluginName;
if (!GetPluginName(pluginName)) {
return false;
}
bool retval = mHangUIParent->Init(pluginName);
if (retval) {
/* Once the UI is shown we switch the timeout over to use
kChildTimeoutPref, allowing us to terminate a hung plugin
after kChildTimeoutPref seconds if the user doesn't respond to
the hang UI. */
EvaluateHangUIState(false);
}
return retval;
}
void
PluginModuleParent::FinishHangUI()
{
if (mHangUIEnabled && mHangUIParent) {
bool needsCancel = mHangUIParent->IsShowing();
// If we're still showing, send a Cancel notification
if (needsCancel) {
mHangUIParent->Cancel();
}
/* If we cancelled the UI or if the user issued a response,
we need to reset the child process timeout. */
if (needsCancel ||
!mIsTimerReset && mHangUIParent->WasShown()) {
/* We changed the timeout to kChildTimeoutPref when the plugin hang
UI was displayed. Now that we're finishing the UI, we need to
switch it back to kHangUITimeoutPref. */
EvaluateHangUIState(true);
}
}
}
#endif // XP_WIN
#ifdef MOZ_CRASHREPORTER
CrashReporterParent*
PluginModuleParent::CrashReporter()

View File

@ -45,6 +45,10 @@ namespace plugins {
class BrowserStreamParent;
#ifdef XP_WIN
class PluginHangUIParent;
#endif
/**
* PluginModuleParent
*
@ -130,6 +134,13 @@ public:
void ProcessRemoteNativeEventsInRPCCall();
void TerminateChildProcess(MessageLoop* aMsgLoop);
#ifdef XP_WIN
void
ExitedCxxStack() MOZ_OVERRIDE;
#endif // XP_WIN
protected:
virtual mozilla::ipc::RPCChannel::RacyRPCPolicy
MediateRPCRace(const Message& parent, const Message& child) MOZ_OVERRIDE
@ -286,6 +297,7 @@ private:
void WriteExtraDataForMinidump(CrashReporter::AnnotationTable& notes);
#endif
void CleanupFromTimeout();
void SetChildTimeout(const int32_t aChildTimeout);
static int TimeoutChanged(const char* aPref, void* aModule);
void NotifyPluginCrashed();
@ -304,6 +316,31 @@ private:
nsString mHangID;
#ifdef XP_WIN
InfallibleTArray<float> mPluginCpuUsageOnHang;
PluginHangUIParent *mHangUIParent;
bool mHangUIEnabled;
bool mIsTimerReset;
void
EvaluateHangUIState(const bool aReset);
bool
GetPluginName(nsAString& aPluginName);
/**
* Launches the Plugin Hang UI.
*
* @return true if plugin-hang-ui.exe has been successfully launched.
* false if the Plugin Hang UI is disabled, already showing,
* or the launch failed.
*/
bool
LaunchHangUI();
/**
* Finishes the Plugin Hang UI and cancels if it is being shown to the user.
*/
void
FinishHangUI();
#endif
#ifdef MOZ_X11

View File

@ -0,0 +1,18 @@
/* -*- 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/. */
#ifndef mozilla_plugins_HangUIDlg_h
#define mozilla_plugins_HangUIDlg_h
#define IDD_HANGUIDLG 102
#define IDC_MSG 1000
#define IDC_CONTINUE 1001
#define IDC_STOP 1002
#define IDC_NOFUTURE 1003
#define IDC_ICON 1004
#endif // mozilla_plugins_HangUIDlg_h

View File

@ -0,0 +1,26 @@
/* 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 "HangUIDlg.h"
#include <windows.h>
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_HANGUIDLG DIALOGEX 0, 0, 400, 75
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
DEFPUSHBUTTON "Continue",IDC_CONTINUE,283,51,50,18
PUSHBUTTON "Stop",IDC_STOP,341,51,50,18
CONTROL "Check1",IDC_NOFUTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,37,32,354,10
LTEXT "Static",IDC_MSG,37,7,353,24
ICON "",IDC_ICON,7,7,20,20
END

View File

@ -0,0 +1,39 @@
# 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/.
DEPTH = @DEPTH@
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
FAIL_ON_WARNINGS := 1
include $(DEPTH)/config/autoconf.mk
CPPSRCS = \
MiniShmChild.cpp \
PluginHangUIChild.cpp \
$(NULL)
PROGRAM = plugin-hang-ui$(BIN_SUFFIX)
OS_LIBS = comctl32.lib
RCINCLUDE = HangUIDlg.rc
include $(topsrcdir)/config/config.mk
DEFINES += \
-DNS_NO_XPCOM \
$(NULL)
STL_FLAGS = \
-D_HAS_EXCEPTIONS=0 \
$(NULL)
MOZ_GLUE_LDFLAGS =
include $(topsrcdir)/ipc/chromium/chromium-config.mk
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,315 @@
/* -*- 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/. */
#ifndef mozilla_plugins_MiniShmBase_h
#define mozilla_plugins_MiniShmBase_h
#include "base/basictypes.h"
#include "nsDebug.h"
#include <windows.h>
namespace mozilla {
namespace plugins {
/**
* This class is used to provide RAII semantics for mapped views.
* @see ScopedHandle
*/
class ScopedMappedFileView
{
public:
explicit
ScopedMappedFileView(LPVOID aView)
: mView(aView)
{
}
~ScopedMappedFileView()
{
Close();
}
void
Close()
{
if (mView) {
::UnmapViewOfFile(mView);
mView = nullptr;
}
}
void
Set(LPVOID aView)
{
Close();
mView = aView;
}
LPVOID
Get() const
{
return mView;
}
LPVOID
Take()
{
LPVOID result = mView;
mView = nullptr;
return result;
}
operator LPVOID()
{
return mView;
}
bool
IsValid() const
{
return (mView);
}
private:
DISALLOW_COPY_AND_ASSIGN(ScopedMappedFileView);
LPVOID mView;
};
class MiniShmBase;
class MiniShmObserver
{
public:
/**
* This function is called whenever there is a new shared memory request.
* @param aMiniShmObj MiniShmBase object that may be used to read and
* write from shared memory.
*/
virtual void OnMiniShmEvent(MiniShmBase *aMiniShmObj) = 0;
/**
* This function is called once when a MiniShmParent and a MiniShmChild
* object have successfully negotiated a connection.
*
* @param aMiniShmObj MiniShmBase object that may be used to read and
* write from shared memory.
*/
virtual void OnMiniShmConnect(MiniShmBase *aMiniShmObj) { }
};
/**
* Base class for MiniShm connections. This class defines the common
* interfaces and code between parent and child.
*/
class MiniShmBase
{
public:
/**
* Obtains a writable pointer into shared memory of type T.
* typename T must be plain-old-data and contain an unsigned integral
* member T::identifier that uniquely identifies T with respect to
* other types used by the protocol being implemented.
*
* @param aPtr Pointer to receive the shared memory address.
* This value is set if and only if the function
* succeeded.
* @return NS_OK if and only if aPtr was successfully obtained.
* NS_ERROR_ILLEGAL_VALUE if type T is not valid for MiniShm.
* NS_ERROR_NOT_INITIALIZED if there is no valid MiniShm connection.
*/
template<typename T> nsresult
GetWritePtr(T*& aPtr)
{
if (!mWriteHeader) {
return NS_ERROR_NOT_INITIALIZED;
}
if (sizeof(T) > mPayloadMaxLen ||
T::identifier <= RESERVED_CODE_LAST) {
return NS_ERROR_ILLEGAL_VALUE;
}
mWriteHeader->mId = T::identifier;
mWriteHeader->mPayloadLen = sizeof(T);
aPtr = reinterpret_cast<T*>(mWriteHeader + 1);
return NS_OK;
}
/**
* Obtains a readable pointer into shared memory of type T.
* typename T must be plain-old-data and contain an unsigned integral
* member T::identifier that uniquely identifies T with respect to
* other types used by the protocol being implemented.
*
* @param aPtr Pointer to receive the shared memory address.
* This value is set if and only if the function
* succeeded.
* @return NS_OK if and only if aPtr was successfully obtained.
* NS_ERROR_ILLEGAL_VALUE if type T is not valid for MiniShm or if
* type T does not match the type of the data
* stored in shared memory.
* NS_ERROR_NOT_INITIALIZED if there is no valid MiniShm connection.
*/
template<typename T> nsresult
GetReadPtr(const T*& aPtr)
{
if (!mReadHeader) {
return NS_ERROR_NOT_INITIALIZED;
}
if (mReadHeader->mId != T::identifier ||
sizeof(T) != mReadHeader->mPayloadLen) {
return NS_ERROR_ILLEGAL_VALUE;
}
aPtr = reinterpret_cast<const T*>(mReadHeader + 1);
return NS_OK;
}
/**
* Fires the peer's event causing its request handler to execute.
*
* @return Should return NS_OK if the send was successful.
*/
virtual nsresult
Send() = 0;
protected:
/**
* MiniShm reserves some identifier codes for its own use. Any
* identifiers used by MiniShm protocol implementations must be
* greater than RESERVED_CODE_LAST.
*/
enum ReservedCodes
{
RESERVED_CODE_INIT = 0,
RESERVED_CODE_INIT_COMPLETE = 1,
RESERVED_CODE_LAST = RESERVED_CODE_INIT_COMPLETE
};
struct MiniShmHeader
{
unsigned int mId;
unsigned int mPayloadLen;
};
struct MiniShmInit
{
enum identifier_t
{
identifier = RESERVED_CODE_INIT
};
HANDLE mParentEvent;
HANDLE mParentGuard;
HANDLE mChildEvent;
HANDLE mChildGuard;
};
struct MiniShmInitComplete
{
enum identifier_t
{
identifier = RESERVED_CODE_INIT_COMPLETE
};
bool mSucceeded;
};
MiniShmBase()
: mObserver(nullptr),
mWriteHeader(nullptr),
mReadHeader(nullptr),
mPayloadMaxLen(0)
{
}
virtual ~MiniShmBase()
{ }
virtual void
OnEvent()
{
if (mObserver) {
mObserver->OnMiniShmEvent(this);
}
}
virtual void
OnConnect()
{
if (mObserver) {
mObserver->OnMiniShmConnect(this);
}
}
nsresult
SetView(LPVOID aView, const unsigned int aSize, bool aIsChild)
{
if (!aView || aSize <= 2 * sizeof(MiniShmHeader)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Divide the region into halves for parent and child
if (aIsChild) {
mReadHeader = static_cast<MiniShmHeader*>(aView);
mWriteHeader = reinterpret_cast<MiniShmHeader*>(static_cast<char*>(aView)
+ aSize / 2U);
} else {
mWriteHeader = static_cast<MiniShmHeader*>(aView);
mReadHeader = reinterpret_cast<MiniShmHeader*>(static_cast<char*>(aView)
+ aSize / 2U);
}
mPayloadMaxLen = aSize / 2U - sizeof(MiniShmHeader);
return NS_OK;
}
inline void
SetObserver(MiniShmObserver *aObserver) { mObserver = aObserver; }
/**
* Obtains a writable pointer into shared memory of type T. This version
* differs from GetWritePtr in that it allows typename T to be one of
* the private data structures declared in MiniShmBase.
*
* @param aPtr Pointer to receive the shared memory address.
* This value is set if and only if the function
* succeeded.
* @return NS_OK if and only if aPtr was successfully obtained.
* NS_ERROR_ILLEGAL_VALUE if type T not an internal MiniShm struct.
* NS_ERROR_NOT_INITIALIZED if there is no valid MiniShm connection.
*/
template<typename T> nsresult
GetWritePtrInternal(T*& aPtr)
{
if (!mWriteHeader) {
return NS_ERROR_NOT_INITIALIZED;
}
if (sizeof(T) > mPayloadMaxLen ||
T::identifier > RESERVED_CODE_LAST) {
return NS_ERROR_ILLEGAL_VALUE;
}
mWriteHeader->mId = T::identifier;
mWriteHeader->mPayloadLen = sizeof(T);
aPtr = reinterpret_cast<T*>(mWriteHeader + 1);
return NS_OK;
}
static VOID CALLBACK
SOnEvent(PVOID aContext, BOOLEAN aIsTimer)
{
MiniShmBase* object = static_cast<MiniShmBase*>(aContext);
object->OnEvent();
}
private:
MiniShmObserver* mObserver;
MiniShmHeader* mWriteHeader;
MiniShmHeader* mReadHeader;
unsigned int mPayloadMaxLen;
DISALLOW_COPY_AND_ASSIGN(MiniShmBase);
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_MiniShmBase_h

View File

@ -0,0 +1,170 @@
/* -*- 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 "MiniShmChild.h"
#include <limits>
#include <sstream>
namespace mozilla {
namespace plugins {
MiniShmChild::MiniShmChild()
: mParentEvent(NULL),
mParentGuard(NULL),
mChildEvent(NULL),
mChildGuard(NULL),
mFileMapping(NULL),
mRegWait(NULL),
mView(nullptr),
mTimeout(INFINITE)
{}
MiniShmChild::~MiniShmChild()
{
if (mRegWait) {
::UnregisterWaitEx(mRegWait, INVALID_HANDLE_VALUE);
}
if (mParentEvent) {
::CloseHandle(mParentEvent);
}
if (mParentGuard) {
::CloseHandle(mParentGuard);
}
if (mChildEvent) {
::CloseHandle(mChildEvent);
}
if (mChildGuard) {
::CloseHandle(mChildGuard);
}
if (mView) {
::UnmapViewOfFile(mView);
}
if (mFileMapping) {
::CloseHandle(mFileMapping);
}
}
nsresult
MiniShmChild::Init(MiniShmObserver* aObserver, const std::wstring& aCookie,
const DWORD aTimeout)
{
if (aCookie.empty() || !aTimeout) {
return NS_ERROR_ILLEGAL_VALUE;
}
if (mFileMapping) {
return NS_ERROR_ALREADY_INITIALIZED;
}
std::wistringstream iss(aCookie);
HANDLE mapHandle = NULL;
iss >> mapHandle;
if (!iss) {
return NS_ERROR_ILLEGAL_VALUE;
}
ScopedMappedFileView view(::MapViewOfFile(mapHandle,
FILE_MAP_WRITE,
0, 0, 0));
if (!view.IsValid()) {
return NS_ERROR_FAILURE;
}
MEMORY_BASIC_INFORMATION memInfo = {0};
SIZE_T querySize = ::VirtualQuery(view, &memInfo, sizeof(memInfo));
unsigned int mappingSize = 0;
if (querySize) {
if (memInfo.RegionSize <= std::numeric_limits<unsigned int>::max()) {
mappingSize = static_cast<unsigned int>(memInfo.RegionSize);
}
}
if (!querySize || !mappingSize) {
return NS_ERROR_FAILURE;
}
nsresult rv = SetView(view, mappingSize, true);
if (NS_FAILED(rv)) {
return rv;
}
const MiniShmInit* initStruct = nullptr;
rv = GetReadPtr(initStruct);
if (NS_FAILED(rv)) {
return rv;
}
if (!initStruct->mParentEvent || !initStruct->mParentGuard ||
!initStruct->mChildEvent || !initStruct->mChildGuard) {
return NS_ERROR_FAILURE;
}
if (!::RegisterWaitForSingleObject(&mRegWait,
initStruct->mChildEvent,
&SOnEvent,
this,
INFINITE,
WT_EXECUTEDEFAULT)) {
return NS_ERROR_FAILURE;
}
MiniShmInitComplete* initCompleteStruct = nullptr;
rv = GetWritePtrInternal(initCompleteStruct);
if (NS_FAILED(rv)) {
::UnregisterWaitEx(mRegWait, INVALID_HANDLE_VALUE);
mRegWait = NULL;
return NS_ERROR_FAILURE;
}
initCompleteStruct->mSucceeded = true;
// We must set the member variables before we signal the event
mFileMapping = mapHandle;
mView = view.Take();
mParentEvent = initStruct->mParentEvent;
mParentGuard = initStruct->mParentGuard;
mChildEvent = initStruct->mChildEvent;
mChildGuard = initStruct->mChildGuard;
SetObserver(aObserver);
mTimeout = aTimeout;
rv = Send();
if (NS_FAILED(rv)) {
initCompleteStruct->mSucceeded = false;
mFileMapping = NULL;
view.Set(mView);
mView = nullptr;
mParentEvent = NULL;
mParentGuard = NULL;
mChildEvent = NULL;
mChildGuard = NULL;
::UnregisterWaitEx(mRegWait, INVALID_HANDLE_VALUE);
mRegWait = NULL;
return rv;
}
OnConnect();
return NS_OK;
}
nsresult
MiniShmChild::Send()
{
if (!mParentEvent || !mParentGuard) {
return NS_ERROR_NOT_INITIALIZED;
}
if (::WaitForSingleObject(mParentGuard, mTimeout) != WAIT_OBJECT_0) {
return NS_ERROR_FAILURE;
}
if (!::SetEvent(mParentEvent)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
void
MiniShmChild::OnEvent()
{
MiniShmBase::OnEvent();
::SetEvent(mChildGuard);
}
} // namespace plugins
} // namespace mozilla

View File

@ -0,0 +1,68 @@
/* -*- 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/. */
#ifndef mozilla_plugins_MiniShmChild_h
#define mozilla_plugins_MiniShmChild_h
#include "MiniShmBase.h"
#include <string>
namespace mozilla {
namespace plugins {
/**
* This class provides a lightweight shared memory interface for a child
* process in Win32.
* This code assumes that there is a parent-child relationship between
* processes, as it inherits handles from the parent process.
* Note that this class is *not* an IPDL actor.
*
* @see MiniShmParent
*/
class MiniShmChild : public MiniShmBase
{
public:
MiniShmChild();
virtual ~MiniShmChild();
/**
* Initialize shared memory on the child side.
*
* @param aObserver A MiniShmObserver object to receive event notifications.
* @param aCookie Cookie obtained from MiniShmParent::GetCookie
* @param aTimeout Timeout in milliseconds.
* @return nsresult error code
*/
nsresult
Init(MiniShmObserver* aObserver, const std::wstring& aCookie,
const DWORD aTimeout);
virtual nsresult
Send() MOZ_OVERRIDE;
protected:
void
OnEvent() MOZ_OVERRIDE;
private:
HANDLE mParentEvent;
HANDLE mParentGuard;
HANDLE mChildEvent;
HANDLE mChildGuard;
HANDLE mFileMapping;
HANDLE mRegWait;
LPVOID mView;
DWORD mTimeout;
DISALLOW_COPY_AND_ASSIGN(MiniShmChild);
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_MiniShmChild_h

View File

@ -0,0 +1,54 @@
/* -*- 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/. */
#ifndef mozilla_plugins_PluginHangUI_h
#define mozilla_plugins_PluginHangUI_h
namespace mozilla {
namespace plugins {
enum HangUIUserResponse
{
HANGUI_USER_RESPONSE_CANCEL = 1,
HANGUI_USER_RESPONSE_CONTINUE = 2,
HANGUI_USER_RESPONSE_STOP = 4,
HANGUI_USER_RESPONSE_DONT_SHOW_AGAIN = 8
};
enum PluginHangUIStructID
{
PLUGIN_HANGUI_COMMAND = 0x10,
PLUGIN_HANGUI_RESULT
};
struct PluginHangUICommand
{
enum
{
identifier = PLUGIN_HANGUI_COMMAND
};
enum CmdCode
{
HANGUI_CMD_SHOW = 1,
HANGUI_CMD_CANCEL = 2
};
CmdCode mCode;
};
struct PluginHangUIResponse
{
enum
{
identifier = PLUGIN_HANGUI_RESULT
};
unsigned int mResponseBits;
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_PluginHangUI_h

View File

@ -0,0 +1,289 @@
/* -*- 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 "PluginHangUIChild.h"
#include "HangUIDlg.h"
#include <assert.h>
#include <commctrl.h>
#include <windowsx.h>
#include <sstream>
namespace mozilla {
namespace plugins {
PluginHangUIChild* PluginHangUIChild::sSelf = nullptr;
const int PluginHangUIChild::kExpectedMinimumArgc = 9;
const DWORD PluginHangUIChild::kProcessTimeout = 1200000U;
const DWORD PluginHangUIChild::kShmTimeout = 5000U;
PluginHangUIChild::PluginHangUIChild()
: mResponseBits(0),
mParentWindow(NULL),
mDlgHandle(NULL),
mMainThread(NULL),
mParentProcess(NULL),
mRegWaitProcess(NULL)
{
}
PluginHangUIChild::~PluginHangUIChild()
{
if (mMainThread) {
CloseHandle(mMainThread);
}
if (mRegWaitProcess) {
UnregisterWaitEx(mRegWaitProcess, INVALID_HANDLE_VALUE);
}
if (mParentProcess) {
CloseHandle(mParentProcess);
}
sSelf = nullptr;
}
bool
PluginHangUIChild::Init(int aArgc, wchar_t* aArgv[])
{
if (aArgc < kExpectedMinimumArgc) {
return false;
}
unsigned int i = 1;
mMessageText = aArgv[i];
mWindowTitle = aArgv[++i];
mWaitBtnText = aArgv[++i];
mKillBtnText = aArgv[++i];
mNoFutureText = aArgv[++i];
std::wistringstream issHwnd(aArgv[++i]);
issHwnd >> reinterpret_cast<HANDLE&>(mParentWindow);
if (!issHwnd) {
return false;
}
std::wistringstream issProc(aArgv[++i]);
issProc >> mParentProcess;
if (!issProc) {
return false;
}
nsresult rv = mMiniShm.Init(this,
std::wstring(aArgv[++i]),
IsDebuggerPresent() ? INFINITE : kShmTimeout);
if (NS_FAILED(rv)) {
return false;
}
sSelf = this;
return true;
}
void
PluginHangUIChild::OnMiniShmEvent(MiniShmBase* aMiniShmObj)
{
const PluginHangUICommand* cmd = nullptr;
nsresult rv = aMiniShmObj->GetReadPtr(cmd);
assert(NS_SUCCEEDED(rv));
bool returnStatus = false;
if (NS_SUCCEEDED(rv)) {
switch (cmd->mCode) {
case PluginHangUICommand::HANGUI_CMD_SHOW:
returnStatus = RecvShow();
break;
case PluginHangUICommand::HANGUI_CMD_CANCEL:
returnStatus = RecvCancel();
break;
default:
break;
}
}
}
// static
INT_PTR CALLBACK
PluginHangUIChild::SHangUIDlgProc(HWND aDlgHandle, UINT aMsgCode,
WPARAM aWParam, LPARAM aLParam)
{
PluginHangUIChild *self = PluginHangUIChild::sSelf;
if (self) {
return self->HangUIDlgProc(aDlgHandle, aMsgCode, aWParam, aLParam);
}
return FALSE;
}
INT_PTR
PluginHangUIChild::HangUIDlgProc(HWND aDlgHandle, UINT aMsgCode, WPARAM aWParam,
LPARAM aLParam)
{
mDlgHandle = aDlgHandle;
switch (aMsgCode) {
case WM_INITDIALOG: {
// Disentangle our input queue from the hung Firefox process
AttachThreadInput(GetCurrentThreadId(),
GetWindowThreadProcessId(mParentWindow, nullptr),
FALSE);
// Register a wait on the Firefox process so that we will be informed
// if it dies while the dialog is showing
RegisterWaitForSingleObject(&mRegWaitProcess,
mParentProcess,
&SOnParentProcessExit,
this,
INFINITE,
WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE);
SetWindowText(aDlgHandle, mWindowTitle);
SetDlgItemText(aDlgHandle, IDC_MSG, mMessageText);
SetDlgItemText(aDlgHandle, IDC_NOFUTURE, mNoFutureText);
SetDlgItemText(aDlgHandle, IDC_CONTINUE, mWaitBtnText);
SetDlgItemText(aDlgHandle, IDC_STOP, mKillBtnText);
HANDLE icon = LoadImage(NULL, IDI_QUESTION, IMAGE_ICON, 0, 0,
LR_DEFAULTSIZE | LR_SHARED);
if (icon) {
SendDlgItemMessage(aDlgHandle, IDC_ICON, STM_SETICON, (WPARAM)icon, 0);
}
return TRUE;
}
case WM_CLOSE: {
mResponseBits |= HANGUI_USER_RESPONSE_CANCEL;
EndDialog(aDlgHandle, 0);
return TRUE;
}
case WM_COMMAND: {
switch (LOWORD(aWParam)) {
case IDC_CONTINUE:
if (HIWORD(aWParam) == BN_CLICKED) {
mResponseBits |= HANGUI_USER_RESPONSE_CONTINUE;
EndDialog(aDlgHandle, 1);
return TRUE;
}
break;
case IDC_STOP:
if (HIWORD(aWParam) == BN_CLICKED) {
mResponseBits |= HANGUI_USER_RESPONSE_STOP;
EndDialog(aDlgHandle, 1);
return TRUE;
}
break;
case IDC_NOFUTURE:
if (HIWORD(aWParam) == BN_CLICKED) {
if (Button_GetCheck(GetDlgItem(aDlgHandle,
IDC_NOFUTURE)) == BST_CHECKED) {
mResponseBits |= HANGUI_USER_RESPONSE_DONT_SHOW_AGAIN;
} else {
mResponseBits &=
~static_cast<DWORD>(HANGUI_USER_RESPONSE_DONT_SHOW_AGAIN);
}
return TRUE;
}
default:
break;
}
return FALSE;
}
default:
return FALSE;
}
}
// static
VOID CALLBACK
PluginHangUIChild::SOnParentProcessExit(PVOID aObject, BOOLEAN aIsTimer)
{
// Simulate a cancel if the parent process died
PluginHangUIChild* object = static_cast<PluginHangUIChild*>(aObject);
object->RecvCancel();
}
bool
PluginHangUIChild::RecvShow()
{
return (QueueUserAPC(&ShowAPC,
mMainThread,
reinterpret_cast<ULONG_PTR>(this)));
}
bool
PluginHangUIChild::Show()
{
INT_PTR dlgResult = DialogBox(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDD_HANGUIDLG),
mParentWindow,
&SHangUIDlgProc);
mDlgHandle = NULL;
assert(dlgResult != -1);
bool result = false;
if (dlgResult != -1) {
PluginHangUIResponse* response = nullptr;
nsresult rv = mMiniShm.GetWritePtr(response);
if (NS_SUCCEEDED(rv)) {
response->mResponseBits = mResponseBits;
result = NS_SUCCEEDED(mMiniShm.Send());
}
}
return result;
}
// static
VOID CALLBACK
PluginHangUIChild::ShowAPC(ULONG_PTR aContext)
{
PluginHangUIChild* object = reinterpret_cast<PluginHangUIChild*>(aContext);
object->Show();
}
bool
PluginHangUIChild::RecvCancel()
{
if (mDlgHandle) {
PostMessage(mDlgHandle, WM_CLOSE, 0, 0);
}
return true;
}
bool
PluginHangUIChild::WaitForDismissal()
{
if (!SetMainThread()) {
return false;
}
DWORD waitResult = WaitForSingleObjectEx(mParentProcess,
kProcessTimeout,
TRUE);
return waitResult == WAIT_OBJECT_0 ||
waitResult == WAIT_IO_COMPLETION;
}
bool
PluginHangUIChild::SetMainThread()
{
if (mMainThread) {
CloseHandle(mMainThread);
mMainThread = NULL;
}
mMainThread = OpenThread(THREAD_SET_CONTEXT,
FALSE,
GetCurrentThreadId());
return !(!mMainThread);
}
} // namespace plugins
} // namespace mozilla
int
wmain(int argc, wchar_t *argv[])
{
INITCOMMONCONTROLSEX icc = { sizeof(INITCOMMONCONTROLSEX),
ICC_STANDARD_CLASSES };
if (!InitCommonControlsEx(&icc)) {
return 1;
}
mozilla::plugins::PluginHangUIChild hangui;
if (!hangui.Init(argc, argv)) {
return 1;
}
if (!hangui.WaitForDismissal()) {
return 1;
}
return 0;
}

View File

@ -0,0 +1,109 @@
/* -*- 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/. */
#ifndef mozilla_plugins_PluginHangUIChild_h
#define mozilla_plugins_PluginHangUIChild_h
#include "MiniShmChild.h"
#include <string>
#include <windows.h>
namespace mozilla {
namespace plugins {
/**
* This class implements the plugin-hang-ui.
*
* NOTE: PluginHangUIChild is *not* an IPDL actor! In this case, "Child"
* is describing the fact that plugin-hang-ui is a child process to the
* firefox process, which is the PluginHangUIParent.
* PluginHangUIParent and PluginHangUIChild are a matched pair.
* @see PluginHangUIParent
*/
class PluginHangUIChild : public MiniShmObserver
{
public:
PluginHangUIChild();
virtual ~PluginHangUIChild();
bool
Init(int aArgc, wchar_t* aArgv[]);
/**
* Displays the Plugin Hang UI and does not return until the UI has
* been dismissed.
*
* @return true if the UI was displayed and the user response was
* successfully sent back to the parent. Otherwise false.
*/
bool
Show();
/**
* Causes the calling thread to wait either for the Hang UI to be
* dismissed or for the parent process to terminate. This should
* be called by the main thread.
*
* @return true unless there was an error initiating the wait
*/
bool
WaitForDismissal();
virtual void
OnMiniShmEvent(MiniShmBase* aMiniShmObj) MOZ_OVERRIDE;
private:
bool
RecvShow();
bool
RecvCancel();
bool
SetMainThread();
INT_PTR
HangUIDlgProc(HWND aDlgHandle, UINT aMsgCode, WPARAM aWParam, LPARAM aLParam);
static VOID CALLBACK
ShowAPC(ULONG_PTR aContext);
static INT_PTR CALLBACK
SHangUIDlgProc(HWND aDlgHandle, UINT aMsgCode, WPARAM aWParam,
LPARAM aLParam);
static VOID CALLBACK
SOnParentProcessExit(PVOID aObject, BOOLEAN aIsTimer);
static PluginHangUIChild *sSelf;
const wchar_t* mMessageText;
const wchar_t* mWindowTitle;
const wchar_t* mWaitBtnText;
const wchar_t* mKillBtnText;
const wchar_t* mNoFutureText;
unsigned int mResponseBits;
HWND mParentWindow;
HWND mDlgHandle;
HANDLE mMainThread;
HANDLE mParentProcess;
HANDLE mRegWaitProcess;
MiniShmChild mMiniShm;
static const int kExpectedMinimumArgc;
static const DWORD kProcessTimeout;
static const DWORD kShmTimeout;
DISALLOW_COPY_AND_ASSIGN(PluginHangUIChild);
};
} // namespace plugins
} // namespace mozilla
#endif // mozilla_plugins_PluginHangUIChild_h

View File

@ -0,0 +1,6 @@
WIN32_MODULE_COMPANYNAME=Mozilla Corporation
WIN32_MODULE_PRODUCTVERSION=@MOZ_APP_WINVERSION@
WIN32_MODULE_PRODUCTVERSION_STRING=@MOZ_APP_VERSION@
WIN32_MODULE_DESCRIPTION=Plugin Hang UI for @MOZ_APP_DISPLAYNAME@
WIN32_MODULE_PRODUCTNAME=@MOZ_APP_DISPLAYNAME@
WIN32_MODULE_NAME=@MOZ_APP_DISPLAYNAME@

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="plugin-hang-ui"
type="win32"
/>
<description>Firefox Plugin Hang User Interface</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
<ms_asmv3:security>
<ms_asmv3:requestedPrivileges>
<ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</ms_asmv3:requestedPrivileges>
</ms_asmv3:security>
</ms_asmv3:trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
</assembly>

View File

@ -1755,11 +1755,22 @@ pref("dom.ipc.plugins.parentTimeoutSecs", 0);
// How long a plugin launch is allowed to take before
// we consider it failed.
pref("dom.ipc.plugins.processLaunchTimeoutSecs", 45);
#ifdef XP_WIN
// How long a plugin is allowed to process a synchronous IPC message
// before we display the plugin hang UI
pref("dom.ipc.plugins.hangUITimeoutSecs", 5);
// Minimum time that the plugin hang UI will be displayed
pref("dom.ipc.plugins.hangUIMinDisplaySecs", 10);
#endif
#else
// No timeout in DEBUG builds
pref("dom.ipc.plugins.timeoutSecs", 0);
pref("dom.ipc.plugins.processLaunchTimeoutSecs", 0);
pref("dom.ipc.plugins.parentTimeoutSecs", 0);
#ifdef XP_WIN
pref("dom.ipc.plugins.hangUITimeoutSecs", 0);
pref("dom.ipc.plugins.hangUIMinDisplaySecs", 0);
#endif
#endif
#ifdef XP_WIN

View File

@ -1463,6 +1463,15 @@
"n_buckets": 10,
"description": "Time spent checking if Java is enabled (ms)"
},
"PLUGIN_HANG_UI_USER_RESPONSE": {
"kind": "enumerated",
"n_values": 3,
"description": "User response to Plugin Hang UI"
},
"PLUGIN_HANG_UI_DONT_ASK": {
"kind": "boolean",
"description": "Whether the user has requested not to see the Plugin Hang UI again"
},
"PLUGIN_SHUTDOWN_MS": {
"kind": "exponential",
"high": "5000",