Improvements to try resolve the issues with login checksums.
This commit is contained in:
@@ -25,6 +25,13 @@ from .runtime import (
|
||||
)
|
||||
from .settings import Settings
|
||||
|
||||
from ptdiag.logs import (
|
||||
LogFileState,
|
||||
newest_current_native_log,
|
||||
parse_current_run_log,
|
||||
snapshot_native_logs,
|
||||
)
|
||||
|
||||
|
||||
RELAY_REGISTRY_KEY = r"HKCU\Software\Wine\Debug"
|
||||
RELAY_REGISTRY_VALUE = "RelayInclude"
|
||||
@@ -33,7 +40,7 @@ GAME_DLL_LOGIN_COMPARE_RETURN_OFFSET = 0x79F157
|
||||
DEFAULT_TRACE_TIMEOUT_SECONDS = 10 * 60.0
|
||||
|
||||
_LOAD_GAME_DLL = re.compile(
|
||||
r'Loaded L"[^"\r\n]*(?:\\|/)game\.dll" at (?P<base>[0-9a-f]+):',
|
||||
r'Loaded L"[^"\r\n]*[\\/]game\.dll" at (?P<base>[0-9a-f]+):',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_COMPARE_RETURN = re.compile(
|
||||
@@ -78,6 +85,21 @@ class LoginTraceReport:
|
||||
native_log_before_modified_at: str | None
|
||||
native_log_after: str | None
|
||||
native_log_after_modified_at: str | None
|
||||
current_game_log: str | None
|
||||
current_game_log_modified_at: str | None
|
||||
failed_file_count: int
|
||||
failed_import_scenes: tuple[str, ...]
|
||||
load_texture_to_material_failed: bool
|
||||
import_scene_threw: bool
|
||||
stage_without_new_stage: bool
|
||||
module_code_sync_error: bool
|
||||
module_code_sync_left: int | None
|
||||
module_code_sync_right: int | None
|
||||
module_code_sync_delta: int | None
|
||||
banned_code_1: bool
|
||||
runtime_futex_error: bool
|
||||
gstreamer_wrong_elf_class: bool
|
||||
gstreamer_stream_error: bool
|
||||
game_returncode: int | None
|
||||
game_timed_out: bool
|
||||
game_duration_seconds: float
|
||||
@@ -121,22 +143,41 @@ def run_login_trace(
|
||||
raise RuntimeError("the selected Proton 7 runtime path is incomplete")
|
||||
|
||||
started = datetime.now().astimezone()
|
||||
started_ns = int(started.timestamp() * 1_000_000_000)
|
||||
result_directory = _create_result_directory(paths, started)
|
||||
game_exe = paths.runtime_view / "Game.exe"
|
||||
game_dll = paths.runtime_view / "game.dll"
|
||||
log_before, log_before_modified = _native_log_snapshot(paths, settings)
|
||||
native_log_directory = paths.game_save / "logs"
|
||||
native_log_baseline = snapshot_native_logs(
|
||||
native_log_directory,
|
||||
patterns=("log_*.txt",),
|
||||
)
|
||||
registry_before = _query_relay_filter(paths, selection, runner)
|
||||
registry_restored = False
|
||||
trace = RelayTraceResult(None, None, (), 0)
|
||||
outcome: CommandOutcome | None = None
|
||||
raw_directory_path: Path | None = None
|
||||
current_log_path = None
|
||||
current_log_modified_at = None
|
||||
current_log_failure_count = 0
|
||||
current_failed_import_scenes: list[str] = []
|
||||
current_load_texture_to_material_failed = False
|
||||
current_import_scene_threw = False
|
||||
current_stage_without_new_stage = False
|
||||
current_module_code_sync_error = False
|
||||
current_module_code_sync_left = None
|
||||
current_module_code_sync_right = None
|
||||
current_module_code_sync_delta = None
|
||||
current_banned_code_1 = False
|
||||
|
||||
paths.cache.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="ptlinux-login-relay-",
|
||||
dir=paths.cache,
|
||||
) as raw_directory:
|
||||
raw_directory_path = Path(raw_directory)
|
||||
raw_path = Path(raw_directory)
|
||||
raw_directory_path = raw_path
|
||||
try:
|
||||
_set_relay_filter(paths, selection, runner, RELAY_INCLUDE_FILTER)
|
||||
outcome = run_game(
|
||||
@@ -150,14 +191,14 @@ def run_login_trace(
|
||||
check=False,
|
||||
extra_environment={
|
||||
"PROTON_LOG": "1",
|
||||
"PROTON_LOG_DIR": str(raw_directory_path),
|
||||
"PROTON_LOG_DIR": str(raw_path),
|
||||
"SteamAppId": "0",
|
||||
"SteamGameId": "0",
|
||||
"WINEDEBUG": "+relay,+loaddll",
|
||||
},
|
||||
output_line_filter=redact_relay_output_line,
|
||||
)
|
||||
trace = parse_relay_logs(raw_directory_path.rglob("*.log"))
|
||||
trace = parse_relay_logs(raw_path.rglob("*.log"))
|
||||
finally:
|
||||
registry_restored = _restore_relay_filter(
|
||||
paths,
|
||||
@@ -168,6 +209,52 @@ def run_login_trace(
|
||||
|
||||
if outcome is None:
|
||||
raise RuntimeError("the instrumented game process did not start")
|
||||
current_game_log = _current_game_log(
|
||||
native_log_directory,
|
||||
native_log_baseline,
|
||||
started_ns,
|
||||
)
|
||||
if current_game_log is not None:
|
||||
current_log_path = _string_or_none(current_game_log.get("path"))
|
||||
current_log_modified_at = _string_or_none(
|
||||
current_game_log.get("modified_at")
|
||||
)
|
||||
current_log_failure_count = _integer_or_zero(
|
||||
current_game_log.get("failed_file_count")
|
||||
)
|
||||
failed_scenes = current_game_log.get("failed_import_scenes")
|
||||
if isinstance(failed_scenes, (list, tuple)):
|
||||
current_failed_import_scenes = [
|
||||
scene for scene in failed_scenes if isinstance(scene, str)
|
||||
]
|
||||
current_load_texture_to_material_failed = bool(
|
||||
current_game_log.get("load_texture_to_material_failed")
|
||||
)
|
||||
current_import_scene_threw = bool(current_game_log.get("import_scene_threw"))
|
||||
current_stage_without_new_stage = bool(
|
||||
current_game_log.get("stage_without_new_stage")
|
||||
)
|
||||
current_module_code_sync_error = bool(
|
||||
current_game_log.get("module_code_sync_error")
|
||||
)
|
||||
current_module_code_sync_left = _integer_or_none(
|
||||
current_game_log.get("module_code_sync_left")
|
||||
)
|
||||
current_module_code_sync_right = _integer_or_none(
|
||||
current_game_log.get("module_code_sync_right")
|
||||
)
|
||||
current_module_code_sync_delta = _integer_or_none(
|
||||
current_game_log.get("module_code_sync_delta")
|
||||
)
|
||||
current_banned_code_1 = bool(current_game_log.get("banned_code_1"))
|
||||
runtime_futex_error = "futex facility returned an unexpected error code" in (
|
||||
outcome.stderr.casefold()
|
||||
)
|
||||
gstreamer_wrong_elf_class = (
|
||||
"gstreamer-warning" in outcome.stderr.casefold()
|
||||
and "wrong elf class" in outcome.stderr.casefold()
|
||||
)
|
||||
gstreamer_stream_error = "winegstreamer error" in outcome.stderr.casefold()
|
||||
raw_relay_deleted = raw_directory_path is not None and not raw_directory_path.exists()
|
||||
log_after, log_after_modified = _native_log_snapshot(paths, settings)
|
||||
comparison_observed = bool(trace.comparison_results)
|
||||
@@ -176,6 +263,9 @@ def run_login_trace(
|
||||
)
|
||||
conclusion, warnings = _conclusion(
|
||||
trace,
|
||||
module_code_sync_error=current_module_code_sync_error,
|
||||
banned_code_1=current_banned_code_1,
|
||||
runtime_futex_error=runtime_futex_error,
|
||||
registry_restored=registry_restored,
|
||||
raw_relay_deleted=raw_relay_deleted,
|
||||
timed_out=outcome.timed_out,
|
||||
@@ -200,6 +290,21 @@ def run_login_trace(
|
||||
native_log_before_modified_at=log_before_modified,
|
||||
native_log_after=log_after,
|
||||
native_log_after_modified_at=log_after_modified,
|
||||
current_game_log=current_log_path,
|
||||
current_game_log_modified_at=current_log_modified_at,
|
||||
failed_file_count=current_log_failure_count,
|
||||
failed_import_scenes=tuple(current_failed_import_scenes),
|
||||
load_texture_to_material_failed=current_load_texture_to_material_failed,
|
||||
import_scene_threw=current_import_scene_threw,
|
||||
stage_without_new_stage=current_stage_without_new_stage,
|
||||
module_code_sync_error=current_module_code_sync_error,
|
||||
module_code_sync_left=current_module_code_sync_left,
|
||||
module_code_sync_right=current_module_code_sync_right,
|
||||
module_code_sync_delta=current_module_code_sync_delta,
|
||||
banned_code_1=current_banned_code_1,
|
||||
runtime_futex_error=runtime_futex_error,
|
||||
gstreamer_wrong_elf_class=gstreamer_wrong_elf_class,
|
||||
gstreamer_stream_error=gstreamer_stream_error,
|
||||
game_returncode=outcome.returncode,
|
||||
game_timed_out=outcome.timed_out,
|
||||
game_duration_seconds=round(outcome.duration_seconds, 3),
|
||||
@@ -393,15 +498,67 @@ def _native_log_snapshot(
|
||||
return str(path), modified
|
||||
|
||||
|
||||
def _current_game_log(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
started_ns: int,
|
||||
) -> dict[str, object] | None:
|
||||
state = newest_current_native_log(
|
||||
directory,
|
||||
baseline,
|
||||
started_ns,
|
||||
patterns=("log_*.txt",),
|
||||
)
|
||||
if state is None:
|
||||
return None
|
||||
analysis = parse_current_run_log(
|
||||
state,
|
||||
baseline,
|
||||
)
|
||||
return {
|
||||
"path": str(state.path),
|
||||
"modified_at": datetime.fromtimestamp(
|
||||
state.mtime_ns / 1_000_000_000
|
||||
).astimezone().isoformat(),
|
||||
"failed_file_count": len(analysis.failed_files),
|
||||
"failed_import_scenes": tuple(analysis.failed_import_scenes),
|
||||
"load_texture_to_material_failed": analysis.load_texture_to_material_failed,
|
||||
"import_scene_threw": analysis.import_scene_threw,
|
||||
"stage_without_new_stage": analysis.stage_without_new_stage,
|
||||
"module_code_sync_error": analysis.module_code_sync_error,
|
||||
"module_code_sync_left": analysis.module_code_sync_left,
|
||||
"module_code_sync_right": analysis.module_code_sync_right,
|
||||
"module_code_sync_delta": analysis.module_code_sync_delta,
|
||||
"banned_code_1": analysis.banned_code_1,
|
||||
}
|
||||
|
||||
|
||||
def _conclusion(
|
||||
trace: RelayTraceResult,
|
||||
*,
|
||||
module_code_sync_error: bool,
|
||||
banned_code_1: bool,
|
||||
runtime_futex_error: bool,
|
||||
registry_restored: bool,
|
||||
raw_relay_deleted: bool,
|
||||
timed_out: bool,
|
||||
) -> tuple[str, tuple[str, ...]]:
|
||||
warnings: list[str] = []
|
||||
if trace.comparison_results:
|
||||
if module_code_sync_error:
|
||||
conclusion = "module-code-sync-error"
|
||||
warnings.append(
|
||||
"The current-run native game log recorded a module-code integrity "
|
||||
"mismatch. Do not retry this account with an instrumented or modified "
|
||||
"game module."
|
||||
)
|
||||
elif banned_code_1:
|
||||
conclusion = "banned-code-1"
|
||||
warnings.append(
|
||||
"The current-run native game log recorded Banned Code 1. Confirm the "
|
||||
"same account with the unmodified publisher launcher before changing "
|
||||
"the local runtime again."
|
||||
)
|
||||
elif trace.comparison_results:
|
||||
if trace.comparison_results[-1]:
|
||||
conclusion = "account-id-match"
|
||||
else:
|
||||
@@ -422,6 +579,12 @@ def _conclusion(
|
||||
warnings.append("The relay log did not record the loaded game.dll base address.")
|
||||
if timed_out:
|
||||
warnings.append("The diagnostic reached its timeout and terminated the launcher process.")
|
||||
if runtime_futex_error:
|
||||
warnings.append(
|
||||
"A host-side process emitted glibc's fatal futex message. This is not a "
|
||||
"game module-code result; compare the same prefix with direct Proton 7 "
|
||||
"outside UMU before attributing the login failure to authentication."
|
||||
)
|
||||
if not registry_restored:
|
||||
warnings.append("The temporary Wine relay registry value could not be restored.")
|
||||
if not raw_relay_deleted:
|
||||
@@ -445,6 +608,13 @@ def _write_report(report: LoginTraceReport) -> None:
|
||||
"# PristonTale EU login trace\n\n"
|
||||
f"- Started: {report.started_at}\n"
|
||||
f"- Runtime: {report.runtime_kind}\n"
|
||||
f"- Game.exe SHA-256: `{report.game_exe_sha256 or 'missing'}`\n"
|
||||
f"- game.dll SHA-256: `{report.game_dll_sha256 or 'missing'}`\n"
|
||||
f"- Current native log: {report.current_game_log or 'not found'}\n"
|
||||
f"- Module-code mismatch: {report.module_code_sync_error}\n"
|
||||
f"- Banned Code 1 in native log: {report.banned_code_1}\n"
|
||||
f"- Host futex fatal message: {report.runtime_futex_error}\n"
|
||||
f"- GStreamer wrong-ELF warnings: {report.gstreamer_wrong_elf_class}\n"
|
||||
f"- Account-ID comparison observed: {report.comparison_observed}\n"
|
||||
f"- Exact account-ID match: {match}\n"
|
||||
f"- Conclusion: `{report.conclusion}`\n"
|
||||
@@ -455,6 +625,21 @@ def _write_report(report: LoginTraceReport) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _string_or_none(value: object) -> str | None:
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def _integer_or_none(value: object) -> int | None:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _integer_or_zero(value: object) -> int:
|
||||
result = _integer_or_none(value)
|
||||
return result if result is not None else 0
|
||||
|
||||
|
||||
def _sha256_if_file(path: Path) -> str | None:
|
||||
try:
|
||||
if not path.is_file():
|
||||
|
||||
Reference in New Issue
Block a user