Make the service a reachable network peer (discoverable + connectable)
The send-only service could never be found or connected to — it only pushed audio blindly to fixed peer addresses, with no beacon and nothing listening. So a phone could neither discover it nor dial it. This gives the service a real network presence built from the SAME components the interactive app uses, wired the same way, so its discovery / heartbeat / NAT-pinhole / relay behaviour is identical to the app's — which is what makes it "one identity" (both announce under the machine name and pair through the relay the same way). Works LAN and, inheriting the app's relay path, across the internet. New ServiceNetworkPresence (reuses PeerDiscoveryService + AudioReceiver listener + HeartbeatService, wired to the host's AudioSender): - Discoverable: announces send-only under the machine name (LAN broadcast + unicast to the configured peers for across-the-internet). - Reachable: binds the well-known audio-port listener; PLAYBACK stays OFF (send-only never plays received audio — the listener only carries heartbeat/pairing). - Pairable: heartbeat pings the peers (opens the NAT pinhole, drives relay pairing); replies route back on the listener (LAN) or the sender socket (relay). Integrated into ServiceSendHost: comes up alongside the sender while streaming, and — critically — tears ALL the way down to a shell on Suspend (stop announcing, unbind the port, stop the heartbeat) so the service and the interactive app never both hold the network. A brief dropout on that handover is accepted (Ed's call); only one owns the network at a time. Tests: "Service network presence" (Start binds the listener + comes up; Stop unbinds to a shell; re-startable). "Service send host" now also asserts the presence comes up with streaming and drops to a shell on Suspend. Gate 33/33. NOTE: the live discover/connect/relay path can only be proven by the tester's phone — the headless tests prove the lifecycle and teardown, not the internet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
91f5fc74c0
commit
541f66d379
@@ -66,6 +66,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||
RunStep(results, "Service profile isolation (location + hidden from pickers)", ServiceProfileIsolation);
|
||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||
RunStep(results, "Service network presence (reachable + shell teardown)", ServiceNetworkPresenceReachable);
|
||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||
@@ -857,12 +858,14 @@ internal static class SelfTest
|
||||
|
||||
Check(host.ApplyProfile(profile), "ApplyProfile should start streaming");
|
||||
Check(host.IsSending, "host should report sending after ApplyProfile");
|
||||
Check(host.IsNetworkPresenceUpForTest, "the network presence must come up with streaming (discoverable + reachable)");
|
||||
Thread.Sleep(500);
|
||||
var afterStart = receiver.PacketsReceived;
|
||||
Check(afterStart > 0, $"packets must flow from the service host (got {afterStart})");
|
||||
|
||||
host.Suspend();
|
||||
Check(!host.IsSending, "host should report not sending after Suspend");
|
||||
Check(!host.IsNetworkPresenceUpForTest, "the network presence must drop to a shell on Suspend (nothing left on the network for the app to fight)");
|
||||
Thread.Sleep(200);
|
||||
var atSuspend = receiver.PacketsReceived;
|
||||
Thread.Sleep(400);
|
||||
@@ -1402,6 +1405,45 @@ internal static class SelfTest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The service's network presence (what makes it discoverable + connectable, not just a blind
|
||||
/// push): Start binds the well-known-port listener and comes up; Stop tears it ALL the way down to a
|
||||
/// shell (listener unbound) so the interactive app can own the network; and it's re-startable (the
|
||||
/// service resuming after the app closes). Uses a free port so it never fights a real RemSound.</summary>
|
||||
private static string? ServiceNetworkPresenceReachable()
|
||||
{
|
||||
var sender = new RemSound.Sender.AudioSender();
|
||||
var presence = new ServiceNetworkPresence(sender, null);
|
||||
try
|
||||
{
|
||||
var peers = new List<IPEndPoint> { new(IPAddress.Loopback, RemPacket.DefaultPeerDialPort) };
|
||||
|
||||
presence.Start(FreeUdpPort(), peers);
|
||||
Check(presence.IsUp, "presence must report up after Start");
|
||||
Check(presence.ListenerBound, "the well-known-port listener must be bound so a peer can reach the service");
|
||||
|
||||
presence.Stop();
|
||||
Check(!presence.IsUp, "presence must report down after Stop");
|
||||
Check(!presence.ListenerBound, "Stop must unbind the listener — no footprint left for the interactive app to fight over");
|
||||
|
||||
presence.Start(FreeUdpPort(), peers);
|
||||
Check(presence.IsUp && presence.ListenerBound, "presence must come back up after a stop/start cycle (resume after the app closes)");
|
||||
return "presence binds the listener on Start, tears fully down (shell) on Stop, and is re-startable";
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { presence.Dispose(); } catch { /* ignore */ }
|
||||
try { sender.Dispose(); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
private static int FreeUdpPort()
|
||||
{
|
||||
using var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork,
|
||||
System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
|
||||
s.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)s.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
private static int CountControls(Control root, Func<Control, bool> predicate)
|
||||
{
|
||||
var n = 0;
|
||||
|
||||
Reference in New Issue
Block a user