Repository navigation
Fix the desktop window flashing endlessly after a flet run hot reload
#6945
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+264
−7
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d2d859b
Report a socket disconnect only once
ndonkoHenri d56fdd5
Keep one reconnect pending in FletBackend
ndonkoHenri 3c17fcf
Link #6945 in the 1.1.0 changelog entries
ndonkoHenri c2cf7be
Add regression tests for single disconnects and one pending reconnect
ndonkoHenri 530b134
Merge flet-1.1.0 into PR #6945 and resolve changelog conflict
FeodorFitsner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import 'dart:typed_data'; | ||
|
|
||
| import 'package:flet/flet.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
|
|
||
| /// A transport that connects at once and lets the test report disconnects. | ||
| class _Channel implements FletBackendChannel { | ||
| final FletBackendChannelOnDisconnectCallback onDisconnect; | ||
|
|
||
| _Channel(this.onDisconnect); | ||
|
|
||
| @override | ||
| Future connect() async {} | ||
|
|
||
| @override | ||
| bool get isLocalConnection => true; | ||
|
|
||
| @override | ||
| int get defaultReconnectIntervalMs => 10; | ||
|
|
||
| @override | ||
| void send(Uint8List packet) {} | ||
|
|
||
| @override | ||
| void disconnect() {} | ||
| } | ||
|
|
||
| /// Returns a backend whose every connection attempt adds its channel to | ||
| /// [channels]. | ||
| FletBackend _backend(List<_Channel> channels) => FletBackend( | ||
| pageUri: Uri.parse("mock"), | ||
| assetsDir: "", | ||
| extensions: [], | ||
| multiView: false, | ||
| channelBuilder: ({ | ||
| required FletBackendChannelOnPacketCallback onPacket, | ||
| required FletBackendChannelOnDisconnectCallback onDisconnect, | ||
| }) { | ||
| final channel = _Channel(onDisconnect); | ||
| channels.add(channel); | ||
| return channel; | ||
| }, | ||
| ); | ||
|
|
||
| Future<void> _settle() => Future.delayed(const Duration(milliseconds: 100)); | ||
|
|
||
| void main() { | ||
| test('a disconnect reported twice by a channel reconnects once', () async { | ||
| final channels = <_Channel>[]; | ||
| final backend = _backend(channels); | ||
| await backend.connect(); | ||
|
|
||
| channels[0].onDisconnect(); | ||
| channels[0].onDisconnect(); | ||
| await _settle(); | ||
|
|
||
| // The first connection plus exactly one reconnect. | ||
| expect(channels, hasLength(2)); | ||
|
|
||
| backend.dispose(); | ||
| }); | ||
|
|
||
| test('a disconnect from a replaced channel does not reconnect', () async { | ||
| final channels = <_Channel>[]; | ||
| final backend = _backend(channels); | ||
| await backend.connect(); | ||
| channels[0].onDisconnect(); | ||
| await _settle(); | ||
| expect(channels, hasLength(2)); | ||
|
|
||
| channels[0].onDisconnect(); | ||
| await _settle(); | ||
| expect(channels, hasLength(2)); | ||
|
|
||
| // The current channel still reconnects. | ||
| channels[1].onDisconnect(); | ||
| await _settle(); | ||
| expect(channels, hasLength(3)); | ||
|
|
||
| backend.dispose(); | ||
| }); | ||
| } |
147 changes: 147 additions & 0 deletions
147
packages/flet/test/transport/socket_disconnect_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| @TestOn('vm') | ||
| library; | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| import 'package:flet/flet.dart'; | ||
| import 'package:flet/src/transport/flet_backend_channel_socket.dart'; | ||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
|
|
||
| /// Aborts [socket] with a TCP reset (RST) instead of an orderly FIN. | ||
| /// | ||
| /// This is how Windows closes the connection of a terminated process that | ||
| /// still holds unread data, e.g. a Python app killed by a `flet run` hot | ||
| /// reload. `SO_LINGER` with a zero timeout makes the close abortive. | ||
| void _reset(Socket socket) { | ||
| final soLinger = Platform.isLinux ? 13 : 0x0080; | ||
| final linger = Platform.isWindows | ||
| ? (ByteData(4) | ||
| ..setUint16(0, 1, Endian.host) | ||
| ..setUint16(2, 0, Endian.host)) | ||
| : (ByteData(8) | ||
| ..setInt32(0, 1, Endian.host) | ||
| ..setInt32(4, 0, Endian.host)); | ||
| socket.setRawOption(RawSocketOption( | ||
| RawSocketOption.levelSocket, soLinger, linger.buffer.asUint8List())); | ||
| socket.destroy(); | ||
| } | ||
|
|
||
| /// Collects `debugPrint` output while [body] runs. | ||
| Future<List<String>> _capturePrints(Future<void> Function() body) async { | ||
| final lines = <String>[]; | ||
| final original = debugPrint; | ||
| debugPrint = (String? message, {int? wrapWidth}) { | ||
| if (message != null) lines.add(message); | ||
| }; | ||
| try { | ||
| await body(); | ||
| } finally { | ||
| debugPrint = original; | ||
| } | ||
| return lines; | ||
| } | ||
|
|
||
| void main() { | ||
| group('FletSocketBackendChannel', () { | ||
| late ServerSocket server; | ||
|
|
||
| setUp(() async { | ||
| server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); | ||
| }); | ||
|
|
||
| tearDown(() async { | ||
| await server.close(); | ||
| }); | ||
|
|
||
| /// Connects a channel, lets [close] end the accepted server-side socket | ||
| /// once the client is connected, and returns how many times the channel | ||
| /// reported a disconnect. | ||
| Future<int> disconnectsAfter(void Function(Socket socket) close, | ||
| {List<String>? prints}) async { | ||
| final accepted = Completer<Socket>(); | ||
| server.listen(accepted.complete); | ||
| var disconnects = 0; | ||
| final channel = FletSocketBackendChannel( | ||
| address: "tcp://127.0.0.1:${server.port}", | ||
| onDisconnect: () => disconnects++, | ||
| onPacket: (_) {}); | ||
| final lines = await _capturePrints(() async { | ||
| await channel.connect(); | ||
| close(await accepted.future); | ||
| await Future.delayed(const Duration(milliseconds: 300)); | ||
| }); | ||
| prints?.addAll(lines); | ||
| channel.disconnect(); | ||
| return disconnects; | ||
| } | ||
|
|
||
| test('reports a connection reset as a single disconnect', () async { | ||
| final prints = <String>[]; | ||
| final disconnects = await disconnectsAfter(_reset, prints: prints); | ||
|
|
||
| // The reset reaches the channel as a socket error, not a plain close. | ||
| expect(prints, contains(startsWith("Error: "))); | ||
| expect(disconnects, 1); | ||
| }); | ||
|
|
||
| test('reports an orderly close as a single disconnect', () async { | ||
| final disconnects = await disconnectsAfter((socket) => socket.destroy()); | ||
|
|
||
| expect(disconnects, 1); | ||
| }); | ||
| }); | ||
|
|
||
| group('FletBackend over a TCP socket', () { | ||
| test('reconnects once after a connection reset', () async { | ||
| final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); | ||
| var connections = 0; | ||
| Socket? active; | ||
| server.listen((socket) { | ||
| connections++; | ||
| final isFirst = connections == 1; | ||
| // Like FletSocketServer, a new client replaces the active one. | ||
| active?.destroy(); | ||
| active = socket; | ||
| socket.listen((_) { | ||
| // Reset the first connection once its client has registered. | ||
| if (isFirst && identical(active, socket)) { | ||
| active = null; | ||
| _reset(socket); | ||
| } | ||
| }, onError: (_) {}); | ||
| }); | ||
|
|
||
| // A reset also fails the client socket's `done` future, which nothing | ||
| // listens to; keep those errors out of the test zone. | ||
| final otherErrors = <Object>[]; | ||
| late FletBackend backend; | ||
| await _capturePrints(() async { | ||
| final connected = Completer<void>(); | ||
| runZonedGuarded(() { | ||
| backend = FletBackend( | ||
| pageUri: Uri.parse("tcp://127.0.0.1:${server.port}"), | ||
| assetsDir: "", | ||
| extensions: [], | ||
| multiView: false, | ||
| reconnectIntervalMs: 50); | ||
| backend.connect().whenComplete(connected.complete); | ||
| }, (error, stack) { | ||
| if (error is! SocketException) otherErrors.add(error); | ||
| }); | ||
| await connected.future; | ||
| await Future.delayed(const Duration(seconds: 1)); | ||
| }); | ||
|
|
||
| // The reset connection plus exactly one reconnect. Two overlapping | ||
| // reconnects keep replacing each other on the server, so the count | ||
| // would keep growing for as long as the client runs. | ||
| expect(connections, 2); | ||
| expect(otherErrors, isEmpty); | ||
|
|
||
| await server.close(); | ||
| backend.dispose(); | ||
| active?.destroy(); | ||
| }); | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.