Spaces:
Running on Zero
Patch gradio_log.Log in-place for per-session tailing
Browse filesThe SessionLog subclass introduced earlier caused 404s on its
templates/component/{index.js,style.css} when deployed to HF Spaces.
Reason: gradio/routes.py custom_component_path resolves frontend
assets relative to the component's __module__ file. Subclassing in
app.py made Gradio look for <app_dir>/templates/component/index.js,
which doesn't exist, so the bundle was never served and the SPA
hung in a loading loop.
Subclassing with __module__ = "gradio_log.log" also breaks because
Gradio's component_meta generates a .pyi stub at class-definition
time via inspect.getsource(), which then can't locate the source.
Monkey-patch Log.read_to_end / Log.get_current_reading_pos on the
upstream class instead. Log.__init__ captures value=self.read_to_end
at construction, so binding takes effect for a vanilla Log() call;
no subclass, no @gr .render, no asset-path drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@@ -139,56 +139,64 @@ def _cleanup_session_log(path: str | None) -> None:
|
|
| 139 |
Path(path).unlink(missing_ok=True)
|
| 140 |
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
def _session_path(self, session_hash: str) -> Path:
|
| 152 |
-
return LOG_DIR / f"{session_hash}.log"
|
| 153 |
-
|
| 154 |
-
def _find_start_position(self, path: Path) -> int:
|
| 155 |
-
with path.open("rb") as f:
|
| 156 |
-
f.seek(0, 2)
|
| 157 |
-
file_size = f.tell()
|
| 158 |
-
lines_found = 0
|
| 159 |
-
block_size = 1024
|
| 160 |
-
blocks = []
|
| 161 |
-
while f.tell() > 0 and lines_found <= self.tail:
|
| 162 |
-
f.seek(max(f.tell() - block_size, 0))
|
| 163 |
-
block = f.read(block_size)
|
| 164 |
-
blocks.append(block)
|
| 165 |
-
lines_found += block.count(b"\n")
|
| 166 |
-
f.seek(-len(block), 1)
|
| 167 |
-
all_read_bytes = b"".join(reversed(blocks))
|
| 168 |
-
lines = all_read_bytes.splitlines()
|
| 169 |
-
if self.tail >= len(lines):
|
| 170 |
-
return 0
|
| 171 |
-
last_lines = b"\n".join(lines[-self.tail:])
|
| 172 |
-
return file_size - len(last_lines) - 1
|
| 173 |
-
|
| 174 |
-
def get_current_reading_pos(self, session_hash: str) -> int:
|
| 175 |
-
if session_hash not in self.current_reading_positions:
|
| 176 |
-
path = self._session_path(session_hash)
|
| 177 |
-
self.current_reading_positions[session_hash] = (
|
| 178 |
-
self._find_start_position(path) if path.exists() else 0
|
| 179 |
-
)
|
| 180 |
-
return self.current_reading_positions[session_hash]
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
if not path.exists():
|
| 185 |
-
return ""
|
| 186 |
-
with path.open("rb") as f:
|
| 187 |
-
current_pos = self.get_current_reading_pos(session_hash)
|
| 188 |
-
f.seek(current_pos)
|
| 189 |
-
data = f.read().decode()
|
| 190 |
-
self.current_reading_positions[session_hash] = f.tell()
|
| 191 |
-
return data
|
| 192 |
|
| 193 |
|
| 194 |
def _resolve_device() -> None:
|
|
@@ -976,7 +984,7 @@ with gr.Blocks(
|
|
| 976 |
session_log_path = gr.State(delete_callback=_cleanup_session_log)
|
| 977 |
|
| 978 |
with gr.Accordion("Application Logs", open=True):
|
| 979 |
-
|
| 980 |
|
| 981 |
|
| 982 |
gr.Markdown(""" **Links:**
|
|
|
|
| 139 |
Path(path).unlink(missing_ok=True)
|
| 140 |
|
| 141 |
|
| 142 |
+
# Per-session tailing patched onto gradio_log.Log.
|
| 143 |
+
#
|
| 144 |
+
# Why monkey-patch the upstream class instead of subclassing:
|
| 145 |
+
# 1. @gr.render rebuilds the component on every state update, which shuffles
|
| 146 |
+
# Gradio's fn_index registry and triggers KeyError: <int> in the queue.
|
| 147 |
+
# 2. Subclassing Log inside app.py changes the component's __module__, and
|
| 148 |
+
# Gradio's custom-component asset route serves templates/component/{js,css}
|
| 149 |
+
# relative to that module's directory — so the bundle 404s on HF Spaces
|
| 150 |
+
# where <app_dir>/templates/component/ doesn't exist.
|
| 151 |
+
# 3. Log.__init__ captures `value=self.read_to_end` at construction, so
|
| 152 |
+
# patching the instance after the fact wouldn't change what the timer
|
| 153 |
+
# actually polls; we have to patch the class before any Log() call.
|
| 154 |
+
def _session_path(session_hash: str) -> Path:
|
| 155 |
+
return LOG_DIR / f"{session_hash}.log"
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _session_log_find_start(self: Log, path: Path) -> int:
|
| 159 |
+
with path.open("rb") as f:
|
| 160 |
+
f.seek(0, 2)
|
| 161 |
+
file_size = f.tell()
|
| 162 |
+
lines_found = 0
|
| 163 |
+
block_size = 1024
|
| 164 |
+
blocks: list[bytes] = []
|
| 165 |
+
while f.tell() > 0 and lines_found <= self.tail:
|
| 166 |
+
f.seek(max(f.tell() - block_size, 0))
|
| 167 |
+
block = f.read(block_size)
|
| 168 |
+
blocks.append(block)
|
| 169 |
+
lines_found += block.count(b"\n")
|
| 170 |
+
f.seek(-len(block), 1)
|
| 171 |
+
lines = b"".join(reversed(blocks)).splitlines()
|
| 172 |
+
if self.tail >= len(lines):
|
| 173 |
+
return 0
|
| 174 |
+
last_lines = b"\n".join(lines[-self.tail :])
|
| 175 |
+
return file_size - len(last_lines) - 1
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _session_log_get_pos(self: Log, session_hash: str) -> int:
|
| 179 |
+
if session_hash not in self.current_reading_positions:
|
| 180 |
+
path = _session_path(session_hash)
|
| 181 |
+
self.current_reading_positions[session_hash] = (
|
| 182 |
+
_session_log_find_start(self, path) if path.exists() else 0
|
| 183 |
+
)
|
| 184 |
+
return self.current_reading_positions[session_hash]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _session_log_read_to_end(self: Log, session_hash: str) -> str:
|
| 188 |
+
path = _session_path(session_hash)
|
| 189 |
+
if not path.exists():
|
| 190 |
+
return ""
|
| 191 |
+
with path.open("rb") as f:
|
| 192 |
+
f.seek(_session_log_get_pos(self, session_hash))
|
| 193 |
+
data = f.read().decode()
|
| 194 |
+
self.current_reading_positions[session_hash] = f.tell()
|
| 195 |
+
return data
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
+
Log.get_current_reading_pos = _session_log_get_pos
|
| 199 |
+
Log.read_to_end = _session_log_read_to_end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def _resolve_device() -> None:
|
|
|
|
| 984 |
session_log_path = gr.State(delete_callback=_cleanup_session_log)
|
| 985 |
|
| 986 |
with gr.Accordion("Application Logs", open=True):
|
| 987 |
+
Log(str(_LOG_PLACEHOLDER), dark=True, height=300)
|
| 988 |
|
| 989 |
|
| 990 |
gr.Markdown(""" **Links:**
|