From f34ed66b93a0b42a58314af2784eee879a297ed5 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Wed, 22 Feb 2017 13:39:46 -0500 Subject: [PATCH] metrics: Record handler lifetime milestone events It could be useful to put our existing Crashpad.HandlerCrashed metrics into context by getting a sense of handler starts, clean exits, and other types of exits. BUG=crashpad:100 Change-Id: I8982075158ea6d210eb2ddad678302e339a42192 Reviewed-on: https://chromium-review.googlesource.com/444124 Reviewed-by: Scott Graham --- handler/handler_main.cc | 229 ++++++++++++++++++------- util/misc/metrics.cc | 8 + util/misc/metrics.h | 28 +++ util/thread/thread_posix.cc | 10 +- util/thread/thread_win.cc | 4 +- util/util.gyp | 3 + util/util_test.gyp | 2 + util/win/scoped_handle.cc | 2 +- util/win/session_end_watcher.cc | 243 +++++++++++++++++++++++++++ util/win/session_end_watcher.h | 79 +++++++++ util/win/session_end_watcher_test.cc | 62 +++++++ 11 files changed, 601 insertions(+), 69 deletions(-) create mode 100644 util/win/session_end_watcher.cc create mode 100644 util/win/session_end_watcher.h create mode 100644 util/win/session_end_watcher_test.cc diff --git a/handler/handler_main.cc b/handler/handler_main.cc index 97322576..81d3de86 100644 --- a/handler/handler_main.cc +++ b/handler/handler_main.cc @@ -24,8 +24,10 @@ #include #include #include +#include #include "base/auto_reset.h" +#include "base/compiler_specific.h" #include "base/files/file_path.h" #include "base/files/scoped_file.h" #include "base/logging.h" @@ -64,6 +66,7 @@ #include "util/win/exception_handler_server.h" #include "util/win/handle.h" #include "util/win/initial_client_data.h" +#include "util/win/session_end_watcher.h" #endif // OS_MACOSX namespace crashpad { @@ -108,11 +111,65 @@ void Usage(const base::FilePath& me) { ToolSupport::UsageTail(me); } +// Calls Metrics::HandlerLifetimeMilestone, but only on the first call. This is +// to prevent multiple exit events from inadvertently being recorded, which +// might happen if a crash occurs during destruction in what would otherwise be +// a normal exit, or if a CallMetricsRecordNormalExit object is destroyed after +// something else logs an exit event. +void MetricsRecordExit(Metrics::LifetimeMilestone milestone) { + static bool once = [](Metrics::LifetimeMilestone milestone) { + Metrics::HandlerLifetimeMilestone(milestone); + return true; + }(milestone); + ALLOW_UNUSED_LOCAL(once); +} + +// Calls MetricsRecordExit() to record a failure, and returns EXIT_FAILURE for +// the convenience of callers in main() which can simply write “return +// ExitFailure();”. +int ExitFailure() { + MetricsRecordExit(Metrics::LifetimeMilestone::kFailed); + return EXIT_FAILURE; +} + +class CallMetricsRecordNormalExit { + public: + CallMetricsRecordNormalExit() {} + ~CallMetricsRecordNormalExit() { + MetricsRecordExit(Metrics::LifetimeMilestone::kExitedNormally); + } + + private: + DISALLOW_COPY_AND_ASSIGN(CallMetricsRecordNormalExit); +}; + #if defined(OS_MACOSX) -struct sigaction g_original_crash_sigaction[NSIG]; +void InstallSignalHandler(const std::vector& signals, + void (*handler)(int, siginfo_t*, void*)) { + struct sigaction sa = {}; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = handler; + + for (int sig : signals) { + int rv = sigaction(sig, &sa, nullptr); + PCHECK(rv == 0) << "sigaction " << sig; + } +} + +void RestoreDefaultSignalHandler(int sig) { + struct sigaction sa = {}; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = SIG_DFL; + int rv = sigaction(sig, &sa, nullptr); + DPLOG_IF(ERROR, rv != 0) << "sigaction " << sig; +} void HandleCrashSignal(int sig, siginfo_t* siginfo, void* context) { + MetricsRecordExit(Metrics::LifetimeMilestone::kCrashed); + // Is siginfo->si_code useful? The only interesting values on macOS are 0 (not // useful, signals generated asynchronously such as by kill() or raise()) and // small positive numbers (useful, signal generated via a hardware fault). The @@ -144,22 +201,17 @@ void HandleCrashSignal(int sig, siginfo_t* siginfo, void* context) { } Metrics::HandlerCrashed(metrics_code); - // Restore the previous signal handler. - DCHECK_GT(sig, 0); - DCHECK_LT(static_cast(sig), arraysize(g_original_crash_sigaction)); - struct sigaction* osa = &g_original_crash_sigaction[sig]; - int rv = sigaction(sig, osa, nullptr); - DPLOG_IF(ERROR, rv != 0) << "sigaction " << sig; + RestoreDefaultSignalHandler(sig); // If the signal was received synchronously resulting from a hardware fault, // returning from the signal handler will cause the kernel to re-raise it, // because this handler hasn’t done anything to alleviate the condition that - // caused the signal to be raised in the first place. With the old signal - // handler in place (expected to be SIG_DFL), it will cause the same behavior - // to be taken as though this signal handler had never been installed at all - // (expected to be a crash). This is ideal, because the signal is re-raised - // with the same properties and from the same context that initially triggered - // it, providing the best debugging experience. + // caused the signal to be raised in the first place. With the default signal + // handler in place, it will cause the same behavior to be taken as though + // this signal handler had never been installed at all (expected to be a + // crash). This is ideal, because the signal is re-raised with the same + // properties and from the same context that initially triggered it, providing + // the best debugging experience. if ((sig != SIGILL && sig != SIGFPE && sig != SIGBUS && sig != SIGSEGV) || !si_code_valid) { @@ -167,8 +219,7 @@ void HandleCrashSignal(int sig, siginfo_t* siginfo, void* context) { // asynchronously via kill() and raise(), and those arising via hardware // traps such as int3 (resulting in SIGTRAP but advancing the instruction // pointer), will not reoccur on their own when returning from the signal - // handler. Re-raise them or call to the previous signal handler as - // appropriate. + // handler. Re-raise them. // // Unfortunately, when SIGBUS is received asynchronously via kill(), // siginfo->si_code makes it appear as though it was actually received via a @@ -181,49 +232,66 @@ void HandleCrashSignal(int sig, siginfo_t* siginfo, void* context) { // very valuable for debugging and are visible to a Mach exception handler. // Since SIGBUS is normally received synchronously in response to a hardware // fault, don’t sweat the unexpected asynchronous case. - if (osa->sa_handler == SIG_DFL) { - // Because this signal handler executes with the signal blocked, this - // raise() cannot immediately deliver the signal. Delivery is deferred - // until this signal handler returns and the signal becomes unblocked. The - // re-raised signal will appear with the same context as where it was - // initially triggered. - rv = raise(sig); - DPLOG_IF(ERROR, rv != 0) << "raise"; - } else if (osa->sa_handler != SIG_IGN) { - if (osa->sa_flags & SA_SIGINFO) { - osa->sa_sigaction(sig, siginfo, context); - } else { - osa->sa_handler(sig); - } - } + // + // Because this signal handler executes with the signal blocked, this + // raise() cannot immediately deliver the signal. Delivery is deferred until + // this signal handler returns and the signal becomes unblocked. The + // re-raised signal will appear with the same context as where it was + // initially triggered. + int rv = raise(sig); + DPLOG_IF(ERROR, rv != 0) << "raise"; } } -void InstallCrashHandler() { - struct sigaction sa = {}; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_SIGINFO; - sa.sa_sigaction = HandleCrashSignal; +void HandleTerminateSignal(int sig, siginfo_t* siginfo, void* context) { + MetricsRecordExit(Metrics::LifetimeMilestone::kTerminated); + RestoreDefaultSignalHandler(sig); + + // Re-raise the signal. See the explanation in HandleCrashSignal(). Note that + // no checks for signals arising from synchronous hardware faults are made + // because termination signals never originate in that way. + int rv = raise(sig); + DPLOG_IF(ERROR, rv != 0) << "raise"; +} + +void InstallCrashHandler() { // These are the core-generating signals from 10.12.3 // xnu-3789.41.3/bsd/sys/signalvar.h sigprop: entries with SA_CORE are in the // set. - const int kSignals[] = {SIGQUIT, - SIGILL, - SIGTRAP, - SIGABRT, - SIGEMT, - SIGFPE, - SIGBUS, - SIGSEGV, - SIGSYS}; + const int kCrashSignals[] = {SIGQUIT, + SIGILL, + SIGTRAP, + SIGABRT, + SIGEMT, + SIGFPE, + SIGBUS, + SIGSEGV, + SIGSYS}; + InstallSignalHandler( + std::vector(&kCrashSignals[0], + &kCrashSignals[arraysize(kCrashSignals)]), + HandleCrashSignal); - for (int sig : kSignals) { - DCHECK_GT(sig, 0); - DCHECK_LT(static_cast(sig), arraysize(g_original_crash_sigaction)); - int rv = sigaction(sig, &sa, &g_original_crash_sigaction[sig]); - PCHECK(rv == 0) << "sigaction " << sig; - } + // Not a crash handler, but close enough. These are non-core-generating but + // terminating signals from 10.12.3 xnu-3789.41.3/bsd/sys/signalvar.h sigprop: + // entries with SA_KILL but not SA_CORE are in the set. SIGKILL is excluded + // because it is uncatchable. + const int kTerminateSignals[] = {SIGHUP, + SIGINT, + SIGPIPE, + SIGALRM, + SIGTERM, + SIGXCPU, + SIGXFSZ, + SIGVTALRM, + SIGPROF, + SIGUSR1, + SIGUSR2}; + InstallSignalHandler( + std::vector(&kTerminateSignals[0], + &kTerminateSignals[arraysize(kTerminateSignals)]), + HandleTerminateSignal); } struct ResetSIGTERMTraits { @@ -243,6 +311,9 @@ ExceptionHandlerServer* g_exception_handler_server; // This signal handler is only operative when being run from launchd. void HandleSIGTERM(int sig, siginfo_t* siginfo, void* context) { + // Don’t call MetricsRecordExit(). This is part of the normal exit path when + // running from launchd. + DCHECK(g_exception_handler_server); g_exception_handler_server->Stop(); } @@ -252,6 +323,7 @@ void HandleSIGTERM(int sig, siginfo_t* siginfo, void* context) { LONG(WINAPI* g_original_exception_filter)(EXCEPTION_POINTERS*) = nullptr; LONG WINAPI UnhandledExceptionHandler(EXCEPTION_POINTERS* exception_pointers) { + MetricsRecordExit(Metrics::LifetimeMilestone::kCrashed); Metrics::HandlerCrashed(exception_pointers->ExceptionRecord->ExceptionCode); if (g_original_exception_filter) @@ -260,9 +332,36 @@ LONG WINAPI UnhandledExceptionHandler(EXCEPTION_POINTERS* exception_pointers) { return EXCEPTION_CONTINUE_SEARCH; } +// Handles events like Control-C and Control-Break on a console. +BOOL WINAPI ConsoleHandler(DWORD console_event) { + MetricsRecordExit(Metrics::LifetimeMilestone::kTerminated); + return false; +} + +// Handles a WM_ENDSESSION message sent when the user session is ending. +class TerminateHandler final : public SessionEndWatcher { + public: + TerminateHandler() : SessionEndWatcher() {} + ~TerminateHandler() override {} + + private: + // SessionEndWatcher: + void SessionEnding() override { + MetricsRecordExit(Metrics::LifetimeMilestone::kTerminated); + } + + DISALLOW_COPY_AND_ASSIGN(TerminateHandler); +}; + void InstallCrashHandler() { g_original_exception_filter = SetUnhandledExceptionFilter(&UnhandledExceptionHandler); + + // These are termination handlers, not crash handlers, but that’s close + // enough. Note that destroying the TerminateHandler would wait for its thread + // to exit, which isn’t necessary or desirable. + SetConsoleCtrlHandler(ConsoleHandler, true); + static TerminateHandler* terminate_handler = new TerminateHandler(); } #endif // OS_MACOSX @@ -271,6 +370,7 @@ void InstallCrashHandler() { int HandlerMain(int argc, char* argv[]) { InstallCrashHandler(); + CallMetricsRecordNormalExit metrics_record_normal_exit; const base::FilePath argv0( ToolSupport::CommandLineArgumentToFilePathStringType(argv[0])); @@ -367,7 +467,7 @@ int HandlerMain(int argc, char* argv[]) { std::string value; if (!SplitStringFirst(optarg, '=', &key, &value)) { ToolSupport::UsageHint(me, "--annotation requires KEY=VALUE"); - return EXIT_FAILURE; + return ExitFailure(); } std::string old_value; if (!MapInsertOrReplace(&options.annotations, key, value, &old_value)) { @@ -386,7 +486,7 @@ int HandlerMain(int argc, char* argv[]) { options.handshake_fd < 0) { ToolSupport::UsageHint(me, "--handshake-fd requires a file descriptor"); - return EXIT_FAILURE; + return ExitFailure(); } break; } @@ -399,7 +499,7 @@ int HandlerMain(int argc, char* argv[]) { if (!options.initial_client_data.InitializeFromString(optarg)) { ToolSupport::UsageHint( me, "failed to parse --initial-client-data"); - return EXIT_FAILURE; + return ExitFailure(); } break; } @@ -433,15 +533,17 @@ int HandlerMain(int argc, char* argv[]) { } case kOptionHelp: { Usage(me); + MetricsRecordExit(Metrics::LifetimeMilestone::kExitedEarly); return EXIT_SUCCESS; } case kOptionVersion: { ToolSupport::Version(me); + MetricsRecordExit(Metrics::LifetimeMilestone::kExitedEarly); return EXIT_SUCCESS; } default: { ToolSupport::UsageHint(me, nullptr); - return EXIT_FAILURE; + return ExitFailure(); } } } @@ -451,34 +553,34 @@ int HandlerMain(int argc, char* argv[]) { #if defined(OS_MACOSX) if (options.handshake_fd < 0 && options.mach_service.empty()) { ToolSupport::UsageHint(me, "--handshake-fd or --mach-service is required"); - return EXIT_FAILURE; + return ExitFailure(); } if (options.handshake_fd >= 0 && !options.mach_service.empty()) { ToolSupport::UsageHint( me, "--handshake-fd and --mach-service are incompatible"); - return EXIT_FAILURE; + return ExitFailure(); } #elif defined(OS_WIN) if (!options.initial_client_data.IsValid() && options.pipe_name.empty()) { ToolSupport::UsageHint(me, "--initial-client-data or --pipe-name is required"); - return EXIT_FAILURE; + return ExitFailure(); } if (options.initial_client_data.IsValid() && !options.pipe_name.empty()) { ToolSupport::UsageHint( me, "--initial-client-data and --pipe-name are incompatible"); - return EXIT_FAILURE; + return ExitFailure(); } #endif // OS_MACOSX if (!options.database) { ToolSupport::UsageHint(me, "--database is required"); - return EXIT_FAILURE; + return ExitFailure(); } if (argc) { ToolSupport::UsageHint(me, nullptr); - return EXIT_FAILURE; + return ExitFailure(); } #if defined(OS_MACOSX) @@ -503,7 +605,7 @@ int HandlerMain(int argc, char* argv[]) { } if (!receive_right.is_valid()) { - return EXIT_FAILURE; + return ExitFailure(); } ExceptionHandlerServer exception_handler_server( @@ -520,6 +622,7 @@ int HandlerMain(int argc, char* argv[]) { // launchd.plist(5). // // Set up a SIGTERM handler that will call exception_handler_server.Stop(). + // This replaces the HandleTerminateSignal handler for SIGTERM. struct sigaction sa = {}; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; @@ -553,11 +656,13 @@ int HandlerMain(int argc, char* argv[]) { } } + Metrics::HandlerLifetimeMilestone(Metrics::LifetimeMilestone::kStarted); + std::unique_ptr database(CrashReportDatabase::Initialize( base::FilePath(ToolSupport::CommandLineArgumentToFilePathStringType( options.database)))); if (!database) { - return EXIT_FAILURE; + return ExitFailure(); } // TODO(scottmg): options.rate_limit should be removed when we have a diff --git a/util/misc/metrics.cc b/util/misc/metrics.cc index 48be7ecd..e2bd9f0b 100644 --- a/util/misc/metrics.cc +++ b/util/misc/metrics.cc @@ -98,6 +98,14 @@ void Metrics::ExceptionEncountered() { ExceptionProcessing(ExceptionProcessingState::kStarted); } +// static +void Metrics::HandlerLifetimeMilestone(LifetimeMilestone milestone) { + UMA_HISTOGRAM_ENUMERATION("Crashpad.HandlerLifetimeMilestone", + static_cast(milestone), + static_cast(LifetimeMilestone::kMaxValue)); +} + +// static void Metrics::HandlerCrashed(uint32_t exception_code) { UMA_HISTOGRAM_SPARSE_SLOWLY( "Crashpad.HandlerCrash.ExceptionCode." METRICS_OS_NAME, diff --git a/util/misc/metrics.h b/util/misc/metrics.h index fd2e1200..f1c5731a 100644 --- a/util/misc/metrics.h +++ b/util/misc/metrics.h @@ -130,6 +130,34 @@ class Metrics { //! \brief The exception handler server started capturing an exception. static void ExceptionEncountered(); + //! \brief An important event in a handler process’ lifetime. + enum class LifetimeMilestone : int32_t { + //! \brief The handler process started. + kStarted = 0, + + //! \brief The handler process exited normally and cleanly. + kExitedNormally, + + //! \brief The handler process exited early, but was successful in + //! performing some non-default action on user request. + kExitedEarly, + + //! \brief The handler process exited with a failure code. + kFailed, + + //! \brief The handler process was forcibly terminated. + kTerminated, + + //! \brief The handler process crashed. + kCrashed, + + //! \brief The number of values in this enumeration; not a valid value. + kMaxValue + }; + + //! \brief Records a handler start/exit/crash event. + static void HandlerLifetimeMilestone(LifetimeMilestone milestone); + //! \brief The handler process crashed with the given exception code. //! //! This is currently only reported on Windows. diff --git a/util/thread/thread_posix.cc b/util/thread/thread_posix.cc index 7142c786..58a98747 100644 --- a/util/thread/thread_posix.cc +++ b/util/thread/thread_posix.cc @@ -14,20 +14,22 @@ #include "util/thread/thread.h" +#include + #include "base/logging.h" namespace crashpad { void Thread::Start() { DCHECK(!platform_thread_); - int rv = pthread_create(&platform_thread_, nullptr, ThreadEntryThunk, this); - PCHECK(0 == rv); + errno = pthread_create(&platform_thread_, nullptr, ThreadEntryThunk, this); + PCHECK(errno == 0) << "pthread_create"; } void Thread::Join() { DCHECK(platform_thread_); - int rv = pthread_join(platform_thread_, nullptr); - PCHECK(0 == rv); + errno = pthread_join(platform_thread_, nullptr); + PCHECK(errno == 0) << "pthread_join"; platform_thread_ = 0; } diff --git a/util/thread/thread_win.cc b/util/thread/thread_win.cc index c4ac1eb7..466dfa9b 100644 --- a/util/thread/thread_win.cc +++ b/util/thread/thread_win.cc @@ -22,13 +22,13 @@ void Thread::Start() { DCHECK(!platform_thread_); platform_thread_ = CreateThread(nullptr, 0, ThreadEntryThunk, this, 0, nullptr); - PCHECK(platform_thread_); + PCHECK(platform_thread_) << "CreateThread"; } void Thread::Join() { DCHECK(platform_thread_); DWORD result = WaitForSingleObject(platform_thread_, INFINITE); - PCHECK(WAIT_OBJECT_0 == result); + PCHECK(result == WAIT_OBJECT_0) << "WaitForSingleObject"; platform_thread_ = 0; } diff --git a/util/util.gyp b/util/util.gyp index b2354d28..b27337c6 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -198,6 +198,8 @@ 'win/scoped_local_alloc.h', 'win/scoped_process_suspend.cc', 'win/scoped_process_suspend.h', + 'win/session_end_watcher.cc', + 'win/session_end_watcher.h', 'win/termination_codes.h', 'win/time.cc', 'win/time.h', @@ -268,6 +270,7 @@ ['OS=="win"', { 'link_settings': { 'libraries': [ + '-luser32.lib', '-lwinhttp.lib', ], }, diff --git a/util/util_test.gyp b/util/util_test.gyp index 7636941b..e16a292f 100644 --- a/util/util_test.gyp +++ b/util/util_test.gyp @@ -95,6 +95,7 @@ 'win/process_info_test.cc', 'win/registration_protocol_win_test.cc', 'win/scoped_process_suspend_test.cc', + 'win/session_end_watcher_test.cc', 'win/time_test.cc', ], 'conditions': [ @@ -114,6 +115,7 @@ '-ladvapi32.lib', '-limagehlp.lib', '-lrpcrt4.lib', + '-luser32.lib', ], }, }], diff --git a/util/win/scoped_handle.cc b/util/win/scoped_handle.cc index 5eb440e0..57d28217 100644 --- a/util/win/scoped_handle.cc +++ b/util/win/scoped_handle.cc @@ -25,7 +25,7 @@ void ScopedFileHANDLECloseTraits::Free(HANDLE handle) { } void ScopedKernelHANDLECloseTraits::Free(HANDLE handle) { - PCHECK(CloseHandle(handle)); + PCHECK(CloseHandle(handle)) << "CloseHandle"; } } // namespace internal diff --git a/util/win/session_end_watcher.cc b/util/win/session_end_watcher.cc new file mode 100644 index 00000000..e795f22a --- /dev/null +++ b/util/win/session_end_watcher.cc @@ -0,0 +1,243 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/win/session_end_watcher.h" + +#include "base/logging.h" +#include "base/scoped_generic.h" + +extern "C" { +extern IMAGE_DOS_HEADER __ImageBase; +} // extern "C" + +namespace crashpad { + +namespace { + +class ScopedSetEvent { + public: + explicit ScopedSetEvent(HANDLE event) : event_(event) {} + ~ScopedSetEvent() { + if (!SetEvent(event_)) { + PLOG(ERROR) << "SetEvent"; + } + } + + private: + HANDLE event_; + + DISALLOW_COPY_AND_ASSIGN(ScopedSetEvent); +}; + +// ScopedWindowClass and ScopedWindow operate on ATOM* and HWND*, respectively, +// instead of ATOM and HWND, so that the actual storage can exist as a local +// variable or a member variable, and the scoper can be responsible for +// releasing things only if the actual storage hasn’t been released and zeroed +// already by something else. +struct ScopedWindowClassTraits { + static ATOM* InvalidValue() { return nullptr; } + static void Free(ATOM* window_class) { + if (*window_class) { + if (!UnregisterClass(MAKEINTATOM(*window_class), 0)) { + PLOG(ERROR) << "UnregisterClass"; + } else { + *window_class = 0; + } + } + } +}; +using ScopedWindowClass = base::ScopedGeneric; + +struct ScopedWindowTraits { + static HWND* InvalidValue() { return nullptr; } + static void Free(HWND* window) { + if (*window) { + if (!DestroyWindow(*window)) { + PLOG(ERROR) << "DestroyWindow"; + } else { + *window = nullptr; + } + } + } +}; +using ScopedWindow = base::ScopedGeneric; + +// GetWindowLongPtr()’s return value doesn’t unambiguously indicate whether it +// was successful, because 0 could either represent successful retrieval of the +// value 0, or failure. This wrapper is more convenient to use. +bool GetWindowLongPtrAndSuccess(HWND window, int index, LONG_PTR* value) { + SetLastError(ERROR_SUCCESS); + *value = GetWindowLongPtr(window, index); + return *value || GetLastError() == ERROR_SUCCESS; +} + +// SetWindowLongPtr() has the same problem as GetWindowLongPtr(). Use this +// wrapper instead. +bool SetWindowLongPtrAndGetSuccess(HWND window, int index, LONG_PTR value) { + SetLastError(ERROR_SUCCESS); + LONG_PTR previous = SetWindowLongPtr(window, index, value); + return previous || GetLastError() == ERROR_SUCCESS; +} + +} // namespace + +SessionEndWatcher::SessionEndWatcher() + : Thread(), + window_(nullptr), + started_(nullptr), + stopped_(nullptr) { + // Set bManualReset for these events so that WaitForStart() and WaitForStop() + // can be called multiple times. + + started_.reset(CreateEvent(nullptr, true, false, nullptr)); + PLOG_IF(ERROR, !started_.get()) << "CreateEvent"; + + stopped_.reset(CreateEvent(nullptr, true, false, nullptr)); + PLOG_IF(ERROR, !stopped_.get()) << "CreateEvent"; + + Start(); +} + +SessionEndWatcher::~SessionEndWatcher() { + // Tear everything down by posting a WM_CLOSE to the window. This obviously + // can’t work until the window has been created, and that happens on a + // different thread, so wait for the start event to be signaled first. + WaitForStart(); + if (window_) { + if (!PostMessage(window_, WM_CLOSE, 0, 0)) { + PLOG(ERROR) << "PostMessage"; + } + } + + Join(); + DCHECK(!window_); +} + +void SessionEndWatcher::WaitForStart() { + if (WaitForSingleObject(started_.get(), INFINITE) != WAIT_OBJECT_0) { + PLOG(ERROR) << "WaitForSingleObject"; + } +} + +void SessionEndWatcher::WaitForStop() { + if (WaitForSingleObject(stopped_.get(), INFINITE) != WAIT_OBJECT_0) { + PLOG(ERROR) << "WaitForSingleObject"; + } +} + +void SessionEndWatcher::ThreadMain() { + ATOM atom = 0; + ScopedWindowClass window_class(&atom); + ScopedWindow window(&window_); + + ScopedSetEvent call_set_stop(stopped_.get()); + + { + ScopedSetEvent call_set_start(started_.get()); + + WNDCLASS wndclass = {}; + wndclass.lpfnWndProc = WindowProc; + wndclass.hInstance = reinterpret_cast(&__ImageBase); + wndclass.lpszClassName = L"crashpad_SessionEndWatcher"; + atom = RegisterClass(&wndclass); + if (!atom) { + PLOG(ERROR) << "RegisterClass"; + return; + } + + window_ = CreateWindow(MAKEINTATOM(atom), // lpClassName + nullptr, // lpWindowName + 0, // dwStyle + 0, // x + 0, // y + 0, // nWidth + 0, // nHeight + nullptr, // hWndParent + nullptr, // hMenu + nullptr, // hInstance + this); // lpParam + if (!window_) { + PLOG(ERROR) << "CreateWindow"; + return; + } + } + + MSG message; + BOOL rv = 0; + while (window_ && (rv = GetMessage(&message, window_, 0, 0)) > 0) { + TranslateMessage(&message); + DispatchMessage(&message); + } + if (window_ && rv == -1) { + PLOG(ERROR) << "GetMessage"; + return; + } +} + +// static +LRESULT CALLBACK SessionEndWatcher::WindowProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param) { + // Figure out which object this is. A pointer to it is stuffed into the last + // parameter of CreateWindow(), which shows up as CREATESTRUCT::lpCreateParams + // in a WM_CREATE message. That should be processed before any of the other + // messages of interest to this function. Once the object is known, save a + // pointer to it in the GWLP_USERDATA slot for later retrieval when processing + // other messages. + SessionEndWatcher* self; + if (!GetWindowLongPtrAndSuccess( + window, GWLP_USERDATA, reinterpret_cast(&self))) { + PLOG(ERROR) << "GetWindowLongPtr"; + } + if (!self && message == WM_CREATE) { + CREATESTRUCT* create = reinterpret_cast(l_param); + self = reinterpret_cast(create->lpCreateParams); + if (!SetWindowLongPtrAndGetSuccess( + window, GWLP_USERDATA, reinterpret_cast(self))) { + PLOG(ERROR) << "SetWindowLongPtr"; + } + } + + if (self) { + if (message == WM_ENDSESSION) { + // If w_param is false, this WM_ENDSESSION message cancels a previous + // WM_QUERYENDSESSION. + if (w_param) { + self->SessionEnding(); + + // If the session is ending, post a close message which will kick off + // window destruction and cause the message loop thread to terminate. + if (!PostMessage(self->window_, WM_CLOSE, 0, 0)) { + PLOG(ERROR) << "PostMessage"; + } + } + } else if (message == WM_DESTROY) { + // The window is being destroyed. Clear GWLP_USERDATA so that |self| won’t + // be found during a subsequent call into this function for this window. + // Clear self->window_ too, because it refers to an object that soon won’t + // exist. That signals the message loop to stop processing messages. + if (!SetWindowLongPtrAndGetSuccess(window, GWLP_USERDATA, 0)) { + PLOG(ERROR) << "SetWindowLongPtr"; + } + self->window_ = nullptr; + } + } + + // If the message is WM_CLOSE, DefWindowProc() will call DestroyWindow(), and + // this function will be called again with a WM_DESTROY message. + return DefWindowProc(window, message, w_param, l_param); +} + +} // namespace crashpad diff --git a/util/win/session_end_watcher.h b/util/win/session_end_watcher.h new file mode 100644 index 00000000..b23d391d --- /dev/null +++ b/util/win/session_end_watcher.h @@ -0,0 +1,79 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_WIN_SESSION_END_WATCHER_H_ +#define CRASHPAD_UTIL_WIN_SESSION_END_WATCHER_H_ + +#include + +#include "base/macros.h" +#include "util/thread/thread.h" +#include "util/win/scoped_handle.h" + +namespace crashpad { + +//! \brief Creates a hidden window and waits for a `WM_ENDSESSION` message, +//! indicating that the session is ending and the application should +//! terminate. +//! +//! A dedicated thread will be created to run the `GetMessage()`-based message +//! loop required to monitor for this message. +//! +//! Users should subclass this class and receive notifications by implementing +//! the SessionEndWatcherEvent() method. +class SessionEndWatcher : public Thread { + public: + SessionEndWatcher(); + + //! \note The destructor waits for the thread that runs the message loop to + //! terminate. + ~SessionEndWatcher() override; + + protected: + // Exposed for testing. + HWND GetWindow() const { return window_; } + + // Exposed for testing. Blocks until window_ has been created. May be called + // multiple times if necessary. + void WaitForStart(); + + // Exposed for testing. Blocks until the message loop ends. May be called + // multiple times if necessary. + void WaitForStop(); + + private: + // Thread: + void ThreadMain() override; + + static LRESULT CALLBACK WindowProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param); + + //! \brief A `WM_ENDSESSION` message was received and it indicates that the + //! user session will be ending imminently. + //! + //! This method is called on the thread that runs the message loop. + virtual void SessionEnding() = 0; + + HWND window_; // Conceptually strong, but ownership managed in ThreadMain() + ScopedKernelHANDLE started_; + ScopedKernelHANDLE stopped_; + + DISALLOW_COPY_AND_ASSIGN(SessionEndWatcher); +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_WIN_SESSION_END_WATCHER_H_ diff --git a/util/win/session_end_watcher_test.cc b/util/win/session_end_watcher_test.cc new file mode 100644 index 00000000..692d76e3 --- /dev/null +++ b/util/win/session_end_watcher_test.cc @@ -0,0 +1,62 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/win/session_end_watcher.h" + +#include "gtest/gtest.h" +#include "test/errors.h" + +namespace crashpad { +namespace test { +namespace { + +class SessionEndWatcherTest final : public SessionEndWatcher { + public: + SessionEndWatcherTest() : SessionEndWatcher(), called_(false) {} + + ~SessionEndWatcherTest() override {} + + void Run() { + WaitForStart(); + + HWND window = GetWindow(); + ASSERT_TRUE(window); + EXPECT_TRUE(PostMessage(window, WM_ENDSESSION, 1, 0)); + + WaitForStop(); + + EXPECT_TRUE(called_); + } + + private: + // SessionEndWatcher: + void SessionEnding() override { called_ = true; } + + bool called_; + + DISALLOW_COPY_AND_ASSIGN(SessionEndWatcherTest); +}; + +TEST(SessionEndWatcher, SessionEndWatcher) { + SessionEndWatcherTest test; + test.Run(); +} + +TEST(SessionEndWatcher, DoNothing) { + SessionEndWatcherTest test; +} + +} // namespace +} // namespace test +} // namespace crashpad