fix(runtime): close measured portability defects

This commit is contained in:
Tianyi Cui
2026-08-08 21:27:58 +08:00
parent 124fc6a611
commit 674d23118b
6 changed files with 73 additions and 26 deletions
+2 -3
View File
@@ -100,15 +100,14 @@ export async function readHostSource(
for await (const chunk of stream) {
throwIfAborted(signal)
bytes += Buffer.byteLength(chunk)
if (bytes > maxDocumentBytes) {
throw new Error(`source "${filePath}" exceeds the ${maxDocumentBytes}-byte limit`)
}
if (bytes > maxDocumentBytes) break
chunks.push(chunk)
}
} catch (error: unknown) {
throwIfAborted(signal)
throw new Error(`source "${filePath}" could not be read: ${messageOf(error)}`, { cause: error })
}
if (bytes > maxDocumentBytes) throw new Error(`source "${filePath}" exceeds the ${maxDocumentBytes}-byte limit`)
throwIfAborted(signal)
return {
fileUrl: fs.fileUrl(target),
+20 -18
View File
@@ -129,27 +129,29 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
// Resolve every server-local setting before registration so a bad later command or bound cannot
// publish an earlier provider. Registry-level mapping conflicts are rolled back below.
const providers = await (async () => {
const lookups = entries.map(async ([providerId, rawConfig]) => {
if (providerId.trim() === '') throw new Error('lsp-local: server ids must be non-empty strings')
const resolved = rawConfig as ResolvedServerConfig
validateServerConfig(providerId, resolved)
const executable = await ctx.subprocess.resolveExecutable(
resolved.command,
resolved.env,
setupAbort.signal,
)
setupAbort.signal.throwIfAborted()
return new LocalLspProvider(
providerId,
ctx.fs,
resolved,
executable,
spec => ctx.subprocess.spawn(spec),
)
})
try {
return await Promise.all(entries.map(async ([providerId, rawConfig]) => {
if (providerId.trim() === '') throw new Error('lsp-local: server ids must be non-empty strings')
const resolved = rawConfig as ResolvedServerConfig
validateServerConfig(providerId, resolved)
const executable = await ctx.subprocess.resolveExecutable(
resolved.command,
resolved.env,
setupAbort.signal,
)
setupAbort.signal.throwIfAborted()
return new LocalLspProvider(
providerId,
ctx.fs,
resolved,
executable,
spec => ctx.subprocess.spawn(spec),
)
}))
return await Promise.all(lookups)
} catch (error: unknown) {
setupAbort.abort(error)
await Promise.allSettled(lookups)
throw error
} finally {
stopSetupCancellation()
+3 -1
View File
@@ -158,7 +158,9 @@ describe('readHostSource', () => {
it('rejects an oversized source', async () => {
await writeFile(join(ws, 'big.ts'), 'x'.repeat(100))
await expect(readSource('big.ts', 10)).rejects.toThrow(/10-byte limit/)
await expect(readSource('big.ts', 10)).rejects.toMatchObject({
message: 'source "big.ts" exceeds the 10-byte limit',
})
})
it('counts the complete UTF-8 byte length at the configured boundary', async () => {
@@ -195,6 +195,50 @@ describe('lsp-local provider resolution', () => {
await ctx.fiber.dispose()
})
it('waits for aborted sibling executable lookups before setup rejects', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
const slowStarted = Promise.withResolvers<undefined>()
const slowAborted = Promise.withResolvers<undefined>()
const releaseCleanup = Promise.withResolvers<undefined>()
vi.spyOn(ctx.subprocess, 'resolveExecutable').mockImplementation(async (command, _env, signal) => {
if (signal === undefined) throw new Error('missing setup signal')
if (command === 'slow-lsp') {
return await new Promise<string>((_resolve, reject) => {
const onAbort = (): void => {
slowAborted.resolve(undefined)
void releaseCleanup.promise.then(() => {
reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)))
})
}
signal.addEventListener('abort', onAbort, { once: true })
slowStarted.resolve(undefined)
if (signal.aborted) onAbort()
})
}
await slowStarted.promise
throw new Error('lookup failed')
})
const loading = ctx.plugin(LspLocal, {
servers: {
slow: { command: 'slow-lsp', extensionToLanguage: { '.ts': 'typescript' } },
failing: { command: 'failing-lsp', extensionToLanguage: { '.js': 'javascript' } },
},
})
await slowAborted.promise
let settled = false
void loading.then(() => { settled = true }, () => { settled = true })
await new Promise<void>((resolve) => { setImmediate(resolve) })
expect(settled).toBe(false)
releaseCleanup.resolve(undefined)
await expect(loading).rejects.toThrow('lookup failed')
await ctx.fiber.dispose()
})
it('aborts executable resolution when disposed during setup', async () => {
const ctx = new Context()
await ctx.plugin(Lsp)