@luckychen1993/pm-mcp 0.1.3 → 0.1.4
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.md +8 -6
- package/dist/audit-tools.js +8 -7
- package/dist/audit.js +10 -5
- package/dist/idempotency.js +201 -67
- package/dist/index-store.js +163 -94
- package/dist/index.js +22 -17
- package/dist/license.js +7 -4
- package/dist/paths.js +35 -0
- package/dist/search.js +2 -2
- package/dist/security.js +15 -6
- package/dist/setup.js +1 -1
- package/dist/store.js +175 -58
- package/dist/tool-base.js +4 -4
- package/dist/watcher-coordinator.js +182 -0
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
5
|
import { DebugLogFileSchema, FeaturesFileSchema, FileNotesFileSchema, now, ProjectSchema, RoadmapFileSchema, SecurityFileSchema, SessionsFileSchema, TasksFileSchema, } from "./types.js";
|
|
5
6
|
import { DEBUGLOG_JSON, EXTRA_RULES_FILE, FEATURES_JSON, FILE_NOTES_JSON, PROJECT_JSON, ROADMAP_JSON, SECURITY_JSON, SESSIONS_JSON, TASKS_JSON, pmPath, } from "./paths.js";
|
|
6
7
|
import { ExtraRulesFileSchema } from "./types.js";
|
|
@@ -38,92 +39,208 @@ export function atomicWrite(file, content) {
|
|
|
38
39
|
export function saveJson(file, data) {
|
|
39
40
|
atomicWrite(file, JSON.stringify(data, null, 2) + "\n");
|
|
40
41
|
}
|
|
42
|
+
const FILE_LOCK_TIMEOUT_MS = 3_000;
|
|
43
|
+
const FILE_LOCK_STALE_MS = 10_000;
|
|
44
|
+
const FILE_LOCK_POLL_MS = 20;
|
|
45
|
+
const FILE_LOCK_PROTOCOL = "sqlite-guard-v1";
|
|
46
|
+
const FILE_LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
47
|
+
function readFileLockOwner(lockFile) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = fs.readFileSync(lockFile, "utf8");
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === "ENOENT")
|
|
54
|
+
return null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (!Number.isSafeInteger(parsed.pid) || parsed.pid <= 0)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
pid: parsed.pid,
|
|
63
|
+
...(typeof parsed.token === "string" && parsed.token.length > 0 ? { token: parsed.token } : {}),
|
|
64
|
+
...(typeof parsed.created_at === "string" && parsed.created_at.length > 0 ? { created_at: parsed.created_at } : {}),
|
|
65
|
+
...(typeof parsed.protocol === "string" && parsed.protocol.length > 0 ? { protocol: parsed.protocol } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
const pid = Number(raw.trim());
|
|
70
|
+
return Number.isSafeInteger(pid) && pid > 0 ? { pid } : null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function fileLockOwnerAlive(pid) {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// EPERM proves that the process exists even when signalling it is forbidden.
|
|
80
|
+
return error.code === "EPERM";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function sqliteBusy(error) {
|
|
84
|
+
const candidate = error;
|
|
85
|
+
return candidate.errcode === 5 ||
|
|
86
|
+
(typeof candidate.message === "string" && /database is (?:locked|busy)/i.test(candidate.message));
|
|
87
|
+
}
|
|
88
|
+
function rollbackRecoveryGuard(db) {
|
|
89
|
+
if (!db.isTransaction)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
db.exec("ROLLBACK");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// close() below is the final fail-safe and also rolls an open transaction back.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
41
98
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
99
|
+
* 通用跨进程文件锁。SQLite 写事务覆盖 .lock 的检查、接管、创建、调用方临界区和
|
|
100
|
+
* 释放,从而消除 check-then-rm ABA,并在进程崩溃时由操作系统释放真实所有权。
|
|
101
|
+
* 成功取得同一 guard 后,带当前 protocol 的残留 .lock 可立即核销,即使 PID 已复用;
|
|
102
|
+
* 无 protocol 的旧版锁仍须「超过 staleMs 且 PID 已死」才接管,以兼容滚动升级。
|
|
103
|
+
* 释放必须完整匹配 protocol、PID、token 与 created_at,否则保留并 fail-closed。
|
|
104
|
+
* 共享同一 guardFile 的不同 lockFile 会串行,调用方应只在短元数据事务中共享 guard。
|
|
46
105
|
*/
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
49
|
-
|
|
106
|
+
export function withFileRecoveryLock(lockFile, fn, options = {}) {
|
|
107
|
+
const timeoutMs = options.timeoutMs ?? FILE_LOCK_TIMEOUT_MS;
|
|
108
|
+
const staleMs = options.staleMs ?? FILE_LOCK_STALE_MS;
|
|
109
|
+
const guardFile = options.guardFile ?? path.join(path.dirname(lockFile), ".file-recovery-guard.db");
|
|
110
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isFinite(staleMs) || staleMs < 0) {
|
|
111
|
+
throw new RangeError("文件锁 timeoutMs 必须大于 0,staleMs 不得小于 0。");
|
|
112
|
+
}
|
|
113
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
114
|
+
fs.mkdirSync(path.dirname(guardFile), { recursive: true });
|
|
50
115
|
const token = randomUUID();
|
|
51
|
-
const
|
|
52
|
-
const
|
|
116
|
+
const createdAt = new Date().toISOString();
|
|
117
|
+
const payload = JSON.stringify({ pid: process.pid, token, created_at: createdAt, protocol: FILE_LOCK_PROTOCOL });
|
|
118
|
+
const deadline = Date.now() + timeoutMs;
|
|
119
|
+
let fileLockHeld = false;
|
|
120
|
+
const acquireFileLock = () => {
|
|
53
121
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const parsed = JSON.parse(raw);
|
|
57
|
-
return typeof parsed.pid === "number"
|
|
58
|
-
? { pid: parsed.pid, ...(typeof parsed.token === "string" ? { token: parsed.token } : {}) }
|
|
59
|
-
: null;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
const pid = Number(raw.trim());
|
|
63
|
-
return Number.isInteger(pid) && pid > 0 ? { pid } : null;
|
|
64
|
-
}
|
|
122
|
+
fs.writeFileSync(lockFile, payload, { flag: "wx" });
|
|
123
|
+
return true;
|
|
65
124
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== "EEXIST")
|
|
127
|
+
throw error;
|
|
68
128
|
}
|
|
69
|
-
|
|
70
|
-
const alive = (pid) => {
|
|
129
|
+
let stat;
|
|
71
130
|
try {
|
|
72
|
-
|
|
73
|
-
return true;
|
|
131
|
+
stat = fs.statSync(lockFile);
|
|
74
132
|
}
|
|
75
133
|
catch (error) {
|
|
76
|
-
|
|
134
|
+
if (error.code === "ENOENT")
|
|
135
|
+
return false;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
const current = readFileLockOwner(lockFile);
|
|
139
|
+
const guardedResidual = current?.protocol === FILE_LOCK_PROTOCOL;
|
|
140
|
+
if (!guardedResidual && (Date.now() - stat.mtimeMs <= staleMs || (current && fileLockOwnerAlive(current.pid)))) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
// Every current-version acquirer and releaser holds the SQLite write transaction
|
|
144
|
+
// here, so no compliant peer can replace .lock between this removal and creation.
|
|
145
|
+
try {
|
|
146
|
+
fs.rmSync(lockFile);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code === "ENOENT")
|
|
150
|
+
return false;
|
|
151
|
+
throw error;
|
|
77
152
|
}
|
|
78
|
-
};
|
|
79
|
-
const acquire = () => {
|
|
80
153
|
try {
|
|
81
154
|
fs.writeFileSync(lockFile, payload, { flag: "wx" });
|
|
82
155
|
return true;
|
|
83
156
|
}
|
|
84
|
-
catch {
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code === "EEXIST")
|
|
159
|
+
return false;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const beginGuard = (db, until) => {
|
|
164
|
+
while (Date.now() < until) {
|
|
165
|
+
const remaining = until - Date.now();
|
|
166
|
+
if (remaining <= 0)
|
|
167
|
+
return false;
|
|
168
|
+
db.exec(`PRAGMA busy_timeout=${Math.max(1, Math.min(100, remaining))}`);
|
|
85
169
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (Date.now() - st.mtimeMs > 10_000 && (!current || !alive(current.pid))) {
|
|
89
|
-
fs.rmSync(lockFile, { force: true });
|
|
90
|
-
try {
|
|
91
|
-
fs.writeFileSync(lockFile, payload, { flag: "wx" });
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
170
|
+
db.exec("BEGIN IMMEDIATE");
|
|
171
|
+
return true;
|
|
98
172
|
}
|
|
99
|
-
catch {
|
|
100
|
-
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (!sqliteBusy(error))
|
|
175
|
+
throw error;
|
|
176
|
+
const pause = Math.min(FILE_LOCK_POLL_MS, Math.max(0, until - Date.now()));
|
|
177
|
+
if (pause > 0)
|
|
178
|
+
Atomics.wait(FILE_LOCK_WAIT, 0, 0, pause);
|
|
101
179
|
}
|
|
102
|
-
return false;
|
|
103
180
|
}
|
|
181
|
+
return false;
|
|
104
182
|
};
|
|
105
|
-
|
|
106
|
-
for (let i = 0; i < 150 && !held; i++) {
|
|
107
|
-
held = acquire();
|
|
108
|
-
if (!held)
|
|
109
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
110
|
-
}
|
|
111
|
-
if (!held) {
|
|
112
|
-
throw new Error("账本锁获取超时(3s):另一进程正在写入本项目,请稍后重试。");
|
|
113
|
-
}
|
|
183
|
+
const acquisitionGuard = new DatabaseSync(guardFile);
|
|
114
184
|
try {
|
|
115
|
-
|
|
185
|
+
while (!fileLockHeld && Date.now() < deadline) {
|
|
186
|
+
if (!beginGuard(acquisitionGuard, deadline))
|
|
187
|
+
break;
|
|
188
|
+
try {
|
|
189
|
+
fileLockHeld = acquireFileLock();
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
rollbackRecoveryGuard(acquisitionGuard);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
if (!fileLockHeld) {
|
|
196
|
+
rollbackRecoveryGuard(acquisitionGuard);
|
|
197
|
+
const pause = Math.min(FILE_LOCK_POLL_MS, Math.max(0, deadline - Date.now()));
|
|
198
|
+
if (pause > 0)
|
|
199
|
+
Atomics.wait(FILE_LOCK_WAIT, 0, 0, pause);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!fileLockHeld) {
|
|
203
|
+
throw new Error(options.timeoutMessage ?? "文件锁获取超时(3s),请稍后重试。");
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return fn();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
try {
|
|
210
|
+
const current = readFileLockOwner(lockFile);
|
|
211
|
+
if (current?.protocol === FILE_LOCK_PROTOCOL &&
|
|
212
|
+
current.pid === process.pid &&
|
|
213
|
+
current.token === token &&
|
|
214
|
+
current.created_at === createdAt) {
|
|
215
|
+
fs.rmSync(lockFile);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Ownership cannot be proven: leave the file for guarded recovery.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
116
222
|
}
|
|
117
223
|
finally {
|
|
224
|
+
rollbackRecoveryGuard(acquisitionGuard);
|
|
118
225
|
try {
|
|
119
|
-
|
|
120
|
-
fs.rmSync(lockFile, { force: true });
|
|
226
|
+
acquisitionGuard.close();
|
|
121
227
|
}
|
|
122
228
|
catch {
|
|
123
|
-
/*
|
|
229
|
+
/* close after rollback is best effort */
|
|
124
230
|
}
|
|
125
231
|
}
|
|
126
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* 账本专用互斥锁:把「读账本 → 改 → 写」整个事务串行化。活着的长任务不会被误抢,
|
|
235
|
+
* 等待上限 3s,超时明确报错而非静默丢写。
|
|
236
|
+
*/
|
|
237
|
+
export function withLedgerLock(root, fn) {
|
|
238
|
+
const runtimeDirectory = pmPath(root, ".runtime");
|
|
239
|
+
return withFileRecoveryLock(pmPath(root, ".lock"), fn, {
|
|
240
|
+
guardFile: path.join(runtimeDirectory, "ledger-lock.db"),
|
|
241
|
+
timeoutMessage: "账本锁获取超时(3s):另一进程正在写入本项目,请稍后重试。",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
127
244
|
export function loadJson(file, schema, fallback) {
|
|
128
245
|
if (!fs.existsSync(file))
|
|
129
246
|
return fallback;
|
package/dist/tool-base.js
CHANGED
|
@@ -20,12 +20,12 @@ function writeSchema(schema) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/** 同步写工具:业务幂等占位 + 跨进程账本锁覆盖完整读-改-写。 */
|
|
23
|
-
export function toolW(server, root, name, description, schema, fn) {
|
|
23
|
+
export function toolW(server, root, name, description, schema, fn, prepare) {
|
|
24
24
|
server.registerTool(name, { description: `${description} ${idempotencyDescription}`, inputSchema: writeSchema(schema), annotations: { readOnlyHint: false } }, ((args) => {
|
|
25
25
|
try {
|
|
26
|
-
const result = runIdempotentWriteSync(root, name, args, fn);
|
|
26
|
+
const result = runIdempotentWriteSync(root, name, args, fn, prepare);
|
|
27
27
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
28
|
-
return { content: [{ type: "text", text: prefix + result.text }] };
|
|
28
|
+
return { content: [{ type: "text", text: prefix + result.text }], ...(result.pending ? { isError: true } : {}) };
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
|
|
@@ -38,7 +38,7 @@ export function toolI(server, root, name, description, schema, fn) {
|
|
|
38
38
|
try {
|
|
39
39
|
const result = await runIdempotentWrite(root, name, args, fn);
|
|
40
40
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
41
|
-
return { content: [{ type: "text", text: prefix + result.text }] };
|
|
41
|
+
return { content: [{ type: "text", text: prefix + result.text }], ...(result.pending ? { isError: true } : {}) };
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
44
|
return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
const ELECTION_POLL_MS = 1_000;
|
|
5
|
+
const ELECTION_BUSY_MS = 20;
|
|
6
|
+
export function watcherDirtyFile(root) {
|
|
7
|
+
return path.join(path.resolve(root), ".pm", ".runtime", "watcher-dirty");
|
|
8
|
+
}
|
|
9
|
+
export function markWatcherDirty(root) {
|
|
10
|
+
const file = watcherDirtyFile(root);
|
|
11
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
12
|
+
fs.writeFileSync(file, `${new Date().toISOString()}\n`, "utf8");
|
|
13
|
+
}
|
|
14
|
+
export function clearWatcherDirty(root) {
|
|
15
|
+
try {
|
|
16
|
+
fs.rmSync(watcherDirtyFile(root));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== "ENOENT")
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function watcherIsClean(root) {
|
|
24
|
+
return !fs.existsSync(watcherDirtyFile(root));
|
|
25
|
+
}
|
|
26
|
+
function sqliteBusy(error) {
|
|
27
|
+
const candidate = error;
|
|
28
|
+
return candidate.errcode === 5 ||
|
|
29
|
+
(typeof candidate.message === "string" && /database is (?:locked|busy)/i.test(candidate.message));
|
|
30
|
+
}
|
|
31
|
+
function openElection(root) {
|
|
32
|
+
const file = path.join(path.resolve(root), ".pm", ".runtime", "watcher-leader.db");
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
const db = new DatabaseSync(file);
|
|
35
|
+
try {
|
|
36
|
+
db.exec(`PRAGMA busy_timeout=${ELECTION_BUSY_MS}`);
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
try {
|
|
41
|
+
db.close();
|
|
42
|
+
}
|
|
43
|
+
catch { /* best effort */ }
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function acquire(db) {
|
|
48
|
+
if (db.isTransaction)
|
|
49
|
+
return true;
|
|
50
|
+
try {
|
|
51
|
+
// Avoid startup DDL so late standbys can join while a leader holds the DB.
|
|
52
|
+
// The transactional header write forces ownership even when an empty DB
|
|
53
|
+
// would otherwise defer materializing the write reservation on this OS.
|
|
54
|
+
db.exec("BEGIN IMMEDIATE");
|
|
55
|
+
db.exec("PRAGMA user_version=1");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (db.isTransaction) {
|
|
60
|
+
try {
|
|
61
|
+
db.exec("ROLLBACK");
|
|
62
|
+
}
|
|
63
|
+
catch { /* close remains the fail-safe */ }
|
|
64
|
+
}
|
|
65
|
+
if (sqliteBusy(error))
|
|
66
|
+
return false;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function release(db) {
|
|
71
|
+
if (db.isTransaction)
|
|
72
|
+
db.exec("ROLLBACK");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Elect exactly one watcher with a dedicated SQLite write transaction held for
|
|
76
|
+
* its lifetime. The OS releases the transaction after a force kill; standbys
|
|
77
|
+
* poll and take over without PID guesses, leases, or stale-file deletion.
|
|
78
|
+
*/
|
|
79
|
+
export function coordinateWatcherLeader(root, activate, report) {
|
|
80
|
+
let election;
|
|
81
|
+
try {
|
|
82
|
+
election = openElection(root);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
report("leader election unavailable", error);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
let cleanup = null;
|
|
89
|
+
let timer = null;
|
|
90
|
+
let stopped = false;
|
|
91
|
+
let unavailable = false;
|
|
92
|
+
const relinquish = () => {
|
|
93
|
+
try {
|
|
94
|
+
cleanup?.();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
report("leader cleanup", error);
|
|
98
|
+
}
|
|
99
|
+
cleanup = null;
|
|
100
|
+
try {
|
|
101
|
+
release(election);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
report("leadership release", error);
|
|
105
|
+
unavailable = true;
|
|
106
|
+
try {
|
|
107
|
+
election.close();
|
|
108
|
+
}
|
|
109
|
+
catch { /* OS close releases the transaction */ }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const elect = () => {
|
|
113
|
+
if (stopped || unavailable || cleanup)
|
|
114
|
+
return;
|
|
115
|
+
let acquired = false;
|
|
116
|
+
try {
|
|
117
|
+
acquired = acquire(election);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
report("leader election", error);
|
|
121
|
+
unavailable = true;
|
|
122
|
+
stopped = true;
|
|
123
|
+
if (timer)
|
|
124
|
+
clearInterval(timer);
|
|
125
|
+
timer = null;
|
|
126
|
+
try {
|
|
127
|
+
election.close();
|
|
128
|
+
}
|
|
129
|
+
catch { /* best effort */ }
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!acquired)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
const activated = activate();
|
|
136
|
+
if (!activated)
|
|
137
|
+
unavailable = true;
|
|
138
|
+
else
|
|
139
|
+
cleanup = activated;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
report("leader activation", error);
|
|
143
|
+
}
|
|
144
|
+
if (!cleanup)
|
|
145
|
+
relinquish();
|
|
146
|
+
if (unavailable) {
|
|
147
|
+
if (timer)
|
|
148
|
+
clearInterval(timer);
|
|
149
|
+
timer = null;
|
|
150
|
+
stopped = true;
|
|
151
|
+
try {
|
|
152
|
+
election.close();
|
|
153
|
+
}
|
|
154
|
+
catch { /* best effort */ }
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
elect();
|
|
158
|
+
if (unavailable) {
|
|
159
|
+
try {
|
|
160
|
+
election.close();
|
|
161
|
+
}
|
|
162
|
+
catch { /* best effort */ }
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
timer = setInterval(elect, ELECTION_POLL_MS);
|
|
166
|
+
timer.unref();
|
|
167
|
+
return {
|
|
168
|
+
stop() {
|
|
169
|
+
if (stopped)
|
|
170
|
+
return;
|
|
171
|
+
stopped = true;
|
|
172
|
+
if (timer)
|
|
173
|
+
clearInterval(timer);
|
|
174
|
+
timer = null;
|
|
175
|
+
relinquish();
|
|
176
|
+
try {
|
|
177
|
+
election.close();
|
|
178
|
+
}
|
|
179
|
+
catch { /* best effort */ }
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|