From 007c50fbc88fd4853b1dbe4f59f6a1989694413d Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Wed, 28 Apr 2021 05:29:06 +0000 Subject: [PATCH] Add /status endpoint for monitoring (cf. irclog commit 5b809b1b) --- http2irc.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/http2irc.py b/http2irc.py index e8c8c4c..9225aa7 100644 --- a/http2irc.py +++ b/http2irc.py @@ -148,6 +148,8 @@ class Config(dict): raise InvalidConfig(f'Invalid map {key!r} web path: not a string') if not map_['webpath'].startswith('/'): raise InvalidConfig(f'Invalid map {key!r} web path: does not start at the root') + if map_['webpath'] == '/status': + raise InvalidConfig(f'Invalid map {key!r} web path: cannot be "/status"') if map_['webpath'] in seenWebPaths: raise InvalidConfig(f'Invalid map {key!r} web path: collides with map {seenWebPaths[map_["webpath"]]!r}') seenWebPaths[map_['webpath']] = key @@ -312,6 +314,7 @@ class IRCClientProtocol(asyncio.Protocol): self.connectionClosedEvent = connectionClosedEvent self.loop = loop self.config = config + self.lastRecvTime = None self.buffer = b'' self.connected = False self.channels = channels # Currently joined/supposed-to-be-joined channels; set(str) @@ -495,6 +498,7 @@ class IRCClientProtocol(asyncio.Protocol): def data_received(self, data): self.logger.debug(f'Data received: {data!r}') + self.lastRecvTime = time.time() # If there's any data left in the buffer, prepend it to the data. Split on CRLF. # Then, process all messages except the last one (since data might not end on a CRLF) and keep the remainder in the buffer. # If data does end with CRLF, all messages will have been processed and the buffer will be empty again. @@ -647,24 +651,34 @@ class IRCClient: await wait_cancel_pending({asyncio.create_task(connectionClosedEvent.wait()), asyncio.create_task(sigintEvent.wait())}, return_when = concurrent.futures.FIRST_COMPLETED) finally: self._transport.close() #TODO BaseTransport.close is asynchronous and then triggers the protocol's connection_lost callback; need to wait for connectionClosedEvent again perhaps to correctly handle ^C? + self._transport = None + self._protocol = None except (ConnectionRefusedError, asyncio.TimeoutError) as e: self.logger.error(str(e)) await wait_cancel_pending({asyncio.create_task(sigintEvent.wait())}, timeout = 5) if sigintEvent.is_set(): break + @property + def lastRecvTime(self): + return self._protocol.lastRecvTime if self._protocol else None + class WebServer: logger = logging.getLogger('http2irc.WebServer') - def __init__(self, messageQueue, config): + def __init__(self, messageQueue, ircClient, config): self.messageQueue = messageQueue + self.ircClient = ircClient self.config = config self._paths = {} # '/path' => ('#channel', auth, module, moduleargs) where auth is either False (no authentication) or the HTTP header value for basic auth self._app = aiohttp.web.Application() - self._app.add_routes([aiohttp.web.post('/{path:.+}', self.post)]) + self._app.add_routes([ + aiohttp.web.get('/status', self.get_status), + aiohttp.web.post('/{path:.+}', self.post) + ]) self.update_config(config) self._configChanged = asyncio.Event() @@ -688,6 +702,10 @@ class WebServer: break self._configChanged.clear() + async def get_status(self, request): + self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') + return (aiohttp.web.Response if (self.ircClient.lastRecvTime or 0) > time.time() - 600 else aiohttp.web.HTTPInternalServerError)() + async def post(self, request): self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r} with body {(await request.read())!r}') try: @@ -761,7 +779,7 @@ async def main(): messageQueue = MessageQueue() irc = IRCClient(messageQueue, config) - webserver = WebServer(messageQueue, config) + webserver = WebServer(messageQueue, irc, config) sigintEvent = asyncio.Event() def sigint_callback():