Merge branch 'feat/plugin-mgmt-3-settings' into feat/plugin-mgmt-4-web
This commit is contained in:
commit
654ec0d8c1
2 files changed
+65
-13
No files matched your search
@@ -158,17 +158,8 @@ function runChild(options: ProbeOptions, packageDir: string, mainSpecifier: stri
|
||||
)
|
||||
// The tail of stderr, for the failure text: a package that floods stderr at
|
||||
// import must not grow this process's heap by as much.
|
||||
const err: Buffer[] = []
|
||||
let errBytes = 0
|
||||
child.stderr?.on('data', (chunk: Buffer) => {
|
||||
err.push(chunk)
|
||||
errBytes += chunk.length
|
||||
while (errBytes > STDERR_TAIL_BYTES && err.length > 1) errBytes -= (err.shift() as Buffer).length
|
||||
if (errBytes > STDERR_TAIL_BYTES) {
|
||||
err[0] = (err[0] as Buffer).subarray(errBytes - STDERR_TAIL_BYTES)
|
||||
errBytes = STDERR_TAIL_BYTES
|
||||
}
|
||||
})
|
||||
const err = new StreamTail(STDERR_TAIL_BYTES)
|
||||
child.stderr?.on('data', (chunk: Buffer) => { err.push(chunk) })
|
||||
// The outcome lands on `close`. A kill — after the report, or at the
|
||||
// timeout — records its outcome and waits for the close it causes, so the
|
||||
// child's pipes and channel are gone when the caller continues; a spawn
|
||||
@@ -200,7 +191,7 @@ function runChild(options: ProbeOptions, packageDir: string, mainSpecifier: stri
|
||||
outcome()
|
||||
return
|
||||
}
|
||||
const tail = Buffer.concat(err).toString('utf8').trim()
|
||||
const tail = err.text().trim()
|
||||
reject(new Error(`${options.binName}: probe of ${options.packageName} exited with ${String(code)} without a report: ${tail}`))
|
||||
})
|
||||
})
|
||||
@@ -211,6 +202,48 @@ const DEFAULT_TIMEOUT_MS = 20_000
|
||||
/** How much of the child's stderr the failure text keeps: the end, where the cause usually is. */
|
||||
const STDERR_TAIL_BYTES = 16 * 1024
|
||||
|
||||
/**
|
||||
* The last `limit` bytes a stream delivered, kept as the chunks it arrived in:
|
||||
* as more arrives, whole chunks leave the front and the oldest kept chunk is
|
||||
* trimmed, so one chunk larger than the limit keeps its end and a small chunk
|
||||
* after it never drops what was kept.
|
||||
*/
|
||||
export class StreamTail {
|
||||
private readonly chunks: Buffer[] = []
|
||||
private bytes = 0
|
||||
|
||||
/** @param limit - how many bytes of the end to keep. */
|
||||
constructor(private readonly limit: number) {}
|
||||
|
||||
/**
|
||||
* Take one more chunk from the stream.
|
||||
* @param chunk - the bytes as delivered.
|
||||
*/
|
||||
push(chunk: Buffer): void {
|
||||
this.chunks.push(chunk)
|
||||
this.bytes += chunk.length
|
||||
while (this.bytes > this.limit) {
|
||||
const excess = this.bytes - this.limit
|
||||
const first = this.chunks[0] as Buffer
|
||||
if (first.length <= excess) {
|
||||
this.chunks.shift()
|
||||
this.bytes -= first.length
|
||||
} else {
|
||||
this.chunks[0] = first.subarray(excess)
|
||||
this.bytes = this.limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The kept end of the stream.
|
||||
* @returns the last bytes delivered, at most the limit, as UTF-8 text.
|
||||
*/
|
||||
text(): string {
|
||||
return Buffer.concat(this.chunks).toString('utf8')
|
||||
}
|
||||
}
|
||||
|
||||
/** The environment name carrying the run's report token; the child removes it before importing anything. */
|
||||
const PROBE_REPORT_VARIABLE = 'DSH_PROBE_REPORT'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PLUGIN_PROBE_DIR, probePackage, readProbeCache, writeProbeCache, type PluginProbe } from '../src/index.ts'
|
||||
import { PLUGIN_PROBE_FORMAT } from '../src/probe.ts'
|
||||
import { cordisPackageDir, parseProbeRecord } from '../src/probe.ts'
|
||||
import { cordisPackageDir, parseProbeRecord, StreamTail } from '../src/probe.ts'
|
||||
import { parseChildReport, type ChildReport } from '../src/probe-report.ts'
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
@@ -340,3 +340,22 @@ describe('parseChildReport', () => {
|
||||
expect(parseChildReport('report')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('StreamTail', () => {
|
||||
it('keeps the end of one oversized chunk and what a later small chunk pushes out of it', () => {
|
||||
// Linux hands the parent a flood as one read; the marker written after it arrives alone.
|
||||
const tail = new StreamTail(16)
|
||||
tail.push(Buffer.from('x'.repeat(200)))
|
||||
tail.push(Buffer.from('tail-marker'))
|
||||
expect(tail.text()).toBe('xxxxxtail-marker')
|
||||
})
|
||||
|
||||
it('drops whole leading chunks before trimming the oldest kept one', () => {
|
||||
// macOS drains a flood in small reads: the kept chunks are the last few.
|
||||
const tail = new StreamTail(10)
|
||||
for (const piece of ['abcd', 'efgh', 'ijkl', 'mn']) tail.push(Buffer.from(piece))
|
||||
expect(tail.text()).toBe('efghijklmn')
|
||||
tail.push(Buffer.from('opq'))
|
||||
expect(tail.text()).toBe('hijklmnopq')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user