From 1eaf50b6b9fb6cbf4c5b21326c0e8d54028d690c Mon Sep 17 00:00:00 2001 From: JJ <69144954+jonjenssen@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:42:11 +0200 Subject: [PATCH] Support running jobs without blocking GUI (#13040) * Add view job log command. * Add stop job command. * Do not block ui while job is running. * Show progress while job is running * Make sure user gets notified of running jobs when exiting ResInsight or closing current project. --- ApplicationExeCode/Resources/ResInsight.qrc | 3 +- ApplicationExeCode/Resources/stop.svg | 5 + .../Application/RiaGuiApplication.cpp | 29 +++ .../Application/RiaGuiApplication.h | 7 +- .../RicCloseProjectFeature.cpp | 8 +- .../RicExitApplicationFeature.cpp | 3 +- .../RicOpenLastUsedFileFeature.cpp | 2 +- .../RicOpenProjectFeature.cpp | 2 +- .../JobCommands/CMakeLists_files.cmake | 4 + .../JobCommands/RicStopJobFeature.cpp | 60 ++++++ .../Commands/JobCommands/RicStopJobFeature.h | 38 ++++ .../JobCommands/RicViewJobLogFeature.cpp | 60 ++++++ .../JobCommands/RicViewJobLogFeature.h | 33 +++ .../Jobs/CMakeLists_files.cmake | 2 + .../ProjectDataModel/Jobs/RimGenericJob.cpp | 190 +++++++++++++++--- .../ProjectDataModel/Jobs/RimGenericJob.h | 30 ++- .../Jobs/RimJobCollection.cpp | 22 ++ .../ProjectDataModel/Jobs/RimJobCollection.h | 2 + .../ProjectDataModel/Jobs/RimJobMonitor.cpp | 70 +++++++ .../ProjectDataModel/Jobs/RimJobMonitor.h | 41 ++++ .../ProjectDataModel/Jobs/RimOpmFlowJob.cpp | 137 ++++++++++--- .../ProjectDataModel/Jobs/RimOpmFlowJob.h | 7 +- .../ProcessControl/RimProcess.cpp | 98 ++++++--- .../ProcessControl/RimProcess.h | 10 +- .../ProcessControl/RimProcessMonitor.cpp | 48 +++-- .../ProcessControl/RimProcessMonitor.h | 14 +- .../UserInterface/RiuMainWindow.cpp | 2 +- .../UserInterface/RiuPlotMainWindow.cpp | 2 +- .../RiuRecentFileActionProvider.cpp | 2 +- 29 files changed, 809 insertions(+), 122 deletions(-) create mode 100644 ApplicationExeCode/Resources/stop.svg create mode 100644 ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.cpp create mode 100644 ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.h create mode 100644 ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.cpp create mode 100644 ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.h create mode 100644 ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.h diff --git a/ApplicationExeCode/Resources/ResInsight.qrc b/ApplicationExeCode/Resources/ResInsight.qrc index 5a827a61d2..c939613301 100644 --- a/ApplicationExeCode/Resources/ResInsight.qrc +++ b/ApplicationExeCode/Resources/ResInsight.qrc @@ -302,7 +302,8 @@ Select.svg NavigationProperty.svg user-defined-view.svg - gear.svg + stop.svg + gear.svg Parameter.svg diff --git a/ApplicationExeCode/Resources/stop.svg b/ApplicationExeCode/Resources/stop.svg new file mode 100644 index 0000000000..dd499fe9c0 --- /dev/null +++ b/ApplicationExeCode/Resources/stop.svg @@ -0,0 +1,5 @@ + + + + diff --git a/ApplicationLibCode/Application/RiaGuiApplication.cpp b/ApplicationLibCode/Application/RiaGuiApplication.cpp index c45b80339c..4f3bb60deb 100644 --- a/ApplicationLibCode/Application/RiaGuiApplication.cpp +++ b/ApplicationLibCode/Application/RiaGuiApplication.cpp @@ -48,6 +48,7 @@ #include "SummaryPlotCommands/RicSummaryPlotFeatureImpl.h" #include "Formations/RimFormationNamesCollection.h" +#include "Jobs/RimJobCollection.h" #include "Rim2dIntersectionViewCollection.h" #include "RimAnnotationCollection.h" #include "RimAnnotationInViewCollection.h" @@ -289,6 +290,34 @@ bool RiaGuiApplication::saveProjectAs( const QString& fileName ) return true; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RiaGuiApplication::notifyUserAboutRunningJobs() +{ + if ( m_project->jobCollection()->numberOfRunningJobs() > 0 ) + { + QMessageBox::critical( nullptr, + "Active running jobs", + "One or more jobs are still running, you need to stop all jobs before continuing." ); + return false; + } + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RiaGuiApplication::checkWithUserBeforeClose() +{ + // check for any running jobs + if ( !notifyUserAboutRunningJobs() ) return false; + // ask user to save modified project if needed + if ( !askUserToSaveModifiedProject() ) return false; + + return true; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/RiaGuiApplication.h b/ApplicationLibCode/Application/RiaGuiApplication.h index 7d4a97a58f..3784ea24d2 100644 --- a/ApplicationLibCode/Application/RiaGuiApplication.h +++ b/ApplicationLibCode/Application/RiaGuiApplication.h @@ -88,9 +88,10 @@ public: bool saveProject(); QString promptForProjectSaveAsFileName() const; - bool askUserToSaveModifiedProject(); bool saveProjectAs( const QString& fileName ); + bool checkWithUserBeforeClose(); + void runMultiCaseSnapshots( const QString& templateProjectFileName, std::vector gridFileNames, const QString& snapshotFolderName ); bool useShaders() const; @@ -127,6 +128,10 @@ public: protected: bool notify( QObject* receiver, QEvent* event ) override; + // use the checkWithUserBeforeClose function, to get all checks in one go + bool notifyUserAboutRunningJobs(); + bool askUserToSaveModifiedProject(); + // Protected RiaApplication overrides void invokeProcessEvents( QEventLoop::ProcessEventsFlags flags = QEventLoop::AllEvents ) override; void onFileSuccessfullyLoaded( const QString& fileName, RiaDefines::ImportFileType fileType ) override; diff --git a/ApplicationLibCode/Commands/ApplicationCommands/RicCloseProjectFeature.cpp b/ApplicationLibCode/Commands/ApplicationCommands/RicCloseProjectFeature.cpp index 4a6e83fc53..414eb070ab 100644 --- a/ApplicationLibCode/Commands/ApplicationCommands/RicCloseProjectFeature.cpp +++ b/ApplicationLibCode/Commands/ApplicationCommands/RicCloseProjectFeature.cpp @@ -31,10 +31,12 @@ void RicCloseProjectFeature::onActionTriggered( bool isChecked ) { disableModelChangeContribution(); - RiaGuiApplication* app = RiaGuiApplication::instance(); - if ( !app || !app->askUserToSaveModifiedProject() ) return; + if ( RiaGuiApplication* app = RiaGuiApplication::instance() ) + { + if ( !app->checkWithUserBeforeClose() ) return; - app->closeProject(); + app->closeProject(); + } } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/ApplicationCommands/RicExitApplicationFeature.cpp b/ApplicationLibCode/Commands/ApplicationCommands/RicExitApplicationFeature.cpp index 4fb91901f2..36ce30e144 100644 --- a/ApplicationLibCode/Commands/ApplicationCommands/RicExitApplicationFeature.cpp +++ b/ApplicationLibCode/Commands/ApplicationCommands/RicExitApplicationFeature.cpp @@ -35,7 +35,8 @@ void RicExitApplicationFeature::onActionTriggered( bool isChecked ) disableModelChangeContribution(); RiaGuiApplication* app = RiaGuiApplication::instance(); - if ( !app->askUserToSaveModifiedProject() ) return; + + if ( !app->checkWithUserBeforeClose() ) return; if ( app->mainPlotWindow() ) { diff --git a/ApplicationLibCode/Commands/ApplicationCommands/RicOpenLastUsedFileFeature.cpp b/ApplicationLibCode/Commands/ApplicationCommands/RicOpenLastUsedFileFeature.cpp index bfeb4a10f9..23799287c2 100644 --- a/ApplicationLibCode/Commands/ApplicationCommands/RicOpenLastUsedFileFeature.cpp +++ b/ApplicationLibCode/Commands/ApplicationCommands/RicOpenLastUsedFileFeature.cpp @@ -34,7 +34,7 @@ void RicOpenLastUsedFileFeature::onActionTriggered( bool isChecked ) { RiaGuiApplication* app = RiaGuiApplication::instance(); - if ( !app->askUserToSaveModifiedProject() ) return; + if ( !app->checkWithUserBeforeClose() ) return; QString fileName = RiaPreferences::current()->lastUsedProjectFileName; diff --git a/ApplicationLibCode/Commands/ApplicationCommands/RicOpenProjectFeature.cpp b/ApplicationLibCode/Commands/ApplicationCommands/RicOpenProjectFeature.cpp index 136dc2cb67..e79b6878cd 100644 --- a/ApplicationLibCode/Commands/ApplicationCommands/RicOpenProjectFeature.cpp +++ b/ApplicationLibCode/Commands/ApplicationCommands/RicOpenProjectFeature.cpp @@ -36,7 +36,7 @@ void RicOpenProjectFeature::onActionTriggered( bool isChecked ) { RiaGuiApplication* app = RiaGuiApplication::instance(); - if ( !app->askUserToSaveModifiedProject() ) return; + if ( !app->checkWithUserBeforeClose() ) return; QString defaultDir = app->lastUsedDialogDirectory( "BINARY_GRID" ); QString fileName = diff --git a/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake index 69f434e612..5cedf6268d 100644 --- a/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake @@ -2,12 +2,16 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RicRunJobFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicNewOpmFlowJobFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicDuplicateJobFeature.h + ${CMAKE_CURRENT_LIST_DIR}/RicViewJobLogFeature.h + ${CMAKE_CURRENT_LIST_DIR}/RicStopJobFeature.h ) set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RicRunJobFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicNewOpmFlowJobFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicDuplicateJobFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicViewJobLogFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicStopJobFeature.cpp ) list(APPEND COMMAND_CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) diff --git a/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.cpp b/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.cpp new file mode 100644 index 0000000000..480b9186e8 --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.cpp @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RicStopJobFeature.h" + +#include "Jobs/RimGenericJob.h" + +#include "cafSelectionManager.h" + +#include +#include + +CAF_CMD_SOURCE_INIT( RicStopJobFeature, "RicStopJobFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicStopJobFeature::onActionTriggered( bool isChecked ) +{ + stopJob( dynamic_cast( caf::SelectionManager::instance()->selectedItem() ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicStopJobFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setIcon( QIcon( ":/stop.svg" ) ); + actionToSetup->setText( "Stop..." ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RicStopJobFeature::stopJob( RimGenericJob* job ) +{ + if ( job == nullptr ) return false; + + if ( QMessageBox::question( nullptr, job->name(), "Do you want to stop this job?", QMessageBox::Yes | QMessageBox::No ) == QMessageBox::Yes ) + { + return job->stop(); + } + + return false; +} diff --git a/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.h b/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.h new file mode 100644 index 0000000000..62a3003529 --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicStopJobFeature.h @@ -0,0 +1,38 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafCmdFeature.h" + +class RimGenericJob; + +//================================================================================================== +/// +//================================================================================================== +class RicStopJobFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +public: + static bool stopJob( RimGenericJob* job ); + +protected: + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.cpp b/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.cpp new file mode 100644 index 0000000000..10a53ced11 --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.cpp @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RicViewJobLogFeature.h" + +#include "RiuTextDialog.h" + +#include "Jobs/RimGenericJob.h" + +#include "cafSelectionManager.h" + +#include +#include + +CAF_CMD_SOURCE_INIT( RicViewJobLogFeature, "RicViewJobLogFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicViewJobLogFeature::onActionTriggered( bool isChecked ) +{ + if ( auto job = dynamic_cast( caf::SelectionManager::instance()->selectedItem() ) ) + { + const QStringList log = job->jobLog(); + + QString text = log.join( "\n" ); + + auto* textDlg = new RiuTextDialog(); + textDlg->setMinimumSize( 600, 800 ); + + textDlg->setWindowTitle( "Job log for \"" + job->name() + "\"" ); + textDlg->setText( text ); + + textDlg->exec(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicViewJobLogFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setIcon( QIcon( ":/inspect.svg" ) ); + actionToSetup->setText( "View Log..." ); +} diff --git a/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.h b/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.h new file mode 100644 index 0000000000..74419125b3 --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicViewJobLogFeature.h @@ -0,0 +1,33 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafCmdFeature.h" + +//================================================================================================== +/// +//================================================================================================== +class RicViewJobLogFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +protected: + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake index 3cace000c5..69b8114747 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake @@ -5,6 +5,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimDeckPositionDlg.h ${CMAKE_CURRENT_LIST_DIR}/RimKeywordWconprod.h ${CMAKE_CURRENT_LIST_DIR}/RimKeywordWconinje.h + ${CMAKE_CURRENT_LIST_DIR}/RimJobMonitor.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -14,6 +15,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimDeckPositionDlg.cpp ${CMAKE_CURRENT_LIST_DIR}/RimKeywordWconprod.cpp ${CMAKE_CURRENT_LIST_DIR}/RimKeywordWconinje.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimJobMonitor.cpp ) list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp index 62188afb89..c1e98377ab 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp @@ -18,9 +18,16 @@ #include "RimGenericJob.h" +#include "RiaColorTools.h" +#include "RiaLogging.h" + +#include "RimJobMonitor.h" #include "RimProcess.h" +#include "RiuGuiTheme.h" + #include "cafCmdFeatureMenuBuilder.h" +#include "cafPdmUiTreeAttributes.h" #include "cafProgressInfo.h" #include @@ -31,6 +38,10 @@ CAF_PDM_XML_ABSTRACT_SOURCE_INIT( RimGenericJob, "GenericJob" ); // Do not use. /// //-------------------------------------------------------------------------------------------------- RimGenericJob::RimGenericJob() + : m_percentageDone( 0.0 ) + , m_lastRunFailed( false ) + , m_isRunning( false ) + , m_process( nullptr ) { CAF_PDM_InitObject( "Generic Job" ); } @@ -42,13 +53,29 @@ RimGenericJob::~RimGenericJob() { } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimGenericJob::percentageDone() const +{ + return m_percentageDone; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGenericJob::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { - menuBuilder << "RicRunJobFeature"; + if ( isRunning() ) + { + menuBuilder << "RicStopJobFeature"; + } + else + { + menuBuilder << "RicRunJobFeature"; + } menuBuilder << "RicDuplicateJobFeature"; + menuBuilder << "RicViewJobLogFeature"; } //-------------------------------------------------------------------------------------------------- @@ -59,52 +86,157 @@ QString RimGenericJob::workingDirectory() const return ""; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimGenericJob::isRunning() const +{ + return m_isRunning; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimGenericJob::stop() +{ + if ( !m_process.isNull() ) + { + m_process->terminate(); + RiaLogging::info( "Job \"" + name() + "\" stopped by user." ); + return true; + } + return false; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RimGenericJob::execute() { + if ( isRunning() ) return false; + + m_process = nullptr; + m_percentageDone = 0.0; + // job preparations { - caf::ProgressInfo prepProgress( 1, title(), false ); + caf::ProgressInfo prepProgress( 1, name(), false ); auto prepRun = prepProgress.task( "Preparing for run, please wait..." ); - if ( !onPrepare() ) return false; + if ( !onPrepare() ) + { + m_lastRunFailed = true; + onProgress( m_percentageDone ); + return false; + } } // check if we should run if ( !onRun() ) return false; - // run job - bool runOk = false; + QStringList cmdLine = command(); + if ( cmdLine.isEmpty() ) return false; + + // cannot delete job while running + setDeletable( false ); + + m_process = new RimProcess( true, new RimJobMonitor( this ) ); + + m_isRunning = true; + m_lastRunFailed = false; + + onProgress( m_percentageDone ); + + // build process to run + QString cmd = cmdLine.takeFirst(); + m_process->setCommand( cmd ); + if ( !cmdLine.isEmpty() ) m_process->addParameters( cmdLine ); + m_process->setWorkingDirectory( workingDirectory() ); + for ( const auto& [name, value] : environment() ) { - caf::ProgressInfo runProgress( 1, title() ); - - auto taskRun = runProgress.task( "Running job, please wait..." ); - - QStringList cmdLine = command(); - if ( cmdLine.isEmpty() ) return false; - - QString cmd = cmdLine.takeFirst(); - - RimProcess process; - process.setCommand( cmd ); - if ( !cmdLine.isEmpty() ) process.addParameters( cmdLine ); - process.setWorkingDirectory( workingDirectory() ); - for ( const auto& [name, value] : environment() ) - { - process.addEnvironmentVariable( name, value ); - } - - runOk = process.execute(); + m_process->addEnvironmentVariable( name, value ); } + // run process + bool startOk = m_process->start(); + if ( !startOk ) + { + onCompleted( false ); + m_lastRunFailed = true; + m_isRunning = false; + setDeletable( true ); + QMessageBox::critical( nullptr, name(), "Failed to start job. Check log window for additional information." ); + } + + return startOk; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimGenericJob::setFinished( bool runOk ) +{ + m_isRunning = false; + + m_lastRunFailed = !runOk; + + m_percentageDone = 100.0; + onProgress( m_percentageDone ); + setDeletable( true ); + onCompleted( runOk ); - if ( !runOk ) - { - QMessageBox::critical( nullptr, title(), "Failed to run job. Check log window for additional information." ); - } return runOk; -} \ No newline at end of file +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGenericJob::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + if ( auto* treeItemAttribute = dynamic_cast( attribute ) ) + { + if ( m_lastRunFailed ) + { + auto tag = caf::PdmUiTreeViewItemAttribute::createTag( QColor( Qt::red ), + RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), + "!!!" ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + else + { + if ( ( m_percentageDone == 0.0 ) && ( !m_isRunning ) ) return; + + auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); + + if ( m_isRunning ) + { + tag->text = QString( "%1 %" ).arg( m_percentageDone, 0, 'f', 1 ); + } + else + { + tag->text = "Done"; + } + + double factor = m_percentageDone / 100.0; + + cvf::Color3f viewColor = cvf::Color3f( cvf::Color3f::GREEN ); + viewColor.set( viewColor.r() * factor, viewColor.g() * factor, viewColor.b() * factor ); + cvf::Color3f viewTextColor = RiaColorTools::contrastColor( viewColor ); + tag->bgColor = QColor( RiaColorTools::toQColor( viewColor ) ); + tag->fgColor = QColor( RiaColorTools::toQColor( viewTextColor ) ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const QStringList RimGenericJob::jobLog() const +{ + if ( m_process.isNull() ) return QStringList(); + + return m_process->stdOut(); +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h index c92abeceb3..186e1ccdcc 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h @@ -20,11 +20,15 @@ #include "RimNamedObject.h" +#include "cafPdmPointer.h" + #include #include #include +class RimProcess; + //================================================================================================== /// /// @@ -38,15 +42,33 @@ public: ~RimGenericJob() override; bool execute(); + bool setFinished( bool runOk ); + + bool isRunning() const; + bool stop(); + + double percentageDone() const; + const QStringList jobLog() const; + + virtual void decodeProgress( const QString& logLine ) = 0; protected: void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; - virtual QString title() = 0; virtual QStringList command() = 0; virtual std::map environment() = 0; virtual QString workingDirectory() const; - virtual bool onPrepare() = 0; - virtual bool onRun() = 0; - virtual void onCompleted( bool success ) = 0; + virtual bool onPrepare() = 0; + virtual bool onRun() = 0; + virtual void onCompleted( bool success ) = 0; + virtual void onProgress( double percentageDone ) = 0; + +protected: + double m_percentageDone; + +private: + bool m_lastRunFailed; + bool m_isRunning; + caf::PdmPointer m_process; }; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.cpp index 9208d9dd42..b14eae7dfb 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.cpp @@ -74,11 +74,33 @@ void RimJobCollection::addNewJob( RimGenericJob* newJob ) updateAllRequiredEditors(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimJobCollection::numberOfRunningJobs() const +{ + int nRunning = 0; + for ( auto job : jobs() ) + { + if ( job->isRunning() ) nRunning++; + } + + return nRunning; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimJobCollection::deleteAllJobs() { + for ( auto job : jobs() ) + { + if ( job->isRunning() ) + { + job->stop(); + RiaLogging::info( "Stopped running job '" + job->name() ); + } + } m_jobs.deleteChildren(); } diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.h index c412c20306..6a9cc1892b 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobCollection.h @@ -38,6 +38,8 @@ public: bool isEmpty(); + int numberOfRunningJobs() const; + void deleteAllJobs(); std::vector jobs() const; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.cpp new file mode 100644 index 0000000000..129507dd80 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.cpp @@ -0,0 +1,70 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimJobMonitor.h" + +#include "RimGenericJob.h" + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimJobMonitor::RimJobMonitor( RimGenericJob* job ) + : RimProcessMonitor( 0, true ) + , m_job( job ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimJobMonitor::~RimJobMonitor() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobMonitor::readyReadStandardOutput() +{ + if ( QProcess* p = qobject_cast( sender() ) ) + { + p->setReadChannel( QProcess::StandardOutput ); + while ( p->canReadLine() ) + { + QString line = p->readLine(); + line = line.trimmed(); + if ( line.size() == 0 ) continue; + + m_job->decodeProgress( line ); + m_stdOut.append( line ); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobMonitor::finished( int exitCode, QProcess::ExitStatus exitStatus ) +{ + if ( m_job.notNull() ) + { + m_job->setFinished( exitStatus == QProcess::NormalExit && exitCode == 0 ); + } + + RimProcessMonitor::finished( exitCode, exitStatus ); +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.h new file mode 100644 index 0000000000..e62ae69f11 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobMonitor.h @@ -0,0 +1,41 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RimProcessMonitor.h" + +#include "cafPdmPointer.h" + +class RimGenericJob; + +class RimJobMonitor : public RimProcessMonitor +{ + Q_OBJECT + +public: + RimJobMonitor( RimGenericJob* job ); + ~RimJobMonitor(); + +protected: + void readyReadStandardOutput() override; + void finished( int exitCode, QProcess::ExitStatus exitStatus ) override; + +private: + caf::PdmPointer m_job; +}; \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp index 9a56129473..3d6360d206 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp @@ -27,6 +27,8 @@ #include "CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h" #include "EclipseCommands/RicCreateGridCaseEnsemblesFromFilesFeature.h" #include "JobCommands/RicRunJobFeature.h" +#include "JobCommands/RicStopJobFeature.h" + #include "RifOpmFlowDeckFile.h" #include "Ensemble/RimSummaryFileSetEnsemble.h" @@ -95,6 +97,7 @@ void caf::AppEnum::setUp() RimOpmFlowJob::RimOpmFlowJob() : m_fileDeckHasDates( false ) , m_fileDeckIsRestart( false ) + , m_startStepForProgress( -1 ) { CAF_PDM_InitObject( "Opm Flow Simulation", ":/opm.png" ); @@ -144,6 +147,10 @@ RimOpmFlowJob::RimOpmFlowJob() caf::PdmUiPushButtonEditor::configureEditorLabelHidden( &m_runButton ); m_runButton.xmlCapability()->disableIO(); + CAF_PDM_InitField( &m_stopButton, "stopButton", false, "" ); + caf::PdmUiPushButtonEditor::configureEditorLabelHidden( &m_stopButton ); + m_stopButton.xmlCapability()->disableIO(); + CAF_PDM_InitField( &m_resetRunIdButton, "resetRunIdButton", false, " " ); caf::PdmUiPushButtonEditor::configureEditorLabelLeft( &m_resetRunIdButton ); m_resetRunIdButton.xmlCapability()->disableIO(); @@ -178,6 +185,48 @@ void RimOpmFlowJob::initAfterRead() openDeckFile(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::decodeProgress( const QString& logLine ) +{ + // Example log lines: + // Report step 757/773 at day 9466/10958, date = 01-Dec-2025 + // Report step 758/773 at day 9497/10958, date = 01-Jan-2026 + if ( logLine.startsWith( "Report step" ) ) + { + auto parts = logLine.split( ' ', Qt::SkipEmptyParts ); + if ( parts.size() >= 4 ) + { + auto stepPart = parts[2]; // 756/773 + auto stepParts = stepPart.split( '/' ); + if ( stepParts.size() == 2 ) + { + bool ok1 = false; + bool ok2 = false; + int currentStep = stepParts[0].toInt( &ok1 ); + if ( ok1 && ( m_startStepForProgress < 0 ) ) + { + m_startStepForProgress = currentStep; + } + + int totalSteps = stepParts[1].toInt( &ok2 ); + if ( ok1 && ok2 ) + { + auto noSteps = totalSteps - m_startStepForProgress; + if ( noSteps > 0 ) + { + double perc = ( (double)( currentStep - m_startStepForProgress ) / (double)( noSteps ) ) * 100.0; + perc = std::max( 0.0, perc ); + perc = std::min( perc, 100.0 ); + onProgress( perc ); + } + } + } + } + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -196,7 +245,16 @@ void RimOpmFlowJob::defineEditorAttribute( const caf::PdmFieldHandle* field, QSt if ( pbAttribute ) { pbAttribute->m_buttonText = "Run Simulation"; - pbAttribute->m_buttonIcon = QIcon( ":/opm.png" ); + pbAttribute->m_buttonIcon = QIcon( ":/Play.svg" ); + } + } + else if ( field == &m_stopButton ) + { + auto* pbAttribute = dynamic_cast( attribute ); + if ( pbAttribute ) + { + pbAttribute->m_buttonText = "Stop"; + pbAttribute->m_buttonIcon = QIcon( ":/stop.svg" ); } } else if ( field == &m_openSelectButton ) @@ -223,7 +281,7 @@ void RimOpmFlowJob::defineEditorAttribute( const caf::PdmFieldHandle* field, QSt attr->enableEditableContent = true; attr->enableAutoComplete = false; attr->adjustWidthToContents = true; - attr->notifyWhenTextIsEdited = true; + attr->notifyWhenTextIsEdited = false; } } } @@ -233,6 +291,17 @@ void RimOpmFlowJob::defineEditorAttribute( const caf::PdmFieldHandle* field, QSt //-------------------------------------------------------------------------------------------------- void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { + if ( isRunning() ) + { + auto runGrp = uiOrdering.addNewGroup( "Running" ); + m_workDir.uiCapability()->setUiReadOnly( true ); + runGrp->add( &m_workDir ); + runGrp->add( &m_stopButton ); + uiOrdering.skipRemainingFields(); + return; + } + m_workDir.uiCapability()->setUiReadOnly( false ); + auto genGrp = uiOrdering.addNewGroup( "General" ); genGrp->add( nameField() ); genGrp->add( &m_deckFileName ); @@ -346,8 +415,7 @@ QList RimOpmFlowJob::calculateValueOptions( const caf::P } else if ( ( fieldNeedingOptions == &m_openTimeStep ) || ( fieldNeedingOptions == &m_endTimeStep ) ) { - openDeckFile(); - if ( m_deckFile != nullptr ) + if ( openDeckFile() ) { auto timeStepNames = dateStrings(); for ( int i = 0; i < static_cast( timeStepNames.size() - 1 ); ++i ) @@ -363,7 +431,7 @@ QList RimOpmFlowJob::calculateValueOptions( const caf::P } else if ( fieldNeedingOptions == &m_wellGroupName ) { - for ( auto grp : wellgroupsInFileDeck() ) + for ( auto& grp : wellgroupsInFileDeck() ) { options.push_back( caf::PdmOptionItemInfo( grp, QVariant::fromValue( grp ) ) ); } @@ -444,6 +512,11 @@ void RimOpmFlowJob::fieldChangedByUi( const caf::PdmFieldHandle* changedField, c m_runButton = false; RicRunJobFeature::runJob( this ); } + else if ( changedField == &m_stopButton ) + { + m_stopButton = false; + RicStopJobFeature::stopJob( this ); + } else if ( changedField == &m_resetRunIdButton ) { m_resetRunIdButton = false; @@ -499,13 +572,14 @@ void RimOpmFlowJob::setEclipseCase( RimEclipseCase* eCase ) if ( eCase == nullptr ) { m_deckFileName.setValue( QString() ); + closeDeckFile(); return; } QFileInfo fi( eCase->gridFileName() ); m_deckFileName.setValue( fi.absolutePath() + "/" + fi.completeBaseName() + deckExtension() ); m_eclipseCase = eCase; - m_deckFile.reset(); + closeDeckFile(); openDeckFile(); } @@ -516,7 +590,7 @@ void RimOpmFlowJob::setInputDataFile( QString filename ) { m_deckName = ""; m_deckFileName.setValue( filename ); - m_deckFile.reset(); + closeDeckFile(); openDeckFile(); } @@ -525,33 +599,42 @@ void RimOpmFlowJob::setInputDataFile( QString filename ) //-------------------------------------------------------------------------------------------------- bool RimOpmFlowJob::openDeckFile() { - if ( m_deckFile == nullptr ) + if ( m_deckFile.get() == nullptr ) { m_deckFile = std::make_unique(); bool deckLoadOk = false; try { deckLoadOk = m_deckFile->loadDeck( m_deckFileName().path().toStdString() ); + if ( deckLoadOk ) + { + m_fileDeckHasDates = m_deckFile->hasDatesKeyword(); + m_fileDeckIsRestart = m_deckFile->isRestartFile(); + } } catch ( std::filesystem::filesystem_error& ) { + deckLoadOk = false; RiaLogging::error( QString( "Failed to open %1, possibly unsupported or incorrect format." ).arg( m_deckFileName().path() ) ); } - if ( deckLoadOk ) - { - m_fileDeckHasDates = m_deckFile->hasDatesKeyword(); - m_fileDeckIsRestart = m_deckFile->isRestartFile(); - } - else + if ( !deckLoadOk ) { m_fileDeckHasDates = false; m_fileDeckIsRestart = false; - m_deckFile.reset(); + closeDeckFile(); } } - return m_deckFile != nullptr; + return m_deckFile.get() != nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::closeDeckFile() +{ + m_deckFile.reset(); } //-------------------------------------------------------------------------------------------------- @@ -575,14 +658,6 @@ bool RimOpmFlowJob::copyUnrstFileToWorkDir() return false; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -QString RimOpmFlowJob::title() -{ - return name(); -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -718,7 +793,7 @@ std::map RimOpmFlowJob::environment() bool RimOpmFlowJob::onPrepare() { // reload file deck to make sure we start with the original - m_deckFile.reset(); + closeDeckFile(); if ( !openDeckFile() ) { RiaLogging::error( "Unable to open input DATA file " + m_deckFileName().path() ); @@ -865,7 +940,8 @@ bool RimOpmFlowJob::onPrepare() // save DATA file to working folder bool saveOk = m_deckFile->saveDeck( workingDirectory().toStdString(), deckName().toStdString() + deckExtension().toStdString() ); - m_deckFile.reset(); + + closeDeckFile(); return saveOk; } @@ -888,6 +964,15 @@ bool RimOpmFlowJob::onRun() return true; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::onProgress( double percentageDone ) +{ + m_percentageDone = percentageDone; + updateConnectedEditors(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h index 4905fe8573..f5d90bffc3 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h @@ -71,15 +71,18 @@ protected: QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; void initAfterRead() override; - QString title() override; + void decodeProgress( const QString& logLine ) override; + QStringList command() override; std::map environment() override; QString workingDirectory() const override; bool onPrepare() override; bool onRun() override; void onCompleted( bool success ) override; + void onProgress( double percentageDone ) override; bool openDeckFile(); + void closeDeckFile(); bool copyUnrstFileToWorkDir(); private: @@ -106,6 +109,7 @@ private: caf::PdmField m_deckFileName; caf::PdmField m_workDir; caf::PdmField m_runButton; + caf::PdmField m_stopButton; caf::PdmField m_openSelectButton; caf::PdmField m_openWellDeckPosition; @@ -141,4 +145,5 @@ private: std::unique_ptr m_deckFile; bool m_fileDeckHasDates; bool m_fileDeckIsRestart; + int m_startStepForProgress; }; diff --git a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.cpp b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.cpp index 5e68ce331e..62d66312d3 100644 --- a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.cpp +++ b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.cpp @@ -34,11 +34,18 @@ int RimProcess::m_nextProcessId = 1; //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RimProcess::RimProcess( bool logStdOutErr /*true*/ ) +RimProcess::RimProcess( bool logStdOutErr /*true*/, RimProcessMonitor* monitor ) : m_enableLogging( logStdOutErr ) + , m_qProcess( nullptr ) { int defId = m_nextProcessId++; - m_monitor = new RimProcessMonitor( defId, logStdOutErr ); + if ( monitor == nullptr ) + m_monitor = new RimProcessMonitor( defId, logStdOutErr ); + else + { + m_monitor = monitor; + m_monitor->setProcessId( defId ); + } CAF_PDM_InitObject( "ResInsight Process", ":/Erase.png" ); @@ -60,7 +67,11 @@ RimProcess::RimProcess( bool logStdOutErr /*true*/ ) //-------------------------------------------------------------------------------------------------- RimProcess::~RimProcess() { - delete m_monitor; + if ( m_monitor != nullptr ) + { + delete m_monitor; + m_monitor = nullptr; + } } caf::PdmFieldHandle* RimProcess::userDescriptionField() @@ -157,51 +168,86 @@ int RimProcess::ID() const //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -bool RimProcess::execute( bool enableStdOut, bool enableStdErr ) +bool RimProcess::start( bool enableStdOut, bool enableStdErr ) { if ( !m_monitor ) return false; + if ( m_qProcess != nullptr ) return false; - QProcess* proc = new QProcess(); - QString cmd = commandLine(); + m_qProcess = new QProcess(); + QString cmd = commandLine(); if ( m_enableLogging ) RiaLogging::info( QString( "Start process %1: %2" ).arg( m_id ).arg( cmd ) ); m_monitor->clearStdOutErr(); - QObject::connect( proc, SIGNAL( finished( int, QProcess::ExitStatus ) ), m_monitor, SLOT( finished( int, QProcess::ExitStatus ) ) ); - if ( enableStdOut ) QObject::connect( proc, SIGNAL( readyReadStandardOutput() ), m_monitor, SLOT( readyReadStandardOutput() ) ); - if ( enableStdErr ) QObject::connect( proc, SIGNAL( readyReadStandardError() ), m_monitor, SLOT( readyReadStandardError() ) ); - QObject::connect( proc, SIGNAL( started() ), m_monitor, SLOT( started() ) ); - - bool retval = false; + QObject::connect( m_qProcess, SIGNAL( finished( int, QProcess::ExitStatus ) ), m_monitor, SLOT( finished( int, QProcess::ExitStatus ) ) ); + if ( enableStdOut ) QObject::connect( m_qProcess, SIGNAL( readyReadStandardOutput() ), m_monitor, SLOT( readyReadStandardOutput() ) ); + if ( enableStdErr ) QObject::connect( m_qProcess, SIGNAL( readyReadStandardError() ), m_monitor, SLOT( readyReadStandardError() ) ); + QObject::connect( m_qProcess, SIGNAL( started() ), m_monitor, SLOT( started() ) ); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); for ( auto& [key, val] : m_environmentVariables ) { env.insert( key, val ); } - proc->setProcessEnvironment( env ); + m_qProcess->setProcessEnvironment( env ); if ( !m_workDir().isEmpty() ) { - proc->setWorkingDirectory( m_workDir ); + m_qProcess->setWorkingDirectory( m_workDir ); } - proc->start( m_command, m_arguments ); - auto error = proc->errorString(); - if ( proc->waitForStarted( -1 ) ) - { - while ( !proc->waitForFinished( 500 ) ) - { - QApplication::processEvents( QEventLoop::ExcludeUserInputEvents ); - } - retval = ( proc->exitCode() == 0 ); - } - else + m_qProcess->start( m_command, m_arguments ); + auto error = m_qProcess->errorString(); + if ( !m_qProcess->waitForStarted( -1 ) ) { RiaLogging::error( QString( "Failed to start process %1. %2." ).arg( m_id ).arg( error ) ); + return false; } - proc->deleteLater(); + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimProcess::cleanUpAfterRun() +{ + if ( m_qProcess != nullptr ) + { + m_qProcess->deleteLater(); + m_qProcess = nullptr; + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimProcess::terminate() +{ + if ( m_qProcess != nullptr ) + { + if ( m_qProcess->state() == QProcess::Running ) + { + m_qProcess->kill(); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimProcess::execute( bool enableStdOut, bool enableStdErr ) +{ + if ( !start( enableStdOut, enableStdErr ) ) return false; + + while ( !m_qProcess->waitForFinished( 250 ) ) + { + QApplication::processEvents( QEventLoop::ExcludeUserInputEvents ); + } + + bool retval = ( m_qProcess->exitCode() == 0 ); + + cleanUpAfterRun(); return retval; } diff --git a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.h b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.h index a08990a2df..0771c92d2c 100644 --- a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.h +++ b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcess.h @@ -27,13 +27,14 @@ #include class RimProcessMonitor; +class QProcess; class RimProcess : public caf::PdmObject { CAF_PDM_HEADER_INIT; public: - RimProcess( bool logStdOutErr = true ); + RimProcess( bool logStdOutErr = true, RimProcessMonitor* monitor = nullptr ); ~RimProcess() override; void setDescription( QString desc ); @@ -51,8 +52,14 @@ public: QStringList parameters() const; int ID() const; + // blocking run bool execute( bool enableStdOut = true, bool enableStdErr = true ); + // async run + bool start( bool enableStdOut = true, bool enableStdErr = true ); + void cleanUpAfterRun(); + void terminate(); + QStringList stdErr() const; QStringList stdOut() const; @@ -77,4 +84,5 @@ private: static int m_nextProcessId; RimProcessMonitor* m_monitor; bool m_enableLogging; + QProcess* m_qProcess; }; diff --git a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.cpp b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.cpp index 365ae35d5e..22f1c68b04 100644 --- a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.cpp +++ b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.cpp @@ -100,17 +100,19 @@ void RimProcessMonitor::finished( int exitCode, QProcess::ExitStatus exitStatus //-------------------------------------------------------------------------------------------------- void RimProcessMonitor::readyReadStandardError() { - QProcess* p = (QProcess*)sender(); - p->setReadChannel( QProcess::StandardError ); - while ( p->canReadLine() ) + if ( QProcess* p = qobject_cast( sender() ) ) { - QString line = p->readLine(); - line = line.trimmed(); - if ( line.size() == 0 ) continue; - m_stdErr.append( line ); - if ( m_logStdOutErr ) + p->setReadChannel( QProcess::StandardError ); + while ( p->canReadLine() ) { - RiaLogging::error( addPrefix( line ) ); + QString line = p->readLine(); + line = line.trimmed(); + if ( line.size() == 0 ) continue; + m_stdErr.append( line ); + if ( m_logStdOutErr ) + { + RiaLogging::error( addPrefix( line ) ); + } } } } @@ -120,17 +122,19 @@ void RimProcessMonitor::readyReadStandardError() //-------------------------------------------------------------------------------------------------- void RimProcessMonitor::readyReadStandardOutput() { - QProcess* p = (QProcess*)sender(); - p->setReadChannel( QProcess::StandardOutput ); - while ( p->canReadLine() ) + if ( QProcess* p = qobject_cast( sender() ) ) { - QString line = p->readLine(); - line = line.trimmed(); - if ( line.size() == 0 ) continue; - m_stdOut.append( line ); - if ( m_logStdOutErr ) + p->setReadChannel( QProcess::StandardOutput ); + while ( p->canReadLine() ) { - RiaLogging::info( addPrefix( line ) ); + QString line = p->readLine(); + line = line.trimmed(); + if ( line.size() == 0 ) continue; + m_stdOut.append( line ); + if ( m_logStdOutErr ) + { + RiaLogging::info( addPrefix( line ) ); + } } } } @@ -167,3 +171,11 @@ QStringList RimProcessMonitor::stdErr() const { return m_stdErr; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimProcessMonitor::setProcessId( int processId ) +{ + m_processId = processId; +} diff --git a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.h b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.h index 3216017214..ddc9139f7c 100644 --- a/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.h +++ b/ApplicationLibCode/ProjectDataModel/ProcessControl/RimProcessMonitor.h @@ -34,16 +34,18 @@ public: QStringList stdOut() const; QStringList stdErr() const; + void setProcessId( int processId ); + signals: public slots: - void error( QProcess::ProcessError error ); - void finished( int exitCode, QProcess::ExitStatus exitStatus ); - void readyReadStandardError(); - void readyReadStandardOutput(); - void started(); + virtual void error( QProcess::ProcessError error ); + virtual void finished( int exitCode, QProcess::ExitStatus exitStatus ); + virtual void readyReadStandardError(); + virtual void readyReadStandardOutput(); + virtual void started(); -private: +protected: QString addPrefix( QString message ); int m_processId; bool m_logStdOutErr; diff --git a/ApplicationLibCode/UserInterface/RiuMainWindow.cpp b/ApplicationLibCode/UserInterface/RiuMainWindow.cpp index fba52487b0..07e286545e 100644 --- a/ApplicationLibCode/UserInterface/RiuMainWindow.cpp +++ b/ApplicationLibCode/UserInterface/RiuMainWindow.cpp @@ -351,7 +351,7 @@ void RiuMainWindow::closeEvent( QCloseEvent* event ) if ( !app->isMainPlotWindowVisible() ) { - if ( !app->askUserToSaveModifiedProject() ) + if ( !app->checkWithUserBeforeClose() ) { event->ignore(); return; diff --git a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp index 4b059c6f24..cc7e66a4f8 100644 --- a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp +++ b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp @@ -286,7 +286,7 @@ void RiuPlotMainWindow::closeEvent( QCloseEvent* event ) RiaGuiApplication* app = RiaGuiApplication::instance(); if ( !app->isMain3dWindowVisible() ) { - if ( !app->askUserToSaveModifiedProject() ) + if ( !app->checkWithUserBeforeClose() ) { event->ignore(); return; diff --git a/ApplicationLibCode/UserInterface/RiuRecentFileActionProvider.cpp b/ApplicationLibCode/UserInterface/RiuRecentFileActionProvider.cpp index d8dd887c14..2c68e4e921 100644 --- a/ApplicationLibCode/UserInterface/RiuRecentFileActionProvider.cpp +++ b/ApplicationLibCode/UserInterface/RiuRecentFileActionProvider.cpp @@ -128,7 +128,7 @@ void RiuRecentFileActionProvider::slotOpenRecentFile() RiaGuiApplication* app = RiaGuiApplication::instance(); if ( RiaApplication::hasValidProjectFileExtension( fileName ) ) { - if ( !app->askUserToSaveModifiedProject() ) return; + if ( !app->checkWithUserBeforeClose() ) return; } bool loadingSucceded = RiaApplication::instance()->openFile( fileName );