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:
Cyd
2026-07-11 21:01:34 -05:00
co-authored by Claude Fable 5
parent eefb8054e0
commit 91640dcbf2
61 changed files with 250 additions and 419 deletions
+5 -3
View File
@@ -17,11 +17,13 @@ already has squad `bay1` with a provisioned `vPOD` pod at 127.0.0.1; vPOD's
session key persists in `%LocalAppData%\vPOD\TeslaKeyStore.key`):
```
Start-Process vPOD\bin\Debug\net48\vPOD.exe -ArgumentList "-app","bt" # or rp
Start-Process Console\bin\Debug\net48\TeslaConsole.exe
Start-Process vPOD\bin\Debug\net40\vPOD.exe -ArgumentList "-app","bt" # or rp
Start-Process Console\bin\Debug\net40\TeslaConsole.exe
```
Drive the UI with System.Windows.Automation (UIA) — WinForms on net48 exposes
(net40 since the XP11 port — both exes run on the machine's 4.8 runtime.)
Drive the UI with System.Windows.Automation (UIA) — WinForms exposes
menus, buttons, checkboxes, and DataGridView rows/cells as real UIA elements:
- Menus: ExpandCollapsePattern on "Games", InvokePattern on the item.
+2 -2
View File
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2009")]
[assembly: AssemblyConfiguration("")]
[assembly: Guid("581ca4b6-a91c-4d24-b9b5-207f3b5da379")]
[assembly: AssemblyFileVersion("4.11.4.1")]
[assembly: AssemblyFileVersion("4.11.4.3")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: AssemblyTitle("Tesla Console")]
[assembly: AssemblyDescription("All code and UI property of Virtual World Entertainment.\r\n\r\nDeveloped by Elsewhen Studios, LLC in association with VGCorps, LLC.\r\n\r\nElsewhen Studios and the Elsewhen Wormhole are trademarks of Elsewhen Studios, LLC\r\n\r\nIncludes the WeifenLuo DockingPane library. Copyright © 2007 Weifen Luo (email: weifenluo@yahoo.com). Licensed under the MIT License - details can be found in WeifenLuo.txt included in this installation.")]
[assembly: AssemblyVersion("4.11.4.1")]
[assembly: AssemblyVersion("4.11.4.3")]
+19 -14
View File
@@ -39,13 +39,14 @@ The console references these assemblies. Most are vendored as binaries under
Two of these are no longer vendored binaries — they are built from source and
shared across the suite:
- `TeslaConsoleLaunchLib``../Contract/Tesla.Contract.csproj`, a net48 project: the
single source of truth for the Console↔Launcher RPC contract (wire types, the
- `TeslaConsoleLaunchLib``../Contract/Tesla.Contract.csproj` (net40, like the
whole suite since XP11): the single source of truth for the
Console↔Launcher RPC contract (wire types, the
`PodManagerConnection` client, and the framed-JSON `PodRpc` protocol), shared with the
Launcher Service. The assembly keeps the
Launcher. The assembly keeps the
`TeslaConsoleLaunchLib` name so the original-exe baseline still resolves in the
differential tests; the wire no longer embeds assembly names (see RPC note below).
- `TeslaSecureConfiguration``../SecureConfig/Tesla.SecureConfig.csproj` (net48),
- `TeslaSecureConfiguration``../SecureConfig/Tesla.SecureConfig.csproj` (net40),
the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange).
The original `TeslaSecureConfiguration.dll` is retained under `lib/` as the baseline
@@ -55,14 +56,16 @@ decompiled to source the same way if full-source builds are needed.
### Console ↔ Launcher RPC (no BinaryFormatter)
The pod-management channel (TCP 53290) runs **length-prefixed System.Text.Json**
The pod-management channel (TCP 53290) runs **length-prefixed JSON**
frames over the existing OFB-encrypted stream — see `Contract/PodRpcProtocol.cs`,
shared verbatim by both ends. This replaced the original `BinaryFormatter` +
shared verbatim by both ends (Newtonsoft.Json — net40 has no System.Text.Json).
This replaced the original `BinaryFormatter` +
serialized-`MethodBase` scheme (a remote-code-execution sink, and what had pinned the
Launcher to an old runtime); dispatch is now by method-name string. The Launcher
Service/Agent target **net48**, same as the Console. Note the Console still uses
`BinaryFormatter` for *local* disk persistence (`Site` config, mission results) — that
is local file I/O on net48, not the network surface, and is intentionally left alone.
Launcher to an old runtime); dispatch is now by method-name string. The Launcher and
the Console both target **net40** (XP11: XP SP3 through Windows 11). Note the Console
still uses `BinaryFormatter` for *local* disk persistence (`Site` config, mission
results) — that is local file I/O, not the network surface, and is intentionally
left alone.
## Layout
@@ -72,8 +75,10 @@ and still build and run.
```
TeslaConsole/
*.cs, TeslaConsole.*/ decompiled source (by namespace)
*.resx, app.ico embedded resources + icon
TeslaConsole.csproj net48 project
*.resx, app.ico string resources + icon
assets/icons/ UI images (raw originals, embedded as manifest resources —
the resx BinaryFormatter blobs cannot build for net40)
TeslaConsole.csproj net40 project (XP11: runs on XP SP3 through Windows 11)
RedPlanet/ runtime content (RPConfig.xml, RPStrings.xml) — copied to output
images/ source art (pod art / maps / vehicles) — reference only
installer_banner.bmp installer artwork — reference only
@@ -84,14 +89,14 @@ TeslaConsole/
## Building
Requirements: .NET SDK (6.0+) — the `Microsoft.NETFramework.ReferenceAssemblies`
NuGet package supplies the net48 reference assemblies, so a standalone Framework
NuGet package supplies the net40 reference assemblies, so a standalone Framework
targeting pack is **not** required.
```
dotnet build TeslaConsole.csproj -c Release
```
Output: `bin/Release/net48/TeslaConsole.exe` (with `RedPlanet/` and all
Output: `bin/Release/net40/TeslaConsole.exe` (with `RedPlanet/` and all
dependency DLLs copied alongside it).
## Runtime content
File diff suppressed because one or more lines are too long
+65 -208
View File
@@ -1,8 +1,10 @@
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Resources;
using System.Runtime.CompilerServices;
@@ -17,6 +19,15 @@ internal class Resources
private static CultureInfo resourceCulture;
// XP11: the images that used to live in the .resx as BinaryFormatter blobs now
// ship as raw embedded files (logical name "TeslaConsole.Icons.<file>", original
// bytes — see Console\assets\icons\). Building those blobs requires
// System.Resources.Extensions' DeserializingResourceReader at runtime, which is
// net461+ and cannot load on the XP-compatible net40. Strings stay in the .resx.
// Instances are cached like ResourceManager.GetObject cached them: one shared
// object per name.
private static readonly Dictionary<string, object> imageCache = new Dictionary<string, object>();
[EditorBrowsable(EditorBrowsableState.Advanced)]
internal static ResourceManager ResourceManager
{
@@ -43,262 +54,108 @@ internal class Resources
}
}
internal static Bitmap Add
internal static Bitmap EmbeddedBitmap(string file)
{
get
lock (imageCache)
{
object @object = ResourceManager.GetObject("Add", resourceCulture);
return (Bitmap)@object;
object image;
if (!imageCache.TryGetValue(file, out image))
{
// Deliberately not disposed: GDI+ decodes lazily (e.g. the animated
// GIF's frames), so the stream must outlive the Bitmap. It is an
// UnmanagedMemoryStream over the mapped assembly image — no handle.
Stream stream = typeof(Resources).Assembly.GetManifestResourceStream("TeslaConsole.Icons." + file);
image = new Bitmap(stream);
imageCache.Add(file, image);
}
return (Bitmap)image;
}
}
internal static Bitmap Blank
internal static Icon EmbeddedIcon(string file)
{
get
lock (imageCache)
{
object @object = ResourceManager.GetObject("Blank", resourceCulture);
return (Bitmap)@object;
object icon;
if (!imageCache.TryGetValue(file, out icon))
{
using (Stream stream = typeof(Resources).Assembly.GetManifestResourceStream("TeslaConsole.Icons." + file))
{
icon = new Icon(stream);
}
imageCache.Add(file, icon);
}
return (Icon)icon;
}
}
internal static Bitmap DeleteHS
{
get
{
object @object = ResourceManager.GetObject("DeleteHS", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap Add => EmbeddedBitmap("Add.png");
internal static Bitmap Error
{
get
{
object @object = ResourceManager.GetObject("Error", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap Blank => EmbeddedBitmap("Blank.png");
internal static Bitmap DeleteHS => EmbeddedBitmap("DeleteHS.png");
internal static Bitmap Error => EmbeddedBitmap("Error.bmp");
internal static string ErrorReportsDir => ResourceManager.GetString("ErrorReportsDir", resourceCulture);
internal static Bitmap install
{
get
{
object @object = ResourceManager.GetObject("install", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap install => EmbeddedBitmap("install.png");
internal static Bitmap openHS
{
get
{
object @object = ResourceManager.GetObject("openHS", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap openHS => EmbeddedBitmap("openHS.png");
internal static Bitmap Play
{
get
{
object @object = ResourceManager.GetObject("Play", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap Play => EmbeddedBitmap("Play.png");
internal static Bitmap PodBad16
{
get
{
object @object = ResourceManager.GetObject("PodBad16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodBad16 => EmbeddedBitmap("PodBad16.png");
internal static Bitmap PodBad32
{
get
{
object @object = ResourceManager.GetObject("PodBad32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodBad32 => EmbeddedBitmap("PodBad32.png");
internal static string PodBadImageKey => ResourceManager.GetString("PodBadImageKey", resourceCulture);
internal static Bitmap PodGo16
{
get
{
object @object = ResourceManager.GetObject("PodGo16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodGo16 => EmbeddedBitmap("PodGo16.png");
internal static Bitmap PodGo32
{
get
{
object @object = ResourceManager.GetObject("PodGo32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodGo32 => EmbeddedBitmap("PodGo32.png");
internal static string PodGoImageKey => ResourceManager.GetString("PodGoImageKey", resourceCulture);
internal static Bitmap PodOffline16
{
get
{
object @object = ResourceManager.GetObject("PodOffline16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodOffline16 => EmbeddedBitmap("PodOffline16.png");
internal static Bitmap PodOffline32
{
get
{
object @object = ResourceManager.GetObject("PodOffline32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodOffline32 => EmbeddedBitmap("PodOffline32.png");
internal static string PodOfflineImageKey => ResourceManager.GetString("PodOfflineImageKey", resourceCulture);
internal static Bitmap PodOfflineQuestion32
{
get
{
object @object = ResourceManager.GetObject("PodOfflineQuestion32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodOfflineQuestion32 => EmbeddedBitmap("PodOfflineQuestion32.png");
internal static Bitmap PodOnline16
{
get
{
object @object = ResourceManager.GetObject("PodOnline16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodOnline16 => EmbeddedBitmap("PodOnline16.png");
internal static Bitmap PodOnline32
{
get
{
object @object = ResourceManager.GetObject("PodOnline32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodOnline32 => EmbeddedBitmap("PodOnline32.png");
internal static string PodOnlineImageKey => ResourceManager.GetString("PodOnlineImageKey", resourceCulture);
internal static Bitmap PodQuestion16
{
get
{
object @object = ResourceManager.GetObject("PodQuestion16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodQuestion16 => EmbeddedBitmap("PodQuestion16.png");
internal static Bitmap PodQuestion32
{
get
{
object @object = ResourceManager.GetObject("PodQuestion32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodQuestion32 => EmbeddedBitmap("PodQuestion32.png");
internal static string PodQuestionImageKey => ResourceManager.GetString("PodQuestionImageKey", resourceCulture);
internal static Bitmap PodRun16
{
get
{
object @object = ResourceManager.GetObject("PodRun16", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodRun16 => EmbeddedBitmap("PodRun16.png");
internal static Bitmap PodRun32
{
get
{
object @object = ResourceManager.GetObject("PodRun32", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap PodRun32 => EmbeddedBitmap("PodRun32.png");
internal static string PodRunImageKey => ResourceManager.GetString("PodRunImageKey", resourceCulture);
internal static Bitmap Power
{
get
{
object @object = ResourceManager.GetObject("Power", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap Power => EmbeddedBitmap("Power.png");
internal static Bitmap RefreshDoc
{
get
{
object @object = ResourceManager.GetObject("RefreshDoc", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap RefreshDoc => EmbeddedBitmap("RefreshDoc.png");
internal static Bitmap saveHS
{
get
{
object @object = ResourceManager.GetObject("saveHS", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap saveHS => EmbeddedBitmap("saveHS.png");
internal static Bitmap square_throbber
{
get
{
object @object = ResourceManager.GetObject("square_throbber", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap square_throbber => EmbeddedBitmap("square_throbber.gif");
internal static Bitmap Stop
{
get
{
object @object = ResourceManager.GetObject("Stop", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap Stop => EmbeddedBitmap("Stop.png");
internal static Icon swirl
{
get
{
object @object = ResourceManager.GetObject("swirl", resourceCulture);
return (Icon)@object;
}
}
internal static Icon swirl => EmbeddedIcon("swirl.ico");
internal static Bitmap WebRefreshHH
{
get
{
object @object = ResourceManager.GetObject("WebRefreshHH", resourceCulture);
return (Bitmap)@object;
}
}
internal static Bitmap WebRefreshHH => EmbeddedBitmap("WebRefreshHH.png");
internal Resources()
{
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18 -7
View File
@@ -3,8 +3,13 @@
<AssemblyName>TeslaConsole</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<OutputType>WinExe</OutputType>
<UseWindowsForms>True</UseWindowsForms>
<TargetFramework>net48</TargetFramework>
<!-- net40 (XP11): the newest .NET Framework that installs on Windows XP SP3;
net40 assemblies load in-place on the 4.8 runtime in Win10/11, so this ONE
exe covers XP SP3 through Windows 11 — same deal as the Launcher. The
original console was net20, so the decompiled core needs nothing newer.
WinForms comes via plain framework references (UseWindowsForms is not
wired up for net40). -->
<TargetFramework>net40</TargetFramework>
<LangVersion>Preview</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<ApplicationIcon>app.ico</ApplicationIcon>
@@ -12,17 +17,21 @@
<!-- Decompiled from the original net20 TeslaConsole.exe; legacy WinForms 2.0 sources -->
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- .resx files embed BinaryFormatter-serialized bitmaps (non-string resources) -->
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<!-- CS0649: decompiled WinForms designer 'components' fields are never assigned -->
<NoWarn>$(NoWarn);CS0649</NoWarn>
</PropertyGroup>
<ItemGroup>
<!-- net48 reference assemblies so the project builds without a full targeting pack installed -->
<!-- .NET Framework reference assemblies so the project builds without a full targeting pack installed -->
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<!-- Required to read the pre-serialized binary resources above -->
<PackageReference Include="System.Resources.Extensions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<!-- The UI images, embedded as their raw original bytes (extracted 1:1 from the
old .resx BinaryFormatter blobs). Loaded by Properties.Resources.EmbeddedBitmap/
EmbeddedIcon: the .resx blob route needs System.Resources.Extensions at
runtime, which is net461+ and cannot load on net40/XP. -->
<EmbeddedResource Include="assets\icons\*.*" LogicalName="TeslaConsole.Icons.%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
@@ -48,6 +57,8 @@
</ItemGroup>
<ItemGroup>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Drawing" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Xml" />
<Reference Include="System.ServiceProcess" />
+3 -1
View File
@@ -80,7 +80,9 @@ public static class AppRegistry
private static readonly Dictionary<Guid, ProductDefinition> mById = new Dictionary<Guid, ProductDefinition>();
public static IReadOnlyList<ProductDefinition> Products => mProducts;
// IList, not IReadOnlyList: the read-only interfaces are net45+ and the
// XP11 console targets net40. Callers only enumerate/index it.
public static IList<ProductDefinition> Products => mProducts;
public static string CatalogPath =>
Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "RedPlanet\\Apps.xml");
+2 -2
View File
@@ -214,7 +214,7 @@ internal class SitePanel : DockContent
this.tsbManageSite.Text = "Manage Site";
this.tsbManageSite.Click += new System.EventHandler(tsbManageSite_Click);
this.mPowerDropDown.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[5] { this.mRestartAllPods, this.mShutdownAllPods, this.mPowerSeparator, this.mRestartAllCheckedPods, this.mShutdownAllCheckedPods });
this.mPowerDropDown.Image = (System.Drawing.Image)resources.GetObject("mPowerDropDown.Image");
this.mPowerDropDown.Image = TeslaConsole.Properties.Resources.EmbeddedBitmap("SitePanel.mPowerDropDown.Image.png");
this.mPowerDropDown.ImageTransparentColor = System.Drawing.Color.Magenta;
this.mPowerDropDown.Name = "mPowerDropDown";
this.mPowerDropDown.Size = new System.Drawing.Size(69, 22);
@@ -302,7 +302,7 @@ internal class SitePanel : DockContent
this.lblPodName.Text = "Mistress Quickly";
this.picRefresh.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
this.picRefresh.Cursor = System.Windows.Forms.Cursors.Hand;
this.picRefresh.Image = (System.Drawing.Image)resources.GetObject("picRefresh.Image");
this.picRefresh.Image = TeslaConsole.Properties.Resources.EmbeddedBitmap("SitePanel.picRefresh.Image.png");
this.picRefresh.Location = new System.Drawing.Point(308, 3);
this.picRefresh.Name = "picRefresh";
this.picRefresh.Size = new System.Drawing.Size(32, 32);
+2 -2
View File
@@ -758,7 +758,7 @@ public class TeslaConsoleForm : Form
this.mRPPrintPreviewDialog.AutoScrollMinSize = new System.Drawing.Size(0, 0);
this.mRPPrintPreviewDialog.ClientSize = new System.Drawing.Size(400, 300);
this.mRPPrintPreviewDialog.Enabled = true;
this.mRPPrintPreviewDialog.Icon = (System.Drawing.Icon)resources.GetObject("mRPPrintPreviewDialog.Icon");
this.mRPPrintPreviewDialog.Icon = TeslaConsole.Properties.Resources.EmbeddedIcon("TeslaConsoleForm.mRPPrintPreviewDialog.Icon.ico");
this.mRPPrintPreviewDialog.Name = "printPreviewDialog1";
this.mRPPrintPreviewDialog.Visible = false;
this.mRPPrintDialog.UseEXDialog = true;
@@ -773,7 +773,7 @@ public class TeslaConsoleForm : Form
base.ClientSize = new System.Drawing.Size(1168, 591);
base.Controls.Add(this.mMainDockPanel);
base.Controls.Add(this.mMenu);
base.Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
base.Icon = TeslaConsole.Properties.Resources.EmbeddedIcon("TeslaConsoleForm.$this.Icon.ico");
base.IsMdiContainer = true;
base.MainMenuStrip = this.mMenu;
base.Name = "TeslaConsoleForm";
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+5 -4
View File
@@ -2,9 +2,10 @@
:: =============================================================================
:: TeslaConsole - Build ^& Package
:: =============================================================================
:: Publishes the console (net48, framework-dependent) into TeslaConsole\App and
:: assembles the installable package (App\ + install.bat) next to it. net48 is
:: in-box on Windows 10/11, so the target control PC needs no runtime install.
:: Publishes the console (net40, framework-dependent - XP11: runs on XP SP3
:: through Windows 11) into TeslaConsole\App and assembles the installable
:: package (App\ + install.bat) next to it. The 4.x runtime is in-box on
:: Windows 10/11; an XP-era control PC needs dotNetFx40_Full_x86_x64.exe once.
::
:: Requirements: .NET SDK (6.0+) to drive the build; internet for first restore.
::
@@ -24,7 +25,7 @@ set ZIP=%ROOT%dist\TeslaConsole-pkg.zip
echo.
echo ============================================================
echo TeslaConsole - Build ^& Package (net48, framework-dependent)
echo TeslaConsole - Build ^& Package (net40, framework-dependent)
echo Output : %BUILD_DIR%
echo ============================================================
echo.
@@ -6,7 +6,8 @@ namespace TeslaConsole.DiffTests
/// <summary>
/// Locates the two assemblies under comparison:
/// * Original - original/TeslaConsole.exe (the lost-source reference baseline)
/// * Recovered - bin/Release/net48/TeslaConsole.exe (freshly built reconstruction)
/// * Recovered - bin/Release/net40/TeslaConsole.exe (freshly built reconstruction;
/// net40 since XP11 — loads fine in this net48 test host, both are CLR4)
/// </summary>
public static class AssemblyPaths
{
@@ -39,8 +40,8 @@ namespace TeslaConsole.DiffTests
private static string FindRecoveredExe()
{
string release = Path.Combine(RepoRoot, "bin", "Release", "net48", "TeslaConsole.exe");
string debug = Path.Combine(RepoRoot, "bin", "Debug", "net48", "TeslaConsole.exe");
string release = Path.Combine(RepoRoot, "bin", "Release", "net40", "TeslaConsole.exe");
string debug = Path.Combine(RepoRoot, "bin", "Debug", "net40", "TeslaConsole.exe");
// Test whichever build is freshest, so a stale config never silently wins.
string best = null;
@@ -47,7 +47,7 @@ namespace TeslaConsole.DiffTests
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
Assert.Contains("4.11.4.1", _fx.Recovered.AssemblyFullName);
Assert.Contains("4.11.4.3", _fx.Recovered.AssemblyFullName);
}
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Tesla.Net;
using Xunit;
@@ -29,8 +28,8 @@ namespace TeslaConsole.DiffTests
var req = PodRpc.ReadRequest(ms);
Assert.Equal("KillApp", req.Method);
Assert.Equal(2, req.Args.Count);
Assert.Equal(Key, req.Args[0].GetGuid());
Assert.Equal(4242, req.Args[1].GetInt32());
Assert.Equal(Key, req.Args[0].ToObject<Guid>(PodRpc.JsonOptions));
Assert.Equal(4242, req.Args[1].ToObject<int>(PodRpc.JsonOptions));
}
[Fact]
@@ -150,7 +149,7 @@ namespace TeslaConsole.DiffTests
ms.Position = 0;
var resp = PodRpc.ReadResponse(ms);
Assert.Null(resp.Error);
return resp.Result.Deserialize<T>(PodRpc.JsonOptions);
return resp.Result.ToObject<T>(PodRpc.JsonOptions);
}
}
}
@@ -76,7 +76,9 @@ dotnet test tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj
```
A project reference builds the reconstruction first, and the suite always tests
the most recently built `bin/{Debug,Release}/net48/TeslaConsole.exe`.
the most recently built `bin/{Debug,Release}/net40/TeslaConsole.exe` (net40 since
the XP11 port; the net48 test host loads it fine — both are CLR4, so the whole
process runs the net40/Newtonsoft stack that ships).
## Scope / limitations
@@ -50,7 +50,9 @@
</ProjectReference>
<!-- The source-built wire contract (emits TeslaConsoleLaunchLib.dll). Referenced
directly so WireContractCompatTests can construct Tesla.Net types and compare
their BinaryFormatter output against the original vendored DLL. -->
their BinaryFormatter output against the original vendored DLL. net40 (like
everything under test since XP11); loads fine in this net48 host — both are
CLR4 — so the suite exercises the exact Newtonsoft stack that ships. -->
<ProjectReference Include="..\..\..\Contract\Tesla.Contract.csproj" />
<!-- The source-built secure-config (emits TeslaSecureConfiguration.dll), for
SecureConfigCompatTests' byte-identity checks vs the original vendored DLL. -->
+7 -6
View File
@@ -1,5 +1,5 @@
// =============================================================================
// Tesla.Contract — Console-side RPC client (net48 only)
// Tesla.Contract — Console-side RPC client
// =============================================================================
// Opens an OFB-encrypted TCP connection to the pod (port 53290) and dispatches
// ILauncherService calls as framed JSON RpcRequest / RpcResponse pairs (see
@@ -8,7 +8,8 @@
// unchanged.
//
// Depends on Tesla.PodConfigurationServer (Tesla.SecureConfig) for the crypto
// handshake, so it is compiled for net48 only. The Launcher is the server end.
// handshake. Results deserialize with Newtonsoft.Json (see the serializer note
// in PodRpcProtocol.cs).
// =============================================================================
using System;
@@ -16,8 +17,8 @@ using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using Newtonsoft.Json.Linq;
namespace Tesla.Net
{
@@ -129,14 +130,14 @@ namespace Tesla.Net
throw new Exception("Server function threw an exception: " + response.Error);
}
if (resultType == null
|| response.Result.ValueKind == JsonValueKind.Null
|| response.Result.ValueKind == JsonValueKind.Undefined)
|| response.Result == null
|| response.Result.Type == JTokenType.Null)
{
return resultType != null && resultType.IsValueType
? Activator.CreateInstance(resultType)
: null;
}
return response.Result.Deserialize(resultType, PodRpc.JsonOptions);
return response.Result.ToObject(resultType, PodRpc.JsonOptions);
}
}
catch (IOException innerException)
+8 -60
View File
@@ -14,46 +14,34 @@
// Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase
// + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this
// one file, so the request/response shape cannot drift.
//
// Serializer: Newtonsoft.Json — System.Text.Json has no net40 target, and since
// XP11 the whole suite (Console, Launcher, vPOD) is net40. The protocol briefly
// had an STJ leg for the net48 Console era; it wrote shape-identical JSON
// (PascalCase member names, fields included, Guids as strings, ISO-8601 dates),
// so anything that captured wire traffic then still matches what this writes.
// =============================================================================
// NET40 (XP11 single-binary launcher): System.Text.Json has no net40 target, so
// this leg serializes with Newtonsoft.Json instead. Same framing, same JSON shape
// (PascalCase member names, fields included, Guids as strings, ISO-8601 dates) —
// an STJ client and a Newtonsoft server interoperate byte-compatibly on the wire.
// Arguments surface as JToken here instead of JsonElement.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
#if NET40
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
#else
using System.Text.Json;
#endif
namespace Tesla.Net
{
/// <summary>One RPC call: a method name plus its arguments as JSON elements.</summary>
/// <summary>One RPC call: a method name plus its arguments as JSON tokens.</summary>
public sealed class RpcRequest
{
public string Method { get; set; }
#if NET40
public List<JToken> Args { get; set; }
#else
public List<JsonElement> Args { get; set; }
#endif
}
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
public sealed class RpcResponse
{
#if NET40
public JToken Result { get; set; } // JSON null for void / null
#else
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
#endif
public string Error { get; set; } // null on success
}
@@ -65,18 +53,9 @@ namespace Tesla.Net
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
public const int MaxFrameBytes = 16 * 1024 * 1024;
#if NET40
// Newtonsoft serializes public fields of the wire types by default, and
// writes ISO-8601 dates / string Guids like STJ — no special options needed.
// writes ISO-8601 dates / string Guids — no special options needed.
public static readonly JsonSerializer JsonOptions = JsonSerializer.CreateDefault();
#else
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
// FIELDS, which System.Text.Json ignores unless this is set.
IncludeFields = true,
};
#endif
// ── Framing ──────────────────────────────────────────────────────────
@@ -114,7 +93,6 @@ namespace Tesla.Net
// ── Request ──────────────────────────────────────────────────────────
#if NET40
public static void WriteRequest(Stream stream, string method, object[] args)
{
var req = new RpcRequest { Method = method, Args = new List<JToken>() };
@@ -132,23 +110,9 @@ namespace Tesla.Net
// goes back byte-identical instead of reformatted through DateTime.
private static readonly JsonSerializerSettings ReadSettings =
new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
#else
public static void WriteRequest(Stream stream, string method, object[] args)
{
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
if (args != null)
foreach (var a in args)
req.Args.Add(JsonSerializer.SerializeToElement(a, JsonOptions));
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(req, JsonOptions));
}
public static RpcRequest ReadRequest(Stream stream)
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
#endif
// ── Response ─────────────────────────────────────────────────────────
#if NET40
public static void WriteResponse(Stream stream, object result, string error)
{
object payload = error == null ? result : null;
@@ -163,21 +127,5 @@ namespace Tesla.Net
public static RpcResponse ReadResponse(Stream stream)
=> JsonConvert.DeserializeObject<RpcResponse>(
Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings);
#else
public static void WriteResponse(Stream stream, object result, string error)
{
var resp = new RpcResponse
{
// Always a valid element (JSON null when there is no result): a
// default(JsonElement) is ValueKind.Undefined and is not serializable.
Result = JsonSerializer.SerializeToElement(error == null ? result : null, JsonOptions),
Error = error,
};
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(resp, JsonOptions));
}
public static RpcResponse ReadResponse(Stream stream)
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
#endif
}
}
+14 -23
View File
@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- net48: the Console (and the client/crypto stack under Client/**).
net40: the XP11 single-binary Launcher — the oldest framework installable
on Windows XP SP3, and net40 assemblies run in-place on the 4.8 runtime,
so ONE launcher binary covers XP SP3 through Windows 11. -->
<TargetFrameworks>net48;net40</TargetFrameworks>
<!-- net40 only (XP11): the oldest framework installable on Windows XP SP3,
and net40 assemblies run in-place on the 4.8 runtime — so the whole
suite (Console, Launcher, vPOD, and this contract they share) covers
XP SP3 through Windows 11 with one flavor. A net48/System.Text.Json leg
existed while the Console was net48; it was dropped 2026-07-11 when the
last consumer moved to net40 (the JSON on the wire is unchanged). -->
<TargetFramework>net40</TargetFramework>
<!-- CRITICAL: the output assembly MUST be named TeslaConsoleLaunchLib at
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
@@ -32,28 +34,17 @@
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
<!-- JSON serializer per leg. System.Text.Json has no net40 target, so the
net40 leg of PodRpcProtocol.cs uses Newtonsoft.Json (which still ships
lib/net40). The JSON bytes on the wire are shape-identical; the
XpWireCompatTests exercise STJ-client <-> Newtonsoft-server for real. -->
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
<!-- JSON serializer: Newtonsoft.Json (still ships lib/net40; System.Text.Json
never had a net40 target). -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<!-- The TCP/OFB client (Client/**) is net48-only: it depends on the crypto-stream
handshake in TeslaSecureConfiguration.dll. The Launcher is the SERVER end of
this protocol and never references these classes, so the net40 leg carries
only the wire data types + PodRpc framing. -->
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
<Compile Remove="Client\**\*.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<ItemGroup>
<!-- Source-built secure-config (PodConfigurationServer.NegotiateCryptoStreams),
emitting assembly TeslaSecureConfiguration. net48-only, same as Client/**. -->
emitting assembly TeslaSecureConfiguration; needed by the TCP/OFB client
under Client/**. The Launcher never touches the Client types; its package
just carries the extra dll. -->
<ProjectReference Include="..\SecureConfig\Tesla.SecureConfig.csproj" />
</ItemGroup>
+5 -5
View File
@@ -135,11 +135,11 @@ installer both resolve it per-OS; nothing hardcodes `C:\ProgramData` anymore.
The Console talks to the launcher with **length-prefixed JSON frames** over the
OFB-encrypted TCP stream (dispatch by method name) — see
`../Contract/PodRpcProtocol.cs`, shared by both ends. The Contract multi-targets
`net48;net40`: the Console's net48 leg serializes with System.Text.Json, the
launcher's net40 leg with Newtonsoft.Json (STJ has no net40 target). The JSON is
shape-identical on the wire; the request reader keeps date strings raw so a Ping
echo returns byte-identical.
`../Contract/PodRpcProtocol.cs`, shared by both ends. Since the whole suite went
net40 (XP11), both ends serialize with Newtonsoft.Json and the Contract is
net40-only (its former net48/System.Text.Json leg wrote shape-identical JSON and
was dropped once the Console moved to net40). The request reader keeps date
strings raw so a Ping echo returns byte-identical.
Volume on XP falls back from CoreAudio (Vista+) to `nircmd.exe` / winmm
`waveOutSetVolume`.
+2 -2
View File
@@ -7,7 +7,7 @@
installs on Windows XP SP3, and net40 assemblies load in-place on the 4.8
runtime that ships in Windows 10/11 — so this ONE exe covers XP SP3
through Windows 11. Everything net45+ is off-limits here:
- System.Text.Json -> Newtonsoft.Json (net40 leg of Tesla.Contract)
- System.Text.Json -> Newtonsoft.Json (Tesla.Contract)
- ZipFile/ZipArchive -> MiniZip.cs
- async/await, Task.Run -> threads
- RSA.Create(int) -> RSACryptoServiceProvider
@@ -18,7 +18,7 @@
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Version>4.11.4.1</Version>
<Version>4.11.4.3</Version>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>TeslaLauncher</AssemblyName>
<RootNamespace>Tesla.Launcher</RootNamespace>
Binary file not shown.
+1 -1
View File
@@ -46,7 +46,7 @@ for %%a in (%*) do (
echo.
echo ============================================================
echo Tesla Launcher v4.11.4.1 - Build ^& Package (net40 single binary)
echo Tesla Launcher v4.11.4.3 - Build ^& Package (net40 single binary)
echo Output : %BUILD_DIR%
echo ============================================================
echo.
+1 -1
View File
@@ -20,7 +20,7 @@
setlocal enabledelayedexpansion
echo ============================================================
echo Tesla Launcher v4.11.4.1 - Installation (single binary)
echo Tesla Launcher v4.11.4.3 - Installation (single binary)
echo ============================================================
echo.
+6 -3
View File
@@ -1,9 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- net48 only: consumed by the net48 Console and the net48 client half of
Tesla.Contract. The pod side is the Launcher's own SecureConfig.cs. -->
<TargetFramework>net48</TargetFramework>
<!-- net40 only (XP11), like everything that consumes it: the client half of
Tesla.Contract, the Console, and vPOD. The protocol code is net20-era
(RSACryptoServiceProvider / Rijndael / CryptoStream), so the old net48
leg compiled from the same source; it was dropped 2026-07-11 with the
Contract's. The pod side is still the Launcher's own SecureConfig.cs. -->
<TargetFramework>net40</TargetFramework>
<!-- Emit an assembly named TeslaSecureConfiguration (v1.0.0.0) so it is a
drop-in replacement for the original vendored binary. Unlike the wire
+26 -40
View File
@@ -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.
+8 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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>