From cc8674d10846c67b17930f37ca8a0c419d17ee54 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:19:10 +0200 Subject: [PATCH 01/15] add exposed ports and healthcheck --- app/Dockerfile | 5 ++++- docker-compose.yaml | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index 0ef685e..76816df 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -64,7 +64,10 @@ COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY config . COPY src . RUN date > /build-date.txt -EXPOSE 5005 +EXPOSE 5005 8127 10000 + +# HEALTHCHECK --interval=10s --timeout=3s \ +# CMD wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1 # command to run on container start ENTRYPOINT ["/root/entrypoint.sh"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 4566a80..b90b881 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -77,10 +77,15 @@ services: - ${DNS2:-4.4.4.4} ports: - 5005:5005 + - 8127:8127 - 10000:10000 volumes: - ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log - ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1 + interval: 10s + timeout: 3s networks: - outside From dd351176bddd8c71a61ccb5e2722d9f518c0cdd7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:20:38 +0200 Subject: [PATCH 02/15] add wget for healthcheck --- app/hardening_final.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/app/hardening_final.sh b/app/hardening_final.sh index c6896bb..279e1b6 100644 --- a/app/hardening_final.sh +++ b/app/hardening_final.sh @@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \ -name od -o \ -name strings -o \ -name su -o \ - -name wget -o \ \) -delete \ ; fi From a16a19cc2cd179c1c1bc2146850d6b3738eb4484 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:21:15 +0200 Subject: [PATCH 03/15] add aiohttp --- app/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index b151101..ed9dcb0 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,4 @@ aiomqtt==2.0.1 schema==0.7.5 - aiocron==1.8 \ No newline at end of file + aiocron==1.8 + aiohttp==3.9.5 \ No newline at end of file From ae94cd62fc12f4d37a26ff2a5a3e7da452550b26 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:23:57 +0200 Subject: [PATCH 04/15] use config validation for healthcheck --- app/src/config.py | 12 +++++++----- app/tests/test_config.py | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/config.py b/app/src/config.py index e1ef749..115b1fe 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -3,6 +3,7 @@ import shutil import tomllib import logging +from typing import Tuple from schema import Schema, And, Or, Use, Optional @@ -84,7 +85,7 @@ class Config(): ) @classmethod - def class_init(cls): # pragma: no cover + def class_init(cls) -> None | str: # pragma: no cover try: # make the default config transparaent by copying it # in the config.example file @@ -94,11 +95,12 @@ class Config(): "config/config.example.toml") except Exception: pass - cls.read() + return cls.read() @classmethod - def _read_config_file(cls) -> dict: # pragma: no cover + def _read_config_file(cls) -> Tuple[dict, None | str]: # pragma: no cover usr_config = {} + err = None try: with open("config/config.toml", "rb") as f: @@ -110,7 +112,7 @@ class Config(): '\n To create the missing config.toml file, ' 'you can rename the template config.example.toml\n' ' and customize it for your scenario.\n') - return usr_config + return usr_config, err @classmethod def read(cls, path='') -> None | str: @@ -129,7 +131,7 @@ class Config(): # overwrite the default values, with values from # the config.toml file - usr_config = cls._read_config_file() + usr_config, err = cls._read_config_file() # merge the default and the user config config = def_config.copy() diff --git a/app/tests/test_config.py b/app/tests/test_config.py index 746d1d8..a9d598b 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -2,6 +2,7 @@ import tomllib from schema import SchemaMissingKeyError from app.src.config import Config +from typing import Tuple class TstConfig(Config): @@ -10,8 +11,8 @@ class TstConfig(Config): cls.config = cnf @classmethod - def _read_config_file(cls) -> dict: - return cls.config + def _read_config_file(cls) -> Tuple[dict, str| None]: + return cls.config, None def test_empty_config(): From ff3ed83b49f644291f863cddb497e52fcf2487f2 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:29:27 +0200 Subject: [PATCH 05/15] add http server for healthcheck --- app/src/server.py | 71 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/app/src/server.py b/app/src/server.py index f575194..27b90da 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -3,6 +3,7 @@ import asyncio import signal import os from asyncio import StreamReader, StreamWriter +from aiohttp import web from logging import config # noqa F401 from messages import Message from inverter import Inverter @@ -11,6 +12,51 @@ from gen3plus.inverter_g3p import InverterG3P from scheduler import Schedule from config import Config +routes = web.RouteTableDef() +proxy_is_up = False + + +@routes.get('/') +async def hello(request): + return web.Response(text="Hello, world") + + +@routes.get('/-/ready') +async def ready(request): + if proxy_is_up: + status = 200 + text = 'Is ready' + else: + status = 503 + text = 'Not ready' + return web.Response(status=status, text=text) + + +@routes.get('/-/healthy') +async def healthy(request): + + if proxy_is_up: + # logging.info('web reqeust healthy()') + for stream in Message: + try: + res = stream.healthy() + if not res: + return web.Response(status=503, text="I have a problem") + except Exception as err: + logging.info(f'Exception:{err}') + + return web.Response(status=200, text="I'm fine") + + +async def webserver(runner, addr, port): + await runner.setup() + site = web.TCPSite(runner, addr, port) + await site.start() + logging.info(f'HTTP server listen on port: {port}') + + while True: + await asyncio.sleep(3600) # sleep forever + async def handle_client(reader: StreamReader, writer: StreamWriter): '''Handles a new incoming connection and starts an async loop''' @@ -26,10 +72,12 @@ async def handle_client_v2(reader: StreamReader, writer: StreamWriter): await InverterG3P(reader, writer, addr).server_loop(addr) -async def handle_shutdown(loop): +async def handle_shutdown(loop, runner): '''Close all TCP connections and stop the event loop''' logging.info('Shutdown due to SIGTERM') + await runner.cleanup() + logging.info('HTTP server stopped') # # first, disc all open TCP connections gracefully @@ -87,15 +135,22 @@ if __name__ == "__main__": logging.getLogger('tracer').setLevel(log_level) # logging.getLogger('mqtt').setLevel(log_level) - # read config file - Config.class_init() - loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + # read config file + ConfigErr = Config.class_init() + logging.info(f'ConfigErr: {ConfigErr}') Inverter.class_init() Schedule.start() + # + # Setup webserver application and runner + # + app = web.Application() + app.add_routes(routes) + runner = web.AppRunner(app) + # # Register some UNIX Signal handler for a gracefully server shutdown # on Docker restart and stop @@ -103,21 +158,25 @@ if __name__ == "__main__": for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), lambda loop=loop: asyncio.create_task( - handle_shutdown(loop))) + handle_shutdown(loop, runner))) # - # Create taska for our listening servera. These must be tasks! If we call + # Create tasks for our listening servers. These must be tasks! If we call # start_server directly out of our main task, the eventloop will be blocked # and we can't receive and handle the UNIX signals! # loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005)) loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000)) + loop.create_task(webserver(runner, '0.0.0.0', 8127)) try: + if ConfigErr is None: + proxy_is_up = True loop.run_forever() except KeyboardInterrupt: pass finally: + proxy_is_up = False Inverter.class_close(loop) logging.info('Close event loop') loop.close() From ac534c20ed22b6108ebc6c012b527dbd74156ae1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:34:11 +0200 Subject: [PATCH 06/15] calculate msg prossesing time --- app/src/async_stream.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 1bcabf3..39d0cdb 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -1,5 +1,6 @@ import logging import traceback +import time from asyncio import StreamReader, StreamWriter from messages import hex_dump_memory @@ -16,6 +17,8 @@ class AsyncStream(): self.addr = addr self.r_addr = '' self.l_addr = '' + self.proc_start = None # start processing start timestamp + self.proc_max = 0 async def server_loop(self, addr): '''Loop for receiving messages from the inverter (server-side)''' @@ -59,9 +62,14 @@ class AsyncStream(): async def loop(self): self.r_addr = self.writer.get_extra_info('peername') self.l_addr = self.writer.get_extra_info('sockname') - + self.proc_start = time.time() while True: try: + proc = time.time() - self.proc_start + if proc > self.proc_max: + self.proc_max = proc + self.proc_start = None + await self.__async_read() if self.unique_id: @@ -101,12 +109,22 @@ class AsyncStream(): logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}') self.writer.close() + def healthy(self) -> bool: + elapsed = 0 + if self.proc_start is not None: + elapsed = time.time() - self.proc_start + logging.debug('async_stream healthy() elapsed: ' + f'{round(1000*elapsed)}ms' + f' max:{round(1000*self.proc_max)}ms') + return True + ''' Our private methods ''' async def __async_read(self) -> None: data = await self.reader.read(4096) if data: + self.proc_start = time.time() self._recv_buffer += data self.read() # call read in parent class else: From dbff66affd56edadf39c4418176f9aa81ef8ab6f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Jun 2024 23:36:59 +0200 Subject: [PATCH 07/15] add healthy check methods --- app/src/gen3/connection_g3.py | 4 ++++ app/src/gen3plus/connection_g3p.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index 7730069..7e7b96d 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -31,6 +31,10 @@ class ConnectionG3(AsyncStream, Talent): async def async_publ_mqtt(self) -> None: pass + def healthy(self) -> bool: + logger.debug('ConnectionG3 healthy()') + return AsyncStream.healthy(self) + ''' Our private methods ''' diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py index ecc5625..352ba5e 100644 --- a/app/src/gen3plus/connection_g3p.py +++ b/app/src/gen3plus/connection_g3p.py @@ -31,6 +31,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5): async def async_publ_mqtt(self) -> None: pass + def healthy(self) -> bool: + logger.debug('ConnectionG3P healthy()') + return AsyncStream.healthy(self) + ''' Our private methods ''' From f629246dbd0408767d762f7cc71dbf3784f99bb1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 01:18:06 +0200 Subject: [PATCH 08/15] fix typo --- app/src/gen3/inverter_g3.py | 2 +- app/src/server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/gen3/inverter_g3.py b/app/src/gen3/inverter_g3.py index 6f72ca9..a98f6b5 100644 --- a/app/src/gen3/inverter_g3.py +++ b/app/src/gen3/inverter_g3.py @@ -122,7 +122,7 @@ class InverterG3(Inverter, ConnectionG3): def close(self) -> None: logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}') super().close() # call close handler in the parent class -# logger.debug (f'Inverter refs: {gc.get_referrers(self)}') +# logging.info(f'Inverter refs: {gc.get_referrers(self)}') def __del__(self): logging.debug("InverterG3.__del__") diff --git a/app/src/server.py b/app/src/server.py index 27b90da..76a4314 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -36,7 +36,7 @@ async def ready(request): async def healthy(request): if proxy_is_up: - # logging.info('web reqeust healthy()') + # logging.info('web request healthy()') for stream in Message: try: res = stream.healthy() From 10346e888fec14e68e46f4438603503345bd10bf Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 01:52:34 +0200 Subject: [PATCH 09/15] log ConfigErr with DEBUG level --- app/src/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server.py b/app/src/server.py index 76a4314..9d48640 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -140,7 +140,7 @@ if __name__ == "__main__": # read config file ConfigErr = Config.class_init() - logging.info(f'ConfigErr: {ConfigErr}') + logging.debug(f'ConfigErr: {ConfigErr}') Inverter.class_init() Schedule.start() From b053c7e576272a9176648190830cb54adcf737ea Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 16 Jun 2024 02:08:15 +0200 Subject: [PATCH 10/15] Update async_stream.py - check if processing time is < 5 sec --- app/src/async_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 39d0cdb..513ab8e 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -116,7 +116,7 @@ class AsyncStream(): logging.debug('async_stream healthy() elapsed: ' f'{round(1000*elapsed)}ms' f' max:{round(1000*self.proc_max)}ms') - return True + return elapsed < 5 ''' Our private methods From f9b02f34861e3b3fb6afe9b961f09200aab2c35b Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 11:56:03 +0200 Subject: [PATCH 11/15] add a close handler to release internal resources --- app/src/modbus.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/modbus.py b/app/src/modbus.py index 8f3778b..fdb7c24 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -105,7 +105,16 @@ class Modbus(): self.req_pend = False self.tim = None + def close(self): + """free the queue and the callback handler""" + logging.debug('Modbus close:') + self.rsp_handler = None + self.snd_handler = None + while not self.que.empty: + self.que.get_nowait() + def __del__(self): + """log statistics on the deleting of a MODBUS instance""" logging.debug(f'Modbus __del__:\n {self.counter}') def build_msg(self, addr: int, func: int, reg: int, val: int, From 453d8b2aa226cea462914f9c4b1bbd6755950850 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 11:57:51 +0200 Subject: [PATCH 12/15] call modbus close hanlder on a close call --- app/src/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/messages.py b/app/src/messages.py index dbe24b5..6553f36 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -97,7 +97,7 @@ class Message(metaclass=IterRegistry): ''' def close(self) -> None: if self.mb: - del self.mb + self.mb.close() self.mb = None pass # pragma: no cover From b688d048369eaff42b07186513b8bbd3264e963b Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 13:00:02 +0200 Subject: [PATCH 13/15] isolate Modbus fix --- app/Dockerfile | 5 +-- app/hardening_final.sh | 1 + app/requirements.txt | 3 +- app/src/async_stream.py | 19 -------- app/src/config.py | 12 +++--- app/src/gen3/connection_g3.py | 4 -- app/src/gen3plus/connection_g3p.py | 4 -- app/src/server.py | 69 +++--------------------------- app/tests/test_config.py | 5 +-- docker-compose.yaml | 5 --- 10 files changed, 15 insertions(+), 112 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 76816df..0ef685e 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -64,10 +64,7 @@ COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY config . COPY src . RUN date > /build-date.txt -EXPOSE 5005 8127 10000 - -# HEALTHCHECK --interval=10s --timeout=3s \ -# CMD wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1 +EXPOSE 5005 # command to run on container start ENTRYPOINT ["/root/entrypoint.sh"] diff --git a/app/hardening_final.sh b/app/hardening_final.sh index 279e1b6..c6896bb 100644 --- a/app/hardening_final.sh +++ b/app/hardening_final.sh @@ -17,5 +17,6 @@ if [ "$environment" = "production" ] ; then \ -name od -o \ -name strings -o \ -name su -o \ + -name wget -o \ \) -delete \ ; fi diff --git a/app/requirements.txt b/app/requirements.txt index ed9dcb0..b151101 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,4 +1,3 @@ aiomqtt==2.0.1 schema==0.7.5 - aiocron==1.8 - aiohttp==3.9.5 \ No newline at end of file + aiocron==1.8 \ No newline at end of file diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 7cbca8e..3deeb43 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -1,6 +1,5 @@ import logging import traceback -import time from asyncio import StreamReader, StreamWriter from messages import hex_dump_memory from typing import Self @@ -18,8 +17,6 @@ class AsyncStream(): self.addr = addr self.r_addr = '' self.l_addr = '' - self.proc_start = None # start processing start timestamp - self.proc_max = 0 async def server_loop(self, addr: str) -> None: '''Loop for receiving messages from the inverter (server-side)''' @@ -64,14 +61,8 @@ class AsyncStream(): """Async loop handler for precessing all received messages""" self.r_addr = self.writer.get_extra_info('peername') self.l_addr = self.writer.get_extra_info('sockname') - self.proc_start = time.time() while True: try: - proc = time.time() - self.proc_start - if proc > self.proc_max: - self.proc_max = proc - self.proc_start = None - await self.__async_read() if self.unique_id: @@ -126,15 +117,6 @@ class AsyncStream(): logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}') self.writer.close() - def healthy(self) -> bool: - elapsed = 0 - if self.proc_start is not None: - elapsed = time.time() - self.proc_start - logging.debug('async_stream healthy() elapsed: ' - f'{round(1000*elapsed)}ms' - f' max:{round(1000*self.proc_max)}ms') - return elapsed < 5 - ''' Our private methods ''' @@ -142,7 +124,6 @@ class AsyncStream(): """Async read handler to read received data from TCP stream""" data = await self.reader.read(4096) if data: - self.proc_start = time.time() self._recv_buffer += data self.read() # call read in parent class else: diff --git a/app/src/config.py b/app/src/config.py index 115b1fe..e1ef749 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -3,7 +3,6 @@ import shutil import tomllib import logging -from typing import Tuple from schema import Schema, And, Or, Use, Optional @@ -85,7 +84,7 @@ class Config(): ) @classmethod - def class_init(cls) -> None | str: # pragma: no cover + def class_init(cls): # pragma: no cover try: # make the default config transparaent by copying it # in the config.example file @@ -95,12 +94,11 @@ class Config(): "config/config.example.toml") except Exception: pass - return cls.read() + cls.read() @classmethod - def _read_config_file(cls) -> Tuple[dict, None | str]: # pragma: no cover + def _read_config_file(cls) -> dict: # pragma: no cover usr_config = {} - err = None try: with open("config/config.toml", "rb") as f: @@ -112,7 +110,7 @@ class Config(): '\n To create the missing config.toml file, ' 'you can rename the template config.example.toml\n' ' and customize it for your scenario.\n') - return usr_config, err + return usr_config @classmethod def read(cls, path='') -> None | str: @@ -131,7 +129,7 @@ class Config(): # overwrite the default values, with values from # the config.toml file - usr_config, err = cls._read_config_file() + usr_config = cls._read_config_file() # merge the default and the user config config = def_config.copy() diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index 7e7b96d..7730069 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -31,10 +31,6 @@ class ConnectionG3(AsyncStream, Talent): async def async_publ_mqtt(self) -> None: pass - def healthy(self) -> bool: - logger.debug('ConnectionG3 healthy()') - return AsyncStream.healthy(self) - ''' Our private methods ''' diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py index 352ba5e..ecc5625 100644 --- a/app/src/gen3plus/connection_g3p.py +++ b/app/src/gen3plus/connection_g3p.py @@ -31,10 +31,6 @@ class ConnectionG3P(AsyncStream, SolarmanV5): async def async_publ_mqtt(self) -> None: pass - def healthy(self) -> bool: - logger.debug('ConnectionG3P healthy()') - return AsyncStream.healthy(self) - ''' Our private methods ''' diff --git a/app/src/server.py b/app/src/server.py index 9d48640..e941975 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -3,7 +3,6 @@ import asyncio import signal import os from asyncio import StreamReader, StreamWriter -from aiohttp import web from logging import config # noqa F401 from messages import Message from inverter import Inverter @@ -12,51 +11,6 @@ from gen3plus.inverter_g3p import InverterG3P from scheduler import Schedule from config import Config -routes = web.RouteTableDef() -proxy_is_up = False - - -@routes.get('/') -async def hello(request): - return web.Response(text="Hello, world") - - -@routes.get('/-/ready') -async def ready(request): - if proxy_is_up: - status = 200 - text = 'Is ready' - else: - status = 503 - text = 'Not ready' - return web.Response(status=status, text=text) - - -@routes.get('/-/healthy') -async def healthy(request): - - if proxy_is_up: - # logging.info('web request healthy()') - for stream in Message: - try: - res = stream.healthy() - if not res: - return web.Response(status=503, text="I have a problem") - except Exception as err: - logging.info(f'Exception:{err}') - - return web.Response(status=200, text="I'm fine") - - -async def webserver(runner, addr, port): - await runner.setup() - site = web.TCPSite(runner, addr, port) - await site.start() - logging.info(f'HTTP server listen on port: {port}') - - while True: - await asyncio.sleep(3600) # sleep forever - async def handle_client(reader: StreamReader, writer: StreamWriter): '''Handles a new incoming connection and starts an async loop''' @@ -72,12 +26,10 @@ async def handle_client_v2(reader: StreamReader, writer: StreamWriter): await InverterG3P(reader, writer, addr).server_loop(addr) -async def handle_shutdown(loop, runner): +async def handle_shutdown(loop): '''Close all TCP connections and stop the event loop''' logging.info('Shutdown due to SIGTERM') - await runner.cleanup() - logging.info('HTTP server stopped') # # first, disc all open TCP connections gracefully @@ -135,22 +87,15 @@ if __name__ == "__main__": logging.getLogger('tracer').setLevel(log_level) # logging.getLogger('mqtt').setLevel(log_level) + # read config file + Config.class_init() + loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - # read config file - ConfigErr = Config.class_init() - logging.debug(f'ConfigErr: {ConfigErr}') Inverter.class_init() Schedule.start() - # - # Setup webserver application and runner - # - app = web.Application() - app.add_routes(routes) - runner = web.AppRunner(app) - # # Register some UNIX Signal handler for a gracefully server shutdown # on Docker restart and stop @@ -158,7 +103,7 @@ if __name__ == "__main__": for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), lambda loop=loop: asyncio.create_task( - handle_shutdown(loop, runner))) + handle_shutdown(loop))) # # Create tasks for our listening servers. These must be tasks! If we call @@ -167,16 +112,12 @@ if __name__ == "__main__": # loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005)) loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000)) - loop.create_task(webserver(runner, '0.0.0.0', 8127)) try: - if ConfigErr is None: - proxy_is_up = True loop.run_forever() except KeyboardInterrupt: pass finally: - proxy_is_up = False Inverter.class_close(loop) logging.info('Close event loop') loop.close() diff --git a/app/tests/test_config.py b/app/tests/test_config.py index a9d598b..746d1d8 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -2,7 +2,6 @@ import tomllib from schema import SchemaMissingKeyError from app.src.config import Config -from typing import Tuple class TstConfig(Config): @@ -11,8 +10,8 @@ class TstConfig(Config): cls.config = cnf @classmethod - def _read_config_file(cls) -> Tuple[dict, str| None]: - return cls.config, None + def _read_config_file(cls) -> dict: + return cls.config def test_empty_config(): diff --git a/docker-compose.yaml b/docker-compose.yaml index b90b881..4566a80 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -77,15 +77,10 @@ services: - ${DNS2:-4.4.4.4} ports: - 5005:5005 - - 8127:8127 - 10000:10000 volumes: - ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log - ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config - healthcheck: - test: wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1 - interval: 10s - timeout: 3s networks: - outside From 80183598caa432f9aae276efe310171c46ac0530 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 13:03:33 +0200 Subject: [PATCH 14/15] cleanup --- app/src/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/modbus.py b/app/src/modbus.py index fdb7c24..57eb272 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -106,7 +106,7 @@ class Modbus(): self.tim = None def close(self): - """free the queue and the callback handler""" + """free the queue and erase the callback handlers""" logging.debug('Modbus close:') self.rsp_handler = None self.snd_handler = None From 9138affdb94c428b7e96dee34e04cd90c4561c8a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Jun 2024 13:05:20 +0200 Subject: [PATCH 15/15] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a9ac8..1d12db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93) - add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94) - print imgae build time during proxy start - add type annotations