mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(process): keep secret pipe errors handled (#112550)
This commit is contained in:
committed by
GitHub
parent
344feae7a1
commit
3ed2a144ac
@@ -0,0 +1,61 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { writeSecretInputToChild } from "./spawn-secret-input.js";
|
||||
|
||||
class ControlledSecretStream extends EventEmitter {
|
||||
private endCallback: ((error?: Error | null) => void) | undefined;
|
||||
|
||||
end(_data: Buffer, callback?: (error?: Error | null) => void): this {
|
||||
this.endCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
finishWrite(error?: Error): void {
|
||||
if (!this.endCallback) {
|
||||
throw new Error("secret write callback was not registered");
|
||||
}
|
||||
this.endCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
function childWithSecretStream(stream: ControlledSecretStream): ChildProcess {
|
||||
return { stdio: [null, null, null, stream] } as unknown as ChildProcess;
|
||||
}
|
||||
|
||||
function writeSecret(stream: ControlledSecretStream): Promise<void> {
|
||||
return writeSecretInputToChild(childWithSecretStream(stream), {
|
||||
fd: 3,
|
||||
createData: () => Buffer.from("selected-secret"),
|
||||
});
|
||||
}
|
||||
|
||||
describe("writeSecretInputToChild", () => {
|
||||
it("consumes pipe errors after delivery until the stream closes", async () => {
|
||||
const stream = new ControlledSecretStream();
|
||||
const write = writeSecret(stream);
|
||||
|
||||
stream.finishWrite();
|
||||
await expect(write).resolves.toBeUndefined();
|
||||
|
||||
const reset = Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" });
|
||||
expect(() => stream.emit("error", reset)).not.toThrow();
|
||||
expect(stream.listenerCount("error")).toBe(1);
|
||||
|
||||
stream.emit("close");
|
||||
expect(stream.listenerCount("error")).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects delivery errors before consuming their later stream event", async () => {
|
||||
const stream = new ControlledSecretStream();
|
||||
const write = writeSecret(stream);
|
||||
const deliveryError = new Error("secret delivery failed");
|
||||
|
||||
stream.finishWrite(deliveryError);
|
||||
await expect(write).rejects.toBe(deliveryError);
|
||||
expect(() => stream.emit("error", deliveryError)).not.toThrow();
|
||||
|
||||
stream.emit("close");
|
||||
expect(stream.listenerCount("error")).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -37,15 +37,28 @@ export async function writeSecretInputToChild(
|
||||
// End the parent pipe immediately after delivery so descendants cannot
|
||||
// inherit a still-readable credential stream.
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (error: Error) => {
|
||||
stream.off("error", onError);
|
||||
reject(error);
|
||||
let settled = false;
|
||||
const settle = (error?: Error | null) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
stream.once("error", onError);
|
||||
stream.end(data, () => {
|
||||
const onError = (error: Error) => {
|
||||
settle(error);
|
||||
};
|
||||
// A child-process pipe can emit its terminal error after end's callback.
|
||||
// Keep it handled until close while only the first outcome settles delivery.
|
||||
stream.on("error", onError);
|
||||
stream.once("close", () => {
|
||||
stream.off("error", onError);
|
||||
resolve();
|
||||
});
|
||||
stream.end(data, settle);
|
||||
});
|
||||
} finally {
|
||||
data?.fill(0);
|
||||
|
||||
Reference in New Issue
Block a user