Skip to content

Commit 1147dac

Browse files
committed
fix: harden web ui restart handling
1 parent f47a82d commit 1147dac

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

cli.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9094,6 +9094,7 @@ function createSerializedWebUiRestartHandler(runRestart) {
90949094
restartInFlight = null;
90959095
if (restartQueued) {
90969096
restartInFlight = drainRestartQueue();
9097+
return restartInFlight;
90979098
}
90989099
}
90999100
};
@@ -9129,7 +9130,7 @@ async function restartWebUiServerAfterFrontendChange({
91299130
await new Promise((resolve) => wait(resolve, delayMs));
91309131

91319132
try {
9132-
const nextServerHandle = createServer(serverOptions);
9133+
const nextServerHandle = await createServer(serverOptions);
91339134
logger.log('✓ 已重启 Web UI 服务\n');
91349135
return nextServerHandle;
91359136
} catch (e) {

tests/unit/web-ui-restart.test.mjs

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ test('restartWebUiServerAfterFrontendChange waits 3 seconds after stop before re
5454
const events = [];
5555
const nextServerHandle = { stop: async () => {} };
5656
let resolveStop = null;
57+
let resolveDelay = null;
5758
const stopDone = new Promise((resolve) => {
5859
resolveStop = resolve;
5960
});
61+
const delayDone = new Promise((resolve) => {
62+
resolveDelay = resolve;
63+
});
6064
const currentServerHandle = {
6165
stop: () => {
6266
events.push('stop:start');
@@ -90,8 +94,9 @@ test('restartWebUiServerAfterFrontendChange waits 3 seconds after stop before re
9094
},
9195
wait: (callback, ms) => {
9296
events.push(`wait:${ms}`);
93-
callback();
94-
return 1;
97+
return delayDone.then(() => {
98+
callback();
99+
});
95100
},
96101
logger: {
97102
log: () => {},
@@ -102,6 +107,10 @@ test('restartWebUiServerAfterFrontendChange waits 3 seconds after stop before re
102107

103108
assert.deepStrictEqual(events, ['stop:start']);
104109
resolveStop();
110+
await Promise.resolve();
111+
await Promise.resolve();
112+
assert.deepStrictEqual(events, ['stop:start', 'stop:done', 'wait:3000']);
113+
resolveDelay();
105114
const result = await pending;
106115

107116
assert.strictEqual(result, nextServerHandle);
@@ -113,6 +122,40 @@ test('restartWebUiServerAfterFrontendChange waits 3 seconds after stop before re
113122
]);
114123
});
115124

125+
test('restartWebUiServerAfterFrontendChange catches async createServer failures', async () => {
126+
const currentServerHandle = { stop: async () => {} };
127+
const loggedErrors = [];
128+
129+
const result = await restartWebUiServerAfterFrontendChange({
130+
serverHandle: currentServerHandle,
131+
serverOptions: {
132+
htmlPath: '/tmp/index.html',
133+
assetsDir: '/tmp/res',
134+
webDir: '/tmp/web-ui',
135+
host: '127.0.0.1',
136+
port: 3737,
137+
openBrowser: false
138+
},
139+
createServer: async () => {
140+
throw new Error('async create failed');
141+
},
142+
wait: (callback) => {
143+
callback();
144+
return 1;
145+
},
146+
logger: {
147+
log: () => {},
148+
warn: () => {},
149+
error: (...args) => {
150+
loggedErrors.push(args);
151+
}
152+
}
153+
});
154+
155+
assert.strictEqual(result, currentServerHandle);
156+
assert.deepStrictEqual(loggedErrors, [['! 重启失败:', 'async create failed']]);
157+
});
158+
116159
test('createSerializedWebUiRestartHandler coalesces overlapping restarts to the latest change', async () => {
117160
const events = [];
118161
let restartCount = 0;
@@ -151,3 +194,40 @@ test('createSerializedWebUiRestartHandler coalesces overlapping restarts to the
151194
'done:third.js'
152195
]);
153196
});
197+
198+
test('createSerializedWebUiRestartHandler chains queued callers to the retry after a failure', async () => {
199+
const events = [];
200+
let restartCount = 0;
201+
let rejectFirstRestart = null;
202+
const firstRestartDone = new Promise((_, reject) => {
203+
rejectFirstRestart = reject;
204+
});
205+
206+
const requestRestart = createSerializedWebUiRestartHandler(async (info) => {
207+
restartCount += 1;
208+
const label = info && info.filename ? info.filename : 'unknown';
209+
events.push(`start:${label}`);
210+
if (restartCount === 1) {
211+
await firstRestartDone;
212+
return;
213+
}
214+
events.push(`done:${label}`);
215+
});
216+
217+
const first = requestRestart({ filename: 'first.js' });
218+
const second = requestRestart({ filename: 'second.js' });
219+
220+
await Promise.resolve();
221+
222+
assert.strictEqual(first, second);
223+
assert.deepStrictEqual(events, ['start:first.js']);
224+
225+
rejectFirstRestart(new Error('first restart failed'));
226+
await first;
227+
228+
assert.deepStrictEqual(events, [
229+
'start:first.js',
230+
'start:second.js',
231+
'done:second.js'
232+
]);
233+
});

0 commit comments

Comments
 (0)