Implementación del Launcher C# y mejoras en la interfaz de usuario

- Se añadió un nuevo launcher para proyectos C# que permite gestionar, ejecutar y categorizar aplicaciones compiladas.
- Se implementaron tres pestañas en la interfaz: "Scripts (Config)", "Launcher GUI (Python)" y "Launcher C#", mejorando la organización y accesibilidad.
- Se actualizaron los archivos de configuración y se mejoró la lógica de inicialización para soportar el nuevo sistema de C#.
- Se realizaron ajustes en la interfaz para incluir un panel de favoritos y un sistema de gestión de procesos en ejecución para C#.
- Se mejoró la documentación en `adicion_launcher4GUI.md` para reflejar las nuevas funcionalidades y estructura del proyecto.
This commit is contained in:
Miguel 2025-06-17 17:48:13 +02:00
parent bf30b2db52
commit 7ab11a94ce
9 changed files with 1687 additions and 6875 deletions

File diff suppressed because it is too large Load Diff

4
app.py
View File

@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, url_for
from flask_sock import Sock from flask_sock import Sock
from lib.config_manager import ConfigurationManager from lib.config_manager import ConfigurationManager
from lib.launcher_manager import LauncherManager from lib.launcher_manager import LauncherManager
from lib.csharp_launcher_manager import CSharpLauncherManager
import os import os
import json # Added import import json # Added import
from datetime import datetime from datetime import datetime
@ -27,6 +28,9 @@ config_manager = ConfigurationManager()
# Inicializar launcher manager # Inicializar launcher manager
launcher_manager = LauncherManager(config_manager.data_path) launcher_manager = LauncherManager(config_manager.data_path)
# Inicializar C# launcher manager
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
# Lista global para mantener las conexiones WebSocket activas # Lista global para mantener las conexiones WebSocket activas
websocket_connections = set() websocket_connections = set()

View File

@ -0,0 +1,17 @@
{
"folders": [
{
"path": "."
},
{
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
},
{
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
},
{
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
}
],
"settings": {}
}

View File

@ -1,5 +1,31 @@
{ {
"history": [ "history": [
{
"id": "b321622a",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-15T18:19:14.681042Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 27400,
"execution_time": null
},
{
"id": "754d0df9",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-15T18:01:45.840069Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 38228,
"execution_time": null
},
{ {
"id": "15176a5f", "id": "15176a5f",
"group_id": "1", "group_id": "1",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,740 @@
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)
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
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_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}")

View File

@ -0,0 +1,467 @@
// static/js/csharp_launcher.js - Gestor para Launcher C#
class CSharpLauncherManager {
constructor() {
this.currentProject = null;
this.projects = [];
this.executables = [];
this.favorites = new Set();
this.runningProcesses = [];
this.categories = [
'Aplicaciones', 'Herramientas', 'Análisis',
'Desarrollo', 'APIs', 'Otros'
];
this.currentCategory = 'all';
}
async init() {
console.log('Initializing C# Launcher Manager...');
await this.loadProjects();
await this.loadFavorites();
this.setupEventListeners();
this.renderInterface();
await this.refreshProcesses();
// Actualizar procesos cada 10 segundos
setInterval(() => this.refreshProcesses(), 10000);
}
async loadProjects() {
try {
const response = await fetch('/api/csharp-projects');
if (response.ok) {
this.projects = await response.json();
this.renderProjectSelector();
} else {
console.error('Error loading C# projects:', await response.text());
}
} catch (error) {
console.error('Error loading C# projects:', error);
}
}
async loadFavorites() {
try {
const response = await fetch('/api/csharp-favorites');
if (response.ok) {
const data = await response.json();
this.favorites = new Set(data.favorites.map(fav => `${fav.project_id}_${fav.exe_name}`));
this.renderFavoritesPanel();
} else {
console.error('Error loading C# favorites:', await response.text());
}
} catch (error) {
console.error('Error loading C# favorites:', error);
}
}
setupEventListeners() {
// Event listeners específicos del launcher C#
const projectSelect = document.getElementById('csharp-project-select');
if (projectSelect) {
projectSelect.addEventListener('change', (e) => this.onProjectChange(e));
}
}
renderInterface() {
this.renderProjectSelector();
this.renderCategoryFilter();
this.renderFavoritesPanel();
}
renderProjectSelector() {
const select = document.getElementById('csharp-project-select');
if (!select) return;
select.innerHTML = '<option value="">-- Seleccionar Proyecto --</option>';
this.projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.name;
option.setAttribute('data-description', project.description || '');
option.setAttribute('data-category', project.category || 'Otros');
select.appendChild(option);
});
}
async onProjectChange(e) {
const projectId = e.target.value;
if (!projectId) {
this.currentProject = null;
this.hideCSharpProjectButtons();
this.clearExecutables();
return;
}
try {
this.currentProject = this.projects.find(p => p.id === projectId);
this.showCSharpProjectButtons();
await this.loadProjectExecutables(projectId);
} catch (error) {
console.error('Error changing project:', error);
}
}
showCSharpProjectButtons() {
const buttons = ['cursor-csharp-btn', 'vs2022-csharp-btn', 'folder-csharp-btn', 'copy-path-csharp-btn'];
buttons.forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.style.display = 'block';
});
}
hideCSharpProjectButtons() {
const buttons = ['cursor-csharp-btn', 'vs2022-csharp-btn', 'folder-csharp-btn', 'copy-path-csharp-btn'];
buttons.forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.style.display = 'none';
});
}
async loadProjectExecutables(projectId) {
try {
const response = await fetch(`/api/csharp-executables/${projectId}`);
if (response.ok) {
this.executables = await response.json();
this.renderExecutables();
} else {
console.error('Error loading executables:', await response.text());
this.executables = [];
this.renderExecutables();
}
} catch (error) {
console.error('Error loading executables:', error);
this.executables = [];
this.renderExecutables();
}
}
renderExecutables() {
const grid = document.getElementById('csharp-executables-grid');
if (!grid) return;
// Filtrar por categoría si no es 'all'
let filteredExecutables = this.executables;
if (this.currentCategory !== 'all' && this.currentProject) {
filteredExecutables = this.executables.filter(exe =>
this.currentProject.category === this.currentCategory
);
}
if (filteredExecutables.length === 0) {
grid.innerHTML = `
<div class="col-span-full text-center py-8 text-gray-500">
<div class="text-4xl mb-2">🔍</div>
<div>No se encontraron ejecutables en este proyecto</div>
<div class="text-sm mt-1">Busque en: bin/Release y bin/Debug</div>
</div>
`;
return;
}
grid.innerHTML = filteredExecutables.map(exe => this.createExecutableCard(exe)).join('');
}
createExecutableCard(exe) {
const favoriteKey = `${this.currentProject.id}_${exe.filename}`;
const isFavorite = this.favorites.has(favoriteKey);
const buildTypeBadge = exe.build_type === 'Release' ?
'<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Release</span>' :
'<span class="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Debug</span>';
return `
<div class="executable-card bg-white border rounded-lg p-4 hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-sm">${exe.display_name}</h4>
<button class="favorite-star text-gray-300 hover:text-yellow-500 ${isFavorite ? 'text-yellow-500' : ''}"
onclick="csharpLauncherManager.toggleFavorite('${this.currentProject.id}', '${exe.filename}')"></button>
</div>
<p class="text-xs text-gray-600 mb-2">${exe.short_description}</p>
<div class="flex justify-between items-center mb-3">
${buildTypeBadge}
<span class="text-xs text-gray-500">${exe.filename}</span>
</div>
<div class="flex justify-between items-center">
<div class="space-x-1">
<button class="text-blue-500 hover:underline text-xs"
onclick="showCSharpExecutableArgs('${this.currentProject.id}', '${exe.filename}', '${exe.display_name}')">
Con Argumentos
</button>
</div>
<button class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600"
onclick="csharpLauncherManager.executeExecutable('${this.currentProject.id}', '${exe.filename}')">
Ejecutar
</button>
</div>
</div>
`;
}
async executeExecutable(projectId, exeName, args = [], workingDir = null) {
try {
const response = await fetch('/api/execute-csharp-executable', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
project_id: projectId,
exe_name: exeName,
args: args,
working_dir: workingDir
})
});
const result = await response.json();
if (result.status === 'success') {
// Actualizar procesos después de un breve delay
setTimeout(() => this.refreshProcesses(), 1000);
}
return result;
} catch (error) {
console.error('Error executing executable:', error);
return { status: 'error', message: error.message };
}
}
async toggleFavorite(projectId, exeName) {
try {
const response = await fetch('/api/csharp-favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
project_id: projectId,
exe_name: exeName
})
});
if (response.ok) {
const result = await response.json();
const favoriteKey = `${projectId}_${exeName}`;
if (result.is_favorite) {
this.favorites.add(favoriteKey);
} else {
this.favorites.delete(favoriteKey);
}
this.renderExecutables();
this.renderFavoritesPanel();
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
}
renderFavoritesPanel() {
const panel = document.getElementById('csharp-favorites-list');
const counter = document.getElementById('csharp-favorites-count');
if (!panel || !counter) return;
counter.textContent = `${this.favorites.size} favoritos`;
if (this.favorites.size === 0) {
panel.innerHTML = '<div class="text-center text-gray-500 py-2">No hay favoritos guardados</div>';
return;
}
// Crear lista de favoritos (simplificada - solo mostrar nombres)
const favoriteItems = Array.from(this.favorites).map(key => {
const [projectId, exeName] = key.split('_', 2);
const project = this.projects.find(p => p.id === projectId);
const projectName = project ? project.name : 'Proyecto desconocido';
return `
<div class="flex justify-between items-center bg-white p-2 rounded border">
<div>
<span class="font-medium text-sm">${exeName}</span>
<span class="text-xs text-gray-500 ml-2">${projectName}</span>
</div>
<button class="text-blue-500 hover:text-blue-700 text-xs"
onclick="csharpLauncherManager.executeFavorite('${projectId}', '${exeName}')">
Ejecutar
</button>
</div>
`;
}).join('');
panel.innerHTML = favoriteItems;
}
async executeFavorite(projectId, exeName) {
// Cambiar al proyecto si no está seleccionado
if (!this.currentProject || this.currentProject.id !== projectId) {
const select = document.getElementById('csharp-project-select');
if (select) {
select.value = projectId;
await this.onProjectChange({ target: { value: projectId } });
}
}
// Ejecutar el ejecutable
await this.executeExecutable(projectId, exeName);
}
renderCategoryFilter() {
// Las categorías ya están en el HTML, solo necesitamos la funcionalidad
console.log('Category filter rendered');
}
filterByCategory(category) {
this.currentCategory = category;
// Actualizar botones activos
document.querySelectorAll('.csharp-category-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.getAttribute('data-category') === category) {
btn.classList.add('active');
}
});
this.renderExecutables();
}
async refreshProcesses() {
try {
const response = await fetch('/api/csharp-running-processes');
if (response.ok) {
const data = await response.json();
this.runningProcesses = data.processes || [];
this.renderRunningProcesses();
}
} catch (error) {
console.error('Error refreshing processes:', error);
}
}
renderRunningProcesses() {
const container = document.getElementById('csharp-running-processes');
if (!container) return;
if (this.runningProcesses.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-2">No hay procesos C# en ejecución</div>';
return;
}
const processItems = this.runningProcesses.map(process => {
const startTime = new Date(process.start_time).toLocaleTimeString();
return `
<div class="flex justify-between items-center bg-gray-50 p-3 rounded border">
<div>
<span class="font-medium">${process.display_name}</span>
<span class="text-sm text-gray-500 block">PID: ${process.pid} | Iniciado: ${startTime}</span>
</div>
<button class="text-red-500 hover:text-red-700 text-sm"
onclick="csharpLauncherManager.terminateProcess(${process.pid})">
Cerrar
</button>
</div>
`;
}).join('');
container.innerHTML = processItems;
}
async terminateProcess(pid) {
try {
const response = await fetch(`/api/csharp-process-terminate/${pid}`, {
method: 'POST'
});
if (response.ok) {
await this.refreshProcesses();
}
} catch (error) {
console.error('Error terminating process:', error);
}
}
clearExecutables() {
const grid = document.getElementById('csharp-executables-grid');
if (grid) {
grid.innerHTML = '<div class="col-span-full text-center py-8 text-gray-500">Selecciona un proyecto para ver los ejecutables</div>';
}
}
}
// Funciones globales para el HTML
function loadCSharpExecutables() {
const select = document.getElementById('csharp-project-select');
if (select && window.csharpLauncherManager) {
window.csharpLauncherManager.onProjectChange({ target: select });
}
}
function filterCSharpByCategory(category) {
if (window.csharpLauncherManager) {
window.csharpLauncherManager.filterByCategory(category);
}
}
function refreshCSharpProcesses() {
if (window.csharpLauncherManager) {
window.csharpLauncherManager.refreshProcesses();
}
}
function openCSharpProjectEditor() {
// TODO: Implementar editor de proyectos C#
alert('Editor de proyectos C# - Por implementar');
}
function openCSharpProjectInEditor(editor) {
if (!window.csharpLauncherManager?.currentProject) {
alert('Selecciona un proyecto primero');
return;
}
const projectId = window.csharpLauncherManager.currentProject.id;
if (editor === 'cursor') {
// Implementar apertura en Cursor
alert(`Abriendo proyecto ${projectId} en Cursor - Por implementar`);
} else if (editor === 'vs2022') {
// Implementar apertura en Visual Studio 2022
alert(`Abriendo proyecto ${projectId} en Visual Studio 2022 - Por implementar`);
}
}
function openCSharpProjectFolder() {
if (!window.csharpLauncherManager?.currentProject) {
alert('Selecciona un proyecto primero');
return;
}
// Implementar apertura de carpeta
alert('Abriendo carpeta del proyecto - Por implementar');
}
function copyCSharpProjectPath() {
if (!window.csharpLauncherManager?.currentProject) {
alert('Selecciona un proyecto primero');
return;
}
// Implementar copia de path
navigator.clipboard.writeText(window.csharpLauncherManager.currentProject.directory);
}
function openCSharpExecutableManager() {
// TODO: Implementar gestor de ejecutables
alert('Gestor de ejecutables C# - Por implementar');
}
function showCSharpExecutableArgs(projectId, exeName, displayName) {
// TODO: Implementar modal de argumentos para C#
const args = prompt(`Argumentos para ${displayName}:`, '');
if (args !== null) {
const argArray = args.trim() ? args.split(' ') : [];
window.csharpLauncherManager.executeExecutable(projectId, exeName, argArray);
}
}
// Inicialización global
window.csharpLauncherManager = new CSharpLauncherManager();

View File

@ -1369,6 +1369,11 @@ function switchTab(tabName) {
window.launcherManager = new LauncherManager(); window.launcherManager = new LauncherManager();
window.launcherManager.init(); window.launcherManager.init();
} }
// Inicializar C# launcher si es la primera vez
if (tabName === 'csharp' && !window.csharpLauncherManager.currentProject) {
window.csharpLauncherManager.init();
}
} }
// Funciones para modales // Funciones para modales

View File

@ -114,7 +114,18 @@
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"> d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
</path> </path>
</svg> </svg>
Launcher GUI Launcher GUI (Python)
</span>
</button>
<button id="csharp-tab" onclick="switchTab('csharp')"
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
<span class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0-1.125.504-1.125 1.125V11.25a9 9 0 00-9-9z">
</path>
</svg>
Launcher C#
</span> </span>
</button> </button>
</nav> </nav>
@ -191,7 +202,7 @@
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()"> <button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
Salvar Salvar
</button> </button>
</div> </div>
</div> </div>
<!-- Add directory history dropdown --> <!-- Add directory history dropdown -->
<div class="mt-2"> <div class="mt-2">
@ -232,7 +243,7 @@
<!-- Launcher Controls --> <!-- Launcher Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow"> <div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Launcher GUI - Scripts Independientes</h2> <h2 class="text-xl font-bold">Launcher GUI - Scripts Python Independientes</h2>
<button onclick="openGroupEditor()" <button onclick="openGroupEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"> class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Gestionar Grupos Gestionar Grupos
@ -367,6 +378,125 @@
</div> </div>
</div> </div>
<!-- Tab Content: Launcher C# -->
<div id="csharp-content" class="tab-content hidden">
<!-- C# Project Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Launcher C# - Proyectos Compilados</h2>
<button onclick="openCSharpProjectEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Gestionar Proyectos
</button>
</div>
<!-- Project Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto C#</label>
<div class="flex gap-2">
<div class="relative flex-1">
<select id="csharp-project-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadCSharpExecutables()">
<option value="">-- Seleccionar Proyecto --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-csharp-project-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🗂️
</div>
</div>
</div>
<button onclick="openCSharpProjectInEditor('cursor')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="cursor-csharp-btn" style="display: none;" title="Abrir proyecto en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<button onclick="openCSharpProjectInEditor('vs2022')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="vs2022-csharp-btn" style="display: none;" title="Abrir proyecto en Visual Studio 2022">
💜
</button>
<button onclick="openCSharpProjectFolder()"
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
id="folder-csharp-btn" style="display: none;" title="Abrir carpeta del proyecto">
📁
</button>
<button onclick="copyCSharpProjectPath()"
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
id="copy-path-csharp-btn" style="display: none;" title="Copiar path del proyecto">
📋
</button>
</div>
</div>
<!-- Category Filter -->
<div class="mb-4">
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
<div class="flex flex-wrap gap-2">
<button class="csharp-category-btn active px-3 py-1 rounded-full text-sm border"
data-category="all" onclick="filterCSharpByCategory('all')">
Todas
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Aplicaciones" onclick="filterCSharpByCategory('Aplicaciones')">
🖥️ Aplicaciones
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Herramientas" onclick="filterCSharpByCategory('Herramientas')">
🔧 Herramientas
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border" data-category="APIs"
onclick="filterCSharpByCategory('APIs')">
🌐 APIs
</button>
</div>
</div>
</div>
<!-- C# Favorites Panel -->
<div id="csharp-favorites-panel" class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-blue-800">
⭐ Ejecutables Favoritos
</h3>
<span class="text-sm text-blue-600" id="csharp-favorites-count">
0 favoritos
</span>
</div>
<div id="csharp-favorites-list" class="space-y-2">
<!-- Lista dinámica de favoritos C# -->
</div>
</div>
<!-- C# Executables Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Ejecutables Disponibles</h2>
<button onclick="openCSharpExecutableManager()"
class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600"
id="manage-csharp-executables-btn" style="display: none;">
Gestionar Ejecutables
</button>
</div>
<div id="csharp-executables-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Executable cards dinámicos -->
</div>
</div>
<!-- Running Processes Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
<button onclick="refreshCSharpProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
Actualizar
</button>
</div>
<div id="csharp-running-processes" class="space-y-2">
<!-- Lista dinámica de procesos -->
</div>
</div>
</div>
<!-- Logs (común para ambos sistemas) --> <!-- Logs (común para ambos sistemas) -->
<div class="bg-white p-6 rounded-lg shadow"> <div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
@ -783,6 +913,7 @@
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script> <script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script> <script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script> <script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
<script> <script>
// Inicializar markdown-it globalmente // Inicializar markdown-it globalmente
window.markdownit = window.markdownit || markdownit; window.markdownit = window.markdownit || markdownit;