fix(install): actualización desatendida en Windows, con reversión

install.sh recibió la maquinaria de actualización segura y install.ps1 nunca
recibió el equivalente. La asimetría se notaba en producción: actualizar desde
el PANEL dejaba el servidor sin agente.

- -UpdateInPlace: actualiza conservando la tarea y la configuración, sin correr
  el bootstrap (una segunda instancia purga el Temp\ de la que está viva).
- No se interrumpe una restauración en curso: sale con 75 (EX_TEMPFAIL), que el
  PANEL traduce a "reintenta luego". Windows no tenía esta guarda y una
  reinstalación a destiempo dejaba el respaldo vetado y la base en SINGLE_USER.
- Respaldo del binario anterior y reversión automática si el nuevo no arranca.
- Rearranque garantizado en TODOS los modos: la detención corría siempre, pero
  solo -Service volvía a arrancar algo.
- Espera de liberación del .exe de 5s a 30s con reintentos de la copia.
- Se distingue "no es administrador" de "es administrador con el token filtrado
  por UAC", que es lo que recibe una sesión de OpenSSH. Se veían idénticos y el
  remedio es el opuesto.

Y --headless deja de ser un no-op en Windows: _ensure_qt_platform() salía de
inmediato en win32, así que la tarea ONSTART arrancaba como SYSTEM en la sesión 0
con el plugin Qt 'windows' intentando crear una ventana real. En Linux el unit
fija QT_QPA_PLATFORM=offscreen por fuera, y esa asimetría escondió el defecto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:46:12 -06:00
parent c8133a5108
commit c2afa52d6f
4 changed files with 441 additions and 59 deletions

View File

@@ -77,19 +77,33 @@ def _print_version() -> int:
def _ensure_qt_platform(headless: bool = False) -> str:
"""
Selecciona el plugin de plataforma Qt en Linux. En un servidor headless (sin
DISPLAY/WAYLAND_DISPLAY) o con --headless, usa 'offscreen' para que la app
arranque y el motor trabaje sin X; con display usa el default ('xcb'). No toca
nada si el usuario ya fijó QT_QPA_PLATFORM, ni en Windows/macOS.
Selecciona el plugin de plataforma Qt.
Devuelve la plataforma forzada ("offscreen") o "" si se deja el default.
`--headless` fuerza 'offscreen' en TODAS las plataformas. Antes se salía de inmediato en
Windows, así que la bandera no hacía nada ahí: la tarea programada ONSTART —que corre como
SYSTEM, en la sesión 0, sin escritorio interactivo— arrancaba con el plugin 'windows' e
intentaba crear una ventana real. Ese es el motivo de que el agente no levantara tras
instalarse en Windows mientras en Linux, donde el unit fija QT_QPA_PLATFORM=offscreen, sí.
El plugin va embebido en el binario de las dos plataformas (qoffscreen.dll / libqoffscreen.so).
La autodetección por DISPLAY/WAYLAND_DISPLAY sigue siendo solo de Linux: es donde su ausencia
significa "no hay servidor gráfico". En Windows y macOS no existen esas variables y tomarlas
como señal mandaría a offscreen a cualquiera que abra la app con doble clic.
No toca nada si el usuario ya fijó QT_QPA_PLATFORM: una elección explícita manda sobre todo
lo demás.
Devuelve la plataforma en uso ("offscreen") o "" si se deja el default.
"""
if sys.platform in ("win32", "darwin"):
return ""
if os.environ.get("QT_QPA_PLATFORM"):
return os.environ["QT_QPA_PLATFORM"]
if headless:
os.environ["QT_QPA_PLATFORM"] = "offscreen"
return "offscreen"
if sys.platform in ("win32", "darwin"):
return ""
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
if headless or not has_display:
if not has_display:
os.environ["QT_QPA_PLATFORM"] = "offscreen"
return "offscreen"
return ""
@@ -206,11 +220,16 @@ def _run() -> int:
start_engine=start_engine,
panel_configured=panel_ok,
)
# La ventana SIEMPRE se muestra al iniciar; cerrar (X) la manda a la bandeja.
window.show()
window.activateWindow()
window.raise_()
app_logger.info("Ventana principal mostrada")
# Con display, la ventana SIEMPRE se muestra al iniciar; cerrar (X) la manda a la bandeja.
# En offscreen no hay a quién mostrarla, y pedirlo igual solo da trabajo al plugin y ruido en
# el log. El motor no depende de esto: arranca por su propio temporizador en MainWindow.
if qt_platform == "offscreen":
app_logger.info("Sin interfaz: la ventana no se muestra (motor headless)")
else:
window.show()
window.activateWindow()
window.raise_()
app_logger.info("Ventana principal mostrada")
exit_code = app.exec()
app_logger.info(f"Aplicación finalizada con código: {exit_code}")