This is an automated email from the ASF dual-hosted git repository.

fgerlits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new a403655df MINIFICPP-2933 Stop MiNiFi C++ if C2 connection is lost for 
some time (#2292)
a403655df is described below

commit a403655df295b83d863292ec0d75532ca7a426af
Author: Gabor Gyimesi <[email protected]>
AuthorDate: Fri Nov 6 14:57:38 2026 +0100

    MINIFICPP-2933 Stop MiNiFi C++ if C2 connection is lost for some time 
(#2292)
---
 C2.md                                              |  10 ++
 .../minifi_behave/containers/container_linux.py    |  13 +++
 .../minifi_behave/containers/container_protocol.py |   3 +
 .../minifi_behave/containers/container_windows.py  |  13 +++
 .../src/minifi_behave/steps/checking_steps.py      |   7 ++
 bin/minifi.service.in                              |   1 +
 conf/minifi.properties.in                          |   2 +
 .../tests/features/minifi_c2_server.feature        |  20 ++++
 .../tests/features/steps/steps.py                  |  25 +++-
 libminifi/include/FlowController.h                 |   4 +-
 libminifi/include/RequestShutdown.h                |  32 ++++++
 libminifi/include/c2/C2Agent.h                     |   8 +-
 libminifi/include/c2/C2ExitCodes.h                 |  25 ++++
 libminifi/src/Configuration.cpp                    |   2 +
 libminifi/src/FlowController.cpp                   |   4 +-
 libminifi/src/c2/C2Agent.cpp                       |  48 +++++++-
 .../test/integration/C2ConnectionTimeoutTest.cpp   |  93 +++++++++++++++
 .../test/libtest/integration/IntegrationBase.cpp   |  10 +-
 .../test/libtest/integration/IntegrationBase.h     |   6 +
 .../test/unit/C2AgentConnectionTimeoutTests.cpp    | 127 +++++++++++++++++++++
 .../include/minifi-cpp/properties/Configuration.h  |   1 +
 minifi_main/MiNiFiMain.cpp                         |  81 +++++++++----
 minifi_main/MiNiFiWindowsService.cpp               |  17 ++-
 23 files changed, 519 insertions(+), 33 deletions(-)

diff --git a/C2.md b/C2.md
index f934cd0e7..1f171fcc4 100644
--- a/C2.md
+++ b/C2.md
@@ -28,6 +28,7 @@ options defined are located in minifi.properties.
     - [Base Options](#base-options)
       - [Flow Id and URL](#flow-id-and-url)
       - [Agent Identifier Fallback](#agent-identifier-fallback)
+      - [C2 Connection Timeout](#c2-connection-timeout)
     - [Metrics](#metrics)
     - [UpdatePolicies](#updatepolicies)
     - [Triggers](#triggers)
@@ -136,6 +137,15 @@ It is possible to set a persistent fallback agent id. This 
is needed so that the
     # in minifi.properties
     nifi.c2.agent.identifier.fallback=my_fallback_id
 
+
+#### C2 Connection Timeout
+
+There is an option to shut down the agent if the connection to the C2 server 
exceeds a specified timeout. This is controlled by the 
`nifi.c2.connection.timeout` property, which defines the maximum time allowed 
without a successful heartbeat response. If the connection is not established 
(or is lost) within this period, the agent will trigger a shutdown with exit 
code 90. If the agent is shut down with exit 90 neither the systemd service nor 
the Windows service will restart it. The featur [...]
+
+    # in minifi.properties
+    # timeout for C2 connection, setting to 0 disables the timeout
+    nifi.c2.connection.timeout=30 min
+
 ### Metrics
 
 Command and Control metrics can be used to send metrics through the heartbeat 
or via the DESCRIBE
diff --git a/behave_framework/src/minifi_behave/containers/container_linux.py 
b/behave_framework/src/minifi_behave/containers/container_linux.py
index 034f53c69..9df8567f6 100644
--- a/behave_framework/src/minifi_behave/containers/container_linux.py
+++ b/behave_framework/src/minifi_behave/containers/container_linux.py
@@ -330,6 +330,19 @@ class LinuxContainer(ContainerProtocol):
         except Exception:
             return False
 
+    @property
+    def exit_code(self) -> int:
+        if not self.container:
+            return -1
+        try:
+            self.container.reload()
+            return self.container.attrs["State"].get("ExitCode", -1)
+        except docker.errors.NotFound:
+            self.container = None
+            return -1
+        except Exception:
+            return -1
+
     def get_number_of_files(self, directory_path: str) -> int:
         if not self.container:
             logger.warning("Container not running")
diff --git 
a/behave_framework/src/minifi_behave/containers/container_protocol.py 
b/behave_framework/src/minifi_behave/containers/container_protocol.py
index cc167c33b..12e832c3b 100644
--- a/behave_framework/src/minifi_behave/containers/container_protocol.py
+++ b/behave_framework/src/minifi_behave/containers/container_protocol.py
@@ -44,6 +44,9 @@ class ContainerProtocol(Protocol):
     @property
     def exited(self) -> bool: ...
 
+    @property
+    def exit_code(self) -> int: ...
+
     def get_number_of_files(self, directory_path: str) -> int: ...
 
     def verify_file_contents(self, directory_path: str, expected_contents: 
list[str]) -> bool: ...
diff --git a/behave_framework/src/minifi_behave/containers/container_windows.py 
b/behave_framework/src/minifi_behave/containers/container_windows.py
index 5117562b7..2e5f3d68c 100644
--- a/behave_framework/src/minifi_behave/containers/container_windows.py
+++ b/behave_framework/src/minifi_behave/containers/container_windows.py
@@ -318,6 +318,19 @@ class WindowsContainer(ContainerProtocol):
         except Exception:
             return False
 
+    @property
+    def exit_code(self) -> int:
+        if not self.container:
+            return -1
+        try:
+            self.container.reload()
+            return self.container.attrs["State"].get("ExitCode", -1)
+        except docker.errors.NotFound:
+            self.container = None
+            return -1
+        except Exception:
+            return -1
+
     def get_number_of_files(self, directory_path: str) -> int:
         if not self.container:
             return -1
diff --git a/behave_framework/src/minifi_behave/steps/checking_steps.py 
b/behave_framework/src/minifi_behave/steps/checking_steps.py
index 83ff073b7..a67f64c4d 100644
--- a/behave_framework/src/minifi_behave/steps/checking_steps.py
+++ b/behave_framework/src/minifi_behave/steps/checking_steps.py
@@ -131,6 +131,13 @@ def verify_minifi_logs_do_not_contain_warnings(context: 
MinifiTestContext):
     )
 
 
+@then("the logs of the exited Minifi container contain the following message: 
'{message}'")
+@then('the logs of the exited Minifi container contain the following message: 
"{message}"')
+def verify_exited_minifi_container_logs_contain_message(context: 
MinifiTestContext, message: str):
+    assert context.containers[DEFAULT_MINIFI_CONTAINER_NAME].exited or 
log_due_to_failure(context)
+    assert message in 
context.containers[DEFAULT_MINIFI_CONTAINER_NAME].get_logs() or 
log_due_to_failure(context)
+
+
 @then("the logs of the '{container}' container contain the following message: 
'{message}' in less than {duration}")
 @then('the logs of the "{container}" container contain the following message: 
"{message}" in less than {duration}')
 def verify_container_logs_contain_message(context: MinifiTestContext, 
container: str, message: str, duration: str):
diff --git a/bin/minifi.service.in b/bin/minifi.service.in
index 2c7f84068..0b8932ccc 100644
--- a/bin/minifi.service.in
+++ b/bin/minifi.service.in
@@ -12,6 +12,7 @@ RestartSec=3
 KillSignal=SIGTERM
 TimeoutStopSec=20
 RestartForceExitStatus=3
+RestartPreventExitStatus=90
 
 [Install]
 WantedBy=multi-user.target
diff --git a/conf/minifi.properties.in b/conf/minifi.properties.in
index 722fc1ca7..8b55d19db 100644
--- a/conf/minifi.properties.in
+++ b/conf/minifi.properties.in
@@ -121,6 +121,8 @@ nifi.c2.full.heartbeat=false
 # 
nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.name=ProcessorMetric
 # 
nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.classes=GetFileMetrics
 
+# nifi.c2.connection.timeout=30 min
+
 ## Specify multiple heartbeat handlers.
 ## HeartbeatLogger logs the heartbeats on TRACE for debugging.
 #nifi.c2.agent.heartbeat.reporter.classes=HeartbeatLogger
diff --git 
a/extensions/standard-processors/tests/features/minifi_c2_server.feature 
b/extensions/standard-processors/tests/features/minifi_c2_server.feature
index c35ddeed9..9daba8f7d 100644
--- a/extensions/standard-processors/tests/features/minifi_c2_server.feature
+++ b/extensions/standard-processors/tests/features/minifi_c2_server.feature
@@ -55,3 +55,23 @@ Feature: MiNiFi can communicate with Apache NiFi MiNiFi C2 
server
     Then the MiNiFi C2 server logs contain the following message: 
"acknowledged with a state of FULLY_APPLIED(DONE)" in less than 30 seconds
     And a single file with the content "test" is placed in the "/tmp/output" 
directory in less than 10 seconds
     And the Minifi logs do not contain the following message: "Failed to parse 
json response: The document is empty. at 0" after 0 seconds
+
+  Scenario: MiNiFi shuts down when the connection to the MiNiFi C2 server is 
lost
+    Given a GenerateFlowFile processor
+    And C2 is enabled in MiNiFi
+    And MiNiFi configuration "nifi.c2.connection.timeout" is set to "10 sec"
+    And a MiNiFi C2 server is started
+    When the MiNiFi instance starts up
+    Then the MiNiFi C2 server logs contain the following message: 
"acknowledged with a state of FULLY_APPLIED(DONE)" in less than 30 seconds
+    And the Minifi logs do not contain the following message: "C2 connection 
timeout exceeded, shutting down..." after 15 seconds
+    When the MiNiFi C2 server is stopped
+    Then MiNiFi exits with exit code 90 in less than 60 seconds
+    And the logs of the exited Minifi container contain the following message: 
"C2 connection timeout exceeded, shutting down..."
+
+  Scenario: MiNiFi shuts down when the connection to the MiNiFi C2 server is 
not available
+    Given a GenerateFlowFile processor
+    And C2 is enabled in MiNiFi
+    And MiNiFi configuration "nifi.c2.connection.timeout" is set to "10 sec"
+    When the MiNiFi instance starts up
+    Then MiNiFi exits with exit code 90 in less than 60 seconds
+    And the logs of the exited Minifi container contain the following message: 
"C2 connection timeout exceeded, shutting down..."
diff --git a/extensions/standard-processors/tests/features/steps/steps.py 
b/extensions/standard-processors/tests/features/steps/steps.py
index b4469fbe9..df4b7940b 100644
--- a/extensions/standard-processors/tests/features/steps/steps.py
+++ b/extensions/standard-processors/tests/features/steps/steps.py
@@ -16,12 +16,15 @@
 #
 
 import humanfriendly
-from behave import given, step, then
+from behave import given, step, then, when
 from containers.diag_slave_container import DiagSlave
 from containers.syslog_container import SyslogContainer
 from containers.tcp_client_container import TcpClientContainer
 from minifi_behave.containers.minifi_protocol import set_up_ssl_properties
-from minifi_behave.core.helpers import wait_for_condition
+from minifi_behave.core.helpers import (
+    log_due_to_failure,
+    wait_for_condition,
+)
 from minifi_behave.core.minifi_test_context import MinifiTestContext
 from minifi_behave.steps import (
     checking_steps,  # noqa: F401
@@ -126,6 +129,24 @@ def start_minifi_c2_server(context: MinifiTestContext):
     assert context.containers["minifi-c2-server"].deploy(context)
 
 
+@when("the MiNiFi C2 server is stopped")
+def stop_minifi_c2_server(context: MinifiTestContext):
+    context.containers["minifi-c2-server"].stop()
+
+
+@then("MiNiFi exits with exit code {exit_code:d} in less than {duration}")
+def verify_minifi_exit_code(context: MinifiTestContext, exit_code: int, 
duration: str):
+    duration_seconds = humanfriendly.parse_timespan(duration)
+    minifi_container = context.get_or_create_default_minifi_container()
+    assert wait_for_condition(
+        condition=lambda: minifi_container.exited,
+        timeout_seconds=duration_seconds,
+        bail_condition=lambda: False,
+        context=context,
+    )
+    assert minifi_container.exit_code == exit_code or 
log_due_to_failure(context)
+
+
 @then('the MiNiFi C2 server logs contain the following message: 
"{log_message}" in less than {duration}')
 def verify_c2_server_logs_contain_message(context: MinifiTestContext, 
log_message: str, duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
diff --git a/libminifi/include/FlowController.h 
b/libminifi/include/FlowController.h
index 00ff3f756..5177e2957 100644
--- a/libminifi/include/FlowController.h
+++ b/libminifi/include/FlowController.h
@@ -46,6 +46,7 @@
 #include "minifi-cpp/core/state/nodes/MetricsBase.h"
 #include "core/state/UpdateController.h"
 #include "c2/C2Agent.h"
+#include "RequestShutdown.h"
 #include "CronDrivenSchedulingAgent.h"
 #include "EventDrivenSchedulingAgent.h"
 #include "properties/Configure.h"
@@ -69,7 +70,8 @@ class FlowController : public 
core::controller::ForwardingControllerServiceProvi
   FlowController(std::shared_ptr<core::Repository> provenance_repo, 
std::shared_ptr<core::Repository> flow_file_repo,
                  std::shared_ptr<Configure> configure, 
std::shared_ptr<core::FlowConfiguration> flow_configuration,
                  std::shared_ptr<core::ContentRepository> content_repo, 
std::unique_ptr<state::MetricsPublisherStore> metrics_publisher_store = nullptr,
-                 std::shared_ptr<utils::file::FileSystem> filesystem = 
std::make_shared<utils::file::FileSystem>(), std::function<void()> 
request_restart = []{},
+                 std::shared_ptr<utils::file::FileSystem> filesystem = 
std::make_shared<utils::file::FileSystem>(),
+                 RequestShutdownCallback request_shutdown = [](ShutdownType, 
std::optional<int32_t>){},
                  utils::file::AssetManager* asset_manager = {}, 
core::BulletinStore* bulletin_store = {});
 
   ~FlowController() override;
diff --git a/libminifi/include/RequestShutdown.h 
b/libminifi/include/RequestShutdown.h
new file mode 100644
index 000000000..7451fb6a0
--- /dev/null
+++ b/libminifi/include/RequestShutdown.h
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <cstdint>
+#include <functional>
+#include <optional>
+
+namespace org::apache::nifi::minifi {
+
+enum class ShutdownType {
+  RESTART,
+  SHUTDOWN
+};
+
+using RequestShutdownCallback = std::function<void(ShutdownType shutdown_type, 
std::optional<int32_t> exit_code)>;
+
+}  // namespace org::apache::nifi::minifi
diff --git a/libminifi/include/c2/C2Agent.h b/libminifi/include/c2/C2Agent.h
index 691d4be57..1267d68cd 100644
--- a/libminifi/include/c2/C2Agent.h
+++ b/libminifi/include/c2/C2Agent.h
@@ -18,6 +18,7 @@
 
 #pragma once
 
+#include <chrono>
 #include <functional>
 #include <future>
 #include <map>
@@ -43,6 +44,7 @@
 #include "utils/file/FileSystem.h"
 #include "C2Utils.h"
 #include "utils/file/AssetManager.h"
+#include "RequestShutdown.h"
 
 namespace org::apache::nifi::minifi::c2 {
 
@@ -62,7 +64,7 @@ class C2Agent : public state::UpdateController {
   C2Agent(std::shared_ptr<Configure> configuration,
           std::weak_ptr<state::response::NodeReporter> node_reporter,
           std::shared_ptr<utils::file::FileSystem> filesystem,
-          std::function<void()> request_restart,
+          minifi::RequestShutdownCallback request_shutdown,
           utils::file::AssetManager* asset_manager);
 
   void initialize(core::controller::ControllerServiceProvider *controller, 
state::Pausable *pause_handler, state::StateMonitor* update_sink);
@@ -235,12 +237,14 @@ class C2Agent : public state::UpdateController {
   const uint64_t C2RESPONSE_POLL_MS = 100;
 
   std::atomic<bool> restart_needed_ = false;
-  std::function<void()> request_restart_;
+  minifi::RequestShutdownCallback request_shutdown_;
 
   // time point the last time we performed a heartbeat.
   std::chrono::steady_clock::time_point last_run_;
 
   utils::file::AssetManager* asset_manager_;
+  std::optional<std::chrono::milliseconds> c2_connection_timeout_;
+  std::chrono::steady_clock::time_point last_successful_heartbeat_{};
 };
 
 }  // namespace org::apache::nifi::minifi::c2
diff --git a/libminifi/include/c2/C2ExitCodes.h 
b/libminifi/include/c2/C2ExitCodes.h
new file mode 100644
index 000000000..0a0c70917
--- /dev/null
+++ b/libminifi/include/c2/C2ExitCodes.h
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <cstdint>
+
+namespace org::apache::nifi::minifi::c2 {
+
+constexpr int32_t C2_CONNECTION_TIMEOUT_EXIT_CODE = 90;
+
+}  // namespace org::apache::nifi::minifi::c2
diff --git a/libminifi/src/Configuration.cpp b/libminifi/src/Configuration.cpp
index 73c9519c2..f236ff814 100644
--- a/libminifi/src/Configuration.cpp
+++ b/libminifi/src/Configuration.cpp
@@ -97,6 +97,8 @@ const std::unordered_map<std::string_view, 
gsl::not_null<const core::PropertyVal
   {Configuration::nifi_c2_rest_request_encoding, 
gsl::make_not_null(&core::StandardPropertyValidators::ALWAYS_VALID_VALIDATOR)},
   {Configuration::nifi_c2_rest_heartbeat_minimize_updates, 
gsl::make_not_null(&core::StandardPropertyValidators::BOOLEAN_VALIDATOR)},
   {Configuration::nifi_c2_flow_info_processor_bulletin_limit, 
gsl::make_not_null(&core::StandardPropertyValidators::UNSIGNED_INTEGER_VALIDATOR)},
+  {Configuration::nifi_c2_asset_download_timeout, 
gsl::make_not_null(&core::StandardPropertyValidators::TIME_PERIOD_VALIDATOR)},
+  {Configuration::nifi_c2_connection_timeout, 
gsl::make_not_null(&core::StandardPropertyValidators::TIME_PERIOD_VALIDATOR)},
   {Configuration::nifi_state_storage_local, 
gsl::make_not_null(&core::StandardPropertyValidators::ALWAYS_VALID_VALIDATOR)},
   {Configuration::nifi_state_storage_local_old, 
gsl::make_not_null(&core::StandardPropertyValidators::ALWAYS_VALID_VALIDATOR)},
   {Configuration::nifi_state_storage_local_class_name, 
gsl::make_not_null(&core::StandardPropertyValidators::ALWAYS_VALID_VALIDATOR)},
diff --git a/libminifi/src/FlowController.cpp b/libminifi/src/FlowController.cpp
index 916299805..e315ae3b5 100644
--- a/libminifi/src/FlowController.cpp
+++ b/libminifi/src/FlowController.cpp
@@ -49,7 +49,7 @@ namespace org::apache::nifi::minifi {
 FlowController::FlowController(std::shared_ptr<core::Repository> 
provenance_repo, std::shared_ptr<core::Repository> flow_file_repo,
                                std::shared_ptr<Configure> configure, 
std::shared_ptr<core::FlowConfiguration> flow_configuration,
                                std::shared_ptr<core::ContentRepository> 
content_repo, std::unique_ptr<state::MetricsPublisherStore> 
metrics_publisher_store,
-                               std::shared_ptr<utils::file::FileSystem> 
filesystem, std::function<void()> request_restart,
+                               std::shared_ptr<utils::file::FileSystem> 
filesystem, RequestShutdownCallback request_shutdown,
                                utils::file::AssetManager* asset_manager, 
core::BulletinStore* bulletin_store)
     : 
core::controller::ForwardingControllerServiceProvider(core::className<FlowController>()),
       running_(false),
@@ -82,7 +82,7 @@ 
FlowController::FlowController(std::shared_ptr<core::Repository> provenance_repo
     if (auto publisher = 
metrics_publisher_store_->getMetricsPublisher(c2::C2_METRICS_PUBLISHER).lock()) 
{
       c2_metrics_publisher = 
std::dynamic_pointer_cast<c2::C2MetricsPublisher>(publisher);
     }
-    c2_agent_ = std::make_unique<c2::C2Agent>(configuration_, 
c2_metrics_publisher, std::move(filesystem), std::move(request_restart), 
asset_manager);
+    c2_agent_ = std::make_unique<c2::C2Agent>(configuration_, 
c2_metrics_publisher, std::move(filesystem), std::move(request_shutdown), 
asset_manager);
   }
 
   if (c2::isControllerSocketEnabled(configuration_)) {
diff --git a/libminifi/src/c2/C2Agent.cpp b/libminifi/src/c2/C2Agent.cpp
index 67ec8b72a..d4b44a12b 100644
--- a/libminifi/src/c2/C2Agent.cpp
+++ b/libminifi/src/c2/C2Agent.cpp
@@ -43,6 +43,7 @@
 #include "minifi-cpp/io/ArchiveStream.h"
 #include "io/StreamPipe.h"
 #include "utils/Id.h"
+#include "c2/C2ExitCodes.h"
 #include "c2/C2Utils.h"
 #include "c2/protocols/RESTSender.h"
 #include "rapidjson/error/en.h"
@@ -52,10 +53,29 @@ using namespace std::literals::chrono_literals;
 
 namespace org::apache::nifi::minifi::c2 {
 
+namespace {
+std::optional<std::chrono::milliseconds> parseTimeoutPeriod(const 
std::shared_ptr<Configure>& configuration, const std::string& config_param,
+    const std::shared_ptr<core::logging::Logger>& logger) {
+  return configuration->get(config_param) | utils::andThen([&](const auto& 
timeout_str) -> std::optional<std::chrono::milliseconds> {
+    auto duration = 
parsing::parseDuration<std::chrono::milliseconds>(timeout_str) | 
utils::toOptional();
+    if (!duration) {
+      logger->log_error("Failed to parse C2 configuration parameter '{}': 
'{}'", config_param, timeout_str);
+    }
+    if (duration && *duration <= 0ms) {
+      if (*duration < 0ms) {
+        logger->log_warn("Negative value for C2 configuration parameter '{}': 
'{}', ignoring it", config_param, timeout_str);
+      }
+      return std::nullopt;
+    }
+    return duration;
+  });
+}
+}  // namespace
+
 C2Agent::C2Agent(std::shared_ptr<Configure> configuration,
                  std::weak_ptr<state::response::NodeReporter> node_reporter,
                  std::shared_ptr<utils::file::FileSystem> filesystem,
-                 std::function<void()> request_restart,
+                 minifi::RequestShutdownCallback request_shutdown,
                  utils::file::AssetManager* asset_manager)
     : heart_beat_period_(3s),
       max_c2_responses(5),
@@ -63,7 +83,7 @@ C2Agent::C2Agent(std::shared_ptr<Configure> configuration,
       node_reporter_(std::move(node_reporter)),
       filesystem_(std::move(filesystem)),
       thread_pool_(2, "C2 threadpool"),
-      request_restart_(std::move(request_restart)),
+      request_shutdown_(std::move(request_shutdown)),
       last_run_(std::chrono::steady_clock::now()),
       asset_manager_(asset_manager) {
   if (!configuration_->getAgentClass()) {
@@ -109,6 +129,7 @@ void C2Agent::start() {
   if (controller_running_) {
     return;
   }
+  last_successful_heartbeat_ = std::chrono::steady_clock::now();
   task_ids_.clear();
   for (const auto& function : functions_) {
     utils::Identifier uuid = utils::IdGenerator::getIdGenerator()->generate();
@@ -179,6 +200,17 @@ void C2Agent::configure(const std::shared_ptr<Configure> 
&configure, bool reconf
   }
   logger_->log_debug("Using {} as the heartbeat period", heart_beat_period_);
 
+  c2_connection_timeout_ = parseTimeoutPeriod(configuration_, 
std::string{Configuration::nifi_c2_connection_timeout}, logger_);
+  if (c2_connection_timeout_ && *c2_connection_timeout_ < 2 * 
heart_beat_period_) {
+    logger_->log_warn(
+        "C2 connection timeout is less than twice the heartbeat period: C2 
connection timeout={}, heartbeat period={}. Adjusting it to be twice the "
+        "heartbeat period",
+        *c2_connection_timeout_,
+        heart_beat_period_);
+    c2_connection_timeout_ = heart_beat_period_ * 2;
+    logger_->log_debug("Using {} as the C2 connection timeout", 
*c2_connection_timeout_);
+  }
+
   std::string heartbeat_reporters;
   if (configure->get(Configuration::nifi_c2_agent_heartbeat_reporter_classes, 
"c2.agent.heartbeat.reporter.classes", heartbeat_reporters)) {
     std::vector<std::string> reporters = 
utils::string::splitAndTrim(heartbeat_reporters, ",");
@@ -234,6 +266,10 @@ void C2Agent::performHeartBeat() {
   }
   C2Payload response = protocol_->consumePayload(payload);
 
+  if (response.getStatus().getState() != state::UpdateState::READ_ERROR) {
+    last_successful_heartbeat_ = std::chrono::steady_clock::now();
+  }
+
   enqueue_c2_server_response(std::move(response));
 
   std::lock_guard<std::mutex> lock(heartbeat_mutex);
@@ -944,7 +980,7 @@ utils::TaskRescheduleInfo C2Agent::produce() {
 
     if (restart_needed_ && requests.empty()) {
       configuration_->commitChanges();
-      request_restart_();
+      request_shutdown_(minifi::ShutdownType::RESTART, std::nullopt);
       return utils::TaskRescheduleInfo::Done();
     }
 
@@ -957,6 +993,12 @@ utils::TaskRescheduleInfo C2Agent::produce() {
     catch (...) {
       logger_->log_error("Unknown exception occurred while performing 
heartbeat, type: {}", getCurrentExceptionTypeName());
     }
+
+    if (c2_connection_timeout_ && controller_running_ && 
std::chrono::steady_clock::now() - last_successful_heartbeat_ > 
*c2_connection_timeout_) {
+      logger_->log_error("C2 connection timeout exceeded, shutting down...");
+      request_shutdown_(minifi::ShutdownType::SHUTDOWN, 
C2_CONNECTION_TIMEOUT_EXIT_CODE);
+      return utils::TaskRescheduleInfo::Done();
+    }
   }
 
   checkTriggers();
diff --git a/libminifi/test/integration/C2ConnectionTimeoutTest.cpp 
b/libminifi/test/integration/C2ConnectionTimeoutTest.cpp
new file mode 100644
index 000000000..2ad6447dc
--- /dev/null
+++ b/libminifi/test/integration/C2ConnectionTimeoutTest.cpp
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <atomic>
+#include <string>
+
+#include "c2/C2Agent.h"
+#include "c2/C2ExitCodes.h"
+#include "integration/HTTPIntegrationBase.h"
+#include "integration/ServerAwareHandler.h"
+#include "properties/Configuration.h"
+#include "unit/Catch.h"
+#include "unit/TestBase.h"
+#include "unit/TestUtils.h"
+
+using namespace std::literals::chrono_literals;
+
+namespace org::apache::nifi::minifi::test {
+
+class FailingHeartbeatHandler : public ServerAwareHandler {
+ public:
+  bool handlePost(CivetServer* /*server*/, struct mg_connection* conn) 
override {
+    ++heartbeat_count_;
+    if (fail_) {
+      mg_printf(conn, "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 
0\r\nConnection: close\r\n\r\n");
+    } else {
+      static const std::string response = R"({"operation": "heartbeat", 
"requested_operations": []})";
+      mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: 
application/json\r\nContent-Length: %lu\r\nConnection: close\r\n\r\n", 
response.length());
+      mg_printf(conn, "%s", response.c_str());
+    }
+    return true;
+  }
+
+  std::atomic<bool> fail_{false};
+  std::atomic<int> heartbeat_count_{0};
+};
+
+class VerifyC2ConnectionTimeout : public VerifyC2Base {
+ public:
+  VerifyC2ConnectionTimeout(const std::filesystem::path& test_file_path, 
FailingHeartbeatHandler& handler)
+    : VerifyC2Base(test_file_path),
+      handler_(handler) {
+  }
+
+  void testSetup() override {
+    LogTestController::getInstance().setDebug<minifi::c2::C2Agent>();
+    VerifyC2Base::testSetup();
+  }
+
+  void configureC2() override {
+    VerifyC2Base::configureC2();
+    configuration->set(minifi::Configuration::nifi_c2_agent_heartbeat_period, 
"100 ms");
+    configuration->set(minifi::Configuration::nifi_c2_connection_timeout, "1 
sec");
+  }
+
+  void runAssertions() override {
+    // successful heartbeats keep the agent alive well past the connection 
timeout
+    std::this_thread::sleep_for(3s);
+    REQUIRE(handler_.heartbeat_count_ > 0);
+    REQUIRE(shutdown_requested_count_ == 0);
+
+    handler_.fail_ = true;
+    REQUIRE(utils::verifyEventHappenedInPollTime(5s, [&] { return 
shutdown_requested_count_ > 0; }));
+    CHECK(shutdown_requested_count_ == 1);
+    CHECK(last_shutdown_exit_code_ == 
minifi::c2::C2_CONNECTION_TIMEOUT_EXIT_CODE);
+    CHECK(utils::verifyLogLinePresenceInPollTime(1s, "C2 connection timeout 
exceeded, shutting down..."));
+  }
+
+ private:
+  FailingHeartbeatHandler& handler_;
+};
+
+TEST_CASE("C2 agent requests shutdown when the C2 server stops responding 
successfully", "[c2test]") {
+  FailingHeartbeatHandler handler;
+  VerifyC2ConnectionTimeout harness(std::filesystem::path(TEST_RESOURCES) / 
"TestEmpty.yml", handler);
+  harness.setUrl("http://localhost:0/heartbeat";, &handler);
+  harness.run();
+}
+
+}  // namespace org::apache::nifi::minifi::test
diff --git a/libminifi/test/libtest/integration/IntegrationBase.cpp 
b/libminifi/test/libtest/integration/IntegrationBase.cpp
index 753acda82..c825ef6d6 100644
--- a/libminifi/test/libtest/integration/IntegrationBase.cpp
+++ b/libminifi/test/libtest/integration/IntegrationBase.cpp
@@ -129,7 +129,13 @@ void IntegrationBase::run() {
     std::shared_ptr<core::ProcessGroup> pg(flow_config->getRoot());
     queryRootProcessGroup(pg);
 
-    const auto request_restart = [&, this] {
+    const auto request_shutdown = [&, this] (ShutdownType shutdown_type, 
std::optional<int32_t> exit_code) {
+      if (shutdown_type == ShutdownType::SHUTDOWN) {
+        last_shutdown_exit_code_ = exit_code.value_or(0);
+        ++shutdown_requested_count_;
+        running = false;
+        return;
+      }
       ++restart_requested_count_;
       running = true;
     };
@@ -138,7 +144,7 @@ void IntegrationBase::run() {
     asset_manager_ = 
std::make_unique<minifi::utils::file::AssetManager>(*configuration);
     auto metrics_publisher_store = 
std::make_unique<minifi::state::MetricsPublisherStore>(configuration, 
repo_metric_sources, flow_config, asset_manager_.get(), bulletin_store_.get());
     flowController_ = std::make_unique<minifi::FlowController>(test_repo, 
test_flow_repo, configuration,
-      std::move(flow_config), content_repo, 
std::move(metrics_publisher_store), filesystem, request_restart, 
asset_manager_.get());
+      std::move(flow_config), content_repo, 
std::move(metrics_publisher_store), filesystem, request_shutdown, 
asset_manager_.get());
     flowController_->load();
     updateProperties(*flowController_);
     flowController_->start();
diff --git a/libminifi/test/libtest/integration/IntegrationBase.h 
b/libminifi/test/libtest/integration/IntegrationBase.h
index 1b68a2317..cb74a62a6 100644
--- a/libminifi/test/libtest/integration/IntegrationBase.h
+++ b/libminifi/test/libtest/integration/IntegrationBase.h
@@ -60,6 +60,8 @@ class IntegrationBase {
         key_dir{std::move(other.key_dir)},
         state_dir{std::move(other.state_dir)},
         restart_requested_count_{other.restart_requested_count_.load()},
+        shutdown_requested_count_{other.shutdown_requested_count_.load()},
+        last_shutdown_exit_code_{other.last_shutdown_exit_code_.load()},
         home_path_{std::move(other.home_path_)},
         flow_config_path_{std::move(other.flow_config_path_)}
   {}
@@ -75,6 +77,8 @@ class IntegrationBase {
     key_dir = std::move(other.key_dir);
     state_dir = std::move(other.state_dir);
     restart_requested_count_ = other.restart_requested_count_.load();
+    shutdown_requested_count_ = other.shutdown_requested_count_.load();
+    last_shutdown_exit_code_ = other.last_shutdown_exit_code_.load();
     home_path_ = std::move(other.home_path_);
     flow_config_path_ = std::move(other.flow_config_path_);
     return *this;
@@ -142,6 +146,8 @@ class IntegrationBase {
   std::filesystem::path key_dir;
   std::filesystem::path state_dir;
   std::atomic<int> restart_requested_count_{0};
+  std::atomic<int> shutdown_requested_count_{0};
+  std::atomic<int32_t> last_shutdown_exit_code_{0};
   std::optional<std::filesystem::path> home_path_;
   FlowConfigPath flow_config_path_;
 };
diff --git a/libminifi/test/unit/C2AgentConnectionTimeoutTests.cpp 
b/libminifi/test/unit/C2AgentConnectionTimeoutTests.cpp
new file mode 100644
index 000000000..9ef49654e
--- /dev/null
+++ b/libminifi/test/unit/C2AgentConnectionTimeoutTests.cpp
@@ -0,0 +1,127 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <atomic>
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "c2/C2Agent.h"
+#include "c2/C2ExitCodes.h"
+#include "properties/Configure.h"
+#include "unit/Catch.h"
+#include "unit/TestBase.h"
+#include "unit/TestUtils.h"
+#include "utils/file/FileSystem.h"
+
+using namespace std::literals::chrono_literals;
+
+namespace org::apache::nifi::minifi::test {
+
+class C2AgentConnectionTimeoutTestFixture {
+ public:
+  C2AgentConnectionTimeoutTestFixture() {
+    LogTestController::getInstance().reset();
+    LogTestController::getInstance().setTrace<c2::C2Agent>();
+    configuration_->set(Configuration::nifi_c2_agent_heartbeat_period, "100 
ms");
+  }
+
+  C2AgentConnectionTimeoutTestFixture(C2AgentConnectionTimeoutTestFixture&&) = 
delete;
+  C2AgentConnectionTimeoutTestFixture(const 
C2AgentConnectionTimeoutTestFixture&) = delete;
+  C2AgentConnectionTimeoutTestFixture& 
operator=(C2AgentConnectionTimeoutTestFixture&&) = delete;
+  C2AgentConnectionTimeoutTestFixture& operator=(const 
C2AgentConnectionTimeoutTestFixture&) = delete;
+
+  ~C2AgentConnectionTimeoutTestFixture() {
+    if (agent_) {
+      agent_->stop();
+    }
+    LogTestController::getInstance().reset();
+  }
+
+  void startAgent() {
+    agent_ = std::make_unique<c2::C2Agent>(configuration_,
+        std::weak_ptr<state::response::NodeReporter>{},
+        std::make_shared<minifi::utils::file::FileSystem>(),
+        [this](ShutdownType shutdown_type, std::optional<int32_t> exit_code) {
+          last_shutdown_type_ = shutdown_type;
+          last_exit_code_ = exit_code.value_or(0);
+          ++shutdown_request_count_;
+        },
+        nullptr);
+    agent_->initialize(nullptr, nullptr, nullptr);
+    agent_->start();
+  }
+
+ protected:
+  std::shared_ptr<Configure> configuration_ = 
std::make_shared<ConfigureImpl>();
+  std::atomic<int> shutdown_request_count_{0};
+  std::atomic<ShutdownType> last_shutdown_type_{ShutdownType::RESTART};
+  std::atomic<int32_t> last_exit_code_{0};
+  std::unique_ptr<c2::C2Agent> agent_;
+};
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "No shutdown is 
requested when the C2 connection timeout is not set", "[c2connectiontimeout]") {
+  startAgent();
+  REQUIRE(LogTestController::getInstance().contains("Performing heartbeat", 
1s));
+  std::this_thread::sleep_for(1s);
+  REQUIRE(shutdown_request_count_ == 0);
+}
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "C2 connection timeout 
of 0 disables the C2 connection timeout", "[c2connectiontimeout]") {
+  configuration_->set(Configuration::nifi_c2_connection_timeout, "0 sec");
+  startAgent();
+  REQUIRE(LogTestController::getInstance().contains("Performing heartbeat", 
1s));
+  std::this_thread::sleep_for(1s);
+  REQUIRE(shutdown_request_count_ == 0);
+  REQUIRE_FALSE(LogTestController::getInstance().contains("Failed to parse C2 
configuration parameter", 0ms));
+}
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "Invalid C2 connection 
timeout disables the C2 connection timeout", "[c2connectiontimeout]") {
+  configuration_->set(Configuration::nifi_c2_connection_timeout, "abc");
+  startAgent();
+  REQUIRE(LogTestController::getInstance().contains("Failed to parse C2 
configuration parameter 'nifi.c2.connection.timeout': 'abc'", 1s));
+  std::this_thread::sleep_for(1s);
+  REQUIRE(shutdown_request_count_ == 0);
+}
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "C2 connection timeout 
shorter than twice the heartbeat period is increased",
+    "[c2connectiontimeout]") {
+  configuration_->set(Configuration::nifi_c2_connection_timeout, "150 ms");
+  startAgent();
+  REQUIRE(LogTestController::getInstance().contains("C2 connection timeout is 
less than twice the heartbeat period", 1s));
+  REQUIRE(LogTestController::getInstance().contains("Using 200ms as the C2 
connection timeout", 1s));
+}
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "Failed heartbeats do 
not prevent the C2 connection timeout", "[c2connectiontimeout]") {
+  configuration_->set(Configuration::nifi_c2_connection_timeout, "300 ms");
+  startAgent();
+  REQUIRE(utils::verifyEventHappenedInPollTime(3s, [&] { return 
shutdown_request_count_ > 0; }, 10ms));
+  REQUIRE(LogTestController::getInstance().contains("Performing heartbeat", 
0ms));
+  std::this_thread::sleep_for(500ms);
+  REQUIRE(shutdown_request_count_ == 1);
+  CHECK(last_shutdown_type_ == ShutdownType::SHUTDOWN);
+  CHECK(last_exit_code_ == c2::C2_CONNECTION_TIMEOUT_EXIT_CODE);
+}
+
+TEST_CASE_METHOD(C2AgentConnectionTimeoutTestFixture, "Stopping the C2 agent 
stops the C2 connection timeout check", "[c2connectiontimeout]") {
+  configuration_->set(Configuration::nifi_c2_connection_timeout, "300 ms");
+  startAgent();
+  agent_->stop();
+  std::this_thread::sleep_for(600ms);
+  REQUIRE(shutdown_request_count_ == 0);
+}
+
+}  // namespace org::apache::nifi::minifi::test
diff --git a/minifi-api/include/minifi-cpp/properties/Configuration.h 
b/minifi-api/include/minifi-cpp/properties/Configuration.h
index 1ec60afa5..9272c2152 100644
--- a/minifi-api/include/minifi-cpp/properties/Configuration.h
+++ b/minifi-api/include/minifi-cpp/properties/Configuration.h
@@ -127,6 +127,7 @@ class Configuration : public virtual Properties {
   static constexpr const char *nifi_c2_rest_request_encoding = 
"nifi.c2.rest.request.encoding";
   static constexpr const char *nifi_c2_flow_info_processor_bulletin_limit = 
"nifi.c2.flow.info.processor.bulletin.limit";
   static constexpr const char *nifi_c2_asset_download_timeout = 
"nifi.c2.asset.download.timeout";
+  static constexpr const char *nifi_c2_connection_timeout = 
"nifi.c2.connection.timeout";
 
   // state management options
   static constexpr const char *nifi_state_storage_local = 
"nifi.state.storage.local";
diff --git a/minifi_main/MiNiFiMain.cpp b/minifi_main/MiNiFiMain.cpp
index fcc2ba9d6..1516a22f8 100644
--- a/minifi_main/MiNiFiMain.cpp
+++ b/minifi_main/MiNiFiMain.cpp
@@ -43,6 +43,7 @@
 #include <algorithm>
 #include <cstdlib>
 #include <iostream>
+#include <mutex>
 #include <vector>
 
 #include "AgentDocs.h"
@@ -51,6 +52,7 @@
 #include "Fips.h"
 #include "FlowController.h"
 #include "MainHelper.h"
+#include "RequestShutdown.h"
 #include "ResourceClaim.h"
 #include "agent/JsonSchema.h"
 #include "minifi-cpp/agent/agent_version.h"
@@ -79,6 +81,45 @@ namespace utils = minifi::utils;
 static std::atomic_flag flow_controller_running;
 static std::atomic_flag process_running;
 
+enum class LifecycleState : int32_t {
+  RUNNING,
+  RESTART_REQUESTED,
+  SHUTDOWN_REQUESTED
+};
+
+struct LifecycleRequest {
+  LifecycleState state = LifecycleState::RUNNING;
+  int32_t exit_code = 0;
+};
+
+// Ensure that LifecycleRequest can be safely used with atomic operations in 
signal handlers
+// Extending LifecycleRequest in the future may break the lock-free guarantee
+static_assert(std::atomic<LifecycleRequest>::is_always_lock_free,
+    "lifecycle_request is modified from signal handlers (see sigHandler), 
where only lock-free atomic operations are safe. "
+    "A non-lock-free std::atomic falls back to hidden mutex or a lock, which 
can deadlock if a signal interrupts a thread holding it.");
+static std::atomic<LifecycleRequest> lifecycle_request;
+
+bool requestLifecycleChange(minifi::ShutdownType shutdown_type, int32_t 
exit_code) {
+  auto current_request = lifecycle_request.load();
+  while (true) {
+    if (current_request.state == LifecycleState::SHUTDOWN_REQUESTED) {
+      return false;
+    }
+    if (shutdown_type == minifi::ShutdownType::RESTART && 
current_request.state == LifecycleState::RESTART_REQUESTED) {
+      return false;
+    }
+    const auto desired = shutdown_type == minifi::ShutdownType::SHUTDOWN
+        ? LifecycleRequest{.state = LifecycleState::SHUTDOWN_REQUESTED, 
.exit_code = exit_code}
+        : LifecycleRequest{.state = LifecycleState::RESTART_REQUESTED, 
.exit_code = 0};
+    if (lifecycle_request.compare_exchange_weak(current_request, desired)) {
+      break;
+    }
+  }
+  flow_controller_running.clear();
+  flow_controller_running.notify_all();
+  return true;
+}
+
 /**
  * Removed the stop command from the signal handler so that we could trigger
  * unload after we exit the semaphore controlled critical section in main.
@@ -93,8 +134,7 @@ static std::atomic_flag process_running;
 BOOL WINAPI consoleSignalHandler(DWORD signal) {
   if (!process_running.test()) { exit(0); return TRUE; }
   if (signal == CTRL_C_EVENT || signal == CTRL_BREAK_EVENT) {
-    flow_controller_running.clear();
-    flow_controller_running.notify_all();
+    requestLifecycleChange(minifi::ShutdownType::SHUTDOWN, 0);
     process_running.wait(true);
     return TRUE;
   }
@@ -102,15 +142,13 @@ BOOL WINAPI consoleSignalHandler(DWORD signal) {
 }
 
 void SignalExitProcess() {
-  flow_controller_running.clear();
-  flow_controller_running.notify_all();
+  requestLifecycleChange(minifi::ShutdownType::SHUTDOWN, 0);
 }
 #endif
 
 void sigHandler(int signal) {
   if (signal == SIGINT || signal == SIGTERM) {
-    flow_controller_running.clear();
-    flow_controller_running.notify_all();
+    requestLifecycleChange(minifi::ShutdownType::SHUTDOWN, 0);
   }
 }
 
@@ -175,6 +213,8 @@ void overridePropertiesFromCommandLine(const 
argparse::ArgumentParser& parser, c
 }
 
 int main(int argc, char **argv) {
+  flow_controller_running.test_and_set();
+
   argparse::ArgumentParser argument_parser("Apache MiNiFi C++", 
minifi::AgentBuild::VERSION);
   argument_parser.add_argument("-p", "--property")
     .append()
@@ -289,19 +329,18 @@ int main(int argc, char **argv) {
 
   process_running.test_and_set();
 
-  std::atomic<bool> restart_token{false};
-  const auto request_restart = [&] {
-    if (!restart_token.exchange(true)) {
-      // only trigger if a restart is not already in progress (the flag was 
unset before the exchange)
-      flow_controller_running.clear();
-      flow_controller_running.notify_all();
+  const auto request_shutdown = [&] (minifi::ShutdownType shutdown_type, 
std::optional<int32_t> shutdown_exit_code) {
+    if (!requestLifecycleChange(shutdown_type, 
shutdown_exit_code.value_or(0))) {
+      return;
+    }
+    if (shutdown_type == minifi::ShutdownType::SHUTDOWN) {
+      logger->log_info("Initiating shutdown...");
+    } else {
       logger->log_info("Initiating restart...");
     }
   };
 
   do {
-    flow_controller_running.test_and_set();
-
     std::string prov_repo_class = "provenancerepository";
     std::string flow_repo_class = "flowfilerepository";
     std::string nifi_configuration_class_name = "adaptiveconfiguration";
@@ -408,7 +447,7 @@ int main(int argc, char **argv) {
     auto metrics_publisher_store = 
std::make_unique<minifi::state::MetricsPublisherStore>(configure, 
repo_metric_sources, flow_configuration, asset_manager.get(), 
bulletin_store.get());
     const auto controller = std::make_unique<minifi::FlowController>(
         prov_repo, flow_repo, configure, std::move(flow_configuration), 
content_repo,
-        std::move(metrics_publisher_store), filesystem, request_restart, 
asset_manager.get(), bulletin_store.get());
+        std::move(metrics_publisher_store), filesystem, request_shutdown, 
asset_manager.get(), bulletin_store.get());
 
     const bool disk_space_watchdog_enable = 
configure->get(minifi::Configure::minifi_disk_space_watchdog_enable)
         | utils::andThen(utils::string::toBool)
@@ -495,15 +534,17 @@ int main(int argc, char **argv) {
     flow_repo = nullptr;
     prov_repo = nullptr;
   } while ([&] {
-    const auto restart_token_temp = restart_token.exchange(false);
-    if (restart_token_temp) {
-      logger->log_info("Restarting MiNiFi");
+    flow_controller_running.test_and_set();
+    auto expected = LifecycleRequest{.state = 
LifecycleState::RESTART_REQUESTED, .exit_code = 0};
+    if (!lifecycle_request.compare_exchange_strong(expected, 
LifecycleRequest{})) {
+      return false;
     }
-    return restart_token_temp;
+    logger->log_info("Restarting MiNiFi");
+    return true;
   }());
 
   process_running.clear();
   process_running.notify_all();
   logger->log_info("MiNiFi exit");
-  return 0;
+  return lifecycle_request.load().exit_code;
 }
diff --git a/minifi_main/MiNiFiWindowsService.cpp 
b/minifi_main/MiNiFiWindowsService.cpp
index dd5e446f3..e3be00894 100644
--- a/minifi_main/MiNiFiWindowsService.cpp
+++ b/minifi_main/MiNiFiWindowsService.cpp
@@ -26,6 +26,7 @@
 #include <tuple>
 
 #include "MainHelper.h"
+#include "c2/C2ExitCodes.h"
 
 namespace minifi = org::apache::nifi::minifi;
 
@@ -176,6 +177,8 @@ void RunAsServiceIfNeeded() {
           return;
         }
 
+        DWORD serviceSpecificExitCode = 0;
+
         do {
           Log()->log_debug("Start exe path {}", filePath);
 
@@ -194,6 +197,11 @@ void RunAsServiceIfNeeded() {
           Log()->log_info("{} started", filePath);
 
           auto res = WaitForSingleObject(processInformation.hProcess, 
INFINITE);
+          DWORD exeExitCode = 0;
+          const bool exeExitCodeRetrieved = 
GetExitCodeProcess(processInformation.hProcess, &exeExitCode);
+          if (!exeExitCodeRetrieved) {
+            Log()->log_error("!GetExitCodeProcess lastError {:#x}", 
GetLastError());
+          }
           CloseHandle(processInformation.hProcess);
           CloseHandle(processInformation.hThread);
 
@@ -203,6 +211,12 @@ void RunAsServiceIfNeeded() {
             Log()->log_error("!WaitForSingleObject hProcess return {}", res);
           }
 
+          if (exeExitCodeRetrieved && exeExitCode == 
static_cast<DWORD>(minifi::c2::C2_CONNECTION_TIMEOUT_EXIT_CODE)) {
+            Log()->log_info("Exe exited with exit code {} due to C2 connection 
timeout, exe won't be restarted", exeExitCode);
+            serviceSpecificExitCode = exeExitCode;
+            break;
+          }
+
           Log()->log_info("Sleep {} sec before restarting exe", 
WAIT_TIME_EXE_RESTART/1000);
           res = WaitForSingleObject(s_hEvent, WAIT_TIME_EXE_RESTART);
 
@@ -220,7 +234,8 @@ void RunAsServiceIfNeeded() {
 
         s_serviceStatus.dwControlsAccepted = 0;
         s_serviceStatus.dwCurrentState = SERVICE_STOPPED;
-        s_serviceStatus.dwWin32ExitCode = 0;
+        s_serviceStatus.dwWin32ExitCode = serviceSpecificExitCode == 0 ? 0 : 
ERROR_SERVICE_SPECIFIC_ERROR;
+        s_serviceStatus.dwServiceSpecificExitCode = serviceSpecificExitCode;
 
         if (!SetServiceStatus(s_statusHandle, &s_serviceStatus)) {
           Log()->log_error("!SetServiceStatus SERVICE_STOPPED lastError 
{:#x}", GetLastError());

Reply via email to