426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
287 lines
7.2 KiB
JavaScript
287 lines
7.2 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import net from "node:net";
|
|
import path from "node:path";
|
|
|
|
export const DEFAULT_HOST_AGENT_PORT = 31338;
|
|
export const DEFAULT_HOST_AGENT_HOST = "127.0.0.1";
|
|
export const DEFAULT_HOST_AGENT_HEALTH_PATH = "/api/health";
|
|
|
|
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
const DEFAULT_READY_ATTEMPTS = 90;
|
|
const DEFAULT_READY_DELAY_MS = 2000;
|
|
|
|
export function parsePort(value, label = "port") {
|
|
const raw = String(value ?? "").trim();
|
|
if (!/^\d+$/.test(raw)) {
|
|
throw new Error(`Invalid ${label}: ${value}`);
|
|
}
|
|
const port = Number.parseInt(raw, 10);
|
|
if (port < 1 || port > 65535) {
|
|
throw new Error(`Invalid ${label}: ${value}`);
|
|
}
|
|
return port;
|
|
}
|
|
|
|
export function hostAgentApiBase(port, host = DEFAULT_HOST_AGENT_HOST) {
|
|
return `http://${host}:${port}`;
|
|
}
|
|
|
|
export async function isPortAvailable(port, host = DEFAULT_HOST_AGENT_HOST) {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer();
|
|
server.once("error", () => resolve(false));
|
|
server.once("listening", () => {
|
|
server.close(() => resolve(true));
|
|
});
|
|
server.listen(port, host);
|
|
});
|
|
}
|
|
|
|
export async function chooseHostAgentPort({
|
|
preferredPort = DEFAULT_HOST_AGENT_PORT,
|
|
requestedPort = null,
|
|
host = DEFAULT_HOST_AGENT_HOST,
|
|
} = {}) {
|
|
if (requestedPort !== null && requestedPort !== undefined) {
|
|
const port = parsePort(requestedPort, "host-agent port");
|
|
if (!(await isPortAvailable(port, host))) {
|
|
throw new Error(`Requested host-agent port ${port} is already in use.`);
|
|
}
|
|
return port;
|
|
}
|
|
|
|
const preferred = parsePort(preferredPort, "host-agent preferred port");
|
|
if (await isPortAvailable(preferred, host)) return preferred;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.once("error", reject);
|
|
server.once("listening", () => {
|
|
const address = server.address();
|
|
const port = typeof address === "object" && address ? address.port : null;
|
|
server.close(() => {
|
|
if (typeof port === "number") resolve(port);
|
|
else reject(new Error("Unable to allocate a free host-agent port."));
|
|
});
|
|
});
|
|
server.listen(0, host);
|
|
});
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function tailFile(filePath, maxBytes = 12_000) {
|
|
try {
|
|
const stats = fs.statSync(filePath);
|
|
const start = Math.max(0, stats.size - maxBytes);
|
|
const fd = fs.openSync(filePath, "r");
|
|
try {
|
|
const buffer = Buffer.alloc(stats.size - start);
|
|
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
return buffer.toString("utf8").trim();
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
async function waitForHealth({
|
|
apiBase,
|
|
child,
|
|
getChildError,
|
|
logPath,
|
|
attempts,
|
|
delayMs,
|
|
log,
|
|
}) {
|
|
const healthUrl = new URL(DEFAULT_HOST_AGENT_HEALTH_PATH, apiBase).toString();
|
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
const childError = getChildError?.();
|
|
if (childError) {
|
|
throw new Error(
|
|
[`Host agent failed to start: ${childError.message}`, tailFile(logPath)]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
);
|
|
}
|
|
|
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
throw new Error(
|
|
[
|
|
`Host agent exited before ${healthUrl} became ready.`,
|
|
tailFile(logPath),
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(healthUrl);
|
|
if (response.ok) {
|
|
log?.(`host agent ready at ${apiBase}`);
|
|
return;
|
|
}
|
|
} catch {
|
|
// Retry until attempts are exhausted or the child exits.
|
|
}
|
|
|
|
await sleep(delayMs);
|
|
}
|
|
|
|
throw new Error(
|
|
[
|
|
`Timed out waiting for host agent health at ${healthUrl}.`,
|
|
tailFile(logPath),
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
);
|
|
}
|
|
|
|
export async function startDeviceE2eHostAgent({
|
|
repoRoot,
|
|
artifactDir,
|
|
requestedPort = null,
|
|
preferredPort = process.env.ELIZA_IOS_HOST_AGENT_PORT ??
|
|
DEFAULT_HOST_AGENT_PORT,
|
|
host = DEFAULT_HOST_AGENT_HOST,
|
|
readyAttempts = Number.parseInt(
|
|
process.env.ELIZA_HOST_AGENT_READY_ATTEMPTS ??
|
|
String(DEFAULT_READY_ATTEMPTS),
|
|
10,
|
|
),
|
|
readyDelayMs = Number.parseInt(
|
|
process.env.ELIZA_HOST_AGENT_READY_DELAY_MS ??
|
|
String(DEFAULT_READY_DELAY_MS),
|
|
10,
|
|
),
|
|
log = null,
|
|
command = process.execPath,
|
|
args = [
|
|
path.join(repoRoot, "packages/app-core/scripts/run-node-tsx.mjs"),
|
|
path.join(repoRoot, "packages/app-core/scripts/serve-real-local-agent.ts"),
|
|
],
|
|
env = process.env,
|
|
} = {}) {
|
|
if (!repoRoot) throw new Error("startDeviceE2eHostAgent requires repoRoot.");
|
|
if (!artifactDir) {
|
|
throw new Error("startDeviceE2eHostAgent requires artifactDir.");
|
|
}
|
|
|
|
const port = await chooseHostAgentPort({
|
|
preferredPort,
|
|
requestedPort,
|
|
host,
|
|
});
|
|
const apiBase = hostAgentApiBase(port, host);
|
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
const logPath = path.join(artifactDir, "host-agent.log");
|
|
const logFd = fs.openSync(logPath, "w");
|
|
const child = spawn(command, args, {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...env,
|
|
ELIZA_API_PORT: String(port),
|
|
ELIZA_PAIRING_DISABLED: "1",
|
|
},
|
|
stdio: ["ignore", logFd, logFd],
|
|
});
|
|
let childError = null;
|
|
let childExited = false;
|
|
child.once("error", (error) => {
|
|
childError = error;
|
|
});
|
|
child.once("exit", () => {
|
|
childExited = true;
|
|
});
|
|
|
|
let stopped = false;
|
|
let stopPromise = null;
|
|
const stop = async () => {
|
|
if (stopped) return stopPromise;
|
|
stopped = true;
|
|
stopPromise = new Promise((resolve) => {
|
|
const finish = () => {
|
|
try {
|
|
fs.closeSync(logFd);
|
|
} catch {
|
|
// Already closed by the platform.
|
|
}
|
|
resolve();
|
|
};
|
|
|
|
if (
|
|
child.pid === undefined ||
|
|
childExited ||
|
|
child.exitCode !== null ||
|
|
child.signalCode !== null
|
|
) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill("SIGKILL");
|
|
}
|
|
}, 10_000);
|
|
timer.unref?.();
|
|
child.once("exit", () => {
|
|
clearTimeout(timer);
|
|
finish();
|
|
});
|
|
child.kill("SIGTERM");
|
|
});
|
|
return stopPromise;
|
|
};
|
|
|
|
const signalHandlers = new Map();
|
|
for (const signal of SIGNALS) {
|
|
const handler = () => {
|
|
void stop().finally(() =>
|
|
process.exit(
|
|
128 + (signal === "SIGHUP" ? 1 : signal === "SIGINT" ? 2 : 15),
|
|
),
|
|
);
|
|
};
|
|
signalHandlers.set(signal, handler);
|
|
process.once(signal, handler);
|
|
}
|
|
|
|
try {
|
|
log?.(`starting host agent at ${apiBase} (log: ${logPath})`);
|
|
await waitForHealth({
|
|
apiBase,
|
|
child,
|
|
getChildError: () => childError,
|
|
logPath,
|
|
attempts: readyAttempts,
|
|
delayMs: readyDelayMs,
|
|
log,
|
|
});
|
|
} catch (error) {
|
|
await stop();
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
apiBase,
|
|
port,
|
|
logPath,
|
|
pid: child.pid,
|
|
async stop() {
|
|
for (const [signal, handler] of signalHandlers) {
|
|
process.off(signal, handler);
|
|
}
|
|
await stop();
|
|
log?.(`stopped host agent at ${apiBase}`);
|
|
},
|
|
};
|
|
}
|