gdharness 1.2.25 → 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 +277 -50
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +294 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +150 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +5 -0
- package/build/godot/operations/import_pipeline.gd +92 -0
- package/build/godot/operations/input_actions.gd +3 -2
- package/build/godot/operations/plugins.gd +3 -2
- package/build/godot/operations/project_config.gd +17 -9
- package/build/godot/operations/project_file.gd +202 -0
- package/build/index.js +272 -48
- package/build/keeper.js +1207 -750
- package/package.json +1 -1
package/build/keeper.js
CHANGED
|
@@ -226,742 +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
|
-
});
|
|
391
|
-
var SET_BY_ARGUMENT = Object.freeze({
|
|
392
|
-
"--headless": "headless",
|
|
393
|
-
"--audio-driver": "silent",
|
|
394
|
-
"--quit-after": "frames, on editor_run check"
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// src/runtime-client.ts
|
|
398
|
-
function runtimeDirectory(variables = process.env) {
|
|
399
|
-
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
400
|
-
if (explicit) {
|
|
401
|
-
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();
|
|
402
331
|
}
|
|
403
|
-
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
404
|
-
return join2(perUser ?? tmpdir2(), "gdharness");
|
|
405
332
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
var lateAnswers = new Map;
|
|
410
|
-
|
|
411
|
-
// src/whole-file.ts
|
|
412
|
-
import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
413
|
-
var HELD_FILE_MS = 1000;
|
|
414
|
-
var HELD_FILE_RETRY_MS = 10;
|
|
415
|
-
var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
416
|
-
function writeWhole(path, text) {
|
|
417
|
-
const partial = `${path}.${process.pid}.partial`;
|
|
418
|
-
try {
|
|
419
|
-
writeFileSync2(partial, text, "utf8");
|
|
420
|
-
} catch {
|
|
421
|
-
return false;
|
|
333
|
+
function decodeValue(reader, depth) {
|
|
334
|
+
if (depth > MAX_DEPTH) {
|
|
335
|
+
throw new RangeError("a value nests deeper than the engine allows");
|
|
422
336
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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");
|
|
435
373
|
}
|
|
436
|
-
|
|
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()) };
|
|
437
381
|
}
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
function runsDirectory() {
|
|
444
|
-
return join3(runtimeDirectory(), "runs");
|
|
445
|
-
}
|
|
446
|
-
function realAsFarAsItExists(path) {
|
|
447
|
-
const missing = [];
|
|
448
|
-
let at = path;
|
|
449
|
-
for (;; ) {
|
|
450
|
-
try {
|
|
451
|
-
return join3(realpathSync.native(at), ...missing);
|
|
452
|
-
} catch {
|
|
453
|
-
const parent = dirname2(at);
|
|
454
|
-
if (parent === at) {
|
|
455
|
-
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");
|
|
456
387
|
}
|
|
457
|
-
|
|
458
|
-
|
|
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));
|
|
459
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;
|
|
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`);
|
|
460
432
|
}
|
|
461
433
|
}
|
|
462
|
-
function
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
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;
|
|
466
441
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
442
|
+
function pushU32(out, value) {
|
|
443
|
+
out.push(value & 255, value >>> 8 & 255, value >>> 16 & 255, value >>> 24 & 255);
|
|
469
444
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
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
|
+
}
|
|
475
477
|
}
|
|
476
478
|
}
|
|
477
|
-
function
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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;
|
|
482
515
|
}
|
|
483
|
-
const written = {
|
|
484
|
-
...record,
|
|
485
|
-
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
486
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
487
|
-
endedAt: Date.now()
|
|
488
|
-
};
|
|
489
|
-
writeWhole(path, JSON.stringify(written, null, 2));
|
|
490
516
|
}
|
|
491
|
-
function
|
|
492
|
-
let
|
|
517
|
+
function readMessage(body) {
|
|
518
|
+
let value;
|
|
493
519
|
try {
|
|
494
|
-
|
|
495
|
-
} catch {
|
|
496
|
-
return
|
|
520
|
+
value = decodeVariant(body);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return { unreadable: error instanceof Error ? error.message : String(error) };
|
|
497
523
|
}
|
|
498
|
-
if (!
|
|
499
|
-
return
|
|
524
|
+
if (!Array.isArray(value) || value.length !== 3) {
|
|
525
|
+
return { unreadable: "a message that is not [message, thread, data]" };
|
|
500
526
|
}
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const startedAt = fields["startedAt"];
|
|
505
|
-
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
506
|
-
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" };
|
|
507
530
|
}
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
pid,
|
|
511
|
-
transcript,
|
|
512
|
-
startedAt,
|
|
513
|
-
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
514
|
-
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
515
|
-
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
516
|
-
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
517
|
-
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
518
|
-
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
519
|
-
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
520
|
-
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
521
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
522
|
-
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
523
|
-
};
|
|
531
|
+
return { message, thread, data };
|
|
524
532
|
}
|
|
525
533
|
|
|
526
|
-
// src/
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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);
|
|
537
592
|
}
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (!path) {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
try {
|
|
545
|
-
remove(path);
|
|
546
|
-
} catch (error) {
|
|
547
|
-
left.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
548
|
-
}
|
|
593
|
+
function shownValue(value, typeHint = "") {
|
|
594
|
+
if (value === null) {
|
|
595
|
+
return "null";
|
|
549
596
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
function discard(...paths) {
|
|
553
|
-
const left = discardWith(remove, paths);
|
|
554
|
-
for (const { path } of left) {
|
|
555
|
-
tryAgain(path, 0);
|
|
597
|
+
if (typeof value === "string") {
|
|
598
|
+
return value;
|
|
556
599
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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(", ")})`;
|
|
561
620
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
566
627
|
}
|
|
567
|
-
|
|
568
|
-
atExit.delete(path);
|
|
569
|
-
lastTryOnExit();
|
|
628
|
+
frames.push({ id: frames.length, name, line, script });
|
|
570
629
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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;
|
|
578
652
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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");
|
|
585
665
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return;
|
|
666
|
+
const peer = new GameDebugger(server, address.port, breakpoints, observers);
|
|
667
|
+
server.on("connection", (socket) => {
|
|
668
|
+
peer.accept(socket);
|
|
669
|
+
});
|
|
670
|
+
return peer;
|
|
592
671
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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;
|
|
599
685
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
"namespace",
|
|
640
|
-
"not",
|
|
641
|
-
"null",
|
|
642
|
-
"or",
|
|
643
|
-
"pass",
|
|
644
|
-
"preload",
|
|
645
|
-
"return",
|
|
646
|
-
"self",
|
|
647
|
-
"signal",
|
|
648
|
-
"static",
|
|
649
|
-
"super",
|
|
650
|
-
"trait",
|
|
651
|
-
"true",
|
|
652
|
-
"var",
|
|
653
|
-
"void",
|
|
654
|
-
"when",
|
|
655
|
-
"while",
|
|
656
|
-
"yield",
|
|
657
|
-
"INF",
|
|
658
|
-
"NAN",
|
|
659
|
-
"PI",
|
|
660
|
-
"TAU"
|
|
661
|
-
]);
|
|
662
|
-
|
|
663
|
-
// src/godot-variant.ts
|
|
664
|
-
var NIL = 0;
|
|
665
|
-
var BOOL = 1;
|
|
666
|
-
var INT = 2;
|
|
667
|
-
var FLOAT = 3;
|
|
668
|
-
var STRING = 4;
|
|
669
|
-
var ARRAY = 28;
|
|
670
|
-
var FLAG_64 = 1 << 16;
|
|
671
|
-
var MATH = {
|
|
672
|
-
5: { name: "Vector2", count: 2 },
|
|
673
|
-
6: { name: "Vector2i", count: 2, ints: true },
|
|
674
|
-
7: { name: "Rect2", count: 4 },
|
|
675
|
-
8: { name: "Rect2i", count: 4, ints: true },
|
|
676
|
-
9: { name: "Vector3", count: 3 },
|
|
677
|
-
10: { name: "Vector3i", count: 3, ints: true },
|
|
678
|
-
11: { name: "Transform2D", count: 6 },
|
|
679
|
-
12: { name: "Vector4", count: 4 },
|
|
680
|
-
13: { name: "Vector4i", count: 4, ints: true },
|
|
681
|
-
14: { name: "Plane", count: 4 },
|
|
682
|
-
15: { name: "Quaternion", count: 4 },
|
|
683
|
-
16: { name: "AABB", count: 6 },
|
|
684
|
-
17: { name: "Basis", count: 9 },
|
|
685
|
-
18: { name: "Transform3D", count: 12 },
|
|
686
|
-
19: { name: "Projection", count: 16 }
|
|
687
|
-
};
|
|
688
|
-
var PACKED_VECTORS = { 35: 2, 36: 3, 38: 4 };
|
|
689
|
-
|
|
690
|
-
class Reader {
|
|
691
|
-
offset = 0;
|
|
692
|
-
view;
|
|
693
|
-
constructor(view) {
|
|
694
|
-
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
|
+
});
|
|
695
725
|
}
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
});
|
|
698
737
|
}
|
|
699
|
-
|
|
700
|
-
if (
|
|
701
|
-
|
|
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
|
+
}
|
|
702
773
|
}
|
|
703
774
|
}
|
|
704
|
-
|
|
705
|
-
this.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
}
|
|
709
782
|
}
|
|
710
|
-
|
|
711
|
-
this.
|
|
712
|
-
const value = this.view.getInt32(this.offset, true);
|
|
713
|
-
this.offset += 4;
|
|
714
|
-
return value;
|
|
783
|
+
send(message, data, thread = MAIN_THREAD) {
|
|
784
|
+
this.connection?.write(framedCommand(message, data, thread));
|
|
715
785
|
}
|
|
716
|
-
|
|
717
|
-
this.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
});
|
|
721
811
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const value = Number(this.view.getBigUint64(this.offset, true));
|
|
725
|
-
this.offset += 8;
|
|
726
|
-
return value;
|
|
812
|
+
static named(message) {
|
|
813
|
+
return (received) => ("message" in received) && received.message === message;
|
|
727
814
|
}
|
|
728
|
-
|
|
729
|
-
this.
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
}
|
|
733
866
|
}
|
|
734
|
-
|
|
735
|
-
this.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
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
|
+
};
|
|
739
876
|
}
|
|
740
|
-
|
|
741
|
-
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
|
+
};
|
|
742
881
|
}
|
|
743
|
-
|
|
744
|
-
this.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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 ?? [] };
|
|
748
922
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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);
|
|
754
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]) };
|
|
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;
|
|
755
1172
|
}
|
|
756
|
-
var
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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;
|
|
762
1209
|
}
|
|
1210
|
+
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
1211
|
+
return join2(perUser ?? tmpdir2(), "gdharness");
|
|
763
1212
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const count = reader.u32();
|
|
781
|
-
return Array.from({ length: count }, () => ({
|
|
782
|
-
kind: "Vector",
|
|
783
|
-
values: Array.from({ length: vectorWidth }, () => reader.real(wide))
|
|
784
|
-
}));
|
|
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;
|
|
785
1229
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
case 20:
|
|
799
|
-
return { kind: "Color", values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()] };
|
|
800
|
-
case 22: {
|
|
801
|
-
const first = reader.u32();
|
|
802
|
-
if ((first & 2147483648) === 0) {
|
|
803
|
-
throw new RangeError("a node path in the format before 3.0");
|
|
804
|
-
}
|
|
805
|
-
const names = first & 2147483647;
|
|
806
|
-
let subnames = reader.u32();
|
|
807
|
-
const flags = reader.u32();
|
|
808
|
-
if ((flags & 2) !== 0) {
|
|
809
|
-
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;
|
|
810
1242
|
}
|
|
811
|
-
|
|
1243
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
|
|
812
1244
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
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;
|
|
832
1263
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
case ARRAY: {
|
|
836
|
-
containerType(reader, header >> 16 & 3);
|
|
837
|
-
const count = reader.u32() & 2147483647;
|
|
838
|
-
return Array.from({ length: count }, () => decodeValue(reader, depth + 1));
|
|
839
|
-
}
|
|
840
|
-
case 29: {
|
|
841
|
-
const count = reader.u32();
|
|
842
|
-
const bytes = [...reader.bytes(count)];
|
|
843
|
-
reader.bytes((4 - count % 4) % 4);
|
|
844
|
-
return bytes;
|
|
1264
|
+
missing.unshift(basename(at));
|
|
1265
|
+
at = parent;
|
|
845
1266
|
}
|
|
846
|
-
case 30:
|
|
847
|
-
return Array.from({ length: reader.u32() }, () => reader.i32());
|
|
848
|
-
case 31:
|
|
849
|
-
return Array.from({ length: reader.u32() }, () => reader.i64());
|
|
850
|
-
case 32:
|
|
851
|
-
return Array.from({ length: reader.u32() }, () => reader.f32());
|
|
852
|
-
case 33:
|
|
853
|
-
return Array.from({ length: reader.u32() }, () => reader.f64());
|
|
854
|
-
case 34:
|
|
855
|
-
return Array.from({ length: reader.u32() }, () => reader.string().replace(/\0$/, ""));
|
|
856
|
-
case 37:
|
|
857
|
-
return Array.from({ length: reader.u32() }, () => ({
|
|
858
|
-
kind: "Color",
|
|
859
|
-
values: [reader.f32(), reader.f32(), reader.f32(), reader.f32()]
|
|
860
|
-
}));
|
|
861
|
-
default:
|
|
862
|
-
throw new RangeError(`a value of type ${type}, which Godot 4.7 does not have`);
|
|
863
1267
|
}
|
|
864
1268
|
}
|
|
865
|
-
function
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
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}`);
|
|
870
1282
|
}
|
|
871
|
-
return value;
|
|
872
1283
|
}
|
|
873
|
-
function
|
|
874
|
-
|
|
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}-`));
|
|
875
1344
|
}
|
|
876
|
-
function
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
pushU32(out, value ? 1 : 0);
|
|
882
|
-
} else if (typeof value === "number") {
|
|
883
|
-
const view = new DataView(new ArrayBuffer(8));
|
|
884
|
-
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
885
|
-
pushU32(out, INT);
|
|
886
|
-
view.setInt32(0, value, true);
|
|
887
|
-
out.push(...new Uint8Array(view.buffer, 0, 4));
|
|
888
|
-
} else if (Number.isInteger(value)) {
|
|
889
|
-
pushU32(out, INT | FLAG_64);
|
|
890
|
-
view.setBigInt64(0, BigInt(value), true);
|
|
891
|
-
out.push(...new Uint8Array(view.buffer));
|
|
892
|
-
} else {
|
|
893
|
-
pushU32(out, FLOAT | FLAG_64);
|
|
894
|
-
view.setFloat64(0, value, true);
|
|
895
|
-
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;
|
|
896
1350
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
out.push(...bytes, ...Array.from({ length: (4 - bytes.length % 4) % 4 }, () => 0));
|
|
902
|
-
} else {
|
|
903
|
-
pushU32(out, ARRAY);
|
|
904
|
-
pushU32(out, value.length);
|
|
905
|
-
for (const element of value) {
|
|
906
|
-
encodeInto(out, element);
|
|
1351
|
+
try {
|
|
1352
|
+
remove(path);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
left.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
907
1355
|
}
|
|
908
1356
|
}
|
|
1357
|
+
return left;
|
|
909
1358
|
}
|
|
910
|
-
function
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
1359
|
+
function discard(...paths) {
|
|
1360
|
+
const left = discardWith(remove, paths);
|
|
1361
|
+
for (const { path } of left) {
|
|
1362
|
+
tryAgain(path, 0);
|
|
1363
|
+
}
|
|
1364
|
+
return left;
|
|
914
1365
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const body = encodeVariant([message, MAIN_THREAD, data]);
|
|
918
|
-
const framed = new Uint8Array(body.length + 4);
|
|
919
|
-
new DataView(framed.buffer).setUint32(0, body.length, true);
|
|
920
|
-
framed.set(body, 4);
|
|
921
|
-
return framed;
|
|
1366
|
+
function remove(path) {
|
|
1367
|
+
rmSync4(path, SWEPT);
|
|
922
1368
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
push(chunk) {
|
|
928
|
-
const joined = new Uint8Array(this.pending.length + chunk.length);
|
|
929
|
-
joined.set(this.pending, 0);
|
|
930
|
-
joined.set(chunk, this.pending.length);
|
|
931
|
-
const out = [];
|
|
932
|
-
let at = 0;
|
|
933
|
-
while (joined.length - at >= 4) {
|
|
934
|
-
const length = new DataView(joined.buffer, joined.byteOffset + at, 4).getUint32(0, true);
|
|
935
|
-
if (length > MOST_MESSAGE_BYTES) {
|
|
936
|
-
throw new RangeError(`a message of ${length} bytes, more than the engine sends`);
|
|
937
|
-
}
|
|
938
|
-
if (joined.length - at - 4 < length) {
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
941
|
-
out.push(readMessage(joined.subarray(at + 4, at + 4 + length)));
|
|
942
|
-
at += 4 + length;
|
|
1369
|
+
function tryAgain(path, attempt) {
|
|
1370
|
+
if (attempt === 0) {
|
|
1371
|
+
if (waiting.has(path)) {
|
|
1372
|
+
return;
|
|
943
1373
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
function readMessage(body) {
|
|
949
|
-
let value;
|
|
950
|
-
try {
|
|
951
|
-
value = decodeVariant(body);
|
|
952
|
-
} catch (error) {
|
|
953
|
-
return { unreadable: error instanceof Error ? error.message : String(error) };
|
|
1374
|
+
waiting.add(path);
|
|
1375
|
+
atExit.delete(path);
|
|
1376
|
+
lastTryOnExit();
|
|
954
1377
|
}
|
|
955
|
-
|
|
956
|
-
|
|
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;
|
|
957
1385
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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;
|
|
961
1399
|
}
|
|
962
|
-
|
|
1400
|
+
lastTryWatched = true;
|
|
1401
|
+
process3.once("exit", () => {
|
|
1402
|
+
for (const path of [...waiting, ...atExit]) {
|
|
1403
|
+
try {
|
|
1404
|
+
remove(path);
|
|
1405
|
+
} catch {}
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
963
1408
|
}
|
|
964
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
|
+
|
|
965
1467
|
// src/answer-json.ts
|
|
966
1468
|
var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
|
|
967
1469
|
var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
|
|
@@ -1147,9 +1649,7 @@ function queuedMessagesOf(projectGodot) {
|
|
|
1147
1649
|
var PROFILE_SENT = "gdharness:profile_sent";
|
|
1148
1650
|
var PROFILE_RECEIVED = "gdharness:profile_received";
|
|
1149
1651
|
var UNNAMED_PREFIX = "#";
|
|
1150
|
-
|
|
1151
|
-
return framedCommand("profiler:servers", [true, [frameFunctions, false]]);
|
|
1152
|
-
}
|
|
1652
|
+
var PROFILER_SWITCH = "profiler:servers";
|
|
1153
1653
|
function withEngineArguments(args, extra) {
|
|
1154
1654
|
const user = args.indexOf("--");
|
|
1155
1655
|
return user < 0 ? [...args, ...extra] : [...args.slice(0, user), ...extra, ...args.slice(user)];
|
|
@@ -1249,88 +1749,36 @@ class ProfileAggregate {
|
|
|
1249
1749
|
};
|
|
1250
1750
|
}
|
|
1251
1751
|
}
|
|
1252
|
-
class
|
|
1253
|
-
connection = null;
|
|
1752
|
+
class ProfileObserver {
|
|
1254
1753
|
aggregate = null;
|
|
1255
|
-
server;
|
|
1256
|
-
port;
|
|
1257
1754
|
changed;
|
|
1258
1755
|
frameFunctions;
|
|
1259
|
-
constructor(
|
|
1260
|
-
this.server = server;
|
|
1261
|
-
this.port = port;
|
|
1756
|
+
constructor(changed, frameFunctions) {
|
|
1262
1757
|
this.changed = changed;
|
|
1263
1758
|
this.frameFunctions = frameFunctions;
|
|
1264
1759
|
}
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1267
|
-
await new Promise((resolve, reject) => {
|
|
1268
|
-
server.once("error", reject);
|
|
1269
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1270
|
-
resolve();
|
|
1271
|
-
});
|
|
1272
|
-
});
|
|
1273
|
-
const address = server.address();
|
|
1274
|
-
if (address === null || typeof address === "string") {
|
|
1275
|
-
server.close();
|
|
1276
|
-
throw new Error("the profiler listener has no port");
|
|
1277
|
-
}
|
|
1278
|
-
const listener = new ProfileListener(server, address.port, changed, frameFunctions);
|
|
1279
|
-
server.on("connection", (socket) => {
|
|
1280
|
-
listener.accept(socket);
|
|
1281
|
-
});
|
|
1282
|
-
return listener;
|
|
1283
|
-
}
|
|
1284
|
-
get engineArguments() {
|
|
1285
|
-
return ["--remote-debug", `tcp://127.0.0.1:${this.port}`, "--skip-breakpoints", "--ignore-error-breaks"];
|
|
1286
|
-
}
|
|
1287
|
-
accept(socket) {
|
|
1288
|
-
if (this.connection !== null) {
|
|
1289
|
-
socket.destroy();
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
this.connection = socket;
|
|
1293
|
-
this.server.close();
|
|
1294
|
-
const started = Date.now();
|
|
1295
|
-
const aggregate = new ProfileAggregate(started, this.frameFunctions);
|
|
1760
|
+
connected(send) {
|
|
1761
|
+
const aggregate = new ProfileAggregate(Date.now(), this.frameFunctions);
|
|
1296
1762
|
this.aggregate = aggregate;
|
|
1297
|
-
|
|
1298
|
-
socket.write(profilerOn(this.frameFunctions));
|
|
1763
|
+
send(PROFILER_SWITCH, [true, [this.frameFunctions, false]]);
|
|
1299
1764
|
this.changed(aggregate.snapshot());
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
} catch {
|
|
1305
|
-
socket.destroy();
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
let frames = false;
|
|
1309
|
-
for (const received of messages) {
|
|
1310
|
-
if ("message" in received && received.message === "debug_enter") {
|
|
1311
|
-
socket.write(framedCommand("continue", []));
|
|
1312
|
-
}
|
|
1313
|
-
if ("message" in received && received.message === PROFILE_SENT) {
|
|
1314
|
-
socket.write(framedCommand(PROFILE_RECEIVED, []));
|
|
1315
|
-
}
|
|
1316
|
-
aggregate.take(received, Date.now());
|
|
1317
|
-
frames ||= "message" in received && (received.message === "servers:profile_frame" || received.message === "servers:profile_total");
|
|
1318
|
-
}
|
|
1319
|
-
if (frames) {
|
|
1320
|
-
this.changed(aggregate.snapshot());
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
socket.on("error", () => {
|
|
1765
|
+
}
|
|
1766
|
+
message(received, send) {
|
|
1767
|
+
const aggregate = this.aggregate;
|
|
1768
|
+
if (aggregate === null) {
|
|
1324
1769
|
return;
|
|
1325
|
-
}
|
|
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
|
+
}
|
|
1326
1778
|
}
|
|
1327
1779
|
totals() {
|
|
1328
1780
|
return this.aggregate?.snapshot() ?? null;
|
|
1329
1781
|
}
|
|
1330
|
-
close() {
|
|
1331
|
-
this.connection?.destroy();
|
|
1332
|
-
this.server.close();
|
|
1333
|
-
}
|
|
1334
1782
|
}
|
|
1335
1783
|
|
|
1336
1784
|
// src/keeper.ts
|
|
@@ -1391,7 +1839,7 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1391
1839
|
}
|
|
1392
1840
|
var PROFILE_WRITE_MS = 500;
|
|
1393
1841
|
var LAST_WRITE_TRIES = 5;
|
|
1394
|
-
|
|
1842
|
+
function profiledBy(path, projectPath) {
|
|
1395
1843
|
let latest = null;
|
|
1396
1844
|
let timer = null;
|
|
1397
1845
|
const write = () => {
|
|
@@ -1404,7 +1852,7 @@ async function profiledBy(path, projectPath) {
|
|
|
1404
1852
|
try {
|
|
1405
1853
|
projectGodot = readFileSync2(join5(projectPath, "project.godot"), "utf8");
|
|
1406
1854
|
} catch {}
|
|
1407
|
-
const
|
|
1855
|
+
const observer = new ProfileObserver((totals) => {
|
|
1408
1856
|
const first = latest === null;
|
|
1409
1857
|
latest = totals;
|
|
1410
1858
|
if (first) {
|
|
@@ -1415,20 +1863,20 @@ async function profiledBy(path, projectPath) {
|
|
|
1415
1863
|
}, frameFunctionsFor(queuedMessagesOf(projectGodot)));
|
|
1416
1864
|
writeProfileFile(path, { connected: false, listeningSince: Date.now() });
|
|
1417
1865
|
return {
|
|
1418
|
-
|
|
1866
|
+
observer,
|
|
1419
1867
|
finish: () => {
|
|
1420
1868
|
if (timer !== null) {
|
|
1421
1869
|
clearTimeout(timer);
|
|
1422
1870
|
}
|
|
1423
|
-
latest =
|
|
1871
|
+
latest = observer.totals() ?? latest;
|
|
1424
1872
|
let tries = 0;
|
|
1425
1873
|
while (latest !== null && !writeProfileFile(path, { ...latest, connected: true }) && tries < LAST_WRITE_TRIES) {
|
|
1426
1874
|
tries += 1;
|
|
1427
1875
|
}
|
|
1428
|
-
listener.close();
|
|
1429
1876
|
}
|
|
1430
1877
|
};
|
|
1431
1878
|
}
|
|
1879
|
+
var DRAIN_MS = 2000;
|
|
1432
1880
|
var DESKTOP_START_MS = 60000;
|
|
1433
1881
|
async function startDetached(spec, output, hidden, stopFile = "") {
|
|
1434
1882
|
return throughTheHelper(spec) ? await startThroughHelper(spec, output, stopFile) : await startDirectly(spec, output, hidden);
|
|
@@ -1502,8 +1950,9 @@ async function keep() {
|
|
|
1502
1950
|
`);
|
|
1503
1951
|
process.exit(1);
|
|
1504
1952
|
}
|
|
1505
|
-
const profiled = run?.profile === undefined ? null :
|
|
1506
|
-
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) };
|
|
1507
1956
|
const transcript = run === undefined ? "ignore" : openSync2(run.transcript, "a");
|
|
1508
1957
|
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync3(join5(tmpdir4(), "gdharness-stop-")) : null;
|
|
1509
1958
|
const stopFile = own === null ? "" : join5(own, "stop");
|
|
@@ -1516,7 +1965,7 @@ async function keep() {
|
|
|
1516
1965
|
}
|
|
1517
1966
|
}
|
|
1518
1967
|
if ("error" in started) {
|
|
1519
|
-
|
|
1968
|
+
debuggerPeer?.close();
|
|
1520
1969
|
process.stdout.write(`error ${started.error}
|
|
1521
1970
|
`);
|
|
1522
1971
|
process.exit(1);
|
|
@@ -1538,24 +1987,32 @@ async function keep() {
|
|
|
1538
1987
|
}
|
|
1539
1988
|
let listening = null;
|
|
1540
1989
|
child.once("exit", (code, signal) => {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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);
|
|
1548
2006
|
}
|
|
1549
|
-
process.exit(0);
|
|
1550
2007
|
});
|
|
1551
2008
|
if (record !== null) {
|
|
1552
|
-
listening = await
|
|
2009
|
+
listening = await listenForTheServer(() => {
|
|
1553
2010
|
if (stopFile !== "") {
|
|
1554
2011
|
writeFileSync3(stopFile, "");
|
|
1555
2012
|
return "signalled";
|
|
1556
2013
|
}
|
|
1557
2014
|
return child.kill() ? "signalled" : "gone";
|
|
1558
|
-
});
|
|
2015
|
+
}, debuggerPeer === null ? null : (request) => debuggerPeer.ask(request));
|
|
1559
2016
|
if (listening !== null) {
|
|
1560
2017
|
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
1561
2018
|
}
|