1520 lines
71 KiB
Python
Executable File
1520 lines
71 KiB
Python
Executable File
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog, filedialog
|
|
import threading
|
|
import time
|
|
import datetime
|
|
import subprocess
|
|
import webbrowser
|
|
import shutil
|
|
import socket
|
|
import queue
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
import platform
|
|
|
|
try:
|
|
import pygame
|
|
pygame.mixer.init()
|
|
except ModuleNotFoundError:
|
|
pygame = None
|
|
|
|
try:
|
|
from cryptography.hazmat.primitives import hashes as crypto_hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding as rsa_padding
|
|
from cryptography.hazmat.backends import default_backend
|
|
except ModuleNotFoundError:
|
|
rsa = None
|
|
|
|
|
|
def start_client():
|
|
root = tk.Tk()
|
|
root.title("Cliente - Panel PSP (mockup)")
|
|
root.geometry("1200x800")
|
|
|
|
# Cola de eventos para comunicación hilo->GUI
|
|
event_queue = queue.Queue()
|
|
|
|
# Función para abrir ventana de correo (T4. Servicios)
|
|
def abrir_ventana_correo():
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
# --- Sesión en memoria ---
|
|
email_session = {'server': None, 'user': None, 'password': None, 'main_win': None}
|
|
|
|
def logout():
|
|
if email_session['main_win']:
|
|
email_session['main_win'].destroy()
|
|
email_session['main_win'] = None
|
|
email_session['server'] = None
|
|
email_session['user'] = None
|
|
email_session['password'] = None
|
|
abrir_ventana_correo() # Volver a pedir login
|
|
|
|
def show_login():
|
|
login_win = tk.Toplevel(root)
|
|
login_win.title("Login Email")
|
|
login_win.geometry("350x250")
|
|
login_win.resizable(False, False)
|
|
login_win.configure(bg="#f5f5f5")
|
|
tk.Label(login_win, text="Acceso Email", font=('Helvetica', 15, 'bold'), bg="#f5f5f5").pack(pady=12)
|
|
frm = tk.Frame(login_win, bg="#f5f5f5")
|
|
frm.pack(pady=10)
|
|
tk.Label(frm, text="Servidor:", bg="#f5f5f5").grid(row=0, column=0, sticky="e", pady=4)
|
|
entry_server = tk.Entry(frm, width=22)
|
|
entry_server.insert(0, email_session['server'] or "10.10.0.101")
|
|
entry_server.grid(row=0, column=1, pady=4)
|
|
tk.Label(frm, text="Usuario:", bg="#f5f5f5").grid(row=1, column=0, sticky="e", pady=4)
|
|
entry_user = tk.Entry(frm, width=22)
|
|
if email_session['user']:
|
|
entry_user.insert(0, email_session['user'])
|
|
entry_user.grid(row=1, column=1, pady=4)
|
|
tk.Label(frm, text="Contraseña:", bg="#f5f5f5").grid(row=2, column=0, sticky="e", pady=4)
|
|
entry_pass = tk.Entry(frm, width=22, show="*")
|
|
if email_session['password']:
|
|
entry_pass.insert(0, email_session['password'])
|
|
entry_pass.grid(row=2, column=1, pady=4)
|
|
status = tk.Label(login_win, text="", bg="#f5f5f5", fg="red")
|
|
status.pack(pady=4)
|
|
|
|
def intentar_login():
|
|
import imaplib
|
|
server = entry_server.get().strip()
|
|
user = entry_user.get().strip()
|
|
password = entry_pass.get()
|
|
if not all([server, user, password]):
|
|
status.config(text="Completa todos los campos")
|
|
return
|
|
try:
|
|
mail = imaplib.IMAP4(server, 143)
|
|
mail.login(user, password)
|
|
mail.logout()
|
|
login_win.destroy()
|
|
email_session['server'] = server
|
|
email_session['user'] = user
|
|
email_session['password'] = password
|
|
mostrar_inbox_y_envio()
|
|
except Exception as e:
|
|
status.config(text=f"Error: {e}")
|
|
|
|
tk.Button(login_win, text="Entrar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=intentar_login).pack(pady=10)
|
|
|
|
def mostrar_inbox_y_envio():
|
|
win = tk.Toplevel(root)
|
|
win.title(f"Correo - {email_session['user']}")
|
|
win.geometry("900x600")
|
|
win.configure(bg="#f5f5f5")
|
|
email_session['main_win'] = win
|
|
top = tk.Frame(win, bg="#f5f5f5")
|
|
top.pack(fill="x")
|
|
tk.Label(top, text=f"Usuario: {email_session['user']}", font=('Arial', 12, 'bold'), bg="#f5f5f5").pack(side="left", padx=10, pady=8)
|
|
tk.Button(top, text="Logout", bg="#ffcccc", command=logout).pack(side="right", padx=10, pady=8)
|
|
btns = tk.Frame(win, bg="#f5f5f5")
|
|
btns.pack(pady=4)
|
|
tk.Button(btns, text="📥 Ver INBOX", bg="#ddeeff", font=('Arial', 11), width=14, command=lambda: ver_correos_recibidos(email_session['server'], email_session['user'], email_session['password'], parent=win)).pack(side="left", padx=10)
|
|
tk.Button(btns, text="📤 Enviar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=lambda: abrir_envio_correo(email_session['server'], email_session['user'], email_session['password'])).pack(side="left", padx=10)
|
|
# Mostrar INBOX directamente
|
|
ver_correos_recibidos(email_session['server'], email_session['user'], email_session['password'], parent=win)
|
|
|
|
# Iniciar login o sesión
|
|
if not email_session['user']:
|
|
show_login()
|
|
else:
|
|
mostrar_inbox_y_envio()
|
|
|
|
# (Eliminada definición duplicada de ver_correos_recibidos)
|
|
def ver_correos_recibidos(server, usuario, contrasena, parent=None):
|
|
import imaplib, email, datetime
|
|
from email.utils import parsedate_to_datetime
|
|
imap_port = 143
|
|
# Si parent es None, crear ventana nueva, si no, usar parent
|
|
if parent is None:
|
|
win = tk.Toplevel(root)
|
|
else:
|
|
# Limpiar parent
|
|
for widget in parent.winfo_children():
|
|
if isinstance(widget, ttk.Treeview) or isinstance(widget, tk.Label):
|
|
widget.destroy()
|
|
win = parent
|
|
win.title(f"INBOX de {usuario}")
|
|
win.configure(bg="#f5f5f5")
|
|
tk.Label(win, text=f"INBOX de {usuario}", font=('Arial', 14, 'bold'), bg="#f5f5f5").pack(pady=8)
|
|
columns = ("De", "Asunto", "Fecha", "Acción")
|
|
tree = ttk.Treeview(win, columns=columns, show="headings", height=18)
|
|
for col, w in zip(columns, (200, 320, 120, 80)):
|
|
tree.heading(col, text=col)
|
|
tree.column(col, width=w, anchor="w")
|
|
tree.pack(padx=10, pady=8, fill="both", expand=True)
|
|
status = tk.Label(win, text="Cargando...", fg="gray", bg="#f5f5f5")
|
|
status.pack(pady=4)
|
|
correos = []
|
|
def cargar():
|
|
try:
|
|
mail = imaplib.IMAP4(server, imap_port)
|
|
mail.login(usuario, contrasena)
|
|
mail.select('INBOX')
|
|
typ, data = mail.search(None, 'ALL')
|
|
ids = data[0].split()
|
|
if not ids:
|
|
status.config(text="No hay correos.")
|
|
return
|
|
for num in reversed(ids):
|
|
typ, msg_data = mail.fetch(num, '(RFC822)')
|
|
for response_part in msg_data:
|
|
if isinstance(response_part, tuple):
|
|
msg = email.message_from_bytes(response_part[1])
|
|
asunto = email.header.decode_header(msg.get('Subject'))[0][0]
|
|
if isinstance(asunto, bytes):
|
|
asunto = asunto.decode(errors='ignore')
|
|
de = msg.get('From')
|
|
fecha_raw = msg.get('Date')
|
|
fecha = fecha_raw or ''
|
|
try:
|
|
if fecha_raw:
|
|
fecha_dt = parsedate_to_datetime(fecha_raw)
|
|
fecha = fecha_dt.strftime('%d/%m/%Y %H:%M')
|
|
except Exception:
|
|
pass
|
|
correos.append((num, de, asunto, fecha, msg))
|
|
tree.insert('', 'end', values=(de, asunto, fecha, 'Ver/Responder'))
|
|
status.config(text=f"Mostrando {len(ids)} correos.")
|
|
mail.logout()
|
|
except Exception as e:
|
|
status.config(text=f"Error: {e}", fg="red")
|
|
threading.Thread(target=cargar, daemon=True).start()
|
|
def on_select(event):
|
|
item = tree.selection()
|
|
if not item:
|
|
return
|
|
idx = tree.index(item[0])
|
|
num, de, asunto, fecha, msg = correos[idx]
|
|
detalle = tk.Toplevel(win)
|
|
detalle.title(f"Correo de {de}")
|
|
detalle.geometry("600x500")
|
|
detalle.configure(bg="#f5f5f5")
|
|
tk.Label(detalle, text=f"De: {de}", font=('Arial', 11, 'bold'), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
|
|
tk.Label(detalle, text=f"Asunto: {asunto}", font=('Arial', 11), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
|
|
tk.Label(detalle, text=f"Fecha: {fecha}", font=('Arial', 11), bg="#f5f5f5").pack(anchor="w", padx=12, pady=4)
|
|
tk.Label(detalle, text="Mensaje:", font=('Arial', 11, 'bold'), bg="#f5f5f5").pack(anchor="w", padx=12, pady=(8,0))
|
|
txt = tk.Text(detalle, width=70, height=14)
|
|
txt.pack(padx=12, pady=4)
|
|
cuerpo = ""
|
|
if msg.is_multipart():
|
|
for part in msg.walk():
|
|
if part.get_content_type() == "text/plain" and not part.get('Content-Disposition'):
|
|
try:
|
|
cuerpo = part.get_payload(decode=True).decode(errors='ignore')
|
|
break
|
|
except:
|
|
pass
|
|
else:
|
|
try:
|
|
cuerpo = msg.get_payload(decode=True).decode(errors='ignore')
|
|
except:
|
|
cuerpo = msg.get_payload()
|
|
txt.insert('1.0', cuerpo)
|
|
txt.config(state='disabled')
|
|
def responder():
|
|
abrir_envio_correo(server, usuario, contrasena, reply_to=de, reply_subject=asunto)
|
|
tk.Button(detalle, text="Responder", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=responder).pack(pady=10)
|
|
tree.bind('<Double-1>', on_select)
|
|
|
|
def abrir_envio_correo(server, user, password):
|
|
envio_win = tk.Toplevel(root)
|
|
envio_win.title("Enviar Correo")
|
|
envio_win.geometry("520x400")
|
|
envio_win.resizable(False, False)
|
|
envio_win.configure(bg="#f5f5f5")
|
|
tk.Label(envio_win, text="Enviar Correo", font=('Helvetica', 15, 'bold'), bg="#f5f5f5").pack(pady=10)
|
|
frm = tk.Frame(envio_win, bg="#f5f5f5")
|
|
frm.pack(pady=8)
|
|
tk.Label(frm, text="Para:", bg="#f5f5f5").grid(row=0, column=0, sticky="e", pady=4)
|
|
entry_to = tk.Entry(frm, width=35)
|
|
entry_to.grid(row=0, column=1, pady=4)
|
|
tk.Label(frm, text="Asunto:", bg="#f5f5f5").grid(row=1, column=0, sticky="e", pady=4)
|
|
entry_subject = tk.Entry(frm, width=35)
|
|
entry_subject.grid(row=1, column=1, pady=4)
|
|
tk.Label(frm, text="Mensaje:", bg="#f5f5f5").grid(row=2, column=0, sticky="ne", pady=4)
|
|
entry_body = tk.Text(frm, width=35, height=8)
|
|
entry_body.grid(row=2, column=1, pady=4)
|
|
status = tk.Label(envio_win, text="", bg="#f5f5f5", fg="gray")
|
|
status.pack(pady=4)
|
|
def enviar():
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
to_addr = entry_to.get().strip()
|
|
subject = entry_subject.get().strip()
|
|
body = entry_body.get("1.0", "end-1c").strip()
|
|
if not all([to_addr, subject, body]):
|
|
status.config(text="Completa todos los campos", fg="red")
|
|
return
|
|
def worker():
|
|
try:
|
|
status.config(text="Enviando...", fg="blue")
|
|
msg = MIMEMultipart()
|
|
msg['From'] = user
|
|
msg['To'] = to_addr
|
|
msg['Subject'] = subject
|
|
msg.attach(MIMEText(body, 'plain'))
|
|
with smtplib.SMTP(server, 25, timeout=10) as smtp:
|
|
try:
|
|
smtp.starttls()
|
|
except:
|
|
pass
|
|
if user and password:
|
|
try:
|
|
smtp.login(user, password)
|
|
except:
|
|
pass
|
|
smtp.send_message(msg)
|
|
status.config(text="✓ Correo enviado correctamente", fg="green")
|
|
except Exception as e:
|
|
status.config(text=f"✗ Error: {str(e)[:60]}", fg="red")
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
tk.Button(envio_win, text="Enviar", bg="#90EE90", font=('Arial', 11, 'bold'), width=12, command=enviar).pack(pady=10)
|
|
|
|
# Grid principal: top bar, main content, status
|
|
root.columnconfigure(0, weight=0, minsize=240)
|
|
root.columnconfigure(1, weight=1)
|
|
root.columnconfigure(2, weight=0, minsize=320)
|
|
root.rowconfigure(0, weight=0) # barra superior fija
|
|
root.rowconfigure(1, weight=1) # contenido
|
|
root.rowconfigure(2, weight=0) # barra estado
|
|
|
|
# Barra superior T1..T5 + Configuración
|
|
top_bar = tk.Frame(root, bg='#ffffff', height=40)
|
|
top_bar.grid(row=0, column=0, columnspan=3, sticky='ew')
|
|
top_bar.grid_propagate(False)
|
|
|
|
categorias = [
|
|
('T1. Procesos', '#1e90ff'),
|
|
('T2.Threads', '#c71585'),
|
|
('T3. Sockets', '#ff4500'),
|
|
('T4. Servicios', '#228b22'),
|
|
('T5. Seguridad', '#daa520'),
|
|
('Configuración', '#d3d3d3')
|
|
]
|
|
|
|
def seleccionar_categoria(nombre):
|
|
if nombre == 'T4. Servicios':
|
|
abrir_ventana_correo()
|
|
else:
|
|
info_label.config(text=f"Categoría seleccionada: {nombre}")
|
|
# Creamos después info_label; el callback se ejecuta luego y tendrá acceso.
|
|
|
|
for i, (texto, color) in enumerate(categorias):
|
|
lbl = tk.Label(top_bar, text=texto, bg=color, fg='white', font=('Helvetica', 11, 'bold'), padx=10, pady=6, cursor='hand2')
|
|
lbl.pack(side='left', padx=(8 if i==0 else 4, 4), pady=4)
|
|
lbl.bind('<Button-1>', lambda e, n=texto: seleccionar_categoria(n))
|
|
|
|
# Frames principales
|
|
left = tk.Frame(root, bg="#f8f8f8")
|
|
center = tk.Frame(root, bg="#ffffff")
|
|
right = tk.Frame(root, bg="#ffffff")
|
|
|
|
left.grid(row=1, column=0, sticky="nsw", padx=6, pady=(6,6))
|
|
center.grid(row=1, column=1, sticky="nsew", padx=6, pady=(6,6))
|
|
right.grid(row=1, column=2, sticky="nse", padx=6, pady=(6,6))
|
|
|
|
left.grid_propagate(False)
|
|
right.grid_propagate(False)
|
|
|
|
# LEFT: acciones y listas
|
|
def section(parent, title):
|
|
f = tk.LabelFrame(parent, text=title, padx=6, pady=6)
|
|
f.pack(fill="x", pady=8, padx=8)
|
|
return f
|
|
|
|
s_actions = section(left, "")
|
|
|
|
# --- funcionalidad adicional ---
|
|
def open_vscode():
|
|
exe = shutil.which('code') or shutil.which('code-insiders') or shutil.which('code-oss')
|
|
if exe:
|
|
try:
|
|
subprocess.Popen([exe])
|
|
info_label.config(text="Abriendo Visual Studio Code...")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"No se pudo abrir VS Code: {e}")
|
|
else:
|
|
webbrowser.open('https://code.visualstudio.com/')
|
|
info_label.config(text="VS Code no encontrado: abriendo web como alternativa")
|
|
|
|
def open_browser():
|
|
url = simpledialog.askstring('Abrir URL', 'Ingresa la URL a abrir:')
|
|
if url and url.strip():
|
|
url = url.strip()
|
|
if not url.startswith(('http://', 'https://')):
|
|
url = 'https://' + url
|
|
try:
|
|
webbrowser.open(url)
|
|
event_queue.put(('status', f'Abierto navegador: {url}'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error: {e}'))
|
|
|
|
def buscar_google():
|
|
url = 'https://publicapis.dev/'
|
|
def worker():
|
|
event_queue.put(('status', f'Scraping APIs desde {url}...'))
|
|
try:
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
|
|
# Extraer APIs con sus URLs
|
|
apis = []
|
|
|
|
# Buscar todos los links que contengan URLs de APIs
|
|
links = soup.find_all('a', href=True)
|
|
for link in links:
|
|
href = link.get('href', '').strip()
|
|
texto = link.get_text().strip()
|
|
|
|
# Filtrar links válidos
|
|
if (href and (href.startswith('http://') or href.startswith('https://')) and
|
|
len(texto) > 2 and len(texto) < 100 and
|
|
texto not in [t[0] for t in apis]): # Evitar duplicados por nombre
|
|
apis.append((texto, href))
|
|
|
|
# Si no encuentra suficientes, buscar en divs
|
|
if len(apis) < 10:
|
|
api_elements = soup.find_all(['div', 'section', 'article'])
|
|
for elem in api_elements[:50]:
|
|
elem_link = elem.find('a', href=True)
|
|
if elem_link:
|
|
href = elem_link.get('href', '').strip()
|
|
texto = elem.get_text().strip()[:80]
|
|
if (href and (href.startswith('http://') or href.startswith('https://')) and
|
|
len(texto) > 2 and
|
|
(texto, href) not in apis):
|
|
apis.append((texto, href))
|
|
|
|
# Construir resultado - TODAS las APIs con URLs
|
|
resultado = f'\n{"="*70}\n'
|
|
resultado += f'URL: {url}\n'
|
|
resultado += f'{"="*70}\n\n'
|
|
resultado += f'🔗 APIs ENCONTRADAS ({len(apis)})\n'
|
|
resultado += f'{"-"*70}\n'
|
|
if apis:
|
|
for nombre, api_url in apis:
|
|
resultado += f' • {nombre}\n'
|
|
resultado += f' URL: {api_url}\n\n'
|
|
else:
|
|
resultado += ' [No se encontraron]\n'
|
|
resultado += f'{"="*70}\n'
|
|
|
|
event_queue.put(('scrape_result', resultado))
|
|
event_queue.put(('status', f'✓ APIs encontradas: {len(apis)}'))
|
|
except Exception as e:
|
|
event_queue.put(('scrape_result', f'ERROR AL PROCESAR:\n{str(e)}'))
|
|
event_queue.put(('status', f'✗ Error: {e}'))
|
|
threading.Thread(target=worker, daemon=True, name='APIsScraper').start()
|
|
|
|
|
|
# Alarma
|
|
def configurar_alarma():
|
|
minutos = simpledialog.askinteger('Alarma', 'Minutos hasta alarma', minvalue=1, maxvalue=720)
|
|
if not minutos:
|
|
return
|
|
def worker():
|
|
target = time.time() + minutos*60
|
|
while True:
|
|
restante = int(target - time.time())
|
|
if restante <= 0:
|
|
event_queue.put(('alarm', '¡Alarma!'))
|
|
break
|
|
event_queue.put(('alarm_progress', restante))
|
|
time.sleep(1)
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
tk.Button(s_actions, text='Programar Alarma', bg='#ffe0ff', width=24, command=configurar_alarma).pack(pady=6)
|
|
tk.Button(s_actions, text="Navegar (URL)", bg="#dff0d8", width=24, command=open_browser).pack(pady=6)
|
|
tk.Button(s_actions, text="Buscar API Google", bg="#dff0d8", width=24, command=buscar_google).pack(pady=6)
|
|
|
|
# Launch external command with parameters
|
|
def launch_command():
|
|
cmd = simpledialog.askstring("Lanzar comando", "Introduce el comando a ejecutar (ej: firefox https://google.com):")
|
|
if not cmd:
|
|
return
|
|
try:
|
|
# split by shell to allow parameters; run via shell for convenience
|
|
subprocess.Popen(cmd, shell=True)
|
|
info_label.config(text=f"Lanzado: {cmd}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"No se pudo ejecutar el comando: {e}")
|
|
|
|
tk.Button(s_actions, text="Lanzar aplicación (con parámetros)", bg="#e8f4ff", width=30, command=launch_command).pack(pady=6)
|
|
|
|
# Execute PowerShell script (.ps1) if pwsh/powershell available
|
|
def run_powershell_script():
|
|
path = filedialog.askopenfilename(title="Selecciona script PowerShell (.ps1)", filetypes=[("PowerShell", "*.ps1" )])
|
|
if not path:
|
|
return
|
|
exe = shutil.which('pwsh') or shutil.which('powershell')
|
|
if not exe:
|
|
messagebox.showwarning("pwsh no encontrado", "No se encontró 'pwsh' ni 'powershell' en PATH. En Linux puedes instalar PowerShell Core (pwsh) o ejecutar el script en Windows.")
|
|
return
|
|
try:
|
|
subprocess.Popen([exe, '-File', path])
|
|
info_label.config(text=f"Ejecutando script PowerShell: {path}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Fallo al ejecutar el script: {e}")
|
|
|
|
tk.Button(s_actions, text="Ejecutar .ps1 (PowerShell)", bg="#ffeedd", width=30, command=run_powershell_script).pack(pady=6)
|
|
|
|
s_apps = section(left, "Aplicaciones")
|
|
tk.Button(s_apps, text="Visual Code", bg="#e6f7ff", width=24, command=open_vscode).pack(pady=4)
|
|
tk.Button(s_apps, text="App2", bg="#e6f7ff", width=24).pack(pady=4)
|
|
tk.Button(s_apps, text="App3", bg="#e6f7ff", width=24).pack(pady=4)
|
|
|
|
# Resource monitor and editor
|
|
def open_text_editor():
|
|
ed = tk.Toplevel(root)
|
|
ed.title("Editor - Notepad simple")
|
|
ed.geometry("700x500")
|
|
text = tk.Text(ed)
|
|
text.pack(fill='both', expand=True)
|
|
|
|
def save_file():
|
|
p = filedialog.asksaveasfilename(defaultextension='.txt')
|
|
if p:
|
|
with open(p, 'w', encoding='utf-8') as f:
|
|
f.write(text.get('1.0', 'end'))
|
|
info_label.config(text=f"Fichero guardado: {p}")
|
|
|
|
def open_file():
|
|
p = filedialog.askopenfilename()
|
|
if p:
|
|
with open(p, 'r', encoding='utf-8', errors='ignore') as f:
|
|
text.delete('1.0', 'end')
|
|
text.insert('1.0', f.read())
|
|
info_label.config(text=f"Fichero abierto: {p}")
|
|
|
|
btns = tk.Frame(ed)
|
|
btns.pack(fill='x')
|
|
tk.Button(btns, text='Abrir', command=open_file).pack(side='left')
|
|
tk.Button(btns, text='Guardar', command=save_file).pack(side='left')
|
|
|
|
tk.Button(s_apps, text="Editor texto", bg="#f0e6ff", width=24, command=open_text_editor).pack(pady=6)
|
|
# Hash archivo
|
|
def hash_archivo():
|
|
path = filedialog.askopenfilename(title='Selecciona archivo para hash')
|
|
if not path:
|
|
return
|
|
def worker():
|
|
event_queue.put(('status', f'Calculando SHA256 de {path}'))
|
|
try:
|
|
h = hashlib.sha256()
|
|
with open(path, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(8192), b''):
|
|
h.update(chunk)
|
|
event_queue.put(('hash', f'{path}\nSHA256: {h.hexdigest()}'))
|
|
event_queue.put(('status', 'Hash completado'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error hash: {e}'))
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
tk.Button(s_apps, text='Hash archivo', bg='#e6ffe6', width=24, command=hash_archivo).pack(pady=4)
|
|
|
|
def open_resource_monitor():
|
|
# Monitor gráfico sin depender de Pillow: Canvas puro.
|
|
try:
|
|
import psutil
|
|
except ModuleNotFoundError:
|
|
messagebox.showerror("Dependencia falta", "Instala 'psutil' para monitor de recursos: pip install psutil")
|
|
return
|
|
|
|
win = tk.Toplevel(root)
|
|
win.title('Monitor recursos (Canvas)')
|
|
width, height = 600, 360
|
|
cv = tk.Canvas(win, width=width, height=height, bg='white')
|
|
cv.pack(fill='both', expand=True)
|
|
|
|
# Series
|
|
cpu_data = []
|
|
mem_data = []
|
|
thr_data = []
|
|
maxlen = 120
|
|
|
|
def draw_axes():
|
|
cv.delete('axis')
|
|
cv.create_rectangle(50,20,width-20,height-20, outline='#444', tags='axis')
|
|
for i in range(6):
|
|
y = 20 + (height-40)*i/5
|
|
cv.create_line(50,y,width-20,y, fill='#eee', tags='axis')
|
|
cv.create_text(10,20, text='100%', anchor='nw', tags='axis')
|
|
cv.create_text(10,height-40, text='0%', anchor='nw', tags='axis')
|
|
cv.create_text(width-120,10, text='CPU( rojo ) MEM( verde ) HILOS( azul )', anchor='nw', tags='axis')
|
|
|
|
def scale_y(val, series_max=100):
|
|
# map 0..series_max to canvas space
|
|
return 20 + (height-40)*(1 - val/series_max)
|
|
|
|
def draw_series():
|
|
cv.delete('series')
|
|
# Determine dynamic max for threads
|
|
thr_max = max(thr_data) if thr_data else 1
|
|
def draw_line(data, color, yscale_max):
|
|
if len(data) < 2:
|
|
return
|
|
step_x = (width-70)/ (maxlen-1)
|
|
pts = []
|
|
start_index = max(0, len(data)-maxlen)
|
|
for idx, val in enumerate(data[start_index:]):
|
|
x = 50 + step_x*idx
|
|
y = scale_y(val, yscale_max)
|
|
pts.append((x,y))
|
|
for i in range(len(pts)-1):
|
|
cv.create_line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1], fill=color, width=2, tags='series')
|
|
draw_line(cpu_data, 'red', 100)
|
|
draw_line(mem_data, 'green', 100)
|
|
draw_line(thr_data, 'blue', thr_max)
|
|
if thr_data:
|
|
cv.create_text(width-180,height-25, text=f'Threads max: {thr_max}', anchor='nw', tags='series', fill='blue')
|
|
|
|
def update():
|
|
try:
|
|
cpu = psutil.cpu_percent(interval=None)
|
|
mem = psutil.virtual_memory().percent
|
|
thr = sum(p.num_threads() for p in psutil.process_iter())
|
|
cpu_data.append(cpu); mem_data.append(mem); thr_data.append(thr)
|
|
cpu_data[:] = cpu_data[-maxlen:]
|
|
mem_data[:] = mem_data[-maxlen:]
|
|
thr_data[:] = thr_data[-maxlen:]
|
|
draw_axes(); draw_series()
|
|
cv.create_text(60, height-25, text=f'CPU {cpu:.1f}% MEM {mem:.1f}% HILOS {thr}', anchor='nw', fill='#222', tags='series')
|
|
except tk.TclError:
|
|
return
|
|
win.after(1000, update)
|
|
draw_axes(); update()
|
|
|
|
tk.Button(left, text="Monitor recursos (gráficas)", bg="#fff2cc", width=30, command=open_resource_monitor).pack(pady=6)
|
|
|
|
s_batch = section(left, "Procesos batch")
|
|
def realizar_backup():
|
|
# OS-aware backup: Windows uses PowerShell Compress-Archive; Linux uses tar.gz
|
|
src = filedialog.askdirectory(title='Directorio origen a respaldar')
|
|
if not src:
|
|
return
|
|
dest = filedialog.asksaveasfilename(title='Archivo destino backup', defaultextension='.zip' if platform.system()=='Windows' else '.tar.gz')
|
|
if not dest:
|
|
return
|
|
def worker():
|
|
event_queue.put(('status', f'Iniciando backup de {src} -> {dest}'))
|
|
if platform.system()=='Windows':
|
|
exe = shutil.which('powershell') or shutil.which('pwsh')
|
|
if not exe:
|
|
event_queue.put(('status','PowerShell no encontrado'))
|
|
return
|
|
# Compress-Archive -Path src -DestinationPath dest
|
|
cmd = [exe, '-Command', f"Compress-Archive -Path '{src}' -DestinationPath '{dest}' -Force"]
|
|
try:
|
|
subprocess.run(cmd, timeout=600)
|
|
event_queue.put(('status','Backup completado (Windows)'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error backup: {e}'))
|
|
else:
|
|
# Linux/Unix: tar -czf dest -C parent basename
|
|
import os
|
|
parent = os.path.dirname(src)
|
|
base = os.path.basename(src)
|
|
cmd = ['tar','-czf',dest,'-C',parent,base]
|
|
try:
|
|
subprocess.run(cmd, timeout=600)
|
|
event_queue.put(('status','Backup completado (tar.gz)'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error backup: {e}'))
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
tk.Button(s_batch, text="Copias de seguridad", bg="#fff0d6", width=24, command=realizar_backup).pack(pady=6)
|
|
|
|
# CENTER: Notebook grande + panel inferior
|
|
center.rowconfigure(0, weight=1)
|
|
center.rowconfigure(1, weight=0)
|
|
center.columnconfigure(0, weight=1)
|
|
|
|
notebook = ttk.Notebook(center)
|
|
notebook.grid(row=0, column=0, sticky="nsew", padx=6, pady=6)
|
|
|
|
# store text widgets so we can update a specific tab programmatically
|
|
tab_texts = {}
|
|
tab_names = ["Resultados", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces", "Servicios", "Seguridad"]
|
|
for name in tab_names:
|
|
f = ttk.Frame(notebook)
|
|
notebook.add(f, text=name)
|
|
|
|
# Añadir botones ANTES del área de texto para que sean visibles
|
|
if name == 'Navegador':
|
|
tk.Label(f, text='Herramientas de navegación:', font=('Arial', 10, 'bold')).pack(anchor='w', padx=6, pady=4)
|
|
nav_frame = tk.Frame(f)
|
|
nav_frame.pack(anchor='w', padx=6, pady=4)
|
|
|
|
def abrir_url_navegador():
|
|
url = simpledialog.askstring('Abrir URL', 'Ingresa la URL a abrir:')
|
|
if url and url.strip():
|
|
url = url.strip()
|
|
if not url.startswith(('http://', 'https://')):
|
|
url = 'https://' + url
|
|
try:
|
|
webbrowser.open(url)
|
|
event_queue.put(('status', f'Abriendo: {url}'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error: {e}'))
|
|
|
|
def iniciar_scraping():
|
|
url = simpledialog.askstring('Web Scraping', 'Ingresa la URL a escanear:')
|
|
if url and url.strip():
|
|
url = url.strip()
|
|
if not url.startswith(('http://', 'https://')):
|
|
url = 'https://' + url
|
|
def worker():
|
|
event_queue.put(('status', f'Scraping: {url}...'))
|
|
try:
|
|
import requests, re
|
|
from bs4 import BeautifulSoup
|
|
response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
|
|
# Extraer emails
|
|
emails = sorted(list(set(re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', response.text))))
|
|
|
|
# Extraer teléfonos
|
|
telefonos = sorted(list(set(re.findall(r'(?:\+?\d{1,3}[-.]?)?\(?\d{2,4}\)?[-.]?\d{2,4}[-.]?\d{2,4}|\d{9,}', response.text))))
|
|
telefonos = [t for t in telefonos if len(t) >= 9]
|
|
|
|
# Extraer nombres/títulos
|
|
nombres = set()
|
|
for elem in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'b', 'strong', 'a']):
|
|
texto = elem.get_text().strip()
|
|
if 2 <= len(texto) <= 50:
|
|
nombres.add(texto)
|
|
nombres = sorted(list(nombres))[:30]
|
|
|
|
# Construir resultado con mejor formato
|
|
resultado = f'\n{"="*70}\n'
|
|
resultado += f'URL: {url}\n'
|
|
resultado += f'{"="*70}\n\n'
|
|
|
|
# Sección de Emails
|
|
resultado += f'📧 EMAILS ({len(emails)})\n'
|
|
resultado += f'{"-"*70}\n'
|
|
if emails:
|
|
for e in emails[:15]:
|
|
resultado += f' {e}\n'
|
|
if len(emails) > 15:
|
|
resultado += f'\n ... y {len(emails)-15} más\n'
|
|
else:
|
|
resultado += ' [No se encontraron]\n'
|
|
resultado += '\n'
|
|
|
|
# Sección de Teléfonos
|
|
resultado += f'📞 TELÉFONOS ({len(telefonos)})\n'
|
|
resultado += f'{"-"*70}\n'
|
|
if telefonos:
|
|
for t in telefonos[:15]:
|
|
resultado += f' {t}\n'
|
|
if len(telefonos) > 15:
|
|
resultado += f'\n ... y {len(telefonos)-15} más\n'
|
|
else:
|
|
resultado += ' [No se encontraron]\n'
|
|
resultado += '\n'
|
|
|
|
# Sección de Títulos/Nombres
|
|
resultado += f'👥 TÍTULOS/NOMBRES ({len(nombres)})\n'
|
|
resultado += f'{"-"*70}\n'
|
|
if nombres:
|
|
for n in nombres[:20]:
|
|
resultado += f' {n}\n'
|
|
if len(nombres) > 20:
|
|
resultado += f'\n ... y {len(nombres)-20} más\n'
|
|
else:
|
|
resultado += ' [No se encontraron]\n'
|
|
resultado += f'\n{"="*70}\n'
|
|
|
|
event_queue.put(('scrape_result', resultado))
|
|
event_queue.put(('status', f'✓ Completado: {len(emails)} emails, {len(telefonos)} telefonos'))
|
|
except Exception as e:
|
|
event_queue.put(('scrape_result', f'ERROR AL PROCESAR:\n{str(e)}'))
|
|
event_queue.put(('status', f'✗ Error: {e}'))
|
|
threading.Thread(target=worker, daemon=True, name='Scraper').start()
|
|
|
|
tk.Button(nav_frame, text='🌐 Abrir URL', bg='#87CEEB', font=('Arial', 10, 'bold'), command=abrir_url_navegador).pack(side='left', padx=2)
|
|
tk.Button(nav_frame, text='🔍 Extraer Datos', bg='#FFB6C1', font=('Arial', 10, 'bold'), command=iniciar_scraping).pack(side='left', padx=2)
|
|
|
|
if name == 'Tareas':
|
|
btn_frame = tk.Frame(f)
|
|
btn_frame.pack(anchor='w', padx=6, pady=4)
|
|
tk.Button(btn_frame, text='🏁 Iniciar Carrera de Camellos', bg='#90EE90', font=('Arial', 10, 'bold'),
|
|
command=lambda: iniciar_carrera(tab_texts['Tareas'])).pack(side='left', padx=2)
|
|
|
|
if name == 'Servicios':
|
|
srv_frame = tk.Frame(f)
|
|
srv_frame.pack(anchor='w', padx=6, pady=4)
|
|
tk.Button(srv_frame, text='POP3 listar', command=lambda: servicio_pop3()).pack(side='left', padx=2)
|
|
tk.Button(srv_frame, text='SMTP enviar', command=lambda: servicio_smtp()).pack(side='left', padx=2)
|
|
tk.Button(srv_frame, text='FTP listar', command=lambda: servicio_ftp()).pack(side='left', padx=2)
|
|
tk.Button(srv_frame, text='HTTP GET', command=lambda: consumir_api('https://httpbin.org/get')).pack(side='left', padx=2)
|
|
|
|
if name == 'Seguridad':
|
|
sec_frame = tk.Frame(f)
|
|
sec_frame.pack(anchor='w', padx=6, pady=4)
|
|
tk.Button(sec_frame, text='Generar RSA', command=lambda: generar_rsa()).pack(side='left', padx=2)
|
|
tk.Button(sec_frame, text='AES Cifrar', command=lambda: aes_cifrar()).pack(side='left', padx=2)
|
|
|
|
# Área de texto después de los botones
|
|
txt = tk.Text(f)
|
|
txt.insert("1.0", f"{name} - área de contenido\n\n")
|
|
txt.pack(fill="both", expand=True, padx=6, pady=6)
|
|
tab_texts[name] = txt
|
|
|
|
info = tk.Frame(center, bg="#f7fff0", height=120)
|
|
info.grid(row=1, column=0, sticky="ew", padx=6, pady=(0,6))
|
|
info.grid_propagate(False)
|
|
info_label = tk.Label(info, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#f7fff0", anchor="w")
|
|
info_label.pack(fill='both', expand=True, padx=8, pady=8)
|
|
|
|
|
|
|
|
|
|
# Chat único en panel derecho, requiere nombre de usuario y conexión
|
|
|
|
chat_box = tk.LabelFrame(right, text="Chat - Servidor", padx=6, pady=6)
|
|
chat_box.pack(fill="both", expand=True, padx=8, pady=(4,8))
|
|
chat_history = tk.Text(chat_box, height=18, state='disabled', bg='#f5f5f5', wrap='word')
|
|
chat_history.pack(fill="both", expand=True, pady=4)
|
|
chat_history.tag_config('tu', foreground='#1565C0', font=('Arial', 9, 'bold'))
|
|
chat_history.tag_config('otro', foreground='#388E3C', font=('Arial', 9))
|
|
chat_history.tag_config('sistema', foreground='#757575', font=('Arial', 8, 'italic'))
|
|
def agregar_chat(texto, tag='sistema'):
|
|
chat_history.config(state='normal')
|
|
chat_history.insert('end', texto + '\n', tag)
|
|
chat_history.see('end')
|
|
chat_history.config(state='disabled')
|
|
|
|
# Lista de usuarios conectados
|
|
users_frame = tk.Frame(chat_box)
|
|
users_frame.pack(fill="x", pady=(0,4))
|
|
tk.Label(users_frame, text="Conectados:").pack(side="left")
|
|
|
|
users_listbox = tk.Listbox(users_frame, height=4, width=24)
|
|
users_listbox.pack(side="left", padx=(2,8))
|
|
|
|
def escribir_privado(event):
|
|
seleccion = users_listbox.curselection()
|
|
if not seleccion:
|
|
return
|
|
usuario_destino = users_listbox.get(seleccion[0])
|
|
mi_usuario = entry_user.get().strip()
|
|
if usuario_destino == mi_usuario:
|
|
return
|
|
# Ventana para mensaje privado
|
|
win = tk.Toplevel(root)
|
|
win.title(f"Mensaje privado a {usuario_destino}")
|
|
win.geometry("350x180")
|
|
tk.Label(win, text=f"Privado para: {usuario_destino}", font=('Arial', 11, 'bold')).pack(pady=8)
|
|
entry_msg = tk.Text(win, height=4)
|
|
entry_msg.pack(fill="x", padx=10, pady=6)
|
|
def enviar():
|
|
texto = entry_msg.get("1.0", "end-1c").strip()
|
|
if texto:
|
|
msg_obj = {'type': 'msg', 'from': mi_usuario, 'text': texto, 'to': usuario_destino}
|
|
try:
|
|
chat_state['socket'].send(json.dumps(msg_obj).encode('utf-8'))
|
|
agregar_chat(f"(Privado a {usuario_destino}) {mi_usuario}: {texto}", 'tu')
|
|
except Exception as e:
|
|
agregar_chat(f"Error al enviar: {e}", 'sistema')
|
|
win.destroy()
|
|
tk.Button(win, text="Enviar", bg="#cfe8cf", command=enviar).pack(pady=8)
|
|
entry_msg.focus_set()
|
|
|
|
users_listbox.bind('<Double-Button-1>', escribir_privado)
|
|
|
|
user_frame = tk.Frame(chat_box)
|
|
user_frame.pack(fill="x", pady=(0,4))
|
|
tk.Label(user_frame, text="Usuario:").pack(side="left")
|
|
entry_user = tk.Entry(user_frame, width=18)
|
|
entry_user.pack(side="left", padx=(2,8))
|
|
conn_frame = tk.Frame(chat_box)
|
|
conn_frame.pack(fill="x", pady=(0,4))
|
|
tk.Label(conn_frame, text="IP:").pack(side="left")
|
|
ip_entry = tk.Entry(conn_frame, width=12)
|
|
ip_entry.insert(0, "127.0.0.1")
|
|
ip_entry.pack(side="left", padx=(2,8))
|
|
tk.Label(conn_frame, text="Puerto:").pack(side="left")
|
|
puerto_entry = tk.Entry(conn_frame, width=6)
|
|
puerto_entry.insert(0, "3333")
|
|
puerto_entry.pack(side="left", padx=(2,8))
|
|
chat_state = {'socket': None, 'connected': False}
|
|
|
|
import json
|
|
|
|
def actualizar_usuarios(usuarios):
|
|
users_listbox.delete(0, 'end')
|
|
for u in usuarios:
|
|
users_listbox.insert('end', u)
|
|
|
|
def conectar():
|
|
if chat_state['connected']:
|
|
messagebox.showinfo("Chat", "Ya estás conectado")
|
|
return
|
|
try:
|
|
ip = ip_entry.get().strip()
|
|
puerto = int(puerto_entry.get().strip())
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect((ip, puerto))
|
|
usuario = entry_user.get().strip()
|
|
if not usuario:
|
|
messagebox.showwarning("Chat", "Debes poner un nombre de usuario")
|
|
return
|
|
# Enviar login
|
|
login_msg = json.dumps({'type': 'login', 'username': usuario}).encode('utf-8')
|
|
sock.send(login_msg)
|
|
chat_state['socket'] = sock
|
|
chat_state['connected'] = True
|
|
agregar_chat(f"Conectado a {ip}:{puerto}", 'sistema')
|
|
btn_conectar.config(text="Desconectar", bg="#ffcccc", command=desconectar)
|
|
def recibir():
|
|
while chat_state['connected']:
|
|
try:
|
|
data = sock.recv(1024)
|
|
if not data:
|
|
break
|
|
try:
|
|
msg = data.decode('utf-8')
|
|
if msg.startswith('{'):
|
|
obj = json.loads(msg)
|
|
if obj.get('type') == 'user_list':
|
|
actualizar_usuarios(obj.get('users', []))
|
|
elif obj.get('type') == 'msg':
|
|
remitente = obj.get('from', 'otro')
|
|
texto = obj.get('text', '')
|
|
es_privado = obj.get('private', False)
|
|
# Solo mostrar si el remitente no es el usuario actual
|
|
if remitente != entry_user.get().strip():
|
|
if es_privado:
|
|
agregar_chat(f"(Privado) {remitente}: {texto}", 'otro')
|
|
else:
|
|
agregar_chat(f"{remitente}: {texto}", 'otro')
|
|
else:
|
|
agregar_chat(msg, 'sistema')
|
|
else:
|
|
agregar_chat(msg, 'otro')
|
|
except Exception:
|
|
agregar_chat(data.decode('utf-8'), 'otro')
|
|
except:
|
|
break
|
|
chat_state['connected'] = False
|
|
chat_state['socket'] = None
|
|
agregar_chat("Desconectado del servidor", 'sistema')
|
|
btn_conectar.config(text="Conectar", bg="#ddeeff", command=conectar)
|
|
actualizar_usuarios([])
|
|
threading.Thread(target=recibir, daemon=True).start()
|
|
except Exception as e:
|
|
agregar_chat(f"Error: {e}", 'sistema')
|
|
|
|
def desconectar():
|
|
if chat_state['connected'] and chat_state['socket']:
|
|
try:
|
|
chat_state['socket'].close()
|
|
except:
|
|
pass
|
|
chat_state['connected'] = False
|
|
chat_state['socket'] = None
|
|
agregar_chat("Desconectado", 'sistema')
|
|
btn_conectar.config(text="Conectar", bg="#ddeeff", command=conectar)
|
|
actualizar_usuarios([])
|
|
|
|
btn_conectar = tk.Button(conn_frame, text="Conectar", bg="#ddeeff", command=conectar)
|
|
btn_conectar.pack(side="left", padx=4)
|
|
tk.Label(chat_box, text="Mensaje:").pack(anchor="w")
|
|
msg = tk.Text(chat_box, height=3)
|
|
msg.pack(fill="x", pady=4)
|
|
|
|
|
|
def enviar_mensaje(event=None):
|
|
usuario = entry_user.get().strip()
|
|
if not usuario:
|
|
messagebox.showwarning("Chat", "Debes poner un nombre de usuario")
|
|
return "break"
|
|
if not chat_state['connected']:
|
|
messagebox.showwarning("Chat", "Primero conecta al servidor")
|
|
return "break"
|
|
texto = msg.get("1.0", "end-1c").strip()
|
|
if texto:
|
|
try:
|
|
seleccion = users_listbox.curselection()
|
|
to_users = []
|
|
for idx in seleccion:
|
|
u = users_listbox.get(idx)
|
|
if u != usuario:
|
|
to_users.append(u)
|
|
msg_obj = {'type': 'msg', 'from': usuario, 'text': texto}
|
|
if len(to_users) == 1:
|
|
msg_obj['to'] = to_users[0]
|
|
agregar_chat(f"(Privado a {to_users[0]}) {usuario}: {texto}", 'tu')
|
|
elif len(to_users) > 1:
|
|
msg_obj['to'] = to_users
|
|
agregar_chat(f"(Grupo a {', '.join(to_users)}) {usuario}: {texto}", 'tu')
|
|
else:
|
|
agregar_chat(f"{usuario}: {texto}", 'tu')
|
|
chat_state['socket'].send(json.dumps(msg_obj).encode('utf-8'))
|
|
msg.delete("1.0", "end")
|
|
except Exception as e:
|
|
agregar_chat(f"Error al enviar: {e}", 'sistema')
|
|
return "break"
|
|
|
|
msg.bind('<Return>', enviar_mensaje)
|
|
tk.Button(chat_box, text="Enviar", bg="#cfe8cf", command=enviar_mensaje).pack(pady=(0,6))
|
|
|
|
# Reproductor música
|
|
music_state = {'path': None, 'thread': None, 'playing': False, 'stopping': False}
|
|
def seleccionar_musica():
|
|
p = filedialog.askopenfilename(title='Seleccionar audio', filetypes=[('Audio','*.wav *.mp3 *.ogg'), ('Todos','*.*')])
|
|
if p:
|
|
music_state['path'] = p
|
|
info_label.config(text=f'Audio: {p}')
|
|
def reproducir_musica():
|
|
if pygame is None:
|
|
messagebox.showwarning('Audio', 'Instala pygame: pip install pygame')
|
|
return
|
|
path = music_state.get('path')
|
|
if not path:
|
|
seleccionar_musica(); path = music_state.get('path')
|
|
if not path:
|
|
return
|
|
if pygame.mixer.music.get_busy():
|
|
event_queue.put(('status', 'Ya reproduciendo'))
|
|
return
|
|
music_state['stopping'] = False
|
|
def worker():
|
|
event_queue.put(('status', f'Reproduciendo {path}'))
|
|
try:
|
|
pygame.mixer.music.load(path)
|
|
pygame.mixer.music.play()
|
|
music_state['playing'] = True
|
|
# Esperar mientras reproduce y no se detiene
|
|
while pygame.mixer.music.get_busy() and not music_state['stopping']:
|
|
time.sleep(0.1)
|
|
if not music_state['stopping']:
|
|
event_queue.put(('status', 'Audio terminado'))
|
|
music_state['playing'] = False
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error audio: {e}'))
|
|
music_state['playing'] = False
|
|
t = threading.Thread(target=worker, daemon=True); music_state['thread']=t; t.start()
|
|
def detener_musica():
|
|
try:
|
|
if pygame and pygame.mixer.music.get_busy():
|
|
music_state['stopping'] = True
|
|
pygame.mixer.music.stop()
|
|
event_queue.put(('status', 'Audio detenido'))
|
|
else:
|
|
event_queue.put(('status', 'No hay audio reproduciéndose'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error al detener: {e}'))
|
|
music_box = tk.LabelFrame(right, text='Reproductor música', padx=4, pady=4)
|
|
music_box.pack(fill='x', padx=8, pady=(0,8))
|
|
tk.Button(music_box, text='Seleccionar', command=seleccionar_musica).pack(side='left', padx=2)
|
|
tk.Button(music_box, text='Play', command=reproducir_musica).pack(side='left', padx=2)
|
|
tk.Button(music_box, text='Stop', command=detener_musica).pack(side='left', padx=2)
|
|
|
|
# Sección Sockets (TCP/UDP servers) añadida al panel izquierdo
|
|
s_sockets = section(left, 'Sockets Locales')
|
|
tcp_state = {'thread': None, 'stop': False, 'sock': None}
|
|
udp_state = {'thread': None, 'stop': False, 'sock': None}
|
|
|
|
def start_tcp_server():
|
|
if tcp_state['thread'] and tcp_state['thread'].is_alive():
|
|
info_label.config(text='TCP Server ya iniciado')
|
|
return
|
|
port = simpledialog.askinteger('TCP Server','Puerto', initialvalue=5555)
|
|
if not port:
|
|
return
|
|
def worker():
|
|
event_queue.put(('status', f'TCP Server escuchando {port}'))
|
|
import socket as s
|
|
srv = s.socket(s.AF_INET, s.SOCK_STREAM)
|
|
srv.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR,1)
|
|
srv.bind(('0.0.0.0', port))
|
|
srv.listen(5)
|
|
tcp_state['sock']=srv
|
|
while not tcp_state['stop']:
|
|
try:
|
|
srv.settimeout(1.0)
|
|
try:
|
|
c, addr = srv.accept()
|
|
except s.timeout:
|
|
continue
|
|
event_queue.put(('status', f'Nueva conexión TCP {addr}'))
|
|
threading.Thread(target=lambda: manejar_tcp_cliente(c, addr), daemon=True).start()
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error server TCP: {e}'))
|
|
break
|
|
srv.close(); tcp_state['sock']=None; tcp_state['stop']=False
|
|
event_queue.put(('status','TCP Server detenido'))
|
|
tcp_state['stop']=False
|
|
t = threading.Thread(target=worker, daemon=True); tcp_state['thread']=t; t.start()
|
|
|
|
def manejar_tcp_cliente(c, addr):
|
|
try:
|
|
while True:
|
|
data = c.recv(1024)
|
|
if not data:
|
|
break
|
|
event_queue.put(('status', f'TCP {addr} -> {data[:40]!r}'))
|
|
c.send(b'ACK')
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error cliente TCP {addr}: {e}'))
|
|
finally:
|
|
c.close()
|
|
|
|
def stop_tcp_server():
|
|
tcp_state['stop']=True
|
|
|
|
def start_udp_server():
|
|
if udp_state['thread'] and udp_state['thread'].is_alive():
|
|
info_label.config(text='UDP Server ya iniciado')
|
|
return
|
|
port = simpledialog.askinteger('UDP Server','Puerto', initialvalue=5556)
|
|
if not port:
|
|
return
|
|
def worker():
|
|
event_queue.put(('status', f'UDP Server escuchando {port}'))
|
|
import socket as s
|
|
srv = s.socket(s.AF_INET, s.SOCK_DGRAM)
|
|
srv.bind(('0.0.0.0', port))
|
|
udp_state['sock']=srv
|
|
srv.settimeout(1.0)
|
|
while not udp_state['stop']:
|
|
try:
|
|
try:
|
|
data, addr = srv.recvfrom(1024)
|
|
except s.timeout:
|
|
continue
|
|
event_queue.put(('status', f'UDP {addr} -> {data[:40]!r}'))
|
|
srv.sendto(b'ACK', addr)
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error server UDP: {e}'))
|
|
break
|
|
srv.close(); udp_state['sock']=None; udp_state['stop']=False
|
|
event_queue.put(('status','UDP Server detenido'))
|
|
udp_state['stop']=False
|
|
t = threading.Thread(target=worker, daemon=True); udp_state['thread']=t; t.start()
|
|
|
|
def stop_udp_server():
|
|
udp_state['stop']=True
|
|
|
|
tk.Button(s_sockets, text='Start TCP', bg='#e0ffe0', command=start_tcp_server).pack(pady=2, fill='x')
|
|
tk.Button(s_sockets, text='Stop TCP', bg='#ffe0e0', command=stop_tcp_server).pack(pady=2, fill='x')
|
|
tk.Button(s_sockets, text='Start UDP', bg='#e0ffe0', command=start_udp_server).pack(pady=2, fill='x')
|
|
tk.Button(s_sockets, text='Stop UDP', bg='#ffe0e0', command=stop_udp_server).pack(pady=2, fill='x')
|
|
|
|
# Carrera camellos con sincronización mejorada
|
|
race_state = {'running': False, 'winner': None, 'lock': threading.Lock(), 'condition': threading.Condition()}
|
|
|
|
def iniciar_carrera(text_widget):
|
|
# Verificar si ya hay una carrera en curso
|
|
with race_state['lock']:
|
|
if race_state['running']:
|
|
event_queue.put(('status', 'Ya hay una carrera en curso'))
|
|
return
|
|
race_state['running'] = True
|
|
race_state['winner'] = None
|
|
|
|
corredores = 5
|
|
posiciones = [0] * corredores
|
|
meta = 50
|
|
ganador_declarado = threading.Event()
|
|
|
|
def corredor(i):
|
|
import random
|
|
nombre = f"Camello {i+1}"
|
|
try:
|
|
while not ganador_declarado.is_set():
|
|
# Sincronización: solo un corredor avanza a la vez
|
|
with race_state['lock']:
|
|
# Verificar si alguien ya ganó
|
|
if ganador_declarado.is_set():
|
|
break
|
|
|
|
# Avanzar
|
|
avance = random.randint(1, 3)
|
|
posiciones[i] += avance
|
|
|
|
# Actualizar visualización
|
|
event_queue.put(('race_update', (i, posiciones[i], meta)))
|
|
|
|
# Verificar si alcanzó la meta
|
|
if posiciones[i] >= meta and not ganador_declarado.is_set():
|
|
ganador_declarado.set()
|
|
race_state['winner'] = i
|
|
event_queue.put(('race_end', i))
|
|
break
|
|
|
|
# Esperar un poco antes del siguiente avance
|
|
time.sleep(random.uniform(0.1, 0.2))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error en {nombre}: {e}'))
|
|
finally:
|
|
# Asegurar que liberamos recursos
|
|
with race_state['lock']:
|
|
pass
|
|
|
|
# Iniciar todos los corredores
|
|
event_queue.put(('race_start', corredores))
|
|
for i in range(corredores):
|
|
threading.Thread(target=corredor, args=(i,), daemon=True, name=f'Corredor-{i+1}').start()
|
|
|
|
# Thread que monitorea finalización
|
|
def monitor_finalizacion():
|
|
ganador_declarado.wait(timeout=30) # Timeout de seguridad
|
|
time.sleep(0.5) # Esperar a que se procesen últimos eventos
|
|
with race_state['lock']:
|
|
race_state['running'] = False
|
|
if race_state['winner'] is None:
|
|
event_queue.put(('status', 'Carrera terminada (timeout)'))
|
|
|
|
threading.Thread(target=monitor_finalizacion, daemon=True, name='Monitor-Carrera').start()
|
|
|
|
# Servicios placeholders
|
|
def servicio_pop3():
|
|
event_queue.put(('status','POP3 placeholder (implementación futura)'))
|
|
|
|
def servicio_smtp():
|
|
"""Ventana para enviar correo por SMTP"""
|
|
smtp_win = tk.Toplevel(root)
|
|
smtp_win.title("Enviar Correo - SMTP")
|
|
smtp_win.geometry("450x500")
|
|
smtp_win.resizable(False, False)
|
|
|
|
# Configuración del servidor
|
|
config_frame = tk.LabelFrame(smtp_win, text="Configuración SMTP", padx=8, pady=8)
|
|
config_frame.pack(fill="x", padx=10, pady=5)
|
|
|
|
tk.Label(config_frame, text="Servidor SMTP:").grid(row=0, column=0, sticky="w", pady=2)
|
|
smtp_server = tk.Entry(config_frame, width=30)
|
|
smtp_server.insert(0, "10.10.0.101")
|
|
smtp_server.grid(row=0, column=1, pady=2)
|
|
|
|
tk.Label(config_frame, text="Puerto:").grid(row=1, column=0, sticky="w", pady=2)
|
|
smtp_port = tk.Entry(config_frame, width=10)
|
|
smtp_port.insert(0, "25")
|
|
smtp_port.grid(row=1, column=1, sticky="w", pady=2)
|
|
|
|
tk.Label(config_frame, text="Tu email:").grid(row=2, column=0, sticky="w", pady=2)
|
|
smtp_user = tk.Entry(config_frame, width=30)
|
|
smtp_user.grid(row=2, column=1, pady=2)
|
|
|
|
tk.Label(config_frame, text="Contraseña:").grid(row=3, column=0, sticky="w", pady=2)
|
|
smtp_pass = tk.Entry(config_frame, width=30, show="*")
|
|
smtp_pass.grid(row=3, column=1, pady=2)
|
|
|
|
# Datos del correo
|
|
mail_frame = tk.LabelFrame(smtp_win, text="Datos del Correo", padx=8, pady=8)
|
|
mail_frame.pack(fill="x", padx=10, pady=5)
|
|
|
|
tk.Label(mail_frame, text="Para:").grid(row=0, column=0, sticky="w", pady=2)
|
|
mail_to = tk.Entry(mail_frame, width=35)
|
|
mail_to.grid(row=0, column=1, pady=2)
|
|
|
|
tk.Label(mail_frame, text="Asunto:").grid(row=1, column=0, sticky="w", pady=2)
|
|
mail_subject = tk.Entry(mail_frame, width=35)
|
|
mail_subject.grid(row=1, column=1, pady=2)
|
|
|
|
tk.Label(mail_frame, text="Mensaje:").grid(row=2, column=0, sticky="nw", pady=2)
|
|
mail_body = tk.Text(mail_frame, width=35, height=8)
|
|
mail_body.grid(row=2, column=1, pady=2)
|
|
|
|
# Estado
|
|
status_label = tk.Label(smtp_win, text="", fg="gray")
|
|
status_label.pack(pady=5)
|
|
|
|
def enviar_correo():
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
server = smtp_server.get().strip()
|
|
port = smtp_port.get().strip()
|
|
user = smtp_user.get().strip()
|
|
password = smtp_pass.get()
|
|
to_addr = mail_to.get().strip()
|
|
subject = mail_subject.get().strip()
|
|
body = mail_body.get("1.0", "end-1c").strip()
|
|
|
|
if not all([server, port, user, password, to_addr, subject, body]):
|
|
messagebox.showwarning("SMTP", "Completa todos los campos")
|
|
return
|
|
|
|
def worker():
|
|
try:
|
|
status_label.config(text="Conectando...", fg="blue")
|
|
smtp_win.update()
|
|
|
|
msg = MIMEMultipart()
|
|
msg['From'] = user
|
|
msg['To'] = to_addr
|
|
msg['Subject'] = subject
|
|
msg.attach(MIMEText(body, 'plain'))
|
|
|
|
with smtplib.SMTP(server, int(port), timeout=10) as smtp:
|
|
# Intentar TLS si está disponible (para servidores que lo soporten)
|
|
try:
|
|
smtp.starttls()
|
|
except:
|
|
pass # Servidor local sin TLS
|
|
# Login solo si hay credenciales
|
|
if user and password:
|
|
try:
|
|
smtp.login(user, password)
|
|
except:
|
|
pass # Servidor local sin autenticación
|
|
smtp.send_message(msg)
|
|
|
|
status_label.config(text="✓ Correo enviado correctamente", fg="green")
|
|
event_queue.put(('status', f'Correo enviado a {to_addr}'))
|
|
except Exception as e:
|
|
status_label.config(text=f"✗ Error: {str(e)[:50]}", fg="red")
|
|
event_queue.put(('status', f'Error SMTP: {e}'))
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
btn_frame = tk.Frame(smtp_win)
|
|
btn_frame.pack(pady=10)
|
|
tk.Button(btn_frame, text="Enviar Correo", bg="#90EE90", font=('Arial', 10, 'bold'),
|
|
command=enviar_correo).pack(side="left", padx=5)
|
|
tk.Button(btn_frame, text="Cerrar", command=smtp_win.destroy).pack(side="left", padx=5)
|
|
|
|
def servicio_ftp():
|
|
event_queue.put(('status','FTP placeholder (implementación futura)'))
|
|
|
|
# Seguridad avanzada
|
|
def generar_rsa():
|
|
def worker():
|
|
if rsa is None:
|
|
event_queue.put(('status','Instala cryptography para RSA'))
|
|
return
|
|
event_queue.put(('status','Generando claves RSA...'))
|
|
try:
|
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
|
|
pub = key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
event_queue.put(('hash', f'Llave pública RSA:\n{pub.decode()}'))
|
|
event_queue.put(('status','RSA generado'))
|
|
except Exception as e:
|
|
event_queue.put(('status', f'Error RSA: {e}'))
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def aes_cifrar():
|
|
texto = simpledialog.askstring('AES','Texto a cifrar')
|
|
if not texto:
|
|
return
|
|
def worker():
|
|
try:
|
|
from cryptography.fernet import Fernet
|
|
except ModuleNotFoundError:
|
|
event_queue.put(('status','Instala cryptography para AES/Fernet'))
|
|
return
|
|
key = Fernet.generate_key(); f = Fernet(key)
|
|
ct = f.encrypt(texto.encode())
|
|
event_queue.put(('hash', f'AES(Fernet)\nKey: {key.decode()}\nCT: {ct.decode()}'))
|
|
event_queue.put(('status','Texto cifrado'))
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
# STATUS BAR
|
|
status = tk.Frame(root, bd=1, relief="sunken")
|
|
status.grid(row=2, column=0, columnspan=3, sticky="ew")
|
|
status.columnconfigure(0, weight=1)
|
|
status.columnconfigure(1, weight=1)
|
|
status.columnconfigure(2, weight=1)
|
|
status.columnconfigure(3, weight=1)
|
|
status.columnconfigure(4, weight=1)
|
|
|
|
lbl_mail = tk.Label(status, text="Correos sin leer", anchor="w", padx=6)
|
|
lbl_temp = tk.Label(status, text="Temperatura local", anchor="w")
|
|
lbl_net = tk.Label(status, text="Net 0 KB/s in / 0 KB/s out", anchor="w")
|
|
lbl_dt = tk.Label(status, text="Fecha Día y Hora", anchor="e")
|
|
lbl_alarm = tk.Label(status, text='Alarma: --', anchor='w')
|
|
|
|
lbl_mail.grid(row=0, column=0, sticky="w")
|
|
lbl_temp.grid(row=0, column=1, sticky="w")
|
|
lbl_net.grid(row=0, column=2, sticky="w")
|
|
lbl_dt.grid(row=0, column=3, sticky="e", padx=6)
|
|
lbl_alarm.grid(row=0, column=4, sticky='w')
|
|
|
|
# Hilo para actualizar la fecha/hora
|
|
def updater(lbl):
|
|
try:
|
|
while True:
|
|
now = datetime.datetime.now()
|
|
lbl_text = f"{now.strftime('%A')}, {now.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
lbl.after(0, lbl.config, {"text": lbl_text})
|
|
time.sleep(1)
|
|
except tk.TclError:
|
|
return
|
|
|
|
th = threading.Thread(target=updater, args=(lbl_dt,), daemon=True)
|
|
th.start()
|
|
|
|
# Network I/O monitor (kb/s)
|
|
def net_io_runner(lbl):
|
|
try:
|
|
import psutil
|
|
except ModuleNotFoundError:
|
|
lbl.after(0, lbl.config, {"text": "Instala psutil para monitor red (pip install psutil)"})
|
|
return
|
|
prev = psutil.net_io_counters()
|
|
prev_time = time.time()
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
cur = psutil.net_io_counters()
|
|
now = time.time()
|
|
dt = now - prev_time if now - prev_time > 0 else 1
|
|
sent_b = cur.bytes_sent - prev.bytes_sent
|
|
recv_b = cur.bytes_recv - prev.bytes_recv
|
|
sent_k = sent_b / 1024.0 / dt
|
|
recv_k = recv_b / 1024.0 / dt
|
|
prev = cur
|
|
prev_time = now
|
|
lbl_text = f"Net {recv_k:.1f} KB/s in / {sent_k:.1f} KB/s out"
|
|
lbl.after(0, lbl.config, {"text": lbl_text})
|
|
except tk.TclError:
|
|
return
|
|
|
|
threading.Thread(target=net_io_runner, args=(lbl_net,), daemon=True).start()
|
|
|
|
# Consumir API REST con detalles mejorados
|
|
def consumir_api(url='https://datatracker.ietf.org/doc/html/rfc6749'):
|
|
def worker():
|
|
event_queue.put(('status', f'GET {url}'))
|
|
try:
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
r = requests.get(url, timeout=10)
|
|
|
|
# Construir respuesta detallada
|
|
output = []
|
|
output.append("="*80)
|
|
output.append(f"URL: {url}")
|
|
output.append(f"Status: {r.status_code} {r.reason}")
|
|
output.append(f"Content-Type: {r.headers.get('Content-Type', 'N/A')}")
|
|
output.append(f"Content-Length: {r.headers.get('Content-Length', 'N/A')}")
|
|
output.append(f"Server: {r.headers.get('Server', 'N/A')}")
|
|
output.append("="*80)
|
|
output.append("\nHEADERS:")
|
|
for k, v in list(r.headers.items())[:10]:
|
|
output.append(f" {k}: {v}")
|
|
output.append("\n" + "="*80)
|
|
output.append("RESPONSE BODY:\n")
|
|
|
|
# Intentar formatear según tipo de contenido
|
|
content_type = r.headers.get('Content-Type', '').lower()
|
|
|
|
if 'application/json' in content_type:
|
|
try:
|
|
data = r.json()
|
|
if isinstance(data, list):
|
|
output.append(f"Array with {len(data)} elements:\n")
|
|
output.append(json.dumps(data[:5], indent=2)) # Primeros 5 elementos
|
|
if len(data) > 5:
|
|
output.append(f"\n... and {len(data) - 5} more items")
|
|
else:
|
|
output.append(json.dumps(data, indent=2)[:3000])
|
|
except Exception as e:
|
|
output.append(f"JSON parse error: {e}\n{r.text[:2000]}")
|
|
|
|
elif 'text/html' in content_type:
|
|
try:
|
|
soup = BeautifulSoup(r.text, 'html.parser')
|
|
# Extraer título
|
|
title = soup.find('title')
|
|
if title:
|
|
output.append(f"Title: {title.get_text()}\n")
|
|
# Extraer primeros párrafos
|
|
paragraphs = soup.find_all('p')[:5]
|
|
for p in paragraphs:
|
|
text = p.get_text().strip()
|
|
if text:
|
|
output.append(f"{text}\n")
|
|
output.append(f"\n[HTML document with {len(soup.find_all())} tags]")
|
|
except Exception:
|
|
output.append(r.text[:2000])
|
|
|
|
else:
|
|
# Texto plano u otro
|
|
output.append(r.text[:3000])
|
|
|
|
texto = '\n'.join(output)
|
|
event_queue.put(('api', texto))
|
|
event_queue.put(('status', f'✓ Respuesta {r.status_code} - {len(r.content)} bytes'))
|
|
except Exception as e:
|
|
event_queue.put(('api', f'ERROR: {str(e)}'))
|
|
event_queue.put(('status', f'✗ Error API: {e}'))
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
# Añadir botón API en pestaña Navegador después de crear pestañas (modificar contenido)
|
|
|
|
# Pump de cola
|
|
def pump_queue():
|
|
try:
|
|
while True:
|
|
tipo, payload = event_queue.get_nowait()
|
|
if tipo == 'status':
|
|
info_label.config(text=payload)
|
|
elif tipo == 'scrape':
|
|
tab_texts['Resultados'].insert('end', payload + '\n---\n')
|
|
elif tipo == 'hash':
|
|
tab_texts['Resultados'].insert('end', payload + '\n')
|
|
elif tipo == 'chat':
|
|
tab_texts['Resultados'].insert('end', f'[CHAT] {payload}\n')
|
|
elif tipo == 'alarm_progress':
|
|
lbl_alarm.config(text=f'Alarma: {payload}s')
|
|
elif tipo == 'alarm':
|
|
lbl_alarm.config(text='Alarma disparada')
|
|
messagebox.showinfo('Alarma', '¡Tiempo cumplido!')
|
|
elif tipo == 'api':
|
|
tab_texts['Navegador'].insert('end', payload + '\n')
|
|
elif tipo == 'race_start':
|
|
num_corredores = payload
|
|
tab_texts['Tareas'].delete('1.0', 'end')
|
|
# Crear tags con colores para cada camello
|
|
colores = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
|
|
for i in range(num_corredores):
|
|
tab_texts['Tareas'].tag_config(f'camello_{i}', foreground=colores[i % len(colores)], font=('Courier', 10, 'bold'))
|
|
tab_texts['Tareas'].insert('end', f'🏁 CARRERA INICIADA - {num_corredores} camellos 🏁\n', 'title')
|
|
tab_texts['Tareas'].insert('end', '='*60 + '\n\n')
|
|
for i in range(num_corredores):
|
|
tab_texts['Tareas'].insert('end', f'Camello {i+1}: [░'*40 + f'] 0/{num_corredores*10}\n', f'camello_{i}')
|
|
elif tipo == 'race_update':
|
|
idx, pos, meta = payload
|
|
progreso = int((pos / meta) * 40)
|
|
barra = '█' * progreso + '░' * (40 - progreso)
|
|
line_num = idx + 4
|
|
try:
|
|
inicio = tab_texts['Tareas'].index(f'{line_num}.0')
|
|
fin = tab_texts['Tareas'].index(f'{line_num}.end')
|
|
tab_texts['Tareas'].delete(inicio, fin)
|
|
nueva_linea = f'Camello {idx+1}: [{barra}] {pos}/{meta}'
|
|
tab_texts['Tareas'].insert(inicio, nueva_linea, f'camello_{idx}')
|
|
except:
|
|
pass
|
|
elif tipo == 'race_end':
|
|
g = payload
|
|
tab_texts['Tareas'].insert('end', '\n' + '='*60 + '\n')
|
|
tab_texts['Tareas'].insert('end', f'🏆 ¡GANADOR: CAMELLO {g+1}! 🏆\n')
|
|
tab_texts['Tareas'].insert('end', '='*60 + '\n')
|
|
event_queue.put(('status', f'Carrera finalizada - Ganó Camello {g+1}'))
|
|
elif tipo == 'scrape_result':
|
|
tab_texts['Navegador'].delete('1.0', 'end')
|
|
tab_texts['Navegador'].insert('end', payload)
|
|
except queue.Empty:
|
|
pass
|
|
root.after(200, pump_queue)
|
|
root.after(200, pump_queue)
|
|
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
start_client() |