feat(FN-2689): standardize cross-platform shell selection
- Add engine shell utility to resolve the correct shell per platform - Update routine runner to execute commands through shared shell selection - Update cron runner to use the same cross-platform shell behavior - Apply shared shell handling in dashboard routes for command execution - Add shell utility tests covering platform-specific selection behavior
This commit is contained in:
@@ -3976,11 +3976,13 @@ async function executeSingleCommand(
|
|||||||
const { promisify } = await import("node:util");
|
const { promisify } = await import("node:util");
|
||||||
const execAsyncFn = promisify(exec);
|
const execAsyncFn = promisify(exec);
|
||||||
|
|
||||||
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execAsyncFn(command, {
|
const { stdout, stderr } = await execAsyncFn(command, {
|
||||||
timeout: timeoutMs ?? DEFAULT_AUTOMATION_TIMEOUT_MS,
|
timeout: timeoutMs ?? DEFAULT_AUTOMATION_TIMEOUT_MS,
|
||||||
maxBuffer: AUTOMATION_MAX_BUFFER,
|
maxBuffer: AUTOMATION_MAX_BUFFER,
|
||||||
shell: "/bin/sh",
|
shell: isWindows ? "cmd.exe" : "/bin/sh",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
19
packages/engine/src/__tests__/shell-utils.test.ts
Normal file
19
packages/engine/src/__tests__/shell-utils.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
|
describe("shell-utils", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["win32", "cmd.exe"],
|
||||||
|
["linux", "/bin/sh"],
|
||||||
|
])("returns %s shell as %s", async (platform, expectedShell) => {
|
||||||
|
vi.spyOn(process, "platform", "get").mockReturnValue(platform as NodeJS.Platform);
|
||||||
|
|
||||||
|
const { defaultShell } = await import("../shell-utils.js");
|
||||||
|
|
||||||
|
expect(defaultShell).toBe(expectedShell);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import type { TaskStore } from "@fusion/core";
|
|||||||
import type { AutomationStore } from "@fusion/core";
|
import type { AutomationStore } from "@fusion/core";
|
||||||
import type { ScheduledTask, AutomationRunResult, AutomationStep, AutomationStepResult, Column, TaskCreateInput } from "@fusion/core";
|
import type { ScheduledTask, AutomationRunResult, AutomationStep, AutomationStepResult, Column, TaskCreateInput } from "@fusion/core";
|
||||||
import { createLogger } from "./logger.js";
|
import { createLogger } from "./logger.js";
|
||||||
|
import { defaultShell } from "./shell-utils.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const log = createLogger("cron-runner");
|
const log = createLogger("cron-runner");
|
||||||
@@ -266,7 +267,7 @@ export class CronRunner {
|
|||||||
const { stdout, stderr } = await execAsync(schedule.command, {
|
const { stdout, stderr } = await execAsync(schedule.command, {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
maxBuffer: MAX_BUFFER,
|
maxBuffer: MAX_BUFFER,
|
||||||
shell: "/bin/sh",
|
shell: defaultShell,
|
||||||
});
|
});
|
||||||
|
|
||||||
const output = truncateOutput(stdout, stderr);
|
const output = truncateOutput(stdout, stderr);
|
||||||
@@ -421,7 +422,7 @@ export class CronRunner {
|
|||||||
const { stdout, stderr } = await execAsync(step.command, {
|
const { stdout, stderr } = await execAsync(step.command, {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
maxBuffer: MAX_BUFFER,
|
maxBuffer: MAX_BUFFER,
|
||||||
shell: "/bin/sh",
|
shell: defaultShell,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
import type { HeartbeatMonitor } from "./agent-heartbeat.js";
|
import type { HeartbeatMonitor } from "./agent-heartbeat.js";
|
||||||
import type { AiPromptExecutor } from "./cron-runner.js";
|
import type { AiPromptExecutor } from "./cron-runner.js";
|
||||||
import { createLogger } from "./logger.js";
|
import { createLogger } from "./logger.js";
|
||||||
|
import { defaultShell } from "./shell-utils.js";
|
||||||
|
|
||||||
const log = createLogger("routine-runner");
|
const log = createLogger("routine-runner");
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -264,7 +265,7 @@ export class RoutineRunner {
|
|||||||
const { stdout, stderr } = await execAsync(command, {
|
const { stdout, stderr } = await execAsync(command, {
|
||||||
timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||||
maxBuffer: MAX_BUFFER,
|
maxBuffer: MAX_BUFFER,
|
||||||
shell: "/bin/sh",
|
shell: defaultShell,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
1
packages/engine/src/shell-utils.ts
Normal file
1
packages/engine/src/shell-utils.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const defaultShell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
|
||||||
Reference in New Issue
Block a user