Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/services/debug_diagnostics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -53,6 +54,10 @@ class DebugDiagnostics {
final List<TcpServerInfo> 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<MdnsQueryLogEntry> recentQueries;

const DebugDiagnostics({
required this.advertised,
required this.backend,
Expand All @@ -63,6 +68,7 @@ class DebugDiagnostics {
required this.addressReport,
required this.servers,
required this.permissions,
this.recentQueries = const [],
});

static Future<DebugDiagnostics> gather({
Expand Down Expand Up @@ -119,6 +125,7 @@ class DebugDiagnostics {
addressReport: addressReport,
servers: servers,
permissions: permissions,
recentQueries: isResponder ? advertiser.recentQueries : const [],
);
}

Expand Down Expand Up @@ -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}',
);
}
}
Comment on lines +180 to +191

b.writeln(' TCP servers:');
if (servers.isEmpty) {
b.writeln(' (none)');
Expand Down
2 changes: 1 addition & 1 deletion prop
Submodule prop updated from be5ba5 to 9ad4f7
43 changes: 43 additions & 0 deletions test/services/debug_diagnostics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<MdnsQueryLogEntry> 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)'));
});
Comment on lines +117 to +122
});
}