class MonitorApi: """The API that monitor clients can access. The MonitorApi creates and exposes a few "shared resources" that monitor clients can access. Client access the shared resources through their SharedMemoryManager which connects to napari. Exactly what resources we should expose is TBD. Here we are experimenting with having queue for sending message in each direction, and a shared dict for sharing data in both directions. The advantage of a Queue is presumably the other party will definitely get the message. While the advantage of dict is kind of the opposite, the other party can check the dict if they want, or they can ignore it. Again we're not sure what's best yet. But this illustrates some options. Shared Resources ---------------- napari_data : dict Napari shares data in this dict for clients to read. napari_messages : Queue Napari puts messages in here for clients to read. napari_shutdown : Event Napari signals this event when shutting down. Although today napari does not wait on anything, so typically the client just gets a connection error when napari goes away, rather than seeing this event. client_data : Queue Client shares data in here for napari to read. client_messages : Queue Client puts messages in here for napari to read, such as commands. Notes ----- The SharedMemoryManager provides the same proxy objects as SyncManager including list, dict, Barrier, BoundedSemaphore, Condition, Event, Lock, Namespace, Queue, RLock, Semaphore, Array, Value. SharedMemoryManager is derived from BaseManager, but it has similar functionality to SyncManager. See the official Python docs for multiprocessing.managers.SyncManager. Numpy can natively use shared memory buffers, something we want to try. """ # BaseManager.register() is a bit weird. Not sure now to best deal with # it. Most ways I tried led to pickling errors, because this class is being run # in the shared memory server process? Feel free to find a better approach. _napari_data_dict = dict() _napari_messages_queue = Queue() _napari_shutdown_event = Event() _client_data_dict = dict() _client_messages_queue = Queue() @staticmethod def _napari_data() -> Queue: return MonitorApi._napari_data_dict @staticmethod def _napari_messages() -> Queue: return MonitorApi._napari_messages_queue @staticmethod def _napari_shutdown() -> Event: return MonitorApi._napari_shutdown_event @staticmethod def _client_data() -> Queue: return MonitorApi._client_data_dict @staticmethod def _client_messages() -> Queue: return MonitorApi._client_messages_queue def __init__(self): # RemoteCommands listens to our run_command event. It executes # commands from the clients. self.events = EmitterGroup( source=self, auto_connect=True, run_command=None ) # We must register all callbacks before we create our instance of # SharedMemoryManager. The client must do the same thing, but it # only needs to know the names. We allocate the shared memory. SharedMemoryManager.register('napari_data', callable=self._napari_data) SharedMemoryManager.register( 'napari_messages', callable=self._napari_messages ) SharedMemoryManager.register( 'napari_shutdown', callable=self._napari_shutdown ) SharedMemoryManager.register('client_data', callable=self._client_data) SharedMemoryManager.register( 'client_messages', callable=self._client_messages ) # Start our shared memory server. self._manager = SharedMemoryManager( address=('127.0.0.1', SERVER_PORT), authkey=str.encode(AUTH_KEY) ) self._manager.start() # Get the shared resources the server created. Clients will access # these same resources. self._remote = NapariRemoteAPI( self._manager.napari_data(), self._manager.napari_messages(), self._manager.napari_shutdown(), self._manager.client_data(), self._manager.client_messages(), ) @property def manager(self) -> SharedMemoryManager: """Our shared memory manager. The wrapper Monitor class accesses this and passes it to the MonitorService. Returns ------- SharedMemoryManager The manager we created and are using. """ return self._manager def stop(self) -> None: """Notify clients we are shutting down. If we wanted a graceful shutdown, we could wait on "connected" clients to exit. With a short timeout in case they are hung. Today we just signal this event and immediately exit. So most of the time clients just get a connection error. They never see that this event was set. """ self._remote.napari_shutdown.set() def poll(self): """Poll client_messages for new messages.""" assert self._manager is not None self._process_client_messages() def _process_client_messages(self) -> None: """Process every new message in the queue.""" client_messages = self._remote.client_messages while True: try: message = client_messages.get_nowait() if not isinstance(message, dict): LOGGER.warning( "Ignore message that was not a dict: %s", message ) continue # Assume every message is a command that napari should # execute. We might have other types of messages later. self.events.run_command(command=message) except Empty: return # No commands to process. def add_napari_data(self, data: dict) -> None: """Add data for shared memory clients to read. Parameters ---------- data : dict Add this data, replacing anything with the same key. """ self._remote.napari_data.update(data) def send_napari_message(self, message: dict) -> None: """Send a message to shared memory clients. Parameters ---------- message : dict Message to send to clients. """ self._remote.napari_messages.put(message)
class MonitorApi: """The API that monitor clients can access. MonitorApi will execute commands from the clients when it is polled. Shared Resources ---------------- Clients can access these shared resources via their SharedMemoryManager that connects to napari. shutdown : Event Signaled when napari is shutting down. commands : Queue Client can put "commands" on this queue. client_messages : Queue Clients receive messages on this queue. data : dict Generic data from monitor.add() Notes ----- The SharedMemoryManager provides the same proxy objects as SyncManager including list, dict, Barrier, BoundedSemaphore, Condition, Event, Lock, Namespace, Queue, RLock, Semaphore, Array, Value. SharedMemoryManager is derived from BaseManager, but it has similar functionality to SyncManager. See the docs for multiprocessing.managers.SyncManager. Parameters ---------- Layer : LayerList The viewer's layers. """ # BaseManager.register() is a bit weird. There must be a better wa # to do this. But most things including lambda result in pickling # errors due to multiprocessing stuff. # # So we create these and then use staticmethods as our callables. _napari_shutting_down_event = Event() _commands_queue = Queue() _client_messages_queue = Queue() _data_dict = dict() @staticmethod def _napari_shutting_down() -> Event: return MonitorApi._napari_shutting_down_event @staticmethod def _commands() -> Queue: return MonitorApi._commands_queue @staticmethod def _client_messages() -> Queue: return MonitorApi._client_messages_queue @staticmethod def _data() -> dict: return MonitorApi._data_dict def __init__(self): # We expose the run_command event so RemoteCommands can hook to it, # so it can execute commands we receive from clients. self.events = EmitterGroup(source=self, auto_connect=True, run_command=None) # Must register all callbacks before we create our instance of # SharedMemoryManager. SharedMemoryManager.register('napari_shutting_down', callable=self._napari_shutting_down) SharedMemoryManager.register('commands', callable=self._commands) SharedMemoryManager.register('client_messages', callable=self._client_messages) SharedMemoryManager.register('data', callable=self._data) # We ask for port 0 which means let the OS choose a port. We send # the chosen port to the client in its NAPARI_MON_CLIENT variable. self._manager = SharedMemoryManager(address=('127.0.0.1', 0), authkey=str.encode('napari')) self._manager.start() # Get the shared resources. self._remote = NapariRemoteAPI( self._manager.napari_shutting_down(), self._manager.commands(), self._manager.client_messages(), self._manager.data(), ) @property def manager(self) -> SharedMemoryManager: """Our shared memory manager.""" return self._manager def stop(self) -> None: """Notify clients we are shutting down.""" self._remote.napari_shutting_down.set() def poll(self): """Poll the MonitorApi for new commands, etc.""" assert self._manager is not None self._process_commands() def _process_commands(self) -> None: """Process every new command in the queue.""" while True: try: command = self._remote.commands.get_nowait() if not isinstance(command, dict): LOGGER.warning("Command was not a dict: %s", command) continue self.events.run_command(command=command) except Empty: return # No commands to process. def add(self, data: dict) -> None: """Add data for shared memory clients to read. Parameters ---------- data : dict Add this data, replacing anything with the same key. """ self._remote.data.update(data) def send(self, message: dict) -> None: """Send a message to shared memory clients.""" LOGGER.info("MonitorApi.send: %s", json.dumps(message)) self._remote.client_messages.put(message)