980 lines
42 KiB
Python
980 lines
42 KiB
Python
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}") |