112 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-18 15:36:46 +00:00
1 parent 94867222bd
commit b1fd0abc94
112 files changed
+11348 -2701

No files matched your search

+71 -14
View File
@@ -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 {