import importlib.util import argparse import os import subprocess import sys import fcntl import select from ..services import config as Config from ..lib import color_log def set_nonblocking(fd): """Make a file descriptor non-blocking.""" flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) def run_command(cmd, description): """Run a command and display output in real-time.""" try: color_log.Show("INFO", f"Executing: {description}") # Create process without output buffering process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, # No buffering universal_newlines=True ) # Set both pipes to non-blocking mode set_nonblocking(process.stdout.fileno()) set_nonblocking(process.stderr.fileno()) # Use select to read from pipes as data becomes available while True: reads = [process.stdout.fileno(), process.stderr.fileno()] ret = select.select(reads, [], []) for fd in ret[0]: if fd == process.stdout.fileno(): line = process.stdout.readline() if line: print(line.rstrip()) if fd == process.stderr.fileno(): line = process.stderr.readline() if line: print(line.rstrip()) # Check if process has finished if process.poll() is not None: break # Read any remaining output stdout, stderr = process.communicate() if stdout: print(stdout.rstrip()) if stderr: print(stderr.rstrip()) return process.returncode == 0 except Exception as e: color_log.Show("FAILED", f"Error executing {description}: {str(e)}") return False def update_instance(instance_name, action, force_pull=False): """Update a single instance with git pull, module update, and service restart.""" color_log.Show("INFO", f"\n=== Starting update process for {instance_name} ===") # 1. Pull latest code color_log.Show("INFO", "Step 1/3: Pulling latest code...") pull_cmd = ["python", "utility/main.py", "git", "pull", instance_name] if force_pull: pull_cmd.append("--force") if not run_command(pull_cmd, f"Pulling latest code for {instance_name}"): color_log.Show("WARNING", f"Skipping module update for {instance_name} due to pull failure") return False # 2. Update modules color_log.Show("INFO", "Step 2/3: Updating modules...") module_cmd = ["python", "utility/main.py", "module", action, instance_name] if not run_command(module_cmd, f"Updating modules for {instance_name}"): color_log.Show("WARNING", f"Module update failed for {instance_name}") return False # 3. Restart service color_log.Show("INFO", "Step 3/3: Restarting service...") restart_cmd = ["python", "utility/main.py", "service", "restart", instance_name] if not run_command(restart_cmd, f"Restarting service for {instance_name}"): color_log.Show("WARNING", f"Service restart failed for {instance_name}") return False color_log.Show("OK", f"=== Update process completed for {instance_name} ===\n") return True def main(): # Parse arguments parser = argparse.ArgumentParser(description="Update modules for all instances") parser.add_argument("instance", type=str, help="Instance Name") parser.add_argument( "action", help="Action to perform", choices=["uninstall", "install", "upgrade"] ) parser.add_argument( "config_path", help="Path to the config file" ) parser.add_argument( "--force-pull", action="store_true", help="Force pull with hard reset (discards local changes)" ) args = parser.parse_args() # Load the configuration color_log.Show("INFO", f"Loading configuration from {args.config_path}") config = Config.Config(config_path=args.config_path) if args.instance == "all": # Get instances instances = config.get_instances() if not isinstance(instances, list): color_log.Show("FAILED", "Error: instances is not a valid list.") sys.exit(1) # Process each instance success_count = 0 for instance in instances: if "name" not in instance: color_log.Show("WARNING", f"Instance missing 'name' field. Skipping.") continue instance_name = instance["name"] color_log.Show("INFO", f"\nProcessing instance: {instance_name}") if update_instance(instance_name, args.action, args.force_pull): success_count += 1 color_log.Show("OK", f"Successfully updated {instance_name}") else: color_log.Show("FAILED", f"Failed to update {instance_name}") # Summary color_log.Show("INFO", f"\nUpdate Summary:") color_log.Show("INFO", f"Total instances: {len(instances)}") color_log.Show("INFO", f"Successful updates: {success_count}") color_log.Show("INFO", f"Failed updates: {len(instances) - success_count}") else: if update_instance(args.instance, args.action, args.force_pull): color_log.Show("OK", f"Successfully updated {args.instance}") else: color_log.Show("FAILED", f"Failed to update {args.instance}") if __name__ == "__main__": main()