def load_shared_memory_manager(): # type: () -> None """ Connects to the main shared memory manager initiated in piper_worker.py. :return: None """ global SHARED_MEMORY_MANAGER SHARED_MEMORY_MANAGER = SharedMemoryManager(address=(IP, PORT), authkey=AUTH_KEY) SHARED_MEMORY_MANAGER.connect()
class NapariClient(Thread): """Shared memory client for napari. Napari launches this process. We get config information from our NAPARI_MON_CLIENT environment variable. We connect our SharedMemoryManager to the server_port specified in that config file. See napari's MonitorApi in for documention on the shared resources that napari exposes. Parameters ---------- config : dict The parsed configuration from the NAPARI_MON_CLIENT env variable. on_shutdown : Callable[[], None] We call then when shutting down. """ def __init__(self, config: dict, on_shutdown: Callable[[], None]): super().__init__() self.config = config self._on_shutdown = on_shutdown self._running = False LOGGER.info("Starting process %s", os.getpid()) _log_env() # Log our startup environment. server_port = config['server_port'] LOGGER.info("connecting to napari on port %d.", server_port) # We have to register these before creating the SharedMemoryManager. # Note that we don't have to give the types, just the names. # Although to use them we probably want to know the types! for name in NapariRemoteAPI.RESOURCES: SharedMemoryManager.register(name) # Connect to napari's shared memory on the server_port that napari # passed us in our NAPARI_MON_CLIENT configuration. self._manager = SharedMemoryManager( address=('localhost', config['server_port']), authkey=str.encode('napari'), ) self._manager.connect() # Get the shared resources as a convenient named tuple. self._remote = NapariRemoteAPI.from_manager(self._manager) # Start our thread which will poll napari. self.start() def run(self) -> None: """Thread that communicates with napari. Poll until we see the napari_shutdown event was set, or we get a connection error. Right now napari does not wait after signaling a shutdown, so more than 9/10 times the first indication we receive the napari is shutting down is getting a connection error. Which is fine for now. But a graceful handshake-exit might be something to look into. Obviously napari should have a short timeout so if the client is hung, it still exits quickly. """ self._running = True tid = threading.get_ident() LOGGER.info("Started NapariClient.run with thread_id = %d.", tid) while True: try: if not self._poll(): break # Shutdown event, exit the thread. except ConnectionResetError: LOGGER.info("ConnectionResetError polling napari.") break # Napari exited, exit the thread. # Sleep until ready to poll again. time.sleep(POLL_INTERVAL_SECONDS) LOGGER.info("Thread %d is exiting.", tid) # Notify webmon that we shutdown. self._running = False self._on_shutdown() def _poll(self) -> bool: """Communicate with napari. Return ------ bool Return True if we should keep polling. """ if self._remote.napari_shutdown.is_set(): LOGGER.info("Napari signaled shutdown.") return False # Stop polling. return True # Keep polling. def get_napari_data(self, key): """Get data from napari.""" return self._remote.napari_data.get(key) def send_message(self, message: dict) -> None: """Send new message to napari. Parameters ---------- message : dict The message/command to send to napari. """ LOGGER.info("Sending message %s", message) try: # Put message into the queue. self._remote.client_messages.put(message) except ConnectionRefusedError: LOGGER.error("ConnectionRefusedError sending message to napari.") def get_one_napari_message(self) -> Optional[dict]: """Get one message from napari, non-blocking. Return ------ Optional[dict] The message or None if no message was available. """ if not self._running: return None # Can't get messages from napari. napari_messages = self._remote.napari_messages try: message = napari_messages.get_nowait() assert isinstance(message, dict) # For now. return message except ConnectionResetError: LOGGER.error("ConnectionResetError getting messages from napari") # Napari is probably gone, but we let NapariClient._poll() # notice this and exit. We just say there as no message. return None except Empty: return None # No message in the queue. @classmethod def create(cls, on_shutdown: Callable[[], None]): """Create and return the NapariClient instance. Parameters ---------- on_shutdown : Callable[[], None] NapariClient will call this when it shuts down. Return ------ Optional[NapariClient] The newly created client or None on error. """ config = _get_client_config() if config is None: return None LOGGER.info("Creating NapariClient pid=%s", os.getpid()) return cls(config, on_shutdown)
class MonitorClient(Thread): """Client for napari shared memory monitor. Napari launches us. We get config information from our NAPARI_MON_CLIENT environment variable. That contains a port number to connect to. We connect our SharedMemoryManager to that port. We get these resources from the manager: 1) shutdown_event() If this is set napari is exiting. Ususally it exists so fast we get at ConnectionResetError exception instead of see this was set. We have no clean way to exit the SocketIO server yet. 2) command_queue() We put command onto this queue for napari to execute. 3) data() Data from napari's monitor.add() command. """ def __init__(self, config: dict, client_name="?"): super().__init__() assert config self.config = config self.client_name = client_name self.running = True self.napari_data = None LOGGER.info("Starting MonitorClient process %s", os.getpid()) _log_env() server_port = config['server_port'] LOGGER.info("Connecting to port %d...", server_port) # Right now we just need to magically know these callback names, # maybe we can come up with a better way. napari_api = ['shutdown_event', 'command_queue', 'data'] for name in napari_api: SharedMemoryManager.register(name) # Connect to napari's shared memory. self._manager = SharedMemoryManager( address=('localhost', config['server_port']), authkey=str.encode('napari'), ) self._manager.connect() # Get the shared resources. self._shared = SharedResources( self._manager.shutdown_event(), self._manager.command_queue(), self._manager.data(), ) # Start our thread so we can poll napari. self.start() def run(self) -> None: """Check shared memory for new data.""" LOGGER.info("MonitorClient thread is running...") while True: if not self._poll(): LOGGER.info("Exiting...") break time.sleep(POLL_INTERVAL_MS / 1000) # webmon checks this and stops/exits. self.running = False def _poll(self) -> bool: """See if there is now information in shared mem.""" # LOGGER.info("Poll...") try: if self._shared.shutdown.is_set(): # We sometimes do see the shutdown event was set. But usually # we just get ConnectionResetError, because napari is exiting. LOGGER.info("Shutdown event was set.") return False # Stop polling except ConnectionResetError: LOGGER.info("ConnectionResetError.") return False # Stop polling # Do we need to copy here? self.napari_data = { "tile_config": self._shared.data.get('tile_config'), "tile_state": self._shared.data.get('tile_state'), } if DUMP_DATA_FROM_NAPARI: pretty_str = json.dumps(self.napari_data, indent=4) LOGGER.info("New data from napari: %s", pretty_str) return True # Keep polling def post_command(self, command) -> None: """Send new command to napari. """ LOGGER.info(f"Posting command {command}") try: self._shared.commands.put(command) except ConnectionRefusedError: self._log("ConnectionRefusedError") def stop(self) -> None: """Call on shutdown. TODO_MON: no one calls this yet?""" self._manager.shutdown()