@co0ontty/wand 4.75.1 → 4.75.2-beta.g0c40bc0
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 +4 -4
- package/dist/render-protocol.d.ts +9 -1
- package/dist/render-protocol.js +19 -9
- package/dist/terminal-daemon-protocol.d.ts +1 -1
- package/dist/terminal-daemon-protocol.js +5 -10
- package/dist/terminal-daemon-server.d.ts +1 -1
- package/dist/terminal-daemon-server.js +2 -2
- package/dist/tui/commands.d.ts +2 -0
- package/dist/tui/commands.js +46 -9
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-24T07:
|
|
4
|
-
"version": "4.75.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "0c40bc05212ddd1238c5e34b8e22c360948d714d",
|
|
3
|
+
"builtAt": "2026-09-24T07:47:55.588Z",
|
|
4
|
+
"version": "4.75.2-beta.g0c40bc0",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
|
@@ -112,7 +112,15 @@ export interface RenderPaths {
|
|
|
112
112
|
* `.terminald-<suffix>.token`)**刻意使用不同文件名**:升级期两套并存,
|
|
113
113
|
* 但永不互相领养。
|
|
114
114
|
*/
|
|
115
|
-
export declare function renderPaths(configPath: string): RenderPaths;
|
|
115
|
+
export declare function renderPaths(configPath: string, uidOverride?: number): RenderPaths;
|
|
116
|
+
/**
|
|
117
|
+
* 端点里的 uid 是**跑 daemon 那个用户**的,不是调用者的。
|
|
118
|
+
*
|
|
119
|
+
* `wand service:install` 是拿 sudo 跑的,而 daemon 在 unit/plist 里被钉成 config 的
|
|
120
|
+
* owner;调用方必须显式传 owner uid,否则 root 会算出 `/tmp/wand-render-0-<hash>.sock`
|
|
121
|
+
* 这种永远不存在的路径(2026-09-24 就这么把两个健康 daemon 误判成僵尸杀掉了)。
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveEndpointUid(uidOverride?: number): number;
|
|
116
124
|
/** 编码一帧:`u32` 大端长度 + UTF-8 JSON。 */
|
|
117
125
|
export declare function encodeRenderFrame(value: unknown): Buffer;
|
|
118
126
|
/**
|
package/dist/render-protocol.js
CHANGED
|
@@ -39,18 +39,11 @@ function canonicalConfigPath(configPath) {
|
|
|
39
39
|
* `.terminald-<suffix>.token`)**刻意使用不同文件名**:升级期两套并存,
|
|
40
40
|
* 但永不互相领养。
|
|
41
41
|
*/
|
|
42
|
-
export function renderPaths(configPath) {
|
|
42
|
+
export function renderPaths(configPath, uidOverride) {
|
|
43
43
|
const resolved = canonicalConfigPath(configPath);
|
|
44
44
|
const suffix = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
45
45
|
const dir = path.dirname(resolved);
|
|
46
|
-
const uid = (
|
|
47
|
-
try {
|
|
48
|
-
return os.userInfo().uid;
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
})();
|
|
46
|
+
const uid = resolveEndpointUid(uidOverride);
|
|
54
47
|
return {
|
|
55
48
|
// macOS 的 Unix socket 路径上限约 100 字节,所以 socket 放 /tmp 保持短。
|
|
56
49
|
socketPath: path.join("/tmp", `wand-render-${uid}-${suffix}.sock`),
|
|
@@ -59,6 +52,23 @@ export function renderPaths(configPath) {
|
|
|
59
52
|
metaPath: path.join(dir, `.render-${suffix}.json`),
|
|
60
53
|
};
|
|
61
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* 端点里的 uid 是**跑 daemon 那个用户**的,不是调用者的。
|
|
57
|
+
*
|
|
58
|
+
* `wand service:install` 是拿 sudo 跑的,而 daemon 在 unit/plist 里被钉成 config 的
|
|
59
|
+
* owner;调用方必须显式传 owner uid,否则 root 会算出 `/tmp/wand-render-0-<hash>.sock`
|
|
60
|
+
* 这种永远不存在的路径(2026-09-24 就这么把两个健康 daemon 误判成僵尸杀掉了)。
|
|
61
|
+
*/
|
|
62
|
+
export function resolveEndpointUid(uidOverride) {
|
|
63
|
+
if (typeof uidOverride === "number" && Number.isInteger(uidOverride) && uidOverride >= 0)
|
|
64
|
+
return uidOverride;
|
|
65
|
+
try {
|
|
66
|
+
return os.userInfo().uid;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
62
72
|
/** 编码一帧:`u32` 大端长度 + UTF-8 JSON。 */
|
|
63
73
|
export function encodeRenderFrame(value) {
|
|
64
74
|
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
@@ -42,4 +42,4 @@ export interface TerminalDaemonAttachPayload {
|
|
|
42
42
|
state: TerminalSessionState;
|
|
43
43
|
isNew: boolean;
|
|
44
44
|
}
|
|
45
|
-
export declare function terminalDaemonPaths(configPath: string): TerminalDaemonPaths;
|
|
45
|
+
export declare function terminalDaemonPaths(configPath: string, uidOverride?: number): TerminalDaemonPaths;
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { resolveEndpointUid } from "./render-protocol.js";
|
|
4
4
|
export const TERMINAL_DAEMON_PROTOCOL_VERSION = 2;
|
|
5
|
-
export function terminalDaemonPaths(configPath) {
|
|
5
|
+
export function terminalDaemonPaths(configPath, uidOverride) {
|
|
6
6
|
const resolved = path.resolve(configPath);
|
|
7
7
|
const suffix = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
8
8
|
const dir = path.dirname(resolved);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return 0;
|
|
15
|
-
}
|
|
16
|
-
})();
|
|
9
|
+
// uid 属于跑 daemon 的那个用户;拿 sudo 调用的命令必须显式传 config owner 的 uid,
|
|
10
|
+
// 否则 root 会算出 `-0-` 这种不存在的端点(见 render-protocol 的 resolveEndpointUid)。
|
|
11
|
+
const uid = resolveEndpointUid(uidOverride);
|
|
17
12
|
return {
|
|
18
13
|
socketPath: process.platform === "win32"
|
|
19
14
|
? `\\\\.\\pipe\\wand-terminald-${suffix}`
|
|
@@ -11,4 +11,4 @@ export interface StructuredRunRetentionEntry {
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function structuredRunEvictionOrder(entries: readonly StructuredRunRetentionEntry[], now?: number): string[];
|
|
13
13
|
export declare function runTerminalDaemon(configPath: string): Promise<void>;
|
|
14
|
-
export declare function readTerminalDaemonPid(configPath: string): number | null;
|
|
14
|
+
export declare function readTerminalDaemonPid(configPath: string, uidOverride?: number): number | null;
|
|
@@ -531,9 +531,9 @@ function fileHoldsValue(filePath, expected) {
|
|
|
531
531
|
return false;
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
|
-
export function readTerminalDaemonPid(configPath) {
|
|
534
|
+
export function readTerminalDaemonPid(configPath, uidOverride) {
|
|
535
535
|
try {
|
|
536
|
-
const value = Number(readFileSync(terminalDaemonPaths(configPath).pidPath, "utf8").trim());
|
|
536
|
+
const value = Number(readFileSync(terminalDaemonPaths(configPath, uidOverride).pidPath, "utf8").trim());
|
|
537
537
|
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
538
538
|
}
|
|
539
539
|
catch {
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -90,5 +90,7 @@ export declare function serviceRestart(opts?: ServiceOpts): CommandResult;
|
|
|
90
90
|
export declare function serviceLogs(lines?: number, opts?: ServiceOpts): CommandResult;
|
|
91
91
|
export declare function installService(ctx: ServiceContext): CommandResult;
|
|
92
92
|
export declare function uninstallService(opts?: ServiceOpts): CommandResult;
|
|
93
|
+
/** pid 的命令行是否确实是「本 config 的 render / terminald」。 */
|
|
94
|
+
export declare function isDaemonProcessForConfig(pid: number, configPath: string, kind: "render" | "terminald"): boolean;
|
|
93
95
|
/** 读取已安装服务 unit / plist 的入口命令;未安装或平台不支持时返回 null。 */
|
|
94
96
|
export declare function readServiceEntrypoint(scope: ServiceScope): string | null;
|
package/dist/tui/commands.js
CHANGED
|
@@ -643,21 +643,44 @@ function terminalServicePathFor(scope) {
|
|
|
643
643
|
}
|
|
644
644
|
return "";
|
|
645
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* 本 config 的 config 文件 owner uid。
|
|
648
|
+
*
|
|
649
|
+
* daemon 在 unit/plist 里被钉成 config owner,端点路径里的 uid 也是它;而
|
|
650
|
+
* `wand service:install` 常常是 sudo 跑的(uid 0),所以任何涉及 daemon 文件的读取
|
|
651
|
+
* 都必须用这个 uid,不能用调用者的。
|
|
652
|
+
*/
|
|
653
|
+
function configOwnerUid(configPath) {
|
|
654
|
+
try {
|
|
655
|
+
return statUid(path.resolve(configPath));
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
646
661
|
function hasLiveTerminalDaemon(configPath) {
|
|
647
|
-
const pid = readTerminalDaemonPid(configPath);
|
|
662
|
+
const pid = readTerminalDaemonPid(configPath, configOwnerUid(configPath));
|
|
648
663
|
return pid !== null && isPidAlive(pid);
|
|
649
664
|
}
|
|
650
|
-
/** 按 config 派生两套 daemon 的端点(socket 在 /tmp、pid 在 config 目录旁边)。
|
|
665
|
+
/** 按 config 派生两套 daemon 的端点(socket 在 /tmp、pid 在 config 目录旁边)。
|
|
666
|
+
*
|
|
667
|
+
* uid 用 **config 文件 owner** 的,不是调用者的:`wand service:install` 常常是 sudo 跑的,
|
|
668
|
+
* 而 daemon 在 unit/plist 里被钉成 config owner。用调用者的 uid 会算出 `-0-` 这种不存在的
|
|
669
|
+
* 端点,把健康 daemon 当成僵尸(2026-09-24 真实发生过,两个 daemon 被误杀)。
|
|
670
|
+
*/
|
|
651
671
|
function daemonEndpointStates(configPath) {
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
{ label: "
|
|
657
|
-
|
|
672
|
+
const ownerUid = configOwnerUid(configPath);
|
|
673
|
+
const render = renderPaths(configPath, ownerUid);
|
|
674
|
+
const terminal = terminalDaemonPaths(configPath, ownerUid);
|
|
675
|
+
const endpoints = [
|
|
676
|
+
{ label: "Render", kind: "render", pidPath: render.pidPath, socketPath: render.socketPath },
|
|
677
|
+
{ label: "terminald", kind: "terminald", pidPath: terminal.pidPath, socketPath: terminal.socketPath },
|
|
678
|
+
];
|
|
679
|
+
return endpoints.map(({ label, kind, pidPath, socketPath }) => {
|
|
658
680
|
const pid = readDaemonPidFile(pidPath);
|
|
659
681
|
return {
|
|
660
682
|
label,
|
|
683
|
+
kind,
|
|
661
684
|
pid,
|
|
662
685
|
pidAlive: pid !== null && isPidAlive(pid),
|
|
663
686
|
socketPath,
|
|
@@ -695,6 +718,12 @@ function reapZombieDaemons(configPath) {
|
|
|
695
718
|
for (const state of daemonEndpointStates(configPath)) {
|
|
696
719
|
if (!state.pidAlive || state.socketExists)
|
|
697
720
|
continue;
|
|
721
|
+
// 最后一道闸:确认这个 pid 的**命令行**就是本 config 的这个 daemon。
|
|
722
|
+
// 路径解析一旦与 daemon 不一致(比如 uid 算错),这条能挡住误杀。
|
|
723
|
+
if (!isDaemonProcessForConfig(state.pid, configPath, state.kind)) {
|
|
724
|
+
actions.push(`${state.label} pid ${state.pid} 端点缺失但命令行不像是本 config 的 daemon,跳过清理(保存证据)`);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
698
727
|
try {
|
|
699
728
|
process.kill(state.pid, "SIGKILL");
|
|
700
729
|
actions.push(`${state.label} daemon pid ${state.pid} 进程活着但端点已丢失(${state.socketPath}),已清理`);
|
|
@@ -705,6 +734,14 @@ function reapZombieDaemons(configPath) {
|
|
|
705
734
|
}
|
|
706
735
|
return actions;
|
|
707
736
|
}
|
|
737
|
+
/** pid 的命令行是否确实是「本 config 的 render / terminald」。 */
|
|
738
|
+
export function isDaemonProcessForConfig(pid, configPath, kind) {
|
|
739
|
+
const result = spawnSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8", timeout: 5_000 });
|
|
740
|
+
const command = (result.stdout ?? "").trim();
|
|
741
|
+
if (!command || !command.includes(configPath))
|
|
742
|
+
return false;
|
|
743
|
+
return kind === "render" ? command.includes("wand-render") : command.includes("terminald");
|
|
744
|
+
}
|
|
708
745
|
/**
|
|
709
746
|
* 「正在跑的 daemon 比装好的二进制旧」的提示(没有这种情形时返回 null)。
|
|
710
747
|
*
|
|
@@ -714,7 +751,7 @@ function reapZombieDaemons(configPath) {
|
|
|
714
751
|
* 看不到 detail,所以这句拼进 message 里。
|
|
715
752
|
*/
|
|
716
753
|
function staleDaemonNote(configPath) {
|
|
717
|
-
const render = renderPaths(configPath);
|
|
754
|
+
const render = renderPaths(configPath, configOwnerUid(configPath));
|
|
718
755
|
const pid = readDaemonPidFile(render.pidPath);
|
|
719
756
|
if (pid === null || !isPidAlive(pid))
|
|
720
757
|
return null;
|