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
+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>