2020-05-01 07:37:48 -06:00
|
|
|
/*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/** \file
|
|
|
|
* \ingroup bli
|
|
|
|
*/
|
|
|
|
#include <Windows.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
|
|
|
|
#include <dbghelp.h>
|
|
|
|
#include <shlwapi.h>
|
|
|
|
#include <tlhelp32.h>
|
|
|
|
|
|
|
|
#include "BLI_string.h"
|
|
|
|
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
|
2020-05-08 09:39:41 -06:00
|
|
|
static EXCEPTION_POINTERS *current_exception = NULL;
|
2020-05-01 07:37:48 -06:00
|
|
|
|
|
|
|
static const char *bli_windows_get_exception_description(const DWORD exceptioncode)
|
|
|
|
{
|
|
|
|
switch (exceptioncode) {
|
|
|
|
case EXCEPTION_ACCESS_VIOLATION:
|
|
|
|
return "EXCEPTION_ACCESS_VIOLATION";
|
|
|
|
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
|
|
|
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
|
|
|
case EXCEPTION_BREAKPOINT:
|
|
|
|
return "EXCEPTION_BREAKPOINT";
|
|
|
|
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
|
|
|
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
|
|
|
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
|
|
|
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
|
|
|
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
|
|
|
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
|
|
|
case EXCEPTION_FLT_INEXACT_RESULT:
|
|
|
|
return "EXCEPTION_FLT_INEXACT_RESULT";
|
|
|
|
case EXCEPTION_FLT_INVALID_OPERATION:
|
|
|
|
return "EXCEPTION_FLT_INVALID_OPERATION";
|
|
|
|
case EXCEPTION_FLT_OVERFLOW:
|
|
|
|
return "EXCEPTION_FLT_OVERFLOW";
|
|
|
|
case EXCEPTION_FLT_STACK_CHECK:
|
|
|
|
return "EXCEPTION_FLT_STACK_CHECK";
|
|
|
|
case EXCEPTION_FLT_UNDERFLOW:
|
|
|
|
return "EXCEPTION_FLT_UNDERFLOW";
|
|
|
|
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
|
|
|
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
|
|
|
case EXCEPTION_IN_PAGE_ERROR:
|
|
|
|
return "EXCEPTION_IN_PAGE_ERROR";
|
|
|
|
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
|
|
|
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
|
|
|
case EXCEPTION_INT_OVERFLOW:
|
|
|
|
return "EXCEPTION_INT_OVERFLOW";
|
|
|
|
case EXCEPTION_INVALID_DISPOSITION:
|
|
|
|
return "EXCEPTION_INVALID_DISPOSITION";
|
|
|
|
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
|
|
|
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
|
|
|
case EXCEPTION_PRIV_INSTRUCTION:
|
|
|
|
return "EXCEPTION_PRIV_INSTRUCTION";
|
|
|
|
case EXCEPTION_SINGLE_STEP:
|
|
|
|
return "EXCEPTION_SINGLE_STEP";
|
|
|
|
case EXCEPTION_STACK_OVERFLOW:
|
|
|
|
return "EXCEPTION_STACK_OVERFLOW";
|
|
|
|
default:
|
|
|
|
return "UNKNOWN EXCEPTION";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_windows_get_module_name(LPVOID address, PCHAR buffer, size_t size)
|
|
|
|
{
|
|
|
|
HMODULE mod;
|
|
|
|
buffer[0] = 0;
|
|
|
|
if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, address, &mod)) {
|
|
|
|
if (GetModuleFileName(mod, buffer, size)) {
|
|
|
|
PathStripPath(buffer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_windows_get_module_version(const char *file, char *buffer, size_t buffersize)
|
|
|
|
{
|
|
|
|
buffer[0] = 0;
|
|
|
|
DWORD verHandle = 0;
|
|
|
|
UINT size = 0;
|
|
|
|
LPBYTE lpBuffer = NULL;
|
|
|
|
DWORD verSize = GetFileVersionInfoSize(file, &verHandle);
|
|
|
|
if (verSize != 0) {
|
|
|
|
LPSTR verData = (LPSTR)MEM_callocN(verSize, "crash module version");
|
|
|
|
|
|
|
|
if (GetFileVersionInfo(file, verHandle, verSize, verData)) {
|
|
|
|
if (VerQueryValue(verData, "\\", (VOID FAR * FAR *)&lpBuffer, &size)) {
|
|
|
|
if (size) {
|
|
|
|
VS_FIXEDFILEINFO *verInfo = (VS_FIXEDFILEINFO *)lpBuffer;
|
|
|
|
/* Magic value from
|
|
|
|
* https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
|
|
|
*/
|
|
|
|
if (verInfo->dwSignature == 0xfeef04bd) {
|
|
|
|
BLI_snprintf(buffer,
|
|
|
|
buffersize,
|
|
|
|
"%d.%d.%d.%d",
|
|
|
|
(verInfo->dwFileVersionMS >> 16) & 0xffff,
|
|
|
|
(verInfo->dwFileVersionMS >> 0) & 0xffff,
|
|
|
|
(verInfo->dwFileVersionLS >> 16) & 0xffff,
|
|
|
|
(verInfo->dwFileVersionLS >> 0) & 0xffff);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MEM_freeN(verData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_windows_system_backtrace_exception_record(FILE *fp, PEXCEPTION_RECORD record)
|
|
|
|
{
|
|
|
|
char module[MAX_PATH];
|
|
|
|
fprintf(fp, "Exception Record:\n\n");
|
|
|
|
fprintf(fp,
|
|
|
|
"ExceptionCode : %s\n",
|
|
|
|
bli_windows_get_exception_description(record->ExceptionCode));
|
|
|
|
fprintf(fp, "Exception Address : 0x%p\n", record->ExceptionAddress);
|
|
|
|
bli_windows_get_module_name(record->ExceptionAddress, module, sizeof(module));
|
|
|
|
fprintf(fp, "Exception Module : %s\n", module);
|
|
|
|
fprintf(fp, "Exception Flags : 0x%.8x\n", record->ExceptionFlags);
|
|
|
|
fprintf(fp, "Exception Parameters : 0x%x\n", record->NumberParameters);
|
|
|
|
for (DWORD idx = 0; idx < record->NumberParameters; idx++) {
|
|
|
|
fprintf(fp, "\tParameters[%d] : 0x%p\n", idx, (LPVOID *)record->ExceptionInformation[idx]);
|
|
|
|
}
|
|
|
|
if (record->ExceptionRecord) {
|
|
|
|
fprintf(fp, "Nested ");
|
|
|
|
bli_windows_system_backtrace_exception_record(fp, record->ExceptionRecord);
|
|
|
|
}
|
|
|
|
fprintf(fp, "\n\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool BLI_windows_system_backtrace_run_trace(FILE *fp, HANDLE hThread, PCONTEXT context)
|
|
|
|
{
|
|
|
|
const int max_symbol_length = 100;
|
|
|
|
|
|
|
|
bool result = true;
|
|
|
|
|
|
|
|
PSYMBOL_INFO symbolinfo = MEM_callocN(sizeof(SYMBOL_INFO) + max_symbol_length * sizeof(char),
|
|
|
|
"crash Symbol table");
|
|
|
|
symbolinfo->MaxNameLen = max_symbol_length - 1;
|
|
|
|
symbolinfo->SizeOfStruct = sizeof(SYMBOL_INFO);
|
|
|
|
|
|
|
|
STACKFRAME frame = {0};
|
|
|
|
frame.AddrPC.Offset = context->Rip;
|
|
|
|
frame.AddrPC.Mode = AddrModeFlat;
|
|
|
|
frame.AddrFrame.Offset = context->Rsp;
|
|
|
|
frame.AddrFrame.Mode = AddrModeFlat;
|
|
|
|
frame.AddrStack.Offset = context->Rsp;
|
|
|
|
frame.AddrStack.Mode = AddrModeFlat;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
if (StackWalk64(IMAGE_FILE_MACHINE_AMD64,
|
|
|
|
GetCurrentProcess(),
|
|
|
|
hThread,
|
|
|
|
&frame,
|
|
|
|
context,
|
|
|
|
NULL,
|
|
|
|
SymFunctionTableAccess64,
|
|
|
|
SymGetModuleBase64,
|
|
|
|
0)) {
|
|
|
|
if (frame.AddrPC.Offset) {
|
|
|
|
char module[MAX_PATH];
|
|
|
|
|
|
|
|
bli_windows_get_module_name((LPVOID)frame.AddrPC.Offset, module, sizeof(module));
|
|
|
|
|
|
|
|
if (SymFromAddr(GetCurrentProcess(), (DWORD64)(frame.AddrPC.Offset), 0, symbolinfo)) {
|
|
|
|
fprintf(fp, "%-20s:0x%p %s", module, (LPVOID)symbolinfo->Address, symbolinfo->Name);
|
|
|
|
IMAGEHLP_LINE lineinfo;
|
|
|
|
lineinfo.SizeOfStruct = sizeof(lineinfo);
|
|
|
|
DWORD displacement = 0;
|
|
|
|
if (SymGetLineFromAddr(
|
|
|
|
GetCurrentProcess(), (DWORD64)(frame.AddrPC.Offset), &displacement, &lineinfo)) {
|
|
|
|
fprintf(fp, " %s:%d", lineinfo.FileName, lineinfo.LineNumber);
|
|
|
|
}
|
|
|
|
fprintf(fp, "\n");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
fprintf(fp,
|
|
|
|
"%-20s:0x%p %s\n",
|
|
|
|
module,
|
|
|
|
(LPVOID)frame.AddrPC.Offset,
|
|
|
|
"Symbols not available");
|
|
|
|
result = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MEM_freeN(symbolinfo);
|
|
|
|
fprintf(fp, "\n\n");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-05-08 09:39:41 -06:00
|
|
|
static bool bli_windows_system_backtrace_stack_thread(FILE *fp, HANDLE hThread)
|
2020-05-01 07:37:48 -06:00
|
|
|
{
|
2020-05-08 09:39:41 -06:00
|
|
|
CONTEXT context = {0};
|
2020-05-01 07:37:48 -06:00
|
|
|
context.ContextFlags = CONTEXT_ALL;
|
2020-05-08 09:39:41 -06:00
|
|
|
/* GetThreadContext requires the thread to be in a suspended state, which is problematic for the
|
|
|
|
* currently running thread, RtlCaptureContext is used as an alternative to sidestep this */
|
2020-05-01 07:37:48 -06:00
|
|
|
if (hThread != GetCurrentThread()) {
|
2020-05-08 09:39:41 -06:00
|
|
|
SuspendThread(hThread);
|
|
|
|
bool success = GetThreadContext(hThread, &context);
|
2020-05-01 07:37:48 -06:00
|
|
|
ResumeThread(hThread);
|
2020-05-08 09:39:41 -06:00
|
|
|
if (!success) {
|
|
|
|
fprintf(fp, "Cannot get thread context : 0x0%.8x\n", GetLastError());
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
RtlCaptureContext(&context);
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
2020-05-08 09:39:41 -06:00
|
|
|
return BLI_windows_system_backtrace_run_trace(fp, hThread, &context);
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_windows_system_backtrace_modules(FILE *fp)
|
|
|
|
{
|
|
|
|
fprintf(fp, "Loaded Modules :\n");
|
|
|
|
HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0);
|
|
|
|
if (hModuleSnap == INVALID_HANDLE_VALUE)
|
|
|
|
return;
|
|
|
|
|
|
|
|
MODULEENTRY32 me32;
|
|
|
|
me32.dwSize = sizeof(MODULEENTRY32);
|
|
|
|
|
|
|
|
if (!Module32First(hModuleSnap, &me32)) {
|
|
|
|
CloseHandle(hModuleSnap); /* Must clean up the snapshot object! */
|
|
|
|
fprintf(fp, " Error getting module list.\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
if (me32.th32ProcessID == GetCurrentProcessId()) {
|
|
|
|
char version[MAX_PATH];
|
|
|
|
bli_windows_get_module_version(me32.szExePath, version, sizeof(version));
|
2020-05-08 10:01:56 -06:00
|
|
|
|
|
|
|
IMAGEHLP_MODULE64 m64;
|
|
|
|
m64.SizeOfStruct = sizeof(m64);
|
|
|
|
if (SymGetModuleInfo64(GetCurrentProcess(), (DWORD64)me32.modBaseAddr, &m64)) {
|
|
|
|
fprintf(fp,
|
|
|
|
"0x%p %-20s %s %s %s\n",
|
|
|
|
me32.modBaseAddr,
|
|
|
|
version,
|
|
|
|
me32.szModule,
|
|
|
|
m64.LoadedPdbName,
|
|
|
|
m64.PdbUnmatched ? "[unmatched]" : "");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
fprintf(fp, "0x%p %-20s %s\n", me32.modBaseAddr, version, me32.szModule);
|
|
|
|
}
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
|
|
|
} while (Module32Next(hModuleSnap, &me32));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_windows_system_backtrace_threads(FILE *fp)
|
|
|
|
{
|
|
|
|
fprintf(fp, "Threads:\n");
|
|
|
|
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
|
|
|
|
THREADENTRY32 te32;
|
|
|
|
|
|
|
|
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
|
|
|
if (hThreadSnap == INVALID_HANDLE_VALUE) {
|
|
|
|
fprintf(fp, "Unable to retrieve threads list.\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
te32.dwSize = sizeof(THREADENTRY32);
|
|
|
|
|
|
|
|
if (!Thread32First(hThreadSnap, &te32)) {
|
|
|
|
CloseHandle(hThreadSnap);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
if (te32.th32OwnerProcessID == GetCurrentProcessId()) {
|
|
|
|
if (GetCurrentThreadId() != te32.th32ThreadID) {
|
|
|
|
fprintf(fp, "Thread : %.8x\n", te32.th32ThreadID);
|
|
|
|
HANDLE ht = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
|
|
|
|
bli_windows_system_backtrace_stack_thread(fp, ht);
|
|
|
|
CloseHandle(ht);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} while (Thread32Next(hThreadSnap, &te32));
|
|
|
|
CloseHandle(hThreadSnap);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool BLI_windows_system_backtrace_stack(FILE *fp)
|
|
|
|
{
|
|
|
|
fprintf(fp, "Stack trace:\n");
|
2020-05-08 09:39:41 -06:00
|
|
|
/* If we are handling an exception use the context record from that. */
|
2020-05-09 18:12:14 -06:00
|
|
|
if (current_exception && current_exception->ExceptionRecord->ExceptionAddress) {
|
2020-05-08 09:39:41 -06:00
|
|
|
/* The back trace code will write to the context record, to protect the original record from
|
|
|
|
* modifications give the backtrace a copy to work on. */
|
|
|
|
CONTEXT TempContext = *current_exception->ContextRecord;
|
|
|
|
return BLI_windows_system_backtrace_run_trace(fp, GetCurrentThread(), &TempContext);
|
|
|
|
}
|
|
|
|
else {
|
2020-05-09 18:12:14 -06:00
|
|
|
/* If there is no current exception or the address is not set, walk the current stack. */
|
2020-05-08 09:39:41 -06:00
|
|
|
return bli_windows_system_backtrace_stack_thread(fp, GetCurrentThread());
|
|
|
|
}
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
static bool bli_private_symbols_loaded()
|
|
|
|
{
|
|
|
|
IMAGEHLP_MODULE64 m64;
|
|
|
|
m64.SizeOfStruct = sizeof(m64);
|
|
|
|
if (SymGetModuleInfo64(GetCurrentProcess(), (DWORD64)GetModuleHandle(NULL), &m64)) {
|
2020-05-08 09:46:39 -06:00
|
|
|
return m64.GlobalSymbols;
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void bli_load_symbols()
|
|
|
|
{
|
|
|
|
/* If this is a developer station and the private pdb is already loaded leave it be. */
|
|
|
|
if (bli_private_symbols_loaded()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
char pdb_file[MAX_PATH] = {0};
|
|
|
|
|
|
|
|
/* get the currently executing image */
|
|
|
|
if (GetModuleFileNameA(NULL, pdb_file, sizeof(pdb_file))) {
|
|
|
|
/* remove the filename */
|
|
|
|
PathRemoveFileSpecA(pdb_file);
|
|
|
|
/* append blender.pdb */
|
|
|
|
PathAppendA(pdb_file, "blender.pdb");
|
2020-05-03 12:07:22 -06:00
|
|
|
if (PathFileExistsA(pdb_file)) {
|
2020-05-01 07:37:48 -06:00
|
|
|
HMODULE mod = GetModuleHandle(NULL);
|
|
|
|
if (mod) {
|
2020-05-03 12:07:22 -06:00
|
|
|
WIN32_FILE_ATTRIBUTE_DATA file_data;
|
|
|
|
if (GetFileAttributesExA(pdb_file, GetFileExInfoStandard, &file_data)) {
|
|
|
|
/* SymInitialize will try to load symbols on its own, so we first must unload whatever it
|
|
|
|
* did trying to help */
|
|
|
|
SymUnloadModule64(GetCurrentProcess(), (DWORD64)mod);
|
|
|
|
|
|
|
|
DWORD64 module_base = SymLoadModule(GetCurrentProcess(),
|
|
|
|
NULL,
|
|
|
|
pdb_file,
|
|
|
|
NULL,
|
|
|
|
(DWORD64)mod,
|
|
|
|
(DWORD)file_data.nFileSizeLow);
|
|
|
|
if (module_base == 0) {
|
|
|
|
fprintf(stderr,
|
|
|
|
"Error loading symbols %s\n\terror:0x%.8x\n\tsize = %d\n\tbase=0x%p\n",
|
|
|
|
pdb_file,
|
|
|
|
GetLastError(),
|
|
|
|
file_data.nFileSizeLow,
|
|
|
|
(LPVOID)mod);
|
|
|
|
}
|
2020-05-01 07:37:48 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void BLI_system_backtrace(FILE *fp)
|
|
|
|
{
|
|
|
|
SymInitialize(GetCurrentProcess(), NULL, TRUE);
|
|
|
|
bli_load_symbols();
|
2020-05-08 09:39:41 -06:00
|
|
|
if (current_exception) {
|
|
|
|
bli_windows_system_backtrace_exception_record(fp, current_exception->ExceptionRecord);
|
|
|
|
}
|
2020-05-01 07:37:48 -06:00
|
|
|
if (BLI_windows_system_backtrace_stack(fp)) {
|
|
|
|
/* When the blender symbols are missing the stack traces will be unreliable
|
|
|
|
* so only run if the previous step completed successfully. */
|
|
|
|
bli_windows_system_backtrace_threads(fp);
|
|
|
|
}
|
|
|
|
bli_windows_system_backtrace_modules(fp);
|
|
|
|
}
|
|
|
|
|
|
|
|
void BLI_windows_handle_exception(EXCEPTION_POINTERS *exception)
|
|
|
|
{
|
|
|
|
current_exception = exception;
|
2020-05-08 09:39:41 -06:00
|
|
|
if (current_exception) {
|
|
|
|
fprintf(stderr,
|
|
|
|
"Error : %s\n",
|
|
|
|
bli_windows_get_exception_description(exception->ExceptionRecord->ExceptionCode));
|
|
|
|
fflush(stderr);
|
|
|
|
|
|
|
|
LPVOID address = exception->ExceptionRecord->ExceptionAddress;
|
|
|
|
fprintf(stderr, "Address : 0x%p\n", address);
|
|
|
|
|
|
|
|
CHAR modulename[MAX_PATH];
|
|
|
|
bli_windows_get_module_name(address, modulename, sizeof(modulename));
|
|
|
|
fprintf(stderr, "Module : %s\n", modulename);
|
2020-05-09 18:12:14 -06:00
|
|
|
fprintf(stderr, "Thread : %.8x\n", GetCurrentThreadId());
|
2020-05-08 09:39:41 -06:00
|
|
|
}
|
2020-05-01 07:37:48 -06:00
|
|
|
fflush(stderr);
|
|
|
|
}
|