fix: improve egress and job diagnostics (#969)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
+42
-11
@@ -1,8 +1,10 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// UniDesk CLI dispatcher with bounded server lifecycle and job drill-down output.
|
||||
import { readConfig } from "./src/config";
|
||||
import { debugDispatch, debugHealth, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
||||
import { isRebuildableService, isRestartableService, rebuildService, restartService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService, unsupportedRestartService } from "./src/docker";
|
||||
import { emitError, emitJson, emitText, isRenderedCliResult } from "./src/output";
|
||||
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, renderJobStatusSummary, runJob } from "./src/jobs";
|
||||
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, renderJobLaunchSummary, renderJobStatusSummary, runJob } from "./src/jobs";
|
||||
import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check";
|
||||
import { runSsh } from "./src/ssh";
|
||||
import { autoRemoteCiPublishUserServiceDryRunPlan, extractRemoteCliOptions, runRemoteCli } from "./src/remote";
|
||||
@@ -193,6 +195,23 @@ function latestJobId(): string {
|
||||
return jobs[0].id;
|
||||
}
|
||||
|
||||
function wantsFullDisclosure(): boolean {
|
||||
return args.includes("--full") || args.includes("--raw");
|
||||
}
|
||||
|
||||
function emitServerLifecycleResult(result: unknown, ok = true): void {
|
||||
if (!wantsFullDisclosure()) {
|
||||
const rendered = renderJobLaunchSummary(commandName, result);
|
||||
if (rendered !== null) {
|
||||
emitText(rendered.renderedText, rendered.command || commandName);
|
||||
if (!rendered.ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
if (remoteOptions.host !== null) {
|
||||
process.exitCode = await runRemoteCli(remoteOptions, readConfig());
|
||||
@@ -444,12 +463,11 @@ async function main(): Promise<void> {
|
||||
if (sub === "start") {
|
||||
const result = startStack(config);
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
emitServerLifecycleResult(result, ok);
|
||||
return;
|
||||
}
|
||||
if (sub === "stop") {
|
||||
emitJson(commandName, stopStack(config));
|
||||
emitServerLifecycleResult(stopStack(config));
|
||||
return;
|
||||
}
|
||||
if (sub === "status") {
|
||||
@@ -484,7 +502,7 @@ async function main(): Promise<void> {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, rebuildService(config, third));
|
||||
emitServerLifecycleResult(rebuildService(config, third));
|
||||
return;
|
||||
}
|
||||
if (sub === "restart") {
|
||||
@@ -494,7 +512,7 @@ async function main(): Promise<void> {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, restartService(config, third));
|
||||
emitServerLifecycleResult(restartService(config, third));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -564,22 +582,35 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "job") {
|
||||
if (sub === "list") {
|
||||
if (top === "job" || top === "jobs") {
|
||||
const jobSub = sub === "get" || sub === "read" ? "status" : sub;
|
||||
if (jobSub === "list" || jobSub === undefined || isHelpToken(jobSub)) {
|
||||
if (jobSub === undefined || isHelpToken(jobSub)) {
|
||||
emitJson(commandName, {
|
||||
command: "job|jobs list|status|get|read|cancel",
|
||||
aliases: ["jobs list", "jobs get <jobId|latest>", "jobs read <jobId|latest>"],
|
||||
usage: [
|
||||
"bun scripts/cli.ts job list [--limit N] [--include-command]",
|
||||
"bun scripts/cli.ts job status <jobId|latest> [--tail-bytes N] [--full|--raw]",
|
||||
"bun scripts/cli.ts jobs get <jobId|latest>",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, listJobsSummary({ limit: boundedNumberOption("--limit", 50, 500), includeCommand: args.includes("--include-command") }));
|
||||
return;
|
||||
}
|
||||
if (sub === "status") {
|
||||
if (jobSub === "status") {
|
||||
const id = third === "latest" || third === undefined ? latestJobId() : third;
|
||||
const job = jobWithTail(readJob(id), boundedNumberOption("--tail-bytes", 12000, 500_000));
|
||||
if (args.includes("--full") || args.includes("--raw")) {
|
||||
if (wantsFullDisclosure()) {
|
||||
emitJson(commandName, { job });
|
||||
return;
|
||||
}
|
||||
emitText(renderJobStatusSummary(job).renderedText, commandName);
|
||||
return;
|
||||
}
|
||||
if (sub === "cancel") {
|
||||
if (jobSub === "cancel") {
|
||||
if (!third) throw new Error("job cancel requires job id");
|
||||
emitJson(commandName, cancelJob(third));
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// Main-server Docker lifecycle helpers with bounded status probes and job summaries.
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { commandOk, runCommand, tailFile } from "./command";
|
||||
@@ -608,7 +610,7 @@ export function dockerContainers(config: UniDeskConfig): ContainerStatus[] {
|
||||
"label=com.docker.compose.oneoff=False",
|
||||
"--format",
|
||||
"{{json .}}",
|
||||
], repoRoot);
|
||||
], repoRoot, { timeoutMs: 5000 });
|
||||
if (result.exitCode !== 0 || result.stdout.trim().length === 0) return [];
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
@@ -643,9 +645,9 @@ function dockerExecJson(container: string, path: string): unknown {
|
||||
"export url",
|
||||
"bun -e 'const url=process.env.url; fetch(url).then(async r=>{const text=await r.text(); console.log(JSON.stringify({ok:r.ok,status:r.status,body:text?JSON.parse(text):null})); process.exit(r.ok?0:1);}).catch(e=>{console.error(e); process.exit(1)})'",
|
||||
].join("\n");
|
||||
const result = runCommand(["docker", "exec", container, "sh", "-lc", script], repoRoot);
|
||||
const result = runCommand(["docker", "exec", container, "sh", "-lc", script], repoRoot, { timeoutMs: 5000 });
|
||||
if (result.exitCode !== 0) {
|
||||
return { ok: false, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
|
||||
return { ok: false, exitCode: result.exitCode, timedOut: result.timedOut, signal: result.signal, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout.trim()) as unknown;
|
||||
@@ -655,8 +657,8 @@ function dockerExecJson(container: string, path: string): unknown {
|
||||
}
|
||||
|
||||
function dockerExec(config: UniDeskConfig, container: string, command: string[]): unknown {
|
||||
const result = runCommand(["docker", "exec", container, ...command], repoRoot);
|
||||
return { ok: result.exitCode === 0, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
|
||||
const result = runCommand(["docker", "exec", container, ...command], repoRoot, { timeoutMs: 5000 });
|
||||
return { ok: result.exitCode === 0, exitCode: result.exitCode, timedOut: result.timedOut, signal: result.signal, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
|
||||
}
|
||||
|
||||
export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
|
||||
|
||||
+7
-4
@@ -1,3 +1,5 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// Static CLI help for job aliases and server lifecycle progressive disclosure.
|
||||
import { ghHelp, ghScopedHelp } from "./gh";
|
||||
import { authBrokerHelp } from "./auth-broker";
|
||||
import { platformDbHelp } from "./platform-db";
|
||||
@@ -86,8 +88,8 @@ export function rootHelp(): unknown {
|
||||
{ command: "codex steer-confirm <taskId> --steer-id <id> [--raw]", description: "Read-only lookup for a steerId in task trace so deliveryUnconfirmed can be resolved without resending the corrective prompt." },
|
||||
{ command: "codex interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
|
||||
{ command: "codex queues [--full|--all] [--limit N] [--page N|--offset N]", description: "Read legacy Code Queue archive summaries. Legacy queue create/merge and move are frozen; use agentrun create/apply/get/cancel for new work." },
|
||||
{ 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|jobs list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page and progress summaries." },
|
||||
{ command: "job status|get|read <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 ssh-pool <providerId>", description: "Show bounded host.ssh.tcp-pool labels for one provider, including ready/claimed/desired/lastError." },
|
||||
@@ -503,14 +505,15 @@ function codexHelp(): unknown {
|
||||
|
||||
function jobHelp(): unknown {
|
||||
return {
|
||||
command: "job list|status|cancel",
|
||||
command: "job|jobs list|status|get|read|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 jobs get <jobId|latest> [--tail-bytes N]",
|
||||
"bun scripts/cli.ts job cancel <jobId>",
|
||||
],
|
||||
description: "Inspect or cancel 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. `jobs get/read` are compatibility aliases for `job status`; server lifecycle commands return this drill-down by default and reserve full JSON for --full/--raw.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// Async job state, summaries, and lifecycle drill-down rendering for UniDesk CLI.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -270,6 +272,50 @@ export function renderJobStatusSummary(job: ReturnType<typeof jobWithTail>): Ren
|
||||
};
|
||||
}
|
||||
|
||||
export function renderJobLaunchSummary(command: string, result: unknown): RenderedCliResult | null {
|
||||
if (typeof result !== "object" || result === null || !("job" in result)) return null;
|
||||
const record = result as Record<string, unknown>;
|
||||
const job = record.job as Partial<JobRecord> | undefined;
|
||||
if (typeof job !== "object" || job === null || typeof job.id !== "string") return null;
|
||||
const service = typeof record.service === "string" ? record.service : "-";
|
||||
const runtimeEnv = typeof record.runtimeEnv === "object" && record.runtimeEnv !== null ? record.runtimeEnv as Record<string, unknown> : {};
|
||||
const stdoutFile = typeof job.stdoutFile === "string" ? job.stdoutFile : "-";
|
||||
const stderrFile = typeof job.stderrFile === "string" ? job.stderrFile : "-";
|
||||
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
|
||||
const fullCommand = command.includes("--full") || command.includes("--raw") ? null : `bun scripts/cli.ts ${command} --full`;
|
||||
const lines = [
|
||||
"ASYNC_JOB",
|
||||
jobStatusTable(
|
||||
["JOB", "NAME", "STATUS", "SERVICE", "RUNNER", "CREATED"],
|
||||
[[job.id, job.name ?? "-", job.status ?? "-", service, job.runner ?? "-", job.createdAt ?? "-"]],
|
||||
),
|
||||
"",
|
||||
jobStatusTable(
|
||||
["STREAM", "PATH"],
|
||||
[
|
||||
["stdout", stdoutFile],
|
||||
["stderr", stderrFile],
|
||||
["runtimeEnv", typeof runtimeEnv.envFile === "string" ? runtimeEnv.envFile : "-"],
|
||||
["logDir", typeof runtimeEnv.logDir === "string" ? runtimeEnv.logDir : "-"],
|
||||
],
|
||||
),
|
||||
"",
|
||||
"NEXT",
|
||||
` status: ${statusCommand}`,
|
||||
" latest: bun scripts/cli.ts job status latest --tail-bytes 12000",
|
||||
" list: bun scripts/cli.ts job list --limit 20",
|
||||
" logs: bun scripts/cli.ts server logs --tail-bytes 3000",
|
||||
" server: bun scripts/cli.ts server status",
|
||||
fullCommand === null ? null : ` full: ${fullCommand}`,
|
||||
].filter((line): line is string => line !== null);
|
||||
return {
|
||||
ok: job.status !== "failed",
|
||||
command,
|
||||
contentType: "text/plain",
|
||||
renderedText: `${lines.join("\n")}\n`,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeJobProgress(job: JobRecord, maxBytes = 96_000, tails?: { stdoutTail: string; stderrTail: string }): JobProgressSummary {
|
||||
const nowMs = Date.now();
|
||||
const knownWorkflow = job.name === "hwlab_g14_v02_trigger_current";
|
||||
|
||||
Reference in New Issue
Block a user