Ctrl K
ComunidadGuías…

src/services/vps-operation-lock.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import {
  createVpsLockCommand, releaseVpsLockCommand, createVpsBuildStateCommand,
  scopeVpsBuildCommand, cleanupVpsBuildStateCommand,
  claimVpsResumeLockCommand, releaseVpsResumeClaimCommand,
} from "../domain/vps-build-state.js";

const validIdentity = value => typeof value === "string" && /^\d+:\d+$/u.test(value);
// The server shows a failure only when it is at most 240 characters and holds
// no absolute path, so these texts name the lock by its file name. The code
// lets the setup guide say where that lock lives.
const SAFE_MESSAGE_LIMIT = 240;
const LOCK_CODE = "vps_operation_lock";
const lockName = lock => lock.slice(lock.lastIndexOf("/") + 1);
const heldMessage = lock => `The operation lock ${lockName(lock)} is held or uncertain. Another setup may be running, or an earlier one was interrupted. An administrator must inspect it before retrying.`;
const recoveryMessage = lock => `The operation lock ${lockName(lock)} or its temporary build state could not be removed safely. An administrator must inspect it before retrying; Relmio will not delete uncertain state.`;
const recoveryError = lock => Object.assign(new Error(recoveryMessage(lock)), { code: LOCK_CODE });
// A failed operation keeps its own sentence. The lock notice shrinks before the
// whole text can pass the limit, and replaces it only as a last resort.
const withLockNotice = (base, lock) => [
  `${base} ${recoveryMessage(lock)}`,
  `${base} An administrator must inspect ${lockName(lock)} before retrying.`,
].find(text => text.length <= SAFE_MESSAGE_LIMIT) ?? recoveryMessage(lock);

// Not every caller has passed a final confirmation: the bridge's image sign-in poll and cancel
// take this lock without one. So this helper and its lock commands create and remove only the
// lock folder and its build state, and never change the shared folder itself. Build state is
// lazy: status, removal and cached retries never create it. Grok credential launches use
// Compose run, which may build a missing image and therefore needs isolation.
export async function withVpsOperationLock(remote, lock, operation, { resumeIdentity } = {}) {
  const uncertainMessage = heldMessage(lock);
  let acquired;
  let claimIdentity;
  try {
    acquired = await remote.exec(resumeIdentity
      ? claimVpsResumeLockCommand(lock, resumeIdentity) : createVpsLockCommand(lock));
  } catch (cause) {
    throw Object.assign(new Error(uncertainMessage, { cause }), {
      safeMessage: uncertainMessage, code: LOCK_CODE, operationLockRetained: true,
      ...(cause?.remoteOutcomeUnknown === true ? { remoteOutcomeUnknown: true } : {}),
      ...(cause?.timedOut === true ? { timedOut: true } : {}),
    });
  }
  if (resumeIdentity) claimIdentity = acquired.stdout?.trim();
  const identity = resumeIdentity ?? acquired.stdout?.trim();
  if (acquired.code !== 0 || !validIdentity(identity) ||
      (resumeIdentity && !validIdentity(claimIdentity))) {
    throw Object.assign(new Error(uncertainMessage), {
      safeMessage: uncertainMessage, code: LOCK_CODE, operationLockRetained: true,
    });
  }
  let stateIdentity;
  let stateUncertain = false;
  let buildUncertain = false;
  let primary;
  let result;
  const build = async command => {
    if (!stateIdentity) {
      stateUncertain = true;
      const created = await remote.exec(createVpsBuildStateCommand(lock, identity));
      stateIdentity = created.stdout?.trim();
      if (created.code !== 0 || !validIdentity(stateIdentity)) {
        stateIdentity = undefined;
        throw recoveryError(lock);
      }
      stateUncertain = false;
    }
    const scoped = scopeVpsBuildCommand(lock, identity, stateIdentity, command);
    buildUncertain = true;
    const built = await remote.exec(scoped, { timeoutMs: 1_800_000 });
    if (!Number.isInteger(built?.code)) throw Object.assign(
      new Error("The companion build result could not be confirmed."), { remoteOutcomeUnknown: true });
    buildUncertain = false;
    if (built.code !== 0) throw Object.assign(new Error("The confirmed companion image build or launch failed. The existing n8n deployment was not changed."), { code: "vps_step_build" });
  };
  build.releaseState = async () => {
    if (!stateIdentity || stateUncertain || buildUncertain) throw recoveryError(lock);
    stateUncertain = true;
    const removed = await remote.exec(cleanupVpsBuildStateCommand(lock, identity, stateIdentity));
    if (removed.code !== 0) throw recoveryError(lock);
    stateIdentity = undefined;
    stateUncertain = false;
  };
  try {
    result = await operation(build);
  } catch (error) {
    primary = error;
  }
  let cleanupFailed = stateUncertain || buildUncertain ||
    primary?.remoteOutcomeUnknown === true || result?.remoteOutcomeUnknown === true;
  try {
    // A disconnected exec may leave Compose running. Never clean underneath it.
    if (!cleanupFailed && stateIdentity && (await remote.exec(cleanupVpsBuildStateCommand(lock, identity, stateIdentity))).code !== 0) cleanupFailed = true;
    if (!cleanupFailed && claimIdentity &&
        (await remote.exec(releaseVpsResumeClaimCommand(lock, identity, claimIdentity))).code !== 0) cleanupFailed = true;
    // Never release an uncertain lease, even if a foreign actor emptied it.
    if (!cleanupFailed && (await remote.exec(releaseVpsLockCommand(lock, identity))).code !== 0) cleanupFailed = true;
  } catch {
    cleanupFailed = true;
  }
  if (cleanupFailed) {
    const message = recoveryMessage(lock);
    if (!primary && result?.credentialShownOnce === true && typeof result.clientCredential === "string") {
      const partial = {
        ...result, deploymentMode: "partial", readiness: "unverified",
        operationLockRetained: true,
        finalizationFailure: { error: message, recovery: "resolve-handoff" },
      };
      if (result.migratedLegacy === true) partial.migrationPending = true;
      if (result.replacedAccount === true) partial.replacementPending = true;
      delete partial.migratedLegacy;
      delete partial.replacedAccount;
      return partial;
    }
    if (primary) {
      // Keep the primary exception and recovery classification; add only fixed,
      // safe cleanup text, never remote command output or credential material.
      primary.message = `${primary.message} ${message}`;
      primary.safeMessage = withLockNotice(primary.safeMessage ?? "The confirmed operation failed.", lock);
      primary.code ??= LOCK_CODE;
      primary.operationLockRetained = true;
      throw primary;
    }
    throw Object.assign(new Error(message), { safeMessage: message, code: LOCK_CODE, operationLockRetained: true });
  }
  if (primary) throw primary;
  return result;
}