@co0ontty/wand 2.9.1 → 2.10.0
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/dist/build-info.json +3 -3
- package/dist/npm-update-utils.d.ts +11 -0
- package/dist/npm-update-utils.js +470 -82
- package/dist/server.js +16 -16
- package/dist/service-self-repair.d.ts +1 -1
- package/dist/service-self-repair.js +1 -1
- package/dist/tui/commands.d.ts +2 -2
- package/dist/tui/commands.js +52 -5
- package/dist/update-helper.d.ts +14 -0
- package/dist/update-helper.js +217 -102
- package/dist/web-ui/content/scripts.js +16 -16
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -25,7 +25,7 @@ import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateCh
|
|
|
25
25
|
import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
|
|
26
26
|
import { computeRelaunch } from "./relaunch.js";
|
|
27
27
|
import { isServiceInstalled } from "./tui/commands.js";
|
|
28
|
-
import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
|
|
28
|
+
import { canUseDetachedUpdateHelper, checkManagedServiceUpdatePreflight, startDetachedUpdateHelper, } from "./update-helper.js";
|
|
29
29
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
30
30
|
import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
|
|
31
31
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
@@ -1884,9 +1884,6 @@ export async function startServer(config, configPath) {
|
|
|
1884
1884
|
version: targetLabel,
|
|
1885
1885
|
logPath: helper.logPath,
|
|
1886
1886
|
});
|
|
1887
|
-
setTimeout(() => {
|
|
1888
|
-
shutdownForDetachedUpdate();
|
|
1889
|
-
}, 500);
|
|
1890
1887
|
}
|
|
1891
1888
|
catch (error) {
|
|
1892
1889
|
res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
|
|
@@ -2467,18 +2464,6 @@ export async function startServer(config, configPath) {
|
|
|
2467
2464
|
// Force exit after 5s if graceful shutdown stalls
|
|
2468
2465
|
setTimeout(() => process.exit(0), 5000);
|
|
2469
2466
|
}
|
|
2470
|
-
function shutdownForDetachedUpdate() {
|
|
2471
|
-
try {
|
|
2472
|
-
wss.clients.forEach((client) => client.close());
|
|
2473
|
-
}
|
|
2474
|
-
catch {
|
|
2475
|
-
/* noop */
|
|
2476
|
-
}
|
|
2477
|
-
server.close(() => {
|
|
2478
|
-
process.exit(0);
|
|
2479
|
-
});
|
|
2480
|
-
setTimeout(() => process.exit(0), 3000);
|
|
2481
|
-
}
|
|
2482
2467
|
app.post("/api/restart", async (_req, res) => {
|
|
2483
2468
|
res.json({ ok: true, message: "服务正在重启..." });
|
|
2484
2469
|
wsManager.emitEvent({
|
|
@@ -2607,6 +2592,21 @@ export async function startServer(config, configPath) {
|
|
|
2607
2592
|
});
|
|
2608
2593
|
return;
|
|
2609
2594
|
}
|
|
2595
|
+
const servicePreflight = checkManagedServiceUpdatePreflight();
|
|
2596
|
+
if (!servicePreflight.ok) {
|
|
2597
|
+
process.stdout.write(`[wand] 自动更新已取消: ${servicePreflight.message}\n`);
|
|
2598
|
+
wsManager.emitEvent({
|
|
2599
|
+
type: "notification",
|
|
2600
|
+
sessionId: "__system__",
|
|
2601
|
+
data: {
|
|
2602
|
+
kind: "auto-update-failed",
|
|
2603
|
+
current: info.current,
|
|
2604
|
+
latest: info.latest,
|
|
2605
|
+
error: servicePreflight.message,
|
|
2606
|
+
},
|
|
2607
|
+
});
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
2610
|
// Auto-update: install and restart
|
|
2611
2611
|
process.stdout.write(`[wand] 自动更新:正在从 ${info.current} 更新到 ${info.latest}...\n`);
|
|
2612
2612
|
wsManager.emitEvent({
|
|
@@ -10,7 +10,7 @@ import { type ServiceScope } from "./tui/commands.js";
|
|
|
10
10
|
*
|
|
11
11
|
* 这里在装包成功后调用,best-effort:
|
|
12
12
|
* - 没装服务 → 跳过;
|
|
13
|
-
* - 装了服务 → 用 preferGlobalBin 重写 unit(ExecStart
|
|
13
|
+
* - 装了服务 → 用 preferGlobalBin 重写 unit(ExecStart→全局稳定 wand shim,
|
|
14
14
|
* Environment=PATH 取当前已被 path-repair 修复过的 process.env.PATH)+ daemon-reload;
|
|
15
15
|
* - system scope 非 root 无法写 /etc → installService 返回失败,这里捕获成 warning,
|
|
16
16
|
* 绝不抛错中断更新流程。
|
|
@@ -22,7 +22,7 @@ export function repairServiceUnitAfterUpdate(configPath) {
|
|
|
22
22
|
return {
|
|
23
23
|
repaired: true,
|
|
24
24
|
scope,
|
|
25
|
-
message:
|
|
25
|
+
message: `已用全局稳定入口重写 ${scope} 服务 unit(刷新 ExecStart / PATH)`,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
const tail = scope === "system"
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -58,8 +58,8 @@ export interface ServiceContext {
|
|
|
58
58
|
/** 显式指定作用域。不传走 DEFAULT_SERVICE_SCOPE。 */
|
|
59
59
|
scope?: ServiceScope;
|
|
60
60
|
/**
|
|
61
|
-
* 更新后自修复场景:优先把 ExecStart
|
|
62
|
-
*
|
|
61
|
+
* 更新后自修复场景:优先把 ExecStart 钉到 npm 全局 wand shim,而不是
|
|
62
|
+
* process.argv[1]。源码安装的用户更新到 npm/GitHub 版后,
|
|
63
63
|
* argv[1] 仍是旧源码路径,沿用它会让重启跑回旧二进制。
|
|
64
64
|
*/
|
|
65
65
|
preferGlobalBin?: boolean;
|
package/dist/tui/commands.js
CHANGED
|
@@ -10,7 +10,7 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import process from "node:process";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { checkPackageUpdateSync, getInstallSpecForChannel, installPackageGloballySync, normalizeUpdateChannel, resolveGlobalWandCli, } from "../npm-update-utils.js";
|
|
13
|
+
import { checkPackageUpdateSync, getInstallSpecForChannel, installPackageGloballySync, normalizeUpdateChannel, resolveGlobalWandBin, resolveGlobalWandCli, } from "../npm-update-utils.js";
|
|
14
14
|
import { whichSync } from "../path-repair.js";
|
|
15
15
|
import { computeRelaunch } from "../relaunch.js";
|
|
16
16
|
import { ensureDatabaseFile, resolveDatabasePath, WandStorage } from "../storage.js";
|
|
@@ -95,6 +95,9 @@ export function checkUpdate(currentVersion, channel = "stable") {
|
|
|
95
95
|
* 返回 npm 输出供调试。
|
|
96
96
|
*/
|
|
97
97
|
export function installUpdate(channel = "stable") {
|
|
98
|
+
const serviceSafety = checkInstalledServiceEntrypointsForUpdate();
|
|
99
|
+
if (!serviceSafety.ok)
|
|
100
|
+
return serviceSafety;
|
|
98
101
|
const res = installPackageGloballySync(getInstallSpecForChannel(channel), 180_000);
|
|
99
102
|
const out = (res.stdout || "") + (res.stderr ? "\n" + res.stderr : "");
|
|
100
103
|
const trail = res.attempts.length > 1
|
|
@@ -537,12 +540,56 @@ function servicePathFor(scope) {
|
|
|
537
540
|
}
|
|
538
541
|
return "";
|
|
539
542
|
}
|
|
543
|
+
function readServiceEntrypointForUpdate(scope) {
|
|
544
|
+
const servicePath = servicePathFor(scope);
|
|
545
|
+
if (process.platform === "darwin") {
|
|
546
|
+
const result = spawnSync("plutil", ["-extract", "ProgramArguments.1", "raw", "-o", "-", servicePath], { encoding: "utf8", timeout: 5_000 });
|
|
547
|
+
return result.status === 0 ? (result.stdout || "").trim() || null : null;
|
|
548
|
+
}
|
|
549
|
+
if (process.platform === "linux") {
|
|
550
|
+
try {
|
|
551
|
+
const execStart = readFileSync(servicePath, "utf8").split("\n").find((line) => line.startsWith("ExecStart="));
|
|
552
|
+
if (!execStart)
|
|
553
|
+
return null;
|
|
554
|
+
return execStart.slice("ExecStart=".length).trim().split(/\s+/)[1] || null;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function checkInstalledServiceEntrypointsForUpdate() {
|
|
563
|
+
const scopes = ["system", "user"].filter((scope) => isServiceInstalled(scope));
|
|
564
|
+
if (scopes.length === 0)
|
|
565
|
+
return { ok: true, message: "未安装服务" };
|
|
566
|
+
const stableBin = resolveGlobalWandBin();
|
|
567
|
+
if (!stableBin) {
|
|
568
|
+
return { ok: false, message: "npm 全局 wand shim 不存在,已取消更新以保护当前服务。" };
|
|
569
|
+
}
|
|
570
|
+
for (const scope of scopes) {
|
|
571
|
+
const entrypoint = readServiceEntrypointForUpdate(scope);
|
|
572
|
+
if (!entrypoint || path.resolve(entrypoint) !== path.resolve(stableBin)) {
|
|
573
|
+
const command = scope === "system"
|
|
574
|
+
? `sudo ${stableBin} service:install`
|
|
575
|
+
: `${stableBin} service:install --user`;
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
message: `检测到 ${scope} 服务仍指向不稳定入口,已取消更新。请先运行: ${command}`,
|
|
579
|
+
detail: `entrypoint: ${entrypoint || "无法读取"}\nstable shim: ${stableBin}`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { ok: true, message: "服务入口预检通过" };
|
|
584
|
+
}
|
|
540
585
|
function resolveWandBin(ctx) {
|
|
541
|
-
//
|
|
586
|
+
// 更新后自修复:优先 npm 全局稳定 shim,避免沿用旧源码路径;不能钉死到包目录
|
|
587
|
+
// dist/cli.js,因为事务回滚切换 canonical 目录时 shim 才能指向同盘安全备份。
|
|
542
588
|
if (ctx.preferGlobalBin) {
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
545
|
-
return
|
|
589
|
+
const globalBin = resolveGlobalWandBin();
|
|
590
|
+
if (globalBin)
|
|
591
|
+
return globalBin;
|
|
592
|
+
throw new Error("npm 全局 wand shim 不存在,拒绝把服务 unit 写到不稳定的包目录路径。");
|
|
546
593
|
}
|
|
547
594
|
if (ctx.wandBin && existsSync(ctx.wandBin))
|
|
548
595
|
return ctx.wandBin;
|
package/dist/update-helper.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ServiceScope } from "./tui/commands.js";
|
|
1
2
|
export interface DetachedUpdateOptions {
|
|
2
3
|
installSpec: string;
|
|
3
4
|
configPath: string;
|
|
@@ -14,5 +15,18 @@ export interface DetachedUpdateResult {
|
|
|
14
15
|
pid?: number;
|
|
15
16
|
message: string;
|
|
16
17
|
}
|
|
18
|
+
/** A manifest alone is not enough: only a process actually launched by its manager may rely on auto-restart. */
|
|
19
|
+
export declare function resolveManagedServiceScope(installedScope: ServiceScope | null, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, pid?: number): ServiceScope | null;
|
|
20
|
+
export declare function resolveManagedServiceScopeCandidates(installedScopes: ServiceScope[], platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, pid?: number): ServiceScope | null;
|
|
21
|
+
export declare function isStableServiceEntrypoint(entrypoint: string | null, stableBin: string | null): boolean;
|
|
22
|
+
export declare function checkManagedServiceUpdatePreflight(): {
|
|
23
|
+
ok: true;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function shouldUseSystemdRunForDetachedUpdate(serviceScope: ServiceScope | null): boolean;
|
|
29
|
+
/** Exported so the safety-critical update flow can be exercised without starting a detached process. */
|
|
30
|
+
export declare function buildDetachedUpdateHelperScript(opts: DetachedUpdateOptions, logPath: string, serviceScope: ServiceScope | null, updateUtilsPath?: string): string;
|
|
17
31
|
export declare function startDetachedUpdateHelper(opts: DetachedUpdateOptions): DetachedUpdateResult;
|
|
18
32
|
export declare function canUseDetachedUpdateHelper(): boolean;
|
package/dist/update-helper.js
CHANGED
|
@@ -1,66 +1,172 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveGlobalWandBin } from "./npm-update-utils.js";
|
|
7
8
|
function shellQuote(value) {
|
|
8
9
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9
10
|
}
|
|
10
11
|
function shellArray(values) {
|
|
11
12
|
return values.map((value) => shellQuote(value)).join(" ");
|
|
12
13
|
}
|
|
13
|
-
function boolShell(value) {
|
|
14
|
-
return value ? "1" : "0";
|
|
15
|
-
}
|
|
16
14
|
function resolveBashPath() {
|
|
17
15
|
return existsSync("/bin/bash") ? "/bin/bash" : "bash";
|
|
18
16
|
}
|
|
19
|
-
function
|
|
17
|
+
function detectInstalledServiceScopes() {
|
|
18
|
+
if (process.platform === "darwin") {
|
|
19
|
+
return [
|
|
20
|
+
existsSync("/Library/LaunchDaemons/com.wand.web.plist") ? "system" : null,
|
|
21
|
+
existsSync(path.join(os.homedir(), "Library/LaunchAgents/com.wand.web.plist")) ? "user" : null,
|
|
22
|
+
].filter((scope) => scope !== null);
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === "linux") {
|
|
25
|
+
return [
|
|
26
|
+
existsSync("/etc/systemd/system/wand.service") ? "system" : null,
|
|
27
|
+
existsSync(path.join(os.homedir(), ".config/systemd/user/wand.service")) ? "user" : null,
|
|
28
|
+
].filter((scope) => scope !== null);
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
/** A manifest alone is not enough: only a process actually launched by its manager may rely on auto-restart. */
|
|
33
|
+
export function resolveManagedServiceScope(installedScope, platform = process.platform, env = process.env, pid = process.pid) {
|
|
34
|
+
if (platform === "darwin") {
|
|
35
|
+
return env.XPC_SERVICE_NAME === "com.wand.web" ? (installedScope ?? "system") : null;
|
|
36
|
+
}
|
|
37
|
+
if (platform === "linux") {
|
|
38
|
+
const hasSystemdPid = typeof env.SYSTEMD_EXEC_PID === "string" && env.SYSTEMD_EXEC_PID.length > 0;
|
|
39
|
+
const systemdPid = Number(env.SYSTEMD_EXEC_PID || "0");
|
|
40
|
+
const managed = hasSystemdPid
|
|
41
|
+
? Number.isSafeInteger(systemdPid) && systemdPid === pid
|
|
42
|
+
: typeof env.INVOCATION_ID === "string" && env.INVOCATION_ID.length > 0;
|
|
43
|
+
return managed ? (installedScope ?? "system") : null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export function resolveManagedServiceScopeCandidates(installedScopes, platform = process.platform, env = process.env, pid = process.pid) {
|
|
48
|
+
const installedScope = installedScopes.length === 1 ? installedScopes[0] : null;
|
|
49
|
+
const managedScope = resolveManagedServiceScope(installedScope, platform, env, pid);
|
|
50
|
+
if (managedScope && installedScopes.length > 1) {
|
|
51
|
+
throw new Error("同时检测到 system 与 user 两份 wand 服务,无法安全判断当前托管 scope;请先卸载其中一份。");
|
|
52
|
+
}
|
|
53
|
+
return managedScope;
|
|
54
|
+
}
|
|
55
|
+
function detectManagedServiceScope() {
|
|
56
|
+
return resolveManagedServiceScopeCandidates(detectInstalledServiceScopes());
|
|
57
|
+
}
|
|
58
|
+
export function isStableServiceEntrypoint(entrypoint, stableBin) {
|
|
59
|
+
if (!entrypoint || !stableBin)
|
|
60
|
+
return false;
|
|
20
61
|
try {
|
|
21
|
-
return
|
|
62
|
+
return path.resolve(entrypoint) === path.resolve(stableBin);
|
|
22
63
|
}
|
|
23
64
|
catch {
|
|
24
|
-
return
|
|
65
|
+
return false;
|
|
25
66
|
}
|
|
26
67
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
function readManagedServiceEntrypoint(scope) {
|
|
69
|
+
if (process.platform === "darwin") {
|
|
70
|
+
const plist = scope === "system"
|
|
71
|
+
? "/Library/LaunchDaemons/com.wand.web.plist"
|
|
72
|
+
: path.join(os.homedir(), "Library/LaunchAgents/com.wand.web.plist");
|
|
73
|
+
const result = spawnSync("plutil", ["-extract", "ProgramArguments.1", "raw", "-o", "-", plist], { encoding: "utf8", timeout: 5_000 });
|
|
74
|
+
return result.status === 0 ? (result.stdout || "").trim() || null : null;
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === "linux") {
|
|
77
|
+
const unit = scope === "system"
|
|
78
|
+
? "/etc/systemd/system/wand.service"
|
|
79
|
+
: path.join(os.homedir(), ".config/systemd/user/wand.service");
|
|
80
|
+
try {
|
|
81
|
+
const execStart = readFileSync(unit, "utf8").split("\n").find((line) => line.startsWith("ExecStart="));
|
|
82
|
+
if (!execStart)
|
|
83
|
+
return null;
|
|
84
|
+
const tokens = execStart.slice("ExecStart=".length).trim().split(/\s+/);
|
|
85
|
+
return tokens[1] || null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function managedServiceUpdatePreflight(scope) {
|
|
94
|
+
const stableBin = resolveGlobalWandBin();
|
|
95
|
+
const entrypoint = readManagedServiceEntrypoint(scope);
|
|
96
|
+
if (isStableServiceEntrypoint(entrypoint, stableBin))
|
|
97
|
+
return { ok: true };
|
|
98
|
+
const command = scope === "system"
|
|
99
|
+
? `sudo ${stableBin || "wand"} service:install -c <config-path>`
|
|
100
|
+
: `${stableBin || "wand"} service:install --user -c <config-path>`;
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
message: `检测到 ${scope} 服务仍指向不稳定入口 (${entrypoint || "无法读取"}),已取消 Web 更新以保持服务在线。` +
|
|
104
|
+
`请先在终端重注册到全局 shim:${command}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function checkManagedServiceUpdatePreflight() {
|
|
108
|
+
try {
|
|
109
|
+
const scope = detectManagedServiceScope();
|
|
110
|
+
return scope ? managedServiceUpdatePreflight(scope) : { ok: true };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function shouldUseSystemdRunForDetachedUpdate(serviceScope) {
|
|
117
|
+
return serviceScope !== null;
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_UPDATE_UTILS_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "npm-update-utils.js");
|
|
120
|
+
/** Exported so the safety-critical update flow can be exercised without starting a detached process. */
|
|
121
|
+
export function buildDetachedUpdateHelperScript(opts, logPath, serviceScope, updateUtilsPath = DEFAULT_UPDATE_UTILS_PATH) {
|
|
122
|
+
if (!Number.isSafeInteger(opts.parentPid) || opts.parentPid <= 1) {
|
|
123
|
+
throw new Error(`Invalid detached update parent pid: ${opts.parentPid}`);
|
|
124
|
+
}
|
|
125
|
+
const home = opts.env.HOME || os.homedir();
|
|
126
|
+
const npmCache = opts.env.npm_config_cache || path.join(home, ".npm");
|
|
127
|
+
const requestedTimeout = opts.timeoutMs ?? 300_000;
|
|
128
|
+
const timeoutMs = Number.isFinite(requestedTimeout)
|
|
129
|
+
? Math.max(30_000, Math.trunc(requestedTimeout))
|
|
130
|
+
: 300_000;
|
|
35
131
|
return `#!/usr/bin/env bash
|
|
36
|
-
set -
|
|
132
|
+
set -euo pipefail
|
|
37
133
|
LOG=${shellQuote(logPath)}
|
|
134
|
+
INSTALL_SPEC=${shellQuote(opts.installSpec)}
|
|
135
|
+
PARENT_PID=${opts.parentPid}
|
|
38
136
|
exec >>"$LOG" 2>&1
|
|
39
|
-
echo "[wand-update] started at $(date -
|
|
40
|
-
echo "[wand-update] parent pid: $
|
|
41
|
-
echo "[wand-update] install spec: $
|
|
137
|
+
echo "[wand-update] started at $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
138
|
+
echo "[wand-update] parent pid: $PARENT_PID"
|
|
139
|
+
echo "[wand-update] install spec: $INSTALL_SPEC"
|
|
42
140
|
|
|
43
141
|
export PATH=${shellQuote(opts.env.PATH || process.env.PATH || "")}
|
|
44
|
-
export HOME=${shellQuote(
|
|
142
|
+
export HOME=${shellQuote(home)}
|
|
45
143
|
export npm_config_cache=${shellQuote(npmCache)}
|
|
46
144
|
CONFIG_PATH=${shellQuote(opts.configPath)}
|
|
47
|
-
INSTALL_SPEC=${shellQuote(opts.installSpec)}
|
|
48
|
-
PARENT_PID=${opts.parentPid}
|
|
49
145
|
SERVICE_SCOPE=${shellQuote(serviceScope ?? "")}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
146
|
+
TIMEOUT_MS=${timeoutMs}
|
|
147
|
+
NODE_BIN=${shellQuote(process.execPath)}
|
|
148
|
+
UPDATE_UTILS=${shellQuote(updateUtilsPath)}
|
|
149
|
+
WORKING_DIRECTORY=${shellQuote(opts.cwd)}
|
|
150
|
+
CLI_ARGS=(${shellArray(opts.cliArgs)})
|
|
151
|
+
GLOBAL_CLI=""
|
|
152
|
+
PARENT_EXITED=0
|
|
54
153
|
|
|
55
154
|
run() {
|
|
56
155
|
echo "+ $*"
|
|
57
156
|
"$@"
|
|
58
157
|
}
|
|
59
158
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
159
|
+
on_exit() {
|
|
160
|
+
local status="$?"
|
|
161
|
+
if [ "$status" -ne 0 ]; then
|
|
162
|
+
if [ "$PARENT_EXITED" = "0" ]; then
|
|
163
|
+
echo "[wand-update] failed with exit code $status; parent service was left running"
|
|
164
|
+
else
|
|
165
|
+
echo "[wand-update] failed with exit code $status after parent service exited"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
63
168
|
}
|
|
169
|
+
trap on_exit EXIT
|
|
64
170
|
|
|
65
171
|
wait_for_parent_exit() {
|
|
66
172
|
local pid="$1"
|
|
@@ -68,84 +174,68 @@ wait_for_parent_exit() {
|
|
|
68
174
|
while kill -0 "$pid" 2>/dev/null; do
|
|
69
175
|
i=$((i + 1))
|
|
70
176
|
if [ "$i" -ge 120 ]; then
|
|
71
|
-
echo "[wand-update] parent still alive after
|
|
72
|
-
return
|
|
177
|
+
echo "[wand-update] parent still alive after SIGTERM timeout"
|
|
178
|
+
return 1
|
|
73
179
|
fi
|
|
74
180
|
sleep 0.5
|
|
75
181
|
done
|
|
76
182
|
}
|
|
77
183
|
|
|
78
|
-
|
|
184
|
+
resolve_global_cli() {
|
|
79
185
|
local npm_root
|
|
80
|
-
npm_root="$(npm root -g
|
|
81
|
-
|
|
82
|
-
|
|
186
|
+
npm_root="$(npm root -g)"
|
|
187
|
+
GLOBAL_CLI="$npm_root/@co0ontty/wand/dist/cli.js"
|
|
188
|
+
if [ ! -f "$GLOBAL_CLI" ]; then
|
|
189
|
+
echo "[wand-update] global CLI not found after install: $GLOBAL_CLI"
|
|
190
|
+
return 1
|
|
83
191
|
fi
|
|
84
192
|
}
|
|
85
193
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
run npm install -g "$INSTALL_SPEC"
|
|
91
|
-
fi
|
|
92
|
-
}
|
|
194
|
+
main() {
|
|
195
|
+
echo "[wand-update] installing while parent service stays online"
|
|
196
|
+
if run "$NODE_BIN" --input-type=module - "$UPDATE_UTILS" "$INSTALL_SPEC" "$TIMEOUT_MS" <<'WAND_INSTALL_NODE'
|
|
197
|
+
import { pathToFileURL } from "node:url";
|
|
93
198
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
199
|
+
const [modulePath, installSpec, timeoutValue] = process.argv.slice(2);
|
|
200
|
+
const timeoutMs = Number(timeoutValue);
|
|
201
|
+
const updateUtils = await import(pathToFileURL(modulePath).href);
|
|
202
|
+
if (typeof updateUtils.installPackageGloballyAsync !== "function") {
|
|
203
|
+
throw new Error("installPackageGloballyAsync is unavailable in " + modulePath);
|
|
204
|
+
}
|
|
205
|
+
await updateUtils.installPackageGloballyAsync(installSpec, timeoutMs, (line) => {
|
|
206
|
+
process.stdout.write(String(line) + "\\n");
|
|
207
|
+
});
|
|
208
|
+
WAND_INSTALL_NODE
|
|
209
|
+
then
|
|
210
|
+
echo "[wand-update] install completed"
|
|
97
211
|
else
|
|
98
|
-
|
|
212
|
+
local install_status="$?"
|
|
213
|
+
echo "[wand-update] install failed; keeping parent service online"
|
|
214
|
+
return "$install_status"
|
|
99
215
|
fi
|
|
100
|
-
}
|
|
101
216
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
run_best_effort wand service:install --user -c "$CONFIG_PATH"
|
|
106
|
-
run_best_effort systemctl --user restart wand.service
|
|
107
|
-
else
|
|
108
|
-
run_best_effort wand service:install -c "$CONFIG_PATH"
|
|
109
|
-
run_best_effort systemctl restart wand.service
|
|
110
|
-
fi
|
|
111
|
-
return
|
|
112
|
-
fi
|
|
217
|
+
resolve_global_cli
|
|
218
|
+
echo "[wand-update] initializing updated installation"
|
|
219
|
+
run "$NODE_BIN" "$GLOBAL_CLI" init -c "$CONFIG_PATH"
|
|
113
220
|
|
|
114
|
-
if
|
|
115
|
-
echo "[wand-update]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
nohup ${shellQuote(process.execPath)} "$global_cli" "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
|
|
121
|
-
else
|
|
122
|
-
nohup wand "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
|
|
123
|
-
fi
|
|
124
|
-
fi
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
main() {
|
|
128
|
-
if [ "$SERVICE_SCOPE" = "user" ]; then
|
|
129
|
-
run_best_effort systemctl --user stop wand.service
|
|
130
|
-
elif [ "$SERVICE_SCOPE" = "system" ]; then
|
|
131
|
-
run_best_effort systemctl stop wand.service
|
|
221
|
+
if kill -0 "$PARENT_PID" 2>/dev/null; then
|
|
222
|
+
echo "[wand-update] sending SIGTERM to parent pid $PARENT_PID"
|
|
223
|
+
run kill -TERM "$PARENT_PID"
|
|
224
|
+
wait_for_parent_exit "$PARENT_PID"
|
|
225
|
+
else
|
|
226
|
+
echo "[wand-update] parent pid $PARENT_PID already exited"
|
|
132
227
|
fi
|
|
228
|
+
PARENT_EXITED=1
|
|
133
229
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
run_best_effort npm uninstall -g @co0ontty/wand
|
|
142
|
-
clean_leftovers
|
|
143
|
-
npm_install_wand_force
|
|
230
|
+
if [ -n "$SERVICE_SCOPE" ]; then
|
|
231
|
+
echo "[wand-update] $SERVICE_SCOPE service detected; waiting for Restart/KeepAlive relaunch"
|
|
232
|
+
else
|
|
233
|
+
echo "[wand-update] starting updated standalone process"
|
|
234
|
+
cd "$WORKING_DIRECTORY"
|
|
235
|
+
nohup "$NODE_BIN" "$GLOBAL_CLI" "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 </dev/null &
|
|
236
|
+
echo "[wand-update] standalone pid: $!"
|
|
144
237
|
fi
|
|
145
|
-
|
|
146
|
-
run wand init -c "$CONFIG_PATH"
|
|
147
|
-
restart_or_start_service
|
|
148
|
-
echo "[wand-update] completed at $(date -Is)"
|
|
238
|
+
echo "[wand-update] completed at $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
149
239
|
}
|
|
150
240
|
|
|
151
241
|
main
|
|
@@ -179,15 +269,15 @@ function spawnDetached(scriptPath, _logPath, env, serviceScope) {
|
|
|
179
269
|
});
|
|
180
270
|
return res.status === 0 ? { method } : null;
|
|
181
271
|
};
|
|
182
|
-
|
|
183
|
-
runTransient(["--user", `--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run --user")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
runTransient([`--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run")
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
272
|
+
if (shouldUseSystemdRunForDetachedUpdate(serviceScope)) {
|
|
273
|
+
const userService = runTransient(["--user", `--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run --user") ??
|
|
274
|
+
runTransient(["--user", `--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run --user");
|
|
275
|
+
if (userService)
|
|
276
|
+
return userService;
|
|
277
|
+
const systemService = runTransient([`--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run") ??
|
|
278
|
+
runTransient([`--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run");
|
|
279
|
+
if (systemService)
|
|
280
|
+
return systemService;
|
|
191
281
|
return null;
|
|
192
282
|
}
|
|
193
283
|
const setsid = spawnSync("setsid", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
@@ -208,8 +298,33 @@ export function startDetachedUpdateHelper(opts) {
|
|
|
208
298
|
const dir = mkdtempSync(path.join(os.tmpdir(), "wand-update-"));
|
|
209
299
|
const scriptPath = path.join(dir, "update.sh");
|
|
210
300
|
const logPath = path.join(dir, "update.log");
|
|
211
|
-
|
|
212
|
-
|
|
301
|
+
let serviceScope;
|
|
302
|
+
try {
|
|
303
|
+
serviceScope = detectManagedServiceScope();
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
started: false,
|
|
308
|
+
scriptPath,
|
|
309
|
+
logPath,
|
|
310
|
+
message: err instanceof Error ? err.message : String(err),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (serviceScope) {
|
|
314
|
+
const preflight = managedServiceUpdatePreflight(serviceScope);
|
|
315
|
+
if (!preflight.ok) {
|
|
316
|
+
return {
|
|
317
|
+
started: false,
|
|
318
|
+
scriptPath,
|
|
319
|
+
logPath,
|
|
320
|
+
message: preflight.message.replace("<config-path>", opts.configPath),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
writeFileSync(scriptPath, buildDetachedUpdateHelperScript(opts, logPath, serviceScope), {
|
|
325
|
+
encoding: "utf8",
|
|
326
|
+
mode: 0o700,
|
|
327
|
+
});
|
|
213
328
|
chmodSync(scriptPath, 0o700);
|
|
214
329
|
const spawned = spawnDetached(scriptPath, logPath, opts.env, serviceScope);
|
|
215
330
|
if (!spawned) {
|