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/keeper.js CHANGED
@@ -226,742 +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
- });
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
- var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
407
- var verdicts = new Map;
408
- var LATE_ANSWER_MS = 30 * 60000;
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 until = Date.now() + HELD_FILE_MS;
424
- for (;; ) {
425
- try {
426
- renameSync(partial, path);
427
- return true;
428
- } catch (error) {
429
- const code = error.code ?? "";
430
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
431
- try {
432
- rmSync2(partial, { force: true });
433
- } catch {}
434
- 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");
435
373
  }
436
- 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()) };
437
381
  }
438
- }
439
- }
440
-
441
- // src/run-record.ts
442
- var execFileAsync = promisify2(execFile2);
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
- missing.unshift(basename(at));
458
- 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));
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 projectKey(projectPath) {
463
- const real = realAsFarAsItExists(resolve2(projectPath)).replace(/[\\/]+$/, "");
464
- const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
465
- 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;
466
441
  }
467
- function runRecordPath(projectPath, directory = runtimeDirectory()) {
468
- 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);
469
444
  }
470
- function writeRunRecord(record) {
471
- mkdirSync(runsDirectory(), { recursive: true });
472
- const path = runRecordPath(record.projectPath);
473
- if (!writeWhole(path, JSON.stringify(record, null, 2))) {
474
- 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
+ }
475
477
  }
476
478
  }
477
- function recordRunEnded(projectPath, pid, ending) {
478
- const path = runRecordPath(projectPath);
479
- const record = recordAt(path);
480
- if (record === null || record.pid !== pid) {
481
- 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;
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 recordAt(path) {
492
- let parsed;
517
+ function readMessage(body) {
518
+ let value;
493
519
  try {
494
- parsed = JSON.parse(readFileSync(path, "utf8"));
495
- } catch {
496
- return null;
520
+ value = decodeVariant(body);
521
+ } catch (error) {
522
+ return { unreadable: error instanceof Error ? error.message : String(error) };
497
523
  }
498
- if (!parsed || typeof parsed !== "object") {
499
- return null;
524
+ if (!Array.isArray(value) || value.length !== 3) {
525
+ return { unreadable: "a message that is not [message, thread, data]" };
500
526
  }
501
- const fields = parsed;
502
- const pid = fields["pid"];
503
- const transcript = fields["transcript"];
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
- const args = fields["arguments"];
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/scratch.ts
527
- import { existsSync, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
528
- import { tmpdir as tmpdir3 } from "node:os";
529
- import { join as join4 } from "node:path";
530
- import process3 from "node:process";
531
- var SWEPT = { recursive: true, force: true };
532
- var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
533
- var waiting = new Set;
534
- var atExit = new Set;
535
- function scratchDirectory(kind) {
536
- 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);
537
592
  }
538
- function discardWith(remove, paths) {
539
- const left = [];
540
- for (const path of paths) {
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
- return left;
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
- return left;
558
- }
559
- function remove(path) {
560
- 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(", ")})`;
561
620
  }
562
- function tryAgain(path, attempt) {
563
- if (attempt === 0) {
564
- if (waiting.has(path)) {
565
- 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;
566
627
  }
567
- waiting.add(path);
568
- atExit.delete(path);
569
- lastTryOnExit();
628
+ frames.push({ id: frames.length, name, line, script });
570
629
  }
571
- const after = AGAIN_AFTER_MS[attempt];
572
- if (after === undefined || !existsSync(path)) {
573
- waiting.delete(path);
574
- if (after === undefined) {
575
- atExit.add(path);
576
- }
577
- 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;
578
652
  }
579
- setTimeout(() => {
580
- try {
581
- remove(path);
582
- waiting.delete(path);
583
- } catch {
584
- 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");
585
665
  }
586
- }, after).unref();
587
- }
588
- var lastTryWatched = false;
589
- function lastTryOnExit() {
590
- if (lastTryWatched) {
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
- lastTryWatched = true;
594
- process3.once("exit", () => {
595
- for (const path of [...waiting, ...atExit]) {
596
- try {
597
- remove(path);
598
- } 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;
599
685
  }
600
- });
601
- }
602
-
603
- // src/script-profile.ts
604
- import { createServer as createServer2 } from "node:net";
605
-
606
- // src/gdscript-source.ts
607
- var DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
608
- var NAME = "[\\p{L}_][\\p{L}\\p{N}_]*";
609
- var ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*";
610
- var DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
611
- var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
612
- var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
613
- var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
614
- var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
615
- var DOTTED = `${NAME}(?:\\.${NAME})*`;
616
- var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
617
- var KEYWORDS = new Set([
618
- "and",
619
- "as",
620
- "assert",
621
- "await",
622
- "break",
623
- "breakpoint",
624
- "class",
625
- "class_name",
626
- "const",
627
- "continue",
628
- "elif",
629
- "else",
630
- "enum",
631
- "extends",
632
- "false",
633
- "for",
634
- "func",
635
- "if",
636
- "in",
637
- "is",
638
- "match",
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
- get remaining() {
697
- 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
+ });
698
737
  }
699
- need(bytes) {
700
- if (bytes < 0 || bytes > this.remaining) {
701
- 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
+ }
702
773
  }
703
774
  }
704
- u32() {
705
- this.need(4);
706
- const value = this.view.getUint32(this.offset, true);
707
- this.offset += 4;
708
- 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
+ }
709
782
  }
710
- i32() {
711
- this.need(4);
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
- i64() {
717
- this.need(8);
718
- const value = Number(this.view.getBigInt64(this.offset, true));
719
- this.offset += 8;
720
- 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
+ });
721
811
  }
722
- u64() {
723
- this.need(8);
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
- f32() {
729
- this.need(4);
730
- const value = this.view.getFloat32(this.offset, true);
731
- this.offset += 4;
732
- 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
+ }
733
866
  }
734
- f64() {
735
- this.need(8);
736
- const value = this.view.getFloat64(this.offset, true);
737
- this.offset += 8;
738
- 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
+ };
739
876
  }
740
- real(wide) {
741
- 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
+ };
742
881
  }
743
- bytes(count) {
744
- this.need(count);
745
- const out = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, count);
746
- this.offset += count;
747
- 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 ?? [] };
748
922
  }
749
- string() {
750
- const length = this.u32();
751
- const text = new TextDecoder().decode(this.bytes(length));
752
- this.bytes((4 - length % 4) % 4);
753
- 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);
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 MAX_DEPTH = 1024;
757
- function containerType(reader, kind) {
758
- if (kind === 1) {
759
- reader.u32();
760
- } else if (kind === 2 || kind === 3) {
761
- 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;
762
1209
  }
1210
+ const perUser = envValue("XDG_RUNTIME_DIR", variables);
1211
+ return join2(perUser ?? tmpdir2(), "gdharness");
763
1212
  }
764
- function decodeValue(reader, depth) {
765
- if (depth > MAX_DEPTH) {
766
- throw new RangeError("a value nests deeper than the engine allows");
767
- }
768
- const header = reader.u32();
769
- const type = header & 255;
770
- const wide = (header & FLAG_64) !== 0;
771
- const math = MATH[type];
772
- if (math !== undefined) {
773
- return {
774
- kind: math.name,
775
- values: Array.from({ length: math.count }, () => math.ints === true ? reader.i32() : reader.real(wide))
776
- };
777
- }
778
- const vectorWidth = PACKED_VECTORS[type];
779
- if (vectorWidth !== undefined) {
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
- switch (type) {
787
- case NIL:
788
- return null;
789
- case BOOL:
790
- return reader.u32() !== 0;
791
- case INT:
792
- return wide ? reader.i64() : reader.i32();
793
- case FLOAT:
794
- return wide ? reader.f64() : reader.f32();
795
- case STRING:
796
- case 21:
797
- return reader.string();
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
- 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);
812
1244
  }
813
- case 23:
814
- return { kind: "RID", values: [reader.u64()] };
815
- case 24:
816
- if ((header & FLAG_64) === 0) {
817
- throw new RangeError("a full object, which this reader does not take");
818
- }
819
- return { kind: "Object", values: [reader.u64()] };
820
- case 25:
821
- return { kind: "Callable", values: [] };
822
- case 26:
823
- return { kind: "Signal", values: [reader.string(), reader.u64()] };
824
- case 27: {
825
- containerType(reader, header >> 16 & 3);
826
- containerType(reader, header >> 18 & 3);
827
- const count = reader.u32() & 2147483647;
828
- const entries = [];
829
- for (let index = 0;index < count; index += 1) {
830
- const key = decodeValue(reader, depth + 1);
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
- return { kind: "dictionary", entries };
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 decodeVariant(bytes) {
866
- const reader = new Reader(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));
867
- const value = decodeValue(reader, 0);
868
- if (reader.remaining !== 0) {
869
- 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}`);
870
1282
  }
871
- return value;
872
1283
  }
873
- function pushU32(out, value) {
874
- 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}-`));
875
1344
  }
876
- function encodeInto(out, value) {
877
- if (value === null) {
878
- pushU32(out, NIL);
879
- } else if (typeof value === "boolean") {
880
- pushU32(out, BOOL);
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
- } else if (typeof value === "string") {
898
- const bytes = new TextEncoder().encode(value);
899
- pushU32(out, STRING);
900
- pushU32(out, bytes.length);
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 encodeVariant(value) {
911
- const out = [];
912
- encodeInto(out, value);
913
- 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;
914
1365
  }
915
- var MAIN_THREAD = 1;
916
- function framedCommand(message, data) {
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
- var MOST_MESSAGE_BYTES = (8 << 20) + 4;
924
-
925
- class DebuggerStream {
926
- pending = new Uint8Array(0);
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
- this.pending = joined.slice(at);
945
- return out;
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
- if (!Array.isArray(value) || value.length !== 3) {
956
- 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;
957
1385
  }
958
- const [message, , data] = value;
959
- if (typeof message !== "string" || !Array.isArray(data)) {
960
- 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;
961
1399
  }
962
- 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
+ });
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
- function profilerOn(frameFunctions) {
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 ProfileListener {
1253
- connection = null;
1752
+ class ProfileObserver {
1254
1753
  aggregate = null;
1255
- server;
1256
- port;
1257
1754
  changed;
1258
1755
  frameFunctions;
1259
- constructor(server, port, changed, frameFunctions) {
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
- static async open(changed, frameFunctions) {
1266
- const server = createServer2();
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
- const stream = new DebuggerStream;
1298
- socket.write(profilerOn(this.frameFunctions));
1763
+ send(PROFILER_SWITCH, [true, [this.frameFunctions, false]]);
1299
1764
  this.changed(aggregate.snapshot());
1300
- socket.on("data", (chunk) => {
1301
- let messages;
1302
- try {
1303
- messages = stream.push(chunk);
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
- async function profiledBy(path, projectPath) {
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 listener = await ProfileListener.open((totals) => {
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
- listener,
1866
+ observer,
1419
1867
  finish: () => {
1420
1868
  if (timer !== null) {
1421
1869
  clearTimeout(timer);
1422
1870
  }
1423
- latest = listener.totals() ?? 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 : await profiledBy(run.profile, run.projectPath);
1506
- 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) };
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
- profiled?.listener.close();
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
- if (run !== undefined) {
1542
- recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
1543
- }
1544
- profiled?.finish();
1545
- listening?.close();
1546
- if (own !== null) {
1547
- 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);
1548
2006
  }
1549
- process.exit(0);
1550
2007
  });
1551
2008
  if (record !== null) {
1552
- listening = await listenForAStop(() => {
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
  }