diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 03840d0e..ebd7e349 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -26,12 +26,7 @@ body: id: logs attributes: label: Relevant logs/screenshots - description: If available, please provide relevant logs or screenshots. - placeholder: | - Crash logs or dmp files may be found under the following: - Windows: crash dmp file - C:\Users\yourusername\AppData\Local\Tahoma2D\Tahoma2D\cache\crashrpt\xxxx\crashdump.dmp - crash logs - Event Viewer -> Windows Logs -> Application. Look for Tahoma2D crash report - macOS: crash logs - Console App -> User Reports. Look for Tahoma2D crash report + description: If available, please provide relevant logs, screenshots or short videos. Windows users can add zipped crash dmp files. validations: required: false - type: markdown @@ -45,6 +40,7 @@ body: label: Software Version description: What version of the software are you running? options: + - 1.3.1 - 1.3 - Nightly (Specify below) - 1.2 diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index a63c7dbd..4561bfd4 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -46,10 +46,6 @@ jobs: with: name: Tahoma2D-win path: artifact - - uses: actions/upload-artifact@v1 - with: - name: debug-symbols.zip - path: toonz\build\debug-symbols.zip - name: Get Nightly Release Date if: ${{ github.repository_owner == 'tahoma2d' && github.event_name == 'push' && github.ref == 'refs/heads/master' }} run: | @@ -74,7 +70,7 @@ jobs: with: allowUpdates: true artifactErrorsFailBuild: false - artifacts: toonz\build\Tahoma2D-win.zip,toonz\build\debug-symbols.zip + artifacts: toonz\build\Tahoma2D-win.zip artifactContentType: "raw" body: ${{ github.event.head_commit.message }} name: Latest Nightly ${{ env.NIGHTLYDATE }} @@ -88,7 +84,7 @@ jobs: with: allowUpdates: true artifactErrorsFailBuild: false - artifacts: toonz\build\Tahoma2D-win.zip,toonz\build\debug-symbols.zip + artifacts: toonz\build\Tahoma2D-win.zip artifactContentType: "raw" body: ${{ github.event.head_commit.message }} name: ${{ env.NIGHTLYDATETIME }} diff --git a/ci-scripts/windows/tahoma-build.bat b/ci-scripts/windows/tahoma-build.bat index 75cc7cea..d75e77f7 100644 --- a/ci-scripts/windows/tahoma-build.bat +++ b/ci-scripts/windows/tahoma-build.bat @@ -28,10 +28,7 @@ IF EXIST ..\..\thirdparty\canon\Header set WITH_CANON=Y set WITH_GPHOTO2=N IF EXIST ..\..\thirdparty\libgphoto2\include set WITH_GPHOTO2=Y -set WITH_CRASHRPT=N -IF EXIST ..\..\thirdparty\crashrpt\include set WITH_CRASHRPT=Y - -cmake ..\sources -G %MSVCVERSION% -Ax64 -DQT_PATH=%QT_PATH% -DBOOST_ROOT=%BOOST_ROOT% -DOpenCV_DIR=%OPENCV_DIR% -DWITH_CANON=%WITH_CANON% -DWITH_GPHOTO2=%WITH_GPHOTO2% -DWITH_CRASHRPT=%WITH_CRASHRPT% +cmake ..\sources -G %MSVCVERSION% -Ax64 -DQT_PATH=%QT_PATH% -DBOOST_ROOT=%BOOST_ROOT% -DOpenCV_DIR=%OPENCV_DIR% -DWITH_CANON=%WITH_CANON% -DWITH_GPHOTO2=%WITH_GPHOTO2% IF EXIST C:\ProgramData\chocolatey\bin\cl.exe ( diff --git a/ci-scripts/windows/tahoma-buildpkg.bat b/ci-scripts/windows/tahoma-buildpkg.bat index 820db2e4..109a3338 100644 --- a/ci-scripts/windows/tahoma-buildpkg.bat +++ b/ci-scripts/windows/tahoma-buildpkg.bat @@ -10,10 +10,6 @@ echo ">>> Copy and configure Tahoma2D installation" copy /y RelWithDebInfo\*.* Tahoma2D -REM Remove PDB and ILK files -del Tahoma2D\*.pdb -del Tahoma2D\*.ilk - copy /Y ..\..\thirdparty\freeglut\bin\x64\freeglut.dll Tahoma2D copy /Y ..\..\thirdparty\glew\glew-1.9.0\bin\64bit\glew32.dll Tahoma2D copy /Y ..\..\thirdparty\libmypaint\dist\64\libiconv-2.dll Tahoma2D @@ -36,11 +32,8 @@ IF EXIST ..\..\thirdparty\libgphoto2\include ( xcopy /Y /E ..\..\thirdparty\libgphoto2\bin Tahoma2D ) -IF EXIST ..\..\thirdparty\crashrpt\include ( - copy /Y ..\..\thirdparty\apps\crashrpt\CrashRpt1500.dll Tahoma2D - copy /Y ..\..\thirdparty\apps\crashrpt\CrashSender1500.exe Tahoma2D - copy /Y ..\..\thirdparty\apps\crashrpt\crashrpt_lang.ini Tahoma2D -) +REM Remove ILK files +del Tahoma2D\*.ilk echo ">>> Copying stuff to Tahoma2D\tahomastuff" @@ -80,14 +73,4 @@ echo ">>> Creating Tahoma2D Windows package" IF EXIST Tahoma2D-win.zip del Tahoma2D-win.zip 7z a Tahoma2D-win.zip Tahoma2D -IF EXIST ..\..\..\tahoma2d_symbols ( - echo ">>> Saving debugging symbols" - mkdir ..\..\..\tahoma2d_symbols\%date:~10,4%-%date:~4,2%-%date:~7,2% - copy /y RelWithDebInfo\*.* ..\..\..\tahoma2d_symbols\%date:~10,4%-%date:~4,2%-%date:~7,2% -) else ( - echo ">>> Creating debugging symbols package" - IF EXIST debug-symbols.zip del debug-symbols.zip - 7z a debug-symbols.zip RelWithDebInfo\*.* -) - cd ../.. diff --git a/ci-scripts/windows/tahoma-get3rdpartyapps.bat b/ci-scripts/windows/tahoma-get3rdpartyapps.bat index 1ed3038e..ee49ca6a 100644 --- a/ci-scripts/windows/tahoma-get3rdpartyapps.bat +++ b/ci-scripts/windows/tahoma-get3rdpartyapps.bat @@ -5,17 +5,6 @@ IF NOT EXIST apps mkdir apps cd apps echo * > .gitignore -echo ">>> Getting CrashRpt" - -IF EXIST crashrpt rmdir /S /Q crashrpt -curl -fsSL -o crashrpt-tahoma2d-win_2019.zip https://github.com/tahoma2d/crashrpt2/releases/download/v1.5.0.0/crashrpt-tahoma2d-win_2019.zip -7z x crashrpt-tahoma2d-win_2019.zip -rename crashrpt-tahoma2d-win_2019 crashrpt -IF EXIST ..\crashrpt\include rmdir /S /Q ..\crashrpt\include -IF EXIST ..\crashrpt\CrashRpt1500.lib del ..\crashrpt\CrashRpt1500.lib -move crashrpt\include ..\crashrpt -move crashrpt\CrashRpt1500.lib ..\crashrpt - echo ">>> Getting FFmpeg" IF EXIST ffmpeg rmdir /S /Q ffmpeg diff --git a/stuff/doc/LICENSE/LICENSE_crashrpt.txt b/stuff/doc/LICENSE/LICENSE_crashrpt.txt deleted file mode 100644 index 36c115f3..00000000 --- a/stuff/doc/LICENSE/LICENSE_crashrpt.txt +++ /dev/null @@ -1,28 +0,0 @@ -CrashRpt - -Copyright (c) 2003, The CrashRpt Project Authors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name of the author nor the names of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT -SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/thirdparty/crashrpt/.gitignore b/thirdparty/crashrpt/.gitignore deleted file mode 100644 index a474c4fd..00000000 --- a/thirdparty/crashrpt/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -include/* -*.lib \ No newline at end of file diff --git a/thirdparty/crashrpt/copy_crashrpt.txt b/thirdparty/crashrpt/copy_crashrpt.txt deleted file mode 100644 index dddd392a..00000000 --- a/thirdparty/crashrpt/copy_crashrpt.txt +++ /dev/null @@ -1,10 +0,0 @@ -For Windows Builds Only! - -Copy the include folder from the CrashRpt source to here. -Compile CrashRpt and copy the CrashRptXXXX.lib here also. - -When releasing with CrashRpt, include: - CrashRptXXXX.dll - CrashSenderXXXX.exe - crashrpt_lang.ini - dbghelp.dll (optional) \ No newline at end of file diff --git a/toonz/sources/CMakeLists.txt b/toonz/sources/CMakeLists.txt index 7c81eadd..a0dd626d 100644 --- a/toonz/sources/CMakeLists.txt +++ b/toonz/sources/CMakeLists.txt @@ -107,7 +107,6 @@ option(WITH_SYSTEM_SUPERLU "Use the system SuperLU library instead of 'thirdpary option(WITH_CANON "Build with Canon DSLR support - Requires Canon SDK" OFF) option(WITH_GPHOTO2 "Build with Libgphoto2" OFF) option(WITH_TRANSLATION "Generate translation projects as well" ON) -option(WITH_CRASHRPT "Build CrashRpt support - Requires CrashRpt Library" OFF) option(WITH_WINTAB "(Windows only) Build with customized Qt with WinTab support. https://github.com/shun-iwasawa/qt5/releases/tag/v5.15.2_wintab" OFF) # avoid using again @@ -284,12 +283,6 @@ if(BUILD_ENV_MSVC) -DGLUT_NO_LIB_PRAGMA ) - if(WITH_CRASHRPT) - include_directories( - ${SDKROOT}/crashrpt/include - ) - endif() - if(WITH_GPHOTO2) include_directories( ${SDKROOT}/libgphoto2/include @@ -399,10 +392,6 @@ if(BUILD_ENV_MSVC) set(MYPAINT_LIB_INCLUDE_DIRS ${SDKROOT}/libmypaint/dist/${PLATFORM}/include/libmypaint) set(MYPAINT_LIB_LDFLAGS ${SDKROOT}/libmypaint/dist/${PLATFORM}/libmypaint.lib) - if(WITH_CRASHRPT) - set(CRASHRPT_LIB ${SDKROOT}/crashrpt/CrashRpt1500.lib) - endif() - if(WITH_GPHOTO2) set(GPHOTO2_LIB ${SDKROOT}/libgphoto2/lib/libgphoto2.lib) set(GPHOTO2_PORT_LIB ${SDKROOT}/libgphoto2/lib/libgphoto2_port.lib) diff --git a/toonz/sources/common/tcore/tstring.cpp b/toonz/sources/common/tcore/tstring.cpp index 743db4b2..928918b7 100644 --- a/toonz/sources/common/tcore/tstring.cpp +++ b/toonz/sources/common/tcore/tstring.cpp @@ -18,13 +18,6 @@ #include -class TStringConvertException final : public TException { - std::string m_string; - -public: - TStringConvertException(const std::string str) : m_string(str) {} -}; - std::wstring to_wstring(std::string s) { #ifdef TNZCORE_LIGHT std::wstring ws; diff --git a/toonz/sources/include/toonz/toonzfolders.h b/toonz/sources/include/toonz/toonzfolders.h index ca3ba460..333073f9 100644 --- a/toonz/sources/include/toonz/toonzfolders.h +++ b/toonz/sources/include/toonz/toonzfolders.h @@ -44,6 +44,7 @@ DVAPI TFilePath getLibraryFolder(); DVAPI TFilePath getPluginsFolder(); DVAPI TFilePath getReslistPath(bool forCleanup); DVAPI TFilePath getCacheRootFolder(); +DVAPI TFilePath getCrashReportFolder(); DVAPI TFilePath getProfileFolder(); DVAPI TFilePath getMyReslistPath(bool forCleanup); diff --git a/toonz/sources/tnzcore/CMakeLists.txt b/toonz/sources/tnzcore/CMakeLists.txt index 118cf23a..398b87bc 100644 --- a/toonz/sources/tnzcore/CMakeLists.txt +++ b/toonz/sources/tnzcore/CMakeLists.txt @@ -316,6 +316,7 @@ if(BUILD_ENV_MSVC) winmm.lib opengl32.lib glu32.lib + dbghelp.lib ) elseif(BUILD_ENV_APPLE) find_library(AUDIOUNIT_LIB AudioUnit) @@ -344,6 +345,7 @@ elseif(BUILD_ENV_UNIXLIKE) -lmpr -lwinmm -lpsapi + -ldbghelp ) endif() endif() diff --git a/toonz/sources/toonz/CMakeLists.txt b/toonz/sources/toonz/CMakeLists.txt index bd433b62..cb94b2b9 100644 --- a/toonz/sources/toonz/CMakeLists.txt +++ b/toonz/sources/toonz/CMakeLists.txt @@ -30,6 +30,7 @@ set(MOC_HEADERS commandbar.h commandbarpopup.h convertpopup.h + crashhandler.h duplicatepopup.h dvdirtreeview.h dvitemview.h @@ -251,6 +252,7 @@ set(SOURCES addfilmstripframespopup.cpp camerasettingspopup.cpp convertpopup.cpp + crashhandler.cpp duplicatepopup.cpp dvdirtreeview.cpp filebrowserpopup.cpp @@ -449,14 +451,6 @@ if (WITH_CANON) add_definitions(-DWITH_CANON) endif() -if (WITH_CRASHRPT) - add_definitions(-DWITH_CRASHRPT) -endif() - -if (WITH_GPHOTO2) - add_definitions(-DWITH_GPHOTO2) -endif() - if (WITH_WINTAB AND BUILD_TARGET_WIN AND (PLATFORM EQUAL 64)) add_definitions(-DWITH_WINTAB) endif() @@ -483,10 +477,6 @@ if(BUILD_ENV_MSVC) set(EXTRA_LIBS ${EXTRA_LIBS} ${CANON_LIB}) endif() - if(WITH_CRASHRPT) - set(EXTRA_LIBS ${EXTRA_LIBS} ${CRASHRPT_LIB}) - endif() - if(WITH_GPHOTO2) set(EXTRA_LIBS ${EXTRA_LIBS} ${GPHOTO2_LIB} ${GPHOTO2_PORT_LIB} ${GPHOTO2_COMPAT_LIB}) endif() diff --git a/toonz/sources/toonz/crashhandler.cpp b/toonz/sources/toonz/crashhandler.cpp new file mode 100644 index 00000000..1271266f --- /dev/null +++ b/toonz/sources/toonz/crashhandler.cpp @@ -0,0 +1,647 @@ +#include "crashhandler.h" + +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include "tgl.h" +#include "tapp.h" +#include "tenv.h" +#include "tconvert.h" +#include "texception.h" +#include "tfilepath_io.h" +#include "toonz/toonzfolders.h" +#include "toonz/tproject.h" +#include "toonz/tscenehandle.h" +#include "toonz/toonzscene.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QWidget *s_parentWindow = NULL; +static bool s_reportProjInfo = false; +#ifdef _WIN32 +static PEXCEPTION_POINTERS s_exceptionPtr = NULL; +#endif + +//----------------------------------------------------------------------------- + +static const char *filenameOnly(const char *path) { + for (int i = strlen(path); i >= 0; --i) { + if (path[i] == '\\' || path[i] == '/') return path + i + 1; + } + return path; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Windows platform functions + +#ifdef _WIN32 + +#define HAS_MINIDUMP +static bool generateMinidump(TFilePath dumpFile, + PEXCEPTION_POINTERS exceptionInfo) { + HANDLE hDumpFile = CreateFileW(dumpFile.getWideString().c_str(), + GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); + if (hDumpFile == INVALID_HANDLE_VALUE) return false; + + MINIDUMP_EXCEPTION_INFORMATION mdei; + mdei.ThreadId = GetCurrentThreadId(); + mdei.ExceptionPointers = exceptionInfo; + mdei.ClientPointers = TRUE; + + if (MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, + MiniDumpNormal, &mdei, 0, NULL)) { + CloseHandle(hDumpFile); + return true; + } + + return false; +} + +#define HAS_MODULES +static void printModules(std::string &out) { + HANDLE hProcess = GetCurrentProcess(); + + HMODULE modules[1024]; + DWORD size; + if (EnumProcessModules(hProcess, modules, sizeof(modules), &size)) { + for (unsigned int i = 0; i < size / sizeof(HMODULE); i++) { + char moduleName[512]; + GetModuleFileNameA(modules[i], moduleName, 512); + out.append(moduleName); + out.append("\n"); + } + } +} + +#define HAS_BACKTRACE +static void printBacktrace(std::string &out) { + int frameStack = 0; + int frameSkip = 3; + + HANDLE hProcess = GetCurrentProcess(); + + SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_INCLUDE_32BIT_MODULES | + SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_LOAD_LINES | + SYMOPT_UNDNAME); + + SymInitialize(hProcess, NULL, TRUE); + + CONTEXT context; + RtlCaptureContext(&context); + + char sourceSymMem[sizeof(IMAGEHLP_SYMBOL64) + 1025]; + PIMAGEHLP_SYMBOL64 sourceSym = (PIMAGEHLP_SYMBOL64)&sourceSymMem; + memset(sourceSymMem, 0, sizeof(sourceSymMem)); + + IMAGEHLP_LINE64 sourceInfo; + memset(&sourceInfo, 0, sizeof(IMAGEHLP_LINE64)); + sourceInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + + IMAGEHLP_MODULE64 moduleInfo; + memset(&moduleInfo, 0, sizeof(moduleInfo)); + moduleInfo.SizeOfStruct = sizeof(IMAGEHLP_MODULE64); + + STACKFRAME64 stackframe; + memset(&stackframe, 0, sizeof(STACKFRAME64)); + +#ifdef _WIN64 + int machineType = IMAGE_FILE_MACHINE_AMD64; + stackframe.AddrPC.Offset = context.Rip; + stackframe.AddrPC.Mode = AddrModeFlat; + stackframe.AddrStack.Offset = context.Rsp; + stackframe.AddrStack.Mode = AddrModeFlat; + stackframe.AddrFrame.Offset = context.Rbp; + stackframe.AddrFrame.Mode = AddrModeFlat; +#else + int machineType = IMAGE_FILE_MACHINE_I386; + stackframe.AddrPC.Offset = context.Eip; + stackframe.AddrPC.Mode = AddrModeFlat; + stackframe.AddrStack.Offset = context.Esp; + stackframe.AddrStack.Mode = AddrModeFlat; + stackframe.AddrFrame.Offset = context.Ebp; + stackframe.AddrFrame.Mode = AddrModeFlat; +#endif + + HANDLE hThread = GetCurrentThread(); + while (StackWalk64(machineType, hProcess, hThread, &stackframe, &context, + NULL, SymFunctionTableAccess64, SymGetModuleBase64, + NULL)) { + // Skip first frames since they point to this function + if (frameStack++ < frameSkip) continue; + char numStr[32]; + memset(numStr, 0, sizeof(numStr)); + sprintf(numStr, "%3i> ", frameStack - frameSkip); + out.append(numStr); + + sourceSym->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64); + sourceSym->MaxNameLength = 1024; + + // Get symbol name + DWORD64 displacement64; + if (SymGetSymFromAddr64(hProcess, (ULONG64)stackframe.AddrPC.Offset, + &displacement64, sourceSym)) { + out.append(sourceSym->Name); + + // Get module filename + char moduleFile[512]; + MEMORY_BASIC_INFORMATION mbi; + VirtualQuery((LPCVOID)stackframe.AddrPC.Offset, &mbi, sizeof(mbi)); + GetModuleFileNameA((HMODULE)mbi.AllocationBase, moduleFile, 512); + + // Get source filename and line + DWORD displacement32; + if (SymGetLineFromAddr64(hProcess, stackframe.AddrPC.Offset, + &displacement32, &sourceInfo) != FALSE) { + out.append(" {"); + out.append(filenameOnly(sourceInfo.FileName)); + out.append(":"); + out.append(std::to_string(sourceInfo.LineNumber)); + out.append("}"); + } else { + memset(numStr, 0, sizeof(numStr)); + sprintf(numStr, " [0x%" PRIx64 "]", stackframe.AddrPC.Offset); + out.append(numStr); + } + out.append(" <"); + out.append(filenameOnly(moduleFile)); + out.append(">"); + } + + out.append("\n"); + } + + SymCleanup(hProcess); +} + +//----------------------------------------------------------------------------- + +LONG WINAPI exceptionHandler(PEXCEPTION_POINTERS info) { + static volatile bool handling = false; + + const char *reason = "Unknown"; + switch (info->ExceptionRecord->ExceptionCode) { + case EXCEPTION_ACCESS_VIOLATION: + reason = "EXCEPTION_ACCESS_VIOLATION"; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + reason = "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + break; + case EXCEPTION_DATATYPE_MISALIGNMENT: + reason = "EXCEPTION_DATATYPE_MISALIGNMENT"; + break; + case EXCEPTION_FLT_DENORMAL_OPERAND: + reason = "EXCEPTION_FLT_DENORMAL_OPERAND"; + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + reason = "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_FLT_INEXACT_RESULT: + reason = "EXCEPTION_FLT_INEXACT_RESULT"; + break; + case EXCEPTION_FLT_INVALID_OPERATION: + reason = "EXCEPTION_FLT_INVALID_OPERATION"; + break; + case EXCEPTION_FLT_OVERFLOW: + reason = "EXCEPTION_FLT_OVERFLOW"; + break; + case EXCEPTION_FLT_STACK_CHECK: + reason = "EXCEPTION_FLT_STACK_CHECK"; + break; + case EXCEPTION_FLT_UNDERFLOW: + reason = "EXCEPTION_FLT_UNDERFLOW"; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + reason = "EXCEPTION_ILLEGAL_INSTRUCTION"; + break; + case EXCEPTION_IN_PAGE_ERROR: + reason = "EXCEPTION_IN_PAGE_ERROR"; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + reason = "EXCEPTION_INT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_INVALID_DISPOSITION: + reason = "EXCEPTION_INVALID_DISPOSITION"; + break; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + reason = "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + break; + case EXCEPTION_PRIV_INSTRUCTION: + reason = "EXCEPTION_PRIV_INSTRUCTION"; + break; + case EXCEPTION_STACK_OVERFLOW: + reason = "EXCEPTION_STACK_OVERFLOW"; + break; + default: + return EXCEPTION_CONTINUE_SEARCH; + } + + // Avoid new exceptions inside the crash handler + if (handling) return EXCEPTION_CONTINUE_SEARCH; + + handling = true; + s_exceptionPtr = info; + if (CrashHandler::trigger(reason, true)) _Exit(1); + handling = false; + + return EXCEPTION_CONTINUE_SEARCH; +} + +#endif + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Linux and Mac OS X platform functions + +#ifndef _WIN32 + +static bool sh(std::string &out, const char *cmd) { + char buffer[128]; + FILE *p = popen(cmd, "r"); + if (p == NULL) return false; + while (fgets(buffer, 128, p)) out.append(buffer); + pclose(p); + return true; +} + +//----------------------------------------------------------------------------- + +static bool addr2line(std::string &out, const char *exepath, const char *addr) { + char cmd[512]; +#ifdef OSX + sprintf(cmd, "atos -o \"%.400s\" %s 2>&1", exepath, addr); +#else + sprintf(cmd, "addr2line -f -p -e \"%.400s\" %s 2>&1", exepath, addr); +#endif + return sh(out, cmd); +} + +//----------------------------------------------------------------------------- + +static bool generateMinidump(TFilePath dumpFile) { return false; } + +//----------------------------------------------------------------------------- + +static void printModules(std::string &out) {} + +//----------------------------------------------------------------------------- + +#define HAS_BACKTRACE +static void printBacktrace(std::string &out) { + int frameStack = 0; + int frameSkip = 3; + + const int size = 256; + void *buffer[size]; + + // Get executable path + char exepath[512]; + memset(exepath, 0, 512); + if (readlink("/proc/self/exe", exepath, 512) < 0) + fprintf(stderr, "Couldn't get exe path\n"); + + // Back trace + int nptrs = backtrace(buffer, size); + char **bts = backtrace_symbols(buffer, nptrs); + std::regex re("\\[(.+)\\]"); + if (bts) { + for (int i = 0; i < nptrs; ++i) { + // Skip first frames since they point to this function + if (frameStack++ < frameSkip) continue; + char numStr[32]; + memset(numStr, 0, sizeof(numStr)); + sprintf(numStr, "%3i> ", frameStack - frameSkip); + out.append(numStr); + + std::string sym = bts[i]; + std::string line; + std::smatch ms; + + bool found = false; + if (std::regex_search(sym, ms, re)) { + std::string addr = ms[1]; + if (addr2line(line, exepath, addr.c_str())) { + found = (line.rfind("??", 0) != 0); + } + } + + out.append(found ? line : (sym + "\n")); + } + } + + free(bts); +} + +void signalHandler(int sig) { + static volatile bool handling = false; + + const char *reason = "Unknown"; + switch (sig) { + case SIGABRT: + reason = "(SIGABRT) Usually caused by an abort() or assert()"; + break; + case SIGFPE: + reason = "(SIGFPE) Arithmetic exception, such as divide by zero"; + break; + case SIGILL: + reason = "(SIGILL) Illegal instruction"; + break; + case SIGINT: + reason = "(SIGINT) Interactive attention signal, (usually ctrl+c)"; + break; + case SIGSEGV: + reason = "(SIGSEGV) Segmentation Fault"; + break; + case SIGTERM: + reason = "(SIGTERM) A termination request was sent to the program"; + break; + } + + // Avoid new signals inside the crash handler + if (handling) return; + + handling = true; + if (CrashHandler::trigger(reason, true)) _Exit(1); + handling = false; +} + +#endif + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +static void printSysInfo(std::string &out) { + out.append("Build ABI: " + QSysInfo::buildAbi().toStdString() + "\n"); + out.append("Operating System: " + QSysInfo::prettyProductName().toStdString() + "\n"); + out.append("OS Kernel: " + QSysInfo::kernelVersion().toStdString() + "\n"); + out.append("CPU Threads: " + std::to_string(QThread::idealThreadCount()) + "\n"); +} + +//----------------------------------------------------------------------------- + +static void printGPUInfo(std::string &out) { + const char *gpuVendorName = (const char *)glGetString(GL_VENDOR); + const char *gpuModelName = (const char *)glGetString(GL_RENDERER); + const char *gpuVersion = (const char *)glGetString(GL_VERSION); + if (gpuVendorName) + out.append("GPU Vendor: " + std::string(gpuVendorName) + "\n"); + if (gpuModelName) + out.append("GPU Model: " + std::string(gpuModelName) + "\n"); + if (gpuVersion) + out.append("GPU Version: " + std::string(gpuVersion) + "\n"); +} + +//----------------------------------------------------------------------------- + +CrashHandler::CrashHandler(QWidget *parent, TFilePath crashFile, QString crashReport) + : QDialog(parent), m_crashFile(crashFile), m_crashReport(crashReport) { + setWindowFlag(Qt::WindowContextHelpButtonHint, false); + + QStringList sl; + sl.append(tr("Tahoma2D crashed unexpectedly.")); + sl.append(""); + sl.append(tr("A crash report has been generated.")); + sl.append( + tr("To report, click 'Open Issue Webpage' to access Tahoma2D's Issues " + "page on GitHub.")); + sl.append(tr("Click on the 'New issue' button and fill out the form.")); + sl.append(""); + sl.append(tr("System Configuration and Problem Details:")); + + QLabel *headtext = new QLabel(sl.join("
")); + headtext->setTextFormat(Qt::RichText); + + QTextEdit *reportTxt = new QTextEdit(); + reportTxt->setText(crashReport); + reportTxt->setReadOnly(true); + reportTxt->setLineWrapMode(QTextEdit::LineWrapMode::NoWrap); + reportTxt->setStyleSheet( + "background:white;\ncolor:black;\nborder:1 solid black;"); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + QHBoxLayout *buttonsLay = new QHBoxLayout(); + + QPushButton *copyBtn = new QPushButton(tr("Copy to Clipboard")); + QPushButton *webBtn = new QPushButton(tr("Open Issue Webpage")); + QPushButton *folderBtn = new QPushButton(tr("Open Reports Folder")); + QPushButton *closeBtn = new QPushButton(tr("Close Application")); + buttonsLay->addWidget(copyBtn); + buttonsLay->addWidget(webBtn); + buttonsLay->addWidget(folderBtn); + buttonsLay->addWidget(closeBtn); + + mainLayout->addWidget(headtext); + mainLayout->addWidget(reportTxt); + mainLayout->addLayout(buttonsLay); + + bool ret = connect(copyBtn, SIGNAL(clicked()), this, SLOT(copyClipboard())); + ret = ret && connect(webBtn, SIGNAL(clicked()), this, SLOT(openWebpage())); + ret = ret && connect(folderBtn, SIGNAL(clicked()), this, SLOT(openFolder())); + ret = ret && connect(closeBtn, SIGNAL(clicked()), this, SLOT(accept())); + if (!ret) throw TException(); + + setWindowTitle(tr("Tahoma2D crashed!")); + setLayout(mainLayout); +} + +void CrashHandler::reject() { + QStringList sl; + sl.append(tr("Application is in unstable state and must be restarted.")); + sl.append(tr("Resuming is not recommended and may lead to an unrecoverable crash.")); + sl.append(tr("Ignore advice and try to resume program?")); + + QMessageBox::StandardButton reply = + QMessageBox::question(this, tr("Ignore crash?"), sl.join("\n"), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + QDialog::reject(); + } +} + +//----------------------------------------------------------------------------- + +void CrashHandler::copyClipboard() { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(m_crashReport); +} + +//----------------------------------------------------------------------------- + +void CrashHandler::openWebpage() { + QDesktopServices::openUrl(QUrl("https://github.com/tahoma2d/tahoma2d/issues")); +} + +//----------------------------------------------------------------------------- + +void CrashHandler::openFolder() { + TFilePath fp = ToonzFolder::getCrashReportFolder(); + QDesktopServices::openUrl(QUrl("file:///" + fp.getQString())); +} + +//----------------------------------------------------------------------------- + +void CrashHandler::install() { +#ifdef _WIN32 + // std library seems to override this + //SetUnhandledExceptionFilter(exceptionHandler); + + void *handler = AddVectoredExceptionHandler(0, exceptionHandler); + assert(handler != NULL); + //RemoveVectoredExceptionHandler(handler); +#else + signal(SIGABRT, signalHandler); + signal(SIGFPE, signalHandler); + signal(SIGILL, signalHandler); + signal(SIGINT, signalHandler); + signal(SIGSEGV, signalHandler); + signal(SIGTERM, signalHandler); +#endif +} + +//----------------------------------------------------------------------------- + +void CrashHandler::reportProjectInfo(bool enableReport) { + s_reportProjInfo = enableReport; +} + +//----------------------------------------------------------------------------- + +void CrashHandler::attachParentWindow(QWidget *parent) { + s_parentWindow = parent; +} + +//----------------------------------------------------------------------------- + +bool CrashHandler::trigger(const QString reason, bool showDialog) { + char fileName[128]; + char dumpName[128]; + char dateName[128]; + std::string out; + + // Get time and build filename + time_t acc_time; + time(&acc_time); + struct tm *tm = localtime(&acc_time); + strftime(dateName, 128, "%Y-%m-%d %H:%M:%S", tm); + strftime(fileName, 128, "Crash-%Y%m%d-%H%M%S.log", tm); + strftime(dumpName, 128, "Crash-%Y%m%d-%H%M%S.dmp", tm); + TFilePath fpCrsh = ToonzFolder::getCrashReportFolder() + fileName; + TFilePath fpDump = ToonzFolder::getCrashReportFolder() + dumpName; + + // Generate minidump +#ifdef _WIN32 + bool minidump = generateMinidump(fpDump, s_exceptionPtr); +#else + bool minidump = generateMinidump(fpDump); +#endif; + + // Generate report + try { + out.append(TEnv::getApplicationFullName() + " (Build " + __DATE__ ")\n"); + out.append("\nReport Date: "); + out.append(dateName); + out.append("\nCrash Reason: "); + out.append(reason.toStdString()); + out.append("\n\n"); + printSysInfo(out); + out.append("\n"); + printGPUInfo(out); + out.append("\nCrash File: "); + out.append(fpCrsh.getQString().toStdString()); +#ifdef HAS_MINIDUMP + out.append("\nMini Dump File: "); + if (minidump) + out.append(fpDump.getQString().toStdString()); + else + out.append("Failed"); +#endif + out.append("\n"); + } catch (...) { + } + try { + if (s_reportProjInfo) { + TProjectManager *pm = TProjectManager::instance(); + TApp *app = TApp::instance(); + + TProjectP currentProject = pm->getCurrentProject(); + TFilePath projectPath = currentProject->getProjectPath(); + + ToonzScene *currentScene = app->getCurrentScene()->getScene(); + std::wstring sceneName = currentScene->getSceneName(); + + out.append("\nApplication Dir: "); + out.append(QCoreApplication::applicationDirPath().toStdString()); + out.append("\nStuff Dir: "); + out.append(TEnv::getStuffDir().getQString().toStdString()); + out.append("\n"); + out.append("\nProject Name: "); + out.append(currentProject->getName().getQString().toStdString()); + out.append("\nScene Name: "); + out.append(QString::fromStdWString(sceneName).toStdString()); + out.append("\nProject Path: "); + out.append(projectPath.getQString().toStdString()); + out.append("\nScene Path: "); + out.append(currentScene->getScenePath().getQString().toStdString()); + out.append("\n"); + } + } catch (...) { + } +#ifdef HAS_BACKTRACE + try { + out.append("\n==== Backtrace ====\n"); + printBacktrace(out); + out.append("==== End ====\n"); + } catch (...) { + } +#endif +#ifdef HAS_MODULES + try { + out.append("\n==== Modules ====\n"); + printModules(out); + out.append("==== End ====\n"); + } catch (...) { + } +#endif + + // Save to crash information to file + FILE *fw = fopen(fpCrsh, "w"); + if (fw != NULL) { + fwrite(out.c_str(), 1, out.size(), fw); + fclose(fw); + } + + if (showDialog) { + // Show crash handler dialog + CrashHandler crashdialog(s_parentWindow, fpCrsh, QString::fromStdString(out)); + return crashdialog.exec() != QDialog::Rejected; + } + + return true; +} + +//----------------------------------------------------------------------------- diff --git a/toonz/sources/toonz/crashhandler.h b/toonz/sources/toonz/crashhandler.h new file mode 100644 index 00000000..3e94ab30 --- /dev/null +++ b/toonz/sources/toonz/crashhandler.h @@ -0,0 +1,33 @@ +#pragma once + +#ifndef CRASHHANDLER_INCLUDED +#define CRASHHANDLER_INCLUDED + +#include "tcommon.h" +#include "tfilepath.h" + +#include + +class CrashHandler : public QDialog { + Q_OBJECT; + + TFilePath m_crashFile; + QString m_crashReport; + +public: + CrashHandler(QWidget *parent, TFilePath crashFile, QString crashTxt); + + void reject(); + + static void install(); + static void reportProjectInfo(bool enableReport); + static void attachParentWindow(QWidget *parent); + static bool trigger(const QString reason, bool showDialog); + +public slots: + void copyClipboard(); + void openWebpage(); + void openFolder(); +}; + +#endif // CRASHHANDLER_INCLUDED diff --git a/toonz/sources/toonz/main.cpp b/toonz/sources/toonz/main.cpp index 38f2c2f2..163d16b9 100644 --- a/toonz/sources/toonz/main.cpp +++ b/toonz/sources/toonz/main.cpp @@ -1,9 +1,7 @@ // Soli Deo gloria -#ifdef WITH_CRASHRPT -#include -#endif // Tnz6 includes +#include "crashhandler.h" #include "mainwindow.h" #include "flipbook.h" #include "tapp.h" @@ -70,10 +68,6 @@ #include "kis_tablet_support_win8.h" -#ifdef WITH_CRASHRPT -#include "CrashRpt.h" -#endif - #ifdef MACOSX #include "tipc.h" #endif @@ -258,18 +252,23 @@ static void script_output(int type, const QString &value) { int main(int argc, char *argv[]) { #ifdef Q_OS_WIN - // Enable standard input/output on Windows Platform for debug - BOOL consoleAttached = ::AttachConsole(ATTACH_PARENT_PROCESS); - if (consoleAttached) { + // Enable standard input/output on Windows Platform for debug + if (::AttachConsole(ATTACH_PARENT_PROCESS)) { freopen("CON", "r", stdin); freopen("CON", "w", stdout); freopen("CON", "w", stderr); + atexit([]() { + ::FreeConsole(); + }); } #endif // Build icon map ThemeManager::getInstance().buildIconPathsMap(":/icons"); + // Install signal handlers to catch crashes + CrashHandler::install(); + // parsing arguments and qualifiers TFilePath loadFilePath; QString argumentLayoutFileName = ""; @@ -578,34 +577,6 @@ int main(int argc, char *argv[]) { // Toonz environment initToonzEnv(argumentPathValues); -#ifdef WITH_CRASHRPT - std::string str; - - CR_INSTALL_INFO pInfo; - memset(&pInfo, 0, sizeof(CR_INSTALL_INFO)); - pInfo.cb = sizeof(CR_INSTALL_INFO); - - str = TEnv::getApplicationName(); - std::wstring wAppName = std::wstring(str.begin(), str.end()); - pInfo.pszAppName = wAppName.c_str(); - - str = TEnv::getApplicationVersion(); - std::wstring wAppVersion = std::wstring(str.begin(), str.end()); - pInfo.pszAppVersion = wAppVersion.c_str(); - - TFilePath crashrptCache = - ToonzFolder::getCacheRootFolder() + TFilePath("crashrpt"); - str = crashrptCache.getQString().toStdString(); - std::wstring wRptdir = std::wstring(str.begin(), str.end()); - pInfo.pszErrorReportSaveDir = wRptdir.c_str(); - - // Install all available exception handlers. - // Don't send reports automaticall, store locally - pInfo.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS | CR_INST_DONT_SEND_REPORT; - - crInstall(&pInfo); -#endif - // prepare for 30bit display if (Preferences::instance()->is30bitDisplayEnabled()) { QSurfaceFormat sFmt = QSurfaceFormat::defaultFormat(); @@ -703,6 +674,8 @@ int main(int argc, char *argv[]) { /*-- Layoutファイル名をMainWindowのctorに渡す --*/ MainWindow w(argumentLayoutFileName); + CrashHandler::attachParentWindow(&w); + CrashHandler::reportProjectInfo(true); TFilePath fp = ToonzFolder::getModuleFile("mainwindow.ini"); QSettings settings(toQString(fp), QSettings::IniFormat); @@ -911,15 +884,5 @@ int main(int argc, char *argv[]) { TUndoManager::manager()->reset(); PreviewFxManager::instance()->reset(); -#ifdef _WIN32 - if (consoleAttached) { - ::FreeConsole(); - } -#endif - -#ifdef WITH_CRASHRPT - crUninstall(); -#endif - return ret; } diff --git a/toonz/sources/toonzlib/toonzfolders.cpp b/toonz/sources/toonzlib/toonzfolders.cpp index dc952283..5200e344 100644 --- a/toonz/sources/toonzlib/toonzfolders.cpp +++ b/toonz/sources/toonzlib/toonzfolders.cpp @@ -125,6 +125,20 @@ TFilePath ToonzFolder::getCacheRootFolder() { return (state == OK) ? TFilePath(cacheDir) : TFilePath(); } +TFilePath ToonzFolder::getCrashReportFolder() { + static enum STATE { FIRSTTIME, OK, NG } state = FIRSTTIME; + QString crashDir = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + + "/crash"; + if (state == FIRSTTIME) { + if (QDir(crashDir).mkpath(".")) + state = OK; + else + state = NG; + } + return (state == OK) ? TFilePath(crashDir) : TFilePath(); +} + TFilePath ToonzFolder::getProfileFolder() { TFilePath fp = getSystemVarPathValue(getSystemVarPrefix() + "PROFILES"); if (fp == TFilePath())