Source code for simstack.util.runner_util

import asyncio
import logging
import os
import re
import socket
import subprocess
from pathlib import Path

from simstack.core.context import context
from simstack.models.runner_model import RunnerEvent, RunnerType, RunnerEventEnum

logger = logging.getLogger("RunnerUtil")


[docs] def ensure_crontab_entry(command, schedule="*/10 * * * *"): """ Checks if a crontab entry exists and adds it if it doesn't. Args: command (str): The command to be executed by cron schedule (str): The cron schedule expression (default: every 10 minutes) Returns: bool: True if entry was added, False if it already existed """ # The full crontab entry to look for cron_entry = f"{schedule} {command}" try: # Get current crontab result = subprocess.run(["crontab", "-l"], capture_output=True, text=True) # Check if command succeeded if result.returncode == 0: current_crontab = result.stdout else: # If crontab is empty or doesn't exist current_crontab = "" # Check if our entry already exists (using regex to handle whitespace variations) entry_pattern = ( re.escape(schedule.strip()) + r"\s+" + re.escape(command.strip()) ) if re.search(entry_pattern, current_crontab): logger.info(f"Crontab entry for '{command}' already exists.") return False # Add our entry to crontab new_crontab = current_crontab if new_crontab and not new_crontab.endswith("\n"): new_crontab += "\n" new_crontab += cron_entry + "\n" # Write back to crontab process = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE, text=True) process.communicate(input=new_crontab) if process.returncode == 0: logger.info(f"Successfully added crontab entry: '{cron_entry}'") return True else: logger.error(f"Failed to update crontab, return code: {process.returncode}") return False except Exception as e: logger.error(f"Error managing crontab: {str(e)}") return False
[docs] async def graceful_shutdown(resource_name): # Log shutdown pid = os.getpid() user = os.environ.get("USER", os.environ.get("USERNAME", "unknown")) hostname = socket.gethostname() # Capture current user info for the subprocess current_user = os.environ.get("USER", os.environ.get("USERNAME")) runner_event = RunnerEvent( resource=resource_name, runner_type=RunnerType.RESOURCE_RUNNER, event=RunnerEventEnum.SHUTDOWN, pid=pid, user=user, hostname=hostname, message=f"Graceful shutdown of {pid} by {current_user}", ) await context.db.save(runner_event) # Cancel all remaining tasks for task in asyncio.all_tasks(): if task is not asyncio.current_task(): task.cancel() # Give tasks time to cancel await asyncio.sleep(0.5) # Signal to the main function to exit raise SystemExit(0)
[docs] async def restart(resource_name): logger.info( f"Restarting node runner for resource: {resource_name} {context.config.python_paths}" ) cmd = Path(context.config.python_paths[0]) / "scripts" / "check_runner.sh" # Get current user information current_uid = os.getuid() current_gid = os.getgid() current_pid = os.getpid() logger.info(f"Current user ID: {current_uid}, group ID: {current_gid}") # Create environment with preserved user information env = os.environ.copy() # Start a new process with the same arguments logger.info(f"Restarting node runner for resource: {resource_name} {str(cmd)}") result = subprocess.Popen(str(cmd), start_new_session=True, env=env) # Log the restart message = f"user: {current_uid} group: {current_gid} pid: {current_pid} result: {result.pid}" runner_event = RunnerEvent( resource=resource_name, runner_type=RunnerType.RESOURCE_RUNNER, event=RunnerEventEnum.RESTART, message=message, ) await context.db.save(runner_event) # Exit current process logger.info(f"Restarted node runner for resource: {resource_name}")
# await graceful_shutdown(resource_name)
[docs] async def schedule_restart(resource_name, restart_minutes): """Schedule a restart after the specified minutes""" try: logger.info( f"Runner will restart in {restart_minutes} minutes on resource {resource_name} " ) await asyncio.sleep(restart_minutes * 60) # Convert minutes to seconds # try: # if context.config.python_path: # path = Path(context.config.python_path[0]) # command = str(path / "scripts" / "check_runner.sh") + " >> " + str(path / "cron_log.log") + " 2>&1" # logger.info(f"Path: {command} ") # if not ensure_crontab_entry(command): # runner_entry = RunnerEvent(resource=resource_name, runner_type=RunnerType.RESOURCE_RUNNER, # event=RunnerEventEnum.CRONTAB_GONE, # message="Crontab entry for resource runner was not found") # await context.db.save(runner_entry) # except Exception as e: # logger.error("Something went wrong for crontab") await restart(resource_name) await graceful_shutdown(resource_name) except asyncio.CancelledError: logger.info("Restart task was cancelled") raise