diff --git a/rosbag2_test_common/CMakeLists.txt b/rosbag2_test_common/CMakeLists.txt
index 91400fc1cb..d5801f6b39 100644
--- a/rosbag2_test_common/CMakeLists.txt
+++ b/rosbag2_test_common/CMakeLists.txt
@@ -42,8 +42,26 @@ install(
DESTINATION include/${PROJECT_NAME})
if(BUILD_TESTING)
+ find_package(ament_cmake_gmock REQUIRED)
find_package(ament_lint_auto REQUIRED)
+ find_package(rcpputils REQUIRED)
ament_lint_auto_find_test_dependencies()
+
+ add_executable(loop_with_ctrl_c_handler test/rosbag2_test_common/loop_with_ctrl_c_handler.cpp)
+ install(
+ TARGETS loop_with_ctrl_c_handler
+ EXPORT export_loop_with_ctrl_c_handler
+ ARCHIVE DESTINATION lib
+ LIBRARY DESTINATION lib
+ RUNTIME DESTINATION bin
+ )
+
+ ament_add_gmock(test_process_execution_helpers
+ test/rosbag2_test_common/test_process_execution_helpers.cpp
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+ if(TARGET test_process_execution_helpers)
+ target_link_libraries(test_process_execution_helpers ${PROJECT_NAME})
+ endif()
endif()
ament_python_install_package(${PROJECT_NAME})
diff --git a/rosbag2_test_common/package.xml b/rosbag2_test_common/package.xml
index 27a1cfaf38..f8ea1e0298 100644
--- a/rosbag2_test_common/package.xml
+++ b/rosbag2_test_common/package.xml
@@ -23,6 +23,7 @@
ament_lint_auto
ament_lint_common
+ rcpputils
ament_cmake
diff --git a/rosbag2_test_common/test/rosbag2_test_common/loop_with_ctrl_c_handler.cpp b/rosbag2_test_common/test/rosbag2_test_common/loop_with_ctrl_c_handler.cpp
new file mode 100644
index 0000000000..c574e9225a
--- /dev/null
+++ b/rosbag2_test_common/test/rosbag2_test_common/loop_with_ctrl_c_handler.cpp
@@ -0,0 +1,78 @@
+// Copyright 2023 Apex.AI, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed 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
+#include
+#include
+
+#ifdef _WIN32
+#include
+int main(void)
+{
+ // Enable default CTRL+C handler first. This is workaround and needed for the cases when
+ // process created with CREATE_NEW_PROCESS_GROUP flag. Without it, installing custom Ctrl+C
+ // handler will not work.
+ if (!SetConsoleCtrlHandler(nullptr, false)) {
+ std::cerr << "Error: Failed to enable default CTL+C handler. \n";
+ }
+
+ static std::atomic_bool running = true;
+ // Installing our own control handler
+ auto CtrlHandler = [](DWORD fdwCtrlType) -> BOOL {
+ switch (fdwCtrlType) {
+ case CTRL_C_EVENT:
+ printf("Ctrl-C event\n");
+ running = false;
+ return TRUE;
+ default:
+ return FALSE;
+ }
+ };
+ if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) {
+ std::cerr << "\nError. Can't install SIGINT handler\n";
+ return EXIT_FAILURE;
+ } else {
+ std::cout << "\nWaiting in a loop for CTRL+C event\n";
+ std::cout.flush();
+ while (running) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
+ }
+ }
+ return EXIT_SUCCESS;
+}
+#else
+#include
+
+int main()
+{
+ auto old_sigint_handler = std::signal(
+ SIGINT, [](int /* signal */) {
+ printf("Ctrl-C event\n");
+ exit(EXIT_SUCCESS);
+ });
+
+ if (old_sigint_handler != SIG_ERR) {
+ std::cout << "\nWaiting in a loop for CTRL+C event\n";
+ std::cout.flush();
+ while (1) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
+ }
+ } else {
+ std::cerr << "\nError. Can't install SIGINT handler\n";
+ return EXIT_FAILURE;
+ }
+ return EXIT_SUCCESS;
+}
+
+#endif
diff --git a/rosbag2_test_common/test/rosbag2_test_common/test_process_execution_helpers.cpp b/rosbag2_test_common/test/rosbag2_test_common/test_process_execution_helpers.cpp
new file mode 100644
index 0000000000..10b91afc42
--- /dev/null
+++ b/rosbag2_test_common/test/rosbag2_test_common/test_process_execution_helpers.cpp
@@ -0,0 +1,44 @@
+// Copyright 2023 Apex.AI, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed 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
+
+#include "rosbag2_test_common/process_execution_helpers.hpp"
+#include "rcpputils/scope_exit.hpp"
+
+class ProcessExecutionHelpersTest : public ::testing::Test
+{
+public:
+ ProcessExecutionHelpersTest() = default;
+};
+
+TEST_F(ProcessExecutionHelpersTest, ctrl_c_event_can_be_send_and_received) {
+ testing::internal::CaptureStdout();
+ auto process_id = start_execution("loop_with_ctrl_c_handler");
+ auto cleanup_process_handle = rcpputils::make_scope_exit(
+ [process_id]() {
+ stop_execution(process_id);
+ });
+
+ // Sleep for 1 second to yield CPU resources to the newly spawned process, to make sure that
+ // signal handlers has been installed.
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+
+ std::string test_output = testing::internal::GetCapturedStdout();
+ EXPECT_THAT(test_output, HasSubstr("Waiting in a loop for CTRL+C event"));
+
+ // Send SIGINT to child process and check exit code
+ stop_execution(process_id, SIGINT);
+ cleanup_process_handle.cancel();
+}