zerochat 9.9.0__tar.gz → 9.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of zerochat might be problematic. Click here for more details.
- {zerochat-9.9.0/zerochat.egg-info → zerochat-9.10.0}/PKG-INFO +1 -1
- {zerochat-9.9.0 → zerochat-9.10.0}/pyproject.toml +1 -1
- {zerochat-9.9.0 → zerochat-9.10.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-9.9.0 → zerochat-9.10.0}/zerochat.py +167 -103
- {zerochat-9.9.0 → zerochat-9.10.0}/LICENSE +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/README.md +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/setup.cfg +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-9.9.0 → zerochat-9.10.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -46,7 +46,7 @@ import zlib
|
|
|
46
46
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
47
47
|
from pathlib import Path
|
|
48
48
|
|
|
49
|
-
SOURCE_BACKEND_VERSION = "9.
|
|
49
|
+
SOURCE_BACKEND_VERSION = "9.10.0"
|
|
50
50
|
|
|
51
51
|
def _read_source_version(filename: str) -> str | None:
|
|
52
52
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -316,7 +316,7 @@ ACTIVE_HOST = DEFAULT_HOST
|
|
|
316
316
|
# Kept only for this Python process: binds an approved browser tool call to
|
|
317
317
|
# the exact JSON-RPC bytes sent to the local host.
|
|
318
318
|
TOOL_AUTH_VERSION = "zerochat-tool-auth-v1"
|
|
319
|
-
TOOL_AUTH_TTL_MS =
|
|
319
|
+
TOOL_AUTH_TTL_MS = 60_000
|
|
320
320
|
TOOL_AUTH_MAX_NONCES = 10_000
|
|
321
321
|
TOOL_AUTH_KEY = secrets.token_bytes(32)
|
|
322
322
|
TOOL_AUTH_SESSION_ID = secrets.token_urlsafe(18)
|
|
@@ -4453,10 +4453,8 @@ class ZeroChatArgumentParser(argparse.ArgumentParser):
|
|
|
4453
4453
|
self.exit(RUN_EXIT_ARGS, f"{self.prog}: error: {message}\n")
|
|
4454
4454
|
|
|
4455
4455
|
|
|
4456
|
-
def
|
|
4457
|
-
|
|
4458
|
-
global TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES, RUN_SESSION
|
|
4459
|
-
|
|
4456
|
+
def build_parser() -> ZeroChatArgumentParser:
|
|
4457
|
+
"""Opciones de la línea de órdenes."""
|
|
4460
4458
|
parser = ZeroChatArgumentParser(description=f"ZeroChat Local Server v{VERSION}")
|
|
4461
4459
|
# Desde Python 3.14 argparse colorea la ayuda con códigos ANSI; la consola la muestra en texto plano.
|
|
4462
4460
|
parser.color = False
|
|
@@ -4464,6 +4462,7 @@ def main():
|
|
|
4464
4462
|
parser.add_argument("--host", default=os.environ.get("ZEROCHAT_HOST", DEFAULT_HOST), help=f"Host de escucha (default: {DEFAULT_HOST})")
|
|
4465
4463
|
parser.add_argument("--ui-url", default=None, help="URL de la interfaz web a abrir (por defecto: interfaz local en desarrollo o GitHub Pages)")
|
|
4466
4464
|
parser.add_argument("--no-browser", action="store_true", help="No abrir automáticamente el navegador")
|
|
4465
|
+
parser.add_argument("--print-url", action="store_true", help="Imprimir la URL de la interfaz con el token, entre líneas en blanco para copiarla (implica --no-browser)")
|
|
4467
4466
|
parser.add_argument("--keep-server", action="store_true", help="Servidor: seguir en marcha al cerrar el navegador (no se combina con --prompt)")
|
|
4468
4467
|
parser.add_argument("--no-venv", action="store_true", help="Omitir la comprobación/creación de ~/zerochat/.venv")
|
|
4469
4468
|
parser.add_argument("--test", action="store_true", help="Ejecutar autocomprobación interna de herramientas")
|
|
@@ -4475,76 +4474,79 @@ def main():
|
|
|
4475
4474
|
parser.add_argument("--allow-dangerous-tools", action="store_true", help="Con --prompt, aprobar también las herramientas que escriben o ejecutan (write_file, edit_file, run_command); las demás se aprueban siempre para la sesión")
|
|
4476
4475
|
parser.add_argument("--headless", action="store_true", help="Con --prompt, abrir el navegador predeterminado sin ventana (si ya está abierto puede verse la pestaña)")
|
|
4477
4476
|
parser.add_argument("--version", action="version", version=f"ZeroChat {VERSION}")
|
|
4478
|
-
|
|
4477
|
+
return parser
|
|
4479
4478
|
|
|
4480
|
-
# Los avisos no persisten entre ejecuciones del servidor.
|
|
4481
|
-
reset_notices()
|
|
4482
4479
|
|
|
4483
|
-
|
|
4480
|
+
def run_session_from_args(parser: ZeroChatArgumentParser, args) -> RunSession | None:
|
|
4481
|
+
"""Valida las combinaciones de opciones y devuelve la sesión de --prompt, si la hay."""
|
|
4482
|
+
# Quien pide la URL la abrirá en otro sitio (otro equipo, un túnel SSH): no se abre el navegador local.
|
|
4483
|
+
if args.print_url and args.prompt is None:
|
|
4484
|
+
args.no_browser = True
|
|
4485
|
+
if args.prompt is None:
|
|
4486
|
+
if args.headless or args.keep_page or args.allow_dangerous_tools or args.profile is not None or args.export is not None:
|
|
4487
|
+
parser.error("--headless, --keep-page, --allow-dangerous-tools, --profile y --export solo se usan con --prompt")
|
|
4488
|
+
return None
|
|
4489
|
+
if not args.prompt.strip():
|
|
4490
|
+
parser.error("el prompt está vacío")
|
|
4491
|
+
if args.no_browser or args.print_url:
|
|
4492
|
+
parser.error("--prompt necesita abrir el navegador: no se combina con --no-browser ni con --print-url")
|
|
4493
|
+
if args.keep_server:
|
|
4494
|
+
parser.error("--prompt termina al devolver el resultado: no se combina con --keep-server (para no cerrar la página, --keep-page)")
|
|
4495
|
+
if args.profile is not None and not args.profile.strip():
|
|
4496
|
+
parser.error("--profile no puede estar vacío")
|
|
4497
|
+
if args.keep_page and args.headless:
|
|
4498
|
+
parser.error("--keep-page necesita la ventana del navegador: no se combina con --headless")
|
|
4499
|
+
export_path = None
|
|
4500
|
+
if args.export is not None:
|
|
4501
|
+
export_path = Path(args.export).expanduser().absolute()
|
|
4502
|
+
if export_path.suffix.lower() not in RUN_EXPORT_FORMATS:
|
|
4503
|
+
parser.error(f"--export necesita una de estas extensiones: {', '.join(RUN_EXPORT_FORMATS)}")
|
|
4504
|
+
if not export_path.parent.is_dir():
|
|
4505
|
+
parser.error(f"--export: no existe el directorio {export_path.parent}")
|
|
4506
|
+
return RunSession(args.prompt.strip(), args.profile.strip() if args.profile else None, export_path, args.keep_page, args.allow_dangerous_tools)
|
|
4507
|
+
|
|
4508
|
+
|
|
4509
|
+
def redirect_log(args):
|
|
4510
|
+
"""Todo el registro pasa por stdout: con --prompt va a stderr para dejar stdout a la respuesta,
|
|
4511
|
+
y con --log off se descarta. Devuelve el stdout original, el de la respuesta."""
|
|
4484
4512
|
result_out = sys.stdout
|
|
4485
|
-
if args.prompt is None and (args.headless or args.keep_page or args.allow_dangerous_tools or args.profile is not None or args.export is not None):
|
|
4486
|
-
parser.error("--headless, --keep-page, --allow-dangerous-tools, --profile y --export solo se usan con --prompt")
|
|
4487
|
-
if args.prompt is not None:
|
|
4488
|
-
if not args.prompt.strip():
|
|
4489
|
-
parser.error("el prompt está vacío")
|
|
4490
|
-
if args.no_browser:
|
|
4491
|
-
parser.error("--prompt necesita abrir el navegador: no se combina con --no-browser")
|
|
4492
|
-
if args.keep_server:
|
|
4493
|
-
parser.error("--prompt termina al devolver el resultado: no se combina con --keep-server (para no cerrar la página, --keep-page)")
|
|
4494
|
-
if args.profile is not None and not args.profile.strip():
|
|
4495
|
-
parser.error("--profile no puede estar vacío")
|
|
4496
|
-
if args.keep_page and args.headless:
|
|
4497
|
-
parser.error("--keep-page necesita la ventana del navegador: no se combina con --headless")
|
|
4498
|
-
export_path = None
|
|
4499
|
-
if args.export is not None:
|
|
4500
|
-
export_path = Path(args.export).expanduser().absolute()
|
|
4501
|
-
if export_path.suffix.lower() not in RUN_EXPORT_FORMATS:
|
|
4502
|
-
parser.error(f"--export necesita una de estas extensiones: {', '.join(RUN_EXPORT_FORMATS)}")
|
|
4503
|
-
if not export_path.parent.is_dir():
|
|
4504
|
-
parser.error(f"--export: no existe el directorio {export_path.parent}")
|
|
4505
|
-
RUN_SESSION = RunSession(args.prompt.strip(), args.profile.strip() if args.profile else None, export_path, args.keep_page, args.allow_dangerous_tools)
|
|
4506
|
-
# Todo el registro pasa por stdout: con --prompt va a stderr para dejar stdout a la respuesta,
|
|
4507
|
-
# y con --log off se descarta.
|
|
4508
4513
|
log_enabled = args.log == "on" if args.log else RUN_SESSION is None
|
|
4509
4514
|
if not log_enabled:
|
|
4510
4515
|
sys.stdout = open(os.devnull, "w", encoding="utf-8")
|
|
4511
4516
|
elif RUN_SESSION:
|
|
4512
4517
|
sys.stdout = sys.stderr
|
|
4518
|
+
return result_out
|
|
4513
4519
|
|
|
4514
|
-
if args.test:
|
|
4515
|
-
print(timestamp_message(f"TEST list_directory {'ok' if json.loads(list_directory('.'))['success'] else 'error'}"))
|
|
4516
|
-
print(timestamp_message(f"TEST read_file {'ok' if json.loads(read_file('package.json', max_lines=5))['success'] else 'error'}"))
|
|
4517
|
-
print(timestamp_message(f"TEST run_command {'ok' if json.loads(run_command('echo hello'))['success'] else 'error'}"))
|
|
4518
|
-
print(timestamp_message("TEST all local tools ready."))
|
|
4519
|
-
return
|
|
4520
4520
|
|
|
4521
|
+
def run_self_test():
|
|
4522
|
+
"""--test: comprueba las herramientas locales básicas sin arrancar el servidor."""
|
|
4523
|
+
print(timestamp_message(f"TEST list_directory {'ok' if json.loads(list_directory('.'))['success'] else 'error'}"))
|
|
4524
|
+
print(timestamp_message(f"TEST read_file {'ok' if json.loads(read_file('package.json', max_lines=5))['success'] else 'error'}"))
|
|
4525
|
+
print(timestamp_message(f"TEST run_command {'ok' if json.loads(run_command('echo hello'))['success'] else 'error'}"))
|
|
4526
|
+
print(timestamp_message("TEST all local tools ready."))
|
|
4527
|
+
|
|
4528
|
+
|
|
4529
|
+
def prepare_environment(args, is_dev: bool):
|
|
4530
|
+
"""Limpia restos de una sesión anterior, prepara el entorno MCP y lanza las comprobaciones de fondo."""
|
|
4521
4531
|
orphans = cleanup_orphan_jobs()
|
|
4522
4532
|
if orphans:
|
|
4523
4533
|
print(timestamp_message(f"Detenidos {orphans} comando(s) que seguían en marcha de una sesión anterior interrumpida."))
|
|
4524
|
-
|
|
4525
|
-
# 1. Asegurar el entorno MCP aislado en ambos modos de distribución.
|
|
4534
|
+
# Entorno MCP aislado en ambos modos de distribución.
|
|
4526
4535
|
if not args.no_venv:
|
|
4527
4536
|
ensure_virtual_environment()
|
|
4528
|
-
|
|
4529
|
-
browser_availability = browser_action_availability()
|
|
4530
|
-
if not browser_availability.get("available"):
|
|
4537
|
+
if not browser_action_availability().get("available"):
|
|
4531
4538
|
add_notice(
|
|
4532
4539
|
"browser_action desactivada: para usarla, inicia el servicio 'playwright' en Ajustes → MCP "
|
|
4533
4540
|
"(se instala solo). Requiere Node.js y disponer de Chrome o Edge en el sistema."
|
|
4534
4541
|
)
|
|
4535
|
-
|
|
4536
|
-
# 2. Detectar entorno de desarrollo y resolver URL de destino
|
|
4537
|
-
dev_root = get_dev_root()
|
|
4538
|
-
static_root = get_static_root()
|
|
4539
|
-
is_dev = dev_root is not None
|
|
4540
|
-
is_installed = is_installed_runtime()
|
|
4541
|
-
|
|
4542
|
-
# 3. Comprobar versión remota en segundo plano (solo fuera del entorno de desarrollo local)
|
|
4542
|
+
# Versión remota en segundo plano, solo fuera del entorno de desarrollo local.
|
|
4543
4543
|
if not is_dev:
|
|
4544
4544
|
threading.Thread(target=check_version, daemon=True).start()
|
|
4545
4545
|
|
|
4546
|
-
|
|
4547
|
-
|
|
4546
|
+
|
|
4547
|
+
def reset_session_credentials():
|
|
4548
|
+
"""Carga el token diario y renueva la clave de firma de herramientas."""
|
|
4549
|
+
global SESSION_TOKEN, TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES
|
|
4548
4550
|
SESSION_TOKEN = get_daily_token()
|
|
4549
4551
|
# A restart deliberately invalidates every browser-side signing credential.
|
|
4550
4552
|
TOOL_AUTH_KEY = secrets.token_bytes(32)
|
|
@@ -4552,10 +4554,14 @@ def main():
|
|
|
4552
4554
|
with TOOL_AUTH_NONCES_LOCK:
|
|
4553
4555
|
TOOL_AUTH_NONCES = {}
|
|
4554
4556
|
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
+
|
|
4558
|
+
def open_server(args, local_ui: bool) -> ThreadingHTTPServer:
|
|
4559
|
+
"""Abre el puerto de escucha; con --prompt, si el puerto por defecto está ocupado, usa uno libre."""
|
|
4560
|
+
global ACTIVE_PORT, ACTIVE_HOST
|
|
4561
|
+
ACTIVE_PORT = args.port if args.port is not None else int(os.environ.get("ZEROCHAT_PORT", DEFAULT_PORT))
|
|
4562
|
+
ACTIVE_HOST = args.host
|
|
4557
4563
|
try:
|
|
4558
|
-
|
|
4564
|
+
return ThreadingHTTPServer((ACTIVE_HOST, ACTIVE_PORT), ZeroChatServerHandler)
|
|
4559
4565
|
except OSError as exc:
|
|
4560
4566
|
if not RUN_SESSION:
|
|
4561
4567
|
raise
|
|
@@ -4563,42 +4569,42 @@ def main():
|
|
|
4563
4569
|
hint = "" if args.port is not None else " (en desarrollo la interfaz local necesita ese puerto: cierra el zerochat que lo usa)"
|
|
4564
4570
|
run_error(f"No se pudo escuchar en {ACTIVE_HOST}:{ACTIVE_PORT}: {exc}{hint}")
|
|
4565
4571
|
sys.exit(RUN_EXIT_SETUP)
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
add_notice(exposure_warning)
|
|
4572
|
+
# Otro zerochat (u otro programa) usa el puerto por defecto: la ejecución usa uno libre.
|
|
4573
|
+
try:
|
|
4574
|
+
server = ThreadingHTTPServer((ACTIVE_HOST, 0), ZeroChatServerHandler)
|
|
4575
|
+
except OSError as retry_exc:
|
|
4576
|
+
run_error(f"No se pudo escuchar en {ACTIVE_HOST}: {retry_exc}")
|
|
4577
|
+
sys.exit(RUN_EXIT_SETUP)
|
|
4578
|
+
log_event(f"El puerto {ACTIVE_PORT} está ocupado; la ejecución usa el {server.server_address[1]}.")
|
|
4579
|
+
ACTIVE_PORT = server.server_address[1]
|
|
4580
|
+
return server
|
|
4581
|
+
|
|
4577
4582
|
|
|
4583
|
+
def interface_urls(args, static_root) -> tuple[str, str]:
|
|
4584
|
+
"""Devuelve la URL de la interfaz y la de destino, con el token y el servidor en el fragmento."""
|
|
4578
4585
|
if args.ui_url:
|
|
4579
4586
|
ui_url = args.ui_url
|
|
4580
4587
|
elif static_root:
|
|
4581
4588
|
ui_url = f"http://{ACTIVE_HOST}:{ACTIVE_PORT}/zerochat.html"
|
|
4582
4589
|
else:
|
|
4583
4590
|
ui_url = DEFAULT_UI_URL
|
|
4584
|
-
|
|
4585
4591
|
browser_host = ACTIVE_HOST if ACTIVE_HOST in {"127.0.0.1", "localhost"} else "127.0.0.1"
|
|
4586
4592
|
fragment = {'token': SESSION_TOKEN, 'host': browser_host, 'port': ACTIVE_PORT}
|
|
4587
4593
|
if RUN_SESSION:
|
|
4588
4594
|
fragment['run'] = RUN_SESSION.run_id
|
|
4589
|
-
|
|
4590
|
-
# El modo ejecución (que rechaza --keep-server) detecta con el latido que el navegador se cerró sin devolver resultado.
|
|
4591
|
-
exit_on_close = not args.keep_server
|
|
4592
|
-
EXIT_ON_CLOSE = exit_on_close
|
|
4595
|
+
return ui_url, f"{ui_url}#{urlencode(fragment)}"
|
|
4593
4596
|
|
|
4597
|
+
|
|
4598
|
+
def print_header(dev_root, ui_url: str, exposure_warning: str | None, exit_on_close: bool):
|
|
4599
|
+
"""Cabecera del servidor con su configuración (sin el token)."""
|
|
4594
4600
|
print("=" * 64)
|
|
4595
4601
|
print(f" ZeroChat Local Server v{VERSION} (UI {UI_VERSION})")
|
|
4596
4602
|
print(f" Directorio de trabajo : {Path.cwd()}")
|
|
4597
4603
|
print(f" Datos y MCP : {get_data_dir()}")
|
|
4598
4604
|
print(f" Entorno MCP : {get_venv_dir()}")
|
|
4599
|
-
if
|
|
4605
|
+
if dev_root is not None:
|
|
4600
4606
|
print(f" Modo de ejecución : Desarrollo local ({dev_root})")
|
|
4601
|
-
elif
|
|
4607
|
+
elif is_installed_runtime():
|
|
4602
4608
|
print(" Modo de ejecución : Paquete PyPI (GitHub Pages)")
|
|
4603
4609
|
else:
|
|
4604
4610
|
print(" Modo de ejecución : Producción (Web universal)")
|
|
@@ -4613,31 +4619,43 @@ def main():
|
|
|
4613
4619
|
print(" Auto-cierre : Desactivado")
|
|
4614
4620
|
print("=" * 64, flush=True)
|
|
4615
4621
|
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
threading.Thread(target=fall_back_when_headless_exits, args=(headless_browser, target_url), daemon=True).start()
|
|
4626
|
-
threading.Thread(target=run_supervisor, args=(server, RUN_SESSION), daemon=True).start()
|
|
4627
|
-
else:
|
|
4622
|
+
|
|
4623
|
+
def start_interface(args, parser, server: ThreadingHTTPServer, target_url: str, result_out):
|
|
4624
|
+
"""Abre la interfaz: en modo ejecución, con su supervisor; si no, con el control de consola.
|
|
4625
|
+
Devuelve el navegador sin ventana de --headless, si se lanzó."""
|
|
4626
|
+
global CONSOLE_CONTROL
|
|
4627
|
+
if args.print_url:
|
|
4628
|
+
# Líneas en blanco alrededor para seleccionarla y copiarla con facilidad (p. ej. en una consola SSH del móvil).
|
|
4629
|
+
print("\n" * 5 + target_url + "\n" * 5, flush=True)
|
|
4630
|
+
if not RUN_SESSION:
|
|
4628
4631
|
CONSOLE_CONTROL = ConsoleControl(server, parser, target_url=target_url)
|
|
4629
4632
|
CONSOLE_CONTROL.start()
|
|
4630
4633
|
if not args.no_browser:
|
|
4631
4634
|
launch_browser(target_url)
|
|
4635
|
+
return None
|
|
4636
|
+
headless_browser = launch_headless_browser(target_url) if args.headless else None
|
|
4637
|
+
if headless_browser is None and not launch_browser(target_url):
|
|
4638
|
+
run_error("No se pudo abrir el navegador.")
|
|
4639
|
+
server.server_close()
|
|
4640
|
+
sys.stdout = result_out
|
|
4641
|
+
sys.exit(RUN_EXIT_SETUP)
|
|
4642
|
+
if headless_browser is not None:
|
|
4643
|
+
threading.Thread(target=fall_back_when_headless_exits, args=(headless_browser, target_url), daemon=True).start()
|
|
4644
|
+
threading.Thread(target=run_supervisor, args=(server, RUN_SESSION), daemon=True).start()
|
|
4645
|
+
return headless_browser
|
|
4646
|
+
|
|
4647
|
+
|
|
4648
|
+
def start_heartbeat_watchdog(server: ThreadingHTTPServer, require_initial: bool):
|
|
4649
|
+
"""Detiene el servidor cuando el navegador deja de enviar latidos."""
|
|
4650
|
+
threading.Thread(
|
|
4651
|
+
target=heartbeat_watchdog,
|
|
4652
|
+
args=(server, DEFAULT_HEARTBEAT_GRACE, DEFAULT_HEARTBEAT_TIMEOUT, require_initial),
|
|
4653
|
+
daemon=True
|
|
4654
|
+
).start()
|
|
4632
4655
|
|
|
4633
|
-
if exit_on_close:
|
|
4634
|
-
require_initial = not args.no_browser
|
|
4635
|
-
threading.Thread(
|
|
4636
|
-
target=heartbeat_watchdog,
|
|
4637
|
-
args=(server, DEFAULT_HEARTBEAT_GRACE, DEFAULT_HEARTBEAT_TIMEOUT, require_initial),
|
|
4638
|
-
daemon=True
|
|
4639
|
-
).start()
|
|
4640
4656
|
|
|
4657
|
+
def install_signal_handlers(server: ThreadingHTTPServer):
|
|
4658
|
+
"""Ctrl+C, SIGTERM y el cierre del terminal detienen el servidor de forma ordenada."""
|
|
4641
4659
|
def shutdown(*_):
|
|
4642
4660
|
console_log("\n" + timestamp_message("Deteniendo servidor ZeroChat..."))
|
|
4643
4661
|
stop_zerochat_server(server)
|
|
@@ -4648,6 +4666,10 @@ def main():
|
|
|
4648
4666
|
if hasattr(signal, "SIGHUP"):
|
|
4649
4667
|
signal.signal(signal.SIGHUP, shutdown)
|
|
4650
4668
|
|
|
4669
|
+
|
|
4670
|
+
def serve(server: ThreadingHTTPServer):
|
|
4671
|
+
"""Atiende peticiones hasta la parada y libera después el servidor y la consola."""
|
|
4672
|
+
global CONSOLE_CONTROL
|
|
4651
4673
|
try:
|
|
4652
4674
|
server.serve_forever()
|
|
4653
4675
|
finally:
|
|
@@ -4657,16 +4679,58 @@ def main():
|
|
|
4657
4679
|
CONSOLE_CONTROL.close()
|
|
4658
4680
|
CONSOLE_CONTROL = None
|
|
4659
4681
|
|
|
4682
|
+
|
|
4683
|
+
def finish_run(headless_browser, result_out):
|
|
4684
|
+
"""Modo ejecución: escribe la respuesta final en el stdout original y sale con su código."""
|
|
4685
|
+
# Firefox sin ventana sigue vivo al cerrar la página; uno ya abierto solo recibió la pestaña.
|
|
4686
|
+
if headless_browser is not None and headless_browser.poll() is None:
|
|
4687
|
+
_kill_process_tree(headless_browser)
|
|
4688
|
+
code, text = run_outcome(RUN_SESSION)
|
|
4689
|
+
sys.stdout = result_out
|
|
4690
|
+
if text is not None:
|
|
4691
|
+
result_out.write(text if text.endswith("\n") else text + "\n")
|
|
4692
|
+
result_out.flush()
|
|
4693
|
+
sys.exit(code)
|
|
4694
|
+
|
|
4695
|
+
|
|
4696
|
+
def main():
|
|
4697
|
+
global RUN_SESSION, EXIT_ON_CLOSE
|
|
4698
|
+
|
|
4699
|
+
parser = build_parser()
|
|
4700
|
+
args = parser.parse_args()
|
|
4701
|
+
# Los avisos no persisten entre ejecuciones del servidor.
|
|
4702
|
+
reset_notices()
|
|
4703
|
+
RUN_SESSION = run_session_from_args(parser, args)
|
|
4704
|
+
result_out = redirect_log(args)
|
|
4705
|
+
|
|
4706
|
+
if args.test:
|
|
4707
|
+
run_self_test()
|
|
4708
|
+
return
|
|
4709
|
+
|
|
4710
|
+
dev_root = get_dev_root()
|
|
4711
|
+
static_root = get_static_root()
|
|
4712
|
+
prepare_environment(args, is_dev=dev_root is not None)
|
|
4713
|
+
reset_session_credentials()
|
|
4714
|
+
|
|
4715
|
+
# La interfaz local de desarrollo tiene el puerto en su origen: en otro no vería los datos guardados.
|
|
4716
|
+
server = open_server(args, local_ui=static_root is not None and not args.ui_url)
|
|
4717
|
+
exposure_warning = remote_exposure_warning(ACTIVE_HOST)
|
|
4718
|
+
if exposure_warning:
|
|
4719
|
+
add_notice(exposure_warning)
|
|
4720
|
+
|
|
4721
|
+
ui_url, target_url = interface_urls(args, static_root)
|
|
4722
|
+
# El modo ejecución (que rechaza --keep-server) detecta con el latido que el navegador se cerró sin devolver resultado.
|
|
4723
|
+
EXIT_ON_CLOSE = not args.keep_server
|
|
4724
|
+
print_header(dev_root, ui_url, exposure_warning, EXIT_ON_CLOSE)
|
|
4725
|
+
|
|
4726
|
+
headless_browser = start_interface(args, parser, server, target_url, result_out)
|
|
4727
|
+
if EXIT_ON_CLOSE:
|
|
4728
|
+
start_heartbeat_watchdog(server, require_initial=not args.no_browser)
|
|
4729
|
+
install_signal_handlers(server)
|
|
4730
|
+
serve(server)
|
|
4731
|
+
|
|
4660
4732
|
if RUN_SESSION:
|
|
4661
|
-
|
|
4662
|
-
if headless_browser is not None and headless_browser.poll() is None:
|
|
4663
|
-
_kill_process_tree(headless_browser)
|
|
4664
|
-
code, text = run_outcome(RUN_SESSION)
|
|
4665
|
-
sys.stdout = result_out
|
|
4666
|
-
if text is not None:
|
|
4667
|
-
result_out.write(text if text.endswith("\n") else text + "\n")
|
|
4668
|
-
result_out.flush()
|
|
4669
|
-
sys.exit(code)
|
|
4733
|
+
finish_run(headless_browser, result_out)
|
|
4670
4734
|
|
|
4671
4735
|
|
|
4672
4736
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|