Report native crashes to T5DE
The next start reports a native crash that ended the last run (IMVU's crash.log, this client's own only, each once), with consent; checked on the build (native_crash). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
19a3fc4e80
commit
419303d0f6
3 files changed
+167
-1
No files matched your search
@@ -0,0 +1,97 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: dhkatz <[email protected]>
|
||||
Date: Fri, 30 Oct 2026 08:16:59 -0700
|
||||
Subject: [PATCH] Report native crashes to T5DE
|
||||
|
||||
A native crash (in IMVU's C++ code, or a renderer) is reported to T5DE the next time the client starts,
|
||||
with the person's consent, like its Python errors: the exception and the crashing thread's stack
|
||||
(each call's module and offset), from the crash log IMVU's crash handler leaves in %APPDATA%\IMVU, which
|
||||
the client never read (its data is in T5DE's folder). Each run records its process ID, so only the
|
||||
client's own crashes are sent, each once.
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
||||
T5de-Id: t5de-native-crashes
|
||||
T5de-Name: Native Crash Reports
|
||||
T5de-Group: core
|
||||
T5de-Required: true
|
||||
T5de-Depends: t5de-diagnostics
|
||||
---
|
||||
library/main/client.py | 50 +++++++++++++++++++++++++++++++++++++++
|
||||
library/main/clientapp.py | 3 +++
|
||||
2 files changed, 53 insertions(+)
|
||||
|
||||
diff --git a/library/main/client.py b/library/main/client.py
|
||||
index 9cad8bcf9f775a3ad5b3fa0fbad9b4534e215918..e59ee3f55843e2967d1f5919bd2c17971af292e5 100644
|
||||
--- a/library/main/client.py
|
||||
+++ b/library/main/client.py
|
||||
@@ -96,6 +96,56 @@ def t5deReport(excInfo, kind):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+def t5deNativeCrash():
|
||||
+ """Report the native crash that ended this client's last run, if there was one. IMVU's crash handler writes
|
||||
+ %APPDATA%/IMVU/crash.log, which the client itself never reads (its data is in T5DE's folder). Only the exception
|
||||
+ and the crashing thread's stack (module and offset) are sent. Each run records its process ID, so only this
|
||||
+ client's own crashes are sent (not IMVU's, which writes there too), and each once."""
|
||||
+ try:
|
||||
+ import json, os, re, tempfile, time
|
||||
+ import imvu.fs.windows
|
||||
+ import imvu.version
|
||||
+ record = os.path.join(imvu.fs.windows.getUserDirectory(), 't5de_last_run.json')
|
||||
+ try:
|
||||
+ with open(record) as f:
|
||||
+ last = json.load(f)
|
||||
+ except Exception:
|
||||
+ last = {}
|
||||
+ with open(record, 'w') as f:
|
||||
+ json.dump({'pid': os.getpid(), 'start': time.time()}, f)
|
||||
+ log = os.path.join(os.environ.get('APPDATA', ''), 'IMVU', 'crash.log')
|
||||
+ if not last.get('pid') or not os.path.isfile(log) or os.path.getmtime(log) < last.get('start', 0):
|
||||
+ return
|
||||
+ with open(log) as f:
|
||||
+ text = f.read(4000000)
|
||||
+ pid = re.search(r'Process ID:\s*(\d+)', text)
|
||||
+ if not pid or int(pid.group(1)) != last['pid']:
|
||||
+ return
|
||||
+ code = re.search(r'ExceptionCode:\s*(0x[0-9a-fA-F]+)', text)
|
||||
+ code = code.group(1).lower() if code else 'unknown'
|
||||
+ frames = []
|
||||
+ crashing = text.split('CRASHING THREAD:', 1)
|
||||
+ if len(crashing) == 2:
|
||||
+ for line in re.split(r'\n\s*\n', crashing[1], 1)[0].splitlines():
|
||||
+ m = re.match(r'\s*\(([^#!,()]+?)(?:#[0-9a-fA-F]+)?!([0-9a-fA-F]+)', line)
|
||||
+ if m:
|
||||
+ frames.append({'file': m.group(1), 'function': '+0x' + m.group(2).lower(), 'line': 0})
|
||||
+ names = {'0xc0000005': 'access violation', '0xc00000fd': 'stack overflow', '0xc0000409': 'stack buffer overrun',
|
||||
+ '0xe06d7363': 'C++ exception', '0x80000003': 'breakpoint', '0xc000001d': 'illegal instruction'}
|
||||
+ # The log lists the outermost call first. The crash is named after the innermost module that isn't
|
||||
+ # Windows' or the C runtime's (which only raise it).
|
||||
+ system = re.compile(r'^(ntdll|kernelbase|kernel32|msvcr\d*|msvcp\d*|ucrtbase|vcruntime\d*)\.dll$', re.I)
|
||||
+ own = [f['file'] for f in frames if not system.match(f['file'])]
|
||||
+ where = own[-1] if own else (frames[-1]['file'] if frames else 'unknown module')
|
||||
+ report = {'platform': 'native', 'type': 'NativeCrash', 'value': 'exception %s (%s) in %s' % (code, names.get(code, 'unknown'), where), 'frames': frames[:100], 'tags': {'kind': 'native', 'imvu': imvu.version.__version__}}
|
||||
+ fd, path = tempfile.mkstemp(prefix='t5de-report-', suffix='.json')
|
||||
+ os.write(fd, json.dumps(report))
|
||||
+ os.close(fd)
|
||||
+ t5deHelper('report', path)
|
||||
+ except Exception:
|
||||
+ pass
|
||||
+
|
||||
+
|
||||
def t5deUpdateFile():
|
||||
import os, tempfile
|
||||
return os.path.join(tempfile.gettempdir(), 't5de-update.json')
|
||||
diff --git a/library/main/clientapp.py b/library/main/clientapp.py
|
||||
index 4a1778d5aaf11f5cc6502c30ec81ac46fbf5c654..9807b988b17cd867994fbcac8ff74f6f528aecf1 100644
|
||||
--- a/library/main/clientapp.py
|
||||
+++ b/library/main/clientapp.py
|
||||
@@ -714,6 +714,9 @@ def reportUnrecoverableError(serviceProvider, userAccount, dialogFactory):
|
||||
def runMain(argv=sys.argv):
|
||||
from main import client
|
||||
client.t5deTrack('launch')
|
||||
+ # T5DE: a native crash ending the last run, to T5DE (with consent). Here, not at login: ClientApp binds
|
||||
+ # maybeReportUncleanShutdownAfterLogin as a default argument, which a patch can't reach.
|
||||
+ client.t5deNativeCrash()
|
||||
return _t5de_original_runMain(argv)
|
||||
|
||||
|
||||
@@ -339,5 +339,74 @@ def proxy():
|
||||
return not wrong, '; '.join(wrong) or 'requests, errors, post_multipart, IMQ and fallback as expected'
|
||||
|
||||
|
||||
# The shape of IMVU's native crash log (%APPDATA%\IMVU\crash.log), cut down: outermost call first.
|
||||
CRASH_LOG = '''-------------------------------------------------
|
||||
IMVU CRASH LOG
|
||||
|
||||
PROCESS INFO:
|
||||
Process ID: 4242
|
||||
Main thread ID: 0x0000307c
|
||||
|
||||
EXCEPTION:
|
||||
ExceptionRecord: 0x00cfcea0
|
||||
ExceptionCode: 0xc0000005 <unknown exception code!>
|
||||
|
||||
CRASHING THREAD: 0x0000307c()
|
||||
Priority: 0
|
||||
Stack:
|
||||
(PYTHON27.DLL#7e2905e5022c4e4b8281138443d73afa3!00123bbd, pc=72a43bbd)
|
||||
(SceneWindow.dll#4322532c99964c0aa698af609c8076181!000a9d7d, pc=718b9d7d)
|
||||
(MSVCR100.dll#da7b3ed8d4af49efa87d61651bd1d6492!0002872d, pc=72bf872d)
|
||||
|
||||
THREAD: 0x00001bc8()
|
||||
Stack:
|
||||
(ntdll.dll#dbc4b3053c0f3189895b2be009a0d2a41!0006c6e1, pc=7757c6e1)
|
||||
'''
|
||||
|
||||
|
||||
def native_crash():
|
||||
root = tempfile.mkdtemp()
|
||||
try:
|
||||
os.makedirs(os.path.join(root, 'IMVU'))
|
||||
os.makedirs(os.path.join(root, 'T5DE'))
|
||||
log = os.path.join(root, 'IMVU', 'crash.log')
|
||||
with open(log, 'w') as f:
|
||||
f.write(CRASH_LOG)
|
||||
os.environ['APPDATA'] = root
|
||||
imvu = types.ModuleType('imvu')
|
||||
imvu.fs = types.ModuleType('imvu.fs')
|
||||
imvu.fs.windows = types.ModuleType('imvu.fs.windows')
|
||||
imvu.fs.windows.getUserDirectory = lambda: os.path.join(root, 'T5DE')
|
||||
imvu.version = types.ModuleType('imvu.version')
|
||||
imvu.version.__version__ = '554.8'
|
||||
sys.modules.update({'imvu': imvu, 'imvu.fs': imvu.fs, 'imvu.fs.windows': imvu.fs.windows,
|
||||
'imvu.version': imvu.version})
|
||||
client = _no_proxy()
|
||||
sent = []
|
||||
client.t5deHelper = lambda *args: sent.append(json.load(open(args[1])))
|
||||
record = os.path.join(root, 'T5DE', 't5de_last_run.json')
|
||||
|
||||
def last_run(pid):
|
||||
with open(record, 'w') as f:
|
||||
json.dump({'pid': pid, 'start': os.path.getmtime(log) - 60}, f)
|
||||
|
||||
last_run(4241) # another process's crash (IMVU's own client writes there too): not sent
|
||||
client.t5deNativeCrash()
|
||||
unsent = len(sent)
|
||||
last_run(4242) # the last run's: sent once
|
||||
client.t5deNativeCrash()
|
||||
client.t5deNativeCrash()
|
||||
want = {'platform': 'native', 'type': 'NativeCrash',
|
||||
'value': 'exception 0xc0000005 (access violation) in SceneWindow.dll',
|
||||
'frames': [{'file': 'PYTHON27.DLL', 'function': '+0x00123bbd', 'line': 0},
|
||||
{'file': 'SceneWindow.dll', 'function': '+0x000a9d7d', 'line': 0},
|
||||
{'file': 'MSVCR100.dll', 'function': '+0x0002872d', 'line': 0}],
|
||||
'tags': {'kind': 'native', 'imvu': '554.8'}}
|
||||
ok = unsent == 0 and sent == [want]
|
||||
return ok, 'one report of the last run\'s crash' if ok else 'sent %r' % sent
|
||||
finally:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
|
||||
ok, detail = globals()[CHECK]()
|
||||
print(json.dumps({'ok': ok, 'detail': detail}))
|
||||
@@ -23,7 +23,7 @@ def manifest(project):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("check", ["embeds_originals", "devicefingerprint", "prefs_key", "user_directory",
|
||||
"network_original", "network_blocklist", "proxy"])
|
||||
"network_original", "network_blocklist", "proxy", "native_crash"])
|
||||
def test_spliced_library(client, run_py27, check):
|
||||
install, built = client
|
||||
result = run_py27("check_build.py", install / "library.zip", built / "library.zip", check)
|
||||
|
||||
Reference in new issue
Block a user