Commit Graph
7 Commits
Author SHA1 Message Date
EdnunpandClaude Opus 4.8 541f66d379 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>
2026-07-14 19:52:38 +01:00
EdnunpandClaude Opus 4.8 92ed477ddf Service: harden for unattended running (auto-restart, findable logs, resume)
Local checkpoint - NOT for public release. A "what else does a service need" pass.

- AUTO-RESTART ON CRASH: DoInstall now sets sc failure actions (restart 5s/10s/then 60s,
  reset daily). Without this a crashed service stays dead until reboot - fatal for an
  always-on streamer.
- FINDABLE LOGS: --run-service redirects the service's data dir to the machine-wide
  ProgramData\RemSound\service location, so its log sits next to its profile instead of
  buried in the SYSTEM account's AppData.
- POWER RESUME: the service handles OnPowerEvent and re-opens capture on wake (audio
  devices re-initialise after sleep; the device-change watcher usually catches it, but a
  resume doesn't always fire an endpoint change, so we re-open explicitly).
- Start/stop already auto-log to the Windows Event Log via ServiceBase.

Test: "Service registration args" now also checks the audio-service dependency and the
auto-restart failure args. Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:43:33 +01:00
EdnunpandClaude Opus 4.8 4f5265d8b0 Service: isolate the profile in ProgramData, out of all normal profile paths
Local checkpoint - NOT for public release. Ed: the service profile must never be
reachable except through the Service menu.

Also fixes a real bug: the service runs as SYSTEM, whose per-user data folder is NOT the
interactive user's - so a profile saved in the user's profiles folder (or AppConfig, both
per-user) was invisible to the service. It would have idled, never streaming.

- New RemSound.Core.ServiceStore: the service profile + its settings (logging) live in a
  MACHINE-WIDE ProgramData\RemSound\service location - same absolute path for the user
  (config dialog) and SYSTEM (service). Moved ServiceProfileName/ServiceLoggingEnabled off
  AppConfig (per-user) onto this store.
- ServiceSendHost.FromConfig + RemSoundService now read ServiceStore; ConfigureServiceProfile
  saves there (and migrates + deletes any profile left in the old user-folder location).
- Because it's no longer in the user's profiles folder, it can't appear in the startup
  picker, File->Open, Recent profiles, or the password manager (all of which read the user
  ProfileStore); the reserved-title filter in ListProfileTitles stays as belt-and-braces.
- Password button renamed "Set service profile password".
- New self-test "Service profile isolation": store is under ProgramData, the reserved title
  is filtered from the listing, and it round-trips through the machine-wide store.

Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:23:10 +01:00
EdnunpandClaude Opus 4.8 78a9aa6572 Service: fix divergences from the main app's send path
Local checkpoint - NOT for public release. Audited ServiceSendHost against MainForm's
send path (Ed: make the service reuse the same code, be just as stable). Three real
divergences found and fixed:

1. ENCRYPTION FINGERPRINT (critical): the host set sender.AudioKey but NOT
   sender.AudioFingerprint. The main app (RecomputeAudioCrypto) sets both, and the peer
   verifies the fingerprint before accepting a stream - so the service's encrypted audio
   would have been REJECTED at the far end. Now derives and sets both from the password.
2. OPUS FRAME: the main app applies EffectiveOpusFrameSamples (the "Small" send rate
   halves the Opus frame); the host passed the raw frame, so it would encode differently
   than the main app for the same profile. Now reuses MainForm.EffectiveOpusFrameSamples
   (made internal - same code, not a copy).
3. PEER PORT: send target fell back to the profile's LOCAL AudioPort; the correct default
   is RemPacket.DefaultPeerDialPort (what the main app's manual-peer path uses). Same value
   today but the right constant.

Reviewed and OK: sender defaults to WasapiOnly (no SetAudioMode needed); BuildSendSpecs
matches ApplySendSources for explicit-device profiles; default-device changes are covered
by the device-change watcher; direct-send (no relay/StartReceiving) is the intended v1 scope.

New self-test "Service sender parity" asserts key + fingerprint + effective Opus frame
match the main app. Gate 26/26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:13:48 +01:00
EdnunpandClaude Opus 4.8 4fb490b2d5 Service: use the main app's event-driven device watcher, not a poll
Local checkpoint - NOT for public release. Ed: don't add continuous background checks
(they've piled up before) - reuse the disconnect/reconnect mechanism the main app uses.

Replaced the 5s packet-flow health poll with AudioDeviceChangeNotifier - the SAME
event-driven watcher the main window uses. It fires only when a device is added/removed/
changes state or the default changes (nothing polls, nothing accumulates - it's a single
registered COM callback, disposed with the host). While the service intends to send, that
event re-opens capture, covering: the audio stack finishing coming up at boot, a device
plugged/unplugged, and the audio service restarting. Debounced (a hot-plug fires a burst).

Also dropped the per-tick Resume retry: now one start attempt per app-absence, then the
device watcher drives any re-open. The only remaining periodic thing is the tiny 1s
presence-token check for the app-yield (a mutex probe - allocates nothing, accumulates
nothing). Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:04:26 +01:00
EdnunpandClaude Opus 4.8 a7742fe2ec Service: self-heal if capture isn't ready (or a device drops) at start
Local checkpoint - NOT for public release. Answers "what if it looks for sound
devices before their services are up?"

Two layers now:
- depend= Audiosrv/AudioEndpointBuilder (prior commit) makes Windows start the service
  only once the audio services are running.
- Self-heal in the run loop: while it should be sending, if no packets have flowed for
  ~5s then no capture is actually running (endpoints not fully ready at boot, a device
  dropped, or the audio service restarted) - it re-opens the capture. And while NOT
  sending it already re-tries every second, so a slow-to-appear audio stack or a device
  that returns later is picked up automatically.

So even if the service races ahead of the endpoints being fully enumerated, it keeps
retrying/re-opening until audio actually flows, rather than sitting silent. Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:52:16 +01:00
EdnunpandClaude Opus 4.8 e648bee531 Lock-screen service: spike + headless send host + app-yield coordination
Local checkpoint - NOT for public release. First increment of the send-only
Windows service feature (design in memory/project_remsound_service).

THE SPIKE PASSED: the send engine runs fully headless (no window, no message
pump) and streams, proven by a real self-test — the one genuine unknown that
gated the whole feature. Also proves the app-yield model end to end.

What's in this increment (all headless, all tested, 19/19 gate):
- AppConfig: ServiceProfileName + ServiceLoggingEnabled (machine-wide).
- InteractivePresence (Core): the cross-session app-yield token. App holds a
  Global\ mutex for its lifetime; the service checks it and yields while an
  interactive app is present, resuming when it closes OR crashes (OS frees the
  mutex). Name-parameterised internal seams for isolated testing.
- ServiceSendHost (App): loads a send-only profile and streams it to its peers,
  WASAPI-only, no ASIO/receive. ApplyProfile/Suspend/Resume + a RunLoop that
  drives them from the presence token with a settle delay. v1 sends to direct
  peer addresses (LAN/port-forwarded); NAT/relay discovery stays the app's job.
- Program.cs: the interactive app now acquires the presence token at startup so
  a future service yields to it.
- Tests: "Service app-yield token" (held=present, released=absent) and "Service
  send host (headless stream + yield)" — streams a captured device to a local
  receiver over loopback, verifies start/suspend/resume, then drives the full
  RunLoop against the token (held=suspended, released=resumes-and-flows).

Still to come (later increments): the --run-service entry + Windows-service
registration, the Service menu, the 3-tab config dialog, updater integration,
docs. None user-facing yet, so nothing deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:59:31 +01:00