mcp-google-multi 6.0.0-alpha.76 → 6.0.0-alpha.77
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/fs-atomic.js +98 -12
- package/package.json +1 -1
package/dist/fs-atomic.js
CHANGED
|
@@ -10,6 +10,9 @@ const RENAME_RETRY_MS = 20;
|
|
|
10
10
|
const LOCK_LEASE_MS = 60_000;
|
|
11
11
|
// Breaking a lock takes microseconds, so an older breaker file was left by a crash.
|
|
12
12
|
const BREAKER_STALE_MS = 2_000;
|
|
13
|
+
// The no-hard-link fallback writes the lock in two syscalls (create, then the
|
|
14
|
+
// pid); a reader can catch it empty in between. See docs/internals.md.
|
|
15
|
+
const LOCK_WRITE_GRACE_MS = 2_000;
|
|
13
16
|
const PROCESS_START_MS = performance.timeOrigin;
|
|
14
17
|
const OWNER_NAME = /^(\d+)\.(\d+|host)\.(\d+)\.([0-9a-f]{8}|0)\.[0-9a-f]{12}\.owner$/;
|
|
15
18
|
function startOf(pid) {
|
|
@@ -63,11 +66,19 @@ function sameFile(a, b) {
|
|
|
63
66
|
function isEnoent(error) {
|
|
64
67
|
return error.code === 'ENOENT';
|
|
65
68
|
}
|
|
69
|
+
function bindingRecord(id) {
|
|
70
|
+
return `${id.dev}:${id.ino}:${id.mtimeNs}`;
|
|
71
|
+
}
|
|
72
|
+
function matchesBinding(content, id) {
|
|
73
|
+
const m = /^(\d+):(\d+):(\d+)$/.exec(content);
|
|
74
|
+
return m !== null && BigInt(m[1]) === id.dev && BigInt(m[2]) === id.ino && BigInt(m[3]) === id.mtimeNs;
|
|
75
|
+
}
|
|
66
76
|
// Undefined when the lock changed hands while it was being read.
|
|
67
77
|
function readHolder(lock) {
|
|
68
78
|
const before = fs.lstatSync(lock, { bigint: true });
|
|
69
79
|
const body = before.isFile() ? fs.readFileSync(lock, 'utf8') : '';
|
|
70
80
|
let owner;
|
|
81
|
+
let ownerPending = false;
|
|
71
82
|
if (before.isFile()) {
|
|
72
83
|
const dir = path.dirname(lock);
|
|
73
84
|
const prefix = `${path.basename(lock)}.`;
|
|
@@ -79,10 +90,12 @@ function readHolder(lock) {
|
|
|
79
90
|
continue;
|
|
80
91
|
const ownerPath = path.join(dir, name);
|
|
81
92
|
try {
|
|
82
|
-
|
|
93
|
+
const ownerStat = fs.lstatSync(ownerPath, { bigint: true });
|
|
94
|
+
if (sameFile(ownerStat, before) || matchesBinding(fs.readFileSync(ownerPath, 'utf8'), before)) {
|
|
83
95
|
owner = { path: ownerPath, ns: m[2], start: m[3], boot: m[4] };
|
|
84
96
|
break;
|
|
85
97
|
}
|
|
98
|
+
ownerPending = true;
|
|
86
99
|
}
|
|
87
100
|
catch (error) {
|
|
88
101
|
if (!isEnoent(error))
|
|
@@ -93,7 +106,7 @@ function readHolder(lock) {
|
|
|
93
106
|
const after = fs.lstatSync(lock, { bigint: true });
|
|
94
107
|
if (!sameFile(before, after))
|
|
95
108
|
return undefined;
|
|
96
|
-
return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner };
|
|
109
|
+
return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner, ownerPending };
|
|
97
110
|
}
|
|
98
111
|
// The owner file's random name tells two holders apart even when the second
|
|
99
112
|
// lock reuses the first one's freed inode.
|
|
@@ -115,10 +128,21 @@ function probeDead(pid) {
|
|
|
115
128
|
return false;
|
|
116
129
|
}
|
|
117
130
|
}
|
|
131
|
+
// Symmetric: a clock step back must not make a write-in-progress lock look
|
|
132
|
+
// permanently fresh (see docs/internals.md).
|
|
133
|
+
function withinWriteGrace(now, mtimeMs) {
|
|
134
|
+
return Math.abs(now - mtimeMs) < LOCK_WRITE_GRACE_MS;
|
|
135
|
+
}
|
|
118
136
|
function holderDead(holder, now) {
|
|
137
|
+
if (holder.body === '' && withinWriteGrace(now, holder.mtimeMs))
|
|
138
|
+
return false;
|
|
119
139
|
const pid = Number(holder.body);
|
|
120
140
|
if (!(Number.isSafeInteger(pid) && pid > 0))
|
|
121
141
|
return true;
|
|
142
|
+
// A same-pid owner file exists but is not yet bound (the fallback lock's two
|
|
143
|
+
// writes race a reader); give it the same grace as an empty lock.
|
|
144
|
+
if (!holder.owner && holder.ownerPending && withinWriteGrace(now, holder.mtimeMs))
|
|
145
|
+
return false;
|
|
122
146
|
const leaseOver = now - holder.mtimeMs >= LOCK_LEASE_MS;
|
|
123
147
|
const owner = holder.owner;
|
|
124
148
|
// No owner file: an older writer, whose namespace is unknown.
|
|
@@ -196,6 +220,53 @@ function releaseBreaker(breaker, token) {
|
|
|
196
220
|
throw error;
|
|
197
221
|
}
|
|
198
222
|
}
|
|
223
|
+
// No hard links here (Android/Termux denies link() in private storage, some
|
|
224
|
+
// filesystems lack it entirely): the lock is created directly with O_EXCL,
|
|
225
|
+
// carrying the bare pid a linked owner file would carry, and the owner file
|
|
226
|
+
// is bound to it by a recorded identity instead of a hard link. See docs/internals.md.
|
|
227
|
+
function takeFallbackLock(lock, ownerFile) {
|
|
228
|
+
const fd = fs.openSync(lock, 'wx', 0o600);
|
|
229
|
+
let acquired;
|
|
230
|
+
try {
|
|
231
|
+
fs.writeSync(fd, String(process.pid));
|
|
232
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
233
|
+
acquired = { dev: stat.dev, ino: stat.ino, mtimeNs: stat.mtimeNs };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
// The lock is left empty; remove it only if it is still the one we just
|
|
237
|
+
// created, then rethrow so the caller never retries a write failure.
|
|
238
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
239
|
+
fs.closeSync(fd);
|
|
240
|
+
try {
|
|
241
|
+
if (sameFile(fs.lstatSync(lock, { bigint: true }), stat))
|
|
242
|
+
fs.rmSync(lock, { force: true });
|
|
243
|
+
}
|
|
244
|
+
catch (cleanupError) {
|
|
245
|
+
if (!isEnoent(cleanupError))
|
|
246
|
+
throw cleanupError;
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
fs.closeSync(fd);
|
|
251
|
+
try {
|
|
252
|
+
fs.writeFileSync(ownerFile, bindingRecord(acquired));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// The lock already exists holding our live pid; without this cleanup
|
|
256
|
+
// every other writer sees an ownerless holder and waits out the full lease.
|
|
257
|
+
try {
|
|
258
|
+
const current = fs.lstatSync(lock, { bigint: true });
|
|
259
|
+
if (sameFile(current, acquired) && current.mtimeNs === acquired.mtimeNs)
|
|
260
|
+
fs.rmSync(lock, { force: true });
|
|
261
|
+
}
|
|
262
|
+
catch (cleanupError) {
|
|
263
|
+
if (!isEnoent(cleanupError))
|
|
264
|
+
throw cleanupError;
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
return acquired;
|
|
269
|
+
}
|
|
199
270
|
// Hardlink-based advisory lock; the lock body is the bare pid, as older
|
|
200
271
|
// readers expect. See docs/internals.md.
|
|
201
272
|
export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
@@ -206,17 +277,31 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
206
277
|
fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
|
|
207
278
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
208
279
|
let linked = false;
|
|
280
|
+
let acquired;
|
|
209
281
|
try {
|
|
210
282
|
while (true) {
|
|
211
283
|
try {
|
|
212
284
|
fs.linkSync(ownerFile, lock);
|
|
213
285
|
linked = true;
|
|
286
|
+
acquired = fs.lstatSync(lock, { bigint: true });
|
|
214
287
|
break;
|
|
215
288
|
}
|
|
216
289
|
catch (error) {
|
|
217
290
|
const err = error;
|
|
218
|
-
if (err.code
|
|
291
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
292
|
+
try {
|
|
293
|
+
acquired = takeFallbackLock(lock, ownerFile);
|
|
294
|
+
linked = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
catch (fbError) {
|
|
298
|
+
if (fbError.code !== 'EEXIST')
|
|
299
|
+
throw fbError;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (err.code !== 'EEXIST') {
|
|
219
303
|
throw error;
|
|
304
|
+
}
|
|
220
305
|
try {
|
|
221
306
|
const holder = readHolder(lock);
|
|
222
307
|
if (holder && holderDead(holder, Date.now()) && breakLock(lock, holder))
|
|
@@ -238,23 +323,24 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
238
323
|
if (!linked)
|
|
239
324
|
fs.rmSync(ownerFile, { force: true });
|
|
240
325
|
}
|
|
241
|
-
let bound = false;
|
|
242
326
|
try {
|
|
243
|
-
// Some network and FUSE filesystems give each name of one file its own
|
|
244
|
-
// inode number; there the owner file cannot vouch for the lock.
|
|
245
|
-
bound = sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true }));
|
|
246
327
|
return fn();
|
|
247
328
|
}
|
|
248
329
|
finally {
|
|
249
|
-
releaseLock(lock, ownerFile,
|
|
330
|
+
releaseLock(lock, ownerFile, acquired);
|
|
250
331
|
}
|
|
251
332
|
}
|
|
252
333
|
// A holder whose lock was broken (its owner file removed with it) must not
|
|
253
|
-
// remove its successor's lock
|
|
254
|
-
|
|
334
|
+
// remove its successor's lock: only drop the lock if it is still the exact
|
|
335
|
+
// file this call created, and our own owner file is still there to vouch for it.
|
|
336
|
+
function releaseLock(lock, ownerFile, acquired) {
|
|
255
337
|
try {
|
|
256
|
-
if (
|
|
257
|
-
fs.
|
|
338
|
+
if (acquired) {
|
|
339
|
+
const current = fs.lstatSync(lock, { bigint: true });
|
|
340
|
+
if (sameFile(current, acquired) && current.mtimeNs === acquired.mtimeNs && fs.existsSync(ownerFile)) {
|
|
341
|
+
fs.rmSync(lock, { force: true });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
258
344
|
}
|
|
259
345
|
catch (error) {
|
|
260
346
|
if (!isEnoent(error))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.77",
|
|
4
4
|
"mcpName": "io.github.bakissation/mcp-google-multi",
|
|
5
5
|
"description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
|
|
6
6
|
"type": "module",
|