luckytri 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/CHANGELOG.md +10 -0
- package/docs/en/guide.md +2 -0
- package/docs/en/npm-release.md +6 -0
- package/docs/zh/CHANGELOG.md +10 -0
- package/docs/zh/guide.md +2 -0
- package/docs/zh/npm-release.md +8 -0
- package/package.json +1 -1
- package/public/app/assets/{ChatsPage-eDqFIg97.js → ChatsPage-GGOaUl3H.js} +1 -1
- package/public/app/assets/{Empty-CAGOJsfD.js → Empty-BSmsWoGM.js} +1 -1
- package/public/app/assets/{Field-CRz-JmN1.js → Field-tUGp0-YL.js} +1 -1
- package/public/app/assets/{HeartPage-BGWoeMrC.js → HeartPage-B5_e1hI9.js} +1 -1
- package/public/app/assets/{LifePage-iao3o3wt.js → LifePage-D5P5kTzy.js} +1 -1
- package/public/app/assets/{MemoryPage-Dwi2yi7E.js → MemoryPage-BrSPiqOS.js} +1 -1
- package/public/app/assets/{NaturePage-DniImo9U.js → NaturePage-C0BdvfFu.js} +1 -1
- package/public/app/assets/{PeoplePage-D-MC3JEt.js → PeoplePage-DBT70lu7.js} +1 -1
- package/public/app/assets/{PluginsPage-D7dJTf96.js → PluginsPage-QBiQg3Jc.js} +1 -1
- package/public/app/assets/{SystemPage-C_Y5M7JU.css → SystemPage-Cf8lKh5C.css} +1 -1
- package/public/app/assets/{SystemPage-C5CUHjGh.js → SystemPage-EFOEvom-.js} +1 -1
- package/public/app/assets/{TimePage-Dwhr3-92.js → TimePage-BuNKjCgN.js} +1 -1
- package/public/app/assets/{index-L0bHax8C.js → index-BaXEYeFH.js} +3 -3
- package/public/app/assets/{knowledge-BTDNafzl.js → knowledge-COyBves6.js} +1 -1
- package/public/app/assets/{media-BBzOAfNG.js → media-8uRD5Ocs.js} +1 -1
- package/public/app/assets/{mind-CBTkFje2.js → mind-CQkvddhx.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +14 -3
- package/public/guide.html +15 -3
- package/public/tutorial.en.md +2 -0
- package/public/tutorial.md +2 -0
- package/scripts/service-supervisor.js +8 -13
- package/scripts/start-server.js +8 -1
- package/scripts/update-instance.js +93 -27
- package/server/i18n/errors.js +36 -0
- package/server/runtime/update-errors.js +73 -0
- package/server/runtime/update-execution.js +13 -8
- package/server/runtime/update-manager.js +30 -8
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"downloading",
|
|
8
|
-
"checking",
|
|
9
|
-
"stopping",
|
|
10
|
-
"backup",
|
|
11
|
-
"restarting",
|
|
12
|
-
"verifying",
|
|
13
|
-
"restoring",
|
|
14
|
-
]);
|
|
3
|
+
import {
|
|
4
|
+
processAlive,
|
|
5
|
+
isUpdateFinished,
|
|
6
|
+
} from "../server/runtime/update-manager.js";
|
|
15
7
|
|
|
16
8
|
function readJob(home) {
|
|
17
9
|
try {
|
|
@@ -34,7 +26,10 @@ export async function updatedServerAfterExit(
|
|
|
34
26
|
while (true) {
|
|
35
27
|
const job = read();
|
|
36
28
|
if (!job || job.parent !== parentPid) return { matched: false, pid: null };
|
|
37
|
-
|
|
29
|
+
// This supervisor can outlive its installed release. A future worker may
|
|
30
|
+
// introduce stages this older process does not know; only explicit terminal
|
|
31
|
+
// states permit handover while the worker is still alive.
|
|
32
|
+
if (isUpdateFinished(job.status))
|
|
38
33
|
return { matched: true, pid: alive(job.childPid) ? job.childPid : null };
|
|
39
34
|
if (!alive(job.workerPid))
|
|
40
35
|
return { matched: true, pid: alive(job.childPid) ? job.childPid : null };
|
package/scripts/start-server.js
CHANGED
|
@@ -30,7 +30,12 @@ else console.log("未检测到系统代理,外部 API 请求将直连。");
|
|
|
30
30
|
let currentPid;
|
|
31
31
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
32
32
|
process.on(signal, () => {
|
|
33
|
-
if (currentPid)
|
|
33
|
+
if (currentPid)
|
|
34
|
+
try {
|
|
35
|
+
process.kill(currentPid, signal);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code !== "ESRCH") throw error;
|
|
38
|
+
}
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
async function start(root) {
|
|
@@ -71,6 +76,7 @@ try {
|
|
|
71
76
|
} else {
|
|
72
77
|
await waitForProcessExit(tracked.pid);
|
|
73
78
|
}
|
|
79
|
+
currentPid = undefined;
|
|
74
80
|
|
|
75
81
|
const update = await updatedServerAfterExit(paths.home, tracked.pid);
|
|
76
82
|
if (!update.matched) break;
|
|
@@ -80,6 +86,7 @@ try {
|
|
|
80
86
|
// after rollback). Keep this foreground process alive so systemd does not
|
|
81
87
|
// kill that process or any still-running update work in the same cgroup.
|
|
82
88
|
tracked = { pid: update.pid };
|
|
89
|
+
currentPid = update.pid;
|
|
83
90
|
direct = false;
|
|
84
91
|
continue;
|
|
85
92
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
realpathSync,
|
|
8
|
+
writeSync,
|
|
8
9
|
} from "node:fs";
|
|
9
10
|
import { dirname, join, delimiter } from "node:path";
|
|
10
11
|
import { pathToFileURL } from "node:url";
|
|
@@ -23,6 +24,21 @@ import {
|
|
|
23
24
|
writeUpdateJob,
|
|
24
25
|
} from "../server/runtime/update-manager.js";
|
|
25
26
|
import { updateRoot } from "../server/runtime/installation.js";
|
|
27
|
+
import {
|
|
28
|
+
npmFailureCode,
|
|
29
|
+
updateError,
|
|
30
|
+
} from "../server/runtime/update-errors.js";
|
|
31
|
+
|
|
32
|
+
export function requireUpdateNode(version = process.versions.node) {
|
|
33
|
+
const [major, minor] = String(version).split(".").map(Number);
|
|
34
|
+
if (
|
|
35
|
+
!Number.isInteger(major) ||
|
|
36
|
+
!Number.isInteger(minor) ||
|
|
37
|
+
major < 24 ||
|
|
38
|
+
(major === 24 && minor < 5)
|
|
39
|
+
)
|
|
40
|
+
throw updateError("NODE_UNSUPPORTED");
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
export function installEnvironment(env) {
|
|
28
44
|
const kept = {};
|
|
@@ -35,11 +51,11 @@ export function installEnvironment(env) {
|
|
|
35
51
|
kept[key] = value;
|
|
36
52
|
return kept;
|
|
37
53
|
}
|
|
38
|
-
export function findNpmCli(env = process.env) {
|
|
54
|
+
export function findNpmCli(env = process.env, execPath = process.execPath) {
|
|
39
55
|
const candidates = [
|
|
40
56
|
env.npm_execpath,
|
|
41
|
-
join(dirname(
|
|
42
|
-
join(dirname(
|
|
57
|
+
join(dirname(execPath), "node_modules/npm/bin/npm-cli.js"),
|
|
58
|
+
join(dirname(execPath), "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
43
59
|
...String(env.PATH || env.Path || "")
|
|
44
60
|
.split(delimiter)
|
|
45
61
|
.flatMap((dir) => [
|
|
@@ -60,31 +76,73 @@ export function findNpmCli(env = process.env) {
|
|
|
60
76
|
/* Try the next native Node/npm installation location. */
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
|
-
throw
|
|
79
|
+
throw updateError("NPM_NOT_FOUND");
|
|
64
80
|
}
|
|
65
|
-
function
|
|
81
|
+
export function runUpdateCommand(
|
|
82
|
+
args,
|
|
83
|
+
{
|
|
84
|
+
cwd,
|
|
85
|
+
env = process.env,
|
|
86
|
+
timeout = 600000,
|
|
87
|
+
log,
|
|
88
|
+
failureCode = "STEP_FAILED",
|
|
89
|
+
npmInstall = false,
|
|
90
|
+
} = {},
|
|
91
|
+
) {
|
|
66
92
|
return new Promise((resolve, reject) => {
|
|
67
93
|
const fd = log ? openSync(log, "a", 0o600) : null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
let timer,
|
|
95
|
+
child,
|
|
96
|
+
settled = false,
|
|
97
|
+
timedOut = false,
|
|
98
|
+
outputError,
|
|
99
|
+
tail = "";
|
|
100
|
+
const finish = (error) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
try {
|
|
105
|
+
if (fd !== null) closeSync(fd);
|
|
106
|
+
} catch (closeError) {
|
|
107
|
+
error ||= closeError;
|
|
108
|
+
}
|
|
109
|
+
if (error) reject(error);
|
|
110
|
+
else resolve();
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
child = spawn(process.execPath, args, {
|
|
114
|
+
cwd,
|
|
115
|
+
env,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
finish(error);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const capture = (chunk) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
tail = (tail + chunk.toString("utf8")).slice(-16384);
|
|
126
|
+
try {
|
|
127
|
+
if (fd !== null) writeSync(fd, chunk);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
outputError = error;
|
|
130
|
+
// Wait for exit before rollback can rename a migration database.
|
|
131
|
+
child.kill("SIGKILL");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout.on("data", capture);
|
|
135
|
+
child.stderr.on("data", capture);
|
|
136
|
+
timer = setTimeout(() => {
|
|
77
137
|
timedOut = true;
|
|
78
|
-
child.kill();
|
|
138
|
+
child.kill("SIGKILL");
|
|
79
139
|
}, timeout);
|
|
80
|
-
child.once("error",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (timedOut) reject(Error("更新步骤超时"));
|
|
87
|
-
else code === 0 ? resolve() : reject(Error("更新程序校验失败"));
|
|
140
|
+
child.once("error", finish);
|
|
141
|
+
child.once("close", (code) => {
|
|
142
|
+
if (outputError) finish(outputError);
|
|
143
|
+
else if (timedOut) finish(updateError("STEP_TIMEOUT"));
|
|
144
|
+
else if (code === 0) finish();
|
|
145
|
+
else finish(updateError(npmInstall ? npmFailureCode(tail) : failureCode));
|
|
88
146
|
});
|
|
89
147
|
});
|
|
90
148
|
}
|
|
@@ -94,10 +152,11 @@ export function updateExecution(paths) {
|
|
|
94
152
|
const pause = () => new Promise((r) => setTimeout(r, 250));
|
|
95
153
|
return {
|
|
96
154
|
async install(slot, version) {
|
|
155
|
+
requireUpdateNode();
|
|
97
156
|
const cli = findNpmCli();
|
|
98
157
|
const userconfig = join(updateRoot(paths.home), "install.npmrc");
|
|
99
158
|
writeFileSync(userconfig, "", { mode: 0o600 });
|
|
100
|
-
await
|
|
159
|
+
await runUpdateCommand(
|
|
101
160
|
[
|
|
102
161
|
cli,
|
|
103
162
|
"install",
|
|
@@ -112,20 +171,26 @@ export function updateExecution(paths) {
|
|
|
112
171
|
"--engine-strict",
|
|
113
172
|
`luckytri@${version}`,
|
|
114
173
|
],
|
|
115
|
-
{
|
|
174
|
+
{
|
|
175
|
+
cwd: slot,
|
|
176
|
+
env: installEnvironment(process.env),
|
|
177
|
+
log: paths.log,
|
|
178
|
+
npmInstall: true,
|
|
179
|
+
},
|
|
116
180
|
);
|
|
117
181
|
},
|
|
118
182
|
async check(root, version) {
|
|
119
183
|
const script = `import fs from 'node:fs';import {createRequire} from 'node:module';const p=JSON.parse(fs.readFileSync(${JSON.stringify(join(root, "package.json"))},'utf8'));if(p.version!==${JSON.stringify(version)})process.exit(1);await import(${JSON.stringify(pathToFileURL(join(root, "server/app.js")).href)});const sharp=createRequire(${JSON.stringify(pathToFileURL(join(root, "package.json")).href)})('sharp');await sharp({create:{width:1,height:1,channels:3,background:'#fff'}}).png().toBuffer();`;
|
|
120
|
-
await
|
|
184
|
+
await runUpdateCommand(["--input-type=module", "-e", script], {
|
|
121
185
|
cwd: root,
|
|
122
186
|
env: installEnvironment(process.env),
|
|
123
187
|
timeout: 30000,
|
|
124
188
|
log: paths.log,
|
|
189
|
+
failureCode: "PACKAGE_CHECK_FAILED",
|
|
125
190
|
});
|
|
126
191
|
},
|
|
127
192
|
async migrate(root, database, { previous, version, databaseId }) {
|
|
128
|
-
await
|
|
193
|
+
await runUpdateCommand(
|
|
129
194
|
[
|
|
130
195
|
join(root, "scripts", "migrate-instance.js"),
|
|
131
196
|
database,
|
|
@@ -138,6 +203,7 @@ export function updateExecution(paths) {
|
|
|
138
203
|
env: installEnvironment(process.env),
|
|
139
204
|
timeout: 3600000,
|
|
140
205
|
log: paths.log,
|
|
206
|
+
failureCode: "MIGRATION_FAILED",
|
|
141
207
|
},
|
|
142
208
|
);
|
|
143
209
|
},
|
package/server/i18n/errors.js
CHANGED
|
@@ -34,6 +34,42 @@ export const ERRORS = {
|
|
|
34
34
|
"The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
|
|
35
35
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
|
|
36
36
|
"Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
|
|
37
|
+
"Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。":
|
|
38
|
+
"This Node.js version does not meet the new release requirements. Upgrade Node.js and try again.",
|
|
39
|
+
"当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。":
|
|
40
|
+
"The service cannot find npm. Install npm and check the service PATH.",
|
|
41
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。":
|
|
42
|
+
"npm cannot download this release or its runtime dependencies yet. Check for updates again later.",
|
|
43
|
+
"连接 npm 失败,请检查服务器的网络、DNS 和代理配置。":
|
|
44
|
+
"Cannot reach npm. Check the server network, DNS and proxy settings.",
|
|
45
|
+
"连接 npm 时证书校验失败,请检查服务器的证书和代理配置。":
|
|
46
|
+
"Certificate verification failed when connecting to npm. Check the server certificates and proxy settings.",
|
|
47
|
+
"npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。":
|
|
48
|
+
"npm rejected the download request. Check network or proxy access restrictions.",
|
|
49
|
+
"磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。":
|
|
50
|
+
"There is not enough disk space for the new program, database snapshot and migration copy.",
|
|
51
|
+
"服务账号没有所需的读写权限,请检查实例和更新目录的权限。":
|
|
52
|
+
"The service account lacks read or write access. Check the instance and update directory permissions.",
|
|
53
|
+
"当前更新步骤超时,请检查运行日志后重试。":
|
|
54
|
+
"The update step timed out. Check the runtime log and try again.",
|
|
55
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。":
|
|
56
|
+
"Installation failed while the previous instance remains running. Check the npm errors in the runtime log.",
|
|
57
|
+
"新程序或本机运行依赖校验失败,请查看运行日志。":
|
|
58
|
+
"The new program or its local runtime dependencies failed validation. Check the runtime log.",
|
|
59
|
+
"旧实例未能安全停止,请检查进程管理和实例端口。":
|
|
60
|
+
"The previous instance could not be stopped safely. Check process management and the instance port.",
|
|
61
|
+
"更新前的数据库快照未能完成或通过校验,请查看运行日志。":
|
|
62
|
+
"The pre-update database snapshot could not be completed or verified. Check the runtime log.",
|
|
63
|
+
"记忆与数据迁移未通过校验,请查看运行日志。":
|
|
64
|
+
"Memory and data migration failed validation. Check the runtime log.",
|
|
65
|
+
"新实例未能正常启动,请检查运行日志和端口占用。":
|
|
66
|
+
"The new instance could not start correctly. Check the runtime log and port usage.",
|
|
67
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。":
|
|
68
|
+
"The update process exited unexpectedly. Check the service manager, process exit records and runtime log.",
|
|
69
|
+
"旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。":
|
|
70
|
+
"The previous program or database could not be restored. Recover from the verified pre-update backup.",
|
|
71
|
+
"当前更新步骤未完成,请查看运行日志。":
|
|
72
|
+
"The update step did not complete. Check the runtime log.",
|
|
37
73
|
"缺少候选版本的数据迁移器,已停止更新":
|
|
38
74
|
"The candidate release has no data migrator. The update was stopped.",
|
|
39
75
|
迁移目标不是现有的绝对路径数据库文件:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Only these descriptions reach the management UI. Subprocess output can
|
|
2
|
+
// contain local paths or credentials and belongs in the local log only.
|
|
3
|
+
export const UPDATE_FAILURES = Object.freeze({
|
|
4
|
+
NODE_UNSUPPORTED: "Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。",
|
|
5
|
+
NPM_NOT_FOUND: "当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。",
|
|
6
|
+
VERSION_UNAVAILABLE:
|
|
7
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。",
|
|
8
|
+
NETWORK_ERROR: "连接 npm 失败,请检查服务器的网络、DNS 和代理配置。",
|
|
9
|
+
TLS_ERROR: "连接 npm 时证书校验失败,请检查服务器的证书和代理配置。",
|
|
10
|
+
REGISTRY_ACCESS: "npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。",
|
|
11
|
+
DISK_FULL: "磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。",
|
|
12
|
+
PERMISSION_DENIED: "服务账号没有所需的读写权限,请检查实例和更新目录的权限。",
|
|
13
|
+
STEP_TIMEOUT: "当前更新步骤超时,请检查运行日志后重试。",
|
|
14
|
+
INSTALL_FAILED:
|
|
15
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。",
|
|
16
|
+
PACKAGE_CHECK_FAILED: "新程序或本机运行依赖校验失败,请查看运行日志。",
|
|
17
|
+
STOP_FAILED: "旧实例未能安全停止,请检查进程管理和实例端口。",
|
|
18
|
+
BACKUP_FAILED: "更新前的数据库快照未能完成或通过校验,请查看运行日志。",
|
|
19
|
+
MIGRATION_FAILED: "记忆与数据迁移未通过校验,请查看运行日志。",
|
|
20
|
+
START_FAILED: "新实例未能正常启动,请检查运行日志和端口占用。",
|
|
21
|
+
UPDATE_INTERRUPTED:
|
|
22
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。",
|
|
23
|
+
RECOVERY_FAILED: "旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。",
|
|
24
|
+
STEP_FAILED: "当前更新步骤未完成,请查看运行日志。",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function updateError(code) {
|
|
28
|
+
const known = Object.hasOwn(UPDATE_FAILURES, code) ? code : "STEP_FAILED";
|
|
29
|
+
return Object.assign(Error(UPDATE_FAILURES[known]), { updateCode: known });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function npmFailureCode(output) {
|
|
33
|
+
for (const [pattern, code] of [
|
|
34
|
+
[/\bEBADENGINE\b/, "NODE_UNSUPPORTED"],
|
|
35
|
+
[/\bENOSPC\b/, "DISK_FULL"],
|
|
36
|
+
[/\b(?:EACCES|EPERM)\b/, "PERMISSION_DENIED"],
|
|
37
|
+
[/\b(?:E404|ETARGET)\b/, "VERSION_UNAVAILABLE"],
|
|
38
|
+
[/\b(?:E401|E403)\b/, "REGISTRY_ACCESS"],
|
|
39
|
+
[
|
|
40
|
+
/\b(?:CERT_[A-Z_]+|[A-Z_]*CERTIFICATE[A-Z_]*|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/,
|
|
41
|
+
"TLS_ERROR",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
/\b(?:ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ESOCKETTIMEDOUT|ENETUNREACH|EHOSTUNREACH)\b/,
|
|
45
|
+
"NETWORK_ERROR",
|
|
46
|
+
],
|
|
47
|
+
])
|
|
48
|
+
if (pattern.test(output)) return code;
|
|
49
|
+
return "INSTALL_FAILED";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function updateFailure(error, stage) {
|
|
53
|
+
const osCode = {
|
|
54
|
+
ENOSPC: "DISK_FULL",
|
|
55
|
+
EACCES: "PERMISSION_DENIED",
|
|
56
|
+
EPERM: "PERMISSION_DENIED",
|
|
57
|
+
}[error?.code];
|
|
58
|
+
const fallback =
|
|
59
|
+
{
|
|
60
|
+
downloading: "INSTALL_FAILED",
|
|
61
|
+
checking: "PACKAGE_CHECK_FAILED",
|
|
62
|
+
stopping: "STOP_FAILED",
|
|
63
|
+
backup: "BACKUP_FAILED",
|
|
64
|
+
migrating: "MIGRATION_FAILED",
|
|
65
|
+
restarting: "START_FAILED",
|
|
66
|
+
verifying: "START_FAILED",
|
|
67
|
+
restoring: "RECOVERY_FAILED",
|
|
68
|
+
}[stage] || "STEP_FAILED";
|
|
69
|
+
const failureCode = Object.hasOwn(UPDATE_FAILURES, error?.updateCode)
|
|
70
|
+
? error.updateCode
|
|
71
|
+
: osCode || fallback;
|
|
72
|
+
return { failureCode, detail: UPDATE_FAILURES[failureCode] };
|
|
73
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./update-manager.js";
|
|
16
16
|
import { backupDatabase } from "../../scripts/backup.js";
|
|
17
17
|
import { restoreToNewFile, verifyArchive } from "../storage/archive.js";
|
|
18
|
+
import { updateFailure } from "./update-errors.js";
|
|
18
19
|
|
|
19
20
|
// Executed outside the server so a large verified snapshot does not stall the
|
|
20
21
|
// WebUI. Dependencies are explicit for failure, restart and rollback tests.
|
|
@@ -105,12 +106,9 @@ export async function performUpdate(
|
|
|
105
106
|
return job;
|
|
106
107
|
} catch (error) {
|
|
107
108
|
const failedStage = job.status;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
failedStage,
|
|
112
|
-
error.code || error.cause?.code || "STEP_FAILED",
|
|
113
|
-
);
|
|
109
|
+
const failure = updateFailure(error, failedStage);
|
|
110
|
+
job = { ...job, failedStage, ...failure };
|
|
111
|
+
console.error("更新步骤未完成:", failedStage, failure.failureCode, error);
|
|
114
112
|
if (!stopped) {
|
|
115
113
|
save("error", { error: UPDATE_FAILED, finished: Date.now() });
|
|
116
114
|
return job;
|
|
@@ -150,6 +148,7 @@ export async function performUpdate(
|
|
|
150
148
|
`${paths.database}.update-failed-${id}.migration${suffix}`,
|
|
151
149
|
);
|
|
152
150
|
const restored = await start(paths.package);
|
|
151
|
+
save("restoring", { childPid: restored });
|
|
153
152
|
await health(restored, job.previous);
|
|
154
153
|
save("error", {
|
|
155
154
|
error: UPDATE_FAILED,
|
|
@@ -157,8 +156,14 @@ export async function performUpdate(
|
|
|
157
156
|
childPid: restored,
|
|
158
157
|
finished: Date.now(),
|
|
159
158
|
});
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
159
|
+
} catch (recoveryError) {
|
|
160
|
+
const recovery = updateFailure(recoveryError, "restoring");
|
|
161
|
+
console.error("更新恢复未完成:", recovery.failureCode, recoveryError);
|
|
162
|
+
save("error", {
|
|
163
|
+
error: UPDATE_RECOVERY_FAILED,
|
|
164
|
+
recoveryCode: recovery.failureCode,
|
|
165
|
+
finished: Date.now(),
|
|
166
|
+
});
|
|
162
167
|
}
|
|
163
168
|
return job;
|
|
164
169
|
} finally {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
updateError,
|
|
6
|
+
updateFailure,
|
|
7
|
+
UPDATE_FAILURES,
|
|
8
|
+
} from "./update-errors.js";
|
|
4
9
|
import {
|
|
5
10
|
atomicJson,
|
|
6
11
|
UPDATE_ID,
|
|
@@ -11,7 +16,7 @@ export const UPDATE_FAILED =
|
|
|
11
16
|
"更新未完成,原程序和数据已保留,请查看运行日志后重试。";
|
|
12
17
|
export const UPDATE_RECOVERY_FAILED =
|
|
13
18
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。";
|
|
14
|
-
const
|
|
19
|
+
export const UPDATE_ACTIVE_STAGES = Object.freeze([
|
|
15
20
|
"queued",
|
|
16
21
|
"downloading",
|
|
17
22
|
"checking",
|
|
@@ -22,6 +27,12 @@ const ACTIVE = new Set([
|
|
|
22
27
|
"verifying",
|
|
23
28
|
"restoring",
|
|
24
29
|
]);
|
|
30
|
+
const ACTIVE = new Set(UPDATE_ACTIVE_STAGES);
|
|
31
|
+
export const isUpdateActive = (stage) => ACTIVE.has(stage);
|
|
32
|
+
export const isUpdateFinished = (stage) =>
|
|
33
|
+
stage === "done" || stage === "error";
|
|
34
|
+
const beforeStop = (stage) =>
|
|
35
|
+
["queued", "downloading", "checking"].includes(stage);
|
|
25
36
|
export const processAlive = (pid) => {
|
|
26
37
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
27
38
|
try {
|
|
@@ -70,18 +81,17 @@ export class InstanceUpdater {
|
|
|
70
81
|
return null;
|
|
71
82
|
}
|
|
72
83
|
if (
|
|
73
|
-
|
|
84
|
+
isUpdateActive(job.status) &&
|
|
74
85
|
job.workerPid &&
|
|
75
86
|
!processAlive(job.workerPid)
|
|
76
87
|
) {
|
|
88
|
+
const failedStage = job.status;
|
|
77
89
|
job = writeUpdateJob(this.paths.home, {
|
|
78
90
|
...job,
|
|
79
91
|
status: "error",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
? UPDATE_FAILED
|
|
84
|
-
: UPDATE_RECOVERY_FAILED,
|
|
92
|
+
failedStage,
|
|
93
|
+
...updateFailure(updateError("UPDATE_INTERRUPTED"), failedStage),
|
|
94
|
+
error: beforeStop(failedStage) ? UPDATE_FAILED : UPDATE_RECOVERY_FAILED,
|
|
85
95
|
finished: this.now(),
|
|
86
96
|
});
|
|
87
97
|
}
|
|
@@ -95,10 +105,22 @@ export class InstanceUpdater {
|
|
|
95
105
|
finished: job.finished,
|
|
96
106
|
error: job.error || "",
|
|
97
107
|
restored: !!job.restored,
|
|
108
|
+
...(isUpdateActive(job.failedStage)
|
|
109
|
+
? { failedStage: job.failedStage }
|
|
110
|
+
: {}),
|
|
111
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.failureCode)
|
|
112
|
+
? {
|
|
113
|
+
failureCode: job.failureCode,
|
|
114
|
+
detail: UPDATE_FAILURES[job.failureCode],
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.recoveryCode)
|
|
118
|
+
? { recoveryDetail: UPDATE_FAILURES[job.recoveryCode] }
|
|
119
|
+
: {}),
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
get busy() {
|
|
101
|
-
return this.starting ||
|
|
123
|
+
return this.starting || isUpdateActive(this.status()?.status);
|
|
102
124
|
}
|
|
103
125
|
async install(version) {
|
|
104
126
|
if (this.busy) return this.status();
|