exit on idle

This commit is contained in:
Will McGugan
2023-09-24 17:18:14 +01:00
parent 8fbd76a48f
commit 5c167d116c
4 changed files with 86 additions and 12 deletions

View File

@@ -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"

View File

@@ -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:

View 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

View File

@@ -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: