terminaide 0.0.1__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.
- terminaide-0.0.1/PKG-INFO +53 -0
- terminaide-0.0.1/README.md +30 -0
- terminaide-0.0.1/pyproject.toml +100 -0
- terminaide-0.0.1/terminaide/__init__.py +148 -0
- terminaide-0.0.1/terminaide/core/manager.py +272 -0
- terminaide-0.0.1/terminaide/core/proxy.py +281 -0
- terminaide-0.0.1/terminaide/core/settings.py +159 -0
- terminaide-0.0.1/terminaide/exceptions.py +130 -0
- terminaide-0.0.1/terminaide/installer.py +231 -0
- terminaide-0.0.1/terminaide/serve.py +259 -0
- terminaide-0.0.1/terminaide/static/favicon.ico +0 -0
- terminaide-0.0.1/terminaide/templates/terminal.html +132 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: terminaide
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Serve Python CLI applications in the browser using ttyd
|
|
5
|
+
Home-page: https://github.com/anotherbazeinthewall/terminaide
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Alex Basile
|
|
8
|
+
Author-email: basileaw@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: chatline (>=0.0.5)
|
|
15
|
+
Requires-Dist: fastapi (>=0.104.0)
|
|
16
|
+
Requires-Dist: httpx (>=0.25.0)
|
|
17
|
+
Requires-Dist: jinja2 (>=3.1.2)
|
|
18
|
+
Requires-Dist: pydantic (>=2.4.2)
|
|
19
|
+
Requires-Dist: uvicorn (>=0.24.0)
|
|
20
|
+
Requires-Dist: websockets (>=12.0)
|
|
21
|
+
Project-URL: Repository, https://github.com/anotherbazeinthewall/terminaide
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# terminaide
|
|
25
|
+
|
|
26
|
+
A Python package for serving CLI applications in the browser using ttyd.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install terminaide
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from fastapi import FastAPI
|
|
38
|
+
from terminaide import serve_tty
|
|
39
|
+
|
|
40
|
+
app = FastAPI()
|
|
41
|
+
|
|
42
|
+
serve_tty(app, client_script="path/to/your/script.py")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Install dependencies
|
|
49
|
+
poetry install
|
|
50
|
+
|
|
51
|
+
# Run test server
|
|
52
|
+
poetry run python test-server/main.py
|
|
53
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# terminaide
|
|
2
|
+
|
|
3
|
+
A Python package for serving CLI applications in the browser using ttyd.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install terminaide
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from terminaide import serve_tty
|
|
16
|
+
|
|
17
|
+
app = FastAPI()
|
|
18
|
+
|
|
19
|
+
serve_tty(app, client_script="path/to/your/script.py")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Install dependencies
|
|
26
|
+
poetry install
|
|
27
|
+
|
|
28
|
+
# Run test server
|
|
29
|
+
poetry run python test-server/main.py
|
|
30
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "terminaide"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Serve Python CLI applications in the browser using ttyd"
|
|
5
|
+
authors = ["Alex Basile <basileaw@gmail.com>"]
|
|
6
|
+
packages = [{ include = "terminaide" }]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/anotherbazeinthewall/terminaide"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["poetry-core"]
|
|
13
|
+
build-backend = "poetry.core.masonry.api"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = "^3.12"
|
|
17
|
+
fastapi = ">=0.104.0"
|
|
18
|
+
uvicorn = ">=0.24.0"
|
|
19
|
+
jinja2 = ">=3.1.2"
|
|
20
|
+
httpx = ">=0.25.0"
|
|
21
|
+
websockets = ">=12.0"
|
|
22
|
+
pydantic = ">=2.4.2"
|
|
23
|
+
chatline = ">=0.0.5"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.group.test-server.dependencies]
|
|
26
|
+
bs4 = ">=0.0.2"
|
|
27
|
+
requests = ">=2.32.3"
|
|
28
|
+
rich = "*"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
|
31
|
+
pyyaml = "*"
|
|
32
|
+
boto3 = "*"
|
|
33
|
+
botocore = "*"
|
|
34
|
+
|
|
35
|
+
[tool.poe.tasks]
|
|
36
|
+
serve-local = "python example/server.py"
|
|
37
|
+
serve-container = "docker compose up --build"
|
|
38
|
+
|
|
39
|
+
# Publish scripts
|
|
40
|
+
check-changes = "bash -c 'if [ ! -z \"$(git status --porcelain)\" ]; then echo \"Error: You have uncommitted changes.\"; exit 1; fi'"
|
|
41
|
+
|
|
42
|
+
publish-patch = """
|
|
43
|
+
bash -c '
|
|
44
|
+
poe check-changes && \
|
|
45
|
+
git pull origin main && \
|
|
46
|
+
VERSION=$(poetry version -s) && \
|
|
47
|
+
if git rev-parse "v$VERSION" >/dev/null 2>&1; then \
|
|
48
|
+
echo "Error: Tag v$VERSION already exists" && exit 1; \
|
|
49
|
+
fi && \
|
|
50
|
+
poetry version patch && \
|
|
51
|
+
VERSION=$(poetry version -s) && \
|
|
52
|
+
poetry build && \
|
|
53
|
+
poetry publish && \
|
|
54
|
+
git add pyproject.toml && \
|
|
55
|
+
git commit -m "release v$VERSION" && \
|
|
56
|
+
git tag v$VERSION && \
|
|
57
|
+
git push origin main && \
|
|
58
|
+
git push origin --tags
|
|
59
|
+
'
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
publish-minor = """
|
|
63
|
+
bash -c '
|
|
64
|
+
poe check-changes && \
|
|
65
|
+
git pull origin main && \
|
|
66
|
+
VERSION=$(poetry version -s) && \
|
|
67
|
+
if git rev-parse "v$VERSION" >/dev/null 2>&1; then \
|
|
68
|
+
echo "Error: Tag v$VERSION already exists" && exit 1; \
|
|
69
|
+
fi && \
|
|
70
|
+
poetry version minor && \
|
|
71
|
+
VERSION=$(poetry version -s) && \
|
|
72
|
+
poetry build && \
|
|
73
|
+
poetry publish && \
|
|
74
|
+
git add pyproject.toml && \
|
|
75
|
+
git commit -m "release v$VERSION" && \
|
|
76
|
+
git tag v$VERSION && \
|
|
77
|
+
git push origin main && \
|
|
78
|
+
git push origin --tags
|
|
79
|
+
'
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
publish-major = """
|
|
83
|
+
bash -c '
|
|
84
|
+
poe check-changes && \
|
|
85
|
+
git pull origin main && \
|
|
86
|
+
VERSION=$(poetry version -s) && \
|
|
87
|
+
if git rev-parse "v$VERSION" >/dev/null 2>&1; then \
|
|
88
|
+
echo "Error: Tag v$VERSION already exists" && exit 1; \
|
|
89
|
+
fi && \
|
|
90
|
+
poetry version major && \
|
|
91
|
+
VERSION=$(poetry version -s) && \
|
|
92
|
+
poetry build && \
|
|
93
|
+
poetry publish && \
|
|
94
|
+
git add pyproject.toml && \
|
|
95
|
+
git commit -m "release v$VERSION" && \
|
|
96
|
+
git tag v$VERSION && \
|
|
97
|
+
git push origin main && \
|
|
98
|
+
git push origin --tags
|
|
99
|
+
'
|
|
100
|
+
"""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# terminaide/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
terminaide: Serve Python CLI applications in the browser using ttyd.
|
|
5
|
+
|
|
6
|
+
This package provides tools to easily serve Python CLI applications through
|
|
7
|
+
a browser-based terminal using ttyd. It handles binary installation and
|
|
8
|
+
management automatically across supported platforms.
|
|
9
|
+
|
|
10
|
+
Supported Platforms:
|
|
11
|
+
- Linux x86_64 (Docker containers)
|
|
12
|
+
- macOS ARM64 (Apple Silicon)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from fastapi import FastAPI
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, Dict, Any, Union
|
|
19
|
+
|
|
20
|
+
# Configure package-level logging
|
|
21
|
+
logging.getLogger("terminaide").addHandler(logging.NullHandler())
|
|
22
|
+
|
|
23
|
+
# Core functionality
|
|
24
|
+
from .core.settings import TTYDConfig
|
|
25
|
+
from .serve import serve_tty, _configure_app
|
|
26
|
+
|
|
27
|
+
# Installation management
|
|
28
|
+
from .installer import setup_ttyd, get_platform_info
|
|
29
|
+
|
|
30
|
+
# Expose all exceptions
|
|
31
|
+
from .exceptions import (
|
|
32
|
+
terminaideError,
|
|
33
|
+
BinaryError,
|
|
34
|
+
InstallationError,
|
|
35
|
+
PlatformNotSupportedError,
|
|
36
|
+
DependencyError,
|
|
37
|
+
DownloadError,
|
|
38
|
+
TTYDStartupError,
|
|
39
|
+
TTYDProcessError,
|
|
40
|
+
ClientScriptError,
|
|
41
|
+
TemplateError,
|
|
42
|
+
ProxyError,
|
|
43
|
+
ConfigurationError
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "0.2.0" # Updated version number
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Main functionality
|
|
49
|
+
"serve_tty",
|
|
50
|
+
"TTYDConfig",
|
|
51
|
+
|
|
52
|
+
# Binary management
|
|
53
|
+
"setup_ttyd",
|
|
54
|
+
"get_platform_info",
|
|
55
|
+
|
|
56
|
+
# Exceptions
|
|
57
|
+
"terminaideError",
|
|
58
|
+
"BinaryError",
|
|
59
|
+
"InstallationError",
|
|
60
|
+
"PlatformNotSupportedError",
|
|
61
|
+
"DependencyError",
|
|
62
|
+
"DownloadError",
|
|
63
|
+
"TTYDStartupError",
|
|
64
|
+
"TTYDProcessError",
|
|
65
|
+
"ClientScriptError",
|
|
66
|
+
"TemplateError",
|
|
67
|
+
"ProxyError",
|
|
68
|
+
"ConfigurationError"
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Type aliases for better documentation
|
|
72
|
+
ThemeConfig = Dict[str, str]
|
|
73
|
+
TTYDOptions = Dict[str, Any]
|
|
74
|
+
|
|
75
|
+
def serve_tty(
|
|
76
|
+
app: FastAPI,
|
|
77
|
+
client_script: Union[str, Path],
|
|
78
|
+
*,
|
|
79
|
+
mount_path: str = "/tty",
|
|
80
|
+
port: int = 7681,
|
|
81
|
+
theme: Optional[ThemeConfig] = None,
|
|
82
|
+
ttyd_options: Optional[TTYDOptions] = None,
|
|
83
|
+
template_override: Optional[Union[str, Path]] = None,
|
|
84
|
+
debug: bool = False
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Configure FastAPI application to serve a Python script through a browser-based terminal.
|
|
88
|
+
|
|
89
|
+
This function automatically handles ttyd binary installation and setup for the
|
|
90
|
+
current platform. Supported platforms are Linux x86_64 (for Docker) and
|
|
91
|
+
macOS ARM64 (Apple Silicon).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
app: FastAPI application instance
|
|
95
|
+
client_script: Path to Python script to run in terminal
|
|
96
|
+
mount_path: URL path to mount terminal (default: "/tty")
|
|
97
|
+
port: Port for ttyd process (default: 7681)
|
|
98
|
+
theme: Terminal theme configuration (default: {"background": "black"})
|
|
99
|
+
ttyd_options: Additional ttyd process options
|
|
100
|
+
template_override: Custom HTML template path
|
|
101
|
+
debug: Enable development mode with auto-reload (default: False)
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
InstallationError: If ttyd binary installation fails
|
|
105
|
+
PlatformNotSupportedError: If running on an unsupported platform
|
|
106
|
+
DependencyError: If required system libraries are missing
|
|
107
|
+
TTYDStartupError: If ttyd fails to start
|
|
108
|
+
ClientScriptError: If client script cannot be found or executed
|
|
109
|
+
ConfigurationError: If provided configuration values are invalid
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
```python
|
|
113
|
+
from fastapi import FastAPI
|
|
114
|
+
from terminaide import serve_tty
|
|
115
|
+
|
|
116
|
+
app = FastAPI()
|
|
117
|
+
|
|
118
|
+
# Basic usage
|
|
119
|
+
serve_tty(app, "client.py")
|
|
120
|
+
|
|
121
|
+
# Custom configuration
|
|
122
|
+
serve_tty(
|
|
123
|
+
app,
|
|
124
|
+
"client.py",
|
|
125
|
+
mount_path="/terminal",
|
|
126
|
+
theme={"background": "#1a1a1a"},
|
|
127
|
+
debug=True
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Notes:
|
|
132
|
+
- For Docker deployments, the package automatically handles ttyd installation
|
|
133
|
+
- Binary installation happens on first use and is cached for subsequent runs
|
|
134
|
+
- In Docker, required system libraries (libwebsockets, json-c) must be present
|
|
135
|
+
"""
|
|
136
|
+
# Create configuration object
|
|
137
|
+
config = TTYDConfig(
|
|
138
|
+
client_script=client_script,
|
|
139
|
+
mount_path=mount_path,
|
|
140
|
+
port=port,
|
|
141
|
+
theme=theme or {"background": "black"},
|
|
142
|
+
ttyd_options=ttyd_options or {},
|
|
143
|
+
template_override=template_override,
|
|
144
|
+
debug=debug
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Configure the application with our ttyd setup
|
|
148
|
+
_configure_app(app, config)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# terminaide/core/manager.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
TTYd process management and lifecycle control.
|
|
5
|
+
|
|
6
|
+
This module is responsible for starting, monitoring, and stopping the ttyd process.
|
|
7
|
+
It ensures proper process cleanup and provides health monitoring capabilities.
|
|
8
|
+
The manager adapts its behavior based on whether we're using root or non-root
|
|
9
|
+
mounting configurations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import signal
|
|
14
|
+
import logging
|
|
15
|
+
import subprocess
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Optional, List, Dict, Any
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
|
|
23
|
+
from ..exceptions import TTYDStartupError, TTYDProcessError
|
|
24
|
+
from ..installer import setup_ttyd
|
|
25
|
+
from .settings import TTYDConfig
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("terminaide")
|
|
28
|
+
|
|
29
|
+
class TTYDManager:
|
|
30
|
+
"""
|
|
31
|
+
Manages the lifecycle of the ttyd process.
|
|
32
|
+
|
|
33
|
+
This class handles all aspects of the ttyd process, including:
|
|
34
|
+
- Process startup and shutdown
|
|
35
|
+
- Health monitoring
|
|
36
|
+
- Resource cleanup
|
|
37
|
+
- Signal handling
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: TTYDConfig):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the TTYDManager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: TTYDConfig instance with process configuration
|
|
46
|
+
"""
|
|
47
|
+
self.config = config
|
|
48
|
+
self.process: Optional[subprocess.Popen] = None
|
|
49
|
+
self._start_time: Optional[datetime] = None
|
|
50
|
+
self._ttyd_path: Optional[Path] = None
|
|
51
|
+
self._setup_ttyd()
|
|
52
|
+
|
|
53
|
+
def _setup_ttyd(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Set up ttyd binary and verify it's ready to use.
|
|
56
|
+
|
|
57
|
+
This method handles the installation and verification of the ttyd binary,
|
|
58
|
+
using our installer module to manage platform-specific binaries.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
self._ttyd_path = setup_ttyd()
|
|
62
|
+
logger.info(f"Using ttyd binary at: {self._ttyd_path}")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Failed to set up ttyd: {e}")
|
|
65
|
+
raise TTYDStartupError(f"Failed to set up ttyd: {e}")
|
|
66
|
+
|
|
67
|
+
def _build_command(self) -> List[str]:
|
|
68
|
+
"""
|
|
69
|
+
Build the ttyd command with all necessary arguments.
|
|
70
|
+
|
|
71
|
+
This method constructs the command line arguments for ttyd based on
|
|
72
|
+
the current configuration, taking into account both root and non-root
|
|
73
|
+
mounting scenarios.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of command arguments for ttyd process
|
|
77
|
+
"""
|
|
78
|
+
if not self._ttyd_path:
|
|
79
|
+
raise TTYDStartupError("ttyd binary path not set")
|
|
80
|
+
|
|
81
|
+
cmd = [str(self._ttyd_path)]
|
|
82
|
+
|
|
83
|
+
# Basic configuration
|
|
84
|
+
cmd.extend(['-p', str(self.config.port)])
|
|
85
|
+
cmd.extend(['-i', self.config.ttyd_options.interface])
|
|
86
|
+
|
|
87
|
+
# Security settings
|
|
88
|
+
if not self.config.ttyd_options.check_origin:
|
|
89
|
+
cmd.append('--no-check-origin')
|
|
90
|
+
|
|
91
|
+
if self.config.ttyd_options.credential_required:
|
|
92
|
+
if not (self.config.ttyd_options.username and self.config.ttyd_options.password):
|
|
93
|
+
raise TTYDStartupError("Credentials required but not provided")
|
|
94
|
+
cmd.extend([
|
|
95
|
+
'-c',
|
|
96
|
+
f"{self.config.ttyd_options.username}:{self.config.ttyd_options.password}"
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
# Debug mode settings
|
|
100
|
+
if self.config.debug:
|
|
101
|
+
cmd.extend(['-d', '3']) # Maximum debug output
|
|
102
|
+
|
|
103
|
+
# Terminal customization
|
|
104
|
+
theme_json = self.config.theme.model_dump_json()
|
|
105
|
+
cmd.extend(['-t', f'theme={theme_json}'])
|
|
106
|
+
|
|
107
|
+
# Explicitly set writable or read-only mode
|
|
108
|
+
if self.config.ttyd_options.writable:
|
|
109
|
+
cmd.append('--writable')
|
|
110
|
+
else:
|
|
111
|
+
cmd.append('-R')
|
|
112
|
+
|
|
113
|
+
# Add client script to run in the terminal
|
|
114
|
+
cmd.extend([
|
|
115
|
+
'python',
|
|
116
|
+
str(self.config.client_script)
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
return cmd
|
|
120
|
+
|
|
121
|
+
def start(self) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Start the ttyd process with the current configuration.
|
|
124
|
+
|
|
125
|
+
This method launches ttyd with appropriate settings and monitors
|
|
126
|
+
its startup to ensure it's running correctly.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
TTYDStartupError: If process fails to start
|
|
130
|
+
TTYDProcessError: If process is already running
|
|
131
|
+
"""
|
|
132
|
+
if self.is_running:
|
|
133
|
+
raise TTYDProcessError("TTYd process is already running")
|
|
134
|
+
|
|
135
|
+
cmd = self._build_command()
|
|
136
|
+
cmd_str = ' '.join(cmd)
|
|
137
|
+
logger.info(f"Starting ttyd with command: {cmd_str}")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Start the process in a new session to isolate signals
|
|
141
|
+
self.process = subprocess.Popen(
|
|
142
|
+
cmd,
|
|
143
|
+
stdout=subprocess.PIPE,
|
|
144
|
+
stderr=subprocess.PIPE,
|
|
145
|
+
start_new_session=True
|
|
146
|
+
)
|
|
147
|
+
self._start_time = datetime.now()
|
|
148
|
+
|
|
149
|
+
# Monitor startup with longer timeout in debug mode
|
|
150
|
+
timeout = 4 if self.config.debug else 2
|
|
151
|
+
check_interval = 0.1
|
|
152
|
+
checks = int(timeout / check_interval)
|
|
153
|
+
|
|
154
|
+
for _ in range(checks):
|
|
155
|
+
if self.process.poll() is not None:
|
|
156
|
+
stderr = self.process.stderr.read().decode('utf-8')
|
|
157
|
+
logger.error(f"ttyd failed to start. Error: {stderr}")
|
|
158
|
+
raise TTYDStartupError(stderr=stderr)
|
|
159
|
+
|
|
160
|
+
if self.is_running:
|
|
161
|
+
mount_type = "root" if self.config.is_root_mounted else "non-root"
|
|
162
|
+
logger.info(
|
|
163
|
+
f"ttyd process started successfully with PID {self.process.pid} "
|
|
164
|
+
f"({mount_type} mounting)"
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
import time
|
|
169
|
+
time.sleep(check_interval)
|
|
170
|
+
|
|
171
|
+
logger.error("ttyd process did not start within timeout")
|
|
172
|
+
raise TTYDStartupError("ttyd process did not start within timeout")
|
|
173
|
+
|
|
174
|
+
except subprocess.SubprocessError as e:
|
|
175
|
+
logger.error(f"Failed to start ttyd: {e}")
|
|
176
|
+
raise TTYDStartupError(str(e))
|
|
177
|
+
|
|
178
|
+
def stop(self) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Stop the ttyd process if it's running.
|
|
181
|
+
|
|
182
|
+
This method ensures clean process termination using SIGTERM first,
|
|
183
|
+
followed by SIGKILL if necessary. It handles cases where the process
|
|
184
|
+
might have already been terminated.
|
|
185
|
+
"""
|
|
186
|
+
if self.process:
|
|
187
|
+
logger.info("Stopping ttyd process...")
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Try graceful shutdown first
|
|
191
|
+
if os.name == 'nt': # Windows
|
|
192
|
+
self.process.terminate()
|
|
193
|
+
else: # Unix-like
|
|
194
|
+
try:
|
|
195
|
+
pgid = os.getpgid(self.process.pid)
|
|
196
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
197
|
+
except ProcessLookupError:
|
|
198
|
+
# Process is already gone, which is fine
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
self.process.wait(timeout=5) # Wait up to 5 seconds
|
|
203
|
+
except subprocess.TimeoutExpired:
|
|
204
|
+
# Force kill if graceful shutdown fails
|
|
205
|
+
if os.name == 'nt':
|
|
206
|
+
self.process.kill()
|
|
207
|
+
else:
|
|
208
|
+
try:
|
|
209
|
+
pgid = os.getpgid(self.process.pid)
|
|
210
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
211
|
+
except ProcessLookupError:
|
|
212
|
+
# Process is already gone, which is fine
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
self.process.wait(timeout=1)
|
|
217
|
+
except subprocess.TimeoutExpired:
|
|
218
|
+
# If we still can't wait, the process is probably zombie or gone
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.warning(f"Error during process cleanup: {e}")
|
|
223
|
+
|
|
224
|
+
self.process = None
|
|
225
|
+
self._start_time = None
|
|
226
|
+
logger.info("ttyd process stopped successfully")
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def is_running(self) -> bool:
|
|
230
|
+
"""Check if ttyd process is currently running."""
|
|
231
|
+
return bool(self.process and self.process.poll() is None)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def uptime(self) -> Optional[float]:
|
|
235
|
+
"""Get process uptime in seconds, if running."""
|
|
236
|
+
if self._start_time and self.is_running:
|
|
237
|
+
return (datetime.now() - self._start_time).total_seconds()
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def check_health(self) -> Dict[str, Any]:
|
|
241
|
+
"""
|
|
242
|
+
Get comprehensive health check information about the process.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Dictionary containing process status, uptime, and configuration details
|
|
246
|
+
"""
|
|
247
|
+
return {
|
|
248
|
+
"status": "running" if self.is_running else "stopped",
|
|
249
|
+
"uptime": self.uptime,
|
|
250
|
+
"pid": self.process.pid if self.process else None,
|
|
251
|
+
"mounting": "root" if self.config.is_root_mounted else "non-root",
|
|
252
|
+
"terminal_path": self.config.terminal_path,
|
|
253
|
+
"ttyd_path": str(self._ttyd_path) if self._ttyd_path else None,
|
|
254
|
+
**self.config.get_health_check_info()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@asynccontextmanager
|
|
258
|
+
async def lifespan(self, app: FastAPI):
|
|
259
|
+
"""
|
|
260
|
+
Manage ttyd process lifecycle within FastAPI application.
|
|
261
|
+
|
|
262
|
+
This context manager ensures proper startup and cleanup of the ttyd
|
|
263
|
+
process during the application lifecycle.
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
app = FastAPI(lifespan=manager.lifespan)
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
self.start()
|
|
270
|
+
yield
|
|
271
|
+
finally:
|
|
272
|
+
self.stop()
|