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>
@@ -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.
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 938 B |
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 314 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 454 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 501 B |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 772 B |
|
After Width: | Height: | Size: 700 B |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 138 KiB |
@@ -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. -->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||