]> SALOME platform Git repositories - modules/shaper.git/commitdiff
Salome HOME
[bos#35161][EDF](2023-T1) Automatic backup - initial commit
authormbs <martin.bernhard@opencascade.com>
Sat, 9 Dec 2023 22:09:53 +0000 (22:09 +0000)
committermbs <martin.bernhard@opencascade.com>
Mon, 19 Feb 2024 17:05:37 +0000 (17:05 +0000)
Added temporary debug files. To be removed again when task is finished.

updated location of test scripts

ensure the old backup folder gets removed

fixed creation of backup folder preference

check the output of the check_validity.py script

code refactoring and bug fixing
* backupDoc returns now an error code instead of bool
* added check of dumped python script
* show warning on error
* refactored test and validation scripts

removed DEBUG macros

21 files changed:
CMakeLists.txt
check_validity.py [new file with mode: 0755]
src/Model/Model_Document.cpp
src/Model/Model_Document.h
src/Model/Model_Session.cpp
src/Model/Model_Session.h
src/ModelAPI/ModelAPI_Session.h
src/ModuleBase/ModuleBase_Preferences.cpp
src/SHAPERGUI/CMakeLists.txt
src/SHAPERGUI/SHAPERGUI.cpp
src/SHAPERGUI/SHAPERGUI.h
src/SHAPERGUI/SHAPERGUI_CheckBackup.cpp [new file with mode: 0644]
src/SHAPERGUI/SHAPERGUI_CheckBackup.h [new file with mode: 0644]
src/SHAPERGUI/SHAPERGUI_DataModel.cpp
src/SHAPERGUI/SHAPERGUI_DataModel.h
src/SHAPERGUI/SHAPERGUI_msg_fr.ts
src/XGUI/MBDebug.h [new file with mode: 0644]
src/XGUI/XGUI_Workshop.cpp
src/XGUI/XGUI_Workshop.h
test.hdfs/testme.py
test_backup.py [new file with mode: 0755]

index 0804d5a4e3b66548a28fe3992b42c446463635ab..d1c324798bf6478b4accee3b10fcd8c513773b1d 100644 (file)
@@ -129,6 +129,8 @@ IF(${HAVE_SALOME})
     # without SALOME there is another kind of documentation, separated one
 
     SALOME_INSTALL_SCRIPTS("shaper_test.py" ${SHAPER_INSTALL_PYTHON_FILES})
+    SALOME_INSTALL_SCRIPTS("test_backup.py" ${SHAPER_INSTALL_PYTHON_FILES})
+    SALOME_INSTALL_SCRIPTS("check_validity.py" ${SHAPER_INSTALL_PYTHON_FILES})
 ELSE(${HAVE_SALOME})
     SET(SHAPER_INSTALL_EXE bin CACHE INTERNAL "" FORCE)
     SET(SHAPER_INSTALL_ADDONS addons CACHE INTERNAL "" FORCE)
diff --git a/check_validity.py b/check_validity.py
new file mode 100755 (executable)
index 0000000..8dc1110
--- /dev/null
@@ -0,0 +1,146 @@
+import salome
+import sys
+import os
+import SalomePyQt
+import ModelAPI
+from salome.shaper import model
+
+
+#------------------------------------------------------------------------
+def validateSession(logFile):
+  """
+  Iterate through and write all feature names of each part in the active study
+  to the logFile.
+  """
+  errCode = 0
+  try:
+    logFile.write("getting SHAPER session\n")
+    session = ModelAPI.ModelAPI_Session.get()
+    # aFactory = session.validators()
+    logFile.write("getting PartSet\n")
+    aPartSet = session.moduleDocument()
+    numParts = aPartSet.size("Parts")
+    for partIdx in range(numParts+1):
+      aPart = session.document(partIdx)
+      if aPart is None:
+        continue
+      aPart.setActive(True)
+      if partIdx == 0:
+        logFile.write("---PartSet:------------------\n")
+      else:
+        logFile.write(f"---Part_{partIdx}:------------------\n")
+
+      for aFeat in aPart.allFeatures():
+        if aFeat.isInHistory():
+          logFile.write("  * {} --> [{}]\n".format(aFeat.getKind(), aFeat.data().name()))
+        else:
+          logFile.write("    - {}\n".format(aFeat.data().name()))
+  except:
+    errCode = 128
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+def checkHDF(logFile, hdfFile):
+  """
+  Open and validate a HDF document
+  """
+  errCode = 0
+  try:
+    logFile.write("-----CHECKING HDF DOCUMENT-------\n")
+    salome.standalone()
+    logFile.write("initializing SALOME\n")
+    salome.salome_init(hdfFile, embedded=True, forced=True)
+    logFile.write("StudyName = {}\n".format(salome.myStudyName))
+
+    logFile.write("getting KERNEL session\n")
+    session = salome.naming_service.Resolve('/Kernel/Session')
+    logFile.write("connecting to study\n")
+    session.emitMessage("connect_to_study")
+
+    logFile.write("activating SHAPER module\n")
+    sg = SalomePyQt.SalomePyQt()
+    sg.activateModule("Shaper")
+
+    errCode = validateSession(logFile)
+  except:
+    errCode = 129
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+def checkPyScript(logFile, pyFile):
+  """
+  Load and validate a Python script
+  """
+  errCode = 0
+  try:
+    logFile.write("-----CHECKING PYTHON SCRIPT-------\n")
+    logFile.write("executing dumped script\n")
+    exec(compile(open(pyFile, 'rb').read(), pyFile, 'exec'))
+
+    errCode = validateSession(logFile)
+  except:
+    errCode = 130
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+def validateBackup(fullName):
+  """
+  Open the backed up file and validate its content.
+  This test script will be called once for the stored HDF file
+  end once for the dumped Python script.
+  """
+  errCode = 0
+  try:
+    outFolder,filename = os.path.split(fullName)
+    base,ext = os.path.splitext(filename)
+    ext = ext[1:].lower() # remove the dot from the extension
+
+    # Create a log file in the backup folder starting with the same base name
+    logFile = os.path.join(outFolder, base+"_valid.log")
+    if os.path.exists(logFile):
+        append_write = 'a' # append if already exists
+    else:
+        append_write = 'w' # make a new file if not
+
+    with open(logFile, append_write) as f:
+      try:
+        f.write(f"fullName = {fullName}\n")
+        if ext == "hdf":
+          errCode = checkHDF(f, fullName)
+        elif ext == "py":
+          errCode = checkPyScript(f, fullName)
+        else:
+          f.write(f"-----UNSUPPORTED FILE TYPE [{ext}]-------\n")
+          errCode = 131
+        f.write(f"errCode = {errCode}\n")
+        if errCode == 0:
+          f.write("{} Test - PASSED\n\n".format(ext.upper()))
+        else:
+          f.write("{} Test - FAILED\n\n".format(ext.upper()))
+      except:
+        errCode = 132
+        f.write("Exception caught\n")
+  except:
+    errCode = 133
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+errCode = 0
+try:
+  if (len(sys.argv) != 2):
+    errCode = 134
+  else:
+    fullName = sys.argv[1]
+    errCode = validateBackup(fullName)
+except:
+  errCode = 135
+
+exit(errCode)
index 1927466b408559dc09e5cd4acaa5963af7e787bd..badd2ae196ad0a5e98bf4bc466d2b055a10df316 100644 (file)
@@ -87,6 +87,7 @@
 # define _separator_ '/'
 #endif
 
+
 static const int UNDO_LIMIT = 1000;  // number of possible undo operations (big for sketcher)
 
 static const int TAG_GENERAL = 1;  // general properties tag
@@ -485,7 +486,9 @@ static bool saveDocument(Handle(Model_Application) theApp,
 }
 
 bool Model_Document::save(
-  const char* theDirName, const char* theFileName, std::list<std::string>& theResults)
+  const char* theDirName, const char* theFileName,
+  std::list<std::string>& theResults,
+  bool doBackup/*=false*/)
 {
   // if the history line is not in the end, move it to the end before save, otherwise
   // problems with results restore and (the most important) naming problems will appear
index 0017589196a7ca69e9475d5951634b6c60e8169b..f8f0d74d9da5d7398e74f00b6a30099391fccd17 100644 (file)
@@ -73,7 +73,7 @@ class Model_Document : public ModelAPI_Document
   //! \param theResults the result full file names that were stored by "save"
   //! \returns true if file was stored successfully
   MODEL_EXPORT virtual bool save(
-    const char* theDirName, const char* theFileName, std::list<std::string>& theResults);
+    const char* theDirName, const char* theFileName, std::list<std::string>& theResults, bool doBackup=false);
 
   //! Export the list of features to the file
   //! \param theFilename path to save the file
index 270fa1e365ab8fa711d53c5287a90eedc7099df2..da1fb36adba8ee7394bb40de26913d59a6d6acc6 100644 (file)
@@ -52,6 +52,7 @@
 
 #include <TopoDS_Shape.hxx>
 
+
 static Model_Session* myImpl = new Model_Session();
 
 // to redirect all calls to the root document
@@ -65,9 +66,9 @@ bool Model_Session::load(const char* theFileName)
   return aRes;
 }
 
-bool Model_Session::save(const char* theFileName, std::list<std::string>& theResults)
+bool Model_Session::save(const char* theFileName, std::list<std::string>& theResults, bool doBackup=false)
 {
-  return ROOT_DOC->save(theFileName, "root", theResults);
+  return ROOT_DOC->save(theFileName, "root", theResults, doBackup);
 }
 
 void Model_Session::closeAll()
index a93901a20c0af36fb0bc3861365673cca31f435a..e355bb52a7819f64880b290d5a0418f946329064 100644 (file)
@@ -66,7 +66,7 @@ class Model_Session : public ModelAPI_Session, public Events_Listener
   //! \param theFileName full name of the file to store
   //! \param theResults the result full file names that were stored by "save"
   //! \returns true if file was stored successfully
-  MODEL_EXPORT virtual bool save(const char* theFileName, std::list<std::string>& theResults);
+  MODEL_EXPORT virtual bool save(const char* theFileName, std::list<std::string>& theResults, bool dobackup=false);
 
   //! Closes all documents
   MODEL_EXPORT virtual void closeAll();
index 733e3b6ff4796750c292844437e1e691cb6f4e7a..44befaa2406065681d6ebd2259176c17e67e3f49 100644 (file)
@@ -59,7 +59,7 @@ protected:
   //! \param theFileName full name of the file to store
   //! \param theResults the result full file names that were stored by "save"
   //! \returns true if file was stored successfully
-  virtual bool save(const char* theFileName, std::list<std::string>& theResults) = 0;
+  virtual bool save(const char* theFileName, std::list<std::string>& theResults, bool isBackup=false) = 0;
 
   //! Closes all documents
   virtual void closeAll() = 0;
index 6cb464b7b267f6f1c40ff8fe65604a30b3d33328..af734166f70d0dab869f21d6f36b718b008e02f6 100644 (file)
@@ -220,6 +220,36 @@ void ModuleBase_Preferences::createGeneralTab(ModuleBase_IPrefMgr* thePref, int
                                   "part_visualization_study");
   thePref->setItemProperty("strings", visuItemList, visuId);
   thePref->setItemProperty("indexes", visuIdList, visuId);
+
+#if 1
+  // Group related to automatic backup
+  group = thePref->addPreference(QObject::tr("Automatically backup a study"), generalTab,
+                                 SUIT_PreferenceMgr::Auto, QString(), QString());
+
+  thePref->addPreference(QObject::tr("Enable automatic backup"), group, SUIT_PreferenceMgr::Bool,
+                         ModuleBase_Preferences::GENERAL_SECTION, "use_auto_backup");
+
+  int delay = thePref->addPreference(QObject::tr("Backup Interval" ), group, SUIT_PreferenceMgr::IntSpin,
+                                     ModuleBase_Preferences::GENERAL_SECTION, "backup_interval" );
+  thePref->setItemProperty( "min", 5, delay );
+  thePref->setItemProperty( "max", 1440, delay );
+  thePref->setItemProperty( "suffix", " min", delay );
+
+  int folderPref = thePref->addPreference(QObject::tr("Backup Folder"), group, SUIT_PreferenceMgr::File,
+                                          ModuleBase_Preferences::GENERAL_SECTION, "backup_folder");
+  thePref->setItemProperty( "mode", Qtx::PT_Directory, folderPref );
+
+  visuItemList.clear();
+  visuItemList << QObject::tr("Store last backup only")
+               << QObject::tr("Store full backup history");
+  visuIdList.clear();
+  visuIdList << 0 << 1;
+  visuId = thePref->addPreference(QObject::tr("Backup Storage"), group, SUIT_PreferenceMgr::Selector,
+                                  ModuleBase_Preferences::GENERAL_SECTION,
+                                  "auto_backup_storage");
+  thePref->setItemProperty("strings", visuItemList, visuId);
+  thePref->setItemProperty("indexes", visuIdList, visuId);
+#endif
 }
 
 void ModuleBase_Preferences::updateSketchTab(ModuleBase_IPrefMgr* thePref, int thePageId)
index cba839757b8cec47b69bbe91b8738774bfe9b93c..986f0ab2042f66d4e27efa0c2cb0456c4d9ecbfc 100644 (file)
@@ -30,6 +30,7 @@ SET(UPDATE_TRANSLATION OFF)
 SET(PROJECT_HEADERS
     SHAPER_SHAPERGUI.h
     SHAPERGUI.h
+    SHAPERGUI_CheckBackup.h
     SHAPERGUI_DataModel.h
     SHAPERGUI_OCCSelector.h
     SHAPERGUI_SalomeViewer.h
@@ -39,6 +40,7 @@ SET(PROJECT_HEADERS
 
 SET(PROJECT_MOC_HEADERS
     SHAPERGUI.h
+    SHAPERGUI_CheckBackup.h
     SHAPERGUI_DataModel.h
     SHAPERGUI_NestedButton.h
     SHAPERGUI_SalomeViewer.h
@@ -50,6 +52,7 @@ QT_WRAP_MOC(PROJECT_AUTOMOC ${PROJECT_MOC_HEADERS})
 
 SET(PROJECT_SOURCES
     SHAPERGUI.cpp
+    SHAPERGUI_CheckBackup.cpp
     SHAPERGUI_DataModel.cpp
     SHAPERGUI_OCCSelector.cpp
     SHAPERGUI_SalomeViewer.cpp
index 93883f60436b55fcd5253ad15f82a6eae570b062..2e6ffda4467757061a41f503cd81b7bd7e2b600e 100644 (file)
@@ -18,6 +18,7 @@
 //
 
 #include "SHAPERGUI.h"
+#include "SHAPERGUI_CheckBackup.h"
 #include "SHAPERGUI_DataModel.h"
 #include "SHAPERGUI_OCCSelector.h"
 #include "SHAPERGUI_NestedButton.h"
@@ -33,6 +34,7 @@
 #include <XGUI_FacesPanel.h>
 #include <XGUI_SelectionActivate.h>
 #include <XGUI_InspectionPanel.h>
+#include <XGUI_Tools.h>
 #include <XGUI_ViewerProxy.h>
 
 #include <ModuleBase_Operation.h>
 #include <QTimer>
 #include <QMenu>
 #include <QToolBar>
+#include <QFileInfo>
+#include <QDir>
+#include <QMessageBox>
+
+#include <chrono>
+#include <future>
+#include <thread>
 
 #include <ModelAPI_Session.h>
 #include <Events_MessageBool.h>
 
+//---------------------------------------------------------
+// Use a short 1 min interval for auto backup for debugging
+// Uncomment the following line for debugging.
+//#define DBG_BACKUP_INTERVAL   1
+//---------------------------------------------------------
+
 #if OCC_VERSION_HEX < 0x070400
   #define SALOME_PATCH_FOR_CTRL_WHEEL
 #endif
@@ -137,7 +152,6 @@ private:
 
 
 
-
 //******************************************************
 SHAPERGUI::SHAPERGUI()
     : LightApp_Module("SHAPER"),
@@ -153,6 +167,12 @@ SHAPERGUI::SHAPERGUI()
 
   ModuleBase_Preferences::setResourceMgr(application()->resourceMgr());
 
+  // initialize backup timer
+  myBackupTimer = new QTimer( this );
+  myBackupTimer->setSingleShot( true );
+  connect( myBackupTimer, SIGNAL( timeout() ), this, SLOT( onBackupDoc() ) );
+  connect( this, SIGNAL( backupDone(QString,int) ), this, SLOT( onBackupDone(QString,int) ));
+
   // It will be called in XGUI_Workshop::startApplication
   // ModuleBase_Preferences::loadCustomProps();
 }
@@ -383,6 +403,22 @@ bool SHAPERGUI::activateModule(SUIT_Study* theStudy)
           this, SLOT(onSaveDocByShaper()));
   connect(getApp()->action(LightApp_Application::FileSaveAsId), SIGNAL(triggered(bool)),
           this, SLOT(onSaveAsDocByShaper()));
+
+  SUIT_ResourceMgr* aResMgr = application()->resourceMgr();
+  if ( aResMgr && application()->activeStudy() ) {
+    bool useBackup = aResMgr->booleanValue( ModuleBase_Preferences::GENERAL_SECTION, "use_auto_backup", true );
+    if (useBackup) {
+      int backupInterval = aResMgr->integerValue( ModuleBase_Preferences::GENERAL_SECTION, "backup_interval", 5 );
+      if ( backupInterval > 0 ){
+#ifdef DBG_BACKUP_INTERVAL
+        backupInterval = DBG_BACKUP_INTERVAL; // MBS: use shorter interval for debugging
+#endif
+        myBackupTimer->start( backupInterval*60000 );
+      }
+    }
+  }
+  //connect();
+
   updateInfoPanel();
 
   //connect(myWorkshop->operationMgr(), SIGNAL(operationResumed(ModuleBase_Operation*)),
@@ -431,11 +467,12 @@ bool SHAPERGUI::deactivateModule(SUIT_Study* theStudy)
   saveToolbarsConfig();
   myWorkshop->deactivateModule();
 
+  myBackupTimer->stop();
+
   myIsInspectionVisible = myInspectionPanel->isVisible();
   myIsFacesPanelVisible = myWorkshop->facesPanel()->isVisible();
   hideInternalWindows();
 
-
   // the active operation should be stopped for the next activation.
   // There should not be active operation and visualized preview.
   // Abort operation should be performed before the selection's remove
@@ -530,16 +567,58 @@ static void onOperationGeneric( ModuleBase_Operation* theOperation,
                                            event );
 }
 
+class WaitBackupResetter {
+public:
+  WaitBackupResetter(XGUI_Workshop *theWshop)
+    :myWorkshop(theWshop)
+  {
+  }
+  WaitBackupResetter() = delete;
+  ~WaitBackupResetter()
+  {
+    if (myWorkshop)
+    {
+      myWorkshop->setWaitForBackup(false);
+      myWorkshop = nullptr;
+    }
+  }
+private:
+  XGUI_Workshop *myWorkshop;
+};
+
 //******************************************************
 void SHAPERGUI::onOperationCommitted(ModuleBase_Operation* theOperation)
 {
   onOperationGeneric(theOperation, moduleName(), "committed");
+
+  checkForWaitingBackup();
 }
 
 //******************************************************
 void SHAPERGUI::onOperationAborted(ModuleBase_Operation* theOperation)
 {
   onOperationGeneric(theOperation, moduleName(), "aborted");
+
+  checkForWaitingBackup();
+}
+
+//******************************************************
+void SHAPERGUI::checkForWaitingBackup()
+{
+  if (myWorkshop && myWorkshop->waitForBackup()) 
+  {
+    XGUI_OperationMgr *operMgr = myWorkshop->operationMgr();
+    if (operMgr && operMgr->hasOperation())
+    {
+      // If the user is still running an operation, e.g. we left the line creation
+      // during sketch creation, do not yet create the backup
+      std::cout << "---> There are still active operations!" << std::endl;
+      return;
+    }
+    // There are no more active operations => we can now do the backup
+    WaitBackupResetter _resetter(myWorkshop);
+    onBackupDoc();
+  }
 }
 
 //******************************************************
@@ -644,6 +723,227 @@ void SHAPERGUI::onSaveAsDocByShaper()
   getApp()->onSaveAsDoc();
 }
 
+//******************************************************
+void SHAPERGUI::onBackupDoc()
+{
+  // We cannot save the study while we are still in an ongoing operation
+  // => so test for this case first and delay the backup to the time when operation finishes
+  if (myWorkshop && myWorkshop->operationMgr()) 
+  {
+    if (myWorkshop->operationMgr()->hasOperation())
+    {
+      // If the user is running an operation, only do the backup
+      // after the operation has finished (committed or cancelled).
+      myWorkshop->setWaitForBackup();
+      return;
+    }
+    // Run the backup in a separate thread!!
+    myBackupResult = std::async(std::launch::async, [this]{return this->backupDoc();});
+  }
+}
+
+class LockBackupState {
+public:
+  LockBackupState(XGUI_Workshop *wshop) : myWorkshop(wshop)
+  {
+    if (myWorkshop) myWorkshop->setBackupState();
+  }
+  ~LockBackupState()
+  {
+    if (myWorkshop) myWorkshop->setBackupState(false);
+    myWorkshop = nullptr;
+  }
+  XGUI_Workshop *myWorkshop;
+};
+
+//******************************************************
+int SHAPERGUI::backupDoc()
+{
+  if (myWorkshop->backupState()) {
+    // This should never happen as I restart the backup timer only when a backup has finished
+    myBackupError = tr("Another backup is still running");
+    return 32;
+  }
+
+  int aResult = 0;
+  bool isOk = false;
+  SUIT_Study* study = application()->activeStudy();
+  if ( !study ) {
+    myBackupError = tr("There is no active study");
+    return 33;
+  }
+
+  LockBackupState lockBackup(myWorkshop);
+
+  QString aFolder{};
+  try
+  {
+    QString aName = study->studyName();
+    if ( aName.isEmpty() ) {
+      myBackupError = tr("Study name is empty");
+      return 34;
+    }
+
+    const QChar aSep = QDir::separator();
+    SUIT_ResourceMgr* aResMgr = application()->resourceMgr();
+    if ( aResMgr && application()->activeStudy() ) {
+      aFolder = aResMgr->path( ModuleBase_Preferences::GENERAL_SECTION, "backup_folder", "" );
+    }
+    if (aFolder.isEmpty()) {
+#ifdef HAVE_SALOME
+      aFolder = XGUI_Tools::getTmpDirByEnv("SALOME_TMP_DIR").c_str();
+#else
+      aFolder = XGUI_Tools::getTmpDirByEnv("").c_str();
+#endif
+    }
+    if (aFolder.endsWith(aSep))
+      aFolder = aFolder.remove(aFolder.length()-1,1);
+
+    QDateTime now = QDateTime::currentDateTime();
+    aFolder += aSep + now.toString("yyyyMMdd_hhmmss");
+
+    QDir aDir(aFolder);
+    if (!aDir.exists()) {
+      aDir.mkpath(aFolder);
+      aDir.mkdir(aFolder);
+      if (!aDir.exists()) {
+        myBackupError = tr("Cannot create backup folder");
+        return 35;
+      }
+    }
+
+    if (study->isSaved()) {
+      // Retrieve the filename only from the fullpath
+      QFileInfo fi(aName);
+      aName = fi.completeBaseName();
+    }
+    QString aFullName = aFolder + aSep + aName + QString(".hdf");
+
+    // Save the study into a single HDF file
+    isOk = study->saveDocumentAs( aFullName, true );
+    if (!isOk){
+      myBackupError = tr("Cannot backup study document");
+      return 36;
+    }
+
+    // Now, dump the python script
+    LightApp_Study *lightStudy = dynamic_cast<LightApp_Study*>(study);
+    if (!lightStudy) {
+      myBackupError = tr("Study is not dumpable");
+      return 37;
+    }
+
+    aFullName = aFolder + aSep + aName + QString(".py");
+    isOk = lightStudy->dump(aFullName, true, false, false);
+    if (!isOk){
+      myBackupError = tr("Cannot backup python script");
+      return 38;
+    }
+
+    // Finally start another salome process and reload the saved document & script for verification
+    SHAPERGUI_CheckBackup checkBackup(aFolder, aName);
+    QString testBackup("check_validity.py");
+    QStringList dirs;
+    dirs << QString(std::getenv("SHAPER_ROOT_DIR"))
+        << QString("bin")
+        << QString("salome")
+        << testBackup;
+    QString testScript = dirs.join( QDir::separator() );
+    aResult = checkBackup.run(testScript);
+  }
+  catch (std::exception &ex)
+  {
+    myBackupError = tr("std::exception caught");
+    aResult = 39;
+  }
+  catch (...)
+  {
+    myBackupError = tr("unknown exception caught");
+    aResult = 40;
+  }
+
+  emit backupDone(aFolder, aResult);
+  return aResult;
+}
+
+//******************************************************
+void SHAPERGUI::onBackupDone(QString aFolder, int aResult)
+{
+  int aErrCode = myBackupResult.get();
+  bool isOk = (aResult == 0);
+  if (isOk)
+  {
+    putInfo(tr("Backup done in folder: %1").arg(aFolder), 5000 );
+  }
+  else
+  {
+    QString aMsg = tr("Failed to backup active study!\nError Code: %1").arg(aResult);
+    QMessageBox::warning(application()->desktop(), tr("Automatic Backup"), aMsg);
+  }
+
+  int aBackupStorage{-1};
+  SUIT_ResourceMgr* aResMgr = application()->resourceMgr();
+  if ( aResMgr )
+  {
+    aBackupStorage = aResMgr->integerValue( ModuleBase_Preferences::GENERAL_SECTION, "auto_backup_storage", -1);
+  }
+  if (aBackupStorage == 0/*StoreLastBackupOnly*/)
+  {
+    // Only keep the latest successful backup => delete the previous one, if it exists
+    if (isOk && !myLastBackupFolder.isEmpty())
+    {
+      // Delete the previous backup folder
+      // To avoid deleting accidently an incorrect folder, check for
+      // the correct content. A backup folder should have 3-5 files:
+      //  * <FileName>.hdf       - the study itself
+      //  * <FileName>.py        - the python dump
+      //  * <FileName>_test.log  - the output of the "test_backup.py" script
+      //  * <FileName>_valid.log - the output of the "check_validity.py" script
+      QDir dir(myLastBackupFolder);
+      QStringList files = dir.entryList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot);
+      // I am afraid of accidently removing an entire folder tree, therefore check
+      // if "dir" contains really the latest backups and nothing more
+      if (!files.isEmpty() && files.length() <= 4)
+      {
+        QString baseName = files.constFirst();
+        baseName = baseName.left(baseName.lastIndexOf('.'));
+        if (!baseName.isEmpty() && files.filter(baseName).length() == files.length())
+        {
+          const bool success = dir.removeRecursively();
+          if (!success)
+          {
+            QString aMsg = tr("The previous backup folder could not be removed!");
+            QMessageBox::warning(application()->desktop(), tr("Automatic Backup"), aMsg);
+          }
+        }
+      }
+      else
+      {
+        QString aMsg = tr("The previous backup folder was not deleted,\nas there are more files in it than it is expected!");
+        QMessageBox::warning(application()->desktop(), tr("Automatic Backup"), aMsg);
+      }
+    }
+    myLastBackupFolder = aFolder;
+  }
+
+  // Start the timer again
+  if ( aResMgr && application()->activeStudy() )
+  {
+    bool useBackup = aResMgr->booleanValue( ModuleBase_Preferences::GENERAL_SECTION, "use_auto_backup", true );
+    if (useBackup)
+    {
+      int backupInterval = aResMgr->integerValue( ModuleBase_Preferences::GENERAL_SECTION, "backup_interval", 5 );
+      if ( backupInterval > 0 )
+      {
+#ifdef DBG_BACKUP_INTERVAL
+        backupInterval = DBG_BACKUP_INTERVAL; // MBS: use shorter interval for debugging
+#endif
+        myBackupTimer->start( backupInterval*60000 );
+      }
+    }
+  }
+}
+
 //******************************************************
 void SHAPERGUI::onUpdateCommandStatus()
 {
@@ -1081,11 +1381,32 @@ void SHAPERGUI::preferencesChanged(const QString& theSection, const QString& the
       }
     }
   }
-  else if (theSection == ModuleBase_Preferences::GENERAL_SECTION && theParam == "create_init_part") {
-    bool aCreate = ModuleBase_Preferences::resourceMgr()->booleanValue(
-      ModuleBase_Preferences::GENERAL_SECTION, "create_init_part", true);
-    Events_MessageBool aCreateMsg(Events_Loop::eventByName(EVENT_CREATE_PART_ON_START), aCreate);
-    aCreateMsg.send();
+  else if (theSection == ModuleBase_Preferences::GENERAL_SECTION) {
+    if (theParam == "create_init_part") {
+      bool aCreate = ModuleBase_Preferences::resourceMgr()->booleanValue(
+        ModuleBase_Preferences::GENERAL_SECTION, "create_init_part", true);
+      Events_MessageBool aCreateMsg(Events_Loop::eventByName(EVENT_CREATE_PART_ON_START), aCreate);
+      aCreateMsg.send();
+    }
+    else if (theParam == "use_auto_backup") {
+      bool useBackup = ModuleBase_Preferences::resourceMgr()->booleanValue(
+        ModuleBase_Preferences::GENERAL_SECTION, "use_auto_backup", true);
+      if (useBackup) {
+        int backupInterval = aResMgr->integerValue( ModuleBase_Preferences::GENERAL_SECTION, "backup_interval", 5 );
+        if ( backupInterval > 0 ){
+#ifdef DBG_BACKUP_INTERVAL
+          backupInterval = DBG_BACKUP_INTERVAL; // MBS: use shorter interval for debugging
+#endif
+          myBackupTimer->start( backupInterval*60000 );
+        }
+        else {
+          myBackupTimer->stop();
+        }
+      }
+      else {
+        myBackupTimer->stop();
+      }
+    }
   }
   else if (theSection == ModuleBase_Preferences::VIEWER_SECTION &&
            theParam.startsWith("group_names_"))
index 631ac080eb263a7c8fec3fdb87a40e814f14cff4..0ba515d8b46aa1cd95e09b3de67654639c19253a 100644 (file)
@@ -30,6 +30,7 @@
 
 #include <QList>
 #include <QMap>
+#include <future>
 
 class XGUI_Workshop;
 class SHAPERGUI_OCCSelector;
@@ -177,6 +178,9 @@ Q_OBJECT
 
   virtual void updateInfoPanel();
 
+ signals:
+  void backupDone(QString aName, int aResult);
+
  public slots:
   /// \brief The method is redefined to connect to the study viewer before the data
   /// model is filled by opened file. This file open will flush redisplay signals for,
@@ -212,6 +216,12 @@ Q_OBJECT
   /// Save application functionality with additional processing of aborting the current operation
   void onSaveAsDocByShaper();
 
+  /// Backup document functionality with additional file validation
+  void onBackupDoc();
+
+  /// Document has been backed-up
+  void onBackupDone(QString aName, int aResult);
+
   /// Obtains the current application and updates its actions
   void onUpdateCommandStatus();
 
@@ -228,6 +238,9 @@ Q_OBJECT
   /// Abort all operations
   virtual bool abortAllOperations();
 
+  /// The automatic backup thread function
+  int backupDoc();
+
 private slots:
   void onWhatIs(bool isToggled);
 
@@ -265,6 +278,9 @@ private slots:
 
   void hideInternalWindows();
 
+  // Check, whether we are waiting for a backup => if yes, do it now
+  void checkForWaitingBackup();
+
   /// List of registered nested actions
   QStringList myNestedActionsList;
 
@@ -306,6 +322,12 @@ private slots:
   Handle(Graphic3d_AspectMarker3d) myHighlightPointAspect;
 
   double myAxisArrowRate;
+
+  /// Automatic backup
+  QTimer*          myBackupTimer;      // The timer which triggers the automatic backup
+  std::future<int> myBackupResult;     // The result (succes/failure) of the backup
+  QString          myBackupError;      // The backup error message in case of a failure
+  QString          myLastBackupFolder; // The folder of the last backup
 };
 
 #endif
diff --git a/src/SHAPERGUI/SHAPERGUI_CheckBackup.cpp b/src/SHAPERGUI/SHAPERGUI_CheckBackup.cpp
new file mode 100644 (file)
index 0000000..fca6279
--- /dev/null
@@ -0,0 +1,166 @@
+// Copyright (C) 2024  CEA, EDF, OPEN CASCADE SAS
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library 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
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+#include "SHAPERGUI_CheckBackup.h"
+#include <fstream>
+#include <QDir>
+#include <QFileInfo>
+#include <QStringList>
+
+
+
+SHAPERGUI_CheckBackup::SHAPERGUI_CheckBackup(const QString &theFolder, const QString &theBaseName)
+  : myProcess(nullptr)
+  , myFolder(theFolder)
+  , myBaseName(theBaseName)
+{
+}
+
+
+int SHAPERGUI_CheckBackup::run(const QString &theTestScript)
+{
+  int aResult = 0;
+  if (myProcess)
+    return 64;
+
+  QString aProgName = std::getenv("PYTHONBIN");
+  if (aProgName.isEmpty())
+    aProgName = "python3";
+
+  const QChar aSep = QDir::separator();
+
+  QString testBackup("test_backup.py");
+  QStringList dirs;
+  dirs << QString(std::getenv("SHAPER_ROOT_DIR"))
+       << QString("bin")
+       << QString("salome")
+       << testBackup;
+  QString scriptName = dirs.join( QDir::separator() );
+
+  QStringList args;
+  args << scriptName << myFolder << myBaseName << theTestScript;
+
+  myProcess = new QProcess(this);
+  if (!myProcess)
+    return 65;
+
+  // connect(myProcess, SIGNAL(started()), this, SLOT(procStarted()));
+  // connect(myProcess, SIGNAL(error(QProcess::ProcessError)), this,
+  //         SLOT(procError(QProcess::ProcessError)));
+  // connect(myProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this,
+  //         SLOT(procFinished(int, QProcess::ExitStatus)));
+
+  // Start a python script (test_backup.py), which itself
+  //   * launches a SALOME process
+  //   * opens the previously backed up HDF study
+  myProcess->start(aProgName, args);
+  myProcess->waitForFinished(300000);
+  QProcess::ExitStatus exitStat = myProcess->exitStatus(); // 0=NormalExit, 1=CrashExit
+  int exitCode = myProcess->exitCode();
+  if (exitStat == QProcess::NormalExit && exitCode != 0) 
+    aResult = exitCode;
+  else if (exitStat == QProcess::CrashExit)
+    aResult = 66;
+  myProcess->deleteLater();
+
+  // Check the output of the log file
+  if (aResult == 0)
+  {
+    std::ifstream log((QStringList() << myFolder << myBaseName+"_valid.log").join(aSep).toStdString().c_str());
+    if (log)
+    {
+      uint8_t testFlag = 0;
+      std::string line;
+      while (std::getline(log, line))
+      {
+        if (line.find("HDF Test - ") == 0)
+        {
+          testFlag |= 0x1; // 0x01 = HDF Test performed
+          std::string strResult = line.substr(11);
+          if (strResult.find("PASSED") == 0)
+          {
+            if (testFlag == 0x03)
+              break;
+          }
+          else if (strResult.find("FAILED") == 0)
+          {
+            aResult = 67;
+            break;
+          }
+          else
+          {
+            aResult = 68;
+            break;
+          }
+        }
+        else if (line.find("PY Test - ") == 0)
+        {
+          testFlag |= 0x2; // 0x02 = PY Test performed
+          std::string strResult = line.substr(10);
+          if (strResult.find("PASSED") == 0)
+          {
+            if (testFlag == 0x03)
+              break;
+          }
+          else if (strResult.find("FAILED") == 0)
+          {
+            aResult = 69;
+            break;
+          }
+          else
+          {
+            aResult = 70;
+            break;
+          }
+        }
+      }
+      if (aResult == 0 && testFlag != 0x03)
+      {
+        // Not all tests were performed or they were interrupted
+        switch (testFlag)
+        {
+          case 0x00:  aResult = 71; break;
+          case 0x01:  aResult = 72; break;
+          case 0x02:  aResult = 73; break;
+        }
+      }
+    }
+    else
+    {
+      aResult = 74; // log file not found
+    }
+  }
+
+  return aResult;
+}
+
+
+void SHAPERGUI_CheckBackup::procStarted()
+{
+}
+
+
+void SHAPERGUI_CheckBackup::procFinished(int code, QProcess::ExitStatus stat)
+{
+}
+
+
+void SHAPERGUI_CheckBackup::procError(QProcess::ProcessError err)
+{
+}
diff --git a/src/SHAPERGUI/SHAPERGUI_CheckBackup.h b/src/SHAPERGUI/SHAPERGUI_CheckBackup.h
new file mode 100644 (file)
index 0000000..b491912
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) 2023  CEA, EDF, OPEN CASCADE SAS
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library 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
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+#ifndef SHAPERGUI_CHECKBACKUP_H
+#define SHAPERGUI_CHECKBACKUP_H
+
+#include "SHAPER_SHAPERGUI.h"
+#include <QProcess>
+
+
+class SHAPERGUI_EXPORT SHAPERGUI_CheckBackup : public QObject
+{
+  Q_OBJECT
+public:
+  SHAPERGUI_CheckBackup(const QString &theFolder, const QString &theBaseName);
+  int run(const QString &theTestScript);
+
+private slots:
+  void procStarted();
+  void procFinished(int, QProcess::ExitStatus);
+  void procError(QProcess::ProcessError);
+
+private:
+  QProcess* myProcess;
+  QString   myFolder;
+  QString   myBaseName;
+};
+
+#endif
index 0d976bd9efdc0defb4b1cb2f790f34687661104d..e9ec48d01b116dc0219d16a914993e0d8d1cb3f7 100644 (file)
@@ -37,6 +37,7 @@
 #include <QDir>
 #include <QTextStream>
 
+
 #define DUMP_NAME "shaper_dump.py"
 
 
@@ -83,7 +84,7 @@ bool SHAPERGUI_DataModel::open(const QString& thePath, CAM_Study* theStudy, QStr
   return true;
 }
 
-bool SHAPERGUI_DataModel::save(QStringList& theFiles)
+bool SHAPERGUI_DataModel::save(QStringList& theFiles, bool isBackup/*=false*/)
 {
   // Publish to study before saving of the data model
   myModule->publishToStudy();
@@ -120,10 +121,10 @@ bool SHAPERGUI_DataModel::save(QStringList& theFiles)
   return true;
 }
 
-bool SHAPERGUI_DataModel::saveAs(const QString& thePath, CAM_Study* theStudy, QStringList& theFiles)
+bool SHAPERGUI_DataModel::saveAs(const QString& thePath, CAM_Study* theStudy, QStringList& theFiles, bool isBackup/*=false*/)
 {
   myStudyPath = thePath;
-  return save(theFiles);
+  return save(theFiles, isBackup);
 }
 
 bool SHAPERGUI_DataModel::close()
index aacc4483b9af5e139b047f513a3f4fbe9a5279c3..18d2970b8edbf068a4499f17ef252f3689210e1e 100644 (file)
@@ -46,13 +46,13 @@ class SHAPERGUI_EXPORT SHAPERGUI_DataModel : public LightApp_DataModel
 
   /// Save module data to file
   /// \param theFiles list of created files
-  virtual bool save(QStringList& theFiles);
+  virtual bool save(QStringList& theFiles, bool isBackup=false);
 
   /// Save module data to a file
   /// \param thePath a path to the directory
   /// \param theStudy a current study
   /// \param theFiles a list of files to open
-  virtual bool saveAs(const QString& thePath, CAM_Study* theStudy, QStringList& theFiles);
+  virtual bool saveAs(const QString& thePath, CAM_Study* theStudy, QStringList& theFiles, bool isBackup=false);
 
   /// Close data structure
   virtual bool close();
index 740d35f89941f9f4877f9afd6962d3f9fe9b23ac..2bf82b2822f5437cd92cf3893ca90f1704f2b8d2 100644 (file)
         <source>Text color</source>
         <translation>Couleur du texte</translation>
     </message>
+    <message>
+        <source>Backup done in folder: %1</source>
+        <translation>Sauvegarde effectuée dans le dossier: %1</translation>
+    </message>
+    <message>
+        <source>Failed to backup active study!</source>
+        <translation>Échec de la sauvegarde de l&apos;étude active!</translation>
+    </message>
 </context>
 <context>
     <name>SHAPERGUI_ToolbarItemsDlg</name>
diff --git a/src/XGUI/MBDebug.h b/src/XGUI/MBDebug.h
new file mode 100644 (file)
index 0000000..9b4a83e
--- /dev/null
@@ -0,0 +1,313 @@
+#ifndef MBDebug_HeaderFile
+#define MBDebug_HeaderFile
+
+//---------------------------------------------------------------
+// Usage of the logging facilities:
+//
+//  (1) At the beginning of each class file to be debugged, there
+//      should be a static string variable defined with the name
+//      of the class. Then, include the "MBDebug.h" header file.
+//
+//      //---------------------------------------------------------
+//      #define USE_DEBUG
+//      //#define MB_IGNORE_QT
+//      //#define MB_FULL_DUMP
+//      #define MBCLASSNAME "ClassName"
+//      #include "MBDebug.h"
+//      // <-- insert includes for addtional debug headers here!
+//      //---------------------------------------------------------
+//
+//  (2) At the beginning of each class method, call the DBG_FUN
+//      macro.
+//
+//      int ClassName::MyMethod(int x)
+//      {
+//        DBG_FUN();
+//        ...
+//      }
+//
+//      NOTE: For static methods, call the DBG_FUNC() macro!!
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+//  This debugging/logging class is a "header-only" solution and
+//  does NOT require any additional implementation (.cpp) file!
+//---------------------------------------------------------------
+
+#include <iostream>
+#include <string>
+#include <locale>
+#include <codecvt>
+#include <list>
+#include <map>
+#include <set>
+#include <vector>
+#include <mutex>
+#include <thread>
+#ifndef MB_IGNORE_QT
+# include <QString>
+# include <QStringList>
+#endif
+
+static std::mutex mtx;
+
+//---------------------------------------------------------------
+//     Set the debug flags dependent on the preprocessor definitions
+//---------------------------------------------------------------
+#ifdef USE_DEBUG
+#      define MBS_DEBUG_FLAG           MBDebug::DF_DEBUG
+#else
+#      define MBS_DEBUG_FLAG           0
+#endif /*DEBUG*/ 
+
+#define        MBS_DBG_FLAGS                   (MBS_DEBUG_FLAG)
+
+
+//---------------------------------------------------------------
+//     Define the global debug macros
+//---------------------------------------------------------------
+#define DLOG                                                   MBDebug::LogPrint()
+#define RETURN(var)                            { RET(var); return (var); }
+
+#ifdef USE_DEBUG
+
+# define DBG_FUN()                             MBDebug _dbg(MBCLASSNAME, __FUNCTION__, MBS_DBG_FLAGS, (void*)this)
+# define DBG_FUNC()                            MBDebug _dbg(MBCLASSNAME, __FUNCTION__, MBS_DBG_FLAGS)
+# define DBG_FUNB(blk)         MBDebug _dbg(MBCLASSNAME, blk, MBS_DBG_FLAGS)
+#      define MSGEL(txt)                               MBDebug::LogPrint() <<  ":" << txt << std::endl
+#      define PRINT(txt)                               MBDebug::LogPrint() << txt
+#      define SHOW2(var,typ)           do { PRINT(std::this_thread::get_id()); DumpVar(#var,(typ)(var)); } while (0)
+#      define SHOW(var)                                do { PRINT(std::this_thread::get_id()); DumpVar(#var,var); } while (0)
+#      define ARG(var)                                 do { PRINT(std::this_thread::get_id() << ":in:"); DumpVar(#var,var); } while (0)
+#      define ARG2(var,typ)            do { PRINT(std::this_thread::get_id() << ":in:"); DumpVar(#var,(typ)(var)); } while (0)
+#      define RET(var)                                 do { PRINT(std::this_thread::get_id() << ":out:"); DumpVar(#var,var); } while (0)
+#      define MSG(txt)                                 MBDebug::LogPrint() << std::this_thread::get_id() << ":" << txt
+
+#else  /*!USE_DEBUG*/ 
+
+#      define DBG_FUN()
+#      define DBG_FUNC()
+#      define DBG_FUNB(blk)
+#      define MSGEL(txt)
+#      define PRINT(txt)
+#      define SHOW2(var,typ)
+#      define SHOW(var)
+#      define ARG(var)
+#      define ARG2(var,typ)
+#      define RET(var)
+#      define MSG(txt)
+
+#endif /*USE_DEBUG*/ 
+
+
+//---------------------------------------------------------------
+//     Declare the debugging and profiling class
+//---------------------------------------------------------------
+class MBDebug
+{
+public:
+       enum {
+               DF_NONE                 = 0x00,         // no debug
+               DF_DEBUG                = 0x01          // debug a function
+       };
+
+       MBDebug(const char* aClassName, const char* aFuncName, const short aFlag, void* aThis=NULL)
+       :mClassName(aClassName),mFuncName(aFuncName),mThis(aThis),mFlags((unsigned char)aFlag)
+  {
+       if (mFlags & (DF_DEBUG))
+       {
+      std::lock_guard<std::mutex> lck(mtx);
+               std::cout << std::this_thread::get_id() << ":{ENTER: " << mClassName + "::" + mFuncName;
+               if (mThis) std::cout << "(this=" << mThis << ")";
+               std::cout << std::endl;
+       }
+  }
+       virtual ~MBDebug()
+  {
+       if (mFlags & (DF_DEBUG))
+    {
+      std::lock_guard<std::mutex> lck(mtx);
+               std::cout << std::this_thread::get_id() << ":}LEAVE: " << mClassName << "::" << mFuncName << std::endl;
+    }
+  }
+
+       // Log file output management
+       static std::ostream&    LogPrint()      { return std::cout; }
+
+private:
+       std::string                     mClassName;     // Name of class to be debugged
+       std::string                     mFuncName;      // Name of function to be debugged
+       void*                           mThis;            // The "this" pointer to the class being debugged
+       unsigned char           mFlags;                 // Debug mode flags
+};
+
+
+
+#define YesNo(b)       (b ? "Yes" : "No")
+
+
+
+inline std::string w2s(std::wstring ws)
+{
+       using convert_typeX = std::codecvt_utf8<wchar_t>;
+       std::wstring_convert<convert_typeX, wchar_t> converterX;
+       return(converterX.to_bytes(ws));
+}
+
+// Primitive types
+inline void DumpVar(const char *szName, char value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[chr]: " << szName << "='" << value << "'" << std::endl;
+}
+
+inline void DumpVar(const char *szName, bool value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[bool]: " << szName << "=" << (value ? "true" : "false") << std::endl;
+}
+
+inline void DumpVar(const char *szName, short value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG  << "[shrt]: " << szName << "=" << value << std::endl;
+}
+
+inline void DumpVar(const char *szName, int value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[int]: " << szName << "=" << value << std::endl;
+}
+
+inline void DumpVar(const char *szName, long value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[long]: " << szName << "=" << value << std::endl;
+}
+
+inline void DumpVar(const char *szName, double value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[dbl]: " << szName << "=" << value << std::endl;
+}
+
+inline void DumpVar(const char *szName, unsigned char value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[byte]: " << szName << "=0x" << std::hex << value << std::dec << std::endl;
+}
+
+inline void DumpVar(const char *szName, unsigned short value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[word]: " << szName << "=0x" << std::hex << value << std::dec << std::endl;
+}
+
+inline void DumpVar(const char *szName, unsigned int value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[uint]: " << szName << "=0x" << std::hex << value << std::dec << std::endl;
+}
+
+inline void DumpVar(const char *szName, unsigned long value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[dword]: " << szName << "=0x" << std::hex << value << std::dec << std::endl;
+}
+
+inline void DumpVar(const char *szName, const char* value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[str]: " << szName << "=\"" << (value ? value : "") << "\"" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const std::string &value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[Str]: " << szName << "=\"" << value << "\"" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const std::wstring &value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[WStr]: " << szName << "=\"" << w2s(value) << "\"" << std::endl;
+}
+
+#ifndef MB_IGNORE_QT
+inline void DumpVar(const char *szName, const QString &value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[QStr]: " << szName << "=\"" << value.toStdString() << "\"" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const QStringList &value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[QStrLst]: " << szName << "=[len=" << value.length() << "] {";
+  bool first = true;
+  QStringList::const_iterator it = value.constBegin();
+  for ( ; it != value.constEnd(); ++it)
+  {
+    DLOG << (first ? "" : ",") << "\"" << (*it).toStdString() << "\"";
+    first = false;
+  }
+  DLOG << "}" << std::endl;
+}
+#endif
+
+inline void DumpVar(const char *szName, const void* value)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[ptr]: " << szName << "=" << value << std::endl;
+}
+
+
+// Collection of primitive types
+inline void DumpVar(const char *szName, const std::set<int> &values)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[intSet]: " << szName << "={" << values.size() << "}[";
+       bool bFirst = true;
+       for (auto it=values.cbegin(); it!=values.cend(); ++it) {
+               DLOG << (bFirst ? "" : ",") << *it;
+    bFirst = false;
+  }
+       DLOG << "]" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const std::vector<int> &values)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[intVect]: " << szName << "={" << values.size() << "}[";
+       bool bFirst = true;
+       for (auto it=values.cbegin(); it!=values.cend(); ++it) {
+               DLOG << (bFirst ? "" : ",") << *it;
+    bFirst = false;
+  }
+       DLOG << "]" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const std::list<bool>& values)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[boolList]: " << szName << "={" << values.size() << "}[";
+       bool bFirst = true;
+       for (auto it=values.cbegin(); it!=values.cend(); ++it) {
+               DLOG << (bFirst ? "" : ",") << (*it ? "Y" : "N");
+    bFirst = false;
+  }
+       DLOG << "]" << std::endl;
+}
+
+inline void DumpVar(const char *szName, const std::list<std::string> &values)
+{
+  std::lock_guard<std::mutex> lck(mtx);
+       DLOG << "[strLst]: " << szName << "={" << values.size() << "}[";
+       bool bFirst = true;
+       for (auto it=values.cbegin(); it!=values.cend(); ++it) {
+               DLOG << (bFirst ? "\"" : ", \"") << *it << "\"";
+    bFirst = false;
+  }
+       DLOG << "]" << std::endl;
+}
+
+#endif // MBDebug_HeaderFile
+
index 0df895c850db18a1e6de7e1528bcddf0d0bdf874..78146b8c65c380112587fb0ffff7a1a73daf5a46 100644 (file)
@@ -180,6 +180,15 @@ static Handle(VInspector_CallBack) MyVCallBack;
 #include <dlfcn.h>
 #endif
 
+//---------------------------------------------------------
+#define USE_DEBUG
+//#define MB_IGNORE_QT
+//#define MB_FULL_DUMP
+#define MBCLASSNAME "XGUI_Workshop"
+#include "MBDebug.h"
+// <-- insert includes for addtional debug headers here!
+//---------------------------------------------------------
+
 //#define DEBUG_WITH_MESSAGE_REPORT
 
 QString XGUI_Workshop::MOVE_TO_END_COMMAND = QObject::tr("Move to the end");
@@ -212,8 +221,10 @@ XGUI_Workshop::XGUI_Workshop(XGUI_SalomeConnector* theConnector)
       myDisplayer(0),
       mySalomeConnector(theConnector),
       //myViewerSelMode(TopAbs_FACE),
-      myInspectionPanel(0)
+      myInspectionPanel(0),
+      myDoBackup(false)
 {
+  DBG_FUN();
   mySelector = new XGUI_SelectionMgr(this);
   myModuleConnector = new XGUI_ModuleConnector(this);
   myOperationMgr = new XGUI_OperationMgr(this, myModuleConnector);
@@ -315,6 +326,7 @@ XGUI_Workshop::XGUI_Workshop(XGUI_SalomeConnector* theConnector)
 //******************************************************
 XGUI_Workshop::~XGUI_Workshop(void)
 {
+  DBG_FUN();
 #ifdef _DEBUG
 #ifdef MISSED_TRANSLATION
   // Save Missed translations
@@ -332,6 +344,7 @@ XGUI_Workshop::~XGUI_Workshop(void)
 //******************************************************
 void XGUI_Workshop::startApplication()
 {
+  DBG_FUN();
   //Initialize event listening
   myEventsListener->initializeEventListening();
 
@@ -700,6 +713,28 @@ void XGUI_Workshop::showHelpPage(const QString& thePage) const
 }
 
 
+//******************************************************
+void XGUI_Workshop::setBackupState(bool doBackup/*=true*/)
+{
+  DBG_FUN();
+  ARG(doBackup);
+  myDoBackup = doBackup;
+}
+
+
+//******************************************************
+void XGUI_Workshop::setWaitForBackup(bool theDoWait/*=true*/)
+{
+  DBG_FUN();
+  ARG(theDoWait);
+  myWaitForBackup = theDoWait;
+  if (theDoWait)
+  {
+    std::cout << "..... waiting for operation to finish to start backup." << std::endl;
+  }
+}
+
+
 //******************************************************
 void XGUI_Workshop::deactivateActiveObject(const ObjectPtr& theObject, const bool theUpdateViewer)
 {
@@ -856,6 +891,9 @@ void XGUI_Workshop::connectToPropertyPanel(const bool isToConnect)
 //******************************************************
 void XGUI_Workshop::onOperationResumed(ModuleBase_Operation* theOperation)
 {
+  DBG_FUN();
+  ARG(theOperation->getDescription()->operationId());
+
   setGrantedFeatures(theOperation);
 
   if (theOperation->getDescription()->hasXmlRepresentation()) {  //!< No need for property panel
@@ -876,6 +914,9 @@ void XGUI_Workshop::onOperationResumed(ModuleBase_Operation* theOperation)
 //******************************************************
 void XGUI_Workshop::onOperationStopped(ModuleBase_Operation* theOperation)
 {
+  DBG_FUN();
+  ARG(theOperation->getDescription()->operationId());
+
   updateCommandStatus();
 
   ModuleBase_OperationFeature* aFOperation = dynamic_cast<ModuleBase_OperationFeature*>
@@ -958,6 +999,10 @@ void XGUI_Workshop::setGrantedFeatures(ModuleBase_Operation* theOperation)
 //******************************************************
 void XGUI_Workshop::saveDocument(const QString& theName, std::list<std::string>& theFileNames)
 {
+  DBG_FUN();
+  ARG(theName);
+  ARG(theFileNames);
+  SHOW(myDoBackup);
   QApplication::restoreOverrideCursor();
   SessionPtr aMgr = ModelAPI_Session::get();
 
@@ -976,12 +1021,15 @@ void XGUI_Workshop::saveDocument(const QString& theName, std::list<std::string>&
 //******************************************************
 bool XGUI_Workshop::abortAllOperations()
 {
+  DBG_FUN();
   return myOperationMgr->abortAllOperations();
 }
 
 //******************************************************
 void XGUI_Workshop::operationStarted(ModuleBase_Operation* theOperation)
 {
+  DBG_FUN();
+  ARG(theOperation->getDescription()->operationId());
   setGrantedFeatures(theOperation);
   if (!theOperation->getDescription()->hasXmlRepresentation()) {  //!< No need for property panel
     updateCommandStatus();
@@ -994,6 +1042,7 @@ void XGUI_Workshop::operationStarted(ModuleBase_Operation* theOperation)
 //******************************************************
 void XGUI_Workshop::onOpen()
 {
+  DBG_FUN();
   if(!abortAllOperations())
     return;
   //save current file before close if modified
@@ -1024,6 +1073,8 @@ void XGUI_Workshop::onOpen()
 //******************************************************
 void XGUI_Workshop::openFile(const QString& theDirectory)
 {
+  DBG_FUN();
+  ARG(theDirectory);
   myCurrentFile = theDirectory;
   if (myCurrentFile.isEmpty())
     return;
@@ -1196,7 +1247,10 @@ void XGUI_Workshop::onTrihedronVisibilityChanged(bool theState)
 //******************************************************
 bool XGUI_Workshop::onSave()
 {
-  if(!myOperationMgr->abortAllOperations(XGUI_OperationMgr::XGUI_InformationMessage))
+  DBG_FUN();
+  SHOW(myDoBackup);
+
+  if(!myDoBackup && !myOperationMgr->abortAllOperations(XGUI_OperationMgr::XGUI_InformationMessage))
     return false;
   if (myCurrentFile.isEmpty()) {
     return onSaveAs();
@@ -1228,6 +1282,8 @@ bool XGUI_Workshop::onSave()
 //******************************************************
 bool XGUI_Workshop::onSaveAs()
 {
+  DBG_FUN();
+  SHOW(myDoBackup);
   if(!myOperationMgr->abortAllOperations(XGUI_OperationMgr::XGUI_InformationMessage))
     return false;
   qreal aRatio = ModuleBase_Tools::currentPixelRatio();
@@ -1446,6 +1502,8 @@ void XGUI_Workshop::onExportPart()
 //******************************************************
 ModuleBase_IModule* XGUI_Workshop::loadModule(const QString& theModule)
 {
+  DBG_FUN();
+  ARG(theModule);
   QString libName = QString::fromStdString(library(theModule.toStdString()));
   if (libName.isEmpty()) {
     qWarning(qPrintable(tr("Information about module \"%1\" doesn't exist.").arg(theModule)));
@@ -2355,10 +2413,12 @@ bool hasResults(QObjectPtrList theObjects, const std::set<std::string>& theTypes
 // all nested parts.
 std::list<FeaturePtr> allFeatures(const DocumentPtr& theDocument)
 {
+  DBG_FUNC();
   std::list<FeaturePtr> aResultList;
   std::list<FeaturePtr> anAllFeatures = theDocument->allFeatures();
   foreach (const FeaturePtr& aFeature, anAllFeatures) {
     // The order of appending features of the part and the part itself is important
+    MSGEL("* aFeature = [" << aFeature->getKind() << "]  action=" << aFeature->isAction());
 
     // Append features from a part feature
     std::list<ResultPtr> aResults;
index e0d97cbae43f2f3d1d32645c17366114efe949d4..d84457249fa467282e3742c831d13c64f78b45ca 100644 (file)
@@ -354,6 +354,14 @@ Q_OBJECT
 
   void showHelpPage(const QString& thePage) const;
 
+  void setBackupState(bool theDoBackup=true);
+
+  bool backupState() const    { return myDoBackup; }
+
+  void setWaitForBackup(bool theDoWait=true);
+
+  bool waitForBackup() const  { return myWaitForBackup; }
+
 signals:
   /// Emitted when selection happens in Salome viewer
   void salomeViewerSelection();
@@ -592,6 +600,8 @@ private:
   Config_DataModelReader* myDataModelXMLReader; ///< XML reader of data model
   XGUI_InspectionPanel* myInspectionPanel; ///< container of feature attributes widgets
   QTemporaryDir myTmpDir; ///< a direcory for uncompressed files
+  bool myDoBackup; ///< whether the current save was initiated by automatic backup
+  bool myWaitForBackup; ///< whether to do a backup when current operation finishes
 };
 
 #endif
index 01f8c1f7c1a7d14afe1314439d62a82218ae83de..747451f91e82729f49fc89934524bad90625bded 100644 (file)
@@ -64,7 +64,7 @@ if __name__ == '__main__':
 
   with open(testlogfile, 'r') as inputFile:
     s = inputFile.read()
-    #print("logfile: ", s)
+    print("logfile: ", s)
     if s.find("FAIL") > 0:
       isOk = False
       error = s
diff --git a/test_backup.py b/test_backup.py
new file mode 100755 (executable)
index 0000000..f8cc291
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2023  OPEN CASCADE SAS
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+
+import subprocess
+import sys, os
+import tempfile
+
+
+#------------------------------------------------------------------------
+def checkFileInSubprocess(logFile, title, args):
+  errCode = 0
+  try:
+    logFile.write(f"starting {title} process:\n")
+    logFile.write("  cmd_line = '{}'\n".format(" ".join(args)))
+    proc = subprocess.Popen(args)
+    try:
+      logFile.write(f"  start communication with {title}\n")
+      proc.communicate(timeout = 500)
+      logFile.write(f"{title} terminated\n")
+      errCode = proc.returncode
+      logFile.write(f"{title} returned: {errCode}\n")
+    except subprocess.TimeoutExpired:
+      errCode = 96
+      logFile.write(f"{title} timed out\n")
+    except Exception as ex:
+      errCode = 97
+      logFile.write(f"Exception caught: {ex}\n")
+    except:
+      errCode = 98
+      logFile.write("Unknown Exception caught\n")
+
+    logFile.write(f"errCode = {errCode}\n")
+  except:
+    errCode = 99
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+def checkBackups(backupFolder, baseName, checkScript):
+  errCode = 0
+  try:
+    # Create a log file in the backup folder starting with the same base name
+    with open(os.path.join(backupFolder, baseName+"_test.log"), "w") as logFile:
+      logFile.write("test_backup script started\n")
+      logFile.write(f"  backupFolder = {backupFolder}\n")
+      logFile.write(f"  baseName     = {baseName}\n")
+      logFile.write(f"  checkScript  = {checkScript}\n")
+      hdfFile = os.path.join(backupFolder, baseName+".hdf")
+      logFile.write(f"  hdfFile      = {hdfFile}\n")
+
+      args = ["runSalome.py", "--modules", "SHAPER,SHAPERSTUDY", "--batch", "--splash", "0", checkScript, "args:" + hdfFile]
+      errCode = checkFileInSubprocess(logFile, "SALOME", args)
+      logFile.write(f"errCode = {errCode}\n")
+
+      if errCode == 0:
+        pyFile = os.path.join(backupFolder, baseName+".py")
+        logFile.write(f"  pyfile       = {pyFile}\n")
+
+        args = ["python", checkScript, pyFile]
+        errCode = checkFileInSubprocess(logFile, "PYTHON", args)
+        logFile.write(f"errCode = {errCode}\n")
+
+  except:
+    errCode = 100
+
+  return errCode
+
+
+#------------------------------------------------------------------------
+errCode = 0
+try:
+  if (len(sys.argv) != 4):
+    errCode = 101
+  else:
+    backupFolder = sys.argv[1]
+    baseName = sys.argv[2]
+    checkScript = sys.argv[3]
+    errCode = checkBackups(backupFolder, baseName, checkScript)
+except:
+  errCode = 102
+
+exit(errCode)