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:
Jørgen Herje authored and GitHub committed 2026-10-02 12:44:33 +02:00
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;
};
+4 -3
View File
@@ -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
View File
@@ -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.