fix: add job cancel and tune artifact download
This commit is contained in:
@@ -1677,6 +1677,8 @@ function downloadRemoteFile(options: ArtifactRegistryOptions, remotePath: string
|
||||
"ssh",
|
||||
options.providerId,
|
||||
"download",
|
||||
"--chunk-bytes",
|
||||
"64000",
|
||||
remotePath,
|
||||
localPath,
|
||||
], repoRoot, { timeoutMs });
|
||||
|
||||
+4
-2
@@ -84,6 +84,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "codex (queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List low-noise queue summaries by default, including effective activity counts that distinguish scheduler-local queues, DB running tasks, and heartbeat-fresh runners; full queue rows require --full/--all." },
|
||||
{ command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page and progress summaries." },
|
||||
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with a structured progress summary and bounded stdout/stderr tails." },
|
||||
{ command: "job cancel <jobId>", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." },
|
||||
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
|
||||
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
|
||||
@@ -480,13 +481,14 @@ function codexHelp(): unknown {
|
||||
|
||||
function jobHelp(): unknown {
|
||||
return {
|
||||
command: "job list|status",
|
||||
command: "job list|status|cancel",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts job list [--limit N] [--include-command]",
|
||||
"bun scripts/cli.ts job status <jobId|latest> [--tail-bytes N]",
|
||||
"bun scripts/cli.ts job cancel <jobId>",
|
||||
],
|
||||
description: "Inspect fire-and-forget job state from .state/jobs with structured progress summaries and bounded log tails.",
|
||||
description: "Inspect or cancel fire-and-forget job state from .state/jobs with structured progress summaries and bounded log tails.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+47
-11
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import { runCommandToFiles, tailFile } from "./command";
|
||||
|
||||
export type JobStatus = "queued" | "running" | "succeeded" | "failed";
|
||||
export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
|
||||
|
||||
export interface JobRecord {
|
||||
id: string;
|
||||
@@ -68,6 +68,34 @@ export function readJob(id: string): JobRecord {
|
||||
return JSON.parse(readFileSync(path, "utf8")) as JobRecord;
|
||||
}
|
||||
|
||||
export function cancelJob(id: string): unknown {
|
||||
const job = readJob(id);
|
||||
if (job.status !== "queued" && job.status !== "running") {
|
||||
return { ok: true, action: "cancel-job", id, alreadyTerminal: true, job };
|
||||
}
|
||||
const actions: Array<Record<string, unknown>> = [];
|
||||
if (job.runnerPid !== null && job.runnerPid !== undefined) {
|
||||
try {
|
||||
process.kill(-job.runnerPid, "SIGTERM");
|
||||
actions.push({ signal: "SIGTERM", target: "process-group", pid: job.runnerPid, ok: true });
|
||||
} catch (error) {
|
||||
actions.push({ signal: "SIGTERM", target: "process-group", pid: job.runnerPid, ok: false, error: error instanceof Error ? error.message : String(error) });
|
||||
try {
|
||||
process.kill(job.runnerPid, "SIGTERM");
|
||||
actions.push({ signal: "SIGTERM", target: "process", pid: job.runnerPid, ok: true });
|
||||
} catch (innerError) {
|
||||
actions.push({ signal: "SIGTERM", target: "process", pid: job.runnerPid, ok: false, error: innerError instanceof Error ? innerError.message : String(innerError) });
|
||||
}
|
||||
}
|
||||
}
|
||||
job.status = "canceled";
|
||||
job.finishedAt = new Date().toISOString();
|
||||
job.exitCode = 130;
|
||||
writeFileSync(job.stderrFile, `${tailFile(job.stderrFile, 512_000)}${JSON.stringify({ event: "unidesk.job.cancel", at: job.finishedAt, jobId: id, actions })}\n`, "utf8");
|
||||
writeJob(job);
|
||||
return { ok: true, action: "cancel-job", id, actions, job };
|
||||
}
|
||||
|
||||
export function listJobs(): JobRecord[] {
|
||||
return readdirSync(jobsDir())
|
||||
.filter((name) => name.endsWith(".json"))
|
||||
@@ -323,32 +351,40 @@ function summarizeGitMirrorJobProgress(job: JobRecord, stdoutTail: string, stder
|
||||
|
||||
function genericJobProgress(job: JobRecord): JobProgressSummary {
|
||||
const nowMs = Date.now();
|
||||
const stderrTail = tailFile(job.stderrFile, 96_000);
|
||||
const downloadEvents = parseJsonLineEvents(stderrTail, "unidesk.ssh.download.progress");
|
||||
const lastDownload = downloadEvents.at(-1) ?? null;
|
||||
const lastDownloadAt = stringField(lastDownload?.at);
|
||||
const lastEventAgeSeconds = lastDownloadAt === null ? null : secondsSince(lastDownloadAt, job.finishedAt ?? nowMs);
|
||||
const elapsedSeconds = jobElapsedSeconds(job, nowMs);
|
||||
const warnings = jobProgressWarnings({
|
||||
job,
|
||||
eventsObserved: 0,
|
||||
eventsObserved: downloadEvents.length,
|
||||
elapsedSeconds,
|
||||
stage: null,
|
||||
stageStatus: null,
|
||||
stage: lastDownload === null ? null : "ssh-download",
|
||||
stageStatus: lastDownload === null ? null : "running",
|
||||
stageElapsedSeconds: null,
|
||||
lastEventAgeSeconds: null,
|
||||
lastEventAgeSeconds,
|
||||
});
|
||||
const downloadSummary = lastDownload === null
|
||||
? null
|
||||
: ` ssh-download ${Number(lastDownload.actualBytes ?? 0)}/${Number(lastDownload.expectedBytes ?? 0)} bytes chunks=${Number(lastDownload.chunks ?? 0)}`;
|
||||
return {
|
||||
kind: "generic",
|
||||
stage: null,
|
||||
stageStatus: null,
|
||||
stage: lastDownload === null ? null : "ssh-download",
|
||||
stageStatus: lastDownload === null ? null : "running",
|
||||
sourceCommit: null,
|
||||
pipelineRun: null,
|
||||
pipelineCreated: null,
|
||||
elapsedSeconds,
|
||||
stageElapsedSeconds: null,
|
||||
lastEventAt: null,
|
||||
lastEventAgeSeconds: null,
|
||||
eventsObserved: 0,
|
||||
lastEventAt: lastDownloadAt,
|
||||
lastEventAgeSeconds,
|
||||
eventsObserved: downloadEvents.length,
|
||||
slow: warnings.length > 0,
|
||||
warnings,
|
||||
timings: {},
|
||||
summary: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}`,
|
||||
summary: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}${downloadSummary ?? ""}`,
|
||||
nextCommand: job.status === "running" ? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000` : null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user