@tinycloud/operations 0.3.3-beta.12 → 0.3.3-beta.14

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/artifacts.js CHANGED
@@ -6928,11 +6928,15 @@ function canonicalizeCaveats(caveats) {
6928
6928
  import { randomUUID } from "crypto";
6929
6929
  import { AsyncLocalStorage } from "async_hooks";
6930
6930
  import {
6931
+ chmod,
6932
+ link,
6931
6933
  mkdir,
6932
6934
  readFile,
6935
+ readdir,
6933
6936
  rename,
6934
6937
  rmdir,
6935
6938
  rm,
6939
+ stat,
6936
6940
  writeFile
6937
6941
  } from "fs/promises";
6938
6942
  import { homedir } from "os";
@@ -6941,8 +6945,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
6941
6945
  var DEFAULT_LOCK_RETRY_MS = 25;
6942
6946
  var DEFAULT_STALE_LOCK_MS = 3e4;
6943
6947
  var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
6948
+ var PRIVATE_FILE_MODE = 384;
6949
+ var PRIVATE_DIR_MODE = 448;
6944
6950
  var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
6951
+ var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
6952
+ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
6953
+ var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
6954
+ var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
6955
+ var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
6956
+ var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
6957
+ var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
6945
6958
  var invocationStateRoot = new AsyncLocalStorage();
6959
+ var heldProfileLocks = new AsyncLocalStorage();
6946
6960
  var ProfileLockTimeoutError = class extends Error {
6947
6961
  constructor(profile, timeoutMs) {
6948
6962
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -6969,9 +6983,6 @@ function profileStoreMetadataPath(profile, store) {
6969
6983
  function profileLockPath(profile) {
6970
6984
  return join(profilePath(profile), ".lock");
6971
6985
  }
6972
- function profileLockMetadataPath(profile) {
6973
- return join(profileLockPath(profile), "owner.json");
6974
- }
6975
6986
  async function readJson(filePath) {
6976
6987
  try {
6977
6988
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -6988,9 +6999,9 @@ async function writeJsonAtomic(filePath, value) {
6988
6999
  );
6989
7000
  const contents = `${JSON.stringify(value, null, 2)}
6990
7001
  `;
6991
- await mkdir(directory, { recursive: true });
7002
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
6992
7003
  try {
6993
- await writeFile(temporaryPath, contents, "utf8");
7004
+ await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
6994
7005
  await rename(temporaryPath, filePath);
6995
7006
  } catch (error) {
6996
7007
  await rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -7020,10 +7031,15 @@ async function readProfileStore(profile, store) {
7020
7031
  }
7021
7032
  async function withProfileLock(profile, action, options = {}) {
7022
7033
  const normalizedProfile = validateProfileName(profile);
7034
+ const lockPath = profileLockPath(normalizedProfile);
7035
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7036
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7023
7037
  const release = await acquireProfileLock(normalizedProfile, options);
7038
+ const ownership = { lockPath, active: true };
7024
7039
  try {
7025
- return await action();
7040
+ return await heldProfileLocks.run([...held, ownership], action);
7026
7041
  } finally {
7042
+ ownership.active = false;
7027
7043
  await release();
7028
7044
  }
7029
7045
  }
@@ -7050,28 +7066,18 @@ async function acquireProfileLock(profile, options) {
7050
7066
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7051
7067
  const startedAt = Date.now();
7052
7068
  const lockPath = profileLockPath(profile);
7053
- await mkdir(profilePath(profile), { recursive: true });
7069
+ await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7070
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7071
+ await chmod(directory, PRIVATE_DIR_MODE);
7072
+ }
7054
7073
  while (true) {
7055
- try {
7056
- await mkdir(lockPath);
7057
- const token = randomUUID();
7058
- try {
7059
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7060
- pid: process.pid,
7061
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7062
- token
7063
- });
7064
- } catch (error) {
7065
- await rmdir(lockPath).catch(() => void 0);
7066
- throw error;
7067
- }
7074
+ const token = randomUUID();
7075
+ if (await publishProfileLock(profile, lockPath, token)) {
7068
7076
  return async () => {
7069
- await releaseProfileLock(lockPath, token);
7077
+ await releaseProfileLock(profile, lockPath, token);
7070
7078
  };
7071
- } catch (error) {
7072
- if (!isLockAlreadyHeld(error)) throw error;
7073
- await signalTestLockContention(profile);
7074
7079
  }
7080
+ await signalTestLockContention(profile);
7075
7081
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7076
7082
  const elapsedMs = Date.now() - startedAt;
7077
7083
  if (elapsedMs >= timeoutMs) {
@@ -7080,21 +7086,35 @@ async function acquireProfileLock(profile, options) {
7080
7086
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7081
7087
  }
7082
7088
  }
7083
- async function releaseProfileLock(lockPath, token) {
7084
- const ownerPath = join(lockPath, "owner.json");
7085
- const claimPath = join(lockPath, `.release-${token}.json`);
7089
+ async function publishProfileLock(profile, lockPath, token) {
7086
7090
  try {
7087
- await rename(ownerPath, claimPath);
7091
+ await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
7088
7092
  } catch (error) {
7089
- if (isErrno(error, "ENOENT")) return;
7093
+ if (isErrno(error, "EEXIST")) return false;
7090
7094
  throw error;
7091
7095
  }
7092
- const claimed = await readJson(claimPath).catch(() => null);
7093
- if (claimed?.token !== token) {
7094
- await rename(claimPath, ownerPath).catch(() => void 0);
7095
- return;
7096
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7097
+ const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
7098
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7099
+ try {
7100
+ await writeFile(staged, `${JSON.stringify(owner, null, 2)}
7101
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7102
+ await link(staged, join(lockPath, "owner.json"));
7103
+ return true;
7104
+ } catch (error) {
7105
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7106
+ await rmdir(lockPath).catch(() => void 0);
7107
+ throw error;
7108
+ } finally {
7109
+ await rm(staged, { force: true }).catch(() => void 0);
7096
7110
  }
7097
- await rm(claimPath, { force: true });
7111
+ }
7112
+ async function releaseProfileLock(profile, lockPath, token) {
7113
+ const ownerPath = join(lockPath, "owner.json");
7114
+ const owner = await readJson(ownerPath).catch(() => null);
7115
+ if (owner?.token !== token) return;
7116
+ await rm(ownerPath, { force: true });
7117
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7098
7118
  await rmdir(lockPath).catch(() => void 0);
7099
7119
  }
7100
7120
  async function signalTestLockContention(profile) {
@@ -7111,32 +7131,79 @@ async function signalTestLockContention(profile) {
7111
7131
  await writeFile(signalPath, `${profile}
7112
7132
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7113
7133
  }
7134
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7135
+ try {
7136
+ const { mtimeMs } = await stat(lockPath);
7137
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7138
+ } catch {
7139
+ return false;
7140
+ }
7141
+ const checkedAt = performance.now();
7142
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7143
+ try {
7144
+ const entries = await readdir(lockPath);
7145
+ if (entries.includes("owner.json")) return false;
7146
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7147
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7148
+ await rm(join(lockPath, name), { force: true });
7149
+ }
7150
+ await rmdir(lockPath);
7151
+ } catch {
7152
+ return false;
7153
+ }
7154
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7155
+ return true;
7156
+ }
7157
+ function recoveryFenced(since, staleAfterMs) {
7158
+ return performance.now() - since >= staleAfterMs / 2;
7159
+ }
7160
+ var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7161
+ var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7162
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7163
+ const directory = dirname(lockPath);
7164
+ const names = await readdir(directory).catch(() => []);
7165
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7166
+ const path = join(directory, name);
7167
+ const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7168
+ if (aged) await rm(path, { force: true }).catch(() => void 0);
7169
+ }
7170
+ }
7114
7171
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7115
7172
  const ownerPath = join(lockPath, "owner.json");
7116
7173
  const owner = await readJson(ownerPath).catch(() => null);
7174
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7117
7175
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7118
7176
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7119
- await waitForTestStaleRecoveryBarrier(profile);
7177
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7120
7178
  const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
7121
7179
  try {
7122
- await rename(ownerPath, claimPath);
7123
- } catch (error) {
7124
- if (isErrno(error, "ENOENT")) return false;
7180
+ await link(ownerPath, claimPath);
7181
+ } catch {
7125
7182
  return false;
7126
7183
  }
7184
+ const claimedAt = performance.now();
7185
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7127
7186
  const claimed = await readJson(claimPath).catch(() => null);
7128
7187
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7129
- if (!sameInstance) {
7130
- await rename(claimPath, ownerPath).catch(() => void 0);
7188
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7189
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7190
+ await rm(claimPath, { force: true });
7191
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7131
7192
  return false;
7132
7193
  }
7194
+ if (sameInstance) {
7195
+ await rm(ownerPath, { force: true });
7196
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7197
+ }
7133
7198
  await rm(claimPath, { force: true });
7134
7199
  await rmdir(lockPath).catch(() => void 0);
7200
+ if (!sameInstance) return false;
7201
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7135
7202
  return true;
7136
7203
  }
7137
- async function waitForTestStaleRecoveryBarrier(profile) {
7204
+ async function waitForTestBarrier(environmentName, profile) {
7138
7205
  if (process.env.NODE_ENV !== "test") return;
7139
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7206
+ const barrierDirectory = process.env[environmentName];
7140
7207
  if (!barrierDirectory) return;
7141
7208
  await mkdir(barrierDirectory, { recursive: true });
7142
7209
  const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7160,9 +7227,6 @@ function isStaleOwner(owner, staleAfterMs) {
7160
7227
  }
7161
7228
  return !isProcessAlive(owner.pid);
7162
7229
  }
7163
- function isLockAlreadyHeld(error) {
7164
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7165
- }
7166
7230
  function isProcessAlive(pid) {
7167
7231
  try {
7168
7232
  process.kill(pid, 0);