Download src/cron/command-runner.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.45 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cron/command-runner.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cron/command-runner.ts
-
curl -L -o command-runner.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cron/command-runner.ts
6.45 kB
| import { finiteSecondsToTimerSafeMilliseconds } from "@openclaw/normalization-core/number-coercion"; | |
| import { runCommandWithTimeout } from "../process/exec.js"; | |
| import { | |
| buildCronCommandSummary, | |
| isCronCommandActionCriticalLine, | |
| } from "./command-output-summary.js"; | |
| import type { CronRunDiagnostics, CronRunOutcome, CronRunStatus, CronJob } from "./types.js"; | |
| const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60_000; | |
| const EFFECTIVELY_UNBOUNDED_TIMEOUT_MS = 2_147_483_647; | |
| function secondsToMs(value: number | undefined): number | undefined { | |
| if (typeof value !== "number") { | |
| return undefined; | |
| } | |
| if (value <= 0) { | |
| return EFFECTIVELY_UNBOUNDED_TIMEOUT_MS; | |
| } | |
| return finiteSecondsToTimerSafeMilliseconds(value) ?? undefined; | |
| } | |
| function formatCommand(argv: string[]): string { | |
| return argv.map((arg) => JSON.stringify(arg)).join(" "); | |
| } | |
| function commandErrorMessage(params: { | |
| code: number | null; | |
| signal: NodeJS.Signals | null; | |
| termination: string; | |
| }): string { | |
| if (params.termination === "timeout") { | |
| return "command timed out"; | |
| } | |
| if (params.termination === "no-output-timeout") { | |
| return "command produced no output before noOutputTimeoutSeconds"; | |
| } | |
| if (params.termination === "signal") { | |
| return params.signal ? `command stopped by signal ${params.signal}` : "command stopped"; | |
| } | |
| if (typeof params.code === "number") { | |
| return `command exited with code ${params.code}`; | |
| } | |
| return "command failed"; | |
| } | |
| function buildDiagnostics(params: { | |
| command: string; | |
| status: CronRunStatus; | |
| summary?: string; | |
| code: number | null; | |
| signal: NodeJS.Signals | null; | |
| stdoutTruncatedBytes?: number; | |
| stderrTruncatedBytes?: number; | |
| nowMs: () => number; | |
| }): CronRunDiagnostics { | |
| const truncated = | |
| Boolean(params.stdoutTruncatedBytes && params.stdoutTruncatedBytes > 0) || | |
| Boolean(params.stderrTruncatedBytes && params.stderrTruncatedBytes > 0); | |
| return { | |
| ...(params.summary ? { summary: params.summary } : {}), | |
| entries: [ | |
| { | |
| ts: params.nowMs(), | |
| source: "exec", | |
| severity: params.status === "ok" ? "info" : "error", | |
| message: params.summary | |
| ? `command ${params.status}: ${params.command}` | |
| : `command ${params.status} with no output: ${params.command}`, | |
| exitCode: params.code, | |
| truncated, | |
| ...(params.signal ? { toolName: `signal:${params.signal}` } : {}), | |
| }, | |
| ], | |
| }; | |
| } | |
| /** Executes a cron command payload without starting an agent/model run. */ | |
| export async function runCronCommandJob(params: { | |
| job: CronJob; | |
| abortSignal?: AbortSignal; | |
| nowMs?: () => number; | |
| }): Promise<CronRunOutcome> { | |
| const nowMs = params.nowMs ?? Date.now; | |
| const { payload } = params.job; | |
| if (payload.kind !== "command") { | |
| return { | |
| status: "skipped", | |
| error: 'command runner requires payload.kind="command"', | |
| }; | |
| } | |
| if (!Array.isArray(payload.argv) || payload.argv.length === 0) { | |
| return { | |
| status: "skipped", | |
| error: 'command payload requires non-empty "argv"', | |
| }; | |
| } | |
| const command = formatCommand(payload.argv); | |
| const noOutputTimeoutMs = secondsToMs(payload.noOutputTimeoutSeconds); | |
| try { | |
| const result = await runCommandWithTimeout(payload.argv, { | |
| timeoutMs: secondsToMs(payload.timeoutSeconds) ?? DEFAULT_COMMAND_TIMEOUT_MS, | |
| ...(payload.cwd ? { cwd: payload.cwd } : {}), | |
| ...(payload.input !== undefined ? { input: payload.input } : {}), | |
| ...(payload.env ? { env: payload.env } : {}), | |
| ...(noOutputTimeoutMs !== undefined ? { noOutputTimeoutMs } : {}), | |
| ...(payload.outputMaxBytes !== undefined ? { maxOutputBytes: payload.outputMaxBytes } : {}), | |
| preserveOutputLine: isCronCommandActionCriticalLine, | |
| ...(params.abortSignal ? { signal: params.abortSignal } : {}), | |
| killProcessTree: true, | |
| }); | |
| const termination = | |
| result.termination === "signal" && | |
| params.abortSignal?.reason instanceof Error && | |
| params.abortSignal.reason.name === "TimeoutError" | |
| ? "timeout" | |
| : result.termination; | |
| const ok = | |
| result.code === 0 && | |
| !result.killed && | |
| termination !== "timeout" && | |
| termination !== "no-output-timeout" && | |
| termination !== "signal"; | |
| const status: CronRunStatus = ok ? "ok" : "error"; | |
| const summary = buildCronCommandSummary({ | |
| stdout: result.stdout, | |
| stderr: result.stderr, | |
| preservedStdoutLines: result.preservedStdoutLines, | |
| preservedStderrLines: result.preservedStderrLines, | |
| }); | |
| const error = ok | |
| ? undefined | |
| : commandErrorMessage({ | |
| code: result.code, | |
| signal: result.signal, | |
| termination, | |
| }); | |
| const failureNotificationDetail = | |
| termination === "timeout" | |
| ? ({ kind: "command-timeout", mode: "wall-clock" } as const) | |
| : termination === "no-output-timeout" | |
| ? ({ kind: "command-timeout", mode: "no-output" } as const) | |
| : termination === "exit" && typeof result.code === "number" && result.code !== 0 | |
| ? ({ kind: "command-exit", exitCode: result.code } as const) | |
| : undefined; | |
| return { | |
| status, | |
| ...(error ? { error } : {}), | |
| ...(failureNotificationDetail | |
| ? { | |
| failureNotificationDetail, | |
| errorClassification: | |
| failureNotificationDetail.kind === "command-timeout" | |
| ? ({ kind: "reason", reason: "timeout" } as const) | |
| : ({ kind: "permanent" } as const), | |
| } | |
| : {}), | |
| ...(summary ? { summary } : {}), | |
| diagnostics: buildDiagnostics({ | |
| command, | |
| status, | |
| summary, | |
| code: result.code, | |
| signal: result.signal, | |
| stdoutTruncatedBytes: result.stdoutTruncatedBytes, | |
| stderrTruncatedBytes: result.stderrTruncatedBytes, | |
| nowMs, | |
| }), | |
| }; | |
| } catch (err) { | |
| const error = err instanceof Error ? err.message : String(err); | |
| return { | |
| status: "error", | |
| error, | |
| ...(err instanceof Error && "code" in err && err.code === "ENOENT" | |
| ? { errorClassification: { kind: "permanent" as const } } | |
| : {}), | |
| diagnostics: { | |
| summary: error, | |
| entries: [ | |
| { | |
| ts: nowMs(), | |
| source: "exec", | |
| severity: "error", | |
| message: `command failed to start: ${command}: ${error}`, | |
| exitCode: null, | |
| }, | |
| ], | |
| }, | |
| }; | |
| } | |
| } | |