This commit is contained in:
parent
c23e289e0c
commit
0cc2a7022c
@ -2,6 +2,8 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import shlex
|
import shlex
|
||||||
import time
|
import time
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
@ -204,13 +206,60 @@ class Zhurnal(BaseApplication):
|
|||||||
def __init__(self, commands: List[str], *args, **kwargs):
|
def __init__(self, commands: List[str], *args, **kwargs):
|
||||||
self.commands = commands or []
|
self.commands = commands or []
|
||||||
self.processes = {}
|
self.processes = {}
|
||||||
|
self.process_tasks = {}
|
||||||
|
self.shutdown_event = asyncio.Event()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Register signal handlers for graceful shutdown
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, self.handle_exit)
|
||||||
|
|
||||||
self.on_startup.append(self.start_processes)
|
self.on_startup.append(self.start_processes)
|
||||||
|
self.on_cleanup.append(self.cleanup_processes)
|
||||||
|
|
||||||
self.clients = []
|
self.clients = []
|
||||||
self.router.add_get("/ws", self.websocket_handler)
|
self.router.add_get("/ws", self.websocket_handler)
|
||||||
self.router.add_get("/", self.index_handler)
|
self.router.add_get("/", self.index_handler)
|
||||||
log.info("Application created")
|
log.info("Application created")
|
||||||
|
|
||||||
|
def handle_exit(self):
|
||||||
|
"""Handle application exit signals."""
|
||||||
|
log.info("Received exit signal. Initiating graceful shutdown...")
|
||||||
|
self.shutdown_event.set()
|
||||||
|
|
||||||
|
async def cleanup_processes(self, app):
|
||||||
|
"""Cleanup all running subprocesses."""
|
||||||
|
log.info("Cleaning up processes...")
|
||||||
|
|
||||||
|
# Terminate all running processes
|
||||||
|
for command, process_info in list(self.processes.items()):
|
||||||
|
try:
|
||||||
|
# Get the subprocess
|
||||||
|
process = process_info.get('process')
|
||||||
|
if process and process.returncode is None:
|
||||||
|
log.info(f"Terminating process: {command}")
|
||||||
|
try:
|
||||||
|
# First try to terminate gracefully
|
||||||
|
process.terminate()
|
||||||
|
# Wait a short time for process to exit
|
||||||
|
await asyncio.wait_for(process.wait(), timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# If process doesn't exit, force kill
|
||||||
|
log.warning(f"Force killing process: {command}")
|
||||||
|
process.kill()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error cleaning up process {command}: {e}")
|
||||||
|
|
||||||
|
# Cancel any running tasks
|
||||||
|
for task in list(self.process_tasks.values()):
|
||||||
|
if not task.done():
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
async def index_handler(self, request):
|
async def index_handler(self, request):
|
||||||
return web.Response(text=index_html, content_type="text/html")
|
return web.Response(text=index_html, content_type="text/html")
|
||||||
|
|
||||||
@ -240,68 +289,102 @@ class Zhurnal(BaseApplication):
|
|||||||
return ws
|
return ws
|
||||||
|
|
||||||
async def run_process(self, process_name, command):
|
async def run_process(self, process_name, command):
|
||||||
process = await asyncio.create_subprocess_exec(
|
"""Run a single process with enhanced monitoring and error handling."""
|
||||||
*shlex.split(command),
|
try:
|
||||||
stdout=asyncio.subprocess.PIPE,
|
# Create subprocess
|
||||||
stderr=asyncio.subprocess.PIPE,
|
process = await asyncio.create_subprocess_exec(
|
||||||
)
|
*shlex.split(command),
|
||||||
log.info(f"Running process {process_name}: {command}")
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store process information
|
||||||
|
self.processes[command] = {
|
||||||
|
'process': process,
|
||||||
|
'name': process_name
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(f"Running process {process_name}: {command}")
|
||||||
|
|
||||||
async def read_output(app, name, process, f):
|
async def read_output(app, name, process, f, is_stderr=False):
|
||||||
time_previous = 0
|
time_previous = 0
|
||||||
async for line in f:
|
try:
|
||||||
time_current = time.time()
|
async for line in f:
|
||||||
time_elapsed = round(
|
# Check if shutdown is requested
|
||||||
time_previous and time_current - time_previous or 0, 4
|
if self.shutdown_event.is_set():
|
||||||
)
|
break
|
||||||
decoded_line = line.decode("utf-8", "ignore").strip()
|
|
||||||
print(decoded_line)
|
time_current = time.time()
|
||||||
decoded_line = "".join(c for c in decoded_line if c.isprintable())
|
time_elapsed = round(
|
||||||
for client in app.clients:
|
time_previous and time_current - time_previous or 0, 4
|
||||||
await client.send_str(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"elapsed": time_elapsed,
|
|
||||||
"timestamp": datetime.now().strftime(
|
|
||||||
"%Y-%m-%d %H:%M:%S"
|
|
||||||
),
|
|
||||||
"line": decoded_line,
|
|
||||||
"name": name,
|
|
||||||
"command": command,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
decoded_line = line.decode("utf-8", "ignore").strip()
|
||||||
time_previous = time.time()
|
print(decoded_line)
|
||||||
|
decoded_line = "".join(c for c in decoded_line if c.isprintable())
|
||||||
|
|
||||||
|
# Broadcast to all WebSocket clients
|
||||||
|
for client in app.clients:
|
||||||
|
await client.send_str(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"elapsed": time_elapsed,
|
||||||
|
"timestamp": datetime.now().strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
),
|
||||||
|
"line": decoded_line,
|
||||||
|
"name": name,
|
||||||
|
"command": command,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
time_previous = time.time()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error reading {name} output: {e}")
|
||||||
|
finally:
|
||||||
|
log.info(f"Finished reading {name} output")
|
||||||
|
|
||||||
await asyncio.gather(
|
# Read stdout and stderr concurrently
|
||||||
read_output(self, f"{process_name}:stdout", command, process.stdout),
|
await asyncio.gather(
|
||||||
read_output(self, f"{process_name}:stderr", process, process.stderr),
|
read_output(self, f"{process_name}:stdout", process, process.stdout),
|
||||||
)
|
read_output(self, f"{process_name}:stderr", process, process.stderr, is_stderr=True)
|
||||||
if process.returncode == 0:
|
|
||||||
log.info(
|
|
||||||
f"Process {process_name}:{command} exited with {process.returncode}."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.error(
|
|
||||||
f"Process {process_name}:{command} exited with {process.returncode}."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
await process.wait()
|
||||||
|
|
||||||
|
# Log process exit status
|
||||||
|
if process.returncode == 0:
|
||||||
|
log.info(
|
||||||
|
f"Process {process_name}:{command} exited successfully with {process.returncode}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
f"Process {process_name}:{command} exited with non-zero status {process.returncode}."
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error running process {process_name}: {e}")
|
||||||
|
finally:
|
||||||
|
# Remove process from tracking
|
||||||
|
if command in self.processes:
|
||||||
|
del self.processes[command]
|
||||||
|
|
||||||
async def start_processes(self, app):
|
async def start_processes(self, app):
|
||||||
|
"""Start all configured processes."""
|
||||||
for x, command in enumerate(self.commands):
|
for x, command in enumerate(self.commands):
|
||||||
self.processes[command] = asyncio.create_task(
|
# Create a task for each process
|
||||||
self.run_process(f"process-{x}", command)
|
task = asyncio.create_task(self.run_process(f"process-{x}", command))
|
||||||
)
|
self.process_tasks[command] = task
|
||||||
# asyncio.create_task(asyncio.gather(*self.processes.values()))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Executle proccesses and monitor trough web interface."
|
description="Execute processes and monitor through web interface."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"commands", nargs="+", help="List of files to commands to execute and monitor."
|
"commands", nargs="+", help="List of commands to execute and monitor."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host",
|
"--host",
|
||||||
@ -313,7 +396,7 @@ def parse_args():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port",
|
"--port",
|
||||||
type=int,
|
type=int,
|
||||||
default=0,
|
default=8080,
|
||||||
required=False,
|
required=False,
|
||||||
help="Port number (default: 8080).",
|
help="Port number (default: 8080).",
|
||||||
)
|
)
|
||||||
@ -338,3 +421,6 @@ def cli():
|
|||||||
log.info(f"Host: {args.host} Port: {args.port}")
|
log.info(f"Host: {args.host} Port: {args.port}")
|
||||||
|
|
||||||
app.run(host=args.host, port=args.port)
|
app.run(host=args.host, port=args.port)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
|
Loading…
Reference in New Issue
Block a user