Skip to content

Commit 42ded0d

Browse files
authored
fix(dart_frog_cli): kill dev server child process on windows (#64)
1 parent 53559b4 commit 42ded0d

File tree

5 files changed

+143
-16
lines changed

5 files changed

+143
-16
lines changed

packages/dart_frog_cli/lib/src/commands/create/create.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class CreateCommand extends DartFrogCommand {
4141
final outputDirectory = _outputDirectory;
4242
final projectName = _projectName;
4343
final generator = await _generator(createDartFrogBundle);
44-
final generateDone = logger.progress('Creating $projectName');
44+
final generateProgress = logger.progress('Creating $projectName');
4545
final vars = <String, dynamic>{
4646
'name': projectName,
4747
'output_directory': outputDirectory.absolute.path
@@ -51,7 +51,7 @@ class CreateCommand extends DartFrogCommand {
5151
DirectoryGeneratorTarget(outputDirectory),
5252
vars: vars,
5353
);
54-
generateDone();
54+
generateProgress.complete();
5555

5656
await generator.hooks.postGen(vars: vars, workingDirectory: cwd.path);
5757

packages/dart_frog_cli/lib/src/commands/dev/dev.dart

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'dart:convert';
2-
import 'dart:io';
2+
import 'dart:io' as io;
33

44
import 'package:dart_frog_cli/src/command.dart';
55
import 'package:dart_frog_cli/src/commands/commands.dart';
@@ -8,18 +8,27 @@ import 'package:mason/mason.dart';
88
import 'package:path/path.dart' as path;
99
import 'package:watcher/watcher.dart';
1010

11-
/// Typedef for [Process.start].
12-
typedef ProcessStart = Future<Process> Function(
11+
/// Typedef for [io.Process.start].
12+
typedef ProcessStart = Future<io.Process> Function(
1313
String executable,
1414
List<String> arguments, {
1515
bool runInShell,
1616
});
1717

18+
/// Typedef for [io.Process.run].
19+
typedef ProcessRun = Future<io.ProcessResult> Function(
20+
String executable,
21+
List<String> arguments,
22+
);
23+
1824
/// Typedef for [DirectoryWatcher.new].
1925
typedef DirectoryWatcherBuilder = DirectoryWatcher Function(
2026
String directory,
2127
);
2228

29+
/// Typedef for [io.exit].
30+
typedef Exit = dynamic Function(int exitCode);
31+
2332
/// {@template dev_command}
2433
/// `dart_frog dev` command which starts the dev server`.
2534
/// {@endtemplate}
@@ -29,14 +38,26 @@ class DevCommand extends DartFrogCommand {
2938
super.logger,
3039
DirectoryWatcherBuilder? directoryWatcher,
3140
GeneratorBuilder? generator,
41+
Exit? exit,
42+
bool? isWindows,
43+
ProcessRun? runProcess,
44+
io.ProcessSignal? sigint,
3245
ProcessStart? startProcess,
3346
}) : _directoryWatcher = directoryWatcher ?? DirectoryWatcher.new,
3447
_generator = generator ?? MasonGenerator.fromBundle,
35-
_startProcess = startProcess ?? Process.start;
48+
_exit = exit ?? io.exit,
49+
_isWindows = isWindows ?? io.Platform.isWindows,
50+
_runProcess = runProcess ?? io.Process.run,
51+
_sigint = sigint ?? io.ProcessSignal.sigint,
52+
_startProcess = startProcess ?? io.Process.start;
3653

54+
final DirectoryWatcherBuilder _directoryWatcher;
3755
final GeneratorBuilder _generator;
56+
final Exit _exit;
57+
final bool _isWindows;
58+
final ProcessRun _runProcess;
59+
final io.ProcessSignal _sigint;
3860
final ProcessStart _startProcess;
39-
final DirectoryWatcherBuilder _directoryWatcher;
4061

4162
@override
4263
final String description = 'Run a local development server.';
@@ -56,7 +77,9 @@ class DevCommand extends DartFrogCommand {
5677
);
5778

5879
final _ = await generator.generate(
59-
DirectoryGeneratorTarget(Directory(path.join(cwd.path, '.dart_frog'))),
80+
DirectoryGeneratorTarget(
81+
io.Directory(path.join(cwd.path, '.dart_frog')),
82+
),
6083
vars: vars,
6184
fileConflictResolution: FileConflictResolution.overwrite,
6285
);
@@ -69,18 +92,31 @@ class DevCommand extends DartFrogCommand {
6992
runInShell: true,
7093
);
7194

95+
// On Windows listen for CTRL-C and use taskkill to kill
96+
// the spawned process along with any child processes.
97+
// https://github.com/dart-lang/sdk/issues/22470
98+
if (_isWindows) {
99+
_sigint.watch().listen((_) async {
100+
final result = await _runProcess(
101+
'taskkill',
102+
['/F', '/T', '/PID', '${process.pid}'],
103+
);
104+
_exit(result.exitCode);
105+
});
106+
}
107+
72108
process.stdout.listen((_) => logger.info(utf8.decode(_)));
73109
process.stderr.listen((_) => logger.err(utf8.decode(_)));
74110
}
75111

76-
final done = logger.progress('Serving');
112+
final progress = logger.progress('Serving');
77113
await codegen();
78114
await serve();
79-
done('Running on http://localhost:8080');
115+
progress.complete('Running on http://localhost:8080');
80116

81117
final watcher = _directoryWatcher(path.join(cwd.path, 'routes'));
82118
final subscription = watcher.events.listen((event) async {
83-
final file = File(event.path);
119+
final file = io.File(event.path);
84120
if (file.existsSync()) {
85121
final contents = await file.readAsString();
86122
if (contents.isNotEmpty) {

packages/dart_frog_cli/test/src/commands/build/build_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ class _MockMasonGenerator extends Mock implements MasonGenerator {}
99

1010
class _MockGeneratorHooks extends Mock implements GeneratorHooks {}
1111

12+
class _MockProgress extends Mock implements Progress {}
13+
1214
class _FakeDirectoryGeneratorTarget extends Fake
1315
implements DirectoryGeneratorTarget {}
1416

@@ -19,12 +21,14 @@ void main() {
1921
});
2022

2123
late Logger logger;
24+
late Progress progress;
2225
late MasonGenerator generator;
2326
late BuildCommand command;
2427

2528
setUp(() {
2629
logger = _MockLogger();
27-
when(() => logger.progress(any())).thenReturn(([_]) {});
30+
progress = _MockProgress();
31+
when(() => logger.progress(any())).thenReturn(progress);
2832
generator = _MockMasonGenerator();
2933
command = BuildCommand(
3034
logger: logger,

packages/dart_frog_cli/test/src/commands/create/create_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class _MockArgResults extends Mock implements ArgResults {}
1111

1212
class _MockLogger extends Mock implements Logger {}
1313

14+
class _MockProgress extends Mock implements Progress {}
15+
1416
class _MockMasonGenerator extends Mock implements MasonGenerator {}
1517

1618
class _MockGeneratorHooks extends Mock implements GeneratorHooks {}
@@ -26,13 +28,15 @@ void main() {
2628

2729
late ArgResults argResults;
2830
late Logger logger;
31+
late Progress progress;
2932
late MasonGenerator generator;
3033
late CreateCommand command;
3134

3235
setUp(() {
3336
argResults = _MockArgResults();
3437
logger = _MockLogger();
35-
when(() => logger.progress(any())).thenReturn(([_]) {});
38+
progress = _MockProgress();
39+
when(() => logger.progress(any())).thenReturn(progress);
3640
generator = _MockMasonGenerator();
3741
command = CreateCommand(
3842
logger: logger,

packages/dart_frog_cli/test/src/commands/dev/dev_test.dart

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:io';
23

34
import 'package:dart_frog_cli/src/commands/commands.dart';
@@ -16,6 +17,12 @@ class _MockGeneratorHooks extends Mock implements GeneratorHooks {}
1617

1718
class _MockProcess extends Mock implements Process {}
1819

20+
class _MockProcessResult extends Mock implements ProcessResult {}
21+
22+
class _MockProcessSignal extends Mock implements ProcessSignal {}
23+
24+
class _MockProgress extends Mock implements Progress {}
25+
1926
class _FakeDirectoryGeneratorTarget extends Fake
2027
implements DirectoryGeneratorTarget {}
2128

@@ -26,28 +33,41 @@ void main() {
2633
});
2734

2835
late DirectoryWatcher directoryWatcher;
36+
late MasonGenerator generator;
37+
late bool isWindows;
38+
late Progress progress;
2939
late Logger logger;
3040
late Process process;
31-
late MasonGenerator generator;
41+
late ProcessResult processResult;
42+
late ProcessSignal sigint;
3243
late DevCommand command;
3344

3445
setUp(() {
3546
directoryWatcher = _MockDirectoryWatcher();
47+
generator = _MockMasonGenerator();
48+
isWindows = false;
3649
logger = _MockLogger();
37-
when(() => logger.progress(any())).thenReturn(([_]) {});
50+
progress = _MockProgress();
51+
when(() => logger.progress(any())).thenReturn(progress);
3852
process = _MockProcess();
39-
generator = _MockMasonGenerator();
53+
processResult = _MockProcessResult();
54+
sigint = _MockProcessSignal();
4055
command = DevCommand(
4156
logger: logger,
4257
directoryWatcher: (_) => directoryWatcher,
4358
generator: (_) async => generator,
59+
isWindows: isWindows,
60+
runProcess: (String executable, List<String> arguments) async {
61+
return processResult;
62+
},
4463
startProcess: (
4564
String executable,
4665
List<String> arguments, {
4766
bool runInShell = false,
4867
}) async {
4968
return process;
5069
},
70+
sigint: sigint,
5171
);
5272
});
5373

@@ -83,5 +103,68 @@ void main() {
83103
final exitCode = await command.run();
84104
expect(exitCode, equals(ExitCode.success.code));
85105
});
106+
107+
test('kills all child processes when sigint received on windows', () async {
108+
const processId = 42;
109+
final generatorHooks = _MockGeneratorHooks();
110+
final processRunCalls = <List<String>>[];
111+
int? exitCode;
112+
when(
113+
() => generatorHooks.preGen(
114+
vars: any(named: 'vars'),
115+
workingDirectory: any(named: 'workingDirectory'),
116+
onVarsChanged: any(named: 'onVarsChanged'),
117+
),
118+
).thenAnswer((invocation) async {
119+
(invocation.namedArguments[const Symbol('onVarsChanged')] as Function(
120+
Map<String, dynamic> vars,
121+
))
122+
.call(<String, dynamic>{});
123+
});
124+
when(
125+
() => generator.generate(
126+
any(),
127+
vars: any(named: 'vars'),
128+
fileConflictResolution: FileConflictResolution.overwrite,
129+
),
130+
).thenAnswer((_) async => []);
131+
when(() => generator.hooks).thenReturn(generatorHooks);
132+
when(() => process.stdout).thenAnswer((_) => const Stream.empty());
133+
when(() => process.stderr).thenAnswer((_) => const Stream.empty());
134+
when(() => process.pid).thenReturn(processId);
135+
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
136+
when(
137+
() => directoryWatcher.events,
138+
).thenAnswer((_) => StreamController<WatchEvent>().stream);
139+
when(() => sigint.watch()).thenAnswer((_) => Stream.value(sigint));
140+
command = DevCommand(
141+
logger: logger,
142+
directoryWatcher: (_) => directoryWatcher,
143+
generator: (_) async => generator,
144+
exit: (code) => exitCode = code,
145+
isWindows: true,
146+
runProcess: (String executable, List<String> arguments) async {
147+
processRunCalls.add([executable, ...arguments]);
148+
return processResult;
149+
},
150+
startProcess: (
151+
String executable,
152+
List<String> arguments, {
153+
bool runInShell = false,
154+
}) async {
155+
return process;
156+
},
157+
sigint: sigint,
158+
);
159+
command.run().ignore();
160+
await untilCalled(() => process.pid);
161+
expect(exitCode, equals(ExitCode.success.code));
162+
expect(
163+
processRunCalls,
164+
equals([
165+
['taskkill', '/F', '/T', '/PID', '$processId']
166+
]),
167+
);
168+
});
86169
});
87170
}

0 commit comments

Comments
 (0)