Example #1
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)
Example #2
0
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()