112 files
This commit is contained in:
1 parent
94867222bd
commit
b1fd0abc94
112 files changed
+11348
-2701
No files matched your search
@@ -271,8 +271,15 @@ ALLOWED_READ_FILES = [
|
||||
"scenes.yaml",
|
||||
"secrets.yaml",
|
||||
"home-assistant.log",
|
||||
# faulthandler's crash dump (HA Core's ``__main__`` enables it on this file).
|
||||
# Written only on a native fatal signal, so it is the one place that
|
||||
# traceback exists: journald / error_log never see it (issue #2373).
|
||||
"home-assistant.log.fault",
|
||||
]
|
||||
|
||||
# Root-level log files that the read service tails by default.
|
||||
TAILED_LOG_FILES = frozenset({"home-assistant.log", "home-assistant.log.fault"})
|
||||
|
||||
# Default tail lines for log files
|
||||
DEFAULT_LOG_TAIL_LINES = 1000
|
||||
|
||||
@@ -1345,6 +1352,53 @@ def _read_file_sync(target_file: Path) -> dict[str, Any]:
|
||||
return {"content": content, "size": stat.st_size, "mtime": stat.st_mtime}
|
||||
|
||||
|
||||
def _read_log_tail_sync(
|
||||
target_file: Path, tail_lines: int, *, chunk_size: int = 1 << 16
|
||||
) -> dict[str, Any]:
|
||||
"""Tail a log for one executor offload without materialising the whole file.
|
||||
|
||||
HA Core appends to ``home-assistant.log.fault`` for the life of the
|
||||
install, so reading the file whole just to keep its last ``tail_lines``
|
||||
lines scales with the crash history rather than the request. Line count
|
||||
streams the file in chunks; the tail seeks back from the end until it holds
|
||||
``tail_lines`` newlines. The result carries the same ``content`` /
|
||||
``total_lines`` / ``truncated`` the whole-file split produced (``split("\n")``
|
||||
semantics, so a trailing newline yields one empty last line), plus
|
||||
``size``/``mtime`` like ``_read_file_sync``.
|
||||
"""
|
||||
if not target_file.exists():
|
||||
return {"_error": "not_found"}
|
||||
if not target_file.is_file():
|
||||
return {"_error": "not_a_file"}
|
||||
stat = target_file.stat()
|
||||
newlines = 0
|
||||
with target_file.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(chunk_size), b""):
|
||||
newlines += chunk.count(b"\n")
|
||||
total_lines = newlines + 1
|
||||
pos = fh.tell()
|
||||
buf = b""
|
||||
while pos > 0 and buf.count(b"\n") < tail_lines:
|
||||
step = min(chunk_size, pos)
|
||||
pos -= step
|
||||
fh.seek(pos)
|
||||
buf = fh.read(step) + buf
|
||||
parts = buf.split(b"\n")
|
||||
truncated = total_lines > tail_lines
|
||||
if truncated:
|
||||
# The leading part may start mid-character at a chunk boundary; it is
|
||||
# never part of the tail, so decode only what is returned.
|
||||
parts = parts[-tail_lines:]
|
||||
content = b"\n".join(parts).decode("utf-8")
|
||||
return {
|
||||
"content": content,
|
||||
"size": stat.st_size,
|
||||
"mtime": stat.st_mtime,
|
||||
"total_lines": total_lines,
|
||||
"truncated": truncated,
|
||||
}
|
||||
|
||||
|
||||
def _write_file_sync(
|
||||
target_file: Path,
|
||||
content: str,
|
||||
@@ -2772,25 +2826,19 @@ async def _shape_read_file_response(
|
||||
if normalized == "secrets.yaml":
|
||||
content = await hass.async_add_executor_job(_mask_secrets_content, content)
|
||||
|
||||
# Apply tail for log files
|
||||
if normalized == "home-assistant.log":
|
||||
lines = content.split("\n")
|
||||
# Log files arrive already tailed by ``_read_log_tail_sync``.
|
||||
if normalized in TAILED_LOG_FILES:
|
||||
limit = tail_lines if tail_lines else DEFAULT_LOG_TAIL_LINES
|
||||
if len(lines) > limit:
|
||||
content = "\n".join(lines[-limit:])
|
||||
truncated = True
|
||||
else:
|
||||
truncated = False
|
||||
|
||||
total_lines = result["total_lines"]
|
||||
return {
|
||||
"success": True,
|
||||
"path": rel_path,
|
||||
"content": content,
|
||||
"size": stat_size,
|
||||
"modified": modified_dt.isoformat(),
|
||||
"lines_returned": min(len(lines), limit),
|
||||
"total_lines": len(lines),
|
||||
"truncated": truncated,
|
||||
"lines_returned": min(total_lines, limit),
|
||||
"total_lines": total_lines,
|
||||
"truncated": result["truncated"],
|
||||
}
|
||||
|
||||
# Apply tail for other files if requested
|
||||
@@ -2853,13 +2901,22 @@ def _build_read_file_handler(
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Path not allowed. Allowed patterns: {', '.join(allowed_patterns)}",
|
||||
"error": f"Path not allowed. Allowed paths: {', '.join(allowed_patterns)}",
|
||||
}
|
||||
|
||||
target_file = config_dir / rel_path
|
||||
|
||||
try:
|
||||
result = await hass.async_add_executor_job(_read_file_sync, target_file)
|
||||
if os.path.normpath(rel_path) in TAILED_LOG_FILES: # noqa: ASYNC240
|
||||
# Bounded read: the fault log is append-only for the life of
|
||||
# the install, so never materialise the whole file for a tail.
|
||||
result = await hass.async_add_executor_job(
|
||||
_read_log_tail_sync,
|
||||
target_file,
|
||||
tail_lines if tail_lines else DEFAULT_LOG_TAIL_LINES,
|
||||
)
|
||||
else:
|
||||
result = await hass.async_add_executor_job(_read_file_sync, target_file)
|
||||
except PermissionError:
|
||||
_LOGGER.error("Permission denied reading: %s", rel_path)
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user