/*
crashhandler.c - advanced crashhandler
Copyright (C) 2016 Mittorn

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 3 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.
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include "common.h"

/*
================
Sys_Crash

Crash handler, called from system
================
*/
#if XASH_WIN32

#if DBGHELP

#pragma comment( lib, "dbghelp" )

#include <winnt.h>
#include <dbghelp.h>
#include <psapi.h>
#include <time.h>

#ifndef XASH_SDL
typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR;
#endif

static int Sys_ModuleName( HANDLE process, char *name, void *address, int len )
{
	DWORD_PTR   baseAddress = 0;
	static HMODULE     *moduleArray;
	static unsigned int moduleCount;
	LPBYTE      moduleArrayBytes;
	DWORD       bytesRequired;
	int i;

	if( len < 3 )
		return 0;

	if( !moduleArray && EnumProcessModules( process, NULL, 0, &bytesRequired ) )
	{
		if ( bytesRequired )
		{
			moduleArrayBytes = (LPBYTE)LocalAlloc( LPTR, bytesRequired );

			if( moduleArrayBytes && EnumProcessModules( process, (HMODULE *)moduleArrayBytes, bytesRequired, &bytesRequired ) )
			{
				moduleCount = bytesRequired / sizeof( HMODULE );
				moduleArray = (HMODULE *)moduleArrayBytes;
			}
		}
	}

	for( i = 0; i < moduleCount; i++ )
	{
		MODULEINFO info;
		GetModuleInformation( process, moduleArray[i], &info, sizeof(MODULEINFO) );

		if( ( address > info.lpBaseOfDll ) &&
				( (DWORD64)address < (DWORD64)info.lpBaseOfDll + (DWORD64)info.SizeOfImage ) )
			return GetModuleBaseName( process, moduleArray[i], name, len );
	}
	return Q_snprintf( name, len, "???" );
}

static void Sys_StackTrace( PEXCEPTION_POINTERS pInfo )
{
	char message[1024];
	int len = 0;
	size_t i;
	HANDLE process = GetCurrentProcess();
	HANDLE thread = GetCurrentThread();
	IMAGEHLP_LINE64 line;
	DWORD dline = 0;
	DWORD options;
	CONTEXT context;
	STACKFRAME64 stackframe;
	DWORD image;

	memcpy( &context, pInfo->ContextRecord, sizeof( CONTEXT ));

	options = SymGetOptions();
	options |= SYMOPT_DEBUG;
	options |= SYMOPT_LOAD_LINES;
	SymSetOptions( options );

	SymInitialize( process, NULL, TRUE );

	ZeroMemory( &stackframe, sizeof( STACKFRAME64 ));

#ifdef _M_IX86
	image = IMAGE_FILE_MACHINE_I386;
	stackframe.AddrPC.Offset = context.Eip;
	stackframe.AddrPC.Mode = AddrModeFlat;
	stackframe.AddrFrame.Offset = context.Ebp;
	stackframe.AddrFrame.Mode = AddrModeFlat;
	stackframe.AddrStack.Offset = context.Esp;
	stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
	image = IMAGE_FILE_MACHINE_AMD64;
	stackframe.AddrPC.Offset = context.Rip;
	stackframe.AddrPC.Mode = AddrModeFlat;
	stackframe.AddrFrame.Offset = context.Rsp;
	stackframe.AddrFrame.Mode = AddrModeFlat;
	stackframe.AddrStack.Offset = context.Rsp;
	stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
	image = IMAGE_FILE_MACHINE_IA64;
	stackframe.AddrPC.Offset = context.StIIP;
	stackframe.AddrPC.Mode = AddrModeFlat;
	stackframe.AddrFrame.Offset = context.IntSp;
	stackframe.AddrFrame.Mode = AddrModeFlat;
	stackframe.AddrBStore.Offset = context.RsBSP;
	stackframe.AddrBStore.Mode = AddrModeFlat;
	stackframe.AddrStack.Offset = context.IntSp;
	stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_ARM
	image = IMAGE_FILE_MACHINE_ARMNT;
	stackframe.AddrPC.Offset = context.Pc;
	stackframe.AddrPC.Mode = AddrModeFlat;
	stackframe.AddrFrame.Offset = context.R11;
	stackframe.AddrFrame.Mode = AddrModeFlat;
	stackframe.AddrStack.Offset = context.Sp;
	stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_ARM64
	image = IMAGE_FILE_MACHINE_ARM64;
	stackframe.AddrPC.Offset = context.Pc;
	stackframe.AddrPC.Mode = AddrModeFlat;
	stackframe.AddrFrame.Offset = context.Fp;
	stackframe.AddrFrame.Mode = AddrModeFlat;
	stackframe.AddrStack.Offset = context.Sp;
	stackframe.AddrStack.Mode = AddrModeFlat;	
#elif
#error
#endif
	len += Q_snprintf( message + len, 1024 - len, "Sys_Crash: address %p, code %p\n",
		pInfo->ExceptionRecord->ExceptionAddress, (void*)pInfo->ExceptionRecord->ExceptionCode );
	if( SymGetLineFromAddr64( process, (DWORD64)pInfo->ExceptionRecord->ExceptionAddress, &dline, &line ) )
	{
		len += Q_snprintf(message + len, 1024 - len, "Exception: %s:%d:%d\n",
			(char*)line.FileName, (int)line.LineNumber, (int)dline);
	}
	if( SymGetLineFromAddr64( process, stackframe.AddrPC.Offset, &dline, &line ) )
	{
		len += Q_snprintf(message + len, 1024 - len,"PC: %s:%d:%d\n",
			(char*)line.FileName, (int)line.LineNumber, (int)dline);
	}
	if( SymGetLineFromAddr64( process, stackframe.AddrFrame.Offset, &dline, &line ) )
	{
		len += Q_snprintf(message + len, 1024 - len,"Frame: %s:%d:%d\n",
			(char*)line.FileName, (int)line.LineNumber, (int)dline);
	}
	for( i = 0; i < 25; i++ )
	{
		char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
		PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer;
		BOOL result = StackWalk64(
			image, process, thread,
			&stackframe, &context, NULL,
			SymFunctionTableAccess64, SymGetModuleBase64, NULL);
		DWORD64 displacement = 0;

		if( !result )
			break;

		symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
		symbol->MaxNameLen = MAX_SYM_NAME;

		len += Q_snprintf( message + len, 1024 - len, "% 2d %p",
			i, (void*)stackframe.AddrPC.Offset );
		if( SymFromAddr( process, stackframe.AddrPC.Offset, &displacement, symbol ) )
		{
			len += Q_snprintf( message + len, 1024 - len, " %s ", symbol->Name );
		}
		if( SymGetLineFromAddr64( process, stackframe.AddrPC.Offset, &dline, &line ) )
		{
			len += Q_snprintf(message + len, 1024 - len,"(%s:%d:%d) ",
				(char*)line.FileName, (int)line.LineNumber, (int)dline);
		}
		len += Q_snprintf( message + len, 1024 - len, "(");
		len += Sys_ModuleName( process, message + len, (void*)stackframe.AddrPC.Offset, 1024 - len );
		len += Q_snprintf( message + len, 1024 - len, ")\n");
	}

#if XASH_SDL == 2
	if( host.type != HOST_DEDICATED ) // let system to restart server automaticly
		SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Sys_Crash", message, host.hWnd );
#endif

	Sys_PrintLog( message );

	SymCleanup( process );
}

static void Sys_GetProcessName( char *processName, size_t bufferSize )
{
	char fullpath[MAX_PATH];

	GetModuleBaseName( GetCurrentProcess(), NULL, fullpath, sizeof( fullpath ) - 1 );
	COM_FileBase( fullpath, processName, bufferSize );
}

static void Sys_GetMinidumpFileName( const char *processName, char *mdmpFileName, size_t bufferSize )
{
	time_t currentUtcTime = time( NULL );
	struct tm *currentLocalTime = localtime( &currentUtcTime );

	Q_snprintf( mdmpFileName, bufferSize, "%s_%s_crash_%d%.2d%.2d_%.2d%.2d%.2d.mdmp",
		processName,
		Q_buildcommit(),
		currentLocalTime->tm_year + 1900,
		currentLocalTime->tm_mon + 1,
		currentLocalTime->tm_mday,
		currentLocalTime->tm_hour,
		currentLocalTime->tm_min,
		currentLocalTime->tm_sec);
}

static qboolean Sys_WriteMinidump(PEXCEPTION_POINTERS exceptionInfo, MINIDUMP_TYPE minidumpType)
{
	HRESULT errorCode;
	string processName;
	string mdmpFileName;
	MINIDUMP_EXCEPTION_INFORMATION minidumpInfo;

	Sys_GetProcessName( processName, sizeof( processName ));
	Sys_GetMinidumpFileName( processName, mdmpFileName, sizeof( mdmpFileName ));

	SetLastError( NOERROR );
	HANDLE fileHandle = CreateFile(
		mdmpFileName, GENERIC_WRITE, FILE_SHARE_WRITE,
		NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

	errorCode = HRESULT_FROM_WIN32( GetLastError( ));
	if( !SUCCEEDED( errorCode )) {
		CloseHandle( fileHandle );
		return false;
	}

	minidumpInfo.ThreadId = GetCurrentThreadId();
	minidumpInfo.ExceptionPointers = exceptionInfo;
	minidumpInfo.ClientPointers = FALSE;

	qboolean status = MiniDumpWriteDump(
		GetCurrentProcess(), GetCurrentProcessId(), fileHandle,
		minidumpType, &minidumpInfo, NULL, NULL);

	CloseHandle( fileHandle );
	return status;
}

#endif /* DBGHELP */

LPTOP_LEVEL_EXCEPTION_FILTER       oldFilter;
static long _stdcall Sys_Crash( PEXCEPTION_POINTERS pInfo )
{
	// save config
	if( host.status != HOST_CRASHED )
	{
		// check to avoid recursive call
		host.crashed = true;

#if DBGHELP
		Sys_StackTrace( pInfo );
#else
		Sys_Warn( "Sys_Crash: call %p at address %p", pInfo->ExceptionRecord->ExceptionAddress, pInfo->ExceptionRecord->ExceptionCode );
#endif

		if( host.type == HOST_NORMAL )
			CL_Crashed(); // tell client about crash
		else host.status = HOST_CRASHED;

#if DBGHELP
		if( Sys_CheckParm( "-minidumps" ))
		{
			int minidumpFlags = (
				MiniDumpWithDataSegs | 
				MiniDumpWithCodeSegs |
				MiniDumpWithHandleData | 
				MiniDumpWithFullMemory |
				MiniDumpWithFullMemoryInfo |
				MiniDumpWithIndirectlyReferencedMemory |
				MiniDumpWithThreadInfo |
				MiniDumpWithModuleHeaders);

			if( !Sys_WriteMinidump( pInfo, (MINIDUMP_TYPE)minidumpFlags )) {
				// fallback method, create minidump with minimal info in it
				Sys_WriteMinidump( pInfo, MiniDumpWithDataSegs );
			}
		}
#endif

		if( host_developer.value <= 0 )
		{
			// no reason to call debugger in release build - just exit
			Sys_Quit();
			return EXCEPTION_CONTINUE_EXECUTION;
		}

		// all other states keep unchanged to let debugger find bug
		Sys_DestroyConsole();
	}

	if( oldFilter )
		return oldFilter( pInfo );
	return EXCEPTION_CONTINUE_EXECUTION;
}

void Sys_SetupCrashHandler( void )
{
	SetErrorMode( SEM_FAILCRITICALERRORS );	// no abort/retry/fail errors
	oldFilter = SetUnhandledExceptionFilter( Sys_Crash );
	host.hInst = GetModuleHandle( NULL );
}

void Sys_RestoreCrashHandler( void )
{
	// restore filter
	if( oldFilter ) SetUnhandledExceptionFilter( oldFilter );
}

#elif XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD || XASH_ANDROID || XASH_LINUX
// Posix signal handler
#ifndef XASH_OPENBSD
#include <ucontext.h>
#endif
#include <signal.h>
#include <sys/mman.h>
#include "library.h"

#define STACK_BACKTRACE_STR     "Stack backtrace:\n"
#define STACK_DUMP_STR          "Stack dump:\n"

#define STACK_BACKTRACE_STR_LEN ( sizeof( STACK_BACKTRACE_STR ) - 1 )
#define STACK_DUMP_STR_LEN      ( sizeof( STACK_DUMP_STR ) - 1 )
#define ALIGN( x, y ) (((uintptr_t) ( x ) + (( y ) - 1 )) & ~(( y ) - 1 ))

static struct sigaction oldFilter;

static int Sys_PrintFrame( char *buf, int len, int i, void *addr )
{
	Dl_info dlinfo;
	if( len <= 0 )
		return 0; // overflow

	if( dladdr( addr, &dlinfo ))
	{
		if( dlinfo.dli_sname )
			return Q_snprintf( buf, len, "%2d: %p <%s+%lu> (%s)\n", i, addr, dlinfo.dli_sname,
				(unsigned long)addr - (unsigned long)dlinfo.dli_saddr, dlinfo.dli_fname ); // print symbol, module and address

		return Q_snprintf( buf, len, "%2d: %p (%s)\n", i, addr, dlinfo.dli_fname ); // print module and address
	}

	return Q_snprintf( buf, len, "%2d: %p\n", i, addr ); // print only address
}

static void Sys_Crash( int signal, siginfo_t *si, void *context)
{
	void *pc = NULL, **bp = NULL, **sp = NULL; // this must be set for every OS!
	char message[8192];
	int len, logfd, i = 0;

#if XASH_OPENBSD
	struct sigcontext *ucontext = (struct sigcontext*)context;
#else
	ucontext_t *ucontext = (ucontext_t*)context;
#endif

#if XASH_AMD64
	#if XASH_FREEBSD
		pc = (void*)ucontext->uc_mcontext.mc_rip;
		bp = (void**)ucontext->uc_mcontext.mc_rbp;
		sp = (void**)ucontext->uc_mcontext.mc_rsp;
	#elif XASH_NETBSD
		pc = (void*)ucontext->uc_mcontext.__gregs[_REG_RIP];
		bp = (void**)ucontext->uc_mcontext.__gregs[_REG_RBP];
		sp = (void**)ucontext->uc_mcontext.__gregs[_REG_RSP];
	#elif XASH_OPENBSD
		pc = (void*)ucontext->sc_rip;
		bp = (void**)ucontext->sc_rbp;
		sp = (void**)ucontext->sc_rsp;
	#else
		pc = (void*)ucontext->uc_mcontext.gregs[REG_RIP];
		bp = (void**)ucontext->uc_mcontext.gregs[REG_RBP];
		sp = (void**)ucontext->uc_mcontext.gregs[REG_RSP];
	#endif
#elif XASH_X86
	#if XASH_FREEBSD
		pc = (void*)ucontext->uc_mcontext.mc_eip;
		bp = (void**)ucontext->uc_mcontext.mc_ebp;
		sp = (void**)ucontext->uc_mcontext.mc_esp;
	#elif XASH_NETBSD
		pc = (void*)ucontext->uc_mcontext.__gregs[_REG_EIP];
		bp = (void**)ucontext->uc_mcontext.__gregs[_REG_EBP];
		sp = (void**)ucontext->uc_mcontext.__gregs[_REG_ESP];
	#elif XASH_OPENBSD
		pc = (void*)ucontext->sc_eip;
		bp = (void**)ucontext->sc_ebp;
		sp = (void**)ucontext->sc_esp;
	#else
		pc = (void*)ucontext->uc_mcontext.gregs[REG_EIP];
		bp = (void**)ucontext->uc_mcontext.gregs[REG_EBP];
		sp = (void**)ucontext->uc_mcontext.gregs[REG_ESP];
	#endif
#elif XASH_ARM && XASH_64BIT
	pc = (void*)ucontext->uc_mcontext.pc;
	bp = (void*)ucontext->uc_mcontext.regs[29];
	sp = (void*)ucontext->uc_mcontext.sp;
#elif XASH_ARM
	pc = (void*)ucontext->uc_mcontext.arm_pc;
	bp = (void*)ucontext->uc_mcontext.arm_fp;
	sp = (void*)ucontext->uc_mcontext.arm_sp;
#endif

	// safe actions first, stack and memory may be corrupted
	len = Q_snprintf( message, sizeof( message ), "Ver: " XASH_ENGINE_NAME " " XASH_VERSION " (build %i-%s, %s-%s)\n",
		Q_buildnum(), Q_buildcommit(), Q_buildos(), Q_buildarch() );

#if !XASH_FREEBSD && !XASH_NETBSD && !XASH_OPENBSD
	len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p %p\n", signal, si->si_errno, si->si_code, si->si_addr, si->si_ptr );
#else
	len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p\n", signal, si->si_errno, si->si_code, si->si_addr );
#endif

	write( STDERR_FILENO, message, len );

	// flush buffers before writing directly to descriptors
	fflush( stdout );
	fflush( stderr );

	// now get log fd and write trace directly to log
	logfd = Sys_LogFileNo();
	write( logfd, message, len );

	if( pc && bp && sp )
	{
		size_t pagesize = sysconf( _SC_PAGESIZE );

		// try to print backtrace
		write( STDERR_FILENO, STACK_BACKTRACE_STR, STACK_BACKTRACE_STR_LEN );
		write( logfd, STACK_BACKTRACE_STR, STACK_BACKTRACE_STR_LEN );
		Q_strncpy( message + len, STACK_BACKTRACE_STR, sizeof( message ) - len );
		len += STACK_BACKTRACE_STR_LEN;

// false on success, true on failure
#define try_allow_read(pointer, pagesize) \
	((mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_WRITE | PROT_EXEC ) == -1) && \
	( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_EXEC ) == -1) && \
	( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_WRITE ) == -1) && \
	( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ ) == -1))

		do
		{
			int line = Sys_PrintFrame( message + len, sizeof( message ) - len, ++i, pc);
			write( STDERR_FILENO, message + len, line );
			write( logfd, message + len, line );
			len += line;
			//if( !dladdr(bp,0) ) break; // only when bp is in module
			if( try_allow_read( bp, pagesize ) )
				break;
			if( try_allow_read( bp[0], pagesize ) )
				break;
			pc = bp[1];
			bp = (void**)bp[0];
		}
		while( bp && i < 128 );

		// try to print stack
		write( STDERR_FILENO, STACK_DUMP_STR, STACK_DUMP_STR_LEN );
		write( logfd, STACK_DUMP_STR, STACK_DUMP_STR_LEN );
		Q_strncpy( message + len, STACK_DUMP_STR, sizeof( message ) - len );
		len += STACK_DUMP_STR_LEN;

		if( !try_allow_read( sp, pagesize ) )
		{
			for( i = 0; i < 32; i++ )
			{
				int line = Sys_PrintFrame( message + len, sizeof( message ) - len, i, sp[i] );
				write( STDERR_FILENO, message + len, line );
				write( logfd, message + len, line );
				len += line;
			}
		}

#undef try_allow_read
	}

	// put MessageBox as Sys_Error
	Msg( "%s\n", message );
#ifdef XASH_SDL
	SDL_SetWindowGrab( host.hWnd, SDL_FALSE );
#endif
	host.crashed = true;
	Platform_MessageBox( "Xash Error", message, false );

	// log saved, now we can try to save configs and close log correctly, it may crash
	if( host.type == HOST_NORMAL )
		CL_Crashed();
	host.status = HOST_CRASHED;


	Sys_Quit();
}

void Sys_SetupCrashHandler( void )
{
	struct sigaction act = { 0 };
	act.sa_sigaction = Sys_Crash;
	act.sa_flags = SA_SIGINFO | SA_ONSTACK;
	sigaction( SIGSEGV, &act, &oldFilter );
	sigaction( SIGABRT, &act, &oldFilter );
	sigaction( SIGBUS,  &act, &oldFilter );
	sigaction( SIGILL,  &act, &oldFilter );
}

void Sys_RestoreCrashHandler( void )
{
	sigaction( SIGSEGV, &oldFilter, NULL );
	sigaction( SIGABRT, &oldFilter, NULL );
	sigaction( SIGBUS,  &oldFilter, NULL );
	sigaction( SIGILL,  &oldFilter, NULL );
}

#else

void Sys_SetupCrashHandler( void )
{
	// stub
}

void Sys_RestoreCrashHandler( void )
{
	// stub
}
#endif
