///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2026- 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 "RiuWorkflowValidationRunner.h" #include "Workflow/RimWorkflow.h" #include "Workflow/RimWorkflowHelperProcess.h" #include "Workflow/RimWorkflowValidationTools.h" #include "RiaLogging.h" #include #include namespace { RimWorkflowIssue issueWithMessage( const QString& message ) { RimWorkflowIssue issue; issue.message = message; return issue; } } // namespace //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiuWorkflowValidationRunner::RiuWorkflowValidationRunner( QObject* parent ) : QObject( parent ) , m_debounceTimer( new QTimer( this ) ) , m_watchdogTimer( new QTimer( this ) ) { m_debounceTimer->setSingleShot( true ); m_debounceTimer->setInterval( debounceMs ); connect( m_debounceTimer, &QTimer::timeout, this, &RiuWorkflowValidationRunner::start ); m_watchdogTimer->setSingleShot( true ); m_watchdogTimer->setInterval( watchdogMs ); connect( m_watchdogTimer, &QTimer::timeout, this, &RiuWorkflowValidationRunner::onTimeout ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiuWorkflowValidationRunner::~RiuWorkflowValidationRunner() { stopProcess(); } //-------------------------------------------------------------------------------------------------- /// Validate the current revision of the workflow after a short pause. A run in progress for an /// older revision is stopped. //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::schedule( RimWorkflow* workflow ) { if ( !workflow || !workflow->isEditable() || workflow->isValidationCurrent() ) { if ( workflow == m_workflow.p() || !workflow ) m_debounceTimer->stop(); return; } m_workflow = workflow; if ( m_process && ( m_runningWorkflow.p() != workflow || m_runningRevision != workflow->revision() ) ) stopProcess(); m_debounceTimer->start(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::cancel() { m_debounceTimer->stop(); stopProcess(); m_workflow = nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RiuWorkflowValidationRunner::isRunning() const { return m_process != nullptr || m_debounceTimer->isActive(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::start() { RimWorkflow* workflow = m_workflow.p(); if ( !workflow || !workflow->isEditable() || workflow->isValidationCurrent() ) return; if ( m_process && m_runningWorkflow.p() == workflow && m_runningRevision == workflow->revision() ) return; stopProcess(); const QString python = RimWorkflow::findPythonExecutable(); auto editDirectory = workflow->editDirectory(); if ( python.isEmpty() || !editDirectory ) { const QString message = python.isEmpty() ? QString( "No usable Python interpreter found" ) : editDirectory.error(); workflow->setValidationResult( workflow->revision(), { issueWithMessage( message ) }, false ); emit validationFinished( workflow ); return; } m_runningWorkflow = workflow; m_runningRevision = workflow->revision(); auto* process = new QProcess( this ); m_process = process; process->setProcessEnvironment( RimWorkflowHelperProcess::environment( workflow->sourceDirectory() ) ); connect( process, &QProcess::finished, this, &RiuWorkflowValidationRunner::onFinished ); connect( process, &QProcess::errorOccurred, this, [this]( QProcess::ProcessError error ) { if ( error == QProcess::FailedToStart ) onFinished(); } ); process->start( python, RimWorkflowHelperProcess::helperArguments( { "save", "--out-dir", editDirectory.value(), "--describe" } ) ); // FailedToStart can be reported from within start(), which already finished the validation if ( m_process != process ) return; process->write( workflow->definitionJsonForSave() ); process->closeWriteChannel(); m_watchdogTimer->start(); emit validationStarted(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::onFinished() { if ( !m_process ) return; m_watchdogTimer->stop(); QProcess* process = m_process; const int exitCode = process->exitStatus() == QProcess::NormalExit && process->error() != QProcess::FailedToStart ? process->exitCode() : -1; const QByteArray standardOutput = process->readAllStandardOutput(); const QByteArray standardError = process->readAllStandardError(); // An invalid definition is reported with the task and field it refers to std::expected output; if ( const auto invalid = RimWorkflowHelperProcess::invalidResult( standardOutput ) ) output = invalid.value(); else output = RimWorkflowHelperProcess::parseOutput( exitCode, standardOutput, standardError ); m_process = nullptr; process->deleteLater(); RimWorkflow* workflow = m_runningWorkflow.p(); const int revision = m_runningRevision; m_runningWorkflow = nullptr; m_runningRevision = -1; if ( !workflow || workflow->revision() != revision ) return; if ( output ) { const auto validation = RimWorkflowValidationTools::issuesFromHelperResult( output.value(), workflow->definition().nodeNames() ); workflow->setValidationResult( revision, validation.issues, validation.describeSucceeded ); } else { RiaLogging::debug( QString( "Workflow validation failed: %1" ).arg( output.error() ).toStdString() ); workflow->setValidationResult( revision, { issueWithMessage( output.error() ) }, false ); } emit validationFinished( workflow ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::onTimeout() { RimWorkflow* workflow = m_runningWorkflow.p(); const int revision = m_runningRevision; stopProcess(); if ( workflow && workflow->revision() == revision ) { workflow->setValidationResult( revision, { issueWithMessage( "Validation timed out" ) }, false ); emit validationFinished( workflow ); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuWorkflowValidationRunner::stopProcess() { m_watchdogTimer->stop(); if ( m_process ) { QProcess* process = m_process; m_process = nullptr; process->disconnect( this ); if ( process->state() == QProcess::NotRunning ) { process->deleteLater(); } else { // Delete when the killed process has exited, without blocking the GUI thread connect( process, &QProcess::finished, process, &QObject::deleteLater ); process->kill(); } } m_runningWorkflow = nullptr; m_runningRevision = -1; }