Read chat logs incrementally and drain retention (Refs #62)

This commit is contained in:
2026-10-08 17:28:49 -04:00
parent 0cc4498b53
commit c10f602e8e
18 changed files with 646 additions and 39 deletions
+30 -2
View File
@@ -35,7 +35,7 @@ import urllib.parse
import urllib.request
import uuid
VERSION = "1.8.4"
VERSION = "1.8.5"
LABEL = "io.electric-cherry"
MAX_FILE = 32 * 1024 * 1024
MANIFEST_MAX_ENTRIES = 10000
@@ -1497,7 +1497,7 @@ class Agent:
if line.startswith("PRETTY_NAME="):
os_name = line.split("=", 1)[1].strip('"')
update = self.update_inventory()
capabilities = ["library.remove", "plutonium.cache", "instance.cancel-create", "instance.safety-override", "instance.unhealthy-recovery", "instance.autoscaling-remove", "operations.read-lane", "operations.health-lane", "rcon.status-batch", "instance.heartbeat-state", "files.manifest", "files.blobs"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
capabilities = ["library.remove", "plutonium.cache", "instance.cancel-create", "instance.safety-override", "instance.unhealthy-recovery", "instance.autoscaling-remove", "operations.read-lane", "operations.health-lane", "rcon.status-batch", "instance.heartbeat-state", "files.manifest", "files.blobs", "logs.read-offset"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
plutonium_cache = self.plutonium_cache_inventory()
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "agentLanes": self.lane_inventory(), "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, **resources, **({"cpuUsagePercent": cpu_usage} if cpu_usage is not None else {}), "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), **({"plutoniumCache": plutonium_cache} if plutonium_cache else {}), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
@@ -2231,6 +2231,8 @@ class Agent:
# swapping either the original link or a target component cannot escape.
with managed_open(app, relative) as stream:
stat = os.fstat(stream.fileno())
if payload.get("mode") == "incremental":
return self.read_log_chunk(stream, stat, payload.get("cursor"))
start = max(0, stat.st_size - 256 * 1024)
stream.seek(start)
lines = stream.read(256 * 1024).decode("utf-8", "replace").splitlines()
@@ -2238,6 +2240,32 @@ class Agent:
lines = lines[1:]
return {"lines": lines[-count:], "size": stat.st_size, "mtime": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(stat.st_mtime)), "truncated": start > 0}
@staticmethod
def read_log_chunk(stream, stat, cursor):
file_id, modified = f"{stat.st_dev}:{stat.st_ino}", f"{stat.st_mtime_ns}:{stat.st_ctime_ns}"
size = stat.st_size
mtime = datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat()
if cursor is not None and (not isinstance(cursor, dict)
or any(type(cursor.get(key)) is not int or cursor[key] < 0 for key in ("offset", "size", "anchorLength"))
or cursor["anchorLength"] > min(256, cursor["offset"])
or any(not isinstance(cursor.get(key), str) for key in ("fileId", "modified", "anchorHash"))):
raise AgentError("Invalid log cursor")
if cursor and cursor["fileId"] == file_id and cursor["modified"] == modified and cursor["size"] == size and cursor["offset"] == size:
return {"contentBase64": "", "size": size, "mtime": mtime, "truncated": size > 0, "cursor": cursor, "reset": False}
reset = not cursor or cursor["fileId"] != file_id or cursor["offset"] > size
if not reset:
stream.seek(cursor["offset"] - cursor["anchorLength"])
anchor = stream.read(cursor["anchorLength"])
reset = len(anchor) != cursor["anchorLength"] or digest(anchor) != cursor["anchorHash"]
start = max(0, size - 256 * 1024) if reset else cursor["offset"]
stream.seek(start)
content = stream.read(min(256 * 1024, size - start))
offset = start + len(content)
stream.seek(max(0, offset - 256))
anchor = stream.read(min(256, offset))
return {"contentBase64": base64.b64encode(content).decode(), "size": size, "mtime": mtime, "truncated": start > 0, "reset": reset,
"cursor": {"fileId": file_id, "modified": modified, "size": size, "offset": offset, "anchorLength": len(anchor), "anchorHash": digest(anchor)}}
def library_dir(self, game, library_id):
if game not in ("t5", "t6"):
raise AgentError("Unsupported library game")
+2 -2
View File
@@ -1,8 +1,8 @@
{
"version": "1.8.4",
"version": "1.8.5",
"protocolVersion": 1,
"platform": "debian-amd64",
"sha256": "7e7c0049161a6f64743210b787a8ec30bd32d726c30587962bc5c705d94dfbac",
"sha256": "56187a244b551af884db6bd6419daf7dd7d931fa1e5d5c5414559bb6e25f1094",
"updaterProtocolVersion": 1,
"updaterSha256": "066bb896726600ece9ce72697ce523c9a707cc9a621505931ba0892dec59d3c8"
}