@earendil-works/pi-durable 1.0.1 → 1.0.3
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/CHANGELOG.md +36 -0
- package/README.md +21 -5
- package/dist/env/decode.d.ts +16 -0
- package/dist/env/decode.d.ts.map +1 -0
- package/dist/env/decode.js +28 -0
- package/dist/env/decode.js.map +1 -0
- package/dist/env/index.d.ts +155 -3
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js +2 -0
- package/dist/env/index.js.map +1 -1
- package/dist/env/line-scan.d.ts +13 -0
- package/dist/env/line-scan.d.ts.map +1 -0
- package/dist/env/line-scan.js +147 -0
- package/dist/env/line-scan.js.map +1 -0
- package/dist/env/node-watch.d.ts +29 -0
- package/dist/env/node-watch.d.ts.map +1 -0
- package/dist/env/node-watch.js +431 -0
- package/dist/env/node-watch.js.map +1 -0
- package/dist/env/node.d.ts +11 -2
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +284 -21
- package/dist/env/node.js.map +1 -1
- package/dist/harness/agent.d.ts +2 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +5 -0
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +2 -0
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +8 -5
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +4 -1
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/output.d.ts +13 -6
- package/dist/harness/output.d.ts.map +1 -1
- package/dist/harness/output.js +61 -16
- package/dist/harness/output.js.map +1 -1
- package/dist/harness/provider.d.ts +14 -0
- package/dist/harness/provider.d.ts.map +1 -0
- package/dist/harness/provider.js +30 -0
- package/dist/harness/provider.js.map +1 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +12 -4
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +24 -3
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/view.d.ts +1 -1
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +8 -1
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/testing/env-conformance.d.ts +7 -0
- package/dist/testing/env-conformance.d.ts.map +1 -0
- package/dist/testing/env-conformance.js +397 -0
- package/dist/testing/env-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/runner.d.ts +4 -2
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +9 -0
- package/dist/testing/runner.js.map +1 -1
- package/dist/testing/types.d.ts +18 -0
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/types.js.map +1 -1
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +5 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/image.d.ts +10 -0
- package/dist/tools/image.d.ts.map +1 -1
- package/dist/tools/image.js +39 -0
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +148 -75
- package/dist/tools/read.js.map +1 -1
- package/dist/truncate.d.ts +9 -0
- package/dist/truncate.d.ts.map +1 -1
- package/dist/truncate.js +12 -4
- package/dist/truncate.js.map +1 -1
- package/package.json +3 -3
package/dist/env/node.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { constants, createWriteStream } from "node:fs";
|
|
4
|
-
import { access, appendFile, lstat, mkdir, mkdtemp, open as openFile, readdir, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { access, appendFile, lstat, mkdir, mkdtemp, opendir, open as openFile, readdir, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { homedir, constants as osConstants, tmpdir } from "node:os";
|
|
6
6
|
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { StreamDecoder } from "./decode.js";
|
|
8
9
|
import { ExecutionError, err, FileError, ok, toError, } from "./index.js";
|
|
10
|
+
import { LineScanner } from "./line-scan.js";
|
|
11
|
+
import { NodeFileWatcher } from "./node-watch.js";
|
|
9
12
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
10
13
|
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
11
14
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
12
15
|
const SPILL_HIGH_WATER_MARK = 1024 * 1024;
|
|
16
|
+
/** Largest single read of a `BinaryReader`, so a huge `length` allocates only as much as the file yields. */
|
|
17
|
+
const BINARY_READ_CHUNK = 1024 * 1024;
|
|
13
18
|
function resolveTimeoutMs(timeout) {
|
|
14
19
|
if (timeout === undefined)
|
|
15
20
|
return ok(undefined);
|
|
@@ -314,7 +319,7 @@ function waitForChildProcess(child, spillIsDraining) {
|
|
|
314
319
|
class NodeTextLineReader {
|
|
315
320
|
file;
|
|
316
321
|
path;
|
|
317
|
-
decoder = new
|
|
322
|
+
decoder = new StreamDecoder();
|
|
318
323
|
chunk = new Uint8Array(64 * 1024);
|
|
319
324
|
byteOffset = 0;
|
|
320
325
|
buffered = "";
|
|
@@ -356,7 +361,7 @@ class NodeTextLineReader {
|
|
|
356
361
|
this.ended = true;
|
|
357
362
|
}
|
|
358
363
|
else {
|
|
359
|
-
this.buffered += this.decoder.decode(this.chunk.subarray(0, bytesRead)
|
|
364
|
+
this.buffered += this.decoder.decode(this.chunk.subarray(0, bytesRead));
|
|
360
365
|
}
|
|
361
366
|
}
|
|
362
367
|
}
|
|
@@ -377,17 +382,194 @@ class NodeTextLineReader {
|
|
|
377
382
|
}
|
|
378
383
|
}
|
|
379
384
|
}
|
|
385
|
+
function closedResult(what, path) {
|
|
386
|
+
return err(new FileError("invalid", `${what} is closed`, path));
|
|
387
|
+
}
|
|
388
|
+
class NodeBinaryReader {
|
|
389
|
+
file;
|
|
390
|
+
path;
|
|
391
|
+
closed = false;
|
|
392
|
+
constructor(file, path) {
|
|
393
|
+
this.file = file;
|
|
394
|
+
this.path = path;
|
|
395
|
+
}
|
|
396
|
+
async info(context) {
|
|
397
|
+
const aborted = abortResult(context.abortSignal, this.path);
|
|
398
|
+
if (aborted)
|
|
399
|
+
return aborted;
|
|
400
|
+
if (this.closed)
|
|
401
|
+
return closedResult("Binary reader", this.path);
|
|
402
|
+
try {
|
|
403
|
+
return fileInfoFromStats(this.path, await this.file.stat());
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
return err(toFileError(error, this.path));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async read(offset, length, context) {
|
|
410
|
+
const aborted = abortResult(context.abortSignal, this.path);
|
|
411
|
+
if (aborted)
|
|
412
|
+
return aborted;
|
|
413
|
+
if (this.closed)
|
|
414
|
+
return closedResult("Binary reader", this.path);
|
|
415
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0) {
|
|
416
|
+
return err(new FileError("invalid", "Offset and length must be non-negative safe integers", this.path));
|
|
417
|
+
}
|
|
418
|
+
const chunks = [];
|
|
419
|
+
let total = 0;
|
|
420
|
+
try {
|
|
421
|
+
while (total < length) {
|
|
422
|
+
const chunk = new Uint8Array(Math.min(length - total, BINARY_READ_CHUNK));
|
|
423
|
+
const { bytesRead } = await this.file.read(chunk, 0, chunk.length, offset + total);
|
|
424
|
+
const afterReadAbort = abortResult(context.abortSignal, this.path);
|
|
425
|
+
if (afterReadAbort)
|
|
426
|
+
return afterReadAbort;
|
|
427
|
+
if (bytesRead === 0)
|
|
428
|
+
break;
|
|
429
|
+
chunks.push(bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead));
|
|
430
|
+
total += bytesRead;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return err(toFileError(error, this.path));
|
|
435
|
+
}
|
|
436
|
+
if (chunks.length === 1)
|
|
437
|
+
return ok(chunks[0]);
|
|
438
|
+
const bytes = new Uint8Array(total);
|
|
439
|
+
let position = 0;
|
|
440
|
+
for (const chunk of chunks) {
|
|
441
|
+
bytes.set(chunk, position);
|
|
442
|
+
position += chunk.length;
|
|
443
|
+
}
|
|
444
|
+
return ok(bytes);
|
|
445
|
+
}
|
|
446
|
+
async scanLines(options, context) {
|
|
447
|
+
const aborted = abortResult(context.abortSignal, this.path);
|
|
448
|
+
if (aborted)
|
|
449
|
+
return aborted;
|
|
450
|
+
if (this.closed)
|
|
451
|
+
return closedResult("Binary reader", this.path);
|
|
452
|
+
let scanner;
|
|
453
|
+
try {
|
|
454
|
+
scanner = new LineScanner(options.startLine, options.endLine);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return err(new FileError("invalid", "Invalid line range", this.path));
|
|
458
|
+
}
|
|
459
|
+
const chunk = new Uint8Array(64 * 1024);
|
|
460
|
+
try {
|
|
461
|
+
for (let position = 0;;) {
|
|
462
|
+
const { bytesRead } = await this.file.read(chunk, 0, chunk.length, position);
|
|
463
|
+
const afterReadAbort = abortResult(context.abortSignal, this.path);
|
|
464
|
+
if (afterReadAbort)
|
|
465
|
+
return afterReadAbort;
|
|
466
|
+
if (bytesRead === 0)
|
|
467
|
+
return ok(scanner.finish());
|
|
468
|
+
scanner.push(chunk.subarray(0, bytesRead));
|
|
469
|
+
position += bytesRead;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
return err(toFileError(error, this.path));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async close(_context) {
|
|
477
|
+
if (this.closed)
|
|
478
|
+
return;
|
|
479
|
+
this.closed = true;
|
|
480
|
+
await this.file.close().catch(() => undefined);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
class NodeDirReader {
|
|
484
|
+
dir;
|
|
485
|
+
path;
|
|
486
|
+
done = false;
|
|
487
|
+
closed = false;
|
|
488
|
+
constructor(dir, path) {
|
|
489
|
+
this.dir = dir;
|
|
490
|
+
this.path = path;
|
|
491
|
+
}
|
|
492
|
+
async next(maxEntries, context) {
|
|
493
|
+
const aborted = abortResult(context.abortSignal, this.path);
|
|
494
|
+
if (aborted)
|
|
495
|
+
return aborted;
|
|
496
|
+
if (this.closed)
|
|
497
|
+
return closedResult("Directory reader", this.path);
|
|
498
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) {
|
|
499
|
+
return err(new FileError("invalid", "maxEntries must be a positive safe integer", this.path));
|
|
500
|
+
}
|
|
501
|
+
const entries = [];
|
|
502
|
+
try {
|
|
503
|
+
while (!this.done && entries.length < maxEntries) {
|
|
504
|
+
const entry = await this.dir.read();
|
|
505
|
+
if (entry === null) {
|
|
506
|
+
this.done = true;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
const loopAbort = abortResult(context.abortSignal, this.path);
|
|
510
|
+
if (loopAbort)
|
|
511
|
+
return loopAbort;
|
|
512
|
+
const entryPath = resolve(this.path, entry.name);
|
|
513
|
+
let stats;
|
|
514
|
+
try {
|
|
515
|
+
stats = await lstat(entryPath);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
// Removed between enumeration and lstat: not part of the listing any more.
|
|
519
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
520
|
+
continue;
|
|
521
|
+
return err(toFileError(error, entryPath));
|
|
522
|
+
}
|
|
523
|
+
const info = fileInfoFromStats(entryPath, stats);
|
|
524
|
+
if (info.ok)
|
|
525
|
+
entries.push(info.value);
|
|
526
|
+
}
|
|
527
|
+
return ok({ entries, done: this.done });
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
return err(toFileError(error, this.path));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async close(_context) {
|
|
534
|
+
if (this.closed)
|
|
535
|
+
return;
|
|
536
|
+
this.closed = true;
|
|
537
|
+
await this.dir.close().catch(() => undefined);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function symlinkRefused(path, cause) {
|
|
541
|
+
return new FileError("invalid", "Refusing to follow a symbolic link", path, cause);
|
|
542
|
+
}
|
|
380
543
|
export class NodeExecutionEnv {
|
|
381
544
|
/** Every local environment sees the same files. */
|
|
382
545
|
id = "node:local";
|
|
383
546
|
cwd;
|
|
384
547
|
shellPath;
|
|
385
548
|
shellEnv;
|
|
549
|
+
watchOptions;
|
|
386
550
|
activeChildPids = new Set();
|
|
387
551
|
constructor(options) {
|
|
388
552
|
this.cwd = options.cwd;
|
|
389
553
|
this.shellPath = options.shellPath;
|
|
390
554
|
this.shellEnv = options.shellEnv;
|
|
555
|
+
this.watchOptions = options.watch ?? {};
|
|
556
|
+
}
|
|
557
|
+
async watch(targets, onChange, context) {
|
|
558
|
+
const aborted = abortResult(context.abortSignal);
|
|
559
|
+
if (aborted)
|
|
560
|
+
return aborted;
|
|
561
|
+
try {
|
|
562
|
+
const watcher = await NodeFileWatcher.open(targets, (path) => resolvePath(this.cwd, path), onChange, this.watchOptions);
|
|
563
|
+
const afterOpenAbort = abortResult(context.abortSignal);
|
|
564
|
+
if (afterOpenAbort) {
|
|
565
|
+
await watcher.close(context);
|
|
566
|
+
return afterOpenAbort;
|
|
567
|
+
}
|
|
568
|
+
return ok(watcher);
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
return err(toFileError(error));
|
|
572
|
+
}
|
|
391
573
|
}
|
|
392
574
|
async absolutePath(path, _context) {
|
|
393
575
|
return ok(resolvePath(this.cwd, path));
|
|
@@ -404,9 +586,26 @@ export class NodeExecutionEnv {
|
|
|
404
586
|
return err(timeoutMsResult.error);
|
|
405
587
|
const timeoutMs = timeoutMsResult.value;
|
|
406
588
|
const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
589
|
+
// A string runs through the shell; an argv array runs its program directly, without shell parsing.
|
|
590
|
+
let program;
|
|
591
|
+
let args;
|
|
592
|
+
let stdinCommand;
|
|
593
|
+
if (typeof command === "string") {
|
|
594
|
+
const shellConfig = await getShellConfig(this.shellPath);
|
|
595
|
+
if (!shellConfig.ok)
|
|
596
|
+
return shellConfig;
|
|
597
|
+
const commandFromStdin = shellConfig.value.commandTransport === "stdin";
|
|
598
|
+
program = shellConfig.value.shell;
|
|
599
|
+
args = commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command];
|
|
600
|
+
stdinCommand = commandFromStdin ? command : undefined;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
const [first, ...rest] = command;
|
|
604
|
+
if (first === undefined)
|
|
605
|
+
return err(new ExecutionError("spawn_error", "Empty argv: no program to run"));
|
|
606
|
+
program = first;
|
|
607
|
+
args = rest;
|
|
608
|
+
}
|
|
410
609
|
try {
|
|
411
610
|
await access(cwd, constants.F_OK);
|
|
412
611
|
}
|
|
@@ -439,14 +638,14 @@ export class NodeExecutionEnv {
|
|
|
439
638
|
onAbort();
|
|
440
639
|
};
|
|
441
640
|
// One decoder per stream, so a character split across chunks of one stream survives interleaving.
|
|
442
|
-
const stdoutDecoder = new
|
|
443
|
-
const stderrDecoder = new
|
|
641
|
+
const stdoutDecoder = new StreamDecoder();
|
|
642
|
+
const stderrDecoder = new StreamDecoder();
|
|
444
643
|
// No output reaches the caller after exec() settled, for example from a descendant holding stdio open.
|
|
445
|
-
const emit = (text) => {
|
|
644
|
+
const emit = (text, stream) => {
|
|
446
645
|
if (settled || text === "" || options?.onOutput === undefined || callbackError !== undefined)
|
|
447
646
|
return;
|
|
448
647
|
try {
|
|
449
|
-
options.onOutput(text, context);
|
|
648
|
+
options.onOutput(text, context, { stream });
|
|
450
649
|
}
|
|
451
650
|
catch (error) {
|
|
452
651
|
failCallback(error);
|
|
@@ -532,19 +731,18 @@ export class NodeExecutionEnv {
|
|
|
532
731
|
});
|
|
533
732
|
};
|
|
534
733
|
try {
|
|
535
|
-
|
|
536
|
-
child = spawn(shellConfig.value.shell, commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command], {
|
|
734
|
+
child = spawn(program, args, {
|
|
537
735
|
cwd,
|
|
538
736
|
detached: process.platform !== "win32",
|
|
539
737
|
env: getShellEnv(this.shellEnv, options?.env, options?.inheritEnv),
|
|
540
|
-
stdio: [
|
|
738
|
+
stdio: [stdinCommand === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
541
739
|
windowsHide: true,
|
|
542
740
|
});
|
|
543
741
|
if (child.pid)
|
|
544
742
|
this.activeChildPids.add(child.pid);
|
|
545
|
-
if (
|
|
743
|
+
if (stdinCommand !== undefined) {
|
|
546
744
|
child.stdin?.on("error", () => { });
|
|
547
|
-
child.stdin?.end(
|
|
745
|
+
child.stdin?.end(stdinCommand);
|
|
548
746
|
}
|
|
549
747
|
}
|
|
550
748
|
catch (error) {
|
|
@@ -565,8 +763,8 @@ export class NodeExecutionEnv {
|
|
|
565
763
|
else
|
|
566
764
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
567
765
|
}
|
|
568
|
-
const feed = (decoder) => (chunk) => {
|
|
569
|
-
emit(decoder.decode(chunk,
|
|
766
|
+
const feed = (source, decoder) => (chunk) => {
|
|
767
|
+
emit(decoder.decode(chunk), source);
|
|
570
768
|
const spill = options?.spill;
|
|
571
769
|
if (spill === undefined || chunk.length === 0)
|
|
572
770
|
return;
|
|
@@ -587,14 +785,14 @@ export class NodeExecutionEnv {
|
|
|
587
785
|
spillPrefix.length = 0;
|
|
588
786
|
startSpill(chunk);
|
|
589
787
|
};
|
|
590
|
-
child.stdout?.on("data", feed(stdoutDecoder));
|
|
591
|
-
child.stderr?.on("data", feed(stderrDecoder));
|
|
788
|
+
child.stdout?.on("data", feed("stdout", stdoutDecoder));
|
|
789
|
+
child.stderr?.on("data", feed("stderr", stderrDecoder));
|
|
592
790
|
void waitForChildProcess(child, () => spillError === undefined &&
|
|
593
791
|
spillStart !== undefined &&
|
|
594
792
|
(spillStream === undefined || spillBackpressured)).then(async ({ code, signal: exitSignal }) => {
|
|
595
793
|
await finishSpill();
|
|
596
|
-
emit(stdoutDecoder.decode());
|
|
597
|
-
emit(stderrDecoder.decode());
|
|
794
|
+
emit(stdoutDecoder.decode(), "stdout");
|
|
795
|
+
emit(stderrDecoder.decode(), "stderr");
|
|
598
796
|
if (callbackError) {
|
|
599
797
|
settle(err(callbackError));
|
|
600
798
|
return;
|
|
@@ -688,6 +886,50 @@ export class NodeExecutionEnv {
|
|
|
688
886
|
return err(toFileError(error, resolved));
|
|
689
887
|
}
|
|
690
888
|
}
|
|
889
|
+
async openBinaryReader(path, options, context) {
|
|
890
|
+
const resolved = resolvePath(this.cwd, path);
|
|
891
|
+
const aborted = abortResult(context.abortSignal, resolved);
|
|
892
|
+
if (aborted)
|
|
893
|
+
return aborted;
|
|
894
|
+
const noFollow = options?.noFollow === true;
|
|
895
|
+
let file;
|
|
896
|
+
try {
|
|
897
|
+
let flags = constants.O_RDONLY;
|
|
898
|
+
if (process.platform === "win32") {
|
|
899
|
+
// Windows has no O_NOFOLLOW; check the final component before opening (not race-free).
|
|
900
|
+
if (noFollow && (await lstat(resolved)).isSymbolicLink())
|
|
901
|
+
return err(symlinkRefused(resolved));
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
// Nonblocking, so opening a FIFO does not wait for a writer.
|
|
905
|
+
flags |= constants.O_NONBLOCK;
|
|
906
|
+
if (noFollow)
|
|
907
|
+
flags |= constants.O_NOFOLLOW;
|
|
908
|
+
}
|
|
909
|
+
file = await openFile(resolved, flags);
|
|
910
|
+
const stats = await file.stat();
|
|
911
|
+
if (!stats.isFile()) {
|
|
912
|
+
await file.close().catch(() => undefined);
|
|
913
|
+
return err(stats.isDirectory()
|
|
914
|
+
? new FileError("is_directory", "EISDIR: illegal operation on a directory, read", resolved)
|
|
915
|
+
: new FileError("invalid", "Not a regular file", resolved));
|
|
916
|
+
}
|
|
917
|
+
const afterOpenAbort = abortResult(context.abortSignal, resolved);
|
|
918
|
+
if (afterOpenAbort) {
|
|
919
|
+
await file.close().catch(() => undefined);
|
|
920
|
+
return afterOpenAbort;
|
|
921
|
+
}
|
|
922
|
+
return ok(new NodeBinaryReader(file, resolved));
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
await file?.close().catch(() => undefined);
|
|
926
|
+
// O_NOFOLLOW reports a final-component symlink as ELOOP (EMLINK on some BSDs).
|
|
927
|
+
if (noFollow && isNodeError(error) && (error.code === "ELOOP" || error.code === "EMLINK")) {
|
|
928
|
+
return err(symlinkRefused(resolved, toError(error)));
|
|
929
|
+
}
|
|
930
|
+
return err(toFileError(error, resolved));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
691
933
|
async writeFile(path, content, context) {
|
|
692
934
|
const resolved = resolvePath(this.cwd, path);
|
|
693
935
|
const signal = context.abortSignal;
|
|
@@ -757,6 +999,9 @@ export class NodeExecutionEnv {
|
|
|
757
999
|
let file;
|
|
758
1000
|
try {
|
|
759
1001
|
file = await openFile(resolved, "r+");
|
|
1002
|
+
// POSIX refuses to open a directory for writing; Windows opens it, so check explicitly.
|
|
1003
|
+
if ((await file.stat()).isDirectory())
|
|
1004
|
+
return err(new FileError("is_directory", "Is a directory", resolved));
|
|
760
1005
|
await file.sync();
|
|
761
1006
|
const afterSyncAbort = abortResult(signal, resolved);
|
|
762
1007
|
return afterSyncAbort ?? ok(undefined);
|
|
@@ -823,6 +1068,24 @@ export class NodeExecutionEnv {
|
|
|
823
1068
|
return err(toFileError(error, resolved));
|
|
824
1069
|
}
|
|
825
1070
|
}
|
|
1071
|
+
async openDirReader(path, context) {
|
|
1072
|
+
const resolved = resolvePath(this.cwd, path);
|
|
1073
|
+
const aborted = abortResult(context.abortSignal, resolved);
|
|
1074
|
+
if (aborted)
|
|
1075
|
+
return aborted;
|
|
1076
|
+
try {
|
|
1077
|
+
const dir = await opendir(resolved);
|
|
1078
|
+
const afterOpenAbort = abortResult(context.abortSignal, resolved);
|
|
1079
|
+
if (afterOpenAbort) {
|
|
1080
|
+
await dir.close().catch(() => undefined);
|
|
1081
|
+
return afterOpenAbort;
|
|
1082
|
+
}
|
|
1083
|
+
return ok(new NodeDirReader(dir, resolved));
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
return err(toFileError(error, resolved));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
826
1089
|
async canonicalPath(path, context) {
|
|
827
1090
|
const resolved = resolvePath(this.cwd, path);
|
|
828
1091
|
const aborted = abortResult(context.abortSignal, resolved);
|