Skip to content

Add a new flutter cli command, running-apps, using mDNS app discovery#180098

Merged
jwren merged 21 commits into
flutter:masterfrom
jwren:mdns-3
Feb 9, 2026
Merged

Add a new flutter cli command, running-apps, using mDNS app discovery#180098
jwren merged 21 commits into
flutter:masterfrom
jwren:mdns-3

Conversation

@jwren

@jwren jwren commented Dec 18, 2025

Copy link
Copy Markdown
Member

Includes:

  • Display running apps in a formatted table with age calculation
  • Implement mDNS discovery for running apps (multiple devices/interfaces)
  • Deduplicate apps by WebSocket URI
  • Centralize mDNS device advertisement in MDNSDeviceDiscovery
  • Ensure ResidentRunner advertises correct app name and cleans up
  • Add network utility functions and JSON support for running-apps
  • Add comprehensive tests for discovery and list command

Lastly, this change uses mDNS to discover running Flutter apps, the multicast_dns package is used instead of mdns_dart as the discovery functionality is insufficient in the mdns_dart package, only discovering a maximum of one service.

@github-actions github-actions Bot added the tool Affects the "flutter" command-line tool. See also t: labels. label Dec 18, 2025

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant feature: mDNS-based discovery and advertisement of running Flutter applications. It adds a new running-apps command to display these apps, enhances network utilities, and integrates mDNS advertisement into the resident runners for both mobile/desktop and web platforms. The changes are well-structured and include comprehensive tests for the new functionality. My review includes a few suggestions for improving type safety and performance in the new code.

Comment on lines +119 to +132
apps.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
final int? epochA = int.tryParse(a['epoch'] as String? ?? '');
final int? epochB = int.tryParse(b['epoch'] as String? ?? '');
if (epochA == null && epochB == null) {
return 0;
}
if (epochA == null) {
return 1; // Put unknown age last
}
if (epochB == null) {
return -1; // Put unknown age last
}
return epochB.compareTo(epochA);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The apps list is of type List<Map<String, String>>. The sort callback should use Map<String, String> for its parameters to match this type. This improves type safety and allows removing the unnecessary cast.

Suggested change
apps.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
final int? epochA = int.tryParse(a['epoch'] as String? ?? '');
final int? epochB = int.tryParse(b['epoch'] as String? ?? '');
if (epochA == null && epochB == null) {
return 0;
}
if (epochA == null) {
return 1; // Put unknown age last
}
if (epochB == null) {
return -1; // Put unknown age last
}
return epochB.compareTo(epochA);
});
apps.sort((Map<String, String> a, Map<String, String> b) {
final int? epochA = int.tryParse(a['epoch'] ?? '');
final int? epochB = int.tryParse(b['epoch'] ?? '');
if (epochA == null && epochB == null) {
return 0;
}
if (epochA == null) {
return 1; // Put unknown age last
}
if (epochB == null) {
return -1; // Put unknown age last
}
return epochB.compareTo(epochA);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, these type parameters can just be String.

Comment on lines +136 to +143
for (final Map<String, dynamic> app in apps) {
final String projectName = app['project_name'] as String? ?? 'Unknown';
final String mode = app['mode'] as String? ?? 'Unknown';
final String deviceName = app['device_name'] as String? ?? 'Unknown';
final String deviceId = app['device_id'] as String? ?? 'Unknown';
final String platform = app['target_platform'] as String? ?? 'Unknown';
final String vmServiceUri = app['ws_uri'] as String? ?? 'Unknown';
final String age = getProcessAge(app['epoch'] as String?, globals.systemClock);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The apps list is of type List<Map<String, String>>. The loop variable app should be typed as Map<String, String> to match. This improves type safety and allows removing the unnecessary casts.

Suggested change
for (final Map<String, dynamic> app in apps) {
final String projectName = app['project_name'] as String? ?? 'Unknown';
final String mode = app['mode'] as String? ?? 'Unknown';
final String deviceName = app['device_name'] as String? ?? 'Unknown';
final String deviceId = app['device_id'] as String? ?? 'Unknown';
final String platform = app['target_platform'] as String? ?? 'Unknown';
final String vmServiceUri = app['ws_uri'] as String? ?? 'Unknown';
final String age = getProcessAge(app['epoch'] as String?, globals.systemClock);
for (final Map<String, String> app in apps) {
final String projectName = app['project_name'] ?? 'Unknown';
final String mode = app['mode'] ?? 'Unknown';
final String deviceName = app['device_name'] ?? 'Unknown';
final String deviceId = app['device_id'] ?? 'Unknown';
final String platform = app['target_platform'] ?? 'Unknown';
final String vmServiceUri = app['ws_uri'] ?? 'Unknown';
final String age = getProcessAge(app['epoch'], globals.systemClock);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

final MDNSService mdnsService = await MDNSService.create(
instance: 'Flutter Tools on $hostname',
service: '_flutter_devices._tcp',
port: await findUnusedPort(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The port for the mDNS service should correspond to the service being advertised, which is the VM service. Using vmServiceUri.port is more accurate and avoids the overhead of calling findUnusedPort() repeatedly in a loop for each network interface. findUnusedPort() binds and closes a socket, which is an unnecessary operation here.

Suggested change
port: await findUnusedPort(),
port: vmServiceUri.port,

Comment on lines +1390 to +1392
appName: FlutterProject.fromDirectory(
globals.fs.directory(projectRootPath),
).manifest.appName,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling FlutterProject.fromDirectory inside this loop is inefficient as it may perform file I/O on each iteration. It's better to determine the appName once before the loop starts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be FlutterProject.current(). It wouldn't hurt to hoist this out of the loop, although it shouldn't cause repeated I/O.

@jwren jwren force-pushed the mdns-3 branch 2 times, most recently from 9dbae97 to 1dda0ac Compare January 5, 2026 08:25
@jwren jwren requested a review from bkonyi January 5, 2026 18:21

@bkonyi bkonyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass

Comment thread packages/flutter_tools/lib/src/base/net.dart Outdated
Comment thread packages/flutter_tools/lib/src/base/net.dart Outdated
} on SocketException {
// If the bind operation fails for any reason (e.g., no interfaces available),
// rethrow.
rethrow;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're just rethrowing, I don't think we need this block.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

// This callback can't throw.
unawaited(
appStartedTimeRecorder.future.then<void>((_) {
// This callback is executed once the application has successfully started (or re-started).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this only run once when the application started?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed "(or re-started)"

Comment thread packages/flutter_tools/lib/src/commands/running_apps.dart
]);
}

// Calculate column widths

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also duplicate this logic in Emulator.descriptions and Device.descriptions. We should probably create a helper utility to handle sizing and printing tables to the terminal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered it for this PR, but it should be a follow up, given the size of this PR already. Created an issue, and commented in the code. -- #180949

// Join columns into lines of text
for (final row in table) {
globals.printStatus(
' ${indices.map<String>((int i) => row[i].padRight(widths[i])).followedBy(<String>[row.last]).join(' • ')}',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: avoid making multiple function calls in the context of string interpolation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

// Let's try to split by common delimiters just in case, or handle it as a single pair.
// Given the serving side uses a list of strings, they might come as separate TXT records or joined.

final List<String> parts = txt.text.split('\n'); // Try newline first

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says "Try newline first", but I don't see logic trying anything else?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments cleaned up.

globals.printStatus('Found ${apps.length} running Flutter ${pluralize('app', apps.length)}:');
final table = <List<String>>[];
for (final Map<String, dynamic> app in apps) {
final String projectName = app['project_name'] as String? ?? 'Unknown';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should define constants for all of these keys and use them in place of string literals.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// used instead of mdns_dart as the discovery functionality is insufficient
// in the mdns_dart package, only discovering a maximum of one service.

globals.printStatus('Searching for running Flutter apps...');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're trying to avoid introducing additional uses of globals in new code, so any instances accessed via globals should be passed into the command's constructor.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// dynamically assign a free port by using port 0.
socket = await ServerSocket.bind(hostname, 0);

final int port = socket.port;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this can just be return socket.port;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@visibleForTesting SystemClock? systemClock,
required Logger logger,
}) : _mdnsClient = mdnsClient,
_systemClock = systemClock ?? globals.systemClock,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The systemClock parameter should just be required so we don't need to use globals here. In general, we shouldn't be importing globals.dart in any new files.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// in the mdns_dart package, only discovering a maximum of one service.

_logger.printStatus('Searching for running Flutter apps...');
final MDnsClient client = _mdnsClient ?? MDnsClient();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider just setting _mdnsClient = mdnsClient ?? MDnsClient() in the initialization list, then _mdnsClient can be non-nullable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Listen for pointer records (PTR) to find services
await for (final PtrResourceRecord ptr
in client
.lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer('_flutter_devices._tcp'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: '_flutter_devices._tcp' should be defined as a constant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

for (final part in parts) {
final int equalsIndex = part.indexOf('=');
if (equalsIndex != -1) {
metadata[part.substring(0, equalsIndex)] = part.substring(equalsIndex + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unlikely that we'll be seeing different formats given that it's other tool instances that will be advertising, but should we consider trimming these key/value pairs to ensure there's no extra whitespace?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, test added as well

Comment thread packages/flutter_tools/lib/src/run_hot.dart
Comment thread packages/flutter_tools/lib/src/resident_runner.dart
Comment thread packages/flutter_tools/test/commands.shard/hermetic/running_apps_test.dart Outdated
Comment thread packages/flutter_tools/test/commands.shard/hermetic/running_apps_test.dart Outdated
Comment thread packages/flutter_tools/test/commands.shard/hermetic/running_apps_test.dart Outdated
@jwren

jwren commented Jan 17, 2026

Copy link
Copy Markdown
Member Author

I fixed a few bot issues and addressed a bunch of comments, not all done, but getting closer.

@jwren jwren changed the title feat: Enhance running-apps with mDNS discovery, deduplication, and table output Add a new flutter cli command, running-apps, using mDNS app discovery Jan 21, 2026

@natebosch natebosch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there security implications for this? Should we make responding to discovery an optional feature?

/// Finds all non-loopback IPv4 and IPv6 addresses of the local machine.
///
/// If no non-loopback addresses are found, returns loopback addresses.
Future<List<InternetAddress>> getLocalInternetAddresses() async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://dart.dev/effective-dart/design#avoid-starting-a-method-name-with-get

How about get localInternetAddresses and get localIpAddress

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +60 to +62
// Use mDNS to discover running Flutter apps, the multicast_dns package is
// used instead of mdns_dart as the discovery functionality is insufficient
// in the mdns_dart package, only discovering a maximum of one service.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] this comment seems very temporally local - is this information that future readers of this code will need to be reminded of each time they read it?

How about recording this detail in the commit message?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/// Formats the elapsed time since the given epoch.
@visibleForTesting
String getProcessAge(String? epochString, SystemClock systemClock) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a noun-named method String processAge(String? epochString, SystemClock systemClock)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return 'unknown age';
}
final Duration elapsed = systemClock.now().difference(DateTime.fromMillisecondsSinceEpoch(epoch));
if (elapsed.inDays > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional] Consider a switch expression.

  return switch (systemClock.now().difference(
    DateTime.fromMillisecondsSinceEpoch(epoch),
  )) {
    Duration(:final inDays) when inDays > 0 => '${inDays}d',
    Duration(:final inHours) when inHours > 0 => '${inHours}h',
    Duration(:final inMinutes) when inMinutes > 0 => '${inMinutes}m',
    Duration(:final inSeconds) => '${inSeconds}s',
  };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding off for now, I am going to revisit having this method (see TODO.)

})(),
);
}
await Future.wait(pendingLookups);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using await pendingLookups.wait;

https://api.dart.dev/dart-async/FutureIterable/wait.html

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, here and in the other location it was used.


/// Discovers devices via mDNS and advertises the current Flutter application to them.
class MDNSDeviceDiscovery {
/// Creates a new [MDNSDeviceDiscovery] instance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] I'm not sure the Flutter repo stance on fully redundant doc comments. In Dart repos and internally to google3 we prefer omitting docs over including docs which are redundant with the signature. Consider expanding this, and if it can't be expanded consider removing this depending on Flutter repo conventions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this comment. Thanks Nate.

/// Advertises the Flutter application via mDNS.
///
/// The advertisement includes metadata about the application, device, and environment.
Future<void> advertise({required String appName, required Uri? vmServiceUri}) async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This method is over 80 lines and indents to at least 5 levels. Consider splitting this up into composed submethods to reduce nesting and method length.

}

if (await botDetector.isRunningOnBot ||
platform.environment['BOT'] == 'true' ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these two cases not covered by isRunningOnBot?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point I wasn't convinced, given that the bots are in a good state now, I am removing them to see for sure.

const kFiveSecondsMs = 5_000;
const kFiftyNineSecondsMs = 59_000;
const kOneMinuteMs = 60_000;
const kFiftyNineMinutesMs = 3_540_000; // 59 * 60 * 1000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ubernit: maybe it's worth defining these constants based on each other rather than using the final integers?

Something like:

const kSecondMs = 1000;
const kMinuteMs = kSecondMs * 60;
const kHourMs = kMinuteMs * 60;
const kDayMs = kHourMs * 24;
// ...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

);
}

Future<int> findUnusedPort({String hostname = '0.0.0.0'}) async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used outside of this file? If not, I think we can remove the optional named parameter.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

onTimeout: (EventSink<PtrResourceRecord> sink) => sink.close(),
)) {
pendingLookups.add(
(() async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still a closure being evaluated here. Is that intended?

)) {
final metadata = <String, String>{};
// The multicast_dns package joins the strings of a TXT record with newlines.
final List<String> parts = txt.text.split('\n');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these always be Unix style line endings? Would it be safer to use LineSplitter.split(...)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to LineSplitter

client.stop();
}

if (boolArg('machine')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use FlutterGlobalOptions.kMachineFlag instead.

}

if (boolArg('machine')) {
_logger.printStatus(json.encode(apps));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just return after this print and remove the else statement to reduce nesting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Used with the new compiler to generate a bootstrap file containing plugins
// and platform initialization.
Directory? _generatedEntrypointDirectory;
final _mdnsDiscoveries = <MDNSDeviceDiscovery>[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this list for?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is for cleanup which I didn't add, see resident_web_runner.dart now.

required this.botDetector,
});

static const String _kWsUri = 'ws_uri';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using the same constants as we use in the command class to ensure they don't diverge.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved by having a single object now, all the constants are defined in one place.

// Silence mDNS logs unless verbose logging is enabled.
// mdns_dart uses the 'mdns_dart' logger.
log.hierarchicalLoggingEnabled = true;
if (logger.isVerbose) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needs to be addressed.

try {
final MDNSService mdnsService = await MDNSService.create(
instance: 'Flutter Tools on $hostname',
service: '_flutter_devices._tcp',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be using a shared constant here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// is modified during iteration.
final serversToStop = List<MDNSServer>.of(_servers);
_servers.clear();
for (final server in serversToStop) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ubernit: maybe this instead?

  await serversToStop
      .map(
        (s) => s.stop().catchError(
          (e) => logger.printTrace('Error stopping mDNS server: $e'),
        ),
      )
      .wait;

That way we don't have to block waiting on each server to start individually.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Tests have been updated as well. PTAL.

Comment on lines +101 to +104
if (seenUris.contains(uri)) {
continue;
}
seenUris.add(uri);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set.add returns a boolean you can check to see if it was new:

Suggested change
if (seenUris.contains(uri)) {
continue;
}
seenUris.add(uri);
if (!seenUris.add(uri)) {
continue;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread packages/flutter_tools/lib/src/commands/running_apps.dart Outdated
}
seenUris.add(uri);
}
apps.add(metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to do this if the uri is null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion if devs are running the new command, flutter running-apps and they get content back without a URI to use, it would be better to know that the rest of the mechanism is working -- reporting an app, just not with uri. @bkonyi -- does this seem reasonable to you?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's anything actionable on the user side if there's an app without a URI, so I don't think it would hurt to just throw the entry away. Is it even possible to advertise without a VM service URI?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like FlutterVmService only has null HTTP / WS URI entries in tests, so they should never be null in practice and we should never encounter a null uri. We should update the type to be non-nullable to indicate that.

@jwren jwren left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made changes, rebased and pushed the branch again. Thank you @bkonyi!

@bkonyi bkonyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there!

import 'package:path/path.dart' as p; // flutter_ignore: package_path_import
import 'package:process/process.dart';

import '../convert.dart';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this import needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed in this class for the Encoding object reference.


void addEnableLocalDiscoveryFlag() {
argParser.addFlag(
'enable-local-discovery',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably just be part of run.dart since we're not using it in any other command. Also, we should define a constant for enable-local-discovery and use it in the error message where it's referenced.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// The advertisement includes metadata about the application, device, and environment.
Future<void> advertise({required String appName, required Uri? vmServiceUri}) async {
try {
await stop(); // Stop any existing advertisements before starting new ones.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect this to be called more than once for a given instance?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method advertise is called only once per MDNSDeviceDiscovery instance, immediately after the device connects. However, the stop() call at the beginning of advertise serves as a defensive measure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we never expect for advertise to be invoked more than once I think it'd be better to fail loudly. Being defensive here instead of raising an exception could result in unexpected behavior that could be hard to pin down.

}

// Silence mDNS logs unless verbose logging is enabled.
// mdns_dart uses the 'mdns_dart' logger.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: is this logger within package:mdns_dart? If so, I'd reference package:mdns_dart instead of just mdns_dart here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, maybe it makes sense to move this further up in the call stack where it can only be invoked once? Maybe even behind a static flag in the constructor of this class? It feels strange that we're resetting this state each time this method is invoked, particularly since hierarchicalLoggingEnabled applies to all loggers and could technically be set elsewhere.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/// Stops the mDNS advertisement.
Future<void> stop() async {
// Create a copy of the list to avoid ConcurrentModificationError as the list

@bkonyi bkonyi Feb 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? I don't see _servers being modified in the serversToStop.map(...) code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the comment.


/// A class representing the metadata discovered from a running Flutter application
/// via mDNS.
class MDnsObservation {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: MDNSObservation for consistency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

final String? targetPlatform;
final String? mode;
final String? wsUri;
final String? epoch;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think epoch and pid can still be int or double as there's implicit toString() calls when using string interpolation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// Finds all non-loopback IPv4 and IPv6 addresses of the local machine.
///
/// If no non-loopback addresses are found, returns loopback addresses.
Future<List<InternetAddress>> get localInternetAddresses async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions don't actually seem to be used outside of tests. Are they needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, tests removed as well.

static const String _kDartVersion = 'dart_version';
static const String _kHostname = 'hostname';

final String? hostname;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these types should all be non-nullable and we should reject any records that don't match the expected signature.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jwren jwren left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments addressed

@jwren jwren requested a review from bkonyi February 3, 2026 23:54
@jwren jwren added this pull request to the merge queue Feb 9, 2026
Merged via the queue into flutter:master with commit 119bbe3 Feb 9, 2026
141 checks passed
@jwren jwren deleted the mdns-3 branch February 9, 2026 08:26
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Feb 10, 2026
Roll Flutter from e8f9dc50356d to 9bda20a11f1e (34 revisions)

flutter/flutter@e8f9dc5...9bda20a

2026-02-10 brackenavaron@gmail.com Remove Material import from focus_traversal_test.dart (flutter/flutter#180994)
2026-02-10 engine-flutter-autoroll@skia.org Roll Skia from 6e217430c052 to cffb3bf918df (1 revision) (flutter/flutter#182131)
2026-02-10 divyansh.shah.ece23@itbhu.ac.in Encourage splitting large test files in testing documentation 2 (flutter/flutter#182051)
2026-02-10 34465683+rkishan516@users.noreply.github.com refactor: migrate CupertinoPageTransitionsBuilder to cupertino folder (flutter/flutter#179776)
2026-02-10 30870216+gaaclarke@users.noreply.github.com Delete the last remaining skia only fragment shader tests (flutter/flutter#182127)
2026-02-10 jhy03261997@gmail.com [a11y][android] Set new CheckState APIs for android API 36  (flutter/flutter#182113)
2026-02-10 dkwingsmt@users.noreply.github.com Add missing dependencies to framework_tests_misc_leak_tracking (flutter/flutter#181929)
2026-02-10 engine-flutter-autoroll@skia.org Roll Dart SDK from eee0e2e11174 to 69eb951f8f7e (2 revisions) (flutter/flutter#182128)
2026-02-10 fluttergithubbot@gmail.com Marks Linux_android_emu android_display_cutout to be flaky (flutter/flutter#181901)
2026-02-10 737941+loic-sharma@users.noreply.github.com Bump Dart to 3.10 (flutter/flutter#174066)
2026-02-10 engine-flutter-autoroll@skia.org Roll Skia from d4b7e24a209b to 6e217430c052 (6 revisions) (flutter/flutter#182126)
2026-02-09 15619084+vashworth@users.noreply.github.com Intercept UIScene device log and print a guided warning (flutter/flutter#181515)
2026-02-09 47866232+chunhtai@users.noreply.github.com Introduce ScrollCacheExtent and also fixes unbound shrinkwrap cache ex… (flutter/flutter#181092)
2026-02-09 43054281+camsim99@users.noreply.github.com [Android] Add mechanism for setting Android engine flags via Android manifest (take 2) (flutter/flutter#181632)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Fix wrong comment about default impeller value (flutter/flutter#181831)
2026-02-09 huqian123hq@hotmail.com fix build fail for wayland only platform (flutter/flutter#182057)
2026-02-09 jesswon@google.com [AGP 9] Added Warning Against Updating to AGP 9 (flutter/flutter#181977)
2026-02-09 30870216+gaaclarke@users.noreply.github.com Updated Shaderc dep (flutter/flutter#180976)
2026-02-09 47866232+chunhtai@users.noreply.github.com Refactor accessibility guidelines out to widget layer (flutter/flutter#181672)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 68dff53238e5 to d4b7e24a209b (2 revisions) (flutter/flutter#182087)
2026-02-09 ikramhasan.dev@gmail.com fix: OutlineInputBorder not respecting BorderSide stroke alignment (flutter/flutter#180487)
2026-02-09 97480502+b-luk@users.noreply.github.com Adds opengles to engine dart tests (flutter/flutter#181933)
2026-02-09 15619084+vashworth@users.noreply.github.com Add command to build a Swift Package for Add to App and generate FlutterPluginRegistrant (flutter/flutter#181224)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Remove unused constant in `bundle.dart` (flutter/flutter#182023)
2026-02-09 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from iqtwdXlgKIyZkL5Li... to 7BGf7mPQvgLi7Axb6... (flutter/flutter#182082)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Remove unused getters in `user_messages.dart` (flutter/flutter#181867)
2026-02-09 engine-flutter-autoroll@skia.org Roll Packages from 7805d3e to 3d5eaa5 (3 revisions) (flutter/flutter#182083)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 5d891cd7fb7f to 68dff53238e5 (1 revision) (flutter/flutter#182080)
2026-02-09 robert.ancell@canonical.com Update example description (flutter/flutter#182067)
2026-02-09 engine-flutter-autoroll@skia.org Roll Dart SDK from 965b51c219d3 to eee0e2e11174 (1 revision) (flutter/flutter#182073)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 9533d7533d59 to 5d891cd7fb7f (6 revisions) (flutter/flutter#182070)
2026-02-09 jwren@google.com Add a new flutter cli command, running-apps, using mDNS app discovery (flutter/flutter#180098)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from b7db9f35f0f2 to 9533d7533d59 (2 revisions) (flutter/flutter#182069)
2026-02-08 robert.ancell@canonical.com Improve FlWindowMonitor API (flutter/flutter#181885)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC boetger@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

...
flutter-zl pushed a commit to flutter-zl/flutter that referenced this pull request Feb 10, 2026
…flutter#180098)

Includes:
- Display running apps in a formatted table with age calculation
- Implement mDNS discovery for running apps (multiple
devices/interfaces)
- Deduplicate apps by WebSocket URI
- Centralize mDNS device advertisement in MDNSDeviceDiscovery
- Ensure ResidentRunner advertises correct app name and cleans up
- Add network utility functions and JSON support for running-apps
- Add comprehensive tests for discovery and list command

Lastly, this change uses mDNS to discover running Flutter apps, the
multicast_dns package is used instead of mdns_dart as the discovery
functionality is insufficient in the mdns_dart package, only discovering
a maximum of one service.
github-merge-queue Bot pushed a commit that referenced this pull request Feb 14, 2026
…utility function. (#182196)

This resolves #180949

This is follow-up on #180098
rickhohler pushed a commit to rickhohler/flutter that referenced this pull request Feb 19, 2026
…flutter#180098)

Includes:
- Display running apps in a formatted table with age calculation
- Implement mDNS discovery for running apps (multiple
devices/interfaces)
- Deduplicate apps by WebSocket URI
- Centralize mDNS device advertisement in MDNSDeviceDiscovery
- Ensure ResidentRunner advertises correct app name and cleans up
- Add network utility functions and JSON support for running-apps
- Add comprehensive tests for discovery and list command

Lastly, this change uses mDNS to discover running Flutter apps, the
multicast_dns package is used instead of mdns_dart as the discovery
functionality is insufficient in the mdns_dart package, only discovering
a maximum of one service.
rickhohler pushed a commit to rickhohler/flutter that referenced this pull request Feb 19, 2026
ahmedsameha1 pushed a commit to ahmedsameha1/flutter that referenced this pull request Feb 27, 2026
mboetger pushed a commit to mboetger/flutter that referenced this pull request Mar 26, 2026
…flutter#180098)

Includes:
- Display running apps in a formatted table with age calculation
- Implement mDNS discovery for running apps (multiple
devices/interfaces)
- Deduplicate apps by WebSocket URI
- Centralize mDNS device advertisement in MDNSDeviceDiscovery
- Ensure ResidentRunner advertises correct app name and cleans up
- Add network utility functions and JSON support for running-apps
- Add comprehensive tests for discovery and list command

Lastly, this change uses mDNS to discover running Flutter apps, the
multicast_dns package is used instead of mdns_dart as the discovery
functionality is insufficient in the mdns_dart package, only discovering
a maximum of one service.
mboetger pushed a commit to mboetger/flutter that referenced this pull request Mar 26, 2026
mboetger pushed a commit to mboetger/flutter that referenced this pull request Mar 26, 2026
creatorpiyush pushed a commit to creatorpiyush/packages that referenced this pull request Jun 10, 2026
…r#10992)

Roll Flutter from e8f9dc50356d to 9bda20a11f1e (34 revisions)

flutter/flutter@e8f9dc5...9bda20a

2026-02-10 brackenavaron@gmail.com Remove Material import from focus_traversal_test.dart (flutter/flutter#180994)
2026-02-10 engine-flutter-autoroll@skia.org Roll Skia from 6e217430c052 to cffb3bf918df (1 revision) (flutter/flutter#182131)
2026-02-10 divyansh.shah.ece23@itbhu.ac.in Encourage splitting large test files in testing documentation 2 (flutter/flutter#182051)
2026-02-10 34465683+rkishan516@users.noreply.github.com refactor: migrate CupertinoPageTransitionsBuilder to cupertino folder (flutter/flutter#179776)
2026-02-10 30870216+gaaclarke@users.noreply.github.com Delete the last remaining skia only fragment shader tests (flutter/flutter#182127)
2026-02-10 jhy03261997@gmail.com [a11y][android] Set new CheckState APIs for android API 36  (flutter/flutter#182113)
2026-02-10 dkwingsmt@users.noreply.github.com Add missing dependencies to framework_tests_misc_leak_tracking (flutter/flutter#181929)
2026-02-10 engine-flutter-autoroll@skia.org Roll Dart SDK from eee0e2e11174 to 69eb951f8f7e (2 revisions) (flutter/flutter#182128)
2026-02-10 fluttergithubbot@gmail.com Marks Linux_android_emu android_display_cutout to be flaky (flutter/flutter#181901)
2026-02-10 737941+loic-sharma@users.noreply.github.com Bump Dart to 3.10 (flutter/flutter#174066)
2026-02-10 engine-flutter-autoroll@skia.org Roll Skia from d4b7e24a209b to 6e217430c052 (6 revisions) (flutter/flutter#182126)
2026-02-09 15619084+vashworth@users.noreply.github.com Intercept UIScene device log and print a guided warning (flutter/flutter#181515)
2026-02-09 47866232+chunhtai@users.noreply.github.com Introduce ScrollCacheExtent and also fixes unbound shrinkwrap cache ex… (flutter/flutter#181092)
2026-02-09 43054281+camsim99@users.noreply.github.com [Android] Add mechanism for setting Android engine flags via Android manifest (take 2) (flutter/flutter#181632)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Fix wrong comment about default impeller value (flutter/flutter#181831)
2026-02-09 huqian123hq@hotmail.com fix build fail for wayland only platform (flutter/flutter#182057)
2026-02-09 jesswon@google.com [AGP 9] Added Warning Against Updating to AGP 9 (flutter/flutter#181977)
2026-02-09 30870216+gaaclarke@users.noreply.github.com Updated Shaderc dep (flutter/flutter#180976)
2026-02-09 47866232+chunhtai@users.noreply.github.com Refactor accessibility guidelines out to widget layer (flutter/flutter#181672)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 68dff53238e5 to d4b7e24a209b (2 revisions) (flutter/flutter#182087)
2026-02-09 ikramhasan.dev@gmail.com fix: OutlineInputBorder not respecting BorderSide stroke alignment (flutter/flutter#180487)
2026-02-09 97480502+b-luk@users.noreply.github.com Adds opengles to engine dart tests (flutter/flutter#181933)
2026-02-09 15619084+vashworth@users.noreply.github.com Add command to build a Swift Package for Add to App and generate FlutterPluginRegistrant (flutter/flutter#181224)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Remove unused constant in `bundle.dart` (flutter/flutter#182023)
2026-02-09 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from iqtwdXlgKIyZkL5Li... to 7BGf7mPQvgLi7Axb6... (flutter/flutter#182082)
2026-02-09 116356835+AbdeMohlbi@users.noreply.github.com Remove unused getters in `user_messages.dart` (flutter/flutter#181867)
2026-02-09 engine-flutter-autoroll@skia.org Roll Packages from 7805d3e to 3d5eaa5 (3 revisions) (flutter/flutter#182083)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 5d891cd7fb7f to 68dff53238e5 (1 revision) (flutter/flutter#182080)
2026-02-09 robert.ancell@canonical.com Update example description (flutter/flutter#182067)
2026-02-09 engine-flutter-autoroll@skia.org Roll Dart SDK from 965b51c219d3 to eee0e2e11174 (1 revision) (flutter/flutter#182073)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from 9533d7533d59 to 5d891cd7fb7f (6 revisions) (flutter/flutter#182070)
2026-02-09 jwren@google.com Add a new flutter cli command, running-apps, using mDNS app discovery (flutter/flutter#180098)
2026-02-09 engine-flutter-autoroll@skia.org Roll Skia from b7db9f35f0f2 to 9533d7533d59 (2 revisions) (flutter/flutter#182069)
2026-02-08 robert.ancell@canonical.com Improve FlWindowMonitor API (flutter/flutter#181885)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC boetger@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants