@rage-against-the-pixel/unity-cli 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -1
- package/dist/cli.js +73 -3
- package/dist/cli.js.map +1 -1
- package/dist/license-client.d.ts +24 -3
- package/dist/license-client.js +96 -38
- package/dist/license-client.js.map +1 -1
- package/dist/logging.d.ts +2 -0
- package/dist/logging.js +40 -29
- package/dist/logging.js.map +1 -1
- package/dist/unity-editor.js +3 -5
- package/dist/unity-editor.js.map +1 -1
- package/dist/unity-hub.js +9 -1
- package/dist/unity-hub.js.map +1 -1
- package/dist/unity-logging.d.ts +68 -0
- package/dist/unity-logging.js +956 -0
- package/dist/unity-logging.js.map +1 -0
- package/dist/utilities.d.ts +0 -18
- package/dist/utilities.js +0 -126
- package/dist/utilities.js.map +1 -1
- package/dist/utp.d.ts +41 -0
- package/dist/utp.js +44 -0
- package/dist/utp.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ActionTableRenderer = void 0;
|
|
37
|
+
exports.sanitizeTelemetryJson = sanitizeTelemetryJson;
|
|
38
|
+
exports.stringDisplayWidth = stringDisplayWidth;
|
|
39
|
+
exports.formatActionTimelineTable = formatActionTimelineTable;
|
|
40
|
+
exports.TailLogFile = TailLogFile;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const logging_1 = require("./logging");
|
|
44
|
+
const utilities_1 = require("./utilities");
|
|
45
|
+
const utp_1 = require("./utp");
|
|
46
|
+
/**
|
|
47
|
+
* Editor log messages whose severity has been changed.
|
|
48
|
+
* Useful for making certain error messages that are not critical less noisy.
|
|
49
|
+
* Key is the exact log message, value is the remapped LogLevel.
|
|
50
|
+
*/
|
|
51
|
+
const remappedEditorLogs = {
|
|
52
|
+
'OpenCL device, baking cannot use GPU lightmapper.': logging_1.LogLevel.INFO,
|
|
53
|
+
'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.': logging_1.LogLevel.INFO,
|
|
54
|
+
};
|
|
55
|
+
// Detects GitHub-style annotation markers to avoid emitting duplicates
|
|
56
|
+
const annotationPrefixRegex = /\n::[a-z]+::/i;
|
|
57
|
+
const TIMELINE_HEADING = '🔨 Unity Build Timeline';
|
|
58
|
+
const PLAYER_BUILD_INFO_HEADING = '📋 Player Build Info';
|
|
59
|
+
function sanitizeTelemetryJson(raw) {
|
|
60
|
+
if (!raw) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
return raw
|
|
64
|
+
.replace(/\uFEFF/gu, '')
|
|
65
|
+
.replace(/\u0000/gu, '')
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
const MIN_DESCRIPTION_COLUMN_WIDTH = 16;
|
|
69
|
+
const DEFAULT_TERMINAL_WIDTH = 120;
|
|
70
|
+
const TERMINAL_WIDTH_SAFETY_MARGIN = 2;
|
|
71
|
+
const MIN_TERMINAL_WIDTH = 40;
|
|
72
|
+
const extendedPictographicRegex = /\p{Extended_Pictographic}/u;
|
|
73
|
+
class ActionTelemetryAccumulator {
|
|
74
|
+
pendingActions = new Map();
|
|
75
|
+
completedActions = [];
|
|
76
|
+
totalDurationMs = 0;
|
|
77
|
+
totalErrorCount = 0;
|
|
78
|
+
playerBuildInfoSteps = [];
|
|
79
|
+
record(action) {
|
|
80
|
+
if (action.phase === utp_1.Phase.Begin) {
|
|
81
|
+
this.pendingActions.set(this.getActionKey(action), action);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (action.phase === utp_1.Phase.End) {
|
|
85
|
+
const key = this.getActionKey(action);
|
|
86
|
+
let start = this.pendingActions.get(key);
|
|
87
|
+
if (start) {
|
|
88
|
+
this.pendingActions.delete(key);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const fallback = this.findPendingMatchFor(action);
|
|
92
|
+
if (fallback) {
|
|
93
|
+
start = fallback.action;
|
|
94
|
+
this.pendingActions.delete(fallback.key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const durationMs = this.calculateDurationMs(start, action);
|
|
98
|
+
const errors = this.extractErrors(action);
|
|
99
|
+
const summary = {
|
|
100
|
+
name: action.name ?? 'Unnamed Action',
|
|
101
|
+
description: action.description ?? '',
|
|
102
|
+
durationMs,
|
|
103
|
+
errors,
|
|
104
|
+
};
|
|
105
|
+
this.completedActions.push(summary);
|
|
106
|
+
this.totalDurationMs += durationMs;
|
|
107
|
+
this.totalErrorCount += errors.length;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
recordPlayerBuildInfo(info) {
|
|
113
|
+
if (!Array.isArray(info.steps) || info.steps.length === 0) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const normalizedSteps = info.steps
|
|
117
|
+
.map(step => {
|
|
118
|
+
const description = step.description?.trim();
|
|
119
|
+
if (!description) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const durationMs = Math.max(0, step.duration ?? 0);
|
|
123
|
+
const errorCount = Math.max(0, step.errors ?? 0);
|
|
124
|
+
return {
|
|
125
|
+
description,
|
|
126
|
+
durationMs,
|
|
127
|
+
errorCount,
|
|
128
|
+
};
|
|
129
|
+
})
|
|
130
|
+
.filter((step) => step !== undefined);
|
|
131
|
+
if (normalizedSteps.length === 0) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
this.playerBuildInfoSteps = normalizedSteps;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
snapshot() {
|
|
138
|
+
if (this.completedActions.length === 0 && this.pendingActions.size === 0 && this.playerBuildInfoSteps.length === 0) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
completed: [...this.completedActions],
|
|
143
|
+
pending: Array.from(this.pendingActions.values()).map(action => ({
|
|
144
|
+
name: action.name ?? 'Unnamed Action',
|
|
145
|
+
description: action.description ?? '',
|
|
146
|
+
})),
|
|
147
|
+
totalDurationMs: this.totalDurationMs,
|
|
148
|
+
totalErrorCount: this.totalErrorCount,
|
|
149
|
+
playerBuildInfo: this.playerBuildInfoSteps.length > 0
|
|
150
|
+
? {
|
|
151
|
+
steps: [...this.playerBuildInfoSteps],
|
|
152
|
+
totalDurationMs: this.playerBuildInfoSteps.reduce((sum, step) => sum + step.durationMs, 0),
|
|
153
|
+
totalErrorCount: this.playerBuildInfoSteps.reduce((sum, step) => sum + step.errorCount, 0),
|
|
154
|
+
}
|
|
155
|
+
: undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
getActionKey(action) {
|
|
159
|
+
const pid = action.processId ?? 'na';
|
|
160
|
+
const name = action.name ?? '';
|
|
161
|
+
const description = action.description ?? '';
|
|
162
|
+
return `${pid}::${name}::${description}`;
|
|
163
|
+
}
|
|
164
|
+
findPendingMatchFor(action) {
|
|
165
|
+
for (const [key, pending] of this.pendingActions.entries()) {
|
|
166
|
+
const sameProcess = (pending.processId ?? 'na') === (action.processId ?? 'na');
|
|
167
|
+
const sameName = (pending.name ?? '') === (action.name ?? '');
|
|
168
|
+
if (!sameProcess || !sameName) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const pendingDescription = pending.description ?? '';
|
|
172
|
+
const endingDescription = action.description ?? '';
|
|
173
|
+
if (!pendingDescription || !endingDescription) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (endingDescription.startsWith(pendingDescription) || pendingDescription.startsWith(endingDescription)) {
|
|
177
|
+
return { key, action: pending };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
calculateDurationMs(start, end) {
|
|
183
|
+
if (start?.time != null && end.time != null) {
|
|
184
|
+
return Math.max(0, end.time - start.time);
|
|
185
|
+
}
|
|
186
|
+
if (typeof end.duration === 'number') {
|
|
187
|
+
return Math.max(0, end.duration);
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
extractErrors(action) {
|
|
192
|
+
if (!Array.isArray(action.errors) || action.errors.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return action.errors.map(formatErrorDetailValue);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function formatErrorDetailValue(value) {
|
|
199
|
+
let raw = '';
|
|
200
|
+
if (value instanceof Error) {
|
|
201
|
+
raw = value.stack && value.stack.length > 0 ? value.stack : (value.message || value.toString());
|
|
202
|
+
}
|
|
203
|
+
else if (typeof value === 'string') {
|
|
204
|
+
raw = value;
|
|
205
|
+
}
|
|
206
|
+
else if (value === undefined || value === null) {
|
|
207
|
+
raw = '';
|
|
208
|
+
}
|
|
209
|
+
else if (typeof value === 'object') {
|
|
210
|
+
try {
|
|
211
|
+
raw = JSON.stringify(value, null, 2);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
raw = String(value);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
raw = String(value);
|
|
219
|
+
}
|
|
220
|
+
return raw.replace(/\r\n/g, '\n').trimEnd();
|
|
221
|
+
}
|
|
222
|
+
function formatDuration(ms) {
|
|
223
|
+
if (ms >= 86_400_000) {
|
|
224
|
+
const days = ms / 86_400_000;
|
|
225
|
+
return `${days.toFixed(days >= 10 ? 0 : 1)} d`;
|
|
226
|
+
}
|
|
227
|
+
if (ms >= 3_600_000) {
|
|
228
|
+
const hours = ms / 3_600_000;
|
|
229
|
+
return `${hours.toFixed(hours >= 10 ? 0 : 1)} h`;
|
|
230
|
+
}
|
|
231
|
+
if (ms >= 60_000) {
|
|
232
|
+
const minutes = ms / 60_000;
|
|
233
|
+
return `${minutes.toFixed(minutes >= 10 ? 0 : 1)} m`;
|
|
234
|
+
}
|
|
235
|
+
if (ms >= 1_000) {
|
|
236
|
+
const seconds = ms / 1_000;
|
|
237
|
+
return `${seconds.toFixed(seconds >= 10 ? 0 : 1)} s`;
|
|
238
|
+
}
|
|
239
|
+
return `${ms} ms`;
|
|
240
|
+
}
|
|
241
|
+
function truncateDisplay(value, maxWidth) {
|
|
242
|
+
if (maxWidth <= 0) {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
if (stringDisplayWidth(value) <= maxWidth) {
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
if (maxWidth <= 3) {
|
|
249
|
+
let width = 0;
|
|
250
|
+
let result = '';
|
|
251
|
+
for (const symbol of [...value]) {
|
|
252
|
+
const codePoint = symbol.codePointAt(0);
|
|
253
|
+
if (codePoint === undefined) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const charWidth = charDisplayWidth(codePoint);
|
|
257
|
+
if (width + charWidth > maxWidth) {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
width += charWidth;
|
|
261
|
+
result += symbol;
|
|
262
|
+
if (width >= maxWidth) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
const ellipsis = '...';
|
|
269
|
+
const ellipsisWidth = stringDisplayWidth(ellipsis);
|
|
270
|
+
const targetWidth = Math.max(1, maxWidth - ellipsisWidth);
|
|
271
|
+
let width = 0;
|
|
272
|
+
let result = '';
|
|
273
|
+
for (const symbol of [...value]) {
|
|
274
|
+
const codePoint = symbol.codePointAt(0);
|
|
275
|
+
if (codePoint === undefined) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const charWidth = charDisplayWidth(codePoint);
|
|
279
|
+
if (width + charWidth > targetWidth) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
width += charWidth;
|
|
283
|
+
result += symbol;
|
|
284
|
+
}
|
|
285
|
+
if (!result) {
|
|
286
|
+
return ellipsis;
|
|
287
|
+
}
|
|
288
|
+
return `${result}${ellipsis}`;
|
|
289
|
+
}
|
|
290
|
+
function stringDisplayWidth(value) {
|
|
291
|
+
let width = 0;
|
|
292
|
+
for (const symbol of [...value]) {
|
|
293
|
+
const codePoint = symbol.codePointAt(0);
|
|
294
|
+
if (codePoint === undefined) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
width += charDisplayWidth(codePoint);
|
|
298
|
+
}
|
|
299
|
+
return width;
|
|
300
|
+
}
|
|
301
|
+
function charDisplayWidth(codePoint) {
|
|
302
|
+
if (isZeroWidthCodePoint(codePoint)) {
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
if (isEmojiPresentation(codePoint) || isFullWidthCodePoint(codePoint)) {
|
|
306
|
+
return 2;
|
|
307
|
+
}
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
311
|
+
if (codePoint === 0) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (codePoint === 0x200d) {
|
|
315
|
+
return true; // zero width joiner used in emoji sequences
|
|
316
|
+
}
|
|
317
|
+
if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (isCombiningMark(codePoint)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if ((codePoint >= 0xfe00 && codePoint <= 0xfe0f) || (codePoint >= 0xe0100 && codePoint <= 0xe01ef)) {
|
|
324
|
+
return true; // variation selectors
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
function isEmojiPresentation(codePoint) {
|
|
329
|
+
// Use Unicode Extended_Pictographic to detect emoji that are rendered double-width
|
|
330
|
+
return extendedPictographicRegex.test(String.fromCodePoint(codePoint));
|
|
331
|
+
}
|
|
332
|
+
function isCombiningMark(codePoint) {
|
|
333
|
+
const ranges = [
|
|
334
|
+
[0x0300, 0x036f],
|
|
335
|
+
[0x0483, 0x0489],
|
|
336
|
+
[0x07eb, 0x07f3],
|
|
337
|
+
[0x135d, 0x135f],
|
|
338
|
+
[0x1ab0, 0x1aff],
|
|
339
|
+
[0x1dc0, 0x1dff],
|
|
340
|
+
[0x20d0, 0x20ff],
|
|
341
|
+
[0xfe20, 0xfe2f],
|
|
342
|
+
];
|
|
343
|
+
return ranges.some(([start, end]) => codePoint >= start && codePoint <= end);
|
|
344
|
+
}
|
|
345
|
+
function isFullWidthCodePoint(codePoint) {
|
|
346
|
+
return codePoint >= 0x1100 && (codePoint <= 0x115f ||
|
|
347
|
+
codePoint === 0x2329 ||
|
|
348
|
+
codePoint === 0x232a ||
|
|
349
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
350
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
351
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
352
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
353
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
|
|
354
|
+
(codePoint >= 0xff01 && codePoint <= 0xff60) ||
|
|
355
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
356
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
357
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
358
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
359
|
+
}
|
|
360
|
+
function padDisplay(value, width, alignment = 'left') {
|
|
361
|
+
if (width <= 0) {
|
|
362
|
+
return '';
|
|
363
|
+
}
|
|
364
|
+
let text = value;
|
|
365
|
+
let valueWidth = stringDisplayWidth(text);
|
|
366
|
+
if (valueWidth > width) {
|
|
367
|
+
text = truncateDisplay(text, width);
|
|
368
|
+
valueWidth = stringDisplayWidth(text);
|
|
369
|
+
}
|
|
370
|
+
if (valueWidth === width) {
|
|
371
|
+
return text;
|
|
372
|
+
}
|
|
373
|
+
const padding = width - valueWidth;
|
|
374
|
+
if (alignment === 'right') {
|
|
375
|
+
return `${' '.repeat(padding)}${text}`;
|
|
376
|
+
}
|
|
377
|
+
if (alignment === 'center') {
|
|
378
|
+
const left = Math.floor(padding / 2);
|
|
379
|
+
return `${' '.repeat(left)}${text}${' '.repeat(padding - left)}`;
|
|
380
|
+
}
|
|
381
|
+
return `${text}${' '.repeat(padding)}`;
|
|
382
|
+
}
|
|
383
|
+
function computeTablePadding(columnCount) {
|
|
384
|
+
return columnCount * 3 + 1;
|
|
385
|
+
}
|
|
386
|
+
function computeTableWidth(columnWidths) {
|
|
387
|
+
let sum = 0;
|
|
388
|
+
for (const width of columnWidths) {
|
|
389
|
+
sum += width ?? 0;
|
|
390
|
+
}
|
|
391
|
+
return sum + computeTablePadding(columnWidths.length);
|
|
392
|
+
}
|
|
393
|
+
function adjustDescriptionColumnWidth(columnWidths, descriptionColumnIndex, descriptionHeaderWidth, maxWidth) {
|
|
394
|
+
if (maxWidth === undefined || !Number.isFinite(maxWidth) || maxWidth <= 0) {
|
|
395
|
+
return columnWidths;
|
|
396
|
+
}
|
|
397
|
+
const targetWidth = Math.floor(maxWidth);
|
|
398
|
+
const totalWidth = computeTableWidth(columnWidths);
|
|
399
|
+
const paddingWidth = computeTablePadding(columnWidths.length);
|
|
400
|
+
let sumWithoutDescription = 0;
|
|
401
|
+
columnWidths.forEach((width, index) => {
|
|
402
|
+
if (index === descriptionColumnIndex) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
sumWithoutDescription += width ?? 0;
|
|
406
|
+
});
|
|
407
|
+
const minDescriptionWidth = Math.max(descriptionHeaderWidth, MIN_DESCRIPTION_COLUMN_WIDTH);
|
|
408
|
+
const currentDescriptionWidth = columnWidths[descriptionColumnIndex] ?? minDescriptionWidth;
|
|
409
|
+
const availableWidthForDescription = targetWidth - paddingWidth - sumWithoutDescription;
|
|
410
|
+
if (totalWidth <= targetWidth) {
|
|
411
|
+
columnWidths[descriptionColumnIndex] = Math.max(currentDescriptionWidth, availableWidthForDescription);
|
|
412
|
+
return columnWidths;
|
|
413
|
+
}
|
|
414
|
+
if (availableWidthForDescription >= minDescriptionWidth) {
|
|
415
|
+
columnWidths[descriptionColumnIndex] = Math.max(minDescriptionWidth, Math.min(currentDescriptionWidth, availableWidthForDescription));
|
|
416
|
+
return columnWidths;
|
|
417
|
+
}
|
|
418
|
+
columnWidths[descriptionColumnIndex] = minDescriptionWidth;
|
|
419
|
+
return columnWidths;
|
|
420
|
+
}
|
|
421
|
+
function buildBorderLine(columnWidths, left, middle, right) {
|
|
422
|
+
let result = left;
|
|
423
|
+
columnWidths.forEach((width, index) => {
|
|
424
|
+
const segmentWidth = Math.max(0, width + 2);
|
|
425
|
+
result += '─'.repeat(segmentWidth);
|
|
426
|
+
result += index === columnWidths.length - 1 ? right : middle;
|
|
427
|
+
});
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
function formatActionTimelineTable(snapshot, options) {
|
|
431
|
+
const showErrorsColumn = snapshot.totalErrorCount > 0;
|
|
432
|
+
const tableRows = [];
|
|
433
|
+
snapshot.pending.forEach(action => {
|
|
434
|
+
const row = {
|
|
435
|
+
status: '⏳',
|
|
436
|
+
description: action.description ?? '',
|
|
437
|
+
durationText: '...',
|
|
438
|
+
};
|
|
439
|
+
if (showErrorsColumn) {
|
|
440
|
+
row.errorsText = '';
|
|
441
|
+
}
|
|
442
|
+
tableRows.push(row);
|
|
443
|
+
});
|
|
444
|
+
snapshot.completed.forEach(action => {
|
|
445
|
+
const row = {
|
|
446
|
+
status: action.errors.length > 0 ? '❌' : '✅',
|
|
447
|
+
description: action.description ?? '',
|
|
448
|
+
durationText: formatDuration(action.durationMs),
|
|
449
|
+
};
|
|
450
|
+
if (showErrorsColumn) {
|
|
451
|
+
row.errorsText = action.errors.length.toString();
|
|
452
|
+
}
|
|
453
|
+
tableRows.push(row);
|
|
454
|
+
});
|
|
455
|
+
if (tableRows.length === 0 && snapshot.playerBuildInfo === undefined) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
const totalsRow = {
|
|
459
|
+
status: 'Σ',
|
|
460
|
+
description: ' Total Build Duration',
|
|
461
|
+
durationText: formatDuration(snapshot.totalDurationMs),
|
|
462
|
+
};
|
|
463
|
+
if (showErrorsColumn) {
|
|
464
|
+
totalsRow.errorsText = snapshot.totalErrorCount.toString();
|
|
465
|
+
}
|
|
466
|
+
const statusHeader = 'Status';
|
|
467
|
+
const descriptionHeader = 'Description';
|
|
468
|
+
const durationHeader = 'Duration';
|
|
469
|
+
const errorsHeader = '# of Errors';
|
|
470
|
+
let statusWidth = Math.max(stringDisplayWidth(statusHeader), ...tableRows.map(row => stringDisplayWidth(row.status)), stringDisplayWidth(totalsRow.status));
|
|
471
|
+
let descriptionWidth = Math.max(stringDisplayWidth(descriptionHeader), ...tableRows.map(row => stringDisplayWidth(row.description)), stringDisplayWidth(totalsRow.description));
|
|
472
|
+
let durationWidth = Math.max(stringDisplayWidth(durationHeader), ...tableRows.map(row => stringDisplayWidth(row.durationText)), stringDisplayWidth(totalsRow.durationText));
|
|
473
|
+
let errorsWidth = showErrorsColumn ? Math.max(stringDisplayWidth(errorsHeader), ...tableRows.map(row => stringDisplayWidth(row.errorsText ?? '')), stringDisplayWidth(totalsRow.errorsText ?? '')) : 0;
|
|
474
|
+
let columns = showErrorsColumn
|
|
475
|
+
? [statusWidth, descriptionWidth, durationWidth, errorsWidth]
|
|
476
|
+
: [statusWidth, descriptionWidth, durationWidth];
|
|
477
|
+
columns = adjustDescriptionColumnWidth(columns, 1, stringDisplayWidth(descriptionHeader), options?.maxWidth);
|
|
478
|
+
statusWidth = columns[0] ?? statusWidth;
|
|
479
|
+
descriptionWidth = columns[1] ?? descriptionWidth;
|
|
480
|
+
durationWidth = columns[2] ?? durationWidth;
|
|
481
|
+
if (showErrorsColumn) {
|
|
482
|
+
errorsWidth = columns[3] ?? errorsWidth;
|
|
483
|
+
}
|
|
484
|
+
const resolvedColumns = showErrorsColumn
|
|
485
|
+
? [statusWidth, descriptionWidth, durationWidth, errorsWidth]
|
|
486
|
+
: [statusWidth, descriptionWidth, durationWidth];
|
|
487
|
+
const padStatus = (value) => padDisplay(value, statusWidth, 'center');
|
|
488
|
+
const formatRow = (row) => {
|
|
489
|
+
let line = `│ ${padStatus(row.status)} │ ${padDisplay(row.description, descriptionWidth)} │ ${padDisplay(row.durationText, durationWidth, 'right')} │`;
|
|
490
|
+
if (showErrorsColumn) {
|
|
491
|
+
line += ` ${padDisplay(row.errorsText ?? '', errorsWidth, 'right')} │`;
|
|
492
|
+
}
|
|
493
|
+
return line;
|
|
494
|
+
};
|
|
495
|
+
const topBorder = buildBorderLine(resolvedColumns, '┌', '┬', '┐');
|
|
496
|
+
const headerRow = showErrorsColumn
|
|
497
|
+
? `│ ${padStatus(statusHeader)} │ ${padDisplay(descriptionHeader, descriptionWidth)} │ ${padDisplay(durationHeader, durationWidth, 'right')} │ ${padDisplay(errorsHeader, errorsWidth, 'right')} │`
|
|
498
|
+
: `│ ${padStatus(statusHeader)} │ ${padDisplay(descriptionHeader, descriptionWidth)} │ ${padDisplay(durationHeader, durationWidth, 'right')} │`;
|
|
499
|
+
const headerDivider = buildBorderLine(resolvedColumns, '├', '┼', '┤');
|
|
500
|
+
const totalsDivider = buildBorderLine(resolvedColumns, '├', '┼', '┤');
|
|
501
|
+
const bottomBorder = buildBorderLine(resolvedColumns, '└', '┴', '┘');
|
|
502
|
+
let output = `${TIMELINE_HEADING}\n`;
|
|
503
|
+
output += `${topBorder}\n`;
|
|
504
|
+
output += `${headerRow}\n`;
|
|
505
|
+
output += `${headerDivider}\n`;
|
|
506
|
+
tableRows.forEach(row => {
|
|
507
|
+
output += `${formatRow(row)}\n`;
|
|
508
|
+
});
|
|
509
|
+
output += `${totalsDivider}\n`;
|
|
510
|
+
output += `${formatRow(totalsRow)}\n`;
|
|
511
|
+
output += `${bottomBorder}\n`;
|
|
512
|
+
const playerBuildInfoOptions = options?.maxWidth === undefined ? undefined : { maxWidth: options.maxWidth };
|
|
513
|
+
if (snapshot.playerBuildInfo) {
|
|
514
|
+
const playerBuildInfoSection = formatPlayerBuildInfoTable(snapshot.playerBuildInfo, playerBuildInfoOptions);
|
|
515
|
+
if (playerBuildInfoSection) {
|
|
516
|
+
output += `\n${playerBuildInfoSection}`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (showErrorsColumn && snapshot.totalErrorCount > 0) {
|
|
520
|
+
const errorSection = formatErrorDetailsSection(snapshot.completed);
|
|
521
|
+
if (errorSection) {
|
|
522
|
+
output += `\n${errorSection}\n`;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
output += '\n';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
output += '\n';
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
text: output,
|
|
533
|
+
lineCount: countLines(output),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function countLines(block) {
|
|
537
|
+
if (!block) {
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
const normalized = block.endsWith('\n') ? block : `${block}\n`;
|
|
541
|
+
return normalized.split('\n').length - 1;
|
|
542
|
+
}
|
|
543
|
+
function formatPlayerBuildInfoTable(playerInfo, options) {
|
|
544
|
+
if (!playerInfo.steps || playerInfo.steps.length === 0) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
const rows = playerInfo.steps.map(step => ({
|
|
548
|
+
description: step.description,
|
|
549
|
+
durationText: formatDuration(step.durationMs),
|
|
550
|
+
errorsText: step.errorCount.toString(),
|
|
551
|
+
}));
|
|
552
|
+
const totalsRow = {
|
|
553
|
+
description: ' Total Player Build Duration',
|
|
554
|
+
durationText: formatDuration(playerInfo.totalDurationMs),
|
|
555
|
+
errorsText: playerInfo.totalErrorCount.toString(),
|
|
556
|
+
};
|
|
557
|
+
const descriptionHeader = 'Description';
|
|
558
|
+
const durationHeader = 'Duration';
|
|
559
|
+
const errorsHeader = '# of Errors';
|
|
560
|
+
let descriptionWidth = Math.max(stringDisplayWidth(descriptionHeader), ...rows.map(row => stringDisplayWidth(row.description)), stringDisplayWidth(totalsRow.description));
|
|
561
|
+
let durationWidth = Math.max(stringDisplayWidth(durationHeader), ...rows.map(row => stringDisplayWidth(row.durationText)), stringDisplayWidth(totalsRow.durationText));
|
|
562
|
+
let errorWidth = Math.max(stringDisplayWidth(errorsHeader), ...rows.map(row => stringDisplayWidth(row.errorsText)), stringDisplayWidth(totalsRow.errorsText));
|
|
563
|
+
let columns = [descriptionWidth, durationWidth, errorWidth];
|
|
564
|
+
columns = adjustDescriptionColumnWidth(columns, 0, stringDisplayWidth(descriptionHeader), options?.maxWidth);
|
|
565
|
+
descriptionWidth = columns[0] ?? descriptionWidth;
|
|
566
|
+
durationWidth = columns[1] ?? durationWidth;
|
|
567
|
+
errorWidth = columns[2] ?? errorWidth;
|
|
568
|
+
const padDescription = (value) => padDisplay(value, descriptionWidth);
|
|
569
|
+
const topBorder = buildBorderLine([descriptionWidth, durationWidth, errorWidth], '┌', '┬', '┐');
|
|
570
|
+
const headerDivider = buildBorderLine([descriptionWidth, durationWidth, errorWidth], '├', '┼', '┤');
|
|
571
|
+
const totalsDivider = buildBorderLine([descriptionWidth, durationWidth, errorWidth], '├', '┼', '┤');
|
|
572
|
+
const bottomBorder = buildBorderLine([descriptionWidth, durationWidth, errorWidth], '└', '┴', '┘');
|
|
573
|
+
let output = `${PLAYER_BUILD_INFO_HEADING}\n`;
|
|
574
|
+
output += `${topBorder}\n`;
|
|
575
|
+
output += `│ ${padDescription(descriptionHeader)} │ ${padDisplay(durationHeader, durationWidth, 'right')} │ ${padDisplay(errorsHeader, errorWidth, 'right')} │\n`;
|
|
576
|
+
output += `${headerDivider}\n`;
|
|
577
|
+
rows.forEach(row => {
|
|
578
|
+
output += `│ ${padDescription(row.description)} │ ${padDisplay(row.durationText, durationWidth, 'right')} │ ${padDisplay(row.errorsText, errorWidth, 'right')} │\n`;
|
|
579
|
+
});
|
|
580
|
+
output += `${totalsDivider}\n`;
|
|
581
|
+
output += `│ ${padDescription(totalsRow.description)} │ ${padDisplay(totalsRow.durationText, durationWidth, 'right')} │ ${padDisplay(totalsRow.errorsText, errorWidth, 'right')} │\n`;
|
|
582
|
+
output += `${bottomBorder}\n`;
|
|
583
|
+
if (!output.endsWith('\n')) {
|
|
584
|
+
output += '\n';
|
|
585
|
+
}
|
|
586
|
+
return output;
|
|
587
|
+
}
|
|
588
|
+
function formatErrorDetailsSection(actions) {
|
|
589
|
+
const actionsWithErrors = actions.filter(action => action.errors.length > 0);
|
|
590
|
+
if (actionsWithErrors.length === 0) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
const lines = ['Error Details'];
|
|
594
|
+
actionsWithErrors.forEach(action => {
|
|
595
|
+
const headerText = (action.description && action.description.trim().length > 0)
|
|
596
|
+
? action.description
|
|
597
|
+
: (action.name && action.name.trim().length > 0 ? action.name : 'Unnamed Action');
|
|
598
|
+
lines.push('');
|
|
599
|
+
lines.push(headerText);
|
|
600
|
+
action.errors.forEach(errorText => {
|
|
601
|
+
const normalized = errorText.length > 0 ? errorText : '(no details provided)';
|
|
602
|
+
const segments = normalized.split('\n');
|
|
603
|
+
const [firstLine, ...rest] = segments;
|
|
604
|
+
lines.push(` - ${firstLine}`);
|
|
605
|
+
rest.forEach(segment => {
|
|
606
|
+
if (segment.length === 0) {
|
|
607
|
+
lines.push('');
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
lines.push(` ${segment}`);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
return lines.join('\n').trimEnd();
|
|
616
|
+
}
|
|
617
|
+
class ActionTableRenderer {
|
|
618
|
+
canUpdateTerminal;
|
|
619
|
+
lastRenderLineCount = 0;
|
|
620
|
+
anchorActive = false;
|
|
621
|
+
constructor(canUpdateTerminal) {
|
|
622
|
+
this.canUpdateTerminal = canUpdateTerminal;
|
|
623
|
+
}
|
|
624
|
+
prepareForContent() {
|
|
625
|
+
if (!this.canUpdateTerminal) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
this.clearTimelineRegion();
|
|
629
|
+
}
|
|
630
|
+
render(snapshot) {
|
|
631
|
+
if (!snapshot) {
|
|
632
|
+
this.clearTimelineRegion();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const formatted = formatActionTimelineTable(snapshot, { maxWidth: this.getMaxWidth() });
|
|
636
|
+
if (!formatted) {
|
|
637
|
+
this.clearTimelineRegion();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (this.canUpdateTerminal) {
|
|
641
|
+
this.clearTimelineRegion();
|
|
642
|
+
process.stdout.write(formatted.text);
|
|
643
|
+
this.anchorActive = true;
|
|
644
|
+
this.lastRenderLineCount = formatted.lineCount;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
process.stdout.write(formatted.text);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
clearTimelineRegion() {
|
|
651
|
+
if (!this.anchorActive || this.lastRenderLineCount <= 0) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.rewindToAnchor();
|
|
655
|
+
process.stdout.write('\u001b[J');
|
|
656
|
+
this.anchorActive = false;
|
|
657
|
+
this.lastRenderLineCount = 0;
|
|
658
|
+
}
|
|
659
|
+
rewindToAnchor() {
|
|
660
|
+
// The TIMELINE_HEADING line is our anchor; rewind to it before clearing so updates stay in place.
|
|
661
|
+
const linesToMove = this.lastRenderLineCount;
|
|
662
|
+
if (linesToMove <= 0) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
process.stdout.write(`\u001b[${linesToMove}F`);
|
|
666
|
+
}
|
|
667
|
+
getMaxWidth() {
|
|
668
|
+
const detectedWidth = this.detectTerminalWidth();
|
|
669
|
+
const adjustedWidth = detectedWidth - TERMINAL_WIDTH_SAFETY_MARGIN;
|
|
670
|
+
return Math.max(MIN_TERMINAL_WIDTH, adjustedWidth);
|
|
671
|
+
}
|
|
672
|
+
detectTerminalWidth() {
|
|
673
|
+
const stdoutColumns = typeof process.stdout.columns === 'number' ? process.stdout.columns : undefined;
|
|
674
|
+
if (stdoutColumns && stdoutColumns > 0) {
|
|
675
|
+
return stdoutColumns;
|
|
676
|
+
}
|
|
677
|
+
const envColumns = Number(process.env.COLUMNS);
|
|
678
|
+
if (Number.isFinite(envColumns) && envColumns > 0) {
|
|
679
|
+
return envColumns;
|
|
680
|
+
}
|
|
681
|
+
return DEFAULT_TERMINAL_WIDTH;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
exports.ActionTableRenderer = ActionTableRenderer;
|
|
685
|
+
function toNumeric(value) {
|
|
686
|
+
if (typeof value === 'number') {
|
|
687
|
+
return Number.isFinite(value) ? value : undefined;
|
|
688
|
+
}
|
|
689
|
+
const parsed = Number(value);
|
|
690
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
691
|
+
}
|
|
692
|
+
function normalizeMemoryLabelEntries(memoryLabels) {
|
|
693
|
+
if (!memoryLabels) {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(memoryLabels)) {
|
|
697
|
+
const entries = [];
|
|
698
|
+
for (const labelObject of memoryLabels) {
|
|
699
|
+
for (const [label, value] of Object.entries(labelObject)) {
|
|
700
|
+
const numericValue = toNumeric(value);
|
|
701
|
+
if (numericValue !== undefined) {
|
|
702
|
+
entries.push([label, numericValue]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return entries;
|
|
707
|
+
}
|
|
708
|
+
return Object.entries(memoryLabels)
|
|
709
|
+
.map(([label, value]) => {
|
|
710
|
+
const numericValue = toNumeric(value);
|
|
711
|
+
return numericValue === undefined ? undefined : [label, numericValue];
|
|
712
|
+
})
|
|
713
|
+
.filter((entry) => entry !== undefined);
|
|
714
|
+
}
|
|
715
|
+
function formatMemoryLeakTable(memLeaks) {
|
|
716
|
+
const rows = normalizeMemoryLabelEntries(memLeaks.memoryLabels);
|
|
717
|
+
const allocated = memLeaks.allocatedMemory ?? 0;
|
|
718
|
+
const labelHeader = 'Label';
|
|
719
|
+
const sizeHeader = 'Size';
|
|
720
|
+
const totalLabel = 'Total';
|
|
721
|
+
const nonePlaceholder = '(none)';
|
|
722
|
+
const totalValueStr = allocated.toString();
|
|
723
|
+
const rowLabelWidth = rows.length > 0 ? Math.max(...rows.map(([label]) => label.length)) : nonePlaceholder.length;
|
|
724
|
+
const rowSizeWidth = rows.length > 0 ? Math.max(...rows.map(([, size]) => size.toString().length)) : 0;
|
|
725
|
+
const labelWidth = Math.max(labelHeader.length, totalLabel.length, rowLabelWidth);
|
|
726
|
+
const sizeWidth = Math.max(sizeHeader.length, totalValueStr.length, rowSizeWidth);
|
|
727
|
+
const columns = [labelWidth, sizeWidth];
|
|
728
|
+
const topBorder = buildBorderLine(columns, '┌', '┬', '┐');
|
|
729
|
+
const headerDivider = buildBorderLine(columns, '├', '┼', '┤');
|
|
730
|
+
const bottomBorder = buildBorderLine(columns, '└', '┴', '┘');
|
|
731
|
+
let output = 'Memory Leaks Detected:\n';
|
|
732
|
+
output += `${topBorder}\n`;
|
|
733
|
+
output += `│ ${labelHeader.padEnd(labelWidth)} │ ${sizeHeader.padStart(sizeWidth)} │\n`;
|
|
734
|
+
output += `${headerDivider}\n`;
|
|
735
|
+
if (rows.length === 0) {
|
|
736
|
+
output += `│ ${nonePlaceholder.padEnd(labelWidth)} │ ${''.padStart(sizeWidth)} │\n`;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
for (const [label, size] of rows) {
|
|
740
|
+
output += `│ ${label.padEnd(labelWidth)} │ ${size.toString().padStart(sizeWidth)} │\n`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
output += `${headerDivider}\n`;
|
|
744
|
+
output += `│ ${totalLabel.padEnd(labelWidth)} │ ${totalValueStr.padStart(sizeWidth)} │\n`;
|
|
745
|
+
output += `${bottomBorder}\n`;
|
|
746
|
+
return output;
|
|
747
|
+
}
|
|
748
|
+
function buildUtpLogPath(logPath) {
|
|
749
|
+
const parsed = path.parse(logPath);
|
|
750
|
+
const utpFileName = `utp-${parsed.name}.json`;
|
|
751
|
+
return parsed.dir ? path.join(parsed.dir, utpFileName) : utpFileName;
|
|
752
|
+
}
|
|
753
|
+
async function writeUtpTelemetryLog(filePath, entries, logger) {
|
|
754
|
+
try {
|
|
755
|
+
const content = `${JSON.stringify(entries, null, 2)}\n`;
|
|
756
|
+
await fs.promises.writeFile(filePath, content, 'utf8');
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
logger.warn(`Failed to write UTP telemetry log (${filePath}): ${error}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Tails a log file using fs.watch and ReadStream for efficient reading.
|
|
764
|
+
* @param logPath The path to the log file to tail.
|
|
765
|
+
* @param projectPath The path to the project (used for log annotation).
|
|
766
|
+
* @returns An object containing the tail promise and signalEnd function.
|
|
767
|
+
*/
|
|
768
|
+
function TailLogFile(logPath, projectPath) {
|
|
769
|
+
let logEnded = false;
|
|
770
|
+
let lastSize = 0;
|
|
771
|
+
const logPollingInterval = 250;
|
|
772
|
+
const telemetry = [];
|
|
773
|
+
const logger = logging_1.Logger.instance;
|
|
774
|
+
const actionAccumulator = new ActionTelemetryAccumulator();
|
|
775
|
+
const actionTableRenderer = new ActionTableRenderer(process.stdout.isTTY === true && process.env.CI !== 'true');
|
|
776
|
+
const utpLogPath = buildUtpLogPath(logPath);
|
|
777
|
+
let telemetryFlushed = false;
|
|
778
|
+
const renderActionTable = () => {
|
|
779
|
+
const snapshot = actionAccumulator.snapshot();
|
|
780
|
+
if (snapshot) {
|
|
781
|
+
actionTableRenderer.render(snapshot);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
const flushTelemetryLog = async () => {
|
|
785
|
+
if (telemetryFlushed) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
telemetryFlushed = true;
|
|
789
|
+
await writeUtpTelemetryLog(utpLogPath, telemetry, logger);
|
|
790
|
+
};
|
|
791
|
+
const writeStdout = (content, restoreTable = true) => {
|
|
792
|
+
actionTableRenderer.prepareForContent();
|
|
793
|
+
process.stdout.write(content);
|
|
794
|
+
if (restoreTable) {
|
|
795
|
+
renderActionTable();
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
async function readNewLogContent() {
|
|
799
|
+
try {
|
|
800
|
+
if (!fs.existsSync(logPath)) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const stats = await fs.promises.stat(logPath);
|
|
804
|
+
if (stats.size < lastSize) {
|
|
805
|
+
lastSize = 0;
|
|
806
|
+
}
|
|
807
|
+
if (stats.size > lastSize) {
|
|
808
|
+
const bytesToRead = stats.size - lastSize;
|
|
809
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
810
|
+
let fh;
|
|
811
|
+
try {
|
|
812
|
+
fh = await fs.promises.open(logPath, fs.constants.O_RDONLY);
|
|
813
|
+
await fh.read(buffer, 0, bytesToRead, lastSize);
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
await fh?.close();
|
|
817
|
+
}
|
|
818
|
+
lastSize = stats.size;
|
|
819
|
+
if (bytesToRead > 0) {
|
|
820
|
+
const chunk = buffer.toString('utf8');
|
|
821
|
+
// Parse telemetry lines in this chunk (lines starting with '##utp:')
|
|
822
|
+
try {
|
|
823
|
+
const lines = chunk.split(/\r?\n/);
|
|
824
|
+
for (const rawLine of lines) {
|
|
825
|
+
const line = rawLine.trim();
|
|
826
|
+
if (!line) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
// Attempt to parse telemetry utp JSON
|
|
830
|
+
if (line.startsWith('##utp:')) {
|
|
831
|
+
const jsonPart = line.substring('##utp:'.length).trim();
|
|
832
|
+
try {
|
|
833
|
+
const sanitizedJson = sanitizeTelemetryJson(jsonPart);
|
|
834
|
+
if (!sanitizedJson) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const utpJson = JSON.parse(sanitizedJson);
|
|
838
|
+
const utp = utpJson;
|
|
839
|
+
telemetry.push(utp);
|
|
840
|
+
if (utp.message && 'severity' in utp && (utp.severity === utp_1.Severity.Error || utp.severity === utp_1.Severity.Exception || utp.severity === utp_1.Severity.Assert)) {
|
|
841
|
+
let messageLevel = logging_1.LogLevel.ERROR;
|
|
842
|
+
if (remappedEditorLogs[utp.message] !== undefined) {
|
|
843
|
+
messageLevel = remappedEditorLogs[utp.message];
|
|
844
|
+
}
|
|
845
|
+
const file = utp.file ? utp.file.replace(/\\/g, '/') : undefined;
|
|
846
|
+
const lineNum = utp.line ? utp.line : undefined;
|
|
847
|
+
const message = utp.message;
|
|
848
|
+
const stacktrace = utp.stacktrace ? `${utp.stacktrace}` : undefined;
|
|
849
|
+
if (!annotationPrefixRegex.test(message)) {
|
|
850
|
+
// only annotate if the file is within the current project
|
|
851
|
+
if (projectPath && file && file.startsWith(projectPath)) {
|
|
852
|
+
logger.annotate(logging_1.LogLevel.ERROR, stacktrace == undefined ? message : `${message}\n${stacktrace}`, file, lineNum);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
switch (messageLevel) {
|
|
856
|
+
case logging_1.LogLevel.WARN:
|
|
857
|
+
logger.warn(stacktrace == undefined ? message : `${message}\n${stacktrace}`);
|
|
858
|
+
break;
|
|
859
|
+
case logging_1.LogLevel.ERROR:
|
|
860
|
+
logger.error(stacktrace == undefined ? message : `${message}\n${stacktrace}`);
|
|
861
|
+
break;
|
|
862
|
+
case logging_1.LogLevel.INFO:
|
|
863
|
+
default:
|
|
864
|
+
logger.info(stacktrace == undefined ? message : `${message}\n${stacktrace}`);
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
// switch utp types, fallback to json if we don't have a toString() implementation or a type implementation
|
|
872
|
+
switch (utp.type) {
|
|
873
|
+
case 'Action': {
|
|
874
|
+
const actionEntry = utp;
|
|
875
|
+
const tableChanged = actionAccumulator.record(actionEntry);
|
|
876
|
+
if (tableChanged) {
|
|
877
|
+
renderActionTable();
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case 'MemoryLeaks':
|
|
882
|
+
logger.debug(formatMemoryLeakTable(utp));
|
|
883
|
+
break;
|
|
884
|
+
case 'PlayerBuildInfo': {
|
|
885
|
+
const infoEntry = utp;
|
|
886
|
+
const changed = actionAccumulator.recordPlayerBuildInfo(infoEntry);
|
|
887
|
+
if (changed) {
|
|
888
|
+
renderActionTable();
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
default:
|
|
893
|
+
// Print raw JSON for unhandled UTP types
|
|
894
|
+
writeStdout(`${jsonPart}\n`);
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
logger.warn(`Failed to parse telemetry JSON: ${error} -- raw: ${jsonPart}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
if (logging_1.Logger.instance.logLevel !== logging_1.LogLevel.UTP) {
|
|
905
|
+
writeStdout(`${line}\n`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
if (error.code !== 'EPIPE') {
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
logger.warn(`Error while parsing telemetry from log chunk: ${error}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
logger.warn(`Error while tailing log file: ${error}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const tailPromise = new Promise((resolve, reject) => {
|
|
924
|
+
(async () => {
|
|
925
|
+
try {
|
|
926
|
+
while (!logEnded) {
|
|
927
|
+
await (0, utilities_1.Delay)(logPollingInterval);
|
|
928
|
+
await readNewLogContent();
|
|
929
|
+
}
|
|
930
|
+
// Final read to capture any remaining content after tailing stops
|
|
931
|
+
await (0, utilities_1.WaitForFileToBeUnlocked)(logPath, 10_000);
|
|
932
|
+
await readNewLogContent();
|
|
933
|
+
try {
|
|
934
|
+
// write a final newline to separate log output
|
|
935
|
+
writeStdout('\n');
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
if (error.code !== 'EPIPE') {
|
|
939
|
+
logger.warn(`Error while writing log tail: ${error}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
await flushTelemetryLog();
|
|
943
|
+
resolve();
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
await flushTelemetryLog();
|
|
947
|
+
reject(error);
|
|
948
|
+
}
|
|
949
|
+
})();
|
|
950
|
+
});
|
|
951
|
+
function stopLogTail() {
|
|
952
|
+
logEnded = true;
|
|
953
|
+
}
|
|
954
|
+
return { tailPromise, stopLogTail, telemetry };
|
|
955
|
+
}
|
|
956
|
+
//# sourceMappingURL=unity-logging.js.map
|