Skip to content

Commit 612bd96

Browse files
committed
Improve MPQ diagnostics and error reporting
1 parent 7bd9de5 commit 612bd96

10 files changed

Lines changed: 251 additions & 38 deletions

File tree

package.json

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
"onCustomEditor:wurst.objModPreview",
2828
"onCustomEditor:wurst.soundPreview",
2929
"onCommand:wurst.previewMap",
30-
"onCommand:wurst.reportIssue"
30+
"onCommand:wurst.reportIssue",
31+
"onCommand:wurst.showDiagnosticsActions",
32+
"onCommand:wurst.openWurstHome",
33+
"onCommand:wurst.copyDiagnostics",
34+
"onCommand:wurst.showLogs"
3135
],
3236
"main": "./dist/extension",
3337
"browser": "./dist/web/extension.js",
@@ -694,7 +698,22 @@
694698
},
695699
{
696700
"command": "wurst.showLogs",
697-
"title": "Open VSCode output panel for Wurst logs",
701+
"title": "Open Wurst output",
702+
"category": "wurst"
703+
},
704+
{
705+
"command": "wurst.showDiagnosticsActions",
706+
"title": "Wurst: Show diagnostics actions",
707+
"category": "wurst"
708+
},
709+
{
710+
"command": "wurst.openWurstHome",
711+
"title": "Wurst: Open Wurst home",
712+
"category": "wurst"
713+
},
714+
{
715+
"command": "wurst.copyDiagnostics",
716+
"title": "Wurst: Copy diagnostics",
698717
"category": "wurst"
699718
},
700719
{
@@ -858,8 +877,9 @@
858877
"lint": "eslint .",
859878
"compile-web": "webpack",
860879
"watch-web": "webpack --watch",
861-
"test": "npm run test:fuzzy && npm run test:image-decoders && npm run test:webview",
880+
"test": "npm run test:fuzzy && npm run test:image-decoders && npm run test:diagnostics && npm run test:webview",
862881
"test:image-decoders": "node ./scripts/test-image-decoders.js",
882+
"test:diagnostics": "node ./scripts/test-diagnostics.js",
863883
"test:vsix-contents": "node ./scripts/test-vsix-contents.js",
864884
"sync:wc3-knowledge-base": "node ./scripts/sync-wc3-knowledge-base.js",
865885
"test:webview": "node ./scripts/test-webview.js",

scripts/test-diagnostics.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const fs = require('fs');
5+
const os = require('os');
6+
const path = require('path');
7+
const ts = require('typescript');
8+
9+
const root = path.resolve(__dirname, '..');
10+
const source = fs.readFileSync(path.join(root, 'src', 'diagnostics.ts'), 'utf8');
11+
const js = ts.transpileModule(source, {
12+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
13+
}).outputText;
14+
const mod = { exports: {} };
15+
new Function('exports', 'module', 'require', js)(mod.exports, mod, require);
16+
17+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'wurst-diagnostics-'));
18+
fs.mkdirSync(path.join(tempHome, 'logs'));
19+
fs.writeFileSync(
20+
path.join(tempHome, 'logs', 'languageServer.log'),
21+
Array.from({ length: 125 }, (_, index) => `language-server-${index + 1}`).join('\n')
22+
);
23+
24+
mod.exports.appendDiagnostic('WC3 data', 'PKExplode: invalid literal size byte 40\n at explode (pkware.ts:42:7)');
25+
mod.exports.appendDiagnostic('MPQ', 'MPQ archive opened');
26+
mod.exports.appendDiagnostic('Inline icons', 'thumb generation failed');
27+
const report = mod.exports.buildDiagnosticsText(tempHome);
28+
29+
assert(report.includes('PKExplode: invalid literal size byte 40'));
30+
assert(report.includes('at explode (pkware.ts:42:7)'));
31+
assert(report.includes('MPQ archive opened'));
32+
assert(report.includes('language-server-125'));
33+
assert(!report.includes('language-server-25\n'));
34+
assert(report.split('\n').filter((line) => line.includes('language-server-')).length === 100);
35+
console.log('diagnostics tests passed (bounded tails and stack traces)');

src/diagnostics.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
6+
export const MAX_DIAGNOSTIC_LINES = 100;
7+
8+
export type DiagnosticSource = 'WC3 data' | 'MPQ' | 'Inline icons' | 'VS Code extension';
9+
10+
const recentLines = new Map<DiagnosticSource, string[]>();
11+
12+
/** Keep extension-side diagnostics available even though VS Code output channels are write-only. */
13+
export function appendDiagnostic(source: DiagnosticSource, message: string): void {
14+
const lines = recentLines.get(source) ?? [];
15+
for (const line of String(message).split(/\r?\n/)) {
16+
lines.push(line);
17+
}
18+
if (lines.length > MAX_DIAGNOSTIC_LINES) {
19+
lines.splice(0, lines.length - MAX_DIAGNOSTIC_LINES);
20+
}
21+
recentLines.set(source, lines);
22+
}
23+
24+
export function formatDiagnosticError(error: unknown): string {
25+
if (error instanceof Error) {
26+
return error.stack ?? `${error.name}: ${error.message}`;
27+
}
28+
return String(error);
29+
}
30+
31+
function readTail(filePath: string): string[] {
32+
try {
33+
const text = fs.readFileSync(filePath, 'utf8');
34+
const lines = text.split(/\r?\n/);
35+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
36+
return lines.slice(-MAX_DIAGNOSTIC_LINES);
37+
} catch (error) {
38+
return [`[unavailable: ${formatDiagnosticError(error)}]`];
39+
}
40+
}
41+
42+
function section(title: string, lines: string[]): string[] {
43+
return [`--- ${title} (last ${MAX_DIAGNOSTIC_LINES} lines) ---`, ...(lines.length ? lines : ['[no entries recorded]'])];
44+
}
45+
46+
/** Build a compact, copy/paste-friendly report for remote diagnostics. */
47+
export function buildDiagnosticsText(wurstHome: string): string {
48+
const lines: string[] = [
49+
'WurstScript diagnostics',
50+
`Generated: ${new Date().toISOString()}`,
51+
`Wurst home: ${wurstHome}`,
52+
'',
53+
];
54+
lines.push(...section('WC3 data / CASC', recentLines.get('WC3 data') ?? []), '');
55+
lines.push(...section('MPQ archive viewer', recentLines.get('MPQ') ?? []), '');
56+
lines.push(...section('Inline icons', recentLines.get('Inline icons') ?? []), '');
57+
lines.push(...section('Wurst VS Code extension output', recentLines.get('VS Code extension') ?? []), '');
58+
lines.push(...section('languageServer.log', readTail(path.join(wurstHome, 'logs', 'languageServer.log'))));
59+
return lines.join('\n');
60+
}

src/extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as vscode from 'vscode';
44
import { workspace, ExtensionContext } from 'vscode';
55
import { initPathManager } from './install/pathManager';
66
import { installWithRetry } from './install/installer';
7-
import { startLanguageClient, stopLanguageServerIfRunning } from './languageServer';
7+
import { registerWurstDiagnosticsCommands, startLanguageClient, stopLanguageServerIfRunning } from './languageServer';
88
import {
99
findConflictingWurstProcesses,
1010
forceStopWurstProcesses,
@@ -27,6 +27,7 @@ import { registerMapPreview } from './features/mapPreview';
2727
import { registerAgentsGuideOffer } from './features/agentsGuide';
2828
import { openIssueReport } from './features/issueReporting';
2929
import { registerCascDiagnosticsCommand } from './features/preview/cascStorage';
30+
import { appendDiagnostic, formatDiagnosticError } from './diagnostics';
3031

3132
export async function activate(context: ExtensionContext) {
3233
console.log('Wurst extension activated!');
@@ -47,6 +48,7 @@ export async function activate(context: ExtensionContext) {
4748
context.subscriptions.push(registerMapPreview(context));
4849
context.subscriptions.push(registerAgentsGuideOffer(context));
4950
context.subscriptions.push(registerCascDiagnosticsCommand());
51+
registerWurstDiagnosticsCommands(context);
5052

5153
registerBasicCommands(context);
5254
openObjModE2eFixture();
@@ -89,6 +91,7 @@ function registerBasicCommands(context: ExtensionContext) {
8991
await vscode.commands.executeCommand('workbench.action.reloadWindow');
9092
} catch (e: any) {
9193
if (e instanceof InstallCoordinationCancelledError) return;
94+
appendDiagnostic('VS Code extension', `Install/update failed: ${formatDiagnosticError(e)}`);
9295
vscode.window.showErrorMessage(`Install/Update failed: ${e?.message || e}`);
9396
}
9497
}),
@@ -126,6 +129,7 @@ function registerBasicCommands(context: ExtensionContext) {
126129
try {
127130
await createNewWurstProject();
128131
} catch (e: any) {
132+
appendDiagnostic('VS Code extension', `New project creation failed: ${formatDiagnosticError(e)}`);
129133
vscode.window.showErrorMessage(`Failed to create Wurst project: ${e?.message ?? String(e)}`);
130134
}
131135
}),
@@ -148,7 +152,8 @@ async function startLanguageClientWhenWorkspaceIsOpen(context: ExtensionContext)
148152
try {
149153
await startLanguageClient(context);
150154
} catch (err) {
151-
console.error('Failed to start language client:', err);
155+
appendDiagnostic('VS Code extension', `Failed to start language client: ${formatDiagnosticError(err)}`);
156+
console.error('Failed to start language client:', formatDiagnosticError(err));
152157
vscode.window.showWarningMessage(`Wurst language features disabled: ${err}`);
153158
}
154159
}

src/features/inlineImageDecorations.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
scaleDown,
2323
} from './imageAssetSupport';
2424
import { AssetIndex, getAssetIndex, invalidateAssetIndex } from '../utils/assetIndex';
25+
import { appendDiagnostic, formatDiagnosticError } from '../diagnostics';
2526

2627
// ── Config ────────────────────────────────────────────────────────────────────
2728

@@ -85,8 +86,10 @@ function log(message: string): void {
8586
if (logEpoch === 0) logEpoch = Date.now();
8687
const ms = Date.now() - logEpoch;
8788
const ts = `+${ms}ms`;
89+
const line = `[inline-icons] ${ts} ${message}`;
90+
appendDiagnostic('Inline icons', line);
8891
try {
89-
output.appendLine(`[inline-icons] ${ts} ${message}`);
92+
output.appendLine(line);
9093
} catch {
9194
return;
9295
}
@@ -177,7 +180,7 @@ async function getThumbnailUri(fsPath: string): Promise<vscode.Uri | undefined>
177180
log(`thumb generated: ${path.basename(fsPath)} -> ${previewPath}`);
178181
return vscode.Uri.file(previewPath);
179182
} catch (error) {
180-
log(`thumb failed: ${fsPath} :: ${error instanceof Error ? error.message : String(error)}`);
183+
log(`thumb failed: ${fsPath} :: ${formatDiagnosticError(error)}`);
181184
return undefined;
182185
}
183186
}
@@ -584,7 +587,7 @@ async function updateDecorations(editor: vscode.TextEditor): Promise<void> {
584587
clearMissingRanges(active, assetPath);
585588
safeSetDecorations(active, getMdxFoundType(), [...mdxFoundRangesByPath.values()].flat());
586589
} catch (error) {
587-
log(`casc model error: ${assetPath} :: ${error instanceof Error ? error.message : String(error)}`);
590+
log(`casc model error: ${assetPath} :: ${formatDiagnosticError(error)}`);
588591
} finally {
589592
extracting.delete(assetPath);
590593
}
@@ -648,7 +651,7 @@ async function updateDecorations(editor: vscode.TextEditor): Promise<void> {
648651
});
649652
}
650653
} catch (error) {
651-
log(`casc error: ${assetPath} :: ${error instanceof Error ? error.message : String(error)}`);
654+
log(`casc error: ${assetPath} :: ${formatDiagnosticError(error)}`);
652655
} finally {
653656
extracting.delete(assetPath);
654657
}

src/features/issueReporting.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as path from 'path';
44
import * as vscode from 'vscode';
5+
import { appendDiagnostic } from '../diagnostics';
56

67
const ISSUE_URL = 'https://github.com/wurstscript/wurst4vscode/issues/new';
78
const seenFailures = new Set<string>();
@@ -96,6 +97,14 @@ function failureKey(issue: ExtensionIssue): string {
9697

9798
/** Offer a non-modal, privacy-preserving report action once per failure shape and session. */
9899
export function offerIssueReport(issue: ExtensionIssue): void {
100+
const resourceSuffix = issue.resource ? ` resource=${resourceName(issue.resource)}` : '';
101+
appendDiagnostic(
102+
'VS Code extension',
103+
[
104+
`Preview failure [${issue.area}]${resourceSuffix}: ${issue.message}`,
105+
issue.details ?? '',
106+
].filter(Boolean).join('\n'),
107+
);
99108
const enabled = vscode.workspace.getConfiguration('wurst').get<boolean>('issueReportingHints', true);
100109
const key = failureKey(issue);
101110
if (!enabled || promptActive || seenFailures.has(key)) return;

src/features/mpqViewer.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
import * as vscode from 'vscode';
4+
import { appendDiagnostic, formatDiagnosticError } from '../diagnostics';
45
import * as path from 'path';
56
import * as os from 'os';
67
import * as fs from 'fs';
@@ -18,6 +19,7 @@ function getOut(): vscode.OutputChannel {
1819
}
1920
function log(msg: string): void {
2021
console.log('[MpqViewer] ' + msg);
22+
appendDiagnostic('MPQ', msg);
2123
getOut().appendLine(msg);
2224
}
2325

@@ -72,7 +74,7 @@ async function extractTriggerStringsSidecar(
7274
fs.mkdirSync(path.dirname(outPath), { recursive: true });
7375
fs.writeFileSync(outPath, data);
7476
} catch (e) {
75-
log(`Could not extract ${wtsEntry.name} sidecar: ${e instanceof Error ? e.message : String(e)}`);
77+
log(`Could not extract ${wtsEntry.name} sidecar: ${formatDiagnosticError(e)}`);
7678
}
7779
}
7880

@@ -148,7 +150,7 @@ class MpqViewerProvider implements vscode.CustomReadonlyEditorProvider<MpqDocume
148150
log(`MPQ opened: ${document.entries.length} files, ${document.archiveSize} bytes`);
149151
} catch (e) {
150152
document.parseError = e instanceof Error ? e.message : String(e);
151-
log(`ERROR loading MPQ: ${document.parseError}`);
153+
log(`ERROR loading MPQ: ${formatDiagnosticError(e)}`);
152154
offerIssueReport({
153155
area: 'MPQ map viewer',
154156
message: document.parseError,
@@ -196,8 +198,9 @@ class MpqViewerProvider implements vscode.CustomReadonlyEditorProvider<MpqDocume
196198
void vscode.commands.executeCommand('vscode.open', uri, { preview: false, preserveFocus: false });
197199
}
198200
} catch (e) {
201+
log(`ERROR extracting ${name}: ${formatDiagnosticError(e)}`);
199202
void vscode.window.showErrorMessage(
200-
`Failed to extract ${name}: ${e instanceof Error ? e.message : String(e)}`
203+
`Failed to extract ${name}: ${formatDiagnosticError(e)}`
201204
);
202205
}
203206
return;
@@ -252,7 +255,8 @@ async function extractAllFiles(
252255
}
253256
fs.mkdirSync(path.dirname(outPath), { recursive: true });
254257
fs.writeFileSync(outPath, data);
255-
} catch {
258+
} catch (error) {
259+
log(`ERROR extracting ${entry.name}: ${formatDiagnosticError(error)}`);
256260
failed++;
257261
}
258262
}
@@ -266,9 +270,8 @@ async function extractAllFiles(
266270
}
267271
} catch (e) {
268272
onComplete?.();
269-
void vscode.window.showErrorMessage(
270-
`Extraction failed: ${e instanceof Error ? e.message : String(e)}`
271-
);
273+
log(`ERROR during extraction: ${formatDiagnosticError(e)}`);
274+
void vscode.window.showErrorMessage(`Extraction failed: ${formatDiagnosticError(e)}`);
272275
}
273276
}
274277

0 commit comments

Comments
 (0)