XP11: whole suite on net40 — Console + vPOD run on XP SP3 through Win11
The Launcher's XP11 port (8730b9b) now extends to everything: one net40
flavor across Console, vPOD, Contract, and SecureConfig (Newtonsoft.Json
everywhere; the net48/System.Text.Json legs and their #if splits are gone
since nothing consumed them).
Console (net40, single TFM like the Launcher):
- The ~31 BinaryFormatter bitmap blobs in the .resx files became raw
embedded files under assets/icons/ (extracted byte-faithfully via a
serialization surrogate — the animated square_throbber.gif survives),
loaded by Properties.Resources.EmbeddedBitmap/EmbeddedIcon. Reason:
System.Resources.Extensions' DeserializingResourceReader is net461+
and cannot load on net40. Strings stay in the .resx.
- IReadOnlyList -> IList in AppRegistry (net45+ interface).
vPOD (net40, single TFM):
- Zip extraction now shares the Launcher's MiniZip.cs (linked source), so
the diff-test install round-trip exercises it against ZipArchive zips.
- RPC args as JTokens; LaunchApps.json persistence via Newtonsoft;
Thread.VolatileRead instead of Volatile.Read.
Contract/SecureConfig: net40-only; Client/** (PodManagerConnection) now
ships in the one build. The Launcher package gains
TeslaSecureConfiguration.dll as a dependency of the client half.
Tests: the net48 xunit host loads the net40 assemblies (both CLR4), so
the suite exercises exactly what ships — 106/106 green. Also verified
live: net40 console provisioned, managed, and ran a full RP mission
against net40 vPOD (beacon/passphrase/RSA, 53290 RPC, egg load,
Run/Stop Mission).
Version: 4.11.4.3 across Launcher, Console, and vPOD (vPOD joins the
suite version line; was 1.0.0). Ship the dotNetFx40 redistributable in
Launcher/assets for XP-era pods.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+26
-40
@@ -2,12 +2,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tesla;
|
||||
using Tesla.Launcher;
|
||||
using Tesla.Net;
|
||||
|
||||
namespace VPod;
|
||||
@@ -184,7 +184,7 @@ internal sealed class LauncherRpcServer
|
||||
}
|
||||
|
||||
string method = request.Method ?? "???";
|
||||
IReadOnlyList<JsonElement> args = request.Args ?? new List<JsonElement>();
|
||||
List<JToken> args = request.Args ?? new List<JToken>();
|
||||
// GetOutOfBandProgress is polled 4x/second during installs — don't log it.
|
||||
if (method != "GetOutOfBandProgress" && method != "Ping")
|
||||
{
|
||||
@@ -222,16 +222,22 @@ internal sealed class LauncherRpcServer
|
||||
}
|
||||
}
|
||||
|
||||
// RPC args surface as Newtonsoft JTokens (the Contract is net40;
|
||||
// System.Text.Json has no net40 target).
|
||||
private static bool IsNull(JToken arg) => arg == null || arg.Type == JTokenType.Null;
|
||||
|
||||
private static T Arg<T>(JToken arg) => arg.ToObject<T>(PodRpc.JsonOptions);
|
||||
|
||||
/// <summary>Maps the console's method names (dispatch-by-name, including the
|
||||
/// get_/set_ property accessors) onto the VirtualLauncher. Mirrors the real
|
||||
/// service's DispatchCommandAsync.</summary>
|
||||
private object Dispatch(string method, IReadOnlyList<JsonElement> args)
|
||||
private object Dispatch(string method, List<JToken> args)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "Ping":
|
||||
return args.Count > 0 && args[0].ValueKind != JsonValueKind.Null
|
||||
? mLauncher.Ping(args[0].GetDateTime())
|
||||
return args.Count > 0 && !IsNull(args[0])
|
||||
? mLauncher.Ping(Arg<DateTime>(args[0]))
|
||||
: DateTime.Now;
|
||||
|
||||
case "GetInstalledApps":
|
||||
@@ -247,32 +253,32 @@ internal sealed class LauncherRpcServer
|
||||
return mLauncher.FullUpdate();
|
||||
|
||||
case "GetOutOfBandProgress":
|
||||
return mLauncher.GetOutOfBandProgress(args[0].GetGuid());
|
||||
return mLauncher.GetOutOfBandProgress(Arg<Guid>(args[0]));
|
||||
|
||||
case "InitiateInstallProduct":
|
||||
return mLauncher.InitiateInstallProduct();
|
||||
|
||||
case "InstallApp":
|
||||
mLauncher.InstallApp(args[0].Deserialize<LaunchData>(PodRpc.JsonOptions));
|
||||
mLauncher.InstallApp(Arg<LaunchData>(args[0]));
|
||||
return null;
|
||||
|
||||
case "UninstallApp":
|
||||
mLauncher.UninstallApp(args[0].GetGuid());
|
||||
mLauncher.UninstallApp(Arg<Guid>(args[0]));
|
||||
return null;
|
||||
|
||||
case "RemoveApp":
|
||||
mLauncher.RemoveApp(args[0].GetGuid());
|
||||
mLauncher.RemoveApp(Arg<Guid>(args[0]));
|
||||
return null;
|
||||
|
||||
case "LaunchApp":
|
||||
return mLauncher.LaunchApp(args[0].GetGuid());
|
||||
return mLauncher.LaunchApp(Arg<Guid>(args[0]));
|
||||
|
||||
case "KillApp":
|
||||
mLauncher.KillApp(args[0].GetGuid(), args[1].GetInt32());
|
||||
mLauncher.KillApp(Arg<Guid>(args[0]), Arg<int>(args[1]));
|
||||
return null;
|
||||
|
||||
case "KillAllOfType":
|
||||
mLauncher.KillAllOfType(args[0].GetGuid());
|
||||
mLauncher.KillAllOfType(Arg<Guid>(args[0]));
|
||||
return null;
|
||||
|
||||
case "KillAllApps":
|
||||
@@ -280,7 +286,7 @@ internal sealed class LauncherRpcServer
|
||||
return null;
|
||||
|
||||
case "Shutdown":
|
||||
mLauncher.Shutdown(args[0].GetBoolean());
|
||||
mLauncher.Shutdown(Arg<bool>(args[0]));
|
||||
return null;
|
||||
|
||||
case "ClearStore":
|
||||
@@ -291,7 +297,7 @@ internal sealed class LauncherRpcServer
|
||||
return mLauncher.VolumeLevel;
|
||||
|
||||
case "set_VolumeLevel":
|
||||
mLauncher.VolumeLevel = args[0].GetSingle();
|
||||
mLauncher.VolumeLevel = Arg<float>(args[0]);
|
||||
return null;
|
||||
|
||||
default:
|
||||
@@ -336,31 +342,11 @@ internal sealed class LauncherRpcServer
|
||||
mLauncher.UpdateProgress(callId, 50, "Extracting...");
|
||||
string gamesRoot = Path.GetFullPath(mLauncher.GamesRoot);
|
||||
Directory.CreateDirectory(gamesRoot);
|
||||
using (ZipArchive zip = ZipFile.OpenRead(tempZip))
|
||||
{
|
||||
int total = zip.Entries.Count;
|
||||
int done = 0;
|
||||
foreach (ZipArchiveEntry entry in zip.Entries)
|
||||
{
|
||||
string destPath = Path.GetFullPath(Path.Combine(gamesRoot, entry.FullName));
|
||||
// Zip-slip protection, as in the real service.
|
||||
if (!destPath.StartsWith(gamesRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(destPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
|
||||
entry.ExtractToFile(destPath, overwrite: true);
|
||||
}
|
||||
done++;
|
||||
mLauncher.UpdateProgress(callId, 50 + done * 45 / Math.Max(total, 1), "Extracting...");
|
||||
}
|
||||
}
|
||||
// The Launcher's own extractor (zip-slip protection included): net40
|
||||
// has no ZipFile/ZipArchive, and sharing it keeps vPOD's extraction
|
||||
// byte-identical to the real pod service.
|
||||
MiniZip.ExtractToDirectory(tempZip, gamesRoot, (done, total) =>
|
||||
mLauncher.UpdateProgress(callId, 50 + done * 45 / Math.Max(total, 1), "Extracting..."));
|
||||
Log?.Invoke($"Install {callId:N}: extracted to {gamesRoot}");
|
||||
|
||||
// The real service runs (then deletes) a packaged postinstall.bat here.
|
||||
|
||||
@@ -2,8 +2,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
// No System.Text.Json on net40 — persistence goes through Newtonsoft, which
|
||||
// writes the same JSON shape (public fields, PascalCase; see PodRpcProtocol).
|
||||
using Newtonsoft.Json;
|
||||
using Tesla.Net;
|
||||
|
||||
namespace VPod;
|
||||
@@ -356,7 +358,8 @@ internal sealed class VirtualLauncher
|
||||
}
|
||||
AppsChanged?.Invoke();
|
||||
|
||||
int generation = Volatile.Read(ref mWatchdogGeneration);
|
||||
// Thread.VolatileRead, not Volatile.Read: the latter is net45+.
|
||||
int generation = Thread.VolatileRead(ref mWatchdogGeneration);
|
||||
if (!RealAutoRestart || !app.AutoRestart)
|
||||
{
|
||||
Log?.Invoke($"\"{name}\" (PID {pid}) exited on its own (no watchdog restart).");
|
||||
@@ -367,7 +370,7 @@ internal sealed class VirtualLauncher
|
||||
|
||||
// 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)
|
||||
if (generation != Thread.VolatileRead(ref mWatchdogGeneration) || !RealLaunch || !RealAutoRestart)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -584,7 +587,7 @@ internal sealed class VirtualLauncher
|
||||
{
|
||||
return;
|
||||
}
|
||||
LaunchData[] apps = JsonSerializer.Deserialize<LaunchData[]>(File.ReadAllText(LaunchAppsPath), PodRpc.JsonOptions);
|
||||
LaunchData[] apps = JsonConvert.DeserializeObject<LaunchData[]>(File.ReadAllText(LaunchAppsPath));
|
||||
if (apps != null)
|
||||
{
|
||||
mInstalledApps.AddRange(apps);
|
||||
@@ -600,7 +603,7 @@ internal sealed class VirtualLauncher
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(LaunchAppsPath, JsonSerializer.Serialize(mInstalledApps.ToArray(), PodRpc.JsonOptions));
|
||||
File.WriteAllText(LaunchAppsPath, JsonConvert.SerializeObject(mInstalledApps.ToArray()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ param([string]$Config = 'Release')
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$bin = Join-Path $root "bin\$Config\net48"
|
||||
# net40 leg: the XP11 build that runs on XP SP3 pods through Windows 11.
|
||||
$bin = Join-Path $root "bin\$Config\net40"
|
||||
$distDir = Join-Path $root 'dist'
|
||||
$stage = Join-Path $distDir 'vPOD' # -> C:\Games\vPOD on the pod
|
||||
$zipPath = Join-Path $distDir 'vPOD.zip'
|
||||
|
||||
+25
-9
@@ -10,26 +10,40 @@
|
||||
and shows both on a live display. Deployable to a pod machine via the
|
||||
console's Manage Site -> Install Product (see dist\ + Console\RedPlanet\Apps.xml).
|
||||
|
||||
net48 to match the rest of the suite and the vendored Munga Net.dll.
|
||||
net40 (XP11): runs on XP SP3 through Windows 11, like the Launcher and the
|
||||
Console — one flavor everywhere (Contract and SecureConfig included). The
|
||||
differential tests' net48 host loads all of it fine (net40 and net48 are both
|
||||
CLR4). The vendored Munga Net.dll is CLR2 pure-IL, so it loads anywhere.
|
||||
WinForms comes via plain framework references: UseWindowsForms is not wired
|
||||
up for net40, and all UI here is code-built (no designer).
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<TargetFramework>net40</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<AssemblyName>vPOD</AssemblyName>
|
||||
<RootNamespace>VPod</RootNamespace>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<Version>1.0.0</Version>
|
||||
<!-- Versioned with the suite since v4.11.4.3 (was its own 1.0.0 line). -->
|
||||
<AssemblyVersion>4.11.4.3</AssemblyVersion>
|
||||
<Version>4.11.4.3</Version>
|
||||
<Product>vPOD</Product>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- net48 reference assemblies so this builds without a full targeting pack installed -->
|
||||
<!-- .NET Framework reference assemblies so this builds without a full
|
||||
targeting pack installed (resolves per-TFM, covers net40 and net48) -->
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Drawing" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- JSON for LaunchApps.json persistence + RPC arg materialization: Newtonsoft,
|
||||
matching the Contract (System.Text.Json has no net40 target). -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -39,9 +53,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Framework assemblies for extracting InstallProduct zips (virtual launcher) -->
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<!-- InstallProduct zips are extracted with the Launcher's own MiniZip on BOTH
|
||||
legs (ZipFile/ZipArchive are net45+, absent on net40): identical extraction
|
||||
behavior to the real pod service, and the differential suite's install
|
||||
round-trip exercises MiniZip against real ZipArchive-built archives. -->
|
||||
<Compile Include="..\Launcher\MiniZip.cs" Link="MiniZip.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user