/** * @file * * Cross-process advisory lock that serializes whole integration-test runs. * * Two test runs that drive the same shared resources corrupt each other: on * desktop they share the single local Obsidian instance, its `obsidian.json` * registry, and the CDP port; on Android they share the emulator and the Appium * server. One run's setup/teardown kills or reconfigures the instance the other * run is mid-eval on (observed as `ECONNREFUSED` or "vault not open"), so both * runs fail. * * This lock makes the second run **wait** for the first to finish instead of * running concurrently. It is held for the entire run: acquired at the start of * the core setup and released by the core teardown (and by the process cleanup * handlers). The lock is a sentinel file in a shared temp directory, and its * holder keeps a **heartbeat** timestamp inside it fresh for as long as it holds * the lock — so a run that crashed without releasing stops beating and the next * run detects the lock as stale and steals it. * * The heartbeat, not the holder's PID, is what proves a holder is still there: a * PID probe only answers "does *a* process with this PID exist", which a * recycled PID satisfies long after the real holder died — leaving every later * run to block for the full acquisition timeout. */ /** * The scope every Android run serializes on: the Appium transport's global * setup, a project that takes the lock itself, and the emulator reaper all * have to name the same one, or they serialize against nothing. */ export declare const ANDROID_SETUP_LOCK_SCOPE = "android"; /** * Parameters for {@link acquireSetupLock}. */ export interface AcquireSetupLockParams { /** Short transport label for log messages (e.g. `"obsidian-cli"`). */ readonly label: string; /** * Logical scope the lock serializes within. Runs that share resources must * use the same scope (e.g. `"desktop"` for the CLI/CDP transports, `"android"` * for the Appium transport). */ readonly scope: string; /** * Maximum time to wait for a competing run to release the lock before giving * up and throwing. * * @default 1 hour */ readonly timeoutInMilliseconds?: number | undefined; } /** * A held setup lock. Release it once the run's teardown is complete. */ export interface SetupLock { /** Releases the lock. Safe to call more than once. */ release: () => void; } /** * Parameters for {@link tryAcquireSetupLock}. */ export interface TryAcquireSetupLockParams { /** Short transport label for log messages, recorded in the lock file as the holder's. */ readonly label: string; /** Logical scope the lock serializes within — see {@link AcquireSetupLockParams.scope}. */ readonly scope: string; /** * How long a same-host holder whose PID is still alive may go without a * heartbeat before its lock counts as abandoned. * * A dead holder PID is abandoned at once whatever this says. The default is * the threshold every waiting run steals on; a caller that is *always* waiting * — the emulator reaper — passes a far wider one, so a live run that blocks * its event loop for a couple of minutes is not robbed of its lock. * * @default 2 minutes */ readonly staleAfterSilenceInMilliseconds?: number | undefined; } /** * Acquires the cross-process setup lock for the given scope, waiting until any * competing run releases it (or its lock is detected as stale). * * @param params - The lock parameters. * @returns A handle whose {@link SetupLock.release} frees the lock. * @throws If the lock cannot be acquired within `timeoutInMilliseconds`. */ export declare function acquireSetupLock(params: AcquireSetupLockParams): Promise; /** * Reports whether a run currently holds the setup lock for a scope. * * A lock file that exists but cannot be read counts as held — the same answer * {@link acquireSetupLock} gives by waiting on it: it is either mid-write or * corrupt, and either way no run can take it. * * @param scope - The lock scope. * @returns `true` when a holder that is not stale owns the lock. */ export declare function checkIsSetupLockHeld(scope: string): boolean; /** * Takes the setup lock for a scope if it is free or abandoned, without waiting. * * One attempt: a live holder — or a lock file that cannot be read — means * `undefined`, and the caller decides whether to try again later. * * @param params - The lock parameters. * @returns A handle whose {@link SetupLock.release} frees the lock, or `undefined` while another run holds it. * @throws If the file system fails for any reason other than the lock file already existing. */ export declare function tryAcquireSetupLock(params: TryAcquireSetupLockParams): SetupLock | undefined;