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.
Files changed (2) hide show
  1. package/dist/fs-atomic.js +98 -12
  2. 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
- if (sameFile(fs.lstatSync(ownerPath, { bigint: true }), before)) {
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 !== 'EEXIST')
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, bound);
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
- function releaseLock(lock, ownerFile, bound) {
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 (!bound || sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true })))
257
- fs.rmSync(lock, { force: true });
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.76",
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",