Skip to content

Commit 995611c

Browse files
authored
Avoid an open Peer with a closed Client (flutter#65)
May fix dart-lang/sdk#43012 If a `Peer` is created with a `StreamChannel` that does not follow the stated contract it's possible that the `sink` gets closed without receiving a done event from the `channel` which leaves the `Peer` instance in a state that's inconsistent with the underlying `Client`. The result is that it's possible to get a bad state trying to send a message even with `isClosed` returns `false`. - Make `isClosed` and `done` forward to the `_client` and `_peer` fields so that they can't be inconsistent. - Forward errors to the `_server` so that it can forward them through `done` without an extra `Completer` to manage. - Avoid closing the `sink` in the `Peer`. It will end up being closed by the server when it is handling the error, and it's the same `sink` instance in both places. - Add a test that ensures that `isClosed` behaves as expected following a call to `close()` even when the `StreamChannel` does not follow it's contract.
1 parent 413ee9d commit 995611c

File tree

2 files changed

+25
-10
lines changed

2 files changed

+25
-10
lines changed

lib/src/peer.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ class Peer implements Client, Server {
3535
/// they're responses.
3636
final _clientIncomingForwarder = StreamController(sync: true);
3737

38-
final _done = Completer<void>();
38+
Future<void> _done;
3939
@override
40-
Future get done => _done.future;
40+
Future get done => _done ??= Future.wait([_client.done, _server.done]);
4141
@override
42-
bool get isClosed => _done.isCompleted;
42+
bool get isClosed => _client.isClosed || _server.isClosed;
4343

4444
@override
4545
ErrorCallback get onUnhandledError => _server?.onUnhandledError;
@@ -142,15 +142,15 @@ class Peer implements Client, Server {
142142
_serverIncomingForwarder.add(message);
143143
}
144144
}, onError: (error, stackTrace) {
145-
_done.completeError(error, stackTrace);
146-
_channel.sink.close();
147-
}, onDone: () {
148-
if (!_done.isCompleted) _done.complete();
149-
close();
150-
});
145+
_serverIncomingForwarder.addError(error, stackTrace);
146+
}, onDone: close);
151147
return done;
152148
}
153149

154150
@override
155-
Future close() => Future.wait([_client.close(), _server.close()]);
151+
Future close() {
152+
_client.close();
153+
_server.close();
154+
return done;
155+
}
156156
}

test/peer_test.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ void main() {
113113
await peer.close();
114114
});
115115

116+
test('considered closed with misbehaving StreamChannel', () async {
117+
// If a StreamChannel does not enforce the guarantees stated in it's
118+
// contract - specifically that "Closing the sink causes the stream to close
119+
// before it emits any more events." - The `Peer` should still understand
120+
// when it has been closed manually.
121+
var channel = StreamChannel(
122+
StreamController().stream,
123+
StreamController(),
124+
);
125+
var peer = json_rpc.Peer.withoutJson(channel);
126+
unawaited(peer.listen());
127+
unawaited(peer.close());
128+
expect(peer.isClosed, true);
129+
});
130+
116131
group('like a server,', () {
117132
test('can receive a call and return a response', () {
118133
expect(outgoing.first,

0 commit comments

Comments
 (0)