Repository navigation
fix(land): keep the fixer alive through output bursts and say why it stopped #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8238d89
78db495
a4bfede
6a1e3d7
68bc9a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { | |
| rmSync, | ||
| writeFileSync, | ||
| } from "node:fs"; | ||
| import { constants as osConstants } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { MERGE_LABEL } from "../core"; | ||
| import { GitHub, type GitHubDependencies, object, string } from "./github"; | ||
|
|
@@ -58,6 +59,10 @@ interface Job { | |
| terminating?: number; | ||
| pid?: number; | ||
| identity?: string; | ||
| /** The launching daemon's process identity, to tell a restart from a crash. */ | ||
| daemon?: string; | ||
| /** Packs fetched from the daemon's repository at launch; their objects are already there. */ | ||
| packs?: string[]; | ||
| results?: FixResult[]; | ||
| error?: string; | ||
| handbacks?: Record<number, string>; | ||
|
|
@@ -194,6 +199,8 @@ export async function startFixer( | |
| plan: failure.plan, | ||
| numbers: entries.map((entry) => entry.number), | ||
| started: deps.now(), | ||
| daemon: identity(process.pid), | ||
| packs: readdirSync(join(checkout, ".git", "objects", "pack")).filter((name) => name.endsWith(".pack")), | ||
| published: [], | ||
| }; | ||
| save(path, job); | ||
|
|
@@ -327,7 +334,8 @@ export async function collectFixers( | |
| try { | ||
| output.push(...(await collectJob(root, state, path, job, deps, token, timeout, read))); | ||
| } catch (error) { | ||
| job.error = `fixer collection failed: ${String(error)}; run /quest-iterate`; | ||
| // Keep the first reason: a GitHub error while handing back a crash must not replace the crash. | ||
| job.error ??= `fixer collection failed: ${String(error)}; run /quest-iterate`; | ||
| try { | ||
| save(path, job); | ||
| output.push(...(await collectJob(root, state, path, job, deps, token, timeout, read))); | ||
|
|
@@ -339,6 +347,12 @@ export async function collectFixers( | |
| return output; | ||
| } | ||
|
|
||
| /** An exit status as a cause: a signal by name (143 is SIGTERM), or a crash with its code (101 is a Rust panic). */ | ||
| function exited(code: string): string { | ||
| const signal = Object.entries(osConstants.signals).find(([, number]) => 128 + number === Number(code))?.[0]; | ||
| return signal ? `stopped by ${signal} (exit ${code})` : `crashed (exit ${code})`; | ||
|
Comment on lines
+352
to
+353
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not identify a signal from an exit code alone. If a custom fixer exits with code 143, the supervisor records
|
||
| } | ||
|
|
||
| async function collectJob( | ||
| root: string, | ||
| state: string, | ||
|
|
@@ -352,10 +366,13 @@ async function collectJob( | |
| const github = new GitHub(deps); | ||
| const output: string[] = []; | ||
| if (!job.results && !job.error) { | ||
| // A restart stops the daemon's whole unit, fixers included, unless the host keeps them running. | ||
| const daemon = identity(process.pid); | ||
| const restarted = job.daemon && daemon && job.daemon !== daemon ? "; the daemon restarted while it ran" : ""; | ||
| if (existsSync(`${path}.exit`)) { | ||
| const code = readFileSync(`${path}.exit`, "utf8"); | ||
| if (job.terminating !== undefined) job.error = "fixer timed out; run /quest-iterate"; | ||
| else if (code !== "0") job.error = `fixer crashed (exit ${code}); run /quest-iterate`; | ||
| else if (code !== "0") job.error = `fixer ${exited(code)}${restarted}; run /quest-iterate`; | ||
| else | ||
| try { | ||
| job.results = parseFixResult(readFixResult(job.checkout), job.numbers); | ||
|
|
@@ -380,8 +397,10 @@ async function collectJob( | |
| } catch {} | ||
| } | ||
| job.error = "fixer timed out; run /quest-iterate to diagnose and retry"; | ||
| } else if (job.pid && job.identity && identity(job.pid) === job.identity) return output; | ||
| else job.error = "fixer disappeared before recording an exit status; run /quest-iterate"; | ||
| } | ||
| // Without a launch identity (an unreadable /proc), only its exit status or the timeout can end the job. | ||
| else if (!job.identity || (job.pid && identity(job.pid) === job.identity)) return output; | ||
| else job.error = `fixer stopped without recording an exit status${restarted}; run /quest-iterate`; | ||
| save(path, job); | ||
| } | ||
| while (job.numbers.length) { | ||
|
|
@@ -420,7 +439,7 @@ async function collectJob( | |
| if (entry.repository !== job.plan.repository && !entry.maintainerCanModify) | ||
| throw new FixerRefusal("fork does not allow maintainer edits"); | ||
| try { | ||
| importFixer(root, state, job.checkout, number, head); | ||
| importFixer(root, state, job.checkout, number, head, job.packs); | ||
| } catch (error) { | ||
| throw new FixerRefusal(String(error)); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: moq-dev/quest
Length of output: 42172
Script executed:
Repository: moq-dev/quest
Length of output: 16762
Script executed:
Repository: moq-dev/quest
Length of output: 32479
Restrict the alternate to launch-fetched objects.
When
knownis nonempty, the alternate exposes the daemon’s entire object directory to the clean import. A fixer that knows a daemon-only object ID can reference it from a new tree under an unprotected path. The import can resolve that object through the alternate, whilevalidateFixchecks ancestry and protected paths but not object provenance. Restrict borrowing to the launch-fetched object set before pushing the fixed head.