1 /* Copyright (C) 2021 Wildfire Games.
3 * Permission is hereby granted, free of charge, to any person obtaining
4 * a copy of this software and associated documentation files (the
5 * "Software"), to deal in the Software without restriction, including
6 * without limitation the rights to use, copy, modify, merge, publish,
7 * distribute, sublicense, and/or sell copies of the Software, and to
8 * permit persons to whom the Software is furnished to do so, subject to
9 * the following conditions:
11 * The above copyright notice and this permission notice shall be included
12 * in all copies or substantial portions of the Software.
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 #include "precompiled.h"
24 #include "lib/debug.h"
26 #include "lib/alignment.h"
27 #include "lib/app_hooks.h"
28 #include "lib/fnv_hash.h"
29 #include "lib/sysdep/cpu.h" // cpu_CAS
30 #include "lib/sysdep/sysdep.h"
31 #include "lib/sysdep/vm.h"
34 # include "lib/sysdep/os/win/wdbg_heap.h"
44 // (NB: this may appear obscene, but deep stack traces have been
45 // observed to take up > 256 KiB)
46 constexpr std::size_t MESSAGE_SIZE
= 512 * KiB
/ sizeof(wchar_t);
47 wchar_t g_MessageBuffer
[MESSAGE_SIZE
];
49 } // anonymous namespace
51 static const StatusDefinition debugStatusDefinitions
[] = {
52 { ERR::SYM_NO_STACK_FRAMES_FOUND
, L
"No stack frames found" },
53 { ERR::SYM_UNRETRIEVABLE_STATIC
, L
"Value unretrievable (stored in external module)" },
54 { ERR::SYM_UNRETRIEVABLE
, L
"Value unretrievable" },
55 { ERR::SYM_TYPE_INFO_UNAVAILABLE
, L
"Error getting type_info" },
56 { ERR::SYM_INTERNAL_ERROR
, L
"Exception raised while processing a symbol" },
57 { ERR::SYM_UNSUPPORTED
, L
"Symbol type not (fully) supported" },
58 { ERR::SYM_CHILD_NOT_FOUND
, L
"Symbol does not have the given child" },
59 { ERR::SYM_NESTING_LIMIT
, L
"Symbol nesting too deep or infinite recursion" },
60 { ERR::SYM_SINGLE_SYMBOL_LIMIT
, L
"Symbol has produced too much output" },
61 { INFO::SYM_SUPPRESS_OUTPUT
, L
"Symbol was suppressed" }
63 STATUS_ADD_DEFINITIONS(debugStatusDefinitions
);
66 // need to shoehorn printf-style variable params into
67 // the OutputDebugString call.
68 // - don't want to split into multiple calls - would add newlines to output.
69 // - fixing Win32 _vsnprintf to return # characters that would be written,
70 // as required by C99, looks difficult and unnecessary. if any other code
71 // needs that, implement GNU vasprintf.
72 // - fixed size buffers aren't nice, but much simpler than vasprintf-style
73 // allocate+expand_until_it_fits. these calls are for quick debug output,
74 // not loads of data, anyway.
76 // rationale: static data instead of std::set to allow setting at any time.
77 // we store FNV hash of tag strings for fast comparison; collisions are
78 // extremely unlikely and can only result in displaying more/less text.
79 static const size_t MAX_TAGS
= 20;
80 static u32 tags
[MAX_TAGS
];
81 static size_t num_tags
;
83 void debug_filter_add(const char* tag
)
85 const u32 hash
= fnv_hash(tag
, strlen(tag
)*sizeof(tag
[0]));
87 // make sure it isn't already in the list
88 for(size_t i
= 0; i
< MAX_TAGS
; i
++)
93 if(num_tags
== MAX_TAGS
)
95 DEBUG_WARN_ERR(ERR::LOGIC
); // increase MAX_TAGS
99 tags
[num_tags
++] = hash
;
102 void debug_filter_remove(const char* tag
)
104 const u32 hash
= fnv_hash(tag
, strlen(tag
)*sizeof(tag
[0]));
106 for(size_t i
= 0; i
< MAX_TAGS
; i
++)
108 if(tags
[i
] == hash
) // found it
110 // replace with last element (avoid holes)
111 tags
[i
] = tags
[MAX_TAGS
-1];
114 // can only happen once, so we're done.
120 void debug_filter_clear()
122 std::fill(tags
, tags
+MAX_TAGS
, 0);
125 bool debug_filter_allows(const char* text
)
130 // no | found => no tag => should always be displayed
131 if(text
[i
] == ' ' || text
[i
] == '\0')
133 if(text
[i
] == '|' && i
!= 0)
137 const u32 hash
= fnv_hash(text
, i
*sizeof(text
[0]));
139 // check if entry allowing this tag is found
140 for(i
= 0; i
< MAX_TAGS
; i
++)
147 #undef debug_printf // allowing #defining it out
148 void debug_printf(const char* fmt
, ...)
154 const int numChars
= vsprintf_s(buf
, ARRAY_SIZE(buf
), fmt
, ap
);
156 debug_break(); // poor man's assert - avoid infinite loop because ENSURE also uses debug_printf
159 debug_puts_filtered(buf
);
162 void debug_puts_filtered(const char* text
)
164 if(debug_filter_allows(text
))
169 //-----------------------------------------------------------------------------
171 Status
debug_WriteCrashlog(const wchar_t* text
)
173 // (avoid infinite recursion and/or reentering this function if it
174 // fails/reports an error)
181 // note: the initial state is IDLE. we rely on zero-init because
182 // initializing local static objects from constants may happen when
183 // this is first called, which isn't thread-safe. (see C++ 6.7.4)
185 static volatile intptr_t state
;
187 if(!cpu_CAS(&state
, IDLE
, BUSY
))
188 return ERR::REENTERED
; // NOWARN
190 OsPath pathname
= ah_get_log_dir()/"crashlog.txt";
191 FILE* f
= sys_OpenFile(pathname
, "w");
194 state
= FAILED
; // must come before DEBUG_DISPLAY_ERROR
195 DEBUG_DISPLAY_ERROR(L
"Unable to open crashlog.txt for writing (please ensure the log directory is writable)");
196 return ERR::FAIL
; // NOWARN (the above text is more helpful than a generic error code)
199 fputwc(0xFEFF, f
); // BOM
200 fwprintf(f
, L
"%ls\n", text
);
201 fwprintf(f
, L
"\n\n====================================\n\n");
203 // allow user to bundle whatever information they want
212 //-----------------------------------------------------------------------------
214 //-----------------------------------------------------------------------------
217 // a stream with printf-style varargs and the possibility of
218 // writing directly to the output buffer.
222 PrintfWriter(wchar_t* buf
, size_t maxChars
)
223 : m_pos(buf
), m_charsLeft(maxChars
)
227 bool operator()(const wchar_t* fmt
, ...) WPRINTF_ARGS(2)
231 const int len
= vswprintf(m_pos
, m_charsLeft
, fmt
, ap
);
240 wchar_t* Position() const
245 size_t CharsLeft() const
250 void CountAddedChars()
252 const size_t len
= wcslen(m_pos
);
263 // split out of debug_DisplayError because it's used by the self-test.
264 const wchar_t* debug_BuildErrorMessage(
265 const wchar_t* description
,
266 const wchar_t* filename
, int line
, const char* func
,
267 void* context
, const wchar_t* lastFuncToSkip
)
269 // retrieve errno (might be relevant) before doing anything else
270 // that might overwrite it.
271 wchar_t description_buf
[100] = L
"?";
272 wchar_t os_error
[100] = L
"?";
273 Status errno_equiv
= StatusFromErrno(); // NOWARN
274 if(errno_equiv
!= ERR::FAIL
) // meaningful translation
275 StatusDescription(errno_equiv
, description_buf
, ARRAY_SIZE(description_buf
));
276 sys_StatusDescription(0, os_error
, ARRAY_SIZE(os_error
));
278 PrintfWriter
writer(g_MessageBuffer
, MESSAGE_SIZE
);
283 L
"Location: %ls:%d (%hs)\r\n"
287 description
, filename
, line
, func
291 return L
"(error while formatting error message)";
294 // append stack trace
295 Status ret
= debug_DumpStack(writer
.Position(), writer
.CharsLeft(), context
, lastFuncToSkip
);
296 if(ret
== ERR::REENTERED
)
299 L
"While generating an error report, we encountered a second "
300 L
"problem. Please be sure to report both this and the subsequent "
305 else if(ret
!= INFO::OK
)
307 wchar_t error_buf
[100] = {'?'};
309 L
"(error while dumping stack: %ls)",
310 StatusDescription(ret
, error_buf
, ARRAY_SIZE(error_buf
))
316 writer
.CountAddedChars();
322 L
"errno = %d (%ls)\r\n"
323 L
"OS error = %ls\r\n",
324 errno
, description_buf
, os_error
328 return g_MessageBuffer
;
332 //-----------------------------------------------------------------------------
333 // display error messages
334 //-----------------------------------------------------------------------------
336 // translates and displays the given strings in a dialog.
337 // this is typically only used when debug_DisplayError has failed or
338 // is unavailable because that function is much more capable.
339 // implemented via sys_display_msg; see documentation there.
340 void debug_DisplayMessage(const wchar_t* caption
, const wchar_t* msg
)
342 sys_display_msg(caption
, msg
);
346 // when an error has come up and user clicks Exit, we don't want any further
347 // errors (e.g. caused by atexit handlers) to come up, possibly causing an
348 // infinite loop. hiding errors isn't good, but we assume that whoever clicked
349 // exit really doesn't want to see any more messages.
350 static atomic_bool isExiting
;
352 // this logic is applicable to any type of error. special cases such as
353 // suppressing certain expected WARN_ERRs are done there.
354 static bool ShouldSuppressError(atomic_bool
* suppress
)
362 if(*suppress
== DEBUG_SUPPRESS
)
368 static ErrorReactionInternal
CallDisplayError(const wchar_t* text
, size_t flags
)
370 // first try app hook implementation
371 ErrorReactionInternal er
= ah_display_error(text
, flags
);
372 // .. it's only a stub: default to normal implementation
373 if(er
== ERI_NOT_IMPLEMENTED
)
374 er
= sys_display_error(text
, flags
);
379 static ErrorReaction
PerformErrorReaction(ErrorReactionInternal er
, size_t flags
, atomic_bool
* suppress
)
381 const bool shouldHandleBreak
= (flags
& DE_MANUAL_BREAK
) == 0;
389 // handle "break" request unless the caller wants to (doing so here
390 // instead of within the dlgproc yields a correct call stack)
391 if(shouldHandleBreak
)
400 (void)cpu_CAS(suppress
, 0, DEBUG_SUPPRESS
);
404 isExiting
= 1; // see declaration
408 // prevent (slow) heap reporting since we're exiting abnormally and
409 // thus probably leaking like a sieve.
410 wdbg_heap_Enable(false);
415 case ERI_NOT_IMPLEMENTED
:
417 debug_break(); // not expected to be reached
422 ErrorReaction
debug_DisplayError(const wchar_t* description
,
423 size_t flags
, void* context
, const wchar_t* lastFuncToSkip
,
424 const wchar_t* pathname
, int line
, const char* func
,
425 atomic_bool
* suppress
)
427 // "suppressing" this error means doing nothing and returning ER_CONTINUE.
428 if(ShouldSuppressError(suppress
))
432 // .. caller supports a suppress flag; set the corresponding flag so that
433 // the error display implementation enables the Suppress option.
435 flags
|= DE_ALLOW_SUPPRESS
;
437 if(flags
& DE_NO_DEBUG_INFO
)
439 // in non-debug-info mode, simply display the given description
440 // and then return immediately
441 ErrorReactionInternal er
= CallDisplayError(description
, flags
);
442 return PerformErrorReaction(er
, flags
, suppress
);
445 // .. deal with incomplete file/line info
446 if(!pathname
|| pathname
[0] == '\0')
447 pathname
= L
"unknown";
450 if(!func
|| func
[0] == '\0')
452 // .. _FILE__ evaluates to the full path (albeit without drive letter)
453 // which is rather long. we only display the base name for clarity.
454 const wchar_t* filename
= path_name_only(pathname
);
456 // display in output window; double-click will navigate to error location.
457 const wchar_t* text
= debug_BuildErrorMessage(description
, filename
, line
, func
, context
, lastFuncToSkip
);
459 (void)debug_WriteCrashlog(text
);
460 ErrorReactionInternal er
= CallDisplayError(text
, flags
);
462 // TODO: use utf8 conversion without internal allocations.
463 debug_printf("%s(%d): %s\n", utf8_from_wstring(filename
).c_str(), line
, utf8_from_wstring(description
).c_str());
465 // note: debug_break-ing here to make sure the app doesn't continue
466 // running is no longer necessary. debug_DisplayError now determines our
467 // window handle and is modal.
469 return PerformErrorReaction(er
, flags
, suppress
);
473 // is errorToSkip valid? (also guarantees mutual exclusion)
478 static intptr_t skipStatus
= INVALID
;
479 static Status errorToSkip
;
480 static size_t numSkipped
;
482 void debug_SkipErrors(Status err
)
484 if(cpu_CAS(&skipStatus
, INVALID
, BUSY
))
489 skipStatus
= VALID
; // linearization point
492 DEBUG_WARN_ERR(ERR::REENTERED
);
495 size_t debug_StopSkippingErrors()
497 if(cpu_CAS(&skipStatus
, VALID
, BUSY
))
499 const size_t ret
= numSkipped
;
501 skipStatus
= INVALID
; // linearization point
506 DEBUG_WARN_ERR(ERR::REENTERED
);
511 static bool ShouldSkipError(Status err
)
513 if(cpu_CAS(&skipStatus
, VALID
, BUSY
))
516 const bool ret
= (err
== errorToSkip
);
524 ErrorReaction
debug_OnError(Status err
, atomic_bool
* suppress
, const wchar_t* file
, int line
, const char* func
)
526 CACHE_ALIGNED(u8
) context
[DEBUG_CONTEXT_SIZE
];
527 (void)debug_CaptureContext(context
);
529 if(ShouldSkipError(err
))
532 const wchar_t* lastFuncToSkip
= L
"debug_OnError";
534 wchar_t err_buf
[200]; StatusDescription(err
, err_buf
, ARRAY_SIZE(err_buf
));
535 swprintf_s(buf
, ARRAY_SIZE(buf
), L
"Function call failed: return value was %lld (%ls)", (long long)err
, err_buf
);
536 return debug_DisplayError(buf
, DE_MANUAL_BREAK
, context
, lastFuncToSkip
, file
,line
,func
, suppress
);
539 ErrorReaction
debug_OnAssertionFailure(const wchar_t* expr
, atomic_bool
* suppress
, const wchar_t* file
, int line
, const char* func
)
541 CACHE_ALIGNED(u8
) context
[DEBUG_CONTEXT_SIZE
];
542 (void)debug_CaptureContext(context
);
544 const wchar_t* lastFuncToSkip
= L
"debug_OnAssertionFailure";
546 swprintf_s(buf
, ARRAY_SIZE(buf
), L
"Assertion failed: \"%ls\"", expr
);
547 return debug_DisplayError(buf
, DE_MANUAL_BREAK
, context
, lastFuncToSkip
, file
,line
,func
, suppress
);