Files
vercel__next.js/packages/devlow-bench/src/shell.ts
Will Binns-Smith 007058ef60 Lint devlow-bench with the root eslint config (#96871)
Follow-up to #96860, addressing @lukesandberg's review.

That PR moved `devlow-bench` into `packages/`, where the root eslint
config applies for the first time, and exempted the package from the
three rules it didn't satisfy rather than fixing the violations.

This drops the exemption block:

- **`import/no-extraneous-dependencies`** — had no violations. The
exemption was unnecessary.
- **`@typescript-eslint/consistent-type-imports`** — 13 violations, all
autofixable.
- **`@typescript-eslint/no-shadow`** — 17 violations, renamed by hand.

The shadow fixes are mostly loop variables and callback parameters that
restated their outer binding. Two worth a look:

- `table.ts` — `getValue(data, ...)` took a single entry but shadowed
the `data` array it indexes into. Renamed the parameter to `entry`, and
the `row`/`column` grouping keys to `rowKey`/`columnKey` so the loop
variables can keep the shorter names.
- `shell.ts` — `command(command, args)` shadowed the exported function
with its own first parameter. Renamed to `executable`, which also fixes
the `[SHELL]` debug log that was interpolating the function rather than
the argument.

## Verification

- `eslint` clean under both the root config and the package's own `lint`
script
- `tsc` compiles, `pnpm run --filter=devlow-bench test` 3/3
- prettier clean

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
2026-08-06 20:39:39 -07:00

207 lines
5.2 KiB
TypeScript

import type { ChildProcess } from 'child_process'
import { spawn } from 'child_process'
import split2 from 'split2'
import treeKill from 'tree-kill'
import pidusage from 'pidusage-tree'
import type { PREVIOUS } from './describe.js'
import { reportMeasurement } from './describe.js'
export interface Command {
ok(): Promise<void>
kill(): Promise<void>
end(): Promise<number>
waitForOutput(
regex: RegExp,
options?: {
timeoutMs?: number
}
): Promise<RegExpMatchArray>
reportMemUsage(
metricName: string,
options: {
relativeTo?: string | typeof PREVIOUS
scenario?: string
props?: Record<string, string | number | null>
}
): Promise<void>
stdout: string
stderr: string
output: string
}
const shellOutput = !!process.env.SHELL_OUTPUT
class CommandImpl {
stdout: string = ''
stderr: string = ''
output: string = ''
outputCursor: number = 0
exitPromise: Promise<number>
waitingForOutput: (() => void)[] = []
constructor(private process: ChildProcess) {
process.stdout?.pipe(split2()).on('data', (data) => {
const str = data.toString()
this.stdout += str + '\n'
this.output += str + '\n'
if (shellOutput) {
console.log(`[STDOUT] ${str}`)
}
if (this.waitingForOutput.length !== 0) {
const waitingForOutput = this.waitingForOutput
this.waitingForOutput = []
for (const fn of waitingForOutput) {
fn()
}
}
})
process.stderr?.pipe(split2()).on('data', (data) => {
const str = data.toString()
this.stderr += str + '\n'
this.output += str + '\n'
if (shellOutput) {
console.log(`[STDERR] ${str}`)
}
if (this.waitingForOutput.length !== 0) {
const waitingForOutput = this.waitingForOutput
this.waitingForOutput = []
for (const fn of waitingForOutput) {
fn()
}
}
})
this.exitPromise = new Promise<number>((resolve, reject) => {
process.on('error', reject)
process.on('exit', resolve)
})
}
async ok() {
const exitCode = await this.exitPromise
if (exitCode !== 0) {
throw new Error(
`Command exited with code ${exitCode}\n\nOutput:\n${this.output}`
)
}
}
async end() {
return await this.exitPromise
}
async kill() {
const pid = this.process.pid!
await new Promise<void>((resolve, reject) =>
treeKill(pid, (err) => {
if (err) reject(err)
else resolve()
})
)
await this.exitPromise
}
async waitForOutput(
regex: RegExp,
options: {
timeoutMs?: number
} = {}
) {
let start = this.outputCursor
const deadline =
options.timeoutMs === undefined
? undefined
: Date.now() + options.timeoutMs
while (true) {
const outputToSearch = this.output.slice(start)
const match = outputToSearch.match(regex)
if (match) {
const matchIndex = match.index ?? outputToSearch.search(regex)
if (matchIndex >= 0) {
this.outputCursor = start + matchIndex + match[0].length
} else {
this.outputCursor = this.output.length
}
return match
}
const promises: Promise<number | 'output' | 'timeout'>[] = [
this.exitPromise,
new Promise<void>((resolve) => {
this.waitingForOutput.push(resolve)
}).then(() => 'output'),
]
let timeoutId: ReturnType<typeof setTimeout> | undefined
if (deadline !== undefined) {
const timeoutMs = deadline - Date.now()
if (timeoutMs <= 0) {
throw new Error(
`Timed out waiting for output matching ${regex}\n\nOutput:\n${this.output}`
)
}
promises.push(
new Promise<'timeout'>((resolve) => {
timeoutId = setTimeout(() => resolve('timeout'), timeoutMs)
})
)
}
const waitResult = await Promise.race(promises)
if (timeoutId) clearTimeout(timeoutId)
if (waitResult !== 'output') {
if (waitResult === 'timeout') {
throw new Error(
`Timed out waiting for output matching ${regex}\n\nOutput:\n${this.output}`
)
}
throw new Error(
`Command exited with code ${waitResult}\n\nOutput:\n${this.output}`
)
}
}
}
async reportMemUsage(
metricName: string,
options: {
relativeTo?: string | typeof PREVIOUS
scenario?: string
props?: Record<string, string | number | null>
} = {}
) {
try {
const pid = this.process.pid!
const report = await pidusage(pid)
const memUsage = Object.values(report)
.filter((x) => x)
.map((x) => (x as any).memory)
.reduce((a, b) => a + b, 0)
await reportMeasurement(metricName, memUsage, 'bytes', options)
} catch (e) {
// ignore
}
}
}
export function command(
executable: string,
args: string[],
options: {
env?: Record<string, string>
cwd?: string
} = {}
): Command {
const process = spawn(executable, args, {
shell: true,
...options,
stdio: ['ignore', 'pipe', 'pipe'],
})
if (shellOutput) {
console.log(
`[SHELL] ${executable} ${args.join(' ')} ${JSON.stringify(options)}`
)
}
return new CommandImpl(process)
}