Skip to content

Commit ab2e845

Browse files
authored
[node-modules] Fixes race between mkdirp and remove (#1108)
* Fixes race between `mkdirp` and `remove` during module linking by doing all `remove` first. * Get rid of keepNodeModules by keeping inner node_modules by default during all the operations * Add assertion check that removed directory has node_modules
1 parent ac26689 commit ab2e845

File tree

2 files changed

+69
-34
lines changed

2 files changed

+69
-34
lines changed

.yarn/versions/37861a83.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
releases:
2+
"@yarnpkg/cli": prerelease
3+
"@yarnpkg/plugin-node-modules": prerelease
4+
5+
declined:
6+
- "@yarnpkg/plugin-constraints"
7+
- "@yarnpkg/plugin-dlx"
8+
- "@yarnpkg/plugin-essentials"
9+
- "@yarnpkg/plugin-init"
10+
- "@yarnpkg/plugin-interactive-tools"
11+
- "@yarnpkg/plugin-npm-cli"
12+
- "@yarnpkg/plugin-pack"
13+
- "@yarnpkg/plugin-patch"
14+
- "@yarnpkg/plugin-pnp"
15+
- "@yarnpkg/plugin-stage"
16+
- "@yarnpkg/plugin-typescript"
17+
- "@yarnpkg/plugin-version"
18+
- "@yarnpkg/plugin-workspace-tools"
19+
- "@yarnpkg/core"
20+
- "@yarnpkg/doctor"

packages/plugin-node-modules/sources/NodeModulesLinker.ts

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,10 @@ function getLocationMap(installState: NodeModulesLocatorMap) {
317317
return locationMap;
318318
}
319319

320-
const removeDir = async (dir: PortablePath, options?: {innerLoop?: boolean, excludeNodeModules?: boolean}): Promise<any> => {
320+
const removeDir = async (dir: PortablePath, options?: {innerLoop?: boolean}): Promise<any> => {
321+
if (dir.split(ppath.sep).indexOf(NODE_MODULES) < 0)
322+
throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${dir}`);
323+
321324
try {
322325
if (!options || !options.innerLoop) {
323326
const stats = await xfs.lstatPromise(dir);
@@ -330,7 +333,7 @@ const removeDir = async (dir: PortablePath, options?: {innerLoop?: boolean, excl
330333
for (const entry of entries) {
331334
const targetPath = ppath.join(dir, toFilename(entry.name));
332335
if (entry.isDirectory()) {
333-
if (entry.name !== NODE_MODULES || !options || !options.excludeNodeModules) {
336+
if (entry.name !== NODE_MODULES || (options && options.innerLoop)) {
334337
await removeDir(targetPath, {innerLoop: true});
335338
}
336339
} else {
@@ -447,7 +450,7 @@ const buildLocationTree = (locatorMap: NodeModulesLocatorMap | null, {skipPrefix
447450
const symlinkPromise = async (srcDir: PortablePath, dstDir: PortablePath) =>
448451
xfs.symlinkPromise(process.platform !== 'win32' ? ppath.relative(ppath.dirname(dstDir), srcDir) : srcDir, dstDir, process.platform === 'win32' ? 'junction' : undefined);
449452

450-
const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs}: {baseFs: FakeFS<PortablePath>}) => {
453+
const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs, innerLoop}: {baseFs: FakeFS<PortablePath>, innerLoop?: boolean}) => {
451454
await xfs.mkdirpPromise(dstDir);
452455
const entries = await baseFs.readdirPromise(srcDir, {withFileTypes: true});
453456

@@ -472,7 +475,9 @@ const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs}:
472475
const srcPath = ppath.join(srcDir, toFilename(entry.name));
473476
const dstPath = ppath.join(dstDir, toFilename(entry.name));
474477
if (entry.isDirectory()) {
475-
await copyPromise(dstPath, srcPath, {baseFs});
478+
if (entry.name !== NODE_MODULES || innerLoop) {
479+
await copyPromise(dstPath, srcPath, {baseFs, innerLoop: true});
480+
}
476481
} else {
477482
await copy(dstPath, srcPath, entry);
478483
}
@@ -574,10 +579,9 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
574579
const locationTree = buildLocationTree(installState, {skipPrefix: project.cwd});
575580

576581
const addQueue: Promise<void>[] = [];
577-
const addModule = async ({srcDir, dstDir, linkType, keepNodeModules}: {srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType, keepNodeModules: boolean}) => {
582+
const addModule = async ({srcDir, dstDir, linkType}: {srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType}) => {
578583
const promise: Promise<any> = (async () => {
579584
try {
580-
await removeDir(dstDir, {excludeNodeModules: keepNodeModules});
581585
if (linkType === LinkType.SOFT) {
582586
await xfs.mkdirpPromise(ppath.dirname(dstDir));
583587
await symlinkPromise(ppath.resolve(srcDir), dstDir);
@@ -597,14 +601,12 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
597601
}
598602
};
599603

600-
const cloneModule = async (srcDir: PortablePath, dstDir: PortablePath, options?: { keepSrcNodeModules?: boolean, keepDstNodeModules?: boolean, innerLoop?: boolean }) => {
604+
const cloneModule = async (srcDir: PortablePath, dstDir: PortablePath, options?: { innerLoop?: boolean }) => {
601605
const promise: Promise<any> = (async () => {
602-
const cloneDir = async (srcDir: PortablePath, dstDir: PortablePath, options?: { keepSrcNodeModules?: boolean, keepDstNodeModules?: boolean, innerLoop?: boolean }) => {
606+
const cloneDir = async (srcDir: PortablePath, dstDir: PortablePath, options?: { innerLoop?: boolean }) => {
603607
try {
604-
if (!options || !options.innerLoop) {
605-
await removeDir(dstDir, {excludeNodeModules: options && options.keepDstNodeModules});
608+
if (!options || !options.innerLoop)
606609
await xfs.mkdirpPromise(dstDir);
607-
}
608610

609611
const entries = await xfs.readdirPromise(srcDir, {withFileTypes: true});
610612
for (const entry of entries) {
@@ -614,13 +616,13 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
614616
const src = ppath.join(srcDir, entry.name);
615617
const dst = ppath.join(dstDir, entry.name);
616618

617-
if (entry.name !== NODE_MODULES || !options || !options.keepSrcNodeModules) {
618-
if (entry.isDirectory()) {
619+
if (entry.isDirectory()) {
620+
if (entry.name !== NODE_MODULES || (options && options.innerLoop)) {
619621
await xfs.mkdirpPromise(dst);
620-
await cloneDir(src, dst, {keepSrcNodeModules: false, keepDstNodeModules: false, innerLoop: true});
621-
} else {
622-
await xfs.copyFilePromise(src, dst, fs.constants.COPYFILE_FICLONE);
622+
await cloneDir(src, dst, {innerLoop: true});
623623
}
624+
} else {
625+
await xfs.copyFilePromise(src, dst, fs.constants.COPYFILE_FICLONE);
624626
}
625627
}
626628
} catch (e) {
@@ -661,39 +663,53 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
661663

662664

663665
// Delete locations that no longer exist
664-
const deleteList: PortablePath[] = [];
666+
const deleteList = new Set<PortablePath>();
667+
// Delete locations of inner node_modules
668+
const innerDeleteList = new Set<PortablePath>();
665669
for (const {locations} of preinstallState.locatorMap.values()) {
666670
for (const location of locations) {
667671
const {locationRoot, segments} = parseLocation(location, {
668672
skipPrefix: project.cwd,
669673
});
670674

675+
let prevNode = prevLocationTree.get(locationRoot);
671676
let node = locationTree.get(locationRoot);
672677
let curLocation = locationRoot;
673-
if (!node) {
674-
deleteList.push(curLocation);
675-
} else {
678+
if (node) {
676679
for (const segment of segments) {
677680
// '.' segment exists only for top-level locator, skip it
678681
if (segment === '.')
679682
continue;
680683

681684
curLocation = ppath.join(curLocation, segment);
682685
node = node.children.get(segment);
686+
if (prevNode)
687+
prevNode = prevNode.children.get(segment);
688+
683689
if (!node) {
684-
deleteList.push(curLocation);
690+
deleteList.add(curLocation);
691+
// If previous install had inner node_modules folder, we should explicitely list it for
692+
// `removeDir` to delete it, but we need to delete it first, so we add it to inner delete list
693+
if (prevNode && prevNode.children.has(NODE_MODULES))
694+
innerDeleteList.add(ppath.join(curLocation, NODE_MODULES));
685695
break;
686696
}
687697
}
688698
}
689699
}
690700
}
691701

702+
// Handle inner node_modules deletions first
703+
for (const dstDir of innerDeleteList)
704+
await deleteModule(dstDir);
705+
await Promise.all(deleteQueue);
706+
deleteQueue.length = 0;
707+
692708
for (const dstDir of deleteList)
693709
await deleteModule(dstDir);
694710

695711
// Update changed locations
696-
const addList: Array<{srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType, keepNodeModules: boolean}> = [];
712+
const addList: Array<{srcDir: PortablePath, dstDir: PortablePath, linkType: LinkType}> = [];
697713
for (const [prevLocator, {locations}] of preinstallState.locatorMap.entries()) {
698714
for (const location of locations) {
699715
const {locationRoot, segments} = parseLocation(location, {
@@ -715,9 +731,9 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
715731
const srcDir = info.target;
716732
const dstDir = curLocation;
717733
const linkType = info.linkType;
718-
const keepNodeModules = node.children.size > 0;
719734
if (srcDir !== dstDir) {
720-
addList.push({srcDir, dstDir, linkType, keepNodeModules});
735+
deleteList.add(dstDir);
736+
addList.push({srcDir, dstDir, linkType});
721737
}
722738
}
723739
}
@@ -747,13 +763,15 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
747763
node = node!.children.get(segment);
748764

749765
if (!prevTreeNode) {
750-
addList.push({srcDir, dstDir, linkType, keepNodeModules: node!.children.size > 0});
766+
deleteList.add(dstDir);
767+
addList.push({srcDir, dstDir, linkType});
751768
} else {
752769
for (const segment of segments) {
753770
curLocation = ppath.join(curLocation, segment);
754771
prevTreeNode = prevTreeNode.children.get(segment);
755772
if (!prevTreeNode) {
756-
addList.push({srcDir, dstDir, linkType, keepNodeModules: node!.children.size > 0});
773+
deleteList.add(dstDir);
774+
addList.push({srcDir, dstDir, linkType});
757775
break;
758776
}
759777
}
@@ -765,18 +783,15 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
765783
const reportedProgress = report.reportProgress(progress);
766784

767785
try {
768-
const persistedLocations = new Map<PortablePath, {
769-
dstDir: PortablePath,
770-
keepNodeModules: boolean,
771-
}>();
786+
const persistedLocations = new Map<PortablePath, PortablePath>();
772787

773788
// For the first pass we'll only want to install a single copy for each
774789
// source directory. We'll later use the resulting install directories for
775790
// the other instances of the same package (this will avoid us having to
776791
// crawl the zip archives for each package).
777792
for (const entry of addList) {
778793
if (entry.linkType === LinkType.SOFT || !persistedLocations.has(entry.srcDir)) {
779-
persistedLocations.set(entry.srcDir, {dstDir: entry.dstDir, keepNodeModules: entry.keepNodeModules});
794+
persistedLocations.set(entry.srcDir, entry.dstDir);
780795
await addModule({...entry});
781796
}
782797
}
@@ -787,9 +802,9 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
787802

788803
// Second pass: clone module duplicates
789804
for (const entry of addList) {
790-
const locationInfo = persistedLocations.get(entry.srcDir)!;
791-
if (entry.linkType !== LinkType.SOFT && entry.dstDir !== locationInfo.dstDir) {
792-
await cloneModule(locationInfo.dstDir, entry.dstDir, {keepSrcNodeModules: locationInfo.keepNodeModules, keepDstNodeModules: entry.keepNodeModules});
805+
const persistedDir = persistedLocations.get(entry.srcDir)!;
806+
if (entry.linkType !== LinkType.SOFT && entry.dstDir !== persistedDir) {
807+
await cloneModule(persistedDir, entry.dstDir);
793808
}
794809
}
795810

0 commit comments

Comments
 (0)