From 8b8b94b0af53fd3007de932dad50b1c969e35058 Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Thu, 30 Jul 2026 13:11:24 +0200 Subject: [PATCH] fix(diagnostics): surface received mDNS queries; bump prop submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the "mDNS queries received" section to the diagnostics report, listing each query the responder saw with its source, the QU bit and whether the reply went out unicast, multicast, both or not at all. This is the missing half of diagnosing "the trainer app on this machine can't find BikeControl". A same-host querier appears with the advertised address as its source, so the report now distinguishes three very different situations: the app never queried us at all, it queried for the service (PTR/SRV — the browse step), or it queried for our hostname (A — the resolve step). Empty on iOS, where the OS responder handles queries and never reports them to us. Picks up the prop-side fix for the Windows shared-port QU delivery problem. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PanTG9kRBi4QTQ2psvpTxP --- lib/services/debug_diagnostics.dart | 23 ++++++++++++ prop | 2 +- test/services/debug_diagnostics_test.dart | 43 +++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) 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)')); + }); + }); }