# coding: utf-8 import collections from copy import copy from multiprocessing import Queue from multiprocessing import Process from queue import Empty import time from synergine2.base import BaseObject from synergine2.config import Config from synergine2.log import SynergineLogger from synergine2.simulation import Subject from synergine2.simulation import Event STOP_SIGNAL = '__STOP_SIGNAL__' class TerminalPackage(BaseObject): """ TODO: Update this class considering shared data across processes """ def __init__( self, subjects: [Subject]=None, add_subjects: [Subject]=None, remove_subjects: [Subject]=None, events: [Event]=None, simulation_actions: [tuple]=None, subject_actions: [tuple]=None, is_cycle: bool=False, *args, **kwargs ): self.subjects = subjects self.add_subjects = add_subjects or [] self.remove_subjects = remove_subjects or [] self.events = events or [] self.simulation_actions = simulation_actions or [] self.subject_actions = subject_actions or [] self.is_cycle = is_cycle def repr_debug(self) -> str: subjects = self.subjects or [] return str(dict( subjects=subjects, add_subjects=[s.id for s in self.add_subjects], remove_subjects=[s.id for s in self.remove_subjects], events=[e.repr_debug() for e in self.events], simulation_actions=['{}: {}'.format(a.__class__.__name__, p) for a, p in self.simulation_actions], subject_actions=['{}: {}'.format(a.__class__.__name__, p) for a, p in self.subject_actions], is_cycle=self.is_cycle, )) class Terminal(BaseObject): # Default behaviour is to do nothing. # DEFAULT_SLEEP is sleep time between each queue read default_sleep = 1 # List of subscribed Event classes. Terminal will not receive events # who are not instance of listed classes subscribed_events = [Event] def __init__( self, config: Config, logger: SynergineLogger, asynchronous: bool=True, ): self.config = config self.logger = logger self._input_queue = None self._output_queue = None self._stop_required = False self.subjects = {} self.cycle_events = [] self.event_handlers = collections.defaultdict(list) self.asynchronous = asynchronous def accept_event(self, event: Event) -> bool: for event_class in self.subscribed_events: if isinstance(event, event_class): return True return False def start(self, input_queue: Queue, output_queue: Queue) -> None: self._input_queue = input_queue self._output_queue = output_queue self.run() def run(self): """ Override this method to create your daemon terminal """ try: while self.read(): time.sleep(self.default_sleep) except KeyboardInterrupt: pass def read(self): while True: try: package = self._input_queue.get(block=False, timeout=None) if package == STOP_SIGNAL: self._stop_required = True return False self.receive(package) except Empty: return True # Finished to read Queue def receive(self, package: TerminalPackage): self.update_with_package(package) # End of cycle management signal self.send(TerminalPackage(is_cycle=True)) def send(self, package: TerminalPackage): self._output_queue.put(package) def register_event_handler(self, event_class, func): self.event_handlers[event_class].append(func) def update_with_package(self, package: TerminalPackage): if package.subjects: self.subjects = {s.id: s for s in package.subjects} for new_subject in package.add_subjects: self.subjects[new_subject.id] = new_subject for deleted_subject in package.remove_subjects: del self.subjects[deleted_subject.id] self.cycle_events = package.events self.execute_event_handlers(package.events) def execute_event_handlers(self, events: [Event]): for event in events: for event_class, handlers in self.event_handlers.items(): if isinstance(event, event_class): for handler in handlers: handler(event) class TerminalManager(BaseObject): def __init__( self, config: Config, logger: SynergineLogger, terminals: [Terminal] ): self.config = config self.logger = logger self.terminals = terminals self.outputs_queues = {} self.inputs_queues = {} def start(self) -> None: self.logger.info('Start terminals') for terminal in self.terminals: # TODO: logs output_queue = Queue() self.outputs_queues[terminal] = output_queue input_queue = Queue() self.inputs_queues[terminal] = input_queue process = Process(target=terminal.start, kwargs=dict( input_queue=output_queue, output_queue=input_queue, )) process.start() def stop(self): for output_queue in self.outputs_queues.values(): output_queue.put(STOP_SIGNAL) def send(self, package: TerminalPackage): self.logger.info('Send package to terminals') if self.logger.is_debug: self.logger.debug('Send package to terminals: {}'.format( str(package.repr_debug()), )) for terminal, output_queue in self.outputs_queues.items(): self.logger.info('Send package to terminal {}'.format(terminal.__class__.__name__)) # Terminal maybe don't want all events, so reduce list of event # Thirst make a copy to personalize this package terminal_adapted_package = copy(package) # Duplicate events list to personalize it terminal_adapted_package.events = terminal_adapted_package.events[:] for package_event in terminal_adapted_package.events[:]: if not terminal.accept_event(package_event): terminal_adapted_package.events.remove(package_event) if self.logger.is_debug: self.logger.debug('Send package to terminal {}: {}'.format( terminal.__class__.__name__, terminal_adapted_package.repr_debug(), )) output_queue.put(terminal_adapted_package) def receive(self) -> [TerminalPackage]: self.logger.info('Receive terminals packages') packages = [] for terminal, input_queue in self.inputs_queues.items(): self.logger.info('Receive terminal {} packages ({})'.format( terminal.__class__.__name__, 'sync' if not terminal.asynchronous else 'async' )) # When terminal is synchronous, wait it's cycle package if not terminal.asynchronous: continue_ = True while continue_: package = input_queue.get() # In case where terminal send package before end of cycle # management continue_ = not package.is_cycle if self.logger.is_debug: self.logger.debug('Receive package from {}: {}'.format( terminal.__class__.__name__, str(package.repr_debug()), )) packages.append(package) else: try: while True: package = input_queue.get(block=False, timeout=None) if self.logger.is_debug: self.logger.debug('Receive package from {}: {}'.format( str(terminal), str(package.repr_debug()), )) packages.append(package) except Empty: pass # Queue is empty self.logger.info('{} package(s) received'.format(len(packages))) return packages