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/keeper.js CHANGED
@@ -226,745 +226,1244 @@ function throughHelper(how, command, args, files) {
226
226
  };
227
227
  }
228
228
 
229
- // src/keeper-channel.ts
230
- import { randomUUID } from "node:crypto";
231
- import { mkdtempSync, rmSync } from "node:fs";
232
- import { connect, createServer } from "node:net";
233
- import { tmpdir } from "node:os";
234
- import { join } from "node:path";
235
- import process2 from "node:process";
236
- function endedOrWhyNot(end) {
237
- try {
238
- return end();
239
- } catch (error) {
240
- return `could not end it: ${error instanceof Error ? error.message : String(error)}`.replaceAll(`
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
- async function listenForAStop(end) {
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
- function listening(server, address) {
283
- return new Promise((resolve, reject) => {
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
- return env;
304
- }
305
- function sendSpec(child, spec) {
306
- child.stdin?.end(JSON.stringify(spec), "utf8");
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
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
314
- }
315
- var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
316
- function readLaunched(printed) {
317
- const line = printed.split(`
318
- `).find((one) => one.trim() !== "")?.trim() ?? "";
319
- const pid = /^pid (\d+)$/.exec(line);
320
- if (pid?.[1] !== undefined) {
321
- return { pid: Number(pid[1]) };
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 ENGINE_FLAGS = Object.freeze({
370
- "--verbose": "none",
371
- "--quiet": "none",
372
- "--debug-collisions": "none",
373
- "--debug-paths": "none",
374
- "--debug-navigation": "none",
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
- var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
410
- var verdicts = new Map;
411
- var LATE_ANSWER_MS = 30 * 60000;
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 until = Date.now() + HELD_FILE_MS;
427
- for (;; ) {
428
- try {
429
- renameSync(partial, path);
430
- return true;
431
- } catch (error) {
432
- const code = error.code ?? "";
433
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
434
- try {
435
- rmSync2(partial, { force: true });
436
- } catch {}
437
- return false;
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
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
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
- // src/run-record.ts
445
- var execFileAsync = promisify2(execFile2);
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
- missing.unshift(basename(at));
461
- at = parent;
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 projectKey(projectPath) {
466
- const real = realAsFarAsItExists(resolve2(projectPath)).replace(/[\\/]+$/, "");
467
- const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
468
- return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
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 runRecordPath(projectPath, directory = runtimeDirectory()) {
471
- return join3(directory, "runs", `note-${projectKey(projectPath)}.json`);
442
+ function pushU32(out, value) {
443
+ out.push(value & 255, value >>> 8 & 255, value >>> 16 & 255, value >>> 24 & 255);
472
444
  }
473
- function writeRunRecord(record) {
474
- mkdirSync(runsDirectory(), { recursive: true });
475
- const path = runRecordPath(record.projectPath);
476
- if (!writeWhole(path, JSON.stringify(record, null, 2))) {
477
- throw new Error(`the run note could not be written at ${path}`);
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 recordRunEnded(projectPath, pid, ending) {
481
- const path = runRecordPath(projectPath);
482
- const record = recordAt(path);
483
- if (record === null || record.pid !== pid) {
484
- return;
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 recordAt(path) {
495
- let parsed;
517
+ function readMessage(body) {
518
+ let value;
496
519
  try {
497
- parsed = JSON.parse(readFileSync(path, "utf8"));
498
- } catch {
499
- return null;
520
+ value = decodeVariant(body);
521
+ } catch (error) {
522
+ return { unreadable: error instanceof Error ? error.message : String(error) };
500
523
  }
501
- if (!parsed || typeof parsed !== "object") {
502
- return null;
524
+ if (!Array.isArray(value) || value.length !== 3) {
525
+ return { unreadable: "a message that is not [message, thread, data]" };
503
526
  }
504
- const fields = parsed;
505
- const pid = fields["pid"];
506
- const transcript = fields["transcript"];
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
- const args = fields["arguments"];
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/scratch.ts
530
- import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
531
- import { tmpdir as tmpdir3 } from "node:os";
532
- import { join as join4 } from "node:path";
533
- import process3 from "node:process";
534
- var SWEPT = { recursive: true, force: true };
535
- var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
536
- var waiting = new Set;
537
- var atExit = new Set;
538
- function scratchDirectory(kind) {
539
- return mkdtempSync2(join4(tmpdir3(), `gdharness-${kind}-${process3.pid}-`));
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 discardWith(remove, paths) {
542
- const left = [];
543
- for (const path of paths) {
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
- return left;
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
- return left;
561
- }
562
- function remove(path) {
563
- rmSync4(path, SWEPT);
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 tryAgain(path, attempt) {
566
- if (attempt === 0) {
567
- if (waiting.has(path)) {
568
- return;
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
- waiting.add(path);
571
- atExit.delete(path);
572
- lastTryOnExit();
628
+ frames.push({ id: frames.length, name, line, script });
573
629
  }
574
- const after = AGAIN_AFTER_MS[attempt];
575
- if (after === undefined || !existsSync(path)) {
576
- waiting.delete(path);
577
- if (after === undefined) {
578
- atExit.add(path);
579
- }
580
- return;
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
- setTimeout(() => {
583
- try {
584
- remove(path);
585
- waiting.delete(path);
586
- } catch {
587
- tryAgain(path, attempt + 1);
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
- }, after).unref();
590
- }
591
- var lastTryWatched = false;
592
- function lastTryOnExit() {
593
- if (lastTryWatched) {
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
- lastTryWatched = true;
597
- process3.once("exit", () => {
598
- for (const path of [...waiting, ...atExit]) {
599
- try {
600
- remove(path);
601
- } catch {}
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
- // src/script-profile.ts
607
- import { createServer as createServer2 } from "node:net";
608
-
609
- // src/gdscript-source.ts
610
- var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
611
- var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
612
- var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
613
- var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
614
- var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
615
- var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
616
- var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
617
- var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
618
- var DOTTED = `${NAME}(?:\\.${NAME})*`;
619
- var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
620
- var KEYWORDS = new Set([
621
- "and",
622
- "as",
623
- "assert",
624
- "await",
625
- "break",
626
- "breakpoint",
627
- "class",
628
- "class_name",
629
- "const",
630
- "continue",
631
- "elif",
632
- "else",
633
- "enum",
634
- "extends",
635
- "false",
636
- "for",
637
- "func",
638
- "if",
639
- "in",
640
- "is",
641
- "match",
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
- get remaining() {
700
- return this.view.byteLength - this.offset;
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
- need(bytes) {
703
- if (bytes < 0 || bytes > this.remaining) {
704
- throw new RangeError(`a value runs past the end of its message (${bytes} bytes wanted, ${this.remaining} left)`);
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
- u32() {
708
- this.need(4);
709
- const value = this.view.getUint32(this.offset, true);
710
- this.offset += 4;
711
- return value;
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
- i32() {
714
- this.need(4);
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
- i64() {
720
- this.need(8);
721
- const value = Number(this.view.getBigInt64(this.offset, true));
722
- this.offset += 8;
723
- return value;
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
- u64() {
726
- this.need(8);
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
- f32() {
732
- this.need(4);
733
- const value = this.view.getFloat32(this.offset, true);
734
- this.offset += 4;
735
- return value;
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
- f64() {
738
- this.need(8);
739
- const value = this.view.getFloat64(this.offset, true);
740
- this.offset += 8;
741
- return value;
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
- real(wide) {
744
- return wide ? this.f64() : this.f32();
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
- bytes(count) {
747
- this.need(count);
748
- const out = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, count);
749
- this.offset += count;
750
- return out;
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
- string() {
753
- const length = this.u32();
754
- const text = new TextDecoder().decode(this.bytes(length));
755
- this.bytes((4 - length % 4) % 4);
756
- return text;
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 MAX_DEPTH = 1024;
760
- function containerType(reader, kind) {
761
- if (kind === 1) {
762
- reader.u32();
763
- } else if (kind === 2 || kind === 3) {
764
- reader.string();
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
- function decodeValue(reader, depth) {
768
- if (depth > MAX_DEPTH) {
769
- throw new RangeError("a value nests deeper than the engine allows");
770
- }
771
- const header = reader.u32();
772
- const type = header & 255;
773
- const wide = (header & FLAG_64) !== 0;
774
- const math = MATH[type];
775
- if (math !== undefined) {
776
- return {
777
- kind: math.name,
778
- values: Array.from({ length: math.count }, () => math.ints === true ? reader.i32() : reader.real(wide))
779
- };
780
- }
781
- const vectorWidth = PACKED_VECTORS[type];
782
- if (vectorWidth !== undefined) {
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
- switch (type) {
790
- case NIL:
791
- return null;
792
- case BOOL:
793
- return reader.u32() !== 0;
794
- case INT:
795
- return wide ? reader.i64() : reader.i32();
796
- case FLOAT:
797
- return wide ? reader.f64() : reader.f32();
798
- case STRING:
799
- case 21:
800
- return reader.string();
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
- return { kind: "NodePath", values: Array.from({ length: names + subnames }, () => reader.string()) };
1243
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
815
1244
  }
816
- case 23:
817
- return { kind: "RID", values: [reader.u64()] };
818
- case 24:
819
- if ((header & FLAG_64) === 0) {
820
- throw new RangeError("a full object, which this reader does not take");
821
- }
822
- return { kind: "Object", values: [reader.u64()] };
823
- case 25:
824
- return { kind: "Callable", values: [] };
825
- case 26:
826
- return { kind: "Signal", values: [reader.string(), reader.u64()] };
827
- case 27: {
828
- containerType(reader, header >> 16 & 3);
829
- containerType(reader, header >> 18 & 3);
830
- const count = reader.u32() & 2147483647;
831
- const entries = [];
832
- for (let index = 0;index < count; index += 1) {
833
- const key = decodeValue(reader, depth + 1);
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
- return { kind: "dictionary", entries };
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 decodeVariant(bytes) {
869
- const reader = new Reader(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));
870
- const value = decodeValue(reader, 0);
871
- if (reader.remaining !== 0) {
872
- throw new RangeError(`${reader.remaining} bytes left over after the value`);
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 pushU32(out, value) {
877
- out.push(value & 255, value >>> 8 & 255, value >>> 16 & 255, value >>> 24 & 255);
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 encodeInto(out, value) {
880
- if (value === null) {
881
- pushU32(out, NIL);
882
- } else if (typeof value === "boolean") {
883
- pushU32(out, BOOL);
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
- } else if (typeof value === "string") {
901
- const bytes = new TextEncoder().encode(value);
902
- pushU32(out, STRING);
903
- pushU32(out, bytes.length);
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 encodeVariant(value) {
914
- const out = [];
915
- encodeInto(out, value);
916
- return Uint8Array.from(out);
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
- var MAIN_THREAD = 1;
919
- function framedCommand(message, data) {
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
- var MOST_MESSAGE_BYTES = (8 << 20) + 4;
927
-
928
- class DebuggerStream {
929
- pending = new Uint8Array(0);
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
- this.pending = joined.slice(at);
948
- return out;
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
- if (!Array.isArray(value) || value.length !== 3) {
959
- return { unreadable: "a message that is not [message, thread, data]" };
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
- const [message, , data] = value;
962
- if (typeof message !== "string" || !Array.isArray(data)) {
963
- return { unreadable: "a message whose name or data is the wrong type" };
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
- return { message, data };
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
- function profilerOn(frameFunctions) {
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 ProfileListener {
1256
- connection = null;
1752
+ class ProfileObserver {
1257
1753
  aggregate = null;
1258
- server;
1259
- port;
1260
1754
  changed;
1261
1755
  frameFunctions;
1262
- constructor(server, port, changed, frameFunctions) {
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
- static async open(changed, frameFunctions) {
1269
- const server = createServer2();
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
- const stream = new DebuggerStream;
1301
- socket.write(profilerOn(this.frameFunctions));
1763
+ send(PROFILER_SWITCH, [true, [this.frameFunctions, false]]);
1302
1764
  this.changed(aggregate.snapshot());
1303
- socket.on("data", (chunk) => {
1304
- let messages;
1305
- try {
1306
- messages = stream.push(chunk);
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
- async function profiledBy(path, projectPath) {
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 listener = await ProfileListener.open((totals) => {
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
- listener,
1866
+ observer,
1422
1867
  finish: () => {
1423
1868
  if (timer !== null) {
1424
1869
  clearTimeout(timer);
1425
1870
  }
1426
- latest = listener.totals() ?? 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 : await profiledBy(run.profile, run.projectPath);
1509
- const toStart = profiled === null ? spec : { ...spec, args: withEngineArguments(spec.args, profiled.listener.engineArguments) };
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
- profiled?.listener.close();
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
- if (run !== undefined) {
1545
- recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
1546
- }
1547
- profiled?.finish();
1548
- listening?.close();
1549
- if (own !== null) {
1550
- rmSync5(own, { recursive: true, force: true });
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 listenForAStop(() => {
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
  }