Skip to content

Commit a47f209

Browse files
authored
fix(core): close stale watcher after shortcut restart (#8153)
1 parent ba3eabc commit a47f209

6 files changed

Lines changed: 80 additions & 11 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { runCLI } from '@rsbuild/core';
2+
3+
process.stdin.isTTY = true;
4+
delete process.env.CI;
5+
6+
runCLI({ argv: ['node', 'rsbuild', 'dev'] });
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { expect, test } from '@e2e/helper';
4+
import { getRandomPort } from '@rstackjs/test-utils';
5+
6+
const watchedFile = path.join(import.meta.dirname, 'test-temp-watch.txt');
7+
const restartLog = 'restarting server as test-temp-watch.txt changed';
8+
9+
test.beforeEach(() => {
10+
fs.writeFileSync(watchedFile, '1');
11+
});
12+
13+
test.afterAll(() => {
14+
fs.rmSync(watchedFile, { force: true });
15+
});
16+
17+
test('should close the old watcher after a shortcut restart', async ({ exec, logHelper }) => {
18+
const port = await getRandomPort();
19+
const { childProcess } = exec('node ./dev.js', {
20+
env: {
21+
PORT: String(port),
22+
},
23+
});
24+
const { clearLogs, expectBuildEnd, expectLog, logs } = logHelper;
25+
26+
await expectBuildEnd();
27+
clearLogs();
28+
childProcess.stdin?.write('r\n');
29+
await expectLog('restarting server');
30+
await expectBuildEnd();
31+
32+
clearLogs();
33+
fs.writeFileSync(watchedFile, '2');
34+
await expectLog(restartLog);
35+
await expectBuildEnd();
36+
37+
expect(logs.filter((log) => log.includes(restartLog))).toHaveLength(1);
38+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default {
2+
dev: {
3+
watchFiles: {
4+
paths: './test-temp-watch.txt',
5+
type: 'restart',
6+
},
7+
},
8+
server: {
9+
port: Number(process.env.PORT),
10+
},
11+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('shortcut restart watcher');

packages/core/src/build.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export const build = async (
6565

6666
let closingPromise: Promise<void> | undefined;
6767
let unregisterRestart: (() => void) | undefined;
68+
69+
// Keep the restart watcher active when closing build resources,
70+
// so failed restarts can be retried.
6871
const closeBuild = () => {
6972
closingPromise ||= (async () => {
7073
unregisterRestart?.();
@@ -77,7 +80,6 @@ export const build = async (
7780

7881
let restartWatcher: ReturnType<typeof watchFilesForRestart>;
7982
if (watch) {
80-
// Only close build resources before restart; keep the watcher alive for retries.
8183
unregisterRestart = context.restartManager.registerCleanup(closeBuild);
8284
restartWatcher = watchFilesForRestart({
8385
watchFiles: context.normalizedConfig!.dev.watchFiles,
@@ -86,6 +88,7 @@ export const build = async (
8688
});
8789
}
8890

91+
// Fully close the build and its restart watcher.
8992
const close = async () => {
9093
await restartWatcher?.close();
9194
await closeBuild();

packages/core/src/server/devServer.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ export async function createDevServer<
203203
let closingPromise: Promise<void> | undefined;
204204
let unregisterRestart: (() => void) | undefined;
205205

206+
// Keep the restart watcher active when closing server resources,
207+
// so failed restarts can be retried.
206208
const closeServerResources = () => {
207209
if (!closingPromise) {
208210
unregisterRestart?.();
@@ -217,11 +219,28 @@ export async function createDevServer<
217219
return closingPromise;
218220
};
219221

222+
// Fully close the server and its restart watcher.
220223
const closeServer = async () => {
221224
await state.restartWatcher?.close();
222225
await closeServerResources();
223226
};
224227

228+
// Request a manual restart and close the old watcher only after it succeeds.
229+
const restartServer = async () => {
230+
const restarted = await requestRestart({
231+
action: 'dev',
232+
clear: false,
233+
logger,
234+
restartManager: context.restartManager,
235+
});
236+
237+
if (restarted) {
238+
await state.restartWatcher?.close();
239+
}
240+
241+
return restarted;
242+
};
243+
225244
if (!middlewareMode) {
226245
registerCleanup(closeServer);
227246
}
@@ -237,15 +256,7 @@ export async function createDevServer<
237256
openPage,
238257
closeServer,
239258
printUrls,
240-
restartServer: context.restartManager.canRestart
241-
? () =>
242-
requestRestart({
243-
action: 'dev',
244-
clear: false,
245-
logger,
246-
restartManager: context.restartManager,
247-
})
248-
: undefined,
259+
restartServer: context.restartManager.canRestart ? restartServer : undefined,
249260
help: shortcutsOptions.help,
250261
customShortcuts: shortcutsOptions.custom,
251262
logger,
@@ -440,7 +451,6 @@ export async function createDevServer<
440451
// start watching
441452
state.buildManager?.watch();
442453

443-
// Only close server resources before restart; keep the watcher alive for retries.
444454
unregisterRestart = context.restartManager.registerCleanup(closeServerResources);
445455
state.restartWatcher = watchFilesForRestart({
446456
watchFiles: config.dev.watchFiles,

0 commit comments

Comments
 (0)