openrvdas.logger.readers.sealog_reader

No module-level documentation available.
  1#!/usr/bin/env python3
  2
  3import asyncio
  4import logging
  5import queue
  6import ssl
  7import json
  8import threading
  9import time
 10import websockets
 11
 12from logger.utils.das_record import DASRecord  # noqa: E402
 13from logger.utils.timestamp import timestamp  # noqa: E402
 14
 15
 16################################################################################
 17class SealogReader():
 18    """Connect to a websocket served by a WebsocketWriter, and service read()
 19    requests from it.
 20    """
 21
 22    def __init__(self, uri, client_wsid=None, subs=None, check_cert=False, **kwargs):
 23        """
 24        ```
 25        uri - Hostname, port and protocol, (e.g. wss://localhost:8080) of the
 26              target Sealog Server
 27
 28        client_wsid - Unique identifier, useful when running multiple
 29                      SealogReaders
 30
 31        subs - Websocket Pub/Subs to subscribe to when listening to the Sealog
 32               Server
 33
 34        check_cert - If True, and uri protocol is 'wss', check the server's TLS
 35                     certificate for validity; if a str, use as local filepath
 36                     location of .pem file to check against.
 37        ```
 38        """
 39        super().__init__(**kwargs)
 40
 41        self.uri = uri
 42        self.client_wsid = client_wsid or 'OpenRVDAS_Sealog_Reader'
 43        self.subs = subs or ['/ws/status/newEvents', '/ws/status/updateEvents']
 44        self.check_cert = check_cert
 45
 46        self.ping = {
 47            'type': 'ping',
 48            'id': self.client_wsid
 49        }
 50
 51        self.hello = {
 52            'type': 'hello',
 53            'id': self.client_wsid,
 54            'version': '2',
 55            'subs': self.subs
 56        }
 57
 58        # We won't initialize our websocket connection until the first read()
 59        # call. At that point we'll launch an async process in a separate
 60        # thread that will wait for data from the websocket and put it in a
 61        # queue that read() will pop from.
 62        self.websocket_thread = None
 63        self.queue = queue.Queue()
 64        self.quit_flag = False
 65
 66    ############################
 67    def _start_websocket(self):
 68        """We'll run this in a separate thread as soon as we get our first
 69        call to read()."""
 70
 71        ############################
 72        def _event_to_das_record(event) -> DASRecord:
 73
 74            fields = {
 75                'event_id': event.get('id'),
 76                'event_value': event.get('event_value'),
 77                'event_author': event.get('event_author'),
 78                'event_free_text': event.get('event_free_text'),
 79                **{
 80                    f"event_option_{option['event_option_name']}": option["event_option_value"]
 81                    for option in event.get("event_options", [])
 82                }
 83            }
 84
 85            return DASRecord(
 86                data_id=self.client_wsid,
 87                message_type='sealog_event',
 88                timestamp=timestamp(event.get('ts')),
 89                fields=fields
 90            )
 91
 92        ############################
 93        async def _websocket_loop(self):
 94            """Asynchronous inner function that will connect to websocket,
 95            iteratively try to read from it and put the result in our queue.
 96            """
 97            # Iterate if we lose the websocket for some reason other than a 'quit'
 98            while not self.quit_flag:
 99                try:
100                    if self.uri.find('wss://') == 0:  # using wss
101                        # If check_cert is a str, take it as the location of the
102                        # .pem file we'll check for validity. Otherwise, if not
103                        # False, take as a bool to verify by own means.
104                        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
105                        if self.check_cert:
106                            if isinstance(self.check_cert, str):
107                                ssl_context.load_verify_locations(self.check_cert)
108                            else:
109                                ssl_context.verify_mode = ssl.CERT_REQUIRED
110                        else:
111                            ssl_context.check_hostname = False
112                            ssl_context.verify_mode = ssl.CERT_NONE
113
114                    else:  # not using wss
115                        ssl_context = None
116
117                    logging.debug(f'SealogReader connecting to {self.uri}')
118                    async with websockets.connect(self.uri, ssl=ssl_context) as ws:  # type: ignore
119
120                        await ws.send(json.dumps(self.hello))
121                        logging.info(f'Connected to SealogReader at {self.uri}')
122
123                        while not self.quit_flag:
124                            msg = await ws.recv()
125                            msg_obj = json.loads(msg)
126
127                            if msg_obj['type'] and msg_obj['type'] == 'ping':
128                                await ws.send(json.dumps(self.ping))
129                                continue
130
131                            if msg_obj['type'] and msg_obj['type'] == 'pub':
132
133                                event = msg_obj['message']
134                                logging.debug(
135                                    f'SealogReader got record {json.dumps(event, indent=2)}'
136                                )
137                                self.queue.put(_event_to_das_record(event))
138
139                except BrokenPipeError:
140                    logging.info(f'SealogReader BrokenPipeError connecting to {self.uri}')
141                except AttributeError as e:
142                    logging.warning(f'SealogReader websocket loop error: {e}')
143                except websockets.exceptions.ConnectionClosed:  # type: ignore
144                    logging.info('SealogReader lost websocket connection to '
145                                 'data server; trying to reconnect.')
146                    await asyncio.sleep(0.2)
147                except OSError as e:
148                    logging.info('Unable to connect to websocket. Sleeping to try again...')
149                    logging.info(f'Connection error: {e}')
150                    await asyncio.sleep(2)
151
152        # In the outer function, get a new event loop and fire up the
153        # inner, async routine.
154        # Could we also use asyncio.ensure_future(_websocket_loop(self)) ?
155        websocket_event_loop = asyncio.new_event_loop()
156        websocket_event_loop.run_until_complete(_websocket_loop(self))
157        websocket_event_loop.close()
158
159    ############################
160    def quit(self, seconds=0):
161        """Sleep N seconds, then signal quit."""
162        time.sleep(seconds)
163        self.quit_flag = True
164
165    ############################
166    def read(self):
167        """Read/wait for data from the websocket."""
168
169        # If we've not yet fired up the websocket thread, do that now.
170        if not self.websocket_thread:
171            self.websocket_thread = threading.Thread(
172                name='websocket_thread',
173                target=self._start_websocket,
174                daemon=True)
175            self.websocket_thread.start()
176
177        # Use a timeout in our queue get() so we can periodically check if
178        # we've gotten a 'quit'
179        while not self.quit_flag:
180            try:
181                result = self.queue.get(block=True, timeout=2)
182                logging.debug('Got result from queue: %s', result)
183                return result
184            except queue.Empty:
185                logging.debug('get() timed out - trying again')
186
187        # If we've fallen out because of a quit...
188        return None
class SealogReader:
 18class SealogReader():
 19    """Connect to a websocket served by a WebsocketWriter, and service read()
 20    requests from it.
 21    """
 22
 23    def __init__(self, uri, client_wsid=None, subs=None, check_cert=False, **kwargs):
 24        """
 25        ```
 26        uri - Hostname, port and protocol, (e.g. wss://localhost:8080) of the
 27              target Sealog Server
 28
 29        client_wsid - Unique identifier, useful when running multiple
 30                      SealogReaders
 31
 32        subs - Websocket Pub/Subs to subscribe to when listening to the Sealog
 33               Server
 34
 35        check_cert - If True, and uri protocol is 'wss', check the server's TLS
 36                     certificate for validity; if a str, use as local filepath
 37                     location of .pem file to check against.
 38        ```
 39        """
 40        super().__init__(**kwargs)
 41
 42        self.uri = uri
 43        self.client_wsid = client_wsid or 'OpenRVDAS_Sealog_Reader'
 44        self.subs = subs or ['/ws/status/newEvents', '/ws/status/updateEvents']
 45        self.check_cert = check_cert
 46
 47        self.ping = {
 48            'type': 'ping',
 49            'id': self.client_wsid
 50        }
 51
 52        self.hello = {
 53            'type': 'hello',
 54            'id': self.client_wsid,
 55            'version': '2',
 56            'subs': self.subs
 57        }
 58
 59        # We won't initialize our websocket connection until the first read()
 60        # call. At that point we'll launch an async process in a separate
 61        # thread that will wait for data from the websocket and put it in a
 62        # queue that read() will pop from.
 63        self.websocket_thread = None
 64        self.queue = queue.Queue()
 65        self.quit_flag = False
 66
 67    ############################
 68    def _start_websocket(self):
 69        """We'll run this in a separate thread as soon as we get our first
 70        call to read()."""
 71
 72        ############################
 73        def _event_to_das_record(event) -> DASRecord:
 74
 75            fields = {
 76                'event_id': event.get('id'),
 77                'event_value': event.get('event_value'),
 78                'event_author': event.get('event_author'),
 79                'event_free_text': event.get('event_free_text'),
 80                **{
 81                    f"event_option_{option['event_option_name']}": option["event_option_value"]
 82                    for option in event.get("event_options", [])
 83                }
 84            }
 85
 86            return DASRecord(
 87                data_id=self.client_wsid,
 88                message_type='sealog_event',
 89                timestamp=timestamp(event.get('ts')),
 90                fields=fields
 91            )
 92
 93        ############################
 94        async def _websocket_loop(self):
 95            """Asynchronous inner function that will connect to websocket,
 96            iteratively try to read from it and put the result in our queue.
 97            """
 98            # Iterate if we lose the websocket for some reason other than a 'quit'
 99            while not self.quit_flag:
100                try:
101                    if self.uri.find('wss://') == 0:  # using wss
102                        # If check_cert is a str, take it as the location of the
103                        # .pem file we'll check for validity. Otherwise, if not
104                        # False, take as a bool to verify by own means.
105                        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
106                        if self.check_cert:
107                            if isinstance(self.check_cert, str):
108                                ssl_context.load_verify_locations(self.check_cert)
109                            else:
110                                ssl_context.verify_mode = ssl.CERT_REQUIRED
111                        else:
112                            ssl_context.check_hostname = False
113                            ssl_context.verify_mode = ssl.CERT_NONE
114
115                    else:  # not using wss
116                        ssl_context = None
117
118                    logging.debug(f'SealogReader connecting to {self.uri}')
119                    async with websockets.connect(self.uri, ssl=ssl_context) as ws:  # type: ignore
120
121                        await ws.send(json.dumps(self.hello))
122                        logging.info(f'Connected to SealogReader at {self.uri}')
123
124                        while not self.quit_flag:
125                            msg = await ws.recv()
126                            msg_obj = json.loads(msg)
127
128                            if msg_obj['type'] and msg_obj['type'] == 'ping':
129                                await ws.send(json.dumps(self.ping))
130                                continue
131
132                            if msg_obj['type'] and msg_obj['type'] == 'pub':
133
134                                event = msg_obj['message']
135                                logging.debug(
136                                    f'SealogReader got record {json.dumps(event, indent=2)}'
137                                )
138                                self.queue.put(_event_to_das_record(event))
139
140                except BrokenPipeError:
141                    logging.info(f'SealogReader BrokenPipeError connecting to {self.uri}')
142                except AttributeError as e:
143                    logging.warning(f'SealogReader websocket loop error: {e}')
144                except websockets.exceptions.ConnectionClosed:  # type: ignore
145                    logging.info('SealogReader lost websocket connection to '
146                                 'data server; trying to reconnect.')
147                    await asyncio.sleep(0.2)
148                except OSError as e:
149                    logging.info('Unable to connect to websocket. Sleeping to try again...')
150                    logging.info(f'Connection error: {e}')
151                    await asyncio.sleep(2)
152
153        # In the outer function, get a new event loop and fire up the
154        # inner, async routine.
155        # Could we also use asyncio.ensure_future(_websocket_loop(self)) ?
156        websocket_event_loop = asyncio.new_event_loop()
157        websocket_event_loop.run_until_complete(_websocket_loop(self))
158        websocket_event_loop.close()
159
160    ############################
161    def quit(self, seconds=0):
162        """Sleep N seconds, then signal quit."""
163        time.sleep(seconds)
164        self.quit_flag = True
165
166    ############################
167    def read(self):
168        """Read/wait for data from the websocket."""
169
170        # If we've not yet fired up the websocket thread, do that now.
171        if not self.websocket_thread:
172            self.websocket_thread = threading.Thread(
173                name='websocket_thread',
174                target=self._start_websocket,
175                daemon=True)
176            self.websocket_thread.start()
177
178        # Use a timeout in our queue get() so we can periodically check if
179        # we've gotten a 'quit'
180        while not self.quit_flag:
181            try:
182                result = self.queue.get(block=True, timeout=2)
183                logging.debug('Got result from queue: %s', result)
184                return result
185            except queue.Empty:
186                logging.debug('get() timed out - trying again')
187
188        # If we've fallen out because of a quit...
189        return None

Connect to a websocket served by a WebsocketWriter, and service read() requests from it.

SealogReader(uri, client_wsid=None, subs=None, check_cert=False, **kwargs)
23    def __init__(self, uri, client_wsid=None, subs=None, check_cert=False, **kwargs):
24        """
25        ```
26        uri - Hostname, port and protocol, (e.g. wss://localhost:8080) of the
27              target Sealog Server
28
29        client_wsid - Unique identifier, useful when running multiple
30                      SealogReaders
31
32        subs - Websocket Pub/Subs to subscribe to when listening to the Sealog
33               Server
34
35        check_cert - If True, and uri protocol is 'wss', check the server's TLS
36                     certificate for validity; if a str, use as local filepath
37                     location of .pem file to check against.
38        ```
39        """
40        super().__init__(**kwargs)
41
42        self.uri = uri
43        self.client_wsid = client_wsid or 'OpenRVDAS_Sealog_Reader'
44        self.subs = subs or ['/ws/status/newEvents', '/ws/status/updateEvents']
45        self.check_cert = check_cert
46
47        self.ping = {
48            'type': 'ping',
49            'id': self.client_wsid
50        }
51
52        self.hello = {
53            'type': 'hello',
54            'id': self.client_wsid,
55            'version': '2',
56            'subs': self.subs
57        }
58
59        # We won't initialize our websocket connection until the first read()
60        # call. At that point we'll launch an async process in a separate
61        # thread that will wait for data from the websocket and put it in a
62        # queue that read() will pop from.
63        self.websocket_thread = None
64        self.queue = queue.Queue()
65        self.quit_flag = False
uri - Hostname, port and protocol, (e.g. wss://localhost:8080) of the
      target Sealog Server

client_wsid - Unique identifier, useful when running multiple
              SealogReaders

subs - Websocket Pub/Subs to subscribe to when listening to the Sealog
       Server

check_cert - If True, and uri protocol is 'wss', check the server's TLS
             certificate for validity; if a str, use as local filepath
             location of .pem file to check against.
uri
client_wsid
subs
check_cert
ping
hello
websocket_thread
queue
quit_flag
def quit(self, seconds=0):
161    def quit(self, seconds=0):
162        """Sleep N seconds, then signal quit."""
163        time.sleep(seconds)
164        self.quit_flag = True

Sleep N seconds, then signal quit.

def read(self):
167    def read(self):
168        """Read/wait for data from the websocket."""
169
170        # If we've not yet fired up the websocket thread, do that now.
171        if not self.websocket_thread:
172            self.websocket_thread = threading.Thread(
173                name='websocket_thread',
174                target=self._start_websocket,
175                daemon=True)
176            self.websocket_thread.start()
177
178        # Use a timeout in our queue get() so we can periodically check if
179        # we've gotten a 'quit'
180        while not self.quit_flag:
181            try:
182                result = self.queue.get(block=True, timeout=2)
183                logging.debug('Got result from queue: %s', result)
184                return result
185            except queue.Empty:
186                logging.debug('get() timed out - trying again')
187
188        # If we've fallen out because of a quit...
189        return None

Read/wait for data from the websocket.