From 967a3a377f9b65d242680c22d80db1faad9f19e5 Mon Sep 17 00:00:00 2001 From: Lane Meier Date: Mon, 4 May 2026 16:14:22 -0700 Subject: [PATCH] implemented reload conifg - only applies to service when restarted. Gives user a mechanism to load confiugrations without restarting testbed. --- catkit2/bindings.cpp | 1 + catkit2/testbed/testbed.py | 72 ++++++++++++++++++++++++++++++++++++ catkit_core/TestbedProxy.cpp | 20 ++++++++++ catkit_core/TestbedProxy.h | 1 + proto/service.proto | 51 +++++++++++++++++++++++++ proto/testbed.proto | 9 +++++ 6 files changed, 154 insertions(+) diff --git a/catkit2/bindings.cpp b/catkit2/bindings.cpp index af2aea18f..8bfb7d067 100644 --- a/catkit2/bindings.cpp +++ b/catkit2/bindings.cpp @@ -684,6 +684,7 @@ PYBIND11_MODULE(catkit_bindings, m) .def("interrupt_service", &TestbedProxy::InterruptService) .def("terminate_service", &TestbedProxy::TerminateService) .def("shut_down", &TestbedProxy::ShutDown) + .def("reload_config", &TestbedProxy::ReloadConfig) .def_property_readonly("message_broker", &TestbedProxy::GetMessageBroker) .def_property_readonly("is_simulated", &TestbedProxy::IsSimulated) .def_property_readonly("is_alive", &TestbedProxy::IsAlive) diff --git a/catkit2/testbed/testbed.py b/catkit2/testbed/testbed.py index a8261726b..644e518cc 100644 --- a/catkit2/testbed/testbed.py +++ b/catkit2/testbed/testbed.py @@ -381,6 +381,7 @@ def __init__(self, port, is_simulated, config): self.server.register_request_handler('get_service_info', self.on_get_service_info) self.server.register_request_handler('register_service', self.on_register_service) self.server.register_request_handler('shut_down', self.on_shut_down) + self.server.register_request_handler('reload_config', self.on_reload_config) self.is_running = False self.shutdown_requested = threading.Event() @@ -668,6 +669,16 @@ def on_register_service(self, data): return reply.SerializeToString() + def on_reload_config(self, data): + request = testbed_proto.ReloadConfigRequest() + request.ParseFromString(data) + + new_config = json.loads(request.config) + self.reload_config(new_config) + + reply = testbed_proto.ReloadConfigReply() + return reply.SerializeToString() + def on_shut_down(self, data): self.shutdown_requested.set() @@ -686,6 +697,67 @@ def register_service_type(self, service_type, path): ''' self.service_type_paths[service_type] = path + def reload_config(self, new_config): + '''Reload the testbed configuration. + + This updates the configuration without restarting the testbed. + Services must be restarted to pick up their new configuration. + + Parameters + ---------- + new_config : dict + The new configuration dictionary. + ''' + self.log.info('Reloading configuration...') + + # Update the config + old_config = self.config + self.config = new_config + + # Update simulation mode if it changed + if 'testbed' in new_config and 'simulated' in new_config['testbed']: + self.is_simulated = new_config['testbed']['simulated'] + + # Check for added/removed services + old_services = set(old_config.get('services', {}).keys()) + new_services = set(new_config.get('services', {}).keys()) + + added = new_services - old_services + removed = old_services - new_services + + if added: + self.log.info(f'New services in config: {list(added)}') + # Add new service references + for service_id in added: + service_info = new_config['services'][service_id] + service_type = service_info['service_type'] + + if self.is_simulated and 'simulated_service_type' in service_info: + service_type = service_info['simulated_service_type'] + + dependencies = service_info.get('depends_on', []) + self.services[service_id] = ServiceReference(service_id, service_type, ServiceState.CLOSED, dependencies, self.message_broker) + + if removed: + self.log.warning(f'Services removed from config: {list(removed)}') + # Check if any removed services are running + for service_id in removed: + if service_id in self.services and self.services[service_id].is_alive: + self.log.warning(f'Service "{service_id}" is running but removed from config. Consider stopping it.') + + # Update startup services list + self.startup_services = [] + if 'safety' in self.config.get('testbed', {}): + self.startup_services.append(self.config['testbed']['safety']['service_id']) + + if 'startup_services' in self.config.get('testbed', {}): + self.startup_services.extend(self.config['testbed']['startup_services']) + + if self.is_simulated and 'simulator' not in self.startup_services: + self.startup_services.append('simulator') + + self.log.info('Configuration reloaded successfully.') + def start_service(self, service_id): '''Start a service. diff --git a/catkit_core/TestbedProxy.cpp b/catkit_core/TestbedProxy.cpp index 68d2a7958..c00fe3ab2 100644 --- a/catkit_core/TestbedProxy.cpp +++ b/catkit_core/TestbedProxy.cpp @@ -197,6 +197,26 @@ void TestbedProxy::ShutDown() } } +void TestbedProxy::ReloadConfig(const json &new_config) +{ + catkit_proto::testbed::ReloadConfigRequest request; + request.set_config(new_config.dump()); + + catkit_proto::testbed::ReloadConfigReply reply; + + try + { + reply.ParseFromString(MakeRequest("reload_config", Serialize(request))); + } + catch (...) + { + throw std::runtime_error("Unable to reload config."); + } + + // Invalidate cached config so it gets refetched on next access + m_HasGottenInfo = false; +} + std::shared_ptr TestbedProxy::GetHeartbeat() { GetTestbedInfo(); diff --git a/catkit_core/TestbedProxy.h b/catkit_core/TestbedProxy.h index cad9fd729..24e0698b1 100644 --- a/catkit_core/TestbedProxy.h +++ b/catkit_core/TestbedProxy.h @@ -49,6 +49,7 @@ class TestbedProxy : public Client, public std::enable_shared_from_this GetHeartbeat(); std::shared_ptr GetMessageBroker(); diff --git a/proto/service.proto b/proto/service.proto index f2ac06cc6..c6e0a051d 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -4,6 +4,57 @@ import "core.proto"; package catkit_proto.service; +message GetInfoRequest +{ +} + +message GetInfoReply +{ + string service_id = 1; + string service_type = 2; + string config = 3; + + repeated string property_names = 4; + repeated string command_names = 5; + map datastream_ids = 6; + + map property_datastream_links = 8; + + string heartbeat_stream_id = 7; +} + +message GetPropertyRequest +{ + string property_name = 1; +} + +message GetPropertyReply +{ + Value property_value = 1; +} + +message SetPropertyRequest +{ + string property_name = 1; + Value property_value = 2; +} + +message SetPropertyReply +{ + Value property_value = 1; +} + +message ExecuteCommandRequest +{ + string command_name = 1; + Dict arguments = 2; +} + +message ExecuteCommandReply +{ + Value result = 1; +} + message ShutDownRequest { } diff --git a/proto/testbed.proto b/proto/testbed.proto index 1be6d2d29..99892cd65 100644 --- a/proto/testbed.proto +++ b/proto/testbed.proto @@ -120,3 +120,12 @@ message ShutDownRequest message ShutDownReply { } + +message ReloadConfigRequest +{ + string config = 1; +} + +message ReloadConfigReply +{ +}