mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
packaging: the Control python launcher uses no external command (dirname failed inside the AppImage on newer systems); the engine start error names a missing Windows DLL (0xC0000135) and says how to fix it
This commit is contained in:
1 parent
a122ba0bdf
commit
adfb7ecb2a
3 files changed
+180
-2
No files matched your search
@@ -0,0 +1,105 @@
|
||||
"""serve/loader_diag.py - name the file a Windows engine could not load.
|
||||
|
||||
A Windows program that imports a DLL which is not there ends with exit code 0xC0000135 and prints nothing. The person only sees
|
||||
"the engine stopped". This reads the engine's import table (the same reader as packaging/binary_deps.py, copied because the
|
||||
packaging folder does not travel with Control) and lists the DLLs that are neither beside the engine, nor in the Windows
|
||||
folders, nor part of the GPU driver, so the start error can say which file is missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
LOADER_FAILURE_CODES = (0xC0000135,) # STATUS_DLL_NOT_FOUND
|
||||
|
||||
def pe_imports(path: Path) -> list[str]:
|
||||
"""DLL names a PE file imports at load time (import table) plus delay-load imports."""
|
||||
data = path.read_bytes()
|
||||
if data[:2] != b"MZ":
|
||||
raise ValueError("not a PE file")
|
||||
pe = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
if data[pe:pe + 4] != b"PE\0\0":
|
||||
raise ValueError("bad PE header")
|
||||
nsec, = struct.unpack_from("<H", data, pe + 6)
|
||||
opt_size, = struct.unpack_from("<H", data, pe + 20)
|
||||
opt = pe + 24
|
||||
magic, = struct.unpack_from("<H", data, opt)
|
||||
dd = opt + (112 if magic == 0x20B else 96)
|
||||
sections = []
|
||||
sec = opt + opt_size
|
||||
for i in range(nsec):
|
||||
vsize, va, rsize, raw = struct.unpack_from("<IIII", data, sec + i * 40 + 8)
|
||||
sections.append((va, max(vsize, rsize), raw))
|
||||
|
||||
def off(rva: int) -> int:
|
||||
for va, size, raw in sections:
|
||||
if va <= rva < va + size:
|
||||
return raw + rva - va
|
||||
raise ValueError("RVA outside every section")
|
||||
|
||||
def cstr(o: int) -> str:
|
||||
return data[o:data.index(b"\0", o)].decode("ascii", "replace")
|
||||
|
||||
names: list[str] = []
|
||||
imp_rva, imp_size = struct.unpack_from("<II", data, dd + 8)
|
||||
if imp_rva and imp_size:
|
||||
o = off(imp_rva)
|
||||
while True:
|
||||
_, _, _, name_rva, _ = struct.unpack_from("<IIIII", data, o)
|
||||
if not name_rva:
|
||||
break
|
||||
names.append(cstr(off(name_rva)))
|
||||
o += 20
|
||||
delay_rva, delay_size = struct.unpack_from("<II", data, dd + 13 * 8)
|
||||
if delay_rva and delay_size:
|
||||
o = off(delay_rva)
|
||||
while True:
|
||||
attrs, name_rva = struct.unpack_from("<II", data, o)
|
||||
if not name_rva:
|
||||
break
|
||||
names.append(cstr(off(name_rva)))
|
||||
o += 32
|
||||
return names
|
||||
|
||||
|
||||
def missing_files(exe, system_dirs=None) -> list[str]:
|
||||
"""DLL names the engine (and the DLLs beside it) import that cannot be found: not in the engine's folder, not a Windows or
|
||||
driver library, and not in a Windows folder on this PC. Sorted, without duplicates; [] when nothing is missing or the file
|
||||
cannot be read (this is advice for an error message, it never raises)."""
|
||||
try:
|
||||
exe = Path(exe)
|
||||
folder = exe.parent
|
||||
here = {p.name.lower() for p in folder.iterdir() if p.is_file()}
|
||||
if system_dirs is None:
|
||||
root = os.environ.get("SystemRoot") or r"C:\Windows"
|
||||
system_dirs = [Path(root) / "System32", Path(root)]
|
||||
files = [exe] + [p for p in sorted(folder.glob("*.dll"))]
|
||||
missing: set[str] = set()
|
||||
for f in files:
|
||||
try:
|
||||
names = pe_imports(f)
|
||||
except (ValueError, OSError, struct.error):
|
||||
continue
|
||||
for name in names:
|
||||
low = name.lower()
|
||||
if low in here or low.startswith(("api-ms-win-", "ext-ms-win-")):
|
||||
continue # beside the engine, or a virtual API-set name Windows resolves itself
|
||||
if any((d / name).exists() for d in system_dirs):
|
||||
continue
|
||||
missing.add(name) # not found anywhere the loader looks (a missing driver DLL is named too)
|
||||
return sorted(missing, key=str.lower)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def missing_note(rc, exe) -> str:
|
||||
"""The sentence for the start error, or "" when the exit status is not a missing-library one or nothing can be named."""
|
||||
if rc is None or (rc & 0xFFFFFFFF) not in LOADER_FAILURE_CODES or not exe:
|
||||
return ""
|
||||
names = missing_files(exe)
|
||||
if not names:
|
||||
return ""
|
||||
return (f"\n The engine is missing a file it needs ({', '.join(names)}). Reinstall the engine in Settings > Engine, "
|
||||
f"or send us the log.")
|
||||
+7
-2
@@ -544,13 +544,18 @@ def cpu_isa_words() -> tuple:
|
||||
return f"{model or 'this CPU'} ({have})", missing
|
||||
|
||||
|
||||
def start_exit_note(rc: int | None) -> str:
|
||||
def start_exit_note(rc: int | None, exe=None) -> str:
|
||||
"""The sentence the start error carries about how the engine process ended (the engine log is silent when the
|
||||
system ends it): "" without a status, else a line, with the CPU's AVX flags after an illegal instruction."""
|
||||
words = engine_exit_words(rc)
|
||||
if not words:
|
||||
return ""
|
||||
note = f"\nthe engine process ended: {words}"
|
||||
try: # 0xC0000135: say which DLL (the loader prints nothing)
|
||||
from serve.loader_diag import missing_note
|
||||
note += missing_note(rc, exe)
|
||||
except Exception:
|
||||
pass
|
||||
if rc is not None and ((rc < 0 and os.name != "nt" and -rc == int(getattr(signal, "SIGILL", 4))) or
|
||||
(rc & 0xFFFFFFFF) == 0xC000001D):
|
||||
isa, missing = cpu_isa_words()
|
||||
@@ -1053,7 +1058,7 @@ class StrataEngine:
|
||||
"the engine exited before it was ready")
|
||||
# EngineDied, not bare RuntimeError: a request waiting on this load must end with a 503
|
||||
# through the request path's handler - a RuntimeError there left the turn with no answer (#1527)
|
||||
note = "" if timed_out else start_exit_note(self.proc.returncode)
|
||||
note = "" if timed_out else start_exit_note(self.proc.returncode, exe)
|
||||
tail = start_log_tail(log, log_start)
|
||||
lines = [x for x in tail.split("\n")[2:] if x.strip()]
|
||||
if tail and lines and all(x.strip().startswith("[strata]") for x in lines):
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""A Windows engine that cannot load a DLL ends with 0xC0000135 and no message. The start error must name the missing file
|
||||
(tester report 2026-10-17: cublas64_13.dll was not in the engine component and the app blamed memory)."""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "packaging"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import binary_deps # noqa: E402
|
||||
from serve import loader_diag # noqa: E402
|
||||
from serve.server import start_exit_note # noqa: E402
|
||||
from test_engine_deps import make_pe # noqa: E402
|
||||
|
||||
|
||||
class MissingFiles(unittest.TestCase):
|
||||
def folder(self, d, *, with_cublas):
|
||||
d = Path(d)
|
||||
(d / "strata.exe").write_bytes(make_pe(["cublas64_13.dll", "KERNEL32.dll", "api-ms-win-crt-runtime-l1-1-0.dll"]))
|
||||
(d / "strata_kq.dll").write_bytes(make_pe(["KERNEL32.dll"]))
|
||||
if with_cublas:
|
||||
(d / "cublas64_13.dll").write_bytes(make_pe(["cublasLt64_13.dll", "KERNEL32.dll"]))
|
||||
(d / "cublasLt64_13.dll").write_bytes(make_pe(["KERNEL32.dll"]))
|
||||
return d / "strata.exe"
|
||||
|
||||
def test_reader_matches_the_packaging_reader(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
exe = self.folder(d, with_cublas=True)
|
||||
self.assertEqual(loader_diag.pe_imports(exe), binary_deps.pe_imports(exe))
|
||||
|
||||
def test_names_the_missing_dll(self):
|
||||
with tempfile.TemporaryDirectory() as d, tempfile.TemporaryDirectory() as sysdir:
|
||||
exe = self.folder(d, with_cublas=False)
|
||||
(Path(sysdir) / "KERNEL32.dll").write_bytes(b"MZ")
|
||||
self.assertEqual(loader_diag.missing_files(exe, [Path(sysdir)]), ["cublas64_13.dll"])
|
||||
|
||||
def test_transitive_import_is_named(self):
|
||||
with tempfile.TemporaryDirectory() as d, tempfile.TemporaryDirectory() as sysdir:
|
||||
exe = self.folder(d, with_cublas=True)
|
||||
(exe.parent / "cublasLt64_13.dll").unlink()
|
||||
(Path(sysdir) / "KERNEL32.dll").write_bytes(b"MZ")
|
||||
self.assertEqual(loader_diag.missing_files(exe, [Path(sysdir)]), ["cublasLt64_13.dll"])
|
||||
|
||||
def test_complete_folder_names_nothing(self):
|
||||
with tempfile.TemporaryDirectory() as d, tempfile.TemporaryDirectory() as sysdir:
|
||||
exe = self.folder(d, with_cublas=True)
|
||||
(Path(sysdir) / "KERNEL32.dll").write_bytes(b"MZ")
|
||||
self.assertEqual(loader_diag.missing_files(exe, [Path(sysdir)]), [])
|
||||
|
||||
def test_unreadable_file_never_raises(self):
|
||||
self.assertEqual(loader_diag.missing_files(Path("no-such-folder") / "strata.exe"), [])
|
||||
|
||||
def test_start_note_only_for_the_loader_status(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
exe = self.folder(d, with_cublas=False)
|
||||
note = start_exit_note(0xC0000135, exe)
|
||||
self.assertIn("0xC0000135", note)
|
||||
self.assertIn("cublas64_13.dll", note)
|
||||
self.assertIn("Reinstall the engine in Settings > Engine", note)
|
||||
self.assertNotIn("cublas64_13.dll", start_exit_note(1, exe))
|
||||
self.assertNotIn("missing a file", start_exit_note(0xC0000135, None))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user