feat(shell): auto-commit con 31 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:55:16 +02:00
parent 1430039688
commit e1e9bb7499
31 changed files with 3917 additions and 0 deletions
@@ -0,0 +1,65 @@
---
name: close_onlyoffice_instance
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "close_onlyoffice_instance(instance: string = demo, [--purge]) -> json"
description: "Termina el/los proceso(s) DesktopEditors de una INSTANCIA AISLADA (slot) de ONLYOFFICE Desktop Editors, identificados por su HOME=/tmp/oo_<instance> leido de /proc/<pid>/environ — asi NUNCA mata la instancia personal del usuario, solo la aislada. Envia SIGTERM, espera ~3s por evento (read -t, sin sleep foreground) y SIGKILL a los que sigan vivos. Con el flag --purge borra ademas los directorios del slot (/tmp/oo_<instance>*). Imprime JSON con instance, killed_pids (array), purged y status (closed|not_running)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: instance
desc: "nombre del slot aislado a cerrar (default: demo). Solo se matan procesos DesktopEditors cuyo HOME sea /tmp/oo_<instance>"
- name: --purge
desc: "flag opcional: si se pasa, borra los directorios del slot (/tmp/oo_<instance>*) tras matar los procesos. Sin el flag, solo termina procesos y deja el estado del slot en disco"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"killed_pids\":[<pids>],\"purged\":true|false,\"status\":\"closed\"|\"not_running\"}. Exit 0 siempre que opere bien (closed si mato procesos, not_running si no habia ninguno del slot), exit 1 si falta dependencia, exit 2 si flag desconocido"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/close_onlyoffice_instance.sh"
---
## Ejemplo
```bash
# Cerrar el slot demo (deja /tmp/oo_demo* en disco para reusar la config)
bash bash/functions/shell/close_onlyoffice_instance.sh demo
# Cerrar y limpiar todo el estado del slot
bash bash/functions/shell/close_onlyoffice_instance.sh demo --purge
# Slot por defecto (demo) sin argumentos
bash bash/functions/shell/close_onlyoffice_instance.sh
# Via fn run
./fn run close_onlyoffice_instance_bash_shell reporte --purge
# Sourceado
source bash/functions/shell/close_onlyoffice_instance.sh
out=$(close_onlyoffice_instance demo --purge)
echo "$out"
# {"instance":"demo","killed_pids":[12345,12350],"purged":true,"status":"closed"}
```
## Cuando usarla
- Cuando terminas un flujo automatizado con ONLYOFFICE Desktop y quieres **cerrar la instancia aislada por completo** (cerrar la ventana con `wmctrl` deja el proceso vivo; esta funcion mata el proceso real).
- Para **liberar recursos** de un slot que ya no usas, opcionalmente borrando su estado en /tmp con `--purge`.
- Como ultimo paso del ciclo open -> reload -> close, garantizando que no quedan procesos huerfanos de la instancia aislada.
## Gotchas
- **Solo mata la instancia aislada**: identifica procesos por `HOME=/tmp/oo_<instance>` en `/proc/<pid>/environ`. La instancia personal del usuario (HOME real) NUNCA se toca. Esto es por diseño y por seguridad.
- **Cerrar la ventana NO mata el proceso**: por eso esta funcion existe. Tras `reload`/`wmctrl -ic` el proceso de la instancia aislada sigue vivo (deseable para reusar). Usa esta funcion para terminarlo de verdad.
- **`--purge` borra /tmp/oo_<instance>***: pierdes la config del slot (perfil, recientes). El slot se recreara limpio en el siguiente `open`. Sin `--purge`, el estado persiste y el siguiente arranque reusa esa config.
- **El slot vive en /tmp**: aunque no purgues, `/tmp/oo_<instance>*` se pierde al reiniciar el PC. Estado desechable.
- **Requiere X11 + wmctrl + xdotool** instalados (coherencia con el grupo, aunque esta funcion solo usa /proc para matar). Comprueba `command -v` y falla claro si falta alguna; no funciona en Wayland puro sin XWayland para el resto del grupo.
- **Carrera de /proc**: si un pid muere entre listarlo y leer su environ, se ignora silenciosamente (guardas `2>/dev/null || true`); no rompe la funcion (`set -uo pipefail` sin `-e`).
- **SIGKILL como ultimo recurso**: tras ~3s de SIGTERM, los procesos vivos reciben SIGKILL. Cambios sin guardar en la app (si los hubiera) se pierden — pero el flujo previsto edita en disco, no en la app, asi que no deberia haber estado sin guardar.
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# close_onlyoffice_instance — termina el/los proceso(s) DesktopEditors de una
# INSTANCIA AISLADA (slot) de ONLYOFFICE Desktop Editors, identificados por su
# HOME=/tmp/oo_<instance> en /proc/<pid>/environ. Opcionalmente limpia los
# directorios del slot con --purge.
#
# Funcion impura: lee /proc, envia señales a procesos y (con --purge) borra
# directorios bajo /tmp. NO toca la instancia personal del usuario: solo mata
# procesos cuyo HOME apunta al slot aislado.
#
# Slot aislado: cada instance usa HOME=/tmp/oo_<instance>,
# XDG_RUNTIME_DIR=/tmp/oo_<instance>_run, XDG_CONFIG_HOME=/tmp/oo_<instance>/.config.
# Sin -e: lecturas de /proc/<pid>/environ pueden fallar por carrera (el pid
# muere entre listar y leer); no deben abortar la funcion.
set -uo pipefail
close_onlyoffice_instance() {
local instance="demo"
local purge=false
# Parseo de args: [instance] y/o --purge en cualquier orden.
local a
for a in "$@"; do
case "$a" in
--purge) purge=true ;;
-*) echo "close_onlyoffice_instance: flag desconocido '$a'" >&2; return 2 ;;
*) instance="$a" ;;
esac
done
# 1. Dependencias del sistema (consistencia con el grupo, aunque aqui solo
# se usa /proc; onlyoffice/wmctrl/xdotool deben existir para operar el slot).
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "close_onlyoffice_instance: falta dependencia '$dep'" >&2
return 1
fi
done
local oo_home="/tmp/oo_${instance}"
# 2. Encontrar pids de DesktopEditors con HOME=/tmp/oo_<instance>.
local pids=() pid environ
for pid in $(pgrep -f '/opt/onlyoffice/desktopeditors/DesktopEditors' 2>/dev/null || true); do
# Leer el entorno del proceso; saltar si no se puede (carrera/permisos).
environ=$(tr '\0' '\n' <"/proc/${pid}/environ" 2>/dev/null || true)
[[ -z "$environ" ]] && continue
if grep -qx "HOME=${oo_home}" <<<"$environ" 2>/dev/null; then
pids+=("$pid")
fi
done
# 3. Si no hay procesos del slot: not_running (purge opcional igualmente).
if [[ ${#pids[@]} -eq 0 ]]; then
local purged=false
if [[ "$purge" == true ]]; then
rm -rf -- /tmp/oo_"${instance}"* 2>/dev/null || true
purged=true
fi
printf '{"instance":"%s","killed_pids":[],"purged":%s,"status":"not_running"}\n' \
"$instance" "$purged"
return 0
fi
# 4. SIGTERM a todos los pids del slot.
kill -TERM "${pids[@]}" 2>/dev/null || true
# 5. Esperar ~3s a que mueran (NUNCA sleep foreground): read -t 0.3 x10.
local w=0 wmax=10
while [[ $w -lt $wmax ]]; do
local alive=false p
for p in "${pids[@]}"; do
if kill -0 "$p" 2>/dev/null; then alive=true; break; fi
done
[[ "$alive" == false ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
w=$((w + 1))
done
# 6. SIGKILL a los que sigan vivos.
local p
for p in "${pids[@]}"; do
if kill -0 "$p" 2>/dev/null; then
kill -KILL "$p" 2>/dev/null || true
fi
done
# 7. Purge opcional de los dirs del slot.
local purged=false
if [[ "$purge" == true ]]; then
rm -rf -- /tmp/oo_"${instance}"* 2>/dev/null || true
purged=true
fi
# 8. JSON con el array de pids terminados.
local pids_json
pids_json=$(printf '%s,' "${pids[@]}")
pids_json="[${pids_json%,}]"
printf '{"instance":"%s","killed_pids":%s,"purged":%s,"status":"closed"}\n' \
"$instance" "$pids_json" "$purged"
return 0
}
# Ejecutable directo o sourceado.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
close_onlyoffice_instance "$@"
fi
@@ -0,0 +1,78 @@
---
name: monitor_listening_ports
kind: function
lang: bash
domain: shell
version: "0.3.0"
purity: impure
signature: "monitor_listening_ports([--interval N], [--once]) -> void"
description: "TUI ligera de terminal que refresca cada N segundos una tabla de los sockets TCP en escucha (LISTEN) del equipo local: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO | CMD (cmdline real, util para distinguir python3/node genericos), ordenada por tiempo de vida del proceso dueño (descendente). Una fila por pid. Lanzada como root rellena tambien los sockets de otros usuarios. Modo --once imprime un solo frame y sale."
tags: [recon, ports, monitor, tui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: --interval N
desc: "segundos entre refrescos en modo bucle (default: 1, acepta decimales)"
- name: --once
desc: "imprime un único frame de la tabla y termina con exit 0 (no interactivo; úsalo en tests y en `fn run` para no colgar)"
output: "tabla a stdout con columnas IP, PUERTO, PROCESO, PID, TIEMPO ACTIVO ordenada por uptime del proceso descendente; sin --once refresca en bucle infinito hasta Ctrl-C"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/monitor_listening_ports.sh"
---
## Ejemplo
```bash
# Un solo frame (no cuelga) — ideal para fn run o un pipe
./fn run monitor_listening_ports_bash_shell --once
# Como script directo
bash bash/functions/shell/monitor_listening_ports.sh --once
# Sourceada, en bucle interactivo refrescando cada segundo (Ctrl-C para salir)
source bash/functions/shell/monitor_listening_ports.sh
monitor_listening_ports --interval 1
# Refresco mas lento
monitor_listening_ports --interval 5
```
Salida (frame `--once`, recortado):
```
IP PUERTO PROCESO PID TIEMPO ACTIVO
* 8420 registry_api 1885 4d 23:40:46
:: 8889 mitmweb 1892 4d 23:40:46
127.0.0.1 8484 sqlite_api 1889 4d 23:40:42
127.0.0.1 8899 jupyter-lab 155100 4d 19:33:55
::1 631 - - ?
```
## Cuando usarla
- Cuando quieras vigilar **qué puertos abren tus dev-servers / procesos web locales y desde cuándo** llevan vivos, en una sola pantalla que se actualiza sola.
- Para detectar de un vistazo un proceso recién levantado (aparece al fondo, con poco TIEMPO ACTIVO) o uno que lleva días escuchando (arriba del todo).
- Como paso de reconocimiento local del grupo `recon`: inventario rápido de superficie de escucha TCP del propio equipo, con el dueño de cada socket.
- En tests o automatizaciones que solo necesitan un snapshot: añade `--once` para obtener un frame y salir.
## Gotchas
- **Impura**: depende de `ss` (paquete iproute2) y `ps` (procps). Si falta cualquiera, sale con exit 1 y un mensaje a stderr.
- **Sin sudo no ves PROCESO/PID/CMD de sockets de otros usuarios** (típicamente procesos de root, ej. systemd-resolved en `127.0.0.54:53`, kernels Jupyter de otra sesión, o servidores en contenedores). Esas filas muestran `-`/`?`. La función **no usa sudo** a propósito; para **rellenarlos, lánzala como root**: `pass show claude/sudo | sudo -S bash bash/functions/shell/monitor_listening_ports.sh --interval 1` (el password se pipea, no queda en la cmdline). Como root, `ss` resuelve el dueño de todos los sockets.
- **Columna CMD = cmdline real** (`ps -o args=`, recortada a 90 chars). Es lo que distingue un `python3`/`node` genérico (PROCESO) de lo que realmente ejecuta: `python3 -m ipykernel_launcher ...`, `registry_api -port 8420`, etc. Procesos en distinto namespace (docker) pueden seguir sin CMD aunque corras como root.
- **Una fila por pid**: un mismo puerto con varios workers (ej. nginx, gunicorn) genera varias filas, una por cada pid dueño del socket.
- **`--once` evita colgar**: sin `--once` corre en bucle infinito. No lo lances así en tests ni en `fn run` desatendido — usa `--once`.
- **El orden es por uptime del PROCESO, no por el tiempo de la conexión**. `ps -o etimes=` mide cuánto lleva vivo el proceso completo, no cuándo abrió ese socket concreto.
- **Carrera ps**: si un pid muere entre `ss` y `ps`, su TIEMPO ACTIVO sale como `?` y la fila se ordena al final (no rompe el bucle; el script usa `set -uo pipefail` sin `-e`).
- En modo bucle oculta el cursor (`tput civis`) y lo restaura + limpia en un `trap` EXIT/INT/TERM, de modo que Ctrl-C deja la terminal limpia.
## Capability growth log
- v0.3.0 (14/06/2026) — añade columna **CMD** con la cmdline real del proceso (mapa pid→args construido en la misma llamada `ps -eo pid=,etimes=,args=`), para distinguir un `python3`/`node` genérico de lo que realmente ejecuta. Documenta cómo rellenar los sockets de otros usuarios (`-`) lanzando la TUI como root. Anchos de columna reajustados para dar sitio a CMD.
- v0.2.0 (14/06/2026) — corrige parpadeo y cuelgue del modo bucle. (1) Doble-buffer ANSI: cada frame se computa completo en una variable y se pinta con cursor-home `\033[H` + clear-to-end `\033[J` en vez de `tput clear` antes de recolectar, eliminando el instante en blanco. (2) Rendimiento: una sola llamada a `ps -eo pid=,etimes=` (mapa pid→uptime en memoria, antes era un fork de `ps` por pid) y construcción de filas con `printf -v` (builtin, antes un `$( )` por fila); frame de ~130 ms con cientos de sockets. (3) Bugfix de cuelgue: el avance del parser multi-pid usaba `BASH_REMATCH[0]`, que queda sobrescrito por el `[[ =~ ]]` interno de `_mlp_fmt_etime` → no recortaba el string y entraba en bucle infinito. Ahora el needle se captura justo tras el match, con guard anti-cuelgue si el recorte no progresa.
@@ -0,0 +1,271 @@
#!/usr/bin/env bash
# monitor_listening_ports — TUI ligera que refresca una tabla de sockets TCP en
# escucha (LISTEN) del equipo local, ordenada por tiempo de vida del proceso
# dueño (descendente). Columnas: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO.
#
# Funcion impura: lee estado del sistema (sockets via `ss`, uptime de procesos
# via `ps`). Sin --once corre en bucle infinito refrescando cada N segundos.
#
# Rendimiento: cada frame hace UNA sola llamada a `ss` y UNA sola a `ps`
# (mapa pid->etimes en memoria). El parseo de cada socket es bash puro y SIN
# command substitution por fila: las cadenas se construyen con `printf -v`
# (builtin, cero forks) y el formato de tiempo se devuelve en una variable
# global. El modo bucle usa doble-buffer ANSI (cursor home + clear-to-end) en
# lugar de limpiar la pantalla antes de computar, para que nunca se vea vacia
# entre refrescos.
# No usamos -e a proposito: una carrera donde un pid muere entre `ss` y `ps`
# no debe matar el bucle entero. -u y pipefail se mantienen para robustez.
set -uo pipefail
# Formatea segundos a texto humano legible y lo deja en la global _mlp_human.
# Se evita `$( )` (un fork por fila) usando una variable de retorno.
# <1h -> MM:SS ej. 12:45
# <1d -> HH:MM:SS ej. 03:12:45
# >=1d -> Nd HH:MM:SS ej. 1d 03:12:45
_mlp_human=""
_mlp_fmt_etime() {
local secs="$1"
# Si no es un numero entero valido, devolver tal cual (ej. "?").
if ! [[ "$secs" =~ ^[0-9]+$ ]]; then
_mlp_human="$secs"
return 0
fi
local days=$(( secs / 86400 ))
local rem=$(( secs % 86400 ))
local hours=$(( rem / 3600 ))
local mins=$(( (rem % 3600) / 60 ))
local s=$(( rem % 60 ))
if (( days > 0 )); then
printf -v _mlp_human '%dd %02d:%02d:%02d' "$days" "$hours" "$mins" "$s"
elif (( hours > 0 )); then
printf -v _mlp_human '%02d:%02d:%02d' "$hours" "$mins" "$s"
else
printf -v _mlp_human '%02d:%02d' "$mins" "$s"
fi
}
# Imprime un unico frame de la tabla a stdout.
# Estrategia de rendimiento (cero forks por fila):
# 1. Un solo `ps -eo pid=,etimes=` construye un mapa pid -> segundos vivo.
# 2. Un solo `ss -H -tlnp` lista los sockets en escucha.
# 3. Cada linea se parsea con bash puro: IP/puerto por parameter expansion,
# (nombre,pid) del campo users:(...) iterando con BASH_REMATCH, y cada
# fila se arma con `printf -v` (builtin). El uptime se resuelve por lookup
# O(1) en el mapa.
# 4. Se ordena por segundos vivo descendente con un unico `sort`.
_mlp_render_frame() {
# Mapas pid -> etimes (segundos vivo) y pid -> cmdline completa. Una sola
# invocacion de ps por frame. `args=` va al ultimo porque lleva espacios,
# asi `read` lo captura entero en la tercera variable.
local -A etmap=() argmap=()
local _pid _et _args
while read -r _pid _et _args; do
[[ -z "$_pid" ]] && continue
etmap["$_pid"]="$_et"
argmap["$_pid"]="$_args"
done < <(ps -eo pid=,etimes=,args= 2>/dev/null)
# Cada fila intermedia: "<etimes>\t<ip>\t<puerto>\t<proceso>\t<pid>\t<humano>"
local -a rows=()
local line row
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Campos de `ss -H -tlnp`: State Recv-Q Send-Q Local:Port Peer:Port users:(...)
# Local:Port es el 4o token. Lo extraemos sin fork con read en array.
local -a F=()
read -ra F <<<"$line"
local local_addr="${F[3]:-}"
[[ -z "$local_addr" ]] && continue
# Separar IP y PUERTO partiendo por el ULTIMO ':'.
local ip port
port="${local_addr##*:}"
ip="${local_addr%:*}"
# Quitar corchetes de IPv6: [::] -> :: , [::1] -> ::1
ip="${ip#[}"
ip="${ip%]}"
# Caso de bind sin direccion explicita (raro): dejar marcador.
[[ -z "$ip" ]] && ip="*"
# Extraer el bloque users:(...) del final de la linea (si existe).
local users=""
[[ "$line" == *"users:("* ]] && users="${line#*users:(}"
if [[ -z "$users" ]]; then
# Socket sin info de proceso (pertenece a otro usuario y no corremos
# como root). Para verlo, lanzar la TUI como root (ver Gotchas).
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "-1" "$ip" "$port" "-" "-" "?" "-"
rows+=("$row")
continue
fi
# Dentro de users puede haber varios ("nombre",pid=N,fd=M). Una fila por
# pid. Iteramos con BASH_REMATCH avanzando sobre el string (cero forks).
local s="$users" pname pid etimes needle prev_s cmd found_any=0
while [[ "$s" =~ \"([^\"]*)\",pid=([0-9]+) ]]; do
# IMPORTANTE: capturar nombre/pid/needle ANTES de cualquier otra
# comparacion `[[ =~ ]]` (p.ej. dentro de _mlp_fmt_etime), porque
# cada `=~` SOBREESCRIBE BASH_REMATCH. Si se usara BASH_REMATCH[0]
# despues, contendria el match del ultimo `=~` y el recorte de `s`
# no avanzaria -> bucle infinito.
pname="${BASH_REMATCH[1]}"
pid="${BASH_REMATCH[2]}"
needle="${BASH_REMATCH[0]}"
found_any=1
# Lookup O(1) en el mapa. Si el pid ya no esta (carrera), marcar "?".
etimes="${etmap[$pid]:-}"
if [[ -z "$etimes" || ! "$etimes" =~ ^[0-9]+$ ]]; then
etimes="-1"
_mlp_human="?"
else
_mlp_fmt_etime "$etimes"
fi
# Comando real (cmdline completa) del pid; dice QUE es realmente un
# "python3"/"node" generico. Se recorta para no romper la tabla.
cmd="${argmap[$pid]:-}"
[[ -z "$cmd" ]] && cmd="-"
cmd="${cmd:0:90}"
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "$etimes" "$ip" "$port" "$pname" "$pid" "$_mlp_human" "$cmd"
rows+=("$row")
# Avanzar mas alla del match actual para no repetir el primer pid.
# Guard: si el recorte no cambia `s`, cortar para no colgar nunca.
prev_s="$s"
s="${s#*"$needle"}"
[[ "$s" == "$prev_s" ]] && break
done
# Si el formato fue inesperado y no se parseo ningun par, fila placeholder.
if (( found_any == 0 )); then
printf -v row '%s\t%s\t%s\t%s\t%s\t%s\t%s' "-1" "$ip" "$port" "-" "-" "?" "-"
rows+=("$row")
fi
done < <(ss -H -tlnp 2>/dev/null)
# Estilo de cabecera (negrita) si la terminal lo soporta.
local bold="" reset=""
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
bold=$(tput bold 2>/dev/null || true)
reset=$(tput sgr0 2>/dev/null || true)
fi
# Anchos fijos para alineacion estable (no usamos column -t). La ultima
# columna (CMD) es libre: muestra la cmdline real del proceso.
local fmt='%-26s %-7s %-16s %-8s %-13s %s\n'
# shellcheck disable=SC2059
printf "${bold}${fmt}${reset}" "IP" "PUERTO" "PROCESO" "PID" "TIEMPO ACTIVO" "CMD"
if (( ${#rows[@]} == 0 )); then
printf '(sin sockets TCP en escucha)\n'
return 0
fi
# Ordenar por la primera columna (etimes) numerica descendente y emitir las
# 5 columnas visibles (descartando la columna de orden).
printf '%s\n' "${rows[@]}" \
| sort -t$'\t' -k1,1nr \
| while IFS=$'\t' read -r _etimes ip port pname pid human cmd; do
# shellcheck disable=SC2059
printf "$fmt" "$ip" "$port" "$pname" "$pid" "$human" "$cmd"
done
}
monitor_listening_ports() {
local interval=1
local once=0
# Parseo de flags.
while (( $# > 0 )); do
case "$1" in
--interval)
interval="${2:-1}"
shift 2
;;
--interval=*)
interval="${1#*=}"
shift
;;
--once)
once=1
shift
;;
-h|--help)
cat <<'USAGE'
monitor_listening_ports [--interval N] [--once]
--interval N Segundos entre refrescos (default: 1, acepta decimales).
--once Imprime un solo frame de la tabla y termina (exit 0).
Tabla de sockets TCP en escucha (LISTEN) ordenada por tiempo de vida del
proceso dueño (descendente). Columnas: IP | PUERTO | PROCESO | PID | TIEMPO ACTIVO.
USAGE
return 0
;;
*)
printf 'monitor_listening_ports: argumento desconocido: %s\n' "$1" >&2
return 1
;;
esac
done
# Dependencias minimas.
if ! command -v ss >/dev/null 2>&1; then
printf 'monitor_listening_ports: requiere `ss` (paquete iproute2)\n' >&2
return 1
fi
if ! command -v ps >/dev/null 2>&1; then
printf 'monitor_listening_ports: requiere `ps` (paquete procps)\n' >&2
return 1
fi
# Modo single-frame: util para tests y para `fn run` sin colgar.
if (( once == 1 )); then
_mlp_render_frame
return 0
fi
# Modo bucle interactivo: oculta cursor y lo restaura + limpia al salir.
local have_tput=0
command -v tput >/dev/null 2>&1 && have_tput=1
_mlp_cleanup() {
if (( have_tput == 1 )); then
tput cnorm 2>/dev/null || true # restaurar cursor
tput sgr0 2>/dev/null || true # resetear atributos
fi
printf '\n'
}
trap '_mlp_cleanup; trap - INT TERM EXIT; return 0 2>/dev/null || exit 0' INT TERM EXIT
(( have_tput == 1 )) && tput civis 2>/dev/null || true # ocultar cursor
# Limpiamos la pantalla UNA sola vez al entrar. A partir de aqui cada frame
# se computa COMPLETO en una variable y luego se pinta con doble-buffer:
# cursor a home (\033[H), volcado del frame, y clear-to-end (\033[J) para
# borrar restos de un frame anterior mas largo. Asi nunca hay un instante
# con la pantalla vacia mientras se recolectan los datos.
printf '\033[2J'
local frame
while true; do
frame=$(
printf 'monitor_listening_ports — %s — intervalo %ss — orden: TIEMPO ACTIVO desc (Ctrl-C para salir)\n\n' \
"$(date '+%d/%m/%Y %H:%M:%S')" "$interval"
_mlp_render_frame
)
printf '\033[H' # cursor al inicio (sin borrar todavia)
printf '%s\n' "$frame" # volcar el frame ya calculado de golpe
printf '\033[J' # borrar de aqui al final (restos del frame previo)
sleep "$interval" || break
done
}
# Auto-invocacion cuando se ejecuta como script (no al hacer source).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
monitor_listening_ports "$@"
fi
@@ -0,0 +1,62 @@
---
name: open_onlyoffice_file
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "open_onlyoffice_file(file_path: string, instance: string = demo) -> json"
description: "Abre un archivo en una INSTANCIA AISLADA de ONLYOFFICE Desktop Editors (Linux/X11) sin perturbar la instancia personal del usuario. Cada 'instance' (slot, default demo) usa su propio HOME=/tmp/oo_<instance>, XDG_RUNTIME_DIR y XDG_CONFIG_HOME bajo /tmp, lo que rompe el single-instance lock de ONLYOFFICE y permite una ventana propia en vez de una pestaña en la instancia del usuario. Espera la ventana por evento (xdotool, basename del archivo, timeout ~25s) sin sleep en foreground. Idempotente: si ya hay ventana para ese basename, no relanza y devuelve el wid existente. NO crea archivos: si file_path no existe, falla. Imprime una linea JSON con instance, file (ruta absoluta), wid (hex), pid y status (open|timeout)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_path
desc: "ruta (relativa o absoluta) al archivo a abrir; DEBE existir, esta funcion no crea archivos. Se normaliza con readlink -f y se busca la ventana por su basename"
- name: instance
desc: "nombre del slot aislado (default: demo). Determina el env: HOME=/tmp/oo_<instance>, XDG_RUNTIME_DIR=/tmp/oo_<instance>_run, XDG_CONFIG_HOME=/tmp/oo_<instance>/.config. Usa el MISMO instance en reload/close para operar la misma instancia"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"file\":\"<abs>\",\"wid\":\"<hex>|null\",\"pid\":<n>|null,\"status\":\"open\"|\"timeout\"}. Exit 0 si abrio (status open), exit 1 si la ventana no aparecio en el timeout (status timeout) o falta dependencia/archivo, exit 2 si falta el argumento file_path"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/open_onlyoffice_file.sh"
---
## Ejemplo
```bash
# Como script directo (slot 'demo' por defecto)
bash bash/functions/shell/open_onlyoffice_file.sh /tmp/demo_reload.xlsx
# Slot nombrado distinto (ventana propia, no perturba la instancia personal)
bash bash/functions/shell/open_onlyoffice_file.sh /tmp/informe.docx reporte
# Via fn run
./fn run open_onlyoffice_file_bash_shell /tmp/demo_reload.xlsx demo
# Sourceado, capturando el wid del JSON
source bash/functions/shell/open_onlyoffice_file.sh
out=$(open_onlyoffice_file /tmp/demo_reload.xlsx demo)
echo "$out"
# {"instance":"demo","file":"/tmp/demo_reload.xlsx","wid":"0x3c00007","pid":12345,"status":"open"}
```
## Cuando usarla
- Cuando necesites **abrir un archivo en ONLYOFFICE Desktop desde terminal en su propia ventana aislada**, sin que se agregue como pestaña a la instancia personal del usuario.
- Como primer paso de un flujo automatizado open -> (editas el archivo en disco) -> `reload_onlyoffice_file` -> `close_onlyoffice_instance`.
- Cuando quieras un slot reproducible por nombre (`instance`) que reuse la misma instancia aislada entre llamadas (reabrir rapido en vez de arrancar el motor de cero).
## Gotchas
- **ONLYOFFICE Desktop es single-instance por usuario**: sin el slot aislado (HOME/XDG_RUNTIME_DIR propios), un segundo lanzamiento se reenvia a la instancia viva y abre el archivo como PESTAÑA, no ventana nueva. El lock NO se rompe con XDG_CONFIG_HOME solo; SI con HOME + XDG_RUNTIME_DIR propios. Esta funcion ya aplica esa convencion.
- **NO hay reload nativo de cambios externos** (GitHub Issue #2313 abierto, no implementado). Esta funcion solo abre; para reflejar ediciones hechas en disco hay que cerrar+reabrir con `reload_onlyoffice_file`.
- **NO crea archivos**: si `file_path` no existe, falla con exit 1. Crea el archivo por tu cuenta antes de llamar.
- **El slot vive en /tmp**: los dirs `/tmp/oo_<instance>*` se pierden al reiniciar el PC (tmpfs en muchos sistemas). No guardes nada importante ahi; es estado desechable de la instancia aislada.
- **Requiere X11 + wmctrl + xdotool**: no funciona en Wayland puro sin XWayland (xdotool no encontrara la ventana). La funcion comprueba `command -v` de las 3 deps y falla claro si falta alguna.
- **El pid reportado es el del launcher** (`onlyoffice-desktopeditors`), que puede reexec/fork al proceso real `DesktopEditors`; sirve como referencia best-effort, no para `kill` fiable (usa `close_onlyoffice_instance`, que localiza el proceso real por su HOME).
- **Idempotencia por basename**: si ya existe una ventana cuyo titulo contiene el basename del archivo (lo abrio el usuario en su instancia personal, por ejemplo), la funcion la considera "ya abierta" y devuelve ese wid sin relanzar. Usa un basename unico para el slot de pruebas si quieres evitar colisiones.
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# open_onlyoffice_file — abre un archivo en una INSTANCIA AISLADA de ONLYOFFICE
# Desktop Editors (Linux/X11), sin perturbar la instancia personal del usuario.
#
# Funcion impura: lanza un proceso GUI, lee estado de ventanas (xdotool) y
# escribe directorios en /tmp. Imprime una linea JSON con el resultado.
#
# Por que "instancia aislada": ONLYOFFICE Desktop es single-instance por
# usuario — un segundo `onlyoffice-desktopeditors <file>` se reenvia a la
# instancia viva y abre el archivo como PESTAÑA en su ventana. El lock
# single-instance NO se rompe con XDG_CONFIG_HOME, pero SI se rompe lanzando
# con HOME y XDG_RUNTIME_DIR propios. Por eso cada "slot" nombrado (instance)
# usa su propio HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME bajo /tmp.
# Sin -e: las busquedas de ventana (xdotool search) pueden no matchear y
# devolver exit !=0; no deben abortar la funcion. -u y pipefail se mantienen.
set -uo pipefail
open_onlyoffice_file() {
local file_path="${1:-}"
local instance="${2:-demo}"
if [[ -z "$file_path" ]]; then
echo "open_onlyoffice_file: falta <file_path>" >&2
echo "uso: open_onlyoffice_file <file_path> [instance]" >&2
return 2
fi
# 1. Dependencias del sistema.
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "open_onlyoffice_file: falta dependencia '$dep' (instala el paquete correspondiente)" >&2
return 1
fi
done
# 2. El archivo DEBE existir — esta funcion no crea archivos.
if [[ ! -f "$file_path" ]]; then
echo "open_onlyoffice_file: el archivo no existe: $file_path (esta funcion no crea archivos)" >&2
return 1
fi
# Ruta absoluta y basename para titular/buscar la ventana.
local abs_path base
abs_path=$(readlink -f -- "$file_path")
base=$(basename -- "$abs_path")
# 3. Slot aislado: HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME propios bajo /tmp.
local oo_home="/tmp/oo_${instance}"
local oo_run="/tmp/oo_${instance}_run"
local oo_cfg="${oo_home}/.config"
mkdir -p "$oo_home" "$oo_cfg" "$oo_run"
chmod 700 "$oo_run" 2>/dev/null || true
# 4. Idempotencia: si ya hay ventana para ese basename, no relanzar.
local existing_wid
existing_wid=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
if [[ -n "$existing_wid" ]]; then
local wid_hex
wid_hex=$(printf '0x%x' "$existing_wid" 2>/dev/null || echo "$existing_wid")
printf '{"instance":"%s","file":"%s","wid":"%s","pid":null,"status":"open"}\n' \
"$instance" "$abs_path" "$wid_hex"
return 0
fi
# 5. Lanzar la instancia aislada con su env propio. setsid lo desacopla de
# la terminal; redirige todo a un log del slot.
env HOME="$oo_home" XDG_RUNTIME_DIR="$oo_run" XDG_CONFIG_HOME="$oo_cfg" \
setsid onlyoffice-desktopeditors "$abs_path" \
>"/tmp/oo_${instance}.log" 2>&1 </dev/null &
local launch_pid=$!
# 6. Esperar la ventana por evento (NUNCA sleep en foreground).
# ~25s con read -t 0.3 => ~83 iteraciones.
local wid="" i=0 max=83
while [[ $i -lt $max ]]; do
wid=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
[[ -n "$wid" ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
i=$((i + 1))
done
if [[ -z "$wid" ]]; then
printf '{"instance":"%s","file":"%s","wid":null,"pid":%s,"status":"timeout"}\n' \
"$instance" "$abs_path" "$launch_pid"
return 1
fi
local wid_hex
wid_hex=$(printf '0x%x' "$wid" 2>/dev/null || echo "$wid")
# El pid del proceso real (DesktopEditors) puede diferir del launcher; el
# launcher reexec/fork. Reportamos el pid del launcher (best-effort).
printf '{"instance":"%s","file":"%s","wid":"%s","pid":%s,"status":"open"}\n' \
"$instance" "$abs_path" "$wid_hex" "$launch_pid"
return 0
}
# Ejecutable directo: `bash open_onlyoffice_file.sh <file> [instance]`.
# Sourceado: define la funcion sin ejecutarla.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
open_onlyoffice_file "$@"
fi
@@ -0,0 +1,61 @@
---
name: reload_onlyoffice_file
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "reload_onlyoffice_file(file_path: string, instance: string = demo) -> json"
description: "Recarga en la ventana de ONLYOFFICE Desktop Editors los datos que el caller edito EN DISCO, cerrando y reabriendo el archivo en la INSTANCIA AISLADA (slot). Es la funcion estrella del grupo: ONLYOFFICE no recarga cambios externos del archivo (GitHub Issue #2313 abierto, no implementado), asi que la unica forma de mostrar datos editados fuera de la app es cerrar la ventana (wmctrl -ic) y reabrir (ONLYOFFICE lee fresco del disco al abrir). Localiza la ventana por basename, la cierra y espera a que desaparezca (timeout ~10s), relanza con el env del slot aislado y espera la ventana nueva (timeout ~25s), todo por evento sin sleep en foreground. Si no habia ventana previa, actua como open. NO edita el archivo: el caller lo edita antes de llamar. Imprime JSON con wid_old, wid_new, reopened, elapsed_s y status (reloaded|timeout)."
tags: [onlyoffice, desktop, x11, shell]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_path
desc: "ruta (relativa o absoluta) al archivo cuya ventana se recarga; DEBE existir. El caller ya lo edito en disco antes de llamar. Se busca la ventana por su basename"
- name: instance
desc: "nombre del slot aislado (default: demo); debe coincidir con el usado en open_onlyoffice_file para reusar la misma instancia. Determina HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME bajo /tmp"
output: "una linea JSON a stdout: {\"instance\":\"<i>\",\"file\":\"<abs>\",\"wid_old\":\"<hex>|null\",\"wid_new\":\"<hex>|null\",\"reopened\":true|false,\"elapsed_s\":<n>,\"status\":\"reloaded\"|\"timeout\"}. Exit 0 si reabrio (status reloaded), exit 1 si la ventana nueva no aparecio en el timeout (status timeout) o falta dependencia/archivo, exit 2 si falta file_path"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/reload_onlyoffice_file.sh"
---
## Ejemplo
```bash
# Flujo tipico: editas el .xlsx en disco con tu herramienta y refrescas la vista
# (este ejemplo asume que /tmp/demo_reload.xlsx ya esta abierto en el slot demo)
bash bash/functions/shell/reload_onlyoffice_file.sh /tmp/demo_reload.xlsx demo
# Via fn run
./fn run reload_onlyoffice_file_bash_shell /tmp/demo_reload.xlsx demo
# Sourceado, dentro de un bucle de "editar en disco -> ver en ONLYOFFICE"
source bash/functions/shell/reload_onlyoffice_file.sh
# ... el caller modifica /tmp/demo_reload.xlsx por su cuenta ...
out=$(reload_onlyoffice_file /tmp/demo_reload.xlsx demo)
echo "$out"
# {"instance":"demo","file":"/tmp/demo_reload.xlsx","wid_old":"0x3c00007","wid_new":"0x3c0000b","reopened":true,"elapsed_s":4,"status":"reloaded"}
```
## Cuando usarla
- Cuando **editaste un archivo en disco fuera de ONLYOFFICE** (script, otra herramienta, generador) y necesitas que la ventana de ONLYOFFICE muestre los datos nuevos: esta funcion cierra y reabre para forzar la lectura fresca del disco.
- En bucles de iteracion rapida "modificar el archivo -> ver el resultado en ONLYOFFICE" sin tocar la instancia personal del usuario.
- Como reemplazo del reload nativo inexistente (Issue #2313): es la unica via fiable de refrescar la vista desde disco.
## Gotchas
- **No edita el archivo**: solo recarga la ventana desde disco. El caller es responsable de modificar el archivo ANTES de llamar; si no lo modifico, reabrira los mismos datos.
- **ONLYOFFICE no tiene reload de cambios externos** (GitHub Issue #2313 abierto, no implementado): por eso esta funcion existe y hace cerrar+reabrir. No hay forma "in-place" de refrescar.
- **`wmctrl -ic` puede disparar el dialogo "Guardar cambios"** si el usuario edito EN la app (no en disco) y hay cambios sin guardar en esa ventana. El flujo previsto es editar SOLO en disco con la ventana sin tocar; si editaste en la app, guarda o descarta antes, o el cierre se quedara esperando interaccion (la funcion saldra por timeout).
- **Single-instance + slot aislado**: usa el mismo `instance` que en `open_onlyoffice_file`. Con HOME/XDG_RUNTIME_DIR propios el relaunch reenvia a la instancia aislada viva y reabre rapido; con env por defecto se reenviaria a la instancia personal del usuario (no deseado).
- **El slot vive en /tmp**: `/tmp/oo_<instance>*` se pierde al reiniciar el PC. Estado desechable.
- **Requiere X11 + wmctrl + xdotool**: no funciona en Wayland puro sin XWayland. Comprueba las 3 deps y falla claro si falta alguna.
- **Carrera de cierre**: si la ventana tarda mas de ~10s en cerrarse (dialogo modal, app ocupada), la funcion continua igualmente al relaunch; el resultado puede acabar en `timeout` si la ventana nueva no aparece a tiempo.
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# reload_onlyoffice_file — cierra y reabre un archivo en la INSTANCIA AISLADA de
# ONLYOFFICE Desktop Editors para que la ventana muestre los datos editados
# EN DISCO por el caller (ONLYOFFICE no recarga cambios externos: GitHub Issue
# #2313 abierto, no implementado — la unica forma es cerrar+reabrir).
#
# Funcion impura: cierra una ventana GUI (wmctrl), relanza un proceso y espera
# la ventana nueva por evento. NO edita el archivo — solo recarga la ventana
# desde el disco. El caller edita el archivo antes de llamar a esta funcion.
#
# Instancia aislada (slot): mismo HOME/XDG_RUNTIME_DIR/XDG_CONFIG_HOME que usa
# open_onlyoffice_file, para que el relaunch reenvie a la instancia aislada
# viva y reabra rapido en vez de arrancar el motor de cero.
# Sin -e: busquedas de ventana (xdotool/wmctrl) pueden no matchear; no deben
# abortar la funcion. -u y pipefail se mantienen.
set -uo pipefail
reload_onlyoffice_file() {
local file_path="${1:-}"
local instance="${2:-demo}"
if [[ -z "$file_path" ]]; then
echo "reload_onlyoffice_file: falta <file_path>" >&2
echo "uso: reload_onlyoffice_file <file_path> [instance]" >&2
return 2
fi
# 1. Dependencias del sistema.
local dep
for dep in onlyoffice-desktopeditors wmctrl xdotool; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "reload_onlyoffice_file: falta dependencia '$dep' (instala el paquete correspondiente)" >&2
return 1
fi
done
# 2. El archivo DEBE existir — no editamos ni creamos archivos.
if [[ ! -f "$file_path" ]]; then
echo "reload_onlyoffice_file: el archivo no existe: $file_path" >&2
return 1
fi
local abs_path base
abs_path=$(readlink -f -- "$file_path")
base=$(basename -- "$abs_path")
# 3. Slot aislado (identico a open_onlyoffice_file).
local oo_home="/tmp/oo_${instance}"
local oo_run="/tmp/oo_${instance}_run"
local oo_cfg="${oo_home}/.config"
mkdir -p "$oo_home" "$oo_cfg" "$oo_run"
chmod 700 "$oo_run" 2>/dev/null || true
local start_ts
start_ts=$(date +%s)
# 4. Localizar la ventana actual del archivo por basename.
local wid_old=""
wid_old=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
local wid_old_hex="null"
if [[ -n "$wid_old" ]]; then
wid_old_hex=$(printf '0x%x' "$wid_old" 2>/dev/null || echo "$wid_old")
# 5. Cerrar la ventana (sin teclear en la app) y esperar a que
# desaparezca (~10s con read -t 0.3 => ~33 iteraciones).
wmctrl -ic "$wid_old" 2>/dev/null || true
local g=0 gmax=33
while [[ $g -lt $gmax ]]; do
if ! xdotool search --name -- "$base" 2>/dev/null | grep -q .; then
break
fi
read -t 0.3 _ </dev/null 2>/dev/null || true
g=$((g + 1))
done
fi
# 6. Relanzar con el env del slot aislado. (Si no habia ventana previa,
# esto actua simplemente como open.)
env HOME="$oo_home" XDG_RUNTIME_DIR="$oo_run" XDG_CONFIG_HOME="$oo_cfg" \
setsid onlyoffice-desktopeditors "$abs_path" \
>"/tmp/oo_${instance}.log" 2>&1 </dev/null &
# 7. Esperar la ventana nueva por evento (~25s => ~83 iteraciones).
local wid_new="" i=0 max=83
while [[ $i -lt $max ]]; do
wid_new=$(xdotool search --name -- "$base" 2>/dev/null | head -1 || true)
# Si hubo ventana previa, aceptar cualquier wid que aparezca (el old
# ya se cerro; el nuevo puede reutilizar id o no). Si no la hubo,
# cualquier wid sirve.
[[ -n "$wid_new" ]] && break
read -t 0.3 _ </dev/null 2>/dev/null || true
i=$((i + 1))
done
local now_ts elapsed
now_ts=$(date +%s)
elapsed=$((now_ts - start_ts))
if [[ -z "$wid_new" ]]; then
printf '{"instance":"%s","file":"%s","wid_old":"%s","wid_new":null,"reopened":false,"elapsed_s":%s,"status":"timeout"}\n' \
"$instance" "$abs_path" "$wid_old_hex" "$elapsed"
return 1
fi
local wid_new_hex
wid_new_hex=$(printf '0x%x' "$wid_new" 2>/dev/null || echo "$wid_new")
printf '{"instance":"%s","file":"%s","wid_old":"%s","wid_new":"%s","reopened":true,"elapsed_s":%s,"status":"reloaded"}\n' \
"$instance" "$abs_path" "$wid_old_hex" "$wid_new_hex" "$elapsed"
return 0
}
# Ejecutable directo o sourceado.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
reload_onlyoffice_file "$@"
fi