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.
@@ -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()