Skip to content

Commit dc20c70

Browse files
authored
feat(dart_frog_cli): start daemon implementation (#749)
1 parent d4e7483 commit dc20c70

File tree

14 files changed

+1314
-84
lines changed

14 files changed

+1314
-84
lines changed

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import 'package:dart_frog_cli/src/command.dart';
22
import 'package:dart_frog_cli/src/daemon/daemon.dart';
3-
import 'package:mason/mason.dart';
43
import 'package:meta/meta.dart';
54

6-
/// Type definition for a function which creates a [Daemon].
7-
typedef DaemonBuilder = Daemon Function(Logger logger);
8-
9-
Daemon _defaultDaemonBuilder(Logger logger) => Daemon(logger: logger);
5+
/// Type definition for a function which creates a [DaemonServer].
6+
typedef DaemonBuilder = DaemonServer Function();
107

118
/// {@template daemon_command}
129
/// `dart_frog daemon` command which starts the Dart Frog daemon.
@@ -16,7 +13,7 @@ class DaemonCommand extends DartFrogCommand {
1613
DaemonCommand({
1714
super.logger,
1815
DaemonBuilder? daemonBuilder,
19-
}) : _daemonBuilder = daemonBuilder ?? _defaultDaemonBuilder;
16+
}) : _daemonBuilder = daemonBuilder ?? DaemonServer.new;
2017

2118
final DaemonBuilder _daemonBuilder;
2219

@@ -30,11 +27,11 @@ class DaemonCommand extends DartFrogCommand {
3027
// TODO(renancaraujo): unhide this command when it's ready
3128
bool get hidden => true;
3229

33-
/// The [Daemon] instance used by this command.
30+
/// The [DaemonServer] instance used by this command.
3431
///
3532
/// Visible for testing purposes only.
3633
@visibleForTesting
37-
Daemon get daemon => _daemonBuilder(logger);
34+
late final DaemonServer daemon = _daemonBuilder();
3835

3936
@override
4037
Future<int> run() async => (await daemon.exitCode).code;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import 'package:dart_frog_cli/src/daemon/daemon.dart';
6+
import 'package:meta/meta.dart';
7+
8+
/// {@template daemon_connection}
9+
/// A class responsible for managing the connection between a [DaemonServer]
10+
/// and its clients through input and output streams.
11+
/// {@endtemplate}
12+
///
13+
/// See also:
14+
/// - [DaemonStdioConnection], a connection that hooks into the stdio.
15+
abstract interface class DaemonConnection {
16+
/// A stream of [DaemonMessage]s that are received from the client.
17+
Stream<DaemonMessage> get inputStream;
18+
19+
/// A sink of [DaemonMessage]s that are to be sent to the client.
20+
StreamSink<DaemonMessage> get outputSink;
21+
22+
/// Closes the connection and free resources.
23+
Future<void> dispose();
24+
}
25+
26+
/// {@template daemon_stdio_connection}
27+
/// A [DaemonConnection] that hooks into the stdio.
28+
///
29+
/// This is the default connection used by the daemon.
30+
///
31+
/// This uses JSON RPC over stdio to communicate with the client.
32+
/// {@endtemplate}
33+
class DaemonStdioConnection implements DaemonConnection {
34+
/// {@macro daemon_stdio_connection}
35+
DaemonStdioConnection({
36+
@visibleForTesting StreamSink<List<int>>? testStdout,
37+
@visibleForTesting Stream<List<int>>? testStdin,
38+
}) : _stdout = testStdout ?? stdout,
39+
_stdin = testStdin ?? stdin {
40+
_outputStreamController.stream.listen((message) {
41+
final json = jsonEncode(message.toJson());
42+
_stdout.add(utf8.encode('[$json]\n'));
43+
});
44+
45+
StreamSubscription<DaemonMessage>? stdinSubscription;
46+
47+
_inputStreamController
48+
..onListen = () {
49+
stdinSubscription = _stdin.readMessages().listen(
50+
_inputStreamController.add,
51+
onError: (dynamic error) {
52+
switch (error) {
53+
case DartFrogDaemonMessageException(message: final message):
54+
outputSink.add(
55+
DaemonEvent(
56+
domain: DaemonDomain.name,
57+
event: 'protocolError',
58+
params: {'message': message},
59+
),
60+
);
61+
case FormatException(message: _):
62+
outputSink.add(
63+
const DaemonEvent(
64+
domain: DaemonDomain.name,
65+
event: 'protocolError',
66+
params: {'message': 'Not a valid JSON'},
67+
),
68+
);
69+
default:
70+
outputSink.add(
71+
DaemonEvent(
72+
domain: DaemonDomain.name,
73+
event: 'protocolError',
74+
params: {'message': 'Unknown error: $error'},
75+
),
76+
);
77+
}
78+
},
79+
);
80+
}
81+
..onCancel = () {
82+
stdinSubscription?.cancel();
83+
};
84+
}
85+
86+
final StreamSink<List<int>> _stdout;
87+
final Stream<List<int>> _stdin;
88+
89+
late final _inputStreamController = StreamController<DaemonMessage>();
90+
91+
late final _outputStreamController = StreamController<DaemonMessage>();
92+
93+
@override
94+
Stream<DaemonMessage> get inputStream => _inputStreamController.stream;
95+
96+
@override
97+
StreamSink<DaemonMessage> get outputSink => _outputStreamController.sink;
98+
99+
@override
100+
Future<void> dispose() async {
101+
await _inputStreamController.close();
102+
await _outputStreamController.close();
103+
}
104+
}
105+
106+
extension on Stream<List<int>> {
107+
Stream<DaemonMessage> readMessages() {
108+
return transform(utf8.decoder).transform(const LineSplitter()).map((event) {
109+
final json = jsonDecode(event);
110+
if (json case final List<dynamic> jsonList) {
111+
if (jsonList.elementAtOrNull(0)
112+
case final Map<String, dynamic> jsonMap) {
113+
return DaemonMessage.fromJson(jsonMap);
114+
}
115+
} else {
116+
throw const DartFrogDaemonMessageException(
117+
'Message should be placed within a JSON list',
118+
);
119+
}
120+
throw DartFrogDaemonMessageException('Invalid message: $event');
121+
});
122+
}
123+
}
Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,5 @@
1-
import 'dart:async';
2-
3-
import 'package:mason/mason.dart';
4-
5-
/// {@template daemon}
6-
/// The Dart Frog daemon.
7-
///
8-
/// The daemon is a persistent routine that runs, analyzes and manages
9-
/// Dart Frog projects.
10-
/// {@endtemplate}
11-
class Daemon {
12-
/// {@macro daemon}
13-
Daemon({
14-
Logger? logger,
15-
}) : _logger = logger ?? Logger() {
16-
// TODO(renancaraujo): this is just a placeholder behavior.
17-
_logger.detail('Starting Dart Frog daemon...');
18-
Future<void>.delayed(const Duration(seconds: 2)).then(
19-
(_) {
20-
_logger.detail('Killing Dart Frog daemon...');
21-
kill(ExitCode.success);
22-
},
23-
);
24-
}
25-
26-
final Logger _logger;
27-
28-
final _exitCodeCompleter = Completer<ExitCode>();
29-
30-
/// A [Future] that completes when the daemon exits.
31-
Future<ExitCode> get exitCode => _exitCodeCompleter.future;
32-
33-
/// Kills the daemon with the given [exitCode].
34-
void kill(ExitCode exitCode) {
35-
if (_exitCodeCompleter.isCompleted) return;
36-
_exitCodeCompleter.complete(exitCode);
37-
}
38-
}
1+
export 'connection.dart';
2+
export 'daemon_server.dart';
3+
export 'domain.dart';
4+
export 'exceptions.dart';
5+
export 'protocol.dart';
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'dart:async';
2+
3+
import 'package:dart_frog_cli/src/daemon/daemon.dart';
4+
import 'package:mason/mason.dart';
5+
import 'package:meta/meta.dart';
6+
7+
/// The version of the Dart Frog daemon protocol.
8+
///
9+
/// This exists so clients can know if they are compatible with the
10+
/// format and order of the events, the methods,
11+
/// its arguments and return values.
12+
///
13+
/// This does not have to follow the version of the
14+
/// package containing this class.
15+
const daemonVersion = '0.0.1';
16+
17+
/// {@template daemon}
18+
/// The Dart Frog daemon.
19+
///
20+
/// The daemon is a persistent routine that runs, analyzes and manages
21+
/// Dart Frog projects.
22+
/// {@endtemplate}
23+
class DaemonServer {
24+
/// {@macro daemon}
25+
DaemonServer({
26+
@visibleForTesting DaemonConnection? connection,
27+
}) : _connection = connection ?? DaemonStdioConnection() {
28+
_connection.inputStream.listen(_handleMessage);
29+
30+
addDomain(DaemonDomain(this));
31+
}
32+
33+
final Map<String, Domain> _domains = {};
34+
35+
final DaemonConnection _connection;
36+
37+
final _exitCodeCompleter = Completer<ExitCode>();
38+
39+
/// A [Future] that completes when the daemon exits.
40+
Future<ExitCode> get exitCode => _exitCodeCompleter.future;
41+
42+
/// The names of the domains in the daemon.
43+
Iterable<String> get domainNames => _domains.keys;
44+
45+
/// Whether the daemon has exited.
46+
bool get isCompleted => _exitCodeCompleter.isCompleted;
47+
48+
/// The version of the Dart Frog daemon protocol.
49+
String get version => daemonVersion;
50+
51+
/// Adds a [domain] to the daemon.
52+
///
53+
/// Visible for testing purposes only.
54+
@visibleForTesting
55+
@protected
56+
void addDomain(Domain domain) {
57+
assert(!_domains.containsKey(domain.domainName), 'Domain already exists');
58+
_domains[domain.domainName] = domain;
59+
}
60+
61+
void _handleMessage(DaemonMessage message) {
62+
if (message is DaemonRequest) {
63+
_handleRequest(message).ignore();
64+
return;
65+
}
66+
// even though the protocol allows the daemon to receive
67+
// events and responses, the current implementation
68+
// only supports requests.
69+
}
70+
71+
Future<void> _handleRequest(DaemonRequest request) async {
72+
final domain = _domains[request.domain];
73+
74+
if (domain == null) {
75+
return _sendMessage(
76+
DaemonResponse.error(
77+
id: request.id,
78+
error: {
79+
'message': 'Invalid domain: ${request.domain}',
80+
},
81+
),
82+
);
83+
}
84+
85+
final response = await domain.handleRequest(request);
86+
87+
_sendMessage(response);
88+
}
89+
90+
/// Kills the daemon with the given [exitCode].
91+
Future<void> kill(ExitCode exitCode) async {
92+
await Future.wait(_domains.values.map((e) => e.dispose()));
93+
await _connection.dispose();
94+
95+
if (_exitCodeCompleter.isCompleted) return;
96+
_exitCodeCompleter.complete(exitCode);
97+
}
98+
99+
void _sendMessage(DaemonMessage message) {
100+
if (isCompleted) {
101+
return;
102+
}
103+
_connection.outputSink.add(message);
104+
}
105+
106+
/// Sends an [event] to the client.
107+
void sendEvent(DaemonEvent event) {
108+
_sendMessage(event);
109+
}
110+
}

0 commit comments

Comments
 (0)