mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-03 04:06:43 -05:00
Dedicated python env for ri-cloud-api (#14831)
- Add ri-cloud-api python env preferences - Prevent loop of restart/reconnect when manually clicking "start"/"Authenticate"
This commit is contained in:
1 parent
3046c7e09d
commit
f015af98ee
11 files changed
+320
-26
No files matched your search
@@ -14,6 +14,7 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesOsdu.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesOpm.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesSumo.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesCloudApi.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaPorosityModel.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaCurveSetDefinition.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaRftPltCurveDefinition.cpp
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "RiaOpenMPTools.h"
|
||||
#include "RiaPlotWindowRedrawScheduler.h"
|
||||
#include "RiaPreferences.h"
|
||||
#include "RiaPreferencesCloudApi.h"
|
||||
#include "RiaPreferencesOsdu.h"
|
||||
#include "RiaPreferencesSumo.h"
|
||||
#include "RiaPreferencesSystem.h"
|
||||
@@ -1382,6 +1383,22 @@ QString RiaApplication::pythonPath() const
|
||||
return m_preferences->pythonExecutable();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The ri-cloud-api service uses a dedicated Python environment when enabled under
|
||||
/// Preferences -> RI Cloud API, and otherwise falls back to the scripting environment so the two do
|
||||
/// not have to share a single interpreter and risk conflicting package versions.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaApplication::cloudApiPythonPath() const
|
||||
{
|
||||
auto cloudApiPreferences = m_preferences->cloudApiPreferences();
|
||||
if ( cloudApiPreferences->useDedicatedPythonEnvironment() )
|
||||
{
|
||||
return cloudApiPreferences->pythonExecutable();
|
||||
}
|
||||
|
||||
return pythonPath();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -1984,7 +2001,7 @@ RiaSumoConnector* RiaApplication::makeSumoConnector()
|
||||
QObject::connect( m_sumoConnector,
|
||||
&RiaSumoConnector::tokenReady,
|
||||
cloudApiService(),
|
||||
[this]( const QString& ) { cloudApiService()->start(); } );
|
||||
[this]( const QString& ) { cloudApiService()->start( RiaCloudApiService::StartTrigger::Automatic ); } );
|
||||
}
|
||||
|
||||
return m_sumoConnector;
|
||||
|
||||
@@ -162,6 +162,7 @@ public:
|
||||
QProcessEnvironment octaveProcessEnvironment() const;
|
||||
|
||||
QString pythonPath() const;
|
||||
QString cloudApiPythonPath() const;
|
||||
virtual QProcessEnvironment pythonProcessEnvironment() const;
|
||||
virtual std::optional<int> activeGrpcPortNumber() const;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "RiaColorTables.h"
|
||||
#include "RiaGuiApplication.h"
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaPreferencesCloudApi.h"
|
||||
#include "RiaPreferencesGeoMech.h"
|
||||
#include "RiaPreferencesGrid.h"
|
||||
#include "RiaPreferencesOpenTelemetry.h"
|
||||
@@ -287,6 +288,9 @@ RiaPreferences::RiaPreferences()
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_openTelemetryPreferences, "openTelemetryPreferences", "openTelemetryPreferences" );
|
||||
m_openTelemetryPreferences = new RiaPreferencesOpenTelemetry;
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_cloudApiPreferences, "cloudApiPreferences", "cloudApiPreferences" );
|
||||
m_cloudApiPreferences = new RiaPreferencesCloudApi;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -482,6 +486,11 @@ void RiaPreferences::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering&
|
||||
scriptGroup->add( &m_maxScriptFoldersDepth );
|
||||
scriptGroup->add( &scriptEditorExecutable );
|
||||
}
|
||||
else if ( uiConfigName == RiaPreferences::tabNameCloudApi() )
|
||||
{
|
||||
caf::PdmUiGroup* pythonGroup = uiOrdering.addNewGroup( "Python Environment" );
|
||||
m_cloudApiPreferences()->appendItems( *pythonGroup );
|
||||
}
|
||||
else if ( uiConfigName == RiaPreferences::tabNameOpmFlow() )
|
||||
{
|
||||
m_opmPreferences()->appendItems( uiOrdering );
|
||||
@@ -647,6 +656,14 @@ QString RiaPreferences::tabNameScripting()
|
||||
return "Scripting";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaPreferences::tabNameCloudApi()
|
||||
{
|
||||
return "RI Cloud API";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -707,6 +724,7 @@ QStringList RiaPreferences::tabNames()
|
||||
names << tabNameSummary();
|
||||
names << tabNamePlotting();
|
||||
names << tabNameScripting();
|
||||
names << tabNameCloudApi();
|
||||
names << tabNameOpmFlow();
|
||||
#ifdef USE_ODB_API
|
||||
names << tabNameGeomech();
|
||||
@@ -1084,6 +1102,14 @@ RiaPreferencesOpenTelemetry* RiaPreferences::openTelemetryPreferences() const
|
||||
return m_openTelemetryPreferences();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RiaPreferencesCloudApi* RiaPreferences::cloudApiPreferences() const
|
||||
{
|
||||
return m_cloudApiPreferences();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -48,6 +48,7 @@ class RiaPreferencesGrid;
|
||||
class RiaPreferencesSumo;
|
||||
class RiaPreferencesOpm;
|
||||
class RiaPreferencesOpenTelemetry;
|
||||
class RiaPreferencesCloudApi;
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
@@ -132,6 +133,7 @@ public:
|
||||
RiaPreferencesGrid* gridPreferences() const;
|
||||
RiaPreferencesOpm* opmPreferences() const;
|
||||
RiaPreferencesOpenTelemetry* openTelemetryPreferences() const;
|
||||
RiaPreferencesCloudApi* cloudApiPreferences() const;
|
||||
|
||||
void importPreferenceValuesFromFile( const QString& fileName );
|
||||
void exportPreferenceValuesToFile( const QString& fileName );
|
||||
@@ -171,6 +173,7 @@ private:
|
||||
static QString tabNameGeomech();
|
||||
static QString tabNamePlotting();
|
||||
static QString tabNameScripting();
|
||||
static QString tabNameCloudApi();
|
||||
static QString tabNameSystem();
|
||||
static QString tabNameImportExport();
|
||||
static QString tabNameOpmFlow();
|
||||
@@ -245,6 +248,9 @@ private:
|
||||
// OpenTelemetry settings
|
||||
caf::PdmChildField<RiaPreferencesOpenTelemetry*> m_openTelemetryPreferences;
|
||||
|
||||
// RI Cloud API settings
|
||||
caf::PdmChildField<RiaPreferencesCloudApi*> m_cloudApiPreferences;
|
||||
|
||||
// 3d view
|
||||
caf::PdmField<caf::AppEnum<RiaDefines::MeshModeType>> m_defaultMeshModeType;
|
||||
caf::PdmField<caf::AppEnum<RiaDefines::RINavigationPolicy>> m_navigationPolicy;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "RiaPreferencesCloudApi.h"
|
||||
|
||||
#include "RiaApplication.h"
|
||||
#include "RiaPreferences.h"
|
||||
|
||||
#include "cafPdmUiCheckBoxEditor.h"
|
||||
#include "cafPdmUiFilePathEditor.h"
|
||||
#include "cafPdmUiOrdering.h"
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RiaPreferencesCloudApi, "RiaPreferencesCloudApi" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RiaPreferencesCloudApi::RiaPreferencesCloudApi()
|
||||
{
|
||||
CAF_PDM_InitField( &m_enableDedicatedPythonEnvironment,
|
||||
"enableDedicatedPythonEnvironment",
|
||||
false,
|
||||
"Enable for Dedicated ri-cloud-api Environment" );
|
||||
caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_enableDedicatedPythonEnvironment );
|
||||
|
||||
CAF_PDM_InitField( &m_pythonExecutable, "pythonExecutable", QString( "python" ), "Python Executable Location" );
|
||||
m_pythonExecutable.uiCapability()->setUiEditorTypeName( caf::PdmUiFilePathEditor::uiEditorTypeName() );
|
||||
m_pythonExecutable.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RiaPreferencesCloudApi* RiaPreferencesCloudApi::current()
|
||||
{
|
||||
return RiaApplication::instance()->preferences()->cloudApiPreferences();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaPreferencesCloudApi::appendItems( caf::PdmUiOrdering& uiOrdering )
|
||||
{
|
||||
uiOrdering.add( &m_enableDedicatedPythonEnvironment );
|
||||
|
||||
// Only show the dedicated interpreter field when it is actually in use. When disabled, the
|
||||
// service falls back to Preferences -> Scripting -> Python Executable Location.
|
||||
if ( m_enableDedicatedPythonEnvironment() )
|
||||
{
|
||||
uiOrdering.add( &m_pythonExecutable );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RiaPreferencesCloudApi::useDedicatedPythonEnvironment() const
|
||||
{
|
||||
return m_enableDedicatedPythonEnvironment();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaPreferencesCloudApi::pythonExecutable() const
|
||||
{
|
||||
return m_pythonExecutable().trimmed();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Preferences for the ri-cloud-api service. By default the service runs with the same Python
|
||||
/// interpreter as the scripting engine (Preferences -> Scripting -> Python Executable Location). A
|
||||
/// dedicated environment can be enabled here to avoid package/version conflicts between the two.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
class RiaPreferencesCloudApi : public caf::PdmObject
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RiaPreferencesCloudApi();
|
||||
|
||||
static RiaPreferencesCloudApi* current();
|
||||
|
||||
void appendItems( caf::PdmUiOrdering& uiOrdering );
|
||||
|
||||
bool useDedicatedPythonEnvironment() const;
|
||||
QString pythonExecutable() const;
|
||||
|
||||
private:
|
||||
caf::PdmField<bool> m_enableDedicatedPythonEnvironment;
|
||||
caf::PdmField<QString> m_pythonExecutable;
|
||||
};
|
||||
@@ -61,6 +61,13 @@ constexpr int readinessPollIntervalMs = 250;
|
||||
|
||||
// Number of ports scanned, starting at the wanted port, when looking for a free port to bind to.
|
||||
constexpr int portRangeLength = 100;
|
||||
|
||||
// Shared hint appended to errors caused by a service that never responded, pointing at the most likely
|
||||
// fix without claiming to have diagnosed the exact cause.
|
||||
const std::string pythonEnvironmentHint =
|
||||
"This could be caused by the configured Python environment (Preferences -> RI Cloud API, or "
|
||||
"Preferences -> Scripting) missing the packages required by ri-cloud-api. Check the log above for "
|
||||
"details, and fix the environment if needed.";
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -74,7 +81,10 @@ RiaCloudApiService::RiaCloudApiService( const QString& serverAddress, int wanted
|
||||
, m_wantedPort( wantedPort )
|
||||
, m_port( -1 )
|
||||
, m_consecutiveFailures( 0 )
|
||||
, m_consecutiveLaunchFailures( 0 )
|
||||
, m_isResponding( false )
|
||||
, m_hasRespondedSinceStart( false )
|
||||
, m_hasGivenUp( false )
|
||||
{
|
||||
// Never route loopback requests through a system/corporate proxy, which can add several seconds
|
||||
// per request.
|
||||
@@ -143,14 +153,31 @@ QString RiaCloudApiService::serverUrl() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaCloudApiService::start()
|
||||
void RiaCloudApiService::start( StartTrigger trigger )
|
||||
{
|
||||
// An Automatic call (e.g. Sumo's tokenReady, which can fire well after the give-up, such as on a
|
||||
// background token refresh) must not silently re-arm a retry loop that already gave up. Only an
|
||||
// explicit Manual start/restart may do that.
|
||||
if ( trigger == StartTrigger::Automatic && m_hasGivenUp ) return;
|
||||
|
||||
if ( isRunning() ) return;
|
||||
|
||||
m_consecutiveLaunchFailures = 0;
|
||||
m_hasRespondedSinceStart = false;
|
||||
m_hasGivenUp = false;
|
||||
|
||||
launchProcess();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaCloudApiService::launchProcess()
|
||||
{
|
||||
// Clean up any stale QProcess instance (e.g. after an unexpected exit) before re-starting.
|
||||
if ( m_process ) stop();
|
||||
|
||||
const QString pythonExecutable = RiaApplication::instance()->pythonPath();
|
||||
const QString pythonExecutable = RiaApplication::instance()->cloudApiPythonPath();
|
||||
if ( pythonExecutable.isEmpty() )
|
||||
{
|
||||
RiaLogging::error( "Cloud API service: no Python executable configured, cannot start service." );
|
||||
@@ -209,7 +236,8 @@ void RiaCloudApiService::start()
|
||||
|
||||
// A non-zero exit means the service never came up, or died. Anything else is an
|
||||
// ordinary shutdown.
|
||||
if ( exitCode != 0 || exitStatus != QProcess::NormalExit )
|
||||
const bool failed = exitCode != 0 || exitStatus != QProcess::NormalExit;
|
||||
if ( failed )
|
||||
{
|
||||
RiaLogging::error( message );
|
||||
}
|
||||
@@ -220,6 +248,15 @@ void RiaCloudApiService::start()
|
||||
|
||||
m_isResponding = false;
|
||||
emit statusChanged();
|
||||
|
||||
// React immediately rather than waiting for a timer. Deferred via singleShot, since
|
||||
// restarting the process here, during its own finished() signal, would not be safe.
|
||||
if ( failed )
|
||||
{
|
||||
m_startupTimer.stop();
|
||||
m_healthTimer.stop();
|
||||
QTimer::singleShot( 0, this, &RiaCloudApiService::autoRestart );
|
||||
}
|
||||
} );
|
||||
|
||||
m_consecutiveFailures = 0;
|
||||
@@ -248,7 +285,7 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs )
|
||||
{
|
||||
if ( m_isResponding ) return true;
|
||||
|
||||
if ( !isRunning() ) start();
|
||||
if ( !isRunning() ) start( StartTrigger::Automatic );
|
||||
|
||||
// Nested event loops below let the periodic health check fire and restart() the service while this
|
||||
// call is still waiting for it to boot. Stop both timers for the duration of the wait to avoid that.
|
||||
@@ -277,7 +314,9 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs )
|
||||
|
||||
if ( answered )
|
||||
{
|
||||
m_consecutiveFailures = 0;
|
||||
m_consecutiveFailures = 0;
|
||||
m_consecutiveLaunchFailures = 0;
|
||||
m_hasRespondedSinceStart = true;
|
||||
|
||||
if ( !m_isResponding )
|
||||
{
|
||||
@@ -305,7 +344,14 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs )
|
||||
delayLoop.exec();
|
||||
}
|
||||
|
||||
RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) );
|
||||
if ( m_hasGivenUp )
|
||||
{
|
||||
RiaLogging::error( std::format( "Cloud API service: not available, giving up. {}", pythonEnvironmentHint ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) );
|
||||
}
|
||||
|
||||
// Resume background health-checking if the process is still running, so it can still recover on
|
||||
// its own without another blocking wait.
|
||||
@@ -351,6 +397,35 @@ void RiaCloudApiService::restart()
|
||||
start();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Called when the process exits/crashes, or when a health check times out. If the service has never
|
||||
/// answered a health check since the current start (see m_hasRespondedSinceStart), this is treated as a
|
||||
/// configuration problem rather than a transient crash: retrying is unlikely to help, so this gives up
|
||||
/// immediately instead of restarting, and sets m_hasGivenUp so a later Automatic start() (e.g. Sumo's
|
||||
/// tokenReady) does not quietly re-arm the loop. If the service HAD been responding, this is instead
|
||||
/// treated as an ordinary crash of an otherwise working server, and is retried no matter how many times it
|
||||
/// takes to come back.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaCloudApiService::autoRestart()
|
||||
{
|
||||
m_consecutiveLaunchFailures++;
|
||||
|
||||
if ( !m_hasRespondedSinceStart )
|
||||
{
|
||||
RiaLogging::error( std::format( "Cloud API service: giving up after {} failed launch attempt{}. {}",
|
||||
m_consecutiveLaunchFailures,
|
||||
m_consecutiveLaunchFailures == 1 ? "" : "s",
|
||||
pythonEnvironmentHint ) );
|
||||
m_hasGivenUp = true;
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
RiaLogging::warning( "Cloud API service: not responding, restarting." );
|
||||
stop();
|
||||
launchProcess();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -370,7 +445,7 @@ void RiaCloudApiService::onHealthCheck()
|
||||
{
|
||||
if ( !isRunning() || m_port < 0 )
|
||||
{
|
||||
restart();
|
||||
autoRestart();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,8 +465,10 @@ void RiaCloudApiService::onHealthCheck()
|
||||
const int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
|
||||
if ( reply->error() == QNetworkReply::NoError && statusCode == 200 )
|
||||
{
|
||||
m_consecutiveFailures = 0;
|
||||
m_isResponding = true;
|
||||
m_consecutiveFailures = 0;
|
||||
m_consecutiveLaunchFailures = 0;
|
||||
m_hasRespondedSinceStart = true;
|
||||
m_isResponding = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -404,7 +481,7 @@ void RiaCloudApiService::onHealthCheck()
|
||||
|
||||
if ( !m_isResponding && m_consecutiveFailures >= maxConsecutiveFailures )
|
||||
{
|
||||
restart();
|
||||
autoRestart();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -48,7 +48,18 @@ public:
|
||||
RiaCloudApiService( const QString& serverAddress, int wantedPort, QObject* parent = nullptr );
|
||||
~RiaCloudApiService() override;
|
||||
|
||||
void start();
|
||||
// Manual: an explicit user action (Start/Restart button). Automatic: triggered by application logic
|
||||
// (e.g. Sumo authentication completing, or waitUntilResponding()).
|
||||
//
|
||||
// Does not affect whether autoRestart() gives up (see autoRestart()), only what happens after it has:
|
||||
// a later Automatic call is then a no-op (see m_hasGivenUp), while a Manual one tries again.
|
||||
enum class StartTrigger
|
||||
{
|
||||
Manual,
|
||||
Automatic
|
||||
};
|
||||
|
||||
void start( StartTrigger trigger = StartTrigger::Manual );
|
||||
void stop();
|
||||
void restart();
|
||||
|
||||
@@ -84,6 +95,15 @@ private slots:
|
||||
void onReadyReadStandardOutput();
|
||||
|
||||
private:
|
||||
// Called after a crash or an unresponsive health check. Gives up immediately if the service has never
|
||||
// responded since this start (see m_hasRespondedSinceStart) -- likely a config problem, not worth
|
||||
// retrying. Otherwise retries indefinitely, treating it as a crash of a previously working server.
|
||||
void autoRestart();
|
||||
|
||||
// Shared process-launch implementation used by start() and by autoRestart()'s retry. Unlike start(),
|
||||
// leaves m_consecutiveLaunchFailures and m_hasRespondedSinceStart untouched.
|
||||
void launchProcess();
|
||||
|
||||
static int findAvailablePortNumber( int firstPort );
|
||||
static QString serviceWorkingDirectory();
|
||||
static QProcessEnvironment buildProcessEnvironment( const QString& workingDirectory );
|
||||
@@ -99,5 +119,14 @@ private:
|
||||
|
||||
int m_port;
|
||||
int m_consecutiveFailures;
|
||||
int m_consecutiveLaunchFailures;
|
||||
bool m_isResponding;
|
||||
|
||||
// True once the current run has answered a health check; autoRestart() then retries indefinitely
|
||||
// instead of giving up on the first failure. Reset to false in start().
|
||||
bool m_hasRespondedSinceStart;
|
||||
|
||||
// True once autoRestart() has given up on a run that never responded. Blocks a later Automatic
|
||||
// start() (e.g. Sumo's tokenReady) from re-arming the loop; a Manual start/restart clears it.
|
||||
bool m_hasGivenUp;
|
||||
};
|
||||
@@ -48,10 +48,11 @@ ThirdParty\vcpkg\bootstrap-vcpkg.bat
|
||||
|
||||
### Cloud Service API
|
||||
|
||||
The local Cloud Service API (`scripts/ri-cloud-api`, a git submodule) runs in the Python
|
||||
The local Cloud Service API (`scripts/ri-cloud-api`, a git submodule) runs, by default, in the Python
|
||||
interpreter configured under *Preferences -> Scripting -> Python Executable Location*, the same one
|
||||
used for `rips`. ResInsight does not install its dependencies, so they have to be installed into
|
||||
that environment up front. See [cloud-service-api.md](../cloud-service-api.md).
|
||||
used for `rips`. A dedicated interpreter can instead be enabled under *Preferences -> RI Cloud API*.
|
||||
ResInsight does not install its dependencies, so they have to be installed into whichever environment
|
||||
is in use, up front. See [cloud-service-api.md](../cloud-service-api.md).
|
||||
|
||||
## Build Commands
|
||||
|
||||
|
||||
+18
-11
@@ -7,11 +7,15 @@ the `scripts/ri-cloud-api` submodule, and ResInsight starts it after you sign in
|
||||
<python> -m uvicorn ri_cloud_api.main:app --host 127.0.0.1 --port <free port>
|
||||
```
|
||||
|
||||
`<python>` is the interpreter from *Preferences -> Scripting -> Python Executable Location*, the same
|
||||
one used for `rips`. ResInsight installs nothing itself, so that environment has to be ready up
|
||||
front. The service code always runs from its source folder -- a development build uses the submodule
|
||||
in place, an installed build a copy of it under `CloudServiceApi` -- so only the dependencies are
|
||||
strictly required in the environment.
|
||||
`<python>` is, by default, the interpreter from *Preferences -> Scripting -> Python Executable
|
||||
Location*, the same one used for `rips`. A dedicated interpreter can be configured instead under
|
||||
*Preferences -> RI Cloud API*: tick *Enable for Dedicated ri-cloud-api Environment* and point
|
||||
*Python Executable Location* in that tab at it -- useful when `rips` and `ri-cloud-api` need
|
||||
different, possibly conflicting, package versions. Leave the checkbox unticked to keep using the
|
||||
scripting environment. Either way, ResInsight installs nothing itself, so the chosen environment has
|
||||
to be ready up front. The service code always runs from its source folder -- a development build
|
||||
uses the submodule in place, an installed build a copy of it under `CloudServiceApi` -- so only the
|
||||
dependencies are strictly required in the environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -25,10 +29,10 @@ strictly required in the environment.
|
||||
## Python Environment
|
||||
|
||||
- **Komodo**: the release already provides an environment with the required packages. Point *Python
|
||||
Executable Location* at it and the service starts on its own; the rest of this document does not
|
||||
apply.
|
||||
- **Custom**: install into the environment you already use for `rips`, or into a dedicated one that
|
||||
*Python Executable Location* points at. The next section covers both.
|
||||
Executable Location* (either tab, see above) at it and the service starts on its own; the rest of
|
||||
this document does not apply.
|
||||
- **Custom**: install into the environment you already use for `rips`, or into a dedicated one
|
||||
configured under *Preferences -> RI Cloud API*. The next section covers both.
|
||||
|
||||
Use [uv](https://docs.astral.sh/uv/getting-started/installation/) and `uv sync` for an environment
|
||||
dedicated to the service, either of the other two recipes when it is shared with `rips`. All three
|
||||
@@ -40,7 +44,9 @@ C:\venvs\venvRiCloudApi\Scripts\activate
|
||||
```
|
||||
|
||||
On Linux, `source ~/venvs/venvRiCloudApi/bin/activate`. Then set *Python Executable Location* to
|
||||
`C:\venvs\venvRiCloudApi\Scripts\python.exe`, or `~/venvs/venvRiCloudApi/bin/python`.
|
||||
`C:\venvs\venvRiCloudApi\Scripts\python.exe`, or `~/venvs/venvRiCloudApi/bin/python` -- under
|
||||
*Preferences -> RI Cloud API* with *Enable for Dedicated ri-cloud-api Environment* ticked for a
|
||||
dedicated environment, or under *Preferences -> Scripting* when it is shared with `rips`.
|
||||
|
||||
Keep the environment out of the repository so `git clean` does not take it, and out of the install
|
||||
tree.
|
||||
@@ -221,7 +227,8 @@ reports no problem even when the service cannot import.
|
||||
|
||||
Service output is forwarded to the ResInsight log, prefixed `Cloud API service:`.
|
||||
|
||||
- `no Python executable configured` -- *Python Executable Location* is empty.
|
||||
- `no Python executable configured` -- *Python Executable Location* is empty, in whichever tab is
|
||||
active (*RI Cloud API* if its dedicated environment is enabled, otherwise *Scripting*).
|
||||
- `'ri_cloud_api' not found in any of: ...` -- submodule missing, or not reachable from *Shared
|
||||
Script Folder(s)*. The message lists every location probed. On an installed build it means
|
||||
`CloudServiceApi` is empty, because the submodule was not checked out when ResInsight was installed.
|
||||
|
||||
Reference in new issue
Block a user