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:
Niko1221 committed 2026-10-19 10:52:51 +02:00
1 parent a122ba0bdf
commit adfb7ecb2a
3 files changed
+180 -2

No files matched your search

+105
View File
@@ -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
View File
@@ -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):
+68
View File
@@ -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()