-
Notifications
You must be signed in to change notification settings - Fork 29.5k
ProxiedDevice, connection to a remotely connected device via flutter daemon. #95738
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
Conversation
8e48b39 to
434b41c
Compare
434b41c to
81666d0
Compare
|
Rebased this PR now that #95290 has been submitted. Also reactored In my local tests passing a 100MB file in binary stream is ~50% faster than including a base64-encoded version in JSON. PTAL, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nits. This review is huge. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lower camel https://dart.dev/guides/language/effective-dart/style#prefer-using-lowercamelcase-for-constant-names
| JSON, | |
| BINARY, | |
| json, | |
| binary, |
Note: We initially used Java’s SCREAMING_CAPS style for constants. We changed for a few reasons:
SCREAMING_CAPS looks bad for many cases, particularly enum values for things like CSS colors.
Constants are often changed to final non-const variables, which would necessitate a name change.
The values property automatically defined on an enum type is const and lowercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, changed to lower case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe handle error case where message is malformed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I preserved the original behavior for backward compatibility. In the original daemon, anything that is not JSON string are simply ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // final BuildInfo buildInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed, thanks for noticing this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a comment here explaining what this id is for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a comment here. The comment should probably answer the question, "What's the difference between getDevices() and discoverDevices()?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: else is unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: else is unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The binary parameter is only used in a few out of many instances of this callback type. Are there any drawbacks to using two callback tables for the two different callback types to avoid passing the binary parameter to many places it isn't used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great idea! Just added a new callback table. Also added assertion so that the same method will not be registered on both callback tables. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a programming error rather than an error that an end-user would be expected to address. In that case, this should be a normal exception so that it will hit crash logging and/or the end-user will see it as unusual and file a bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to throw Exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commented code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding an error handling callback for the server socket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an error handler that prints the error with a logger to help diagnosing issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but I'm curious to know whether these tests touch the global context. Could they be testWithoutContext?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Daemon class in commands/daemon.dart is using the global context in several places, it errors out when I tried to use testWithoutContext.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can these be testWithoutContext?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, changed to testWithoutContext
|
Thanks for the extensive review Jenn, Zech and Chris! I've pushed a new commit based on the feedback, the biggest change is that I've refactored the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this not be:
assert(!_handlers.containsKey(name));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention is to check that the same method name is not registered in both command map.
But I think it doesn't harm to check both maps, and less error prone too. I'll update to check both maps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid throwing strings. is this an internal state error, or user error? Use a throwToolExit('device "$deviceId" not found'); if this is a user error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was following the other throw style in the same file. Should I change all the throw in this file to throwToolExit instead? If so can I do that in a follow-up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't looked at this code in awhile. I wonder if the strings get caught and sent as an error response back to the client. Even if that's the case though, we should wrap in a class to make that intent explicit at the point of the throw.
8498d31 to
ad68ea7
Compare
|
Friendly ping @zanderso if you want to take another look at this? Thanks! |
Also refactor DaemonStreams for better reusability and testability.
ad68ea7 to
74313d8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the slow review.
This change has low test-coverage of error cases. I think even after the error handling that has already been added, and the couple of places that I've suggested in the new round of comments, I'm not confident that all cases are covered. Sockets and streams in Dart are difficult to reason about, so we'll need you to be available to fix crashers in this new code as we discover them from crash logging data. If you are not signing up for that, we will have to back out this change, and reland only with much better test coverage of error cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't looked at this code in awhile. I wonder if the strings get caught and sent as an error response back to the client. Even if that's the case though, we should wrap in a class to make that intent explicit at the point of the throw.
| Future<String> connect(Map<String, dynamic> args) async { | ||
| final int targetPort = _getIntArg(args, 'port', required: true); | ||
| final String id = 'portForwarder_${targetPort}_${_id++}'; | ||
| final Socket socket = await Socket.connect('127.0.0.1', targetPort); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of '127.0.0.1', this should use InternetAddress.loopbackIPv4 and fall back on InternetAddress.loopbackIPv6 if that doesn't work. This call can also throw a SocketException, which should be handled here if it shouldn't propagate like that.
| final String id = 'portForwarder_${targetPort}_${_id++}'; | ||
| final Socket socket = await Socket.connect('127.0.0.1', targetPort); | ||
| _forwardedConnections[id] = socket; | ||
| socket.listen((List<int> data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need an error handler on this listen() and possibly also an error handler on the done future or socket exceptions on remote disconnect may crash the tool.
| @override | ||
| Future<void> dispose() async { | ||
| for (final Socket connection in _forwardedConnections.values) { | ||
| await connection.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want destroy here? https://api.dart.dev/dev/2.17.0-63.0.dev/dart-io/Socket/destroy.html
|
|
||
| int start = 0; | ||
| while (start < chunk.length) { | ||
| if (state == _InputStreamParseState.json) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the state is in fields instead of locals, you can pull the handling of these cases out into helper methods.
| Future<void> dispose() async { | ||
| await (await _socket).close(); | ||
| /// Creates a [DaemonStreams] that uses stdin and stdout as the underlying streams. | ||
| static DaemonStreams fromStdio(Stdio stdio, { required Logger logger }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dart style here would be to use a named forwarding factory constructor:
DaemonStreams.fromStdio(Stdio stdio, {required Logger logger})
: this(stdio.stdin, stdio.stdout, logger: logger);| } | ||
|
|
||
| /// Creates a [DaemonStreams] that uses [Socket] as the underlying streams. | ||
| static DaemonStreams fromSocket(Socket socket, { required Logger logger }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto:
DaemonStreams.fromSocket(Socket socket, { required Logger logger })
: this(socket, socket, logger: logger);| } | ||
|
|
||
| /// Connects to a server and creates a [DaemonStreams] from the connection as the underlying streams. | ||
| static DaemonStreams connect(String host, int port, { required Logger logger }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this should be a factory constructor:
factory DaemonStreams.connect(String host, int port, {required Logger logger}) {
...
}| final Future<Socket> socketFuture = Socket.connect(host, port); | ||
| final StreamController<List<int>> inputStreamController = StreamController<List<int>>(); | ||
| final StreamController<List<int>> outputStreamController = StreamController<List<int>>(); | ||
| socketFuture.then((Socket socket) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The socket future needs an error handler.
| @override | ||
| String toString() => value; | ||
|
|
||
| static Category? fromString(String category) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below, using a const map lookup can help readability:
static Category? fromString(String category) {
return <String, Category>{
'web': web,
...
}[category];
}|
Sorry about that. The changes here will mostly not be used by default (especially the parts related to Sockets), and are only used in google3, so I think most of the changes here should be relatively safe for 3p users. The changes are submitted upstream only because it might be useful for some advanced 3p users. And yes, I can fix upcoming issues or crashes if they come up, and I will send a separate PR to address the feedbacks you sent above. Thanks. |
…daemon. (flutter#95738) Also allow daemon commands to pass binary streams
Also refactored
DaemonStreams(that I wrote in a previous PR) for better reusability and testability, and added the ability to allow daemon commands to pass binary streams.In my local tests passing a 100MB file in binary stream is ~50% faster than including a base64-encoded version in JSON.
Context: b/210724354
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.