refactor(runtime): compose consumers over fs and subprocess

This commit is contained in:
Tianyi Cui
2026-07-28 23:00:00 +08:00
parent 987d477cfc
commit 8917ff8ef4
116 changed files with 2828 additions and 1122 deletions

View File

@@ -6,7 +6,7 @@
*/
import { randomUUID } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { constants, createReadStream } from 'node:fs'
import { chmod, lstat, mkdir, open, readFile, realpath, readdir, rename, rm, stat } from 'node:fs/promises'
import type { BigIntStats, Dirent, Stats } from 'node:fs'
import { basename, dirname, join, resolve } from 'node:path'
@@ -369,6 +369,77 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
return decodeUtf8(raw, 'read', target.displayPath)
}
/**
* Read one regular UTF-8 file through a single no-follow handle, retaining at
* most `maxBytes + 1` bytes so a concurrent grow cannot bypass the bound.
* @param target - the resolved file to read.
* @param maxBytes - positive safe-integer byte ceiling.
* @param signal - aborts between handle operations.
* @returns the complete decoded text when it fits.
*/
export async function readWholeTextBounded(
target: LocalTarget,
maxBytes: number,
signal?: AbortSignal,
): Promise<string> {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
throw new Error('bounded read maxBytes must be a positive safe integer')
}
throwIfAborted(signal, 'read')
let handle: Awaited<ReturnType<typeof open>>
try {
handle = await open(
target.targetKey,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
)
} catch (error: unknown) {
if (isENOENT(error)) throw new FsError(`cannot read "${target.displayPath}": not found`, 'FS_NOT_FOUND', { cause: error })
if (isPermissionError(error)) throw new FsError(`cannot read "${target.displayPath}": permission denied`, 'FS_PERMISSION_DENIED', { cause: error })
throw new FsError(`cannot read "${target.displayPath}" safely: ${errorMessage(error)}`, 'FS_IO_ERROR', { cause: error })
}
try {
throwIfAborted(signal, 'read')
const info = await handle.stat()
if (!info.isFile()) {
throw new FsError(`cannot read "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
}
if (info.size > maxBytes) {
throw new FsError(
`cannot read "${target.displayPath}": ${info.size} bytes exceeds the ${maxBytes}-byte limit`,
'FS_IO_ERROR',
)
}
const chunks: Buffer[] = []
let total = 0
for (;;) {
throwIfAborted(signal, 'read')
// Allocate in fixed internal chunks so a permissive deployment cap does
// not reserve that entire cap for a small file. Once exactly at the
// bound, one final byte detects concurrent growth without retaining it.
const remaining = total === maxBytes ? 1 : Math.min(64 * 1024, maxBytes - total)
const chunk = Buffer.allocUnsafe(remaining)
const { bytesRead } = await handle.read(chunk, 0, chunk.length, total)
if (bytesRead === 0) break
total += bytesRead
if (total > maxBytes) {
throw new FsError(
`cannot read "${target.displayPath}": file grew past the ${maxBytes}-byte limit while reading`,
'FS_IO_ERROR',
)
}
chunks.push(chunk.subarray(0, bytesRead))
}
throwIfAborted(signal, 'read')
const bytes = chunks.length === 1 ? chunks[0] as Buffer : Buffer.concat(chunks, total)
if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {
throw new FsError(`cannot read "${target.displayPath}": binary file`, 'FS_NOT_TEXT')
}
return decodeUtf8(bytes, 'read', target.displayPath)
} finally {
await handle.close()
}
}
/**
* Stream a whole regular UTF-8 text file as decoded text chunks. Same text
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,

View File

@@ -5,7 +5,8 @@
*/
import { Context } from 'cordis'
import { resolve } from 'node:path'
import { isAbsolute, relative, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import z from 'schemastery'
import { FileSystem, FsError, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
@@ -27,6 +28,7 @@ import {
readForEdit,
readTextForDiff,
readWholeText,
readWholeTextBounded,
resolveLocalTarget,
restoreLineEndings,
streamWholeText,
@@ -90,6 +92,19 @@ export class LocalFileSystem extends FileSystem {
return { targetKey: local.targetKey, displayPath: local.displayPath }
}
override processPath(target: FsTarget): string {
return String(target.targetKey)
}
override fileUrl(target: FsTarget): string {
return pathToFileURL(this.processPath(target)).href
}
override contains(parent: FsTarget, child: FsTarget): boolean {
const path = relative(this.processPath(parent), this.processPath(child))
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path))
}
override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
if (signal?.aborted) throw new FsError('stat aborted', 'FS_ABORTED')
const info = await probe(target.targetKey)
@@ -111,6 +126,10 @@ export class LocalFileSystem extends FileSystem {
return readWholeText({ displayPath: target.displayPath, targetKey: target.targetKey }, signal)
}
override async readTextBounded(target: FsTarget, maxBytes: number, signal?: AbortSignal): Promise<string> {
return readWholeTextBounded({ displayPath: target.displayPath, targetKey: target.targetKey }, maxBytes, signal)
}
override streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {
return Promise.resolve(streamWholeText({ displayPath: target.displayPath, targetKey: target.targetKey }, signal))
}