Console: survive a launcher-dropped connection during product installs

The launcher drops RPC sessions idle >30s (easily hit while the operator sits
in the install dialogs), but a dropped socket still reports Connected until an
I/O fails. InstallProductWorker then skipped its reopen, streamed the archive
on the fresh out-of-band connection, and died on the first progress poll —
and the install-completed handler registered launch entries without checking
e.Error, so AddApp's disconnected-guard exception crashed the whole console.

- PodInfo.EnsureConnectionAlive: probe an "open" connection with a Ping and
  reconnect when it is dead; used by the install and uninstall workers.
- SiteManagement.PodInfo_InstallProductCompleted: register launch entries only
  on success, and surface registration failures as the row's Install Failed
  state instead of an unhandled exception.
- Regression test pins the premise + recovery against vPOD's launcher server
  (stale socket reports open, Ping exposes it, reconnect restores service).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-09 21:40:14 -05:00
co-authored by Claude Fable 5
parent 1daaf4af78
commit e0a3c72370
3 changed files with 74 additions and 11 deletions
@@ -199,6 +199,27 @@ namespace TeslaConsole.DiffTests
Assert.Empty(mLauncher.GetInstalledApps());
}
[Fact]
public void Dropped_Session_Still_Reports_Open_Until_Probed_Then_Reconnect_Works()
{
// The launcher (and vPOD) drops sessions idle >30s. Simulate the drop
// server-side and pin the premise PodInfo.EnsureConnectionAlive relies
// on: the client socket still REPORTS open until an I/O fails...
mServer.Stop();
mServer.Start(mKey);
Thread.Sleep(100); // let the FIN arrive
Assert.True(mClient.IsOpen, "a dropped-but-unused connection should still report open (the stale state)");
// ...the cheap Ping probe is what exposes the dead connection...
Assert.ThrowsAny<Exception>(() => mClient.Ping(DateTime.UtcNow));
// ...and close + reopen (EnsureConnectionAlive's recovery) restores service.
mClient.Close();
mClient.Open(new IPEndPoint(IPAddress.Loopback, mPort), mKey);
var now = new DateTime(2026, 7, 9, 12, 0, 0, DateTimeKind.Utc);
Assert.Equal(now, mClient.Ping(now));
}
[Fact]
public void Installed_Apps_Persist_Across_Launcher_Restart()
{