mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
The relay's control lane is a shared 1 MiB budget, and `sendResponse` admitted responses onto it with the fatal default: once the lane was full, admission closed the client. A ~900 KB `fs.listFiles` reply from a large remote workspace therefore took down the whole remote session -- every terminal on it -- rather than failing the one Quick Open request. The substitute `ResponseOverCapacity` frame already there only covered the `legacy-response` lane, because the fatal close beat it to the client. A JSON-RPC response is the droppable class of control frame: it carries an id, so one caller can be told and can retry. `pty.replay` and `notifyControl` keep the fatal default -- they are never re-sent, and a silent drop there desyncs the client with nothing to retry. Both response enqueues now pass `controlOverflow: 'reject'`, so the substitute error is what the caller sees; in the corner where even ~150 bytes will not fit, the caller's own 30s request timeout settles it and the session survives. Old clients are unaffected: they already decode this error code and message generically (`ssh-channel-multiplexer.handleResponse` rejects the pending promise with both), and the frame shape is unchanged. What changes is that a listing which used to drop the connection now returns an error on it.
238 lines
9.8 KiB
TypeScript
238 lines
9.8 KiB
TypeScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { readRelayFileRange } from './fs-handler-file-range'
|
|
import { readFullStreamChunk } from './fs-handler-file-read'
|
|
import {
|
|
FileRangeReadRequestError,
|
|
MAX_FILE_RANGE_READ_BYTES,
|
|
validateFileRangeRequest
|
|
} from '../shared/file-range-read'
|
|
import { HEADER_LENGTH, prepareJsonRpcPayload } from './protocol'
|
|
import { DISPATCHER_CONTROL_QUEUE_MAX_BYTES } from './dispatcher-writer-admission'
|
|
|
|
let roots: string[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })))
|
|
roots = []
|
|
})
|
|
|
|
async function fileWith(contents: Buffer | string): Promise<string> {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-relay-range-'))
|
|
roots.push(root)
|
|
const path = join(root, 'data.bin')
|
|
await writeFile(path, contents)
|
|
return path
|
|
}
|
|
|
|
function decode(base64: string): Buffer {
|
|
return Buffer.from(base64, 'base64')
|
|
}
|
|
|
|
describe('readRelayFileRange', () => {
|
|
it('returns exactly the requested window', async () => {
|
|
const path = await fileWith('0123456789')
|
|
const result = await readRelayFileRange(path, 3, 4)
|
|
expect(decode(result.base64).toString('utf8')).toBe('3456')
|
|
expect(result.bytesRead).toBe(4)
|
|
})
|
|
|
|
it('reads from a non-zero position to the end', async () => {
|
|
const path = await fileWith('abcdef')
|
|
const result = await readRelayFileRange(path, 4, 2)
|
|
expect(decode(result.base64).toString('utf8')).toBe('ef')
|
|
})
|
|
|
|
// A short result must mean EOF and nothing else, or a caller advancing a
|
|
// cursor by bytesRead would silently skip data.
|
|
it('short-reads only at end of file', async () => {
|
|
const path = await fileWith('abc')
|
|
const result = await readRelayFileRange(path, 1, 100)
|
|
expect(result.bytesRead).toBe(2)
|
|
expect(decode(result.base64).toString('utf8')).toBe('bc')
|
|
})
|
|
|
|
it('returns zero bytes when the position is past the end', async () => {
|
|
const path = await fileWith('abc')
|
|
const result = await readRelayFileRange(path, 99, 10)
|
|
expect(result.bytesRead).toBe(0)
|
|
expect(decode(result.base64)).toHaveLength(0)
|
|
})
|
|
|
|
it('returns zero bytes when the position is exactly at the end', async () => {
|
|
const path = await fileWith('abc')
|
|
const result = await readRelayFileRange(path, 3, 10)
|
|
expect(result.bytesRead).toBe(0)
|
|
})
|
|
|
|
// A regular file answers a full-cap read in one syscall, so this pins offset
|
|
// arithmetic over a many-page window, not the fill loop -- that is covered
|
|
// against a stub reader in `readFullStreamChunk` below.
|
|
it('assembles a many-page window without misplacing any byte', async () => {
|
|
const size = MAX_FILE_RANGE_READ_BYTES
|
|
const contents = Buffer.allocUnsafe(size)
|
|
for (let i = 0; i < size; i++) {
|
|
contents[i] = i % 251
|
|
}
|
|
const path = await fileWith(contents)
|
|
const result = await readRelayFileRange(path, 1024, size - 1024)
|
|
expect(result.bytesRead).toBe(size - 1024)
|
|
// .equals(), not toEqual(): a byte-wise diff of a 256 KiB buffer costs
|
|
// minutes when it passes.
|
|
expect(decode(result.base64).equals(contents.subarray(1024))).toBe(true)
|
|
})
|
|
|
|
// Base64 rather than utf-8: a range boundary can split a multi-byte sequence,
|
|
// and a utf-8 round trip would substitute U+FFFD and shift every later offset.
|
|
it('preserves bytes that split a multi-byte character at both edges', async () => {
|
|
const text = Buffer.from('aé漢b', 'utf8')
|
|
const path = await fileWith(text)
|
|
const middle = await readRelayFileRange(path, 1, 3)
|
|
expect(decode(middle.base64)).toEqual(text.subarray(1, 4))
|
|
})
|
|
|
|
it('round-trips arbitrary binary bytes', async () => {
|
|
const bytes = Buffer.from([0x00, 0xff, 0x0a, 0x80, 0x7f])
|
|
const path = await fileWith(bytes)
|
|
const result = await readRelayFileRange(path, 0, bytes.length)
|
|
expect(decode(result.base64)).toEqual(bytes)
|
|
})
|
|
|
|
// The cap is a TRANSPORT budget, not just a memory one. A response frame over
|
|
// DISPATCHER_CONTROL_QUEUE_MAX_BYTES is demoted to the `legacy-response` lane,
|
|
// which the writer refuses once the producer queue is busy -- so an over-wide
|
|
// cap fails with ResponseOverCapacity depending on unrelated load.
|
|
//
|
|
// Fitting the lane once is not enough: the control queue is a SHARED budget,
|
|
// so a full-cap frame has to leave room for a second one. Anything wider lets
|
|
// two pipelined tail reads -- or one read racing an unrelated response --
|
|
// fail as ResponseOverCapacity on load that has nothing to do with them.
|
|
it('leaves control-queue headroom for a second full-cap window', async () => {
|
|
const contents = Buffer.allocUnsafe(MAX_FILE_RANGE_READ_BYTES)
|
|
for (let i = 0; i < contents.length; i++) {
|
|
contents[i] = (i * 37) % 256
|
|
}
|
|
const path = await fileWith(contents)
|
|
const result = await readRelayFileRange(path, 0, MAX_FILE_RANGE_READ_BYTES)
|
|
expect(result.bytesRead).toBe(MAX_FILE_RANGE_READ_BYTES)
|
|
const frameBytes =
|
|
HEADER_LENGTH + prepareJsonRpcPayload({ jsonrpc: '2.0', id: 4294967295, result }).byteLength
|
|
expect(frameBytes).toBeLessThanOrEqual(DISPATCHER_CONTROL_QUEUE_MAX_BYTES)
|
|
expect(frameBytes * 2).toBeLessThanOrEqual(DISPATCHER_CONTROL_QUEUE_MAX_BYTES)
|
|
})
|
|
|
|
it('rejects a missing file', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-relay-range-missing-'))
|
|
roots.push(root)
|
|
await expect(readRelayFileRange(join(root, 'nope.bin'), 0, 4)).rejects.toThrow()
|
|
})
|
|
|
|
// Every rejection below happens before the file is opened: these offsets go
|
|
// straight into a read syscall and the relay has no request schema.
|
|
describe('request validation', () => {
|
|
// Rejecting beats clamping: a clamped read is indistinguishable from EOF.
|
|
it('rejects an over-cap length instead of silently clamping', async () => {
|
|
const path = await fileWith('abc')
|
|
await expect(
|
|
readRelayFileRange(path, 0, MAX_FILE_RANGE_READ_BYTES + 1)
|
|
).rejects.toBeInstanceOf(FileRangeReadRequestError)
|
|
})
|
|
|
|
it('accepts a length exactly at the cap', async () => {
|
|
const path = await fileWith('abc')
|
|
const result = await readRelayFileRange(path, 0, MAX_FILE_RANGE_READ_BYTES)
|
|
expect(result.bytesRead).toBe(3)
|
|
})
|
|
|
|
it.each([
|
|
['a negative position', -1, 4],
|
|
['a fractional position', 1.5, 4],
|
|
['a NaN position', Number.NaN, 4],
|
|
['an infinite position', Number.POSITIVE_INFINITY, 4],
|
|
['a position past the safe-integer range', Number.MAX_SAFE_INTEGER + 2, 4],
|
|
['a window past the safe-integer range', Number.MAX_SAFE_INTEGER - 1, 3],
|
|
['a zero length', 0, 0],
|
|
['a negative length', 0, -4],
|
|
['a fractional length', 0, 4.5],
|
|
['a NaN length', 0, Number.NaN]
|
|
])('rejects %s', async (_label, position, length) => {
|
|
const path = await fileWith('abc')
|
|
await expect(readRelayFileRange(path, position, length)).rejects.toBeInstanceOf(
|
|
FileRangeReadRequestError
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
['a missing position', undefined, 4],
|
|
['a string position', '0', 4],
|
|
['a missing length', 0, undefined],
|
|
['a string length', 0, '4']
|
|
])('rejects %s from an unschema-d param bag', async (_label, position, length) => {
|
|
const path = await fileWith('abc')
|
|
await expect(readRelayFileRange(path, position, length)).rejects.toBeInstanceOf(
|
|
FileRangeReadRequestError
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
// The fill loop is what makes "short result == EOF" true. A regular file hands
|
|
// back a whole window in one syscall, so the loop only shows up against a
|
|
// reader that answers partially -- a pipe, a network filesystem, a large read
|
|
// interrupted by a signal.
|
|
describe('readFullStreamChunk', () => {
|
|
function partialReader(source: Buffer, perCall: number) {
|
|
const calls: { offset: number; length: number; position: number }[] = []
|
|
return {
|
|
calls,
|
|
read(buffer: Buffer, offset: number, length: number, position: number) {
|
|
calls.push({ offset, length, position })
|
|
const slice = source.subarray(position, position + Math.min(length, perCall))
|
|
slice.copy(buffer, offset)
|
|
return Promise.resolve({ bytesRead: slice.length })
|
|
}
|
|
}
|
|
}
|
|
|
|
it('fills the window across partial reads, each landing at its own offset', async () => {
|
|
const source = Buffer.from('abcdefghij')
|
|
const reader = partialReader(source, 3)
|
|
const buffer = Buffer.alloc(6)
|
|
const bytesRead = await readFullStreamChunk(reader, buffer, 6, 2)
|
|
expect(bytesRead).toBe(6)
|
|
expect(buffer.toString('utf8')).toBe('cdefgh')
|
|
expect(reader.calls).toEqual([
|
|
{ offset: 0, length: 6, position: 2 },
|
|
{ offset: 3, length: 3, position: 5 }
|
|
])
|
|
})
|
|
|
|
it('keeps every partial-read offset safe at the upper boundary', async () => {
|
|
const { position: start, length } = validateFileRangeRequest(Number.MAX_SAFE_INTEGER - 2, 3)
|
|
const calls: number[] = []
|
|
const reader = {
|
|
read(buffer: Buffer, offset: number, _length: number, position: number) {
|
|
calls.push(position)
|
|
buffer[offset] = 1
|
|
return Promise.resolve({ bytesRead: 1 })
|
|
}
|
|
}
|
|
const bytesRead = await readFullStreamChunk(reader, Buffer.alloc(length), length, start)
|
|
expect(bytesRead).toBe(length)
|
|
expect(calls).toEqual([start, start + 1, Number.MAX_SAFE_INTEGER])
|
|
expect(calls.every(Number.isSafeInteger)).toBe(true)
|
|
})
|
|
|
|
// Without the zero check the loop would spin forever on a reader at EOF.
|
|
it('stops at the first empty read and reports only what it filled', async () => {
|
|
const source = Buffer.from('abcd')
|
|
const reader = partialReader(source, 3)
|
|
const buffer = Buffer.alloc(16)
|
|
const bytesRead = await readFullStreamChunk(reader, buffer, 16, 0)
|
|
expect(bytesRead).toBe(4)
|
|
expect(buffer.subarray(0, bytesRead).toString('utf8')).toBe('abcd')
|
|
})
|
|
})
|