blob: 13dfe80f138fcf447e355c36b4c26b1fe03d2c81 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:logging/logging.dart' as log;
import 'package:mdns_dart/mdns_dart.dart';
import 'package:vm_service/vm_service.dart' as vmservice;
import 'base/bot_detector.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'base/time.dart';
import 'build_info.dart';
import 'convert.dart';
import 'device.dart';
import 'version.dart';
/// Advertises the current Flutter application to other devices via mDNS.
class MDNSDeviceDiscovery {
MDNSDeviceDiscovery({
required this.device,
required this.vmService,
required this.debuggingOptions,
required this.logger,
required this.platform,
required this.flutterVersion,
required this.systemClock,
required this.botDetector,
}) {
_configureLogging(logger);
}
static const String kFlutterDevicesService = '_flutter_devices._tcp';
final Device device;
final vmservice.VmService vmService;
final DebuggingOptions debuggingOptions;
final Logger logger;
final Platform platform;
final FlutterVersion flutterVersion;
final SystemClock systemClock;
final BotDetector botDetector;
static bool _loggingConfigured = false;
static void _configureLogging(Logger logger) {
if (_loggingConfigured) {
return;
}
// Silence mDNS logs unless verbose logging is enabled.
// package:mdns_dart uses the 'mdns_dart' logger.
log.hierarchicalLoggingEnabled = true;
log.Logger('mdns_dart').level = logger.isVerbose ? log.Level.ALL : log.Level.SEVERE;
_loggingConfigured = true;
}
final _servers = <MDNSServer>[];
/// 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 {
try {
if (_servers.isNotEmpty) {
throw StateError(
'mDNS advertisement is already active. Call stop() before starting a new advertisement.',
);
}
if (vmServiceUri == null) {
logger.printTrace('VM Service URI not available, not starting mDNS server.');
return;
}
if (!debuggingOptions.enableLocalDiscovery) {
logger.printTrace('mDNS local discovery is disabled.');
return;
}
if (await botDetector.isRunningOnBot) {
logger.printTrace('Running on CI/Bot, not starting mDNS server.');
return;
}
final ips = <InternetAddress>[InternetAddress.loopbackIPv4, InternetAddress.loopbackIPv6];
final String hostname = platform.localHostname;
final TargetPlatform targetPlatform = await device.targetPlatform;
final String frameworkVersion = flutterVersion.frameworkVersion;
final String dartVersion = flutterVersion.dartSdkVersion;
final observation = MDNSObservation(
wsUri: vmServiceUri.toString(),
hostname: hostname,
deviceName: device.name,
targetPlatform: getNameForTargetPlatform(targetPlatform),
mode: debuggingOptions.buildInfo.modeName,
epoch: systemClock.now().millisecondsSinceEpoch,
projectName: appName,
deviceId: device.id,
flutterVersion: frameworkVersion,
dartVersion: dartVersion,
pid: pid,
);
final List<String> txt = observation.toTxtRecord();
// Advertise on all available interfaces (IPv4 and IPv6).
for (final ip in ips) {
try {
final MDNSService mdnsService = await MDNSService.create(
instance: 'Flutter Tools on $hostname',
service: kFlutterDevicesService,
port: vmServiceUri.port,
ips: <InternetAddress>[ip],
txt: txt,
);
final server = MDNSServer(
MDNSServerConfig(zone: mdnsService, reusePort: true, logger: logger.printTrace),
);
try {
await server.start();
_servers.add(server);
logger.printTrace(
'mDNS service started for ${device.name} with appName "$appName" on ${ip.address}',
);
} on Exception catch (e) {
logger.printError('Error starting mDNS server on ${ip.address}: $e');
// Ensure we clean up any resources if start partially succeeded.
// mdns_dart server.start might leave sockets open if it throws.
try {
await server.stop();
} on Exception {
// Ignore errors during cleanup of failed start.
}
}
} on Exception catch (e) {
logger.printError('Error starting mDNS server on ${ip.address}: $e');
}
}
} on Exception catch (e) {
logger.printError('Error getting local IPs or starting mDNS: $e');
}
}
/// Stops the mDNS advertisement.
Future<void> stop() async {
// Create a copy of the list so that the original list can be cleared
// immediately to prevent re-entrant calls.
final serversToStop = List<MDNSServer>.of(_servers);
_servers.clear();
await Future.wait<void>(
serversToStop.map(
(MDNSServer server) => server.stop().catchError((Object e) {
logger.printTrace('Error stopping mDNS server: $e');
}, test: (Object error) => error is Exception),
),
);
}
}
/// A class representing the metadata discovered from a running Flutter application
/// via mDNS.
class MDNSObservation {
MDNSObservation({
required this.hostname,
required this.projectName,
required this.deviceName,
required this.deviceId,
required this.targetPlatform,
required this.mode,
required this.wsUri,
required this.epoch,
required this.pid,
required this.flutterVersion,
required this.dartVersion,
});
static const String _kProjectName = 'project_name';
static const String _kDeviceName = 'device_name';
static const String _kDeviceId = 'device_id';
static const String _kTargetPlatform = 'target_platform';
static const String _kMode = 'mode';
static const String _kWsUri = 'ws_uri';
static const String _kEpoch = 'epoch';
static const String _kPid = 'pid';
static const String _kFlutterVersion = 'flutter_version';
static const String _kDartVersion = 'dart_version';
static const String _kHostname = 'hostname';
final String hostname;
final String projectName;
final String deviceName;
final String deviceId;
final String targetPlatform;
final String mode;
final String wsUri;
final int epoch;
final int pid;
final String flutterVersion;
final String dartVersion;
/// Parses a raw TXT record string into an [MDNSObservation].
///
/// Returns `null` if the record is empty or invalid.
static MDNSObservation? parse(String txtRecord) {
final metadata = <String, String>{};
// The multicast_dns package joins the strings of a TXT record with newlines.
final Iterable<String> parts = LineSplitter.split(txtRecord);
for (final part in parts) {
final int equalsIndex = part.indexOf('=');
if (equalsIndex != -1) {
// Trim to remove any whitespace that may be around the delimiters.
metadata[part.substring(0, equalsIndex).trim()] = part.substring(equalsIndex + 1).trim();
}
}
if (metadata.isEmpty) {
return null;
}
final int? epoch = int.tryParse(metadata[_kEpoch] ?? '');
final int? pid = int.tryParse(metadata[_kPid] ?? '');
if (metadata case {
_kHostname: final String hostname,
_kProjectName: final String projectName,
_kDeviceName: final String deviceName,
_kDeviceId: final String deviceId,
_kTargetPlatform: final String targetPlatform,
_kMode: final String mode,
_kWsUri: final String wsUri,
_kFlutterVersion: final String flutterVersion,
_kDartVersion: final String dartVersion,
} when epoch != null && pid != null) {
return MDNSObservation(
hostname: hostname,
projectName: projectName,
deviceName: deviceName,
deviceId: deviceId,
targetPlatform: targetPlatform,
mode: mode,
wsUri: wsUri,
epoch: epoch,
pid: pid,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
);
}
return null;
}
/// Converts the observation to a list of strings for mDNS TXT record.
List<String> toTxtRecord() {
return <String>[
'$_kHostname=$hostname',
'$_kWsUri=$wsUri',
'$_kPid=$pid',
'$_kTargetPlatform=$targetPlatform',
'$_kMode=$mode',
'$_kEpoch=$epoch',
'$_kProjectName=$projectName',
'$_kDeviceName=$deviceName',
'$_kDeviceId=$deviceId',
'$_kFlutterVersion=$flutterVersion',
'$_kDartVersion=$dartVersion',
];
}
Map<String, String> toJson() {
return <String, String>{
_kHostname: hostname,
_kProjectName: projectName,
_kDeviceName: deviceName,
_kDeviceId: deviceId,
_kTargetPlatform: targetPlatform,
_kMode: mode,
_kWsUri: wsUri,
_kEpoch: epoch.toString(),
_kPid: pid.toString(),
_kFlutterVersion: flutterVersion,
_kDartVersion: dartVersion,
};
}
}