1
+ import 'dart:async' ;
1
2
import 'dart:convert' ;
2
3
import 'dart:io' as io;
3
4
@@ -39,9 +40,18 @@ final _dartVmServiceAlreadyInUseErrorRegex = RegExp(
39
40
multiLine: true ,
40
41
);
41
42
42
- // TODO(renancaraujo): Add reload and stop methods.
43
43
/// {@template dev_server_runner}
44
44
/// A class that manages a local development server process lifecycle.
45
+ ///
46
+ /// The [DevServerRunner] is responsible for:
47
+ /// - Generating the dev server runtime code.
48
+ /// - Starting the dev server process.
49
+ /// - Watching for file changes.
50
+ /// - Restarting the dev server process when files change.
51
+ /// - Stopping the dev server process when requested or under external
52
+ /// circumstances (server process killed or watcher stopped).
53
+ ///
54
+ /// After stopped, a [DevServerRunner] instance cannot be restarted.
45
55
/// {@endtemplate}
46
56
class DevServerRunner {
47
57
/// {@macro dev_server_runner}
@@ -58,9 +68,7 @@ class DevServerRunner {
58
68
@visibleForTesting io.ProcessSignal ? sigint,
59
69
@visibleForTesting ProcessStart ? startProcess,
60
70
@visibleForTesting ProcessRun ? runProcess,
61
- @visibleForTesting Exit ? exit,
62
71
}) : _directoryWatcher = directoryWatcher ?? DirectoryWatcher .new ,
63
- _exit = exit ?? io.exit,
64
72
_isWindows = isWindows ?? io.Platform .isWindows,
65
73
_sigint = sigint ?? io.ProcessSignal .sigint,
66
74
_startProcess = startProcess ?? io.Process .start,
@@ -92,16 +100,34 @@ class DevServerRunner {
92
100
final ProcessStart _startProcess;
93
101
final ProcessRun _runProcess;
94
102
final RestorableDirectoryGeneratorTargetBuilder _generatorTarget;
95
- final Exit _exit;
96
103
final bool _isWindows;
97
-
98
104
final io.ProcessSignal _sigint;
99
- late final _target = _generatorTarget (
100
- io.Directory (path. join (workingDirectory.path, '.dart_frog' )),
101
- logger : logger ,
105
+
106
+ late final _generatedDirectory = io.Directory (
107
+ path. join (workingDirectory.path, '.dart_frog' ) ,
102
108
);
109
+ late final _target = _generatorTarget (_generatedDirectory, logger: logger);
103
110
104
111
var _isReloading = false ;
112
+ io.Process ? _serverProcess;
113
+ StreamSubscription <WatchEvent >? _watcherSubscription;
114
+
115
+ /// Whether the dev server is running.
116
+ bool get isServerRunning => _serverProcess != null ;
117
+
118
+ /// Whether the dev server is watching for file changes.
119
+ bool get isWatching => _watcherSubscription != null ;
120
+
121
+ /// Whether the dev server has been started and stopped.
122
+ bool get isCompleted => _exitCodeCompleter.isCompleted;
123
+
124
+ final Completer <ExitCode > _exitCodeCompleter = Completer <ExitCode >();
125
+
126
+ /// A [Future] that completes when the dev server stops.
127
+ ///
128
+ /// The [Future] will complete with the [ExitCode] indicating the conditions
129
+ /// under which the dev server ended.
130
+ Future <ExitCode > get exitCode => _exitCodeCompleter.future;
105
131
106
132
Future <void > _codegen () async {
107
133
logger.detail ('[codegen] running pre-gen...' );
@@ -129,8 +155,15 @@ class DevServerRunner {
129
155
logger.detail ('[codegen] reload complete.' );
130
156
}
131
157
132
- Future <void > _killProcess (io.Process process) async {
158
+ // Internal method to kill the server process.
159
+ // Make sure to call `stop` after calling this method to also stop the
160
+ // watcher.
161
+ Future <void > _killServerProcess () async {
133
162
_isReloading = false ;
163
+ final process = _serverProcess;
164
+ if (process == null ) {
165
+ return ;
166
+ }
134
167
logger.detail ('[process] killing process...' );
135
168
if (_isWindows) {
136
169
logger.detail ('[process] taskkill /F /T /PID ${process .pid }' );
@@ -139,33 +172,73 @@ class DevServerRunner {
139
172
logger.detail ('[process] process.kill()...' );
140
173
process.kill ();
141
174
}
175
+ _serverProcess = null ;
142
176
logger.detail ('[process] killing process complete.' );
143
177
}
144
178
145
- // TODO(renancaraujo): this method returns a future that completes when the
146
- // process is killed, but it should return a future that completes when the
147
- // process is finished starting.
148
- /// Starts the development server.
149
- Future <ExitCode > start () async {
150
- var isHotReloadingEnabled = false ;
179
+ // Internal method to cancel the watcher subscription.
180
+ // Make sure to call `stop` after calling this method to also stop the
181
+ // server process.
182
+ Future <void > _cancelWatcherSubscription () async {
183
+ if (! isWatching) {
184
+ return ;
185
+ }
186
+ logger.detail ('[watcher] cancelling subscription...' );
187
+ await _watcherSubscription! .cancel ();
188
+ _watcherSubscription = null ;
189
+ logger.detail ('[watcher] cancelling subscription complete.' );
190
+ }
191
+
192
+ /// Starts the development server and a [DirectoryWatcher] subscription
193
+ /// that will regenerate the dev server code when files change.
194
+ ///
195
+ /// This method will throw a [DartFrogDevServerException] if called while
196
+ /// the dev server has been started.
197
+ ///
198
+ /// This method will throw a [DartFrogDevServerException] if called after
199
+ /// [stop] has been called.
200
+ Future <void > start () async {
201
+ if (isCompleted) {
202
+ throw DartFrogDevServerException (
203
+ 'Cannot start a dev server after it has been stopped.' ,
204
+ );
205
+ }
206
+
207
+ if (isServerRunning) {
208
+ throw DartFrogDevServerException (
209
+ 'Cannot start a dev server while already running.' ,
210
+ );
211
+ }
151
212
152
213
Future <void > serve () async {
214
+ var isHotReloadingEnabled = false ;
153
215
final enableVmServiceFlag = '--enable-vm-service=$dartVmServicePort ' ;
154
216
217
+ final serverDartFilePath = path.join (
218
+ _generatedDirectory.absolute.path,
219
+ 'server.dart' ,
220
+ );
221
+
155
222
logger.detail (
156
- '''[process] dart $enableVmServiceFlag ${ path . join ( '.dart_frog' , 'server.dart' )} ''' ,
223
+ '''[process] dart $enableVmServiceFlag $serverDartFilePath ''' ,
157
224
);
158
225
159
- final process = await _startProcess (
226
+ final process = _serverProcess = await _startProcess (
160
227
'dart' ,
161
- [enableVmServiceFlag, path. join ( '.dart_frog' , 'server.dart' ) ],
228
+ [enableVmServiceFlag, serverDartFilePath ],
162
229
runInShell: true ,
163
230
);
164
231
165
232
// On Windows listen for CTRL-C and use taskkill to kill
166
233
// the spawned process along with any child processes.
167
234
// https://github.com/dart-lang/sdk/issues/22470
168
- if (_isWindows) _sigint.watch ().listen ((_) => _killProcess (process));
235
+ if (_isWindows) {
236
+ _sigint.watch ().listen ((_) {
237
+ // Do not await on sigint
238
+ _killServerProcess ().ignore ();
239
+ stop ();
240
+ });
241
+ }
169
242
170
243
var hasError = false ;
171
244
process.stderr.listen ((_) async {
@@ -194,9 +267,11 @@ class DevServerRunner {
194
267
195
268
if ((! isHotReloadingEnabled && ! isSDKWarning) ||
196
269
isDartVMServiceAlreadyInUseError) {
197
- await _killProcess (process);
198
- logger.detail ('[process] exit(1)' );
199
- _exit (1 );
270
+ await _killServerProcess ();
271
+ const exitCode = ExitCode .software;
272
+ logger.detail ('[process] exit(${exitCode .code })' );
273
+ await stop (exitCode);
274
+ return ;
200
275
}
201
276
202
277
await _target.rollback ();
@@ -211,6 +286,15 @@ class DevServerRunner {
211
286
if (shouldCacheSnapshot) _target.cacheLatestSnapshot ();
212
287
hasError = false ;
213
288
});
289
+
290
+ process.exitCode.then ((code) async {
291
+ if (isCompleted) return ;
292
+ logger
293
+ ..info ('[process] Server process has been terminated' )
294
+ ..detail ('[process] exit($code )' );
295
+ await _killServerProcess ();
296
+ await stop (ExitCode .unavailable);
297
+ }).ignore ();
214
298
}
215
299
216
300
final progress = logger.progress ('Serving' );
@@ -235,13 +319,59 @@ class DevServerRunner {
235
319
}
236
320
237
321
final watcher = _directoryWatcher (path.join (cwdPath));
238
- final subscription = watcher.events
322
+ _watcherSubscription = watcher.events
239
323
.where (shouldReload)
240
324
.debounce (Duration .zero)
241
325
.listen ((_) => _reload ());
242
326
243
- await subscription.asFuture <void >();
244
- await subscription.cancel ();
245
- return ExitCode .success;
327
+ _watcherSubscription! .asFuture <void >().then ((_) async {
328
+ await _cancelWatcherSubscription ();
329
+ await stop ();
330
+ }).catchError ((_) async {
331
+ await _cancelWatcherSubscription ();
332
+ await stop (ExitCode .software);
333
+ }).ignore ();
334
+ }
335
+
336
+ /// Stops the development server and the watcher then
337
+ /// completes [DevServerRunner.exitCode] with the given [exitCode] .
338
+ ///
339
+ /// If [exitCode] is not provided, it defaults to [ExitCode.success] .
340
+ ///
341
+ /// After calling [stop] , the dev server cannot be restarted.
342
+ ///
343
+ /// This can be called internally if the server process is killed or if the
344
+ /// watcher stops watching.
345
+ Future <void > stop ([ExitCode exitCode = ExitCode .success]) async {
346
+ if (isCompleted) {
347
+ return ;
348
+ }
349
+
350
+ if (isWatching) {
351
+ await _cancelWatcherSubscription ();
352
+ }
353
+ if (isServerRunning) {
354
+ await _killServerProcess ();
355
+ }
356
+
357
+ _exitCodeCompleter.complete (exitCode);
358
+ }
359
+
360
+ /// Regenerates the dev server code and sends a hot reload signal to the
361
+ /// server.
362
+ Future <void > reload () async {
363
+ if (isCompleted || ! isServerRunning || _isReloading) return ;
364
+ return _reload ();
246
365
}
247
366
}
367
+
368
+ /// {@template dart_frog_dev_server_exception}
369
+ /// Exception thrown when the dev server fails to start.
370
+ /// {@endtemplate}
371
+ class DartFrogDevServerException implements Exception {
372
+ /// {@macro dart_frog_dev_server_exception}
373
+ DartFrogDevServerException (this .message);
374
+
375
+ /// The exception message.
376
+ final String message;
377
+ }
0 commit comments