diff --git a/CMakeLists.txt b/CMakeLists.txt index b20fb6d69efe22032e0da05f04dc867356bedfd5..e4d2d6906b29352526f8ef24ba85c80810fd9750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -301,7 +301,7 @@ if ( ENABLE_CPACK ) set( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},OCE-draw,OCE-foundation,OCE-modeling,OCE-ocaf,OCE-visualization") set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},poco-crypto,poco-data,poco-mysql,poco-sqlite,poco-odbc,poco-util,poco-xml,poco-zip,poco-net,poco-netssl,poco-foundation,PyQt4" ) set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},sip >= 4.18" ) - set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},python-six,python-ipython >= 1.1.0,python-ipython-notebook,PyYAML" ) + set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},python-six,python-ipython >= 1.1.0,python-ipython-notebook,PyYAML,python2-psutil" ) # scipy set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},scipy" ) set ( CPACK_RPM_PACKAGE_REQUIRES "${CPACK_RPM_PACKAGE_REQUIRES},mxml,hdf,hdf5,jsoncpp >= 0.7.0" ) @@ -361,7 +361,8 @@ if ( ENABLE_CPACK ) "libhdf5-cpp-11," "python-pycifrw (>= 4.2.1)," "python-yaml," - "python-qtawesome") + "python-qtawesome," + "python-psutil") set ( PERFTOOLS_DEB_PACKAGE "libgoogle-perftools4 (>= 1.7)" ) if( "${UNIX_CODENAME}" STREQUAL "bionic") list ( APPEND DEPENDS_LIST ",libgsl23,liboce-foundation11,liboce-modeling11,libqscintilla2-qt4-13,jupyter-notebook,libhdf5-cpp-100") diff --git a/MantidPlot/CMakeLists.txt b/MantidPlot/CMakeLists.txt index 7ea2943c40f2bdb611a5224926459f37bba02b51..b5b9670e1c502cfef2537ab1deba3d5c29ab58ef 100644 --- a/MantidPlot/CMakeLists.txt +++ b/MantidPlot/CMakeLists.txt @@ -91,6 +91,7 @@ set ( QTIPLOT_SRCS src/ApplicationWindow.cpp src/PluginFit.cpp src/PolynomFitDialog.cpp src/PolynomialFit.cpp + src/Process.cpp src/ProjectRecovery.cpp src/ProjectSaveView.cpp src/ProjectSerialiser.cpp @@ -291,6 +292,7 @@ set ( QTIPLOT_HDRS src/ApplicationWindow.h src/PluginFit.h src/PolynomFitDialog.h src/PolynomialFit.h + src/Process.h src/ProjectRecovery.h src/ProjectSerialiser.h src/ProjectSaveView.h diff --git a/MantidPlot/src/ApplicationWindow.cpp b/MantidPlot/src/ApplicationWindow.cpp index 5dc3ed152afe0869a3c01a5c362278b6a9077699..44d7eae1edbf1091bdcc14113d3b4bb56f0568bc 100644 --- a/MantidPlot/src/ApplicationWindow.cpp +++ b/MantidPlot/src/ApplicationWindow.cpp @@ -91,6 +91,7 @@ #include "PlotWizard.h" #include "PolynomFitDialog.h" #include "PolynomialFit.h" +#include "Process.h" #include "ProjectRecovery.h" #include "ProjectSerialiser.h" #include "QwtErrorPlotCurve.h" @@ -9771,10 +9772,12 @@ void ApplicationWindow::closeEvent(QCloseEvent *ce) { } } - // Stop background saving thread, so it doesn't try to use a destroyed - // resource - m_projectRecovery.stopProjectSaving(); - m_projectRecovery.clearAllCheckpoints(); + if (m_projectRecoveryRunOnStart) { + // Stop background saving thread, so it doesn't try to use a destroyed + // resource + m_projectRecovery.stopProjectSaving(); + m_projectRecovery.clearAllCheckpoints(); + } // Close the remaining MDI windows. The Python API is required to be active // when the MDI window destructor is called so that those references can be @@ -16637,8 +16640,20 @@ void ApplicationWindow::onAboutToStart() { // Make sure we see all of the startup messages resultsLog->scrollToTop(); - // Kick off project recovery - checkForProjectRecovery(); + // Kick off project recovery iff we are able to determine if we are the only + // instance currently running + try { + if (!Process::isAnotherInstanceRunning()) { + checkForProjectRecovery(); + } else { + g_log.debug("Another MantidPlot process is running. Project recovery is " + "disabled."); + } + } catch (std::runtime_error &exc) { + g_log.warning("Unable to determine if other MantidPlot processes are " + "running. Project recovery is disabled. Error msg: " + + std::string(exc.what())); + } } /** @@ -16768,7 +16783,13 @@ bool ApplicationWindow::saveProjectRecovery(std::string destination) { return projectWriter.save(QString::fromStdString(destination)); } +/** + * Checks for any recovery checkpoint and starts project + * saving if one doesn't exist. If one does, it prompts + * the user whether they would like to recover + */ void ApplicationWindow::checkForProjectRecovery() { + m_projectRecoveryRunOnStart = true; if (!m_projectRecovery.checkForRecovery()) { m_projectRecovery.startProjectSaving(); return; diff --git a/MantidPlot/src/ApplicationWindow.h b/MantidPlot/src/ApplicationWindow.h index ba7830dd663b9b52535dd1cd3b8c111d830922f8..1e0122798c3b7846121c284c68e50acb3d48b8fc 100644 --- a/MantidPlot/src/ApplicationWindow.h +++ b/MantidPlot/src/ApplicationWindow.h @@ -1642,6 +1642,8 @@ private: /// Owns a thread which automatically triggers project recovery for the GUI MantidQt::ProjectRecovery m_projectRecovery; + /// True if project recovery was started when MantidPlot started + bool m_projectRecoveryRunOnStart{false}; #ifdef SHARED_MENUBAR QMenuBar *m_sharedMenuBar; ///< Pointer to the shared menubar diff --git a/MantidPlot/src/Process.cpp b/MantidPlot/src/Process.cpp new file mode 100644 index 0000000000000000000000000000000000000000..20fc6d3ead9885a397efa48c784093a72ef7f3b9 --- /dev/null +++ b/MantidPlot/src/Process.cpp @@ -0,0 +1,125 @@ +// clang-format off +#include "MantidQtWidgets/Common/PythonThreading.h" +// clang-format on + +#include "Process.h" + +#include <iostream> +#include <stdexcept> +#include <QCoreApplication> + +namespace { + +class PyObjectNewReference { +public: + explicit PyObjectNewReference(PyObject *object) : m_object(object) {} + ~PyObjectNewReference() { Py_XDECREF(m_object); } + + PyObjectNewReference(const PyObjectNewReference &) = delete; + PyObjectNewReference &operator=(const PyObjectNewReference &) = delete; + + PyObjectNewReference(PyObjectNewReference &&o) { *this = std::move(o); } + + PyObjectNewReference &operator=(PyObjectNewReference &&other) { + this->m_object = other.m_object; + other.m_object = nullptr; + return *this; + } + + inline PyObject *ptr() const { return m_object; } + +private: + PyObject *m_object; +}; + +/** + * @brief Retrieve a named attribute + * @param source The source object + * @param name The name of the attribute + * @return The attribute + * @throws std::runtime_error if an error occurs retrieving the attribute + */ +PyObjectNewReference attr(PyObject *source, const char *name) { + PyObjectNewReference attr(PyObject_GetAttrString(source, name)); + if (attr.ptr()) { + return attr; + } else { + PyErr_Print(); + throw std::runtime_error(std::string("Process: No attribute ") + name + + " found"); + } +} + +/** + * @brief Call a named function with an check for errors + * @param source The source object + * @param name The name of the attribute to call + * @return The return value of the function + * @throws std::runtime_error if an error occurs retrieving the attribute + */ +PyObjectNewReference call(PyObject *source, const char *name) { + auto returnedAttr = attr(source, name); + auto result = PyObject_CallFunction(returnedAttr.ptr(), nullptr); + if (result) + return PyObjectNewReference(result); + else { + PyErr_Print(); + throw std::runtime_error(std::string("Process: Error calling function ") + + name); + } +} + +/** + * @return Return a pointer to the psutil module. A new reference is returned. + */ +PyObjectNewReference psutil() { + if (auto process = PyImport_ImportModule("psutil")) { + return PyObjectNewReference(process); + } else { + PyErr_Clear(); + throw std::runtime_error("Python module psutil cannot be imported."); + } +} +} // namespace + +namespace Process { + +/** + * Returns true is another instance of Mantid is running + * on this machine + * @return True if another instance is running + * @throws std::runtime_error if the PID list cannot be determined + */ +bool isAnotherInstanceRunning() { return !otherInstancePIDs().empty(); } + +/** + * @brief Return a list of process IDs for other instances of this process. + * @return A list of other processes running. The PID for this process is + * removed from the list. An empty list is returned + * if no other processes are running. + * @throws std::runtime_error if the PID list cannot be determined + */ +std::vector<int64_t> otherInstancePIDs() { + ScopedPythonGIL lock; + const int64_t ourPID(QCoreApplication::applicationPid()); + const PyObjectNewReference ourName( + FROM_CSTRING(QCoreApplication::applicationName().toLatin1().data())); + auto psutilModule(psutil()); + auto processIter(call(psutilModule.ptr(), "process_iter")); + + std::vector<int64_t> otherPIDs; + PyObject *item(nullptr); + while ((item = PyIter_Next(processIter.ptr()))) { + auto name = call(item, "name"); + if (PyObject_RichCompareBool(name.ptr(), ourName.ptr(), Py_EQ)) { + auto pid = PyLong_AsLong(attr(item, "pid").ptr()); + if (pid != ourPID) { + otherPIDs.emplace_back(pid); + } + } + Py_DECREF(item); + } + return otherPIDs; +} + +} // namespace Process diff --git a/MantidPlot/src/Process.h b/MantidPlot/src/Process.h new file mode 100644 index 0000000000000000000000000000000000000000..e7a467a8cd4258741bfc8a94ea1480e0f66ef530 --- /dev/null +++ b/MantidPlot/src/Process.h @@ -0,0 +1,39 @@ +#ifndef PROCESS_H_ +#define PROCESS_H_ +/* +Copyright © 2008-18 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge +National Laboratory & European Spallation Source + +This file is part of Mantid. + +Mantid 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. + +Mantid is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. + +File change history is stored at: <https://github.com/mantidproject/mantid> +Code Documentation is available at: <http://doxygen.mantidproject.org> +*/ +#include <cstdint> +#include <vector> + +/* + * A minimal wrapper around Python's psutil package to gather information + * about processes + */ + +namespace Process { + +bool isAnotherInstanceRunning(); + +std::vector<int64_t> otherInstancePIDs(); +} +#endif // PROCESS_H_ diff --git a/MantidPlot/src/ProjectRecovery.cpp b/MantidPlot/src/ProjectRecovery.cpp index 92593535c6c0451df551a51a814e9cc8f0f69742..3e8fb692394adf3f8d699fb49a2d89b5e8c7f4d9 100644 --- a/MantidPlot/src/ProjectRecovery.cpp +++ b/MantidPlot/src/ProjectRecovery.cpp @@ -16,8 +16,10 @@ #include "boost/range/algorithm_ext/erase.hpp" #include "Poco/DirectoryIterator.h" +#include "Poco/Environment.h" #include "Poco/NObserver.h" #include "Poco/Path.h" +#include "Poco/Process.h" #include <QMessageBox> #include <QMetaObject> @@ -62,8 +64,11 @@ boost::optional<bool> getConfigBool(const std::string &key) { /// Returns a string to the current top level recovery folder std::string getRecoveryFolder() { - static std::string recoverFolder = - Mantid::Kernel::ConfigService::Instance().getAppDataDir() + "/recovery/"; + static std::string appData = + Mantid::Kernel::ConfigService::Instance().getAppDataDir(); + static std::string hostname = Poco::Environment::nodeName(); + + static std::string recoverFolder = appData + "/recovery/" + hostname + '/'; return recoverFolder; }