1 /* Copyright (C) 2023 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"
40 // (NB: this may appear obscene, but deep stack traces have been
41 // observed to take up > 256 KiB)
42 constexpr std::size_t MESSAGE_SIZE
= 512 * KiB
/ sizeof(wchar_t);
43 wchar_t g_MessageBuffer
[MESSAGE_SIZE
];
45 } // anonymous namespace
47 static const StatusDefinition debugStatusDefinitions
[] = {
48 { ERR::SYM_NO_STACK_FRAMES_FOUND
, L
"No stack frames found" },
49 { ERR::SYM_UNRETRIEVABLE_STATIC
, L
"Value unretrievable (stored in external module)" },
50 { ERR::SYM_UNRETRIEVABLE
, L
"Value unretrievable" },
51 { ERR::SYM_TYPE_INFO_UNAVAILABLE
, L
"Error getting type_info" },
52 { ERR::SYM_INTERNAL_ERROR
, L
"Exception raised while processing a symbol" },
53 { ERR::SYM_UNSUPPORTED
, L
"Symbol type not (fully) supported" },
54 { ERR::SYM_CHILD_NOT_FOUND
, L
"Symbol does not have the given child" },
55 { ERR::SYM_NESTING_LIMIT
, L
"Symbol nesting too deep or infinite recursion" },
56 { ERR::SYM_SINGLE_SYMBOL_LIMIT
, L
"Symbol has produced too much output" },
57 { INFO::SYM_SUPPRESS_OUTPUT
, L
"Symbol was suppressed" }
59 STATUS_ADD_DEFINITIONS(debugStatusDefinitions
);
62 // need to shoehorn printf-style variable params into
63 // the OutputDebugString call.
64 // - don't want to split into multiple calls - would add newlines to output.
65 // - fixing Win32 _vsnprintf to return # characters that would be written,
66 // as required by C99, looks difficult and unnecessary. if any other code
67 // needs that, implement GNU vasprintf.
68 // - fixed size buffers aren't nice, but much simpler than vasprintf-style
69 // allocate+expand_until_it_fits. these calls are for quick debug output,
70 // not loads of data, anyway.
72 // rationale: static data instead of std::set to allow setting at any time.
73 // we store FNV hash of tag strings for fast comparison; collisions are
74 // extremely unlikely and can only result in displaying more/less text.
75 static const size_t MAX_TAGS
= 20;
76 static u32 tags
[MAX_TAGS
];
77 static size_t num_tags
;
79 void debug_filter_add(const char* tag
)
81 const u32 hash
= fnv_hash(tag
, strlen(tag
)*sizeof(tag
[0]));
83 // make sure it isn't already in the list
84 for(size_t i
= 0; i
< MAX_TAGS
; i
++)
89 if(num_tags
== MAX_TAGS
)
91 DEBUG_WARN_ERR(ERR::LOGIC
); // increase MAX_TAGS
95 tags
[num_tags
++] = hash
;
98 void debug_filter_remove(const char* tag
)
100 const u32 hash
= fnv_hash(tag
, strlen(tag
)*sizeof(tag
[0]));
102 for(size_t i
= 0; i
< MAX_TAGS
; i
++)
104 if(tags
[i
] == hash
) // found it
106 // replace with last element (avoid holes)
107 tags
[i
] = tags
[MAX_TAGS
-1];
110 // can only happen once, so we're done.
116 void debug_filter_clear()
118 std::fill(tags
, tags
+MAX_TAGS
, 0);
121 bool debug_filter_allows(const char* text
)
126 // no | found => no tag => should always be displayed
127 if(text
[i
] == ' ' || text
[i
] == '\0')
129 if(text
[i
] == '|' && i
!= 0)
133 const u32 hash
= fnv_hash(text
, i
*sizeof(text
[0]));
135 // check if entry allowing this tag is found
136 for(i
= 0; i
< MAX_TAGS
; i
++)
143 #undef debug_printf // allowing #defining it out
144 void debug_printf(const char* fmt
, ...)
150 const int numChars
= vsprintf_s(buf
, ARRAY_SIZE(buf
), fmt
, ap
);
152 debug_break(); // poor man's assert - avoid infinite loop because ENSURE also uses debug_printf
155 debug_puts_filtered(buf
);
158 void debug_puts_filtered(const char* text
)
160 if(debug_filter_allows(text
))
165 //-----------------------------------------------------------------------------
167 Status
debug_WriteCrashlog(const wchar_t* text
)
169 // (avoid infinite recursion and/or reentering this function if it
170 // fails/reports an error)
177 // note: the initial state is IDLE. we rely on zero-init because
178 // initializing local static objects from constants may happen when
179 // this is first called, which isn't thread-safe. (see C++ 6.7.4)
181 static volatile intptr_t state
;
183 if(!cpu_CAS(&state
, IDLE
, BUSY
))
184 return ERR::REENTERED
; // NOWARN
186 OsPath pathname
= ah_get_log_dir()/"crashlog.txt";
187 FILE* f
= sys_OpenFile(pathname
, "w");
190 state
= FAILED
; // must come before DEBUG_DISPLAY_ERROR
191 DEBUG_DISPLAY_ERROR(L
"Unable to open crashlog.txt for writing (please ensure the log directory is writable)");
192 return ERR::FAIL
; // NOWARN (the above text is more helpful than a generic error code)
195 fputwc(0xFEFF, f
); // BOM
196 fwprintf(f
, L
"%ls\n", text
);
197 fwprintf(f
, L
"\n\n====================================\n\n");
199 // allow user to bundle whatever information they want
208 //-----------------------------------------------------------------------------
210 //-----------------------------------------------------------------------------
213 // a stream with printf-style varargs and the possibility of
214 // writing directly to the output buffer.
218 PrintfWriter(wchar_t* buf
, size_t maxChars
)
219 : m_pos(buf
), m_charsLeft(maxChars
)
223 bool operator()(const wchar_t* fmt
, ...) WPRINTF_ARGS(2)
227 const int len
= vswprintf(m_pos
, m_charsLeft
, fmt
, ap
);
236 wchar_t* Position() const
241 size_t CharsLeft() const
246 void CountAddedChars()
248 const size_t len
= wcslen(m_pos
);
259 // split out of debug_DisplayError because it's used by the self-test.
260 const wchar_t* debug_BuildErrorMessage(
261 const wchar_t* description
,
262 const wchar_t* filename
, int line
, const char* func
,
263 void* context
, const wchar_t* lastFuncToSkip
)
265 // retrieve errno (might be relevant) before doing anything else
266 // that might overwrite it.
267 wchar_t description_buf
[100] = L
"?";
268 wchar_t os_error
[100] = L
"?";
269 Status errno_equiv
= StatusFromErrno(); // NOWARN
270 if(errno_equiv
!= ERR::FAIL
) // meaningful translation
271 StatusDescription(errno_equiv
, description_buf
, ARRAY_SIZE(description_buf
));
272 sys_StatusDescription(0, os_error
, ARRAY_SIZE(os_error
));
274 PrintfWriter
writer(g_MessageBuffer
, MESSAGE_SIZE
);
279 L
"Location: %ls:%d (%hs)\r\n"
283 description
, filename
, line
, func
287 return L
"(error while formatting error message)";
290 // append stack trace
291 Status ret
= debug_DumpStack(writer
.Position(), writer
.CharsLeft(), context
, lastFuncToSkip
);
292 if(ret
== ERR::REENTERED
)
295 L
"While generating an error report, we encountered a second "
296 L
"problem. Please be sure to report both this and the subsequent "
301 else if(ret
!= INFO::OK
)
303 wchar_t error_buf
[100] = {'?'};
305 L
"(error while dumping stack: %ls)",
306 StatusDescription(ret
, error_buf
, ARRAY_SIZE(error_buf
))
312 writer
.CountAddedChars();
318 L
"errno = %d (%ls)\r\n"
319 L
"OS error = %ls\r\n",
320 errno
, description_buf
, os_error
324 return g_MessageBuffer
;
328 //-----------------------------------------------------------------------------
329 // display error messages
330 //-----------------------------------------------------------------------------
332 // translates and displays the given strings in a dialog.
333 // this is typically only used when debug_DisplayError has failed or
334 // is unavailable because that function is much more capable.
335 // implemented via sys_display_msg; see documentation there.
336 void debug_DisplayMessage(const wchar_t* caption
, const wchar_t* msg
)
338 sys_display_msg(caption
, msg
);
342 // when an error has come up and user clicks Exit, we don't want any further
343 // errors (e.g. caused by atexit handlers) to come up, possibly causing an
344 // infinite loop. hiding errors isn't good, but we assume that whoever clicked
345 // exit really doesn't want to see any more messages.
346 static atomic_bool isExiting
;
348 // this logic is applicable to any type of error. special cases such as
349 // suppressing certain expected WARN_ERRs are done there.
350 static bool ShouldSuppressError(atomic_bool
* suppress
)
358 if(*suppress
== DEBUG_SUPPRESS
)
364 static ErrorReactionInternal
CallDisplayError(const wchar_t* text
, size_t flags
)
366 // first try app hook implementation
367 ErrorReactionInternal er
= ah_display_error(text
, flags
);
368 // .. it's only a stub: default to normal implementation
369 if(er
== ERI_NOT_IMPLEMENTED
)
370 er
= sys_display_error(text
, flags
);
375 static ErrorReaction
PerformErrorReaction(ErrorReactionInternal er
, size_t flags
, atomic_bool
* suppress
)
377 const bool shouldHandleBreak
= (flags
& DE_MANUAL_BREAK
) == 0;
385 // handle "break" request unless the caller wants to (doing so here
386 // instead of within the dlgproc yields a correct call stack)
387 if(shouldHandleBreak
)
396 (void)cpu_CAS(suppress
, 0, DEBUG_SUPPRESS
);
400 isExiting
= 1; // see declaration
405 case ERI_NOT_IMPLEMENTED
:
407 debug_break(); // not expected to be reached
412 ErrorReaction
debug_DisplayError(const wchar_t* description
,
413 size_t flags
, void* context
, const wchar_t* lastFuncToSkip
,
414 const wchar_t* pathname
, int line
, const char* func
,
415 atomic_bool
* suppress
)
417 // "suppressing" this error means doing nothing and returning ER_CONTINUE.
418 if(ShouldSuppressError(suppress
))
422 // .. caller supports a suppress flag; set the corresponding flag so that
423 // the error display implementation enables the Suppress option.
425 flags
|= DE_ALLOW_SUPPRESS
;
427 if(flags
& DE_NO_DEBUG_INFO
)
429 // in non-debug-info mode, simply display the given description
430 // and then return immediately
431 ErrorReactionInternal er
= CallDisplayError(description
, flags
);
432 return PerformErrorReaction(er
, flags
, suppress
);
435 // .. deal with incomplete file/line info
436 if(!pathname
|| pathname
[0] == '\0')
437 pathname
= L
"unknown";
440 if(!func
|| func
[0] == '\0')
442 // .. _FILE__ evaluates to the full path (albeit without drive letter)
443 // which is rather long. we only display the base name for clarity.
444 const wchar_t* filename
= path_name_only(pathname
);
446 // display in output window; double-click will navigate to error location.
447 const wchar_t* text
= debug_BuildErrorMessage(description
, filename
, line
, func
, context
, lastFuncToSkip
);
449 (void)debug_WriteCrashlog(text
);
450 ErrorReactionInternal er
= CallDisplayError(text
, flags
);
452 // TODO: use utf8 conversion without internal allocations.
453 debug_printf("%s(%d): %s\n", utf8_from_wstring(filename
).c_str(), line
, utf8_from_wstring(description
).c_str());
455 // note: debug_break-ing here to make sure the app doesn't continue
456 // running is no longer necessary. debug_DisplayError now determines our
457 // window handle and is modal.
459 return PerformErrorReaction(er
, flags
, suppress
);
463 // is errorToSkip valid? (also guarantees mutual exclusion)
468 static intptr_t skipStatus
= INVALID
;
469 static Status errorToSkip
;
470 static size_t numSkipped
;
472 void debug_SkipErrors(Status err
)
474 if(cpu_CAS(&skipStatus
, INVALID
, BUSY
))
479 skipStatus
= VALID
; // linearization point
482 DEBUG_WARN_ERR(ERR::REENTERED
);
485 size_t debug_StopSkippingErrors()
487 if(cpu_CAS(&skipStatus
, VALID
, BUSY
))
489 const size_t ret
= numSkipped
;
491 skipStatus
= INVALID
; // linearization point
496 DEBUG_WARN_ERR(ERR::REENTERED
);
501 static bool ShouldSkipError(Status err
)
503 if(cpu_CAS(&skipStatus
, VALID
, BUSY
))
506 const bool ret
= (err
== errorToSkip
);
514 ErrorReaction
debug_OnError(Status err
, atomic_bool
* suppress
, const wchar_t* file
, int line
, const char* func
)
516 CACHE_ALIGNED(u8
) context
[DEBUG_CONTEXT_SIZE
];
517 (void)debug_CaptureContext(context
);
519 if(ShouldSkipError(err
))
522 const wchar_t* lastFuncToSkip
= L
"debug_OnError";
524 wchar_t err_buf
[200]; StatusDescription(err
, err_buf
, ARRAY_SIZE(err_buf
));
525 swprintf_s(buf
, ARRAY_SIZE(buf
), L
"Function call failed: return value was %lld (%ls)", (long long)err
, err_buf
);
526 return debug_DisplayError(buf
, DE_MANUAL_BREAK
, context
, lastFuncToSkip
, file
,line
,func
, suppress
);
529 ErrorReaction
debug_OnAssertionFailure(const wchar_t* expr
, atomic_bool
* suppress
, const wchar_t* file
, int line
, const char* func
)
531 CACHE_ALIGNED(u8
) context
[DEBUG_CONTEXT_SIZE
];
532 (void)debug_CaptureContext(context
);
534 const wchar_t* lastFuncToSkip
= L
"debug_OnAssertionFailure";
536 swprintf_s(buf
, ARRAY_SIZE(buf
), L
"Assertion failed: \"%ls\"", expr
);
537 return debug_DisplayError(buf
, DE_MANUAL_BREAK
, context
, lastFuncToSkip
, file
,line
,func
, suppress
);