Index: ps/trunk/source/lib/debug.cpp =================================================================== --- ps/trunk/source/lib/debug.cpp (revision 25299) +++ ps/trunk/source/lib/debug.cpp (revision 25300) @@ -1,564 +1,550 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/* - * platform-independent debug support code. - */ - #include "precompiled.h" #include "lib/debug.h" -#include -#include -#include - #include "lib/alignment.h" #include "lib/app_hooks.h" #include "lib/fnv_hash.h" -#include "lib/sysdep/vm.h" #include "lib/sysdep/cpu.h" // cpu_CAS #include "lib/sysdep/sysdep.h" +#include "lib/sysdep/vm.h" #if OS_WIN # include "lib/sysdep/os/win/wdbg_heap.h" #endif +#include +#include +#include + +namespace +{ + +// (NB: this may appear obscene, but deep stack traces have been +// observed to take up > 256 KiB) +constexpr std::size_t MESSAGE_SIZE = 512 * KiB / sizeof(wchar_t); +wchar_t g_MessageBuffer[MESSAGE_SIZE]; + +} // anonymous namespace + static const StatusDefinition debugStatusDefinitions[] = { { ERR::SYM_NO_STACK_FRAMES_FOUND, L"No stack frames found" }, { ERR::SYM_UNRETRIEVABLE_STATIC, L"Value unretrievable (stored in external module)" }, { ERR::SYM_UNRETRIEVABLE, L"Value unretrievable" }, { ERR::SYM_TYPE_INFO_UNAVAILABLE, L"Error getting type_info" }, { ERR::SYM_INTERNAL_ERROR, L"Exception raised while processing a symbol" }, { ERR::SYM_UNSUPPORTED, L"Symbol type not (fully) supported" }, { ERR::SYM_CHILD_NOT_FOUND, L"Symbol does not have the given child" }, { ERR::SYM_NESTING_LIMIT, L"Symbol nesting too deep or infinite recursion" }, { ERR::SYM_SINGLE_SYMBOL_LIMIT, L"Symbol has produced too much output" }, { INFO::SYM_SUPPRESS_OUTPUT, L"Symbol was suppressed" } }; STATUS_ADD_DEFINITIONS(debugStatusDefinitions); // need to shoehorn printf-style variable params into // the OutputDebugString call. // - don't want to split into multiple calls - would add newlines to output. // - fixing Win32 _vsnprintf to return # characters that would be written, // as required by C99, looks difficult and unnecessary. if any other code // needs that, implement GNU vasprintf. // - fixed size buffers aren't nice, but much simpler than vasprintf-style // allocate+expand_until_it_fits. these calls are for quick debug output, // not loads of data, anyway. // rationale: static data instead of std::set to allow setting at any time. // we store FNV hash of tag strings for fast comparison; collisions are // extremely unlikely and can only result in displaying more/less text. static const size_t MAX_TAGS = 20; static u32 tags[MAX_TAGS]; static size_t num_tags; void debug_filter_add(const char* tag) { const u32 hash = fnv_hash(tag, strlen(tag)*sizeof(tag[0])); // make sure it isn't already in the list for(size_t i = 0; i < MAX_TAGS; i++) if(tags[i] == hash) return; // too many already? if(num_tags == MAX_TAGS) { DEBUG_WARN_ERR(ERR::LOGIC); // increase MAX_TAGS return; } tags[num_tags++] = hash; } void debug_filter_remove(const char* tag) { const u32 hash = fnv_hash(tag, strlen(tag)*sizeof(tag[0])); for(size_t i = 0; i < MAX_TAGS; i++) { if(tags[i] == hash) // found it { // replace with last element (avoid holes) tags[i] = tags[MAX_TAGS-1]; num_tags--; // can only happen once, so we're done. return; } } } void debug_filter_clear() { std::fill(tags, tags+MAX_TAGS, 0); } bool debug_filter_allows(const char* text) { size_t i; for(i = 0; ; i++) { // no | found => no tag => should always be displayed if(text[i] == ' ' || text[i] == '\0') return true; if(text[i] == '|' && i != 0) break; } const u32 hash = fnv_hash(text, i*sizeof(text[0])); // check if entry allowing this tag is found for(i = 0; i < MAX_TAGS; i++) if(tags[i] == hash) return true; return false; } #undef debug_printf // allowing #defining it out void debug_printf(const char* fmt, ...) { char buf[16384]; va_list ap; va_start(ap, fmt); const int numChars = vsprintf_s(buf, ARRAY_SIZE(buf), fmt, ap); if (numChars < 0) debug_break(); // poor man's assert - avoid infinite loop because ENSURE also uses debug_printf va_end(ap); debug_puts_filtered(buf); } void debug_puts_filtered(const char* text) { if(debug_filter_allows(text)) debug_puts(text); } //----------------------------------------------------------------------------- Status debug_WriteCrashlog(const wchar_t* text) { // (avoid infinite recursion and/or reentering this function if it // fails/reports an error) enum State { IDLE, BUSY, FAILED }; // note: the initial state is IDLE. we rely on zero-init because // initializing local static objects from constants may happen when // this is first called, which isn't thread-safe. (see C++ 6.7.4) cassert(IDLE == 0); static volatile intptr_t state; if(!cpu_CAS(&state, IDLE, BUSY)) return ERR::REENTERED; // NOWARN OsPath pathname = ah_get_log_dir()/"crashlog.txt"; FILE* f = sys_OpenFile(pathname, "w"); if(!f) { state = FAILED; // must come before DEBUG_DISPLAY_ERROR DEBUG_DISPLAY_ERROR(L"Unable to open crashlog.txt for writing (please ensure the log directory is writable)"); return ERR::FAIL; // NOWARN (the above text is more helpful than a generic error code) } fputwc(0xFEFF, f); // BOM fwprintf(f, L"%ls\n", text); fwprintf(f, L"\n\n====================================\n\n"); // allow user to bundle whatever information they want ah_bundle_logs(f); fclose(f); state = IDLE; return INFO::OK; } //----------------------------------------------------------------------------- // error message //----------------------------------------------------------------------------- -// (NB: this may appear obscene, but deep stack traces have been -// observed to take up > 256 KiB) -static const size_t messageSize = 512*KiB; - -void debug_FreeErrorMessage(ErrorMessageMem* emm) -{ - vm::Free(emm->pa_mem, messageSize); -} - // a stream with printf-style varargs and the possibility of // writing directly to the output buffer. class PrintfWriter { public: PrintfWriter(wchar_t* buf, size_t maxChars) : m_pos(buf), m_charsLeft(maxChars) { } bool operator()(const wchar_t* fmt, ...) WPRINTF_ARGS(2) { va_list ap; va_start(ap, fmt); const int len = vswprintf(m_pos, m_charsLeft, fmt, ap); va_end(ap); if(len < 0) return false; m_pos += len; m_charsLeft -= len; return true; } wchar_t* Position() const { return m_pos; } size_t CharsLeft() const { return m_charsLeft; } void CountAddedChars() { const size_t len = wcslen(m_pos); m_pos += len; m_charsLeft -= len; } private: wchar_t* m_pos; size_t m_charsLeft; }; // split out of debug_DisplayError because it's used by the self-test. const wchar_t* debug_BuildErrorMessage( const wchar_t* description, const wchar_t* filename, int line, const char* func, - void* context, const wchar_t* lastFuncToSkip, - ErrorMessageMem* emm) + void* context, const wchar_t* lastFuncToSkip) { // retrieve errno (might be relevant) before doing anything else // that might overwrite it. wchar_t description_buf[100] = L"?"; wchar_t os_error[100] = L"?"; Status errno_equiv = StatusFromErrno(); // NOWARN if(errno_equiv != ERR::FAIL) // meaningful translation StatusDescription(errno_equiv, description_buf, ARRAY_SIZE(description_buf)); sys_StatusDescription(0, os_error, ARRAY_SIZE(os_error)); - // rationale: see ErrorMessageMem - emm->pa_mem = vm::Allocate(messageSize); - wchar_t* const buf = (wchar_t*)emm->pa_mem; - if(!buf) - return L"(insufficient memory to generate error message)"; - PrintfWriter writer(buf, messageSize / sizeof(wchar_t)); + PrintfWriter writer(g_MessageBuffer, MESSAGE_SIZE); // header if(!writer( L"%ls\r\n" L"Location: %ls:%d (%hs)\r\n" L"\r\n" L"Call stack:\r\n" L"\r\n", description, filename, line, func )) { fail: return L"(error while formatting error message)"; } // append stack trace Status ret = debug_DumpStack(writer.Position(), writer.CharsLeft(), context, lastFuncToSkip); if(ret == ERR::REENTERED) { if(!writer( L"While generating an error report, we encountered a second " L"problem. Please be sure to report both this and the subsequent " L"error messages." )) goto fail; } else if(ret != INFO::OK) { wchar_t error_buf[100] = {'?'}; if(!writer( L"(error while dumping stack: %ls)", StatusDescription(ret, error_buf, ARRAY_SIZE(error_buf)) )) goto fail; } else // success { writer.CountAddedChars(); } // append errno if(!writer( L"\r\n" L"errno = %d (%ls)\r\n" L"OS error = %ls\r\n", errno, description_buf, os_error )) goto fail; - return buf; + return g_MessageBuffer; } //----------------------------------------------------------------------------- // display error messages //----------------------------------------------------------------------------- // translates and displays the given strings in a dialog. // this is typically only used when debug_DisplayError has failed or // is unavailable because that function is much more capable. // implemented via sys_display_msg; see documentation there. void debug_DisplayMessage(const wchar_t* caption, const wchar_t* msg) { sys_display_msg(ah_translate(caption), ah_translate(msg)); } // when an error has come up and user clicks Exit, we don't want any further // errors (e.g. caused by atexit handlers) to come up, possibly causing an // infinite loop. hiding errors isn't good, but we assume that whoever clicked // exit really doesn't want to see any more messages. static atomic_bool isExiting; // this logic is applicable to any type of error. special cases such as // suppressing certain expected WARN_ERRs are done there. static bool ShouldSuppressError(atomic_bool* suppress) { if(isExiting) return true; if(!suppress) return false; if(*suppress == DEBUG_SUPPRESS) return true; return false; } static ErrorReactionInternal CallDisplayError(const wchar_t* text, size_t flags) { // first try app hook implementation ErrorReactionInternal er = ah_display_error(text, flags); // .. it's only a stub: default to normal implementation if(er == ERI_NOT_IMPLEMENTED) er = sys_display_error(text, flags); return er; } static ErrorReaction PerformErrorReaction(ErrorReactionInternal er, size_t flags, atomic_bool* suppress) { const bool shouldHandleBreak = (flags & DE_MANUAL_BREAK) == 0; switch(er) { case ERI_CONTINUE: return ER_CONTINUE; case ERI_BREAK: // handle "break" request unless the caller wants to (doing so here // instead of within the dlgproc yields a correct call stack) if(shouldHandleBreak) { debug_break(); return ER_CONTINUE; } else return ER_BREAK; case ERI_SUPPRESS: (void)cpu_CAS(suppress, 0, DEBUG_SUPPRESS); return ER_CONTINUE; case ERI_EXIT: isExiting = 1; // see declaration COMPILER_FENCE; #if OS_WIN // prevent (slow) heap reporting since we're exiting abnormally and // thus probably leaking like a sieve. wdbg_heap_Enable(false); #endif exit(EXIT_FAILURE); case ERI_NOT_IMPLEMENTED: default: debug_break(); // not expected to be reached return ER_CONTINUE; } } ErrorReaction debug_DisplayError(const wchar_t* description, size_t flags, void* context, const wchar_t* lastFuncToSkip, const wchar_t* pathname, int line, const char* func, atomic_bool* suppress) { // "suppressing" this error means doing nothing and returning ER_CONTINUE. if(ShouldSuppressError(suppress)) return ER_CONTINUE; // fix up params // .. translate description = ah_translate(description); // .. caller supports a suppress flag; set the corresponding flag so that // the error display implementation enables the Suppress option. if(suppress) flags |= DE_ALLOW_SUPPRESS; if(flags & DE_NO_DEBUG_INFO) { // in non-debug-info mode, simply display the given description // and then return immediately ErrorReactionInternal er = CallDisplayError(description, flags); return PerformErrorReaction(er, flags, suppress); } // .. deal with incomplete file/line info if(!pathname || pathname[0] == '\0') pathname = L"unknown"; if(line <= 0) line = 0; if(!func || func[0] == '\0') func = "?"; // .. _FILE__ evaluates to the full path (albeit without drive letter) // which is rather long. we only display the base name for clarity. const wchar_t* filename = path_name_only(pathname); // display in output window; double-click will navigate to error location. - debug_printf("%s(%d): %s\n", utf8_from_wstring(filename).c_str(), line, utf8_from_wstring(description).c_str()); - - ErrorMessageMem emm; - const wchar_t* text = debug_BuildErrorMessage(description, filename, line, func, context, lastFuncToSkip, &emm); + const wchar_t* text = debug_BuildErrorMessage(description, filename, line, func, context, lastFuncToSkip); (void)debug_WriteCrashlog(text); ErrorReactionInternal er = CallDisplayError(text, flags); + // TODO: use utf8 conversion without internal allocations. + debug_printf("%s(%d): %s\n", utf8_from_wstring(filename).c_str(), line, utf8_from_wstring(description).c_str()); + // note: debug_break-ing here to make sure the app doesn't continue // running is no longer necessary. debug_DisplayError now determines our // window handle and is modal. - // must happen before PerformErrorReaction because that may exit. - debug_FreeErrorMessage(&emm); - return PerformErrorReaction(er, flags, suppress); } // is errorToSkip valid? (also guarantees mutual exclusion) enum SkipStatus { INVALID, VALID, BUSY }; static intptr_t skipStatus = INVALID; static Status errorToSkip; static size_t numSkipped; void debug_SkipErrors(Status err) { if(cpu_CAS(&skipStatus, INVALID, BUSY)) { errorToSkip = err; numSkipped = 0; COMPILER_FENCE; skipStatus = VALID; // linearization point } else DEBUG_WARN_ERR(ERR::REENTERED); } size_t debug_StopSkippingErrors() { if(cpu_CAS(&skipStatus, VALID, BUSY)) { const size_t ret = numSkipped; COMPILER_FENCE; skipStatus = INVALID; // linearization point return ret; } else { DEBUG_WARN_ERR(ERR::REENTERED); return 0; } } static bool ShouldSkipError(Status err) { if(cpu_CAS(&skipStatus, VALID, BUSY)) { numSkipped++; const bool ret = (err == errorToSkip); COMPILER_FENCE; skipStatus = VALID; return ret; } return false; } - ErrorReaction debug_OnError(Status err, atomic_bool* suppress, const wchar_t* file, int line, const char* func) { CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE]; (void)debug_CaptureContext(context); if(ShouldSkipError(err)) return ER_CONTINUE; const wchar_t* lastFuncToSkip = L"debug_OnError"; wchar_t buf[400]; wchar_t err_buf[200]; StatusDescription(err, err_buf, ARRAY_SIZE(err_buf)); swprintf_s(buf, ARRAY_SIZE(buf), L"Function call failed: return value was %lld (%ls)", (long long)err, err_buf); return debug_DisplayError(buf, DE_MANUAL_BREAK, context, lastFuncToSkip, file,line,func, suppress); } - ErrorReaction debug_OnAssertionFailure(const wchar_t* expr, atomic_bool* suppress, const wchar_t* file, int line, const char* func) { CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE]; (void)debug_CaptureContext(context); - const std::wstring lastFuncToSkip = L"debug_OnAssertionFailure"; + const wchar_t* lastFuncToSkip = L"debug_OnAssertionFailure"; wchar_t buf[400]; swprintf_s(buf, ARRAY_SIZE(buf), L"Assertion failed: \"%ls\"", expr); - return debug_DisplayError(buf, DE_MANUAL_BREAK, context, lastFuncToSkip.c_str(), file,line,func, suppress); + return debug_DisplayError(buf, DE_MANUAL_BREAK, context, lastFuncToSkip, file,line,func, suppress); } Index: ps/trunk/source/lib/debug.h =================================================================== --- ps/trunk/source/lib/debug.h (revision 25299) +++ ps/trunk/source/lib/debug.h (revision 25300) @@ -1,582 +1,553 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * platform-independent debug support code. */ #ifndef INCLUDED_DEBUG #define INCLUDED_DEBUG // this module provides platform-independent debug facilities, useful for // diagnosing and reporting program errors. // - a symbol engine provides access to compiler-generated debug information and // can also give a stack trace including local variables; // - our more powerful assert() replacement gives a stack trace so // that the underlying problem becomes apparent; // - the output routines make for platform-independent logging and // crashlogs with "last-known activity" reporting. -#include "lib/lib_api.h" -#include "lib/types.h" // intptr_t -#include "lib/status.h" #include "lib/alignment.h" #include "lib/code_annotation.h" #include "lib/code_generation.h" +#include "lib/lib_api.h" +#include "lib/status.h" +#include "lib/types.h" /** * trigger a breakpoint when reached/"called". * if defined as a macro, the debugger can break directly into the * target function instead of one frame below it as with a conventional * call-based implementation. **/ #if MSC_VERSION # define debug_break __debugbreak // intrinsic "function" #else extern void debug_break(); #endif //----------------------------------------------------------------------------- // output //----------------------------------------------------------------------------- /** * write a formatted string to the debug channel, subject to filtering * (see below). implemented via debug_puts - see performance note there. * * @param fmt Format string and varargs; see printf. **/ LIB_API void debug_printf(const char* fmt, ...) PRINTF_ARGS(1); /** * translates and displays the given strings in a dialog. * this is typically only used when debug_DisplayError has failed or * is unavailable because that function is much more capable. * implemented via sys_display_msg; see documentation there. **/ LIB_API void debug_DisplayMessage(const wchar_t* caption, const wchar_t* msg); /// flags to customize debug_DisplayError behavior enum DebugDisplayErrorFlags { /** * disallow the Continue button. used e.g. if an exception is fatal. **/ DE_NO_CONTINUE = 1, /** * enable the Suppress button. set automatically by debug_DisplayError if * it receives a non-NULL suppress pointer. a flag is necessary because * the sys_display_error interface doesn't get that pointer. * rationale for automatic setting: this may prevent someone from * forgetting to specify it, and disabling Suppress despite having * passed a non-NULL pointer doesn't make much sense. **/ DE_ALLOW_SUPPRESS = 2, /** * do not trigger a breakpoint inside debug_DisplayError; caller * will take care of this if ER_BREAK is returned. this is so that the * debugger can jump directly into the offending function. **/ DE_MANUAL_BREAK = 4, /** * display just the given message; do not add any information about the * call stack, do not write crashlogs, etc. */ DE_NO_DEBUG_INFO = 8 }; /** * a bool that is reasonably certain to be set atomically. * we cannot assume support for OpenMP (requires GCC 4.2) or C++0x, * so we'll have to resort to intptr_t, cpu_CAS and COMPILER_FENCE. **/ typedef volatile intptr_t atomic_bool; /** * value for suppress flag once set by debug_DisplayError. * rationale: this value is fairly distinctive and helps when * debugging the symbol engine. * use 0 as the initial value to avoid allocating .rdata space. **/ static const atomic_bool DEBUG_SUPPRESS = 0xAB; /** * choices offered by the error dialog that are returned * by debug_DisplayError. **/ enum ErrorReaction { /** * ignore, continue as if nothing happened. * note: value doesn't start at 0 because that is interpreted as a * DialogBoxParam failure. **/ ER_CONTINUE = 1, /** * trigger breakpoint, i.e. enter debugger. * only returned if DE_MANUAL_BREAK was passed; otherwise, * debug_DisplayError will trigger a breakpoint itself. **/ ER_BREAK }; /** * all choices offered by the error dialog. those not defined in * ErrorReaction are acted upon by debug_DisplayError and * never returned to callers. * (this separation avoids enumerator-not-handled warnings) **/ enum ErrorReactionInternal { ERI_CONTINUE = ER_CONTINUE, ERI_BREAK = ER_BREAK, /** * ignore and do not report again. * note: non-persistent; only applicable during this program run. **/ ERI_SUPPRESS, /** * exit the program immediately. **/ ERI_EXIT, /** * special return value for the display_error app hook stub to indicate * that it has done nothing and that the normal sys_display_error * implementation should be called instead. **/ ERI_NOT_IMPLEMENTED }; /** * display an error dialog with a message and stack trace. * * @param description text to show. * @param flags: see DebugDisplayErrorFlags. * @param context, lastFuncToSkip: see debug_DumpStack. * @param file, line, func: location of the error (typically passed as * WIDEN(__FILE__), __LINE__, __func__ from a macro) * @param suppress pointer to a caller-allocated flag that can be used to * suppress this error. if NULL, this functionality is skipped and the * "Suppress" dialog button will be disabled. * note: this flag is read and written exclusively here; caller only * provides the storage. values: see DEBUG_SUPPRESS above. * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_DisplayError(const wchar_t* description, size_t flags, void* context, const wchar_t* lastFuncToSkip, const wchar_t* file, int line, const char* func, atomic_bool* suppress); // simplified version for just displaying an error message #define DEBUG_DISPLAY_ERROR_IMPL(description, flags)\ do\ {\ CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE];\ (void)debug_CaptureContext(context);\ (void)debug_DisplayError(description, flags, context, L"debug_DisplayError", WIDEN(__FILE__), __LINE__, __func__, 0);\ }\ while(0) #define DEBUG_DISPLAY_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, 0) // disallow continue for the error. #define DEBUG_DISPLAY_FATAL_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, DE_NO_CONTINUE) // // filtering // /** * debug output is very useful, but "too much of a good thing can kill you". * we don't want to require different LOGn() macros that are enabled * depending on "debug level", because changing that entails lengthy * compiles and it's too coarse-grained. instead, we require all * strings to start with "tag_string|" (exact case and no quotes; * the alphanumeric-only \ identifies output type). * they are then subject to filtering: only if the tag has been * "added" via debug_filter_add is the appendant string displayed. * * this approach is easiest to implement and is fine because we control * all logging code. LIMODS falls from consideration since it's not * portable and too complex. * * notes: * - filter changes only affect subsequent debug_*printf calls; * output that didn't pass the filter is permanently discarded. * - strings not starting with a tag are always displayed. * - debug_filter_* can be called at any time and from the debugger, * but are not reentrant. * * in future, allow output with the given tag to proceed. * no effect if already added. **/ LIB_API void debug_filter_add(const char* tag); /** * in future, discard output with the given tag. * no effect if not currently added. **/ LIB_API void debug_filter_remove(const char* tag); /** * clear all filter state; equivalent to debug_filter_remove for * each tag that was debug_filter_add-ed. **/ LIB_API void debug_filter_clear(); /** * indicate if the given text would be printed. * useful for a series of debug_printfs - avoids needing to add a tag to * each of their format strings. **/ LIB_API bool debug_filter_allows(const char* text); /** * call debug_puts if debug_filter_allows allows the string. **/ LIB_API void debug_puts_filtered(const char* text); /** * write an error description and all logs into crashlog.txt * (in unicode format). * * @param text description of the error (including stack trace); * typically generated by debug_BuildErrorMessage. * * @return Status; ERR::REENTERED if reentered via recursion or * multithreading (not allowed since an infinite loop may result). **/ LIB_API Status debug_WriteCrashlog(const char* text); //----------------------------------------------------------------------------- // assertions //----------------------------------------------------------------------------- /** * ensure the expression \ evaluates to non-zero. used to validate * invariants in the program during development and thus gives a * very helpful warning if something isn't going as expected. * sprinkle these liberally throughout your code! * * to pass more information to users at runtime, you can write * ENSURE(expression && "descriptive string"). **/ #define ENSURE(expr)\ do\ {\ static atomic_bool suppress__;\ if(!(expr))\ {\ switch(debug_OnAssertionFailure(WIDEN(#expr), &suppress__, WIDEN(__FILE__), __LINE__, __func__))\ {\ case ER_CONTINUE:\ break;\ case ER_BREAK:\ default:\ debug_break();\ break;\ }\ }\ }\ while(0) /** * same as ENSURE in debug mode, does nothing in release mode. * (we don't override the `assert' macro because users may * inadvertently include \ afterwards) * (we do not provide an MFC-style VERIFY macro because the distinction * between ENSURE and VERIFY is unclear. to always run code but only * check for success in debug builds without raising unused-variable warnings, * use ASSERT + UNUSED2.) **/ #define ASSERT(expr) ENSURE(expr) #ifdef NDEBUG # undef ASSERT # define ASSERT(expr) #endif /** * display the error dialog with the given text. this is less error-prone than * ENSURE(0 && "text"). note that "conditional expression is constant" warnings * are disabled anyway. * * if being able to suppress the warning is desirable (e.g. for self-tests), * then use DEBUG_WARN_ERR instead. **/ #define debug_warn(expr) ENSURE(0 && (expr)) /** * display the error dialog with text corresponding to the given error code. * used by WARN_RETURN_STATUS_IF_ERR et al., which wrap function calls and automatically * raise warnings and return to the caller. **/ #define DEBUG_WARN_ERR(status)\ do\ {\ static atomic_bool suppress__;\ switch(debug_OnError(status, &suppress__, WIDEN(__FILE__), __LINE__, __func__))\ {\ case ER_CONTINUE:\ break;\ case ER_BREAK:\ default:\ debug_break();\ break;\ }\ }\ while(0) /** * called when a ENSURE/ASSERT fails; * notifies the user via debug_DisplayError. * * @param assert_expr the expression that failed; typically passed as * \#expr in the assert macro. * @param suppress see debug_DisplayError. * @param file, line source file name and line number of the spot that failed * @param func name of the function containing it * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_OnAssertionFailure(const wchar_t* assert_expr, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN; /** * called when a DEBUG_WARN_ERR indicates an error occurred; * notifies the user via debug_DisplayError. * * @param err Status value indicating the error that occurred * @param suppress see debug_DisplayError. * @param file, line source file name and line number of the spot that failed * @param func name of the function containing it * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_OnError(Status err, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN; /** * suppress (prevent from showing) the error dialog from subsequent * debug_OnError for the given Status. * * rationale: for edge cases in some functions, warnings are raised in * addition to returning an error code. self-tests deliberately trigger * these cases and check for the latter but shouldn't cause the former. * we therefore need to squelch them. * * @param err the Status to skip. * * note: only one concurrent skip request is allowed; call * debug_StopSkippingErrors before the next debug_SkipErrors. */ LIB_API void debug_SkipErrors(Status err); /** * @return how many errors were skipped since the call to debug_SkipErrors() **/ LIB_API size_t debug_StopSkippingErrors(); //----------------------------------------------------------------------------- // symbol access //----------------------------------------------------------------------------- namespace ERR { const Status SYM_NO_STACK_FRAMES_FOUND = -100400; const Status SYM_UNRETRIEVABLE_STATIC = -100401; const Status SYM_UNRETRIEVABLE = -100402; const Status SYM_TYPE_INFO_UNAVAILABLE = -100403; const Status SYM_INTERNAL_ERROR = -100404; const Status SYM_UNSUPPORTED = -100405; const Status SYM_CHILD_NOT_FOUND = -100406; // this limit is to prevent infinite recursion. const Status SYM_NESTING_LIMIT = -100407; // this limit is to prevent large symbols (e.g. arrays or linked lists) // from taking up all available output space. const Status SYM_SINGLE_SYMBOL_LIMIT = -100408; } namespace INFO { // one of the dump_sym* functions decided not to output anything at // all (e.g. for member functions in UDTs - we don't want those). // therefore, skip any post-symbol formatting (e.g. ) as well. const Status SYM_SUPPRESS_OUTPUT = +100409; } /** * Maximum number of characters (including null terminator) written to * user's buffers by debug_ResolveSymbol. **/ static const size_t DEBUG_SYMBOL_CHARS = 1000; static const size_t DEBUG_FILE_CHARS = 100; /** * read and return symbol information for the given address. * * NOTE: the PDB implementation is rather slow (~500 us). * * @param ptr_of_interest address of symbol (e.g. function, variable) * @param sym_name optional out; holds at least DEBUG_SYMBOL_CHARS; * receives symbol name returned via debug info. * @param file optional out; holds at least DEBUG_FILE_CHARS; * receives base name only (no path; see rationale in wdbg_sym) of * source file containing the symbol. * @param line optional out; receives source file line number of symbol. * * note: all of the output parameters are optional; we pass back as much * information as is available and desired. * @return Status; INFO::OK iff any information was successfully * retrieved and stored. **/ LIB_API Status debug_ResolveSymbol(void* ptr_of_interest, wchar_t* sym_name, wchar_t* file, int* line); static const size_t DEBUG_CONTEXT_SIZE = 2048; // Win32 CONTEXT is currently 1232 bytes /** * @param context must point to an instance of the platform-specific type * (e.g. CONTEXT) or CACHE_ALIGNED storage of DEBUG_CONTEXT_SIZE bytes. **/ LIB_API Status debug_CaptureContext(void* context); /** * write a complete stack trace (including values of local variables) into * the specified buffer. * * @param buf Target buffer. * @param maxChars Max chars of buffer (should be several thousand). * @param context Platform-specific representation of execution state * (e.g. Win32 CONTEXT). either specify an SEH exception's * context record or use debug_CaptureContext to retrieve the current state. * Rationale: intermediates such as debug_DisplayError change the * context, so it should be captured as soon as possible. * @param lastFuncToSkip Is used for omitting error-reporting functions like * debug_OnAssertionFailure from the stack trace. It is either 0 (skip nothing) or * a substring of a function's name (this allows platform-independent * matching of stdcall-decorated names). * Rationale: this is safer than specifying a fixed number of frames, * which can be incorrect due to inlining. * @return Status; ERR::REENTERED if reentered via recursion or * multithreading (not allowed since static data is used). **/ LIB_API Status debug_DumpStack(wchar_t* buf, size_t maxChars, void* context, const wchar_t* lastFuncToSkip); //----------------------------------------------------------------------------- // helper functions (used by implementation) //----------------------------------------------------------------------------- /** * [system-dependent] write a string to the debug channel. * this can be quite slow (~1 ms)! On Windows, it uses OutputDebugString * (entails context switch), otherwise stdout+fflush (waits for IO). **/ LIB_API void debug_puts(const char* text); /** * return the caller of a certain function on the call stack. * * this function is useful for recording (partial) stack traces for * memory allocation tracking, etc. * * @param context, lastFuncToSkip - see debug_DumpStack * @return address of the caller **/ LIB_API void* debug_GetCaller(void* context, const wchar_t* lastFuncToSkip); /** * check if a pointer appears to be totally invalid. * * this check is not authoritative (the pointer may be "valid" but incorrect) * but can be used to filter out obviously wrong values in a portable manner. * * @param p pointer * @return 1 if totally bogus, otherwise 0. **/ LIB_API int debug_IsPointerBogus(const void* p); /// does the given pointer appear to point to code? LIB_API bool debug_IsCodePointer(void* p); /// does the given pointer appear to point to the stack? LIB_API bool debug_IsStackPointer(void* p); /** * inform the debugger of the current thread's name. * * (threads are easier to keep apart when they are identified by * name rather than TID.) **/ LIB_API void debug_SetThreadName(const char* name); - -/** - * holds memory for an error message. - **/ -struct ErrorMessageMem -{ - // rationale: - // - error messages with stack traces require a good deal of memory - // (hundreds of KB). static buffers of that size are undesirable. - // - the heap may be corrupted, so don't use malloc. - // instead, "lib/sysdep/vm.h" functions should be safe. - // - alloca is a bit iffy (the stack may be maxed out), non-portable and - // complicates the code because it can't be allocated by a subroutine. - // - this method is probably slow, but error messages aren't built often. - // if necessary, first try malloc and use mmap if that fails. - void* pa_mem; -}; - -/** - * free memory from the error message. - * - * @param emm ErrorMessageMem* - **/ -LIB_API void debug_FreeErrorMessage(ErrorMessageMem* emm); - /** * build a string describing the given error. * * this is a helper function used by debug_DumpStack and is made available * so that the self-test doesn't have to display the error dialog. * * @param description: general description of the problem. * @param fn_only filename (no path) of source file that triggered the error. * @param line, func: exact position of the error. * @param context, lastFuncToSkip: see debug_DumpStack. - * @param emm memory for the error message. caller should allocate - * stack memory and set alloc_buf*; if not, there will be no - * fallback in case heap alloc fails. should be freed via - * debug_FreeErrorMessage when no longer needed. **/ -LIB_API const wchar_t* debug_BuildErrorMessage(const wchar_t* description, const wchar_t* fn_only, int line, const char* func, void* context, const wchar_t* lastFuncToSkip, ErrorMessageMem* emm); +LIB_API const wchar_t* debug_BuildErrorMessage(const wchar_t* description, const wchar_t* fn_only, int line, const char* func, void* context, const wchar_t* lastFuncToSkip); #endif // #ifndef INCLUDED_DEBUG Index: ps/trunk/source/lib/sysdep/os/win/tests/test_wdbg_sym.h =================================================================== --- ps/trunk/source/lib/sysdep/os/win/tests/test_wdbg_sym.h (revision 25299) +++ ps/trunk/source/lib/sysdep/os/win/tests/test_wdbg_sym.h (revision 25300) @@ -1,326 +1,319 @@ /* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // note: this is more of an on-demand display of the stack trace than // self-test of it. // TODO: compare against known-good result? // problem: results may differ by compiler (e.g. due to differing STL) #include "lib/self_test.h" #include #include #include #include #include #include "lib/bits.h" #include "lib/code_annotation.h" #include "lib/sysdep/os/win/win.h" // HWND #include "lib/sysdep/sysdep.h" #include "lib/sysdep/os/win/wdbg_sym.h" #include "lib/external_libraries/dbghelp.h" static void* callers[100]; static size_t numCallers; static Status OnFrame(const _tagSTACKFRAME64* frame, uintptr_t UNUSED(cbData)) { callers[numCallers++] = (void*)frame->AddrPC.Offset; return INFO::OK; } #pragma optimize("", off) #pragma warning(disable:4748) // /GS can not protect [..] from local buffer overrun because optimizations are disabled // (these must be outside of TestWdbgSym so that we can simply // search for the function's name as a substring within the ILT // decorated name (which omits the :: scope resolution operator, // while debug_ResolveSymbol for the function does not) __declspec(noinline) static void Func1() { CONTEXT context; UNUSED2(debug_CaptureContext(&context)); wdbg_sym_WalkStack(OnFrame, 0, context); } __declspec(noinline) static void Func2() { Func1(); } __declspec(noinline) static void Func3() { Func2(); } class TestWdbgSym : public CxxTest::TestSuite { // m_test_array might get inlined and that messes // with the test so in order to protect the stack // trace we have to prevent it. __declspec(noinline) static void m_test_array() { struct Small { int i1; int i2; }; struct Large { double d1; double d2; double d3; double d4; }; Large large_array_of_large_structs[8] = { { 0.0,0.0,0.0,0.0 } }; Large small_array_of_large_structs[2] = { { 0.0,0.0,0.0,0.0 } }; Small large_array_of_small_structs[8] = { { 1,2 } }; Small small_array_of_small_structs[2] = { { 1,2 } }; int ints[] = { 1,2,3,4,5 }; wchar_t chars[] = { 'w','c','h','a','r','s',0 }; wchar_t many_wchars[1024]; memset(many_wchars, 'a', sizeof(many_wchars)); debug_printf("\n(dumping stack frames may result in access violations...)\n"); debug_printf(" locals: %.0d %.0c %.0c %.0g %.0g %.0d %.0d\n", ints[0], chars[0], many_wchars[0], large_array_of_large_structs[0].d1, small_array_of_large_structs[0].d1, large_array_of_small_structs[0].i1, small_array_of_small_structs[0].i1); // note: we don't want any kind of dialog to be raised, because // this test now always runs. therefore, just make sure a decent // amount of text (not just "(failed)" error messages) was produced. - ErrorMessageMem emm = {0}; + CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE]; TS_ASSERT_EQUALS(debug_CaptureContext(context), INFO::OK); - const wchar_t* text = debug_BuildErrorMessage(L"dummy", 0,0,0, context, L"m_test_array", &emm); + const wchar_t* text = debug_BuildErrorMessage(L"dummy", 0, 0, 0, context, L"m_test_array"); TS_ASSERT(wcslen(text) > 500); -#if 0 - { - std::wofstream s(L"d:\\out.txt"); - s << text; - } -#endif - debug_FreeErrorMessage(&emm); debug_printf("(done dumping stack frames)\n"); } // also used by test_stl as an element type struct Nested { int nested_member; struct Nested* self_ptr; }; static void m_test_udt() { Nested nested = { 123 }; nested.self_ptr = &nested; typedef struct { u8 s1; u8 s2; char s3; } Small; Small small__ = { 0x55, 0xaa, -1 }; UNUSED2(small__); struct Large { u8 large_member_u8; std::string large_member_string; double large_member_double; } large = { 0xff, "large struct string", 123456.0 }; UNUSED2(large); class Base { int base_int; std::wstring base_wstring; public: Base() : base_int(123), base_wstring(L"base wstring") { } }; class Derived : private Base { double derived_double; public: Derived() : derived_double(-1.0) { } } derived; m_test_array(); } // STL containers and their contents static void m_test_stl() { std::vector v_wstring; v_wstring.push_back(L"ws1"); v_wstring.push_back(L"ws2"); std::deque d_int; d_int.push_back(1); d_int.push_back(2); d_int.push_back(3); std::deque d_string; d_string.push_back("a"); d_string.push_back("b"); d_string.push_back("c"); std::list l_float; l_float.push_back(0.1f); l_float.push_back(0.2f); l_float.push_back(0.3f); l_float.push_back(0.4f); std::map m_string_int; m_string_int.insert(std::make_pair("s5", 5)); m_string_int.insert(std::make_pair("s6", 6)); m_string_int.insert(std::make_pair("s7", 7)); std::map m_int_string; m_int_string.insert(std::make_pair(1, "s1")); m_int_string.insert(std::make_pair(2, "s2")); m_int_string.insert(std::make_pair(3, "s3")); std::map m_int_int; m_int_int.insert(std::make_pair(1, 1)); m_int_int.insert(std::make_pair(2, 2)); m_int_int.insert(std::make_pair(3, 3)); std::set s_uintptr; s_uintptr.insert(0x123); s_uintptr.insert(0x456); // empty std::deque d_u8_empty; std::list l_nested_empty; std::map m_double_empty; std::multimap mm_int_empty; std::set s_uint_empty; std::multiset ms_char_empty; std::vector v_double_empty; std::queue q_double_empty; std::stack st_double_empty; std::string str_empty; std::wstring wstr_empty; m_test_udt(); // uninitialized std::deque d_u8_uninit; std::list l_nested_uninit; std::map m_double_uninit; std::multimap mm_int_uninit; std::set s_uint_uninit; std::multiset ms_char_uninit; std::vector v_double_uninit; std::queue q_double_uninit; std::stack st_double_uninit; std::string str_uninit; std::wstring wstr_uninit; } // also exercises all basic types because we need to display some values // anyway (to see at a glance whether symbol engine addrs are correct) static void m_test_addrs(int p_int, double p_double, char* p_pchar, uintptr_t p_uintptr) { size_t l_uint = 0x1234; bool l_bool = true; UNUSED2(l_bool); wchar_t l_wchars[] = L"wchar string"; enum TestEnum { VAL1=1, VAL2=2 } l_enum = VAL1; u8 l_u8s[] = { 1,2,3,4 }; void (*l_funcptr)(void) = m_test_stl; static double s_double = -2.718; static char s_chars[] = {'c','h','a','r','s',0}; static void (*s_funcptr)(int, double, char*, uintptr_t) = m_test_addrs; static void* s_ptr = (void*)(uintptr_t)0x87654321; static HDC s_hdc = (HDC)0xff0; #if 0 // output only needed when debugging debug_printf("\nTEST_ADDRS\n"); debug_printf("p_int addr=%p val=%d\n", &p_int, p_int); debug_printf("p_double addr=%p val=%g\n", &p_double, p_double); debug_printf("p_pchar addr=%p val=%s\n", &p_pchar, p_pchar); debug_printf("p_uintptr addr=%p val=%lu\n", &p_uintptr, p_uintptr); debug_printf("l_uint addr=%p val=%u\n", &l_uint, l_uint); debug_printf("l_wchars addr=%p val=%s\n", &l_wchars, utf8_from_wstring(l_wchars)); debug_printf("l_enum addr=%p val=%d\n", &l_enum, l_enum); debug_printf("l_u8s addr=%p val=%d\n", &l_u8s, l_u8s); debug_printf("l_funcptr addr=%p val=%p\n", &l_funcptr, l_funcptr); #else UNUSED2(p_uintptr); UNUSED2(p_pchar); UNUSED2(p_double); UNUSED2(p_int); UNUSED2(l_funcptr); UNUSED2(l_enum); UNUSED2(l_uint); UNUSED2(l_u8s); UNUSED2(l_wchars); #endif m_test_stl(); int uninit_int; UNUSED2(uninit_int); float uninit_float; UNUSED2(uninit_float); double uninit_double; UNUSED2(uninit_double); bool uninit_bool; UNUSED2(uninit_bool); HWND uninit_hwnd; UNUSED2(uninit_hwnd); } #pragma optimize("", on) public: void test_stack_trace() { m_test_addrs(123, 3.1415926535897932384626, "pchar string", 0xf00d); } void test_stack_walk() { Status ret; Func3(); TS_ASSERT(numCallers >= 3); size_t foundFunctionBits = 0; void* funcAddresses[3] = { (void*)&Func1, (void*)&Func2, (void*)&Func3 }; for(size_t idxCaller = 0; idxCaller < numCallers; idxCaller++) { wchar_t callerName[DEBUG_SYMBOL_CHARS]; ret = debug_ResolveSymbol(callers[idxCaller], callerName, 0, 0); TS_ASSERT_OK(ret); wchar_t funcName[DEBUG_SYMBOL_CHARS]; for(size_t idxFunc = 0; idxFunc < ARRAY_SIZE(funcAddresses); idxFunc++) { ret = debug_ResolveSymbol(funcAddresses[idxFunc], funcName, 0, 0); TS_ASSERT_OK(ret); if(wcsstr(funcName, callerName)) foundFunctionBits |= BIT(idxFunc); } } TS_ASSERT(foundFunctionBits == bit_mask(ARRAY_SIZE(funcAddresses))); } }; Index: ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp =================================================================== --- ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp (revision 25299) +++ ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp (revision 25300) @@ -1,636 +1,642 @@ /* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Windows backend of the sysdep interface */ #include "precompiled.h" #include "lib/sysdep/sysdep.h" #include "lib/alignment.h" #include "lib/sysdep/os/win/win.h" // includes windows.h; must come before shlobj #include // SDL_SetClipboardText #include // pick_dir #include // open_url #include #include // message crackers #include #include "lib/sysdep/os/win/error_dialog.h" #include "lib/sysdep/os/win/wutil.h" #if CONFIG_ENABLE_BOOST # include #endif #if MSC_VERSION #pragma comment(lib, "shell32.lib") // for sys_pick_directory SH* calls #pragma comment(lib, "winhttp.lib") #endif bool sys_IsDebuggerPresent() { return (IsDebuggerPresent() != 0); } std::wstring sys_WideFromArgv(const char* argv_i) { // NB: despite http://cbloomrants.blogspot.com/2008/06/06-14-08-1.html, // WinXP x64 EN cmd.exe (chcp reports 437) encodes argv u-umlaut // (entered manually or via auto-complete) via cp1252. the same applies // to WinXP SP2 DE (where chcp reports 850). const UINT cp = CP_ACP; const DWORD flags = MB_PRECOMPOSED|MB_ERR_INVALID_CHARS; const int inputSize = -1; // null-terminated std::vector buf(strlen(argv_i)+1); // (upper bound on number of characters) // NB: avoid mbstowcs because it may specify another locale const int ret = MultiByteToWideChar(cp, flags, argv_i, (int)inputSize, &buf[0], (int)buf.size()); ENSURE(ret != 0); return std::wstring(&buf[0]); } void sys_display_msg(const wchar_t* caption, const wchar_t* msg) { MessageBoxW(0, msg, caption, MB_ICONEXCLAMATION|MB_TASKMODAL|MB_SETFOREGROUND); } //----------------------------------------------------------------------------- // "program error" dialog (triggered by ENSURE and exception) //----------------------------------------------------------------------------- // support for resizing the dialog / its controls (must be done manually) static POINTS dlg_clientOrigin; static POINTS dlg_prevClientSize; static void dlg_OnMove(HWND UNUSED(hDlg), int x, int y) { dlg_clientOrigin.x = (short)x; dlg_clientOrigin.y = (short)y; } static const size_t ANCHOR_LEFT = 0x01; static const size_t ANCHOR_RIGHT = 0x02; static const size_t ANCHOR_TOP = 0x04; static const size_t ANCHOR_BOTTOM = 0x08; static const size_t ANCHOR_ALL = 0x0F; static void dlg_ResizeControl(HWND hDlg, int dlgItem, int dx, int dy, size_t anchors) { HWND hControl = GetDlgItem(hDlg, dlgItem); RECT r; GetWindowRect(hControl, &r); int w = r.right - r.left, h = r.bottom - r.top; int x = r.left - dlg_clientOrigin.x, y = r.top - dlg_clientOrigin.y; if(anchors & ANCHOR_RIGHT) { // right only if(!(anchors & ANCHOR_LEFT)) x += dx; // horizontal (stretch width) else w += dx; } if(anchors & ANCHOR_BOTTOM) { // bottom only if(!(anchors & ANCHOR_TOP)) y += dy; // vertical (stretch height) else h += dy; } SetWindowPos(hControl, 0, x,y, w,h, SWP_NOZORDER); } static void dlg_OnSize(HWND hDlg, UINT state, int clientSizeX, int clientSizeY) { // 'minimize' was clicked. we need to ignore this, otherwise // dx/dy would reduce some control positions to less than 0. // since Windows clips them, we wouldn't later be able to // reconstruct the previous values when 'restoring'. if(state == SIZE_MINIMIZED) return; // NB: origin might legitimately be 0, but we know it is invalid // on the first call to this function, where dlg_prevClientSize is 0. const bool isOriginValid = (dlg_prevClientSize.y != 0); const int dx = clientSizeX - dlg_prevClientSize.x; const int dy = clientSizeY - dlg_prevClientSize.y; dlg_prevClientSize.x = (short)clientSizeX; dlg_prevClientSize.y = (short)clientSizeY; if(!isOriginValid) // must not call dlg_ResizeControl return; dlg_ResizeControl(hDlg, IDC_CONTINUE, dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_SUPPRESS, dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_BREAK , dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_EXIT , dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_COPY , dx,dy, ANCHOR_RIGHT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_EDIT1 , dx,dy, ANCHOR_ALL); } static void dlg_OnGetMinMaxInfo(HWND UNUSED(hDlg), LPMINMAXINFO mmi) { // we must make sure resize_control will never set negative coords - // Windows would clip them, and its real position would be lost. // restrict to a reasonable and good looking minimum size [pixels]. mmi->ptMinTrackSize.x = 407; mmi->ptMinTrackSize.y = 159; // determined experimentally } struct DialogParams { const wchar_t* text; size_t flags; }; static BOOL dlg_OnInitDialog(HWND hDlg, HWND UNUSED(hWndFocus), LPARAM lParam) { const DialogParams* params = reinterpret_cast(lParam); SetWindowLongPtr(hDlg, DWLP_USER, lParam); HWND hWnd; // need to reset for new instance of dialog dlg_clientOrigin.x = dlg_clientOrigin.y = 0; dlg_prevClientSize.x = dlg_prevClientSize.y = 0; if(!(params->flags & DE_ALLOW_SUPPRESS)) { hWnd = GetDlgItem(hDlg, IDC_SUPPRESS); EnableWindow(hWnd, FALSE); } if(params->flags & DE_NO_CONTINUE) { hWnd = GetDlgItem(hDlg, IDC_CONTINUE); EnableWindow(hWnd, FALSE); } // set fixed font for readability hWnd = GetDlgItem(hDlg, IDC_EDIT1); HGDIOBJ hObj = (HGDIOBJ)GetStockObject(SYSTEM_FIXED_FONT); LPARAM redraw = FALSE; SendMessage(hWnd, WM_SETFONT, (WPARAM)hObj, redraw); SetDlgItemTextW(hDlg, IDC_EDIT1, params->text); return TRUE; // set default keyboard focus } static void dlg_OnCommand(HWND hDlg, int id, HWND UNUSED(hWndCtl), UINT UNUSED(codeNotify)) { switch(id) { case IDC_COPY: { std::vector buf(128*KiB); // (too big for stack) GetDlgItemTextW(hDlg, IDC_EDIT1, &buf[0], (int)buf.size()); std::string string = utf8_from_wstring(&buf[0]); SDL_SetClipboardText(string.c_str()); break; } case IDC_CONTINUE: EndDialog(hDlg, ERI_CONTINUE); break; case IDC_SUPPRESS: EndDialog(hDlg, ERI_SUPPRESS); break; case IDC_BREAK: EndDialog(hDlg, ERI_BREAK); break; case IDC_EXIT: EndDialog(hDlg, ERI_EXIT); break; default: break; } } static void dlg_OnClose(HWND hDlg) { const DialogParams* params = reinterpret_cast( GetWindowLongPtr(hDlg, DWLP_USER)); if (!params) return; // Interpret close as exit in case we can't continue. if(params->flags & DE_NO_CONTINUE) EndDialog(hDlg, ERI_EXIT); } static void dlg_OnSysCommand(HWND hDlg, UINT cmd, int UNUSED(x), int UNUSED(y)) { switch(cmd & 0xFFF0) // NB: lower 4 bits are reserved { // [X] clicked -> close dialog (doesn't happen automatically) case SC_CLOSE: EndDialog(hDlg, 0); break; default: break; } } static INT_PTR CALLBACK dlg_OnMessage(HWND hDlg, unsigned int msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_INITDIALOG: return HANDLE_WM_INITDIALOG(hDlg, wParam, lParam, dlg_OnInitDialog); case WM_SYSCOMMAND: return HANDLE_WM_SYSCOMMAND(hDlg, wParam, lParam, dlg_OnSysCommand); case WM_COMMAND: return HANDLE_WM_COMMAND(hDlg, wParam, lParam, dlg_OnCommand); case WM_MOVE: return HANDLE_WM_MOVE(hDlg, wParam, lParam, dlg_OnMove); case WM_GETMINMAXINFO: return HANDLE_WM_GETMINMAXINFO(hDlg, wParam, lParam, dlg_OnGetMinMaxInfo); case WM_SIZE: return HANDLE_WM_SIZE(hDlg, wParam, lParam, dlg_OnSize); case WM_CLOSE: return HANDLE_WM_CLOSE(hDlg, wParam, lParam, dlg_OnClose); default: // we didn't process the message; caller will perform default action. return FALSE; } } ErrorReactionInternal sys_display_error(const wchar_t* text, size_t flags) { // note: other threads might still be running, crash and take down the // process before we have a chance to display this error message. // ideally we would suspend them all and resume when finished; however, // they may be holding system-wide locks (e.g. heap or loader) that // are potentially needed by DialogBoxParam. in that case, deadlock // would result; this is much worse than a crash because no error // at all is displayed to the end-user. therefore, do nothing here. // temporarily remove any pending quit message from the queue because // it would prevent the dialog from being displayed (DialogBoxParam // returns IDOK without doing anything). will be restored below. // notes: // - this isn't only relevant at exit - Windows also posts one if // window init fails. therefore, it is important that errors can be // displayed regardless. // - by passing hWnd=0, we check all windows belonging to the current // thread. there is no reason to use hWndParent below. MSG msg; const BOOL isQuitPending = PeekMessage(&msg, 0, WM_QUIT, WM_QUIT, PM_REMOVE); const HINSTANCE hInstance = wutil_LibModuleHandle(); LPCWSTR lpTemplateName = MAKEINTRESOURCEW(IDD_DIALOG1); const DialogParams params = { text, flags }; // get the enclosing app's window handle. we can't just pass 0 or // the desktop window because the dialog must be modal (if the app // continues running, it may crash and take down the process before // we've managed to show the dialog). const HWND hWndParent = wutil_AppWindow(); INT_PTR ret = DialogBoxParamW(hInstance, lpTemplateName, hWndParent, dlg_OnMessage, (LPARAM)¶ms); if(isQuitPending) PostQuitMessage((int)msg.wParam); // failed; warn user and make sure we return an ErrorReactionInternal. if(ret == 0 || ret == -1) { debug_DisplayMessage(L"Error", L"Unable to display detailed error dialog."); return ERI_CONTINUE; } return (ErrorReactionInternal)ret; } //----------------------------------------------------------------------------- // misc //----------------------------------------------------------------------------- Status sys_StatusDescription(int user_err, wchar_t* buf, size_t max_chars) { // validate user_err - Win32 doesn't have negative error numbers if(user_err < 0) return ERR::FAIL; // NOWARN - const DWORD err = user_err? (DWORD)user_err : GetLastError(); + const DWORD errorCode = user_err? (DWORD)user_err : GetLastError(); // no one likes to see "The operation completed successfully" in // error messages, so return more descriptive text instead. - if(err == 0) + if(errorCode == 0) { wcscpy_s(buf, max_chars, L"0 (no error code was set)"); return INFO::OK; } wchar_t message[400]; + if(errorCode == ERROR_NOT_ENOUGH_MEMORY) + { + // We handle out of memory separately to prevent possible subsequent/nested allocations. + swprintf_s(message, ARRAY_SIZE(message), L"Not enough memory resources are available to process this command."); + } + else { const LPCVOID source = 0; // ignored (we're not using FROM_HMODULE etc.) const DWORD lang_id = 0; // look for neutral, then current locale va_list* args = 0; // we don't care about "inserts" - const DWORD charsWritten = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, source, err, lang_id, message, (DWORD)ARRAY_SIZE(message), args); + const DWORD charsWritten = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, source, errorCode, lang_id, message, (DWORD)ARRAY_SIZE(message), args); if(!charsWritten) WARN_RETURN(ERR::FAIL); ENSURE(charsWritten < max_chars); if(message[charsWritten-1] == '\n') message[charsWritten-1] = '\0'; if(message[charsWritten-2] == '\r') message[charsWritten-2] = '\0'; } - const int charsWritten = swprintf_s(buf, max_chars, L"%d (%ls)", err, message); + const int charsWritten = swprintf_s(buf, max_chars, L"%d (%ls)", errorCode, message); ENSURE(charsWritten != -1); return INFO::OK; } static Status GetModulePathname(HMODULE hModule, OsPath& pathname) { wchar_t pathnameBuf[32768]; // NTFS limit const DWORD length = (DWORD)ARRAY_SIZE(pathnameBuf); const DWORD charsWritten = GetModuleFileNameW(hModule, pathnameBuf, length); if(charsWritten == 0) // failed WARN_RETURN(StatusFromWin()); ENSURE(charsWritten < length); // why would the above buffer ever be exceeded? pathname = pathnameBuf; return INFO::OK; } Status sys_get_module_filename(void* addr, OsPath& pathname) { MEMORY_BASIC_INFORMATION mbi; const SIZE_T bytesWritten = VirtualQuery(addr, &mbi, sizeof(mbi)); if(!bytesWritten) WARN_RETURN(StatusFromWin()); ENSURE(bytesWritten >= sizeof(mbi)); return GetModulePathname((HMODULE)mbi.AllocationBase, pathname); } OsPath sys_ExecutablePathname() { WinScopedPreserveLastError s; OsPath pathname; ENSURE(GetModulePathname(0, pathname) == INFO::OK); return pathname; } std::wstring sys_get_user_name() { wchar_t usernameBuf[256]; DWORD size = ARRAY_SIZE(usernameBuf); if(!GetUserNameW(usernameBuf, &size)) return L""; return usernameBuf; } // callback for shell directory picker: used to set starting directory // (for user convenience). static int CALLBACK BrowseCallback(HWND hWnd, unsigned int msg, LPARAM UNUSED(lParam), LPARAM lpData) { if(msg == BFFM_INITIALIZED) { const WPARAM wParam = TRUE; // lpData is a Unicode string, not PIDL. // (MSDN: the return values for both of these BFFM_ notifications are ignored) (void)SendMessage(hWnd, BFFM_SETSELECTIONW, wParam, lpData); } return 0; } Status sys_pick_directory(OsPath& path) { // (must not use multi-threaded apartment due to BIF_NEWDIALOGSTYLE) const HRESULT hr = CoInitialize(0); ENSURE(hr == S_OK || hr == S_FALSE); // S_FALSE == already initialized // note: bi.pszDisplayName isn't the full path, so it isn't of any use. BROWSEINFOW bi; memset(&bi, 0, sizeof(bi)); bi.ulFlags = BIF_RETURNONLYFSDIRS|BIF_NEWDIALOGSTYLE|BIF_NONEWFOLDERBUTTON; // for setting starting directory: bi.lpfn = (BFFCALLBACK)BrowseCallback; const Path::String initialPath = OsString(path); // NB: BFFM_SETSELECTIONW can't deal with '/' separators bi.lParam = (LPARAM)initialPath.c_str(); const LPITEMIDLIST pidl = SHBrowseForFolderW(&bi); if(!pidl) // user canceled return INFO::SKIPPED; // translate ITEMIDLIST to string wchar_t pathBuf[MAX_PATH]; // mandated by SHGetPathFromIDListW const BOOL ok = SHGetPathFromIDListW(pidl, pathBuf); // free the ITEMIDLIST CoTaskMemFree(pidl); if(ok == TRUE) { path = pathBuf; return INFO::OK; } // Balance call to CoInitialize, which must have been successful CoUninitialize(); WARN_RETURN(StatusFromWin()); } Status sys_open_url(const std::string& url) { HINSTANCE r = ShellExecuteA(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); if ((int)(intptr_t)r > 32) return INFO::OK; WARN_RETURN(ERR::FAIL); } Status sys_generate_random_bytes(u8* buffer, size_t size) { HCRYPTPROV hCryptProv = 0; if(!CryptAcquireContext(&hCryptProv, 0, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) WARN_RETURN(StatusFromWin()); memset(buffer, 0, size); if(!CryptGenRandom(hCryptProv, (DWORD)size, (BYTE*)buffer)) WARN_RETURN(StatusFromWin()); if(!CryptReleaseContext(hCryptProv, 0)) WARN_RETURN(StatusFromWin()); return INFO::OK; } #if CONFIG_ENABLE_BOOST /* * Given a string of the form * "example.com:80" * or * "ftp=ftp.example.com:80;http=example.com:80;https=example.com:80" * separated by semicolons or whitespace, * return the string "example.com:80". */ static std::wstring parse_proxy(const std::wstring& input) { if(input.find('=') == input.npos) return input; std::vector parts; split(parts, input, boost::algorithm::is_any_of("; \t\r\n"), boost::algorithm::token_compress_on); for(size_t i = 0; i < parts.size(); ++i) if(boost::algorithm::starts_with(parts[i], "http=")) return parts[i].substr(5); // If we got this far, proxies were only set for non-HTTP protocols return L""; } Status sys_get_proxy_config(const std::wstring& url, std::wstring& proxy) { WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions; memset(&autoProxyOptions, 0, sizeof(autoProxyOptions)); autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT; autoProxyOptions.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A; autoProxyOptions.fAutoLogonIfChallenged = TRUE; WINHTTP_PROXY_INFO proxyInfo; memset(&proxyInfo, 0, sizeof(proxyInfo)); WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig; memset(&ieConfig, 0, sizeof(ieConfig)); HINTERNET hSession = NULL; Status err = INFO::SKIPPED; bool useAutoDetect; if(WinHttpGetIEProxyConfigForCurrentUser(&ieConfig)) { if(ieConfig.lpszAutoConfigUrl) { // Use explicit auto-config script if specified useAutoDetect = true; autoProxyOptions.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL; autoProxyOptions.lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl; } else { // Use auto-discovery if enabled useAutoDetect = (ieConfig.fAutoDetect == TRUE); } } else { // Can't find IE config settings - fall back to auto-discovery useAutoDetect = true; } if(useAutoDetect) { hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if(hSession && WinHttpGetProxyForUrl(hSession, url.c_str(), &autoProxyOptions, &proxyInfo) && proxyInfo.lpszProxy) { proxy = parse_proxy(proxyInfo.lpszProxy); if(!proxy.empty()) { err = INFO::OK; goto done; } } } // No valid auto-config; try explicit proxy instead if(ieConfig.lpszProxy) { proxy = parse_proxy(ieConfig.lpszProxy); if(!proxy.empty()) { err = INFO::OK; goto done; } } done: if(ieConfig.lpszProxy) GlobalFree(ieConfig.lpszProxy); if(ieConfig.lpszProxyBypass) GlobalFree(ieConfig.lpszProxyBypass); if(ieConfig.lpszAutoConfigUrl) GlobalFree(ieConfig.lpszAutoConfigUrl); if(proxyInfo.lpszProxy) GlobalFree(proxyInfo.lpszProxy); if(proxyInfo.lpszProxyBypass) GlobalFree(proxyInfo.lpszProxyBypass); if(hSession) WinHttpCloseHandle(hSession); return err; } #endif FILE* sys_OpenFile(const OsPath& pathname, const char* mode) { FILE* f = 0; const std::wstring wmode(mode, mode+strlen(mode)); (void)_wfopen_s(&f, OsString(pathname).c_str(), wmode.c_str()); return f; }