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()); 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()
{ {
+43 -5
View File
@@ -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
View File
@@ -110,17 +110,24 @@ launch entries point — and reports the launcher's usual `050%` 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
View File
@@ -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
View File
@@ -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)
{ {