Merge remote-tracking branch 'origin/worktree-process-service-seam' into subprocess-simpl/d-one-env-channel
# Conflicts: # docs/cordis-catalog/services.md # docs/core-data-structures/subprocess.i18n.yaml # packages/lsp/lsp-local/src/connection.ts # packages/subagent/subagent-acp/README.i18n.yaml # packages/subprocess/subprocess-local/README.i18n.yaml # packages/subprocess/subprocess-local/src/spawn.ts # packages/subprocess/subprocess/README.i18n.yaml # packages/subprocess/subprocess/README.md # packages/subprocess/subprocess/README.zh.md # packages/subprocess/subprocess/tests/service.spec.ts
This commit is contained in:
@@ -29,11 +29,11 @@ export interface ConnectionSpec {
|
||||
/** Largest stderr tail retained for diagnostics. */
|
||||
readonly maxStderrBytes: number
|
||||
/**
|
||||
* Bound (ms) for draining pipes a surviving helper still holds after the
|
||||
* server exits; the instance passes its kill grace so exit observation is
|
||||
* never slower than the escalation it feeds.
|
||||
* The subprocess spec's `graceMs`: the SIGTERM→SIGKILL window of
|
||||
* {@link LspConnection.terminate}'s escalation, and the bound for draining
|
||||
* pipes a surviving helper still holds after the server exits.
|
||||
*/
|
||||
readonly pipeDrainGraceMs: number
|
||||
readonly killGraceMs: number
|
||||
/** Static answer to every `workspace/configuration` item. */
|
||||
readonly configuration: unknown
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export class LspConnection {
|
||||
stdout: 'pipe',
|
||||
stderr: { maxBytes: spec.maxStderrBytes },
|
||||
},
|
||||
graceMs: spec.pipeDrainGraceMs,
|
||||
graceMs: spec.killGraceMs,
|
||||
// spec.env mixes the scrubbed base with explicit config entries; the
|
||||
// seam merges the whole map after its own ambient scrub, so a
|
||||
// configured DSH_* fact reaches the child.
|
||||
@@ -210,14 +210,9 @@ export class LspConnection {
|
||||
return this.nextId
|
||||
}
|
||||
|
||||
/** Request termination of the server's process tree (SIGTERM, no escalation). */
|
||||
/** Terminate the server's process tree (the seam's SIGTERM→grace→SIGKILL escalation; idempotent). */
|
||||
terminate(): void {
|
||||
this.handle.kill('SIGTERM')
|
||||
}
|
||||
|
||||
/** Force termination of the server's process tree. */
|
||||
kill(): void {
|
||||
this.handle.kill('SIGKILL')
|
||||
this.handle.terminate()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,9 +230,10 @@ export class LspConnection {
|
||||
messages = this.decoder.push(chunk)
|
||||
} catch (error) {
|
||||
// A framing/JSON failure corrupts the stream position irrecoverably: fail the instance and
|
||||
// SIGKILL the whole group so helper processes don't outlive the leader.
|
||||
// terminate the whole group so helper processes don't outlive the leader (SIGTERM first, then
|
||||
// the kill grace's SIGKILL — a misbehaving server still gets its bounded flush window).
|
||||
this.fail(asError(error))
|
||||
this.handle.kill('SIGKILL')
|
||||
this.handle.terminate()
|
||||
return
|
||||
}
|
||||
for (const message of messages) this.dispatch(message)
|
||||
|
||||
@@ -285,8 +285,6 @@ class LocalLspProvider implements LspProvider {
|
||||
initializationOptions: this.config.initializationOptions,
|
||||
maxMessageBytes: this.config.maxMessageBytes,
|
||||
maxStderrBytes: this.config.maxStderrBytes,
|
||||
// Exit observation must never be slower than the escalation it feeds.
|
||||
pipeDrainGraceMs: this.config.killGraceMs,
|
||||
shutdownTimeoutMs: this.config.shutdownTimeoutMs,
|
||||
killGraceMs: this.config.killGraceMs,
|
||||
}
|
||||
|
||||
@@ -35,17 +35,6 @@ export interface InstanceSpec extends ConnectionSpec {
|
||||
readonly initializationOptions: unknown
|
||||
/** Graceful `shutdown`/`exit` budget before escalation (ms). */
|
||||
readonly shutdownTimeoutMs: number
|
||||
/** SIGTERM→SIGKILL grace after graceful shutdown fails (ms). */
|
||||
readonly killGraceMs: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-kill a process tree only when graceful termination did not make it exit.
|
||||
* @param treeExited - whether the tree exited within its grace period.
|
||||
* @param forceKill - forceful process-tree termination primitive.
|
||||
*/
|
||||
export function escalateProcessTree(treeExited: boolean, forceKill: () => void): void {
|
||||
if (!treeExited) forceKill()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,17 +300,14 @@ export class LspInstance {
|
||||
await abortable(this.connection.closed, signal)
|
||||
}
|
||||
|
||||
/** Terminate the tree, escalate after `killGraceMs`, then await leader and helper exit. */
|
||||
/**
|
||||
* Terminate the tree (the seam escalates SIGTERM→`killGraceMs`→SIGKILL),
|
||||
* then await leader and helper exit. The awaits are unbounded on purpose:
|
||||
* the seam's escalation already committed to SIGKILL, so quiescence — not
|
||||
* another timer — is the postcondition disposal owes its callers.
|
||||
*/
|
||||
private async forceTerminate(): Promise<void> {
|
||||
this.connection.terminate()
|
||||
const graceDeadline = deadline(undefined, this.spec.killGraceMs, 'LSP_KILL_GRACE')
|
||||
let treeExited: boolean
|
||||
try {
|
||||
treeExited = await this.connection.waitForProcessTreeExit(graceDeadline.signal)
|
||||
} finally {
|
||||
graceDeadline[Symbol.dispose]()
|
||||
}
|
||||
escalateProcessTree(treeExited, this.connection.kill.bind(this.connection))
|
||||
await Promise.all([
|
||||
this.connection.closed,
|
||||
this.connection.waitForProcessTreeExit(),
|
||||
|
||||
Reference in New Issue
Block a user