|
@@ -10,7 +10,7 @@ import type {
|
|
|
SubprocessTerminalHandle,
|
|
SubprocessTerminalHandle,
|
|
|
SubprocessTerminalSignal,
|
|
SubprocessTerminalSignal,
|
|
|
} from '@deepseek-ai/dsh-subprocess'
|
|
} from '@deepseek-ai/dsh-subprocess'
|
|
|
-import type { ProcessIdentity, ProcessInspector } from './process-inspector.ts'
|
|
|
|
|
|
|
+import type { ProcessIdentity, ProcessInspector, ProcessSnapshot } from './process-inspector.ts'
|
|
|
|
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
function delay(ms: number): Promise<void> {
|
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
@@ -59,7 +59,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
private readonly platform: NodeJS.Platform = process.platform,
|
|
private readonly platform: NodeJS.Platform = process.platform,
|
|
|
) {
|
|
) {
|
|
|
this.pid = terminal.pid
|
|
this.pid = terminal.pid
|
|
|
- this.rootIdentity = inspector.processTree(this.pid).find(member => member.pid === this.pid)
|
|
|
|
|
|
|
+ this.rootIdentity = inspector.snapshot().tree(this.pid).find(member => member.pid === this.pid)
|
|
|
this.done = this.outcome.promise
|
|
this.done = this.outcome.promise
|
|
|
this.dataDisposable = terminal.onData((data) => { this.output.write(Buffer.from(data, 'utf8')) })
|
|
this.dataDisposable = terminal.onData((data) => { this.output.write(Buffer.from(data, 'utf8')) })
|
|
|
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
|
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
|
@@ -83,7 +83,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
// Local inspection is synchronous; the seam returns a promise for remote transports.
|
|
// Local inspection is synchronous; the seam returns a promise for remote transports.
|
|
|
// oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract.
|
|
// oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract.
|
|
|
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
|
|
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
|
|
|
- this.descendants()
|
|
|
|
|
|
|
+ this.descendants(this.inspector.snapshot())
|
|
|
const processGroupId = this.inspector.foregroundPgid(this.pid)
|
|
const processGroupId = this.inspector.foregroundPgid(this.pid)
|
|
|
if (processGroupId === undefined) return undefined
|
|
if (processGroupId === undefined) return undefined
|
|
|
return {
|
|
return {
|
|
@@ -139,7 +139,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
if (this.exited) return
|
|
if (this.exited) return
|
|
|
if (this.rootIdentity !== undefined) {
|
|
if (this.rootIdentity !== undefined) {
|
|
|
try {
|
|
try {
|
|
|
- this.inspector.signalProcess(this.rootIdentity, 'SIGKILL')
|
|
|
|
|
|
|
+ this.inspector.signalProcess(this.rootIdentity, 'SIGKILL', this.inspector.snapshot())
|
|
|
} catch (_rootExitedDuringHostExit) {
|
|
} catch (_rootExitedDuringHostExit) {
|
|
|
// Exact identity signalling contains both exit races and PID reuse.
|
|
// Exact identity signalling contains both exit races and PID reuse.
|
|
|
}
|
|
}
|
|
@@ -152,42 +152,43 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private survivors(members: ProcessIdentity[]): ProcessIdentity[] {
|
|
|
|
|
- return members.filter(member => this.inspector.isAlive(member))
|
|
|
|
|
|
|
+ private survivors(members: ProcessIdentity[], observed: ProcessSnapshot): ProcessIdentity[] {
|
|
|
|
|
+ return members.filter(member => observed.alive(member))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private descendants(): ProcessIdentity[] {
|
|
|
|
|
|
|
+ private descendants(observed: ProcessSnapshot): ProcessIdentity[] {
|
|
|
// Adopt newly scanned members only while the numeric root pid provably
|
|
// Adopt newly scanned members only while the numeric root pid provably
|
|
|
// still carries the spawned shell's start identity: after the shell dies,
|
|
// still carries the spawned shell's start identity: after the shell dies,
|
|
|
// a recycled pid's tree and session must not donate an unrelated
|
|
// a recycled pid's tree and session must not donate an unrelated
|
|
|
// process's children to this session's signalling. Already-adopted
|
|
// process's children to this session's signalling. Already-adopted
|
|
|
// members keep their own start identities, which every signal rechecks.
|
|
// members keep their own start identities, which every signal rechecks.
|
|
|
- const tree = this.inspector.processTree(this.pid)
|
|
|
|
|
|
|
+ const tree = observed.tree(this.pid)
|
|
|
const root = tree.find(member => member.pid === this.pid)
|
|
const root = tree.find(member => member.pid === this.pid)
|
|
|
const rootVerified = this.rootIdentity !== undefined
|
|
const rootVerified = this.rootIdentity !== undefined
|
|
|
&& root !== undefined
|
|
&& root !== undefined
|
|
|
&& root.started === this.rootIdentity.started
|
|
&& root.started === this.rootIdentity.started
|
|
|
this.trackedDescendants = this.survivors(this.unionMembers(
|
|
this.trackedDescendants = this.survivors(this.unionMembers(
|
|
|
this.trackedDescendants,
|
|
this.trackedDescendants,
|
|
|
- ...rootVerified ? [tree, this.inspector.processSession(this.pid)] : [],
|
|
|
|
|
- ).filter(member => member.pid !== this.pid))
|
|
|
|
|
|
|
+ ...rootVerified ? [tree, observed.session(this.pid)] : [],
|
|
|
|
|
+ ).filter(member => member.pid !== this.pid), observed)
|
|
|
return this.trackedDescendants
|
|
return this.trackedDescendants
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async waitForMembers(members: ProcessIdentity[]): Promise<ProcessIdentity[]> {
|
|
private async waitForMembers(members: ProcessIdentity[]): Promise<ProcessIdentity[]> {
|
|
|
const until = Date.now() + this.graceMs
|
|
const until = Date.now() + this.graceMs
|
|
|
- let survivors = this.survivors(members)
|
|
|
|
|
|
|
+ let survivors = this.survivors(members, this.inspector.snapshot())
|
|
|
while (survivors.length > 0 && Date.now() < until) {
|
|
while (survivors.length > 0 && Date.now() < until) {
|
|
|
await delay(Math.min(25, Math.max(1, until - Date.now())))
|
|
await delay(Math.min(25, Math.max(1, until - Date.now())))
|
|
|
- survivors = this.survivors(members)
|
|
|
|
|
|
|
+ survivors = this.survivors(members, this.inspector.snapshot())
|
|
|
}
|
|
}
|
|
|
return survivors
|
|
return survivors
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private signalMembers(members: ProcessIdentity[], signal: 'SIGTERM' | 'SIGKILL'): void {
|
|
private signalMembers(members: ProcessIdentity[], signal: 'SIGTERM' | 'SIGKILL'): void {
|
|
|
|
|
+ const observed = this.inspector.snapshot()
|
|
|
for (const member of members) {
|
|
for (const member of members) {
|
|
|
try {
|
|
try {
|
|
|
- this.inspector.signalProcess(member, signal)
|
|
|
|
|
|
|
+ this.inspector.signalProcess(member, signal, observed)
|
|
|
} catch (_alreadyExitedDuringSignal) {
|
|
} catch (_alreadyExitedDuringSignal) {
|
|
|
// The exact process identity is rechecked; a same-tick exit is success.
|
|
// The exact process identity is rechecked; a same-tick exit is success.
|
|
|
}
|
|
}
|
|
@@ -197,7 +198,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
private forceStopDescendants(): void {
|
|
private forceStopDescendants(): void {
|
|
|
let members = this.trackedDescendants
|
|
let members = this.trackedDescendants
|
|
|
try {
|
|
try {
|
|
|
- members = this.descendants()
|
|
|
|
|
|
|
+ members = this.descendants(this.inspector.snapshot())
|
|
|
} catch (_processTableUnavailableDuringHostExit) {
|
|
} catch (_processTableUnavailableDuringHostExit) {
|
|
|
// Preserve already-captured identities when a final process-table scan fails.
|
|
// Preserve already-captured identities when a final process-table scan fails.
|
|
|
}
|
|
}
|
|
@@ -219,13 +220,14 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async stopDescendants(): Promise<ProcessIdentity[]> {
|
|
private async stopDescendants(): Promise<ProcessIdentity[]> {
|
|
|
- const captured = this.descendants()
|
|
|
|
|
|
|
+ const captured = this.descendants(this.inspector.snapshot())
|
|
|
this.signalMembers(captured, 'SIGTERM')
|
|
this.signalMembers(captured, 'SIGTERM')
|
|
|
const capturedSurvivors = await this.waitForMembers(captured)
|
|
const capturedSurvivors = await this.waitForMembers(captured)
|
|
|
- const members = this.unionMembers(capturedSurvivors, this.descendants())
|
|
|
|
|
|
|
+ const members = this.unionMembers(capturedSurvivors, this.descendants(this.inspector.snapshot()))
|
|
|
this.signalMembers(members, 'SIGKILL')
|
|
this.signalMembers(members, 'SIGKILL')
|
|
|
const survivors = await this.waitForMembers(members)
|
|
const survivors = await this.waitForMembers(members)
|
|
|
- return this.survivors(this.unionMembers(survivors, this.descendants()))
|
|
|
|
|
|
|
+ const observed = this.inspector.snapshot()
|
|
|
|
|
+ return this.survivors(this.unionMembers(survivors, this.descendants(observed)), observed)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async stopShell(): Promise<void> {
|
|
private async stopShell(): Promise<void> {
|
|
@@ -262,9 +264,9 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
// (the same console-list agent), so the tiers verify the shell's absence
|
|
// (the same console-list agent), so the tiers verify the shell's absence
|
|
|
// through the inspector instead of waiting on `done` alone.
|
|
// through the inspector instead of waiting on `done` alone.
|
|
|
const shellGone = (): boolean =>
|
|
const shellGone = (): boolean =>
|
|
|
- this.exited || (this.rootIdentity !== undefined && !this.inspector.isAlive(this.rootIdentity))
|
|
|
|
|
|
|
+ this.exited || (this.rootIdentity !== undefined && !this.inspector.snapshot().alive(this.rootIdentity))
|
|
|
if (!shellGone() && this.rootIdentity !== undefined) {
|
|
if (!shellGone() && this.rootIdentity !== undefined) {
|
|
|
- this.inspector.signalProcess(this.rootIdentity, 'SIGTERM')
|
|
|
|
|
|
|
+ this.inspector.signalProcess(this.rootIdentity, 'SIGTERM', this.inspector.snapshot())
|
|
|
await this.waitForWindowsShellExit()
|
|
await this.waitForWindowsShellExit()
|
|
|
}
|
|
}
|
|
|
if (!shellGone() && this.rootIdentity === undefined) {
|
|
if (!shellGone() && this.rootIdentity === undefined) {
|
|
@@ -276,7 +278,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
await Promise.race([this.done.then(() => undefined), delay(this.graceMs)])
|
|
await Promise.race([this.done.then(() => undefined), delay(this.graceMs)])
|
|
|
}
|
|
}
|
|
|
if (!shellGone() && this.rootIdentity !== undefined) {
|
|
if (!shellGone() && this.rootIdentity !== undefined) {
|
|
|
- this.inspector.signalProcess(this.rootIdentity, 'SIGKILL')
|
|
|
|
|
|
|
+ this.inspector.signalProcess(this.rootIdentity, 'SIGKILL', this.inspector.snapshot())
|
|
|
await this.waitForWindowsShellExit()
|
|
await this.waitForWindowsShellExit()
|
|
|
}
|
|
}
|
|
|
if (!shellGone()) throw new Error(`terminal cleanup failed; surviving pid: ${this.pid}`)
|
|
if (!shellGone()) throw new Error(`terminal cleanup failed; surviving pid: ${this.pid}`)
|
|
@@ -285,7 +287,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
private async waitForWindowsShellExit(): Promise<void> {
|
|
private async waitForWindowsShellExit(): Promise<void> {
|
|
|
const until = Date.now() + this.graceMs
|
|
const until = Date.now() + this.graceMs
|
|
|
while (!this.exited && Date.now() < until) {
|
|
while (!this.exited && Date.now() < until) {
|
|
|
- if (this.rootIdentity !== undefined && !this.inspector.isAlive(this.rootIdentity)) return
|
|
|
|
|
|
|
+ if (this.rootIdentity !== undefined && !this.inspector.snapshot().alive(this.rootIdentity)) return
|
|
|
await delay(Math.min(25, Math.max(1, until - Date.now())))
|
|
await delay(Math.min(25, Math.max(1, until - Date.now())))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -315,7 +317,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
|
|
if (this.exited) return
|
|
if (this.exited) return
|
|
|
/* v8 ignore next -- stopShellWindows() verified the shell is gone or threw;
|
|
/* v8 ignore next -- stopShellWindows() verified the shell is gone or threw;
|
|
|
the identity re-check is a defensive fence for a future caller. */
|
|
the identity re-check is a defensive fence for a future caller. */
|
|
|
- if (this.rootIdentity !== undefined && this.inspector.isAlive(this.rootIdentity)) return
|
|
|
|
|
|
|
+ if (this.rootIdentity !== undefined && this.inspector.snapshot().alive(this.rootIdentity)) return
|
|
|
this.exited = true
|
|
this.exited = true
|
|
|
this.output.end()
|
|
this.output.end()
|
|
|
this.outcome.resolve({ exitCode: null, signal: null })
|
|
this.outcome.resolve({ exitCode: null, signal: null })
|