gdharness 1.2.26 → 1.2.27
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/build/cli.js +195 -34
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +5 -1
- package/build/index.js +190 -32
- package/build/keeper.js +1207 -753
- package/package.json +1 -1
package/build/keeper.js
CHANGED
|
@@ -226,745 +226,1244 @@ function throughHelper(how, command, args, files) {
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// src/
|
|
230
|
-
import {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
229
|
+
// src/game-debugger.ts
|
|
230
|
+
import { createServer } from "node:net";
|
|
231
|
+
|
|
232
|
+
// src/godot-variant.ts
|
|
233
|
+
var NIL = 0;
|
|
234
|
+
var BOOL = 1;
|
|
235
|
+
var INT = 2;
|
|
236
|
+
var FLOAT = 3;
|
|
237
|
+
var STRING = 4;
|
|
238
|
+
var ARRAY = 28;
|
|
239
|
+
var FLAG_64 = 1 << 16;
|
|
240
|
+
var MATH = {
|
|
241
|
+
5: { name: "Vector2", count: 2 },
|
|
242
|
+
6: { name: "Vector2i", count: 2, ints: true },
|
|
243
|
+
7: { name: "Rect2", count: 4 },
|
|
244
|
+
8: { name: "Rect2i", count: 4, ints: true },
|
|
245
|
+
9: { name: "Vector3", count: 3 },
|
|
246
|
+
10: { name: "Vector3i", count: 3, ints: true },
|
|
247
|
+
11: { name: "Transform2D", count: 6 },
|
|
248
|
+
12: { name: "Vector4", count: 4 },
|
|
249
|
+
13: { name: "Vector4i", count: 4, ints: true },
|
|
250
|
+
14: { name: "Plane", count: 4 },
|
|
251
|
+
15: { name: "Quaternion", count: 4 },
|
|
252
|
+
16: { name: "AABB", count: 6 },
|
|
253
|
+
17: { name: "Basis", count: 9 },
|
|
254
|
+
18: { name: "Transform3D", count: 12 },
|
|
255
|
+
19: { name: "Projection", count: 16 }
|
|
256
|
+
};
|
|
257
|
+
var PACKED_VECTORS = { 35: 2, 36: 3, 38: 4 };
|
|
258
|
+
|
|
259
|
+
class Reader {
|
|
260
|
+
offset = 0;
|
|
261
|
+
view;
|
|
262
|
+
constructor(view) {
|
|
263
|
+
this.view = view;
|
|
242
264
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let directory = null;
|
|
246
|
-
try {
|
|
247
|
-
directory = process2.platform === "win32" ? null : mkdtempSync(join(tmpdir(), "gdharness-keeper-"));
|
|
248
|
-
const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join(directory, "keeper.sock");
|
|
249
|
-
const server = createServer((socket) => {
|
|
250
|
-
socket.setEncoding("utf8");
|
|
251
|
-
socket.on("error", () => {});
|
|
252
|
-
let asked = "";
|
|
253
|
-
socket.on("data", (chunk) => {
|
|
254
|
-
asked += chunk;
|
|
255
|
-
if (asked.includes(`
|
|
256
|
-
`)) {
|
|
257
|
-
socket.end(asked.startsWith(`stop
|
|
258
|
-
`) ? `${endedOrWhyNot(end)}
|
|
259
|
-
` : `unknown
|
|
260
|
-
`);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
await listening(server, address);
|
|
265
|
-
const made = directory;
|
|
266
|
-
return {
|
|
267
|
-
address,
|
|
268
|
-
close: () => {
|
|
269
|
-
server.close();
|
|
270
|
-
if (made !== null) {
|
|
271
|
-
rmSync(made, { recursive: true, force: true });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
} catch {
|
|
276
|
-
if (directory !== null) {
|
|
277
|
-
rmSync(directory, { recursive: true, force: true });
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
265
|
+
get remaining() {
|
|
266
|
+
return this.view.byteLength - this.offset;
|
|
280
267
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
server.once("error", reject);
|
|
285
|
-
server.listen(address, () => {
|
|
286
|
-
server.off("error", reject);
|
|
287
|
-
resolve();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// src/outside.ts
|
|
293
|
-
import { fileURLToPath } from "node:url";
|
|
294
|
-
function withChanges(base, changes) {
|
|
295
|
-
const env = { ...base };
|
|
296
|
-
for (const [name, value] of Object.entries(changes)) {
|
|
297
|
-
if (value === null) {
|
|
298
|
-
delete env[name];
|
|
299
|
-
} else {
|
|
300
|
-
env[name] = value;
|
|
268
|
+
need(bytes) {
|
|
269
|
+
if (bytes < 0 || bytes > this.remaining) {
|
|
270
|
+
throw new RangeError(`a value runs past the end of its message (${bytes} bytes wanted, ${this.remaining} left)`);
|
|
301
271
|
}
|
|
302
272
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
async function receivedSpec() {
|
|
309
|
-
const chunks = [];
|
|
310
|
-
for await (const chunk of process.stdin) {
|
|
311
|
-
chunks.push(chunk);
|
|
273
|
+
u32() {
|
|
274
|
+
this.need(4);
|
|
275
|
+
const value = this.view.getUint32(this.offset, true);
|
|
276
|
+
this.offset += 4;
|
|
277
|
+
return value;
|
|
312
278
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
279
|
+
i32() {
|
|
280
|
+
this.need(4);
|
|
281
|
+
const value = this.view.getInt32(this.offset, true);
|
|
282
|
+
this.offset += 4;
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
i64() {
|
|
286
|
+
this.need(8);
|
|
287
|
+
const value = Number(this.view.getBigInt64(this.offset, true));
|
|
288
|
+
this.offset += 8;
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
u64() {
|
|
292
|
+
this.need(8);
|
|
293
|
+
const value = Number(this.view.getBigUint64(this.offset, true));
|
|
294
|
+
this.offset += 8;
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
f32() {
|
|
298
|
+
this.need(4);
|
|
299
|
+
const value = this.view.getFloat32(this.offset, true);
|
|
300
|
+
this.offset += 4;
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
f64() {
|
|
304
|
+
this.need(8);
|
|
305
|
+
const value = this.view.getFloat64(this.offset, true);
|
|
306
|
+
this.offset += 8;
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
real(wide) {
|
|
310
|
+
return wide ? this.f64() : this.f32();
|
|
311
|
+
}
|
|
312
|
+
bytes(count) {
|
|
313
|
+
this.need(count);
|
|
314
|
+
const out = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, count);
|
|
315
|
+
this.offset += count;
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
string() {
|
|
319
|
+
const length = this.u32();
|
|
320
|
+
const text = new TextDecoder().decode(this.bytes(length));
|
|
321
|
+
this.bytes((4 - length % 4) % 4);
|
|
322
|
+
return text;
|
|
322
323
|
}
|
|
323
|
-
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/run-record.ts
|
|
327
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
328
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
329
|
-
import { mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync as rmSync3, statSync } from "node:fs";
|
|
330
|
-
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
331
|
-
import { promisify as promisify2 } from "node:util";
|
|
332
|
-
|
|
333
|
-
// src/process-children.ts
|
|
334
|
-
import { execFile } from "node:child_process";
|
|
335
|
-
import { promisify } from "node:util";
|
|
336
|
-
|
|
337
|
-
// src/windows-ask.ts
|
|
338
|
-
var SCRIPT = [
|
|
339
|
-
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
340
|
-
"$in = [Console]::In",
|
|
341
|
-
"while ($null -ne ($line = $in.ReadLine())) {",
|
|
342
|
-
' $parts = $line.Split(" ", 2)',
|
|
343
|
-
" try {",
|
|
344
|
-
" $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
|
|
345
|
-
" $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
|
|
346
|
-
' $tag = "OK"',
|
|
347
|
-
" } catch {",
|
|
348
|
-
" $said = $_.Exception.Message",
|
|
349
|
-
' $tag = "ERR"',
|
|
350
|
-
" }",
|
|
351
|
-
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
352
|
-
" [Console]::Out.Flush()",
|
|
353
|
-
"}"
|
|
354
|
-
].join(`
|
|
355
|
-
`);
|
|
356
|
-
|
|
357
|
-
// src/process-children.ts
|
|
358
|
-
var run = promisify(execFile);
|
|
359
|
-
|
|
360
|
-
// src/runtime-client.ts
|
|
361
|
-
import { tmpdir as tmpdir2 } from "node:os";
|
|
362
|
-
import { dirname, join as join2, resolve } from "node:path";
|
|
363
|
-
|
|
364
|
-
// src/launch.ts
|
|
365
|
-
function envValue(name, variables = process.env) {
|
|
366
|
-
const value = variables[name];
|
|
367
|
-
return value === undefined || value === "" ? undefined : value;
|
|
368
324
|
}
|
|
369
|
-
var
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"--debug-avoidance": "none",
|
|
376
|
-
"--debug-stringnames": "none",
|
|
377
|
-
"--debug-canvas-item-redraw": "none",
|
|
378
|
-
"--debug-mute-audio": "none",
|
|
379
|
-
"--print-fps": "none",
|
|
380
|
-
"--gpu-profile": "none",
|
|
381
|
-
"--gpu-validation": "none",
|
|
382
|
-
"--gpu-abort": "none",
|
|
383
|
-
"--disable-vsync": "none",
|
|
384
|
-
"--frame-delay": "number",
|
|
385
|
-
"--time-scale": "number",
|
|
386
|
-
"--fixed-fps": "number",
|
|
387
|
-
"--max-fps": "number",
|
|
388
|
-
"--resolution": "size",
|
|
389
|
-
"--write-movie": "file",
|
|
390
|
-
"--rendering-driver": "driver",
|
|
391
|
-
"--rendering-method": "method",
|
|
392
|
-
"--accessibility": "accessibility"
|
|
393
|
-
});
|
|
394
|
-
var SET_BY_ARGUMENT = Object.freeze({
|
|
395
|
-
"--headless": "headless",
|
|
396
|
-
"--audio-driver": "silent",
|
|
397
|
-
"--quit-after": "frames, on editor_run check"
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// src/runtime-client.ts
|
|
401
|
-
function runtimeDirectory(variables = process.env) {
|
|
402
|
-
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
403
|
-
if (explicit) {
|
|
404
|
-
return explicit;
|
|
325
|
+
var MAX_DEPTH = 1024;
|
|
326
|
+
function containerType(reader, kind) {
|
|
327
|
+
if (kind === 1) {
|
|
328
|
+
reader.u32();
|
|
329
|
+
} else if (kind === 2 || kind === 3) {
|
|
330
|
+
reader.string();
|
|
405
331
|
}
|
|
406
|
-
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
407
|
-
return join2(perUser ?? tmpdir2(), "gdharness");
|
|
408
332
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
var lateAnswers = new Map;
|
|
413
|
-
|
|
414
|
-
// src/whole-file.ts
|
|
415
|
-
import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
416
|
-
var HELD_FILE_MS = 1000;
|
|
417
|
-
var HELD_FILE_RETRY_MS = 10;
|
|
418
|
-
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
419
|
-
function writeWhole(path, text) {
|
|
420
|
-
const partial = `${path}.${process.pid}.partial`;
|
|
421
|
-
try {
|
|
422
|
-
writeFileSync2(partial, text, "utf8");
|
|
423
|
-
} catch {
|
|
424
|
-
return false;
|
|
333
|
+
function decodeValue(reader, depth) {
|
|
334
|
+
if (depth > MAX_DEPTH) {
|
|
335
|
+
throw new RangeError("a value nests deeper than the engine allows");
|
|
425
336
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
337
|
+
const header = reader.u32();
|
|
338
|
+
const type = header & 255;
|
|
339
|
+
const wide = (header & FLAG_64) !== 0;
|
|
340
|
+
const math = MATH[type];
|
|
341
|
+
if (math !== undefined) {
|
|
342
|
+
return {
|
|
343
|
+
kind: math.name,
|
|
344
|
+
values: Array.from({ length: math.count }, () => math.ints === true ? reader.i32() : reader.real(wide))
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const vectorWidth = PACKED_VECTORS[type];
|
|
348
|
+
if (vectorWidth !== undefined) {
|
|
349
|
+
const count = reader.u32();
|
|
350
|
+
return Array.from({ length: count }, () => ({
|
|
351
|
+
kind: "Vector",
|
|
352
|
+
values: Array.from({ length: vectorWidth }, () => reader.real(wide))
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
switch (type) {
|
|
356
|
+
case NIL:
|
|
357
|
+
return null;
|
|
358
|
+
case BOOL:
|
|
359
|
+
return reader.u32() !== 0;
|
|
360
|
+
case INT:
|
|
361
|
+
return wide ? reader.i64() : reader.i32();
|
|
362
|
+
case FLOAT:
|
|
363
|
+
return wide ? reader.f64() : reader.f32();
|
|
364
|
+
case STRING:
|
|
365
|
+
case 21:
|
|
366
|
+
return reader.string();
|
|
367
|
+
case 20:
|
|
368
|
+
return { kind: "Color", values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()] };
|
|
369
|
+
case 22: {
|
|
370
|
+
const first = reader.u32();
|
|
371
|
+
if ((first & 2147483648) === 0) {
|
|
372
|
+
throw new RangeError("a node path in the format before 3.0");
|
|
438
373
|
}
|
|
439
|
-
|
|
374
|
+
const names = first & 2147483647;
|
|
375
|
+
let subnames = reader.u32();
|
|
376
|
+
const flags = reader.u32();
|
|
377
|
+
if ((flags & 2) !== 0) {
|
|
378
|
+
subnames += 1;
|
|
379
|
+
}
|
|
380
|
+
return { kind: "NodePath", values: Array.from({ length: names + subnames }, () => reader.string()) };
|
|
440
381
|
}
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
function runsDirectory() {
|
|
447
|
-
return join3(runtimeDirectory(), "runs");
|
|
448
|
-
}
|
|
449
|
-
function realAsFarAsItExists(path) {
|
|
450
|
-
const missing = [];
|
|
451
|
-
let at = path;
|
|
452
|
-
for (;; ) {
|
|
453
|
-
try {
|
|
454
|
-
return join3(realpathSync.native(at), ...missing);
|
|
455
|
-
} catch {
|
|
456
|
-
const parent = dirname2(at);
|
|
457
|
-
if (parent === at) {
|
|
458
|
-
return path;
|
|
382
|
+
case 23:
|
|
383
|
+
return { kind: "RID", values: [reader.u64()] };
|
|
384
|
+
case 24:
|
|
385
|
+
if ((header & FLAG_64) === 0) {
|
|
386
|
+
throw new RangeError("a full object, which this reader does not take");
|
|
459
387
|
}
|
|
460
|
-
|
|
461
|
-
|
|
388
|
+
return { kind: "Object", values: [reader.u64()] };
|
|
389
|
+
case 25:
|
|
390
|
+
return { kind: "Callable", values: [] };
|
|
391
|
+
case 26:
|
|
392
|
+
return { kind: "Signal", values: [reader.string(), reader.u64()] };
|
|
393
|
+
case 27: {
|
|
394
|
+
containerType(reader, header >> 16 & 3);
|
|
395
|
+
containerType(reader, header >> 18 & 3);
|
|
396
|
+
const count = reader.u32() & 2147483647;
|
|
397
|
+
const entries = [];
|
|
398
|
+
for (let index = 0;index < count; index += 1) {
|
|
399
|
+
const key = decodeValue(reader, depth + 1);
|
|
400
|
+
entries.push([key, decodeValue(reader, depth + 1)]);
|
|
401
|
+
}
|
|
402
|
+
return { kind: "dictionary", entries };
|
|
403
|
+
}
|
|
404
|
+
case ARRAY: {
|
|
405
|
+
containerType(reader, header >> 16 & 3);
|
|
406
|
+
const count = reader.u32() & 2147483647;
|
|
407
|
+
return Array.from({ length: count }, () => decodeValue(reader, depth + 1));
|
|
408
|
+
}
|
|
409
|
+
case 29: {
|
|
410
|
+
const count = reader.u32();
|
|
411
|
+
const bytes = [...reader.bytes(count)];
|
|
412
|
+
reader.bytes((4 - count % 4) % 4);
|
|
413
|
+
return bytes;
|
|
462
414
|
}
|
|
415
|
+
case 30:
|
|
416
|
+
return Array.from({ length: reader.u32() }, () => reader.i32());
|
|
417
|
+
case 31:
|
|
418
|
+
return Array.from({ length: reader.u32() }, () => reader.i64());
|
|
419
|
+
case 32:
|
|
420
|
+
return Array.from({ length: reader.u32() }, () => reader.f32());
|
|
421
|
+
case 33:
|
|
422
|
+
return Array.from({ length: reader.u32() }, () => reader.f64());
|
|
423
|
+
case 34:
|
|
424
|
+
return Array.from({ length: reader.u32() }, () => reader.string().replace(/\0$/, ""));
|
|
425
|
+
case 37:
|
|
426
|
+
return Array.from({ length: reader.u32() }, () => ({
|
|
427
|
+
kind: "Color",
|
|
428
|
+
values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()]
|
|
429
|
+
}));
|
|
430
|
+
default:
|
|
431
|
+
throw new RangeError(`a value of type ${type}, which Godot 4.7 does not have`);
|
|
463
432
|
}
|
|
464
433
|
}
|
|
465
|
-
function
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
434
|
+
function decodeVariant(bytes) {
|
|
435
|
+
const reader = new Reader(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
436
|
+
const value = decodeValue(reader, 0);
|
|
437
|
+
if (reader.remaining !== 0) {
|
|
438
|
+
throw new RangeError(`${reader.remaining} bytes left over after the value`);
|
|
439
|
+
}
|
|
440
|
+
return value;
|
|
469
441
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
442
|
+
function pushU32(out, value) {
|
|
443
|
+
out.push(value & 255, value >>> 8 & 255, value >>> 16 & 255, value >>> 24 & 255);
|
|
472
444
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
|
|
445
|
+
function encodeInto(out, value) {
|
|
446
|
+
if (value === null) {
|
|
447
|
+
pushU32(out, NIL);
|
|
448
|
+
} else if (typeof value === "boolean") {
|
|
449
|
+
pushU32(out, BOOL);
|
|
450
|
+
pushU32(out, value ? 1 : 0);
|
|
451
|
+
} else if (typeof value === "number") {
|
|
452
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
453
|
+
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
454
|
+
pushU32(out, INT);
|
|
455
|
+
view.setInt32(0, value, true);
|
|
456
|
+
out.push(...new Uint8Array(view.buffer, 0, 4));
|
|
457
|
+
} else if (Number.isInteger(value)) {
|
|
458
|
+
pushU32(out, INT | FLAG_64);
|
|
459
|
+
view.setBigInt64(0, BigInt(value), true);
|
|
460
|
+
out.push(...new Uint8Array(view.buffer));
|
|
461
|
+
} else {
|
|
462
|
+
pushU32(out, FLOAT | FLAG_64);
|
|
463
|
+
view.setFloat64(0, value, true);
|
|
464
|
+
out.push(...new Uint8Array(view.buffer));
|
|
465
|
+
}
|
|
466
|
+
} else if (typeof value === "string") {
|
|
467
|
+
const bytes = new TextEncoder().encode(value);
|
|
468
|
+
pushU32(out, STRING);
|
|
469
|
+
pushU32(out, bytes.length);
|
|
470
|
+
out.push(...bytes, ...Array.from({ length: (4 - bytes.length % 4) % 4 }, () => 0));
|
|
471
|
+
} else {
|
|
472
|
+
pushU32(out, ARRAY);
|
|
473
|
+
pushU32(out, value.length);
|
|
474
|
+
for (const element of value) {
|
|
475
|
+
encodeInto(out, element);
|
|
476
|
+
}
|
|
478
477
|
}
|
|
479
478
|
}
|
|
480
|
-
function
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
479
|
+
function encodeVariant(value) {
|
|
480
|
+
const out = [];
|
|
481
|
+
encodeInto(out, value);
|
|
482
|
+
return Uint8Array.from(out);
|
|
483
|
+
}
|
|
484
|
+
var MAIN_THREAD = 1;
|
|
485
|
+
function framedCommand(message, data, thread = MAIN_THREAD) {
|
|
486
|
+
const body = encodeVariant([message, thread, data]);
|
|
487
|
+
const framed = new Uint8Array(body.length + 4);
|
|
488
|
+
new DataView(framed.buffer).setUint32(0, body.length, true);
|
|
489
|
+
framed.set(body, 4);
|
|
490
|
+
return framed;
|
|
491
|
+
}
|
|
492
|
+
var MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
493
|
+
|
|
494
|
+
class DebuggerStream {
|
|
495
|
+
pending = new Uint8Array(0);
|
|
496
|
+
push(chunk) {
|
|
497
|
+
const joined = new Uint8Array(this.pending.length + chunk.length);
|
|
498
|
+
joined.set(this.pending, 0);
|
|
499
|
+
joined.set(chunk, this.pending.length);
|
|
500
|
+
const out = [];
|
|
501
|
+
let at = 0;
|
|
502
|
+
while (joined.length - at >= 4) {
|
|
503
|
+
const length = new DataView(joined.buffer, joined.byteOffset + at, 4).getUint32(0, true);
|
|
504
|
+
if (length > MOST_MESSAGE_BYTES) {
|
|
505
|
+
throw new RangeError(`a message of ${length} bytes, more than the engine sends`);
|
|
506
|
+
}
|
|
507
|
+
if (joined.length - at - 4 < length) {
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
out.push(readMessage(joined.subarray(at + 4, at + 4 + length)));
|
|
511
|
+
at += 4 + length;
|
|
512
|
+
}
|
|
513
|
+
this.pending = joined.slice(at);
|
|
514
|
+
return out;
|
|
485
515
|
}
|
|
486
|
-
const written = {
|
|
487
|
-
...record,
|
|
488
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
489
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
490
|
-
endedAt: Date.now()
|
|
491
|
-
};
|
|
492
|
-
writeWhole(path, JSON.stringify(written, null, 2));
|
|
493
516
|
}
|
|
494
|
-
function
|
|
495
|
-
let
|
|
517
|
+
function readMessage(body) {
|
|
518
|
+
let value;
|
|
496
519
|
try {
|
|
497
|
-
|
|
498
|
-
} catch {
|
|
499
|
-
return
|
|
520
|
+
value = decodeVariant(body);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return { unreadable: error instanceof Error ? error.message : String(error) };
|
|
500
523
|
}
|
|
501
|
-
if (!
|
|
502
|
-
return
|
|
524
|
+
if (!Array.isArray(value) || value.length !== 3) {
|
|
525
|
+
return { unreadable: "a message that is not [message, thread, data]" };
|
|
503
526
|
}
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const startedAt = fields["startedAt"];
|
|
508
|
-
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
509
|
-
return null;
|
|
527
|
+
const [message, thread, data] = value;
|
|
528
|
+
if (typeof message !== "string" || typeof thread !== "number" || !Array.isArray(data)) {
|
|
529
|
+
return { unreadable: "a message whose name, thread or data is the wrong type" };
|
|
510
530
|
}
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
pid,
|
|
514
|
-
transcript,
|
|
515
|
-
startedAt,
|
|
516
|
-
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
517
|
-
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
518
|
-
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
519
|
-
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
520
|
-
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
521
|
-
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
522
|
-
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
523
|
-
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
524
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
525
|
-
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
526
|
-
};
|
|
531
|
+
return { message, thread, data };
|
|
527
532
|
}
|
|
528
533
|
|
|
529
|
-
// src/
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
534
|
+
// src/game-debugger.ts
|
|
535
|
+
var PAUSE_MS = 30000;
|
|
536
|
+
var STEP_MS = 1e4;
|
|
537
|
+
var READ_MS = 5000;
|
|
538
|
+
var REPLIES = new Set([
|
|
539
|
+
"debug_enter",
|
|
540
|
+
"debug_exit",
|
|
541
|
+
"stack_dump",
|
|
542
|
+
"stack_frame_vars",
|
|
543
|
+
"stack_frame_var"
|
|
544
|
+
]);
|
|
545
|
+
var INBOX_KEPT = 1e4;
|
|
546
|
+
var OUTPUT_KEPT = 1000;
|
|
547
|
+
var TYPE_NAMES = [
|
|
548
|
+
"Nil",
|
|
549
|
+
"bool",
|
|
550
|
+
"int",
|
|
551
|
+
"float",
|
|
552
|
+
"String",
|
|
553
|
+
"Vector2",
|
|
554
|
+
"Vector2i",
|
|
555
|
+
"Rect2",
|
|
556
|
+
"Rect2i",
|
|
557
|
+
"Vector3",
|
|
558
|
+
"Vector3i",
|
|
559
|
+
"Transform2D",
|
|
560
|
+
"Vector4",
|
|
561
|
+
"Vector4i",
|
|
562
|
+
"Plane",
|
|
563
|
+
"Quaternion",
|
|
564
|
+
"AABB",
|
|
565
|
+
"Basis",
|
|
566
|
+
"Transform3D",
|
|
567
|
+
"Projection",
|
|
568
|
+
"Color",
|
|
569
|
+
"StringName",
|
|
570
|
+
"NodePath",
|
|
571
|
+
"RID",
|
|
572
|
+
"Object",
|
|
573
|
+
"Callable",
|
|
574
|
+
"Signal",
|
|
575
|
+
"Dictionary",
|
|
576
|
+
"Array",
|
|
577
|
+
"PackedByteArray",
|
|
578
|
+
"PackedInt32Array",
|
|
579
|
+
"PackedInt64Array",
|
|
580
|
+
"PackedFloat32Array",
|
|
581
|
+
"PackedFloat64Array",
|
|
582
|
+
"PackedStringArray",
|
|
583
|
+
"PackedVector2Array",
|
|
584
|
+
"PackedVector3Array",
|
|
585
|
+
"PackedColorArray",
|
|
586
|
+
"PackedVector4Array"
|
|
587
|
+
];
|
|
588
|
+
var SCOPES = ["Locals", "Members", "Globals"];
|
|
589
|
+
var ELEMENTS_SHOWN = 20;
|
|
590
|
+
function isList(value) {
|
|
591
|
+
return Array.isArray(value);
|
|
540
592
|
}
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (!path) {
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
try {
|
|
548
|
-
remove(path);
|
|
549
|
-
} catch (error) {
|
|
550
|
-
left.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
551
|
-
}
|
|
593
|
+
function shownValue(value, typeHint = "") {
|
|
594
|
+
if (value === null) {
|
|
595
|
+
return "null";
|
|
552
596
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
function discard(...paths) {
|
|
556
|
-
const left = discardWith(remove, paths);
|
|
557
|
-
for (const { path } of left) {
|
|
558
|
-
tryAgain(path, 0);
|
|
597
|
+
if (typeof value === "string") {
|
|
598
|
+
return value;
|
|
559
599
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
600
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
601
|
+
return String(value);
|
|
602
|
+
}
|
|
603
|
+
if (isList(value)) {
|
|
604
|
+
const shown = value.slice(0, ELEMENTS_SHOWN).map((each) => shownValue(each));
|
|
605
|
+
const more = value.length - shown.length;
|
|
606
|
+
return `[${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}]`;
|
|
607
|
+
}
|
|
608
|
+
if (value.kind === "dictionary" && "entries" in value) {
|
|
609
|
+
const shown = value.entries.slice(0, ELEMENTS_SHOWN).map(([key, each]) => `${shownValue(key)}: ${shownValue(each)}`);
|
|
610
|
+
const more = value.entries.length - shown.length;
|
|
611
|
+
return `{${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}}`;
|
|
612
|
+
}
|
|
613
|
+
if (!("values" in value)) {
|
|
614
|
+
return value.kind;
|
|
615
|
+
}
|
|
616
|
+
if (value.kind === "Object") {
|
|
617
|
+
return `<${typeHint === "" ? "Object" : typeHint}#${String(value.values[0])}>`;
|
|
618
|
+
}
|
|
619
|
+
return `${value.kind}(${value.values.join(", ")})`;
|
|
564
620
|
}
|
|
565
|
-
function
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
621
|
+
function framesOf(data) {
|
|
622
|
+
const frames = [];
|
|
623
|
+
for (let at = 1;at + 2 < data.length; at += 3) {
|
|
624
|
+
const [script, line, name] = [data[at], data[at + 1], data[at + 2]];
|
|
625
|
+
if (typeof script !== "string" || typeof line !== "number" || typeof name !== "string") {
|
|
626
|
+
break;
|
|
569
627
|
}
|
|
570
|
-
|
|
571
|
-
atExit.delete(path);
|
|
572
|
-
lastTryOnExit();
|
|
628
|
+
frames.push({ id: frames.length, name, line, script });
|
|
573
629
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
630
|
+
return frames;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
class GameDebugger {
|
|
634
|
+
connection = null;
|
|
635
|
+
held = null;
|
|
636
|
+
ended = false;
|
|
637
|
+
waiters = [];
|
|
638
|
+
inbox = [];
|
|
639
|
+
onClosed = [];
|
|
640
|
+
output = [];
|
|
641
|
+
outputDropped = 0;
|
|
642
|
+
queue = Promise.resolve();
|
|
643
|
+
server;
|
|
644
|
+
port;
|
|
645
|
+
breakpoints;
|
|
646
|
+
observers;
|
|
647
|
+
constructor(server, port, breakpoints, observers) {
|
|
648
|
+
this.server = server;
|
|
649
|
+
this.port = port;
|
|
650
|
+
this.breakpoints = breakpoints;
|
|
651
|
+
this.observers = observers;
|
|
581
652
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
653
|
+
static async open(breakpoints, observers = []) {
|
|
654
|
+
const server = createServer();
|
|
655
|
+
await new Promise((resolve, reject) => {
|
|
656
|
+
server.once("error", reject);
|
|
657
|
+
server.listen(0, "127.0.0.1", () => {
|
|
658
|
+
resolve();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
const address = server.address();
|
|
662
|
+
if (address === null || typeof address === "string") {
|
|
663
|
+
server.close();
|
|
664
|
+
throw new Error("the debugger has no port");
|
|
588
665
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return;
|
|
666
|
+
const peer = new GameDebugger(server, address.port, breakpoints, observers);
|
|
667
|
+
server.on("connection", (socket) => {
|
|
668
|
+
peer.accept(socket);
|
|
669
|
+
});
|
|
670
|
+
return peer;
|
|
595
671
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
672
|
+
get engineArguments() {
|
|
673
|
+
const listed = this.breakpoints.map((one) => one.replaceAll(" ", "%20")).join(",");
|
|
674
|
+
return [
|
|
675
|
+
"--remote-debug",
|
|
676
|
+
`tcp://127.0.0.1:${this.port}`,
|
|
677
|
+
"--ignore-error-breaks",
|
|
678
|
+
...listed === "" ? [] : ["--breakpoints", listed]
|
|
679
|
+
];
|
|
680
|
+
}
|
|
681
|
+
accept(socket) {
|
|
682
|
+
if (this.connection !== null) {
|
|
683
|
+
socket.destroy();
|
|
684
|
+
return;
|
|
602
685
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
"namespace",
|
|
643
|
-
"not",
|
|
644
|
-
"null",
|
|
645
|
-
"or",
|
|
646
|
-
"pass",
|
|
647
|
-
"preload",
|
|
648
|
-
"return",
|
|
649
|
-
"self",
|
|
650
|
-
"signal",
|
|
651
|
-
"static",
|
|
652
|
-
"super",
|
|
653
|
-
"trait",
|
|
654
|
-
"true",
|
|
655
|
-
"var",
|
|
656
|
-
"void",
|
|
657
|
-
"when",
|
|
658
|
-
"while",
|
|
659
|
-
"yield",
|
|
660
|
-
"INF",
|
|
661
|
-
"NAN",
|
|
662
|
-
"PI",
|
|
663
|
-
"TAU"
|
|
664
|
-
]);
|
|
665
|
-
|
|
666
|
-
// src/godot-variant.ts
|
|
667
|
-
var NIL = 0;
|
|
668
|
-
var BOOL = 1;
|
|
669
|
-
var INT = 2;
|
|
670
|
-
var FLOAT = 3;
|
|
671
|
-
var STRING = 4;
|
|
672
|
-
var ARRAY = 28;
|
|
673
|
-
var FLAG_64 = 1 << 16;
|
|
674
|
-
var MATH = {
|
|
675
|
-
5: { name: "Vector2", count: 2 },
|
|
676
|
-
6: { name: "Vector2i", count: 2, ints: true },
|
|
677
|
-
7: { name: "Rect2", count: 4 },
|
|
678
|
-
8: { name: "Rect2i", count: 4, ints: true },
|
|
679
|
-
9: { name: "Vector3", count: 3 },
|
|
680
|
-
10: { name: "Vector3i", count: 3, ints: true },
|
|
681
|
-
11: { name: "Transform2D", count: 6 },
|
|
682
|
-
12: { name: "Vector4", count: 4 },
|
|
683
|
-
13: { name: "Vector4i", count: 4, ints: true },
|
|
684
|
-
14: { name: "Plane", count: 4 },
|
|
685
|
-
15: { name: "Quaternion", count: 4 },
|
|
686
|
-
16: { name: "AABB", count: 6 },
|
|
687
|
-
17: { name: "Basis", count: 9 },
|
|
688
|
-
18: { name: "Transform3D", count: 12 },
|
|
689
|
-
19: { name: "Projection", count: 16 }
|
|
690
|
-
};
|
|
691
|
-
var PACKED_VECTORS = { 35: 2, 36: 3, 38: 4 };
|
|
692
|
-
|
|
693
|
-
class Reader {
|
|
694
|
-
offset = 0;
|
|
695
|
-
view;
|
|
696
|
-
constructor(view) {
|
|
697
|
-
this.view = view;
|
|
686
|
+
this.connection = socket;
|
|
687
|
+
this.server.close();
|
|
688
|
+
const send = (message, data) => {
|
|
689
|
+
this.send(message, data);
|
|
690
|
+
};
|
|
691
|
+
for (const observer of this.observers) {
|
|
692
|
+
observer.connected(send);
|
|
693
|
+
}
|
|
694
|
+
const stream = new DebuggerStream;
|
|
695
|
+
socket.on("data", (chunk) => {
|
|
696
|
+
let messages;
|
|
697
|
+
try {
|
|
698
|
+
messages = stream.push(chunk);
|
|
699
|
+
} catch {
|
|
700
|
+
socket.destroy();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
for (const received of messages) {
|
|
704
|
+
if ("message" in received) {
|
|
705
|
+
this.take(received);
|
|
706
|
+
for (const observer of this.observers) {
|
|
707
|
+
observer.message(received, send);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
socket.on("error", () => {
|
|
713
|
+
return;
|
|
714
|
+
});
|
|
715
|
+
socket.on("close", () => {
|
|
716
|
+
this.ended = true;
|
|
717
|
+
this.held = null;
|
|
718
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
719
|
+
waiter.resolve(null);
|
|
720
|
+
}
|
|
721
|
+
for (const done of this.onClosed.splice(0)) {
|
|
722
|
+
done();
|
|
723
|
+
}
|
|
724
|
+
});
|
|
698
725
|
}
|
|
699
|
-
|
|
700
|
-
|
|
726
|
+
drained(ms) {
|
|
727
|
+
if (this.connection === null || this.ended) {
|
|
728
|
+
return Promise.resolve();
|
|
729
|
+
}
|
|
730
|
+
return new Promise((resolve) => {
|
|
731
|
+
const timer = setTimeout(resolve, ms);
|
|
732
|
+
this.onClosed.push(() => {
|
|
733
|
+
clearTimeout(timer);
|
|
734
|
+
resolve();
|
|
735
|
+
});
|
|
736
|
+
});
|
|
701
737
|
}
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
738
|
+
take(received) {
|
|
739
|
+
if (received.message === "debug_enter") {
|
|
740
|
+
const [canContinue, reason, hasStack] = received.data;
|
|
741
|
+
this.held = {
|
|
742
|
+
thread: received.thread,
|
|
743
|
+
canContinue: canContinue === true,
|
|
744
|
+
reason: typeof reason === "string" ? reason : "",
|
|
745
|
+
hasStack: hasStack === true
|
|
746
|
+
};
|
|
747
|
+
} else if (received.message === "debug_exit" && this.held?.thread === received.thread) {
|
|
748
|
+
this.held = null;
|
|
749
|
+
} else if (received.message === "output") {
|
|
750
|
+
const [strings] = received.data;
|
|
751
|
+
if (Array.isArray(strings)) {
|
|
752
|
+
for (const each of strings) {
|
|
753
|
+
if (typeof each === "string") {
|
|
754
|
+
this.output.push(...each.split(`
|
|
755
|
+
`));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const over = this.output.length - OUTPUT_KEPT;
|
|
759
|
+
if (over > 0) {
|
|
760
|
+
this.output.splice(0, over);
|
|
761
|
+
this.outputDropped += over;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const at = this.waiters.findIndex((waiter) => waiter.wanted(received));
|
|
766
|
+
if (at !== -1) {
|
|
767
|
+
this.waiters.splice(at, 1)[0]?.resolve(received);
|
|
768
|
+
} else if (REPLIES.has(received.message)) {
|
|
769
|
+
this.inbox.push(received);
|
|
770
|
+
if (this.inbox.length > INBOX_KEPT) {
|
|
771
|
+
this.inbox.shift();
|
|
772
|
+
}
|
|
705
773
|
}
|
|
706
774
|
}
|
|
707
|
-
|
|
708
|
-
this.
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
775
|
+
forget(message) {
|
|
776
|
+
for (let at = this.inbox.length - 1;at >= 0; at -= 1) {
|
|
777
|
+
const kept = this.inbox[at];
|
|
778
|
+
if (kept !== undefined && "message" in kept && kept.message === message) {
|
|
779
|
+
this.inbox.splice(at, 1);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
712
782
|
}
|
|
713
|
-
|
|
714
|
-
this.
|
|
715
|
-
const value = this.view.getInt32(this.offset, true);
|
|
716
|
-
this.offset += 4;
|
|
717
|
-
return value;
|
|
783
|
+
send(message, data, thread = MAIN_THREAD) {
|
|
784
|
+
this.connection?.write(framedCommand(message, data, thread));
|
|
718
785
|
}
|
|
719
|
-
|
|
720
|
-
this.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
786
|
+
next(wanted, ms) {
|
|
787
|
+
const kept = this.inbox.findIndex(wanted);
|
|
788
|
+
if (kept !== -1) {
|
|
789
|
+
return Promise.resolve(this.inbox.splice(kept, 1)[0] ?? null);
|
|
790
|
+
}
|
|
791
|
+
if (this.ended) {
|
|
792
|
+
return Promise.resolve(null);
|
|
793
|
+
}
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
const waiter = {
|
|
796
|
+
wanted,
|
|
797
|
+
resolve: (received) => {
|
|
798
|
+
clearTimeout(timer);
|
|
799
|
+
resolve(received);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const timer = setTimeout(() => {
|
|
803
|
+
const at = this.waiters.indexOf(waiter);
|
|
804
|
+
if (at !== -1) {
|
|
805
|
+
this.waiters.splice(at, 1);
|
|
806
|
+
}
|
|
807
|
+
resolve(null);
|
|
808
|
+
}, ms);
|
|
809
|
+
this.waiters.push(waiter);
|
|
810
|
+
});
|
|
724
811
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const value = Number(this.view.getBigUint64(this.offset, true));
|
|
728
|
-
this.offset += 8;
|
|
729
|
-
return value;
|
|
812
|
+
static named(message) {
|
|
813
|
+
return (received) => ("message" in received) && received.message === message;
|
|
730
814
|
}
|
|
731
|
-
|
|
732
|
-
this.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
815
|
+
ask(request) {
|
|
816
|
+
const answered = this.queue.then(() => this.answer(request));
|
|
817
|
+
this.queue = answered.catch(() => {
|
|
818
|
+
return;
|
|
819
|
+
});
|
|
820
|
+
return answered;
|
|
821
|
+
}
|
|
822
|
+
async answer(request) {
|
|
823
|
+
if (request.op === "output") {
|
|
824
|
+
return {
|
|
825
|
+
output: [...this.output],
|
|
826
|
+
...this.outputDropped > 0 ? { notShown: this.outputDropped } : {}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (request.op === "state") {
|
|
830
|
+
return { connected: this.connection !== null && !this.ended, held: this.heldState() };
|
|
831
|
+
}
|
|
832
|
+
if (this.connection === null) {
|
|
833
|
+
return {
|
|
834
|
+
refused: "The game never connected to its debugger: it dials once as it starts, and a game that has not got that far, or that was started without a debugger, has none."
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (this.ended) {
|
|
838
|
+
return { refused: "The game has closed its debugger connection, which it does as it exits." };
|
|
839
|
+
}
|
|
840
|
+
switch (request.op) {
|
|
841
|
+
case "breakpoint":
|
|
842
|
+
this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
|
|
843
|
+
return { sent: true };
|
|
844
|
+
case "pause":
|
|
845
|
+
return await this.pause();
|
|
846
|
+
case "resume":
|
|
847
|
+
return await this.resume(request.how);
|
|
848
|
+
case "stack": {
|
|
849
|
+
const frames = await this.stack();
|
|
850
|
+
if (frames === null) {
|
|
851
|
+
return this.notHeld();
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
held: this.heldState(),
|
|
855
|
+
stack: frames,
|
|
856
|
+
...frames.length === 0 ? {
|
|
857
|
+
note: "The game is held between frames, where no script runs, so there is no stack: debug_control step_over takes it to the next line of script it runs."
|
|
858
|
+
} : {}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
case "variables":
|
|
862
|
+
return await this.variables(request.frame);
|
|
863
|
+
default:
|
|
864
|
+
return { refused: `Nothing here answers ${JSON.stringify(request)}.` };
|
|
865
|
+
}
|
|
736
866
|
}
|
|
737
|
-
|
|
738
|
-
this.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return
|
|
867
|
+
heldState() {
|
|
868
|
+
if (this.held === null) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
thread: this.held.thread,
|
|
873
|
+
...this.held.reason === "" ? {} : { reason: this.held.reason },
|
|
874
|
+
...this.held.canContinue ? {} : { canContinue: false }
|
|
875
|
+
};
|
|
742
876
|
}
|
|
743
|
-
|
|
744
|
-
return
|
|
877
|
+
notHeld() {
|
|
878
|
+
return {
|
|
879
|
+
refused: "The game is running, not held, so it has no stack to read: debug_control pause stops it where it is, and a breakpoint stops it on its line."
|
|
880
|
+
};
|
|
745
881
|
}
|
|
746
|
-
|
|
747
|
-
this.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
882
|
+
async stack() {
|
|
883
|
+
const held = this.held;
|
|
884
|
+
if (held === null) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
this.forget("stack_dump");
|
|
888
|
+
this.send("get_stack_dump", [], held.thread);
|
|
889
|
+
const dumped = await this.next(GameDebugger.named("stack_dump"), READ_MS);
|
|
890
|
+
return dumped !== null && "data" in dumped ? framesOf(dumped.data) : null;
|
|
891
|
+
}
|
|
892
|
+
async pause() {
|
|
893
|
+
if (this.held === null) {
|
|
894
|
+
this.forget("debug_enter");
|
|
895
|
+
this.send("break", []);
|
|
896
|
+
const entered = await this.next(GameDebugger.named("debug_enter"), PAUSE_MS);
|
|
897
|
+
if (entered === null) {
|
|
898
|
+
return {
|
|
899
|
+
paused: false,
|
|
900
|
+
note: `The game has been asked to stop and had not within ${PAUSE_MS / 1000}s. It takes the request between frames, and in a script every 2048 lines it runs, so its main thread is in a loop that sleeps between few lines, or inside one long engine call or a native extension's own loop. The request stays with the game, which stops when it next reaches that point; debug_state stack reads where once it has.`
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const held = this.held;
|
|
905
|
+
if (held === null) {
|
|
906
|
+
return { refused: "The game stopped and was let go again before its stack could be read." };
|
|
907
|
+
}
|
|
908
|
+
if (!held.hasStack) {
|
|
909
|
+
this.forget("debug_enter");
|
|
910
|
+
this.send("next", [], held.thread);
|
|
911
|
+
const again = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
912
|
+
if (again === null) {
|
|
913
|
+
return {
|
|
914
|
+
paused: false,
|
|
915
|
+
note: `The game stopped between frames, where no script runs, so it was let go to stop at the next line of script it runs, and none ran within ${STEP_MS / 1000}s. It stops there when one does; debug_state stack reads it then.`
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
this.stoppedBy("Paused");
|
|
920
|
+
const frames = await this.stack();
|
|
921
|
+
return { paused: true, held: this.heldState(), stack: frames ?? [] };
|
|
751
922
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
923
|
+
stoppedBy(cause) {
|
|
924
|
+
if (this.held !== null) {
|
|
925
|
+
this.held = { ...this.held, reason: cause };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async resume(how) {
|
|
929
|
+
const held = this.held;
|
|
930
|
+
if (held === null) {
|
|
931
|
+
return {
|
|
932
|
+
refused: "The game is running, not held, so there is nothing to continue or step from: debug_control pause stops it, and a breakpoint stops it on its line."
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (how === "continue") {
|
|
936
|
+
this.forget("debug_exit");
|
|
937
|
+
const exited = this.next((received) => ("message" in received) && received.message === "debug_exit" && received.thread === held.thread, READ_MS);
|
|
938
|
+
this.send("continue", [], held.thread);
|
|
939
|
+
return await exited === null ? { refused: `The game did not say it had gone on within ${READ_MS / 1000}s.` } : { continued: true };
|
|
940
|
+
}
|
|
941
|
+
this.forget("debug_enter");
|
|
942
|
+
this.send(how, [], held.thread);
|
|
943
|
+
const entered = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
944
|
+
if (entered === null) {
|
|
945
|
+
return {
|
|
946
|
+
stepped: true,
|
|
947
|
+
heldAgain: false,
|
|
948
|
+
note: `The game had not stopped again ${STEP_MS / 1000}s after the step: the step is still running, as one out of a function that never returns does, or it went on without reaching another line. debug_state stack answers once it stops, and debug_control pause stops it wherever it is.`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
this.stoppedBy("Stepped");
|
|
952
|
+
return { stepped: true, heldAgain: true, held: this.heldState(), stack: await this.stack() ?? [] };
|
|
953
|
+
}
|
|
954
|
+
async variables(frame) {
|
|
955
|
+
const held = this.held;
|
|
956
|
+
if (held === null) {
|
|
957
|
+
return this.notHeld();
|
|
958
|
+
}
|
|
959
|
+
const frames = await this.stack();
|
|
960
|
+
if (frames === null) {
|
|
961
|
+
return { refused: `The game did not send its stack within ${READ_MS / 1000}s.` };
|
|
962
|
+
}
|
|
963
|
+
if (frames.find((each) => each.id === frame) === undefined) {
|
|
964
|
+
return {
|
|
965
|
+
refused: frames.length === 0 ? "The game is held where no script runs, so it has no frames: debug_control step_over takes it to the next line of script." : `The stack has no frame ${frame}; its frames are ${frames.map((each) => each.id).join(", ")}.`
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
this.forget("stack_frame_vars");
|
|
969
|
+
this.forget("stack_frame_var");
|
|
970
|
+
this.send("get_stack_frame_vars", [frame], held.thread);
|
|
971
|
+
const counted = await this.next(GameDebugger.named("stack_frame_vars"), READ_MS);
|
|
972
|
+
const count = counted !== null && "data" in counted ? counted.data[0] : undefined;
|
|
973
|
+
if (typeof count !== "number") {
|
|
974
|
+
return { refused: `The game did not send the frame's values within ${READ_MS / 1000}s.` };
|
|
975
|
+
}
|
|
976
|
+
const scopes = SCOPES.map((name) => ({
|
|
977
|
+
name,
|
|
978
|
+
variables: []
|
|
979
|
+
}));
|
|
980
|
+
for (let index = 0;index < count; index += 1) {
|
|
981
|
+
const one = await this.next(GameDebugger.named("stack_frame_var"), READ_MS);
|
|
982
|
+
if (one === null || !("data" in one)) {
|
|
983
|
+
return {
|
|
984
|
+
scopes,
|
|
985
|
+
valuesMissing: count - index,
|
|
986
|
+
note: `${count - index} of the frame's ${count} values did not arrive within ${READ_MS / 1000}s, so the scopes are not whole. Ask again.`
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const [name, scope, type, value, hint] = one.data;
|
|
990
|
+
const into = scopes[typeof scope === "number" ? scope : 0];
|
|
991
|
+
into?.variables.push({
|
|
992
|
+
name: typeof name === "string" ? name : shownValue(name ?? null),
|
|
993
|
+
value: shownValue(value ?? null, typeof hint === "string" ? hint : ""),
|
|
994
|
+
type: typeof hint === "string" && hint !== "" ? hint : typeof type === "number" ? TYPE_NAMES[type] ?? String(type) : "Variant"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return { held: this.heldState(), frame, scopes };
|
|
998
|
+
}
|
|
999
|
+
close() {
|
|
1000
|
+
this.connection?.destroy();
|
|
1001
|
+
this.server.close();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/keeper-channel.ts
|
|
1006
|
+
import { randomUUID } from "node:crypto";
|
|
1007
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
1008
|
+
import { connect, createServer as createServer2 } from "node:net";
|
|
1009
|
+
import { tmpdir } from "node:os";
|
|
1010
|
+
import { join } from "node:path";
|
|
1011
|
+
import process2 from "node:process";
|
|
1012
|
+
var DEBUG_PREFIX = "debug ";
|
|
1013
|
+
function endedOrWhyNot(end) {
|
|
1014
|
+
try {
|
|
1015
|
+
return end();
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
return `could not end it: ${error instanceof Error ? error.message : String(error)}`.replaceAll(`
|
|
1018
|
+
`, " ");
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function debuggedOrWhyNot(debug, asked) {
|
|
1022
|
+
if (debug === null) {
|
|
1023
|
+
return JSON.stringify({ refused: "This run has no debugger." });
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
return JSON.stringify(await debug(JSON.parse(asked)));
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
return JSON.stringify({ refused: `The debugger could not answer: ${errorText(error)}` });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function errorText(error) {
|
|
1032
|
+
return (error instanceof Error ? error.message : String(error)).replaceAll(`
|
|
1033
|
+
`, " ");
|
|
1034
|
+
}
|
|
1035
|
+
async function listenForTheServer(end, debug) {
|
|
1036
|
+
let directory = null;
|
|
1037
|
+
try {
|
|
1038
|
+
directory = process2.platform === "win32" ? null : mkdtempSync(join(tmpdir(), "gdharness-keeper-"));
|
|
1039
|
+
const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join(directory, "keeper.sock");
|
|
1040
|
+
const server = createServer2((socket) => {
|
|
1041
|
+
socket.setEncoding("utf8");
|
|
1042
|
+
socket.on("error", () => {});
|
|
1043
|
+
let asked = "";
|
|
1044
|
+
let answering = false;
|
|
1045
|
+
socket.on("data", (chunk) => {
|
|
1046
|
+
asked += chunk;
|
|
1047
|
+
const line = asked.split(`
|
|
1048
|
+
`)[0] ?? "";
|
|
1049
|
+
if (answering || !asked.includes(`
|
|
1050
|
+
`)) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
answering = true;
|
|
1054
|
+
if (line === "stop") {
|
|
1055
|
+
socket.end(`${endedOrWhyNot(end)}
|
|
1056
|
+
`);
|
|
1057
|
+
} else if (line.startsWith(DEBUG_PREFIX)) {
|
|
1058
|
+
debuggedOrWhyNot(debug, line.slice(DEBUG_PREFIX.length)).then((answer) => {
|
|
1059
|
+
socket.end(`${answer}
|
|
1060
|
+
`);
|
|
1061
|
+
});
|
|
1062
|
+
} else {
|
|
1063
|
+
socket.end(`unknown
|
|
1064
|
+
`);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
await listening(server, address);
|
|
1069
|
+
const made = directory;
|
|
1070
|
+
return {
|
|
1071
|
+
address,
|
|
1072
|
+
close: () => {
|
|
1073
|
+
server.close();
|
|
1074
|
+
if (made !== null) {
|
|
1075
|
+
rmSync(made, { recursive: true, force: true });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
} catch {
|
|
1080
|
+
if (directory !== null) {
|
|
1081
|
+
rmSync(directory, { recursive: true, force: true });
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function listening(server, address) {
|
|
1087
|
+
return new Promise((resolve, reject) => {
|
|
1088
|
+
server.once("error", reject);
|
|
1089
|
+
server.listen(address, () => {
|
|
1090
|
+
server.off("error", reject);
|
|
1091
|
+
resolve();
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/outside.ts
|
|
1097
|
+
import { fileURLToPath } from "node:url";
|
|
1098
|
+
function withChanges(base, changes) {
|
|
1099
|
+
const env = { ...base };
|
|
1100
|
+
for (const [name, value] of Object.entries(changes)) {
|
|
1101
|
+
if (value === null) {
|
|
1102
|
+
delete env[name];
|
|
1103
|
+
} else {
|
|
1104
|
+
env[name] = value;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return env;
|
|
1108
|
+
}
|
|
1109
|
+
function sendSpec(child, spec) {
|
|
1110
|
+
child.stdin?.end(JSON.stringify(spec), "utf8");
|
|
1111
|
+
}
|
|
1112
|
+
async function receivedSpec() {
|
|
1113
|
+
const chunks = [];
|
|
1114
|
+
for await (const chunk of process.stdin) {
|
|
1115
|
+
chunks.push(chunk);
|
|
1116
|
+
}
|
|
1117
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1118
|
+
}
|
|
1119
|
+
var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
|
|
1120
|
+
function readLaunched(printed) {
|
|
1121
|
+
const line = printed.split(`
|
|
1122
|
+
`).find((one) => one.trim() !== "")?.trim() ?? "";
|
|
1123
|
+
const pid = /^pid (\d+)$/.exec(line);
|
|
1124
|
+
if (pid?.[1] !== undefined) {
|
|
1125
|
+
return { pid: Number(pid[1]) };
|
|
757
1126
|
}
|
|
1127
|
+
return { error: line.replace(/^error /, "") || "the launcher said nothing" };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/run-record.ts
|
|
1131
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1132
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1133
|
+
import { mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync as rmSync3, statSync } from "node:fs";
|
|
1134
|
+
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
1135
|
+
import { promisify as promisify2 } from "node:util";
|
|
1136
|
+
|
|
1137
|
+
// src/process-children.ts
|
|
1138
|
+
import { execFile } from "node:child_process";
|
|
1139
|
+
import { promisify } from "node:util";
|
|
1140
|
+
|
|
1141
|
+
// src/windows-ask.ts
|
|
1142
|
+
var SCRIPT = [
|
|
1143
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
1144
|
+
"$in = [Console]::In",
|
|
1145
|
+
"while ($null -ne ($line = $in.ReadLine())) {",
|
|
1146
|
+
' $parts = $line.Split(" ", 2)',
|
|
1147
|
+
" try {",
|
|
1148
|
+
" $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
|
|
1149
|
+
" $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
|
|
1150
|
+
' $tag = "OK"',
|
|
1151
|
+
" } catch {",
|
|
1152
|
+
" $said = $_.Exception.Message",
|
|
1153
|
+
' $tag = "ERR"',
|
|
1154
|
+
" }",
|
|
1155
|
+
' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
|
|
1156
|
+
" [Console]::Out.Flush()",
|
|
1157
|
+
"}"
|
|
1158
|
+
].join(`
|
|
1159
|
+
`);
|
|
1160
|
+
|
|
1161
|
+
// src/process-children.ts
|
|
1162
|
+
var run = promisify(execFile);
|
|
1163
|
+
|
|
1164
|
+
// src/runtime-client.ts
|
|
1165
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1166
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
1167
|
+
|
|
1168
|
+
// src/launch.ts
|
|
1169
|
+
function envValue(name, variables = process.env) {
|
|
1170
|
+
const value = variables[name];
|
|
1171
|
+
return value === undefined || value === "" ? undefined : value;
|
|
758
1172
|
}
|
|
759
|
-
var
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1173
|
+
var ENGINE_FLAGS = Object.freeze({
|
|
1174
|
+
"--verbose": "none",
|
|
1175
|
+
"--quiet": "none",
|
|
1176
|
+
"--debug-collisions": "none",
|
|
1177
|
+
"--debug-paths": "none",
|
|
1178
|
+
"--debug-navigation": "none",
|
|
1179
|
+
"--debug-avoidance": "none",
|
|
1180
|
+
"--debug-stringnames": "none",
|
|
1181
|
+
"--debug-canvas-item-redraw": "none",
|
|
1182
|
+
"--debug-mute-audio": "none",
|
|
1183
|
+
"--print-fps": "none",
|
|
1184
|
+
"--gpu-profile": "none",
|
|
1185
|
+
"--gpu-validation": "none",
|
|
1186
|
+
"--gpu-abort": "none",
|
|
1187
|
+
"--disable-vsync": "none",
|
|
1188
|
+
"--frame-delay": "number",
|
|
1189
|
+
"--time-scale": "number",
|
|
1190
|
+
"--fixed-fps": "number",
|
|
1191
|
+
"--max-fps": "number",
|
|
1192
|
+
"--resolution": "size",
|
|
1193
|
+
"--write-movie": "file",
|
|
1194
|
+
"--rendering-driver": "driver",
|
|
1195
|
+
"--rendering-method": "method",
|
|
1196
|
+
"--accessibility": "accessibility"
|
|
1197
|
+
});
|
|
1198
|
+
var SET_BY_ARGUMENT = Object.freeze({
|
|
1199
|
+
"--headless": "headless",
|
|
1200
|
+
"--audio-driver": "silent",
|
|
1201
|
+
"--quit-after": "frames, on editor_run check"
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// src/runtime-client.ts
|
|
1205
|
+
function runtimeDirectory(variables = process.env) {
|
|
1206
|
+
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
1207
|
+
if (explicit) {
|
|
1208
|
+
return explicit;
|
|
765
1209
|
}
|
|
1210
|
+
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
1211
|
+
return join2(perUser ?? tmpdir2(), "gdharness");
|
|
766
1212
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const count = reader.u32();
|
|
784
|
-
return Array.from({ length: count }, () => ({
|
|
785
|
-
kind: "Vector",
|
|
786
|
-
values: Array.from({ length: vectorWidth }, () => reader.real(wide))
|
|
787
|
-
}));
|
|
1213
|
+
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
1214
|
+
var verdicts = new Map;
|
|
1215
|
+
var LATE_ANSWER_MS = 30 * 60000;
|
|
1216
|
+
var lateAnswers = new Map;
|
|
1217
|
+
|
|
1218
|
+
// src/whole-file.ts
|
|
1219
|
+
import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1220
|
+
var HELD_FILE_MS = 1000;
|
|
1221
|
+
var HELD_FILE_RETRY_MS = 10;
|
|
1222
|
+
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
1223
|
+
function writeWhole(path, text) {
|
|
1224
|
+
const partial = `${path}.${process.pid}.partial`;
|
|
1225
|
+
try {
|
|
1226
|
+
writeFileSync2(partial, text, "utf8");
|
|
1227
|
+
} catch {
|
|
1228
|
+
return false;
|
|
788
1229
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
return
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
case 20:
|
|
802
|
-
return { kind: "Color", values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()] };
|
|
803
|
-
case 22: {
|
|
804
|
-
const first = reader.u32();
|
|
805
|
-
if ((first & 2147483648) === 0) {
|
|
806
|
-
throw new RangeError("a node path in the format before 3.0");
|
|
807
|
-
}
|
|
808
|
-
const names = first & 2147483647;
|
|
809
|
-
let subnames = reader.u32();
|
|
810
|
-
const flags = reader.u32();
|
|
811
|
-
if ((flags & 2) !== 0) {
|
|
812
|
-
subnames += 1;
|
|
1230
|
+
const until = Date.now() + HELD_FILE_MS;
|
|
1231
|
+
for (;; ) {
|
|
1232
|
+
try {
|
|
1233
|
+
renameSync(partial, path);
|
|
1234
|
+
return true;
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
const code = error.code ?? "";
|
|
1237
|
+
if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
|
|
1238
|
+
try {
|
|
1239
|
+
rmSync2(partial, { force: true });
|
|
1240
|
+
} catch {}
|
|
1241
|
+
return false;
|
|
813
1242
|
}
|
|
814
|
-
|
|
1243
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
815
1244
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
entries.push([key, decodeValue(reader, depth + 1)]);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/run-record.ts
|
|
1249
|
+
var execFileAsync = promisify2(execFile2);
|
|
1250
|
+
function runsDirectory() {
|
|
1251
|
+
return join3(runtimeDirectory(), "runs");
|
|
1252
|
+
}
|
|
1253
|
+
function realAsFarAsItExists(path) {
|
|
1254
|
+
const missing = [];
|
|
1255
|
+
let at = path;
|
|
1256
|
+
for (;; ) {
|
|
1257
|
+
try {
|
|
1258
|
+
return join3(realpathSync.native(at), ...missing);
|
|
1259
|
+
} catch {
|
|
1260
|
+
const parent = dirname2(at);
|
|
1261
|
+
if (parent === at) {
|
|
1262
|
+
return path;
|
|
835
1263
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
case ARRAY: {
|
|
839
|
-
containerType(reader, header >> 16 & 3);
|
|
840
|
-
const count = reader.u32() & 2147483647;
|
|
841
|
-
return Array.from({ length: count }, () => decodeValue(reader, depth + 1));
|
|
842
|
-
}
|
|
843
|
-
case 29: {
|
|
844
|
-
const count = reader.u32();
|
|
845
|
-
const bytes = [...reader.bytes(count)];
|
|
846
|
-
reader.bytes((4 - count % 4) % 4);
|
|
847
|
-
return bytes;
|
|
1264
|
+
missing.unshift(basename(at));
|
|
1265
|
+
at = parent;
|
|
848
1266
|
}
|
|
849
|
-
case 30:
|
|
850
|
-
return Array.from({ length: reader.u32() }, () => reader.i32());
|
|
851
|
-
case 31:
|
|
852
|
-
return Array.from({ length: reader.u32() }, () => reader.i64());
|
|
853
|
-
case 32:
|
|
854
|
-
return Array.from({ length: reader.u32() }, () => reader.f32());
|
|
855
|
-
case 33:
|
|
856
|
-
return Array.from({ length: reader.u32() }, () => reader.f64());
|
|
857
|
-
case 34:
|
|
858
|
-
return Array.from({ length: reader.u32() }, () => reader.string().replace(/\0$/, ""));
|
|
859
|
-
case 37:
|
|
860
|
-
return Array.from({ length: reader.u32() }, () => ({
|
|
861
|
-
kind: "Color",
|
|
862
|
-
values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()]
|
|
863
|
-
}));
|
|
864
|
-
default:
|
|
865
|
-
throw new RangeError(`a value of type ${type}, which Godot 4.7 does not have`);
|
|
866
1267
|
}
|
|
867
1268
|
}
|
|
868
|
-
function
|
|
869
|
-
const
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
1269
|
+
function projectKey(projectPath) {
|
|
1270
|
+
const real = realAsFarAsItExists(resolve2(projectPath)).replace(/[\\/]+$/, "");
|
|
1271
|
+
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
1272
|
+
return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
1273
|
+
}
|
|
1274
|
+
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
1275
|
+
return join3(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
1276
|
+
}
|
|
1277
|
+
function writeRunRecord(record) {
|
|
1278
|
+
mkdirSync(runsDirectory(), { recursive: true });
|
|
1279
|
+
const path = runRecordPath(record.projectPath);
|
|
1280
|
+
if (!writeWhole(path, JSON.stringify(record, null, 2))) {
|
|
1281
|
+
throw new Error(`the run note could not be written at ${path}`);
|
|
873
1282
|
}
|
|
874
|
-
return value;
|
|
875
1283
|
}
|
|
876
|
-
function
|
|
877
|
-
|
|
1284
|
+
function recordRunEnded(projectPath, pid, ending) {
|
|
1285
|
+
const path = runRecordPath(projectPath);
|
|
1286
|
+
const record = recordAt(path);
|
|
1287
|
+
if (record === null || record.pid !== pid) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const written = {
|
|
1291
|
+
...record,
|
|
1292
|
+
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
1293
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
1294
|
+
endedAt: Date.now()
|
|
1295
|
+
};
|
|
1296
|
+
writeWhole(path, JSON.stringify(written, null, 2));
|
|
1297
|
+
}
|
|
1298
|
+
function recordAt(path) {
|
|
1299
|
+
let parsed;
|
|
1300
|
+
try {
|
|
1301
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1302
|
+
} catch {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
const fields = parsed;
|
|
1309
|
+
const pid = fields["pid"];
|
|
1310
|
+
const transcript = fields["transcript"];
|
|
1311
|
+
const startedAt = fields["startedAt"];
|
|
1312
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
const args = fields["arguments"];
|
|
1316
|
+
return {
|
|
1317
|
+
pid,
|
|
1318
|
+
transcript,
|
|
1319
|
+
startedAt,
|
|
1320
|
+
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
1321
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
1322
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
1323
|
+
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
1324
|
+
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
1325
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
1326
|
+
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
1327
|
+
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
1328
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
1329
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/scratch.ts
|
|
1334
|
+
import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
|
|
1335
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1336
|
+
import { join as join4 } from "node:path";
|
|
1337
|
+
import process3 from "node:process";
|
|
1338
|
+
var SWEPT = { recursive: true, force: true };
|
|
1339
|
+
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
1340
|
+
var waiting = new Set;
|
|
1341
|
+
var atExit = new Set;
|
|
1342
|
+
function scratchDirectory(kind) {
|
|
1343
|
+
return mkdtempSync2(join4(tmpdir3(), `gdharness-${kind}-${process3.pid}-`));
|
|
878
1344
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
pushU32(out, value ? 1 : 0);
|
|
885
|
-
} else if (typeof value === "number") {
|
|
886
|
-
const view = new DataView(new ArrayBuffer(8));
|
|
887
|
-
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
888
|
-
pushU32(out, INT);
|
|
889
|
-
view.setInt32(0, value, true);
|
|
890
|
-
out.push(...new Uint8Array(view.buffer, 0, 4));
|
|
891
|
-
} else if (Number.isInteger(value)) {
|
|
892
|
-
pushU32(out, INT | FLAG_64);
|
|
893
|
-
view.setBigInt64(0, BigInt(value), true);
|
|
894
|
-
out.push(...new Uint8Array(view.buffer));
|
|
895
|
-
} else {
|
|
896
|
-
pushU32(out, FLOAT | FLAG_64);
|
|
897
|
-
view.setFloat64(0, value, true);
|
|
898
|
-
out.push(...new Uint8Array(view.buffer));
|
|
1345
|
+
function discardWith(remove, paths) {
|
|
1346
|
+
const left = [];
|
|
1347
|
+
for (const path of paths) {
|
|
1348
|
+
if (!path) {
|
|
1349
|
+
continue;
|
|
899
1350
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
out.push(...bytes, ...Array.from({ length: (4 - bytes.length % 4) % 4 }, () => 0));
|
|
905
|
-
} else {
|
|
906
|
-
pushU32(out, ARRAY);
|
|
907
|
-
pushU32(out, value.length);
|
|
908
|
-
for (const element of value) {
|
|
909
|
-
encodeInto(out, element);
|
|
1351
|
+
try {
|
|
1352
|
+
remove(path);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
left.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
910
1355
|
}
|
|
911
1356
|
}
|
|
1357
|
+
return left;
|
|
912
1358
|
}
|
|
913
|
-
function
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
1359
|
+
function discard(...paths) {
|
|
1360
|
+
const left = discardWith(remove, paths);
|
|
1361
|
+
for (const { path } of left) {
|
|
1362
|
+
tryAgain(path, 0);
|
|
1363
|
+
}
|
|
1364
|
+
return left;
|
|
917
1365
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const body = encodeVariant([message, MAIN_THREAD, data]);
|
|
921
|
-
const framed = new Uint8Array(body.length + 4);
|
|
922
|
-
new DataView(framed.buffer).setUint32(0, body.length, true);
|
|
923
|
-
framed.set(body, 4);
|
|
924
|
-
return framed;
|
|
1366
|
+
function remove(path) {
|
|
1367
|
+
rmSync4(path, SWEPT);
|
|
925
1368
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
push(chunk) {
|
|
931
|
-
const joined = new Uint8Array(this.pending.length + chunk.length);
|
|
932
|
-
joined.set(this.pending, 0);
|
|
933
|
-
joined.set(chunk, this.pending.length);
|
|
934
|
-
const out = [];
|
|
935
|
-
let at = 0;
|
|
936
|
-
while (joined.length - at >= 4) {
|
|
937
|
-
const length = new DataView(joined.buffer, joined.byteOffset + at, 4).getUint32(0, true);
|
|
938
|
-
if (length > MOST_MESSAGE_BYTES) {
|
|
939
|
-
throw new RangeError(`a message of ${length} bytes, more than the engine sends`);
|
|
940
|
-
}
|
|
941
|
-
if (joined.length - at - 4 < length) {
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
out.push(readMessage(joined.subarray(at + 4, at + 4 + length)));
|
|
945
|
-
at += 4 + length;
|
|
1369
|
+
function tryAgain(path, attempt) {
|
|
1370
|
+
if (attempt === 0) {
|
|
1371
|
+
if (waiting.has(path)) {
|
|
1372
|
+
return;
|
|
946
1373
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
function readMessage(body) {
|
|
952
|
-
let value;
|
|
953
|
-
try {
|
|
954
|
-
value = decodeVariant(body);
|
|
955
|
-
} catch (error) {
|
|
956
|
-
return { unreadable: error instanceof Error ? error.message : String(error) };
|
|
1374
|
+
waiting.add(path);
|
|
1375
|
+
atExit.delete(path);
|
|
1376
|
+
lastTryOnExit();
|
|
957
1377
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1378
|
+
const after = AGAIN_AFTER_MS[attempt];
|
|
1379
|
+
if (after === undefined || !existsSync(path)) {
|
|
1380
|
+
waiting.delete(path);
|
|
1381
|
+
if (after === undefined) {
|
|
1382
|
+
atExit.add(path);
|
|
1383
|
+
}
|
|
1384
|
+
return;
|
|
960
1385
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1386
|
+
setTimeout(() => {
|
|
1387
|
+
try {
|
|
1388
|
+
remove(path);
|
|
1389
|
+
waiting.delete(path);
|
|
1390
|
+
} catch {
|
|
1391
|
+
tryAgain(path, attempt + 1);
|
|
1392
|
+
}
|
|
1393
|
+
}, after).unref();
|
|
1394
|
+
}
|
|
1395
|
+
var lastTryWatched = false;
|
|
1396
|
+
function lastTryOnExit() {
|
|
1397
|
+
if (lastTryWatched) {
|
|
1398
|
+
return;
|
|
964
1399
|
}
|
|
965
|
-
|
|
1400
|
+
lastTryWatched = true;
|
|
1401
|
+
process3.once("exit", () => {
|
|
1402
|
+
for (const path of [...waiting, ...atExit]) {
|
|
1403
|
+
try {
|
|
1404
|
+
remove(path);
|
|
1405
|
+
} catch {}
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
966
1408
|
}
|
|
967
1409
|
|
|
1410
|
+
// src/gdscript-source.ts
|
|
1411
|
+
var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
|
|
1412
|
+
var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
|
|
1413
|
+
var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
|
|
1414
|
+
var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
|
|
1415
|
+
var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
1416
|
+
var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
1417
|
+
var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
|
|
1418
|
+
var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
|
|
1419
|
+
var DOTTED = `${NAME}(?:\\.${NAME})*`;
|
|
1420
|
+
var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
|
|
1421
|
+
var KEYWORDS = new Set([
|
|
1422
|
+
"and",
|
|
1423
|
+
"as",
|
|
1424
|
+
"assert",
|
|
1425
|
+
"await",
|
|
1426
|
+
"break",
|
|
1427
|
+
"breakpoint",
|
|
1428
|
+
"class",
|
|
1429
|
+
"class_name",
|
|
1430
|
+
"const",
|
|
1431
|
+
"continue",
|
|
1432
|
+
"elif",
|
|
1433
|
+
"else",
|
|
1434
|
+
"enum",
|
|
1435
|
+
"extends",
|
|
1436
|
+
"false",
|
|
1437
|
+
"for",
|
|
1438
|
+
"func",
|
|
1439
|
+
"if",
|
|
1440
|
+
"in",
|
|
1441
|
+
"is",
|
|
1442
|
+
"match",
|
|
1443
|
+
"namespace",
|
|
1444
|
+
"not",
|
|
1445
|
+
"null",
|
|
1446
|
+
"or",
|
|
1447
|
+
"pass",
|
|
1448
|
+
"preload",
|
|
1449
|
+
"return",
|
|
1450
|
+
"self",
|
|
1451
|
+
"signal",
|
|
1452
|
+
"static",
|
|
1453
|
+
"super",
|
|
1454
|
+
"trait",
|
|
1455
|
+
"true",
|
|
1456
|
+
"var",
|
|
1457
|
+
"void",
|
|
1458
|
+
"when",
|
|
1459
|
+
"while",
|
|
1460
|
+
"yield",
|
|
1461
|
+
"INF",
|
|
1462
|
+
"NAN",
|
|
1463
|
+
"PI",
|
|
1464
|
+
"TAU"
|
|
1465
|
+
]);
|
|
1466
|
+
|
|
968
1467
|
// src/answer-json.ts
|
|
969
1468
|
var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
|
|
970
1469
|
var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
|
|
@@ -1150,9 +1649,7 @@ function queuedMessagesOf(projectGodot) {
|
|
|
1150
1649
|
var PROFILE_SENT = "gdharness:profile_sent";
|
|
1151
1650
|
var PROFILE_RECEIVED = "gdharness:profile_received";
|
|
1152
1651
|
var UNNAMED_PREFIX = "#";
|
|
1153
|
-
|
|
1154
|
-
return framedCommand("profiler:servers", [true, [frameFunctions, false]]);
|
|
1155
|
-
}
|
|
1652
|
+
var PROFILER_SWITCH = "profiler:servers";
|
|
1156
1653
|
function withEngineArguments(args, extra) {
|
|
1157
1654
|
const user = args.indexOf("--");
|
|
1158
1655
|
return user < 0 ? [...args, ...extra] : [...args.slice(0, user), ...extra, ...args.slice(user)];
|
|
@@ -1252,88 +1749,36 @@ class ProfileAggregate {
|
|
|
1252
1749
|
};
|
|
1253
1750
|
}
|
|
1254
1751
|
}
|
|
1255
|
-
class
|
|
1256
|
-
connection = null;
|
|
1752
|
+
class ProfileObserver {
|
|
1257
1753
|
aggregate = null;
|
|
1258
|
-
server;
|
|
1259
|
-
port;
|
|
1260
1754
|
changed;
|
|
1261
1755
|
frameFunctions;
|
|
1262
|
-
constructor(
|
|
1263
|
-
this.server = server;
|
|
1264
|
-
this.port = port;
|
|
1756
|
+
constructor(changed, frameFunctions) {
|
|
1265
1757
|
this.changed = changed;
|
|
1266
1758
|
this.frameFunctions = frameFunctions;
|
|
1267
1759
|
}
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
await new Promise((resolve, reject) => {
|
|
1271
|
-
server.once("error", reject);
|
|
1272
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1273
|
-
resolve();
|
|
1274
|
-
});
|
|
1275
|
-
});
|
|
1276
|
-
const address = server.address();
|
|
1277
|
-
if (address === null || typeof address === "string") {
|
|
1278
|
-
server.close();
|
|
1279
|
-
throw new Error("the profiler listener has no port");
|
|
1280
|
-
}
|
|
1281
|
-
const listener = new ProfileListener(server, address.port, changed, frameFunctions);
|
|
1282
|
-
server.on("connection", (socket) => {
|
|
1283
|
-
listener.accept(socket);
|
|
1284
|
-
});
|
|
1285
|
-
return listener;
|
|
1286
|
-
}
|
|
1287
|
-
get engineArguments() {
|
|
1288
|
-
return ["--remote-debug", `tcp://127.0.0.1:${this.port}`, "--skip-breakpoints", "--ignore-error-breaks"];
|
|
1289
|
-
}
|
|
1290
|
-
accept(socket) {
|
|
1291
|
-
if (this.connection !== null) {
|
|
1292
|
-
socket.destroy();
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
this.connection = socket;
|
|
1296
|
-
this.server.close();
|
|
1297
|
-
const started = Date.now();
|
|
1298
|
-
const aggregate = new ProfileAggregate(started, this.frameFunctions);
|
|
1760
|
+
connected(send) {
|
|
1761
|
+
const aggregate = new ProfileAggregate(Date.now(), this.frameFunctions);
|
|
1299
1762
|
this.aggregate = aggregate;
|
|
1300
|
-
|
|
1301
|
-
socket.write(profilerOn(this.frameFunctions));
|
|
1763
|
+
send(PROFILER_SWITCH, [true, [this.frameFunctions, false]]);
|
|
1302
1764
|
this.changed(aggregate.snapshot());
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
} catch {
|
|
1308
|
-
socket.destroy();
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
let frames = false;
|
|
1312
|
-
for (const received of messages) {
|
|
1313
|
-
if ("message" in received && received.message === "debug_enter") {
|
|
1314
|
-
socket.write(framedCommand("continue", []));
|
|
1315
|
-
}
|
|
1316
|
-
if ("message" in received && received.message === PROFILE_SENT) {
|
|
1317
|
-
socket.write(framedCommand(PROFILE_RECEIVED, []));
|
|
1318
|
-
}
|
|
1319
|
-
aggregate.take(received, Date.now());
|
|
1320
|
-
frames ||= "message" in received && (received.message === "servers:profile_frame" || received.message === "servers:profile_total");
|
|
1321
|
-
}
|
|
1322
|
-
if (frames) {
|
|
1323
|
-
this.changed(aggregate.snapshot());
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
socket.on("error", () => {
|
|
1765
|
+
}
|
|
1766
|
+
message(received, send) {
|
|
1767
|
+
const aggregate = this.aggregate;
|
|
1768
|
+
if (aggregate === null) {
|
|
1327
1769
|
return;
|
|
1328
|
-
}
|
|
1770
|
+
}
|
|
1771
|
+
if ("message" in received && received.message === PROFILE_SENT) {
|
|
1772
|
+
send(PROFILE_RECEIVED, []);
|
|
1773
|
+
}
|
|
1774
|
+
aggregate.take(received, Date.now());
|
|
1775
|
+
if ("message" in received && (received.message === "servers:profile_frame" || received.message === "servers:profile_total")) {
|
|
1776
|
+
this.changed(aggregate.snapshot());
|
|
1777
|
+
}
|
|
1329
1778
|
}
|
|
1330
1779
|
totals() {
|
|
1331
1780
|
return this.aggregate?.snapshot() ?? null;
|
|
1332
1781
|
}
|
|
1333
|
-
close() {
|
|
1334
|
-
this.connection?.destroy();
|
|
1335
|
-
this.server.close();
|
|
1336
|
-
}
|
|
1337
1782
|
}
|
|
1338
1783
|
|
|
1339
1784
|
// src/keeper.ts
|
|
@@ -1394,7 +1839,7 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1394
1839
|
}
|
|
1395
1840
|
var PROFILE_WRITE_MS = 500;
|
|
1396
1841
|
var LAST_WRITE_TRIES = 5;
|
|
1397
|
-
|
|
1842
|
+
function profiledBy(path, projectPath) {
|
|
1398
1843
|
let latest = null;
|
|
1399
1844
|
let timer = null;
|
|
1400
1845
|
const write = () => {
|
|
@@ -1407,7 +1852,7 @@ async function profiledBy(path, projectPath) {
|
|
|
1407
1852
|
try {
|
|
1408
1853
|
projectGodot = readFileSync2(join5(projectPath, "project.godot"), "utf8");
|
|
1409
1854
|
} catch {}
|
|
1410
|
-
const
|
|
1855
|
+
const observer = new ProfileObserver((totals) => {
|
|
1411
1856
|
const first = latest === null;
|
|
1412
1857
|
latest = totals;
|
|
1413
1858
|
if (first) {
|
|
@@ -1418,20 +1863,20 @@ async function profiledBy(path, projectPath) {
|
|
|
1418
1863
|
}, frameFunctionsFor(queuedMessagesOf(projectGodot)));
|
|
1419
1864
|
writeProfileFile(path, { connected: false, listeningSince: Date.now() });
|
|
1420
1865
|
return {
|
|
1421
|
-
|
|
1866
|
+
observer,
|
|
1422
1867
|
finish: () => {
|
|
1423
1868
|
if (timer !== null) {
|
|
1424
1869
|
clearTimeout(timer);
|
|
1425
1870
|
}
|
|
1426
|
-
latest =
|
|
1871
|
+
latest = observer.totals() ?? latest;
|
|
1427
1872
|
let tries = 0;
|
|
1428
1873
|
while (latest !== null && !writeProfileFile(path, { ...latest, connected: true }) && tries < LAST_WRITE_TRIES) {
|
|
1429
1874
|
tries += 1;
|
|
1430
1875
|
}
|
|
1431
|
-
listener.close();
|
|
1432
1876
|
}
|
|
1433
1877
|
};
|
|
1434
1878
|
}
|
|
1879
|
+
var DRAIN_MS = 2000;
|
|
1435
1880
|
var DESKTOP_START_MS = 60000;
|
|
1436
1881
|
async function startDetached(spec, output, hidden, stopFile = "") {
|
|
1437
1882
|
return throughTheHelper(spec) ? await startThroughHelper(spec, output, stopFile) : await startDirectly(spec, output, hidden);
|
|
@@ -1505,8 +1950,9 @@ async function keep() {
|
|
|
1505
1950
|
`);
|
|
1506
1951
|
process.exit(1);
|
|
1507
1952
|
}
|
|
1508
|
-
const profiled = run?.profile === undefined ? null :
|
|
1509
|
-
const
|
|
1953
|
+
const profiled = run?.profile === undefined ? null : profiledBy(run.profile, run.projectPath);
|
|
1954
|
+
const debuggerPeer = run === undefined ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
|
|
1955
|
+
const toStart = debuggerPeer === null ? spec : { ...spec, args: withEngineArguments(spec.args, debuggerPeer.engineArguments) };
|
|
1510
1956
|
const transcript = run === undefined ? "ignore" : openSync2(run.transcript, "a");
|
|
1511
1957
|
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync3(join5(tmpdir4(), "gdharness-stop-")) : null;
|
|
1512
1958
|
const stopFile = own === null ? "" : join5(own, "stop");
|
|
@@ -1519,7 +1965,7 @@ async function keep() {
|
|
|
1519
1965
|
}
|
|
1520
1966
|
}
|
|
1521
1967
|
if ("error" in started) {
|
|
1522
|
-
|
|
1968
|
+
debuggerPeer?.close();
|
|
1523
1969
|
process.stdout.write(`error ${started.error}
|
|
1524
1970
|
`);
|
|
1525
1971
|
process.exit(1);
|
|
@@ -1541,24 +1987,32 @@ async function keep() {
|
|
|
1541
1987
|
}
|
|
1542
1988
|
let listening = null;
|
|
1543
1989
|
child.once("exit", (code, signal) => {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1990
|
+
const finish = () => {
|
|
1991
|
+
profiled?.finish();
|
|
1992
|
+
if (run !== undefined) {
|
|
1993
|
+
recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
1994
|
+
}
|
|
1995
|
+
debuggerPeer?.close();
|
|
1996
|
+
listening?.close();
|
|
1997
|
+
if (own !== null) {
|
|
1998
|
+
rmSync5(own, { recursive: true, force: true });
|
|
1999
|
+
}
|
|
2000
|
+
process.exit(0);
|
|
2001
|
+
};
|
|
2002
|
+
if (debuggerPeer === null) {
|
|
2003
|
+
finish();
|
|
2004
|
+
} else {
|
|
2005
|
+
debuggerPeer.drained(DRAIN_MS).then(finish);
|
|
1551
2006
|
}
|
|
1552
|
-
process.exit(0);
|
|
1553
2007
|
});
|
|
1554
2008
|
if (record !== null) {
|
|
1555
|
-
listening = await
|
|
2009
|
+
listening = await listenForTheServer(() => {
|
|
1556
2010
|
if (stopFile !== "") {
|
|
1557
2011
|
writeFileSync3(stopFile, "");
|
|
1558
2012
|
return "signalled";
|
|
1559
2013
|
}
|
|
1560
2014
|
return child.kill() ? "signalled" : "gone";
|
|
1561
|
-
});
|
|
2015
|
+
}, debuggerPeer === null ? null : (request) => debuggerPeer.ask(request));
|
|
1562
2016
|
if (listening !== null) {
|
|
1563
2017
|
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
1564
2018
|
}
|