mirror of
https://github.com/Textualize/textual-web.git
synced 2025-10-17 02:36:40 +03:00
exit on idle
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "textual_web"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Serve Textual apps"
|
||||
authors = ["Will McGugan <will@textualize.io>"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -84,6 +84,14 @@ def print_disclaimer() -> None:
|
||||
@click.option(
|
||||
"-t", "--terminal", is_flag=True, help="Publish a remote terminal on a random URL."
|
||||
)
|
||||
@click.option(
|
||||
"-x",
|
||||
"--exit-on-idle",
|
||||
type=int,
|
||||
metavar="WAIT",
|
||||
default=0,
|
||||
help="Exit textual-web when no apps have been launched in WAIT seconds",
|
||||
)
|
||||
@click.option("-s", "--signup", is_flag=True, help="Create a textual-web account.")
|
||||
@click.option("--welcome", is_flag=True, help="Launch an example app.")
|
||||
@click.option("--merlin", is_flag=True, help="Launch Merlin game.")
|
||||
@@ -93,23 +101,23 @@ def app(
|
||||
run: list[str],
|
||||
dev: bool,
|
||||
terminal: bool,
|
||||
exit_on_idle: int,
|
||||
api_key: str,
|
||||
signup: bool,
|
||||
welcome: bool,
|
||||
merlin: bool,
|
||||
) -> None:
|
||||
"""Main entry point for the CLI.
|
||||
"""Textual-web can server Textual apps and terminals."""
|
||||
|
||||
Args:
|
||||
config: Path to config.
|
||||
environment: environment switch.
|
||||
devtools: Enable devtools.
|
||||
terminal: Enable a terminal.
|
||||
api_key: API key.
|
||||
signup: Signup dialog.
|
||||
welcome: Welcome app.
|
||||
merlin: Merlin app.
|
||||
"""
|
||||
# Args:
|
||||
# config: Path to config.
|
||||
# environment: environment switch.
|
||||
# devtools: Enable devtools.
|
||||
# terminal: Enable a terminal.
|
||||
# api_key: API key.
|
||||
# signup: Signup dialog.
|
||||
# welcome: Welcome app.
|
||||
# merlin: Merlin app.
|
||||
|
||||
error_console = Console(stderr=True)
|
||||
from .config import load_config, default_config
|
||||
@@ -176,6 +184,7 @@ def app(
|
||||
_environment,
|
||||
api_key=api_key or None,
|
||||
devtools=dev,
|
||||
exit_on_idle=exit_on_idle,
|
||||
)
|
||||
|
||||
for app_command in run:
|
||||
|
||||
53
src/textual_web/exit_poller.py
Normal file
53
src/textual_web/exit_poller.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from time import monotonic
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
EXIT_POLL_RATE = 15
|
||||
|
||||
log = logging.getLogger("textual-web")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ganglion_client import GanglionClient
|
||||
|
||||
|
||||
class ExitPoller:
|
||||
"""Monitors the client for an idle state, and exits."""
|
||||
|
||||
def __init__(self, client: GanglionClient, idle_wait: int) -> None:
|
||||
self.client = client
|
||||
self.idle_wait = idle_wait
|
||||
self._task: asyncio.Task | None = None
|
||||
self._idle_start_time: float | None = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start polling."""
|
||||
self._task = asyncio.create_task(self.run())
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop polling"""
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the poller."""
|
||||
if not self.idle_wait:
|
||||
return
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(EXIT_POLL_RATE)
|
||||
is_idle = not self.client.session_manager.sessions
|
||||
if is_idle:
|
||||
if self._idle_start_time is not None:
|
||||
if monotonic() - self._idle_start_time > self.idle_wait:
|
||||
log.info("Exiting due to --exit-on-idle")
|
||||
self.client.force_exit()
|
||||
else:
|
||||
self._idle_start_time = monotonic()
|
||||
else:
|
||||
self._idle_start_time = None
|
||||
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
@@ -14,6 +14,7 @@ from aiohttp.client_exceptions import WSServerHandshakeError
|
||||
|
||||
from . import constants, packets
|
||||
from .environment import Environment
|
||||
from .exit_poller import ExitPoller
|
||||
from .identity import generate
|
||||
from .packets import (
|
||||
PACKET_MAP,
|
||||
@@ -76,9 +77,11 @@ class GanglionClient(Handlers):
|
||||
environment: Environment,
|
||||
api_key: str | None,
|
||||
devtools: bool = False,
|
||||
exit_on_idle: int = 0,
|
||||
) -> None:
|
||||
self.environment = environment
|
||||
self.websocket_url = environment.url
|
||||
self.exit_on_idle = exit_on_idle
|
||||
|
||||
abs_path = Path(config_path).absolute()
|
||||
path = abs_path if abs_path.is_dir() else abs_path.parent
|
||||
@@ -90,6 +93,7 @@ class GanglionClient(Handlers):
|
||||
self.session_manager = SessionManager(self._poller, path, config.apps)
|
||||
self.exit_event = asyncio.Event()
|
||||
self._task: asyncio.Task | None = None
|
||||
self._exit_poller = ExitPoller(self, exit_on_idle)
|
||||
|
||||
@property
|
||||
def app_count(self) -> int:
|
||||
@@ -162,8 +166,10 @@ class GanglionClient(Handlers):
|
||||
async def run(self) -> None:
|
||||
"""Run the connection loop."""
|
||||
try:
|
||||
self._exit_poller.start()
|
||||
await self._run()
|
||||
finally:
|
||||
self._exit_poller.stop()
|
||||
# Shut down the poller thread
|
||||
if not WINDOWS:
|
||||
try:
|
||||
@@ -198,6 +204,12 @@ class GanglionClient(Handlers):
|
||||
self._task = asyncio.create_task(self.connect())
|
||||
await self._task
|
||||
|
||||
def force_exit(self) -> None:
|
||||
"""Force the app to exit."""
|
||||
self.exit_event.set()
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to the Ganglion server."""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user