diff --git a/Framework/Kernel/inc/MantidKernel/ErrorReporter.h b/Framework/Kernel/inc/MantidKernel/ErrorReporter.h index 2531a430113c..daa4aa45c81a 100644 --- a/Framework/Kernel/inc/MantidKernel/ErrorReporter.h +++ b/Framework/Kernel/inc/MantidKernel/ErrorReporter.h @@ -30,7 +30,8 @@ class MANTID_KERNEL_DLL ErrorReporter { const std::string &textBox); /// Constructor ErrorReporter(std::string application, Types::Core::time_duration startTime, std::string exitCode, bool share, - std::string name, std::string email, std::string textBox, std::string stacktrace); + std::string name, std::string email, std::string textBox, std::string stacktrace, + std::string cppTraces); /// Sends an error report Kernel::InternetHelper::HTTPStatus sendErrorReport(); /// Generates an error string in json format @@ -50,6 +51,7 @@ class MANTID_KERNEL_DLL ErrorReporter { const std::string m_textbox; std::string m_url; const std::string m_stacktrace; + const std::string m_cppTraces; }; } // namespace Kernel diff --git a/Framework/Kernel/src/ErrorReporter.cpp b/Framework/Kernel/src/ErrorReporter.cpp index 10d2f82bffbc..5bd62547cdab 100644 --- a/Framework/Kernel/src/ErrorReporter.cpp +++ b/Framework/Kernel/src/ErrorReporter.cpp @@ -33,21 +33,21 @@ Logger g_log("ErrorReporter"); */ ErrorReporter::ErrorReporter(const std::string &application, const Types::Core::time_duration &upTime, const std::string &exitCode, const bool share) - : ErrorReporter(application, upTime, exitCode, share, "", "", "", "") {} + : ErrorReporter(application, upTime, exitCode, share, "", "", "", "", "") {} /** Constructor */ ErrorReporter::ErrorReporter(const std::string &application, const Types::Core::time_duration &upTime, const std::string &exitCode, const bool share, const std::string &name, const std::string &email, const std::string &textBox) - : ErrorReporter(application, upTime, exitCode, share, name, email, textBox, "") {} + : ErrorReporter(application, upTime, exitCode, share, name, email, textBox, "", "") {} ErrorReporter::ErrorReporter(std::string application, Types::Core::time_duration upTime, std::string exitCode, const bool share, std::string name, std::string email, std::string textBox, - std::string traceback) + std::string traceback, std::string cppTraces) : m_application(std::move(application)), m_exitCode(std::move(exitCode)), m_upTime(std::move(upTime)), m_share(share), m_name(std::move(name)), m_email(std::move(email)), m_textbox(std::move(textBox)), - m_stacktrace(std::move(traceback)) { + m_stacktrace(std::move(traceback)), m_cppTraces(std::move(cppTraces)) { auto url = Mantid::Kernel::ConfigService::Instance().getValue("errorreports.rooturl"); if (!url.has_value()) { g_log.debug() << "Failed to load error report url\n"; @@ -113,11 +113,13 @@ std::string ErrorReporter::generateErrorMessage() const { message["email"] = m_email; message["name"] = m_name; message["stacktrace"] = m_stacktrace; + message["cppCompressedTraces"] = m_cppTraces; } else { message["email"] = ""; message["name"] = ""; message["textBox"] = m_textbox; message["stacktrace"] = ""; + message["cppCompressedTraces"] = ""; } return Mantid::JsonHelpers::jsonToString(message); diff --git a/Framework/Kernel/test/ErrorReporterTest.h b/Framework/Kernel/test/ErrorReporterTest.h index 6fcd6d5f04b8..1a6a7b892ad2 100644 --- a/Framework/Kernel/test/ErrorReporterTest.h +++ b/Framework/Kernel/test/ErrorReporterTest.h @@ -66,7 +66,7 @@ class ErrorReporterTest : public CxxTest::TestSuite { const std::string appName = "My testing application name"; const Mantid::Types::Core::time_duration upTime(5, 0, 7, 0); const std::string stackTrace = "File \" C :\\file\\path\\file.py\", line 194, in broken_function"; - TestableErrorReporter reporter(appName, upTime, "0", true, "name", "email", "textBox", stackTrace); + TestableErrorReporter reporter(appName, upTime, "0", true, "name", "email", "textBox", stackTrace, ""); const std::string message = reporter.generateErrorMessage(); ::Json::Value root; @@ -102,7 +102,7 @@ class ErrorReporterTest : public CxxTest::TestSuite { void test_errorMessageWithShareAndRecoveryFileHash() { const std::string name = "My testing application name"; const Mantid::Types::Core::time_duration upTime(5, 0, 7, 0); - TestableErrorReporter errorService(name, upTime, "0", true, "name", "email", "textBox", "stacktrace"); + TestableErrorReporter errorService(name, upTime, "0", true, "name", "email", "textBox", "stacktrace", "cppTraces"); const std::string message = errorService.generateErrorMessage(); ::Json::Value root; @@ -111,7 +111,7 @@ class ErrorReporterTest : public CxxTest::TestSuite { const std::vector expectedMembers{ "ParaView", "application", "host", "mantidSha1", "mantidVersion", "osArch", "osName", "osReadable", "osVersion", "uid", "facility", "upTime", - "exitCode", "textBox", "name", "email", "stacktrace"}; + "exitCode", "textBox", "name", "email", "stacktrace", "cppCompressedTraces"}; for (auto expectedMember : expectedMembers) { TSM_ASSERT(expectedMember + " not found", std::find(members.begin(), members.end(), expectedMember) != members.end()); @@ -124,12 +124,13 @@ class ErrorReporterTest : public CxxTest::TestSuite { TS_ASSERT_EQUALS(root["email"].asString(), "email"); TS_ASSERT_EQUALS(root["textBox"].asString(), "textBox"); TS_ASSERT_EQUALS(root["stacktrace"].asString(), "stacktrace"); + TS_ASSERT_EQUALS(root["cppCompressedTraces"].asString(), "cppTraces"); } void test_errorMessageWithNoShareAndRecoveryFileHash() { const std::string name = "My testing application name"; const Mantid::Types::Core::time_duration upTime(5, 0, 7, 0); - TestableErrorReporter errorService(name, upTime, "0", false, "name", "email", "textBox", "stacktrace"); + TestableErrorReporter errorService(name, upTime, "0", false, "name", "email", "textBox", "stacktrace", "cppTraces"); const std::string message = errorService.generateErrorMessage(); ::Json::Value root; @@ -138,7 +139,8 @@ class ErrorReporterTest : public CxxTest::TestSuite { const std::vector expectedMembers{ "ParaView", "application", "host", "mantidSha1", "mantidVersion", "osArch", "osName", "osReadable", "osVersion", "uid", "facility", "upTime", - "exitCode", "textBox", "name", "email", "stacktrace"}; + "exitCode", "textBox", "name", "email", "stacktrace", "upTime", + "exitCode", "textBox", "name", "email", "stacktrace", "cppCompressedTraces"}; for (auto expectedMember : expectedMembers) { TSM_ASSERT(expectedMember + " not found", std::find(members.begin(), members.end(), expectedMember) != members.end()); @@ -151,5 +153,6 @@ class ErrorReporterTest : public CxxTest::TestSuite { TS_ASSERT_EQUALS(root["email"].asString(), ""); TS_ASSERT_EQUALS(root["textBox"].asString(), "textBox"); TS_ASSERT_EQUALS(root["stacktrace"].asString(), ""); + TS_ASSERT_EQUALS(root["cppCompressedTraces"].asString(), ""); } }; diff --git a/Framework/Properties/Mantid.properties.template b/Framework/Properties/Mantid.properties.template index 773aa9fc36a5..3a2042228f85 100644 --- a/Framework/Properties/Mantid.properties.template +++ b/Framework/Properties/Mantid.properties.template @@ -59,6 +59,9 @@ usagereports.enabled = @ENABLE_USAGE_REPORTS@ errorreports.rooturl = https://errorreports.mantidproject.org usagereports.rooturl = https://reports.mantidproject.org +# Location of core dump files (linux only feature) +errorreports.core_dumps = + # Where to load Grouping files (that are shipped with Mantid) from groupingFiles.directory = @MANTID_ROOT@/instrument/Grouping diff --git a/Framework/PythonInterface/mantid/kernel/src/Exports/ErrorReporter.cpp b/Framework/PythonInterface/mantid/kernel/src/Exports/ErrorReporter.cpp index e05b36a52fcd..aa50b60cb945 100644 --- a/Framework/PythonInterface/mantid/kernel/src/Exports/ErrorReporter.cpp +++ b/Framework/PythonInterface/mantid/kernel/src/Exports/ErrorReporter.cpp @@ -19,7 +19,7 @@ void export_ErrorReporter() { std::string>()) .def(init()) + std::string, std::string, std::string>()) .def("sendErrorReport", &ErrorReporter::sendErrorReport, arg("self"), "Sends an error report") diff --git a/Framework/PythonInterface/test/python/mantid/kernel/ConfigServiceTest.py b/Framework/PythonInterface/test/python/mantid/kernel/ConfigServiceTest.py index aa81b30e3438..742cbeeada01 100644 --- a/Framework/PythonInterface/test/python/mantid/kernel/ConfigServiceTest.py +++ b/Framework/PythonInterface/test/python/mantid/kernel/ConfigServiceTest.py @@ -157,6 +157,7 @@ def test_properties_documented(self): "UpdateInstrumentDefinitions.URL", # shouldn't be changed by users "docs.html.root", # shouldn't be changed by users "errorreports.rooturl", # shouldn't be changed by users + "errorreports.core_dumps", "usagereports.rooturl", # shouldn't be changed by users "workspace.sendto.SansView.arguments", "workspace.sendto.SansView.saveusing", # related to SASview in menu diff --git a/conda/recipes/mantid-developer/meta.yaml b/conda/recipes/mantid-developer/meta.yaml index 13068d9f8a9a..36f4d7f6cae6 100644 --- a/conda/recipes/mantid-developer/meta.yaml +++ b/conda/recipes/mantid-developer/meta.yaml @@ -25,6 +25,7 @@ requirements: - libboost-python-devel {{ libboost_python_devel }} - libopenblas {{ libopenblas }} # [osx or linux] - librdkafka {{ librdkafka }} + - lz4 # [linux] - matplotlib {{ matplotlib }} - mkl {{ mkl }} # [win] - muparser {{ muparser }} @@ -38,6 +39,7 @@ requirements: - pydantic - pyqt {{ pyqt }} - pyqtwebengine + - pystack # [linux] - python-dateutil {{ python_dateutil }} - python {{ python }} - python.app # [osx] diff --git a/conda/recipes/mantidworkbench/meta.yaml b/conda/recipes/mantidworkbench/meta.yaml index 779e3bf28bbd..12e9e0d211e4 100644 --- a/conda/recipes/mantidworkbench/meta.yaml +++ b/conda/recipes/mantidworkbench/meta.yaml @@ -39,11 +39,13 @@ requirements: - versioningit {{ versioningit }} run: - ipykernel + - lz4 # [linux] - psutil {{ psutil }} - {{ pin_compatible("python", max_pin="x.x") }} - matplotlib {{ matplotlib }} - mslice {{ mslice }} - python.app # [osx] + - pystack # [linux] - qtconsole {{ qtconsole }} - {{ pin_compatible("setuptools", max_pin="x.x") }} {% if environ.get('INCLUDE_MANTIDDOCS', 'True') != 'False' %} diff --git a/docs/source/release/v6.13.0/Workbench/New_features/38405.rst b/docs/source/release/v6.13.0/Workbench/New_features/38405.rst new file mode 100644 index 000000000000..7083dad992ad --- /dev/null +++ b/docs/source/release/v6.13.0/Workbench/New_features/38405.rst @@ -0,0 +1,3 @@ +- Added property ``errorreports.core_dumps``. Linux users can set this to the directory on their system where core dump files are put after a crash (e.g ``errorreports.core_dumps=/var/lib/apport/coredump``). + Workbench will then be able to use this property to extract useful information from the core dump file created after a crash and give that to the error reporting service. + This will help us to diagnose some problems where previously no stacktrace was available after a crash. On Linux, core dumps are now always turned on for the workbench process. diff --git a/qt/applications/workbench/workbench/app/start.py b/qt/applications/workbench/workbench/app/start.py index fde959993d0a..c4bf515d2d49 100644 --- a/qt/applications/workbench/workbench/app/start.py +++ b/qt/applications/workbench/workbench/app/start.py @@ -6,234 +6,49 @@ # SPDX - License - Identifier: GPL - 3.0 + # This file is part of the mantid workbench. import argparse -import atexit -import os +import subprocess import sys -from sys import setswitchinterval -from functools import partial -import multiprocessing -from mantid.api import FrameworkManagerImpl -from mantid.kernel import ConfigService, UsageService, version_str as mantid_version_str +from mantid.kernel.environment import is_linux, is_windows +from mantid.kernel import Logger + +if is_linux(): + import resource + from mantidqt.utils.qt import plugins -import mantidqt.utils.qt as qtutils # Find Qt plugins for development builds on some platforms plugins.setup_library_paths() -# This import is needed for new PythonHelpWindow implementation, -# these imports are needed before starting the application -from qtpy.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings # noqa: F401, E402 - -from qtpy.QtGui import QIcon, QSurfaceFormat # noqa: E402 -from qtpy.QtWidgets import QApplication # noqa: E402 -from qtpy.QtCore import QCoreApplication, Qt # noqa: E402 - # Importing resources loads the data in. This must be imported before the # QApplication is created or paths to Qt's resources will not be set up correctly -from workbench.app.resources import qCleanupResources # noqa: E402 from workbench.config import APPNAME, ORG_DOMAIN, ORGANIZATION # noqa: E402 -from workbench.widgets.about.presenter import AboutPresenter # noqa: E402 -# Constants -SYSCHECK_INTERVAL = 50 -ORIGINAL_SYS_EXIT = sys.exit -ORIGINAL_STDOUT = sys.stdout -ORIGINAL_STDERR = sys.stderr -STACKTRACE_FILE = "workbench_stacktrace.txt" +import workbench.app.workbench_process as wp # noqa: E402 -def qapplication(): - """Either return a reference to an existing application instance - or create a new one - :return: A reference to the QApplication object - """ - app = QApplication.instance() - if app is None: - # share OpenGL contexts across the application - QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) - - # set global compatability profile for OpenGL - # We use deprecated OpenGL calls so anything with a profile version >= 3 - # causes widgets like the instrument view to fail to render - gl_surface_format = QSurfaceFormat.defaultFormat() - gl_surface_format.setProfile(QSurfaceFormat.CompatibilityProfile) - gl_surface_format.setSwapBehavior(QSurfaceFormat.DoubleBuffer) - QSurfaceFormat.setDefaultFormat(gl_surface_format) - - argv = sys.argv[:] - argv[0] = APPNAME # replace application name - - app = QApplication(argv) - app.setOrganizationName(ORGANIZATION) - app.setOrganizationDomain(ORG_DOMAIN) - app.setApplicationName(APPNAME) - app.setApplicationVersion(mantid_version_str()) - # Spin up the usage service and set the name for the usage reporting - # The report is sent when the FrameworkManager kicks up - UsageService.setApplicationName(APPNAME) - - app.setAttribute(Qt.AA_UseHighDpiPixmaps) - if hasattr(Qt, "AA_DisableWindowContextHelpButton"): - app.setAttribute(Qt.AA_DisableWindowContextHelpButton) - - return app - - -def initialize(): - """Perform an initialization of the application instance. - - - Patches sys.exit so that it does nothing. - - Uses WindowsSelectorEventLoop required by Tornado - - :return: A reference to the existing application instance - """ - if sys.platform == "win32": - # Tornado requires WindowsSelectorEventLoop - # https://www.tornadoweb.org/en/stable/#installation - import asyncio - - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - if sys.platform == "darwin": - qtutils.force_layer_backing_BigSur() - - app = qapplication() - - # Monkey patching sys.exit so users can't kill - # the application this way - def fake_sys_exit(arg=[]): - pass - - sys.exit = fake_sys_exit - - return app - - -def initialise_qapp_and_launch_workbench(command_line_options): - # Set the global figure manager in matplotlib. Very important this happens first. - from workbench.plotting.config import init_mpl_gcf - - init_mpl_gcf() - - # cleanup static resources at exit - atexit.register(qCleanupResources) - - # fix/validate arguments - if command_line_options.script is not None: - # convert into absolute path - command_line_options.script = os.path.abspath(os.path.expanduser(command_line_options.script)) - if not os.path.exists(command_line_options.script): - print('script "{}" does not exist'.format(command_line_options.script)) - command_line_options.script = None - - app = initialize() - # the default sys check interval leads to long lags - # when request scripts to be aborted - setswitchinterval(SYSCHECK_INTERVAL) - - create_and_launch_workbench(app, command_line_options) - - -def start_error_reporter(): +def start_error_reporter(workbench_pid): """ Used to start the error reporter if the program has segfaulted. """ from mantidqt.dialogs.errorreports import main as errorreports_main - errorreports_main.main(["--application", APPNAME, "--orgname", ORGANIZATION, "--orgdomain", ORG_DOMAIN]) + errorreports_main.main( + ["--application", APPNAME, "--workbench_pid", workbench_pid, "--orgname", ORGANIZATION, "--orgdomain", ORG_DOMAIN] + ) -def create_and_launch_workbench(app, command_line_options): - """Given an application instance create the MainWindow, - show it and start the main event loop +def setup_core_dump_files(): """ - exit_value = 0 - try: - # MainWindow needs to be imported locally to ensure the matplotlib - # backend is not imported too early. - from workbench.app.mainwindow import MainWindow - - # The ordering here is very delicate. Test thoroughly when - # changing anything! - main_window = MainWindow() - - # Set the mainwindow as the parent for additional QMainWindow instances - from workbench.config import set_additional_windows_parent - - set_additional_windows_parent(main_window) - - # decorates the excepthook callback with the reference to the main window - # this is used in case the user wants to terminate the workbench from the error window shown - from workbench.plugins.exception_handler import exception_logger - - sys.excepthook = partial(exception_logger, main_window) - - # Load matplotlib as early as possible and set our defaults - # Setup our custom backend and monkey patch in custom current figure manager - main_window.set_splash("Preloading matplotlib") - from workbench.plotting.config import initialize_matplotlib - - initialize_matplotlib() - - # Setup widget layouts etc. mantid.simple cannot be used before this - # or the log messages don't get through to the widget - main_window.setup() - # start mantid - main_window.set_splash("Initializing mantid framework") - FrameworkManagerImpl.Instance() - main_window.post_mantid_init() - - if main_window.splash: - main_window.splash.hide() - - if command_line_options.script is not None: - main_window.editor.open_file_in_new_tab(command_line_options.script) - editor_task = None - if command_line_options.execute: - # if the quit flag is not specified, this task reference will be - # GC'ed, and the task will be finished alongside the GUI startup - editor_task = main_window.editor.execute_current_async() - - if command_line_options.quit: - # wait for the code interpreter thread to finish executing the script - editor_task.join() - main_window.close() - - # for task exit code descriptions see the classes AsyncTask and TaskExitCode - return int(editor_task.exit_code) if editor_task else 0 - - main_window.show() - main_window.setWindowIcon(QIcon(":/images/MantidIcon.ico")) - # Project Recovery on startup - main_window.project_recovery.repair_checkpoints() - if main_window.project_recovery.check_for_recover_checkpoint(): - main_window.project_recovery.attempt_recovery() - else: - main_window.project_recovery.start_recovery_thread() - - if not (command_line_options.execute or command_line_options.quit): - if AboutPresenter.should_show_on_startup(): - AboutPresenter(main_window).show() - - # lift-off! - exit_value = app.exec_() - except BaseException: - # We count this as a crash - import traceback - - # This is type of thing we want to capture and have reports - # about. Prints to stderr as we can't really count on anything - # else - traceback.print_exc(file=ORIGINAL_STDERR) + This is done so that the error reporter can locate and read any core files produced. + This allows us to recover traces from c++ based crashes. + """ + if is_linux(): try: - print_file_path = os.path.join(ConfigService.getAppDataDirectory(), STACKTRACE_FILE) - with open(print_file_path, "w") as print_file: - traceback.print_exc(file=print_file) - except OSError: - pass - exit_value = -1 - finally: - ORIGINAL_SYS_EXIT(exit_value) + resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.getrlimit(resource.RLIMIT_CORE)[1])) + except ValueError as e: + log = Logger("Mantid Start") + log.warning(f"Problem when enabling core dumps\n{str(e)}") def start(options: argparse.ArgumentParser): @@ -242,7 +57,7 @@ def start(options: argparse.ArgumentParser): :param options: An object describing the command-line arguments passed in """ if options.single_process: - initialise_qapp_and_launch_workbench(options) + wp.initialise_qapp_and_launch_workbench(options) else: # Mantid's FrameworkManagerImpl::Instance Python export uses a process-wide static flag to ensure code # only runs once (see Instance function in Framework/PythonInterface/mantid/api/src/Exports/FrameworkManager.cpp). @@ -251,17 +66,29 @@ def start(options: argparse.ArgumentParser): # this is already the default on Windows/macOS. # This will mean the relevant 'atexit' code will execute in the child process, and therefore the # FrameworkManager and UsageService will be shutdown as expected. - context = multiprocessing.get_context("spawn") - workbench_process = context.Process(target=initialise_qapp_and_launch_workbench, args=[options]) - workbench_process.start() - workbench_process.join() + launch_command = f"python {wp.__file__}" + if options.script: + launch_command += f" {options.script}" + if options.execute: + launch_command += " --execute" + if options.quit: + launch_command += " --quit" + + if not is_windows(): + # preexec_fn is not supported on Windows + workbench_process = subprocess.Popen(launch_command, shell=True, preexec_fn=setup_core_dump_files) + else: + workbench_process = subprocess.Popen(launch_command, shell=True) + + workbench_pid = str(workbench_process.pid) + workbench_process.wait() # handle exit information - exit_code = workbench_process.exitcode if workbench_process.exitcode is not None else 1 + exit_code = workbench_process.returncode if workbench_process.returncode is not None else 1 if exit_code != 0: # start error reporter if requested if not options.no_error_reporter: - start_error_reporter() + start_error_reporter(workbench_pid) # a signal was emited so raise the signal from the application if exit_code < 0: diff --git a/qt/applications/workbench/workbench/app/workbench_process.py b/qt/applications/workbench/workbench/app/workbench_process.py new file mode 100644 index 000000000000..421a84684479 --- /dev/null +++ b/qt/applications/workbench/workbench/app/workbench_process.py @@ -0,0 +1,234 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source, +# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +# SPDX - License - Identifier: GPL - 3.0 + +# This file is part of the mantid workbench. +import argparse +import atexit +import os +import sys +from sys import setswitchinterval +from functools import partial + +from mantid.api import FrameworkManagerImpl +from mantid.kernel import ConfigService, UsageService, version_str as mantid_version_str +from mantidqt.utils.qt import plugins +import mantidqt.utils.qt as qtutils + +# Find Qt plugins for development builds on some platforms +plugins.setup_library_paths() + +# This import is needed for new PythonHelpWindow implementation, +# these imports are needed before starting the application +from qtpy.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings # noqa: F401, E402 + +from qtpy.QtGui import QIcon, QSurfaceFormat # noqa: E402 +from qtpy.QtWidgets import QApplication # noqa: E402 +from qtpy.QtCore import QCoreApplication, Qt # noqa: E402 + +# Importing resources loads the data in. This must be imported before the +# QApplication is created or paths to Qt's resources will not be set up correctly +from workbench.app.resources import qCleanupResources # noqa: E402 +from workbench.config import APPNAME, ORG_DOMAIN, ORGANIZATION # noqa: E402 +from workbench.widgets.about.presenter import AboutPresenter # noqa: E402 + +# Constants +SYSCHECK_INTERVAL = 50 +ORIGINAL_SYS_EXIT = sys.exit +ORIGINAL_STDERR = sys.stderr +STACKTRACE_FILE = "workbench_stacktrace.txt" + + +def qapplication(): + """Either return a reference to an existing application instance + or create a new one + :return: A reference to the QApplication object + """ + app = QApplication.instance() + if app is None: + # share OpenGL contexts across the application + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + + # set global compatability profile for OpenGL + # We use deprecated OpenGL calls so anything with a profile version >= 3 + # causes widgets like the instrument view to fail to render + gl_surface_format = QSurfaceFormat.defaultFormat() + gl_surface_format.setProfile(QSurfaceFormat.CompatibilityProfile) + gl_surface_format.setSwapBehavior(QSurfaceFormat.DoubleBuffer) + QSurfaceFormat.setDefaultFormat(gl_surface_format) + + argv = sys.argv[:] + argv[0] = APPNAME # replace application name + + app = QApplication(argv) + app.setOrganizationName(ORGANIZATION) + app.setOrganizationDomain(ORG_DOMAIN) + app.setApplicationName(APPNAME) + app.setApplicationVersion(mantid_version_str()) + # Spin up the usage service and set the name for the usage reporting + # The report is sent when the FrameworkManager kicks up + UsageService.setApplicationName(APPNAME) + + app.setAttribute(Qt.AA_UseHighDpiPixmaps) + if hasattr(Qt, "AA_DisableWindowContextHelpButton"): + app.setAttribute(Qt.AA_DisableWindowContextHelpButton) + + return app + + +def initialize(): + """Perform an initialization of the application instance. + + - Patches sys.exit so that it does nothing. + - Uses WindowsSelectorEventLoop required by Tornado + + :return: A reference to the existing application instance + """ + if sys.platform == "win32": + # Tornado requires WindowsSelectorEventLoop + # https://www.tornadoweb.org/en/stable/#installation + import asyncio + + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + if sys.platform == "darwin": + qtutils.force_layer_backing_BigSur() + + app = qapplication() + + # Monkey patching sys.exit so users can't kill + # the application this way + def fake_sys_exit(arg=[]): + pass + + sys.exit = fake_sys_exit + + return app + + +def create_and_launch_workbench(app, command_line_options): + """Given an application instance create the MainWindow, + show it and start the main event loop + """ + exit_value = 0 + try: + # MainWindow needs to be imported locally to ensure the matplotlib + # backend is not imported too early. + from workbench.app.mainwindow import MainWindow + + # The ordering here is very delicate. Test thoroughly when + # changing anything! + main_window = MainWindow() + + # Set the mainwindow as the parent for additional QMainWindow instances + from workbench.config import set_additional_windows_parent + + set_additional_windows_parent(main_window) + + # decorates the excepthook callback with the reference to the main window + # this is used in case the user wants to terminate the workbench from the error window shown + from workbench.plugins.exception_handler import exception_logger + + sys.excepthook = partial(exception_logger, main_window) + + # Load matplotlib as early as possible and set our defaults + # Setup our custom backend and monkey patch in custom current figure manager + main_window.set_splash("Preloading matplotlib") + from workbench.plotting.config import initialize_matplotlib + + initialize_matplotlib() + + # Setup widget layouts etc. mantid.simple cannot be used before this + # or the log messages don't get through to the widget + main_window.setup() + # start mantid + main_window.set_splash("Initializing mantid framework") + FrameworkManagerImpl.Instance() + main_window.post_mantid_init() + + if main_window.splash: + main_window.splash.hide() + + if command_line_options.script is not None: + main_window.editor.open_file_in_new_tab(command_line_options.script) + editor_task = None + if command_line_options.execute: + # if the quit flag is not specified, this task reference will be + # GC'ed, and the task will be finished alongside the GUI startup + editor_task = main_window.editor.execute_current_async() + + if command_line_options.quit: + # wait for the code interpreter thread to finish executing the script + editor_task.join() + main_window.close() + + # for task exit code descriptions see the classes AsyncTask and TaskExitCode + return int(editor_task.exit_code) if editor_task else 0 + + main_window.show() + main_window.setWindowIcon(QIcon(":/images/MantidIcon.ico")) + # Project Recovery on startup + main_window.project_recovery.repair_checkpoints() + if main_window.project_recovery.check_for_recover_checkpoint(): + main_window.project_recovery.attempt_recovery() + else: + main_window.project_recovery.start_recovery_thread() + + if not (command_line_options.execute or command_line_options.quit): + if AboutPresenter.should_show_on_startup(): + AboutPresenter(main_window).show() + + # lift-off! + exit_value = app.exec_() + except BaseException: + # We count this as a crash + import traceback + + # This is type of thing we want to capture and have reports + # about. Prints to stderr as we can't really count on anything + # else + traceback.print_exc(file=ORIGINAL_STDERR) + try: + print_file_path = os.path.join(ConfigService.getAppDataDirectory(), STACKTRACE_FILE) + with open(print_file_path, "w") as print_file: + traceback.print_exc(file=print_file) + except OSError: + pass + exit_value = -1 + finally: + ORIGINAL_SYS_EXIT(exit_value) + + +def initialise_qapp_and_launch_workbench(command_line_options): + # Set the global figure manager in matplotlib. Very important this happens first. + from workbench.plotting.config import init_mpl_gcf + + init_mpl_gcf() + + # cleanup static resources at exit + atexit.register(qCleanupResources) + + # fix/validate arguments + if command_line_options.script is not None: + # convert into absolute path + command_line_options.script = os.path.abspath(os.path.expanduser(command_line_options.script)) + if not os.path.exists(command_line_options.script): + print('script "{}" does not exist'.format(command_line_options.script)) + command_line_options.script = None + + app = initialize() + # the default sys check interval leads to long lags + # when request scripts to be aborted + setswitchinterval(SYSCHECK_INTERVAL) + + create_and_launch_workbench(app, command_line_options) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("script", nargs="?") + parser.add_argument("-x", "--execute", action="store_true", help="execute the script file given as argument") + parser.add_argument("-q", "--quit", action="store_true", help="execute the script file with '-x' given as argument and then exit") + options = parser.parse_args() + initialise_qapp_and_launch_workbench(options) diff --git a/qt/applications/workbench/workbench/plugins/exception_handler/__init__.py b/qt/applications/workbench/workbench/plugins/exception_handler/__init__.py index d3317536d6ab..f8e8bd000693 100644 --- a/qt/applications/workbench/workbench/plugins/exception_handler/__init__.py +++ b/qt/applications/workbench/workbench/plugins/exception_handler/__init__.py @@ -28,7 +28,7 @@ def exception_logger(main_window, exc_type, exc_value, exc_traceback): if UsageService.isEnabled(): page = CrashReportPage(show_continue_terminate=True) - presenter = ErrorReporterPresenter(page, "", "workbench", traceback.format_exception(exc_type, exc_value, exc_traceback)) + presenter = ErrorReporterPresenter(page, "", "workbench", None, traceback.format_exception(exc_type, exc_value, exc_traceback)) presenter.show_view_blocking() if not page.continue_working: main_window.close() diff --git a/qt/python/mantidqt/CMakeLists.txt b/qt/python/mantidqt/CMakeLists.txt index 81420650c293..eeb0596cedf6 100644 --- a/qt/python/mantidqt/CMakeLists.txt +++ b/qt/python/mantidqt/CMakeLists.txt @@ -46,6 +46,7 @@ set(PYTHON_TEST_FILES mantidqt/test/test_algorithm_observer.py mantidqt/test/test_import.py mantidqt/dialogs/errorreports/test/test_errorreport_presenter.py + mantidqt/dialogs/errorreports/test/test_run_pystack.py mantidqt/dialogs/test/test_algorithm_dialog.py mantidqt/dialogs/test/test_spectraselectiondialog.py mantidqt/dialogs/test/test_spectraselectorutils.py diff --git a/qt/python/mantidqt/mantidqt/dialogs/errorreports/main.py b/qt/python/mantidqt/mantidqt/dialogs/errorreports/main.py index ad20af590d6e..fa7ffa485057 100644 --- a/qt/python/mantidqt/mantidqt/dialogs/errorreports/main.py +++ b/qt/python/mantidqt/mantidqt/dialogs/errorreports/main.py @@ -47,7 +47,7 @@ def main(argv: Sequence[str] = None) -> int: app.setApplicationName(command_line_args.application) QSettings.setDefaultFormat(QSettings.IniFormat) form = CrashReportPage(show_continue_terminate=False) - presenter = ErrorReporterPresenter(form, exit_code_str, command_line_args.application) + presenter = ErrorReporterPresenter(form, exit_code_str, command_line_args.application, command_line_args.workbench_pid) presenter.show_view() app.exec_() @@ -64,6 +64,7 @@ def parse_commandline(argv: Sequence[str]) -> argparse.Namespace: parser.add_argument("--orgname", dest="org_name", default="unknown") parser.add_argument("--orgdomain", dest="org_domain", default="unknown") parser.add_argument("--application", dest="application", default="unknown") + parser.add_argument("--workbench_pid", dest="workbench_pid", default="") return parser.parse_args(argv) diff --git a/qt/python/mantidqt/mantidqt/dialogs/errorreports/presenter.py b/qt/python/mantidqt/mantidqt/dialogs/errorreports/presenter.py index 973517f3eadd..bbdad9f85aab 100644 --- a/qt/python/mantidqt/mantidqt/dialogs/errorreports/presenter.py +++ b/qt/python/mantidqt/mantidqt/dialogs/errorreports/presenter.py @@ -4,19 +4,23 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +import base64 import json import os +import zlib from typing import Optional from qtpy.QtCore import QSettings from mantid.kernel import ConfigService, ErrorReporter, Logger, UsageService +from mantid.kernel.environment import is_linux from mantidqt.dialogs.errorreports.report import MAX_STACK_TRACE_LENGTH +from mantidqt.dialogs.errorreports.run_pystack import retrieve_thread_traces_from_coredump_file class ErrorReporterPresenter(object): SENDING_ERROR_MESSAGE = "There was an error when sending the report.\nPlease contact mantid-help@mantidproject.org directly" - def __init__(self, view, exit_code: str, application: str, traceback: Optional[str] = None): + def __init__(self, view, exit_code: str, application: str, workbench_pid: str, traceback: Optional[str] = None): """ :param view: A reference to the view managed by this presenter :param exit_code: A string containing the exit_code of the failing application @@ -28,6 +32,7 @@ def __init__(self, view, exit_code: str, application: str, traceback: Optional[s self._exit_code = exit_code self._application = application self._traceback = traceback if traceback else "" + self._cpp_traces = b"" self._view.set_report_callback(self.error_handler) self._view.moreDetailsButton.clicked.connect(self.show_more_details) @@ -39,6 +44,8 @@ def __init__(self, view, exit_code: str, application: str, traceback: Optional[s self._traceback = file.readlines() new_workspace_name = os.path.join(ConfigService.getAppDataDirectory(), "{}_stacktrace_sent.txt".format(application)) os.rename(traceback_file_path, new_workspace_name) + elif is_linux(): + self._cpp_traces = retrieve_thread_traces_from_coredump_file(workbench_pid) except OSError: pass @@ -100,7 +107,15 @@ def _send_report_to_server(self, share_identifiable=False, name="", email="", up ) errorReporter = ErrorReporter( - self._application, uptime, self._exit_code, share_identifiable, str(name), str(email), str(text_box), stacktrace + self._application, + uptime, + self._exit_code, + share_identifiable, + str(name), + str(email), + str(text_box), + stacktrace, + self._cpp_traces, ) status = errorReporter.sendErrorReport() @@ -133,11 +148,16 @@ def show_more_details(self): str(self._view.input_name_line_edit.text()), str(self._view.input_email_line_edit.text()), str(self._view.input_free_text.toPlainText()), - "".join(self._traceback), + "", + "", ) error_message_json = json.loads(error_reporter.generateErrorMessage()) - stacktrace_text = error_message_json["stacktrace"] + if self._cpp_traces: + stacktrace_text = zlib.decompress(base64.standard_b64decode(self._cpp_traces)).decode("utf-8") + else: + stacktrace_text = "".join(self._traceback) del error_message_json["stacktrace"] # remove this entry so it doesn't appear twice. + del error_message_json["cppCompressedTraces"] user_information = "".join("{}: {}\n".format(key, error_message_json[key]) for key in error_message_json) self._view.display_more_details(user_information, stacktrace_text) diff --git a/qt/python/mantidqt/mantidqt/dialogs/errorreports/run_pystack.py b/qt/python/mantidqt/mantidqt/dialogs/errorreports/run_pystack.py new file mode 100644 index 000000000000..4cc573a1b2f8 --- /dev/null +++ b/qt/python/mantidqt/mantidqt/dialogs/errorreports/run_pystack.py @@ -0,0 +1,122 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source, +# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +# SPDX - License - Identifier: GPL - 3.0 + +from datetime import datetime +from pathlib import Path +from tempfile import NamedTemporaryFile + +from mantid.kernel.environment import is_linux +from mantid.kernel import ConfigService, Logger + +import base64 + +if is_linux(): + import lz4.frame +import re +import subprocess +import zlib + + +CORE_DUMP_RECENCY_LIMIT = 30 +log = Logger("errorreports (pystack analysis)") + + +def retrieve_thread_traces_from_coredump_file(workbench_pid: str) -> bytes: + # Locate the core dumps dir + core_dumps_path = None + try: + core_dumps_path = _get_core_dumps_dir() + except ValueError as e: + log.error(str(e)) + return b"" + + # Get most recent dump file. + core_file = _get_most_recent_core_dump_file(core_dumps_path, workbench_pid) + if core_file is None: + return b"" + + # Run file through pystack and capture output + pystack_output = _get_output_from_pystack(core_file) + + # Compress output and return + compressed_bytes = zlib.compress(pystack_output.encode("utf-8")) + return base64.standard_b64encode(compressed_bytes) + + +def _get_core_dumps_dir() -> Path: + core_dumps_str = ConfigService.getString("errorreports.core_dumps") + if not core_dumps_str: + raise ValueError("errorreports.core_dumps not set") + core_dumps_path = Path(core_dumps_str) + if not core_dumps_path.exists(): + raise ValueError(f"errorreports.core_dumps value ({core_dumps_str}) does not exist") + elif not core_dumps_path.is_dir(): + raise ValueError(f"errorreports.core_dumps value ({core_dumps_str}) is not a directory") + return core_dumps_path + + +def _get_most_recent_core_dump_file(core_dumps_dir: Path, workbench_pid: str) -> Path | None: + files = core_dumps_dir.iterdir() + files_sorted_by_latest = sorted([file for file in files], key=lambda file: file.stat().st_ctime, reverse=True) + if files_sorted_by_latest: + for latest_core_dump_file in files_sorted_by_latest: + # test it's recent enough + age = datetime.now() - datetime.fromtimestamp(latest_core_dump_file.stat().st_ctime) + if age.seconds < CORE_DUMP_RECENCY_LIMIT: + log.notice(f"Found recent file {latest_core_dump_file.as_posix()}") + if _is_lz4_file(latest_core_dump_file): + latest_core_dump_file = _decompress_lz4_file(latest_core_dump_file) + log.notice(f"Decompressed lz4 core file to {latest_core_dump_file.as_posix()}") + # test it's the correct process. + if _check_core_file_is_the_workbench_process(latest_core_dump_file, workbench_pid): + log.notice(f"{latest_core_dump_file.as_posix()} identified as a mantid workbench core dump") + return latest_core_dump_file + else: + log.notice(f"{latest_core_dump_file.as_posix()} not itdentified as a mantid workbench core dump") + else: + log.notice( + f"Could not find recent enough ( < {CORE_DUMP_RECENCY_LIMIT} " + "seconds old) valid core dump file in {core_dumps_dir.as_posix()}" + ) + return None + log.notice(f"No valid files found in {core_dumps_dir.as_posix()}") + return None + + +def _check_core_file_is_the_workbench_process(core_dump_file: Path, workbench_pid: str) -> bool: + args = ["pystack", "core", core_dump_file.as_posix()] + process = subprocess.run(args, capture_output=True, text=True) + if process.stderr: + log.error(f"Pystack executable check failed: {process.stderr}") + return False + stdout = process.stdout + search_result = re.search(r"pid: (\d+) ppid: (\d+) ", stdout) + if search_result is not None: + # Since the process id comes from Popen with shell=True, it might be the pid of the parent shell + # Seems to be inconsistent between distributions. + return workbench_pid in (search_result.group(1), search_result.group(2)) + return False + + +def _get_output_from_pystack(core_dump_file: Path) -> str: + args = ["pystack", "core", core_dump_file.as_posix(), "--native-all"] + process = subprocess.run(args, capture_output=True, text=True) + if process.stderr: + log.error(f"Error when running Pystack: {process.stderr}") + return process.stdout + + +def _decompress_lz4_file(lz4_core_dump_file: Path) -> Path: + with NamedTemporaryFile(delete=False) as tmp_decompressed_core_file: + with lz4.frame.open(lz4_core_dump_file.as_posix(), "r") as lz4_fp: + tmp_decompressed_core_file.write(lz4_fp.read()) + return Path(tmp_decompressed_core_file.name) + + +def _is_lz4_file(core_dump_file: Path) -> bool: + lz4_magic_number = b"\x04\x22\x4d\x18" + with open(core_dump_file, "rb") as f: + return f.read(4) == lz4_magic_number diff --git a/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_errorreport_presenter.py b/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_errorreport_presenter.py index b8e3703ccfdf..829358086499 100644 --- a/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_errorreport_presenter.py +++ b/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_errorreport_presenter.py @@ -30,7 +30,7 @@ def setUp(self): self.view = mock.MagicMock() self.exit_code = 255 self.app_name = "ErrorReportPresenterTest" - self.error_report_presenter = ErrorReporterPresenter(self.view, self.exit_code, application=self.app_name) + self.error_report_presenter = ErrorReporterPresenter(self.view, self.exit_code, application=self.app_name, workbench_pid=None) self.view.CONTACT_INFO = "ContactInfo" self.view.NAME = "John Smith" self.view.EMAIL = "john.smith@example.com" @@ -62,7 +62,7 @@ def test_send_error_report_to_server_calls_ErrorReport_correctly(self): self.errorreport_mock_instance.sendErrorReport.return_value = 201 self.error_report_presenter._send_report_to_server(False, name=name, email=email, uptime=uptime, text_box=text_box) - self.errorreport_mock.assert_called_once_with(self.app_name, uptime, self.exit_code, False, name, email, text_box, "") + self.errorreport_mock.assert_called_once_with(self.app_name, uptime, self.exit_code, False, name, email, text_box, "", b"") self.errorreport_mock_instance.sendErrorReport.assert_called_once_with() def test_send_error_report_to_server_calls_ErrorReport_correctly_and_triggers_view_upon_failure(self): @@ -74,7 +74,7 @@ def test_send_error_report_to_server_calls_ErrorReport_correctly_and_triggers_vi self.errorreport_mock_instance.sendErrorReport.return_value = 500 self.error_report_presenter._send_report_to_server(True, name=name, email=email, uptime=uptime, text_box=text_box) - self.errorreport_mock.assert_called_once_with(self.app_name, uptime, self.exit_code, True, name, email, text_box, "") + self.errorreport_mock.assert_called_once_with(self.app_name, uptime, self.exit_code, True, name, email, text_box, "", b"") self.errorreport_mock_instance.sendErrorReport.assert_called_once_with() self.view.display_message_box.assert_called_once_with( "Error contacting server", ErrorReporterPresenter.SENDING_ERROR_MESSAGE, "http request returned with status 500" diff --git a/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_run_pystack.py b/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_run_pystack.py new file mode 100644 index 000000000000..cf745baa95cf --- /dev/null +++ b/qt/python/mantidqt/mantidqt/dialogs/errorreports/test/test_run_pystack.py @@ -0,0 +1,112 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source, +# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +# SPDX - License - Identifier: GPL - 3.0 + + +from pathlib import Path +from tempfile import NamedTemporaryFile, TemporaryDirectory +from time import sleep +from typing import List +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from mantid.kernel.environment import is_linux +from mantidqt.dialogs.errorreports.run_pystack import _get_core_dumps_dir, _get_most_recent_core_dump_file, _is_lz4_file + +if is_linux(): + import lz4.frame +import os + + +class TestRunPystack(TestCase): + MODULE_PATH = "mantidqt.dialogs.errorreports.run_pystack" + + def setUp(self) -> None: + if not is_linux(): + self.skipTest("pystack is only run on linux") + + @patch(f"{MODULE_PATH}.ConfigService") + def test_get_core_dumps_dir_raises_if_not_set(self, mock_config_service: MagicMock): + mock_config_service.getString.return_value = "" + self.assertRaisesRegex(ValueError, "errorreports.core_dumps not set", _get_core_dumps_dir) + + @patch(f"{MODULE_PATH}.ConfigService") + def test_get_core_dumps_dir_raises_if_does_not_exist(self, mock_config_service: MagicMock): + mock_config_service.getString.return_value = "/a/fake/path" + self.assertRaisesRegex(ValueError, "does not exist", _get_core_dumps_dir) + + @patch(f"{MODULE_PATH}.ConfigService") + def test_get_core_dumps_dir_raises_if_file_is_set(self, mock_config_service: MagicMock): + with NamedTemporaryFile() as tmp_file: + mock_config_service.getString.return_value = tmp_file.name + self.assertRaisesRegex(ValueError, "is not a directory", _get_core_dumps_dir) + + @patch(f"{MODULE_PATH}.ConfigService") + def test_get_core_dumps_dir_returns_a_dir_set_in_the_config(self, mock_config_service: MagicMock): + with TemporaryDirectory() as tmp_dir: + mock_config_service.getString = MagicMock() + mock_config_service.getString.return_value = tmp_dir + path = _get_core_dumps_dir() + mock_config_service.getString.assert_called_once_with("errorreports.core_dumps") + self.assertEqual(path.as_posix(), tmp_dir) + + @patch(f"{MODULE_PATH}._check_core_file_is_the_workbench_process") + def test_get_most_recent_core_dump_file_gets_the_latest_file(self, mock_check_workbench_process: MagicMock): + mock_check_workbench_process.return_value = True + file_names = ["first", "second", "third"] + with SetupSomeFilesInATempDir(file_names) as tmp_dir: + latest_file = _get_most_recent_core_dump_file(Path(tmp_dir), None) + self.assertEqual(latest_file.name, file_names[-1]) + + @patch(f"{MODULE_PATH}.CORE_DUMP_RECENCY_LIMIT", 0.5) + def test_get_most_recent_core_dump_file_returns_none_if_there_are_no_new_files(self): + with TemporaryDirectory() as tmp_dir: + open(f"{tmp_dir}/test", "a").close() + sleep(0.6) + self.assertIsNone(_get_most_recent_core_dump_file(Path(tmp_dir), None)) + + def test_get_most_recent_core_dump_file_returns_none_if_the_dir_is_empty(self): + with TemporaryDirectory() as tmp_dir: + self.assertIsNone(_get_most_recent_core_dump_file(Path(tmp_dir), None)) + + def test_is_lz4_file_is_true_for_lz4_file(self): + random_data = os.urandom(1024) + with NamedTemporaryFile() as tmp_file: + with lz4.frame.open(tmp_file.name, "wb") as fp: + fp.write(random_data) + self.assertTrue(_is_lz4_file(Path(tmp_file.name))) + + def test_is_lz4_is_false_for_tmp_file(self): + with NamedTemporaryFile() as tmp_file: + self.assertFalse(_is_lz4_file(Path(tmp_file.name))) + + @patch(f"{MODULE_PATH}._decompress_lz4_file") + @patch(f"{MODULE_PATH}._is_lz4_file") + @patch(f"{MODULE_PATH}._check_core_file_is_the_workbench_process") + def test_decompress_lz4_file_is_called( + self, mock_check_workbench_process: MagicMock, mock_is_lz4_file: MagicMock, mock_decompress_lz4_file: MagicMock + ): + mock_check_workbench_process.return_value = True + mock_is_lz4_file.return_value = True + tmp_file = Path("/a/tmp/location") + mock_decompress_lz4_file.return_value = tmp_file + with SetupSomeFilesInATempDir(["core_file"]) as tmp_dir: + latest_file = _get_most_recent_core_dump_file(Path(tmp_dir), None) + self.assertEqual(latest_file, tmp_file) + mock_decompress_lz4_file.assert_called_once_with(Path(f"{tmp_dir}/core_file")) + + +class SetupSomeFilesInATempDir: + def __init__(self, file_names: List[str]): + self.tmp_dir = TemporaryDirectory() + for name in file_names: + open(f"{self.tmp_dir.name}/{name}", "a").close() + sleep(0.1) + + def __enter__(self): + return self.tmp_dir.name + + def __exit__(self, type, value, traceback): + self.tmp_dir.cleanup() diff --git a/tools/DefaultConfigFiles/Mantid.user.properties b/tools/DefaultConfigFiles/Mantid.user.properties index bdc53c5f1f79..6424294dd5ac 100644 --- a/tools/DefaultConfigFiles/Mantid.user.properties +++ b/tools/DefaultConfigFiles/Mantid.user.properties @@ -69,3 +69,6 @@ Q.convention = Inelastic ## Uncomment to disable use of OpenGL to render unwrapped instrument views #MantidOptions.InstrumentView.UseOpenGL=Off + +# # Location of core dump files (linux only feature) +# errorreports.core_dumps =