mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
refactor(windows): share the walk's pid index in the stale-link filter
Resolve parent links through the same index the descendant walk builds, so a table that repeats a pid answers both the same way, and drop the non-null assertion on the walk by keeping the "cannot see" null contract. Pin the two filter branches nothing exercised: the root surviving its own recycled ppid, and the root's start bounding a link whose claimed parent denied its creation time.
This commit is contained in:
@@ -53,6 +53,47 @@ describe('captureWindowsDescendantSnapshot', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the root when its own parent PID was reused by a newer process', async () => {
|
||||
// The root's retained ppid now names a process created after it. Pruning the
|
||||
// root drops the whole snapshot, so its own link is never evidence about it.
|
||||
const captured = await captureWindowsDescendantSnapshot(100, {
|
||||
readTable: async () => [
|
||||
{ pid: 100, ppid: 900, creationTimeMs: 5 },
|
||||
{ pid: 900, ppid: 1, creationTimeMs: 50 },
|
||||
{ pid: 200, ppid: 100, creationTimeMs: 7 }
|
||||
],
|
||||
now: () => 42
|
||||
})
|
||||
|
||||
expect(captured).toEqual({
|
||||
root: { pid: 100, creationTimeMs: 5 },
|
||||
descendants: [{ pid: 200, creationTimeMs: 7 }],
|
||||
unidentifiedCount: 0,
|
||||
capturedAtMs: 42
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds a link by the root when the claimed parent denied its creation time', async () => {
|
||||
// 300 has no creation time to compare a child against, so the root's start is
|
||||
// the only bound left: 350 could be its child, 360 predates the tree entirely.
|
||||
const captured = await captureWindowsDescendantSnapshot(100, {
|
||||
readTable: async () => [
|
||||
{ pid: 100, ppid: 1, creationTimeMs: 5 },
|
||||
{ pid: 300, ppid: 100 },
|
||||
{ pid: 350, ppid: 300, creationTimeMs: 9 },
|
||||
{ pid: 360, ppid: 300, creationTimeMs: 2 }
|
||||
],
|
||||
now: () => 42
|
||||
})
|
||||
|
||||
expect(captured).toEqual({
|
||||
root: { pid: 100, creationTimeMs: 5 },
|
||||
descendants: [{ pid: 350, creationTimeMs: 9 }],
|
||||
unidentifiedCount: 1,
|
||||
capturedAtMs: 42
|
||||
})
|
||||
})
|
||||
|
||||
it('walks the whole subtree and keeps only rows a later read can re-identify', async () => {
|
||||
const captured = await captureWindowsDescendantSnapshot(100, {
|
||||
// 400 is a grandchild; 300 denied a creation-time query, so no later read
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getProcessTableIndex } from '../shared/process-table-index'
|
||||
import type { DescendantTreeVerdict } from './pty-descendant-exit-verification'
|
||||
import { windowsDescendantsFromRows } from './providers/windows-foreground-process-rows'
|
||||
import { readWindowsProcessTableFresh } from './windows/windows-process-table'
|
||||
@@ -69,15 +70,24 @@ export async function captureWindowsDescendantSnapshot(
|
||||
// One table read, not a walk plus an identity read: each is bounded in
|
||||
// seconds, and this runs inside the close ladder's budget.
|
||||
const table = await (deps.readTable ?? readWindowsProcessTableFresh)().catch(() => null)
|
||||
const root = table?.find((row) => row.pid === rootPid)
|
||||
if (!table || typeof root?.creationTimeMs !== 'number') {
|
||||
if (!table) {
|
||||
return null
|
||||
}
|
||||
// The same index the walk below builds, so a table that repeats a pid resolves
|
||||
// a parent link to the row the walk will actually traverse.
|
||||
const rowsByPid = getProcessTableIndex(table).byPid
|
||||
const root = rowsByPid.get(rootPid)
|
||||
if (typeof root?.creationTimeMs !== 'number') {
|
||||
return null
|
||||
}
|
||||
const rootCreationTimeMs = root.creationTimeMs
|
||||
const creationTimes = new Map(table.map((row) => [row.pid, row.creationTimeMs]))
|
||||
// Windows retains the original parent PID after exit; a reused PID is not ancestry.
|
||||
// Windows keeps a process's original parent PID after that parent exits, so a
|
||||
// reused PID is not ancestry: no real child predates the parent it claims, and
|
||||
// the root's own start bounds the subtree when a parent denied its time. The
|
||||
// root is never pruned by its own link -- its ppid can be recycled too, and a
|
||||
// pruned root loses the snapshot outright.
|
||||
const currentRows = table.filter((row) => {
|
||||
const parentCreationTimeMs = creationTimes.get(row.ppid)
|
||||
const parentCreationTimeMs = rowsByPid.get(row.ppid)?.creationTimeMs
|
||||
return (
|
||||
row.pid === rootPid ||
|
||||
row.creationTimeMs === undefined ||
|
||||
@@ -85,7 +95,10 @@ export async function captureWindowsDescendantSnapshot(
|
||||
(parentCreationTimeMs === undefined || row.creationTimeMs >= parentCreationTimeMs))
|
||||
)
|
||||
})
|
||||
const descendants = windowsDescendantsFromRows(currentRows, rootPid)!
|
||||
const descendants = windowsDescendantsFromRows(currentRows, rootPid)
|
||||
if (!descendants) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
root: { pid: root.pid, creationTimeMs: root.creationTimeMs },
|
||||
descendants: descendants.flatMap((row) =>
|
||||
|
||||
Reference in New Issue
Block a user