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:
@@ -230,6 +230,50 @@ namespace TeslaConsole.DiffTests
|
|||||||
Assert.Empty(mClient.GetLaunchedApps());
|
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]
|
[Fact]
|
||||||
public void RealLaunch_Missing_Exe_Surfaces_The_Agents_Error()
|
public void RealLaunch_Missing_Exe_Surfaces_The_Agents_Error()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -28,8 +29,9 @@ namespace VPod;
|
|||||||
/// - Install completion reports 99% (not 100) — the console's
|
/// - Install completion reports 99% (not 100) — the console's
|
||||||
/// InstallProductWorker breaks its retry loop only on 99.
|
/// InstallProductWorker breaks its retry loop only on 99.
|
||||||
///
|
///
|
||||||
/// All state lives in <see cref="VirtualLauncher" />; unlike the real service,
|
/// All state lives in <see cref="VirtualLauncher" />; a packaged postinstall.bat
|
||||||
/// postinstall.bat from a package is logged but never executed.
|
/// is logged and removed unrun unless <see cref="VirtualLauncher.RunPostInstall" />
|
||||||
|
/// is set from the vPOD window, in which case it is executed like the real service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LauncherRpcServer
|
internal sealed class LauncherRpcServer
|
||||||
{
|
{
|
||||||
@@ -362,12 +364,48 @@ internal sealed class LauncherRpcServer
|
|||||||
Log?.Invoke($"Install {callId:N}: extracted to {gamesRoot}");
|
Log?.Invoke($"Install {callId:N}: extracted to {gamesRoot}");
|
||||||
|
|
||||||
// The real service runs (then deletes) a packaged postinstall.bat here.
|
// The real service runs (then deletes) a packaged postinstall.bat here.
|
||||||
// vPOD never executes package scripts on the host machine.
|
// vPOD only does so when the operator opts in via RunPostInstall;
|
||||||
|
// otherwise the script is logged and removed unrun (default), since it
|
||||||
|
// runs package code on the host machine.
|
||||||
string postInstall = Path.Combine(gamesRoot, "postinstall.bat");
|
string postInstall = Path.Combine(gamesRoot, "postinstall.bat");
|
||||||
if (File.Exists(postInstall))
|
if (File.Exists(postInstall))
|
||||||
{
|
{
|
||||||
mLauncher.UpdateProgress(callId, 96, "Skipping postinstall (vPOD)...");
|
if (mLauncher.RunPostInstall)
|
||||||
Log?.Invoke($"Install {callId:N}: postinstall.bat present — NOT executed (vPOD), removed.");
|
{
|
||||||
|
mLauncher.UpdateProgress(callId, 96, "Running postinstall...");
|
||||||
|
Log?.Invoke($"Install {callId:N}: running postinstall.bat...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStartInfo psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "cmd.exe",
|
||||||
|
Arguments = "/c \"" + postInstall + "\"",
|
||||||
|
WorkingDirectory = gamesRoot,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using (Process proc = Process.Start(psi))
|
||||||
|
{
|
||||||
|
if (proc.WaitForExit(300000))
|
||||||
|
{
|
||||||
|
Log?.Invoke($"Install {callId:N}: postinstall.bat exited with code {proc.ExitCode}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log?.Invoke($"Install {callId:N}: postinstall.bat still running after 5 min — leaving it, continuing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"Install {callId:N}: postinstall.bat failed to run: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mLauncher.UpdateProgress(callId, 96, "Skipping postinstall (vPOD)...");
|
||||||
|
Log?.Invoke($"Install {callId:N}: postinstall.bat present — NOT executed (vPOD), removed.");
|
||||||
|
}
|
||||||
try { File.Delete(postInstall); } catch { }
|
try { File.Delete(postInstall); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-9
@@ -110,17 +110,24 @@ launch entries point — and reports the launcher's usual `0–50%` receive /
|
|||||||
admin-owned `C:\Games` isn't writable by your account, the install reports
|
admin-owned `C:\Games` isn't writable by your account, the install reports
|
||||||
Failed — fix the folder's ACL or run vPOD elevated). Uninstalling a product
|
Failed — fix the folder's ACL or run vPOD elevated). Uninstalling a product
|
||||||
removes its `C:\Games\<product>` folder, like the real launcher. A packaged
|
removes its `C:\Games\<product>` folder, like the real launcher. A packaged
|
||||||
`postinstall.bat` is logged and removed but **never executed**.
|
`postinstall.bat` is logged and removed **unrun by default**; the **"Run
|
||||||
|
postinstall.bat after install"** checkbox makes the install execute it (via
|
||||||
|
`cmd /c`, waited on up to 5 min) before deleting it, like the real Agent —
|
||||||
|
off by default because it runs package script code on the host machine.
|
||||||
- **Launch/Kill** from the squad panel simulate PIDs by default. The
|
- **Launch/Kill** from the squad panel simulate PIDs by default. The
|
||||||
**"Actually launch apps (real processes)"** checkbox switches to the real
|
**"Actually launch apps (real processes)"** checkbox switches to the real
|
||||||
Agent's behavior: LaunchApp starts the entry's exe from `C:\Games` (missing
|
Agent's behavior: LaunchApp starts the entry's exe from `C:\Games` (missing
|
||||||
exe → the same "registered but not yet installed" error a real pod gives),
|
exe → the same "registered but not yet installed" error a real pod gives),
|
||||||
Kill\* terminate the processes, and apps that exit or crash on their own
|
Kill\* terminate the processes, and apps that exit or crash on their own
|
||||||
disappear from the console's running list. Real processes also die when the
|
disappear from the console's running list. The indented **"Auto-restart
|
||||||
pod is powered off / rebooted / the vPOD window closes. Caveat: launching a
|
after the app exits (watchdog)"** checkbox (on by default) adds the Agent's
|
||||||
*deployed vPOD* or a real game client this way will fight the running vPOD
|
watchdog: an `autoRestart` entry that exits on its own relaunches after 2 s,
|
||||||
for ports 1501/53290. Volume round-trips; **Restart/Shutdown** power-cycle
|
while console-ordered kills stay down — exactly the real pod's behavior.
|
||||||
the virtual pod (dark for a few seconds on restart).
|
Real processes also die when the pod is powered off / rebooted / the vPOD
|
||||||
|
window closes (which also cancels pending watchdog restarts). Caveat:
|
||||||
|
launching a *deployed vPOD* or a real game client this way will fight the
|
||||||
|
running vPOD for ports 1501/53290. Volume round-trips; **Restart/Shutdown**
|
||||||
|
power-cycle the virtual pod (dark for a few seconds on restart).
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
@@ -138,9 +145,7 @@ Same-machine testing needs no firewall changes (loopback). Running vPOD on a
|
|||||||
Not emulated: the console's remote Windows-service control (`ServiceController`
|
Not emulated: the console's remote Windows-service control (`ServiceController`
|
||||||
over SCM/SMB, used by some SitePanel service start/stop paths — dormant against
|
over SCM/SMB, used by some SitePanel service start/stop paths — dormant against
|
||||||
real pods too, since it queries service name `TeslaLauncherService` while the
|
real pods too, since it queries service name `TeslaLauncherService` while the
|
||||||
launcher registers as `Tesla Application Launcher`), and the Agent's
|
launcher registers as `Tesla Application Launcher`).
|
||||||
`autoRestart` watchdog (a real-launched app that dies is pruned from the
|
|
||||||
running list, not relaunched).
|
|
||||||
|
|
||||||
An end-to-end loopback test of this server against the console's real
|
An end-to-end loopback test of this server against the console's real
|
||||||
`PodManagerConnection` client lives in the differential suite:
|
`PodManagerConnection` client lives in the differential suite:
|
||||||
|
|||||||
+45
-5
@@ -55,7 +55,9 @@ internal sealed class VPodForm : Form
|
|||||||
private Label mInstallStatusLabel;
|
private Label mInstallStatusLabel;
|
||||||
private ProgressBar mInstallProgressBar;
|
private ProgressBar mInstallProgressBar;
|
||||||
private Button mReprovisionButton;
|
private Button mReprovisionButton;
|
||||||
|
private CheckBox mRunPostInstallCheckbox;
|
||||||
private CheckBox mRealLaunchCheckbox;
|
private CheckBox mRealLaunchCheckbox;
|
||||||
|
private CheckBox mRealAutoRestartCheckbox;
|
||||||
private ListView mAppsView;
|
private ListView mAppsView;
|
||||||
|
|
||||||
private bool mPoweredOn;
|
private bool mPoweredOn;
|
||||||
@@ -281,7 +283,7 @@ internal sealed class VPodForm : Form
|
|||||||
Padding = new Padding(8)
|
Padding = new Padding(8)
|
||||||
};
|
};
|
||||||
|
|
||||||
Panel siteTop = new Panel { Dock = DockStyle.Top, Height = 234 };
|
Panel siteTop = new Panel { Dock = DockStyle.Top, Height = 278 };
|
||||||
|
|
||||||
mProvisionStatusLabel = new Label
|
mProvisionStatusLabel = new Label
|
||||||
{
|
{
|
||||||
@@ -352,24 +354,60 @@ internal sealed class VPodForm : Form
|
|||||||
};
|
};
|
||||||
mReprovisionButton.Click += ReprovisionClicked;
|
mReprovisionButton.Click += ReprovisionClicked;
|
||||||
|
|
||||||
|
// Off (default) = a packaged postinstall.bat is logged and removed unrun;
|
||||||
|
// on = it is executed at the end of an install, like the real Agent.
|
||||||
|
mRunPostInstallCheckbox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Run postinstall.bat after install",
|
||||||
|
Location = new Point(4, 206),
|
||||||
|
AutoSize = true,
|
||||||
|
Checked = false
|
||||||
|
};
|
||||||
|
mRunPostInstallCheckbox.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
mLauncher.RunPostInstall = mRunPostInstallCheckbox.Checked;
|
||||||
|
OnSiteLog(mRunPostInstallCheckbox.Checked
|
||||||
|
? "Installs now EXECUTE a packaged postinstall.bat."
|
||||||
|
: "Installs skip (and remove) a packaged postinstall.bat.");
|
||||||
|
};
|
||||||
|
|
||||||
// Off = LaunchApp records simulated PIDs; on = start/kill real processes
|
// Off = LaunchApp records simulated PIDs; on = start/kill real processes
|
||||||
// (the entries point into the real C:\Games, so this runs what the
|
// (the entries point into the real C:\Games, so this runs what the
|
||||||
// console deployed — exactly like the Agent).
|
// console deployed — exactly like the Agent).
|
||||||
mRealLaunchCheckbox = new CheckBox
|
mRealLaunchCheckbox = new CheckBox
|
||||||
{
|
{
|
||||||
Text = "Actually launch apps (real processes)",
|
Text = "Actually launch apps (real processes)",
|
||||||
Location = new Point(4, 206),
|
Location = new Point(4, 228),
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Checked = false
|
Checked = false
|
||||||
};
|
};
|
||||||
mRealLaunchCheckbox.CheckedChanged += (s, e) =>
|
mRealLaunchCheckbox.CheckedChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
mLauncher.RealLaunch = mRealLaunchCheckbox.Checked;
|
mLauncher.RealLaunch = mRealLaunchCheckbox.Checked;
|
||||||
|
mRealAutoRestartCheckbox.Enabled = mRealLaunchCheckbox.Checked;
|
||||||
OnSiteLog(mRealLaunchCheckbox.Checked
|
OnSiteLog(mRealLaunchCheckbox.Checked
|
||||||
? "Launch/Kill now start and terminate REAL processes."
|
? "Launch/Kill now start and terminate REAL processes."
|
||||||
: "Launch/Kill now simulate PIDs (no real processes).");
|
: "Launch/Kill now simulate PIDs (no real processes).");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The Agent's autoRestart watchdog for real-launched apps (applies only
|
||||||
|
// to entries registered with autoRestart, like the real pod).
|
||||||
|
mRealAutoRestartCheckbox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Auto-restart after the app exits (watchdog)",
|
||||||
|
Location = new Point(22, 250),
|
||||||
|
AutoSize = true,
|
||||||
|
Checked = true,
|
||||||
|
Enabled = false // meaningful only in real-launch mode
|
||||||
|
};
|
||||||
|
mRealAutoRestartCheckbox.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
mLauncher.RealAutoRestart = mRealAutoRestartCheckbox.Checked;
|
||||||
|
OnSiteLog(mRealAutoRestartCheckbox.Checked
|
||||||
|
? "Watchdog ON: autoRestart apps relaunch 2 s after exiting."
|
||||||
|
: "Watchdog OFF: exited apps stay down.");
|
||||||
|
};
|
||||||
|
|
||||||
siteTop.Controls.Add(mProvisionStatusLabel);
|
siteTop.Controls.Add(mProvisionStatusLabel);
|
||||||
siteTop.Controls.Add(mPassphrasePanel);
|
siteTop.Controls.Add(mPassphrasePanel);
|
||||||
siteTop.Controls.Add(mNetConfigLabel);
|
siteTop.Controls.Add(mNetConfigLabel);
|
||||||
@@ -377,7 +415,9 @@ internal sealed class VPodForm : Form
|
|||||||
siteTop.Controls.Add(mInstallStatusLabel);
|
siteTop.Controls.Add(mInstallStatusLabel);
|
||||||
siteTop.Controls.Add(mInstallProgressBar);
|
siteTop.Controls.Add(mInstallProgressBar);
|
||||||
siteTop.Controls.Add(mReprovisionButton);
|
siteTop.Controls.Add(mReprovisionButton);
|
||||||
|
siteTop.Controls.Add(mRunPostInstallCheckbox);
|
||||||
siteTop.Controls.Add(mRealLaunchCheckbox);
|
siteTop.Controls.Add(mRealLaunchCheckbox);
|
||||||
|
siteTop.Controls.Add(mRealAutoRestartCheckbox);
|
||||||
|
|
||||||
mAppsView = new ListView
|
mAppsView = new ListView
|
||||||
{
|
{
|
||||||
@@ -416,7 +456,7 @@ internal sealed class VPodForm : Form
|
|||||||
{
|
{
|
||||||
mServer.Stop();
|
mServer.Stop();
|
||||||
StopManagement();
|
StopManagement();
|
||||||
mLauncher.KillAllApps(); // don't orphan real launched processes when the tool exits
|
mLauncher.KillAllApps(cancelPendingRestarts: true); // don't orphan real launched processes when the tool exits
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -443,7 +483,7 @@ internal sealed class VPodForm : Form
|
|||||||
mRebootTimer.Stop();
|
mRebootTimer.Stop();
|
||||||
StopGame();
|
StopGame();
|
||||||
StopManagement();
|
StopManagement();
|
||||||
mLauncher.KillAllApps(); // the machine is "off": launched apps (real or simulated) die with it
|
mLauncher.KillAllApps(cancelPendingRestarts: true); // the machine is "off": launched apps (real or simulated) die with it
|
||||||
SetPoweredState(on: false);
|
SetPoweredState(on: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,7 +909,7 @@ internal sealed class VPodForm : Form
|
|||||||
mRebootTimer.Stop();
|
mRebootTimer.Stop();
|
||||||
StopGame();
|
StopGame();
|
||||||
StopManagement();
|
StopManagement();
|
||||||
mLauncher.KillAllApps(); // launched apps die with the rebooting/halting machine
|
mLauncher.KillAllApps(cancelPendingRestarts: true); // launched apps die with the rebooting/halting machine
|
||||||
SetPoweredState(on: false);
|
SetPoweredState(on: false);
|
||||||
if (restart)
|
if (restart)
|
||||||
{
|
{
|
||||||
|
|||||||
+98
-36
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using Tesla.Net;
|
using Tesla.Net;
|
||||||
|
|
||||||
namespace VPod;
|
namespace VPod;
|
||||||
@@ -44,6 +45,23 @@ internal sealed class VirtualLauncher
|
|||||||
/// Entries launched in either mode coexist — kills handle both.</summary>
|
/// Entries launched in either mode coexist — kills handle both.</summary>
|
||||||
public bool RealLaunch { get; set; }
|
public bool RealLaunch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The Agent's autoRestart watchdog for real-launched apps: when a
|
||||||
|
/// tracked process exits on its own (not via a Kill* command), relaunch it
|
||||||
|
/// after 2 s — but only for entries whose LaunchData.AutoRestart is set,
|
||||||
|
/// exactly like the real pod. Toggled from the vPOD window.</summary>
|
||||||
|
public bool RealAutoRestart { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>When set, a packaged postinstall.bat is executed at the end of a
|
||||||
|
/// product install (like the real Agent) before being deleted, instead of
|
||||||
|
/// being logged and removed unrun. Toggled from the vPOD window; off by
|
||||||
|
/// default, since it runs package script code on the host machine.</summary>
|
||||||
|
public bool RunPostInstall { get; set; }
|
||||||
|
|
||||||
|
// Bumped to cancel watchdog restarts pending in their 2 s delay — the pod
|
||||||
|
// "machine" went dark (power off / reboot / reprovision), so nothing may
|
||||||
|
// relaunch after it.
|
||||||
|
private int mWatchdogGeneration;
|
||||||
|
|
||||||
public event Action<string> Log;
|
public event Action<string> Log;
|
||||||
public event Action AppsChanged; // installed or launched list changed
|
public event Action AppsChanged; // installed or launched list changed
|
||||||
public event Action<Guid, OutOfBandProgress> InstallProgressChanged;
|
public event Action<Guid, OutOfBandProgress> InstallProgressChanged;
|
||||||
@@ -144,13 +162,11 @@ internal sealed class VirtualLauncher
|
|||||||
|
|
||||||
public LaunchedAppData[] GetLaunchedApps()
|
public LaunchedAppData[] GetLaunchedApps()
|
||||||
{
|
{
|
||||||
PruneExitedProcesses();
|
|
||||||
lock (mLock) { return mLaunchedApps.ToArray(); }
|
lock (mLock) { return mLaunchedApps.ToArray(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public FullUpdateData FullUpdate()
|
public FullUpdateData FullUpdate()
|
||||||
{
|
{
|
||||||
PruneExitedProcesses();
|
|
||||||
lock (mLock)
|
lock (mLock)
|
||||||
{
|
{
|
||||||
return new FullUpdateData
|
return new FullUpdateData
|
||||||
@@ -304,11 +320,79 @@ internal sealed class VirtualLauncher
|
|||||||
mLaunchedApps.Add(new LaunchedAppData { ProcessId = process.Id, LaunchKey = app.LaunchPair.LaunchKey });
|
mLaunchedApps.Add(new LaunchedAppData { ProcessId = process.Id, LaunchKey = app.LaunchPair.LaunchKey });
|
||||||
mRealProcesses[process.Id] = process;
|
mRealProcesses[process.Id] = process;
|
||||||
}
|
}
|
||||||
|
StartWatcher(app, process, process.Id);
|
||||||
Log?.Invoke($"LaunchApp: \"{name}\" -> started {app.ExeFile} (real PID {process.Id}).");
|
Log?.Invoke($"LaunchApp: \"{name}\" -> started {app.ExeFile} (real PID {process.Id}).");
|
||||||
AppsChanged?.Invoke();
|
AppsChanged?.Invoke();
|
||||||
return process.Id;
|
return process.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The Agent's per-process watcher: waits for exit, and if the process
|
||||||
|
/// is STILL TRACKED (Kill*/Uninstall remove it from the table first, so a
|
||||||
|
/// console-ordered kill never restarts), drops it from the running list and —
|
||||||
|
/// when the watchdog toggle and the entry's AutoRestart both apply — relaunches
|
||||||
|
/// it after the Agent's 2 s delay.</summary>
|
||||||
|
private void StartWatcher(LaunchData app, Process process, int pid)
|
||||||
|
{
|
||||||
|
string name = app.LaunchPair.DisplayName ?? app.LaunchPair.LaunchKey.ToString();
|
||||||
|
new Thread(() =>
|
||||||
|
{
|
||||||
|
try { process.WaitForExit(); }
|
||||||
|
catch { /* handle disposed by a Kill* — it is no longer tracked */ }
|
||||||
|
|
||||||
|
bool stillTracked;
|
||||||
|
lock (mLock)
|
||||||
|
{
|
||||||
|
stillTracked = mRealProcesses.TryGetValue(pid, out Process tracked) && ReferenceEquals(tracked, process);
|
||||||
|
if (stillTracked)
|
||||||
|
{
|
||||||
|
mRealProcesses.Remove(pid);
|
||||||
|
mLaunchedApps.RemoveAll(l => l.ProcessId == pid);
|
||||||
|
process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!stillTracked)
|
||||||
|
{
|
||||||
|
return; // killed via the console / power-off; whoever killed it cleaned up
|
||||||
|
}
|
||||||
|
AppsChanged?.Invoke();
|
||||||
|
|
||||||
|
int generation = Volatile.Read(ref mWatchdogGeneration);
|
||||||
|
if (!RealAutoRestart || !app.AutoRestart)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"\"{name}\" (PID {pid}) exited on its own (no watchdog restart).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log?.Invoke($"\"{name}\" (PID {pid}) exited on its own — watchdog restarting in 2 s...");
|
||||||
|
Thread.Sleep(2000);
|
||||||
|
|
||||||
|
// Still wanted? The pod may have powered off/reprovisioned (generation),
|
||||||
|
// the mode or toggle flipped, or the app been uninstalled meanwhile.
|
||||||
|
if (generation != Volatile.Read(ref mWatchdogGeneration) || !RealLaunch || !RealAutoRestart)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LaunchData current;
|
||||||
|
lock (mLock)
|
||||||
|
{
|
||||||
|
int index = mInstalledApps.FindIndex(a => a.LaunchPair.LaunchKey == app.LaunchPair.LaunchKey);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return; // uninstalled during the delay
|
||||||
|
}
|
||||||
|
current = mInstalledApps[index];
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LaunchRealProcess(current);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"Watchdog restart of \"{name}\" failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{ IsBackground = true, Name = $"vPOD-watchdog-{pid}" }.Start();
|
||||||
|
}
|
||||||
|
|
||||||
public void KillApp(Guid launchKey, int processId)
|
public void KillApp(Guid launchKey, int processId)
|
||||||
{
|
{
|
||||||
List<LaunchedAppData> victims = RemoveLaunched(l => l.LaunchKey == launchKey && l.ProcessId == processId);
|
List<LaunchedAppData> victims = RemoveLaunched(l => l.LaunchKey == launchKey && l.ProcessId == processId);
|
||||||
@@ -331,8 +415,16 @@ internal sealed class VirtualLauncher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void KillAllApps()
|
/// <summary>Kills everything launched. <paramref name="cancelPendingRestarts" />
|
||||||
|
/// is the pod-power path (off/reboot/exit): it also aborts watchdog restarts
|
||||||
|
/// waiting out their 2 s delay. The console's KillAllApps RPC leaves them
|
||||||
|
/// pending, matching the real Agent's race behavior.</summary>
|
||||||
|
public void KillAllApps(bool cancelPendingRestarts = false)
|
||||||
{
|
{
|
||||||
|
if (cancelPendingRestarts)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref mWatchdogGeneration);
|
||||||
|
}
|
||||||
List<LaunchedAppData> victims = RemoveLaunched(l => true);
|
List<LaunchedAppData> victims = RemoveLaunched(l => true);
|
||||||
if (victims.Count > 0)
|
if (victims.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -399,38 +491,6 @@ internal sealed class VirtualLauncher
|
|||||||
return killed;
|
return killed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Drops launched entries whose real process has exited on its own
|
|
||||||
/// (crash or normal exit), so GetLaunchedApps/FullUpdate reflect liveness.</summary>
|
|
||||||
private void PruneExitedProcesses()
|
|
||||||
{
|
|
||||||
int pruned = 0;
|
|
||||||
lock (mLock)
|
|
||||||
{
|
|
||||||
for (int i = mLaunchedApps.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (!mRealProcesses.TryGetValue(mLaunchedApps[i].ProcessId, out Process process))
|
|
||||||
{
|
|
||||||
continue; // simulated entry: lives until killed
|
|
||||||
}
|
|
||||||
bool exited;
|
|
||||||
try { exited = process.HasExited; }
|
|
||||||
catch { exited = true; }
|
|
||||||
if (exited)
|
|
||||||
{
|
|
||||||
mRealProcesses.Remove(mLaunchedApps[i].ProcessId);
|
|
||||||
process.Dispose();
|
|
||||||
mLaunchedApps.RemoveAt(i);
|
|
||||||
pruned++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pruned > 0)
|
|
||||||
{
|
|
||||||
Log?.Invoke($"{pruned} launched process(es) exited on their own.");
|
|
||||||
AppsChanged?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Shutdown(bool restart)
|
public void Shutdown(bool restart)
|
||||||
{
|
{
|
||||||
Log?.Invoke(restart ? "Shutdown(restart) — power-cycling the pod." : "Shutdown — powering the pod off.");
|
Log?.Invoke(restart ? "Shutdown(restart) — power-cycling the pod." : "Shutdown — powering the pod off.");
|
||||||
@@ -450,9 +510,11 @@ internal sealed class VirtualLauncher
|
|||||||
|
|
||||||
/// <summary>Clears the installed/launched app registries (LaunchApps.json) —
|
/// <summary>Clears the installed/launched app registries (LaunchApps.json) —
|
||||||
/// the store-wipe half of ClearStore, also used by the UI's Reprovision.
|
/// the store-wipe half of ClearStore, also used by the UI's Reprovision.
|
||||||
/// Any real launched processes are terminated first.</summary>
|
/// Any real launched processes are terminated first, and pending watchdog
|
||||||
|
/// restarts cancelled.</summary>
|
||||||
public void WipeApps()
|
public void WipeApps()
|
||||||
{
|
{
|
||||||
|
Interlocked.Increment(ref mWatchdogGeneration);
|
||||||
KillRealProcesses(RemoveLaunched(l => true));
|
KillRealProcesses(RemoveLaunched(l => true));
|
||||||
lock (mLock)
|
lock (mLock)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user