diff --git a/cli/module.py b/cli/module.py index b1efe73..d6d0982 100644 --- a/cli/module.py +++ b/cli/module.py @@ -30,26 +30,36 @@ def module(args): f"No modules specified. Using default modules for {args.instance}", ) args.modules = module_manager.get_modules(args.instance) - for module_name in tqdm.tqdm( - args.modules, desc="Processing modules", unit="module" - ): - match args.action: - case "install": - module_manager.install(args.instance, [module_name]) - case "uninstall": - module_manager.uninstall(args.instance, [module_name]) - case "upgrade": - # Check if module is installed first - if not module_manager.is_module_installed(args.instance, module_name): - color_log.Show( - "INFO", - f"Module {module_name} not installed. Installing first...", - ) + + pbar = tqdm.tqdm(total=len(args.modules), desc="Processing modules", unit="module") + + for module_name in args.modules: + try: + match args.action: + case "install": + pbar.set_description(f"Installing {module_name}") module_manager.install(args.instance, [module_name]) - # Now upgrade the module - module_manager.upgrade(args.instance, [module_name]) - case _: - color_log.Show( - "FAILED", - f"Invalid action '{args.action}' for module management.", - ) + case "uninstall": + pbar.set_description(f"Uninstalling {module_name}") + module_manager.uninstall(args.instance, [module_name]) + case "upgrade": + # Check if module is installed first + if not module_manager.is_module_installed(args.instance, module_name): + pbar.set_description(f"Installing {module_name}") + module_manager.install(args.instance, [module_name]) + pbar.set_description(f"Upgrading {module_name}") + module_manager.upgrade(args.instance, [module_name]) + case _: + color_log.Show( + "FAILED", + f"Invalid action '{args.action}' for module management.", + ) + return + pbar.update(1) + except Exception as e: + color_log.Show( + "FAILED", + f"Error processing module {module_name}: {str(e)}", + ) + continue + pbar.close() diff --git a/scripts/modules_management.py b/scripts/modules_management.py new file mode 100644 index 0000000..ec4b368 --- /dev/null +++ b/scripts/modules_management.py @@ -0,0 +1,154 @@ +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() diff --git a/scripts/update_modules.py b/scripts/update_modules.py deleted file mode 100644 index 95349f6..0000000 --- a/scripts/update_modules.py +++ /dev/null @@ -1,99 +0,0 @@ -import importlib.util -import argparse -import os -import subprocess -import sys -from ..services import config as Config -from ..lib import color_log - -def run_command(cmd, description): - """Run a command and handle its output.""" - try: - color_log.Show("INFO", f"Executing: {description}") - result = subprocess.run(cmd, capture_output=True, text=True) - if result.stdout: - print(result.stdout.strip()) - if result.stderr: - print(result.stderr.strip()) - return result.returncode == 0 - except subprocess.CalledProcessError 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.""" - # 1. Pull 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 - 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 - 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 - - return True - -def main(): - # Parse arguments - parser = argparse.ArgumentParser(description="Update modules for all instances") - 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) - - # 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}") - -if __name__ == "__main__": - main() diff --git a/utility.mk b/utility.mk index 01ce927..672e5fb 100644 --- a/utility.mk +++ b/utility.mk @@ -1,8 +1,13 @@ UTILITY_DIR =${PWD}/utility SCRIPTS_DIR = ${UTILITY_DIR}/scripts UTILITY_CONFIG = ${PWD}/utility/config/settings.yaml -update_modules_all: - ${PYTHON} -m utility.scripts.update_modules upgrade ${UTILITY_CONFIG} + +# Default values for update_modules +INSTANCE ?= all +ACTION ?= upgrade + +update_modules: + ${PYTHON} -m utility.scripts.modules_management ${INSTANCE} ${ACTION} ${UTILITY_CONFIG} cleanup_addons: @bash ${SCRIPT_PATH}/clean_up_addons.sh $(shell echo $(ADDONS) | tr ',' ' ') gen_config: