mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-10 17:16:55 +00:00
fix(build): keep CLI help fallback asynchronous (#120454)
This commit is contained in:
committed by
GitHub
parent
e9d3cf1a64
commit
263ad629c1
@@ -665,9 +665,9 @@ export async function renderBundledRootHelpText(
|
||||
});
|
||||
}
|
||||
|
||||
function renderSourceRootHelpText(renderContext?: RootHelpRenderContext): string {
|
||||
async function renderSourceRootHelpText(renderContext?: RootHelpRenderContext): Promise<string> {
|
||||
if (!renderContext) {
|
||||
return withIsolatedRootHelpRenderContext(extensionsDir, renderSourceRootHelpText);
|
||||
return await withIsolatedRootHelpRenderContext(extensionsDir, renderSourceRootHelpText);
|
||||
}
|
||||
const moduleUrl = pathToFileURL(path.join(rootDir, "src/cli/program/root-help.ts")).href;
|
||||
const renderOptions = {
|
||||
@@ -684,28 +684,12 @@ function renderSourceRootHelpText(renderContext?: RootHelpRenderContext): string
|
||||
"process.stdout.write(output);",
|
||||
"process.exit(0);",
|
||||
].join("\n");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "--input-type=module", "--eval", inlineModule],
|
||||
{
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
env: renderContext.env,
|
||||
killSignal: "SIGKILL",
|
||||
timeout: ROOT_HELP_RENDER_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const stderr = result.stderr?.trim();
|
||||
throw new Error(
|
||||
"Failed to render source root help" +
|
||||
(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
|
||||
);
|
||||
}
|
||||
return result.stdout ?? "";
|
||||
return await spawnText(["--import", "tsx", "--input-type=module", "--eval", inlineModule], {
|
||||
cwd: rootDir,
|
||||
env: renderContext.env ?? process.env,
|
||||
failureMessage: "Failed to render source root help",
|
||||
timeoutMs: ROOT_HELP_RENDER_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function renderSourceBrowserHelpText(renderContext: RootHelpRenderContext): Promise<string> {
|
||||
@@ -777,7 +761,7 @@ export async function writeCliStartupMetadata(options?: {
|
||||
extensionsDir?: string;
|
||||
sourceRootDir?: string;
|
||||
renderBundledRootHelpText?: typeof renderBundledRootHelpText;
|
||||
renderSourceRootHelpText?: typeof renderSourceRootHelpText;
|
||||
renderSourceRootHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
|
||||
renderSourceBrowserHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
|
||||
renderSourceSecretsHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
|
||||
renderSourceNodesHelpText?: (renderContext: RootHelpRenderContext) => Awaitable<string>;
|
||||
@@ -877,10 +861,11 @@ export async function writeCliStartupMetadata(options?: {
|
||||
renderContext,
|
||||
);
|
||||
} catch {
|
||||
// The spawnSync source fallback blocks the event loop; that is fine for
|
||||
// this rare recovery path (missing/broken bundle) and only delays
|
||||
// draining sibling render output, not its correctness.
|
||||
return (options?.renderSourceRootHelpText ?? renderSourceRootHelpText)(renderContext);
|
||||
// Keep the fallback asynchronous: sibling help renders share this
|
||||
// event loop, so blocking here can turn completed children into false timeouts.
|
||||
return await (options?.renderSourceRootHelpText ?? renderSourceRootHelpText)(
|
||||
renderContext,
|
||||
);
|
||||
}
|
||||
})();
|
||||
const hasCustomCommandRenderer =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Write Cli Startup Metadata tests cover write cli startup metadata script behavior.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -12,7 +12,7 @@ import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return { ...actual, spawnSync: vi.fn(actual.spawnSync) };
|
||||
return { ...actual, spawn: vi.fn(actual.spawn) };
|
||||
});
|
||||
|
||||
// These subprocess tests use explicit ready/close signals; timeout only catches broken fixtures.
|
||||
@@ -119,25 +119,38 @@ async function waitForChildClose(
|
||||
describe("write-cli-startup-metadata", () => {
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
it("hard-kills synchronous source root help after its timeout", () => {
|
||||
const spawnSyncMock = vi.mocked(spawnSync);
|
||||
const successfulRender = {
|
||||
error: undefined,
|
||||
output: [null, "Usage: openclaw\n", ""],
|
||||
pid: 123,
|
||||
signal: null,
|
||||
status: 0,
|
||||
stderr: "",
|
||||
stdout: "Usage: openclaw\n",
|
||||
};
|
||||
spawnSyncMock.mockReturnValueOnce(successfulRender);
|
||||
it("renders source root help without blocking sibling child events", async () => {
|
||||
const child = createSpawnTextChild();
|
||||
const spawnMock = vi.mocked(spawn);
|
||||
spawnMock.mockImplementationOnce(() => child as unknown as ReturnType<typeof spawn>);
|
||||
let siblingEventObserved = false;
|
||||
const siblingEvent = new Promise<void>((resolve) => {
|
||||
setImmediate(() => {
|
||||
siblingEventObserved = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
expect(__testing.renderSourceRootHelpText()).toBe("Usage: openclaw\n");
|
||||
const render = __testing.renderSourceRootHelpText();
|
||||
child.stdout.write("Usage: openclaw\n");
|
||||
setImmediate(() => {
|
||||
child.emit("close", 0, null);
|
||||
});
|
||||
|
||||
expect(spawnSyncMock).toHaveBeenCalledOnce();
|
||||
expect(spawnSyncMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
killSignal: "SIGKILL",
|
||||
timeout: 120_000,
|
||||
await siblingEvent;
|
||||
expect(siblingEventObserved).toBe(true);
|
||||
await expect(render).resolves.toBe("Usage: openclaw\n");
|
||||
expect(spawnMock).toHaveBeenCalledOnce();
|
||||
expect(spawnMock.mock.calls[0]?.[1]).toEqual([
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
expect.any(String),
|
||||
]);
|
||||
expect(spawnMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user