a27589418c
Both clone POSTs take background: true: every check answers synchronously,
then 202 {job}. git runs in a detached runner (scripts/clone-runner.js, its
own process group, stderr to a file — no SIGPIPE when the server restarts);
the server tails it for progress and runs the after-step (workspace / extra
folder) once when it exits. Jobs persist in <data dir>/runtime/clone-jobs.json
(0600, atomic, throttled progress writes); on start a live runner is adopted
(counted against the clone limit), an ended one finished, a queued one
requeued (interrupted if it carried a typed login), the rest interrupted.
GET /clone/jobs, DELETE /clone/jobs/:id, POST /clone/jobs/:id/cancel (queued:
dequeued; running: SIGTERM to the runner's group -> cancelled, no after-step)
and /retry (new job from the stored request, never a credential). The
held-open path is unchanged; worktree.gitCommand builds the runner's argv
and git-identity.cloneFailure the shared error classification.
78 lines
2.6 KiB
JavaScript
78 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @file Detached runner for one background `git clone`
|
|
* (`server/lib/clone-jobs.js`). The dashboard restarts on every update, and a
|
|
* git whose stderr was a pipe into it died of SIGPIPE on its next progress
|
|
* line — so the clone runs here instead, in its own session/process group,
|
|
* and outlives the server (the unit is `KillMode=process`). It reads its
|
|
* spec (`{cmd, argv, cwd, timeoutMs}` — exactly the command `worktree.git()`
|
|
* would run, built by `gitCommand`; never a secret, which travels in this
|
|
* process's env as it would in git's) from the 0600 file it is given and
|
|
* deletes it, runs the command with stderr appended straight to
|
|
* `<job dir>/stderr.log` (a file, never a pipe: nothing can SIGPIPE it), and
|
|
* writes `<job dir>/exit.json` `{code, signal, timedOut, finishedAt}` when it
|
|
* ends. A cancel SIGTERMs this whole group: git dies (sudo relays it to a
|
|
* member's git) and this runner stays to record how it ended.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
require("../server/lib/windows-hide").install(); // before child_process is read
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { spawn } = require("child_process");
|
|
|
|
const specPath = process.argv[2];
|
|
const dir = path.dirname(specPath);
|
|
let spec;
|
|
try {
|
|
spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
} finally {
|
|
fs.rmSync(specPath, { force: true });
|
|
}
|
|
|
|
function writeExit(fields) {
|
|
const file = path.join(dir, "exit.json");
|
|
const tmp = `${file}.tmp`;
|
|
fs.writeFileSync(tmp, JSON.stringify({ ...fields, finishedAt: new Date().toISOString() }), {
|
|
mode: 0o600,
|
|
});
|
|
fs.renameSync(tmp, file);
|
|
}
|
|
|
|
// Stay to record git's end: a cancel signals the whole group, this included.
|
|
process.on("SIGTERM", () => {});
|
|
process.on("SIGHUP", () => {});
|
|
process.on("SIGINT", () => {});
|
|
|
|
const log = fs.openSync(path.join(dir, "stderr.log"), "a", 0o600);
|
|
let timedOut = false;
|
|
let child;
|
|
try {
|
|
child = spawn(spec.cmd, spec.argv, {
|
|
cwd: spec.cwd,
|
|
env: process.env,
|
|
stdio: ["ignore", "ignore", log],
|
|
windowsHide: true,
|
|
});
|
|
} catch (err) {
|
|
writeExit({ code: null, signal: null, timedOut: false, error: err.message });
|
|
process.exit(0);
|
|
}
|
|
const timer =
|
|
spec.timeoutMs > 0
|
|
? setTimeout(() => {
|
|
timedOut = true;
|
|
child.kill("SIGTERM");
|
|
}, spec.timeoutMs)
|
|
: null;
|
|
child.on("error", (err) => {
|
|
if (timer) clearTimeout(timer);
|
|
writeExit({ code: null, signal: null, timedOut, error: err.message });
|
|
process.exit(0);
|
|
});
|
|
child.on("exit", (code, signal) => {
|
|
if (timer) clearTimeout(timer);
|
|
writeExit({ code, signal, timedOut });
|
|
process.exit(0);
|
|
});
|