fix: stabilize Parallels plugin smoke paths

This commit is contained in:
Peter Steinberger
2026-04-26 12:11:08 +01:00
parent 631552c554
commit ddc2036956
6 changed files with 44 additions and 18 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @codex.
- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex.
- Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.
- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.

View File

@@ -490,6 +490,8 @@ describe("gateway bonjour advertiser", () => {
const stateRef = { value: "announcing" };
const events: string[] = [];
const cleanupException = vi.fn();
const cleanupRejection = vi.fn();
let advertiseCount = 0;
const destroy = vi.fn().mockImplementation(async () => {
events.push("destroy");
@@ -505,6 +507,8 @@ describe("gateway bonjour advertiser", () => {
return Promise.resolve();
});
mockCiaoService({ advertise, destroy, stateRef });
registerUncaughtExceptionHandler.mockImplementation(() => cleanupException);
registerUnhandledRejectionHandler.mockImplementation(() => cleanupRejection);
const started = await startAdvertiser({
gatewayPort: 18789,
@@ -513,6 +517,8 @@ describe("gateway bonjour advertiser", () => {
expect(createService).toHaveBeenCalledTimes(1);
expect(advertise).toHaveBeenCalledTimes(1);
expect(registerUncaughtExceptionHandler).toHaveBeenCalledTimes(1);
expect(registerUnhandledRejectionHandler).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(15_000);
@@ -521,11 +527,15 @@ describe("gateway bonjour advertiser", () => {
expect(advertise).toHaveBeenCalledTimes(2);
expect(destroy).toHaveBeenCalledTimes(1);
expect(shutdown).not.toHaveBeenCalled();
expect(cleanupException).not.toHaveBeenCalled();
expect(cleanupRejection).not.toHaveBeenCalled();
expect(events).toEqual(["advertise:1", "destroy", "advertise:2"]);
await started.stop();
expect(destroy).toHaveBeenCalledTimes(2);
expect(shutdown).toHaveBeenCalledTimes(1);
expect(cleanupException).toHaveBeenCalledTimes(1);
expect(cleanupRejection).toHaveBeenCalledTimes(1);
});
it("treats probing-to-announcing churn as one unhealthy window", async () => {

View File

@@ -50,8 +50,6 @@ type CiaoModule = {
type BonjourCycle = {
responder: BonjourResponder;
services: Array<{ label: string; svc: BonjourService }>;
cleanupUncaughtException?: () => void;
cleanupUnhandledRejection?: () => void;
};
type ServiceStateTracker = {
@@ -179,6 +177,18 @@ export async function startGatewayBonjourAdvertiser(
const { getResponder, Protocol } = await loadCiaoModule();
const restoreConsoleLog = installCiaoConsoleNoiseFilter();
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
let cleanupUnhandledRejection: (() => void) | undefined;
let cleanupUncaughtException: (() => void) | undefined;
let processHandlersCleaned = false;
function cleanupProcessHandlers() {
if (processHandlersCleaned) {
return;
}
processHandlersCleaned = true;
cleanupUncaughtException?.();
cleanupUnhandledRejection?.();
}
const handleCiaoProcessError = (reason: unknown): boolean => {
const classification = classifyCiaoProcessError(reason);
@@ -196,6 +206,8 @@ export async function startGatewayBonjourAdvertiser(
}
return true;
};
cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler?.(handleCiaoProcessError);
cleanupUncaughtException = deps.registerUncaughtExceptionHandler?.(handleCiaoProcessError);
try {
const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || "openclaw";
@@ -259,16 +271,7 @@ export async function startGatewayBonjourAdvertiser(
svc: gateway as unknown as BonjourService,
});
const cleanupUnhandledRejection =
services.length > 0 && deps.registerUnhandledRejectionHandler
? deps.registerUnhandledRejectionHandler(handleCiaoProcessError)
: undefined;
const cleanupUncaughtException =
services.length > 0 && deps.registerUncaughtExceptionHandler
? deps.registerUncaughtExceptionHandler(handleCiaoProcessError)
: undefined;
return { responder, services, cleanupUncaughtException, cleanupUnhandledRejection };
return { responder, services };
}
async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) {
@@ -288,9 +291,6 @@ export async function startGatewayBonjourAdvertiser(
}
} catch {
/* ignore */
} finally {
cycle.cleanupUncaughtException?.();
cycle.cleanupUnhandledRejection?.();
}
}
@@ -483,10 +483,12 @@ export async function startGatewayBonjourAdvertiser(
}
await stopCycle(cycle, { shutdownResponder: true });
restoreConsoleLog();
cleanupProcessHandlers();
},
};
} catch (err) {
restoreConsoleLog();
cleanupProcessHandlers();
throw err;
}
}

View File

@@ -42,6 +42,7 @@ TIMEOUT_ONBOARD_S=180
TIMEOUT_AGENT_S="${OPENCLAW_PARALLELS_LINUX_AGENT_TIMEOUT_S:-300}"
TIMEOUT_GATEWAY_S=240
PHASE_STALE_WARN_S=60
DISABLE_BONJOUR_FOR_GATEWAY=0
FRESH_MAIN_STATUS="skip"
FRESH_MAIN_VERSION="skip"
@@ -230,6 +231,11 @@ esac
API_KEY_VALUE="${!API_KEY_ENV:-}"
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
case "${OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR:-}" in
1|true|TRUE|yes|YES|on|ON)
DISABLE_BONJOUR_FOR_GATEWAY=1
;;
esac
resolve_vm_name() {
local json requested explicit
@@ -725,12 +731,16 @@ EOF
}
start_gateway_background() {
local cmd api_key_value_q
local cmd api_key_value_q bonjour_env
api_key_value_q="$(shell_quote "$API_KEY_VALUE")"
bonjour_env=""
if [[ "$DISABLE_BONJOUR_FOR_GATEWAY" -eq 1 ]]; then
bonjour_env=" OPENCLAW_DISABLE_BONJOUR=1"
fi
cmd="$(cat <<EOF
pkill -f "openclaw gateway run" >/dev/null 2>&1 || true
rm -f /tmp/openclaw-parallels-linux-gateway.log
setsid sh -lc 'exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json ${API_KEY_ENV}=${api_key_value_q} openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1' >/dev/null 2>&1 < /dev/null &
setsid sh -lc 'exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json${bonjour_env} ${API_KEY_ENV}=${api_key_value_q} openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1' >/dev/null 2>&1 < /dev/null &
EOF
)"
guest_exec bash -lc "$cmd"

View File

@@ -1944,7 +1944,7 @@ if platform_enabled windows; then
fi
if platform_enabled linux; then
bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \
OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR=1 bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \
--mode fresh \
--provider "$PROVIDER" \
--model "$MODEL_ID" \

View File

@@ -610,6 +610,9 @@ function buildInstalledPluginIndex(
if (record.setupSource) {
indexRecord.setupSource = record.setupSource;
}
if (record.syntheticAuthRefs && record.syntheticAuthRefs.length > 0) {
indexRecord.syntheticAuthRefs = record.syntheticAuthRefs;
}
if (candidate?.packageName) {
indexRecord.packageName = candidate.packageName;
}