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:
@@ -617,6 +617,39 @@ public class PodInfo : Component
|
||||
asyncControlState.asyncOp.PostOperationCompleted(installProductDelegates.OnCompleted, arg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the management connection if needed. A connection the launcher has
|
||||
/// dropped still reports IsOpen (Connected is stale until an I/O fails), and
|
||||
/// the launcher drops sessions idle for ~30s — easily hit while the operator
|
||||
/// sits in the install dialogs — so probe an "open" connection with a Ping
|
||||
/// and reconnect when it turns out to be dead.
|
||||
/// </summary>
|
||||
private void EnsureConnectionAlive()
|
||||
{
|
||||
if (mConnection.IsOpen)
|
||||
{
|
||||
try
|
||||
{
|
||||
mConnection.Ping(DateTime.Now);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
mConnection.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, ManagePort), mPod.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void InstallProductWorker(string filePath, SendOrPostCallback progressCallback, AsyncOperation asyncOp, SendOrPostCallback completionMethodDelegate)
|
||||
{
|
||||
Exception ex = null;
|
||||
@@ -624,10 +657,7 @@ public class PodInfo : Component
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, 53290), mPod.Key);
|
||||
}
|
||||
EnsureConnectionAlive();
|
||||
Guid guid = mConnection.InstallProduct(filePath);
|
||||
if (guid == Guid.Empty)
|
||||
{
|
||||
@@ -709,10 +739,7 @@ public class PodInfo : Component
|
||||
Exception ex = null;
|
||||
try
|
||||
{
|
||||
if (!mConnection.IsOpen)
|
||||
{
|
||||
mConnection.Open(new IPEndPoint(mPod.IPAddress, 53290), mPod.Key);
|
||||
}
|
||||
EnsureConnectionAlive();
|
||||
mConnection.UninstallApp(launchKey);
|
||||
mAllApps.Expire();
|
||||
}
|
||||
|
||||
@@ -1138,12 +1138,27 @@ public class SiteManagement : Form
|
||||
{
|
||||
Tuple<PodData, Guid> tuple = e.UserState as Tuple<PodData, Guid>;
|
||||
PodData a = tuple.A;
|
||||
foreach (LaunchData appData in BuildLaunchData(tuple.B, a))
|
||||
Exception error = e.Error;
|
||||
// Register the launch entries only when the transfer succeeded, and never
|
||||
// let a pod that dropped mid-install take the console down (AddApp throws
|
||||
// when the connection is gone).
|
||||
if (error == null)
|
||||
{
|
||||
a.Conn.AddApp(appData);
|
||||
try
|
||||
{
|
||||
foreach (LaunchData appData in BuildLaunchData(tuple.B, a))
|
||||
{
|
||||
a.Conn.AddApp(appData);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.LogException(ex, "Registering launch entries failed.");
|
||||
error = ex;
|
||||
}
|
||||
}
|
||||
a.OperationInProgress = false;
|
||||
a.Error = e.Error;
|
||||
a.Error = error;
|
||||
dgvPods.InvalidateCell(colOperationProgress.Index, mPods.IndexOf(a));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user