diff --git a/lib/services/debug_diagnostics.dart b/lib/services/debug_diagnostics.dart index 6432c4a76..1f1bb23bc 100644 --- a/lib/services/debug_diagnostics.dart +++ b/lib/services/debug_diagnostics.dart @@ -3,6 +3,7 @@ import 'dart:io' show Platform; import 'package:bike_control/main.dart' show recordError; import 'package:bike_control/services/mdns_discovery_scan.dart'; import 'package:flutter/foundation.dart'; +import 'package:prop/mdns/mdns_responder.dart' show MdnsQueryLogEntry; import 'package:prop/mdns/service_advertiser.dart'; import 'package:prop/utils/advertised_service_registry.dart'; import 'package:prop/utils/network_address.dart'; @@ -53,6 +54,10 @@ class DebugDiagnostics { final List servers; final PermissionsSnapshot permissions; + /// mDNS queries our responder saw. Empty on the nsd backend (iOS), where the + /// OS responder handles queries and never tells us about them. + final List recentQueries; + const DebugDiagnostics({ required this.advertised, required this.backend, @@ -63,6 +68,7 @@ class DebugDiagnostics { required this.addressReport, required this.servers, required this.permissions, + this.recentQueries = const [], }); static Future gather({ @@ -119,6 +125,7 @@ class DebugDiagnostics { addressReport: addressReport, servers: servers, permissions: permissions, + recentQueries: isResponder ? advertiser.recentQueries : const [], ); } @@ -167,6 +174,22 @@ class DebugDiagnostics { b.writeln(' ${c.interfaceName}/${c.address} = ${c.score}${tags.isEmpty ? '' : ' (${tags.join(', ')})'}'); } + // "Did the trainer app on this machine ask us anything, and did we answer?" + // A same-host querier shows up with the advertised address as its source. + // No entries at all means the failure is upstream of this responder. + b.writeln(' mDNS queries received:'); + if (recentQueries.isEmpty) { + b.writeln(' (none)'); + } else { + for (final q in recentQueries) { + final at = q.at.toIso8601String().split('T').last.split('.').first; + b.writeln( + ' $at ${q.source}:${q.sourcePort} ${q.wantsUnicast ? 'QU' : 'QM'} ' + '${q.questions.join(', ')} → ${q.reply}', + ); + } + } + b.writeln(' TCP servers:'); if (servers.isEmpty) { b.writeln(' (none)'); diff --git a/prop b/prop index be5ba5e48..9ad4f70f1 160000 --- a/prop +++ b/prop @@ -1 +1 @@ -Subproject commit be5ba5e48a6df7dd97b632e620b08c901c13890a +Subproject commit 9ad4f70f1fa1acd3f7b0b594b469fcc7099d3355 diff --git a/test/services/debug_diagnostics_test.dart b/test/services/debug_diagnostics_test.dart index fed93e548..112a4f0f9 100644 --- a/test/services/debug_diagnostics_test.dart +++ b/test/services/debug_diagnostics_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:bike_control/services/debug_diagnostics.dart'; import 'package:bike_control/services/mdns_discovery_scan.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:prop/mdns/mdns_responder.dart' show MdnsQueryLogEntry; import 'package:prop/utils/advertised_service_registry.dart'; import 'package:prop/utils/network_address.dart'; @@ -78,4 +79,46 @@ void main() { expect(diag.toText(), contains('Discovered on network:')); expect(diag.toText(), contains('(skipped)')); }); + + group('mDNS queries received', () { + DebugDiagnostics withQueries(List queries) => DebugDiagnostics( + advertised: const [], + backend: 'responder', + hostLabel: 'bikecontrol-3f2a', + holdsMulticastLock: false, + discovered: const [], + discoveryRan: false, + addressReport: const AddressPickReport(chosen: null, candidates: []), + servers: const [], + permissions: const PermissionsSnapshot(localNetworkInferred: null), + recentQueries: queries, + ); + + test('renders each query with its source, QU bit and how it was answered', () { + final text = withQueries([ + MdnsQueryLogEntry( + at: DateTime(2026, 7, 30, 9, 41, 12), + source: '192.168.178.92', + sourcePort: 5353, + wantsUnicast: true, + questions: const ['PTR _wahoo-fitness-tnp._tcp.local'], + answeredUnicast: true, + answeredMulticast: true, + ), + ]).toText(); + + expect(text, contains('mDNS queries received:')); + expect(text, contains('192.168.178.92:5353')); + expect(text, contains('QU')); + expect(text, contains('PTR _wahoo-fitness-tnp._tcp.local')); + expect(text, contains('unicast+multicast')); + }); + + test('says so when nothing has queried us', () { + // The decisive line for "the trainer app on this machine cannot see + // BikeControl": if no query ever arrived, the problem is upstream of our + // responder, not in how we answer. + expect(withQueries(const []).toText(), contains('(none)')); + }); + }); }