Skip to content

Instantly share code, notes, and snippets.

@micjabbour
Last active March 21, 2021 09:29
Show Gist options
  • Save micjabbour/4b2a3b93a619743bd96d3bb07f2f57de to your computer and use it in GitHub Desktop.
Save micjabbour/4b2a3b93a619743bd96d3bb07f2f57de to your computer and use it in GitHub Desktop.
Suppress Windows Error Reporting crash dialogs

suppress_wer

I wrote this gist to explore multiple ways to disable Windows Error Reporting dialogs ("app has stopped working" messages) when starting a buggy application. Please note that there are many ways to disable WER dialogs globally. This gist, on the other hand, aims to disable WER for a specific application when starting it. This is useful when running such a buggy application in a script for example.

Usage

After cloning the gist, one can use CMake to build the application in visual studio:

mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" -DUSE_DEBUGGER_BASED_SOLUTION=OFF ..
cmake --build . --config Release

After that, the resulting executable can be used to start any buggy command as follows:

suppress_wer <buggy_command> [buggy_args]

This disables the display of Windows Error Reporting dialogs when buggy_command crashes.

How does this work

Two alternative approaches to achieve the result are included in the gist. This section provides a quick overview of them.

Using SetErrorMode

This approach is based on this oldnewthing blog post. This is what gets used when executing the commands provided above. The idea is to change the way the system reacts to errors in the process using SetErrorMode. Since error modes are inherited in child processes, crashes in the buggy child process do not display WER dialogs any more. See suppress_wer_set_error_mode.cpp for details.

The approach can be easily adapted to any scripting language that supports interfacing with C, because it is actually all about executing a single WinAPI call to SetErrorMode. For example, one can call the following function at the beginning of their Python script to disable WER for any subprocess:

def suppress_wer():
    import ctypes
    from ctypes import wintypes
    SEM_NOGPFAULTERRORBOX = 2
    kernel32 = ctypes.windll.kernel32
    kernel32.SetErrorMode.restype = wintypes.UINT
    kernel32.SetErrorMode.argtypes = [wintypes.UINT]
    kernel32.SetErrorMode(SEM_NOGPFAULTERRORBOX)

Using WinAPI Debugging Functions

This approach uses WinAPI Debugging Functions to monitor debug events in the child process and terminate it as soon as it is about to crash (on second-chance exception notifications). One can use this approach by setting -DUSE_DEBUGGER_BASED_SOLUTION=ON in the CMake command above. This approach has the additional requirement that suppress_wer is built using the same architecture as the buggy application that we want to start. However, it has the advantage that more control over the child process is possible. For example, the code can be extended to handle debug events as desired, and invoke code in the child process context when needed (e.g. call _set_abort_behavior(0, _WRITE_ABORT_MSG); when the child process starts to disable MSVCRT debug error dialogs). See suppress_wer_debugger.cpp for details.

cmake_minimum_required(VERSION 3.2)
project(suppress_wer)
option(USE_DEBUGGER_BASED_SOLUTION "Uses a debugger-based solution instead of SetErrorMode. See suppress_wer_debugger.cpp for details" OFF)
if(USE_DEBUGGER_BASED_SOLUTION)
add_executable(suppress_wer suppress_wer_debugger.cpp)
else()
add_executable(suppress_wer suppress_wer_set_error_mode.cpp)
endif()
target_compile_definitions(suppress_wer PUBLIC -DUNICODE -D_UNICODE)
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <tchar.h>
#include <windows.h>
#if defined(UNICODE) || defined(_UNICODE)
std::wostream& tcout(std::wcout);
#else
std::ostream& tcout(std::cout);
#endif
typedef std::basic_string<TCHAR> tstring;
/// Replaces all occurrences of from in str with to.
inline void replaceAll(tstring& str, const tstring& from, const tstring& to) {
std::size_t startPos = 0;
while ((startPos = str.find(from, startPos)) != std::string::npos) {
str.replace(startPos, from.size(), to);
startPos += to.size();
}
}
/// Returns true iff str contains substr, and false otherwise.
inline bool contains(const tstring& str, const tstring& substr) {
return str.find(substr) != std::string::npos;
}
/// Collects argv back into a single string by re-escaping quotes and adding
/// ones where necessary.
inline tstring argvToCommandLine(int argc, _TCHAR* argv[]) {
tstring cmdLine;
const TCHAR* separator = _T("");
for (int i = 0; i < argc; ++i) {
tstring arg = argv[i];
// escape all quotes
replaceAll(arg, _T("\""), _T("\\\""));
cmdLine += separator;
separator = _T(" ");
// enclose arg in quotes if necessary
const bool needsQuotes = contains(arg, _T(" ")) || contains(arg, _T("\t"));
if (needsQuotes)
cmdLine += _T("\"");
cmdLine += arg; // add arg
if (needsQuotes)
cmdLine += _T("\"");
}
return cmdLine;
}
/// Encapsulates a call to WINAPI CreateProcess and automates necessary cleanup
/// in the destructor.
class Process {
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
public:
Process(tstring cmdLine, DWORD creationFlags = 0)
: startupInfo{sizeof(STARTUPINFO)} {
if (!CreateProcess(NULL, &cmdLine[0], NULL, NULL, TRUE, creationFlags, NULL,
NULL, &startupInfo, &processInfo)) {
throw std::runtime_error("CreateProcess failed.");
}
}
~Process() {
WaitForSingleObject(processInfo.hProcess, INFINITE);
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
DWORD getExitCode() const {
WaitForSingleObject(processInfo.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processInfo.hProcess, &exitCode);
return exitCode;
}
const STARTUPINFO& getStartupInfo() const { return startupInfo; }
const PROCESS_INFORMATION& getProcessInfo() const { return processInfo; }
};
#include "common.h"
#include <iostream>
#include <stdexcept>
/// Starts the given command while suppressing windows error reporting crash
/// dialogs.
/// This is done by attaching a debugger to the started process and terminating
/// it once a fatal error occurs.
DWORD suppressWer(tstring cmdLine) {
Process process(std::move(cmdLine), DEBUG_PROCESS);
const PROCESS_INFORMATION& processInfo = process.getProcessInfo();
DEBUG_EVENT debugEvent = {0};
DWORD exitCode = 0;
// debugger loop
while (true) {
if (!WaitForDebugEvent(&debugEvent, INFINITE))
throw std::runtime_error("WaitForDebugEvent failed!");
// if this is a second-chance exception notification, terminate the process
// with the received exception code (to avoid the windows error reporting
// dialog)
if (debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
!debugEvent.u.Exception.dwFirstChance)
TerminateProcess(processInfo.hProcess,
debugEvent.u.Exception.ExceptionRecord.ExceptionCode);
// unfortunately, the above does not suppress msvcrt "debug error" dialogs
// when abort is called in an application that is built using debug
// configuration.
// see https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)
// for details.
// this can be solved by building the debugged process in Release mode, or
// calling `_set_abort_behavior(0, _WRITE_ABORT_MSG);` at the beginning of
// it.
// TODO: suppress_wer can be extended to execute the required function call
// in the context of the debugged process
// ignore all debug events
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED);
// break the debugger loop when EXIT_PROCESS_DEBUG_EVENT is received
if (debugEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT &&
debugEvent.dwProcessId == processInfo.dwProcessId) {
exitCode = debugEvent.u.ExitProcess.dwExitCode;
break;
}
}
return exitCode;
}
int _tmain(int argc, _TCHAR* argv[]) try {
if(argc < 2) {
tcout << _T("Usage:\n");
tcout << argv[0] << _T(" <buggy_command> [buggy_command_args] ...\n");
return 0;
}
return suppressWer(argvToCommandLine(argc - 1, argv + 1));
} catch (const std::exception& exception) {
std::cerr << exception.what() << "\n";
#ifdef _DEBUG
throw;
#else
return -1;
#endif
}
#include "common.h"
#include <iostream>
#include <stdexcept>
int _tmain(int argc, _TCHAR* argv[]) try {
if(argc < 2) {
tcout << _T("Usage:\n");
tcout << argv[0] << _T(" <buggy_command> [buggy_command_args] ...\n");
return 0;
}
// use SetErrorMode to disable WER for the current process and all child
// processes
// based on https://devblogs.microsoft.com/oldnewthing/20160204-00/?p=92972
SetErrorMode(SEM_NOGPFAULTERRORBOX);
// start the process normally
Process process(argvToCommandLine(argc - 1, argv + 1));
return process.getExitCode();
} catch (const std::exception& exception) {
std::cerr << exception.what() << "\n";
#ifdef _DEBUG
throw;
#else
return -1;
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment