mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:50:45 +00:00
fix: hide bonjour Windows ARP shell probe
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd.
|
- Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd.
|
||||||
- Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd.
|
- Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd.
|
||||||
- Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear.
|
- Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear.
|
||||||
|
- Bonjour/Windows: hide the bundled mDNS advertiser's Windows ARP shell probe so Gateway startup no longer flashes command-prompt windows. Fixes #70238. Thanks @alexandre-leng, @PratikRai0101, @infinitypacific, and @tomerpeled.
|
||||||
- Agents/bootstrap: dedupe hook-injected bootstrap context files by workspace-relative path and store normalized resolved paths so duplicate relative and absolute hook paths no longer depend on the process cwd. (#59344; fixes #59319; related #56721, #56725, and #57587) Thanks @koen666.
|
- Agents/bootstrap: dedupe hook-injected bootstrap context files by workspace-relative path and store normalized resolved paths so duplicate relative and absolute hook paths no longer depend on the process cwd. (#59344; fixes #59319; related #56721, #56725, and #57587) Thanks @koen666.
|
||||||
- Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when `AGENTS.md`, `SOUL.md`, `MEMORY.md`, or `TOOLS.md` change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon.
|
- Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when `AGENTS.md`, `SOUL.md`, `MEMORY.md`, or `TOOLS.md` change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon.
|
||||||
- macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.
|
- macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import type { ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { 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(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
createService: vi.fn(),
|
createService: vi.fn(),
|
||||||
getResponder: vi.fn(),
|
getResponder: vi.fn(),
|
||||||
@@ -240,6 +247,42 @@ describe("gateway bonjour advertiser", () => {
|
|||||||
await started.stop();
|
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:"---"', () => {});
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
|
'arp -a | findstr /C:"---"',
|
||||||
|
{ windowsHide: true },
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
|
||||||
|
await started.stop();
|
||||||
|
childProcessModule.exec('arp -a | findstr /C:"---"', () => {});
|
||||||
|
const afterStopOptions = execMock.mock.calls.at(-1)?.[1];
|
||||||
|
expect(afterStopOptions).toEqual(expect.any(Function));
|
||||||
|
} finally {
|
||||||
|
childProcessModule.exec = originalExec;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("attaches conflict listeners for services", async () => {
|
it("attaches conflict listeners for services", async () => {
|
||||||
enableAdvertiserUnitMode();
|
enableAdvertiserUnitMode();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import type { ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
||||||
import { classifyCiaoProcessError, type CiaoProcessErrorClassification } from "./ciao.js";
|
import { classifyCiaoProcessError, type CiaoProcessErrorClassification } from "./ciao.js";
|
||||||
import { formatBonjourError } from "./errors.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;
|
||||||
|
};
|
||||||
|
|
||||||
export type GatewayBonjourAdvertiser = {
|
export type GatewayBonjourAdvertiser = {
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -61,6 +68,8 @@ type ServiceStateTracker = {
|
|||||||
type ConsoleLogFn = (...args: unknown[]) => void;
|
type ConsoleLogFn = (...args: unknown[]) => void;
|
||||||
type UncaughtExceptionHandler = (error: unknown) => boolean;
|
type UncaughtExceptionHandler = (error: unknown) => boolean;
|
||||||
type UnhandledRejectionHandler = (reason: unknown) => boolean;
|
type UnhandledRejectionHandler = (reason: unknown) => boolean;
|
||||||
|
type ExecBridge = (command: string, options?: unknown, callback?: unknown) => ChildProcess;
|
||||||
|
type ExecOptionsRecord = Record<string, unknown> & { windowsHide?: boolean };
|
||||||
|
|
||||||
type BonjourAdvertiserDeps = {
|
type BonjourAdvertiserDeps = {
|
||||||
logger?: Pick<PluginLogger, "info" | "warn" | "debug">;
|
logger?: Pick<PluginLogger, "info" | "warn" | "debug">;
|
||||||
@@ -83,7 +92,10 @@ const defaultLogger = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CIAO_MODULE_ID = "@homebridge/ciao";
|
const CIAO_MODULE_ID = "@homebridge/ciao";
|
||||||
|
const CIAO_WINDOWS_SHELL_COMMANDS = new Set(['arp -a | findstr /C:"---"']);
|
||||||
let ciaoModulePromise: Promise<CiaoModule> | null = null;
|
let ciaoModulePromise: Promise<CiaoModule> | null = null;
|
||||||
|
let ciaoExecHidePatchDepth = 0;
|
||||||
|
let restoreCiaoExecHidePatchOnce: (() => void) | null = null;
|
||||||
|
|
||||||
async function loadCiaoModule(): Promise<CiaoModule> {
|
async function loadCiaoModule(): Promise<CiaoModule> {
|
||||||
ciaoModulePromise ??= import(CIAO_MODULE_ID) as Promise<CiaoModule>;
|
ciaoModulePromise ??= import(CIAO_MODULE_ID) as Promise<CiaoModule>;
|
||||||
@@ -207,6 +219,64 @@ 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function startGatewayBonjourAdvertiser(
|
export async function startGatewayBonjourAdvertiser(
|
||||||
opts: GatewayBonjourAdvertiseOpts,
|
opts: GatewayBonjourAdvertiseOpts,
|
||||||
deps: BonjourAdvertiserDeps = {},
|
deps: BonjourAdvertiserDeps = {},
|
||||||
@@ -220,8 +290,8 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
warn: deps.logger?.warn ?? defaultLogger.warn,
|
warn: deps.logger?.warn ?? defaultLogger.warn,
|
||||||
debug: deps.logger?.debug ?? defaultLogger.debug,
|
debug: deps.logger?.debug ?? defaultLogger.debug,
|
||||||
};
|
};
|
||||||
const { getResponder, Protocol } = await loadCiaoModule();
|
const restoreCiaoExecHidePatch = installCiaoWindowsExecHidePatch();
|
||||||
const restoreConsoleLog = installCiaoConsoleNoiseFilter();
|
let restoreConsoleLog: () => void = () => {};
|
||||||
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
|
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
|
||||||
let cleanupUnhandledRejection: (() => void) | undefined;
|
let cleanupUnhandledRejection: (() => void) | undefined;
|
||||||
let cleanupUncaughtException: (() => void) | undefined;
|
let cleanupUncaughtException: (() => void) | undefined;
|
||||||
@@ -236,26 +306,28 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
cleanupUnhandledRejection?.();
|
cleanupUnhandledRejection?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCiaoProcessError = (reason: unknown): boolean => {
|
|
||||||
const classification = classifyCiaoProcessError(reason);
|
|
||||||
if (!classification) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classification.kind === "cancellation") {
|
|
||||||
logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
|
|
||||||
} else {
|
|
||||||
const label =
|
|
||||||
classification.kind === "netmask-assertion" ? "netmask assertion" : "interface assertion";
|
|
||||||
logger.warn(`bonjour: suppressing ciao ${label}: ${classification.formatted}`);
|
|
||||||
requestCiaoRecovery?.(classification);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler?.(handleCiaoProcessError);
|
|
||||||
cleanupUncaughtException = deps.registerUncaughtExceptionHandler?.(handleCiaoProcessError);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { getResponder, Protocol } = await loadCiaoModule();
|
||||||
|
restoreConsoleLog = installCiaoConsoleNoiseFilter();
|
||||||
|
const handleCiaoProcessError = (reason: unknown): boolean => {
|
||||||
|
const classification = classifyCiaoProcessError(reason);
|
||||||
|
if (!classification) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classification.kind === "cancellation") {
|
||||||
|
logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
|
||||||
|
} else {
|
||||||
|
const label =
|
||||||
|
classification.kind === "netmask-assertion" ? "netmask assertion" : "interface assertion";
|
||||||
|
logger.warn(`bonjour: suppressing ciao ${label}: ${classification.formatted}`);
|
||||||
|
requestCiaoRecovery?.(classification);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler?.(handleCiaoProcessError);
|
||||||
|
cleanupUncaughtException = deps.registerUncaughtExceptionHandler?.(handleCiaoProcessError);
|
||||||
|
|
||||||
const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || "openclaw";
|
const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || "openclaw";
|
||||||
const hostname =
|
const hostname =
|
||||||
hostnameRaw
|
hostnameRaw
|
||||||
@@ -431,6 +503,7 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
stateTracker.clear();
|
stateTracker.clear();
|
||||||
await stopCycle(previous, { shutdownResponder: true });
|
await stopCycle(previous, { shutdownResponder: true });
|
||||||
restoreConsoleLog();
|
restoreConsoleLog();
|
||||||
|
restoreCiaoExecHidePatch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.warn(`bonjour: restarting advertiser (${reason})`);
|
logger.warn(`bonjour: restarting advertiser (${reason})`);
|
||||||
@@ -529,11 +602,13 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
}
|
}
|
||||||
await stopCycle(cycle, { shutdownResponder: true });
|
await stopCycle(cycle, { shutdownResponder: true });
|
||||||
restoreConsoleLog();
|
restoreConsoleLog();
|
||||||
|
restoreCiaoExecHidePatch();
|
||||||
cleanupProcessHandlers();
|
cleanupProcessHandlers();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
restoreConsoleLog();
|
restoreConsoleLog();
|
||||||
|
restoreCiaoExecHidePatch();
|
||||||
cleanupProcessHandlers();
|
cleanupProcessHandlers();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user