Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)