Merge 'remotes/trunk'
[0ad.git] / source / lib / debug.cpp
blob65cb64ea2cc2729959ecfd91b11cc8c22a8ad2f1
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"
33 #include <cstdarg>
34 #include <cstring>
35 #include <cstdio>
37 namespace
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++)
85 if(tags[i] == hash)
86 return;
88 // too many already?
89 if(num_tags == MAX_TAGS)
91 DEBUG_WARN_ERR(ERR::LOGIC); // increase MAX_TAGS
92 return;
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];
108 num_tags--;
110 // can only happen once, so we're done.
111 return;
116 void debug_filter_clear()
118 std::fill(tags, tags+MAX_TAGS, 0);
121 bool debug_filter_allows(const char* text)
123 size_t i;
124 for(i = 0; ; i++)
126 // no | found => no tag => should always be displayed
127 if(text[i] == ' ' || text[i] == '\0')
128 return true;
129 if(text[i] == '|' && i != 0)
130 break;
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++)
137 if(tags[i] == hash)
138 return true;
140 return false;
143 #undef debug_printf // allowing #defining it out
144 void debug_printf(const char* fmt, ...)
146 char buf[16384];
148 va_list ap;
149 va_start(ap, fmt);
150 const int numChars = vsprintf_s(buf, ARRAY_SIZE(buf), fmt, ap);
151 if (numChars < 0)
152 debug_break(); // poor man's assert - avoid infinite loop because ENSURE also uses debug_printf
153 va_end(ap);
155 debug_puts_filtered(buf);
158 void debug_puts_filtered(const char* text)
160 if(debug_filter_allows(text))
161 debug_puts(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)
171 enum State
173 IDLE,
174 BUSY,
175 FAILED
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)
180 cassert(IDLE == 0);
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");
188 if(!f)
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
200 ah_bundle_logs(f);
202 fclose(f);
203 state = IDLE;
204 return INFO::OK;
208 //-----------------------------------------------------------------------------
209 // error message
210 //-----------------------------------------------------------------------------
213 // a stream with printf-style varargs and the possibility of
214 // writing directly to the output buffer.
215 class PrintfWriter
217 public:
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)
225 va_list ap;
226 va_start(ap, fmt);
227 const int len = vswprintf(m_pos, m_charsLeft, fmt, ap);
228 va_end(ap);
229 if(len < 0)
230 return false;
231 m_pos += len;
232 m_charsLeft -= len;
233 return true;
236 wchar_t* Position() const
238 return m_pos;
241 size_t CharsLeft() const
243 return m_charsLeft;
246 void CountAddedChars()
248 const size_t len = wcslen(m_pos);
249 m_pos += len;
250 m_charsLeft -= len;
253 private:
254 wchar_t* m_pos;
255 size_t m_charsLeft;
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);
276 // header
277 if(!writer(
278 L"%ls\r\n"
279 L"Location: %ls:%d (%hs)\r\n"
280 L"\r\n"
281 L"Call stack:\r\n"
282 L"\r\n",
283 description, filename, line, func
286 fail:
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)
294 if(!writer(
295 L"While generating an error report, we encountered a second "
296 L"problem. Please be sure to report both this and the subsequent "
297 L"error messages."
299 goto fail;
301 else if(ret != INFO::OK)
303 wchar_t error_buf[100] = {'?'};
304 if(!writer(
305 L"(error while dumping stack: %ls)",
306 StatusDescription(ret, error_buf, ARRAY_SIZE(error_buf))
308 goto fail;
310 else // success
312 writer.CountAddedChars();
315 // append errno
316 if(!writer(
317 L"\r\n"
318 L"errno = %d (%ls)\r\n"
319 L"OS error = %ls\r\n",
320 errno, description_buf, os_error
322 goto fail;
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)
352 if(isExiting)
353 return true;
355 if(!suppress)
356 return false;
358 if(*suppress == DEBUG_SUPPRESS)
359 return true;
361 return false;
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);
372 return er;
375 static ErrorReaction PerformErrorReaction(ErrorReactionInternal er, size_t flags, atomic_bool* suppress)
377 const bool shouldHandleBreak = (flags & DE_MANUAL_BREAK) == 0;
379 switch(er)
381 case ERI_CONTINUE:
382 return ER_CONTINUE;
384 case ERI_BREAK:
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)
389 debug_break();
390 return ER_CONTINUE;
392 else
393 return ER_BREAK;
395 case ERI_SUPPRESS:
396 (void)cpu_CAS(suppress, 0, DEBUG_SUPPRESS);
397 return ER_CONTINUE;
399 case ERI_EXIT:
400 isExiting = 1; // see declaration
401 COMPILER_FENCE;
403 exit(EXIT_FAILURE);
405 case ERI_NOT_IMPLEMENTED:
406 default:
407 debug_break(); // not expected to be reached
408 return ER_CONTINUE;
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))
419 return ER_CONTINUE;
421 // fix up params
422 // .. caller supports a suppress flag; set the corresponding flag so that
423 // the error display implementation enables the Suppress option.
424 if(suppress)
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";
438 if(line <= 0)
439 line = 0;
440 if(!func || func[0] == '\0')
441 func = "?";
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)
464 enum SkipStatus
466 INVALID, VALID, BUSY
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))
476 errorToSkip = err;
477 numSkipped = 0;
478 COMPILER_FENCE;
479 skipStatus = VALID; // linearization point
481 else
482 DEBUG_WARN_ERR(ERR::REENTERED);
485 size_t debug_StopSkippingErrors()
487 if(cpu_CAS(&skipStatus, VALID, BUSY))
489 const size_t ret = numSkipped;
490 COMPILER_FENCE;
491 skipStatus = INVALID; // linearization point
492 return ret;
494 else
496 DEBUG_WARN_ERR(ERR::REENTERED);
497 return 0;
501 static bool ShouldSkipError(Status err)
503 if(cpu_CAS(&skipStatus, VALID, BUSY))
505 numSkipped++;
506 const bool ret = (err == errorToSkip);
507 COMPILER_FENCE;
508 skipStatus = VALID;
509 return ret;
511 return false;
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))
520 return ER_CONTINUE;
522 const wchar_t* lastFuncToSkip = L"debug_OnError";
523 wchar_t buf[400];
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";
535 wchar_t buf[400];
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);