commit 8ad5b590debfc3bc01e3849e0230711d17c9e587 Author: retoor Date: Wed Nov 27 15:28:30 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3680851 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv +.history +__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26b326c --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +all: ensure_env format build install test + +format: + ./.venv/bin/python -m black . + +ensure_env: + -@python3 -m venv .venv + ./.venv/bin/python -m pip install black + ./.venv/bin/python -m pip install build + +build: + ./.venv/bin/python -m build . + +install: + ./.venv/bin/python -m pip install -e . + +run: + python -m zhurnal "ping -c 1000 google.nl" + +test: + ./.venv/bin/python -m unittest zhurnal.tests diff --git a/dist/Zhurnal-1.3.37-py3-none-any.whl b/dist/Zhurnal-1.3.37-py3-none-any.whl new file mode 100644 index 0000000..fbbf216 Binary files /dev/null and b/dist/Zhurnal-1.3.37-py3-none-any.whl differ diff --git a/dist/zhurnal-0.0.0-py3-none-any.whl b/dist/zhurnal-0.0.0-py3-none-any.whl new file mode 100644 index 0000000..d203a66 Binary files /dev/null and b/dist/zhurnal-0.0.0-py3-none-any.whl differ diff --git a/dist/zhurnal-0.0.0.tar.gz b/dist/zhurnal-0.0.0.tar.gz new file mode 100644 index 0000000..03a0317 Binary files /dev/null and b/dist/zhurnal-0.0.0.tar.gz differ diff --git a/dist/zhurnal-1.3.37.tar.gz b/dist/zhurnal-1.3.37.tar.gz new file mode 100644 index 0000000..89d6730 Binary files /dev/null and b/dist/zhurnal-1.3.37.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07de284 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8adb6ed --- /dev/null +++ b/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = Zhurnal +version = 1.3.37 +description = Web executor and logger +author = retoor +author_email = retoor@molodetz.nl +license = MIT +long_description = file: README.md +long_description_content_type = text/markdown + +[options] +packages = find: +package_dir = + = src +python_requires = >=3.7 +install_requires = + aiohttp + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + zhurnal = zhurnal.app:cli diff --git a/src/Zhurnal.egg-info/PKG-INFO b/src/Zhurnal.egg-info/PKG-INFO new file mode 100644 index 0000000..69565e2 --- /dev/null +++ b/src/Zhurnal.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 2.1 +Name: Zhurnal +Version: 1.3.37 +Summary: Web executor and logger +Author: retoor +Author-email: retoor@molodetz.nl +License: MIT +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Requires-Dist: aiohttp diff --git a/src/Zhurnal.egg-info/SOURCES.txt b/src/Zhurnal.egg-info/SOURCES.txt new file mode 100644 index 0000000..a5fab6d --- /dev/null +++ b/src/Zhurnal.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +pyproject.toml +setup.cfg +src/Zhurnal.egg-info/PKG-INFO +src/Zhurnal.egg-info/SOURCES.txt +src/Zhurnal.egg-info/dependency_links.txt +src/Zhurnal.egg-info/entry_points.txt +src/Zhurnal.egg-info/requires.txt +src/Zhurnal.egg-info/top_level.txt \ No newline at end of file diff --git a/src/Zhurnal.egg-info/dependency_links.txt b/src/Zhurnal.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Zhurnal.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/Zhurnal.egg-info/entry_points.txt b/src/Zhurnal.egg-info/entry_points.txt new file mode 100644 index 0000000..912c411 --- /dev/null +++ b/src/Zhurnal.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +zhurnal = zhurnal.app:cli diff --git a/src/Zhurnal.egg-info/requires.txt b/src/Zhurnal.egg-info/requires.txt new file mode 100644 index 0000000..ee4ba4f --- /dev/null +++ b/src/Zhurnal.egg-info/requires.txt @@ -0,0 +1 @@ +aiohttp diff --git a/src/Zhurnal.egg-info/top_level.txt b/src/Zhurnal.egg-info/top_level.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Zhurnal.egg-info/top_level.txt @@ -0,0 +1 @@ + diff --git a/src/zhurnal.egg-info/PKG-INFO b/src/zhurnal.egg-info/PKG-INFO new file mode 100644 index 0000000..7d8530a --- /dev/null +++ b/src/zhurnal.egg-info/PKG-INFO @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: zhurnal +Version: 0.0.0 diff --git a/src/zhurnal.egg-info/SOURCES.txt b/src/zhurnal.egg-info/SOURCES.txt new file mode 100644 index 0000000..dbc5629 --- /dev/null +++ b/src/zhurnal.egg-info/SOURCES.txt @@ -0,0 +1,6 @@ +pyproject.toml +src/zhurnal/app.py +src/zhurnal.egg-info/PKG-INFO +src/zhurnal.egg-info/SOURCES.txt +src/zhurnal.egg-info/dependency_links.txt +src/zhurnal.egg-info/top_level.txt \ No newline at end of file diff --git a/src/zhurnal.egg-info/dependency_links.txt b/src/zhurnal.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/zhurnal.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/zhurnal.egg-info/top_level.txt b/src/zhurnal.egg-info/top_level.txt new file mode 100644 index 0000000..fb0df89 --- /dev/null +++ b/src/zhurnal.egg-info/top_level.txt @@ -0,0 +1 @@ +zhurnal diff --git a/src/zhurnal/__init__.py b/src/zhurnal/__init__.py new file mode 100644 index 0000000..6c397ad --- /dev/null +++ b/src/zhurnal/__init__.py @@ -0,0 +1,12 @@ +import logging +import sys + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler(sys.stdout), + ], +) + +log = logging.getLogger(__name__) diff --git a/src/zhurnal/__init__py b/src/zhurnal/__init__py new file mode 100644 index 0000000..e69de29 diff --git a/src/zhurnal/__main__py b/src/zhurnal/__main__py new file mode 100644 index 0000000..e69de29 diff --git a/src/zhurnal/app.py b/src/zhurnal/app.py new file mode 100644 index 0000000..dbe5942 --- /dev/null +++ b/src/zhurnal/app.py @@ -0,0 +1,319 @@ +from aiohttp import web +from typing import List +import shlex +import asyncio +import json +from datetime import datetime +from zhurnal import log +import time + + +index_html = """ + + + + + + Zhurnal + + + +
+ + + +""" + + +class Zhurnal(web.Application): + + def __init__(self, commands: List[str], *args, **kwargs): + self.commands = commands or [] + self.processes = {} + super().__init__(*args, **kwargs) + self.on_startup.append(self.start_processes) + self.clients = [] + self.router.add_get("/ws", self.websocket_handler) + self.router.add_get("/", self.index_handler) + log.info("Application created") + + async def index_handler(self, request): + return web.Response(text=index_html, content_type="text/html") + + async def websocket_handler(self, request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + log.info("WebSocket connection established") + self.clients.append(ws) + + async for msg in ws: + if msg.type == web.WSMsgType.TEXT: + log.info(f"Received message: {msg.data}") + if msg.data == "close": + await ws.close() + else: + await ws.send_str(f"Echo: {msg.data}") + elif msg.type == web.WSMsgType.ERROR: + self.clients.remove(ws) + log.info( + f"WebSocket connection closed with exception: {ws.exception()}" + ) + + log.info("WebSocket connection closed") + if ws in self.clients: + self.clients.remove(ws) + return ws + + async def run_process(self, process_name, command): + process = await asyncio.create_subprocess_exec( + *shlex.split(command), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + log.info("Running process {}: {}".format(process_name, command)) + + async def read_output(app, name, process, f): + time_previous = 0 + async for line in f: + time_current = time.time() + time_elapsed = round( + time_previous and time_current - time_previous or 0, 4 + ) + for client in app.clients: + await client.send_str( + json.dumps( + dict( + elapsed=time_elapsed, + timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + line=line.decode().strip(), + name=name, + command=command, + ) + ) + ) + time_previous = time.time() + + await asyncio.gather( + read_output( + self, "{}:stdout".format(process_name), command, process.stdout + ), + read_output( + self, "{}:stderr".format(process_name), process, process.stderr + ), + ) + if process.returncode == 0: + log.info( + "Process {}:{} exited with {}.".format( + process_name, command, process.returncode + ) + ) + else: + log.error( + "Process {}:{} exited with {}.".format( + process_name, command, process.returncode + ) + ) + + async def start_processes(self, app): + print(app) + for x, command in enumerate(self.commands): + self.processes[command] = asyncio.create_task( + self.run_process("process-{}".format(x), command) + ) + # asyncio.create_task(asyncio.gather(*self.processes.values())) + + +def parse_args(): + import argparse + + parser = argparse.ArgumentParser( + description="Executle proccesses and monitor trough web interface." + ) + parser.add_argument( + "commands", nargs="+", help="List of files to commands to execute and monitor." + ) + parser.add_argument( + "--host", + type=str, + default="localhost", + help="Hostname or IP address (default: localhost).", + ) + parser.add_argument( + "--port", type=int, default=8080, help="Port number (default: 8080)." + ) + return parser.parse_args() + + +def cli(): + args = parse_args() + app = Zhurnal(commands=args.commands) + for command in args.commands: + log.info("Preparing execution of {}.".format(command)) + log.info("Host: {} Port: {}".format(args.host, args.port)) + web.run_app(app, host=args.host, port=args.port) diff --git a/src/zhurnal/tests/__init__.py b/src/zhurnal/tests/__init__.py new file mode 100644 index 0000000..ea6df01 --- /dev/null +++ b/src/zhurnal/tests/__init__.py @@ -0,0 +1 @@ +from .app import ZhurnalTestCase diff --git a/src/zhurnal/tests/app.py b/src/zhurnal/tests/app.py new file mode 100644 index 0000000..543b806 --- /dev/null +++ b/src/zhurnal/tests/app.py @@ -0,0 +1,12 @@ +from zhurnal.app import Zhurnal +import unittest + + +class ZhurnalTestCase(unittest.TestCase): + + def setUp(self): + self.commands = ["ls"] + self.app = Zhurnal(commands=self.commands) + + def test_app_available(self): + self.assertTrue(self.app)