|
8 | 8 | * @flow strict-local |
9 | 9 | */ |
10 | 10 |
|
11 | | -import type {FSWatcher} from 'fs'; |
| 11 | +import type {Dirent, FSWatcher} from 'fs'; |
12 | 12 |
|
13 | 13 | import {AbstractWatcher} from './AbstractWatcher'; |
14 | 14 | import {includedByGlob, typeFromStat} from './common'; |
@@ -95,6 +95,42 @@ export default class NativeWatcher extends AbstractWatcher { |
95 | 95 | } |
96 | 96 | } |
97 | 97 |
|
| 98 | + async _handleHardlinkDirectory(relativePath: string) { |
| 99 | + debug( |
| 100 | + 'Crawling hardlinked directory %s (root: %s)', |
| 101 | + relativePath, |
| 102 | + this.root, |
| 103 | + ); |
| 104 | + const direntQueue = [relativePath]; |
| 105 | + let readdirPath: ?string; |
| 106 | + while ((readdirPath = direntQueue.pop()) != null) { |
| 107 | + if (readdirPath == null) { |
| 108 | + return; |
| 109 | + } |
| 110 | + let entries: $ReadOnlyArray<Dirent>; |
| 111 | + try { |
| 112 | + entries = await fsPromises.readdir(readdirPath, {withFileTypes: true}); |
| 113 | + } catch (error) { |
| 114 | + this.emitError(error); |
| 115 | + continue; |
| 116 | + } |
| 117 | + for (const dirent of entries) { |
| 118 | + if (dirent.isDirectory()) { |
| 119 | + // We can ignore directories, to avoid triggering nested hardlink |
| 120 | + // handling, but also since the parent FileMap ultimately discards |
| 121 | + // directory events anyway |
| 122 | + direntQueue.push(path.join(readdirPath, dirent.name.toString())); |
| 123 | + } else if (dirent.isFile() || dirent.isSymbolicLink()) { |
| 124 | + this._handleEvent( |
| 125 | + path.join(readdirPath, dirent.name.toString()), |
| 126 | + ).catch(error => { |
| 127 | + this.emitError(error); |
| 128 | + }); |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
98 | 134 | async _handleEvent(relativePath: string) { |
99 | 135 | const absolutePath = path.resolve(this.root, relativePath); |
100 | 136 | if (this.doIgnore(relativePath)) { |
@@ -125,6 +161,14 @@ export default class NativeWatcher extends AbstractWatcher { |
125 | 161 | size: stat.size, |
126 | 162 | }, |
127 | 163 | }); |
| 164 | + |
| 165 | + // If we have a hardlink on a copy-on-write file system (indicated by nlink i.e. multiple links to same inode) |
| 166 | + // then we should manually crawl the entire directory, since we're not expecting events for files inside it |
| 167 | + if (type === 'd' && stat.nlink > 1) { |
| 168 | + this._handleHardlinkDirectory(relativePath).catch(error => { |
| 169 | + this.emitError(error); |
| 170 | + }); |
| 171 | + } |
128 | 172 | } catch (error) { |
129 | 173 | if (error?.code !== 'ENOENT') { |
130 | 174 | this.emitError(error); |
|
0 commit comments