import os import json import subprocess import sys import threading import time from typing import Dict, Any, List, Optional from datetime import datetime import uuid import glob class CSharpLauncherManager: def __init__(self, data_path: str): self.data_path = data_path self.launcher_config_path = os.path.join(data_path, "csharp_launcher_projects.json") self.favorites_path = os.path.join(data_path, "csharp_launcher_favorites.json") self.script_metadata_path = os.path.join(data_path, "csharp_launcher_metadata.json") # Procesos en ejecución para C# self.running_processes = {} self.process_lock = threading.Lock() # Inicializar archivos si no existen self._initialize_files() def _initialize_files(self): """Crear archivos de configuración por defecto si no existen""" # Inicializar csharp_launcher_projects.json if not os.path.exists(self.launcher_config_path): default_config = { "version": "1.0", "projects": [], "categories": { "Aplicaciones": { "color": "#3B82F6", "icon": "🖥️", "subcategories": ["Desktop", "Consola", "WPF"] }, "Herramientas": { "color": "#10B981", "icon": "🔧", "subcategories": ["Utilidades", "Automatización", "Sistema"] }, "Análisis": { "color": "#8B5CF6", "icon": "📊", "subcategories": ["Datos", "Reportes", "Business Intelligence"] }, "Desarrollo": { "color": "#F59E0B", "icon": "💻", "subcategories": ["Testing", "Build Tools", "DevOps"] }, "APIs": { "color": "#EF4444", "icon": "🌐", "subcategories": ["REST", "GraphQL", "Microservicios"] }, "Otros": { "color": "#6B7280", "icon": "📁", "subcategories": ["Misceláneos"] } }, "settings": { "default_execution_directory": "project_directory", "search_debug_first": True, "show_build_output": True } } with open(self.launcher_config_path, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=2, ensure_ascii=False) # Inicializar csharp_launcher_favorites.json if not os.path.exists(self.favorites_path): default_favorites = {"favorites": []} with open(self.favorites_path, 'w', encoding='utf-8') as f: json.dump(default_favorites, f, indent=2, ensure_ascii=False) # Inicializar csharp_launcher_metadata.json if not os.path.exists(self.script_metadata_path): default_metadata = { "version": "1.0", "executable_metadata": {} } with open(self.script_metadata_path, 'w', encoding='utf-8') as f: json.dump(default_metadata, f, indent=2, ensure_ascii=False) def get_csharp_projects(self) -> List[Dict[str, Any]]: """Obtener todos los proyectos C#""" try: with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) return config.get("projects", []) except Exception as e: print(f"Error loading C# projects: {e}") return [] def get_csharp_project(self, project_id: str) -> Optional[Dict[str, Any]]: """Obtener un proyecto específico por ID""" projects = self.get_csharp_projects() for project in projects: if project.get("id") == project_id: return project return None def add_csharp_project(self, project_data: Dict[str, Any]) -> Dict[str, str]: """Agregar nuevo proyecto C#""" try: # Validar datos requeridos required_fields = ["name", "directory"] for field in required_fields: if not project_data.get(field): return {"status": "error", "message": f"Campo requerido: {field}"} # Validar que el directorio existe if not os.path.isdir(project_data["directory"]): return {"status": "error", "message": "El directorio especificado no existe"} # Generar ID único si no se proporciona if not project_data.get("id"): project_data["id"] = str(uuid.uuid4())[:8] # Verificar que el ID no exista if self.get_csharp_project(project_data["id"]): return {"status": "error", "message": "Ya existe un proyecto con este ID"} # Agregar campos por defecto current_time = datetime.now().isoformat() + "Z" project_data.setdefault("description", "") project_data.setdefault("category", "Otros") project_data.setdefault("version", "1.0") project_data.setdefault("author", "") project_data.setdefault("tags", []) project_data.setdefault("dotnet_version", "") # Versión de .NET project_data.setdefault("created_date", current_time) project_data["updated_date"] = current_time # Cargar configuración y agregar proyecto with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) config["projects"].append(project_data) with open(self.launcher_config_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) return {"status": "success", "message": "Proyecto agregado exitosamente"} except Exception as e: return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"} def update_csharp_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]: """Actualizar proyecto existente""" try: with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) # Buscar y actualizar el proyecto project_found = False for i, project in enumerate(config["projects"]): if project["id"] == project_id: # Mantener ID y fechas de creación project_data["id"] = project_id project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z") project_data["updated_date"] = datetime.now().isoformat() + "Z" config["projects"][i] = project_data project_found = True break if not project_found: return {"status": "error", "message": "Proyecto no encontrado"} with open(self.launcher_config_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) return {"status": "success", "message": "Proyecto actualizado exitosamente"} except Exception as e: return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"} def delete_csharp_project(self, project_id: str) -> Dict[str, str]: """Eliminar proyecto C#""" try: with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) # Filtrar el proyecto a eliminar original_count = len(config["projects"]) config["projects"] = [p for p in config["projects"] if p["id"] != project_id] if len(config["projects"]) == original_count: return {"status": "error", "message": "Proyecto no encontrado"} with open(self.launcher_config_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) # Limpiar metadatos y favoritos relacionados self._cleanup_metadata_for_project(project_id) self._cleanup_favorites_for_project(project_id) self._cleanup_favorites_for_project(project_id) return {"status": "success", "message": "Proyecto eliminado exitosamente"} except Exception as e: return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"} def execute_csharp_executable(self, project_id: str, exe_name: str, exe_args: List[str], broadcast_func, working_dir: str = None) -> Dict[str, Any]: """Ejecutar un ejecutable C#""" try: project = self.get_csharp_project(project_id) if not project: return {"status": "error", "message": "Proyecto no encontrado"} # Buscar el ejecutable executables = self.get_all_project_executables(project_id) exe_info = None for exe in executables: if exe["filename"] == exe_name: exe_info = exe break if not exe_info: return {"status": "error", "message": f"Ejecutable '{exe_name}' no encontrado"} exe_path = exe_info["full_path"] # Determinar directorio de trabajo if working_dir and os.path.isdir(working_dir): work_dir = working_dir else: work_dir = os.path.dirname(exe_path) # Preparar comando cmd = [exe_path] + exe_args execution_id = str(uuid.uuid4())[:8] broadcast_func(f"🚀 Ejecutando: {exe_info['display_name']}") broadcast_func(f"📁 Directorio: {work_dir}") broadcast_func(f"⚡ Comando: {' '.join(cmd)}") broadcast_func("=" * 50) try: # Ejecutar el proceso process = subprocess.Popen( cmd, cwd=work_dir, creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0 ) # Almacenar información del proceso with self.process_lock: self.running_processes[process.pid] = { "project_id": project_id, "exe_name": exe_name, "display_name": exe_info['display_name'], "start_time": datetime.now(), "process": process } broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}") return { "status": "success", "message": f"Ejecutable '{exe_info['display_name']}' iniciado", "pid": process.pid, "execution_id": execution_id } except subprocess.SubprocessError as e: return {"status": "error", "message": f"Error ejecutando el proceso: {str(e)}"} except Exception as e: return {"status": "error", "message": f"Error inesperado: {str(e)}"} except Exception as e: return {"status": "error", "message": f"Error ejecutando ejecutable: {str(e)}"} def get_favorites(self) -> List[Dict[str, Any]]: """Obtener lista de favoritos""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) return data.get("favorites", []) except Exception as e: print(f"Error loading C# favorites: {e}") return [] def toggle_favorite(self, project_id: str, exe_name: str) -> Dict[str, str]: """Agregar o quitar de favoritos""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) favorites = data.get("favorites", []) favorite_key = f"{project_id}_{exe_name}" # Buscar si ya existe existing_favorite = None for fav in favorites: if fav.get("project_id") == project_id and fav.get("exe_name") == exe_name: existing_favorite = fav break if existing_favorite: # Quitar de favoritos favorites.remove(existing_favorite) message = "Removido de favoritos" is_favorite = False else: # Agregar a favoritos favorites.append({ "id": favorite_key, "project_id": project_id, "exe_name": exe_name, "added_date": datetime.now().isoformat() + "Z" }) message = "Agregado a favoritos" is_favorite = True data["favorites"] = favorites with open(self.favorites_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) return {"status": "success", "message": message, "is_favorite": is_favorite} except Exception as e: return {"status": "error", "message": f"Error toggle favorite: {str(e)}"} def get_categories(self) -> Dict[str, Any]: """Obtener categorías disponibles""" try: with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) return config.get("categories", {}) except Exception as e: print(f"Error loading C# categories: {e}") return {} def get_running_processes(self) -> List[Dict[str, Any]]: """Obtener procesos C# en ejecución""" processes = [] with self.process_lock: for pid, info in list(self.running_processes.items()): try: # Verificar si el proceso sigue activo process = info["process"] if process.poll() is None: processes.append({ "pid": pid, "project_id": info["project_id"], "exe_name": info["exe_name"], "display_name": info["display_name"], "start_time": info["start_time"].isoformat() + "Z" }) else: # Proceso terminado, remover de la lista del self.running_processes[pid] except: # Error verificando proceso, remover del self.running_processes[pid] return processes def terminate_process(self, pid: int) -> Dict[str, str]: """Cerrar un proceso C#""" try: with self.process_lock: if pid in self.running_processes: process_info = self.running_processes[pid] process = process_info["process"] try: process.terminate() process.wait(timeout=5) del self.running_processes[pid] return { "status": "success", "message": f"Proceso {process_info['display_name']} (PID: {pid}) terminado" } except subprocess.TimeoutExpired: process.kill() del self.running_processes[pid] return { "status": "success", "message": f"Proceso {process_info['display_name']} (PID: {pid}) forzado a cerrar" } else: return {"status": "error", "message": "Proceso no encontrado en ejecución"} except Exception as e: return {"status": "error", "message": f"Error terminando proceso: {str(e)}"} def _load_executable_metadata(self) -> Dict[str, Any]: """Cargar metadatos de ejecutables desde archivo""" try: with open(self.script_metadata_path, 'r', encoding='utf-8') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {"version": "1.0", "executable_metadata": {}} def get_project_executables(self, project_id: str) -> List[Dict[str, Any]]: """Obtener ejecutables de un proyecto específico (solo visibles)""" project = self.get_csharp_project(project_id) if not project: return [] project_dir = project["directory"] if not os.path.isdir(project_dir): return [] executables = [] metadata = self._load_executable_metadata() # Buscar en bin/Release y bin/Debug search_patterns = [ os.path.join(project_dir, "**/bin/Release/**/*.exe"), os.path.join(project_dir, "**/bin/Debug/**/*.exe") ] found_exe_files = set() for pattern in search_patterns: for exe_path in glob.glob(pattern, recursive=True): if os.path.isfile(exe_path): found_exe_files.add(exe_path) for exe_path in found_exe_files: exe_name = os.path.basename(exe_path) exe_key = f"{project_id}_{exe_name}" # Obtener metadatos o crear por defecto exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {}) # Solo incluir si no está oculto if not exe_metadata.get("hidden", False): # Determinar si es Debug o Release build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug" executables.append({ "filename": exe_name, "full_path": exe_path, "display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')), "short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"), "long_description": exe_metadata.get("long_description", ""), "build_type": build_type, "relative_path": os.path.relpath(exe_path, project_dir) }) # Ordenar por nombre de display executables.sort(key=lambda x: x['display_name']) return executables def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]: """Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión""" project = self.get_csharp_project(project_id) if not project: return [] project_dir = project["directory"] if not os.path.isdir(project_dir): return [] executables = [] metadata = self._load_executable_metadata() # Buscar en bin/Release y bin/Debug search_patterns = [ os.path.join(project_dir, "**/bin/Release/**/*.exe"), os.path.join(project_dir, "**/bin/Debug/**/*.exe") ] found_exe_files = set() for pattern in search_patterns: for exe_path in glob.glob(pattern, recursive=True): if os.path.isfile(exe_path): found_exe_files.add(exe_path) for exe_path in found_exe_files: exe_name = os.path.basename(exe_path) exe_key = f"{project_id}_{exe_name}" # Obtener metadatos o crear por defecto exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {}) # Determinar si es Debug o Release build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug" executables.append({ "filename": exe_name, "full_path": exe_path, "display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')), "short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"), "long_description": exe_metadata.get("long_description", ""), "build_type": build_type, "relative_path": os.path.relpath(exe_path, project_dir), "hidden": exe_metadata.get("hidden", False) }) # Ordenar por nombre de display executables.sort(key=lambda x: x['display_name']) return executables def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]: """Obtener metadatos de un ejecutable específico""" metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" return metadata.get("executable_metadata", {}).get(exe_key, { "display_name": exe_name.replace('.exe', ''), "short_description": "Aplicación C#", "long_description": "", "hidden": False }) def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]: """Actualizar metadatos de un ejecutable específico""" try: metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" if "executable_metadata" not in metadata: metadata["executable_metadata"] = {} if exe_key not in metadata["executable_metadata"]: metadata["executable_metadata"][exe_key] = {} # Actualizar campos metadata["executable_metadata"][exe_key].update(metadata_update) self._save_executable_metadata(metadata) return {"status": "success", "message": "Metadatos actualizados exitosamente"} except Exception as e: return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"} def execute_csharp_executable(self, project_id: str, exe_name: str, exe_args: List[str], broadcast_func, working_dir: str = None) -> Dict[str, Any]: """Ejecutar un ejecutable C#""" try: project = self.get_csharp_project(project_id) if not project: return {"status": "error", "message": "Proyecto no encontrado"} # Buscar el ejecutable executables = self.get_all_project_executables(project_id) exe_info = None for exe in executables: if exe["filename"] == exe_name: exe_info = exe break if not exe_info: return {"status": "error", "message": f"Ejecutable '{exe_name}' no encontrado"} exe_path = exe_info["full_path"] # Determinar directorio de trabajo if working_dir and os.path.isdir(working_dir): work_dir = working_dir else: work_dir = os.path.dirname(exe_path) # Preparar comando cmd = [exe_path] + exe_args execution_id = str(uuid.uuid4())[:8] broadcast_func(f"🚀 Ejecutando: {exe_info['display_name']}") broadcast_func(f"📁 Directorio: {work_dir}") broadcast_func(f"⚡ Comando: {' '.join(cmd)}") broadcast_func("=" * 50) try: # Ejecutar el proceso process = subprocess.Popen( cmd, cwd=work_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0 ) # Almacenar información del proceso with self.process_lock: self.running_processes[process.pid] = { "project_id": project_id, "exe_name": exe_name, "display_name": exe_info['display_name'], "start_time": datetime.now(), "process": process } broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}") # Monitorear salida en hilo separado def read_output(): try: for line in iter(process.stdout.readline, ''): if line.strip(): broadcast_func(line.rstrip()) except Exception as e: broadcast_func(f"Error leyendo salida: {e}") finally: if process.stdout: process.stdout.close() output_thread = threading.Thread(target=read_output, daemon=True) output_thread.start() # Monitorear finalización en hilo separado def monitor_completion(): try: start_time = time.time() return_code = process.wait() execution_time = time.time() - start_time # Limpiar de procesos en ejecución with self.process_lock: if process.pid in self.running_processes: del self.running_processes[process.pid] if return_code == 0: broadcast_func(f"✅ Proceso completado exitosamente (PID: {process.pid})") else: broadcast_func(f"❌ Proceso terminó con código: {return_code} (PID: {process.pid})") broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos") broadcast_func("=" * 50) except Exception as e: broadcast_func(f"Error monitoreando proceso: {e}") monitor_thread = threading.Thread(target=monitor_completion, daemon=True) monitor_thread.start() return { "status": "success", "message": f"Ejecutable '{exe_info['display_name']}' iniciado", "pid": process.pid, "execution_id": execution_id } except subprocess.SubprocessError as e: return {"status": "error", "message": f"Error ejecutando el proceso: {str(e)}"} except Exception as e: return {"status": "error", "message": f"Error inesperado: {str(e)}"} except Exception as e: return {"status": "error", "message": f"Error ejecutando ejecutable: {str(e)}"} def get_favorites(self) -> List[Dict[str, Any]]: """Obtener lista de favoritos""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) return data.get("favorites", []) except Exception as e: print(f"Error loading C# favorites: {e}") return [] def toggle_favorite(self, project_id: str, exe_name: str) -> Dict[str, str]: """Agregar o quitar de favoritos""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) favorites = data.get("favorites", []) favorite_key = f"{project_id}_{exe_name}" # Buscar si ya existe existing_favorite = None for fav in favorites: if fav.get("project_id") == project_id and fav.get("exe_name") == exe_name: existing_favorite = fav break if existing_favorite: # Quitar de favoritos favorites.remove(existing_favorite) message = "Removido de favoritos" is_favorite = False else: # Agregar a favoritos favorites.append({ "id": favorite_key, "project_id": project_id, "exe_name": exe_name, "added_date": datetime.now().isoformat() + "Z" }) message = "Agregado a favoritos" is_favorite = True data["favorites"] = favorites with open(self.favorites_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) return { "status": "success", "message": message, "is_favorite": is_favorite } except Exception as e: return {"status": "error", "message": f"Error gestionando favorito: {str(e)}"} def get_categories(self) -> Dict[str, Any]: """Obtener categorías disponibles""" try: with open(self.launcher_config_path, 'r', encoding='utf-8') as f: config = json.load(f) return config.get("categories", {}) except Exception as e: print(f"Error loading C# categories: {e}") return {} def get_running_processes(self) -> List[Dict[str, Any]]: """Obtener procesos C# en ejecución""" with self.process_lock: processes = [] for pid, info in self.running_processes.items(): try: # Verificar si el proceso sigue activo if info["process"].poll() is None: processes.append({ "pid": pid, "project_id": info["project_id"], "exe_name": info["exe_name"], "display_name": info["display_name"], "start_time": info["start_time"].isoformat() }) else: # Proceso terminado, remover de la lista del self.running_processes[pid] except: # Error verificando proceso, remover del self.running_processes[pid] return processes def terminate_process(self, pid: int) -> Dict[str, str]: """Cerrar un proceso C#""" try: with self.process_lock: if pid in self.running_processes: process_info = self.running_processes[pid] process = process_info["process"] try: process.terminate() process.wait(timeout=5) del self.running_processes[pid] return { "status": "success", "message": f"Proceso {process_info['display_name']} (PID: {pid}) terminado" } except subprocess.TimeoutExpired: process.kill() del self.running_processes[pid] return { "status": "success", "message": f"Proceso {process_info['display_name']} (PID: {pid}) forzado a cerrar" } else: return {"status": "error", "message": "Proceso no encontrado en ejecución"} except Exception as e: return {"status": "error", "message": f"Error terminando proceso: {str(e)}"} def _load_executable_metadata(self) -> Dict[str, Any]: """Cargar metadatos de ejecutables desde archivo""" try: with open(self.script_metadata_path, 'r', encoding='utf-8') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {"version": "1.0", "executable_metadata": {}} def _save_executable_metadata(self, metadata: Dict[str, Any]): """Guardar metadatos de ejecutables a archivo""" try: with open(self.script_metadata_path, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Error saving C# executable metadata: {e}") def _cleanup_metadata_for_project(self, project_id: str): """Limpiar metadatos de un proyecto eliminado""" try: metadata = self._load_executable_metadata() if "executable_metadata" in metadata: # Remover metadatos que empiecen con project_id_ keys_to_remove = [key for key in metadata["executable_metadata"].keys() if key.startswith(f"{project_id}_")] for key in keys_to_remove: del metadata["executable_metadata"][key] self._save_executable_metadata(metadata) except Exception as e: print(f"Error cleaning metadata for project {project_id}: {e}") def _cleanup_favorites_for_project(self, project_id: str): """Limpiar favoritos de un proyecto eliminado""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) # Filtrar favoritos que no sean del proyecto eliminado data["favorites"] = [fav for fav in data.get("favorites", []) if fav.get("project_id") != project_id] with open(self.favorites_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Error cleaning favorites for project {project_id}: {e}") def get_executable_arguments(self, project_id: str, exe_name: str) -> List[Dict[str, str]]: """Obtener argumentos predefinidos de un ejecutable""" try: metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" if "executable_metadata" in metadata and exe_key in metadata["executable_metadata"]: return metadata["executable_metadata"][exe_key].get("arguments", []) return [] except Exception as e: print(f"Error loading executable arguments: {e}") return [] def update_executable_arguments(self, project_id: str, exe_name: str, arguments: List[Dict[str, str]]) -> Dict[str, str]: """Actualizar argumentos predefinidos de un ejecutable""" try: metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" # Crear estructura si no existe if "executable_metadata" not in metadata: metadata["executable_metadata"] = {} if exe_key not in metadata["executable_metadata"]: metadata["executable_metadata"][exe_key] = {} # Validar formato de argumentos for arg in arguments: if not isinstance(arg, dict) or "description" not in arg or "arguments" not in arg: return {"status": "error", "message": "Formato de argumentos inválido. Debe ser [{'description': '...', 'arguments': '...'}]"} # Actualizar argumentos metadata["executable_metadata"][exe_key]["arguments"] = arguments metadata["executable_metadata"][exe_key]["updated_date"] = datetime.now().isoformat() + "Z" self._save_executable_metadata(metadata) return {"status": "success", "message": "Argumentos actualizados exitosamente"} except Exception as e: return {"status": "error", "message": f"Error actualizando argumentos: {str(e)}"} def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]: """Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión""" project = self.get_csharp_project(project_id) if not project: return [] project_dir = project["directory"] if not os.path.isdir(project_dir): return [] executables = [] metadata = self._load_executable_metadata() # Buscar en bin/Release y bin/Debug search_patterns = [ os.path.join(project_dir, "**/bin/Release/**/*.exe"), os.path.join(project_dir, "**/bin/Debug/**/*.exe") ] found_exe_files = set() for pattern in search_patterns: for exe_path in glob.glob(pattern, recursive=True): if os.path.isfile(exe_path): found_exe_files.add(exe_path) for exe_path in found_exe_files: exe_name = os.path.basename(exe_path) exe_key = f"{project_id}_{exe_name}" # Obtener metadatos o crear por defecto exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {}) # Determinar si es Debug o Release build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug" executables.append({ "filename": exe_name, "full_path": exe_path, "display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')), "short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"), "long_description": exe_metadata.get("long_description", ""), "build_type": build_type, "relative_path": os.path.relpath(exe_path, project_dir), "hidden": exe_metadata.get("hidden", False) }) # Ordenar por nombre de display executables.sort(key=lambda x: x['display_name']) return executables def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]: """Obtener metadatos de un ejecutable específico""" metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" return metadata.get("executable_metadata", {}).get(exe_key, { "display_name": exe_name.replace('.exe', ''), "short_description": "Aplicación C#", "long_description": "", "hidden": False }) def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]: """Actualizar metadatos de un ejecutable específico""" try: metadata = self._load_executable_metadata() exe_key = f"{project_id}_{exe_name}" if "executable_metadata" not in metadata: metadata["executable_metadata"] = {} if exe_key not in metadata["executable_metadata"]: metadata["executable_metadata"][exe_key] = {} # Actualizar campos metadata["executable_metadata"][exe_key].update(metadata_update) self._save_executable_metadata(metadata) return {"status": "success", "message": "Metadatos actualizados exitosamente"} except Exception as e: return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"} def _save_executable_metadata(self, metadata: Dict[str, Any]): """Guardar metadatos de ejecutables a archivo""" try: with open(self.script_metadata_path, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Error saving C# executable metadata: {e}") def _cleanup_metadata_for_project(self, project_id: str): """Limpiar metadatos de un proyecto eliminado""" try: metadata = self._load_executable_metadata() if "executable_metadata" in metadata: # Remover metadatos que empiecen con project_id_ keys_to_remove = [key for key in metadata["executable_metadata"].keys() if key.startswith(f"{project_id}_")] for key in keys_to_remove: del metadata["executable_metadata"][key] self._save_executable_metadata(metadata) except Exception as e: print(f"Error cleaning metadata for project {project_id}: {e}") def _cleanup_favorites_for_project(self, project_id: str): """Limpiar favoritos de un proyecto eliminado""" try: with open(self.favorites_path, 'r', encoding='utf-8') as f: data = json.load(f) # Filtrar favoritos que no sean del proyecto eliminado data["favorites"] = [fav for fav in data.get("favorites", []) if fav.get("project_id") != project_id] with open(self.favorites_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Error cleaning favorites for project {project_id}: {e}") def find_solution_file(self, project_id: str) -> Optional[str]: """ Buscar archivo .sln en el directorio del proyecto. Prioridades: 1. Si hay un solo .sln, retornarlo 2. Si hay múltiples .sln, buscar uno que coincida con el nombre del directorio 3. Si no hay coincidencias exactas, retornar el primero alfabéticamente 4. Si no hay .sln, retornar None """ try: project = self.get_csharp_project(project_id) if not project: return None project_dir = project["directory"] if not os.path.isdir(project_dir): return None # Buscar archivos .sln en el directorio raíz del proyecto sln_pattern = os.path.join(project_dir, "*.sln") sln_files = glob.glob(sln_pattern) if not sln_files: return None if len(sln_files) == 1: return sln_files[0] # Si hay múltiples archivos, buscar uno que coincida con el nombre del directorio project_name = os.path.basename(project_dir.rstrip(os.sep)) for sln_file in sln_files: sln_name = os.path.splitext(os.path.basename(sln_file))[0] if sln_name.lower() == project_name.lower(): return sln_file # Si no hay coincidencias exactas, retornar el primero alfabéticamente return sorted(sln_files)[0] except Exception as e: print(f"Error finding solution file for project {project_id}: {e}") return None