path-reservations.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. // A path exclusive reservation system
  2. // reserve([list, of, paths], fn)
  3. // When the fn is first in line for all its paths, it
  4. // is called with a cb that clears the reservation.
  5. //
  6. // Used by async unpack to avoid clobbering paths in use,
  7. // while still allowing maximal safe parallelization.
  8. const assert = require('assert')
  9. const normalize = require('./normalize-unicode.js')
  10. const stripSlashes = require('./strip-trailing-slashes.js')
  11. const { join } = require('path')
  12. const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
  13. const isWindows = platform === 'win32'
  14. module.exports = () => {
  15. // path => [function or Set]
  16. // A Set object means a directory reservation
  17. // A fn is a direct reservation on that path
  18. const queues = new Map()
  19. // fn => {paths:[path,...], dirs:[path, ...]}
  20. const reservations = new Map()
  21. // return a set of parent dirs for a given path
  22. // '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d']
  23. const getDirs = path => {
  24. const dirs = path.split('/').slice(0, -1).reduce((set, path) => {
  25. if (set.length)
  26. path = join(set[set.length - 1], path)
  27. set.push(path || '/')
  28. return set
  29. }, [])
  30. return dirs
  31. }
  32. // functions currently running
  33. const running = new Set()
  34. // return the queues for each path the function cares about
  35. // fn => {paths, dirs}
  36. const getQueues = fn => {
  37. const res = reservations.get(fn)
  38. /* istanbul ignore if - unpossible */
  39. if (!res)
  40. throw new Error('function does not have any path reservations')
  41. return {
  42. paths: res.paths.map(path => queues.get(path)),
  43. dirs: [...res.dirs].map(path => queues.get(path)),
  44. }
  45. }
  46. // check if fn is first in line for all its paths, and is
  47. // included in the first set for all its dir queues
  48. const check = fn => {
  49. const {paths, dirs} = getQueues(fn)
  50. return paths.every(q => q[0] === fn) &&
  51. dirs.every(q => q[0] instanceof Set && q[0].has(fn))
  52. }
  53. // run the function if it's first in line and not already running
  54. const run = fn => {
  55. if (running.has(fn) || !check(fn))
  56. return false
  57. running.add(fn)
  58. fn(() => clear(fn))
  59. return true
  60. }
  61. const clear = fn => {
  62. if (!running.has(fn))
  63. return false
  64. const { paths, dirs } = reservations.get(fn)
  65. const next = new Set()
  66. paths.forEach(path => {
  67. const q = queues.get(path)
  68. assert.equal(q[0], fn)
  69. if (q.length === 1)
  70. queues.delete(path)
  71. else {
  72. q.shift()
  73. if (typeof q[0] === 'function')
  74. next.add(q[0])
  75. else
  76. q[0].forEach(fn => next.add(fn))
  77. }
  78. })
  79. dirs.forEach(dir => {
  80. const q = queues.get(dir)
  81. assert(q[0] instanceof Set)
  82. if (q[0].size === 1 && q.length === 1)
  83. queues.delete(dir)
  84. else if (q[0].size === 1) {
  85. q.shift()
  86. // must be a function or else the Set would've been reused
  87. next.add(q[0])
  88. } else
  89. q[0].delete(fn)
  90. })
  91. running.delete(fn)
  92. next.forEach(fn => run(fn))
  93. return true
  94. }
  95. const reserve = (paths, fn) => {
  96. // collide on matches across case and unicode normalization
  97. // On windows, thanks to the magic of 8.3 shortnames, it is fundamentally
  98. // impossible to determine whether two paths refer to the same thing on
  99. // disk, without asking the kernel for a shortname.
  100. // So, we just pretend that every path matches every other path here,
  101. // effectively removing all parallelization on windows.
  102. paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
  103. // don't need normPath, because we skip this entirely for windows
  104. return normalize(stripSlashes(join(p))).toLowerCase()
  105. })
  106. const dirs = new Set(
  107. paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b))
  108. )
  109. reservations.set(fn, {dirs, paths})
  110. paths.forEach(path => {
  111. const q = queues.get(path)
  112. if (!q)
  113. queues.set(path, [fn])
  114. else
  115. q.push(fn)
  116. })
  117. dirs.forEach(dir => {
  118. const q = queues.get(dir)
  119. if (!q)
  120. queues.set(dir, [new Set([fn])])
  121. else if (q[q.length - 1] instanceof Set)
  122. q[q.length - 1].add(fn)
  123. else
  124. q.push(new Set([fn]))
  125. })
  126. return run(fn)
  127. }
  128. return { check, reserve }
  129. }