Files
2026-07-15 18:01:56 +08:00

114 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// hotstate.js —— 热更新版本状态机(纯 fs,bootstrap 与 main 共用)。
// 维护 userData/hot/current.json:{ target, confirmed, blacklist[], attempts{} }
// - target :期望运行的热更版本
// - confirmed :最近一次证明健康(渲染层成功加载)的版本 = 安全回退点
// - blacklist :损坏版本,永不再选
// - attempts :未确认版本的启动尝试计数(先落盘再加载,崩溃后下次可判砖)
"use strict";
const fs = require("fs");
const path = require("path");
const MAX_TRY = 1; // 未确认版本最多尝试次数;超过即判砖回退
function statePath(hotBase) {
return path.join(hotBase, "current.json");
}
function readState(hotBase) {
try {
const s = JSON.parse(fs.readFileSync(statePath(hotBase), "utf8"));
return {
target: s.target || null,
confirmed: s.confirmed || null,
blacklist: Array.isArray(s.blacklist) ? s.blacklist : [],
attempts: s.attempts && typeof s.attempts === "object" ? s.attempts : {},
};
} catch {
return { target: null, confirmed: null, blacklist: [], attempts: {} };
}
}
function writeState(hotBase, state) {
try {
fs.mkdirSync(hotBase, { recursive: true });
fs.writeFileSync(statePath(hotBase), JSON.stringify(state, null, 2));
return true;
} catch {
return false; // 无写权限等 → 由调用方降级
}
}
// 版本目录是否为可加载的完整结构
function versionUsable(hotBase, ver) {
if (!ver) return false;
try {
return fs.existsSync(path.join(hotBase, ver, "electron", "main.js"));
} catch {
return false;
}
}
// bootstrap 调用:决定本次启动加载哪个版本。返回版本名(字符串)或 null(=用包内)。
// 有副作用:对 pending 版本自增 attempts 并【先落盘】,超限则拉黑回退。
function chooseVersion(hotBase) {
const state = readState(hotBase);
let cand = state.target;
if (!cand) return null;
// 被拉黑或目录不可用 → 回退 confirmed
if (state.blacklist.includes(cand) || !versionUsable(hotBase, cand)) {
cand = state.confirmed;
}
if (!versionUsable(hotBase, cand)) return null; // confirmed 也没了 → 包内
if (cand === state.confirmed) return cand; // 已确认健康,直接用
// cand 是刚应用、尚未确认的新版本(pending)
state.attempts[cand] = (state.attempts[cand] || 0) + 1;
const over = state.attempts[cand] > MAX_TRY;
if (over) {
// 之前给过机会仍未确认 → 判砖,拉黑并回退
if (!state.blacklist.includes(cand)) state.blacklist.push(cand);
delete state.attempts[cand];
state.target = state.confirmed;
writeState(hotBase, state);
return versionUsable(hotBase, state.confirmed) ? state.confirmed : null;
}
writeState(hotBase, state); // 关键:先落盘尝试计数,再返回让 bootstrap 加载
return cand;
}
// 加载崩溃时拉黑某版本
function blacklistVersion(hotBase, ver, reason) {
if (!ver) return;
const state = readState(hotBase);
if (!state.blacklist.includes(ver)) state.blacklist.push(ver);
delete state.attempts[ver];
if (state.target === ver) state.target = state.confirmed;
state.lastError = { ver, reason: String(reason || "").slice(0, 500) };
writeState(hotBase, state);
}
// 渲染层成功加载后确认健康
function confirmHealthy(hotBase, ver) {
if (!ver) return;
const state = readState(hotBase);
state.confirmed = ver;
state.target = ver;
delete state.attempts[ver];
state.blacklist = state.blacklist.filter((v) => v !== ver);
writeState(hotBase, state);
}
// 应用新版本后设为 target(等重启生效)
function setTarget(hotBase, ver) {
const state = readState(hotBase);
state.target = ver;
writeState(hotBase, state);
}
// 当前“有效内容版本”:优先已确认,其次包内(由调用方给 fallback)
function effectiveVersion(hotBase) {
const state = readState(hotBase);
return state.confirmed || null;
}
module.exports = {
MAX_TRY, readState, writeState, versionUsable,
chooseVersion, blacklistVersion, confirmHealthy, setTarget, effectiveVersion,
};