Added more diagnostics to the native world fixer and py logs system.

This commit is contained in:
2026-08-28 20:56:26 +02:00
parent b344506071
commit 010fe14ab4
6 changed files with 590 additions and 37 deletions

View File

@@ -2,6 +2,7 @@
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
@@ -70,12 +71,131 @@ typedef struct WorldFixInstallation
} WorldFixInstallation;
typedef struct StatusLog
{
HANDLE handle;
} StatusLog;
static void initialize_status_log(StatusLog *status)
{
status->handle = INVALID_HANDLE_VALUE;
}
static BOOL status_path_is_allowed(const wchar_t *path)
{
static const wchar_t prefix[] = L"save\\launcher\\ptworld-";
static const wchar_t suffix[] = L".jsonl";
SIZE_T path_length;
SIZE_T prefix_length = (sizeof(prefix) / sizeof(prefix[0])) - 1;
SIZE_T suffix_length = (sizeof(suffix) / sizeof(suffix[0])) - 1;
if (path == NULL)
{
return FALSE;
}
path_length = wcslen(path);
if (
path_length <= prefix_length + suffix_length
|| path_length >= MAX_PATH
|| wcsncmp(path, prefix, prefix_length) != 0
|| wcscmp(path + path_length - suffix_length, suffix) != 0
)
{
return FALSE;
}
return wcschr(path, L'/') == NULL
&& wcschr(path, L':') == NULL
&& wcsstr(path, L"..") == NULL;
}
static BOOL open_status_log(StatusLog *status, const wchar_t *path)
{
if (!status_path_is_allowed(path))
{
return FALSE;
}
status->handle = CreateFileW(
path,
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL
);
return status->handle != INVALID_HANDLE_VALUE;
}
static void write_status_event(
StatusLog *status,
const char *event,
const char *detail_format,
...
)
{
char details[512];
char line[768];
int line_length;
DWORD written = 0;
va_list arguments;
if (status->handle == INVALID_HANDLE_VALUE)
{
return;
}
details[0] = '\0';
if (detail_format != NULL)
{
va_start(arguments, detail_format);
vsnprintf(details, sizeof(details), detail_format, arguments);
va_end(arguments);
details[sizeof(details) - 1] = '\0';
}
line_length = snprintf(
line,
sizeof(line),
"{\"protocol\":1,\"event\":\"%s\",\"tick_ms\":%llu%s}\r\n",
event,
(unsigned long long)GetTickCount64(),
details
);
if (line_length <= 0 || (SIZE_T)line_length >= sizeof(line))
{
return;
}
WriteFile(status->handle, line, (DWORD)line_length, &written, NULL);
FlushFileBuffers(status->handle);
}
static void close_status_log(StatusLog *status)
{
if (status->handle != INVALID_HANDLE_VALUE)
{
CloseHandle(status->handle);
status->handle = INVALID_HANDLE_VALUE;
}
}
static DWORD address_from_rva(DWORD base, DWORD rva)
{
return base + rva;
}
static DWORD image_dword(const BYTE *image, SIZE_T offset)
{
DWORD value;
memcpy(&value, image + offset, sizeof(value));
return value;
}
static BOOL read_exact(HANDLE process, DWORD address, void *buffer, SIZE_T size)
{
SIZE_T read_size = 0;
@@ -188,7 +308,7 @@ static void emit_set_state(CodeBuffer *code, DWORD state_address, BYTE value)
}
static void emit_restore_iat(
static SIZE_T emit_restore_iat(
CodeBuffer *code,
DWORD iat_address,
DWORD original_lstrcmp,
@@ -197,6 +317,17 @@ static void emit_restore_iat(
DWORD temporary_protect_address
)
{
SIZE_T protect_failure_jump;
/* The import table is in publisher .rdata. Make it writable before MOV. */
emit_push_immediate(code, temporary_protect_address);
emit_push_immediate(code, PAGE_READWRITE);
emit_push_immediate(code, sizeof(DWORD));
emit_push_immediate(code, iat_address);
emit_call_iat(code, virtual_protect_iat);
emit_bytes(code, (const BYTE[]){0x85, 0xc0}, 2);
protect_failure_jump = emit_near_jump_placeholder(code, 0x84); /* je finish */
/* mov dword ptr [iat_address], original_lstrcmp */
emit_byte(code, 0xc7);
emit_byte(code, 0x05);
@@ -208,6 +339,7 @@ static void emit_restore_iat(
emit_push_immediate(code, sizeof(DWORD));
emit_push_immediate(code, iat_address);
emit_call_iat(code, virtual_protect_iat);
return protect_failure_jump;
}
@@ -249,6 +381,8 @@ static BOOL build_remote_hook(
SIZE_T already_triggered_jump;
SIZE_T cave_failure_jump;
SIZE_T entry_failure_jump;
SIZE_T success_restore_failure_jump;
SIZE_T failure_restore_failure_jump;
SIZE_T success_jump;
SIZE_T finish_offset;
SIZE_T entry_failure_offset;
@@ -343,7 +477,8 @@ static BOOL build_remote_hook(
emit_push_immediate(&code, code_cave);
emit_call_iat(&code, virtual_protect_iat);
emit_restore_iat(
emit_set_state(&code, state_address, 2);
success_restore_failure_jump = emit_restore_iat(
&code,
lstrcmp_iat,
original_lstrcmp,
@@ -351,7 +486,6 @@ static BOOL build_remote_hook(
original_iat_protect,
temporary_protect
);
emit_set_state(&code, state_address, 2);
success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */
entry_failure_offset = code.length;
@@ -363,7 +497,8 @@ static BOOL build_remote_hook(
emit_call_iat(&code, virtual_protect_iat);
failure_offset = code.length;
emit_restore_iat(
emit_set_state(&code, state_address, 3);
failure_restore_failure_jump = emit_restore_iat(
&code,
lstrcmp_iat,
original_lstrcmp,
@@ -371,7 +506,6 @@ static BOOL build_remote_hook(
original_iat_protect,
temporary_protect
);
emit_set_state(&code, state_address, 3);
success_offset = code.length;
emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */
@@ -383,6 +517,8 @@ static BOOL build_remote_hook(
patch_near_jump(&code, already_triggered_jump, success_offset);
patch_near_jump(&code, cave_failure_jump, failure_offset);
patch_near_jump(&code, entry_failure_jump, entry_failure_offset);
patch_near_jump(&code, success_restore_failure_jump, success_offset);
patch_near_jump(&code, failure_restore_failure_jump, success_offset);
patch_near_jump(&code, success_jump, success_offset);
if (!code.valid)
@@ -394,6 +530,64 @@ static BOOL build_remote_hook(
}
static BOOL remote_iat_restore_is_safe(
const BYTE *image,
SIZE_T code_size,
DWORD game_dll_base,
DWORD original_lstrcmp
)
{
DWORD iat_address = address_from_rva(
game_dll_base,
GAME_DLL_LSTRCMPA_IAT_RVA
);
DWORD virtual_protect_iat = address_from_rva(
game_dll_base,
GAME_DLL_VIRTUAL_PROTECT_IAT_RVA
);
SIZE_T restore_count = 0;
SIZE_T index;
for (index = 0; index + 10 <= code_size; ++index)
{
if (
image[index] != 0xc7
|| image[index + 1] != 0x05
|| image_dword(image, index + 2) != iat_address
|| image_dword(image, index + 6) != original_lstrcmp
)
{
continue;
}
++restore_count;
/*
* Immediately before every IAT MOV, require:
* VirtualProtect(iat, 4, PAGE_READWRITE, ...), TEST, JE.
*/
if (
index < 29
|| image[index - 29] != 0x68
|| image_dword(image, index - 28) != PAGE_READWRITE
|| image[index - 24] != 0x68
|| image_dword(image, index - 23) != sizeof(DWORD)
|| image[index - 19] != 0x68
|| image_dword(image, index - 18) != iat_address
|| image[index - 14] != 0xff
|| image[index - 13] != 0x15
|| image_dword(image, index - 12) != virtual_protect_iat
|| image[index - 8] != 0x85
|| image[index - 7] != 0xc0
|| image[index - 6] != 0x0f
|| image[index - 5] != 0x84
)
{
return FALSE;
}
}
return restore_count == 2;
}
static DWORD find_game_dll(DWORD process_id, DWORD *module_size)
{
HANDLE snapshot;
@@ -647,18 +841,78 @@ static BOOL install_world_fix(
}
static void report_login_accepted(StatusLog *status, BOOL *reported)
{
if (!*reported)
{
write_status_event(status, "login_accepted", NULL);
*reported = TRUE;
}
}
static BOOL report_world_fix_state(
StatusLog *status,
BYTE state,
BOOL *login_reported
)
{
if (state >= 1 && state <= 3)
{
report_login_accepted(status, login_reported);
}
if (state == 2)
{
fprintf(
stderr,
"PTWORLD: login accepted; world-transition fix activated in memory.\n"
);
write_status_event(status, "world_fix_activated", NULL);
return TRUE;
}
if (state == 3)
{
fprintf(
stderr,
"PTWORLD: login succeeded, but the verified memory fix could not be activated.\n"
);
write_status_event(status, "world_fix_activation_failed", NULL);
return TRUE;
}
return FALSE;
}
static DWORD wait_for_game_and_report(
HANDLE process,
const WorldFixInstallation *installation
const WorldFixInstallation *installation,
StatusLog *status
)
{
BOOL reported = FALSE;
BOOL login_reported = FALSE;
for (;;)
{
DWORD wait_result = WaitForSingleObject(process, 100);
if (wait_result == WAIT_OBJECT_0)
{
DWORD exit_code = 1;
if (!reported && installation->remote_state != 0)
{
BYTE state = 0;
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
{
reported = report_world_fix_state(
status,
state,
&login_reported
);
}
}
if (!reported && installation->remote_state != 0)
{
write_status_event(status, "world_fix_not_activated", NULL);
}
GetExitCodeProcess(process, &exit_code);
return exit_code;
}
@@ -667,22 +921,11 @@ static DWORD wait_for_game_and_report(
BYTE state = 0;
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
{
if (state == 2)
{
fprintf(
stderr,
"PTWORLD: login accepted; world-transition fix activated in memory.\n"
reported = report_world_fix_state(
status,
state,
&login_reported
);
reported = TRUE;
}
else if (state == 3)
{
fprintf(
stderr,
"PTWORLD: login succeeded, but the verified memory fix could not be activated.\n"
);
reported = TRUE;
}
}
}
}
@@ -706,6 +949,19 @@ static int run_self_test(void)
fprintf(stderr, "PTWORLD-SELFTEST: remote hook construction failed.\n");
return 1;
}
if (!remote_iat_restore_is_safe(
image,
code_size,
0x10000000u,
0x7b621000u
))
{
fprintf(
stderr,
"PTWORLD-SELFTEST: import-table restore is not write-safe.\n"
);
return 1;
}
if (memcmp(
image + REMOTE_RECLAIM_OFFSET,
PROVEN_RECLAIM_HOOK,
@@ -733,6 +989,8 @@ int wmain(int argument_count, wchar_t **arguments)
STARTUPINFOW startup;
PROCESS_INFORMATION process;
WorldFixInstallation installation;
StatusLog status;
const wchar_t *status_path = NULL;
WCHAR game_path[MAX_PATH];
WCHAR command_line[MAX_PATH + 64];
DWORD game_dll_base;
@@ -747,6 +1005,33 @@ int wmain(int argument_count, wchar_t **arguments)
{
return run_self_test();
}
initialize_status_log(&status);
if (
argument_count == 3
&& wcscmp(arguments[1], L"--status-file") == 0
)
{
status_path = arguments[2];
if (!open_status_log(&status, status_path))
{
fprintf(
stderr,
"PTWORLD: could not create the per-run status file (Windows error %lu).\n",
(unsigned long)GetLastError()
);
}
}
else if (argument_count != 1)
{
fprintf(stderr, "PTWORLD: invalid helper arguments.\n");
return 111;
}
write_status_event(
&status,
"helper_started",
",\"process_id\":%lu",
(unsigned long)GetCurrentProcessId()
);
memset(&startup, 0, sizeof(startup));
memset(&process, 0, sizeof(process));
memset(&installation, 0, sizeof(installation));
@@ -755,6 +1040,8 @@ int wmain(int argument_count, wchar_t **arguments)
if (path_length == 0 || path_length >= MAX_PATH)
{
fprintf(stderr, "PTWORLD: Game.exe is not available in the working directory.\n");
write_status_event(&status, "game_path_failed", NULL);
close_status_log(&status);
return 111;
}
if (swprintf(
@@ -765,6 +1052,8 @@ int wmain(int argument_count, wchar_t **arguments)
) < 0)
{
fprintf(stderr, "PTWORLD: could not construct the game command line.\n");
write_status_event(&status, "game_command_failed", NULL);
close_status_log(&status);
return 111;
}
if (!CreateProcessW(
@@ -780,26 +1069,51 @@ int wmain(int argument_count, wchar_t **arguments)
&process
))
{
DWORD error = GetLastError();
fprintf(
stderr,
"PTWORLD: could not start Game.exe (Windows error %lu).\n",
(unsigned long)GetLastError()
(unsigned long)error
);
write_status_event(
&status,
"game_start_failed",
",\"windows_error\":%lu",
(unsigned long)error
);
close_status_log(&status);
return 111;
}
CloseHandle(process.hThread);
write_status_event(
&status,
"game_started",
",\"process_id\":%lu",
(unsigned long)process.dwProcessId
);
game_dll_base = wait_for_game_dll(
process.hProcess,
process.dwProcessId,
&game_dll_size
);
if (game_dll_base != 0)
{
write_status_event(
&status,
"game_dll_found",
",\"base\":%lu,\"size\":%lu",
(unsigned long)game_dll_base,
(unsigned long)game_dll_size
);
}
if (game_dll_base == 0)
{
fprintf(
stderr,
"PTWORLD: game.dll was not available for verification; continuing pristine.\n"
);
write_status_event(&status, "game_dll_unavailable", NULL);
}
else if (!verify_game_dll_memory(process.hProcess, game_dll_base, game_dll_size))
{
@@ -807,13 +1121,34 @@ int wmain(int argument_count, wchar_t **arguments)
stderr,
"PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n"
);
write_status_event(
&status,
"game_dll_rejected",
",\"base\":%lu,\"size\":%lu",
(unsigned long)game_dll_base,
(unsigned long)game_dll_size
);
}
else if (!install_world_fix(process.hProcess, game_dll_base, &installation))
else
{
write_status_event(
&status,
"game_dll_verified",
NULL
);
if (!install_world_fix(process.hProcess, game_dll_base, &installation))
{
DWORD error = GetLastError();
fprintf(
stderr,
"PTWORLD: the post-login hook could not be armed; continuing pristine.\n"
);
write_status_event(
&status,
"hook_arm_failed",
",\"windows_error\":%lu",
(unsigned long)error
);
}
else
{
@@ -821,9 +1156,24 @@ int wmain(int argument_count, wchar_t **arguments)
stderr,
"PTWORLD: publisher game.dll verified; post-login world fix armed.\n"
);
write_status_event(
&status,
"hook_armed",
",\"base\":%lu,\"size\":%lu",
(unsigned long)game_dll_base,
(unsigned long)game_dll_size
);
}
}
exit_code = wait_for_game_and_report(process.hProcess, &installation);
exit_code = wait_for_game_and_report(process.hProcess, &installation, &status);
write_status_event(
&status,
"game_exited",
",\"exit_code\":%lu",
(unsigned long)exit_code
);
CloseHandle(process.hProcess);
close_status_log(&status);
return (int)exit_code;
}

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"]
__version__ = "0.5.9"
__version__ = "0.5.10"

View File

@@ -49,6 +49,10 @@ from .self_update import (
rollback_launcher_update,
update_check_to_dict,
)
from .world_fix import (
create_world_fix_status_target,
read_world_fix_status,
)
def build_parser() -> argparse.ArgumentParser:
@@ -650,6 +654,12 @@ def _play_locked(
patterns=("log_*.txt",),
)
game_started_ns = time.time_ns()
world_fix_status_target = None
if not dry_run and not publisher_dll:
world_fix_status_target = create_world_fix_status_target(
paths.game_save,
game_started_ns,
)
try:
outcome = run_game(
paths,
@@ -658,6 +668,7 @@ def _play_locked(
runner,
use_gamebox=not no_box,
use_world_fix=not publisher_dll,
world_fix_status_target=world_fix_status_target,
)
except KeyboardInterrupt:
current_game_log = _current_game_log(
@@ -667,12 +678,26 @@ def _play_locked(
)
if current_game_log is not None and not as_json:
_print_interrupted_game_log(current_game_log)
world_fix_status = read_world_fix_status(
world_fix_status_target,
game_started_ns,
)
if world_fix_status is not None and not as_json:
print(
f"World-fix status: {str(world_fix_status['status'])} "
f"({str(world_fix_status['path'])})",
file=sys.stderr,
)
raise
current_game_log = _current_game_log(
native_log_directory,
native_log_baseline,
game_started_ns,
)
world_fix_status = read_world_fix_status(
world_fix_status_target,
game_started_ns,
)
_emit(
{
"ok": outcome.returncode == 0,
@@ -682,6 +707,7 @@ def _play_locked(
"setup_commands": [asdict(result) for result in setup_results],
"game": asdict(outcome),
"current_game_log": current_game_log,
"world_fix": world_fix_status,
},
as_json,
)
@@ -712,7 +738,17 @@ def _current_game_log(
"import_scene_threw": analysis.import_scene_threw,
"failed_import_scenes": analysis.failed_import_scenes,
"stage_without_new_stage": analysis.stage_without_new_stage,
"stage_without_new_stage_events": [
event.to_dict()
for event in analysis.events
if event.kind == "stage_without_new_stage"
],
"scene_imports": {
name: [scene.to_dict() for scene in imports]
for name, imports in sorted(analysis.scenes.items())
},
"failed_file_count": len(analysis.failed_files),
"failed_files": [event.to_dict() for event in analysis.failed_files[:64]],
}

Binary file not shown.

View File

@@ -16,7 +16,11 @@ from .command import CommandOutcome, CommandRunner, OutputLineFilter
from .gamebox import build_gamebox_command
from .paths import AppPaths
from .settings import Settings
from .world_fix import WorldFixError, resolve_world_fix_helper
from .world_fix import (
WorldFixError,
WorldFixStatusTarget,
resolve_world_fix_helper,
)
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
@@ -252,6 +256,7 @@ def run_game(
*,
use_gamebox: bool = True,
use_world_fix: bool = False,
world_fix_status_target: WorldFixStatusTarget | None = None,
timeout_seconds: float = 12 * 60 * 60,
check: bool = True,
extra_environment: Mapping[str, str] | None = None,
@@ -273,6 +278,10 @@ def run_game(
]
if world_fix_helper is None:
game_command.append("PTEU_REBORN")
elif world_fix_status_target is not None:
game_command.extend(
["--status-file", world_fix_status_target.windows_path]
)
environment = _umu_environment(paths)
elif selection.kind == "proton7-container":
if not selection.proton_path:
@@ -283,6 +292,10 @@ def run_game(
]
if world_fix_helper is None:
game_command.append("PTEU_REBORN")
elif world_fix_status_target is not None:
game_command.extend(
["--status-file", world_fix_status_target.windows_path]
)
environment = _umu_proton7_environment(
paths,
selection,
@@ -301,6 +314,10 @@ def run_game(
]
if world_fix_helper is None:
game_command.append("PTEU_REBORN")
elif world_fix_status_target is not None:
game_command.extend(
["--status-file", world_fix_status_target.windows_path]
)
environment = _proton_environment(
paths,
selection,

View File

@@ -2,8 +2,11 @@
from __future__ import annotations
from dataclasses import dataclass
import hashlib
from pathlib import Path
import json
import os
from pathlib import Path, PureWindowsPath
V21_BINARY_HASHES = {
@@ -13,8 +16,16 @@ V21_BINARY_HASHES = {
WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
# Filled from the reproducible stripped PE after the native helper is built.
WORLD_FIX_HELPER_SHA256 = (
"d5bdf655f62dae429eff627cf8960f670ec59c12a9759dcd14342786f53e4019"
"a7a17d29ef4c11f9603790f0110a4d6823b91cce03f70e466b7e0bdbd6fa313e"
)
WORLD_FIX_STATUS_PROTOCOL = 1
WORLD_FIX_STATUS_MAX_BYTES = 64 * 1024
@dataclass(frozen=True)
class WorldFixStatusTarget:
path: Path
windows_path: str
class WorldFixError(RuntimeError):
@@ -58,6 +69,145 @@ def resolve_world_fix_helper(
return candidate
def create_world_fix_status_target(
game_save: Path,
started_ns: int,
) -> WorldFixStatusTarget:
"""Allocate a unique launcher-owned status path without pre-creating it."""
if started_ns <= 0:
raise WorldFixError("world-fix status start time must be positive")
directory = game_save / "launcher"
if directory.is_symlink():
raise WorldFixError(
"the world-fix status directory must not be a symbolic link"
)
directory.mkdir(parents=True, exist_ok=True)
if not directory.is_dir():
raise WorldFixError("the world-fix status directory is unavailable")
filename = f"ptworld-{started_ns}-{os.getpid()}.jsonl"
path = directory / filename
if path.exists() or path.is_symlink():
raise WorldFixError(f"world-fix status target already exists: {path}")
windows_path = str(PureWindowsPath("save", "launcher", filename))
return WorldFixStatusTarget(path=path, windows_path=windows_path)
def read_world_fix_status(
target: WorldFixStatusTarget | None,
started_ns: int,
) -> dict[str, object] | None:
"""Read only the bounded, fresh status stream belonging to this game run."""
if target is None:
return None
report: dict[str, object] = {
"path": str(target.path),
"status": "missing",
"fresh": False,
"events": [],
}
try:
if target.path.is_symlink() or not target.path.is_file():
return report
state = target.path.stat()
except OSError as exc:
report["status"] = "unreadable"
report["error"] = str(exc)
return report
report["modified_at_ns"] = state.st_mtime_ns
report["size"] = state.st_size
if state.st_mtime_ns < started_ns:
report["status"] = "stale"
return report
report["fresh"] = True
if state.st_size > WORLD_FIX_STATUS_MAX_BYTES:
report["status"] = "oversized"
return report
try:
lines = target.path.read_text(encoding="utf-8").splitlines()
except OSError as exc:
report["status"] = "unreadable"
report["error"] = str(exc)
return report
events: list[dict[str, object]] = []
invalid_lines = 0
for line in lines[:64]:
try:
value = json.loads(line)
except (json.JSONDecodeError, TypeError):
invalid_lines += 1
continue
clean = _validated_status_event(value)
if clean is None:
invalid_lines += 1
continue
events.append(clean)
report["events"] = events
if invalid_lines:
report["invalid_lines"] = invalid_lines
event_names = {
event["event"]
for event in events
if isinstance(event.get("event"), str)
}
for name in (
"helper_started",
"game_started",
"game_dll_verified",
"hook_armed",
"login_accepted",
"world_fix_activated",
):
report[name] = name in event_names
if "world_fix_activated" in event_names:
report["status"] = "activated"
elif "world_fix_activation_failed" in event_names:
report["status"] = "activation_failed"
elif "world_fix_not_activated" in event_names:
report["status"] = "not_activated"
elif "hook_armed" in event_names:
report["status"] = "armed"
elif event_names & {
"game_path_failed",
"game_command_failed",
"game_start_failed",
"game_dll_unavailable",
"game_dll_rejected",
"hook_arm_failed",
}:
report["status"] = "helper_failed"
elif events:
report["status"] = "started"
else:
report["status"] = "invalid"
return report
def _validated_status_event(value: object) -> dict[str, object] | None:
if not isinstance(value, dict):
return None
if value.get("protocol") != WORLD_FIX_STATUS_PROTOCOL:
return None
event = value.get("event")
tick_ms = value.get("tick_ms")
if not isinstance(event, str) or not event or len(event) > 64:
return None
if isinstance(tick_ms, bool) or not isinstance(tick_ms, int) or tick_ms < 0:
return None
clean: dict[str, object] = {
"event": event,
"tick_ms": tick_ms,
}
for key in ("process_id", "base", "size", "windows_error", "exit_code"):
item = value.get(key)
if isinstance(item, bool) or not isinstance(item, int) or item < 0:
continue
clean[key] = item
return clean
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle: