diff --git a/Console/TeslaConsole/PodInfo.cs b/Console/TeslaConsole/PodInfo.cs
index 278886a..f537996 100644
--- a/Console/TeslaConsole/PodInfo.cs
+++ b/Console/TeslaConsole/PodInfo.cs
@@ -617,6 +617,39 @@ public class PodInfo : Component
asyncControlState.asyncOp.PostOperationCompleted(installProductDelegates.OnCompleted, arg);
}
+ ///
+ /// 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.
+ ///
+ 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();
}
diff --git a/Console/TeslaConsole/SiteManagement.cs b/Console/TeslaConsole/SiteManagement.cs
index d1cf2b7..5b9398b 100644
--- a/Console/TeslaConsole/SiteManagement.cs
+++ b/Console/TeslaConsole/SiteManagement.cs
@@ -1138,12 +1138,27 @@ public class SiteManagement : Form
{
Tuple tuple = e.UserState as Tuple;
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));
}
diff --git a/Console/tests/TeslaConsole.DiffTests/VPodLauncherServerTests.cs b/Console/tests/TeslaConsole.DiffTests/VPodLauncherServerTests.cs
index db42df7..98fde8d 100644
--- a/Console/tests/TeslaConsole.DiffTests/VPodLauncherServerTests.cs
+++ b/Console/tests/TeslaConsole.DiffTests/VPodLauncherServerTests.cs
@@ -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(() => 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()
{