vPOD: real-launch auto-restart watchdog + optional postinstall.bat

Two additions to the virtual launcher's real-process mode:

- Auto-restart watchdog. Replaces the poll-on-query PruneExitedProcesses
  with a per-process watcher thread (StartWatcher): when a real-launched
  app exits on its own -- not via a Kill*/Uninstall, which untrack it
  first -- it is dropped from the running list and, if its LaunchData has
  AutoRestart and the "Auto-restart after the app exits (watchdog)"
  toggle is on, relaunched after the Agent's 2 s delay. A watchdog
  generation counter cancels pending restarts when the pod goes dark
  (power off / reboot / reprovision / WipeApps); the console's KillAllApps
  leaves them pending, matching the real Agent's race.

- postinstall.bat toggle. A "Run postinstall.bat after install" checkbox
  (above "Actually launch apps", off by default) makes an install execute
  a packaged postinstall.bat via cmd /c (waited up to 5 min) before
  deleting it, like the real service. Off, it is logged and removed unrun
  as before -- it runs package script code on the host.

Both are opt-in from the vPOD window. Verified against the real
LauncherRpcServer over a loopback socket: the watchdog test relaunches an
exited ping.exe with a new PID and stops once toggled off; a crafted
package's postinstall.bat runs (and is removed) only when enabled. Full
differential suite 103/103.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-10 16:30:28 -05:00
co-authored by Claude Fable 5
parent 80ee1d26ea
commit 13f8e0456b
5 changed files with 244 additions and 55 deletions
@@ -230,6 +230,50 @@ namespace TeslaConsole.DiffTests
Assert.Empty(mClient.GetLaunchedApps());
}
[Fact]
public void RealLaunch_Watchdog_Restarts_AutoRestart_Apps_That_Exit_On_Their_Own()
{
mLauncher.RealLaunch = true;
mLauncher.RealAutoRestart = true;
var app = new LaunchData
{
LaunchPair = new LaunchPair { LaunchKey = Guid.NewGuid(), DisplayName = "Short Pinger" },
WorkingDirectory = Environment.SystemDirectory,
ExeFile = Path.Combine(Environment.SystemDirectory, "ping.exe"),
Arguments = "-n 3 127.0.0.1", // exits on its own after ~2 s
AutoRestart = true
};
mClient.InstallApp(app);
int firstPid = mClient.LaunchApp(app.LaunchPair.LaunchKey);
Assert.True(firstPid > 0);
// The app exits by itself; the watchdog must bring up a NEW pid
// (exit ~2 s + the Agent's 2 s restart delay).
int restartedPid = 0;
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(20);
while (DateTime.UtcNow < deadline)
{
var launched = mClient.GetLaunchedApps();
if (launched.Length == 1 && launched[0].ProcessId != firstPid)
{
restartedPid = launched[0].ProcessId;
break;
}
Thread.Sleep(100);
}
Assert.True(restartedPid > 0, "watchdog did not restart the exited app");
// Turning the watchdog off ends the cycle: the current instance
// exits on its own and nothing relaunches it.
mLauncher.RealAutoRestart = false;
deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
while (DateTime.UtcNow < deadline && mClient.GetLaunchedApps().Length > 0)
{
Thread.Sleep(200);
}
Assert.Empty(mClient.GetLaunchedApps());
}
[Fact]
public void RealLaunch_Missing_Exe_Surfaces_The_Agents_Error()
{