mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
refactor(bonjour): use native ciao platform handling
This commit is contained in:
@@ -1,15 +1,8 @@
|
||||
// Bonjour tests cover advertiser plugin behavior.
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const childProcessModule = nodeRequire("node:child_process") as {
|
||||
exec: typeof import("node:child_process").exec;
|
||||
};
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createService: vi.fn(),
|
||||
getResponder: vi.fn(),
|
||||
@@ -301,44 +294,6 @@ describe("gateway bonjour advertiser", () => {
|
||||
await started.stop();
|
||||
});
|
||||
|
||||
it("hides ciao Windows ARP probe shell while advertiser is active", async () => {
|
||||
enableAdvertiserUnitMode();
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const originalExec = childProcessModule.exec;
|
||||
const execMock = vi.fn((command: string, options?: unknown, callback?: unknown) => {
|
||||
const cb = typeof options === "function" ? options : callback;
|
||||
if (typeof cb === "function") {
|
||||
cb(null, "", "");
|
||||
}
|
||||
return { kill: vi.fn() } as unknown as ChildProcess;
|
||||
});
|
||||
childProcessModule.exec = execMock as unknown as typeof childProcessModule.exec;
|
||||
|
||||
const destroy = vi.fn().mockResolvedValue(undefined);
|
||||
const advertise = vi.fn().mockResolvedValue(undefined);
|
||||
mockCiaoService({ advertise, destroy });
|
||||
|
||||
try {
|
||||
const started = await startAdvertiser({ gatewayPort: 18789 });
|
||||
childProcessModule.exec('arp -a | findstr /C:"---"', () => {});
|
||||
|
||||
const execCall = mockCall(execMock);
|
||||
expect(execCall?.[0]).toBe('arp -a | findstr /C:"---"');
|
||||
expect(execCall?.[1]).toEqual({ windowsHide: true });
|
||||
expect(execCall?.[2]).toBeTypeOf("function");
|
||||
|
||||
await started.stop();
|
||||
childProcessModule.exec('arp -a | findstr /C:"---"', () => {});
|
||||
const afterStopCallback = execMock.mock.calls.at(-1)?.[1];
|
||||
if (typeof afterStopCallback !== "function") {
|
||||
throw new Error("expected restored exec callback overload");
|
||||
}
|
||||
afterStopCallback(null, "", "");
|
||||
} finally {
|
||||
childProcessModule.exec = originalExec;
|
||||
}
|
||||
});
|
||||
|
||||
it("attaches conflict listeners for services", async () => {
|
||||
enableAdvertiserUnitMode();
|
||||
|
||||
@@ -445,12 +400,6 @@ describe("gateway bonjour advertiser", () => {
|
||||
expect(handler?.(new Error("CIAO PROBING CANCELLED"))).toBe(true);
|
||||
expectWarnContaining("suppressing ciao cancellation");
|
||||
|
||||
logger.warn.mockClear();
|
||||
expect(
|
||||
handler?.(new Error("Reached illegal state! IPV4 address change from defined to undefined!")),
|
||||
).toBe(true);
|
||||
expectWarnContaining("suppressing ciao interface assertion");
|
||||
|
||||
logger.warn.mockClear();
|
||||
expect(
|
||||
exceptionHandler?.(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/** Publishes gateway/canvas/SSH records and repairs stuck or conflicting ciao advertisements. */
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import type { CiaoService } from "@homebridge/ciao";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
||||
@@ -9,11 +7,6 @@ import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { classifyCiaoProcessError, type CiaoProcessErrorClassification } from "./ciao.js";
|
||||
import { formatBonjourError } from "./errors.js";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const childProcessModule = nodeRequire("node:child_process") as {
|
||||
exec: typeof import("node:child_process").exec;
|
||||
};
|
||||
|
||||
type GatewayBonjourAdvertiser = {
|
||||
stop: () => Promise<void>;
|
||||
};
|
||||
@@ -43,8 +36,6 @@ type ConsoleLogFn = (...args: unknown[]) => void;
|
||||
type UncaughtExceptionHandler = (error: unknown) => boolean;
|
||||
type UnhandledRejectionHandler = (reason: unknown) => boolean;
|
||||
type ProcessUnhandledRejectionListener = (reason: unknown, promise: Promise<unknown>) => void;
|
||||
type ExecBridge = (command: string, options?: unknown, callback?: unknown) => ChildProcess;
|
||||
type ExecOptionsRecord = Record<string, unknown> & { windowsHide?: boolean };
|
||||
|
||||
type BonjourAdvertiserDeps = {
|
||||
logger?: Pick<PluginLogger, "info" | "warn" | "debug">;
|
||||
@@ -73,10 +64,6 @@ const defaultLogger = {
|
||||
debug: (_msg: string) => {},
|
||||
};
|
||||
|
||||
const CIAO_WINDOWS_SHELL_COMMANDS = new Set(['arp -a | findstr /C:"---"']);
|
||||
let ciaoExecHidePatchDepth = 0;
|
||||
let restoreCiaoExecHidePatchOnce: (() => void) | null = null;
|
||||
|
||||
function readBonjourDisableOverride(): boolean | null {
|
||||
const raw = process.env.OPENCLAW_DISABLE_BONJOUR;
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
@@ -216,64 +203,6 @@ function installCiaoConsoleNoiseFilter(): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
function isExecOptionsRecord(value: unknown): value is ExecOptionsRecord {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function shouldHideCiaoWindowsShell(command: string): boolean {
|
||||
return process.platform === "win32" && CIAO_WINDOWS_SHELL_COMMANDS.has(command.trim());
|
||||
}
|
||||
|
||||
function installCiaoWindowsExecHidePatch(): () => void {
|
||||
if (process.platform !== "win32") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
ciaoExecHidePatchDepth += 1;
|
||||
if (!restoreCiaoExecHidePatchOnce) {
|
||||
const previousExec = childProcessModule.exec as ExecBridge;
|
||||
const wrapper = ((command: string, options?: unknown, callback?: unknown) => {
|
||||
if (shouldHideCiaoWindowsShell(command)) {
|
||||
if (typeof options === "function") {
|
||||
return previousExec.call(childProcessModule, command, { windowsHide: true }, options);
|
||||
}
|
||||
if (options == null) {
|
||||
return previousExec.call(childProcessModule, command, { windowsHide: true }, callback);
|
||||
}
|
||||
if (isExecOptionsRecord(options) && options.windowsHide === undefined) {
|
||||
return previousExec.call(
|
||||
childProcessModule,
|
||||
command,
|
||||
{ ...options, windowsHide: true },
|
||||
callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
return previousExec.call(childProcessModule, command, options, callback);
|
||||
}) as typeof childProcessModule.exec;
|
||||
childProcessModule.exec = wrapper;
|
||||
restoreCiaoExecHidePatchOnce = () => {
|
||||
if (childProcessModule.exec === wrapper) {
|
||||
childProcessModule.exec = previousExec as typeof childProcessModule.exec;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let active = true;
|
||||
return () => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
active = false;
|
||||
ciaoExecHidePatchDepth = Math.max(0, ciaoExecHidePatchDepth - 1);
|
||||
if (ciaoExecHidePatchDepth > 0) {
|
||||
return;
|
||||
}
|
||||
restoreCiaoExecHidePatchOnce?.();
|
||||
restoreCiaoExecHidePatchOnce = null;
|
||||
};
|
||||
}
|
||||
|
||||
function installCiaoUnhandledRejectionListener(handler: UnhandledRejectionHandler): () => void {
|
||||
const hadOtherListeners = process.listenerCount("unhandledRejection") > 0;
|
||||
const listener: ProcessUnhandledRejectionListener = (reason) => {
|
||||
@@ -313,7 +242,6 @@ export async function startGatewayBonjourAdvertiser(
|
||||
warn: deps.logger?.warn ?? defaultLogger.warn,
|
||||
debug: deps.logger?.debug ?? defaultLogger.debug,
|
||||
};
|
||||
const restoreCiaoExecHidePatch = installCiaoWindowsExecHidePatch();
|
||||
let restoreConsoleLog: () => void = () => {};
|
||||
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
|
||||
let cleanupUnhandledRejection: (() => void) | undefined;
|
||||
@@ -352,11 +280,7 @@ export async function startGatewayBonjourAdvertiser(
|
||||
);
|
||||
} else {
|
||||
const label =
|
||||
classification.kind === "netmask-assertion"
|
||||
? "netmask assertion"
|
||||
: classification.kind === "self-probe"
|
||||
? "self-probe race"
|
||||
: "interface assertion";
|
||||
classification.kind === "netmask-assertion" ? "netmask assertion" : "self-probe race";
|
||||
logger.warn(`bonjour: suppressing ciao ${label}: ${classification.formatted}`);
|
||||
requestCiaoRecovery?.(classification);
|
||||
}
|
||||
@@ -595,7 +519,6 @@ export async function startGatewayBonjourAdvertiser(
|
||||
conflictTracker.clear();
|
||||
await stopCycle(previous, { shutdownResponder: true });
|
||||
restoreConsoleLog();
|
||||
restoreCiaoExecHidePatch();
|
||||
return;
|
||||
}
|
||||
logger.warn(`bonjour: restarting advertiser (${reason})`);
|
||||
@@ -700,13 +623,11 @@ export async function startGatewayBonjourAdvertiser(
|
||||
}
|
||||
await stopCycle(cycle, { shutdownResponder: true });
|
||||
restoreConsoleLog();
|
||||
restoreCiaoExecHidePatch();
|
||||
cleanupProcessHandlers();
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
restoreConsoleLog();
|
||||
restoreCiaoExecHidePatch();
|
||||
cleanupProcessHandlers();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -11,28 +11,6 @@ describe("bonjour-ciao", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies ciao interface assertions separately from side effects", () => {
|
||||
expect(
|
||||
classifyCiaoProcessError(
|
||||
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
|
||||
),
|
||||
).toEqual({
|
||||
kind: "interface-assertion",
|
||||
formatted: "Reached illegal state! IPV4 address change from defined to undefined!",
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies ciao interface assertions using changed wording", () => {
|
||||
expect(
|
||||
classifyCiaoProcessError(
|
||||
new Error("Reached illegal state! IPv4 address changed from undefined to defined!"),
|
||||
),
|
||||
).toEqual({
|
||||
kind: "interface-assertion",
|
||||
formatted: "Reached illegal state! IPv4 address changed from undefined to defined!",
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies ciao netmask assertions separately from side effects", () => {
|
||||
expect(
|
||||
classifyCiaoProcessError(
|
||||
@@ -85,36 +63,10 @@ describe("bonjour-ciao", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses aggregate ciao assertion rejections", () => {
|
||||
expect(
|
||||
classifyCiaoProcessError(
|
||||
new AggregateError([
|
||||
Object.assign(
|
||||
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
|
||||
{ name: "AssertionError" },
|
||||
),
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
kind: "interface-assertion",
|
||||
formatted:
|
||||
"AssertionError: Reached illegal state! IPV4 address change from defined to undefined!",
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses lower-case string cancellation reasons too", () => {
|
||||
expect(classifyCiaoProcessError("ciao announcement cancelled during cleanup")).not.toBe(null);
|
||||
});
|
||||
|
||||
it("suppresses ciao interface assertion rejections as non-fatal", () => {
|
||||
const error = Object.assign(
|
||||
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
|
||||
{ name: "AssertionError" },
|
||||
);
|
||||
|
||||
expect(classifyCiaoProcessError(error)).not.toBe(null);
|
||||
});
|
||||
|
||||
it("suppresses ciao netmask assertion errors as non-fatal", () => {
|
||||
const error = Object.assign(
|
||||
new Error(
|
||||
|
||||
@@ -6,8 +6,6 @@ import { collectErrorGraphCandidates } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { formatBonjourError } from "./errors.js";
|
||||
|
||||
const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u;
|
||||
const CIAO_INTERFACE_ASSERTION_MESSAGE_RE =
|
||||
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGED? FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u;
|
||||
const CIAO_NETMASK_ASSERTION_MESSAGE_RE =
|
||||
/IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u;
|
||||
const CIAO_SELF_PROBE_MESSAGE_RE =
|
||||
@@ -20,7 +18,6 @@ const CIAO_INTERFACE_ENUMERATION_FAILURE_RE = /\bUV_INTERFACE_ADDRESSES\b/u;
|
||||
/** Known ciao process-level errors that OpenClaw handles specially. */
|
||||
export type CiaoProcessErrorClassification =
|
||||
| { kind: "cancellation"; formatted: string }
|
||||
| { kind: "interface-assertion"; formatted: string }
|
||||
| { kind: "netmask-assertion"; formatted: string }
|
||||
| { kind: "self-probe"; formatted: string }
|
||||
| { kind: "interface-enumeration-failure"; formatted: string };
|
||||
@@ -40,9 +37,6 @@ export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClass
|
||||
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
|
||||
return { kind: "cancellation", formatted };
|
||||
}
|
||||
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
|
||||
return { kind: "interface-assertion", formatted };
|
||||
}
|
||||
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) {
|
||||
return { kind: "netmask-assertion", formatted };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user