diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b5191d..fcdbd51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,10 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - python-version: [3.9, 3.11] + python-version: [3.9, 3.12, 3.14] steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index 773c6b4..6eaf862 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Monitor :target: https://github.com/ReCodEx/wiki/wiki/Changelog Monitor is an optional part of the ReCodEx solution for reporting progress of -job evaluation back to users in the real time. It is a daemon that reads status messages of all running job evaluations from one ZeroMQ socket and send them to proper WebSocket connection. Monitor is written in Python, tested versions are 3.4 and 3.5. +job evaluation back to users in the real time. It is a daemon that reads status messages of all running job evaluations from one ZeroMQ socket and send them to proper WebSocket connection. Monitor is written in Python (>= 3.9). There is just one monitor instance required per broker. Also, monitor has to be publicly visible (has to have public IP address or be behind public proxy @@ -64,7 +64,7 @@ Installation will provide you following files: - `/etc/recodex/monitor/config.yml` -- configuration file - `/etc/systemd/system/recodex-monitor.service` -- systemd startup script - code files will be installed in location depending on your system settings, - mostly into `/usr/lib/python3.5/site-packages/monitor/` or similar + mostly into `/usr/lib/python3.9/site-packages/monitor/` or similar Systemd script runs monitor binary as specific _recodex_ user, so in `postinst` script user and group of this name are created. Also, ownership of configuration diff --git a/monitor/test/test_ConfigManager.py b/monitor/test/test_ConfigManager.py index c1a529e..1989a15 100644 --- a/monitor/test/test_ConfigManager.py +++ b/monitor/test/test_ConfigManager.py @@ -18,6 +18,7 @@ def test_websock_uri_default(self): def test_websock_uri_loaded(self): handle, path = tempfile.mkstemp() + os.close(handle) with open(path, 'w') as f: f.write('websocket_uri:\n - 77.75.76.3\n - 8080') @@ -35,6 +36,7 @@ def test_zeromq_uri_default(self): def test_zeromq_uri_loaded(self): handle, path = tempfile.mkstemp() + os.close(handle) with open(path, 'w') as f: f.write('zeromq_uri:\n - 77.75.76.3\n - 8080') @@ -52,6 +54,7 @@ def test_logger_path_default(self): def test_logger_path_loaded(self): handle, path = tempfile.mkstemp() + os.close(handle) with open(path, 'w') as f: f.write('logger:\n file: /var/log/tmp/file.log\n level: "debug"\n max-size: 564\n rotations: 7') @@ -78,6 +81,7 @@ def test_invalid_path(self): def test_valid_creation(self): handle, path = tempfile.mkstemp() + os.close(handle) logger = init_logger(path, logging.WARNING, 450, 2) logger.debug("aaa") logger.warning("bbb") @@ -90,4 +94,8 @@ def test_valid_creation(self): # expect 5 lines - 3 of header and one with 'bbb' and one 'ccc' self.assertEqual(len(content), 5) + for handler in list(logger.handlers): + handler.close() + logger.removeHandler(handler) + os.remove(path) diff --git a/monitor/test/test_websock_server.py b/monitor/test/test_websock_server.py index b20e6a3..08f5108 100644 --- a/monitor/test/test_websock_server.py +++ b/monitor/test/test_websock_server.py @@ -8,7 +8,7 @@ class TestWebsocketServer(unittest.TestCase): @patch('asyncio.set_event_loop') - @patch('websockets.serve') + @patch('monitor.websocket_connections.serve') def test_init(self, mock_websock_serve, mock_set_loop): loop = MagicMock() logger = MagicMock() @@ -22,7 +22,8 @@ def test_init(self, mock_websock_serve, mock_set_loop): mock_websock_serve.assert_called_once_with(server.connection_handler, "ip_address", 4512) loop.run_until_complete.assert_called_once_with("0101") - def test_connection_handler(self): + @patch('monitor.websocket_connections.serve') + def test_connection_handler(self, mock_websock_serve): queue = asyncio.Queue() logger = MagicMock() connection_mock = MagicMock() @@ -31,20 +32,22 @@ def test_connection_handler(self): websocket_mock = MagicMock() - received_id = asyncio.Future() + loop = asyncio.new_event_loop() + received_id = loop.create_future() received_id.set_result("1234") - response = asyncio.Future() + response = loop.create_future() response.set_result(None) websocket_mock.recv.return_value = received_id websocket_mock.send.return_value = response - - loop = asyncio.new_event_loop() queue.put_nowait("result text") queue.put_nowait(None) + start_server_future = loop.create_future() + start_server_future.set_result("0101") + mock_websock_serve.return_value = start_server_future websock_server = WebsocketServer(("localhost", 11111), connection_mock, loop, logger) # actually call the method - loop.run_until_complete(websock_server.connection_handler(websocket_mock, None)) + loop.run_until_complete(websock_server.connection_handler(websocket_mock)) # test the constraints websocket_mock.recv.assert_called_once_with() @@ -53,9 +56,11 @@ def test_connection_handler(self): connection_mock.remove_client.assert_called_once_with("1234", queue) @patch('asyncio.set_event_loop') - def test_run(self, mock_set_loop): + @patch('monitor.websocket_connections.serve') + def test_run(self, mock_websock_serve, mock_set_loop): loop = MagicMock() logger = MagicMock() + mock_websock_serve.return_value = "0101" server = WebsocketServer(("ip_address", 123), None, loop, logger) server.run() mock_set_loop.assert_called_with(loop) diff --git a/monitor/websocket_connections.py b/monitor/websocket_connections.py index 5dea658..b519129 100644 --- a/monitor/websocket_connections.py +++ b/monitor/websocket_connections.py @@ -4,9 +4,11 @@ """ import asyncio -import websockets import threading +from websockets.asyncio.server import serve +from websockets.exceptions import ConnectionClosed + class ClientConnections: """ @@ -147,16 +149,15 @@ def __init__(self, websock_uri, connections, loop, logger): self._logger = logger hostname, port = websock_uri asyncio.set_event_loop(loop) - start_server = websockets.serve(self.connection_handler, hostname, port) - loop.run_until_complete(start_server) + start_server = serve(self.connection_handler, hostname, port) + self._server = loop.run_until_complete(start_server) self._logger.info("websocket server initialized at {}:{}".format(hostname, port)) - async def connection_handler(self, websocket, path): + async def connection_handler(self, websocket): """ Internal asyncio.coroutine function for handling one websocket request. :param websocket: Socket with request - :param path: Requested path of socket (not used) :return: Returns when socket is closed or poison pill is found in message queue from ClientConnections. """ @@ -175,7 +176,7 @@ async def connection_handler(self, websocket, path): # send message to client await websocket.send(result) self._logger.debug("websocket server: message sent to channel '{}'".format(wanted_id)) - except websockets.ConnectionClosed: + except ConnectionClosed: if wanted_id: self._logger.info("websocket server: connection closed for channel '{}'". format(wanted_id)) finally: diff --git a/pyproject.toml b/pyproject.toml index 5ed4a88..c23cf56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ "pyzmq", - "websockets==12.0", + "websockets>=14.2", "PyYAML", ] requires-python = ">=3.9" diff --git a/recodex-monitor.spec b/recodex-monitor.spec index 9d2d07f..afca75d 100644 --- a/recodex-monitor.spec +++ b/recodex-monitor.spec @@ -1,8 +1,8 @@ %define name recodex-monitor %define short_name monitor %define version 1.2.0 -%define unmangled_version 0086f2b6faa213766bc50f2746ee1bf0bdae77c1 -%define release 3 +%define unmangled_version 3bf3625a97b4970ef2c8b64889e7b32c9ac27e53 +%define release 4 Summary: Publish ZeroMQ messages through WebSockets Name: %{name} @@ -26,7 +26,7 @@ Requires(preun): systemd Requires(postun): systemd # %{?fedora:Requires: python3-PyYAML python3-websockets python3-zmq} # %{?rhel:Requires: python3-PyYAML python3-websockets python3-pyzmq} -Requires: python3-PyYAML python3-websockets <= 12.0 python3-pyzmq +Requires: python3-PyYAML python3-websockets >= 14.2 python3-pyzmq Source0: https://github.com/ReCodEx/%{short_name}/archive/%{unmangled_version}.tar.gz#/%{short_name}-%{unmangled_version}.tar.gz diff --git a/requirements.txt b/requirements.txt index 5601ad8..e3d7456 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ pyzmq # WebSockets -websockets==12.0 +websockets>=14.2 # Configuration parsing PyYAML