File Explorer

/proc/4/root/var/lang/lib/node_modules/npm/node_modules/libnpmexec/lib

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

with-lock.js5.5 KB · 178 lines
const fs = require('node:fs/promises')const { rmdirSync } = require('node:fs')const { promiseRetry } = require('@gar/promise-retry')const { onExit } = require('signal-exit') // a lockfile implementation inspired by the unmaintained proper-lockfile library//// similarities:// - based on mkdir's atomicity// - works across processes and even machines (via NFS)// - cleans up after itself// - detects compromised locks//// differences:// - higher-level API (just a withLock function)// - written in async/await style// - uses mtime + inode for more reliable compromised lock detection// - more ergonomic compromised lock handling (i.e. withLock will reject, and callbacks have access to an AbortSignal)// - uses a more recent version of signal-exit // mtime precision is platform dependent, so deal in secondsconst touchInterval = 1_000// use a reasonably large threshold, in case stat calls take a whileconst staleThreshold = 60_000 // track current locks and their cleanup functionsconst currentLocks = new Map() function cleanupLocks () {  for (const [, cleanup] of currentLocks) {    try {      cleanup()    } catch (err) {      //    }  }} // clean up any locks that were not released normallyonExit(cleanupLocks) /** * Acquire an advisory lock for the given path and hold it for the duration of the callback. * * The lock will be released automatically when the callback resolves or rejects. * Concurrent calls to withLock() for the same path will wait until the lock is released. */async function withLock (lockPath, cb) {  try {    const signal = await acquireLock(lockPath)    return await new Promise((resolve, reject) => {      signal.addEventListener('abort', () => {        reject(Object.assign(new Error('Lock compromised'), { code: 'ECOMPROMISED' }))      });       (async () => {        try {          resolve(await cb(signal))        } catch (err) {          reject(err)        }      })()    })  } finally {    releaseLock(lockPath)  }} function acquireLock (lockPath) {  return promiseRetry(async (retry) => {    try {      await fs.mkdir(lockPath)    } catch (err) {      if (err.code !== 'EEXIST' && err.code !== 'EBUSY' && err.code !== 'EPERM') {        throw err      }       const status = await getLockStatus(lockPath)       if (status === 'locked') {        // let's see if we can acquire it on the next attempt 🤞        return retry(err)      }      if (status === 'stale') {        try {          // there is a very tiny window where another process could also release the stale lock and acquire it before we release it here; the lock compromise checker should detect this and throw an error          deleteLock(lockPath)        } catch (e) {          // on windows, EBUSY/EPERM can happen if another process is (re)creating the lock; maybe we can acquire it on a subsequent attempt 🤞          if (e.code === 'EBUSY' || e.code === 'EPERM') {            return retry(e)          }          throw e        }      }      // immediately attempt to acquire the lock (no backoff)      return await acquireLock(lockPath)    }    try {      const signal = await maintainLock(lockPath)      return signal    } catch (err) {      throw Object.assign(new Error('Lock compromised'), { code: 'ECOMPROMISED' })    }  }, {    minTimeout: 100,    maxTimeout: 5_000,    // if another process legitimately holds the lock, wait for it to release; if it dies abnormally and the lock becomes stale, we'll acquire it automatically    forever: true,  })} function deleteLock (lockPath) {  try {    // synchronous, so we can call in an exit handler    rmdirSync(lockPath)  } catch (err) {    if (err.code !== 'ENOENT') {      throw err    }  }} function releaseLock (lockPath) {  currentLocks.get(lockPath)?.()  currentLocks.delete(lockPath)} async function getLockStatus (lockPath) {  try {    const stat = await fs.stat(lockPath)    return (Date.now() - stat.mtimeMs > staleThreshold) ? 'stale' : 'locked'  } catch (err) {    if (err.code === 'ENOENT') {      return 'unlocked'    }    throw err  }} async function maintainLock (lockPath) {  const controller = new AbortController()  const stats = await fs.stat(lockPath)  // fs.utimes operates on floating points seconds (directly, or via strings/Date objects), which may not match the underlying filesystem's mtime precision, meaning that we might read a slightly different mtime than we write. always round to the nearest second, since all filesystems support at least second precision  let mtime = Math.round(stats.mtimeMs / 1000)  const signal = controller.signal   let timeout  async function touchLock () {    try {      const currentStats = (await fs.stat(lockPath))      const currentMtime = Math.round(currentStats.mtimeMs / 1000)      if (currentStats.ino !== stats.ino || currentMtime !== mtime) {        throw new Error('Lock compromised')      }      mtime = Math.round(Date.now() / 1000)      // touch the lock, unless we just released it during this iteration      if (currentLocks.has(lockPath)) {        await fs.utimes(lockPath, mtime, mtime)      }      timeout = setTimeout(touchLock, touchInterval).unref()    } catch (err) {      // stats mismatch or other fs error means the lock was compromised      controller.abort()    }  }   timeout = setTimeout(touchLock, touchInterval).unref()  function cleanup () {    clearTimeout(timeout)    deleteLock(lockPath)  }  currentLocks.set(lockPath, cleanup)  return signal} module.exports = withLock