mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(cli): limit background process port scans
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Skip inferred background-process port scanning in VS Code sessions to avoid unnecessary Bun subprocess polling.
|
||||
Limit inferred background-process port discovery to the TUI and stop scanning after startup to avoid unnecessary Bun subprocess polling.
|
||||
|
||||
@@ -176,6 +176,7 @@ export const TuiThreadCommand = cmd({
|
||||
const env = sanitizedProcessEnv({
|
||||
[KILO_PROCESS_ROLE]: "worker",
|
||||
[KILO_RUN_ID]: ensureRunID(),
|
||||
KILO_BACKGROUND_PROCESS_PORTS: "true", // kilocode_change - TUI surfaces inferred background process ports
|
||||
})
|
||||
|
||||
const worker = new Worker(file, {
|
||||
|
||||
@@ -23,7 +23,9 @@ export namespace BackgroundProcess {
|
||||
const KILL_MS = 3_000
|
||||
const READY_MS = 30_000
|
||||
const PUBLISH_MS = 500
|
||||
const PORT_MS = 2_000
|
||||
const PORT_START_MS = 500
|
||||
const PORT_MS = 5_000
|
||||
const PORT_LIMIT_MS = 30_000
|
||||
|
||||
const idSchema = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("bgp") }).pipe(
|
||||
Schema.brand("BackgroundProcessID"),
|
||||
@@ -165,7 +167,11 @@ export namespace BackgroundProcess {
|
||||
return a.length === b.length && a.every((port, index) => port === b[index])
|
||||
}
|
||||
|
||||
async function refresh(active: Active) {
|
||||
function infer() {
|
||||
return Flag.KILO_CLIENT === "cli" && process.env.KILO_BACKGROUND_PROCESS_PORTS === "true"
|
||||
}
|
||||
|
||||
function update(active: Active, ports?: number[]) {
|
||||
const pid = active.proc.pid
|
||||
if (!pid || terminal(active.info.status)) {
|
||||
const changed = active.info.ports.length > 0
|
||||
@@ -173,14 +179,19 @@ export namespace BackgroundProcess {
|
||||
return changed
|
||||
}
|
||||
const fallback = active.info.ready && active.start.ready?.port ? [active.start.ready.port] : []
|
||||
const ports = Flag.KILO_CLIENT === "cli" ? await Ports.list(pid) : []
|
||||
const next = Array.from(new Set([...ports, ...fallback])).toSorted((a, b) => a - b)
|
||||
const next = Array.from(new Set([...(ports ?? active.info.ports), ...fallback])).toSorted((a, b) => a - b)
|
||||
if (same(active.info.ports, next)) return false
|
||||
active.info.ports = next
|
||||
active.info.time.updated = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
async function refresh(active: Active) {
|
||||
const pid = active.proc.pid
|
||||
if (!pid || terminal(active.info.status)) return update(active)
|
||||
return update(active, await Ports.list(pid))
|
||||
}
|
||||
|
||||
function emit(active: Active) {
|
||||
Instance.restore(active.ctx, () => {
|
||||
void Bus.publish(Event.Updated, { info: clone(active.info) }).catch((err) => {
|
||||
@@ -191,49 +202,45 @@ export namespace BackgroundProcess {
|
||||
|
||||
function publish(active: Active) {
|
||||
if (active.disposed) return
|
||||
active.scan = (active.scan ?? refresh(active))
|
||||
.then(() => {
|
||||
update(active)
|
||||
emit(active)
|
||||
}
|
||||
|
||||
function done(active: Active) {
|
||||
if (active.disposed) return true
|
||||
if (!infer()) return true
|
||||
if (terminal(active.info.status)) return true
|
||||
if (active.info.ports.length > 0) return true
|
||||
return Date.now() - active.info.time.started >= PORT_LIMIT_MS
|
||||
}
|
||||
|
||||
function scan(active: Active) {
|
||||
if (done(active)) return
|
||||
if (active.scan) return
|
||||
active.scan = refresh(active)
|
||||
.then((changed) => {
|
||||
active.scan = undefined
|
||||
if (active.disposed) return false
|
||||
emit(active)
|
||||
if (changed) emit(active)
|
||||
poll(active)
|
||||
return false
|
||||
return changed
|
||||
})
|
||||
.catch((err) => {
|
||||
active.scan = undefined
|
||||
if (active.disposed) return false
|
||||
log.debug("failed to refresh process ports", { err, id: active.info.id })
|
||||
emit(active)
|
||||
poll(active)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function poll(active: Active) {
|
||||
if (active.disposed) return
|
||||
if (Flag.KILO_CLIENT !== "cli") return
|
||||
if (terminal(active.info.status)) return
|
||||
function poll(active: Active, ms = PORT_MS) {
|
||||
if (done(active)) return
|
||||
if (active.poll) return
|
||||
active.poll = setTimeout(() => {
|
||||
active.poll = undefined
|
||||
if (active.disposed) return
|
||||
if (terminal(active.info.status)) return
|
||||
active.scan = (active.scan ?? refresh(active))
|
||||
.then((changed) => {
|
||||
active.scan = undefined
|
||||
if (active.disposed) return false
|
||||
if (changed) emit(active)
|
||||
poll(active)
|
||||
return changed
|
||||
})
|
||||
.catch((err) => {
|
||||
active.scan = undefined
|
||||
if (active.disposed) return false
|
||||
log.debug("failed to refresh process ports", { err, id: active.info.id })
|
||||
poll(active)
|
||||
return false
|
||||
})
|
||||
}, PORT_MS)
|
||||
scan(active)
|
||||
}, ms)
|
||||
}
|
||||
|
||||
function schedule(active: Active) {
|
||||
@@ -497,6 +504,7 @@ export namespace BackgroundProcess {
|
||||
exited(active, code, signal)
|
||||
})
|
||||
publish(active)
|
||||
poll(active, PORT_START_MS)
|
||||
if (input.ready) await wait(active, input.ready)
|
||||
return clone(active.info)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Process } from "@/util/process"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
const SCAN_MS = 1_000
|
||||
|
||||
function sorted(items: Iterable<number>) {
|
||||
return Array.from(items).toSorted((a, b) => a - b)
|
||||
}
|
||||
@@ -92,7 +94,7 @@ async function linux(root: number) {
|
||||
}
|
||||
|
||||
async function ps(root: number) {
|
||||
const rows = await Process.lines(["ps", "-axo", "pid=,ppid="], { nothrow: true })
|
||||
const rows = await lines(["ps", "-axo", "pid=,ppid="])
|
||||
const children = new Map<number, number[]>()
|
||||
for (const row of rows) {
|
||||
const [pid, parent] = row.trim().split(/\s+/).map(Number)
|
||||
@@ -115,9 +117,7 @@ async function ps(root: number) {
|
||||
|
||||
async function lsof(root: number) {
|
||||
const pids = await ps(root).catch(() => new Set([root]))
|
||||
const rows = await Process.lines(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", Array.from(pids).join(",")], {
|
||||
nothrow: true,
|
||||
})
|
||||
const rows = await lines(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", Array.from(pids).join(",")])
|
||||
return sorted(
|
||||
new Set(
|
||||
rows.flatMap((row) => {
|
||||
@@ -128,6 +128,17 @@ async function lsof(root: number) {
|
||||
)
|
||||
}
|
||||
|
||||
async function lines(cmd: string[]) {
|
||||
const ctrl = new AbortController()
|
||||
const timer = setTimeout(() => ctrl.abort(), SCAN_MS)
|
||||
timer.unref?.()
|
||||
try {
|
||||
return await Process.lines(cmd, { nothrow: true, abort: ctrl.signal, timeout: 500 })
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
export async function list(root: number) {
|
||||
if (process.platform === "linux") {
|
||||
const ports = await linux(root).catch(() => [])
|
||||
|
||||
@@ -150,7 +150,9 @@ setInterval(() => {}, 1_000)
|
||||
),
|
||||
)
|
||||
const client = process.env["KILO_CLIENT"]
|
||||
const scans = process.env["KILO_BACKGROUND_PROCESS_PORTS"]
|
||||
process.env["KILO_CLIENT"] = "cli"
|
||||
process.env["KILO_BACKGROUND_PROCESS_PORTS"] = "true"
|
||||
|
||||
try {
|
||||
const info = yield* Effect.promise(() =>
|
||||
@@ -173,6 +175,48 @@ setInterval(() => {}, 1_000)
|
||||
yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID))
|
||||
if (client === undefined) delete process.env["KILO_CLIENT"]
|
||||
else process.env["KILO_CLIENT"] = client
|
||||
if (scans === undefined) delete process.env["KILO_BACKGROUND_PROCESS_PORTS"]
|
||||
else process.env["KILO_BACKGROUND_PROCESS_PORTS"] = scans
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("does not infer ports for CLI clients without opt in", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const sessionID = SessionID.descending()
|
||||
const listen = port()
|
||||
const command = yield* Effect.promise(() =>
|
||||
script(
|
||||
test.directory,
|
||||
"cli-no-port.mjs",
|
||||
`Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() })
|
||||
`,
|
||||
),
|
||||
)
|
||||
const client = process.env["KILO_CLIENT"]
|
||||
const scans = process.env["KILO_BACKGROUND_PROCESS_PORTS"]
|
||||
process.env["KILO_CLIENT"] = "cli"
|
||||
delete process.env["KILO_BACKGROUND_PROCESS_PORTS"]
|
||||
|
||||
try {
|
||||
const info = yield* Effect.promise(() =>
|
||||
BackgroundProcess.start({
|
||||
sessionID,
|
||||
command,
|
||||
cwd: test.directory,
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.promise(() => Bun.sleep(1_000))
|
||||
const found = yield* Effect.promise(() => BackgroundProcess.get(info.id))
|
||||
expect(found?.ports).toEqual([])
|
||||
} finally {
|
||||
yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID))
|
||||
if (client === undefined) delete process.env["KILO_CLIENT"]
|
||||
else process.env["KILO_CLIENT"] = client
|
||||
if (scans === undefined) delete process.env["KILO_BACKGROUND_PROCESS_PORTS"]
|
||||
else process.env["KILO_BACKGROUND_PROCESS_PORTS"] = scans
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user