UNPKG

next

Version:

The React Framework

133 lines (132 loc) 5.77 kB
import { bold, cyan } from '../lib/picocolors'; import * as Log from './output/log'; import { getBindingsSync } from './swc'; const RETRY_DELAY_MS = 10; const MAX_RETRY_MS = 1000; /** * A cross-platform on-disk best-effort advisory exclusive lockfile * implementation. * * On Windows, this opens a file in write mode with the `FILE_SHARE_WRITE` flag * unset, so it still allows reading the lockfile. This avoids breaking tools * that read the contents of `.next`. * * On POSIX platforms, this uses `flock()` via `std::fs::File::try_lock`: * https://doc.rust-lang.org/std/fs/struct.File.html#method.try_lock * * On WASM, a dummy implementation is used which always "succeeds" in acquiring * the lock. * * This provides a more idiomatic wrapper around the lockfile APIs exposed on * the native bindings object. * * If this lock is not explicitly closed with `unlock`, we will: * - If `unlockOnExit` is set (the default), it will make a best-effort attempt * to unlock the lockfile using `process.on('exit', ...)`. This is preferrable * on Windows where it may take some time after process exit for the operating * system to clean up locks that are not explicitly released by the process. * - If we fail to ever release the lockfile, the operating system will clean up * the lock and file descriptor upon process exit. */ export class Lockfile { constructor(bindings, nativeLockfile){ this.bindings = bindings; this.nativeLockfile = nativeLockfile; } /** * Attempts to create or acquire an exclusive lockfile on disk. Lockfiles are * best-effort, depending on the platform. * * - If we fail to acquire the lock, we return `undefined`. * - If we're on wasm, this always returns a dummy `Lockfile` object. */ static tryAcquire(path, unlockOnExit = true) { const bindings = getBindingsSync(); if (bindings.isWasm) { Log.info(`Skipping creating a lockfile at ${cyan(path)} because we're using WASM bindings`); return new Lockfile(bindings, undefined); } else { let nativeLockfile; try { nativeLockfile = bindings.lockfileTryAcquireSync(path); } catch (e) { // this happens if there's an IO error (e.g. `ENOENT`), which is // different than if we just didn't acquire the lock throw Object.defineProperty(new Error('An IO error occurred while attempting to create and acquire the lockfile', { cause: e }), "__NEXT_ERROR_CODE", { value: "E859", enumerable: false, configurable: true }); } if (nativeLockfile != null) { const jsLockfile = new Lockfile(bindings, nativeLockfile); if (unlockOnExit) { const exitListener = ()=>{ // Best-Effort: If we don't do this, the operating system will // release the lock for us. This gives an opportunity to delete the // unlocked lockfile (which is not otherwise deleted on POSIX). // // This must be synchronous because `process.on('exit', ...)` is // synchronous. jsLockfile.unlockSync(); }; process.on('exit', exitListener); jsLockfile.listener = exitListener; } return jsLockfile; } else { return undefined; } } } /** * Attempts to create or acquire a lockfile using `Lockfile.tryAcquire`. If * that returns `undefined`, indicating that another process or caller has the * lockfile, then this will output an error message and exit the process with * a non-zero exit code. * * This will retry a small number of times. This can be useful when running * processes in a loop, e.g. if cleanup isn't fully synchronous due to child * parent/processes. */ static async acquireWithRetriesOrExit(path, processName, unlockOnExit = true) { const startMs = Date.now(); let lockfile; while(Date.now() - startMs < MAX_RETRY_MS){ lockfile = Lockfile.tryAcquire(path, unlockOnExit); if (lockfile !== undefined) break; await new Promise((resolve)=>setTimeout(resolve, RETRY_DELAY_MS)); } if (lockfile === undefined) { Log.error(`Unable to acquire lock at ${cyan(path)}, is another instance of ${cyan(processName)} running?`); Log.info(`${bold('Suggestion:')} If you intended to restart ${cyan(processName)}, terminate the other process, and then try again.`); process.exit(1); } return lockfile; } /** * Releases the lockfile and closes the file descriptor. * * If this is not called, the lock will be released by the operating system * when the file handle is closed during process exit. */ async unlock() { const { nativeLockfile, listener } = this; if (nativeLockfile !== undefined) { await this.bindings.lockfileUnlock(nativeLockfile); } if (listener !== undefined) { process.off('exit', listener); } } /** * A blocking version of `unlock`. */ unlockSync() { const { nativeLockfile, listener } = this; if (nativeLockfile !== undefined) { this.bindings.lockfileUnlockSync(nativeLockfile); } if (listener !== undefined) { process.off('exit', listener); } } } //# sourceMappingURL=lockfile.js.map