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): """Update a single instance with git pull, module update, and service restart.""" # 2. Update module list color_log.Show("INFO", "Updating module list...") module_list_cmd = [ "python", "utility/main.py", "module", "update_list", instance_name, ] if not run_command(module_list_cmd, f"Updating module list for {instance_name}"): color_log.Show("WARNING", f"Module list update failed for {instance_name}") return False # 3. Update modules color_log.Show("INFO", "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 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 # Group instances by host and port instance_groups = {} for instance in instances: if "name" not in instance: color_log.Show("WARNING", f"Instance missing 'name' field. Skipping.") continue host = instance.get("host", "unknown") port = instance.get("port", "unknown") key = f"{host}:{port}" if key not in instance_groups: instance_groups[key] = [] instance_groups[key].append(instance) # Process instances grouped by host:port for host_port, group_instances in instance_groups.items(): host, port = host_port.split(":") color_log.Show("INFO", f"\nProcessing instances on {host}:{port}") color_log.Show( "INFO", f"\n=== Starting update process for {host}:{port} ===" ) # 1. Pull latest code color_log.Show("INFO", "Pulling latest code...") pull_cmd = ["python", "utility/main.py", "git", "pull", group_instances[0].get("name","")] if args.force_pull: pull_cmd.append("--force") if not run_command(pull_cmd, f"Pulling latest code for {group_instances[0].get("name","")}"): color_log.Show( "WARNING", f"Skipping module update due to pull failure", ) for instance in group_instances: instance_name = instance["name"] color_log.Show("INFO", f"Processing instance: {instance_name}") if update_instance(instance_name, args.action): success_count += 1 color_log.Show("OK", f"Successfully updated {instance_name}") else: color_log.Show("FAILED", f"Failed to update {instance_name}") # 4. Restart service color_log.Show("INFO", "Restarting service...") restart_cmd = [ "python", "utility/main.py", "service", "restart", instance_name, ] if not run_command(restart_cmd, f"Restarting service for {host}:{port}"): color_log.Show("WARNING", f"Service restart failed for {host}:{port}") color_log.Show( "OK", f"=== Update process completed for {host}:{port} ===\n" ) # 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()