Skip to content

Commit b3f141f

Browse files
vzaidmanfacebook-github-bot
authored andcommitted
allow waiting for metro to be torn down
Summary: X-link: facebook/react-native#46620 The following error was thrown when the test `packages/metro/src/integration_tests/__tests__/server-test.js` was running on Metro: {F1816961963} It led me to investigate why don't we wait for Metro to be torn down. Currently we close metro by closing the http server that it launches. ``` const httpServer = await Metro.runServer(/* ... */); httpServer.close(callback); ``` While we can listen to the callback fired when the server is closed, it only covers one of the systems running internally in metro. The systems that are not covered are: * File watchers * File map workers * Dependency graph * Bundler And many systems that were themselves listening to the above like "eslint file map" or the "dependency analysis". **These systems are closed by us _after_ the server is closed.** This means that a listener to `server.on('close'` would only get the indication where these systems has started to close rather than actually got closed. https://www.internalfb.com/code/fbsource/[17e03bc6bd86]/xplat/js/tools/metro/packages/metro/src/index.flow.js?lines=359-361 This diff introduces a way to wait for all of metro to be closed. In this diff I use that new way of listening to Metro closure to get rid of the jest test warning mentioned above in `packages/metro/src/integration_tests/__tests__/server-test.js`: ``` let serverClosedPromise; beforeEach(async () => { config = await Metro.loadConfig({ config: require.resolve('../metro.config.js'), }); let onCloseResolve; serverClosedPromise = new Promise(resolve => (onCloseResolve = resolve)); httpServer = await Metro.runServer(config, { reporter: {update() {}}, onClose: () => { onCloseResolve(); }, }); }); afterEach(async () => { httpServer.close(); await serverClosedPromise; }); ``` Changelog: [Feature] add `onClose` to `Metro.runServer` configuration allowing to wait for metro and all associated processes to be closed. Reviewed By: huntie Differential Revision: D61594124 fbshipit-source-id: e3c50ef986077503bce0caa42a9f9430efc65272
1 parent de641a6 commit b3f141f

File tree

16 files changed

+258
-91
lines changed

16 files changed

+258
-91
lines changed

docs/GettingStarted.md

+11
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ We recommend using `runMetro` instead of `runServer`, `runMetro` calls this func
8383

8484
* `host (string)`: Where to host the server on.
8585
* `onReady (Function)`: Called when the server is ready to serve requests.
86+
* `onClose (Function)`: Called when the server and all other Metro processes are closed. You can also observe the server close event directly using `httpServer.on('close', () => {});`.
87+
88+
```js
89+
const config = await Metro.loadConfig();
90+
91+
const metroHttpServer = await Metro.runServer(config, {
92+
onClose: () => {console.log('metro server and all associated processes are closed')}
93+
});
94+
95+
httpServer.on('close', () => {console.log('metro server is closed')});
96+
```
8697
* `secure (boolean)`: **DEPRECATED** Whether the server should run on `https` instead of `http`.
8798
* `secureKey (string)`: **DEPRECATED** The key to use for `https` when `secure` is on.
8899
* `secureCert (string)`: **DEPRECATED** The cert to use for `https` when `secure` is on.

packages/metro-file-map/src/index.js

+6-8
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ export default class FileMap extends EventEmitter {
736736
try {
737737
await Promise.all(promises);
738738
} finally {
739-
this._cleanup();
739+
await this._cleanup();
740740
}
741741
this._startupPerfLogger?.point('applyFileDelta_process_end');
742742
this._startupPerfLogger?.point('applyFileDelta_add_start');
@@ -758,12 +758,11 @@ export default class FileMap extends EventEmitter {
758758
this._startupPerfLogger?.point('applyFileDelta_end');
759759
}
760760

761-
_cleanup() {
761+
async _cleanup() {
762762
const worker = this._worker;
763763

764764
if (worker && typeof worker.end === 'function') {
765-
// $FlowFixMe[unused-promise]
766-
worker.end();
765+
await worker.end();
767766
}
768767

769768
this._worker = null;
@@ -1113,12 +1112,11 @@ export default class FileMap extends EventEmitter {
11131112
clearInterval(this._healthCheckInterval);
11141113
}
11151114

1115+
await this._cleanup();
1116+
11161117
this._crawlerAbortController.abort();
11171118

1118-
if (!this._watcher) {
1119-
return;
1120-
}
1121-
await this._watcher.close();
1119+
await this._watcher?.close();
11221120
}
11231121

11241122
/**

packages/metro-file-map/src/watchers/FSEventsWatcher.js

+32-20
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default class FSEventsWatcher extends EventEmitter {
5959
+dot: boolean;
6060
+doIgnore: (path: string) => boolean;
6161
+fsEventsWatchStopper: () => Promise<void>;
62+
+watcherInitialReaddirPromise: Promise<void>;
6263
_tracked: Set<string>;
6364

6465
static isSupported(): boolean {
@@ -77,8 +78,7 @@ export default class FSEventsWatcher extends EventEmitter {
7778
dirCallback: (normalizedPath: string, stats: Stats) => void,
7879
fileCallback: (normalizedPath: string, stats: Stats) => void,
7980
symlinkCallback: (normalizedPath: string, stats: Stats) => void,
80-
// $FlowFixMe[unclear-type] Add types for callback
81-
endCallback: Function,
81+
endCallback: () => void,
8282
// $FlowFixMe[unclear-type] Add types for callback
8383
errorCallback: Function,
8484
ignored?: Matcher,
@@ -91,9 +91,7 @@ export default class FSEventsWatcher extends EventEmitter {
9191
.on('file', FSEventsWatcher._normalizeProxy(fileCallback))
9292
.on('symlink', FSEventsWatcher._normalizeProxy(symlinkCallback))
9393
.on('error', errorCallback)
94-
.on('end', () => {
95-
endCallback();
96-
});
94+
.on('end', endCallback);
9795
}
9896

9997
constructor(
@@ -132,29 +130,43 @@ export default class FSEventsWatcher extends EventEmitter {
132130
const trackPath = (filePath: string) => {
133131
this._tracked.add(filePath);
134132
};
135-
FSEventsWatcher._recReaddir(
136-
this.root,
137-
trackPath,
138-
trackPath,
139-
trackPath,
140-
// $FlowFixMe[method-unbinding] - Refactor
141-
this.emit.bind(this, 'ready'),
142-
// $FlowFixMe[method-unbinding] - Refactor
143-
this.emit.bind(this, 'error'),
144-
this.ignored,
145-
);
133+
this.watcherInitialReaddirPromise = new Promise(resolve => {
134+
FSEventsWatcher._recReaddir(
135+
this.root,
136+
trackPath,
137+
trackPath,
138+
trackPath,
139+
() => {
140+
this.emit('ready');
141+
resolve();
142+
},
143+
(...args) => {
144+
this.emit('error', ...args);
145+
resolve();
146+
},
147+
this.ignored,
148+
);
149+
});
146150
}
147151

148152
/**
149153
* End watching.
150154
*/
151155
async close(callback?: () => void): Promise<void> {
156+
await this.watcherInitialReaddirPromise;
152157
await this.fsEventsWatchStopper();
153158
this.removeAllListeners();
154-
if (typeof callback === 'function') {
155-
// $FlowFixMe[extra-arg] - Is this a Node-style callback or as typed?
156-
process.nextTick(callback.bind(null, null, true));
157-
}
159+
160+
await new Promise(resolve => {
161+
// it takes around 100ms for fsevents to release its resounces after
162+
// watching is stopped. See __tests__/server-torn-down-test.js
163+
setTimeout(() => {
164+
if (typeof callback === 'function') {
165+
callback();
166+
}
167+
resolve();
168+
}, 100);
169+
});
158170
}
159171

160172
async _handleEvent(filepath: string) {

packages/metro-file-map/src/watchers/NodeWatcher.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,24 @@ module.exports = class NodeWatcher extends EventEmitter {
184184
/**
185185
* Stop watching a directory.
186186
*/
187-
_stopWatching(dir: string) {
187+
async _stopWatching(dir: string): Promise<void> {
188188
if (this.watched[dir]) {
189-
this.watched[dir].close();
190-
delete this.watched[dir];
189+
await new Promise(resolve => {
190+
this.watched[dir].once('close', () => process.nextTick(resolve));
191+
this.watched[dir].close();
192+
delete this.watched[dir];
193+
});
191194
}
192195
}
193196

194197
/**
195198
* End watching.
196199
*/
197200
async close(): Promise<void> {
198-
Object.keys(this.watched).forEach(dir => this._stopWatching(dir));
201+
const promises = Object.keys(this.watched).map(dir =>
202+
this._stopWatching(dir),
203+
);
204+
await Promise.all(promises);
199205
this.removeAllListeners();
200206
}
201207

@@ -345,11 +351,11 @@ module.exports = class NodeWatcher extends EventEmitter {
345351
return;
346352
}
347353
this._unregister(fullPath);
348-
this._stopWatching(fullPath);
349354
this._unregisterDir(fullPath);
350355
if (registered) {
351356
this._emitEvent(DELETE_EVENT, relativePath);
352357
}
358+
await this._stopWatching(fullPath);
353359
}
354360
}
355361

packages/metro-memory-fs/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1713,6 +1713,7 @@ class FSWatcher extends EventEmitter {
17131713
close() {
17141714
this._node.watchers.splice(this._node.watchers.indexOf(this._nodeWatcher));
17151715
clearInterval(this._persistIntervalId);
1716+
this.emit('close');
17161717
}
17171718

17181719
_listener = (eventType: 'change' | 'rename', filePath: string) => {

packages/metro/src/Bundler.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export type BundlerOptions = $ReadOnly<{
2626

2727
class Bundler {
2828
_depGraph: DependencyGraph;
29-
_readyPromise: Promise<void>;
29+
_initializedPromise: Promise<void>;
3030
_transformer: Transformer;
3131

3232
constructor(config: ConfigT, options?: BundlerOptions) {
3333
this._depGraph = new DependencyGraph(config, options);
3434

35-
this._readyPromise = this._depGraph
35+
this._initializedPromise = this._depGraph
3636
.ready()
3737
.then(() => {
3838
config.reporter.update({type: 'transformer_load_started'});
@@ -55,14 +55,15 @@ class Bundler {
5555
}
5656

5757
async end(): Promise<void> {
58-
await this._depGraph.ready();
58+
await this.ready();
5959

60-
this._transformer.end();
61-
this._depGraph.end();
60+
await this._transformer.end();
61+
await this._depGraph.end();
6262
}
6363

6464
async getDependencyGraph(): Promise<DependencyGraph> {
65-
await this._depGraph.ready();
65+
await this.ready();
66+
6667
return this._depGraph;
6768
}
6869

@@ -74,7 +75,7 @@ class Bundler {
7475
): Promise<TransformResultWithSource<>> {
7576
// We need to be sure that the DependencyGraph has been initialized.
7677
// TODO: Remove this ugly hack!
77-
await this._depGraph.ready();
78+
await this.ready();
7879

7980
return this._transformer.transformFile(
8081
filePath,
@@ -85,7 +86,7 @@ class Bundler {
8586

8687
// Waits for the bundler to become ready.
8788
async ready(): Promise<void> {
88-
await this._readyPromise;
89+
await this._initializedPromise;
8990
}
9091
}
9192

packages/metro/src/DeltaBundler/Transformer.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,8 @@ class Transformer {
188188
};
189189
}
190190

191-
end(): void {
192-
// $FlowFixMe[unused-promise]
193-
this._workerFarm.kill();
191+
async end(): Promise<void> {
192+
await this._workerFarm.kill();
194193
}
195194
}
196195

packages/metro/src/DeltaBundler/__tests__/resolver-test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ function dep(name: string): TransformResultDependency {
316316
type: 'sourceFile',
317317
filePath: p('/root/a.another'),
318318
});
319-
end();
319+
await end();
320320

321321
resolver = await createResolver({
322322
resolver: {sourceExts: ['js', 'another']},
@@ -1508,7 +1508,7 @@ function dep(name: string): TransformResultDependency {
15081508
type: 'sourceFile',
15091509
filePath: p('/root/foo.playstation.js'),
15101510
});
1511-
end();
1511+
await end();
15121512

15131513
resolver = await createResolver(
15141514
{resolver: {platforms: ['playstation']}},
@@ -2181,7 +2181,7 @@ function dep(name: string): TransformResultDependency {
21812181
type: 'sourceFile',
21822182
filePath: p('/root/hasteModule.ios.js'),
21832183
});
2184-
end();
2184+
await end();
21852185

21862186
resolver = await createResolver(config, 'android');
21872187
expect(
@@ -2204,7 +2204,7 @@ function dep(name: string): TransformResultDependency {
22042204
type: 'sourceFile',
22052205
filePath: p('/root/hasteModule.ios.js'),
22062206
});
2207-
end();
2207+
await end();
22082208

22092209
resolver = await createResolver(config, 'android');
22102210
expect(

packages/metro/src/IncrementalBundler.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,9 @@ class IncrementalBundler {
7979
this._deltaBundler = new DeltaBundler(this._bundler.getWatcher());
8080
}
8181

82-
end(): void {
82+
async end(): Promise<void> {
8383
this._deltaBundler.end();
84-
// $FlowFixMe[unused-promise]
85-
this._bundler.end();
84+
await this._bundler.end();
8685
}
8786

8887
getBundler(): Bundler {

packages/metro/src/Server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ class Server {
196196
this._nextBundleBuildNumber = 1;
197197
}
198198

199-
end() {
199+
async end() {
200200
if (!this._isEnded) {
201-
this._bundler.end();
201+
await this._bundler.end();
202202
this._isEnded = true;
203203
}
204204
}

packages/metro/src/commands/dependencies.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async function dependencies(args: Args, config: ConfigT) {
7474
}
7575
});
7676

77-
server.end();
77+
await server.end();
7878
return args.output != null
7979
? // $FlowFixMe[method-unbinding]
8080
promisify(outStream.end).bind(outStream)()

0 commit comments

Comments
 (0)