- Root: no se pide la contraseña al arrancar (echaba para atras). Se pide solo la primera vez que una accion necesita sudo, y se cachea. --candado para el modo seguro. Aviso de root prominente en el README. - install.sh crea un lanzador de escritorio (jarvis.desktop) con el icono naranja: aparece en el menu de aplicaciones para arrancar con un clic. - Captura del panel reemplazada por una mas limpia (solo la ventana). - Quitado "Probado en ..." del README (informacion del equipo). - ventanas.sh: aviso claro segun el compositor en Wayland.
124 lines
5.8 KiB
Python
124 lines
5.8 KiB
Python
"""Lo que JARVIS puede HACER, y el unico sitio por donde pasa.
|
|
|
|
`Manos` es la puerta: el catalogo entra por aqui, el cerebro entra por aqui y
|
|
el panel entrara por aqui. Tener un solo punto importa porque es donde se
|
|
decide que se ejecuta y que no, y esa decision no puede estar repartida.
|
|
|
|
Las cuatro piezas de debajo vienen tal cual de la version sobre Newelle
|
|
—`acciones.py`, `tareas.py`, `sudo_root.py`, `seleccion.py`— y se comprobo al
|
|
copiarlas que 374 frases dan exactamente el mismo resultado en los dos sitios.
|
|
"""
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
|
|
from .acciones import Catalogo, ejecuta, formatea
|
|
|
|
AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
CATALOGO = os.path.join(AQUI, "datos", "acciones.json")
|
|
PROPIO = os.path.expanduser("~/.config/jarvis/acciones.json")
|
|
|
|
# Lo que no se ejecuta por mucho que lo pida el modelo. No es una lista de
|
|
# seguridad completa —eso es imposible— sino un tope contra el accidente: el
|
|
# modelo es servicial y a veces se inventa una limpieza que nadie pidio.
|
|
PROHIBIDO = (
|
|
"rm -rf /", "mkfs", "dd if=", ":(){", "shutdown", "reboot", "halt",
|
|
"> /dev/sd", "chmod -R 777 /", "chown -R", "userdel", "passwd",
|
|
)
|
|
|
|
TOPE_SALIDA = 4000
|
|
|
|
|
|
class Manos:
|
|
def __init__(self, catalogo=None, al_ejecutar=None):
|
|
self.catalogo = catalogo or Catalogo([PROPIO, CATALOGO])
|
|
self.al_ejecutar = al_ejecutar
|
|
# Root para la sesion: la primera vez que una accion necesita sudo se
|
|
# pide la contraseña, y queda aqui hasta que caduque (15 min). No se
|
|
# pide al arrancar: solo cuando de verdad hace falta.
|
|
from manos.sudo_root import GestorRoot
|
|
self.root = GestorRoot([])
|
|
# El panel conecta aqui como pedir la contraseña de root cuando haga
|
|
# falta (muestra el dialogo). Sin panel (modo terminal) se queda en None.
|
|
self.al_pedir_root = None
|
|
|
|
# ------------------------------------------------------------- catalogo
|
|
|
|
def busca(self, texto: str):
|
|
return self.catalogo.busca(texto)
|
|
|
|
def haz(self, accion, argumento="") -> tuple:
|
|
"""Ejecuta una accion ya emparejada. Devuelve (lo que se dice, crudo)."""
|
|
dicho, salida = ejecuta(accion, [], argumento)
|
|
if salida:
|
|
dicho = formatea(accion.respuesta, salida, argumento)
|
|
return dicho, salida
|
|
|
|
def por_orden(self, orden: str, al_ejecutar=None) -> str:
|
|
"""Para el cerebro: empareja contra el catalogo y ejecuta.
|
|
|
|
Devuelve texto para el modelo, no para el usuario: si no encaja, se le
|
|
dice claramente para que pruebe con el shell en vez de insistir.
|
|
"""
|
|
if not orden.strip():
|
|
return "no me has dicho ninguna orden"
|
|
# El modelo a veces cuela un comando aqui de todas formas. Se detecta y
|
|
# se le devuelve el motivo, que es mas util que un "no encontrado".
|
|
if any(c in orden for c in "|><;&$") or orden.split()[0] in ("df", "ls", "ps", "cat"):
|
|
return ("eso es un comando de shell, no una orden del catalogo. "
|
|
"Usa la herramienta comando_de_shell.")
|
|
accion, argumento = self.catalogo.busca(orden)
|
|
if accion is None:
|
|
return (f"'{orden}' no esta en el catalogo. Si hace falta, usa "
|
|
"comando_de_shell.")
|
|
if al_ejecutar or self.al_ejecutar:
|
|
(al_ejecutar or self.al_ejecutar)(accion.id, argumento)
|
|
dicho, salida = self.haz(accion, argumento)
|
|
return (salida or dicho)[:TOPE_SALIDA]
|
|
|
|
# ---------------------------------------------------------------- shell
|
|
|
|
def por_shell(self, comando: str, al_ejecutar=None) -> str:
|
|
if not comando.strip():
|
|
return "no me has dicho ningun comando"
|
|
bajo = comando.lower()
|
|
for veneno in PROHIBIDO:
|
|
if veneno in bajo:
|
|
return (f"no ejecuto eso: contiene '{veneno}'. Si de verdad hace "
|
|
"falta, que lo pida el usuario expresamente.")
|
|
if al_ejecutar or self.al_ejecutar:
|
|
(al_ejecutar or self.al_ejecutar)("shell", comando)
|
|
|
|
from manos.sudo_root import necesita_root
|
|
# Los comandos con sudo van por GestorRoot, que da la contraseña de la
|
|
# sesion por stdin. Sin ella, `bash -c "sudo ..."` se colgaria pidiendo
|
|
# una contraseña que nadie teclea. Y si el candado no se ha abierto, no
|
|
# hay contraseña: el root queda cerrado hasta que el usuario desbloquee.
|
|
if necesita_root(comando):
|
|
if not self.root.sesion_activa():
|
|
# No hay root todavia: se pide la contraseña AHORA (una vez), no
|
|
# al arrancar. El panel muestra el dialogo; el usuario la teclea
|
|
# y repite la orden, que ya correra con la sesion abierta.
|
|
if self.al_pedir_root:
|
|
self.al_pedir_root(comando)
|
|
return ("Esto necesita permiso de administrador, señor. "
|
|
"Introduzca su contraseña y repitamelo.")
|
|
return ("eso necesita permiso de administrador y no hay panel "
|
|
"para pedir la contraseña.")
|
|
try:
|
|
r = self.root.ejecuta(comando, timeout=60)
|
|
except Exception as e:
|
|
return f"no se pudo ejecutar como root: {e}"
|
|
else:
|
|
try:
|
|
r = subprocess.run(["bash", "-c", comando], capture_output=True,
|
|
text=True, timeout=60)
|
|
except subprocess.TimeoutExpired:
|
|
return "el comando tardo mas de un minuto y lo he cortado"
|
|
except OSError as e:
|
|
return f"no se pudo ejecutar: {e}"
|
|
|
|
salida = (r.stdout or r.stderr or "").strip()
|
|
if not salida:
|
|
return "hecho, sin salida" if r.returncode == 0 else f"fallo (codigo {r.returncode})"
|
|
return salida[:TOPE_SALIDA]
|