Skip to content

Commit 1c70b86

Browse files
authored
fix(dart_frog_cli): prevent staggered devserver kills (#1048)
1 parent 21ecbe9 commit 1c70b86

File tree

7 files changed

+188
-44
lines changed

7 files changed

+188
-44
lines changed

packages/dart_frog_cli/e2e/test/daemon/dev_server_domain_test.dart

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,32 @@ void main() {
244244
);
245245
});
246246

247-
test('stop a dev server on project 1', () async {
248-
final response = await daemonStdio.sendDaemonRequest(
249-
DaemonRequest(
250-
id: '${++requestCount}',
251-
domain: 'dev_server',
252-
method: 'stop',
253-
params: {
254-
'applicationId': project1Server1Id,
255-
},
247+
test('try staggered-stop a dev server on project 1', () async {
248+
final (response1, response2) =
249+
await daemonStdio.sendStaggeredDaemonRequest(
250+
(
251+
DaemonRequest(
252+
id: '${++requestCount}',
253+
domain: 'dev_server',
254+
method: 'stop',
255+
params: {
256+
'applicationId': project1Server1Id,
257+
},
258+
),
259+
DaemonRequest(
260+
id: '${++requestCount}',
261+
domain: 'dev_server',
262+
method: 'stop',
263+
params: {
264+
'applicationId': project1Server1Id,
265+
},
266+
),
256267
),
257268
);
258269

259-
expect(response.isSuccess, isTrue);
260-
expect(response.result!['exitCode'], equals(0));
270+
expect(response1.isSuccess, isTrue);
271+
expect(response1.result!['exitCode'], equals(0));
272+
expect(response2.isSuccess, isFalse);
261273
});
262274

263275
test('try to stop same dev server again and expect an error', () async {

packages/dart_frog_cli/e2e/test/daemon/route_configuration_domain_test.dart

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,32 @@ void main() {
235235
);
236236
});
237237

238-
test('stop watcher', () async {
239-
final response = await daemonStdio.sendDaemonRequest(
240-
DaemonRequest(
241-
id: '${requestCount++}',
242-
domain: 'route_configuration',
243-
method: 'watcherStop',
244-
params: {
245-
'watcherId': projectWatcherId,
246-
},
238+
test('staggered-stop watcher', () async {
239+
final (response1, response2) =
240+
await daemonStdio.sendStaggeredDaemonRequest(
241+
(
242+
DaemonRequest(
243+
id: '${requestCount++}',
244+
domain: 'route_configuration',
245+
method: 'watcherStop',
246+
params: {
247+
'watcherId': projectWatcherId,
248+
},
249+
),
250+
DaemonRequest(
251+
id: '${requestCount++}',
252+
domain: 'route_configuration',
253+
method: 'watcherStop',
254+
params: {
255+
'watcherId': projectWatcherId,
256+
},
257+
),
247258
),
248259
);
249260

250-
expect(response.isSuccess, isTrue);
251-
expect(response.result?['exitCode'], equals(0));
261+
expect(response1.isSuccess, isTrue);
262+
expect(response1.result?['exitCode'], equals(0));
263+
expect(response2.isSuccess, isFalse);
252264
});
253265
});
254266
}

packages/dart_frog_cli/e2e/test/helpers/dart_frog_daemon.dart

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ class DaemonStdioHelper {
3939

4040
var _pastMessagesCache = <String>[];
4141

42-
Matcher? messageMatcher;
43-
Completer<String>? messageCompleter;
42+
List<Matcher> messageMatchers = [];
43+
List<Completer<String>> messageCompleters = [];
4444

4545
void _handleStdoutLine(String line) {
46-
final messageMatcher = this.messageMatcher;
46+
final messageMatchers = this.messageMatchers;
4747

4848
stdout.writeln('::debug:: <- $line');
49-
if (messageMatcher != null) {
49+
50+
for (final (index, messageMatcher) in messageMatchers.indexed) {
5051
if (messageMatcher.matches(line, {})) {
51-
messageCompleter?.complete(line);
52+
messageCompleters[index].complete(line);
5253
_pastMessagesCache.clear();
5354
return;
5455
}
@@ -57,9 +58,9 @@ class DaemonStdioHelper {
5758
_pastMessagesCache.add(line);
5859
}
5960

60-
void _clean() {
61-
messageMatcher = null;
62-
messageCompleter = null;
61+
void _clean(Matcher messageMatcher, Completer<String>? completer) {
62+
messageMatchers.remove(messageMatcher);
63+
messageCompleters.remove(completer);
6364
}
6465

6566
/// Awaits for a daemon event with the given [methodKey].
@@ -107,9 +108,7 @@ class DaemonStdioHelper {
107108
Matcher messageMatcher, {
108109
Duration timeout = const Duration(seconds: 1),
109110
}) async {
110-
assert(this.messageMatcher == null, 'Already awaiting for a message');
111-
112-
this.messageMatcher = messageMatcher;
111+
messageMatchers.add(messageMatcher);
113112

114113
// Check if there is already a matching message in the cache.
115114
final existingItem = _pastMessagesCache.indexed.where((pair) {
@@ -121,21 +120,23 @@ class DaemonStdioHelper {
121120
// remove all the previous messages from the cache and
122121
// return the matching message.
123122
_pastMessagesCache = _pastMessagesCache.skip(itemIndex + 1).toList();
124-
_clean();
123+
_clean(messageMatcher, null);
125124
return itemValue;
126125
}
127126

128127
// if there is no matching message in the cache,
129128
// create a completer and wait for the message to be received
130129
// or for the timeout to expire.
131130

132-
final messageCompleter = this.messageCompleter = Completer<String>();
131+
final messageCompleter = Completer<String>();
132+
133+
messageCompleters.add(messageCompleter);
133134
final result = await Future.any(<Future<String?>>[
134135
messageCompleter.future,
135136
Future<String?>.delayed(timeout),
136137
]);
137138

138-
_clean();
139+
_clean(messageMatcher, messageCompleter);
139140

140141
if (result == null) {
141142
throw TimeoutException('Timed out waiting for message', timeout);
@@ -176,6 +177,56 @@ class DaemonStdioHelper {
176177
return responseMessage as DaemonResponse;
177178
}
178179

180+
/// Sends two daemon requests to the daemon via its stdin.
181+
///
182+
/// Returns a tuple with the responses or throws a
183+
/// [TimeoutException] if the timeout expires.
184+
Future<(DaemonResponse, DaemonResponse)> sendStaggeredDaemonRequest(
185+
(DaemonRequest, DaemonRequest) requests, {
186+
Duration timeout = const Duration(seconds: 10),
187+
}) async {
188+
final request1 = requests.$1;
189+
final request2 = requests.$2;
190+
191+
final json1 = jsonEncode(request1.toJson());
192+
final json2 = jsonEncode(request2.toJson());
193+
194+
stdout.writeln('::debug:: -> [$json1]');
195+
daemonProcess.stdin.writeln('[$json1]');
196+
stdout.writeln('::debug:: -> [$json2]');
197+
daemonProcess.stdin.writeln('[$json2]');
198+
await daemonProcess.stdin.flush();
199+
200+
final wrappedMatcher1 = isA<DaemonResponse>().having(
201+
(e) => e.id,
202+
'id is ${request1.id}',
203+
request1.id,
204+
);
205+
206+
final wrappedMatcher2 = isA<DaemonResponse>().having(
207+
(e) => e.id,
208+
'id is ${request2.id}',
209+
request2.id,
210+
);
211+
212+
final responseMessage1 = awaitForDaemonMessage(
213+
wrappedMatcher1,
214+
timeout: timeout,
215+
);
216+
217+
final responseMessage2 = awaitForDaemonMessage(
218+
wrappedMatcher2,
219+
timeout: timeout,
220+
);
221+
222+
final result = await Future.wait([responseMessage1, responseMessage2]);
223+
224+
return (
225+
result.first as DaemonResponse,
226+
result.last as DaemonResponse,
227+
);
228+
}
229+
179230
void dispose() {
180231
subscription.cancel();
181232
}

packages/dart_frog_cli/lib/src/daemon/domain/dev_server_domain.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ class DevServerDomain extends DomainBase {
173173
);
174174
}
175175

176-
final runner = _devServerRunners[applicationId];
176+
final runner = _devServerRunners.remove(applicationId);
177177
if (runner == null) {
178178
return DaemonResponse.error(
179179
id: request.id,
@@ -187,8 +187,6 @@ class DevServerDomain extends DomainBase {
187187
try {
188188
await runner.stop();
189189

190-
_devServerRunners.remove(applicationId);
191-
192190
final exitCode = await runner.exitCode;
193191

194192
return DaemonResponse.success(
@@ -199,11 +197,16 @@ class DevServerDomain extends DomainBase {
199197
},
200198
);
201199
} catch (e) {
200+
if (!runner.isCompleted) {
201+
_devServerRunners[applicationId] = runner;
202+
}
203+
202204
return DaemonResponse.error(
203205
id: request.id,
204206
error: {
205207
'applicationId': applicationId,
206208
'message': e.toString(),
209+
'finished': runner.isCompleted,
207210
},
208211
);
209212
}

packages/dart_frog_cli/lib/src/daemon/domain/route_configuration_domain.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class RouteConfigurationDomain extends DomainBase {
175175
);
176176
}
177177

178-
final watcher = _routeConfigurationWatchers[watcherId];
178+
final watcher = _routeConfigurationWatchers.remove(watcherId);
179179
if (watcher == null) {
180180
return DaemonResponse.error(
181181
id: request.id,
@@ -189,8 +189,6 @@ class RouteConfigurationDomain extends DomainBase {
189189
try {
190190
await watcher.stop();
191191

192-
_routeConfigurationWatchers.remove(watcherId);
193-
194192
final exitCode = await watcher.exitCode;
195193

196194
return DaemonResponse.success(
@@ -201,11 +199,15 @@ class RouteConfigurationDomain extends DomainBase {
201199
},
202200
);
203201
} catch (e) {
202+
if (!watcher.isCompleted) {
203+
_routeConfigurationWatchers[watcherId] = watcher;
204+
}
204205
return DaemonResponse.error(
205206
id: request.id,
206207
error: {
207208
'watcherId': watcherId,
208209
'message': e.toString(),
210+
'finished': watcher.isCompleted,
209211
},
210212
);
211213
}

packages/dart_frog_cli/test/src/daemon/domain/dev_server_domain_test.dart

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ void main() {
4949
completer = Completer();
5050
when(() => runner.start()).thenAnswer((_) async {});
5151
when(() => runner.exitCode).thenAnswer((_) async => completer.future);
52+
when(() => runner.isCompleted).thenReturn(true);
5253
});
5354

5455
test('can be instantiated', () {
@@ -424,7 +425,37 @@ void main() {
424425
equals(
425426
const DaemonResponse.error(
426427
id: '12',
427-
error: {'applicationId': 'id', 'message': 'error'},
428+
error: {
429+
'applicationId': 'id',
430+
'message': 'error',
431+
'finished': true,
432+
},
433+
),
434+
),
435+
);
436+
});
437+
438+
test('on non completed dev server throw', () async {
439+
when(() => runner.stop()).thenThrow('error');
440+
when(() => runner.isCompleted).thenReturn(false);
441+
442+
expect(
443+
await domain.handleRequest(
444+
const DaemonRequest(
445+
id: '12',
446+
domain: 'dev_server',
447+
method: 'stop',
448+
params: {'applicationId': 'id'},
449+
),
450+
),
451+
equals(
452+
const DaemonResponse.error(
453+
id: '12',
454+
error: {
455+
'applicationId': 'id',
456+
'message': 'error',
457+
'finished': false,
458+
},
428459
),
429460
),
430461
);

packages/dart_frog_cli/test/src/daemon/domain/route_configuration_domain_test.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ void main() {
8080

8181
when(() => watcher.start()).thenAnswer((_) async {});
8282
when(() => watcher.exitCode).thenAnswer((_) async => completer.future);
83+
when(() => watcher.isCompleted).thenReturn(true);
8384
});
8485

8586
test('can be instantiated', () {
@@ -292,7 +293,39 @@ void main() {
292293
equals(
293294
const DaemonResponse.error(
294295
id: '12',
295-
error: {'watcherId': 'id', 'message': 'error'},
296+
error: {
297+
'watcherId': 'id',
298+
'message': 'error',
299+
'finished': true,
300+
},
301+
),
302+
),
303+
);
304+
});
305+
306+
test('on non completed dev server throw', () async {
307+
when(() => watcher.stop()).thenThrow('error');
308+
when(() => watcher.isCompleted).thenReturn(false);
309+
310+
expect(
311+
await domain.handleRequest(
312+
const DaemonRequest(
313+
id: '12',
314+
domain: 'route_configuration',
315+
method: 'watcherStop',
316+
params: {
317+
'watcherId': 'id',
318+
},
319+
),
320+
),
321+
equals(
322+
const DaemonResponse.error(
323+
id: '12',
324+
error: {
325+
'watcherId': 'id',
326+
'message': 'error',
327+
'finished': false,
328+
},
296329
),
297330
),
298331
);

0 commit comments

Comments
 (0)