def test_add_block(self): p = Process("proc", MagicMock()) b = Block() b.set_process_path(p, ("name", )) c = MagicMock() p.add_block(b, c) self.assertEqual(p._blocks["name"], b) self.assertEqual(p._controllers["name"], c)
def test_add_block(self): p = Process("proc", MagicMock()) b = Block() b.set_process_path(p, ("name",)) c = MagicMock() p.add_block(b, c) self.assertEqual(p._blocks["name"], b) self.assertEqual(p._controllers["name"], c)
def test_add_block_calls_handle(self): s = SyncFactory("sched") p = Process("proc", s) b = Block() c = MagicMock() b.set_process_path(p, ("myblock",)) p.add_block(b, c) p.start() p.stop() self.assertEqual(len(p._blocks), 2) self.assertEqual(p._blocks, dict(myblock=b, proc=p.process_block))
def test_add_block_calls_handle(self): s = SyncFactory("sched") p = Process("proc", s) b = Block() c = MagicMock() b.set_process_path(p, ("myblock", )) p.add_block(b, c) p.start() p.stop() self.assertEqual(len(p._blocks), 2) self.assertEqual(p._blocks, dict(myblock=b, proc=p.process_block))
def test_get_block(self): p = Process("proc", MagicMock()) p.process_block["remoteBlocks"].set_value(['name1']) b1 = p.get_block("name1") self.assertEqual(b1.status, "Waiting for connection...") self.assertEqual(p.get_block("name1"), b1) b2 = Block() b2.set_process_path(p, ("name2",)) c = MagicMock() p.add_block(b2, c) self.assertEqual(p.get_block("name2"), b2) self.assertEqual(p.get_controller("name2"), c)
def test_get_block(self): p = Process("proc", MagicMock()) p.process_block["remoteBlocks"].set_value(['name1']) b1 = p.get_block("name1") self.assertEqual(b1.status, "Waiting for connection...") self.assertEqual(p.get_block("name1"), b1) b2 = Block() b2.set_process_path(p, ("name2", )) c = MagicMock() p.add_block(b2, c) self.assertEqual(p.get_block("name2"), b2) self.assertEqual(p.get_controller("name2"), c)
class Controller(Loggable): """Implement the logic that takes a Block through its state machine""" stateMachine = sm() # Attributes for all controllers state = None status = None busy = None # BlockMeta for descriptions meta = None def __init__(self, process, parts, params): """ Args: process (Process): The process this should run under params (Map): The parameters specified in method_takes() parts (list): [Part] """ self.process = process self.params = params self.mri = params.mri controller_name = "%s(%s)" % (type(self).__name__, self.mri) self.set_logger_name(controller_name) self.block = Block() self.log_debug("Creating block %r as %r", self.block, self.mri) self.lock = process.create_lock() # {part: task} self.part_tasks = {} # dictionary of dictionaries # {state (str): {Meta/MethodMeta/Attribute: writeable (bool)} self.children_writeable = {} # dict {hook: name} self.hook_names = self._find_hooks() self.parts = self._setup_parts(parts, controller_name) self._set_block_children() self._do_transition(sm.DISABLED, "Disabled") self.block.set_process_path(process, [self.mri]) process.add_block(self.block, self) self.do_initial_reset() def _find_hooks(self): hook_names = {} for n in dir(self): attr = getattr(self, n) if isinstance(attr, Hook): assert attr not in hook_names, \ "Hook %s already in controller as %s" % ( n, hook_names[attr]) hook_names[attr] = n return hook_names def _setup_parts(self, parts, controller_name): parts_dict = OrderedDict() for part in parts: part.set_logger_name("%s.%s" % (controller_name, part.name)) # Check part hooks into one of our hooks for func_name, part_hook, _ in get_hook_decorated(part): assert part_hook in self.hook_names, \ "Part %s func %s not hooked into %s" % ( part.name, func_name, self) parts_dict[part.name] = part return parts_dict def do_initial_reset(self): pass def _set_block_children(self): # reconfigure block with new children child_list = [self.create_meta()] child_list += list(self.create_attributes()) child_list += list(self.create_methods()) for part in self.parts.values(): child_list += list(part.create_attributes()) child_list += list(part.create_methods()) self.children_writeable = {} writeable_functions = {} children = OrderedDict() for name, child, writeable_func in child_list: if isinstance(child, Attribute): states = child.meta.writeable_in else: states = child.writeable_in children[name] = child if states: for state in states: assert state in self.stateMachine.possible_states, \ "State %s is not one of the valid states %s" % \ (state, self.stateMachine.possible_states) elif writeable_func is not None: states = [ state for state in self.stateMachine.possible_states if state not in (sm.DISABLING, sm.DISABLED)] else: continue self.register_child_writeable(name, states) if writeable_func: writeable_functions[name] = functools.partial( self.call_writeable_function, writeable_func) self.block.replace_endpoints(children) self.block.set_writeable_functions(writeable_functions) def call_writeable_function(self, function, child, *args): with self.lock: if not child.writeable: raise ValueError( "Child %r is not writeable" % (child.process_path,)) result = function(*args) return result def create_meta(self): self.meta = BlockMeta() return "meta", self.meta, None def create_attributes(self): """Method that should provide Attribute instances for Block Yields: tuple: (string name, Attribute, callable put_function). """ # Add the state, status and busy attributes self.state = ChoiceMeta( "State of Block", self.stateMachine.possible_states, label="State" ).make_attribute() yield "state", self.state, None self.status = StringMeta( "Status of Block", label="Status" ).make_attribute() yield "status", self.status, None self.busy = BooleanMeta( "Whether Block busy or not", label="Busy" ).make_attribute() yield "busy", self.busy, None def create_methods(self): """Method that should provide MethodMeta instances for Block Yields: tuple: (string name, MethodMeta, callable post_function). """ return get_method_decorated(self) def transition(self, state, message): """ Change to a new state if the transition is allowed Args: state(str): State to transition to message(str): Status message """ with self.lock: if self.stateMachine.is_allowed( initial_state=self.state.value, target_state=state): self._do_transition(state, message) else: raise TypeError("Cannot transition from %s to %s" % (self.state.value, state)) def _do_transition(self, state, message): # transition is allowed, so set attributes changes = [] changes.append([["state", "value"], state]) changes.append([["status", "value"], message]) changes.append([["busy", "value"], state in self.stateMachine.busy_states]) # say which children are now writeable for name in self.block: try: writeable = self.children_writeable[state][name] except KeyError: continue child = self.block[name] if isinstance(child, Attribute): changes.append([[name, "meta", "writeable"], writeable]) elif isinstance(child, MethodMeta): changes.append([[name, "writeable"], writeable]) for ename in child.takes.elements: path = [name, "takes", "elements", ename, "writeable"] changes.append([path, writeable]) self.log_debug("Transitioning to %s", state) self.block.apply_changes(*changes) def register_child_writeable(self, name, states): """ Set the states that the given method can be called in Args: name (str): Child name that will be set writeable or not states (list[str]): states where method is writeable """ for state in self.stateMachine.possible_states: writeable_dict = self.children_writeable.setdefault(state, {}) is_writeable = state in states writeable_dict[name] = is_writeable def create_part_tasks(self): part_tasks = {} for part_name, part in self.parts.items(): part_tasks[part] = Task("Task(%s)" % part_name, self.process) return part_tasks def run_hook(self, hook, part_tasks, *args, **params): hook_queue, hook_runners = self.start_hook( hook, part_tasks, *args, **params) return_dict = self.wait_hook(hook_queue, hook_runners) return return_dict def start_hook(self, hook, part_tasks, *args, **params): assert hook in self.hook_names, \ "Hook %s doesn't appear in controller hooks %s" % ( hook, self.hook_names) # ask the hook to find the functions it should run part_funcs = hook.find_hooked_functions(self.parts) hook_runners = {} self.log_debug("Run %s hook on %s", self.hook_names[hook], [p.name for p in part_funcs]) # now start them off hook_queue = self.process.create_queue() for part, func_name in part_funcs.items(): task = weakref.proxy(part_tasks[part]) hook_runner = HookRunner( hook_queue, part, func_name, task, *args, **params) hook_runner.start() hook_runners[part] = hook_runner return hook_queue, hook_runners def wait_hook(self, hook_queue, hook_runners): # Wait for them all to finish return_dict = {} while hook_runners: part, ret = hook_queue.get() hook_runner = hook_runners.pop(part) if isinstance(ret, AbortedError): # If AbortedError, all tasks have already been stopped. self.log_debug("Part %s Aborted", part.name) # Do not wait on them otherwise we might get a deadlock... raise ret # Wait for the process to terminate hook_runner.wait() return_dict[part.name] = ret self.log_debug("Part %s returned %r. Still waiting for %s", part.name, ret, [p.name for p in hook_runners]) if isinstance(ret, Exception): # Got an error, so stop and wait all hook runners for h in hook_runners.values(): h.stop() # Wait for them to finish for h in hook_runners.values(): h.wait() raise ret return return_dict
class Process(Loggable): """Hosts a number of Blocks, distributing requests between them""" def __init__(self, name, sync_factory): self.set_logger_name(name) self.name = name self.sync_factory = sync_factory self.q = self.create_queue() self._blocks = OrderedDict() # block_name -> Block self._controllers = OrderedDict() # block_name -> Controller self._block_state_cache = Cache() self._recv_spawned = None self._other_spawned = [] # lookup of all Subscribe requests, ordered to guarantee subscription # notification ordering # {Request.generate_key(): Subscribe} self._subscriptions = OrderedDict() self.comms = [] self._client_comms = OrderedDict() # client comms -> list of blocks self._handle_functions = { Post: self._forward_block_request, Put: self._forward_block_request, Get: self._handle_get, Subscribe: self._handle_subscribe, Unsubscribe: self._handle_unsubscribe, BlockChanges: self._handle_block_changes, BlockRespond: self._handle_block_respond, BlockAdd: self._handle_block_add, BlockList: self._handle_block_list, AddSpawned: self._add_spawned, } self.create_process_block() def recv_loop(self): """Service self.q, distributing the requests to the right block""" while True: request = self.q.get() self.log_debug("Received request %s", request) if request is PROCESS_STOP: # Got the sentinel, stop immediately break try: self._handle_functions[type(request)](request) except Exception as e: # pylint:disable=broad-except self.log_exception("Exception while handling %s", request) try: request.respond_with_error(str(e)) except Exception: pass def add_comms(self, comms): assert not self._recv_spawned, \ "Can't add comms when process has been started" self.comms.append(comms) def start(self): """Start the process going""" self._recv_spawned = self.sync_factory.spawn(self.recv_loop) for comms in self.comms: comms.start() def stop(self, timeout=None): """Stop the process and wait for it to finish Args: timeout (float): Maximum amount of time to wait for each spawned process. None means forever """ assert self._recv_spawned, "Process not started" self.q.put(PROCESS_STOP) for comms in self.comms: comms.stop() # Wait for recv_loop to complete first self._recv_spawned.wait(timeout=timeout) # Now wait for anything it spawned to complete for s, _ in self._other_spawned: s.wait(timeout=timeout) # Garbage collect the syncfactory del self.sync_factory def _forward_block_request(self, request): """Lookup target Block and spawn block.handle_request(request) Args: request (Request): The message that should be passed to the Block """ block_name = request.endpoint[0] block = self._blocks[block_name] spawned = self.sync_factory.spawn(block.handle_request, request) self._add_spawned(AddSpawned(spawned, block.handle_request)) def create_queue(self): """ Create a queue using sync_factory object Returns: Queue: New queue """ return self.sync_factory.create_queue() def create_lock(self): """ Create a lock using sync_factory object Returns: New lock object """ return self.sync_factory.create_lock() def spawn(self, function, *args, **kwargs): """Calls SyncFactory.spawn()""" def catching_function(): try: function(*args, **kwargs) except Exception: self.log_exception( "Exception calling %s(*%s, **%s)", function, args, kwargs) raise spawned = self.sync_factory.spawn(catching_function) request = AddSpawned(spawned, function) self.q.put(request) return spawned def _add_spawned(self, request): spawned = self._other_spawned self._other_spawned = [] spawned.append((request.spawned, request.function)) # Filter out the spawned that have completed to stop memory leaks for sp, f in spawned: if not sp.ready(): self._other_spawned.append((sp, f)) def get_client_comms(self, block_name): for client_comms, blocks in list(self._client_comms.items()): if block_name in blocks: return client_comms def create_process_block(self): self.process_block = Block() # TODO: add a meta here children = OrderedDict() children["blocks"] = StringArrayMeta( description="Blocks hosted by this Process" ).make_attribute([]) children["remoteBlocks"] = StringArrayMeta( description="Blocks reachable via ClientComms" ).make_attribute([]) self.process_block.replace_endpoints(children) self.process_block.set_process_path(self, [self.name]) self.add_block(self.process_block, self) def update_block_list(self, client_comms, blocks): self.q.put(BlockList(client_comms=client_comms, blocks=blocks)) def _handle_block_list(self, request): self._client_comms[request.client_comms] = request.blocks remotes = [] for blocks in self._client_comms.values(): remotes += [b for b in blocks if b not in remotes] self.process_block["remoteBlocks"].set_value(remotes) def _handle_block_changes(self, request): """Update subscribers with changes and applies stored changes to the cached structure""" # update cached dict subscription_changes = self._block_state_cache.apply_changes( *request.changes) # Send out the changes for subscription, changes in subscription_changes.items(): if subscription.delta: # respond with the filtered changes subscription.respond_with_delta(changes) else: # respond with the structure of everything # below the endpoint d = self._block_state_cache.walk_path(subscription.endpoint) subscription.respond_with_update(d) def report_changes(self, *changes): self.q.put(BlockChanges(changes=list(changes))) def block_respond(self, response, response_queue): self.q.put(BlockRespond(response, response_queue)) def _handle_block_respond(self, request): """Push the response to the required queue""" request.response_queue.put(request.response) def add_block(self, block, controller): """Add a block to be hosted by this process Args: block (Block): The block to be added controller (Controller): Its controller """ path = block.process_path assert len(path) == 1, \ "Expected block %r to have %r as parent, got path %r" % \ (block, self, path) name = path[0] assert name not in self._blocks, \ "There is already a block called %r" % name request = BlockAdd(block=block, controller=controller, name=name) if self._recv_spawned: # Started, so call in Process thread self.q.put(request) else: # Not started yet so we are safe to add in this thread self._handle_block_add(request) def _handle_block_add(self, request): """Add a block to be hosted by this process""" assert request.name not in self._blocks, \ "There is already a block called %r" % request.name self._blocks[request.name] = request.block self._controllers[request.name] = request.controller serialized = request.block.to_dict() change_request = BlockChanges([[[request.name], serialized]]) self._handle_block_changes(change_request) # Regenerate list of blocks self.process_block["blocks"].set_value(list(self._blocks)) def get_block(self, block_name): try: return self._blocks[block_name] except KeyError: if block_name in self.process_block.remoteBlocks: return self.make_client_block(block_name) else: raise def make_client_block(self, block_name): params = ClientController.MethodMeta.prepare_input_map( mri=block_name) controller = ClientController(self, {}, params) return controller.block def get_controller(self, block_name): return self._controllers[block_name] def _handle_subscribe(self, request): """Add a new subscriber and respond with the current sub-structure state""" key = request.generate_key() assert key not in self._subscriptions, \ "Subscription on %s already exists" % (key,) self._subscriptions[key] = request self._block_state_cache.add_subscriber(request, request.endpoint) d = self._block_state_cache.walk_path(request.endpoint) self.log_debug("Initial subscription value %s", d) if request.delta: request.respond_with_delta([[[], d]]) else: request.respond_with_update(d) def _handle_unsubscribe(self, request): """Remove a subscriber and respond with success or error""" key = request.generate_key() try: subscription = self._subscriptions.pop(key) except KeyError: request.respond_with_error( "No subscription found for %s" % (key,)) else: self._block_state_cache.remove_subscriber( subscription, subscription.endpoint) request.respond_with_return() def _handle_get(self, request): d = self._block_state_cache.walk_path(request.endpoint) request.respond_with_return(d)
class Process(Loggable): """Hosts a number of Blocks, distributing requests between them""" def __init__(self, name, sync_factory): self.set_logger_name(name) self.name = name self.sync_factory = sync_factory self.q = self.create_queue() self._blocks = OrderedDict() # block_name -> Block self._controllers = OrderedDict() # block_name -> Controller self._block_state_cache = Cache() self._recv_spawned = None self._other_spawned = [] # lookup of all Subscribe requests, ordered to guarantee subscription # notification ordering # {Request.generate_key(): Subscribe} self._subscriptions = OrderedDict() self.comms = [] self._client_comms = OrderedDict() # client comms -> list of blocks self._handle_functions = { Post: self._forward_block_request, Put: self._forward_block_request, Get: self._handle_get, Subscribe: self._handle_subscribe, Unsubscribe: self._handle_unsubscribe, BlockChanges: self._handle_block_changes, BlockRespond: self._handle_block_respond, BlockAdd: self._handle_block_add, BlockList: self._handle_block_list, AddSpawned: self._add_spawned, } self.create_process_block() def recv_loop(self): """Service self.q, distributing the requests to the right block""" while True: request = self.q.get() self.log_debug("Received request %s", request) if request is PROCESS_STOP: # Got the sentinel, stop immediately break try: self._handle_functions[type(request)](request) except Exception as e: # pylint:disable=broad-except self.log_exception("Exception while handling %s", request) try: request.respond_with_error(str(e)) except Exception: pass def add_comms(self, comms): assert not self._recv_spawned, \ "Can't add comms when process has been started" self.comms.append(comms) def start(self): """Start the process going""" self._recv_spawned = self.sync_factory.spawn(self.recv_loop) for comms in self.comms: comms.start() def stop(self, timeout=None): """Stop the process and wait for it to finish Args: timeout (float): Maximum amount of time to wait for each spawned process. None means forever """ assert self._recv_spawned, "Process not started" self.q.put(PROCESS_STOP) for comms in self.comms: comms.stop() # Wait for recv_loop to complete first self._recv_spawned.wait(timeout=timeout) # Now wait for anything it spawned to complete for s, _ in self._other_spawned: s.wait(timeout=timeout) # Garbage collect the syncfactory del self.sync_factory def _forward_block_request(self, request): """Lookup target Block and spawn block.handle_request(request) Args: request (Request): The message that should be passed to the Block """ block_name = request.endpoint[0] block = self._blocks[block_name] spawned = self.sync_factory.spawn(block.handle_request, request) self._add_spawned(AddSpawned(spawned, block.handle_request)) def create_queue(self): """ Create a queue using sync_factory object Returns: Queue: New queue """ return self.sync_factory.create_queue() def create_lock(self): """ Create a lock using sync_factory object Returns: New lock object """ return self.sync_factory.create_lock() def spawn(self, function, *args, **kwargs): """Calls SyncFactory.spawn()""" def catching_function(): try: function(*args, **kwargs) except Exception: self.log_exception("Exception calling %s(*%s, **%s)", function, args, kwargs) raise spawned = self.sync_factory.spawn(catching_function) request = AddSpawned(spawned, function) self.q.put(request) return spawned def _add_spawned(self, request): spawned = self._other_spawned self._other_spawned = [] spawned.append((request.spawned, request.function)) # Filter out the spawned that have completed to stop memory leaks for sp, f in spawned: if not sp.ready(): self._other_spawned.append((sp, f)) def get_client_comms(self, block_name): for client_comms, blocks in list(self._client_comms.items()): if block_name in blocks: return client_comms def create_process_block(self): self.process_block = Block() # TODO: add a meta here children = OrderedDict() children["blocks"] = StringArrayMeta( description="Blocks hosted by this Process").make_attribute([]) children["remoteBlocks"] = StringArrayMeta( description="Blocks reachable via ClientComms").make_attribute([]) self.process_block.replace_endpoints(children) self.process_block.set_process_path(self, [self.name]) self.add_block(self.process_block, self) def update_block_list(self, client_comms, blocks): self.q.put(BlockList(client_comms=client_comms, blocks=blocks)) def _handle_block_list(self, request): self._client_comms[request.client_comms] = request.blocks remotes = [] for blocks in self._client_comms.values(): remotes += [b for b in blocks if b not in remotes] self.process_block["remoteBlocks"].set_value(remotes) def _handle_block_changes(self, request): """Update subscribers with changes and applies stored changes to the cached structure""" # update cached dict subscription_changes = self._block_state_cache.apply_changes( *request.changes) # Send out the changes for subscription, changes in subscription_changes.items(): if subscription.delta: # respond with the filtered changes subscription.respond_with_delta(changes) else: # respond with the structure of everything # below the endpoint d = self._block_state_cache.walk_path(subscription.endpoint) subscription.respond_with_update(d) def report_changes(self, *changes): self.q.put(BlockChanges(changes=list(changes))) def block_respond(self, response, response_queue): self.q.put(BlockRespond(response, response_queue)) def _handle_block_respond(self, request): """Push the response to the required queue""" request.response_queue.put(request.response) def add_block(self, block, controller): """Add a block to be hosted by this process Args: block (Block): The block to be added controller (Controller): Its controller """ path = block.process_path assert len(path) == 1, \ "Expected block %r to have %r as parent, got path %r" % \ (block, self, path) name = path[0] assert name not in self._blocks, \ "There is already a block called %r" % name request = BlockAdd(block=block, controller=controller, name=name) if self._recv_spawned: # Started, so call in Process thread self.q.put(request) else: # Not started yet so we are safe to add in this thread self._handle_block_add(request) def _handle_block_add(self, request): """Add a block to be hosted by this process""" assert request.name not in self._blocks, \ "There is already a block called %r" % request.name self._blocks[request.name] = request.block self._controllers[request.name] = request.controller serialized = request.block.to_dict() change_request = BlockChanges([[[request.name], serialized]]) self._handle_block_changes(change_request) # Regenerate list of blocks self.process_block["blocks"].set_value(list(self._blocks)) def get_block(self, block_name): try: return self._blocks[block_name] except KeyError: if block_name in self.process_block.remoteBlocks: return self.make_client_block(block_name) else: raise def make_client_block(self, block_name): params = ClientController.MethodMeta.prepare_input_map(mri=block_name) controller = ClientController(self, {}, params) return controller.block def get_controller(self, block_name): return self._controllers[block_name] def _handle_subscribe(self, request): """Add a new subscriber and respond with the current sub-structure state""" key = request.generate_key() assert key not in self._subscriptions, \ "Subscription on %s already exists" % (key,) self._subscriptions[key] = request self._block_state_cache.add_subscriber(request, request.endpoint) d = self._block_state_cache.walk_path(request.endpoint) self.log_debug("Initial subscription value %s", d) if request.delta: request.respond_with_delta([[[], d]]) else: request.respond_with_update(d) def _handle_unsubscribe(self, request): """Remove a subscriber and respond with success or error""" key = request.generate_key() try: subscription = self._subscriptions.pop(key) except KeyError: request.respond_with_error("No subscription found for %s" % (key, )) else: self._block_state_cache.remove_subscriber(subscription, subscription.endpoint) request.respond_with_return() def _handle_get(self, request): d = self._block_state_cache.walk_path(request.endpoint) request.respond_with_return(d)
class Controller(Loggable): """Implement the logic that takes a Block through its state machine""" stateMachine = sm() # Attributes for all controllers state = None status = None busy = None # BlockMeta for descriptions meta = None def __init__(self, process, parts, params): """ Args: process (Process): The process this should run under params (Map): The parameters specified in method_takes() parts (list): [Part] """ self.process = process self.params = params self.mri = params.mri controller_name = "%s(%s)" % (type(self).__name__, self.mri) self.set_logger_name(controller_name) self.block = Block() self.log_debug("Creating block %r as %r", self.block, self.mri) self.lock = process.create_lock() # {part: task} self.part_tasks = {} # dictionary of dictionaries # {state (str): {Meta/MethodMeta/Attribute: writeable (bool)} self.children_writeable = {} # dict {hook: name} self.hook_names = self._find_hooks() self.parts = self._setup_parts(parts, controller_name) self._set_block_children() self._do_transition(sm.DISABLED, "Disabled") self.block.set_process_path(process, [self.mri]) process.add_block(self.block, self) self.do_initial_reset() def _find_hooks(self): hook_names = {} for n in dir(self): attr = getattr(self, n) if isinstance(attr, Hook): assert attr not in hook_names, \ "Hook %s already in controller as %s" % ( n, hook_names[attr]) hook_names[attr] = n return hook_names def _setup_parts(self, parts, controller_name): parts_dict = OrderedDict() for part in parts: part.set_logger_name("%s.%s" % (controller_name, part.name)) # Check part hooks into one of our hooks for func_name, part_hook, _ in get_hook_decorated(part): assert part_hook in self.hook_names, \ "Part %s func %s not hooked into %s" % ( part.name, func_name, self) parts_dict[part.name] = part return parts_dict def do_initial_reset(self): pass def _set_block_children(self): # reconfigure block with new children child_list = [self.create_meta()] child_list += list(self.create_attributes()) child_list += list(self.create_methods()) for part in self.parts.values(): child_list += list(part.create_attributes()) child_list += list(part.create_methods()) self.children_writeable = {} writeable_functions = {} children = OrderedDict() for name, child, writeable_func in child_list: if isinstance(child, Attribute): states = child.meta.writeable_in else: states = child.writeable_in children[name] = child if states: for state in states: assert state in self.stateMachine.possible_states, \ "State %s is not one of the valid states %s" % \ (state, self.stateMachine.possible_states) elif writeable_func is not None: states = [ state for state in self.stateMachine.possible_states if state not in (sm.DISABLING, sm.DISABLED) ] else: continue self.register_child_writeable(name, states) if writeable_func: writeable_functions[name] = functools.partial( self.call_writeable_function, writeable_func) self.block.replace_endpoints(children) self.block.set_writeable_functions(writeable_functions) def call_writeable_function(self, function, child, *args): with self.lock: if not child.writeable: raise ValueError("Child %r is not writeable" % (child.process_path, )) result = function(*args) return result def create_meta(self): self.meta = BlockMeta() return "meta", self.meta, None def create_attributes(self): """Method that should provide Attribute instances for Block Yields: tuple: (string name, Attribute, callable put_function). """ # Add the state, status and busy attributes self.state = ChoiceMeta("State of Block", self.stateMachine.possible_states, label="State").make_attribute() yield "state", self.state, None self.status = StringMeta("Status of Block", label="Status").make_attribute() yield "status", self.status, None self.busy = BooleanMeta("Whether Block busy or not", label="Busy").make_attribute() yield "busy", self.busy, None def create_methods(self): """Method that should provide MethodMeta instances for Block Yields: tuple: (string name, MethodMeta, callable post_function). """ return get_method_decorated(self) def transition(self, state, message): """ Change to a new state if the transition is allowed Args: state(str): State to transition to message(str): Status message """ with self.lock: if self.stateMachine.is_allowed(initial_state=self.state.value, target_state=state): self._do_transition(state, message) else: raise TypeError("Cannot transition from %s to %s" % (self.state.value, state)) def _do_transition(self, state, message): # transition is allowed, so set attributes changes = [] changes.append([["state", "value"], state]) changes.append([["status", "value"], message]) changes.append([["busy", "value"], state in self.stateMachine.busy_states]) # say which children are now writeable for name in self.block: try: writeable = self.children_writeable[state][name] except KeyError: continue child = self.block[name] if isinstance(child, Attribute): changes.append([[name, "meta", "writeable"], writeable]) elif isinstance(child, MethodMeta): changes.append([[name, "writeable"], writeable]) for ename in child.takes.elements: path = [name, "takes", "elements", ename, "writeable"] changes.append([path, writeable]) self.log_debug("Transitioning to %s", state) self.block.apply_changes(*changes) def register_child_writeable(self, name, states): """ Set the states that the given method can be called in Args: name (str): Child name that will be set writeable or not states (list[str]): states where method is writeable """ for state in self.stateMachine.possible_states: writeable_dict = self.children_writeable.setdefault(state, {}) is_writeable = state in states writeable_dict[name] = is_writeable def create_part_tasks(self): part_tasks = {} for part_name, part in self.parts.items(): part_tasks[part] = Task("Task(%s)" % part_name, self.process) return part_tasks def run_hook(self, hook, part_tasks, *args, **params): hook_queue, hook_runners = self.start_hook(hook, part_tasks, *args, **params) return_dict = self.wait_hook(hook_queue, hook_runners) return return_dict def start_hook(self, hook, part_tasks, *args, **params): assert hook in self.hook_names, \ "Hook %s doesn't appear in controller hooks %s" % ( hook, self.hook_names) # ask the hook to find the functions it should run part_funcs = hook.find_hooked_functions(self.parts) hook_runners = {} self.log_debug("Run %s hook on %s", self.hook_names[hook], [p.name for p in part_funcs]) # now start them off hook_queue = self.process.create_queue() for part, func_name in part_funcs.items(): task = weakref.proxy(part_tasks[part]) hook_runner = HookRunner(hook_queue, part, func_name, task, *args, **params) hook_runner.start() hook_runners[part] = hook_runner return hook_queue, hook_runners def wait_hook(self, hook_queue, hook_runners): # Wait for them all to finish return_dict = {} while hook_runners: part, ret = hook_queue.get() hook_runner = hook_runners.pop(part) if isinstance(ret, AbortedError): # If AbortedError, all tasks have already been stopped. self.log_debug("Part %s Aborted", part.name) # Do not wait on them otherwise we might get a deadlock... raise ret # Wait for the process to terminate hook_runner.wait() return_dict[part.name] = ret self.log_debug("Part %s returned %r. Still waiting for %s", part.name, ret, [p.name for p in hook_runners]) if isinstance(ret, Exception): # Got an error, so stop and wait all hook runners for h in hook_runners.values(): h.stop() # Wait for them to finish for h in hook_runners.values(): h.wait() raise ret return return_dict