diff --git a/flow-typed/fb-watchman.js b/flow-typed/fb-watchman.js index 38b6ea89b5..2f722eeda9 100644 --- a/flow-typed/fb-watchman.js +++ b/flow-typed/fb-watchman.js @@ -19,6 +19,7 @@ declare module 'fb-watchman' { declare type WatchmanSubscribeResponse = $ReadOnly<{ subscribe: string, warning?: string, + 'asserted-states': $ReadOnlyArray, ... }>; diff --git a/packages/metro-file-map/src/Watcher.js b/packages/metro-file-map/src/Watcher.js index d4763a5ab1..48904416cf 100644 --- a/packages/metro-file-map/src/Watcher.js +++ b/packages/metro-file-map/src/Watcher.js @@ -55,6 +55,7 @@ type WatcherOptions = { }; interface WatcherBackend { + getPauseReason(): ?string; close(): Promise; } @@ -63,7 +64,7 @@ let nextInstanceId = 0; export type HealthCheckResult = | {type: 'error', timeout: number, error: Error, watcher: ?string} | {type: 'success', timeout: number, timeElapsed: number, watcher: ?string} - | {type: 'timeout', timeout: number, watcher: ?string}; + | {type: 'timeout', timeout: number, watcher: ?string, pauseReason: ?string}; export class Watcher { _options: WatcherOptions; @@ -249,13 +250,14 @@ export class Watcher { '-' + healthCheckId; const healthCheckPath = path.join(this._options.rootDir, basename); - let result; + let result: ?HealthCheckResult; const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeout), ).then(() => { if (!result) { result = { type: 'timeout', + pauseReason: this._backends[0]?.getPauseReason(), timeout, watcher, }; diff --git a/packages/metro-file-map/src/watchers/FSEventsWatcher.js b/packages/metro-file-map/src/watchers/FSEventsWatcher.js index 6dbd77af96..1a69cf15e3 100644 --- a/packages/metro-file-map/src/watchers/FSEventsWatcher.js +++ b/packages/metro-file-map/src/watchers/FSEventsWatcher.js @@ -204,4 +204,8 @@ export default class FSEventsWatcher extends EventEmitter { this.emit(type, file, this.root, stat); this.emit(ALL_EVENT, type, file, this.root, stat); } + + getPauseReason(): ?string { + return null; + } } diff --git a/packages/metro-file-map/src/watchers/NodeWatcher.js b/packages/metro-file-map/src/watchers/NodeWatcher.js index 76a2dd63f2..c96c30dcb6 100644 --- a/packages/metro-file-map/src/watchers/NodeWatcher.js +++ b/packages/metro-file-map/src/watchers/NodeWatcher.js @@ -330,6 +330,10 @@ module.exports = class NodeWatcher extends EventEmitter { this.emit(type, file, this.root, stat); this.emit(ALL_EVENT, type, file, this.root, stat); } + + getPauseReason(): ?string { + return null; + } }; /** * Determine if a given FS error can be ignored diff --git a/packages/metro-file-map/src/watchers/WatchmanWatcher.js b/packages/metro-file-map/src/watchers/WatchmanWatcher.js index 1d25d0fe43..84e1f60c6b 100644 --- a/packages/metro-file-map/src/watchers/WatchmanWatcher.js +++ b/packages/metro-file-map/src/watchers/WatchmanWatcher.js @@ -55,6 +55,7 @@ export default class WatchmanWatcher extends EventEmitter { root: string, }>; watchmanDeferStates: $ReadOnlyArray; + #deferringStates: Set = new Set(); constructor(dir: string, opts: WatcherOptions) { super(); @@ -157,7 +158,7 @@ export default class WatchmanWatcher extends EventEmitter { ); } - function onSubscribe(error: ?Error, resp: WatchmanSubscribeResponse) { + const onSubscribe = (error: ?Error, resp: WatchmanSubscribeResponse) => { if (handleError(self, error)) { return; } @@ -165,8 +166,12 @@ export default class WatchmanWatcher extends EventEmitter { handleWarning(resp); + for (const state of resp['asserted-states']) { + this.#deferringStates.add(state); + } + self.emit('ready'); - } + }; self.client.command(['watch-project', getWatchRoot()], onWatchProject); } @@ -199,22 +204,25 @@ export default class WatchmanWatcher extends EventEmitter { if (Array.isArray(resp.files)) { resp.files.forEach(change => this._handleFileChange(change)); } + const {'state-enter': stateEnter, 'state-leave': stateLeave} = resp; if ( - resp['state-enter'] != null && - (this.watchmanDeferStates ?? []).includes(resp['state-enter']) + stateEnter != null && + (this.watchmanDeferStates ?? []).includes(stateEnter) ) { + this.#deferringStates.add(stateEnter); debug( 'Watchman reports "%s" just started. Filesystem notifications are paused.', - resp['state-enter'], + stateEnter, ); } if ( - resp['state-leave'] != null && - (this.watchmanDeferStates ?? []).includes(resp['state-leave']) + stateLeave != null && + (this.watchmanDeferStates ?? []).includes(stateLeave) ) { + this.#deferringStates.delete(stateLeave); debug( 'Watchman reports "%s" ended. Filesystem notifications resumed.', - resp['state-leave'], + stateLeave, ); } } @@ -295,6 +303,21 @@ export default class WatchmanWatcher extends EventEmitter { async close() { this.client.removeAllListeners(); this.client.end(); + this.#deferringStates.clear(); + } + + getPauseReason(): ?string { + if (this.#deferringStates.size) { + const states = [...this.#deferringStates]; + if (states.length === 1) { + return `The watch is in the '${states[0]}' state.`; + } + return `The watch is in the ${states + .slice(0, -1) + .map(s => `'${s}'`) + .join(', ')} and '${states[states.length - 1]}' states.`; + } + return null; } } diff --git a/packages/metro/src/lib/TerminalReporter.js b/packages/metro/src/lib/TerminalReporter.js index 0b4b5da645..7fcb5ce16a 100644 --- a/packages/metro/src/lib/TerminalReporter.js +++ b/packages/metro/src/lib/TerminalReporter.js @@ -437,7 +437,11 @@ class TerminalReporter { // Don't be spammy; only report changes in status. if ( !this._prevHealthCheckResult || - result.type !== this._prevHealthCheckResult.type + result.type !== this._prevHealthCheckResult.type || + (result.type === 'timeout' && + this._prevHealthCheckResult.type === 'timeout' && + (result.pauseReason ?? null) !== + (this._prevHealthCheckResult.pauseReason ?? null)) ) { const watcherName = "'" + (result.watcher ?? 'unknown') + "'"; switch (result.type) { @@ -456,9 +460,14 @@ class TerminalReporter { ); break; case 'timeout': + const why = + result.pauseReason != null + ? ` This may be because: ${result.pauseReason}` + : ''; reporting.logWarning( this.terminal, - `Watcher ${watcherName} failed to detect a file change within ${result.timeout}ms.`, + `Watcher ${watcherName} failed to detect a file change within ${result.timeout}ms.` + + why, ); break; }