1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /*
21 * Debug.c
22 *
23 * Various debugging output functions.
24 *
25 */
26
27 #include "frame.h"
28 #include "wztime.h"
29
30 #include <string.h>
31 #include <stdio.h>
32 #include <time.h>
33 #include "string_ext.h"
34 #include "wzapp.h"
35 #include <map>
36 #include <string>
37
38 #if defined(WZ_OS_LINUX) && defined(__GLIBC__)
39 #include <execinfo.h> // Nonfatal runtime backtraces.
40 #endif // defined(WZ_OS_LINUX) && defined(__GLIBC__)
41
42 #if defined(WZ_OS_UNIX)
43 # include <fcntl.h>
44 # ifndef _POSIX_C_SOURCE
45 # define _POSIX_C_SOURCE 1
46 # endif
47 # ifndef _XOPEN_SOURCE
48 # define _XOPEN_SOURCE
49 # endif
50 # ifndef _POSIX_SOURCE
51 # define _POSIX_SOURCE
52 # endif
53 # include <stdio.h>
54 #endif
55
56 #define MAX_LEN_LOG_LINE 512
57
58 char last_called_script_event[MAX_EVENT_NAME_LEN];
59 UDWORD traceID = -1;
60
61 static debug_callback *callbackRegistry = nullptr;
62 bool enabled_debug[LOG_LAST]; // global
63 #ifdef DEBUG
64 bool assertEnabled = true;
65 #else
66 bool assertEnabled = false;
67 #endif
68 #ifdef WZ_OS_MAC
69 #include "cocoa_wrapper.h"
70 #endif
71 /*
72 * This list _must_ match the enum in debug.h!
73 * Names must be 8 chars long at max!
74 */
75 static const char *code_part_names[] =
76 {
77 "all",
78 "main",
79 "sound",
80 "video",
81 "wz",
82 "3d",
83 "texture",
84 "net",
85 "memory",
86 "warning",
87 "error",
88 "never",
89 "script",
90 "movement",
91 "attack",
92 "fog",
93 "sensor",
94 "gui",
95 "map",
96 "save",
97 "sync",
98 "death",
99 "life",
100 "gateway",
101 "message",
102 "info",
103 "terrain",
104 "feature",
105 "fatal",
106 "input",
107 "popup",
108 "console",
109 "activity",
110 "last"
111 };
112
113 static char inputBuffer[2][MAX_LEN_LOG_LINE];
114 static bool useInputBuffer1 = false;
115 static bool debug_flush_stderr = false;
116
117 static std::map<std::string, int> warning_list; // only used for LOG_WARNING
118
119 /**
120 * Convert code_part names to enum. Case insensitive.
121 *
122 * \return Codepart number or LOG_LAST if can't match.
123 */
code_part_from_str(const char * str)124 static code_part code_part_from_str(const char *str)
125 {
126 unsigned int i;
127
128 for (i = 0; i < LOG_LAST; i++)
129 {
130 if (strcasecmp(code_part_names[i], str) == 0)
131 {
132 return (code_part)i;
133 }
134 }
135 return LOG_LAST;
136 }
137
138
139 /**
140 * Callback for outputing to stderr
141 *
142 * \param data Ignored. Use NULL.
143 * \param outputBuffer Buffer containing the preprocessed text to output.
144 */
debug_callback_stderr(WZ_DECL_UNUSED void ** data,const char * outputBuffer)145 void debug_callback_stderr(WZ_DECL_UNUSED void **data, const char *outputBuffer)
146 {
147 if (outputBuffer[strlen(outputBuffer) - 1] != '\n')
148 {
149 fprintf(stderr, "%s\n", outputBuffer);
150 }
151 else
152 {
153 fprintf(stderr, "%s", outputBuffer);
154 }
155
156 // Make sure that all output is flushed to stderr when requested by the user
157 if (debug_flush_stderr)
158 {
159 fflush(stderr);
160 }
161 }
162
163
164 /**
165 * Callback for outputting to a win32 debugger
166 *
167 * \param data Ignored. Use NULL.
168 * \param outputBuffer Buffer containing the preprocessed text to output.
169 */
170 #if defined WIN32 && defined DEBUG
debug_callback_win32debug(WZ_DECL_UNUSED void ** data,const char * outputBuffer)171 void debug_callback_win32debug(WZ_DECL_UNUSED void **data, const char *outputBuffer)
172 {
173 char tmpStr[MAX_LEN_LOG_LINE];
174
175 sstrcpy(tmpStr, outputBuffer);
176 if (!strchr(tmpStr, '\n'))
177 {
178 sstrcat(tmpStr, "\n");
179 }
180
181 OutputDebugStringA(tmpStr);
182 }
183 #endif // WIN32
184
185
186 /**
187 * Callback for outputing to a file
188 *
189 * \param data Filehandle to output to.
190 * \param outputBuffer Buffer containing the preprocessed text to output.
191 */
debug_callback_file(void ** data,const char * outputBuffer)192 void debug_callback_file(void **data, const char *outputBuffer)
193 {
194 FILE *logfile = (FILE *)*data;
195
196 if (!strchr(outputBuffer, '\n'))
197 {
198 fprintf(logfile, "%s\n", outputBuffer);
199 }
200 else
201 {
202 fprintf(logfile, "%s", outputBuffer);
203 }
204 }
205
206 char WZ_DBGFile[PATH_MAX] = {0}; //Used to save path of the created log file
207 /**
208 * Setup the file callback
209 *
210 * Sets data to the filehandle opened for the filename found in data.
211 *
212 * \param[in,out] data In: The filename to output to.
213 * Out: The filehandle.
214 */
215 WzString WZDebugfilename;
debug_callback_file_init(void ** data)216 bool debug_callback_file_init(void **data)
217 {
218 WzString * pFileName = (WzString *)*data;
219 ASSERT(pFileName, "Debug filename required");
220 WZDebugfilename = *pFileName;
221
222 #if defined(WZ_OS_WIN)
223 // On Windows, path strings passed to fopen() are interpreted using the ANSI or OEM codepage
224 // (and not as UTF-8). To support Unicode paths, the string must be converted to a wide-char
225 // string and passed to _wfopen.
226 int wstr_len = MultiByteToWideChar(CP_UTF8, 0, WZDebugfilename.toUtf8().c_str(), -1, NULL, 0);
227 if (wstr_len <= 0)
228 {
229 fprintf(stderr, "Could not not convert string from UTF-8; MultiByteToWideChar failed with error %d: %s\n", GetLastError(), WZDebugfilename.toUtf8().c_str());
230 return false;
231 }
232 std::vector<wchar_t> wstr_filename(wstr_len, 0);
233 if (MultiByteToWideChar(CP_UTF8, 0, WZDebugfilename.toUtf8().c_str(), -1, &wstr_filename[0], wstr_len) == 0)
234 {
235 fprintf(stderr, "Could not not convert string from UTF-8; MultiByteToWideChar[2] failed with error %d: %s\n", GetLastError(), WZDebugfilename.toUtf8().c_str());
236 return false;
237 }
238 FILE *const logfile = _wfopen(&wstr_filename[0], L"w");
239 #else
240 FILE *const logfile = fopen(WZDebugfilename.toUtf8().c_str(), "w");
241 #endif
242 if (!logfile)
243 {
244 fprintf(stderr, "Could not open %s for appending!\n", WZDebugfilename.toUtf8().c_str());
245 return false;
246 }
247 #if defined(WZ_OS_UNIX)
248 int fd = fileno(logfile);
249 if (fd != -1)
250 {
251 fcntl(fd, F_SETFD, FD_CLOEXEC);
252 }
253 #endif
254 snprintf(WZ_DBGFile, sizeof(WZ_DBGFile), "%s", WZDebugfilename.toUtf8().c_str());
255 setbuf(logfile, nullptr);
256 fprintf(logfile, "--- Starting log [%s]---\n", WZDebugfilename.toUtf8().c_str());
257 *data = logfile;
258
259 return true;
260 }
261
262
263 /**
264 * Shutdown the file callback
265 *
266 * Closes the logfile.
267 *
268 * \param data The filehandle to close.
269 */
debug_callback_file_exit(void ** data)270 void debug_callback_file_exit(void **data)
271 {
272 FILE *logfile = (FILE *)*data;
273 fclose(logfile);
274 *data = nullptr;
275 }
276
debugFlushStderr()277 void debugFlushStderr()
278 {
279 debug_flush_stderr = true;
280 }
281 // MSVC specific rotuines to set/clear allocation tracking
282 #if defined(WZ_CC_MSVC) && defined(DEBUG)
debug_MEMCHKOFF()283 void debug_MEMCHKOFF()
284 {
285 // Disable allocation tracking
286 int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
287 flags &= ~_CRTDBG_ALLOC_MEM_DF;
288 _CrtSetDbgFlag(flags);
289 }
debug_MEMCHKON()290 void debug_MEMCHKON()
291 {
292 // Enable allocation tracking
293 int flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
294 flags |= _CRTDBG_ALLOC_MEM_DF;
295 _CrtSetDbgFlag(flags);
296 }
debug_MEMSTATS()297 void debug_MEMSTATS()
298 {
299 _CrtMemState state;
300 _CrtMemCheckpoint(&state);
301 _CrtMemDumpStatistics(&state);
302 }
303 #endif
304
debug_init()305 void debug_init()
306 {
307 /*** Initialize the debug subsystem ***/
308 #if defined(WZ_CC_MSVC) && defined(DEBUG)
309 int tmpDbgFlag;
310 _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); // Output CRT info to debugger
311
312 tmpDbgFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); // Grab current flags
313 # if defined(DEBUG_MEMORY)
314 tmpDbgFlag |= _CRTDBG_CHECK_ALWAYS_DF; // Check every (de)allocation
315 # endif // DEBUG_MEMORY
316 tmpDbgFlag |= _CRTDBG_ALLOC_MEM_DF; // Check allocations
317 tmpDbgFlag |= _CRTDBG_LEAK_CHECK_DF; // Check for memleaks
318 _CrtSetDbgFlag(tmpDbgFlag);
319 #endif // WZ_CC_MSVC && DEBUG
320
321 STATIC_ASSERT(ARRAY_SIZE(code_part_names) - 1 == LOG_LAST); // enums start at 0
322
323 memset(enabled_debug, false, sizeof(enabled_debug));
324 enabled_debug[LOG_ERROR] = true;
325 enabled_debug[LOG_INFO] = true;
326 enabled_debug[LOG_FATAL] = true;
327 enabled_debug[LOG_POPUP] = true;
328 inputBuffer[0][0] = '\0';
329 inputBuffer[1][0] = '\0';
330 #ifdef DEBUG
331 enabled_debug[LOG_WARNING] = true;
332 #endif
333 }
334
335
debug_exit()336 void debug_exit()
337 {
338 debug_callback *curCallback = callbackRegistry, * tmpCallback = nullptr;
339
340 while (curCallback)
341 {
342 if (curCallback->exit)
343 {
344 curCallback->exit(&curCallback->data);
345 }
346 tmpCallback = curCallback->next;
347 free(curCallback);
348 curCallback = tmpCallback;
349 }
350 warning_list.clear();
351 callbackRegistry = nullptr;
352 }
353
354
debug_register_callback(debug_callback_fn callback,debug_callback_init init,debug_callback_exit exit,void * data)355 void debug_register_callback(debug_callback_fn callback, debug_callback_init init, debug_callback_exit exit, void *data)
356 {
357 debug_callback *curCallback = callbackRegistry, * tmpCallback = nullptr;
358
359 tmpCallback = (debug_callback *)malloc(sizeof(*tmpCallback));
360
361 tmpCallback->next = nullptr;
362 tmpCallback->callback = callback;
363 tmpCallback->init = init;
364 tmpCallback->exit = exit;
365 tmpCallback->data = data;
366
367 if (tmpCallback->init
368 && !tmpCallback->init(&tmpCallback->data))
369 {
370 fprintf(stderr, "Failed to initialise debug callback, debugfile set to [%s]!\n", WZDebugfilename.toUtf8().c_str());
371 free(tmpCallback);
372 return;
373 }
374
375 if (!curCallback)
376 {
377 callbackRegistry = tmpCallback;
378 return;
379 }
380
381 while (curCallback->next)
382 {
383 curCallback = curCallback->next;
384 }
385
386 curCallback->next = tmpCallback;
387 }
388
389
debug_enable_switch(const char * str)390 bool debug_enable_switch(const char *str)
391 {
392 code_part part = code_part_from_str(str);
393
394 if (part != LOG_LAST)
395 {
396 enabled_debug[part] = !enabled_debug[part];
397 }
398 if (part == LOG_ALL)
399 {
400 memset(enabled_debug, true, sizeof(enabled_debug));
401 }
402 return (part != LOG_LAST);
403 }
404
405 /** Send the given string to all debug callbacks.
406 *
407 * @param str The string to send to debug callbacks.
408 */
printToDebugCallbacks(const char * const str)409 static void printToDebugCallbacks(const char *const str)
410 {
411 debug_callback *curCallback;
412
413 // Loop over all callbacks, invoking them with the given data string
414 for (curCallback = callbackRegistry; curCallback != nullptr; curCallback = curCallback->next)
415 {
416 curCallback->callback(&curCallback->data, str);
417 }
418 }
419
_realObjTrace(int id,const char * function,const char * str,...)420 void _realObjTrace(int id, const char *function, const char *str, ...)
421 {
422 char vaBuffer[MAX_LEN_LOG_LINE];
423 char outputBuffer[MAX_LEN_LOG_LINE];
424 va_list ap;
425
426 va_start(ap, str);
427 vssprintf(vaBuffer, str, ap);
428 va_end(ap);
429
430 ssprintf(outputBuffer, "[%6d]: [%s] %s", id, function, vaBuffer);
431 printToDebugCallbacks(outputBuffer);
432 }
433
434 // Thread local to prevent a race condition on read and write to this buffer if multiple
435 // threads log errors. This means we will not be reporting any errors to console from threads
436 // other than main. If we want to fix this, make sure accesses are protected by a mutex.
437 static WZ_DECL_THREAD char errorStore[512];
438 static WZ_DECL_THREAD bool errorWaiting = false;
debugLastError()439 const char *debugLastError()
440 {
441 if (errorWaiting)
442 {
443 errorWaiting = false;
444 return errorStore;
445 }
446 else
447 {
448 return nullptr;
449 }
450 }
451
_debug(int line,code_part part,const char * function,const char * str,...)452 void _debug(int line, code_part part, const char *function, const char *str, ...)
453 {
454 va_list ap;
455 static char outputBuffer[MAX_LEN_LOG_LINE];
456 static unsigned int repeated = 0; /* times current message repeated */
457 static unsigned int next = 2; /* next total to print update */
458 static unsigned int prev = 0; /* total on last update */
459
460 va_start(ap, str);
461 vssprintf(outputBuffer, str, ap);
462 va_end(ap);
463
464 if (part == LOG_WARNING)
465 {
466 std::pair<std::map<std::string, int>::iterator, bool> ret;
467 ret = warning_list.insert(std::pair<std::string, int>(std::string(function) + "-" + std::string(outputBuffer), line));
468 if (ret.second)
469 {
470 ssprintf(inputBuffer[useInputBuffer1 ? 1 : 0], "[%s:%d] %s (**Further warnings of this type are suppressed.)", function, line, outputBuffer);
471 }
472 else
473 {
474 return; // don't bother adding any more
475 }
476 }
477 else
478 {
479 ssprintf(inputBuffer[useInputBuffer1 ? 1 : 0], "[%s:%d] %s", function, line, outputBuffer);
480 }
481
482 if (sstrcmp(inputBuffer[0], inputBuffer[1]) == 0)
483 {
484 // Received again the same line
485 repeated++;
486 if (repeated == next)
487 {
488 if (repeated > 2)
489 {
490 ssprintf(outputBuffer, "last message repeated %u times (total %u repeats)", repeated - prev, repeated);
491 }
492 else
493 {
494 ssprintf(outputBuffer, "last message repeated %u times", repeated - prev);
495 }
496 printToDebugCallbacks(outputBuffer);
497 prev = repeated;
498 next *= 2;
499 }
500 }
501 else
502 {
503 // Received another line, cleanup the old
504 if (repeated > 0 && repeated != prev && repeated != 1)
505 {
506 /* just repeat the previous message when only one repeat occurred */
507 if (repeated > 2)
508 {
509 ssprintf(outputBuffer, "last message repeated %u times (total %u repeats)", repeated - prev, repeated);
510 }
511 else
512 {
513 ssprintf(outputBuffer, "last message repeated %u times", repeated - prev);
514 }
515 printToDebugCallbacks(outputBuffer);
516 }
517 repeated = 0;
518 next = 2;
519 prev = 0;
520 }
521
522 if (!repeated)
523 {
524 time_t rawtime;
525 struct tm timeinfo = {};
526 char ourtime[15]; //HH:MM:SS
527
528 time(&rawtime);
529 timeinfo = getLocalTime(rawtime);
530 strftime(ourtime, 15, "%H:%M:%S", &timeinfo);
531
532 // Assemble the outputBuffer:
533 ssprintf(outputBuffer, "%-8s|%s: %s", code_part_names[part], ourtime, useInputBuffer1 ? inputBuffer[1] : inputBuffer[0]);
534
535 printToDebugCallbacks(outputBuffer);
536
537 if (part == LOG_ERROR)
538 {
539 // used to signal user that there was a error condition, and to check the logs.
540 sstrcpy(errorStore, useInputBuffer1 ? inputBuffer[1] : inputBuffer[0]);
541 errorWaiting = true;
542 }
543
544 // Throw up a dialog box for users since most don't have a clue to check the dump file for information. Use for (duh) Fatal errors, that force us to terminate the game.
545 if (part == LOG_FATAL)
546 {
547 if (wzIsFullscreen())
548 {
549 wzChangeWindowMode(WINDOW_MODE::windowed);
550 }
551 #if defined(WZ_OS_WIN)
552 char wbuf[1024];
553 ssprintf(wbuf, "%s\n\nPlease check the file (%s) in your configuration directory for more details. \
554 \nDo not forget to upload the %s file, WZdebuginfo.txt and the warzone2100.rpt files in your bug reports at https://github.com/Warzone2100/warzone2100/issues/new!", useInputBuffer1 ? inputBuffer[1] : inputBuffer[0], WZ_DBGFile, WZ_DBGFile);
555 wzDisplayDialog(Dialog_Error, "Warzone has terminated unexpectedly", wbuf);
556 #elif defined(WZ_OS_MAC)
557 char wbuf[1024];
558 ssprintf(wbuf, "%s\n\nPlease check your logs and attach them along with a bug report. Thanks!", useInputBuffer1 ? inputBuffer[1] : inputBuffer[0]);
559 int clickedIndex = \
560 cocoaShowAlert("Warzone has quit unexpectedly.",
561 wbuf,
562 2, "Show Log Files & Open Bug Reporter", "Ignore", NULL);
563 if (clickedIndex == 0)
564 {
565 if (!cocoaOpenURL("https://github.com/Warzone2100/warzone2100/issues/new"))
566 {
567 cocoaShowAlert("Failed to open URL",
568 "Could not open URL: https://github.com/Warzone2100/warzone2100/issues/new\nPlease open this URL manually in your web browser.",
569 2, "Continue", NULL);
570 }
571 if (strnlen(WZ_DBGFile, sizeof(WZ_DBGFile)/sizeof(WZ_DBGFile[0])) <= 0)
572 {
573 cocoaShowAlert("Unable to open debug log.",
574 "The debug log subsystem has not yet been initialised.",
575 2, "Continue", NULL);
576 }
577 else
578 {
579 if (!cocoaSelectFileInFinder(WZ_DBGFile))
580 {
581 cocoaShowAlert("Cannot Display Log File",
582 "The attempt to open a Finder window highlighting the log file from this run failed.",
583 2, "Continue", NULL);
584 }
585 }
586 cocoaOpenUserCrashReportFolder();
587 }
588 #else
589 const char *popupBuf = useInputBuffer1 ? inputBuffer[1] : inputBuffer[0];
590 wzDisplayDialog(Dialog_Error, "Warzone has terminated unexpectedly", popupBuf);
591 #endif
592 }
593
594 // Throw up a dialog box for windows users since most don't have a clue to check the stderr.txt file for information
595 // This is a popup dialog used for times when the error isn't fatal, but we still need to notify user what is going on.
596 if (part == LOG_POPUP)
597 {
598 wzDisplayDialog(Dialog_Information, "Warzone has detected a problem.", inputBuffer[useInputBuffer1 ? 1 : 0]);
599 }
600
601 }
602 useInputBuffer1 = !useInputBuffer1; // Swap buffers
603 }
604
_debugBacktrace(code_part part)605 void _debugBacktrace(code_part part)
606 {
607 #if defined(WZ_OS_LINUX) && defined(__GLIBC__)
608 void *btv[20];
609 unsigned num = backtrace(btv, sizeof(btv) / sizeof(*btv));
610 char **btc = backtrace_symbols(btv, num);
611 unsigned i;
612 for (i = 1; i + 2 < num; ++i) // =1: Don't print "src/warzone2100(syncDebugBacktrace+0x16) [0x6312d1]". +2: Don't print last two lines of backtrace such as "/lib/libc.so.6(__libc_start_main+0xe6) [0x7f91e040ea26]", since the address varies (even with the same binary).
613 {
614 _debug(0, part, "BT", "%s", btc[i]);
615 }
616 free(btc);
617 #else
618 // debugBacktrace not implemented.
619 #endif
620 }
621
debugPartEnabled(code_part codePart)622 bool debugPartEnabled(code_part codePart)
623 {
624 return enabled_debug[codePart];
625 }
626
debugDisableAssert()627 void debugDisableAssert()
628 {
629 assertEnabled = false;
630 }
631
632 #include <sstream>
633
_debug_multiline(int line,code_part part,const char * function,const std::string & multilineString)634 void _debug_multiline(int line, code_part part, const char *function, const std::string &multilineString)
635 {
636 std::stringstream ss(multilineString);
637 std::string to;
638
639 while(std::getline(ss,to,'\n')){
640 _debug(line, part, function, "%s", to.c_str());
641 }
642 }
643
644