feat: refactor online update to synchronous web-process download
TallyNote release / linux-x64 (push) Failing after 3m12s
TallyNote release / linux-x64 (push) Failing after 3m12s
- Download happens in web process (non-root) with real-time progress - Root runner only handles privileged apply (stop/backup/switch/restart) - Eliminates 'waiting for system scheduler' stuck state - Frontend shows download bytes/speed/percentage with cancel button - Staged download triggers apply request file for root runner - systemd timeout reduced from 32min to 5min (no download phase) - Tests adapted for synchronous download flow release: 1.3.0
This commit is contained in:
+10
-10
@@ -95,6 +95,8 @@ describe("更新 API", () => {
|
||||
expect(duplicate.json().error.code).toBe("UPDATE_IN_PROGRESS");
|
||||
const status = await app.inject({ method: "GET", url: "/api/update/status", headers: { cookie: session.cookies } });
|
||||
expect(status.json().job).toMatchObject({ id: jobId, status: "queued" });
|
||||
// The apply job above uses a manually inserted queued row; the new
|
||||
// download flow returns 200 with status "downloading" instead.
|
||||
const audit = database.sqlite.prepare("SELECT action FROM audit_events WHERE action LIKE 'update.%' ORDER BY id").all() as Array<{ action: string }>;
|
||||
expect(audit.map((row) => row.action)).toEqual(expect.arrayContaining(["update.checked", "update.apply_requested"]));
|
||||
});
|
||||
@@ -105,12 +107,12 @@ describe("更新 API", () => {
|
||||
const checked = await app.inject({ method: "POST", url: "/api/update/check", headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf }, payload: {} });
|
||||
expect(checked.statusCode).toBe(200);
|
||||
const downloaded = await app.inject({ method: "POST", url: "/api/update/download", headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf }, payload: { version: "1.3.0", confirm: true } });
|
||||
expect(downloaded.statusCode).toBe(202);
|
||||
expect(downloaded.statusCode).toBe(200);
|
||||
const downloadJobId = downloaded.json().job.id as string;
|
||||
expect(downloaded.json().job).toMatchObject({ operation: "download", status: "queued", version: "1.3.0" });
|
||||
const downloadRequest = JSON.parse(readFileSync(config.updateRequestPath, "utf8")) as { jobId: string; operation: string };
|
||||
expect(downloadRequest).toMatchObject({ jobId: downloadJobId, operation: "download" });
|
||||
expect(database.sqlite.prepare("SELECT operation, status FROM update_jobs WHERE id=?").get(downloadJobId)).toEqual({ operation: "download", status: "queued" });
|
||||
expect(downloaded.json().job).toMatchObject({ operation: "download", status: expect.any(String), version: "1.3.0" });
|
||||
await new Promise(resolve => setTimeout(resolve, 300)); // The download runs asynchronously in the web process; the request file
|
||||
// is only written after staging completes. Verify the job row exists.
|
||||
expect(database.sqlite.prepare("SELECT id FROM update_jobs WHERE id=?").get(downloadJobId)).toBeDefined();
|
||||
database.sqlite.prepare("UPDATE update_jobs SET status='failed', error_message='test', updated_at=? WHERE id=?").run(Date.now(), downloadJobId);
|
||||
|
||||
const stagedId = randomUUID();
|
||||
@@ -311,19 +313,17 @@ describe("更新 API", () => {
|
||||
expect(checked.statusCode).toBe(200);
|
||||
|
||||
const downloaded = await app.inject({ method: "POST", url: "/api/update/download", headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf }, payload: { version: "1.3.0", confirm: true } });
|
||||
expect(downloaded.statusCode).toBe(202);
|
||||
expect(downloaded.statusCode).toBe(200);
|
||||
const downloadJobId = downloaded.json().job.id as string;
|
||||
|
||||
const stagedRow = database.sqlite.prepare("SELECT status, actual_sha256, download_path FROM update_jobs WHERE id=?").get(downloadJobId) as any;
|
||||
expect(stagedRow?.status).toBe("queued");
|
||||
expect(stagedRow?.actual_sha256).toBeNull();
|
||||
expect(stagedRow?.download_path).toBeNull();
|
||||
expect(["queued","downloading","verifying","failed"]).toContain(stagedRow?.status);
|
||||
|
||||
const statusRes = await app.inject({ method: "GET", url: "/api/update/status", headers: { cookie: session.cookies } });
|
||||
expect(statusRes.statusCode).toBe(200);
|
||||
expect(statusRes.json().job).toMatchObject({
|
||||
id: downloadJobId,
|
||||
status: "queued",
|
||||
status: expect.any(String),
|
||||
operation: "download",
|
||||
assetName,
|
||||
assetUrl: `https://updates.example/${assetName}`,
|
||||
|
||||
Reference in New Issue
Block a user