@mpgd/cli 0.33.1 → 0.33.2

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.
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { constants } from 'node:fs';
2
3
  import { lstat, open, opendir, realpath, stat } from 'node:fs/promises';
3
4
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
5
  import { verifyZipV1Archive, ZipDecodeError, } from '@mpgd/phaser-assets/archive-validation';
@@ -6,6 +7,7 @@ import { defaultArchiveWorkerLimits } from '@mpgd/phaser-assets/archives';
6
7
  import { validatePhaserPackDeliveryManifest, } from '@mpgd/phaser-assets/pack-format';
7
8
  const DEFAULT_MANIFEST_BYTE_CAP = 32 * 1024 * 1024;
8
9
  const DEFAULT_VERIFY_TIMEOUT_MS = 120_000;
10
+ const FIFO_WRITER_GRACE_MS = 1000;
9
11
  /** The watchdog arms one millisecond after the deadline. Leave one
10
12
  * millisecond of headroom so the largest accepted timeout plus that offset
11
13
  * still fits the platform timer range instead of clamping to 1 ms. */
@@ -255,10 +257,10 @@ const inventoryRoot = async (root, deadline) => {
255
257
  return { files, bytes, largest, truncated };
256
258
  };
257
259
  const readManifestCapped = async (manifestPath, cap, deadline) => {
258
- // Open through the deadline: a FIFO with no writer blocks inside
259
- // open(2) before any stream event exists, and destroying a stream
260
- // cannot cancel that pending open.
261
- const handle = await deadline.race(open(resolve(manifestPath), 'r'));
260
+ // Open through the deadline. POSIX O_NONBLOCK keeps FIFO open/read calls
261
+ // bounded; Windows retains the stream fallback under the same deadline.
262
+ const flags = process.platform === 'win32' ? 'r' : constants.O_RDONLY | constants.O_NONBLOCK;
263
+ const handle = await deadline.race(open(resolve(manifestPath), flags));
262
264
  const chunks = [];
263
265
  let received = 0;
264
266
  let stalled = false;
@@ -292,30 +294,85 @@ const readManifestCapped = async (manifestPath, cap, deadline) => {
292
294
  throw new Error(`Delivery manifest changed while reading (expected ${expectedBytes} bytes, received more)`);
293
295
  }
294
296
  }
297
+ else if (process.platform !== 'win32') {
298
+ // A blocking FIFO read can remain pending after stream.destroy() on
299
+ // some Linux hosts. O_NONBLOCK bounds each read: EAGAIN means a writer
300
+ // is connected but has no data; a later zero-byte read is its EOF.
301
+ const buffer = Buffer.allocUnsafe(Math.min(STREAM_CHUNK_BYTES, cap + 1));
302
+ const isFifo = info.isFIFO();
303
+ // A writer can open and close between read polls without ever yielding
304
+ // EAGAIN. Bound the initial connection wait so that empty EOF cannot
305
+ // consume the default two-minute verification budget.
306
+ const writerGraceUntil = performance.now() + Math.min(FIFO_WRITER_GRACE_MS, deadline.remainingMs());
307
+ let writerObserved = false;
308
+ while (true) {
309
+ deadline.sample();
310
+ let bytesRead;
311
+ let wouldBlock = false;
312
+ try {
313
+ ({ bytesRead } = await deadline.race(handle.read(buffer, 0, buffer.byteLength, null)));
314
+ }
315
+ catch (error) {
316
+ if (typeof error !== 'object'
317
+ || error === null
318
+ || !('code' in error)
319
+ || (error.code !== 'EAGAIN' && error.code !== 'EWOULDBLOCK')) {
320
+ throw error;
321
+ }
322
+ writerObserved = true;
323
+ wouldBlock = true;
324
+ bytesRead = 0;
325
+ }
326
+ if (bytesRead === 0) {
327
+ // Other sources have ordinary EOF. An unconnected FIFO waits only
328
+ // for its bounded startup grace; an observed writer has hung up.
329
+ if (!wouldBlock
330
+ && (!isFifo || writerObserved || performance.now() >= writerGraceUntil)) {
331
+ break;
332
+ }
333
+ const waitMs = Math.min(10, Math.max(1, deadline.remainingMs()));
334
+ await deadline.race(new Promise((resume) => setTimeout(resume, waitMs)));
335
+ continue;
336
+ }
337
+ writerObserved = true;
338
+ received += bytesRead;
339
+ if (received > cap) {
340
+ throw new Error(`Delivery manifest exceeds ${cap} bytes`);
341
+ }
342
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
343
+ deadline.sample();
344
+ }
345
+ }
295
346
  else {
296
- // FIFOs and other non-regular sources retain the stream watchdog: a
297
- // pending read must be destroyed after the deadline so a direct
298
- // FileHandle.read cannot leave the smoke process alive behind a
299
- // rejected race.
347
+ // Windows non-regular sources retain the stream watchdog. A pending
348
+ // read must be destroyed at the deadline, then the race can return
349
+ // without waiting for that read to settle.
300
350
  const stream = handle.createReadStream({
301
351
  highWaterMark: STREAM_CHUNK_BYTES,
302
352
  autoClose: false,
303
353
  });
304
354
  const timer = deadline.armStream(stream);
305
355
  try {
306
- for await (const chunk of stream) {
307
- const view = chunk;
308
- received += view.byteLength;
309
- if (received > cap) {
310
- stream.destroy();
311
- throw new Error(`Delivery manifest exceeds ${cap} bytes`);
356
+ await deadline.race((async () => {
357
+ for await (const chunk of stream) {
358
+ const view = chunk;
359
+ received += view.byteLength;
360
+ if (received > cap) {
361
+ stream.destroy();
362
+ throw new Error(`Delivery manifest exceeds ${cap} bytes`);
363
+ }
364
+ chunks.push(Buffer.from(view));
365
+ deadline.sample();
312
366
  }
313
- chunks.push(Buffer.from(view));
314
- deadline.sample();
315
- }
367
+ })());
316
368
  }
317
369
  finally {
370
+ // Destroy may not wake a pending FIFO read until its writer closes.
371
+ // The raced deadline returns the report without waiting for that read.
318
372
  clearTimeout(timer);
373
+ if (!stream.destroyed) {
374
+ stream.destroy();
375
+ }
319
376
  }
320
377
  }
321
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.33.1",
3
+ "version": "0.33.2",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,18 +53,18 @@
53
53
  "@types/node": "^24.0.0",
54
54
  "ttsc": "0.30.4",
55
55
  "typescript": "7.0.2",
56
- "@mpgd/adapter-ait": "0.12.3",
57
- "@mpgd/adapter-browser": "0.7.4",
58
- "@mpgd/adapter-devvit": "0.9.9",
59
- "@mpgd/adapter-capacitor": "0.4.11",
60
- "@mpgd/adapter-verse8": "0.3.7",
61
- "@mpgd/analytics": "0.3.13",
56
+ "@mpgd/adapter-capacitor": "0.4.12",
57
+ "@mpgd/adapter-ait": "0.12.4",
58
+ "@mpgd/adapter-browser": "0.7.5",
59
+ "@mpgd/adapter-devvit": "0.9.10",
60
+ "@mpgd/adapter-verse8": "0.3.8",
61
+ "@mpgd/analytics": "0.3.14",
62
62
  "@mpgd/bridge": "0.9.1",
63
- "@mpgd/game-services": "0.15.2",
64
- "@mpgd/i18n": "0.6.2",
65
- "@mpgd/target-config": "0.15.1",
66
- "@mpgd/platform": "0.12.1",
67
- "@mpgd/catalog": "0.7.3"
63
+ "@mpgd/catalog": "0.7.4",
64
+ "@mpgd/game-services": "0.15.3",
65
+ "@mpgd/platform": "0.12.2",
66
+ "@mpgd/i18n": "0.6.3",
67
+ "@mpgd/target-config": "0.15.2"
68
68
  },
69
69
  "main": "./dist/index.js",
70
70
  "types": "./dist/index.d.ts",
@@ -57,6 +57,7 @@
57
57
  "dependencies": {
58
58
  "@ait-co/polyfill": "^0.1.19",
59
59
  "@apps-in-toss/web-framework": "3.5.0",
60
+ "@capacitor/core": "8.5.2",
60
61
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__",
61
62
  "@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION_ADAPTER_BROWSER__",
62
63
  "@mpgd/adapter-capacitor": "__MPGD_DEPENDENCY_VERSION_ADAPTER_CAPACITOR__",