mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
224 lines
7.0 KiB
C++
224 lines
7.0 KiB
C++
/////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 <http://www.gnu.org/licenses/gpl.html>
|
|
// for more details.
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "RimWorkflow.h"
|
|
|
|
#include "RimWorkflowJob.h"
|
|
#include "RimWorkflowTaskInput.h"
|
|
|
|
#include "RiaLogging.h"
|
|
#include "RiaPreferences.h"
|
|
|
|
#include "cafCmdFeatureMenuBuilder.h"
|
|
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QProcess>
|
|
#include <QProcessEnvironment>
|
|
#include <QStandardPaths>
|
|
|
|
CAF_PDM_SOURCE_INIT( RimWorkflow, "Workflow" );
|
|
|
|
RimWorkflow::RimWorkflow()
|
|
{
|
|
CAF_PDM_InitObject( "Workflow", ":/Folder.png" );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_name, "Name", "Name" );
|
|
m_name.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_description, "Description", "Description" );
|
|
m_description.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_workflowDirectory, "WorkflowDirectory", "Directory" );
|
|
m_workflowDirectory.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_loadError, "LoadError", "Load Error" );
|
|
m_loadError.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_jobs, "Jobs", "" );
|
|
}
|
|
|
|
QString RimWorkflow::name() const
|
|
{
|
|
return m_name();
|
|
}
|
|
|
|
void RimWorkflow::setWorkflowDirectory( const QString& directory )
|
|
{
|
|
m_workflowDirectory = directory;
|
|
QString name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
|
|
m_name = name;
|
|
setUiName( name );
|
|
}
|
|
|
|
QString RimWorkflow::workflowDirectory() const
|
|
{
|
|
return m_workflowDirectory().path();
|
|
}
|
|
|
|
QJsonObject RimWorkflow::graph() const
|
|
{
|
|
return m_graph;
|
|
}
|
|
|
|
QString RimWorkflow::loadError() const
|
|
{
|
|
return m_loadError();
|
|
}
|
|
|
|
std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
|
|
{
|
|
std::vector<RimWorkflowJob*> result;
|
|
result.reserve( m_jobs.size() );
|
|
for ( RimWorkflowJob* j : m_jobs.childrenByType() )
|
|
{
|
|
if ( j ) result.push_back( j );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void RimWorkflow::addJob( RimWorkflowJob* job )
|
|
{
|
|
if ( job ) m_jobs.push_back( job );
|
|
}
|
|
|
|
void RimWorkflow::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
|
|
{
|
|
menuBuilder << "RicNewWorkflowJobFeature";
|
|
}
|
|
|
|
bool RimWorkflow::loadFromDirectory( QString* errorMessage )
|
|
{
|
|
m_jobs.deleteChildren();
|
|
m_graph = {};
|
|
m_loadError = "";
|
|
|
|
const QString dir = workflowDirectory();
|
|
if ( dir.isEmpty() ) return false;
|
|
|
|
QString python = RimWorkflow::findPythonExecutable();
|
|
if ( python.isEmpty() )
|
|
{
|
|
m_loadError = "No usable Python interpreter found";
|
|
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
|
|
QStringList args{ "-m", "rips.taskmaestro_helper", "introspect", dir };
|
|
|
|
QProcess proc;
|
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
|
proc.setProcessEnvironment( env );
|
|
proc.start( python, args );
|
|
if ( !proc.waitForStarted( 10000 ) )
|
|
{
|
|
m_loadError = QString( "Could not launch '%1'" ).arg( python );
|
|
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
if ( !proc.waitForFinished( 30000 ) )
|
|
{
|
|
proc.kill();
|
|
m_loadError = "Introspect helper timed out";
|
|
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
|
|
const QByteArray stdoutBytes = proc.readAllStandardOutput();
|
|
if ( proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0 )
|
|
{
|
|
m_loadError = QString::fromUtf8( proc.readAllStandardError() ).trimmed();
|
|
if ( m_loadError().isEmpty() ) m_loadError = "Introspect helper failed";
|
|
RiaLogging::warning( QString( "Workflow '%1' introspect failed: %2" ).arg( dir, m_loadError() ).toStdString() );
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
|
|
QJsonParseError parseErr{};
|
|
const QJsonDocument doc = QJsonDocument::fromJson( stdoutBytes, &parseErr );
|
|
if ( parseErr.error != QJsonParseError::NoError || !doc.isObject() )
|
|
{
|
|
m_loadError = QString( "Invalid JSON from introspect: %1" ).arg( parseErr.errorString() );
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
|
|
const QJsonObject root = doc.object();
|
|
if ( !root.value( "tasks" ).isArray() || !root.value( "edges" ).isArray() )
|
|
{
|
|
m_loadError = "Invalid workflow graph schema";
|
|
if ( errorMessage ) *errorMessage = m_loadError;
|
|
return false;
|
|
}
|
|
m_graph = root;
|
|
if ( root.contains( "name" ) ) m_name = root.value( "name" ).toString();
|
|
if ( root.contains( "description" ) ) m_description = root.value( "description" ).toString();
|
|
setUiName( m_name() );
|
|
|
|
std::vector<RimWorkflowTaskInput*> taskInputs;
|
|
for ( const QJsonValue& tv : root.value( "tasks" ).toArray() )
|
|
{
|
|
const QJsonObject taskObj = tv.toObject();
|
|
if ( taskObj.value( "config_fields" ).toArray().isEmpty() ) continue;
|
|
auto* input = new RimWorkflowTaskInput;
|
|
input->setTaskName( taskObj.value( "name" ).toString() );
|
|
input->buildFromSchema( taskObj.value( "config_fields" ).toArray() );
|
|
taskInputs.push_back( input );
|
|
}
|
|
|
|
auto* job = new RimWorkflowJob;
|
|
job->setJobName( "Default" );
|
|
job->setTaskInputs( taskInputs );
|
|
m_jobs.push_back( job );
|
|
|
|
RiaLogging::info(
|
|
QString( "Loaded workflow '%1' (%2 tasks with config) from %3" ).arg( m_name() ).arg( taskInputs.size() ).arg( dir ).toStdString() );
|
|
|
|
return true;
|
|
}
|
|
|
|
QString RimWorkflow::findPythonExecutable()
|
|
{
|
|
QStringList candidates;
|
|
if ( auto* prefs = RiaPreferences::current() )
|
|
{
|
|
QString configured = prefs->pythonExecutable();
|
|
if ( !configured.isEmpty() && configured != "python" ) candidates << configured;
|
|
}
|
|
candidates << "python3" << "python";
|
|
|
|
for ( const QString& cand : candidates )
|
|
{
|
|
if ( cand.contains( '/' ) || cand.contains( '\\' ) )
|
|
{
|
|
if ( QFileInfo( cand ).isExecutable() ) return cand;
|
|
}
|
|
else if ( !QStandardPaths::findExecutable( cand ).isEmpty() )
|
|
{
|
|
return cand;
|
|
}
|
|
}
|
|
return {};
|
|
}
|