rdsh-gateway 0.3.0 → 0.4.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/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/join.d.ts +36 -0
- package/dist/join.js +131 -64
- package/dist/lock.d.ts +24 -0
- package/dist/lock.js +73 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { loginPageHtml } from "./login-page.ts";
|
|
|
24
24
|
export { installService, uninstallService, serviceStatus, systemdUnit, launchdPlist, SERVICE_NAME, JOIN_SERVICE_NAME, HOST_SERVICE_NAME, HUB_SERVICE_NAME } from "./service.ts";
|
|
25
25
|
export type { ServiceSpec } from "./service.ts";
|
|
26
26
|
export declare const NAME = "rdsh-gateway";
|
|
27
|
-
export { join, registerJoin, detectInsecure, selfRevoke } from "./join.ts";
|
|
28
|
-
export type { JoinOptions, RegisterOutcome } from "./join.ts";
|
|
27
|
+
export { join, startJoin, registerJoin, detectInsecure, selfRevoke } from "./join.ts";
|
|
28
|
+
export type { JoinOptions, RegisterOutcome, JoinState, JoinHooks, StartJoinOptions, JoinHandle } from "./join.ts";
|
|
29
29
|
export { readPersistedToken, clearPersistedToken } from "./token-store.ts";
|
|
30
|
+
export { acquireJoinLock, releaseJoinLock, readJoinLock, JOIN_LOCK_PATH } from "./lock.ts";
|
|
31
|
+
export type { JoinLock, JoinLockRole } from "./lock.ts";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { loadTls } from "./tls.js";
|
|
|
16
16
|
export { loginPageHtml } from "./login-page.js";
|
|
17
17
|
export { installService, uninstallService, serviceStatus, systemdUnit, launchdPlist, SERVICE_NAME, JOIN_SERVICE_NAME, HOST_SERVICE_NAME, HUB_SERVICE_NAME } from "./service.js";
|
|
18
18
|
export const NAME = "rdsh-gateway";
|
|
19
|
-
export { join, registerJoin, detectInsecure, selfRevoke } from "./join.js";
|
|
19
|
+
export { join, startJoin, registerJoin, detectInsecure, selfRevoke } from "./join.js";
|
|
20
20
|
export { readPersistedToken, clearPersistedToken } from "./token-store.js";
|
|
21
|
+
export { acquireJoinLock, releaseJoinLock, readJoinLock, JOIN_LOCK_PATH } from "./lock.js";
|
|
21
22
|
//# sourceMappingURL=index.js.map
|
package/dist/join.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ProxyTarget } from "./proxy.ts";
|
|
2
|
+
import type { JoinLockRole } from "./lock.ts";
|
|
1
3
|
export interface JoinOptions {
|
|
2
4
|
hubUrl: string;
|
|
3
5
|
/** join token(用户级,register 换 host token) */
|
|
@@ -15,10 +17,44 @@ export interface RegisterOutcome {
|
|
|
15
17
|
token: string;
|
|
16
18
|
insecure: boolean;
|
|
17
19
|
}
|
|
20
|
+
/** 隧道状态机(onState 事件值)。 */
|
|
21
|
+
export type JoinState = "connecting" | "connected" | "reconnecting" | "rejected" | "stopped";
|
|
22
|
+
/** join 核心事件钩子(插件面板实时状态 + 日志预留)。 */
|
|
23
|
+
export interface JoinHooks {
|
|
24
|
+
onState?(state: JoinState, detail?: {
|
|
25
|
+
message?: string;
|
|
26
|
+
delayMs?: number;
|
|
27
|
+
}): void;
|
|
28
|
+
onLog?(level: "info" | "warn" | "error", message: string): void;
|
|
29
|
+
}
|
|
30
|
+
/** no-spawn、外部 target 的 join 隧道启动参数(CLI 与插件共用)。 */
|
|
31
|
+
export interface StartJoinOptions {
|
|
32
|
+
hubUrl: string;
|
|
33
|
+
/** 已解析的 host token(registerJoin 结果) */
|
|
34
|
+
token: string;
|
|
35
|
+
insecure: boolean;
|
|
36
|
+
/** 转发目标(no-spawn:外部 dsh 的 loopback 地址) */
|
|
37
|
+
target: ProxyTarget;
|
|
38
|
+
/** pid 锁 role:cli / plugin */
|
|
39
|
+
role: JoinLockRole;
|
|
40
|
+
/** 锁文件路径(缺省 ~/.rdsh/join.lock;测试可注入临时路径) */
|
|
41
|
+
lockPath?: string;
|
|
42
|
+
hooks?: JoinHooks;
|
|
43
|
+
}
|
|
44
|
+
/** 可停止的 join 隧道句柄。 */
|
|
45
|
+
export interface JoinHandle {
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
}
|
|
18
48
|
/** 探测 hub 是否需 insecure:以严格校验握手一次;证书错误 → true(需 insecure)。 */
|
|
19
49
|
export declare function detectInsecure(hubUrl: string): Promise<boolean>;
|
|
20
50
|
/** 调用 hub self-revoke 注销本机(host 持自己的 host token)。`rdsh host leave` 使用。 */
|
|
21
51
|
export declare function selfRevoke(hubUrl: string, token: string, insecure: boolean): Promise<void>;
|
|
22
52
|
/** 解析 host token(--token 注册 > 持久化复用)+ 自动检测证书;供 CLI 配置命令与 join() 复用。 */
|
|
23
53
|
export declare function registerJoin(opts: JoinOptions): Promise<RegisterOutcome>;
|
|
54
|
+
/**
|
|
55
|
+
* 启动 join 隧道(no-spawn):转发到外部 `opts.target`,不 spawn dsh、不 process.exit。
|
|
56
|
+
* 获取 pid 锁(opts.role);返回 `JoinHandle`,`stop()` 干净停止(关 WS/清 heartbeat/释放锁)。
|
|
57
|
+
*/
|
|
58
|
+
export declare function startJoin(opts: StartJoinOptions): JoinHandle;
|
|
59
|
+
/** `rdsh host serve`(join 模式)的 CLI 封装:spawn dsh + 信号退出 + startJoin(role:cli)。 */
|
|
24
60
|
export declare function join(opts: JoinOptions): Promise<void>;
|
package/dist/join.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* → 帧循环(OPEN http/ws → 本地 dsh 转发 → 响应帧回传)→ 断线指数退避重连。
|
|
6
6
|
*
|
|
7
7
|
* 安全:只出站(不监听任何入站端口);hub 认证在层 1,gateway 侧只认隧道内来源。
|
|
8
|
+
*
|
|
9
|
+
* 06-dsh-plugin 重构(D13 钩子落地):`join()`(CLI 形态,spawn dsh + 信号退出)
|
|
10
|
+
* 拆出 `startJoin()`(no-spawn、外部 target、可停止、onState/onLog)——插件复用。
|
|
8
11
|
*/
|
|
9
12
|
import { request as httpRequest } from "node:http";
|
|
10
13
|
import { request as httpsRequest } from "node:https";
|
|
@@ -13,6 +16,7 @@ import { FrameParser, FRAME_TYPE, encodeFrame, jsonPayload, parseJsonPayload } f
|
|
|
13
16
|
import { findDsh, spawnDsh } from "./spawn-dsh.js";
|
|
14
17
|
import { rewriteHeadersForDsh } from "./proxy.js";
|
|
15
18
|
import { clearPersistedToken, persistToken, readPersistedToken } from "./token-store.js";
|
|
19
|
+
import { acquireJoinLock, releaseJoinLock } from "./lock.js";
|
|
16
20
|
/** 判断错误是否为 TLS 证书类错误(自签/过期/域名不匹配)。 */
|
|
17
21
|
function isCertError(err) {
|
|
18
22
|
const code = err?.code ?? "";
|
|
@@ -111,19 +115,23 @@ async function register(hubUrl, joinToken, name, insecure) {
|
|
|
111
115
|
}
|
|
112
116
|
return { hostId: b.hostId, hostToken: b.hostToken };
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
/**
|
|
119
|
+
* 启动 join 隧道(no-spawn):转发到外部 `opts.target`,不 spawn dsh、不 process.exit。
|
|
120
|
+
* 获取 pid 锁(opts.role);返回 `JoinHandle`,`stop()` 干净停止(关 WS/清 heartbeat/释放锁)。
|
|
121
|
+
*/
|
|
122
|
+
export function startJoin(opts) {
|
|
115
123
|
const hubWsBase = opts.hubUrl.replace(/^https/, "wss").replace(/^http/, "ws");
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
const hooks = opts.hooks ?? {};
|
|
125
|
+
const log = (level, message) => {
|
|
126
|
+
hooks.onLog?.(level, message);
|
|
127
|
+
};
|
|
128
|
+
const setState = (state, detail) => {
|
|
129
|
+
hooks.onState?.(state, detail);
|
|
130
|
+
};
|
|
131
|
+
const lock = acquireJoinLock(opts.role, opts.lockPath);
|
|
132
|
+
if (!lock.ok) {
|
|
133
|
+
throw new Error(`join lock held by ${lock.heldBy.role} (pid ${lock.heldBy.pid}); stop it first`);
|
|
119
134
|
}
|
|
120
|
-
const dsh = await spawnDsh(foundDsh);
|
|
121
|
-
const target = { host: "127.0.0.1", port: dsh.port };
|
|
122
|
-
// 解析 host token(含证书自动检测 + 持久化);进程重启后复用,避免重复配对。
|
|
123
|
-
const { token: initialToken, insecure } = await registerJoin(opts);
|
|
124
|
-
let token = initialToken;
|
|
125
|
-
console.log(`rdsh join: dsh web on 127.0.0.1:${dsh.port}`);
|
|
126
|
-
console.log(`rdsh join: connecting to ${opts.hubUrl}...`);
|
|
127
135
|
const parser = new FrameParser();
|
|
128
136
|
/** http 流:streamId → 本地请求(写请求体 / 结束)。 */
|
|
129
137
|
const httpStreams = new Map();
|
|
@@ -132,23 +140,7 @@ export async function join(opts) {
|
|
|
132
140
|
let shuttingDown = false;
|
|
133
141
|
let reconnectDelay = RECONNECT_BASE_MS;
|
|
134
142
|
let heartbeat;
|
|
135
|
-
|
|
136
|
-
if (shuttingDown)
|
|
137
|
-
return;
|
|
138
|
-
shuttingDown = true;
|
|
139
|
-
if (signal !== "")
|
|
140
|
-
console.log(`\nrdsh: received ${signal}, shutting down...`);
|
|
141
|
-
if (heartbeat !== undefined)
|
|
142
|
-
clearInterval(heartbeat);
|
|
143
|
-
await dsh.stop();
|
|
144
|
-
process.exit(code);
|
|
145
|
-
};
|
|
146
|
-
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
147
|
-
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
148
|
-
process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
149
|
-
// 常驻:进程靠信号退出(shutdown 里 process.exit);防止函数返回后
|
|
150
|
-
// CLI 的 main().then(exit) 误退出服务进程
|
|
151
|
-
const keepAlive = new Promise(() => { });
|
|
143
|
+
let currentClient;
|
|
152
144
|
function handleFrame(frame, client) {
|
|
153
145
|
switch (frame.type) {
|
|
154
146
|
case FRAME_TYPE.PING: {
|
|
@@ -237,11 +229,11 @@ export async function join(opts) {
|
|
|
237
229
|
const streamId = frame.streamId;
|
|
238
230
|
// http 转发:本地 dsh(loopback http)
|
|
239
231
|
const up = httpRequest({
|
|
240
|
-
host: target.host,
|
|
241
|
-
port: target.port,
|
|
232
|
+
host: opts.target.host,
|
|
233
|
+
port: opts.target.port,
|
|
242
234
|
path,
|
|
243
235
|
method,
|
|
244
|
-
headers: rewriteHeadersForDsh(headers, target),
|
|
236
|
+
headers: rewriteHeadersForDsh(headers, opts.target),
|
|
245
237
|
}, (upRes) => {
|
|
246
238
|
client.send(encodeFrame(FRAME_TYPE.OPEN, streamId, jsonPayload({
|
|
247
239
|
kind: "http",
|
|
@@ -277,8 +269,8 @@ export async function join(opts) {
|
|
|
277
269
|
}
|
|
278
270
|
function openWsStream(frame, client, path, headers) {
|
|
279
271
|
const streamId = frame.streamId;
|
|
280
|
-
const upstream = new WebSocket(`ws://${target.host}:${target.port}${path}`, {
|
|
281
|
-
headers: rewriteHeadersForDsh(headers, target),
|
|
272
|
+
const upstream = new WebSocket(`ws://${opts.target.host}:${opts.target.port}${path}`, {
|
|
273
|
+
headers: rewriteHeadersForDsh(headers, opts.target),
|
|
282
274
|
});
|
|
283
275
|
const queue = [];
|
|
284
276
|
wsStreams.set(streamId, { upstream, queue });
|
|
@@ -306,11 +298,38 @@ export async function join(opts) {
|
|
|
306
298
|
upstream.on("close", cleanup);
|
|
307
299
|
upstream.on("error", cleanup);
|
|
308
300
|
}
|
|
301
|
+
/** 清空本地 http/ws 流 + heartbeat(断线/停止时)。 */
|
|
302
|
+
function cleanupStreams() {
|
|
303
|
+
if (heartbeat !== undefined) {
|
|
304
|
+
clearInterval(heartbeat);
|
|
305
|
+
heartbeat = undefined;
|
|
306
|
+
}
|
|
307
|
+
for (const s of httpStreams.values()) {
|
|
308
|
+
try {
|
|
309
|
+
s.up.destroy();
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
/* 已断 */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
httpStreams.clear();
|
|
316
|
+
for (const s of wsStreams.values()) {
|
|
317
|
+
try {
|
|
318
|
+
s.upstream.terminate();
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
/* 已断 */
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
wsStreams.clear();
|
|
325
|
+
}
|
|
309
326
|
function connect() {
|
|
310
327
|
if (shuttingDown)
|
|
311
328
|
return;
|
|
312
|
-
const url = `${hubWsBase}/tunnel?token=${encodeURIComponent(token)}`;
|
|
313
|
-
const client = new WebSocket(url, { rejectUnauthorized: !insecure });
|
|
329
|
+
const url = `${hubWsBase}/tunnel?token=${encodeURIComponent(opts.token)}`;
|
|
330
|
+
const client = new WebSocket(url, { rejectUnauthorized: !opts.insecure });
|
|
331
|
+
currentClient = client;
|
|
332
|
+
setState("connecting", { message: `connecting to ${opts.hubUrl}` });
|
|
314
333
|
// 401/403 = token 被拒(吊销/不存在)。监听此事件后 ws 不再自动 abort,
|
|
315
334
|
// 需手动 terminate → 触发 close → 决定「重配对」还是「普通重连」。
|
|
316
335
|
let tokenRejected = false;
|
|
@@ -326,7 +345,8 @@ export async function join(opts) {
|
|
|
326
345
|
});
|
|
327
346
|
client.on("open", () => {
|
|
328
347
|
reconnectDelay = RECONNECT_BASE_MS;
|
|
329
|
-
|
|
348
|
+
setState("connected");
|
|
349
|
+
log("info", "tunnel established (heartbeat 30s)");
|
|
330
350
|
if (heartbeat !== undefined)
|
|
331
351
|
clearInterval(heartbeat);
|
|
332
352
|
heartbeat = setInterval(() => {
|
|
@@ -355,39 +375,22 @@ export async function join(opts) {
|
|
|
355
375
|
handleFrame(frame, client);
|
|
356
376
|
});
|
|
357
377
|
client.on("close", () => {
|
|
358
|
-
|
|
359
|
-
clearInterval(heartbeat);
|
|
360
|
-
heartbeat = undefined;
|
|
361
|
-
}
|
|
362
|
-
for (const s of httpStreams.values()) {
|
|
363
|
-
try {
|
|
364
|
-
s.up.destroy();
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
/* 已断 */
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
httpStreams.clear();
|
|
371
|
-
for (const s of wsStreams.values()) {
|
|
372
|
-
try {
|
|
373
|
-
s.upstream.terminate();
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
/* 已断 */
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
wsStreams.clear();
|
|
378
|
+
cleanupStreams();
|
|
380
379
|
if (shuttingDown)
|
|
381
380
|
return;
|
|
382
381
|
if (tokenRejected) {
|
|
383
|
-
// token 被拒(吊销/删除)=
|
|
384
|
-
// → 删旧 session + fail-fast
|
|
382
|
+
// token 被拒(吊销/删除)= 永久失败,无法自动恢复
|
|
383
|
+
// → 删旧 session + 释放锁 + 停(fail-fast),不重连。
|
|
385
384
|
clearPersistedToken(opts.hubUrl);
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
const msg = "host token rejected by hub (revoked or removed); re-join with a new join token";
|
|
386
|
+
log("error", msg);
|
|
387
|
+
setState("rejected", { message: msg });
|
|
388
|
+
shuttingDown = true;
|
|
389
|
+
releaseJoinLock(opts.lockPath);
|
|
388
390
|
return;
|
|
389
391
|
}
|
|
390
|
-
|
|
392
|
+
setState("reconnecting", { delayMs: reconnectDelay });
|
|
393
|
+
log("info", `tunnel lost — reconnecting in ${Math.round(reconnectDelay / 1000)}s...`);
|
|
391
394
|
setTimeout(connect, reconnectDelay + Math.random() * 500);
|
|
392
395
|
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
393
396
|
});
|
|
@@ -401,7 +404,71 @@ export async function join(opts) {
|
|
|
401
404
|
});
|
|
402
405
|
}
|
|
403
406
|
connect();
|
|
404
|
-
|
|
407
|
+
return {
|
|
408
|
+
async stop() {
|
|
409
|
+
if (shuttingDown)
|
|
410
|
+
return;
|
|
411
|
+
shuttingDown = true;
|
|
412
|
+
cleanupStreams();
|
|
413
|
+
if (currentClient !== undefined) {
|
|
414
|
+
try {
|
|
415
|
+
currentClient.terminate();
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
/* 已关闭 */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
releaseJoinLock(opts.lockPath);
|
|
422
|
+
setState("stopped");
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** `rdsh host serve`(join 模式)的 CLI 封装:spawn dsh + 信号退出 + startJoin(role:cli)。 */
|
|
427
|
+
export async function join(opts) {
|
|
428
|
+
const foundDsh = findDsh(opts.dshPath);
|
|
429
|
+
if (foundDsh === null) {
|
|
430
|
+
throw new Error("cannot find 'dsh' in PATH. Install DeepSeek Harness first, or pass --dsh <path>.");
|
|
431
|
+
}
|
|
432
|
+
const dsh = await spawnDsh(foundDsh);
|
|
433
|
+
const target = { host: "127.0.0.1", port: dsh.port };
|
|
434
|
+
// 解析 host token(含证书自动检测 + 持久化);进程重启后复用,避免重复配对。
|
|
435
|
+
const { token, insecure } = await registerJoin(opts);
|
|
436
|
+
console.log(`rdsh join: dsh web on 127.0.0.1:${dsh.port}`);
|
|
437
|
+
console.log(`rdsh join: connecting to ${opts.hubUrl}...`);
|
|
438
|
+
const handle = startJoin({
|
|
439
|
+
hubUrl: opts.hubUrl,
|
|
440
|
+
token,
|
|
441
|
+
insecure,
|
|
442
|
+
target,
|
|
443
|
+
role: "cli",
|
|
444
|
+
hooks: {
|
|
445
|
+
onLog: (level, message) => {
|
|
446
|
+
(level === "error" ? console.error : console.log)(`rdsh join: ${message}`);
|
|
447
|
+
},
|
|
448
|
+
onState: (state, detail) => {
|
|
449
|
+
if (state === "rejected") {
|
|
450
|
+
console.error(`rdsh join: ${detail?.message ?? "rejected"}`);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
let shuttingDown = false;
|
|
456
|
+
const shutdown = async (signal, code = 0) => {
|
|
457
|
+
if (shuttingDown)
|
|
458
|
+
return;
|
|
459
|
+
shuttingDown = true;
|
|
460
|
+
if (signal !== "")
|
|
461
|
+
console.log(`\nrdsh: received ${signal}, shutting down...`);
|
|
462
|
+
await handle.stop();
|
|
463
|
+
await dsh.stop();
|
|
464
|
+
process.exit(code);
|
|
465
|
+
};
|
|
466
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
467
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
468
|
+
process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
469
|
+
// 常驻:进程靠信号退出(shutdown 里 process.exit);防止函数返回后
|
|
470
|
+
// CLI 的 main().then(exit) 误退出服务进程
|
|
471
|
+
await new Promise(() => { });
|
|
405
472
|
}
|
|
406
473
|
function normalizeRespHeaders(headers) {
|
|
407
474
|
const out = {};
|
package/dist/lock.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const JOIN_LOCK_PATH: string;
|
|
2
|
+
export type JoinLockRole = "cli" | "plugin";
|
|
3
|
+
export interface JoinLock {
|
|
4
|
+
pid: number;
|
|
5
|
+
role: JoinLockRole;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 读锁。不存在/损坏/过短 → null;pid 已死(stale)→ 清除文件并返回 null。
|
|
9
|
+
* 供面板「外部托管」态判定(role=cli)与 acquire 前检测复用。
|
|
10
|
+
*/
|
|
11
|
+
export declare function readJoinLock(path?: string): JoinLock | null;
|
|
12
|
+
export type AcquireResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
heldBy: JoinLock;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 获取锁:写入 `{pid: process.pid, role}`(目录 0700、文件 0600)。
|
|
20
|
+
* 已有**他人**(不同 pid)持有的活锁 → 拒绝(防同机双隧道)。
|
|
21
|
+
*/
|
|
22
|
+
export declare function acquireJoinLock(role: JoinLockRole, path?: string): AcquireResult;
|
|
23
|
+
/** 释放锁:仅当锁是自己 pid 持有才删(不误删他人锁)。 */
|
|
24
|
+
export declare function releaseJoinLock(path?: string): void;
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lock.ts — join pid 锁:CLI 与插件共享「单身份铁律」(同机单隧道,D5 档2)。
|
|
3
|
+
*
|
|
4
|
+
* 锁文件 `~/.rdsh/join.lock`(0600)记 `{pid, role}`;stale(pid 已死)自动视为无锁并清除。
|
|
5
|
+
* role = "cli"(rdsh CLI / systemd 服务持有)| "plugin"(dsh 插件持有)——面板据此显示「外部托管」。
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
const RDSH_DIR = join(homedir(), ".rdsh");
|
|
11
|
+
export const JOIN_LOCK_PATH = join(RDSH_DIR, "join.lock");
|
|
12
|
+
/** 探测 pid 是否存活:signal 0 不真正发信号;EPERM = 存活但无权限,ESRCH = 不存在。 */
|
|
13
|
+
function isAlive(pid) {
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return err.code === "EPERM";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 读锁。不存在/损坏/过短 → null;pid 已死(stale)→ 清除文件并返回 null。
|
|
24
|
+
* 供面板「外部托管」态判定(role=cli)与 acquire 前检测复用。
|
|
25
|
+
*/
|
|
26
|
+
export function readJoinLock(path = JOIN_LOCK_PATH) {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return null;
|
|
30
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
if (typeof raw.pid !== "number" || (raw.role !== "cli" && raw.role !== "plugin"))
|
|
32
|
+
return null;
|
|
33
|
+
if (!isAlive(raw.pid)) {
|
|
34
|
+
try {
|
|
35
|
+
rmSync(path, { force: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* 忽略 */
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { pid: raw.pid, role: raw.role };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 获取锁:写入 `{pid: process.pid, role}`(目录 0700、文件 0600)。
|
|
50
|
+
* 已有**他人**(不同 pid)持有的活锁 → 拒绝(防同机双隧道)。
|
|
51
|
+
*/
|
|
52
|
+
export function acquireJoinLock(role, path = JOIN_LOCK_PATH) {
|
|
53
|
+
const held = readJoinLock(path);
|
|
54
|
+
if (held !== null && held.pid !== process.pid) {
|
|
55
|
+
return { ok: false, heldBy: held };
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
58
|
+
writeFileSync(path, JSON.stringify({ pid: process.pid, role }), { mode: 0o600 });
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
/** 释放锁:仅当锁是自己 pid 持有才删(不误删他人锁)。 */
|
|
62
|
+
export function releaseJoinLock(path = JOIN_LOCK_PATH) {
|
|
63
|
+
try {
|
|
64
|
+
const held = readJoinLock(path);
|
|
65
|
+
if (held !== null && held.pid === process.pid) {
|
|
66
|
+
rmSync(path, { force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* 忽略 */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=lock.js.map
|