tigrcorn-runtime 0.3.16.dev5__py3-none-any.whl
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.
- tigrcorn_runtime/__init__.py +20 -0
- tigrcorn_runtime/api.py +351 -0
- tigrcorn_runtime/app_interfaces.py +105 -0
- tigrcorn_runtime/cli.py +191 -0
- tigrcorn_runtime/embedded.py +77 -0
- tigrcorn_runtime/py.typed +1 -0
- tigrcorn_runtime/server/__init__.py +9 -0
- tigrcorn_runtime/server/app_loader.py +42 -0
- tigrcorn_runtime/server/bootstrap.py +225 -0
- tigrcorn_runtime/server/hooks.py +24 -0
- tigrcorn_runtime/server/reloader.py +109 -0
- tigrcorn_runtime/server/runner.py +878 -0
- tigrcorn_runtime/server/shutdown.py +12 -0
- tigrcorn_runtime/server/signals.py +33 -0
- tigrcorn_runtime/server/state.py +13 -0
- tigrcorn_runtime/server/supervisor.py +110 -0
- tigrcorn_runtime/workers/__init__.py +6 -0
- tigrcorn_runtime/workers/local.py +36 -0
- tigrcorn_runtime/workers/model.py +9 -0
- tigrcorn_runtime/workers/process.py +120 -0
- tigrcorn_runtime/workers/supervisor.py +58 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/METADATA +245 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/RECORD +26 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable
|
|
11
|
+
|
|
12
|
+
from tigrcorn_config.model import ServerConfig
|
|
13
|
+
from tigrcorn_runtime.server.hooks import run_sync_hooks
|
|
14
|
+
from tigrcorn_runtime.server.signals import install_sync_signal_handlers, restore_signal_handlers
|
|
15
|
+
|
|
16
|
+
_RELOADER_ENV = 'TIGRCORN_INTERNAL_RELOADER_CHILD'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _iter_files(roots: Iterable[str], *, include: list[str], exclude: list[str]) -> list[Path]:
|
|
20
|
+
matches: list[Path] = []
|
|
21
|
+
include = include or ['*.py']
|
|
22
|
+
for root in roots:
|
|
23
|
+
path = Path(root)
|
|
24
|
+
if path.is_file():
|
|
25
|
+
candidates = [path]
|
|
26
|
+
elif path.exists():
|
|
27
|
+
candidates = [p for p in path.rglob('*') if p.is_file()]
|
|
28
|
+
else:
|
|
29
|
+
continue
|
|
30
|
+
for candidate in candidates:
|
|
31
|
+
rel = str(candidate)
|
|
32
|
+
if exclude and any(fnmatch.fnmatch(rel, pattern) for pattern in exclude):
|
|
33
|
+
continue
|
|
34
|
+
if include and not any(fnmatch.fnmatch(candidate.name, pattern) or fnmatch.fnmatch(rel, pattern) for pattern in include):
|
|
35
|
+
continue
|
|
36
|
+
matches.append(candidate)
|
|
37
|
+
return sorted(set(matches))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class PollingReloader:
|
|
42
|
+
argv: list[str]
|
|
43
|
+
config: ServerConfig
|
|
44
|
+
interval: float = 0.5
|
|
45
|
+
child: subprocess.Popen[bytes] | None = None
|
|
46
|
+
stopping: bool = False
|
|
47
|
+
_snapshot: dict[str, float] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def is_child_process(cls) -> bool:
|
|
51
|
+
return os.environ.get(_RELOADER_ENV) == '1'
|
|
52
|
+
|
|
53
|
+
def watch_roots(self) -> list[str]:
|
|
54
|
+
roots = list(self.config.app.reload_dirs)
|
|
55
|
+
if self.config.app.app_dir:
|
|
56
|
+
roots.append(self.config.app.app_dir)
|
|
57
|
+
if not roots:
|
|
58
|
+
roots.append(os.getcwd())
|
|
59
|
+
return roots
|
|
60
|
+
|
|
61
|
+
def snapshot(self) -> dict[str, float]:
|
|
62
|
+
values: dict[str, float] = {}
|
|
63
|
+
for path in _iter_files(self.watch_roots(), include=self.config.app.reload_include, exclude=self.config.app.reload_exclude):
|
|
64
|
+
try:
|
|
65
|
+
values[str(path)] = path.stat().st_mtime_ns
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
continue
|
|
68
|
+
return values
|
|
69
|
+
|
|
70
|
+
def spawn_child(self) -> None:
|
|
71
|
+
env = os.environ.copy()
|
|
72
|
+
env[_RELOADER_ENV] = '1'
|
|
73
|
+
self.child = subprocess.Popen([sys.executable, '-m', 'tigrcorn', *self.argv], env=env)
|
|
74
|
+
|
|
75
|
+
def restart_child(self) -> None:
|
|
76
|
+
if self.config.hooks.on_reload:
|
|
77
|
+
run_sync_hooks(self.config.hooks.on_reload, self.config)
|
|
78
|
+
self.stop_child()
|
|
79
|
+
self.spawn_child()
|
|
80
|
+
|
|
81
|
+
def stop_child(self) -> None:
|
|
82
|
+
if self.child is None:
|
|
83
|
+
return
|
|
84
|
+
if self.child.poll() is None:
|
|
85
|
+
self.child.terminate()
|
|
86
|
+
try:
|
|
87
|
+
self.child.wait(timeout=max(1.0, self.config.http.shutdown_timeout))
|
|
88
|
+
except subprocess.TimeoutExpired:
|
|
89
|
+
self.child.kill()
|
|
90
|
+
self.child.wait(timeout=5.0)
|
|
91
|
+
self.child = None
|
|
92
|
+
|
|
93
|
+
def run(self) -> int:
|
|
94
|
+
self._snapshot = self.snapshot()
|
|
95
|
+
self.spawn_child()
|
|
96
|
+
previous = install_sync_signal_handlers(lambda _sig: setattr(self, 'stopping', True))
|
|
97
|
+
try:
|
|
98
|
+
while not self.stopping:
|
|
99
|
+
time.sleep(self.interval)
|
|
100
|
+
current = self.snapshot()
|
|
101
|
+
if current != self._snapshot:
|
|
102
|
+
self._snapshot = current
|
|
103
|
+
self.restart_child()
|
|
104
|
+
if self.child is not None and self.child.poll() not in (None, 0):
|
|
105
|
+
self.spawn_child()
|
|
106
|
+
return 0
|
|
107
|
+
finally:
|
|
108
|
+
restore_signal_handlers(previous)
|
|
109
|
+
self.stop_child()
|