class Application(object): """ Base class used for DXL applications. """ # The name of the DXL client configuration file DXL_CLIENT_CONFIG_FILE = "dxlclient.config" # The timeout used when registering/unregistering the service DXL_SERVICE_REGISTRATION_TIMEOUT = 60 # The name of the "IncomingMessagePool" section within the configuration file INCOMING_MESSAGE_POOL_CONFIG_SECTION = "IncomingMessagePool" # The name of the "MessageCallbackPool" section within the configuration file MESSAGE_CALLBACK_POOL_CONFIG_SECTION = "MessageCallbackPool" # The property used to specify a queue size QUEUE_SIZE_CONFIG_PROP = "queueSize" # The property used to specify a thread count THREAD_COUNT_CONFIG_PROP = "threadCount" # The default thread count for the incoming message pool DEFAULT_THREAD_COUNT = 10 # The default queue size for the incoming message pool DEFAULT_QUEUE_SIZE = 1000 def __init__(self, config_dir, app_config_file_name): """ Constructs the application :param config_dir: The directory containing the application configuration files :param app_config_file_name: The name of the application-specific configuration file """ self._config_dir = config_dir self._dxlclient_config_path = os.path.join(config_dir, self.DXL_CLIENT_CONFIG_FILE) self._app_config_path = os.path.join(config_dir, app_config_file_name) self._epo_by_topic = {} self._dxl_client = None self._running = False self._destroyed = False self._services = [] self._incoming_thread_count = self.DEFAULT_THREAD_COUNT self._incoming_queue_size = self.DEFAULT_QUEUE_SIZE self._callbacks_pool = None self._callbacks_thread_count = self.DEFAULT_THREAD_COUNT self._callbacks_queue_size = self.DEFAULT_QUEUE_SIZE self._config = None self._lock = RLock() def __del__(self): """destructor""" self.destroy() def __enter__(self): """Enter with""" return self def __exit__(self, exc_type, exc_value, traceback): """Exit with""" self.destroy() def _validate_config_files(self): """ Validates the configuration files necessary for the application. An exception is thrown if any of the required files are inaccessible. """ if not os.access(self._dxlclient_config_path, os.R_OK): raise Exception( "Unable to access client configuration file: {0}".format( self._dxlclient_config_path)) if not os.access(self._app_config_path, os.R_OK): raise Exception( "Unable to access application configuration file: {0}".format( self._app_config_path)) def _load_configuration(self): """ Loads the configuration settings from the application-specific configuration file """ config = ConfigParser() self._config = config read_files = config.read(self._app_config_path) if len(read_files) is not 1: raise Exception( "Error attempting to read application configuration file: {0}". format(self._app_config_path)) # # Load incoming pool settings # try: self._incoming_queue_size = config.getint( self.INCOMING_MESSAGE_POOL_CONFIG_SECTION, self.QUEUE_SIZE_CONFIG_PROP) except: pass try: self._incoming_thread_count = config.getint( self.INCOMING_MESSAGE_POOL_CONFIG_SECTION, self.THREAD_COUNT_CONFIG_PROP) except: pass # # Load callback pool settings # try: self._callbacks_queue_size = config.getint( self.MESSAGE_CALLBACK_POOL_CONFIG_SECTION, self.QUEUE_SIZE_CONFIG_PROP) except: pass try: self._callbacks_thread_count = config.getint( self.MESSAGE_CALLBACK_POOL_CONFIG_SECTION, self.THREAD_COUNT_CONFIG_PROP) except: pass self.on_load_configuration(config) def _dxl_connect(self): """ Attempts to connect to the DXL fabric """ # Connect to fabric config = DxlClientConfig.create_dxl_config_from_file( self._dxlclient_config_path) config.incoming_message_thread_pool_size = self._incoming_thread_count config.incoming_message_queue_size = self._incoming_queue_size logger.info( "Incoming message configuration: queueSize={0}, threadCount={1}". format(config.incoming_message_queue_size, config.incoming_message_thread_pool_size)) logger.info( "Message callback configuration: queueSize={0}, threadCount={1}". format(self._callbacks_queue_size, self._callbacks_thread_count)) self._dxl_client = DxlClient(config) logger.info("Attempting to connect to DXL fabric ...") self._dxl_client.connect() logger.info("Connected to DXL fabric.") self.on_register_event_handlers() self.on_register_services() self.on_dxl_connect() def run(self): """ Runs the application """ with self._lock: if self._running: raise Exception("The application is already running") self._running = True logger.info("Running application ...") self.on_run() self._validate_config_files() self._load_configuration() self._dxl_connect() def destroy(self): """ Destroys the application (disconnects from fabric, frees resources, etc.) """ with self._lock: if self._running and not self._destroyed: logger.info("Destroying application ...") if self._callbacks_pool is not None: self._callbacks_pool.shutdown() if self._dxl_client is not None: self._unregister_services() self._dxl_client.destroy() self._dxl_client = None self._destroyed = True def _get_path(self, in_path): """ Returns an absolute path for a file specified in the configuration file (supports files relative to the configuration file). :param in_path: The specified path :return: An absolute path for a file specified in the configuration file """ if not os.path.isfile(in_path) and not os.path.isabs(in_path): config_rel_path = os.path.join(self._config_dir, in_path) if os.path.isfile(config_rel_path): in_path = config_rel_path return in_path def _unregister_services(self): """ Unregisters the services associated with the Application from the fabric """ for service in self._services: self._dxl_client.unregister_service_sync( service, self.DXL_SERVICE_REGISTRATION_TIMEOUT) def _get_callbacks_pool(self): """ Returns the thread pool used to invoke application-specific message callbacks :return: The thread pool used to invoke application-specific message callbacks """ with self._lock: if self._callbacks_pool is None: self._callbacks_pool = ThreadPool(self._callbacks_queue_size, self._callbacks_thread_count, "CallbacksPool") return self._callbacks_pool def add_event_callback(self, topic, callback, separate_thread): """ Adds a DXL event message callback to the application. :param topic: The topic to associate with the callback :param callback: The event callback :param separate_thread: Whether to invoke the callback on a thread other than the incoming message thread (this is necessary if synchronous requests are made via DXL in this callback). """ if separate_thread: callback = _ThreadedEventCallback(self._get_callbacks_pool(), callback) self._dxl_client.add_event_callback(topic, callback) def add_request_callback(self, service, topic, callback, separate_thread): """ Adds a DXL request message callback to the application. :param service: The service to associate the request callback with :param topic: The topic to associate with the callback :param callback: The request callback :param separate_thread: Whether to invoke the callback on a thread other than the incoming message thread (this is necessary if synchronous requests are made via DXL in this callback). """ if separate_thread: callback = _ThreadedRequestCallback(self._get_callbacks_pool(), callback) service.add_topic(topic, callback) def register_service(self, service): """ Registers the specified service with the fabric :param service: The service to register with the fabric """ self._dxl_client.register_service_sync( service, self.DXL_SERVICE_REGISTRATION_TIMEOUT) self._services.append(service) def on_run(self): """ Invoked when the application has started running. """ pass def on_load_configuration(self, config): """ Invoked after the application-specific configuration has been loaded :param config: The application-specific configuration """ pass def on_dxl_connect(self): """ Invoked after the client associated with the application has connected to the DXL fabric. """ pass def on_register_event_handlers(self): """ Invoked when event handlers should be registered with the application """ pass def on_register_services(self): """ Invoked when services should be registered with the application """ pass
class MonitorModule(Module): # Request topic for service registry queries SERVICE_REGISTRY_QUERY_TOPIC = '/mcafee/service/dxl/svcregistry/query' # Event topics for service registry changes SERVICE_REGISTRY_REGISTER_EVENT_TOPIC = '/mcafee/event/dxl/svcregistry/register' SERVICE_REGISTRY_UNREGISTER_EVENT_TOPIC = '/mcafee/event/dxl/svcregistry/unregister' # How often(in seconds) to refresh the service list SERVICE_UPDATE_INTERVAL = 60 # How long to retain clients without any keep alive before evicting them CLIENT_RETENTION_MINUTES = 30 # A default SmartClient JSON response to show no results NO_RESULT_JSON = u"""{response:{status:0,startRow:0,endRow:0,totalRows:0,data:[]}}""" # Locks for the different dictionaries shared between Monitor Handlers _service_dict_lock = threading.Lock() _client_dict_lock = threading.Lock() _web_socket_dict_lock = threading.Lock() _pending_messages_lock = threading.Lock() def __init__(self, app): super(MonitorModule, self).__init__(app, "monitor", "Fabric Monitor", "/public/images/monitor.png", "monitor_layout") # dictionary to store DXL Client instances unique to each "session" self._client_dict = {} # dictionary to store web sockets for each "session" self._web_socket_dict = {} # dictionary to store incoming messages for each "session" self._pending_messages = {} # dictionary to cache service state self._services = {} self._message_id_topics = {} self._client_config = DxlClientConfig.create_dxl_config_from_file( self.app.bootstrap_app.client_config_path) # DXL Client to perform operations that are the same for all users(svc registry queries) self._dxl_service_client = DxlClient(self._client_config) self._dxl_service_client.connect() self._dxl_service_client.add_event_callback( MonitorModule.SERVICE_REGISTRY_REGISTER_EVENT_TOPIC, _ServiceEventCallback(self)) self._dxl_service_client.add_event_callback( MonitorModule.SERVICE_REGISTRY_UNREGISTER_EVENT_TOPIC, _ServiceEventCallback(self)) self._refresh_all_services() self._service_updater_thread = threading.Thread( target=self._service_updater) self._service_updater_thread.daemon = True self._service_updater_thread.start() self._dxl_client_cleanup_thread = threading.Thread( target=self._cleanup_dxl_clients) self._dxl_client_cleanup_thread.daemon = True self._dxl_client_cleanup_thread.start() @property def handlers(self): return [(r'/update_services', ServiceUpdateHandler, dict(module=self)), (r'/subscriptions', SubscriptionsHandler, dict(module=self)), (r'/messages', MessagesHandler, dict(module=self)), (r'/send_message', SendMessageHandler, dict(module=self)), (r'/websocket', ConsoleWebSocketHandler, dict(module=self))] @property def content(self): content = pkg_resources.resource_string(__name__, "content.html").decode("utf8") return content.replace("@PORT@", str(self.app.bootstrap_app.port)) @property def services(self): with self._service_dict_lock: return self._services.copy() @property def message_id_topics(self): return self._message_id_topics @property def client_config(self): return DxlClientConfig.create_dxl_config_from_file( self.app.bootstrap_app.client_config_path) def get_dxl_client(self, client_id): """ Retrieves the DxlClient for the given request. If there is not one associated with the incoming request it creates a new one and saves the generated client_id as a cookie :param client_id: The client identifier :return: the DxlClient specific to this "session" """ if not self._client_exists_for_connection(client_id): self._create_client_for_connection(client_id) with self._client_dict_lock: client = self._client_dict[client_id][0] if not client.connected: client.connect() logger.debug("Returning DXL client for id: %s", client_id) return client def _create_client_for_connection(self, client_id): """ Creates a DxlClient and stores it for the give client_id :param client_id: the client_id for the DxlClient """ client = DxlClient(self.client_config) client.connect() logger.info("Initializing new dxl client for client_id: %s", client_id) with self._client_dict_lock: self._client_dict[client_id] = (client, datetime.datetime.now()) def _client_exists_for_connection(self, client_id): """ Checks if there is already an existing DxlClient for the given client_id. :param client_id: the ID of the DxlClient to check for :return: whether there is an existing DxlClient for this ID """ with self._client_dict_lock: return client_id in self._client_dict @staticmethod def create_smartclient_response_wrapper(): """ Creates a wrapper object containing the standard fields required by SmartClient responses :return: an initial SmartClient response wrapper """ response_wrapper = {"response": {}} response = response_wrapper["response"] response["status"] = 0 response["startRow"] = 0 response["endRow"] = 0 response["totalRows"] = 0 response["data"] = [] return response_wrapper def create_smartclient_error_response(self, error_message): """ Creates an error response for the SmartClient UI with the given message :param error_message: The error message :return: The SmartClient response in dict form """ response_wrapper = self.create_smartclient_response_wrapper() response = response_wrapper["response"] response["status"] = -1 response["data"] = error_message return response def queue_message(self, message, client_id): """ Adds the given message to the pending messages queue for the give client. :param message: the message to enqueue :param client_id: the client the message is intended for """ with self._pending_messages_lock: if client_id not in self._pending_messages: self._pending_messages[client_id] = [] self._pending_messages[client_id].append(message) def get_messages(self, client_id): """ Retrieves the messages pending for the given client. This does not clear the queue after retrieving. :param client_id: the client to retrieve messages for :return: a List of messages for the client """ with self._pending_messages_lock: if client_id in self._pending_messages: return self._pending_messages[client_id] return None def clear_messages(self, client_id): """ Clears the pending messages for the given client. :param client_id: the client to clear messages for """ with self._pending_messages_lock: self._pending_messages[client_id] = [] def _service_updater(self): """ A thread target that will run forever and do a complete refresh of the service list on an interval or if the DXL client reconnects """ while True: with self._dxl_service_client._connected_lock: self._dxl_service_client._connected_wait_condition.wait( self.SERVICE_UPDATE_INTERVAL) if self._dxl_service_client.connected: logger.debug("Refreshing service list.") self._refresh_all_services() def _cleanup_dxl_clients(self): """ A thread target that will run forever and evict DXL clients if their clients have not sent a keep-alive """ logger.debug("DXL client cleanup thread initialized.") while True: with self._client_dict_lock: for key in list(self._client_dict): if self._client_dict[key][1] < \ (datetime.datetime.now() - datetime.timedelta( minutes=self.CLIENT_RETENTION_MINUTES)): logger.debug("Evicting DXL client for client_id: %s", key) del self._client_dict[key] time.sleep(5) def _refresh_all_services(self): """ Queries the broker for the service list and replaces the currently stored one with the new results. Notifies all connected web sockets that new services are available. """ req = Request(MonitorModule.SERVICE_REGISTRY_QUERY_TOPIC) req.payload = "{}" # Send the request dxl_response = self._dxl_service_client.sync_request(req, 5) dxl_response_dict = MessageUtils.json_payload_to_dict(dxl_response) logger.info("Service registry response: %s", dxl_response_dict) with self._service_dict_lock: self._services = {} for service_guid in dxl_response_dict["services"]: self._services[service_guid] = dxl_response_dict["services"][ service_guid] self.notify_web_sockets() def update_service(self, service_event): """ Replaces a stored service data withe the one from the provided DXL service event :param service_event: the DXL service event containing the service """ with self._service_dict_lock: self._services[service_event['serviceGuid']] = service_event def remove_service(self, service_event): """ Removes a stored service using the provided DXL service event :param service_event: the DXL service event containing the service to be removed """ with self._service_dict_lock: if service_event['serviceGuid'] in self._services: del self._services[service_event['serviceGuid']] def client_keep_alive(self, client_id): logger.debug("Client keep-alive received for client id: %s", client_id) if self._client_exists_for_connection(client_id): with self._client_dict_lock: self._client_dict[client_id] = ( self._client_dict[client_id][0], datetime.datetime.now()) @property def io_loop(self): """ Returns the Tornado IOLoop that the web console uses :return: The Tornado IOLoop instance """ return self.app.io_loop def add_web_socket(self, client_id, web_socket): """ Stores a web socket associated with the given client id :param client_id: the client id key the web socket to :param web_socket: the web socket to store """ logger.debug("Adding web socket for client: %s", client_id) with self._web_socket_dict_lock: self._web_socket_dict[client_id] = web_socket def remove_web_socket(self, client_id): """ Removes any web socket associated with the given client_id :param client_id: The client ID """ logger.debug("Removing web socket for client: %s", client_id) with self._web_socket_dict_lock: self._web_socket_dict.pop(client_id, None) def notify_web_sockets(self): """ Notifies all web sockets that there are pending service updates """ with self._web_socket_dict_lock: for key in self._web_socket_dict: try: self.io_loop.add_callback( self._web_socket_dict[key].write_message, u"serviceUpdates") except Exception: pass def get_message_topic(self, message): """ Determines the topic for the provided message. Replaces the response channel in responses with the topic of the original request :param message: The DXL message :return: The topic to use """ if (message.message_type == Message.MESSAGE_TYPE_RESPONSE or message.message_type == Message.MESSAGE_TYPE_ERROR) and \ message.request_message_id in self.message_id_topics: topic = self.message_id_topics[message.request_message_id] del self.message_id_topics[message.request_message_id] else: topic = message.destination_topic return topic
class DXLBroker(object): class MyEventCallback(EventCallback): def __init__(self, broker): EventCallback.__init__(self) self.broker = broker def on_event(self, event): self.broker.logger.threaddebug( f"{self.broker.device.name}: Message {event.message_id} ({event.message_type}), received: {event.destination_topic}, payload: {event.payload}" ) indigo.activePlugin.processReceivedMessage(self.broker.device.id, event.destination_topic, event.payload) def __init__(self, device): self.logger = logging.getLogger("Plugin.DXLBroker") self.deviceID = device.id address = device.pluginProps.get(u'address', "") port = device.pluginProps.get(u'port', "") ca_bundle = indigo.server.getInstallFolderPath( ) + '/' + device.pluginProps.get(u'ca_bundle', "") cert_file = indigo.server.getInstallFolderPath( ) + '/' + device.pluginProps.get(u'cert_file', "") private_key = indigo.server.getInstallFolderPath( ) + '/' + device.pluginProps.get(u'private_key', "") self.logger.debug( f"{device.name}: Broker __init__ address = {address}, ca_bundle = {ca_bundle}, cert_file = {cert_file}, private_key = {private_key}" ) device.updateStateOnServer(key="status", value="Not Connected") device.updateStateImageOnServer(indigo.kStateImageSel.SensorOff) # Create the client configuration broker = Broker.parse(f"ssl://{address}:{port}") config = DxlClientConfig(broker_ca_bundle=ca_bundle, cert_file=cert_file, private_key=private_key, brokers=[broker]) # Create the DXL client self.dxl_client = DxlClient(config) # Connect to the fabric self.dxl_client.connect() device.updateStateOnServer(key="status", value="Connected") device.updateStateImageOnServer(indigo.kStateImageSel.SensorOn) subs = device.pluginProps.get(u'subscriptions', None) if subs: for topic in subs: self.dxl_client.add_event_callback(topic, self.MyEventCallback(self)) self.logger.info(u"{}: Subscribing to: {}".format( device.name, topic)) def disconnect(self): device = indigo.devices[self.deviceID] self.dxl_client.disconnect() self.dxl_client.destroy() device.updateStateOnServer(key="status", value="Not Connected") device.updateStateImageOnServer(indigo.kStateImageSel.SensorOff) def publish(self, topic, payload=None, qos=0, retain=False): event = Event(topic) event.payload = payload self.dxl_client.send_event(event) def subscribe(self, topic): device = indigo.devices[self.deviceID] self.logger.info(f"{device.name}: Subscribing to: {topic}") self.dxl_client.add_event_callback(topic, self.MyEventCallback(self)) def unsubscribe(self, topic): device = indigo.devices[self.deviceID] self.logger.info(f"{device.name}: Unsubscribing from: {topic}") self.dxl_client.unsubscribe(topic)
class DxlComponentSubscriber(ResilientComponent): """Component that Subscribes to DXL Topics and maps data back into the Resilient Platform""" def __init__(self, opts): super(DxlComponentSubscriber, self).__init__(opts) self.config = verify_config(opts) add_methods_to_global() # Create and connect DXL client config_client_file = self.config.get("config_client") dxl_config = DxlClientConfig.create_dxl_config_from_file( config_client_file) self.client = DxlClient(dxl_config) self.client.connect() # This gets run once to tell the subscriber to listen on defined topics self.main() def main(self): if self.config["topic_listener_on"].lower() == "true": log.info("Service Listener called") self.event_subscriber(self.config) else: log.info( "Event subscriber not listening. To turn on set topic_listener_on to True" ) def event_subscriber(self, config): try: topic_template_dict = get_topic_template_dict( config.get("custom_template_dir")) class ResilientEventSubscriber(EventCallback): def __init__(self, template): super(ResilientEventSubscriber, self).__init__() self.temp = template def on_event(self, event): message = event.payload.decode(encoding="UTF-8") log.info("Event received payload: " + message) message_dict = json.loads(message) # Map values from topic to incident template to create new incident inc_data = map_values(self.temp, message_dict) # Create new Incident in Resilient response = create_incident( get_connected_resilient_client(config), inc_data) log.info("Created incident {}".format( str(response.get("id")))) # Python 2.x solution try: for event_topic, template in topic_template_dict.iteritems(): self.client.add_event_callback( event_topic.encode('ascii', 'ignore'), ResilientEventSubscriber( template.encode('ascii', 'ignore'))) log.info( "Resilient DXL Subscriber listening on {} ...".format( event_topic)) # Python 3.6 solution except AttributeError: for event_topic, template in topic_template_dict.items(): # Ensure unicode and bytes are converted to String self.client.add_event_callback( event_topic.encode('ascii', 'ignore').decode("utf-8"), ResilientEventSubscriber( template.encode('ascii', 'ignore').decode("utf-8"))) log.info( "Resilient DXL Subscriber listening on {} ...".format( event_topic)) except Exception as e: log.error(e) self.client.destroy()
class Application(object): """ Base class used for DXL applications. """ # The name of the DXL client configuration file DXL_CLIENT_CONFIG_FILE = "dxlclient.config" # The location of the logging configuration file (optional) LOGGING_CONFIG_FILE = "logging.config" # The timeout used when registering/unregistering the service DXL_SERVICE_REGISTRATION_TIMEOUT = 60 # The name of the "IncomingMessagePool" section within the configuration file INCOMING_MESSAGE_POOL_CONFIG_SECTION = "IncomingMessagePool" # The name of the "MessageCallbackPool" section within the configuration file MESSAGE_CALLBACK_POOL_CONFIG_SECTION = "MessageCallbackPool" # The property used to specify a queue size QUEUE_SIZE_CONFIG_PROP = "queueSize" # The property used to specify a thread count THREAD_COUNT_CONFIG_PROP = "threadCount" # The default thread count for the incoming message pool DEFAULT_THREAD_COUNT = 10 # The default queue size for the incoming message pool DEFAULT_QUEUE_SIZE = 1000 # The directory containing the configuration files (in the Python library) LIB_CONFIG_DIR = "_config" # The directory containing the application configuration files (in the Python library) LIB_APP_CONFIG_DIR = LIB_CONFIG_DIR + "/app" def __init__(self, config_dir, app_config_file_name): """ Constructs the application :param config_dir: The directory containing the application configuration files :param app_config_file_name: The name of the application-specific configuration file """ self._config_dir = config_dir self._dxlclient_config_path = os.path.join(config_dir, self.DXL_CLIENT_CONFIG_FILE) self._app_config_path = os.path.join(config_dir, app_config_file_name) self._epo_by_topic = {} self._dxl_client = None self._running = False self._destroyed = False self._services = [] self._incoming_thread_count = self.DEFAULT_THREAD_COUNT self._incoming_queue_size = self.DEFAULT_QUEUE_SIZE self._callbacks_pool = None self._callbacks_thread_count = self.DEFAULT_THREAD_COUNT self._callbacks_queue_size = self.DEFAULT_QUEUE_SIZE self._config = None self._lock = RLock() def __del__(self): """destructor""" self.destroy() def __enter__(self): """Enter with""" return self def __exit__(self, exc_type, exc_value, traceback): """Exit with""" self.destroy() def _validate_config_files(self): """ Validates the configuration files necessary for the application. An exception is thrown if any of the required files are inaccessible. """ # Determine the module of the derived class mod = self.__class__.__module__ # If the configuration directory exists in the library, create config files as necessary # This check also provides backwards compatibility for projects that don't have the # configuration files in the library. if pkg_resources.resource_exists(mod, self.LIB_CONFIG_DIR): # Create configuration directory if not found if not os.access(self._config_dir, os.R_OK): logger.info( "Configuration directory '%s' not found, creating...", self._config_dir) os.makedirs(self._config_dir) # Count of current configuration files config_files_count = len([ name for name in os.listdir(self._config_dir) if os.path.isfile(os.path.join(self._config_dir, name)) ]) # Create configuration files if not found files = pkg_resources.resource_listdir(mod, self.LIB_APP_CONFIG_DIR) for file_name in files: config_path = os.path.join(self._config_dir, file_name) if not os.access(config_path, os.R_OK): resource_filename = pkg_resources.resource_filename( mod, self.LIB_APP_CONFIG_DIR + "/" + file_name) f_lower = file_name.lower() # Copy configuration file. Only copy logging file if the # directory was empty if not os.path.isdir(resource_filename) and \ not(f_lower.endswith(".py")) and \ not(f_lower.endswith(".pyc")) and \ (f_lower != Application.LOGGING_CONFIG_FILE or config_files_count == 0): logger.info( "Configuration file '%s' not found, creating...", file_name) shutil.copyfile( pkg_resources.resource_filename( mod, self.LIB_APP_CONFIG_DIR + "/" + file_name), config_path) if not os.access(self._dxlclient_config_path, os.R_OK): raise Exception( "Unable to access client configuration file: {0}".format( self._dxlclient_config_path)) if not os.access(self._app_config_path, os.R_OK): raise Exception( "Unable to access application configuration file: {0}".format( self._app_config_path)) def _load_configuration(self): """ Loads the configuration settings from the application-specific configuration file """ config = ConfigParser() self._config = config read_files = config.read(self._app_config_path) if len(read_files) is not 1: raise Exception( "Error attempting to read application configuration file: {0}". format(self._app_config_path)) # # Load incoming pool settings # # pylint: disable=bare-except try: self._incoming_queue_size = config.getint( self.INCOMING_MESSAGE_POOL_CONFIG_SECTION, self.QUEUE_SIZE_CONFIG_PROP) except: pass try: self._incoming_thread_count = config.getint( self.INCOMING_MESSAGE_POOL_CONFIG_SECTION, self.THREAD_COUNT_CONFIG_PROP) except: pass # # Load callback pool settings # try: self._callbacks_queue_size = config.getint( self.MESSAGE_CALLBACK_POOL_CONFIG_SECTION, self.QUEUE_SIZE_CONFIG_PROP) except: pass try: self._callbacks_thread_count = config.getint( self.MESSAGE_CALLBACK_POOL_CONFIG_SECTION, self.THREAD_COUNT_CONFIG_PROP) except: pass self.on_load_configuration(config) def _dxl_connect(self): """ Attempts to connect to the DXL fabric """ # Connect to fabric config = DxlClientConfig.create_dxl_config_from_file( self._dxlclient_config_path) config.incoming_message_thread_pool_size = self._incoming_thread_count config.incoming_message_queue_size = self._incoming_queue_size logger.info( "Incoming message configuration: queueSize=%d, threadCount=%d", config.incoming_message_queue_size, config.incoming_message_thread_pool_size) logger.info( "Message callback configuration: queueSize=%d, threadCount=%d", self._callbacks_queue_size, self._callbacks_thread_count) self._dxl_client = DxlClient(config) logger.info("Attempting to connect to DXL fabric ...") self._dxl_client.connect() logger.info("Connected to DXL fabric.") self.on_register_event_handlers() self.on_register_services() self.on_dxl_connect() def run(self): """ Runs the application """ with self._lock: if self._running: raise Exception("The application is already running") self._running = True logger.info("Running application ...") self.on_run() self._validate_config_files() self._load_configuration() self._dxl_connect() def destroy(self): """ Destroys the application (disconnects from fabric, frees resources, etc.) """ with self._lock: if self._running and not self._destroyed: logger.info("Destroying application ...") if self._callbacks_pool is not None: self._callbacks_pool.shutdown() if self._dxl_client is not None: self._unregister_services() self._dxl_client.destroy() self._dxl_client = None self._destroyed = True def _get_path(self, in_path): """ Returns an absolute path for a file specified in the configuration file (supports files relative to the configuration file). :param in_path: The specified path :return: An absolute path for a file specified in the configuration file """ if not os.path.isfile(in_path) and not os.path.isabs(in_path): config_rel_path = os.path.join(self._config_dir, in_path) if os.path.isfile(config_rel_path): in_path = config_rel_path return in_path def _unregister_services(self): """ Unregisters the services associated with the Application from the fabric """ for service in self._services: self._dxl_client.unregister_service_sync( service, self.DXL_SERVICE_REGISTRATION_TIMEOUT) def _get_callbacks_pool(self): """ Returns the thread pool used to invoke application-specific message callbacks :return: The thread pool used to invoke application-specific message callbacks """ with self._lock: if self._callbacks_pool is None: self._callbacks_pool = ThreadPool(self._callbacks_queue_size, self._callbacks_thread_count, "CallbacksPool") return self._callbacks_pool def add_event_callback(self, topic, callback, separate_thread): """ Adds a DXL event message callback to the application. :param topic: The topic to associate with the callback :param callback: The event callback :param separate_thread: Whether to invoke the callback on a thread other than the incoming message thread (this is necessary if synchronous requests are made via DXL in this callback). """ if separate_thread: callback = _ThreadedEventCallback(self._get_callbacks_pool(), callback) self._dxl_client.add_event_callback(topic, callback) def add_request_callback(self, service, topic, callback, separate_thread): """ Adds a DXL request message callback to the application. :param service: The service to associate the request callback with :param topic: The topic to associate with the callback :param callback: The request callback :param separate_thread: Whether to invoke the callback on a thread other than the incoming message thread (this is necessary if synchronous requests are made via DXL in this callback). """ if separate_thread: callback = _ThreadedRequestCallback(self._get_callbacks_pool(), callback) service.add_topic(topic, callback) def register_service(self, service): """ Registers the specified service with the fabric :param service: The service to register with the fabric """ self._dxl_client.register_service_sync( service, self.DXL_SERVICE_REGISTRATION_TIMEOUT) self._services.append(service) def on_run(self): """ Invoked when the application has started running. """ pass def on_load_configuration(self, config): """ Invoked after the application-specific configuration has been loaded :param config: The application-specific configuration """ pass def on_dxl_connect(self): """ Invoked after the client associated with the application has connected to the DXL fabric. """ pass def on_register_event_handlers(self): """ Invoked when event handlers should be registered with the application """ pass def on_register_services(self): """ Invoked when services should be registered with the application """ pass